~sircmpwn/hare-ssh

48ad3ad136bb49eabe4aa8537e53a0daa85e4be1 — Drew DeVault a month ago 0b95f7d
net::ssh: packet decoding & state machine
5 files changed, 328 insertions(+), 2 deletions(-)

M cmd/ssh/main.ha
M net/ssh/client.ha
M net/ssh/error.ha
A net/ssh/packet.ha
A net/ssh/proto.ha
M cmd/ssh/main.ha => cmd/ssh/main.ha +53 -1
@@ 1,14 1,66 @@
use errors;
use fmt;
use io;
use net::dial;
use net::ssh;
use os;

export fn main() void = {
	const conn = dial::dial("tcp", os::args[1], "ssh")!;
	defer io::close(conn);
	defer io::close(conn)!;
	const client = ssh::newclient(conn)!;
	defer ssh::client_finish(&client);

	const version = ssh::client_verexch(&client)!;
	fmt::println(version)!;

	for (true) {
		const pkt = match (ssh::client_read(&client)) {
		case errors::again =>
			continue;
		case let pkt: ssh::packet =>
			yield pkt;
		case let err: ssh::error =>
			fmt::fatal("SSH error", ssh::strerror(err));
		};
		defer ssh::packet_finish(&pkt);

		printpkt(&pkt);
	};
};

fn printpkt(pkt: *ssh::packet) void = {
	match (*pkt) {
	case let kex: ssh::kexinit =>
		fmt::println("kexinit")!;
		fmt::println("kex_algorithms:")!;
		printnamelist(kex.kex_algorithms);
		fmt::println("server_host_key_algorithms:")!;
		printnamelist(kex.server_host_key_algorithms);
		fmt::println("encryption_algorithms_client_to_server:")!;
		printnamelist(kex.encryption_algorithms_client_to_server);
		fmt::println("encryption_algorithms_server_to_client:")!;
		printnamelist(kex.encryption_algorithms_server_to_client);
		fmt::println("mac_algorithms_client_to_server:")!;
		printnamelist(kex.mac_algorithms_client_to_server);
		fmt::println("mac_algorithms_server_to_client:")!;
		printnamelist(kex.mac_algorithms_server_to_client);
		fmt::println("compression_algorithms_client_to_server:")!;
		printnamelist(kex.compression_algorithms_client_to_server);
		fmt::println("compression_algorithms_server_to_client:")!;
		printnamelist(kex.compression_algorithms_server_to_client);
	case =>
		abort(); // TODO
	};
};

fn printnamelist(list: []str) void = {
	fmt::print("\t")!;
	for (let i = 0z; i < len(list); i += 1) {
		fmt::print(list[i])!;
		if (i + 1 < len(list)) {
			fmt::print(", ")!;
		};
	};
	fmt::println()!;
};

M net/ssh/client.ha => net/ssh/client.ha +70 -1
@@ 1,8 1,11 @@
use bytes;
use bufio;
use endian;
use errors;
use io;
use os;
use strings;
use types;

// The maximum size of an ordinary SSH packet. Some packets may be larger. This
// is the maximum amount of memory which is dynamically allocated per client by


@@ 13,6 16,8 @@ export type client = struct {
	conn: io::handle,
	rbuf: []u8,
	rptr: size,
	mac: nullable *mac,
	seq: u32,
};

const protoversion: str = "SSH-2.0-hare::ssh-0.0\r\n";


@@ 29,7 34,9 @@ export fn newclient(conn: io::handle) (client | io::error) = {
	return client {
		conn = conn,
		rbuf = alloc([], os::BUFSIZ),
		rptr = 0z,
		rptr = 0,
		mac = null,
		seq = 0,
	};
};



@@ 103,3 110,65 @@ fn toknext(tok: *strings::tokenizer) (str | protoerror) = {
		return token;
	};
};

// Reads an SSH packet from the client connection. This function may return
// [[errors::again]], and the caller should call it again once the file
// descriptor is readable.
export fn client_read(client: *client) (packet | io::EOF | error) = {
	delete(client.rbuf[..client.rptr]);
	client.rptr = 0;
	// XXX: This function is probably useful both for clients and servers,
	// we could share it.

	let msglen = types::SIZE_MAX;
	if (len(client.rbuf) >= 5) {
		const pktlen = endian::begetu32(client.rbuf[..4]);
		const maclen = match (client.mac) {
		case null =>
			yield 0z;
		case let mac: *mac =>
			yield mac_digestsz(mac);
		};
		msglen = pktlen + maclen + 4;
	};
	if (msglen <= len(client.rbuf)) {
		return client_decode(client);
	};

	let buf: [os::BUFSIZ]u8 = [0...];
	const z = match (io::read(client.conn, buf)?) {
	case io::EOF =>
		return io::EOF;
	case let z: size =>
		yield z;
	};
	append(client.rbuf, buf[..z]...);

	// XXX: We could do some kind of streaming decoder for large packets,
	// but it might be a bit of a challenge.
	if (len(client.rbuf) >= MAX_PACKETSIZE) {
		return protoerror;
	};
	// TODO: We could try to decode the packet right here if we already have
	// enough
	return errors::again;
};

fn client_decode(client: *client) (packet | error) = {
	assert(len(client.rbuf) >= 5);
	const pktlen = endian::begetu32(client.rbuf[..4]);
	const padlen = client.rbuf[4];
	const maclen = match (client.mac) {
	case null =>
		yield 0z;
	case let mac: *mac =>
		yield mac_digestsz(mac);
	};
	const msglen = pktlen + maclen + 4;

	// TODO: Verify MAC
	const reader = bufio::fixed(client.rbuf[5..pktlen-6], io::mode::READ);
	const pkt = decode(&reader)?;
	client.rptr = msglen;
	return pkt;
};

M net/ssh/error.ha => net/ssh/error.ha +18 -0
@@ 14,3 14,21 @@ export type unsupported = !void;
// All possible errors returned by this module.
export type error = !(io::error | net::error | errors::again |
	versionmismatch | protoerror | unsupported);

// Converts an [[error]] into a human-friendly string.
export fn strerror(err: error) const str = {
	match (err) {
	case let err: io::error =>
		return io::strerror(err);
	case let err: net::error =>
		return net::strerror(err);
	case errors::again =>
		return "Resource temporarily unavailable";
	case versionmismatch =>
		return "Protocol version mismatch";
	case protoerror =>
		return "Protocol error";
	case unsupported =>
		return "Unsupported feature";
	};
};

A net/ssh/packet.ha => net/ssh/packet.ha +110 -0
@@ 0,0 1,110 @@
use bufio;
use io;

// An SSH packet.
export type packet = (kexinit | newkeys); // TODO: other packets

// An SSH_MSG_KEXINIT packet.
export type kexinit = struct {
	cookie: [16]u8,
	kex_algorithms: []str,
	server_host_key_algorithms: []str,
	encryption_algorithms_client_to_server: []str,
	encryption_algorithms_server_to_client: []str,
	mac_algorithms_client_to_server: []str,
	mac_algorithms_server_to_client: []str,
	compression_algorithms_client_to_server: []str,
	compression_algorithms_server_to_client: []str,
	languages_client_to_server: []str,
	languages_server_to_client: []str,
	first_kex_packet_follows: bool,
	reserved: u32,
};

export type disconnect_reason = enum u32 {
	HOST_NOT_ALLOWED_TO_CONNECT = 1,
	PROTOCOL_ERROR = 2,
	KEY_EXCHANGE_FAILED = 3,
	RESERVED = 4,
	MAC_ERROR = 5,
	COMPRESSION_ERROR = 6,
	SERVICE_NOT_AVAILABLE = 7,
	PROTOCOL_VERSION_NOT_SUPPORTED = 8,
	HOST_KEY_NOT_VERIFIABLE = 9,
	CONNECTION_LOST = 10,
	BY_APPLICATION = 11,
	TOO_MANY_CONNECTIONS = 12,
	AUTH_CANCELLED_BY_USER = 13,
	NO_MORE_AUTH_METHODS_AVAILABLE = 14,
	ILLEGAL_USER_NAME = 15,
};

// An SSH_MSG_DISCONNECT packet.
export type disconnect = struct {
	reason: disconnect_reason,
	desc: str,
	lang: str,
};

// An SSH_MSG_NEWKEYS packet.
export type newkeys = void;

// Decodes an SSH packet.
fn decode(src: io::handle) (packet | protoerror) = {
	switch (readbyte(src)?) {
	case SSH_MSG_KEXINIT =>
		return kexinit_decode(src);
	case SSH_MSG_NEWKEYS =>
		return newkeys;
	case SSH_MSG_DISCONNECT =>
		return disconnect_decode(src);
	case =>
		return protoerror;
	};
};

fn kexinit_decode(src: io::handle) (packet | protoerror) = {
	let pkt = kexinit { ... };
	match (io::readall(src, pkt.cookie)!) {
	case io::EOF =>
		return protoerror;
	case => void;
	};
	pkt.kex_algorithms = readnamelist(src)?;
	pkt.server_host_key_algorithms = readnamelist(src)?;
	pkt.encryption_algorithms_client_to_server = readnamelist(src)?;
	pkt.encryption_algorithms_server_to_client = readnamelist(src)?;
	pkt.mac_algorithms_client_to_server = readnamelist(src)?;
	pkt.mac_algorithms_server_to_client = readnamelist(src)?;
	pkt.compression_algorithms_client_to_server = readnamelist(src)?;
	pkt.compression_algorithms_server_to_client = readnamelist(src)?;
	pkt.languages_client_to_server = readnamelist(src)?;
	pkt.languages_server_to_client = readnamelist(src)?;
	pkt.first_kex_packet_follows = readbool(src)?;
	pkt.reserved = readu32(src)?;
	return pkt;
};

fn disconnect_decode(src: io::handle) (packet | protoerror) = {
	abort(); // TODO
};

// Frees resources associated with a packet.
export fn packet_finish(pkt: *packet) void = {
	// XXX: Match overhaul
	match (*pkt) {
	case let kex: kexinit =>
		free(kex.kex_algorithms);
		free(kex.server_host_key_algorithms);
		free(kex.encryption_algorithms_client_to_server);
		free(kex.encryption_algorithms_server_to_client);
		free(kex.mac_algorithms_client_to_server);
		free(kex.mac_algorithms_server_to_client);
		free(kex.compression_algorithms_client_to_server);
		free(kex.compression_algorithms_server_to_client);
		free(kex.languages_client_to_server);
		free(kex.languages_server_to_client);
	case =>
		yield;
	};
};

A net/ssh/proto.ha => net/ssh/proto.ha +77 -0
@@ 0,0 1,77 @@
use bufio;
use endian;
use io;
use strings;

fn readbyte(src: io::handle) (u8 | protoerror) = {
	match (bufio::scanbyte(src)!) {
	case let u: u8 =>
		return u;
	case io::EOF =>
		return protoerror;
	};
};

fn readbool(src: io::handle) (bool | protoerror) = {
	return readbyte(src)? != 0;
};

fn readu32(src: io::handle) (u32 | protoerror) = {
	let buf: [4]u8 = [0...];
	match (io::readall(src, buf)!) {
	case io::EOF =>
		return protoerror;
	case size =>
		yield;
	};
	return endian::begetu32(buf);
};

// Reads a slice from the stream. The caller must free the return value.
fn readslice(src: io::handle) ([]u8 | protoerror) = {
	// XXX: Would be quite nice to borrow the return value from the input
	const length = readu32(src)?;
	if (length > MAX_PACKETSIZE) {
		return protoerror;
	};
	if (length == 0) {
		return [];
	};
	let slice: []u8 = alloc([0...], length);
	match (io::readall(src, slice)!) {
	case io::EOF =>
		free(slice);
		return protoerror;
	case let z: size =>
		return slice;
	};
};

// Reads a string from the stream. The caller must free the return value.
fn readstr(src: io::handle) (str | protoerror) = {
	const slice = readslice(src)?;
	match (strings::try_fromutf8(slice)) {
	case let s: str =>
		return s;
	case =>
		free(slice);
		return protoerror;
	};
};

// XXX: Freeing the return value here is annoying, should borrow strings from
// the input
fn readnamelist(src: io::handle) ([]str | protoerror) = {
	const list = readstr(src)?;
	const tok = strings::tokenize(list, ",");
	let items: []str = [];
	for (true) {
		match (strings::next_token(&tok)) {
		case void =>
			break;
		case let item: str =>
			append(items, item);
		};
	};
	return items;
};