From 2e9ace2229afce4f81cc335a2a9baed2c745913a Mon Sep 17 00:00:00 2001 From: Michael Lohr Date: Sat, 30 Dec 2023 20:12:50 +0100 Subject: [PATCH] feat: Complete telnet client --- src/client.zig | 225 +++++++++++++++++++++++++++++++++++-------------- src/main.zig | 32 ++++++- src/opts.zig | 5 ++ src/telnet.zig | 40 ++++++--- 4 files changed, 223 insertions(+), 79 deletions(-) diff --git a/src/client.zig b/src/client.zig index ace263b..08fb106 100644 --- a/src/client.zig +++ b/src/client.zig @@ -12,12 +12,14 @@ const State = enum { normal, iac, negotiating, + subnegotiating, }; const StateInfo = union(State) { normal: void, iac: void, negotiating: telnet.Command, + subnegotiating: telnet.Option, }; pub const TelnetClient = struct { @@ -35,146 +37,239 @@ pub const TelnetClient = struct { }; } + pub fn write(self: *TelnetClient, data: []u8) anyerror!void { + std.log.debug("Writing {d} bytes", .{data.len}); + + // TODO: escape IAC bytes + try self.writer.writeAll(data); + } + pub fn read(self: *TelnetClient) anyerror!void { const byte = try self.reader.readByte(); switch (self.state) { + + // Normal state: print characters and wait for IAC byte .normal => { if (byte == telnet.IAC_BYTE) { - print("Server ({s}): IAC ", .{@tagName(self.state)}); self.state = .iac; } else { print("{c}", .{byte}); } }, + + // Command state: determine command and set negotiating state .iac => { var cmd: telnet.Command = @enumFromInt(byte); switch (cmd) { - .se => { - print("SE \n", .{}); - self.state = .normal; - }, .nop => { - print("NOP \n", .{}); - self.state = .normal; - }, - .dm => { - print("DM \n", .{}); + // Do nothing + std.log.debug("Recieved NOP", .{}); self.state = .normal; }, - .brk => { - print("BRK \n", .{}); + .iac => { + // Escaped IAC byte + print("{c}", .{telnet.IAC_BYTE}); self.state = .normal; }, - .ip => { - print("IP \n", .{}); - self.state = .normal; - }, - .ao => { - print("AO \n", .{}); - self.state = .normal; - }, - .ayt => { - print("AYT \n", .{}); - self.state = .normal; + .will, .wont, .do, .dont, .sb => { + self.state = StateInfo{ .negotiating = cmd }; }, - .ec => { - print("EC \n", .{}); - self.state = .normal; - }, - .el => { - print("EL \n", .{}); - self.state = .normal; - }, - .ga => { - print("GA \n", .{}); + .se => { + // Subnegotiation end self.state = .normal; }, - .sb => { - print("SB \n", .{}); + else => { + std.log.warn("Unhandled command: {s} (state: {s})", .{ @tagName(cmd), @tagName(self.state) }); self.state = .normal; }, - .will => { - print("WILL ", .{}); - self.state = StateInfo{ .negotiating = .will }; - }, - .wont => { - print("WONT ", .{}); - self.state = StateInfo{ .negotiating = .wont }; - }, - .do => { - print("DO ", .{}); - self.state = StateInfo{ .negotiating = .do }; - }, - .dont => { - print("DONT ", .{}); - self.state = StateInfo{ .negotiating = .dont }; - }, } }, + + // Negotiating state: determine option and send response .negotiating => |command| { - const opt: telnet.Option = @enumFromInt(byte); - print("{s}\n", .{@tagName(opt)}); + const option: telnet.Option = @enumFromInt(byte); + std.log.debug("S: {s} {s}", .{ @tagName(command), @tagName(option) }); - switch (opt) { + switch (option) { .echo => { + // https://datatracker.ietf.org/doc/html/rfc857 switch (command) { .will => { + std.log.debug("Server wants to echo, we allow him", .{}); try self.send(.do, .echo); }, .wont => { + std.log.debug("Server does not want to echo, we are fine with that", .{}); try self.send(.dont, .echo); }, - else => {}, + .do => { + std.log.debug("Server asks us to echo, we decline", .{}); + try self.send(.wont, .echo); + }, + .dont => { + std.log.debug("Server asks us not to echo, we won't", .{}); + try self.send(.wont, .echo); + }, + else => { + std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) }); + }, } + self.state = .normal; }, .suppressGoAhead => { + // https://datatracker.ietf.org/doc/html/rfc858 switch (command) { .will => { + std.log.debug("Server wants to suppress go ahead, we allow him", .{}); try self.send(.do, .suppressGoAhead); }, .wont => { - try self.send(.dont, .suppressGoAhead); + std.log.warn("Server refused to suppress go ahead", .{}); + try self.send(.do, .suppressGoAhead); + }, + .do => { + std.log.debug("Server asks us to suppress go ahead, we accept", .{}); + try self.send(.will, .suppressGoAhead); + }, + .dont => { + std.log.warn("Server asks us not to suppress go ahead, we won't", .{}); + try self.send(.wont, .suppressGoAhead); + }, + else => { + std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) }); }, - else => {}, } + self.state = .normal; }, .negotiateAboutWindowSize => { // https://datatracker.ietf.org/doc/html/rfc1073 switch (command) { .do => { + std.log.debug("Server wants to negotiate about window size, we send the info", .{}); try self.send(.will, .negotiateAboutWindowSize); - try self.writer.writeAll(&[_]u8{ telnet.IAC_BYTE, telnet.SB_BYTE, @intFromEnum(Option.negotiateAboutWindowSize), 0, 80, 0, 24, telnet.IAC_BYTE, telnet.SE_BYTE }); + + // TODO: get the correct width and height from the terminal + const windowSizeData = &[_]u8{ + 0, 80, // Width + 0, 24, // Height + }; + const negotiation = &telnet.subnegotiate(Option.negotiateAboutWindowSize, windowSizeData); + try self.writer.writeAll(negotiation); }, .dont => { + std.log.debug("Server does not want to negotiate about window size, we accept", .{}); try self.send(.wont, .negotiateAboutWindowSize); }, - else => {}, + else => { + std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) }); + }, } + self.state = .normal; }, .terminalType => { // https://datatracker.ietf.org/doc/html/rfc1091 switch (command) { .do => { - try self.send(.wont, .terminalType); - try self.writer.writeAll(&[_]u8{ telnet.IAC_BYTE, telnet.SB_BYTE, @intFromEnum(Option.terminalType), 0, 0, 0, 0, 0, 0, 0, 0, 0, telnet.IAC_BYTE, telnet.SE_BYTE }); + std.log.debug("Server wants to ask us for our terminal type, we agree", .{}); + try self.send(.will, .terminalType); + + self.state = .normal; }, .dont => { + std.log.debug("Server does not want to know our terminal type", .{}); try self.send(.wont, .terminalType); + + self.state = .normal; + }, + .sb => { + std.log.debug("Server wants to know our terminal type...", .{}); + + self.state = StateInfo{ + .subnegotiating = Option.terminalType, + }; + }, + else => { + std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) }); + }, + } + }, + .transmitBinary => { + // https://datatracker.ietf.org/doc/html/rfc856 + switch (command) { + .do => { + std.log.debug("Server wants to transmit binary, we agree", .{}); + try self.send(.will, .transmitBinary); + }, + .dont => { + std.log.debug("Server does not want to transmit binary, we agree", .{}); + try self.send(.wont, .transmitBinary); + }, + .will => { + std.log.debug("Server wants us to transmit binary, we agree", .{}); + try self.send(.do, .transmitBinary); + }, + .wont => { + std.log.debug("Server does not want us to transmit binary, we agree", .{}); + try self.send(.dont, .transmitBinary); + }, + else => { + std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) }); }, - else => {}, } }, - else => {}, + else => { + switch (command) { + .do => { + std.log.debug("Server wants us to perform subcommand `{s}`, we refuse", .{@tagName(option)}); + try self.send(.wont, option); + }, + .dont => { + std.log.debug("Server does not want us to perform subcommand `{s}`, we refuse", .{@tagName(option)}); + }, + .will, .wont => { + std.log.warn("Server wants to negotiate option `{s}`", .{@tagName(option)}); + }, + else => { + std.log.warn("Unsupported negotiation command `{s}` for option `{s}` (state: {s})", .{ @tagName(command), @tagName(option), @tagName(self.state) }); + }, + } + self.state = .normal; + }, } + }, - self.state = .normal; + // Subnegotiating state: determine option and read until IAC SE + .subnegotiating => |option| { + std.log.debug("Subnegotiating option `{s}`", .{@tagName(option)}); + switch (option) { + .terminalType => { + if (byte == telnet.SEND_BYTE) { + std.log.debug("Send terminal type", .{}); + const terminalTypeData: []const u8 = &[_]u8{ + telnet.IS_BYTE, // Is + 'X', 'T', 'E', 'R', 'M', '-', '2', '5', '6', 'C', 'O', 'L', 'O', 'R', // Terminal type (`XTERM-256COLOR` is what the inetutils implementation sends) + }; + const negotiation: []const u8 = &telnet.subnegotiate(Option.terminalType, terminalTypeData); + try self.writer.writeAll(negotiation); + + self.state = .normal; + } else { + std.log.warn("Unsupported data byte {d} during subnegotiation option `{c}` (state: {s}),", .{ byte, @tagName(option), @tagName(self.state) }); + self.state = .normal; + } + }, + else => { + std.log.warn("Unsupported subnegotiation option `{s}` (state: {s})", .{ @tagName(option), @tagName(self.state) }); + self.state = .normal; + }, + } }, } } fn send(self: *TelnetClient, command: Command, option: Option) anyerror!void { - print("Client ({s}): IAC {s} {s}\n", .{ @tagName(self.state), @tagName(command), @tagName(option) }); + std.log.debug("C: {s} {s}", .{ @tagName(command), @tagName(option) }); try self.writer.writeAll(&telnet.instruction(command, option)); } }; diff --git a/src/main.zig b/src/main.zig index 2015bca..4398a79 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,11 +6,9 @@ const client = @import("client.zig"); const opts = @import("opts.zig"); const clap = @import("clap"); -const Errors = error{ - MissingArgument, - InvalidUri, +pub const std_options = struct { + pub const log_level = .debug; // Set this to `.warn` to disable all debug info }; - pub fn main() !void { // Setup allocator var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -22,13 +20,39 @@ pub fn main() !void { const parsedArgs = try opts.parse(alloc); if (parsedArgs) |args| { defer args.deinit(); + + // Commenct to the server std.log.info("Connecting to {?s}:{?d}\n", .{ args.uri.host, args.uri.port }); const stream = try net.tcpConnectToHost(alloc, args.uri.host.?, args.uri.port.?); defer stream.close(); var tnClient = client.TelnetClient.init(stream); + + // Start the input thread + std.log.info("Press CTL-C to exit.", .{}); + const handle = try std.Thread.spawn(.{}, readInput, .{&tnClient}); + handle.detach(); + while (true) { try tnClient.read(); } } } + +fn readInput(tnClient: *client.TelnetClient) !void { + // Read from stdin and write to the telnet client + + // Sadly, this does not work, since data is only read once enter is pressed + // Normaly, telnet would send a key press as soon as it is pressed + // But for this to work, we would need to use a terminal library + // Also our enter would have to be translated from \n to \r\n + + const stdin = std.io.getStdIn().reader(); + while (true) { + var buf: [64]u8 = undefined; + const len = try stdin.read(&buf); + if (len > 0) { + try tnClient.write(buf[0..len]); + } + } +} diff --git a/src/opts.zig b/src/opts.zig index 9b311ac..2440c88 100644 --- a/src/opts.zig +++ b/src/opts.zig @@ -4,6 +4,11 @@ const utils = @import("utils.zig"); const telnet = @import("telnet.zig"); const io = std.io; +const OptErrors = error{ + MissingArgument, + InvalidUri, +}; + pub const Opts = struct { alloc: std.mem.Allocator, uriStr: []const u8, diff --git a/src/telnet.zig b/src/telnet.zig index 5cb6938..a8c7770 100644 --- a/src/telnet.zig +++ b/src/telnet.zig @@ -1,8 +1,12 @@ +const std = @import("std"); + +// Default port for the application pub const DEFAULT_PORT: u16 = 23; -pub const IAC_BYTE: u8 = 255; -pub const SB_BYTE: u8 = 250; -pub const SE_BYTE: u8 = 240; +// Constant byte values for Telnet protocol +pub const IAC_BYTE: u8 = 255; // "Interpret as Command" byte +pub const IS_BYTE: u8 = 0; // "IS" byte +pub const SEND_BYTE: u8 = 1; // "SEND" byte pub const Command = enum(u8) { se = 240, // End of subnegotiation parameters @@ -15,18 +19,19 @@ pub const Command = enum(u8) { ec = 247, // Erase character el = 248, // Erase line ga = 249, // Go ahead - sb = 250, // Subnegotiation of the indicated option - will = 251, // Indicates the desire to begin performing, or confirmation that you are now performing, the indicated option - wont = 252, // Indicates the refusal to perform, or continue performing, the indicated option - do = 253, // Indicates the request that the other party perform, or confirmation that you are expecting the other party to perform, the indicated option - dont = 254, // Indicates the demand that the other party stop performing, or confirmation that you are no longer expecting the other party to perform, the indicated option + sb = 250, // Start of subnegotiation of the indicated option + will = 251, // Willing to begin performing the indicated option + wont = 252, // Refusing to perform the indicated option + do = 253, // Request to perform the indicated option + dont = 254, // Demand to stop performing the indicated option + iac = 255, // data byte 255 }; pub const Option = enum(u8) { transmitBinary = 0, // Binary Transmission (RFC 856) echo = 1, // Echo (RFC 857) reconnection = 2, // Reconnection (NIC 15391 of 1973) - suppressGoAhead = 3, // Suppress Go Ahead (RFC 858): no "go ahead" signal will be sent (required for half-duplex transmissions) -> full-duplex + suppressGoAhead = 3, // Suppress Go Ahead (RFC 858): No "go ahead" signal will be sent (required for half-duplex transmissions) -> full-duplex approxMessageSizeNegotiation = 4, // Approx Message Size Negotiation (NIC 15393 of 1973) status = 5, // Status (RFC 859) timingMark = 6, // Timing Mark (RFC 860) @@ -47,7 +52,7 @@ pub const Option = enum(u8) { supdup = 21, // SUPDUP (RFC 736, RFC 734) supdupOutput = 22, // SUPDUP Output (RFC 749) sendLocation = 23, // Send Location (RFC 779) - terminalType = 24, // Terminal Type (RFC 1091): requests that the other end of the connection transmit, in ASCII format, the name of the terminal type that it is using + terminalType = 24, // Terminal Type (RFC 1091): Requests the name of the terminal type in ASCII format endOfRecord = 25, // End of Record (RFC 885) tacacsUserIdentification = 26, // TACACS User Identification (RFC 927) outputMarking = 27, // Output Marking (RFC 933) @@ -81,6 +86,21 @@ pub const Option = enum(u8) { extendedOptionsList = 255, // Extended-Options-List (RFC 861) }; +// Function to create an instruction array from a command and option pub fn instruction(command: Command, option: Option) [3]u8 { return [3]u8{ IAC_BYTE, @intFromEnum(command), @intFromEnum(option) }; } + +// Function to create an instruction array from a command and option +pub fn subnegotiate(option: Option, comptime payload: []const u8) [payload.len + 5]u8 { + var data = [_]u8{0} ** (payload.len + 5); + + data[0] = IAC_BYTE; + data[1] = @intFromEnum(Command.sb); + data[2] = @intFromEnum(option); + std.mem.copy(u8, data[3 .. 3 + payload.len], payload); + data[payload.len + 3] = IAC_BYTE; + data[payload.len + 4] = @intFromEnum(Command.se); + + return data; +}