cfe9285adf
The option "--disable-legacy-protocol" was added to the configure script. The new protocol does not depend on any external crypto libraries, so when the option is used tinc is no longer linked to OpenSSL's libcrypto.
1010 lines
23 KiB
C
1010 lines
23 KiB
C
/*
|
|
invitation.c -- Create and accept invitations
|
|
Copyright (C) 2013-2014 Guus Sliepen <guus@tinc-vpn.org>
|
|
|
|
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"
|
|
|
|
#include "ed25519/sha512.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;
|
|
}
|
|
}
|
|
|
|
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 hash[64];
|
|
char *fingerprint = ecdsa_get_base64_public_key(key);
|
|
sha512(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[64];
|
|
memcpy(buf, cookie, 18);
|
|
memcpy(buf + 18, fingerprint, sizeof buf - 18);
|
|
sha512(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);
|
|
ecdsa_free(key);
|
|
|
|
#ifndef DISABLE_LEGACY
|
|
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);
|
|
|
|
rsa_free(rsa);
|
|
#endif
|
|
|
|
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 void *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 void *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;
|
|
char hishash[64];
|
|
if(sha512(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;
|
|
}
|