2a47b9f5
fix: audit refinements — client length check, nothing-listening, actionable stop hint
a73x 2026-08-10 18:28
Commit message
src/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -46,10 +46,6 @@ const Incoming = union(enum) { | |||
| 46 | /// left behind, short enough that a genuinely full session still reports it. | 46 | /// left behind, short enough that a genuinely full session still reports it. |
| 47 | const reconnect_grace_ms: i64 = 5000; | 47 | const reconnect_grace_ms: i64 = 5000; |
| 48 | 48 | ||
| 49 | /// One live connection to a muxd, however it was reached. The point of the | ||
| 50 | /// struct is that it can be closed and opened again from the same recipe | ||
| 51 | /// (`sock_path` or `via`), which is what lets a session outlive its | ||
| 52 | /// transport instead of exiting with it. | ||
| 53 | /// What to say when the link dies. A `--via` transport that died before a | 49 | /// What to say when the link dies. A `--via` transport that died before a |
| 54 | /// single frame arrived never carried a connection at all — ssh refused, the | 50 | /// single frame arrived never carried a connection at all — ssh refused, the |
| 55 | /// host is unreachable, or `muxd` is not on its PATH — and "connection to | 51 | /// host is unreachable, or `muxd` is not on its PATH — and "connection to |
| @@ -85,6 +81,10 @@ pub const QuicTarget = struct { | |||
| 85 | idle_ms: u32 = quic_idle_ms_default, | 81 | idle_ms: u32 = quic_idle_ms_default, |
| 86 | }; | 82 | }; |
| 87 | 83 | ||
| 84 | /// One live connection to a muxd, however it was reached. The point of the | ||
| 85 | /// struct is that it can be closed and opened again from the same recipe | ||
| 86 | /// (`sock_path` or `via`), which is what lets a session outlive its | ||
| 87 | /// transport instead of exiting with it. | ||
| 88 | const Transport = struct { | 88 | const Transport = struct { |
| 89 | conn: Conn, | 89 | conn: Conn, |
| 90 | /// Only set for `--via`: the command whose stdio *is* the transport. | 90 | /// Only set for `--via`: the command whose stdio *is* the transport. |
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -248,8 +248,9 @@ pub fn main() !u8 { | |||
| 248 | } | 248 | } |
| 249 | 249 | ||
| 250 | switch (o.cmd) { | 250 | switch (o.cmd) { |
| 251 | // The socket path resolved above is unused here and harmless: asking | 251 | // The socket path resolved above is unused here and unchecked (see |
| 252 | // a binary its version must work with no daemon and no runtime dir. | 252 | // the length guard above): asking a binary its version must work |
| 253 | // with no daemon and no runtime dir. | ||
| 253 | .version => { | 254 | .version => { |
| 254 | var vbuf: [64]u8 = undefined; | 255 | var vbuf: [64]u8 = undefined; |
| 255 | const s = std.fmt.bufPrint(&vbuf, "muxd {s}\n", .{build_options.version}) catch unreachable; | 256 | const s = std.fmt.bufPrint(&vbuf, "muxd {s}\n", .{build_options.version}) catch unreachable; |
| @@ -389,7 +390,10 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 { | |||
| 389 | return 1; | 390 | return 1; |
| 390 | }, | 391 | }, |
| 391 | error.SockPathNotASocket => { | 392 | error.SockPathNotASocket => { |
| 392 | std.debug.print("muxd: {s} exists and is not a socket (move it, or name another with --sock)\n", .{sock_path}); | 393 | std.debug.print( |
| 394 | "muxd: {s} exists and is not a socket (move it, or name another with --sock)\n", | ||
| 395 | .{sock_path}, | ||
| 396 | ); | ||
| 393 | return 1; | 397 | return 1; |
| 394 | }, | 398 | }, |
| 395 | else => return err, | 399 | else => return err, |
| @@ -415,7 +419,10 @@ pub fn defaultSockPath(alloc: std.mem.Allocator) ![]const u8 { | |||
| 415 | 419 | ||
| 416 | fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool) !u8 { | 420 | fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool) !u8 { |
| 417 | const stream = std.net.connectUnixSocket(sock_path) catch { | 421 | const stream = std.net.connectUnixSocket(sock_path) catch { |
| 418 | std.debug.print("muxd dump: cannot connect to {s} (no daemon; `muxd start` starts one)\n", .{sock_path}); | 422 | std.debug.print( |
| 423 | "muxd dump: nothing listening on {s} (`muxd start` starts a daemon)\n", | ||
| 424 | .{sock_path}, | ||
| 425 | ); | ||
| 419 | return 1; | 426 | return 1; |
| 420 | }; | 427 | }; |
| 421 | defer stream.close(); | 428 | defer stream.close(); |
| @@ -433,7 +440,10 @@ fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool) !u8 { | |||
| 433 | 440 | ||
| 434 | fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 { | 441 | fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 { |
| 435 | const stream = std.net.connectUnixSocket(sock_path) catch { | 442 | const stream = std.net.connectUnixSocket(sock_path) catch { |
| 436 | std.debug.print("muxd stats: cannot connect to {s} (no daemon; `muxd start` starts one)\n", .{sock_path}); | 443 | std.debug.print( |
| 444 | "muxd stats: nothing listening on {s} (`muxd start` starts a daemon)\n", | ||
| 445 | .{sock_path}, | ||
| 446 | ); | ||
| 437 | return 1; | 447 | return 1; |
| 438 | }; | 448 | }; |
| 439 | defer stream.close(); | 449 | defer stream.close(); |
| @@ -551,8 +561,8 @@ fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const | |||
| 551 | }; | 561 | }; |
| 552 | if (r == .already_running) { | 562 | if (r == .already_running) { |
| 553 | std.debug.print( | 563 | std.debug.print( |
| 554 | "muxd: already running on {s} (`muxd stop` it first if you meant different flags)\n", | 564 | "muxd: already running on {s} (stop it first with `muxd stop --sock {s}` if you meant different flags)\n", |
| 555 | .{sock_path}, | 565 | .{ sock_path, sock_path }, |
| 556 | ); | 566 | ); |
| 557 | } | 567 | } |
| 558 | return 0; | 568 | return 0; |
src/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -188,6 +188,21 @@ pub fn main() !u8 { | |||
| 188 | try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()}); | 188 | try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()}); |
| 189 | defer alloc.free(sock_path); | 189 | defer alloc.free(sock_path); |
| 190 | 190 | ||
| 191 | // The same 107-byte `sun_path` guard muxd applies (main.zig), for | ||
| 192 | // the same reason and with the same constant. It sits before the | ||
| 193 | // PATH search rather than at the connect because auto-start would | ||
| 194 | // otherwise reach it first: mux finds muxd, spawns a child that | ||
| 195 | // refuses the path instantly, and polls the full 2s into "daemon | ||
| 196 | // did not answer" — a timeout story about a path that was doomed | ||
| 197 | // at parse. Refusing here costs nothing and says the real thing. | ||
| 198 | if (sock_path.len > 107) { | ||
| 199 | std.debug.print( | ||
| 200 | "mux: socket path too long ({d} bytes, max 107): {s}\n", | ||
| 201 | .{ sock_path.len, sock_path }, | ||
| 202 | ); | ||
| 203 | return 1; | ||
| 204 | } | ||
| 205 | |||
| 191 | // Attach auto-start (M13): give the attach a daemon to land on. | 206 | // Attach auto-start (M13): give the attach a daemon to land on. |
| 192 | // Unix-socket transport only — quic:// has nothing local to | 207 | // Unix-socket transport only — quic:// has nothing local to |
| 193 | // spawn, and --via's auto-starter is the remote proxy. | 208 | // spawn, and --via's auto-starter is the remote proxy. |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -489,7 +489,7 @@ grep -q "transport command failed before a session started" "$OUT.dead" || { | |||
| 489 | # a cause the transport's own stderr had already named. | 489 | # a cause the transport's own stderr had already named. |
| 490 | grep -q "is muxd installed on the host" "$OUT.dead" && { | 490 | grep -q "is muxd installed on the host" "$OUT.dead" && { |
| 491 | echo "e2e FAIL: old lostMsg wording still emitted alongside the new pin" | 491 | echo "e2e FAIL: old lostMsg wording still emitted alongside the new pin" |
| 492 | cat "$OUT.dead"; exit 1; } || true | 492 | cat "$OUT.dead"; exit 1; } |
| 493 | rm -f "$OUT.dead" | 493 | rm -f "$OUT.dead" |
| 494 | 494 | ||
| 495 | # --- M7: aborting a reconnect exits cleanly. The client establishes a real | 495 | # --- M7: aborting a reconnect exits cleanly. The client establishes a real |