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