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