╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ssh.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
612 try proto.writeStringField(&req, allocator, sig_wire.items); 613 try cs.writePacket(io, allocator, cw, req.items); 614 615 const reply = try sc.readPacket(allocator, cr); 616 defer allocator.free(reply); 617 try std.testing.expectEqual(attempt.expect, reply[0]); 618 } 619 } 620 621 // open two channels back to back: server confirms the first, then 622 // refuses the second with OPEN_FAILURE and stays running. 623 for ([_]struct { id: u32, expect: u8 }{ 624 .{ .id = 7, .expect = proto.SSH_MSG_CHANNEL_OPEN_CONFIRMATION }, 625 .{ .id = 8, .expect = proto.SSH_MSG_CHANNEL_OPEN_FAILURE }, 626 }) |case| { 627 var req: std.ArrayList(u8) = .empty; 628 defer req.deinit(allocator); 629 try req.append(allocator, proto.SSH_MSG_CHANNEL_OPEN); 630 try proto.writeStringField(&req, allocator, "session"); 631 try proto.writeU32(&req, allocator, case.id); 632 try proto.writeU32(&req, allocator, 1 << 20); 633 try proto.writeU32(&req, allocator, 32768); 634 try cs.writePacket(io, allocator, cw, req.items); 635 636 const reply = try sc.readPacket(allocator, cr); 637 defer allocator.free(reply); 638 try std.testing.expectEqual(case.expect, reply[0]); 639 } 640 641 // now break the connection: build any encrypted packet, flip one bit of 642 // its trailing Poly1305 tag, and ship the corrupted bytes. server must 643 // surface MacVerificationFailed out of handleConnection. 644 var req: std.ArrayList(u8) = .empty; 645 defer req.deinit(allocator); 646 try req.append(allocator, proto.SSH_MSG_CHANNEL_REQUEST); 647 try proto.writeU32(&req, allocator, 0); 648 try proto.writeStringField(&req, allocator, "shell"); 649 try req.append(allocator, 0); 650 651 var tmp_buf: [512]u8 = undefined; 652 var sink = std.Io.Writer.fixed(&tmp_buf); 653 try cs.writePacket(io, allocator, &sink, req.items); 654 const written = sink.buffered(); 655 tmp_buf[written.len - 1] ^= 0x01; 656 try cw.writeAll(tmp_buf[0..written.len]); 657 try cw.flush(); 658 659 server_future.await(io); 660 try std.testing.expectEqual(@as(?anyerror, error.MacVerificationFailed), server.result); 661 } 662 663 const client_version = "SSH-2.0-haxy_test"; 664 665 // in-memory bidirectional pipe — drives the server side of handleConnection 666 // without a socket. one Pipe carries bytes in one direction. 667 668 const Pipe = std.Io.Queue(u8); 669 670 const PipeReader = struct { 671 interface: std.Io.Reader, 672 io: std.Io, 673 pipe: *Pipe, 674 675 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeReader { 676 return .{ 677 .interface = .{ 678 .vtable = &.{ .readVec = readVec, .stream = stream }, 679 .buffer = buf, 680 .seek = 0, 681 .end = 0, 682 }, 683 .io = io, 684 .pipe = pipe, 685 }; 686 } 687 688 fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 689 // tests don't drive any code path that calls stream, but the vtable 690 // requires a non-default impl. read into a small staging buffer and 691 // forward, so the implementation is correct if it ever fires. 692 const self: *PipeReader = @fieldParentPtr("interface", r); 693 var stage: [256]u8 = undefined; 694 const cap = limit.minInt(stage.len); 695 if (cap == 0) return 0; 696 const n = self.pipe.get(self.io, stage[0..cap], 1) catch |err| switch (err) { 697 error.Closed => return error.EndOfStream, 698 error.Canceled => return error.ReadFailed, 699 }; 700 return try w.write(stage[0..n]); 701 } 702 703 fn readVec(r: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize { 704 const self: *PipeReader = @fieldParentPtr("interface", r); 705 // vtable contract: if data[0] is empty, fill r.buffer[r.end..] instead. 706 const dest = if (data.len == 0 or data[0].len == 0) r.buffer[r.end..] else data[0]; 707 if (dest.len == 0) return 0; 708 const n = self.pipe.get(self.io, dest, 1) catch |err| switch (err) { 709 error.Closed => return error.EndOfStream, 710 error.Canceled => return error.ReadFailed, 711 }; 712 if (data.len == 0 or data[0].len == 0) { 713 r.end += n; 714 return 0; 715 } 716 return n; 717 } 718 }; 719 720 const PipeWriter = struct { 721 interface: std.Io.Writer, 722 io: std.Io, 723 pipe: *Pipe, 724 725 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeWriter { 726 return .{ 727 .interface = .{ 728 .vtable = &.{ .drain = drain }, 729 .buffer = buf, 730 }, 731 .io = io, 732 .pipe = pipe, 733 }; 734 } 735 736 fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 737 const self: *PipeWriter = @fieldParentPtr("interface", w); 738 const buffered = w.buffered(); 739 if (buffered.len > 0) self.pipe.putAll(self.io, buffered) catch return error.WriteFailed; 740 741 var extra: usize = 0; 742 if (data.len > 1) { 743 for (data[0 .. data.len - 1]) |chunk| { 744 if (chunk.len == 0) continue; 745 self.pipe.putAll(self.io, chunk) catch return error.WriteFailed; 746 extra += chunk.len; 747 } 748 } 749 if (data.len > 0) { 750 const pattern = data[data.len - 1]; 751 if (pattern.len > 0) { 752 var i: usize = 0; 753 while (i < splat) : (i += 1) { 754 self.pipe.putAll(self.io, pattern) catch return error.WriteFailed; 755 extra += pattern.len; 756 } 757 } 758 } 759 return w.consume(buffered.len + extra); 760 } 761 }; 762 763 // runs proto.handleConnection in a concurrent task (spawned via std.Io.concurrent). 764 // also acts as the session handler — captures the authenticated key's 765 // fingerprint and returns immediately, leaving runChannelLayer's wrap-up to 766 // send exit-status / EOF / CLOSE. records any error so the test can assert 767 // on it after awaiting the future. 768 const ServerTask = struct { 769 io: std.Io, 770 allocator: std.mem.Allocator, 771 host_key: *const proto.HostKey, 772 reader: *std.Io.Reader, 773 writer: *std.Io.Writer, 774 outgoing: *Pipe, // closed on exit so the client's reads return EOF 775 captured_fp: [proto.fingerprint_len]u8 = undefined, 776 captured_exec_buf: [256]u8 = undefined, 777 captured_exec_len: usize = 0, 778 result: ?anyerror = null, 779 780 pub fn handleSession(self: *ServerTask, sess: *proto.SessionCtx, request: proto.Request) !void { 781 @memcpy(&self.captured_fp, &sess.fingerprint); 782 switch (request) { 783 .exec => |cmd| { 784 // copy out of the protocol's stack-scoped buffer so the 785 // test can assert on it after the handler returns. 786 @memcpy(self.captured_exec_buf[0..cmd.len], cmd); 787 self.captured_exec_len = cmd.len; 788 }, 789 .shell => {}, 790 } 791 } 792 793 fn run(self: *ServerTask) void { 794 defer self.outgoing.close(self.io); 795 proto.handleConnection( 796 self.io, 797 self.allocator, 798 self.reader, 799 self.writer, 800 self.host_key, 801 self, 802 ) catch |e| { 803 self.result = e; 804 }; 805 } 806 }; 807 808 // deterministic keys so failures don't depend on RNG state. 809 810 fn newHostKey() !proto.HostKey { 811 const seed = [_]u8{0xAA} ** Ed25519.KeyPair.seed_length; 812 return .{ .keypair = try Ed25519.KeyPair.generateDeterministic(seed) }; 813 } 814 815 fn newUserKeypair() !Ed25519.KeyPair { 816 const seed = [_]u8{0xBB} ** Ed25519.KeyPair.seed_length; 817 return try Ed25519.KeyPair.generateDeterministic(seed); 818 } 819 820 fn newClientEphemeral() !X25519.KeyPair { 821 const seed = [_]u8{0xCC} ** X25519.seed_length; 822 return try X25519.KeyPair.generateDeterministic(seed); 823 } 824