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