From 0289162552cd85375605044c696e2a3294e7aa9a Mon Sep 17 00:00:00 2001 From: Kirill Isakov Date: Thu, 21 Apr 2022 18:22:32 +0600 Subject: [PATCH] Use actual port in tincd logs / tinc get Port / invitations 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 --- src/control.c | 23 ++--- src/invitation.c | 94 +++++++++++++++---- src/meson.build | 1 + src/net.h | 7 +- src/net_setup.c | 153 +++++++++++++++++++++++-------- src/net_socket.c | 27 +++--- src/netutl.c | 41 +++++++++ src/netutl.h | 3 + src/pidfile.c | 38 ++++++++ src/pidfile.h | 16 ++++ src/protocol_auth.c | 4 +- src/tincctl.c | 50 ++++++---- src/utils.c | 18 ++++ src/utils.h | 6 ++ test/integration/invite.py | 24 ++++- test/integration/testlib/proc.py | 3 - test/unit/meson.build | 6 ++ test/unit/test_netutl.c | 28 ++++++ test/unit/test_utils.c | 70 ++++++++++++++ 19 files changed, 501 insertions(+), 111 deletions(-) create mode 100644 src/pidfile.c create mode 100644 src/pidfile.h create mode 100644 test/unit/test_netutl.c create mode 100644 test/unit/test_utils.c diff --git a/src/control.c b/src/control.c index 4c0ab511..c0895393 100644 --- a/src/control.c +++ b/src/control.c @@ -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); diff --git a/src/invitation.c b/src/invitation.c index c008be2a..cff9e727 100644 --- a/src/invitation.c +++ b/src/invitation.c @@ -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; + } } } diff --git a/src/meson.build b/src/meson.build index d96dd71b..c8e783b7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -109,6 +109,7 @@ src_lib_common = [ 'logger.c', 'names.c', 'netutl.c', + 'pidfile.c', 'script.c', 'splay_tree.c', 'sptps.c', diff --git a/src/net.h b/src/net.h index 1765b1cd..8e256ce7 100644 --- 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; diff --git a/src/net_setup.c b/src/net_setup.c index 60fc692d..a829e822 100644 --- a/src/net_setup.c +++ b/src/net_setup.c @@ -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); diff --git a/src/net_socket.c b/src/net_socket.c index dfee573c..e154f486 100644 --- a/src/net_socket.c +++ b/src/net_socket.c @@ -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; } diff --git a/src/netutl.c b/src/netutl.c index e2ee7719..0c19f5bb 100644 --- a/src/netutl.c +++ b/src/netutl.c @@ -27,6 +27,30 @@ 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; + } +} diff --git a/src/netutl.h b/src/netutl.h index a86a859d..a1ab660a 100644 --- a/src/netutl.h +++ b/src/netutl.h @@ -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 index 00000000..3919ad43 --- /dev/null +++ b/src/pidfile.c @@ -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 index 00000000..8331f2e7 --- /dev/null +++ b/src/pidfile.h @@ -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 diff --git a/src/protocol_auth.c b/src/protocol_auth.c index 0e6ac5de..16abc055 100644 --- a/src/protocol_auth.c +++ b/src/protocol_auth.c @@ -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; } diff --git a/src/tincctl.c b/src/tincctl.c index 9be7d873..4a3682f3 100644 --- a/src/tincctl.c +++ b/src/tincctl.c @@ -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; diff --git a/src/utils.c b/src/utils.c index 4150d92b..c2f4333f 100644 --- a/src/utils.c +++ b/src/utils.c @@ -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; diff --git a/src/utils.h b/src/utils.h index 5f1bd8b8..be629ee0 100644 --- a/src/utils.h +++ b/src/utils.h @@ -30,6 +30,12 @@ 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); diff --git a/test/integration/invite.py b/test/integration/invite.py index 25389f81..2d062524 100755 --- a/test/integration/invite.py +++ b/test/integration/invite.py @@ -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) diff --git a/test/integration/testlib/proc.py b/test/integration/testlib/proc.py index 8ac65baf..1faea550 100755 --- a/test/integration/testlib/proc.py +++ b/test/integration/testlib/proc.py @@ -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: diff --git a/test/unit/meson.build b/test/unit/meson.build index d83a080a..70a02d2e 100644 --- a/test/unit/meson.build +++ b/test/unit/meson.build @@ -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 index 00000000..a3486fcd --- /dev/null +++ b/test/unit/test_netutl.c @@ -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 index 00000000..587a0e1f --- /dev/null +++ b/test/unit/test_utils.c @@ -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); +} -- 2.20.1