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