diff --git a/unix/procnet.c b/unix/procnet.c new file mode 100644 index 00000000..43b1f055 --- /dev/null +++ b/unix/procnet.c @@ -0,0 +1,229 @@ +/* + * Locally authenticate a TCP socket via /proc/net. + * + * Obviously, if a TCP connection comes from a different host, there's + * no way to find out the identity of the thing at the other end (or + * even really to assign that concept a meaning) except by the usual + * method of speaking a protocol over the socket itself which involves + * some form of (preferably cryptographic) authentication exchange. + * + * But if the connection comes from localhost, then on at least some + * operating systems, you can do better. On Linux, /proc/net/tcp and + * /proc/net/tcp6 list the full set of active TCP connection + * endpoints, and they list an owning uid for each one. So once you've + * accepted a connection to a listening socket and found that the + * other end of it is a localhost address, you can look up the _other_ + * endpoint in the right one of those files, and find out which uid + * owns it. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "misc.h" + +static ptrlen get_space_separated_field(ptrlen *string) +{ + const char *p = string->ptr, *end = p + string->len; + + while (p < end && isspace((unsigned char)*p)) + p++; + if (p == end) + return PTRLEN_LITERAL(""); + + const char *start = p; + while (p < end && !isspace((unsigned char)*p)) + p++; + *string = make_ptrlen(p, end - p); + return make_ptrlen(start, p - start); +} + +enum { GOT_LOCAL_UID = 1, GOT_REMOTE_UID = 2 }; + +/* + * Open a file formatted like /proc/net/tcp{,6}, and search it for + * both ends of a particular connection. + * + * The operands 'local' and 'remote' give the expected string + * representations of the local and remote addresses of the connection + * we're looking for. + * + * Return value is the bitwise OR of 1 if we found the local end of + * the connection and 2 if we found the remote. Each output uid_t + * parameter is filled in iff the corresponding bit is set in the + * return value. + */ +static int lookup_uids_in_procnet_file( + const char *path, ptrlen local, ptrlen remote, + uid_t *local_uid, uid_t *remote_uid) +{ + FILE *fp = NULL; + int toret = 0; + ptrlen line, field; + + enum { GF_LOCAL = 1, GF_REMOTE = 2, GF_UID = 4 }; + + fp = fopen(path, "r"); + if (!fp) + goto out; + + /* Expected indices of fields in /proc/net/tcp* */ + const int LOCAL_ADDR_INDEX = 1; + const int REMOTE_ADDR_INDEX = 2; + const int UID_INDEX = 7; + + for (char *linez; (linez = chomp(fgetline(fp))) != NULL ;) { + line = ptrlen_from_asciz(linez); + int gotfields = 0; + ptrlen local_addr = PTRLEN_LITERAL(""); + ptrlen remote_addr = PTRLEN_LITERAL(""); + long uid = -1; + + for (int i = 0; (field = get_space_separated_field(&line)).len != 0; + i++) { + + if (i == LOCAL_ADDR_INDEX) { + gotfields |= GF_LOCAL; + local_addr = field; + } else if (i == REMOTE_ADDR_INDEX) { + gotfields |= GF_REMOTE; + remote_addr = field; + } else if (i == UID_INDEX) { + uid = 0; + for (const char *p = field.ptr, *end = p + field.len; + p < end; p++) { + if (!isdigit((unsigned char)*p)) { + uid = -1; + break; + } + int dval = *p - '0'; + if (uid > LONG_MAX/10) { + uid = -1; + break; + } + uid *= 10; + if (uid > LONG_MAX - dval) { + uid = -1; + break; + } + uid += dval; + } + + gotfields |= GF_UID; + } + } + + if (gotfields == (GF_LOCAL | GF_REMOTE | GF_UID)) { + if (ptrlen_eq_ptrlen(local_addr, local) && + ptrlen_eq_ptrlen(remote_addr, remote)) { + *local_uid = uid; + toret |= GOT_LOCAL_UID; + } + if (ptrlen_eq_ptrlen(local_addr, remote) && + ptrlen_eq_ptrlen(remote_addr, local)) { + *remote_uid = uid; + toret |= GOT_REMOTE_UID; + } + } + + sfree(linez); + } + + fclose(fp); + fp = NULL; + + out: + if (fp) + fclose(fp); + return toret; +} + +static const char *procnet_path(int family) +{ + switch (family) { + case AF_INET: return "/proc/net/tcp"; + case AF_INET6: return "/proc/net/tcp6"; + default: return NULL; + } +} + +static char *format_sockaddr(const void *addr, int family) +{ + if (family == AF_INET) { + const struct sockaddr_in *a = (const struct sockaddr_in *)addr; + assert(a->sin_family == family); + /* Linux /proc/net formats the IP address native-endian, so we + * don't use ntohl */ + return dupprintf("%08X:%04X", a->sin_addr.s_addr, ntohs(a->sin_port)); + } else if (family == AF_INET6) { + struct sockaddr_in6 *a = (struct sockaddr_in6 *)addr; + assert(a->sin6_family == family); + + strbuf *sb = strbuf_new(); + + const uint32_t *addrwords = (const uint32_t *)a->sin6_addr.s6_addr; + for (int i = 0; i < 4; i++) + strbuf_catf(sb, "%08X", addrwords[i]); + strbuf_catf(sb, ":%04X", ntohs(a->sin6_port)); + + return strbuf_to_str(sb); + } else { + return NULL; + } +} + +bool socket_peer_is_same_user(int fd) +{ + struct sockaddr_storage addr; + socklen_t addrlen; + int family; + bool toret = false; + char *local = NULL, *remote = NULL; + const char *path; + + addrlen = sizeof(addr); + if (getsockname(fd, (struct sockaddr *)&addr, &addrlen) != 0) + goto out; + family = addr.ss_family; + if ((path = procnet_path(family)) == NULL) + goto out; + local = format_sockaddr(&addr, family); + if (!local) + goto out; + + addrlen = sizeof(addr); + if (getpeername(fd, (struct sockaddr *)&addr, &addrlen) != 0) + goto out; + if (addr.ss_family != family) + goto out; + remote = format_sockaddr(&addr, family); + if (!remote) + goto out; + + ptrlen locpl = ptrlen_from_asciz(local); + ptrlen rempl = ptrlen_from_asciz(remote); + + /* + * Check that _both_ end of the socket are the uid we expect, as a + * sanity check on the /proc/net file being reasonable at all. + */ + uid_t our_uid = getuid(); + uid_t local_uid = -1, remote_uid = -1; + int got = lookup_uids_in_procnet_file( + path, locpl, rempl, &local_uid, &remote_uid); + if (got == (GOT_LOCAL_UID | GOT_REMOTE_UID) && + local_uid == our_uid && remote_uid == our_uid) + toret = true; + + out: + sfree(local); + sfree(remote); + return toret; +} diff --git a/unix/unix.h b/unix/unix.h index 284d1430..903805dc 100644 --- a/unix/unix.h +++ b/unix/unix.h @@ -369,6 +369,7 @@ bool init_ucs(struct unicode_data *ucsdata, char *line_codepage, * Spare functions exported directly from uxnet.c. */ void *sk_getxdmdata(Socket *sock, int *lenp); +int sk_net_get_fd(Socket *sock); SockAddr *unix_sock_addr(const char *path); Socket *new_unix_listener(SockAddr *listenaddr, Plug *plug); @@ -421,6 +422,16 @@ extern char **pty_argv; char *gtk_askpass_main(const char *display, const char *wintitle, const char *prompt, bool *success); +/* + * procnet.c. + */ +bool socket_peer_is_same_user(int fd); +static inline bool sk_peer_trusted(Socket *sock) +{ + int fd = sk_net_get_fd(sock); + return fd >= 0 && socket_peer_is_same_user(fd); +} + /* * uxsftpserver.c. */ diff --git a/unix/uxnet.c b/unix/uxnet.c index 16680fb9..ac65980b 100644 --- a/unix/uxnet.c +++ b/unix/uxnet.c @@ -1554,6 +1554,15 @@ static SocketPeerInfo *sk_net_peer_info(Socket *sock) return pi; } +int sk_net_get_fd(Socket *sock) +{ + /* This function is not fully general: it only works on NetSocket */ + if (sock->vt != &NetSocket_sockvt) + return -1; /* failure */ + NetSocket *s = container_of(sock, NetSocket, sock); + return s->s; +} + static void uxsel_tell(NetSocket *s) { int rwx = 0;