╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ serve_ssh_protocol.zig ───────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1535 return error.MacVerificationFailed; 1536 } 1537 1538 // decrypt the body in place 1539 ChaCha20.xor(body, body, 1, self.main_key, nonce); 1540 1541 const padding_len = body[0]; 1542 if (padding_len < 4 or 1 + @as(u32, padding_len) > body_len) return error.InvalidPadding; 1543 const payload_len = body_len - 1 - @as(u32, padding_len); 1544 1545 // shift the payload over the padding_length byte, then release the 1546 // trailing padding (caller owns the result) 1547 std.mem.copyForwards(u8, body[0..payload_len], body[1 .. 1 + payload_len]); 1548 const payload = try allocator.realloc(body, payload_len); 1549 1550 self.seq += 1; 1551 return payload; 1552 } 1553 }; 1554 1555 // --------------------------------------------------------------------------- 1556 // post-KEX: service request + user authentication (RFC 4252) 1557 // --------------------------------------------------------------------------- 1558 1559 /// "SHA256:" (7 bytes) + base64-no-padding of a 32-byte SHA256 digest 1560 /// (43 bytes) = 50 bytes. matches what openssh's client logs as 1561 /// `Offering public key: … SHA256:…`. 1562 pub const fingerprint_len = 50; 1563 1564 /// hash the SSH wire-format pubkey blob (`string algo` + `string raw_key`) 1565 /// the same way openssh does for `ssh-keygen -lf` / authorized-key logs. 1566 pub fn formatFingerprint(pubkey_blob: []const u8) [fingerprint_len]u8 { 1567 var digest: [Sha256.digest_length]u8 = undefined; 1568 Sha256.hash(pubkey_blob, &digest, .{}); 1569 1570 var out: [fingerprint_len]u8 = undefined; 1571 @memcpy(out[0..7], "SHA256:"); 1572 const encoded = std.base64.standard_no_pad.Encoder.encode(out[7..], &digest); 1573 std.debug.assert(encoded.len == fingerprint_len - 7); 1574 return out; 1575 } 1576 1577 fn runAuth(conn: *Conn) ![fingerprint_len]u8 { 1578 const allocator = conn.allocator; 1579 1580 // step 1: service request for ssh-userauth → service accept 1581 { 1582 const req = try readSessionPacket(conn); 1583 defer allocator.free(req); 1584 if (req.len < 1 or req[0] != SSH_MSG_SERVICE_REQUEST) return error.UnexpectedMessage; 1585 1586 var req_reader = std.Io.Reader.fixed(req[1..]); 1587 const service = try takeString(&req_reader, 64); 1588 if (!std.mem.eql(u8, service, "ssh-userauth")) return error.UnsupportedService; 1589 1590 var accept: std.ArrayList(u8) = .empty; 1591 defer accept.deinit(allocator); 1592 try accept.append(allocator, SSH_MSG_SERVICE_ACCEPT); 1593 try writeStringField(&accept, allocator, "ssh-userauth"); 1594 try conn.writePacket(accept.items); 1595 } 1596 1597 // step 2: USERAUTH loop until SUCCESS. accept any ed25519 key whose 1598 // signature verifies against the offered pubkey. 1599 var auth_attempts: u32 = 0; 1600 while (true) { 1601 const req = try readSessionPacket(conn); 1602 defer allocator.free(req); 1603 if (req.len < 1 or req[0] != SSH_MSG_USERAUTH_REQUEST) return error.UnexpectedMessage; 1604 1605 var req_reader = std.Io.Reader.fixed(req[1..]); 1606 const user_name = try takeString(&req_reader, 256); 1607 const service_name = try takeString(&req_reader, 64); 1608 const method = try takeString(&req_reader, 64); 1609 1610 // RFC 4252 §5: the server must verify the requested service 1611 if (!std.mem.eql(u8, service_name, "ssh-connection")) { 1612 auth_attempts += 1; 1613 if (auth_attempts >= max_auth_attempts) return error.TooManyAuthAttempts; 1614 try sendUserauthFailure(conn); 1615 continue; 1616 } 1617 1618 if (!std.mem.eql(u8, method, "publickey")) { 1619 auth_attempts += 1; 1620 if (auth_attempts >= max_auth_attempts) return error.TooManyAuthAttempts; 1621 try sendUserauthFailure(conn); 1622 continue; 1623 } 1624 1625 const has_signature = (try req_reader.takeByte()) != 0; 1626 const algo = try takeString(&req_reader, 64); 1627 const pubkey_blob = try takeString(&req_reader, 4096); 1628 1629 if (!std.mem.eql(u8, algo, "ssh-ed25519")) { 1630 auth_attempts += 1; 1631 if (auth_attempts >= max_auth_attempts) return error.TooManyAuthAttempts; 1632 try sendUserauthFailure(conn); 1633 continue; 1634 } 1635 1636 if (!has_signature) { 1637 // probes count as attempts so a client can't loop here forever 1638 auth_attempts += 1; 1639 if (auth_attempts >= max_auth_attempts) return error.TooManyAuthAttempts; 1640 // probe — tell the client this key is acceptable so it'll send 1641 // the signed version next. 1642 var pk_ok: std.ArrayList(u8) = .empty; 1643 defer pk_ok.deinit(allocator); 1644 try pk_ok.append(allocator, SSH_MSG_USERAUTH_PK_OK); 1645 try writeStringField(&pk_ok, allocator, algo); 1646 try writeStringField(&pk_ok, allocator, pubkey_blob); 1647 try conn.writePacket(pk_ok.items); 1648 continue; 1649 } 1650 1651 // has signature — verify it 1652 const signature_blob = try takeString(&req_reader, 1024); 1653 1654 const ok = verifyUserauthSignature( 1655 allocator, 1656 &conn.rekey.session_id, 1657 user_name, 1658 service_name, 1659 algo, 1660 pubkey_blob, 1661 signature_blob, 1662 ) catch false; 1663 1664 if (!ok) { 1665 auth_attempts += 1; 1666 if (auth_attempts >= max_auth_attempts) return error.TooManyAuthAttempts; 1667 try sendUserauthFailure(conn); 1668 continue; 1669 } 1670 1671 try conn.writePacket(&[_]u8{SSH_MSG_USERAUTH_SUCCESS}); 1672 return formatFingerprint(pubkey_blob); 1673 } 1674 } 1675 1676 fn sendUserauthFailure(conn: *Conn) !void { 1677 var buf: std.ArrayList(u8) = .empty; 1678 defer buf.deinit(conn.allocator); 1679 try buf.append(conn.allocator, SSH_MSG_USERAUTH_FAILURE); 1680 try writeNameList(&buf, conn.allocator, &.{"publickey"}); // allowed methods 1681 try buf.append(conn.allocator, 0); // partial_success = false 1682 try conn.writePacket(buf.items); 1683 } 1684 1685 /// append the canonical publickey-signed-data bytes (RFC 4252 §7) to `buf`. 1686 /// used by the server to recompute the signed input for verification 1687 pub fn appendPublickeySignedData( 1688 buf: *std.ArrayList(u8), 1689 allocator: std.mem.Allocator, 1690 session_id: []const u8, 1691 user_name: []const u8, 1692 service_name: []const u8, 1693 algo: []const u8, 1694 pubkey_blob: []const u8, 1695 ) !void { 1696 try writeStringField(buf, allocator, session_id); 1697 try buf.append(allocator, SSH_MSG_USERAUTH_REQUEST); 1698 try writeStringField(buf, allocator, user_name); 1699 try writeStringField(buf, allocator, service_name); 1700 try writeStringField(buf, allocator, "publickey"); 1701 try buf.append(allocator, 1); 1702 try writeStringField(buf, allocator, algo); 1703 try writeStringField(buf, allocator, pubkey_blob); 1704 } 1705 1706 // build the bytes a publickey signature is computed over (RFC 4252 §7) and 1707 // verify the supplied signature against them. 1708 fn verifyUserauthSignature( 1709 allocator: std.mem.Allocator, 1710 session_id: *const [Sha256.digest_length]u8, 1711 user_name: []const u8, 1712 service_name: []const u8, 1713 algo: []const u8, 1714 pubkey_blob: []const u8, 1715 signature_blob: []const u8, 1716 ) !bool { 1717 var signed: std.ArrayList(u8) = .empty; 1718 defer signed.deinit(allocator); 1719 try appendPublickeySignedData(&signed, allocator, session_id, user_name, service_name, algo, pubkey_blob); 1720 1721 // parse pubkey_blob: string "ssh-ed25519" || string raw_pubkey 1722 var pubkey_reader = std.Io.Reader.fixed(pubkey_blob); 1723 const pk_algo = try takeString(&pubkey_reader, 64); 1724 if (!std.mem.eql(u8, pk_algo, "ssh-ed25519")) return false; 1725 const raw_pubkey = try takeString(&pubkey_reader, 64); 1726 if (raw_pubkey.len != Ed25519.PublicKey.encoded_length) return false; 1727 1728 // parse signature_blob: string "ssh-ed25519" || string raw_signature 1729 var sig_reader = std.Io.Reader.fixed(signature_blob); 1730 const sig_algo = try takeString(&sig_reader, 64); 1731 if (!std.mem.eql(u8, sig_algo, "ssh-ed25519")) return false; 1732 const raw_sig = try takeString(&sig_reader, Ed25519.Signature.encoded_length); 1733 if (raw_sig.len != Ed25519.Signature.encoded_length) return false; 1734 1735 var pubkey_bytes: [Ed25519.PublicKey.encoded_length]u8 = undefined; 1736 @memcpy(&pubkey_bytes, raw_pubkey); 1737 var sig_bytes: [Ed25519.Signature.encoded_length]u8 = undefined; 1738 @memcpy(&sig_bytes, raw_sig); 1739 1740 const pk = Ed25519.PublicKey.fromBytes(pubkey_bytes) catch return false; 1741 const sig = Ed25519.Signature.fromBytes(sig_bytes); 1742 sig.verify(signed.items, pk) catch return false; 1743 return true; 1744 } 1745 1746 // --------------------------------------------------------------------------- 1747 // connection layer (RFC 4254): channels, requests, data, flow control 1748 // --------------------------------------------------------------------------- 1749 1750 const Channel = struct { 1751 local_id: u32, 1752 remote_id: u32, 1753 local_window: u32, // bytes peer may still send us before we adjust 1754 remote_window: u32, // bytes we may still send to peer before they adjust 1755 max_packet: u32, // max CHANNEL_DATA payload the peer accepts in one packet 1756 pty: ?PtySize = null, 1757 }; 1758 1759 pub const PtySize = struct { width_cells: u16, height_cells: u16 }; 1760 1761 // take the uint32 width/height cell counts of a pty-req or window-change, 1762 // bounded before either initial rendering or a resize can allocate grids. 1763 // keep zero dimensions intact (unspecified at startup, minimized on resize). 1764 fn takePtySize(r: *std.Io.Reader) !PtySize { 1765 const width = @min(try r.takeInt(u32, .big), max_pty_width); 1766 const height = @min(try r.takeInt(u32, .big), max_pty_height); 1767 return .{ 1768 .width_cells = @intCast(width), 1769 .height_cells = @intCast(height), 1770 }; 1771 } 1772 1773 fn runChannelLayer( 1774 conn: *Conn, 1775 fingerprint: *const [fingerprint_len]u8, 1776 handler: anytype, 1777 ) !void { 1778 const allocator = conn.allocator; 1779 var channel: ?Channel = null; 1780 1781 while (true) { 1782 const packet = try readSessionPacket(conn); 1783 defer allocator.free(packet); 1784 if (packet.len < 1) return error.EmptyPacket; 1785 const msg_type = packet[0]; 1786 1787 switch (msg_type) { 1788 SSH_MSG_GLOBAL_REQUEST => try handleGlobalRequest(conn, packet), 1789 1790 SSH_MSG_CHANNEL_OPEN => { 1791 if (channel != null) { 1792 try sendChannelOpenFailure(conn, packet, SSH_OPEN_RESOURCE_SHORTAGE, "only one channel per connection"); 1793 continue; 1794 } 1795 channel = try handleChannelOpen(conn, packet) orelse continue; 1796 }, 1797 1798 SSH_MSG_CHANNEL_REQUEST => { 1799 const ch = if (channel) |*c| c else continue; 1800 const start_request = try handleChannelRequest(conn, ch, packet); 1801 // the request borrows from the packet, which outlives the session 1802 if (start_request) |request| { 1803 // any key authenticates, so the deadline holds until here 1804 if (conn.idle) |idle| idle.session_started.store(true, .release); 1805 var sess = SessionCtx{ 1806 .conn = conn, 1807 .channel = ch, 1808 .fingerprint = fingerprint.*, 1809 }; 1810 defer sess.deinit(); 1811 handler.handleSession(&sess, request) catch |session_err| { 1812 // try to send a non-zero exit status before closing 1813 sess.exit(1) catch {}; 1814 return session_err; 1815 }; 1816 if (!sess.closed) try sess.exit(0); 1817 return; 1818 } 1819 }, 1820 1821 SSH_MSG_CHANNEL_WINDOW_ADJUST => { 1822 const ch = if (channel) |*c| c else continue; 1823 try handleWindowAdjust(ch, packet); 1824 }, 1825 1826 SSH_MSG_CHANNEL_DATA, SSH_MSG_CHANNEL_EXTENDED_DATA => { 1827 const ch = if (channel) |*c| c else continue; 1828 // before shell/exec the client shouldn't be sending data, but 1829 // some clients are chatty; discard and refill window. nothing 1830 // is buffered yet, so the whole window is available to credit. 1831 try discardChannelData(ch, packet, msg_type == SSH_MSG_CHANNEL_EXTENDED_DATA); 1832 try maybeRefillRecvWindow(conn, ch, initial_recv_window); 1833 }, 1834 1835 SSH_MSG_CHANNEL_EOF => { 1836 if (channel) |*c| try parseChannelId(packet, c.local_id); 1837 }, 1838 1839 SSH_MSG_CHANNEL_CLOSE => { 1840 if (channel) |*c| { 1841 try parseChannelId(packet, c.local_id); 1842 try sendChannelMessage(conn, c, SSH_MSG_CHANNEL_CLOSE); 1843 drainConnection(conn); 1844 } 1845 return; 1846 }, 1847 1848 else => try sendUnimplemented(conn), 1849 } 1850 } 1851 } 1852 1853 fn handleGlobalRequest(conn: *Conn, packet: []const u8) !void { 1854 // byte SSH_MSG_GLOBAL_REQUEST 1855 // string request_name 1856 // boolean want_reply 1857 // …request-specific data we ignore 1858 var r = std.Io.Reader.fixed(packet[1..]); 1859 _ = try takeString(&r, 256); 1860 const want_reply = (try r.takeByte()) != 0; 1861 if (want_reply) { 1862 try conn.writePacket(&[_]u8{SSH_MSG_REQUEST_FAILURE}); 1863 } 1864 } 1865 1866 /// parse a CHANNEL_OPEN. on a session channel, send CHANNEL_OPEN_CONFIRMATION 1867 /// and return the new Channel. on other types, send CHANNEL_OPEN_FAILURE and 1868 /// return null. 1869 fn handleChannelOpen(conn: *Conn, packet: []const u8) !?Channel { 1870 const allocator = conn.allocator; 1871 1872 // byte SSH_MSG_CHANNEL_OPEN 1873 // string channel_type 1874 // uint32 sender_channel (peer's id) 1875 // uint32 initial_window 1876 // uint32 max_packet 1877 var r = std.Io.Reader.fixed(packet[1..]); 1878 const ch_type = try takeString(&r, 64); 1879 const remote_id = try r.takeInt(u32, .big); 1880 const initial_window = try r.takeInt(u32, .big); 1881 const max_packet = try r.takeInt(u32, .big); 1882 1883 if (!std.mem.eql(u8, ch_type, "session")) { 1884 try sendChannelOpenFailure(conn, packet, SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "only session channels are supported"); 1885 return null; 1886 } 1887 1888 // a zero max_packet would leave writeChannel looping without progress 1889 if (max_packet == 0) { 1890 try sendChannelOpenFailure(conn, packet, SSH_OPEN_RESOURCE_SHORTAGE, "max packet size must be non-zero"); 1891 return null; 1892 } 1893 1894 const local_id: u32 = 0; // single-channel connection — id is always 0 from our side 1895 var reply: std.ArrayList(u8) = .empty; 1896 defer reply.deinit(allocator); 1897 try reply.append(allocator, SSH_MSG_CHANNEL_OPEN_CONFIRMATION); 1898 try writeU32(&reply, allocator, remote_id); 1899 try writeU32(&reply, allocator, local_id); 1900 try writeU32(&reply, allocator, initial_recv_window); 1901 try writeU32(&reply, allocator, max_packet_size); 1902 try conn.writePacket(reply.items); 1903 1904 return .{ 1905 .local_id = local_id, 1906 .remote_id = remote_id, 1907 .local_window = initial_recv_window, 1908 .remote_window = initial_window, 1909 .max_packet = max_packet, 1910 }; 1911 } 1912 1913 fn sendChannelOpenFailure(conn: *Conn, open_packet: []const u8, reason: u32, description: []const u8) !void { 1914 const allocator = conn.allocator; 1915 1916 // recover the sender_channel field from the CHANNEL_OPEN packet so we 1917 // address the failure to the correct id 1918 var r = std.Io.Reader.fixed(open_packet[1..]); 1919 const ch_type_len = try r.takeInt(u32, .big); 1920 try r.discardAll(ch_type_len); 1921 const remote_id = try r.takeInt(u32, .big); 1922 1923 var reply: std.ArrayList(u8) = .empty; 1924 defer reply.deinit(allocator); 1925 try reply.append(allocator, SSH_MSG_CHANNEL_OPEN_FAILURE); 1926 try writeU32(&reply, allocator, remote_id); 1927 try writeU32(&reply, allocator, reason); 1928 try writeStringField(&reply, allocator, description); 1929 try writeStringField(&reply, allocator, ""); // language tag 1930 try conn.writePacket(reply.items); 1931 } 1932 1933 /// returns the session request if this one triggered it, else null. an exec 1934 /// command borrows from `packet`. 1935 fn handleChannelRequest(conn: *Conn, ch: *Channel, packet: []const u8) !?Request { 1936 // byte SSH_MSG_CHANNEL_REQUEST 1937 // uint32 recipient_channel (== our local_id) 1938 // string request_type 1939 // boolean want_reply 1940 // …request-specific data 1941 var r = std.Io.Reader.fixed(packet[1..]); 1942 const recipient_channel = try r.takeInt(u32, .big); 1943 if (recipient_channel != ch.local_id) return error.UnknownChannel; 1944 const req_type = try takeString(&r, 64); 1945 const want_reply = (try r.takeByte()) != 0; 1946 1947 if (std.mem.eql(u8, req_type, "pty-req")) { 1948 // string TERM, uint32 width chars, uint32 height rows, uint32 width px, 1949 // uint32 height px, string modes 1950 _ = try takeString(&r, 64); 1951 ch.pty = try takePtySize(&r); 1952 try replyChannelRequest(conn, ch, want_reply, true); 1953 return null; 1954 } 1955 1956 if (std.mem.eql(u8, req_type, "env")) { 1957 // ignore environment variables silently (we don't pass them anywhere) 1958 try replyChannelRequest(conn, ch, want_reply, true); 1959 return null; 1960 } 1961 1962 if (std.mem.eql(u8, req_type, "window-change")) { 1963 // uint32 width cells, uint32 height rows, uint32 width px, uint32 height px 1964 const sz = try takePtySize(&r); 1965 if (ch.pty != null) ch.pty = sz; 1966 // spec: window-change MUST NOT request a reply, but accept either 1967 try replyChannelRequest(conn, ch, want_reply, true); 1968 return null; 1969 } 1970 1971 if (std.mem.eql(u8, req_type, "shell")) { 1972 try replyChannelRequest(conn, ch, want_reply, true); 1973 return .{ .shell = ch.pty }; 1974 } 1975 1976 if (std.mem.eql(u8, req_type, "exec")) { 1977 // capture the command string for the handler to inspect 1978 const cmd = try takeString(&r, 4096); 1979 try replyChannelRequest(conn, ch, want_reply, true); 1980 return .{ .exec = cmd }; 1981 } 1982 1983 // unknown request kind — fail it 1984 try replyChannelRequest(conn, ch, want_reply, false); 1985 return null; 1986 } 1987 1988 fn replyChannelRequest(conn: *Conn, ch: *Channel, want_reply: bool, success: bool) !void { 1989 if (!want_reply) return; 1990 try sendChannelMessage(conn, ch, if (success) SSH_MSG_CHANNEL_SUCCESS else SSH_MSG_CHANNEL_FAILURE); 1991 } 1992 1993 fn sendChannelMessage(conn: *Conn, ch: *Channel, msg_type: u8) !void { 1994 var packet: [5]u8 = undefined; 1995 packet[0] = msg_type; 1996 std.mem.writeInt(u32, packet[1..5], ch.remote_id, .big); 1997 try conn.writePacket(&packet); 1998 } 1999 2000 fn handleWindowAdjust(ch: *Channel, packet: []const u8) !void { 2001 // byte SSH_MSG_CHANNEL_WINDOW_ADJUST 2002 // uint32 recipient_channel 2003 // uint32 bytes_to_add 2004 var r = std.Io.Reader.fixed(packet[1..]); 2005 const recipient_channel = try r.takeInt(u32, .big); 2006 if (recipient_channel != ch.local_id) return error.UnknownChannel; 2007 const add = try r.takeInt(u32, .big); 2008 ch.remote_window +|= add; 2009 } 2010 2011 // charge a payload's bytes against the receive window without keeping them. 2012 // the caller credits the window back, choosing the budget that suits it. 2013 fn discardChannelData(ch: *Channel, packet: []const u8, extended: bool) !void { 2014 // byte SSH_MSG_CHANNEL_DATA/EXTENDED_DATA 2015 // uint32 recipient_channel 2016 // [if extended: uint32 type_code] 2017 // string data 2018 const payload = try parseChannelData(packet, extended, ch.local_id); 2019 const data_len: u32 = @intCast(payload.len); 2020 2021 if (data_len > ch.local_window) return error.WindowExceeded; 2022 ch.local_window -= data_len; 2023 } 2024 2025 pub fn parseChannelData(packet: []const u8, extended: bool, expected_channel: u32) ![]const u8 { 2026 if (packet.len < 1) return error.EmptyPacket; 2027 var r = std.Io.Reader.fixed(packet[1..]); 2028 const recipient_channel = try r.takeInt(u32, .big); 2029 if (recipient_channel != expected_channel) return error.UnknownChannel; 2030 if (extended) { 2031 const data_type = try r.takeInt(u32, .big); 2032 if (data_type != SSH_EXTENDED_DATA_STDERR) return error.UnsupportedExtendedData; 2033 } 2034 const data_len = try r.takeInt(u32, .big); 2035 // catches both data_len > remaining (would otherwise be a bounds-check 2036 // panic on the slice) and trailing junk after the declared payload. 2037 if (data_len != r.buffered().len) return error.InvalidChannelData; 2038 return r.buffered(); 2039 } 2040 2041 fn parseChannelId(packet: []const u8, expected_channel: u32) !void { 2042 if (packet.len < 1) return error.EmptyPacket; 2043 var r = std.Io.Reader.fixed(packet[1..]); 2044 const recipient_channel = try r.takeInt(u32, .big); 2045 if (recipient_channel != expected_channel) return error.UnknownChannel; 2046 if (r.buffered().len != 0) return error.InvalidChannelMessage; 2047 } 2048 2049 /// send a CHANNEL_WINDOW_ADJUST if our receive window has dipped below half 2050 /// of `budget`, the credit we're willing to have outstanding. used both for 2051 /// the pre-session discard path and by SessionCtx after it consumes a 2052 /// CHANNEL_DATA payload. 2053 fn maybeRefillRecvWindow(conn: *Conn, ch: *Channel, budget: u32) !void { 2054 if (ch.local_window >= budget / 2) return; 2055 const add = budget - ch.local_window; 2056 ch.local_window += add; 2057 var adj: std.ArrayList(u8) = .empty; 2058 defer adj.deinit(conn.allocator); 2059 try adj.append(conn.allocator, SSH_MSG_CHANNEL_WINDOW_ADJUST); 2060 try writeU32(&adj, conn.allocator, ch.remote_id); 2061 try writeU32(&adj, conn.allocator, add); 2062 try conn.writePacket(adj.items); 2063 } 2064 2065 pub fn writeU32(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, value: u32) !void { 2066 var bytes: [4]u8 = undefined; 2067 std.mem.writeInt(u32, &bytes, value, .big); 2068 try buf.appendSlice(allocator, &bytes); 2069 } 2070 2071 test "pty dimensions have bounded axes and area, preserving normal and zero sizes" { 2072 for ([_]struct { width: u32, height: u32, expected: PtySize }{ 2073 .{ .width = 80, .height = 24, .expected = .{ .width_cells = 80, .height_cells = 24 } }, 2074 .{ .width = 0, .height = 0, .expected = .{ .width_cells = 0, .height_cells = 0 } }, 2075 .{ .width = 0xffffffff, .height = 0xffffffff, .expected = .{ .width_cells = 512, .height_cells = 128 } }, 2076 }) |case| { 2077 var bytes: [8]u8 = undefined; 2078 std.mem.writeInt(u32, bytes[0..4], case.width, .big); 2079 std.mem.writeInt(u32, bytes[4..8], case.height, .big); 2080 var reader = std.Io.Reader.fixed(&bytes); 2081 try std.testing.expectEqual(case.expected, try takePtySize(&reader)); 2082 } 2083 } 2084 2085 test "channel close before shell or exec is acknowledged" { 2086 const io = std.testing.io; 2087 const allocator = std.testing.allocator; 2088 const key = [_]u8{0x5a} ** 64; 2089 var encoded: [256]u8 = undefined; 2090 var encoded_writer = std.Io.Writer.fixed(&encoded); 2091 var sender = Cipher.init(&key, 0); 2092 var open: std.ArrayList(u8) = .empty; 2093 defer open.deinit(allocator); 2094 try open.append(allocator, SSH_MSG_CHANNEL_OPEN); 2095 try writeStringField(&open, allocator, "session"); 2096 try writeU32(&open, allocator, 7); 2097 try writeU32(&open, allocator, 32768); 2098 try writeU32(&open, allocator, 32768); 2099 try sender.writePacket(io, &encoded_writer, open.items); 2100 try sender.writePacket(io, &encoded_writer, &.{ SSH_MSG_CHANNEL_CLOSE, 0, 0, 0, 0 }); 2101 var reader = std.Io.Reader.fixed(encoded_writer.buffered()); 2102 var output: [256]u8 = undefined; 2103 var writer = std.Io.Writer.fixed(&output); 2104 var conn = Conn{ 2105 .io = io, 2106 .allocator = allocator, 2107 .reader = &reader, 2108 .writer = &writer, 2109 .cs_cipher = Cipher.init(&key, 0), 2110 .sc_cipher = Cipher.init(&key, 0), 2111 .rekey = undefined, 2112 }; 2113 const Handler = struct { 2114 pub fn handleSession(_: *@This(), _: *SessionCtx, _: Request) !void { 2115 return error.UnexpectedSession; 2116 } 2117 }; 2118 var handler = Handler{}; 2119 try runChannelLayer(&conn, &([_]u8{0} ** fingerprint_len), &handler); 2120 var output_reader = std.Io.Reader.fixed(writer.buffered()); 2121 var receiver = Cipher.init(&key, 0); 2122 const confirmation = try receiver.readPacket(allocator, &output_reader); 2123 defer allocator.free(confirmation); 2124 try std.testing.expectEqual(SSH_MSG_CHANNEL_OPEN_CONFIRMATION, confirmation[0]); 2125 const close = try receiver.readPacket(allocator, &output_reader); 2126 defer allocator.free(close); 2127 try std.testing.expectEqualSlices(u8, &.{ SSH_MSG_CHANNEL_CLOSE, 0, 0, 0, 7 }, close); 2128 try std.testing.expectEqual(0, output_reader.buffered().len); 2129 } 2130