Use actual port in tincd logs / tinc get Port / invitations
authorKirill Isakov <bootctl@gmail.com>
Thu, 21 Apr 2022 12:22:32 +0000 (18:22 +0600)
committerKirill Isakov <bootctl@gmail.com>
Thu, 21 Apr 2022 12:22:32 +0000 (18:22 +0600)
If Port 0 option is used (which makes tincd bind to a port chosen
by the operating system), tinc and tincd used to print that value as
it is instead of whatever port was actually allocated.

https://github.com/gsliepen/tinc/issues/363

19 files changed:
src/control.c
src/invitation.c
src/meson.build
src/net.h
src/net_setup.c
src/net_socket.c
src/netutl.c
src/netutl.h
src/pidfile.c [new file with mode: 0644]
src/pidfile.h [new file with mode: 0644]
src/protocol_auth.c
src/tincctl.c
src/utils.c
src/utils.h
test/integration/invite.py
test/integration/testlib/proc.py
test/unit/meson.build
test/unit/test_netutl.c [new file with mode: 0644]
test/unit/test_utils.c [new file with mode: 0644]

index 4c0ab51..c089539 100644 (file)
@@ -30,6 +30,7 @@
 #include "utils.h"
 #include "xalloc.h"
 #include "random.h"
+#include "pidfile.h"
 
 char controlcookie[65];
 
@@ -144,16 +145,6 @@ bool init_control(void) {
        randomize(controlcookie, sizeof(controlcookie) / 2);
        bin2hex(controlcookie, controlcookie, sizeof(controlcookie) / 2);
 
-       mode_t mask = umask(0);
-       umask(mask | 077);
-       FILE *f = fopen(pidfilename, "w");
-       umask(mask);
-
-       if(!f) {
-               logger(DEBUG_ALWAYS, LOG_ERR, "Cannot write control socket cookie file %s: %s", pidfilename, strerror(errno));
-               return false;
-       }
-
        // Get the address and port of the first listening socket
 
        char *localhost = NULL;
@@ -163,7 +154,7 @@ bool init_control(void) {
        // Make sure we have a valid address, and map 0.0.0.0 and :: to 127.0.0.1 and ::1.
 
        if(getsockname(listen_socket[0].tcp.fd, &sa.sa, &len)) {
-               xasprintf(&localhost, "127.0.0.1 port %s", myport);
+               xasprintf(&localhost, "127.0.0.1 port %s", myport.tcp);
        } else {
                if(sa.sa.sa_family == AF_INET) {
                        if(sa.in.sin_addr.s_addr == 0) {
@@ -180,10 +171,13 @@ bool init_control(void) {
                localhost = sockaddr2hostname(&sa);
        }
 
-       fprintf(f, "%d %s %s\n", (int)getpid(), controlcookie, localhost);
-
+       bool wrote = write_pidfile(controlcookie, localhost);
        free(localhost);
-       fclose(f);
+
+       if(!wrote) {
+               logger(DEBUG_ALWAYS, LOG_ERR, "Cannot write control socket cookie file %s: %s", pidfilename, strerror(errno));
+               return false;
+       }
 
 #ifndef HAVE_WINDOWS
        int unix_fd = socket(AF_UNIX, SOCK_STREAM, 0);
@@ -211,6 +205,7 @@ bool init_control(void) {
 
        unlink(unixsocketname);
 
+       mode_t mask = umask(0);
        umask(mask | 077);
        int result = bind(unix_fd, (struct sockaddr *)&sa_un, sizeof(sa_un));
        umask(mask);
index c008be2..cff9e72 100644 (file)
@@ -35,6 +35,7 @@
 #include "utils.h"
 #include "xalloc.h"
 #include "random.h"
+#include "pidfile.h"
 
 #include "ed25519/sha512.h"
 
@@ -80,8 +81,10 @@ static void scan_for_hostname(const char *filename, char **hostname, char **port
                p[strcspn(p, "\t ")] = 0;
 
                if(!*port && !strcasecmp(line, "Port")) {
+                       free(*port);
                        *port = xstrdup(q);
                } else if(!*hostname && !strcasecmp(line, "Address")) {
+                       free(*hostname);
                        *hostname = xstrdup(q);
 
                        if(*p) {
@@ -98,7 +101,7 @@ static void scan_for_hostname(const char *filename, char **hostname, char **port
        fclose(f);
 }
 
-static char *get_my_hostname(void) {
+static bool get_my_hostname(char **out_address, char **out_port) {
        char *hostname = NULL;
        char *port = NULL;
        char *hostport = NULL;
@@ -115,6 +118,19 @@ static char *get_my_hostname(void) {
        free(name);
        name = NULL;
 
+       if(!port || (is_decimal(port) && atoi(port) == 0)) {
+               pidfile_t *pidfile = read_pidfile();
+
+               if(pidfile) {
+                       free(port);
+                       port = xstrdup(pidfile->port);
+                       free(pidfile);
+               } else {
+                       fprintf(stderr, "tincd is using a dynamic port and is not running. Please start tincd or set the Port option to a non-zero value.\n");
+                       goto exit;
+               }
+       }
+
        if(hostname) {
                goto done;
        }
@@ -186,7 +202,7 @@ static char *get_my_hostname(void) {
                if(!hostname) {
                        fprintf(stderr, "Could not determine the external address or hostname. Please set Address manually.\n");
                        free(port);
-                       return NULL;
+                       return false;
                }
 
                goto save;
@@ -205,7 +221,7 @@ again:
                fprintf(stderr, "Error while reading stdin: %s\n", strerror(errno));
                free(hostname);
                free(port);
-               return NULL;
+               return false;
        }
 
        if(!rstrip(line)) {
@@ -257,12 +273,25 @@ done:
                }
        }
 
+exit:
        free(hostname);
+
+       if(hostport && port) {
+               *out_address = hostport;
+               *out_port = port;
+               return true;
+       }
+
+       free(hostport);
        free(port);
-       return hostport;
+       return false;
 }
 
-static bool fcopy(FILE *out, const char *filename) {
+// Copy host configuration file, replacing Port with the value passed here. Host
+// configs may contain this clause: `Port = 0`, which means 'ask the operating
+// system to allocate any available port'. This obviously won't do for invitation
+// files, so replace it with an actual port we've obtained previously.
+static bool copy_config_replacing_port(FILE *out, const char *filename, const char *port) {
        FILE *in = fopen(filename, "r");
 
        if(!in) {
@@ -270,17 +299,33 @@ static bool fcopy(FILE *out, const char *filename) {
                return false;
        }
 
-       char buf[1024];
-       size_t len;
+       char line[1024];
+
+       while(fgets(line, sizeof(line), in)) {
+               const char *var_beg = line + strspn(line, "\t ");
+               const char *var_end = var_beg + strcspn(var_beg, "\t ");
 
-       while((len = fread(buf, 1, sizeof(buf), in))) {
-               fwrite(buf, len, 1, out);
+               // Check the name of the variable we've read. If it's Port, replace it with
+               // a port we'll use in invitation URL. Otherwise, just copy the line.
+               if(var_end > var_beg && !strncasecmp(var_beg, "Port", var_end - var_beg)) {
+                       fprintf(out, "Port = %s\n", port);
+               } else {
+                       fprintf(out, "%s", line);
+               }
        }
 
        fclose(in);
        return true;
 }
 
+static bool append_host_config(FILE *f, const char *nodename, const char *port) {
+       char path[PATH_MAX];
+       snprintf(path, sizeof(path), "%s" SLASH "hosts" SLASH "%s", confbase, nodename);
+       bool success = copy_config_replacing_port(f, path, port);
+       fclose(f);
+       return success;
+}
+
 int cmd_invite(int argc, char *argv[]) {
        if(argc < 2) {
                fprintf(stderr, "Not enough arguments!\n");
@@ -487,7 +532,12 @@ int cmd_invite(int argc, char *argv[]) {
        }
 
        // Get the local address
-       char *address = get_my_hostname();
+       char *address = NULL;
+       char *port = NULL;
+
+       if(!get_my_hostname(&address, &port)) {
+               return 1;
+       }
 
        // Fill in the details.
        fprintf(f, "Name = %s\n", argv[1]);
@@ -522,10 +572,14 @@ int cmd_invite(int argc, char *argv[]) {
        fprintf(f, "#---------------------------------------------------------------#\n");
        fprintf(f, "Name = %s\n", myname);
 
-       char filename2[PATH_MAX];
-       snprintf(filename2, sizeof(filename2), "%s" SLASH "hosts" SLASH "%s", confbase, myname);
-       fcopy(f, filename2);
-       fclose(f);
+       bool appended = append_host_config(f, myname, port);
+       free(port);
+
+       if(!appended) {
+               fprintf(stderr, "Could not append my config to invitation file: %s.\n", strerror(errno));
+               free(address);
+               return 1;
+       }
 
        // Create an URL from the local address, key hash and cookie
        char *url;
@@ -671,11 +725,15 @@ static bool finalize_join(void) {
        }
 
        if(!netname) {
-               netname = xstrdup(grep(data, "NetName"));
+               const char *net = grep(data, "NetName");
 
-               if(netname && !check_netname(netname, true)) {
-                       fprintf(stderr, "Unsafe NetName found in invitation!\n");
-                       return false;
+               if(net) {
+                       netname = xstrdup(net);
+
+                       if(!check_netname(netname, true)) {
+                               fprintf(stderr, "Unsafe NetName found in invitation!\n");
+                               return false;
+                       }
                }
        }
 
index d96dd71..c8e783b 100644 (file)
@@ -109,6 +109,7 @@ src_lib_common = [
   'logger.c',
   'names.c',
   'netutl.c',
+  'pidfile.c',
   'script.c',
   'splay_tree.c',
   'sptps.c',
index 1765b1c..8e256ce 100644 (file)
--- a/src/net.h
+++ b/src/net.h
@@ -124,6 +124,11 @@ typedef struct outgoing_t {
        timeout_t ev;
 } outgoing_t;
 
+typedef struct ports_t {
+       char *tcp;
+       char *udp;
+} ports_t;
+
 extern list_t outgoing_list;
 
 extern int maxoutbufsize;
@@ -151,7 +156,7 @@ extern bool udp_sndbuf_warnings;
 extern int max_connection_burst;
 extern int fwmark;
 extern bool do_prune;
-extern char *myport;
+extern ports_t myport;
 extern bool device_standby;
 extern bool autoconnect;
 extern bool disablebuggypeers;
index 60fc692..a829e82 100644 (file)
@@ -50,7 +50,7 @@
 #include "upnp.h"
 #endif
 
-char *myport;
+ports_t myport;
 static io_t device_io;
 devops_t devops;
 bool device_standby = false;
@@ -535,11 +535,70 @@ bool setup_myself_reloadable(void) {
        return true;
 }
 
+// Get the port that `from_fd` is listening on, and assign it to
+// `sa` if `sa` has a dynamically allocated (zero) port.
+static bool assign_static_port(sockaddr_t *sa, int from_fd) {
+       // We cannot get a port from a bad FD. Bail out.
+       if(from_fd <= 0) {
+               return false;
+       }
+
+       int port = get_bound_port(from_fd);
+
+       if(!port) {
+               return false;
+       }
+
+       // If the port is non-zero, don't reassign it as it's already static.
+       switch(sa->sa.sa_family) {
+       case AF_INET:
+               if(!sa->in.sin_port) {
+                       sa->in.sin_port = htons(port);
+               }
+
+               return true;
+
+       case AF_INET6:
+               if(!sa->in6.sin6_port) {
+                       sa->in6.sin6_port = htons(port);
+               }
+
+               return true;
+
+       default:
+               logger(DEBUG_ALWAYS, LOG_ERR, "Unknown address family 0x%x", sa->sa.sa_family);
+               return false;
+       }
+}
+
+typedef int (*bind_fn_t)(const sockaddr_t *);
+
+static int bind_reusing_port(const sockaddr_t *sa, int from_fd, bind_fn_t setup) {
+       sockaddr_t reuse_sa;
+       memcpy(&reuse_sa, sa, SALEN(sa->sa));
+
+       int fd = -1;
+
+       // Check if the address we've been passed here is using port 0.
+       // If it is, try to get an actual port from an already bound socket, and reuse it here.
+       if(assign_static_port(&reuse_sa, from_fd)) {
+               fd = setup(&reuse_sa);
+       }
+
+       // If we're binding to a hardcoded non-zero port, or no socket is listening yet,
+       // or binding failed, try the original address.
+       if(fd < 0) {
+               fd = setup(sa);
+       }
+
+       return fd;
+}
+
 /*
   Add listening sockets.
 */
 static bool add_listen_address(char *address, bool bindto) {
-       char *port = myport;
+       char *port = myport.tcp;
 
        if(address) {
                char *space = strchr(address, ' ');
@@ -597,30 +656,45 @@ static bool add_listen_address(char *address, bool bindto) {
                        return false;
                }
 
-               int tcp_fd = setup_listen_socket((sockaddr_t *) aip->ai_addr);
+               const sockaddr_t *sa = (sockaddr_t *) aip->ai_addr;
+               int from_fd = listen_socket[0].tcp.fd;
+
+               // If we're binding to a dynamically allocated (zero) port, try to get the actual
+               // port of the first TCP socket, and use it for this one. If that succeeds, our
+               // tincd instance will use the same port for all addresses it listens on.
+               int tcp_fd = bind_reusing_port(sa, from_fd, setup_listen_socket);
 
                if(tcp_fd < 0) {
                        continue;
                }
 
-               int udp_fd = setup_vpn_in_socket((sockaddr_t *) aip->ai_addr);
+               // If we just successfully bound the first socket, use it for the UDP procedure below.
+               // Otherwise, keep using the socket we've obtained from listen_socket[0].
+               if(!from_fd) {
+                       from_fd = tcp_fd;
+               }
+
+               int udp_fd = bind_reusing_port(sa, from_fd, setup_vpn_in_socket);
 
                if(udp_fd < 0) {
                        closesocket(tcp_fd);
                        continue;
                }
 
-               io_add(&listen_socket[listen_sockets].tcp, handle_new_meta_connection, &listen_socket[listen_sockets], tcp_fd, IO_READ);
-               io_add(&listen_socket[listen_sockets].udp, handle_incoming_vpn_data, &listen_socket[listen_sockets], udp_fd, IO_READ);
+               listen_socket_t *sock = &listen_socket[listen_sockets];
+               io_add(&sock->tcp, handle_new_meta_connection, sock, tcp_fd, IO_READ);
+               io_add(&sock->udp, handle_incoming_vpn_data, sock, udp_fd, IO_READ);
 
                if(debug_level >= DEBUG_CONNECTIONS) {
-                       char *hostname = sockaddr2hostname((sockaddr_t *) aip->ai_addr);
-                       logger(DEBUG_CONNECTIONS, LOG_NOTICE, "Listening on %s", hostname);
+                       int tcp_port = get_bound_port(tcp_fd);
+                       char *hostname = NULL;
+                       sockaddr2str(sa, &hostname, NULL);
+                       logger(DEBUG_CONNECTIONS, LOG_NOTICE, "Listening on %s port %d", hostname, tcp_port);
                        free(hostname);
                }
 
-               listen_socket[listen_sockets].bindto = bindto;
-               memcpy(&listen_socket[listen_sockets].sa, aip->ai_addr, aip->ai_addrlen);
+               sock->bindto = bindto;
+               memcpy(&sock->sa, aip->ai_addr, aip->ai_addrlen);
                listen_sockets++;
        }
 
@@ -656,7 +730,7 @@ void device_disable(void) {
   Configure node_t myself and set up the local sockets (listen only)
 */
 static bool setup_myself(void) {
-       char *name, *hostname, *type;
+       char *name, *type;
        char *address = NULL;
        bool port_specified = false;
 
@@ -672,12 +746,14 @@ static bool setup_myself(void) {
        myself->connection->name = xstrdup(name);
        read_host_config(&config_tree, name, true);
 
-       if(!get_config_string(lookup_config(&config_tree, "Port"), &myport)) {
-               myport = xstrdup("655");
+       if(!get_config_string(lookup_config(&config_tree, "Port"), &myport.tcp)) {
+               myport.tcp = xstrdup("655");
        } else {
                port_specified = true;
        }
 
+       myport.udp = xstrdup(myport.tcp);
+
        myself->connection->options = 0;
        myself->connection->protocol_major = PROT_MAJOR;
        myself->connection->protocol_minor = PROT_MINOR;
@@ -726,19 +802,18 @@ static bool setup_myself(void) {
 #endif
 
        /* Ensure myport is numeric */
+       if(!is_decimal(myport.tcp)) {
+               uint16_t port = service_to_port(myport.tcp);
 
-       if(!atoi(myport)) {
-               struct addrinfo *ai = str2addrinfo("localhost", myport, SOCK_DGRAM);
-               sockaddr_t sa;
-
-               if(!ai || !ai->ai_addr) {
+               if(!port) {
                        return false;
                }
 
-               free(myport);
-               memcpy(&sa, ai->ai_addr, ai->ai_addrlen);
-               freeaddrinfo(ai);
-               sockaddr2str(&sa, NULL, &myport);
+               free(myport.tcp);
+               myport.tcp = int_to_str(port);
+
+               free(myport.udp);
+               myport.udp = xstrdup(myport.tcp);
        }
 
        /* Read in all the subnets specified in the host configuration file */
@@ -997,15 +1072,16 @@ static bool setup_myself(void) {
                }
 
                for(int i = 0; i < listen_sockets; i++) {
+                       const int tcp_fd = i + 3;
                        salen = sizeof(sa);
 
-                       if(getsockname(i + 3, &sa.sa, &salen) < 0) {
-                               logger(DEBUG_ALWAYS, LOG_ERR, "Could not get address of listen fd %d: %s", i + 3, sockstrerror(sockerrno));
+                       if(getsockname(tcp_fd, &sa.sa, &salen) < 0) {
+                               logger(DEBUG_ALWAYS, LOG_ERR, "Could not get address of listen fd %d: %s", tcp_fd, sockstrerror(sockerrno));
                                return false;
                        }
 
 #ifdef FD_CLOEXEC
-                       fcntl(i + 3, F_SETFD, FD_CLOEXEC);
+                       fcntl(tcp_fd, F_SETFD, FD_CLOEXEC);
 #endif
 
                        int udp_fd = setup_vpn_in_socket(&sa);
@@ -1014,11 +1090,11 @@ static bool setup_myself(void) {
                                return false;
                        }
 
-                       io_add(&listen_socket[i].tcp, (io_cb_t)handle_new_meta_connection, &listen_socket[i], i + 3, IO_READ);
+                       io_add(&listen_socket[i].tcp, (io_cb_t)handle_new_meta_connection, &listen_socket[i], tcp_fd, IO_READ);
                        io_add(&listen_socket[i].udp, (io_cb_t)handle_incoming_vpn_data, &listen_socket[i], udp_fd, IO_READ);
 
                        if(debug_level >= DEBUG_CONNECTIONS) {
-                               hostname = sockaddr2hostname(&sa);
+                               char *hostname = sockaddr2hostname(&sa);
                                logger(DEBUG_CONNECTIONS, LOG_NOTICE, "Listening on %s", hostname);
                                free(hostname);
                        }
@@ -1060,21 +1136,19 @@ static bool setup_myself(void) {
 
        /* If no Port option was specified, set myport to the port used by the first listening socket. */
 
-       if(!port_specified || atoi(myport) == 0) {
-               sockaddr_t sa;
-               socklen_t salen = sizeof(sa);
+       if(!port_specified || atoi(myport.tcp) == 0) {
+               listen_socket_t *sock = &listen_socket[0];
 
-               if(!getsockname(listen_socket[0].udp.fd, &sa.sa, &salen)) {
-                       free(myport);
-                       sockaddr2str(&sa, NULL, &myport);
+               uint16_t tcp = get_bound_port(sock->tcp.fd);
+               free(myport.tcp);
+               myport.tcp = int_to_str(tcp);
 
-                       if(!myport) {
-                               myport = xstrdup("655");
-                       }
-               }
+               uint16_t udp = get_bound_port(sock->udp.fd);
+               free(myport.udp);
+               myport.udp = int_to_str(udp);
        }
 
-       xasprintf(&myself->hostname, "MYSELF port %s", myport);
+       xasprintf(&myself->hostname, "MYSELF port %s", myport.tcp);
        myself->connection->hostname = xstrdup(myself->hostname);
 
        char *upnp = NULL;
@@ -1194,7 +1268,8 @@ void close_network_connections(void) {
                device_disable();
        }
 
-       free(myport);
+       free(myport.tcp);
+       free(myport.udp);
 
        if(device_fd >= 0) {
                io_del(&device_io);
index dfee573..e154f48 100644 (file)
@@ -176,9 +176,20 @@ static bool bind_to_address(connection_t *c) {
        return true;
 }
 
+static bool try_bind(int nfd, const sockaddr_t *sa, const char *type) {
+       if(!bind(nfd, &sa->sa, SALEN(sa->sa))) {
+               return true;
+       }
+
+       closesocket(nfd);
+       char *addrstr = sockaddr2hostname(sa);
+       logger(DEBUG_ALWAYS, LOG_ERR, "Can't bind to %s/%s: %s", addrstr, type, sockstrerror(sockerrno));
+       free(addrstr);
+       return false;
+}
+
 int setup_listen_socket(const sockaddr_t *sa) {
        int nfd;
-       char *addrstr;
        int option;
        char *iface;
 
@@ -237,11 +248,7 @@ int setup_listen_socket(const sockaddr_t *sa) {
 #endif
        }
 
-       if(bind(nfd, &sa->sa, SALEN(sa->sa))) {
-               closesocket(nfd);
-               addrstr = sockaddr2hostname(sa);
-               logger(DEBUG_ALWAYS, LOG_ERR, "Can't bind to %s/tcp: %s", addrstr, sockstrerror(sockerrno));
-               free(addrstr);
+       if(!try_bind(nfd, sa, "tcp")) {
                return -1;
        }
 
@@ -282,10 +289,8 @@ static void set_udp_buffer(int nfd, int type, const char *name, int size, bool w
        }
 }
 
-
 int setup_vpn_in_socket(const sockaddr_t *sa) {
        int nfd;
-       char *addrstr;
        int option;
 
        nfd = socket(sa->sa.sa_family, SOCK_DGRAM, IPPROTO_UDP);
@@ -386,11 +391,7 @@ int setup_vpn_in_socket(const sockaddr_t *sa) {
                return -1;
        }
 
-       if(bind(nfd, &sa->sa, SALEN(sa->sa))) {
-               closesocket(nfd);
-               addrstr = sockaddr2hostname(sa);
-               logger(DEBUG_ALWAYS, LOG_ERR, "Can't bind to %s/udp: %s", addrstr, sockstrerror(sockerrno));
-               free(addrstr);
+       if(!try_bind(nfd, sa, "udp")) {
                return -1;
        }
 
index e2ee771..0c19f5b 100644 (file)
 
 bool hostnames = false;
 
+uint16_t service_to_port(const char *service) {
+       struct addrinfo *ai = str2addrinfo("localhost", service, SOCK_STREAM);
+
+       if(!ai || !ai->ai_addr) {
+               return 0;
+       }
+
+       sockaddr_t sa;
+       memcpy(&sa, ai->ai_addr, ai->ai_addrlen);
+       freeaddrinfo(ai);
+
+       switch(sa.sa.sa_family) {
+       case AF_INET:
+               return ntohs(sa.in.sin_port);
+
+       case AF_INET6:
+               return ntohs(sa.in6.sin6_port);
+
+       default:
+               logger(DEBUG_ALWAYS, LOG_WARNING, "Unknown address family %d for service %s.", sa.sa.sa_family, service);
+               return 0;
+       }
+}
+
 /*
   Turn a string into a struct addrinfo.
   Return NULL on failure.
@@ -276,3 +300,20 @@ void sockaddr_setport(sockaddr_t *sa, const char *port) {
                return;
        }
 }
+
+uint16_t get_bound_port(int sockfd) {
+       sockaddr_t sa;
+       socklen_t salen = sizeof(sa);
+
+       if(getsockname(sockfd, (struct sockaddr *) &sa, &salen)) {
+               return 0;
+       }
+
+       if(sa.sa.sa_family == AF_INET) {
+               return ntohs(sa.in.sin_port);
+       } else  if(sa.sa.sa_family == AF_INET6) {
+               return ntohs(sa.in6.sin6_port);
+       } else {
+               return 0;
+       }
+}
index a86a859..a1ab660 100644 (file)
@@ -25,6 +25,8 @@
 
 extern bool hostnames;
 
+// Converts service name (as listed in /etc/services) to port number. Returns 0 on error.
+extern uint16_t service_to_port(const char *service);
 extern struct addrinfo *str2addrinfo(const char *address, const char *service, int socktype) ATTR_MALLOC;
 extern sockaddr_t str2sockaddr(const char *address, const char *port);
 extern void sockaddr2str(const sockaddr_t *sa, char **address, char **port);
@@ -35,5 +37,6 @@ extern void sockaddrunmap(sockaddr_t *sa);
 extern void sockaddrfree(sockaddr_t *sa);
 extern void sockaddrcpy(sockaddr_t *dest, const sockaddr_t *src);
 extern void sockaddr_setport(sockaddr_t *sa, const char *port);
+extern uint16_t get_bound_port(int sockfd);
 
 #endif
diff --git a/src/pidfile.c b/src/pidfile.c
new file mode 100644 (file)
index 0000000..3919ad4
--- /dev/null
@@ -0,0 +1,38 @@
+#include "system.h"
+
+#include "pidfile.h"
+#include "names.h"
+
+pidfile_t *read_pidfile(void) {
+       FILE *f = fopen(pidfilename, "r");
+
+       if(!f) {
+               return NULL;
+       }
+
+       pidfile_t *pf = malloc(sizeof(pidfile_t));
+       int read = fscanf(f, "%20d %64s %128s port %128s", &pf->pid, pf->cookie, pf->host, pf->port);
+       fclose(f);
+
+       if(read != 4) {
+               free(pf);
+               pf = NULL;
+       }
+
+       return pf;
+}
+
+
+bool write_pidfile(const char *controlcookie, const char *address) {
+       const mode_t mask = umask(0);
+       umask(mask | 077);
+       FILE *f = fopen(pidfilename, "w");
+
+       if(!f) {
+               return false;
+       }
+
+       umask(mask);
+       fprintf(f, "%d %s %s\n", (int)getpid(), controlcookie, address);
+       return !fclose(f);
+}
diff --git a/src/pidfile.h b/src/pidfile.h
new file mode 100644 (file)
index 0000000..8331f2e
--- /dev/null
@@ -0,0 +1,16 @@
+#ifndef TINC_PIDFILE_H
+#define TINC_PIDFILE_H
+
+#include "system.h"
+
+typedef struct pidfile_t {
+       int pid;
+       char host[129];
+       char port[129];
+       char cookie[65];
+} pidfile_t;
+
+extern pidfile_t *read_pidfile(void) ATTR_MALLOC;
+extern bool write_pidfile(const char *controlcookie, const char *address);
+
+#endif // TINC_PIDFILE_H
index 0e6ac5d..16abc05 100644 (file)
@@ -894,7 +894,7 @@ bool send_ack(connection_t *c) {
                get_config_int(lookup_config(&config_tree, "Weight"), &c->estimated_weight);
        }
 
-       return send_request(c, "%d %s %d %x", ACK, myport, c->estimated_weight, (c->options & 0xffffff) | (experimental ? (PROT_MINOR << 24) : 0));
+       return send_request(c, "%d %s %d %x", ACK, myport.udp, c->estimated_weight, (c->options & 0xffffff) | (experimental ? (PROT_MINOR << 24) : 0));
 }
 
 static void send_everything(connection_t *c) {
@@ -1067,7 +1067,7 @@ bool ack_h(connection_t *c, const char *request) {
        if(getsockname(c->socket, &local_sa.sa, &local_salen) < 0) {
                logger(DEBUG_ALWAYS, LOG_WARNING, "Could not get local socket address for connection with %s", c->name);
        } else {
-               sockaddr_setport(&local_sa, myport);
+               sockaddr_setport(&local_sa, myport.udp);
                c->edge->local_address = local_sa;
        }
 
index 9be7d87..4a3682f 100644 (file)
@@ -41,6 +41,7 @@
 #include "subnet.h"
 #include "keys.h"
 #include "random.h"
+#include "pidfile.h"
 
 #ifndef MSG_NOSIGNAL
 #define MSG_NOSIGNAL 0
@@ -725,9 +726,9 @@ bool connect_tincd(bool verbose) {
                }
        }
 
-       FILE *f = fopen(pidfilename, "r");
+       pidfile_t *pidfile = read_pidfile();
 
-       if(!f) {
+       if(!pidfile) {
                if(verbose) {
                        fprintf(stderr, "Could not open pid file %s: %s\n", pidfilename, strerror(errno));
                }
@@ -735,21 +736,11 @@ bool connect_tincd(bool verbose) {
                return false;
        }
 
-       char host[129];
-       char port[129];
-
-       if(fscanf(f, "%20d %1024s %128s port %128s", &pid, controlcookie, host, port) != 4) {
-               if(verbose) {
-                       fprintf(stderr, "Could not parse pid file %s\n", pidfilename);
-               }
-
-               fclose(f);
-               return false;
-       }
-
-       fclose(f);
+       pid = pidfile->pid;
+       strcpy(controlcookie, pidfile->cookie);
 
 #ifndef HAVE_WINDOWS
+       free(pidfile);
 
        if((pid == 0) || (kill(pid, 0) && (errno == ESRCH))) {
                fprintf(stderr, "Could not find tincd running at pid %d\n", pid);
@@ -800,11 +791,12 @@ bool connect_tincd(bool verbose) {
 
        struct addrinfo *res = NULL;
 
-       if(getaddrinfo(host, port, &hints, &res) || !res) {
+       if(getaddrinfo(pidfile->host, pidfile->port, &hints, &res) || !res) {
                if(verbose) {
-                       fprintf(stderr, "Cannot resolve %s port %s: %s\n", host, port, sockstrerror(sockerrno));
+                       fprintf(stderr, "Cannot resolve %s port %s: %s\n", pidfile->host, pidfile->port, sockstrerror(sockerrno));
                }
 
+               free(pidfile);
                return false;
        }
 
@@ -815,6 +807,7 @@ bool connect_tincd(bool verbose) {
                        fprintf(stderr, "Cannot create TCP socket: %s\n", sockstrerror(sockerrno));
                }
 
+               free(pidfile);
                return false;
        }
 
@@ -828,14 +821,16 @@ bool connect_tincd(bool verbose) {
 
        if(connect(fd, res->ai_addr, res->ai_addrlen) < 0) {
                if(verbose) {
-                       fprintf(stderr, "Cannot connect to %s port %s: %s\n", host, port, sockstrerror(sockerrno));
+                       fprintf(stderr, "Cannot connect to %s port %s: %s\n", pidfile->host, pidfile->port, sockstrerror(sockerrno));
                }
 
+               free(pidfile);
                closesocket(fd);
                fd = -1;
                return false;
        }
 
+       free(pidfile);
        freeaddrinfo(res);
 #endif
 
@@ -1720,6 +1715,20 @@ const var_t variables[] = {
        {NULL, 0}
 };
 
+// Request actual port from tincd
+static bool read_actual_port(void) {
+       pidfile_t *pidfile = read_pidfile();
+
+       if(pidfile) {
+               printf("%s\n", pidfile->port);
+               free(pidfile);
+               return true;
+       } else {
+               fprintf(stderr, "Could not get port from the pidfile.\n");
+               return false;
+       }
+}
+
 static int cmd_config(int argc, char *argv[]) {
        if(argc < 2) {
                fprintf(stderr, "Invalid number of arguments.\n");
@@ -1795,6 +1804,11 @@ static int cmd_config(int argc, char *argv[]) {
                action = SET;
        }
 
+       // If port is requested, try reading it from the pidfile and fall back to configs if that fails
+       if(action == GET && !strcasecmp(variable, "Port") && read_actual_port()) {
+               return 0;
+       }
+
        /* Some simple checks. */
        bool found = false;
        bool warnonremove = false;
index 4150d92..c2f4333 100644 (file)
@@ -119,6 +119,24 @@ size_t b64decode_tinc(const char *src, void *dst, size_t length) {
        }
 }
 
+bool is_decimal(const char *str) {
+       if(!str) {
+               return false;
+       }
+
+       errno = 0;
+       char *badchar = NULL;
+       strtol(str, &badchar, 10);
+       return !errno && badchar != str && !*badchar;
+}
+
+// itoa() conflicts with a similarly named function under MinGW.
+char *int_to_str(int num) {
+       char *str = NULL;
+       xasprintf(&str, "%d", num);
+       return str;
+}
+
 static size_t b64encode_tinc_internal(const void *src, char *dst, size_t length, const char *alphabet) {
        uint32_t triplet;
        const unsigned char *usrc = (unsigned char *)src;
index 5f1bd8b..be629ee 100644 (file)
 extern size_t hex2bin(const char *src, void *dst, size_t length);
 extern size_t bin2hex(const void *src, char *dst, size_t length);
 
+// Returns true if string represents a base-10 integer.
+extern bool is_decimal(const char *str);
+
+// The reverse of atoi().
+extern char *int_to_str(int num);
+
 extern size_t b64encode_tinc(const void *src, char *dst, size_t length);
 extern size_t b64encode_tinc_urlsafe(const void *src, char *dst, size_t length);
 extern size_t b64decode_tinc(const char *src, void *dst, size_t length);
index 25389f8..2d06252 100755 (executable)
@@ -8,15 +8,28 @@ from testlib.log import log
 from testlib.test import Test
 
 
+def run_port0_test(ctx: Test) -> None:
+    """Checks that tinc invite fails if called with Port 0 and tincd stopped."""
+    foo = ctx.node()
+    stdin = f"""
+        init {foo}
+        set Port 0
+        set Address localhost
+        set DeviceType dummy
+    """
+    foo.cmd(stdin=stdin)
+    _, err = foo.cmd("invite", "bar", code=1)
+    check.is_in("Please start tincd", err)
+
+
 def run_invite_test(ctx: Test, start_before_invite: bool) -> None:
     """Run tests. If start_before_invite is True,
     tincd is started *before* creating invitation, and vice versa.
     """
     foo, bar = ctx.node(), ctx.node()
-
     stdin = f"""
         init {foo}
-        set Port 0
+        set Port 12345
         set Address localhost
         set DeviceType dummy
         set Mode switch
@@ -25,6 +38,7 @@ def run_invite_test(ctx: Test, start_before_invite: bool) -> None:
     foo.cmd(stdin=stdin)
 
     if start_before_invite:
+        foo.cmd("set", "Port", "0")
         port = foo.start()
 
     log.info("create invitation")
@@ -33,8 +47,9 @@ def run_invite_test(ctx: Test, start_before_invite: bool) -> None:
     foo_invite = foo_invite.strip()
 
     if not start_before_invite:
+        foo.cmd("set", "Port", "0")
         port = foo.start()
-        foo_invite = foo_invite.replace(":0/", f":{port}/")
+        foo_invite = foo_invite.replace(":12345/", f":{port}/")
 
     log.info("join second node with %s", foo_invite)
     bar.cmd("join", foo_invite)
@@ -85,6 +100,9 @@ def run_invite_test(ctx: Test, start_before_invite: bool) -> None:
     check.nodes(bar, 2)
 
 
+with Test("fail with Port 0 and tincd not running") as context:
+    run_port0_test(context)
+
 with Test("offline mode") as context:
     run_invite_test(context, start_before_invite=False)
 
index 8ac65ba..1faea55 100755 (executable)
@@ -229,9 +229,6 @@ class Tinc:
         if code is not None:
             check.equals(code, res)
 
-        # Check that port was not used by something else
-        check.not_in("Can't bind to ", err)
-
         return out if out else "", err if err else ""
 
     def tinc(self, *args: str) -> subp.Popen:
index d83a080..70a02d2 100644 (file)
@@ -28,6 +28,9 @@ tests = {
     'code': 'test_random_noinit.c',
     'fail': true,
   },
+  'netutl': {
+    'code': 'test_netutl.c',
+  },
   'net': {
     'code': 'test_net.c',
     'mock': ['execute_script', 'environment_init', 'environment_exit'],
@@ -38,6 +41,9 @@ tests = {
   'protocol': {
     'code': 'test_protocol.c',
   },
+  'utils': {
+    'code': 'test_utils.c',
+  },
   'splay_tree': {
     'code': 'test_splay_tree.c',
     'link': link_tinc,
diff --git a/test/unit/test_netutl.c b/test/unit/test_netutl.c
new file mode 100644 (file)
index 0000000..a3486fc
--- /dev/null
@@ -0,0 +1,28 @@
+#include "unittest.h"
+#include "../../src/netutl.h"
+
+static void test_service_to_port_invalid(void **state) {
+       (void)state;
+
+       assert_int_equal(0, service_to_port(NULL));
+       assert_int_equal(0, service_to_port(""));
+       assert_int_equal(0, service_to_port("-1"));
+       assert_int_equal(0, service_to_port("foobar"));
+}
+
+static void test_service_to_port_valid(void **state) {
+       (void)state;
+
+       assert_int_equal(22, service_to_port("ssh"));
+       assert_int_equal(80, service_to_port("http"));
+       assert_int_equal(443, service_to_port("https"));
+       assert_int_equal(1234, service_to_port("1234"));
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_service_to_port_invalid),
+               cmocka_unit_test(test_service_to_port_valid),
+       };
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}
diff --git a/test/unit/test_utils.c b/test/unit/test_utils.c
new file mode 100644 (file)
index 0000000..587a0e1
--- /dev/null
@@ -0,0 +1,70 @@
+#include "unittest.h"
+#include "../../src/utils.h"
+
+static void test_int_to_str(const char *ref, int num) {
+       char *result = int_to_str(num);
+       assert_string_equal(ref, result);
+       free(result);
+}
+
+static void test_int_to_str_return_expected(void **state) {
+       (void)state;
+
+       test_int_to_str("0", 0);
+       test_int_to_str("-1337", -1337);
+       test_int_to_str("65535", 65535);
+}
+
+static void test_is_decimal_fail_empty(void **state) {
+       (void)state;
+
+       assert_false(is_decimal(NULL));
+       assert_false(is_decimal(""));
+}
+
+static void test_is_decimal_fail_hex(void **state) {
+       (void)state;
+
+       assert_false(is_decimal("DEADBEEF"));
+       assert_false(is_decimal("0xCAFE"));
+}
+
+static void test_is_decimal_fail_junk_suffix(void **state) {
+       (void)state;
+
+       assert_false(is_decimal("123foobar"));
+       assert_false(is_decimal("777 "));
+}
+
+static void test_is_decimal_pass_simple(void **state) {
+       (void)state;
+
+       assert_true(is_decimal("0"));
+       assert_true(is_decimal("123"));
+}
+
+static void test_is_decimal_pass_signs(void **state) {
+       (void)state;
+
+       assert_true(is_decimal("-123"));
+       assert_true(is_decimal("+123"));
+}
+
+static void test_is_decimal_pass_whitespace_prefix(void **state) {
+       (void)state;
+
+       assert_true(is_decimal(" \r\n\t 777"));
+}
+
+int main(void) {
+       const struct CMUnitTest tests[] = {
+               cmocka_unit_test(test_int_to_str_return_expected),
+               cmocka_unit_test(test_is_decimal_fail_empty),
+               cmocka_unit_test(test_is_decimal_fail_hex),
+               cmocka_unit_test(test_is_decimal_fail_junk_suffix),
+               cmocka_unit_test(test_is_decimal_pass_simple),
+               cmocka_unit_test(test_is_decimal_pass_signs),
+               cmocka_unit_test(test_is_decimal_pass_whitespace_prefix),
+       };
+       return cmocka_run_group_tests(tests, NULL, NULL);
+}