Home | History | Annotate | Line # | Download | only in dist
      1 /*	$NetBSD: misc-agent.c,v 1.3 2026/04/08 18:58:40 christos Exp $	*/
      2 /* $OpenBSD: misc-agent.c,v 1.7 2026/02/11 17:05:32 dtucker Exp $ */
      3 
      4 /*
      5  * Copyright (c) 2025 Damien Miller <djm (at) mindrot.org>
      6  *
      7  * Permission to use, copy, modify, and distribute this software for any
      8  * purpose with or without fee is hereby granted, provided that the above
      9  * copyright notice and this permission notice appear in all copies.
     10  *
     11  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     12  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     13  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     14  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     15  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     16  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     17  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     18  */
     19 
     20 #include "includes.h"
     21 __RCSID("$NetBSD: misc-agent.c,v 1.3 2026/04/08 18:58:40 christos Exp $");
     22 
     23 #include <sys/types.h>
     24 #include <sys/socket.h>
     25 #include <sys/stat.h>
     26 #include <sys/un.h>
     27 
     28 #include <dirent.h>
     29 #include <errno.h>
     30 #include <fcntl.h>
     31 #include <netdb.h>
     32 #include <stdlib.h>
     33 #include <string.h>
     34 #include <time.h>
     35 #include <unistd.h>
     36 
     37 #include "digest.h"
     38 #include "log.h"
     39 #include "misc.h"
     40 #include "pathnames.h"
     41 #include "ssh.h"
     42 #include "xmalloc.h"
     43 
     44 /* stuff shared by agent listeners (ssh-agent and sshd agent forwarding) */
     45 
     46 #define SOCKET_HOSTNAME_HASHLEN 10 /* length of hostname hash in socket path */
     47 
     48 /* used for presenting random strings in unix_listener_tmp and hostname_hash */
     49 static const char presentation_chars[] =
     50     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
     51 
     52 /* returns a text-encoded hash of the hostname of specified length (max 64) */
     53 static char *
     54 hostname_hash(size_t len)
     55 {
     56 	char hostname[NI_MAXHOST], p[65];
     57 	u_char hash[64];
     58 	int r;
     59 	size_t l, i;
     60 
     61 	l = ssh_digest_bytes(SSH_DIGEST_SHA512);
     62 	if (len > 64) {
     63 		error_f("bad length %zu >= max %zd", len, l);
     64 		return NULL;
     65 	}
     66 	if (gethostname(hostname, sizeof(hostname)) == -1) {
     67 		error_f("gethostname: %s", strerror(errno));
     68 		return NULL;
     69 	}
     70 	if ((r = ssh_digest_memory(SSH_DIGEST_SHA512,
     71 	    hostname, strlen(hostname), hash, sizeof(hash))) != 0) {
     72 		error_fr(r, "ssh_digest_memory");
     73 		return NULL;
     74 	}
     75 	memset(p, '\0', sizeof(p));
     76 	for (i = 0; i < l; i++)
     77 		p[i] = presentation_chars[
     78 		    hash[i] % (sizeof(presentation_chars) - 1)];
     79 	/* debug3_f("hostname \"%s\" => hash \"%s\"", hostname, p); */
     80 	p[len] = '\0';
     81 	return xstrdup(p);
     82 }
     83 
     84 char *
     85 agent_hostname_hash(void)
     86 {
     87 	return hostname_hash(SOCKET_HOSTNAME_HASHLEN);
     88 }
     89 
     90 /*
     91  * Creates a unix listener at a mkstemp(3)-style path, e.g. "/dir/sock.XXXXXX"
     92  * Supplied path is modified to the actual one used.
     93  */
     94 static int
     95 unix_listener_tmp(char *path, int backlog)
     96 {
     97 	struct sockaddr_un sunaddr;
     98 	int good, sock = -1;
     99 	size_t i, xstart;
    100 	mode_t prev_mask;
    101 
    102 	/* Find first 'X' template character back from end of string */
    103 	xstart = strlen(path);
    104 	while (xstart > 0 && path[xstart - 1] == 'X')
    105 		xstart--;
    106 
    107 	memset(&sunaddr, 0, sizeof(sunaddr));
    108 	sunaddr.sun_family = AF_UNIX;
    109 	prev_mask = umask(0177);
    110 	for (good = 0; !good;) {
    111 		sock = -1;
    112 		/* Randomise path suffix */
    113 		for (i = xstart; path[i] != '\0'; i++) {
    114 			path[i] = presentation_chars[
    115 			    arc4random_uniform(sizeof(presentation_chars)-1)];
    116 		}
    117 		debug_f("trying path \"%s\"", path);
    118 
    119 		if (strlcpy(sunaddr.sun_path, path,
    120 		    sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
    121 			error_f("path \"%s\" too long for Unix domain socket",
    122 			    path);
    123 			break;
    124 		}
    125 
    126 		if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
    127 			error_f("socket: %.100s", strerror(errno));
    128 			break;
    129 		}
    130 		if (bind(sock, (struct sockaddr *)&sunaddr,
    131 		    sizeof(sunaddr)) == -1) {
    132 			if (errno == EADDRINUSE) {
    133 				error_f("bind \"%s\": %.100s",
    134 				    path, strerror(errno));
    135 				close(sock);
    136 				sock = -1;
    137 				continue;
    138 			}
    139 			error_f("bind \"%s\": %.100s", path, strerror(errno));
    140 			break;
    141 		}
    142 		if (listen(sock, backlog) == -1) {
    143 			error_f("listen \"%s\": %s", path, strerror(errno));
    144 			break;
    145 		}
    146 		good = 1;
    147 	}
    148 	umask(prev_mask);
    149 	if (good) {
    150 		debug3_f("listening on unix socket \"%s\" as fd=%d",
    151 		    path, sock);
    152 	} else if (sock != -1) {
    153 		close(sock);
    154 		sock = -1;
    155 	}
    156 	return sock;
    157 }
    158 
    159 /*
    160  * Create a subdirectory under the supplied home directory if it
    161  * doesn't already exist
    162  */
    163 static int
    164 ensure_mkdir(const char *homedir, const char *subdir)
    165 {
    166 	char *path;
    167 
    168 	xasprintf(&path, "%s/%s", homedir, subdir);
    169 	if (mkdir(path, 0700) == 0)
    170 		debug("created directory %s", path);
    171 	else if (errno != EEXIST) {
    172 		error_f("mkdir %s: %s", path, strerror(errno));
    173 		free(path);
    174 		return -1;
    175 	}
    176 	free(path);
    177 	return 0;
    178 }
    179 
    180 static int
    181 agent_prepare_sockdir(const char *homedir)
    182 {
    183 	if (homedir == NULL || *homedir == '\0' ||
    184 	    ensure_mkdir(homedir, _PATH_SSH_USER_DIR) != 0 ||
    185 	    ensure_mkdir(homedir, _PATH_SSH_AGENT_SOCKET_DIR) != 0)
    186 		return -1;
    187 	return 0;
    188 }
    189 
    190 
    191 /* Get a path template for an agent socket in the user's homedir */
    192 static char *
    193 agent_socket_template(const char *homedir, const char *tag)
    194 {
    195 	char *hostnamehash, *ret;
    196 
    197 	if ((hostnamehash = hostname_hash(SOCKET_HOSTNAME_HASHLEN)) == NULL)
    198 		return NULL;
    199 	xasprintf(&ret, "%s/%s/s.%s.%s.XXXXXXXXXX",
    200 	    homedir, _PATH_SSH_AGENT_SOCKET_DIR, hostnamehash, tag);
    201 	free(hostnamehash);
    202 	return ret;
    203 }
    204 
    205 int
    206 agent_listener(const char *homedir, const char *tag, int *sockp, char **pathp)
    207 {
    208 	int sock;
    209 	char *path;
    210 
    211 	*sockp = -1;
    212 	*pathp = NULL;
    213 
    214 	if (agent_prepare_sockdir(homedir) != 0)
    215 		return -1; /* error already logged */
    216 	if ((path = agent_socket_template(homedir, tag)) == NULL)
    217 		return -1; /* error already logged */
    218 	if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) {
    219 		free(path);
    220 		return -1; /* error already logged */
    221 	}
    222 	/* success */
    223 	*sockp = sock;
    224 	*pathp = path;
    225 	return 0;
    226 }
    227 
    228 static int
    229 socket_is_stale(const char *path)
    230 {
    231 	int fd, r;
    232 	struct sockaddr_un sunaddr;
    233 	socklen_t l = sizeof(r);
    234 
    235 	/* attempt non-blocking connect on socket */
    236 	memset(&sunaddr, '\0', sizeof(sunaddr));
    237 	sunaddr.sun_family = AF_UNIX;
    238 	if (strlcpy(sunaddr.sun_path, path,
    239 	    sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
    240 		debug_f("path for \"%s\" too long for sockaddr_un", path);
    241 		return 0;
    242 	}
    243 	if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
    244 		error_f("socket: %s", strerror(errno));
    245 		return 0;
    246 	}
    247 	set_nonblock(fd);
    248 	/* a socket without a listener should yield an error immediately */
    249 	if (connect(fd, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) {
    250 		debug_f("connect \"%s\": %s", path, strerror(errno));
    251 		close(fd);
    252 		return 1;
    253 	}
    254 	if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &r, &l) == -1) {
    255 		debug_f("getsockopt: %s", strerror(errno));
    256 		close(fd);
    257 		return 0;
    258 	}
    259 	if (r != 0) {
    260 		debug_f("socket error on %s: %s", path, strerror(errno));
    261 		close(fd);
    262 		return 1;
    263 	}
    264 	close(fd);
    265 	debug_f("socket %s seems still active", path);
    266 	return 0;
    267 }
    268 
    269 void
    270 agent_cleanup_stale(const char *homedir, int ignore_hosthash)
    271 {
    272 	DIR *d = NULL;
    273 	struct dirent *dp;
    274 	struct stat sb;
    275 	char *prefix = NULL, *dirpath = NULL, *path;
    276 	struct timespec now, sub;
    277 
    278 	/* Only consider sockets last modified > 1 hour ago */
    279 	if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
    280 		error_f("clock_gettime: %s", strerror(errno));
    281 		return;
    282 	}
    283 	sub.tv_sec = 60 * 60;
    284 	sub.tv_nsec = 0;
    285 	timespecsub(&now, &sub, &now);
    286 
    287 	/* Only consider sockets from the same hostname */
    288 	if (!ignore_hosthash) {
    289 		if ((path = agent_hostname_hash()) == NULL) {
    290 			error_f("couldn't get hostname hash");
    291 			return;
    292 		}
    293 		xasprintf(&prefix, "s.%s.", path);
    294 		free(path);
    295 	}
    296 
    297 	xasprintf(&dirpath, "%s/%s", homedir, _PATH_SSH_AGENT_SOCKET_DIR);
    298 	if ((d = opendir(dirpath)) == NULL) {
    299 		if (errno != ENOENT)
    300 			error_f("opendir \"%s\": %s", dirpath, strerror(errno));
    301 		goto out;
    302 	}
    303 	while ((dp = readdir(d)) != NULL) {
    304 		if (dp->d_type != DT_SOCK && dp->d_type != DT_UNKNOWN)
    305 			continue;
    306 		if (fstatat(dirfd(d), dp->d_name,
    307 		    &sb, AT_SYMLINK_NOFOLLOW) != 0 && errno != ENOENT) {
    308 			error_f("stat \"%s/%s\": %s",
    309 			    dirpath, dp->d_name, strerror(errno));
    310 			continue;
    311 		}
    312 		if (!S_ISSOCK(sb.st_mode))
    313 			continue;
    314 		if (timespeccmp(&sb.st_mtim, &now, >)) {
    315 			debug3_f("Ignoring recent socket \"%s/%s\"",
    316 			    dirpath, dp->d_name);
    317 			continue;
    318 		}
    319 		if (!ignore_hosthash &&
    320 		    strncmp(dp->d_name, prefix, strlen(prefix)) != 0) {
    321 			debug3_f("Ignoring socket \"%s/%s\" "
    322 			    "from different host", dirpath, dp->d_name);
    323 			continue;
    324 		}
    325 		xasprintf(&path, "%s/%s", dirpath, dp->d_name);
    326 		if (socket_is_stale(path)) {
    327 			debug_f("cleanup stale socket %s", path);
    328 			unlinkat(dirfd(d), dp->d_name, 0);
    329 		}
    330 		free(path);
    331 	}
    332  out:
    333 	if (d != NULL)
    334 		closedir(d);
    335 	free(dirpath);
    336 	free(prefix);
    337 }
    338