/* invitation.c -- Create and accept invitations Copyright (C) 2013-2014 Guus Sliepen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "system.h" #include "control_common.h" #include "crypto.h" #include "ecdsa.h" #include "ecdsagen.h" #include "invitation.h" #include "names.h" #include "netutl.h" #include "rsagen.h" #include "script.h" #include "sptps.h" #include "tincctl.h" #include "utils.h" #include "xalloc.h" int addressfamily = AF_UNSPEC; static void scan_for_hostname(const char *filename, char **hostname, char **port) { if(!filename || (*hostname && *port)) return; FILE *f = fopen(filename, "r"); if(!f) return; while(fgets(line, sizeof line, f)) { if(!rstrip(line)) continue; char *p = line, *q; p += strcspn(p, "\t ="); if(!*p) continue; q = p + strspn(p, "\t "); if(*q == '=') q += 1 + strspn(q + 1, "\t "); *p = 0; p = q + strcspn(q, "\t "); if(*p) *p++ = 0; p += strspn(p, "\t "); p[strcspn(p, "\t ")] = 0; if(!*port && !strcasecmp(line, "Port")) { *port = xstrdup(q); } else if(!*hostname && !strcasecmp(line, "Address")) { *hostname = xstrdup(q); if(*p) { free(*port); *port = xstrdup(p); } } if(*hostname && *port) break; } fclose(f); } char *get_my_hostname() { char *hostname = NULL; char *port = NULL; char *hostport = NULL; char *name = get_my_name(false); char *filename = NULL; // Use first Address statement in own host config file if(check_id(name)) { xasprintf(&filename, "%s" SLASH "hosts" SLASH "%s", confbase, name); scan_for_hostname(filename, &hostname, &port); scan_for_hostname(tinc_conf, &hostname, &port); } if(hostname) goto done; // If that doesn't work, guess externally visible hostname fprintf(stderr, "Trying to discover externally visible hostname...\n"); struct addrinfo *ai = str2addrinfo("tinc-vpn.org", "80", SOCK_STREAM); struct addrinfo *aip = ai; static const char request[] = "GET http://tinc-vpn.org/host.cgi HTTP/1.0\r\n\r\n"; while(aip) { int s = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol); if(s >= 0) { if(connect(s, aip->ai_addr, aip->ai_addrlen)) { closesocket(s); s = -1; } } if(s >= 0) { send(s, request, sizeof request - 1, 0); int len = recv(s, line, sizeof line - 1, MSG_WAITALL); if(len > 0) { line[len] = 0; if(line[len - 1] == '\n') line[--len] = 0; char *p = strrchr(line, '\n'); if(p && p[1]) hostname = xstrdup(p + 1); } closesocket(s); if(hostname) break; } aip = aip->ai_next; continue; } if(ai) freeaddrinfo(ai); // Check that the hostname is reasonable if(hostname) { for(char *p = hostname; *p; p++) { if(isalnum(*p) || *p == '-' || *p == '.' || *p == ':') continue; // If not, forget it. free(hostname); hostname = NULL; break; } } if(!tty) { if(!hostname) { fprintf(stderr, "Could not determine the external address or hostname. Please set Address manually.\n"); return NULL; } goto save; } again: fprintf(stderr, "Please enter your host's external address or hostname"); if(hostname) fprintf(stderr, " [%s]", hostname); fprintf(stderr, ": "); if(!fgets(line, sizeof line, stdin)) { fprintf(stderr, "Error while reading stdin: %s\n", strerror(errno)); free(hostname); return NULL; } if(!rstrip(line)) { if(hostname) goto save; else goto again; } for(char *p = line; *p; p++) { if(isalnum(*p) || *p == '-' || *p == '.') continue; fprintf(stderr, "Invalid address or hostname.\n"); goto again; } free(hostname); hostname = xstrdup(line); save: if(filename) { FILE *f = fopen(filename, "a"); if(f) { fprintf(f, "\nAddress = %s\n", hostname); fclose(f); } else { fprintf(stderr, "Could not append Address to %s: %s\n", filename, strerror(errno)); } } done: if(port) { if(strchr(hostname, ':')) xasprintf(&hostport, "[%s]:%s", hostname, port); else xasprintf(&hostport, "%s:%s", hostname, port); } else { if(strchr(hostname, ':')) xasprintf(&hostport, "[%s]", hostname); else hostport = xstrdup(hostname); } free(hostname); free(port); free(filename); return hostport; } static bool fcopy(FILE *out, const char *filename) { FILE *in = fopen(filename, "r"); if(!in) { fprintf(stderr, "Could not open %s: %s\n", filename, strerror(errno)); return false; } char buf[1024]; size_t len; while((len = fread(buf, 1, sizeof buf, in))) fwrite(buf, len, 1, out); fclose(in); return true; } int cmd_invite(int argc, char *argv[]) { if(argc < 2) { fprintf(stderr, "Not enough arguments!\n"); return 1; } // Check validity of the new node's name if(!check_id(argv[1])) { fprintf(stderr, "Invalid name for node.\n"); return 1; } char *myname = get_my_name(true); if(!myname) return 1; // Ensure no host configuration file with that name exists char *filename = NULL; xasprintf(&filename, "%s" SLASH "hosts" SLASH "%s", confbase, argv[1]); if(!access(filename, F_OK)) { free(filename); fprintf(stderr, "A host config file for %s already exists!\n", argv[1]); return 1; } free(filename); // If a daemon is running, ensure no other nodes know about this name bool found = false; if(connect_tincd(false)) { sendline(fd, "%d %d", CONTROL, REQ_DUMP_NODES); while(recvline(fd, line, sizeof line)) { char node[4096]; int code, req; if(sscanf(line, "%d %d %s", &code, &req, node) != 3) break; if(!strcmp(node, argv[1])) found = true; } if(found) { fprintf(stderr, "A node with name %s is already known!\n", argv[1]); return 1; } } char hash[25]; xasprintf(&filename, "%s" SLASH "invitations", confbase); if(mkdir(filename, 0700) && errno != EEXIST) { fprintf(stderr, "Could not create directory %s: %s\n", filename, strerror(errno)); free(filename); return 1; } // Count the number of valid invitations, clean up old ones DIR *dir = opendir(filename); if(!dir) { fprintf(stderr, "Could not read directory %s: %s\n", filename, strerror(errno)); free(filename); return 1; } errno = 0; int count = 0; struct dirent *ent; time_t deadline = time(NULL) - 604800; // 1 week in the past while((ent = readdir(dir))) { if(strlen(ent->d_name) != 24) continue; char *invname; struct stat st; xasprintf(&invname, "%s" SLASH "%s", filename, ent->d_name); if(!stat(invname, &st)) { if(deadline < st.st_mtime) count++; else unlink(invname); } else { fprintf(stderr, "Could not stat %s: %s\n", invname, strerror(errno)); errno = 0; } free(invname); } if(errno) { fprintf(stderr, "Error while reading directory %s: %s\n", filename, strerror(errno)); closedir(dir); free(filename); return 1; } closedir(dir); free(filename); ecdsa_t *key; xasprintf(&filename, "%s" SLASH "invitations" SLASH "ed25519_key.priv", confbase); // Remove the key if there are no outstanding invitations. if(!count) unlink(filename); // Create a new key if necessary. FILE *f = fopen(filename, "r"); if(!f) { if(errno != ENOENT) { fprintf(stderr, "Could not read %s: %s\n", filename, strerror(errno)); free(filename); return 1; } key = ecdsa_generate(); if(!key) { free(filename); return 1; } f = fopen(filename, "w"); if(!f) { fprintf(stderr, "Could not write %s: %s\n", filename, strerror(errno)); free(filename); return 1; } chmod(filename, 0600); ecdsa_write_pem_private_key(key, f); fclose(f); if(connect_tincd(false)) sendline(fd, "%d %d", CONTROL, REQ_RELOAD); } else { key = ecdsa_read_pem_private_key(f); fclose(f); if(!key) fprintf(stderr, "Could not read private key from %s\n", filename); } free(filename); if(!key) return 1; // Create a hash of the key. char *fingerprint = ecdsa_get_base64_public_key(key); digest_t *digest = digest_open_by_name("sha256", 18); if(!digest) abort(); digest_create(digest, fingerprint, strlen(fingerprint), hash); b64encode_urlsafe(hash, hash, 18); // Create a random cookie for this invitation. char cookie[25]; randomize(cookie, 18); // Create a filename that doesn't reveal the cookie itself char buf[18 + strlen(fingerprint)]; char cookiehash[25]; memcpy(buf, cookie, 18); memcpy(buf + 18, fingerprint, sizeof buf - 18); digest_create(digest, buf, sizeof buf, cookiehash); b64encode_urlsafe(cookiehash, cookiehash, 18); b64encode_urlsafe(cookie, cookie, 18); // Create a file containing the details of the invitation. xasprintf(&filename, "%s" SLASH "invitations" SLASH "%s", confbase, cookiehash); int ifd = open(filename, O_RDWR | O_CREAT | O_EXCL, 0600); if(!ifd) { fprintf(stderr, "Could not create invitation file %s: %s\n", filename, strerror(errno)); free(filename); return 1; } f = fdopen(ifd, "w"); if(!f) abort(); // Get the local address char *address = get_my_hostname(); // Fill in the details. fprintf(f, "Name = %s\n", argv[1]); if(netname) fprintf(f, "NetName = %s\n", netname); fprintf(f, "ConnectTo = %s\n", myname); // Copy Broadcast and Mode FILE *tc = fopen(tinc_conf, "r"); if(tc) { char buf[1024]; while(fgets(buf, sizeof buf, tc)) { if((!strncasecmp(buf, "Mode", 4) && strchr(" \t=", buf[4])) || (!strncasecmp(buf, "Broadcast", 9) && strchr(" \t=", buf[9]))) { fputs(buf, f); // Make sure there is a newline character. if(!strchr(buf, '\n')) fputc('\n', f); } } fclose(tc); } fprintf(f, "#---------------------------------------------------------------#\n"); fprintf(f, "Name = %s\n", myname); char *filename2; xasprintf(&filename2, "%s" SLASH "hosts" SLASH "%s", confbase, myname); fcopy(f, filename2); fclose(f); free(filename2); // Create an URL from the local address, key hash and cookie char *url; xasprintf(&url, "%s/%s%s", address, hash, cookie); // Call the inviation-created script char *envp[6] = {}; xasprintf(&envp[0], "NAME=%s", myname); xasprintf(&envp[1], "NETNAME=%s", netname); xasprintf(&envp[2], "NODE=%s", argv[1]); xasprintf(&envp[3], "INVITATION_FILE=%s", filename); xasprintf(&envp[4], "INVITATION_URL=%s", url); execute_script("invitation-created", envp); for(int i = 0; i < 6 && envp[i]; i++) free(envp[i]); puts(url); free(url); free(filename); free(address); return 0; } static int sock; static char cookie[18]; static sptps_t sptps; static char *data; static size_t datalen; static bool success = false; static char cookie[18], hash[18]; static char *get_line(const char **data) { if(!data || !*data) return NULL; if(!**data) { *data = NULL; return NULL; } static char line[1024]; const char *end = strchr(*data, '\n'); size_t len = end ? end - *data : strlen(*data); if(len >= sizeof line) { fprintf(stderr, "Maximum line length exceeded!\n"); return NULL; } if(len && !isprint(**data)) abort(); memcpy(line, *data, len); line[len] = 0; if(end) *data = end + 1; else *data = NULL; return line; } static char *get_value(const char *data, const char *var) { char *line = get_line(&data); if(!line) return NULL; char *sep = line + strcspn(line, " \t="); char *val = sep + strspn(sep, " \t"); if(*val == '=') val += 1 + strspn(val + 1, " \t"); *sep = 0; if(strcasecmp(line, var)) return NULL; return val; } static char *grep(const char *data, const char *var) { static char value[1024]; const char *p = data; int varlen = strlen(var); // Skip all lines not starting with var while(strncasecmp(p, var, varlen) || !strchr(" \t=", p[varlen])) { p = strchr(p, '\n'); if(!p) break; else p++; } if(!p) return NULL; p += varlen; p += strspn(p, " \t"); if(*p == '=') p += 1 + strspn(p + 1, " \t"); const char *e = strchr(p, '\n'); if(!e) return xstrdup(p); if(e - p >= sizeof value) { fprintf(stderr, "Maximum line length exceeded!\n"); return NULL; } memcpy(value, p, e - p); value[e - p] = 0; return value; } static bool finalize_join(void) { char *name = xstrdup(get_value(data, "Name")); if(!name) { fprintf(stderr, "No Name found in invitation!\n"); return false; } if(!check_id(name)) { fprintf(stderr, "Invalid Name found in invitation: %s!\n", name); return false; } if(!netname) netname = grep(data, "NetName"); bool ask_netname = false; char temp_netname[32]; make_names: if(!confbasegiven) { free(confbase); confbase = NULL; } make_names(); free(tinc_conf); free(hosts_dir); xasprintf(&tinc_conf, "%s" SLASH "tinc.conf", confbase); xasprintf(&hosts_dir, "%s" SLASH "hosts", confbase); if(!access(tinc_conf, F_OK)) { fprintf(stderr, "Configuration file %s already exists!\n", tinc_conf); if(confbasegiven) return false; // Generate a random netname, ask for a better one later. ask_netname = true; snprintf(temp_netname, sizeof temp_netname, "join_%x", rand()); netname = temp_netname; goto make_names; } if(mkdir(confbase, 0777) && errno != EEXIST) { fprintf(stderr, "Could not create directory %s: %s\n", confbase, strerror(errno)); return false; } if(mkdir(hosts_dir, 0777) && errno != EEXIST) { fprintf(stderr, "Could not create directory %s: %s\n", hosts_dir, strerror(errno)); return false; } FILE *f = fopen(tinc_conf, "w"); if(!f) { fprintf(stderr, "Could not create file %s: %s\n", tinc_conf, strerror(errno)); return false; } fprintf(f, "Name = %s\n", name); char *filename; xasprintf(&filename, "%s" SLASH "%s", hosts_dir, name); FILE *fh = fopen(filename, "w"); if(!fh) { fprintf(stderr, "Could not create file %s: %s\n", filename, strerror(errno)); fclose(f); return false; } // Filter first chunk on approved keywords, split between tinc.conf and hosts/Name // Other chunks go unfiltered to their respective host config files const char *p = data; char *l, *value; while((l = get_line(&p))) { // Ignore comments if(*l == '#') continue; // Split line into variable and value int len = strcspn(l, "\t ="); value = l + len; value += strspn(value, "\t "); if(*value == '=') { value++; value += strspn(value, "\t "); } l[len] = 0; // Is it a Name? if(!strcasecmp(l, "Name")) if(strcmp(value, name)) break; else continue; else if(!strcasecmp(l, "NetName")) continue; // Check the list of known variables bool found = false; int i; for(i = 0; variables[i].name; i++) { if(strcasecmp(l, variables[i].name)) continue; found = true; break; } // Ignore unknown and unsafe variables if(!found) { fprintf(stderr, "Ignoring unknown variable '%s' in invitation.\n", l); continue; } else if(!(variables[i].type & VAR_SAFE)) { fprintf(stderr, "Ignoring unsafe variable '%s' in invitation.\n", l); continue; } // Copy the safe variable to the right config file fprintf(variables[i].type & VAR_HOST ? fh : f, "%s = %s\n", l, value); } fclose(f); free(filename); while(l && !strcasecmp(l, "Name")) { if(!check_id(value)) { fprintf(stderr, "Invalid Name found in invitation.\n"); return false; } if(!strcmp(value, name)) { fprintf(stderr, "Secondary chunk would overwrite our own host config file.\n"); return false; } xasprintf(&filename, "%s" SLASH "%s", hosts_dir, value); f = fopen(filename, "w"); if(!f) { fprintf(stderr, "Could not create file %s: %s\n", filename, strerror(errno)); return false; } while((l = get_line(&p))) { if(!strcmp(l, "#---------------------------------------------------------------#")) continue; int len = strcspn(l, "\t ="); if(len == 4 && !strncasecmp(l, "Name", 4)) { value = l + len; value += strspn(value, "\t "); if(*value == '=') { value++; value += strspn(value, "\t "); } l[len] = 0; break; } fputs(l, f); fputc('\n', f); } fclose(f); free(filename); } // Generate our key and send a copy to the server ecdsa_t *key = ecdsa_generate(); if(!key) return false; char *b64key = ecdsa_get_base64_public_key(key); if(!b64key) return false; xasprintf(&filename, "%s" SLASH "ed25519_key.priv", confbase); f = fopenmask(filename, "w", 0600); if(!ecdsa_write_pem_private_key(key, f)) { fprintf(stderr, "Error writing private key!\n"); ecdsa_free(key); fclose(f); return false; } fclose(f); fprintf(fh, "Ed25519PublicKey = %s\n", b64key); sptps_send_record(&sptps, 1, b64key, strlen(b64key)); free(b64key); rsa_t *rsa = rsa_generate(2048, 0x1001); xasprintf(&filename, "%s" SLASH "rsa_key.priv", confbase); f = fopenmask(filename, "w", 0600); rsa_write_pem_private_key(rsa, f); fclose(f); rsa_write_pem_public_key(rsa, fh); fclose(fh); ecdsa_free(key); rsa_free(rsa); check_port(name); ask_netname: if(ask_netname && tty) { fprintf(stderr, "Enter a new netname: "); if(!fgets(line, sizeof line, stdin)) { fprintf(stderr, "Error while reading stdin: %s\n", strerror(errno)); return false; } if(!*line || *line == '\n') goto ask_netname; line[strlen(line) - 1] = 0; char *newbase; xasprintf(&newbase, CONFDIR SLASH "tinc" SLASH "%s", line); if(rename(confbase, newbase)) { fprintf(stderr, "Error trying to rename %s to %s: %s\n", confbase, newbase, strerror(errno)); free(newbase); goto ask_netname; } free(newbase); netname = line; make_names(); } fprintf(stderr, "Configuration stored in: %s\n", confbase); return true; } static bool invitation_send(void *handle, uint8_t type, const char *data, size_t len) { while(len) { int result = send(sock, data, len, 0); if(result == -1 && errno == EINTR) continue; else if(result <= 0) return false; data += result; len -= result; } return true; } static bool invitation_receive(void *handle, uint8_t type, const char *msg, uint16_t len) { switch(type) { case SPTPS_HANDSHAKE: return sptps_send_record(&sptps, 0, cookie, sizeof cookie); case 0: data = xrealloc(data, datalen + len + 1); memcpy(data + datalen, msg, len); datalen += len; data[datalen] = 0; break; case 1: return finalize_join(); case 2: fprintf(stderr, "Invitation succesfully accepted.\n"); shutdown(sock, SHUT_RDWR); success = true; break; default: return false; } return true; } int cmd_join(int argc, char *argv[]) { free(data); data = NULL; datalen = 0; if(argc > 2) { fprintf(stderr, "Too many arguments!\n"); return 1; } // Make sure confbase exists and is accessible. if(!confbase_given && mkdir(confdir, 0755) && errno != EEXIST) { fprintf(stderr, "Could not create directory %s: %s\n", confdir, strerror(errno)); return 1; } if(mkdir(confbase, 0777) && errno != EEXIST) { fprintf(stderr, "Could not create directory %s: %s\n", confbase, strerror(errno)); return 1; } if(access(confbase, R_OK | W_OK | X_OK)) { fprintf(stderr, "No permission to write in directory %s: %s\n", confbase, strerror(errno)); return 1; } // If a netname or explicit configuration directory is specified, check for an existing tinc.conf. if((netname || confbasegiven) && !access(tinc_conf, F_OK)) { fprintf(stderr, "Configuration file %s already exists!\n", tinc_conf); return 1; } // Either read the invitation from the command line or from stdin. char *invitation; if(argc > 1) { invitation = argv[1]; } else { if(tty) fprintf(stderr, "Enter invitation URL: "); errno = EPIPE; if(!fgets(line, sizeof line, stdin)) { fprintf(stderr, "Error while reading stdin: %s\n", strerror(errno)); return false; } invitation = line; } // Parse the invitation URL. rstrip(line); char *slash = strchr(invitation, '/'); if(!slash) goto invalid; *slash++ = 0; if(strlen(slash) != 48) goto invalid; char *address = invitation; char *port = NULL; if(*address == '[') { address++; char *bracket = strchr(address, ']'); if(!bracket) goto invalid; *bracket = 0; if(bracket[1] == ':') port = bracket + 2; } else { port = strchr(address, ':'); if(port) *port++ = 0; } if(!port || !*port) port = "655"; if(!b64decode(slash, hash, 24) || !b64decode(slash + 24, cookie, 24)) goto invalid; // Generate a throw-away key for the invitation. ecdsa_t *key = ecdsa_generate(); if(!key) return 1; char *b64key = ecdsa_get_base64_public_key(key); // Connect to the tinc daemon mentioned in the URL. struct addrinfo *ai = str2addrinfo(address, port, SOCK_STREAM); if(!ai) return 1; sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if(sock <= 0) { fprintf(stderr, "Could not open socket: %s\n", strerror(errno)); return 1; } if(connect(sock, ai->ai_addr, ai->ai_addrlen)) { fprintf(stderr, "Could not connect to %s port %s: %s\n", address, port, strerror(errno)); closesocket(sock); return 1; } fprintf(stderr, "Connected to %s port %s...\n", address, port); // Tell him we have an invitation, and give him our throw-away key. int len = snprintf(line, sizeof line, "0 ?%s %d.%d\n", b64key, PROT_MAJOR, PROT_MINOR); if(len <= 0 || len >= sizeof line) abort(); if(!sendline(sock, "0 ?%s %d.%d", b64key, PROT_MAJOR, 1)) { fprintf(stderr, "Error sending request to %s port %s: %s\n", address, port, strerror(errno)); closesocket(sock); return 1; } char hisname[4096] = ""; int code, hismajor, hisminor = 0; if(!recvline(sock, line, sizeof line) || sscanf(line, "%d %s %d.%d", &code, hisname, &hismajor, &hisminor) < 3 || code != 0 || hismajor != PROT_MAJOR || !check_id(hisname) || !recvline(sock, line, sizeof line) || !rstrip(line) || sscanf(line, "%d ", &code) != 1 || code != ACK || strlen(line) < 3) { fprintf(stderr, "Cannot read greeting from peer\n"); closesocket(sock); return 1; } // Check if the hash of the key he gave us matches the hash in the URL. char *fingerprint = line + 2; digest_t *digest = digest_open_by_name("sha256", 18); if(!digest) abort(); char hishash[18]; if(!digest_create(digest, fingerprint, strlen(fingerprint), hishash)) { fprintf(stderr, "Could not create digest\n%s\n", line + 2); return 1; } if(memcmp(hishash, hash, 18)) { fprintf(stderr, "Peer has an invalid key!\n%s\n", line + 2); return 1; } ecdsa_t *hiskey = ecdsa_set_base64_public_key(fingerprint); if(!hiskey) return 1; // Start an SPTPS session if(!sptps_start(&sptps, NULL, true, false, key, hiskey, "tinc invitation", 15, invitation_send, invitation_receive)) return 1; // Feed rest of input buffer to SPTPS if(!sptps_receive_data(&sptps, buffer, blen)) return 1; while((len = recv(sock, line, sizeof line, 0))) { if(len < 0) { if(errno == EINTR) continue; fprintf(stderr, "Error reading data from %s port %s: %s\n", address, port, strerror(errno)); return 1; } if(!sptps_receive_data(&sptps, line, len)) return 1; } sptps_stop(&sptps); ecdsa_free(hiskey); ecdsa_free(key); closesocket(sock); if(!success) { fprintf(stderr, "Connection closed by peer, invitation cancelled.\n"); return 1; } return 0; invalid: fprintf(stderr, "Invalid invitation URL.\n"); return 1; }