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