╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ serve_ssh_protocol.zig ───────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1734 try writeStringField(buf, allocator, session_id); 1735 try buf.append(allocator, SSH_MSG_USERAUTH_REQUEST); 1736 try writeStringField(buf, allocator, user_name); 1737 try writeStringField(buf, allocator, service_name); 1738 try writeStringField(buf, allocator, "publickey"); 1739 try buf.append(allocator, 1); 1740 try writeStringField(buf, allocator, algo); 1741 try writeStringField(buf, allocator, pubkey_blob); 1742 } 1743 1744 // build the bytes a publickey signature is computed over (RFC 4252 §7) and 1745 // verify the supplied signature against them. 1746 fn verifyUserauthSignature( 1747 allocator: std.mem.Allocator, 1748 session_id: *const [Sha256.digest_length]u8, 1749 user_name: []const u8, 1750 service_name: []const u8, 1751 algo: []const u8, 1752 pubkey_blob: []const u8, 1753 signature_blob: []const u8, 1754 ) !bool { 1755 var signed: std.ArrayList(u8) = .empty; 1756 defer signed.deinit(allocator); 1757 try appendPublickeySignedData(&signed, allocator, session_id, user_name, service_name, algo, pubkey_blob); 1758 1759 // parse pubkey_blob: string "ssh-ed25519" || string raw_pubkey 1760 var pubkey_reader = std.Io.Reader.fixed(pubkey_blob); 1761 const pk_algo = try takeString(&pubkey_reader, 64); 1762 if (!std.mem.eql(u8, pk_algo, "ssh-ed25519")) return false; 1763 const raw_pubkey = try takeString(&pubkey_reader, 64); 1764 if (raw_pubkey.len != Ed25519.PublicKey.encoded_length) return false; 1765 1766 // parse signature_blob: string "ssh-ed25519" || string raw_signature 1767 var sig_reader = std.Io.Reader.fixed(signature_blob); 1768 const sig_algo = try takeString(&sig_reader, 64); 1769 if (!std.mem.eql(u8, sig_algo, "ssh-ed25519")) return false; 1770 const raw_sig = try takeString(&sig_reader, Ed25519.Signature.encoded_length); 1771 if (raw_sig.len != Ed25519.Signature.encoded_length) return false; 1772 1773 var pubkey_bytes: [Ed25519.PublicKey.encoded_length]u8 = undefined; 1774 @memcpy(&pubkey_bytes, raw_pubkey); 1775 var sig_bytes: [Ed25519.Signature.encoded_length]u8 = undefined; 1776 @memcpy(&sig_bytes, raw_sig); 1777 1778 const pk = Ed25519.PublicKey.fromBytes(pubkey_bytes) catch return false; 1779 const sig = Ed25519.Signature.fromBytes(sig_bytes); 1780 sig.verify(signed.items, pk) catch return false; 1781 return true; 1782 } 1783 1784 // --------------------------------------------------------------------------- 1785 // connection layer (RFC 4254): channels, requests, data, flow control 1786 // --------------------------------------------------------------------------- 1787 1788 const Channel = struct { 1789 local_id: u32, 1790 remote_id: u32, 1791 local_window: u32, // bytes peer may still send us before we adjust 1792 remote_window: u32, // bytes we may still send to peer before they adjust 1793 max_packet: u32, // max CHANNEL_DATA payload the peer accepts in one packet 1794 pty: ?PtySize = null, 1795 }; 1796 1797 pub const PtySize = struct { width_cells: u16, height_cells: u16 }; 1798 1799 // take the uint32 width/height cell counts of a pty-req or window-change, 1800 // bounded before either initial rendering or a resize can allocate grids. 1801 // keep zero dimensions intact (unspecified at startup, minimized on resize). 1802 fn takePtySize(r: *std.Io.Reader) !PtySize { 1803 const width = @min(try r.takeInt(u32, .big), max_pty_width); 1804 const height = @min(try r.takeInt(u32, .big), max_pty_height); 1805 return .{ 1806 .width_cells = @intCast(width), 1807 .height_cells = @intCast(height), 1808 }; 1809 } 1810 1811 fn runChannelLayer( 1812 conn: *Conn, 1813 fingerprint: *const [fingerprint_len]u8, 1814 handler: anytype, 1815 ) !void { 1816 const allocator = conn.allocator; 1817 var channel: ?Channel = null; 1818 1819 while (true) { 1820 const packet = try readSessionPacket(conn); 1821 defer allocator.free(packet); 1822 if (packet.len < 1) return error.EmptyPacket; 1823 const msg_type = packet[0]; 1824 1825 switch (msg_type) { 1826 SSH_MSG_GLOBAL_REQUEST => try handleGlobalRequest(conn, packet), 1827 1828 SSH_MSG_CHANNEL_OPEN => { 1829 if (channel != null) { 1830 try sendChannelOpenFailure(conn, packet, SSH_OPEN_RESOURCE_SHORTAGE, "only one channel per connection"); 1831 continue; 1832 } 1833 channel = try handleChannelOpen(conn, packet) orelse continue; 1834 }, 1835 1836 SSH_MSG_CHANNEL_REQUEST => { 1837 const ch = if (channel) |*c| c else continue; 1838 const start_request = try handleChannelRequest(conn, ch, packet); 1839 // the request borrows from the packet, which outlives the session 1840 if (start_request) |request| { 1841 // any key authenticates, so the deadline holds until here 1842 if (conn.idle) |idle| idle.session_started.store(true, .release); 1843 var sess = SessionCtx{ 1844 .conn = conn, 1845 .channel = ch, 1846 .fingerprint = fingerprint.*, 1847 }; 1848 defer sess.deinit(); 1849 handler.handleSession(&sess, request) catch |session_err| { 1850 // try to send a non-zero exit status before closing 1851 sess.exit(1) catch {}; 1852 return sess.underlyingError(session_err); 1853 }; 1854 if (!sess.closed) try sess.exit(0); 1855 return; 1856 } 1857 }, 1858 1859 SSH_MSG_CHANNEL_WINDOW_ADJUST => { 1860 const ch = if (channel) |*c| c else continue; 1861 try handleWindowAdjust(ch, packet); 1862 }, 1863 1864 SSH_MSG_CHANNEL_DATA, SSH_MSG_CHANNEL_EXTENDED_DATA => { 1865 const ch = if (channel) |*c| c else continue; 1866 // before shell/exec the client shouldn't be sending data, but 1867 // some clients are chatty; discard and refill window. nothing 1868 // is buffered yet, so the whole window is available to credit. 1869 try discardChannelData(ch, packet, msg_type == SSH_MSG_CHANNEL_EXTENDED_DATA); 1870 try maybeRefillRecvWindow(conn, ch, initial_recv_window); 1871 }, 1872 1873 SSH_MSG_CHANNEL_EOF => { 1874 if (channel) |*c| try parseChannelId(packet, c.local_id); 1875 }, 1876 1877 SSH_MSG_CHANNEL_CLOSE => { 1878 if (channel) |*c| { 1879 try parseChannelId(packet, c.local_id); 1880 try sendChannelMessage(conn, c, SSH_MSG_CHANNEL_CLOSE); 1881 drainConnection(conn); 1882 } 1883 return; 1884 }, 1885 1886 // RFC 4252 §5.1: silently ignored once authenticated 1887 SSH_MSG_USERAUTH_REQUEST => {}, 1888 else => try sendUnimplemented(conn), 1889 } 1890 } 1891 } 1892 1893 fn handleGlobalRequest(conn: *Conn, packet: []const u8) !void { 1894 // byte SSH_MSG_GLOBAL_REQUEST 1895 // string request_name 1896 // boolean want_reply 1897 // …request-specific data we ignore 1898 var r = std.Io.Reader.fixed(packet[1..]); 1899 _ = try takeString(&r, 256); 1900 const want_reply = (try r.takeByte()) != 0; 1901 if (want_reply) { 1902 try conn.writePacket(&[_]u8{SSH_MSG_REQUEST_FAILURE}); 1903 } 1904 } 1905 1906 /// parse a CHANNEL_OPEN. on a session channel, send CHANNEL_OPEN_CONFIRMATION 1907 /// and return the new Channel. on other types, send CHANNEL_OPEN_FAILURE and 1908 /// return null. 1909 fn handleChannelOpen(conn: *Conn, packet: []const u8) !?Channel { 1910 const allocator = conn.allocator; 1911 1912 // byte SSH_MSG_CHANNEL_OPEN 1913 // string channel_type 1914 // uint32 sender_channel (peer's id) 1915 // uint32 initial_window 1916 // uint32 max_packet 1917 var r = std.Io.Reader.fixed(packet[1..]); 1918 const ch_type = try takeString(&r, 64); 1919 const remote_id = try r.takeInt(u32, .big); 1920 const initial_window = try r.takeInt(u32, .big); 1921 const max_packet = try r.takeInt(u32, .big); 1922 1923 if (!std.mem.eql(u8, ch_type, "session")) { 1924 try sendChannelOpenFailure(conn, packet, SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "only session channels are supported"); 1925 return null; 1926 } 1927 1928 // a zero max_packet would leave writeChannel looping without progress 1929 if (max_packet == 0) { 1930 try sendChannelOpenFailure(conn, packet, SSH_OPEN_RESOURCE_SHORTAGE, "max packet size must be non-zero"); 1931 return null; 1932 } 1933 1934 const local_id: u32 = 0; // single-channel connection — id is always 0 from our side 1935 var reply: std.ArrayList(u8) = .empty; 1936 defer reply.deinit(allocator); 1937 try reply.append(allocator, SSH_MSG_CHANNEL_OPEN_CONFIRMATION); 1938 try writeU32(&reply, allocator, remote_id); 1939 try writeU32(&reply, allocator, local_id); 1940 try writeU32(&reply, allocator, initial_recv_window); 1941 try writeU32(&reply, allocator, max_packet_size); 1942 try conn.writePacket(reply.items); 1943 1944 return .{ 1945 .local_id = local_id, 1946 .remote_id = remote_id, 1947 .local_window = initial_recv_window, 1948 .remote_window = initial_window, 1949 .max_packet = max_packet, 1950 }; 1951 } 1952 1953 fn sendChannelOpenFailure(conn: *Conn, open_packet: []const u8, reason: u32, description: []const u8) !void { 1954 const allocator = conn.allocator; 1955 1956 // recover the sender_channel field from the CHANNEL_OPEN packet so we 1957 // address the failure to the correct id 1958 var r = std.Io.Reader.fixed(open_packet[1..]); 1959 const ch_type_len = try r.takeInt(u32, .big); 1960 try r.discardAll(ch_type_len); 1961 const remote_id = try r.takeInt(u32, .big); 1962 1963 var reply: std.ArrayList(u8) = .empty; 1964 defer reply.deinit(allocator); 1965 try reply.append(allocator, SSH_MSG_CHANNEL_OPEN_FAILURE); 1966 try writeU32(&reply, allocator, remote_id); 1967 try writeU32(&reply, allocator, reason); 1968 try writeStringField(&reply, allocator, description); 1969 try writeStringField(&reply, allocator, ""); // language tag 1970 try conn.writePacket(reply.items); 1971 } 1972 1973 /// returns the session request if this one triggered it, else null. an exec 1974 /// command borrows from `packet`. 1975 fn handleChannelRequest(conn: *Conn, ch: *Channel, packet: []const u8) !?Request { 1976 // byte SSH_MSG_CHANNEL_REQUEST 1977 // uint32 recipient_channel (== our local_id) 1978 // string request_type 1979 // boolean want_reply 1980 // …request-specific data 1981 var r = std.Io.Reader.fixed(packet[1..]); 1982 const recipient_channel = try r.takeInt(u32, .big); 1983 if (recipient_channel != ch.local_id) return error.UnknownChannel; 1984 const req_type = try takeString(&r, 64); 1985 const want_reply = (try r.takeByte()) != 0; 1986 1987 if (std.mem.eql(u8, req_type, "pty-req")) { 1988 // string TERM, uint32 width chars, uint32 height rows, uint32 width px, 1989 // uint32 height px, string modes 1990 _ = try takeString(&r, 64); 1991 ch.pty = try takePtySize(&r); 1992 try replyChannelRequest(conn, ch, want_reply, true); 1993 return null; 1994 } 1995 1996 if (std.mem.eql(u8, req_type, "env")) { 1997 // ignore environment variables silently (we don't pass them anywhere) 1998 try replyChannelRequest(conn, ch, want_reply, true); 1999 return null; 2000 } 2001 2002 if (std.mem.eql(u8, req_type, "window-change")) { 2003 // uint32 width cells, uint32 height rows, uint32 width px, uint32 height px 2004 const sz = try takePtySize(&r); 2005 if (ch.pty != null) ch.pty = sz; 2006 // spec: window-change MUST NOT request a reply, but accept either 2007 try replyChannelRequest(conn, ch, want_reply, true); 2008 return null; 2009 } 2010 2011 if (std.mem.eql(u8, req_type, "shell")) { 2012 try replyChannelRequest(conn, ch, want_reply, true); 2013 return .{ .shell = ch.pty }; 2014 } 2015 2016 if (std.mem.eql(u8, req_type, "exec")) { 2017 // capture the command string for the handler to inspect 2018 const cmd = try takeString(&r, 4096); 2019 try replyChannelRequest(conn, ch, want_reply, true); 2020 return .{ .exec = cmd }; 2021 } 2022 2023 // unknown request kind — fail it 2024 try replyChannelRequest(conn, ch, want_reply, false); 2025 return null; 2026 } 2027 2028 fn replyChannelRequest(conn: *Conn, ch: *Channel, want_reply: bool, success: bool) !void { 2029 if (!want_reply) return; 2030 try sendChannelMessage(conn, ch, if (success) SSH_MSG_CHANNEL_SUCCESS else SSH_MSG_CHANNEL_FAILURE); 2031 } 2032 2033 fn sendChannelMessage(conn: *Conn, ch: *Channel, msg_type: u8) !void { 2034 var packet: [5]u8 = undefined; 2035 packet[0] = msg_type; 2036 std.mem.writeInt(u32, packet[1..5], ch.remote_id, .big); 2037 try conn.writePacket(&packet); 2038 } 2039 2040 fn handleWindowAdjust(ch: *Channel, packet: []const u8) !void { 2041 // byte SSH_MSG_CHANNEL_WINDOW_ADJUST 2042 // uint32 recipient_channel 2043 // uint32 bytes_to_add 2044 var r = std.Io.Reader.fixed(packet[1..]); 2045 const recipient_channel = try r.takeInt(u32, .big); 2046 if (recipient_channel != ch.local_id) return error.UnknownChannel; 2047 const add = try r.takeInt(u32, .big); 2048 ch.remote_window +|= add; 2049 } 2050 2051 // charge a payload's bytes against the receive window without keeping them. 2052 // the caller credits the window back, choosing the budget that suits it. 2053 fn discardChannelData(ch: *Channel, packet: []const u8, extended: bool) !void { 2054 // byte SSH_MSG_CHANNEL_DATA/EXTENDED_DATA 2055 // uint32 recipient_channel 2056 // [if extended: uint32 type_code] 2057 // string data 2058 const payload = try parseChannelData(packet, extended, ch.local_id); 2059 const data_len: u32 = @intCast(payload.len); 2060 2061 if (data_len > ch.local_window) return error.WindowExceeded; 2062 ch.local_window -= data_len; 2063 } 2064 2065 pub fn parseChannelData(packet: []const u8, extended: bool, expected_channel: u32) ![]const u8 { 2066 if (packet.len < 1) return error.EmptyPacket; 2067 var r = std.Io.Reader.fixed(packet[1..]); 2068 const recipient_channel = try r.takeInt(u32, .big); 2069 if (recipient_channel != expected_channel) return error.UnknownChannel; 2070 if (extended) { 2071 const data_type = try r.takeInt(u32, .big); 2072 if (data_type != SSH_EXTENDED_DATA_STDERR) return error.UnsupportedExtendedData; 2073 } 2074 const data_len = try r.takeInt(u32, .big); 2075 // catches both data_len > remaining (would otherwise be a bounds-check 2076 // panic on the slice) and trailing junk after the declared payload. 2077 if (data_len != r.buffered().len) return error.InvalidChannelData; 2078 return r.buffered(); 2079 } 2080 2081 fn parseChannelId(packet: []const u8, expected_channel: u32) !void { 2082 if (packet.len < 1) return error.EmptyPacket; 2083 var r = std.Io.Reader.fixed(packet[1..]); 2084 const recipient_channel = try r.takeInt(u32, .big); 2085 if (recipient_channel != expected_channel) return error.UnknownChannel; 2086 if (r.buffered().len != 0) return error.InvalidChannelMessage; 2087 } 2088 2089 /// send a CHANNEL_WINDOW_ADJUST if our receive window has dipped below half 2090 /// of `budget`, the credit we're willing to have outstanding. used both for 2091 /// the pre-session discard path and by SessionCtx after it consumes a 2092 /// CHANNEL_DATA payload. 2093 fn maybeRefillRecvWindow(conn: *Conn, ch: *Channel, budget: u32) !void { 2094 if (ch.local_window >= budget / 2) return; 2095 const add = budget - ch.local_window; 2096 ch.local_window += add; 2097 var adj: std.ArrayList(u8) = .empty; 2098 defer adj.deinit(conn.allocator); 2099 try adj.append(conn.allocator, SSH_MSG_CHANNEL_WINDOW_ADJUST); 2100 try writeU32(&adj, conn.allocator, ch.remote_id); 2101 try writeU32(&adj, conn.allocator, add); 2102 try conn.writePacket(adj.items); 2103 } 2104 2105 pub fn writeU32(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, value: u32) !void { 2106 var bytes: [4]u8 = undefined; 2107 std.mem.writeInt(u32, &bytes, value, .big); 2108 try buf.appendSlice(allocator, &bytes); 2109 } 2110 2111 test "pty dimensions have bounded axes and area, preserving normal and zero sizes" { 2112 for ([_]struct { width: u32, height: u32, expected: PtySize }{ 2113 .{ .width = 80, .height = 24, .expected = .{ .width_cells = 80, .height_cells = 24 } }, 2114 .{ .width = 0, .height = 0, .expected = .{ .width_cells = 0, .height_cells = 0 } }, 2115 .{ .width = 0xffffffff, .height = 0xffffffff, .expected = .{ .width_cells = 512, .height_cells = 128 } }, 2116 }) |case| { 2117 var bytes: [8]u8 = undefined; 2118 std.mem.writeInt(u32, bytes[0..4], case.width, .big); 2119 std.mem.writeInt(u32, bytes[4..8], case.height, .big); 2120 var reader = std.Io.Reader.fixed(&bytes); 2121 try std.testing.expectEqual(case.expected, try takePtySize(&reader)); 2122 } 2123 } 2124 2125 test "channel close before shell or exec is acknowledged" { 2126 const io = std.testing.io; 2127 const allocator = std.testing.allocator; 2128 const key = [_]u8{0x5a} ** 64; 2129 var encoded: [256]u8 = undefined; 2130 var encoded_writer = std.Io.Writer.fixed(&encoded); 2131 var sender = Cipher.init(&key, 0); 2132 var open: std.ArrayList(u8) = .empty; 2133 defer open.deinit(allocator); 2134 try open.append(allocator, SSH_MSG_CHANNEL_OPEN); 2135 try writeStringField(&open, allocator, "session"); 2136 try writeU32(&open, allocator, 7); 2137 try writeU32(&open, allocator, 32768); 2138 try writeU32(&open, allocator, 32768); 2139 try sender.writePacket(io, &encoded_writer, open.items); 2140 try sender.writePacket(io, &encoded_writer, &.{ SSH_MSG_CHANNEL_CLOSE, 0, 0, 0, 0 }); 2141 var reader = std.Io.Reader.fixed(encoded_writer.buffered()); 2142 var output: [256]u8 = undefined; 2143 var writer = std.Io.Writer.fixed(&output); 2144 var conn = Conn{ 2145 .io = io, 2146 .allocator = allocator, 2147 .reader = &reader, 2148 .writer = &writer, 2149 .cs_cipher = Cipher.init(&key, 0), 2150 .sc_cipher = Cipher.init(&key, 0), 2151 .rekey = undefined, 2152 }; 2153 const Handler = struct { 2154 pub fn handleSession(_: *@This(), _: *SessionCtx, _: Request) !void { 2155 return error.UnexpectedSession; 2156 } 2157 }; 2158 var handler = Handler{}; 2159 try runChannelLayer(&conn, &([_]u8{0} ** fingerprint_len), &handler); 2160 var output_reader = std.Io.Reader.fixed(writer.buffered()); 2161 var receiver = Cipher.init(&key, 0); 2162 const confirmation = try receiver.readPacket(allocator, &output_reader); 2163 defer allocator.free(confirmation); 2164 try std.testing.expectEqual(SSH_MSG_CHANNEL_OPEN_CONFIRMATION, confirmation[0]); 2165 const close = try receiver.readPacket(allocator, &output_reader); 2166 defer allocator.free(close); 2167 try std.testing.expectEqualSlices(u8, &.{ SSH_MSG_CHANNEL_CLOSE, 0, 0, 0, 7 }, close); 2168 try std.testing.expectEqual(0, output_reader.buffered().len); 2169 } 2170