Add an invitation protocol.
authorGuus Sliepen <guus@tinc-vpn.org>
Wed, 29 May 2013 16:31:10 +0000 (18:31 +0200)
committerGuus Sliepen <guus@tinc-vpn.org>
Wed, 29 May 2013 16:31:10 +0000 (18:31 +0200)
Using the tinc command, an administrator of an existing VPN can generate
invitations for new nodes. The invitation is a small URL that can easily
be copy&pasted into email or live chat. Another person can have tinc
automatically setup the necessary configuration files and exchange keys
with the server, by only using the invitation URL.

The invitation protocol uses temporary ECDSA keys. The invitation URL
consists of the hostname and port of the server, a hash of the server's
temporary ECDSA key and a cookie. When the client wants to accept an
invitation, it also creates a temporary ECDSA key, connects to the server
and says it wants to accept an invitation. Both sides exchange their
temporary keys. The client verifies that the server's key matches the hash
in the invitation URL. After setting up an SPTPS connection using the
temporary keys, the client gives the cookie to the server. If the cookie
is valid, the server sends the client an invitation file containing the
client's new name and a copy of the server's host config file. If everything
is ok, the client will generate a long-term ECDSA key and send it to the
server, which will add it to a new host config file for the client.

The invitation protocol currently allows multiple host config files to be
send from the server to the client. However, the client filters out
most configuration variables for its own host configuration file. In
particular, it only accepts Name, Mode, Broadcast, ConnectTo, Subnet and
AutoConnect. Also, at the moment no tinc-up script is generated.

When an invitation has succesfully been accepted, the client needs to start
the tinc daemon manually.

13 files changed:
bash_completion.d/tinc
configure.ac
doc/tinc.8.in
doc/tinc.texi
src/Makefile.am
src/connection.h
src/invitation.c [new file with mode: 0644]
src/invitation.h [new file with mode: 0644]
src/net_setup.c
src/protocol.h
src/protocol_auth.c
src/tincctl.c
src/tincctl.h

index 4c64fa2..3d5814d 100644 (file)
@@ -5,7 +5,7 @@ _tinc() {
        prev="${COMP_WORDS[COMP_CWORD-1]}"
        opts="-c -d -D -K -n -o -L -R -U --config --no-detach --debug --net --option --mlock --logfile --pidfile --chroot --user --help --version"
        confvars="Address AddressFamily BindToAddress BindToInterface Broadcast Cipher ClampMSS Compression ConnectTo DecrementTTL Device DeviceType Digest DirectOnly ECDSAPrivateKeyFile ECDSAPublicKey ECDSAPublicKeyFile ExperimentalProtocol Forwarding GraphDumpFile Hostnames IffOneQueue IndirectData Interface KeyExpire LocalDiscovery MACExpire MACLength MaxOutputBufferSize MaxTimeout Mode Name PMTU PMTUDiscovery PingInterval PingTimeout Port PriorityInheritance PrivateKeyFile ProcessPriority Proxy PublicKeyFile ReplayWindow StrictSubnets Subnet TCPOnly TunnelServer UDPRcvBuf UDPSndBuf VDEGroup VDEPort Weight"
-       commands="add connect debug del disconnect dump edit export export-all generate-ecdsa-keys generate-keys generate-rsa-keys get help import info init log pcap pid purge reload restart retry set start stop top version"
+       commands="add connect debug del disconnect dump edit export export-all generate-ecdsa-keys generate-keys generate-rsa-keys get help import info init invite join log pcap pid purge reload restart retry set start stop top version"
 
        case ${prev} in
                -c|--config)
index 9674994..34e2533 100644 (file)
@@ -6,7 +6,6 @@ AC_CONFIG_SRCDIR([src/tincd.c])
 AC_GNU_SOURCE
 AM_INIT_AUTOMAKE([check-news dist-xz no-dist-gzip std-options subdir-objects -Wall])
 AC_CONFIG_HEADERS([config.h])
-AM_MAINTAINER_MODE
 
 # Enable GNU extensions.
 # Define this here, not in acconfig's @TOP@ section, since definitions
index fba373e..2cff94c 100644 (file)
@@ -90,6 +90,15 @@ is used.
 The same as export followed by import.
 .It exchange-all Op Fl -force
 The same as export-all followed by import.
+.It invite Ar name
+Prepares an invitation for a new node with the given
+.Ar name ,
+and prints a short invitation URL that can be used with the join command.
+.It join Op Ar URL
+Join an existing VPN using an invitation URL created using the invite command.
+If no
+.Ar URL
+is given, it will be read from standard input.
 .It start Op tincd options
 Start
 .Xr tincd 8 ,
index fc94b0a..5e8df2b 100644 (file)
@@ -2214,6 +2214,14 @@ The same as export followed by import.
 @item exchange-all [--force]
 The same as export-all followed by import.
 
+@item invite @var{name}
+Prepares an invitation for a new node with the given @var{name},
+and prints a short invitation URL that can be used with the join command.
+
+@item join [@var{URL}]
+Join an existing VPN using an invitation URL created using the invite command.
+If no @var{URL} is given, it will be read from standard input.
+
 @item start [tincd options]
 Start @samp{tincd}, optionally with the given extra options.
 
@@ -2232,10 +2240,17 @@ in @file{tinc.conf} will be made.
 Shows the PID of the currently running @samp{tincd}.
 
 @item generate-keys [@var{bits}]
-Generate public/private keypair of @var{bits} length. If @var{bits} is not specified,
-1024 is the default. tinc will ask where you want to store the files,
-but will default to the configuration directory (you can use the -c or -n
-option).
+Generate both RSA and ECDSA keypairs (see below) and exit.
+tinc will ask where you want to store the files, but will default to the
+configuration directory (you can use the -c or -n option).
+
+@item generate-ecdsa-keys
+Generate public/private ECDSA keypair and exit.
+
+@item generate-rsa-keys [@var{bits}]
+Generate public/private RSA keypair and exit.  If @var{bits} is omitted, the
+default length will be 2048 bits.  When saving keys to existing files, tinc
+will not delete the old keys; you have to remove them manually.
 
 @item dump [reachable] nodes
 Dump a list of all known nodes in the VPN.
index 4e7de25..57e0b69 100644 (file)
@@ -5,18 +5,67 @@ sbin_PROGRAMS = tincd tinc sptps_test
 EXTRA_DIST = linux bsd solaris cygwin mingw openssl gcrypt
 
 tincd_SOURCES = \
-       utils.c getopt.c getopt1.c list.c splay_tree.c dropin.c fake-getaddrinfo.c fake-getnameinfo.c hash.c \
-       buffer.c conf.c connection.c control.c edge.c graph.c logger.c meta.c net.c net_packet.c net_setup.c \
-       net_socket.c netutl.c node.c process.c protocol.c protocol_auth.c protocol_edge.c protocol_misc.c \
-       protocol_key.c protocol_subnet.c route.c sptps.c subnet.c subnet_parse.c event.c tincd.c \
-       dummy_device.c raw_socket_device.c multicast_device.c names.c
+       buffer.c \
+       conf.c \
+       connection.c \
+       control.c \
+       dropin.c \
+       dummy_device.c \
+       edge.c \
+       event.c \
+       fake-getaddrinfo.c \
+       fake-getnameinfo.c \
+       getopt.c \
+       getopt1.c \
+       graph.c \
+       hash.c \
+       list.c \
+       logger.c \
+       meta.c \
+       multicast_device.c \
+       names.c \
+       net.c \
+       net_packet.c \
+       net_setup.c \
+       net_socket.c \
+       netutl.c \
+       node.c \
+       process.c \
+       protocol.c \
+       protocol_auth.c \
+       protocol_edge.c \
+       protocol_key.c \
+       protocol_misc.c \
+       protocol_subnet.c \
+       raw_socket_device.c \
+       route.c \
+       splay_tree.c \
+       sptps.c \
+       subnet.c \
+       subnet_parse.c \
+       tincd.c \
+       utils.c
 
 tinc_SOURCES = \
-       utils.c getopt.c getopt1.c dropin.c \
-       info.c list.c subnet_parse.c tincctl.c top.c names.c
+       dropin.c \
+       getopt.c \
+       getopt1.c \
+       info.c \
+       invitation.c \
+       list.c \
+       names.c \
+       netutl.c \
+       sptps.c \
+       subnet_parse.c \
+       tincctl.c \
+       top.c \
+       utils.c
 
 sptps_test_SOURCES = \
-       logger.c sptps.c sptps_test.c utils.c
+       logger.c \
+       sptps.c \
+       sptps_test.c \
+       utils.c
 
 ## Conditionally compile device drivers
        
@@ -55,22 +104,27 @@ if OPENSSL
 tincd_SOURCES += \
        openssl/cipher.c \
        openssl/crypto.c \
+       openssl/digest.c \
        openssl/ecdh.c \
        openssl/ecdsa.c \
-       openssl/digest.c \
        openssl/prf.c \
        openssl/rsa.c
 tinc_SOURCES += \
+       openssl/cipher.c \
+       openssl/crypto.c \
+       openssl/digest.c \
+       openssl/ecdh.c \
        openssl/ecdsa.c \
        openssl/ecdsagen.c \
+       openssl/prf.c \
        openssl/rsa.c \
        openssl/rsagen.c
 sptps_test_SOURCES += \
        openssl/cipher.c \
        openssl/crypto.c \
+       openssl/digest.c \
        openssl/ecdh.c \
        openssl/ecdsa.c \
-       openssl/digest.c \
        openssl/prf.c
 endif
 
@@ -78,32 +132,80 @@ if GCRYPT
 tincd_SOURCES += \
        gcrypt/cipher.c \
        gcrypt/crypto.c \
+       gcrypt/digest.c \
        gcrypt/ecdh.c \
        gcrypt/ecdsa.c \
-       gcrypt/digest.c \
        gcrypt/prf.c \
        gcrypt/rsa.c
 tinc_SOURCES += \
+       gcrypt/cipher.c \
+       gcrypt/crypto.c \
+       gcrypt/digest.c \
+       gcrypt/ecdh.c \
        gcrypt/ecdsa.c \
        gcrypt/ecdsagen.c \
+       gcrypt/prf.c \
        gcrypt/rsa.c \
        gcrypt/rsagen.c
 sptps_test_SOURCES += \
        gcrypt/cipher.c \
        gcrypt/crypto.c \
+       gcrypt/digest.c \
        gcrypt/ecdh.c \
        gcrypt/ecdsa.c \
-       gcrypt/digest.c \
        gcrypt/prf.c
 endif
 
 tinc_LDADD = $(READLINE_LIBS) $(CURSES_LIBS)
 
 noinst_HEADERS = \
-       xalloc.h utils.h getopt.h list.h splay_tree.h dropin.h fake-getaddrinfo.h fake-getnameinfo.h fake-gai-errnos.h ipv6.h ipv4.h ethernet.h \
-       buffer.h conf.h connection.h control.h control_common.h device.h edge.h graph.h info.h logger.h meta.h net.h netutl.h node.h process.h \
-       protocol.h route.h subnet.h sptps.h tincctl.h top.h hash.h event.h names.h have.h system.h \
-       cipher.h crypto.h digest.h ecdh.h ecdsa.h ecdsagen.h prf.h rsa.h rsagen.h
+       buffer.h \
+       cipher.h \
+       conf.h \
+       connection.h \
+       control.h \
+       control_common.h \
+       crypto.h \
+       device.h \
+       digest.h \
+       dropin.h \
+       ecdh.h \
+       ecdsa.h \
+       ecdsagen.h \
+       edge.h \
+       ethernet.h \
+       event.h \
+       fake-gai-errnos.h \
+       fake-getaddrinfo.h \
+       fake-getnameinfo.h \
+       getopt.h \
+       graph.h \
+       hash.h \
+       have.h \
+       info.h \
+       ipv4.h \
+       ipv6.h \
+       list.h \
+       logger.h \
+       meta.h \
+       names.h \
+       net.h \
+       netutl.h \
+       node.h \
+       prf.h \
+       process.h \
+       protocol.h \
+       route.h \
+       rsa.h \
+       rsagen.h
+       splay_tree.h \
+       sptps.h \
+       subnet.h \
+       system.h \
+       tincctl.h \
+       top.h \
+       utils.h \
+       xalloc.h
 
 LIBS = @LIBS@ @LIBGCRYPT_LIBS@
 
index 5b6a94c..b5d3d18 100644 (file)
@@ -47,7 +47,9 @@ typedef struct connection_status_t {
                unsigned int control:1;                 /* 1 if this is a control connection */
                unsigned int pcap:1;                    /* 1 if this is a control connection requesting packet capture */
                unsigned int log:1;                     /* 1 if this is a control connection requesting log dump */
-               unsigned int unused:20;
+               unsigned int invitation:1;              /* 1 if this is an invitation */
+               unsigned int invitation_used:1;         /* 1 if the invitation has been consumed */
+               unsigned int unused:19;
 } connection_status_t;
 
 #include "ecdsa.h"
diff --git a/src/invitation.c b/src/invitation.c
new file mode 100644 (file)
index 0000000..7a6a91b
--- /dev/null
@@ -0,0 +1,895 @@
+/*
+    invitation.c -- Create and accept invitations
+    Copyright (C) 2013 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 "sptps.h"
+#include "tincctl.h"
+#include "utils.h"
+#include "xalloc.h"
+
+#ifdef HAVE_MINGW
+#define SCRIPTEXTENSION ".bat"
+#else
+#define SCRIPTEXTENSION ""
+#endif
+
+int addressfamily = AF_UNSPEC;
+
+char *get_my_hostname() {
+       char *hostname = 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);
+               FILE *f = fopen(filename, "r");
+               if(f) {
+                       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;
+                               if(strcasecmp(line, "Address"))
+                                       continue;
+                               p = q + strcspn(q, "\t ");
+                               if(*p)
+                                       *p++ = 0;
+                               p += strspn(p, "\t ");
+                               p[strcspn(p, "\t ")] = 0;
+                               if(*p) {
+                                       if(strchr(q, ':'))
+                                               xasprintf(&hostname, "[%s]:%s", q, p);
+                                       else
+                                               xasprintf(&hostname, "%s:%s", q, p);
+                               } else {
+                                       hostname = xstrdup(q);
+                               }
+                               break;
+                       }
+                       fclose(f);
+               }
+       }
+
+       if(hostname) {
+               free(filename);
+               return hostname;
+       }
+
+       // If that doesn't work, guess externally visible hostname
+       fprintf(stderr, "Trying to discover externally visible hostname...\n");
+       struct addrinfo *ai = str2addrinfo("ifconfig.me", "80", SOCK_STREAM);
+       static const char request[] = "GET /host HTTP/1.0\r\n\r\n";
+       if(ai) {
+               int s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+               if(s >= 0) {
+                       if(connect(s, ai->ai_addr, ai->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);
+               }
+               freeaddrinfo(ai);
+       }
+
+       // Check that the hostname is reasonable
+       if(hostname) {
+               for(char *p = hostname; *p; p++) {
+                       if(isalnum(*p) || *p == '-' || *p == '.')
+                               continue;
+                       // If not, forget it.
+                       free(hostname);
+                       hostname = NULL;
+                       break;
+               }
+       }
+
+again:
+       printf("Please enter your host's external address or hostname");
+       if(hostname)
+               printf(" [%s]", hostname);
+       printf(": ");
+       fflush(stdout);
+
+       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 done;
+               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);
+
+done:
+       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));
+               }
+               free(filename);
+       }
+
+       return hostname;
+}
+
+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 now 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 "ecdsa_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);
+       } else {
+               key = ecdsa_read_pem_private_key(f);
+               if(!key)
+                       fprintf(stderr, "Could not read private key from %s\n", filename);
+       }
+       fclose(f);
+       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);
+       b64encode_urlsafe(cookie, cookie, 18);
+
+       // Create a file containing the details of the invitation.
+       xasprintf(&filename, "%s" SLASH "invitations" SLASH "%s", confbase, cookie);
+       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;
+       }
+       free(filename);
+       f = fdopen(ifd, "w");
+       if(!f)
+               abort();
+
+       // 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);
+       // TODO: copy Broadcast and Mode
+       fprintf(f, "#---------------------------------------------------------------#\n");
+       fprintf(f, "Name = %s\n", myname);
+
+       xasprintf(&filename, "%s" SLASH "hosts" SLASH "%s", confbase, myname);
+       fcopy(f, filename);
+       fclose(f);
+
+       // Create an URL from the local address, key hash and cookie
+       char *address = get_my_hostname();
+       printf("%s/%s%s\n", address, hash, cookie);
+       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");
+
+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(!tty || confbasegiven)
+                       return false;
+ask_netname:
+               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;
+               netname = line;
+               goto make_names;
+       }       
+
+       if(mkdir(confbase, 0755) && errno != EEXIST) {
+               fprintf(stderr, "Could not create directory %s: %s\n", confbase, strerror(errno));
+               return false;
+       }
+
+       if(mkdir(hosts_dir, 0755) && 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));
+               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 "ecdsa_key.priv", confbase);
+       f = fopen(filename, "w");
+
+#ifdef HAVE_FCHMOD
+       /* Make it unreadable for others. */
+       fchmod(fileno(f), 0600);
+#endif
+
+       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, "ECDSAPublicKey = %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 = fopen(filename, "w");
+
+#ifdef HAVE_FCHMOD
+       /* Make it unreadable for others. */
+       fchmod(fileno(f), 0600);
+#endif
+
+       rsa_write_pem_private_key(rsa, f);
+       fclose(f);
+
+       rsa_write_pem_public_key(rsa, fh);
+       fclose(fh);
+
+       ecdsa_free(key);
+       rsa_free(rsa);
+
+       fprintf(stderr, "Invitation succesfully accepted.\n");
+       shutdown(sock, SHUT_RDWR);
+       success = true;
+
+       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();
+
+               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 confdir exists.
+       if(mkdir(confdir, 0755) && errno != EEXIST) {
+               fprintf(stderr, "Could not create directory %s: %s\n", CONFDIR, strerror(errno));
+               return 1;
+       }
+
+       // Either read the invitation from the command line or from stdin.
+       char *invitation;
+
+       if(argc > 1) {
+               invitation = argv[1];
+       } else {
+               if(tty) {
+                       printf("Enter invitation URL: ");
+                       fflush(stdout);
+               }
+               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, 18) || !b64decode(slash + 24, cookie, 18))
+               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 have 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;
+}
diff --git a/src/invitation.h b/src/invitation.h
new file mode 100644 (file)
index 0000000..3d017e9
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+    invitation.h -- header for invitation.c.
+    Copyright (C) 2013 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.
+*/
+
+#ifndef __TINC_INVITATION_H__
+#define __TINC_INVITATION_H__
+
+bool recvdata(int fd, char *data, size_t len);
+int cmd_invite(int argc, char *argv[]);
+int cmd_join(int argc, char *argv[]);
+
+#endif
index cb14c51..747dbd8 100644 (file)
@@ -223,6 +223,30 @@ static bool read_ecdsa_private_key(void) {
        return myself->connection->ecdsa;
 }
 
+static bool read_invitation_key(void) {
+       FILE *fp;
+       char *fname;
+
+       if(invitation_key) {
+               ecdsa_free(invitation_key);
+               invitation_key = NULL;
+       }
+
+       xasprintf(&fname, "%s" SLASH "invitations" SLASH "ecdsa_key.priv", confbase);
+
+       fp = fopen(fname, "r");
+
+       if(fp) {
+               invitation_key = ecdsa_read_pem_private_key(fp);
+               fclose(fp);
+               if(!invitation_key)
+                       logger(DEBUG_ALWAYS, LOG_ERR, "Reading ECDSA private key file `%s' failed: %s", fname, strerror(errno));
+       }
+
+       free(fname);
+       return invitation_key;
+}
+
 static bool read_rsa_private_key(void) {
        FILE *fp;
        char *fname;
@@ -603,6 +627,8 @@ bool setup_myself_reloadable(void) {
 
        get_config_bool(lookup_config(config_tree, "DisableBuggyPeers"), &disablebuggypeers);
 
+       read_invitation_key();
+
        return true;
 }
 
index 1df54fc..93ef3f2 100644 (file)
@@ -21,6 +21,8 @@
 #ifndef __TINC_PROTOCOL_H__
 #define __TINC_PROTOCOL_H__
 
+#include "ecdsa.h"
+
 /* Protocol version. Different major versions are incompatible. */
 
 #define PROT_MAJOR 17
@@ -59,6 +61,8 @@ extern bool tunnelserver;
 extern bool strictsubnets;
 extern bool experimental;
 
+extern ecdsa_t *invitation_key;
+
 /* Maximum size of strings in a request.
  * scanf terminates %2048s with a NUL character,
  * but the NUL character can be written after the 2048th non-NUL character.
index f030b86..05724d6 100644 (file)
 #include "cipher.h"
 #include "crypto.h"
 #include "digest.h"
+#include "ecdsa.h"
 #include "edge.h"
 #include "graph.h"
 #include "logger.h"
 #include "meta.h"
+#include "names.h"
 #include "net.h"
 #include "netutl.h"
 #include "node.h"
@@ -41,6 +43,8 @@
 #include "utils.h"
 #include "xalloc.h"
 
+ecdsa_t *invitation_key = NULL;
+
 static bool send_proxyrequest(connection_t *c) {
        switch(proxytype) {
                case PROXY_HTTP: {
@@ -146,6 +150,107 @@ bool send_id(connection_t *c) {
        return send_request(c, "%d %s %d.%d", ID, myself->connection->name, myself->connection->protocol_major, minor);
 }
 
+static bool finalize_invitation(connection_t *c, const char *data, uint16_t len) {
+       if(strchr(data, '\n')) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Received invalid key from invited node %s (%s)!\n", c->name, c->hostname);
+               return false;
+       }
+
+       // Create a new host config file
+       char filename[PATH_MAX];
+       snprintf(filename, sizeof filename, "%s" SLASH "hosts" SLASH "%s", confbase, c->name);
+       if(!access(filename, F_OK)) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Host config file for %s (%s) already exists!\n", c->name, c->hostname);
+               return false;
+       }
+
+       FILE *f = fopen(filename, "w");
+       if(!f) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Error trying to create %s: %s\n", filename, strerror(errno));
+               return false;
+       }
+
+       fprintf(f, "ECDSAPublicKey = %s\n", data);
+       fclose(f);
+
+       logger(DEBUG_CONNECTIONS, LOG_INFO, "Key succesfully received from %s (%s)", c->name, c->hostname);
+       return true;
+}
+
+static bool receive_invitation_sptps(void *handle, uint8_t type, const char *data, uint16_t len) {
+       connection_t *c = handle;
+
+       if(type == 128)
+               return true;
+
+       if(type == 1 && c->status.invitation_used)
+               return finalize_invitation(c, data, len);
+
+       if(type != 0 || len != 18 || c->status.invitation_used)
+               return false;
+
+       char cookie[25];
+       b64encode_urlsafe(data, cookie, 18);
+
+       char filename[PATH_MAX], usedname[PATH_MAX];
+       snprintf(filename, sizeof filename, "%s" SLASH "invitations" SLASH "%s", confbase, cookie);
+       snprintf(usedname, sizeof usedname, "%s" SLASH "invitations" SLASH "%s.used", confbase, cookie);
+
+       // Atomically rename the invitation file
+       if(rename(filename, usedname)) {
+               if(errno == ENOENT)
+                       logger(DEBUG_ALWAYS, LOG_ERR, "Peer %s tried to use non-existing invitation %s\n", c->hostname, cookie);
+               else
+                       logger(DEBUG_ALWAYS, LOG_ERR, "Error trying to rename invitation %s\n", cookie);
+               return false;
+       }
+
+       // Open the renamed file
+       FILE *f = fopen(usedname, "r");
+       if(!f) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Error trying to open invitation %s\n", cookie);
+               return false;
+       }
+
+       // Read the new node's Name from the file
+       char buf[1024];
+       fgets(buf, sizeof buf, f);
+       if(*buf)
+               buf[strlen(buf) - 1] = 0;
+
+       len = strcspn(buf, " \t=");
+       char *name = buf + len;
+       name += strspn(name, " \t");
+       if(*name == '=') {
+               name++;
+               name += strspn(name, " \t");
+       }
+       buf[len] = 0;
+
+       if(!*buf || !*name || strcasecmp(buf, "Name") || !check_id(name)) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Invalid invitation file %s\n", cookie);
+               fclose(f);
+               return false;
+       }
+
+       free(c->name);
+       c->name = xstrdup(name);
+
+       // Send the node the contents of the invitation file
+       rewind(f);
+       size_t result;
+       while((result = fread(buf, 1, sizeof buf, f)))
+               sptps_send_record(&c->sptps, 0, buf, result);
+       sptps_send_record(&c->sptps, 1, buf, 0);
+       fclose(f);
+       unlink(usedname);
+
+       c->status.invitation_used = true;
+
+       logger(DEBUG_CONNECTIONS, LOG_INFO, "Invitation %s succesfully sent to %s (%s)", cookie, c->name, c->hostname);
+       return true;
+}
+
 bool id_h(connection_t *c, const char *request) {
        char name[MAX_STRING_SIZE];
 
@@ -168,6 +273,31 @@ bool id_h(connection_t *c, const char *request) {
                return send_request(c, "%d %d %d", ACK, TINC_CTL_VERSION_CURRENT, getpid());
        }
 
+       if(name[0] == '?') {
+               if(!invitation_key) {
+                       logger(DEBUG_ALWAYS, LOG_ERR, "Got invitation from %s but we don't have an invitation key", c->hostname);
+                       return false;
+               }
+
+               c->ecdsa = ecdsa_set_base64_public_key(name + 1);
+               if(!c->ecdsa) {
+                       logger(DEBUG_ALWAYS, LOG_ERR, "Got bad invitation from %s", c->hostname);
+                       return false;
+               }
+
+               c->status.invitation = true;
+               char *mykey = ecdsa_get_base64_public_key(invitation_key);
+               if(!mykey)
+                       return false;
+               if(!send_request(c, "%d %s", ACK, mykey))
+                       return false;
+               free(mykey);
+
+               c->protocol_minor = 2;
+
+               return sptps_start(&c->sptps, c, false, false, invitation_key, c->ecdsa, "tinc invitation", 15, send_meta_sptps, receive_invitation_sptps);
+       }
+
        /* Check if identity is a valid name */
 
        if(!check_id(name)) {
index 331299a..fc747e7 100644 (file)
 #include "xalloc.h"
 #include "protocol.h"
 #include "control_common.h"
+#include "crypto.h"
 #include "ecdsagen.h"
 #include "info.h"
+#include "invitation.h"
 #include "names.h"
 #include "rsagen.h"
 #include "utils.h"
@@ -39,6 +41,9 @@
 
 #ifdef HAVE_MINGW
 #define mkdir(a, b) mkdir(a)
+#define SCRIPTEXTENSION ".bat"
+#else
+#define SCRIPTEXTENSION ""
 #endif
 
 static char **orig_argv;
@@ -52,19 +57,21 @@ static bool show_version = false;
 
 static char *name = NULL;
 static char controlcookie[1025];
-static char *tinc_conf = NULL;
-static char *hosts_dir = NULL;
+char *tinc_conf = NULL;
+char *hosts_dir = NULL;
 struct timeval now;
 
 // Horrible global variables...
 static int pid = 0;
-static int fd = -1;
-static char line[4096];
+int fd = -1;
+char line[4096];
 static int code;
 static int req;
 static int result;
 static bool force = false;
-static bool tty = true;
+bool tty = true;
+bool confbasegiven = false;
+bool netnamegiven = false;
 
 #ifdef HAVE_MINGW
 static struct WSAData wsa_state;
@@ -145,6 +152,8 @@ static void usage(bool status) {
                                "  import [--force]           Import host configuration file(s) from standard input\n"
                                "  exchange [--force]         Same as export followed by import\n"
                                "  exchange-all [--force]     Same as export-all followed by import\n"
+                               "  invite NODE [...]          Generate an invitation for NODE\n"
+                               "  join INVITATION            Join a VPN using an INVITIATION\n"
                                "\n");
                printf("Report bugs to tinc@tinc-vpn.org.\n");
        }
@@ -161,6 +170,7 @@ static bool parse_options(int argc, char **argv) {
 
                        case 'c': /* config file */
                                confbase = xstrdup(optarg);
+                               confbasegiven = true;
                                break;
 
                        case 'n': /* net name given */
@@ -296,13 +306,11 @@ static FILE *ask_and_open(const char *filename, const char *what, const char *mo
        /* Check stdin and stdout */
        if(ask && tty) {
                /* Ask for a file and/or directory name. */
-               fprintf(stdout, "Please enter a file to save %s to [%s]: ",
-                               what, filename);
+               fprintf(stdout, "Please enter a file to save %s to [%s]: ", what, filename);
                fflush(stdout);
 
                if(fgets(buf, sizeof buf, stdin) == NULL) {
-                       fprintf(stderr, "Error while reading stdin: %s\n",
-                                       strerror(errno));
+                       fprintf(stderr, "Error while reading stdin: %s\n", strerror(errno));
                        return NULL;
                }
 
@@ -462,12 +470,14 @@ static bool rsa_keygen(int bits, bool ask) {
        return true;
 }
 
-static char buffer[4096];
-static size_t blen = 0;
+char buffer[4096];
+size_t blen = 0;
 
 bool recvline(int fd, char *line, size_t len) {
        char *newline = NULL;
 
+       if(!fd)
+abort();
        while(!(newline = memchr(buffer, '\n', blen))) {
                int result = recv(fd, buffer + blen, sizeof buffer - blen, 0);
                if(result == -1 && errno == EINTR)
@@ -490,7 +500,10 @@ bool recvline(int fd, char *line, size_t len) {
        return true;
 }
 
-static bool recvdata(int fd, char *data, size_t len) {
+bool recvdata(int fd, char *data, size_t len) {
+       if(len == -1)
+               len = blen;
+
        while(blen < len) {
                int result = recv(fd, buffer + blen, sizeof buffer - blen, 0);
                if(result == -1 && errno == EINTR)
@@ -640,7 +653,7 @@ static bool remove_service(void) {
 }
 #endif
 
-static bool connect_tincd(bool verbose) {
+bool connect_tincd(bool verbose) {
        if(fd >= 0) {
                fd_set r;
                FD_ZERO(&r);
@@ -1208,14 +1221,14 @@ static int cmd_pid(int argc, char *argv[]) {
        return 0;
 }
 
-static int rstrip(char *value) {
+int rstrip(char *value) {
        int len = strlen(value);
        while(len && strchr("\t\r\n ", value[len - 1]))
                value[--len] = 0;
        return len;
 }
 
-static char *get_my_name(bool verbose) {
+char *get_my_name(bool verbose) {
        FILE *f = fopen(tinc_conf, "r");
        if(!f) {
                if(verbose)
@@ -1250,22 +1263,14 @@ static char *get_my_name(bool verbose) {
        return NULL;
 }
 
-#define VAR_SERVER 1    /* Should be in tinc.conf */
-#define VAR_HOST 2      /* Can be in host config file */
-#define VAR_MULTIPLE 4  /* Multiple statements allowed */
-#define VAR_OBSOLETE 8  /* Should not be used anymore */
-
-static struct {
-       const char *name;
-       int type;
-} const variables[] = {
+const var_t variables[] = {
        /* Server configuration */
        {"AddressFamily", VAR_SERVER},
-       {"AutoConnect", VAR_SERVER},
+       {"AutoConnect", VAR_SERVER | VAR_SAFE},
        {"BindToAddress", VAR_SERVER | VAR_MULTIPLE},
        {"BindToInterface", VAR_SERVER},
-       {"Broadcast", VAR_SERVER},
-       {"ConnectTo", VAR_SERVER | VAR_MULTIPLE},
+       {"Broadcast", VAR_SERVER | VAR_SAFE},
+       {"ConnectTo", VAR_SERVER | VAR_MULTIPLE | VAR_SAFE},
        {"DecrementTTL", VAR_SERVER},
        {"Device", VAR_SERVER},
        {"DeviceType", VAR_SERVER},
@@ -1282,7 +1287,7 @@ static struct {
        {"MACExpire", VAR_SERVER},
        {"MaxOutputBufferSize", VAR_SERVER},
        {"MaxTimeout", VAR_SERVER},
-       {"Mode", VAR_SERVER},
+       {"Mode", VAR_SERVER | VAR_SAFE},
        {"Name", VAR_SERVER},
        {"PingInterval", VAR_SERVER},
        {"PingTimeout", VAR_SERVER},
@@ -1315,9 +1320,9 @@ static struct {
        {"Port", VAR_HOST},
        {"PublicKey", VAR_HOST | VAR_OBSOLETE},
        {"PublicKeyFile", VAR_SERVER | VAR_HOST | VAR_OBSOLETE},
-       {"Subnet", VAR_HOST | VAR_MULTIPLE},
+       {"Subnet", VAR_HOST | VAR_MULTIPLE | VAR_SAFE},
        {"TCPOnly", VAR_SERVER | VAR_HOST},
-       {"Weight", VAR_HOST},
+       {"Weight", VAR_HOST | VAR_SAFE},
        {NULL, 0}
 };
 
@@ -2014,6 +2019,8 @@ static const struct {
        {"import", cmd_import},
        {"exchange", cmd_exchange},
        {"exchange-all", cmd_exchange_all},
+       {"invite", cmd_invite},
+       {"join", cmd_join},
        {NULL, NULL},
 };
 
@@ -2066,7 +2073,7 @@ static char *complete_config(const char *text, int state) {
                if(dot) {
                        if((variables[i].type & VAR_HOST) && !strncasecmp(variables[i].name, dot + 1, strlen(dot + 1))) {
                                char *match;
-                               xasprintf(&match, "%.*s.%s", dot - text, text, variables[i].name);
+                               xasprintf(&match, "%.*s.%s", (int)(dot - text), text, variables[i].name);
                                return match;
                        }
                } else {
@@ -2262,6 +2269,8 @@ int main(int argc, char *argv[]) {
                return 0;
        }
 
+       crypto_init();
+
        tty = isatty(0) && isatty(1);
 
        if(optind >= argc)
index 114b931..64413a3 100644 (file)
 #ifndef __TINC_TINCCTL_H__
 #define __TINC_TINCCTL_H__
 
+extern bool tty;
+extern char line[4096];
+extern int fd;
+extern char buffer[4096];
+extern size_t blen;
+extern bool confbasegiven;
+extern char *tinc_conf;
+extern char *hosts_dir;
+
+#define VAR_SERVER 1    /* Should be in tinc.conf */
+#define VAR_HOST 2      /* Can be in host config file */
+#define VAR_MULTIPLE 4  /* Multiple statements allowed */
+#define VAR_OBSOLETE 8  /* Should not be used anymore */
+#define VAR_SAFE 16     /* Variable is safe when accepting invitations */
+
+typedef struct {
+       const char *name;
+       int type;
+} var_t;
+
+extern const var_t variables[];
+
+extern int rstrip(char *value);
+extern char *get_my_name(bool verbose);
+extern bool connect_tincd(bool verbose);
 extern bool sendline(int fd, char *format, ...);
 extern bool recvline(int fd, char *line, size_t len);