Skip to main content

telnetd/
main.rs

1#![no_std]
2#![no_main]
3#![feature(alloc_error_handler)]
4
5extern crate alloc;
6
7use core::{alloc::Layout, panic::PanicInfo};
8use strat9_syscall::{call, data::TimeSpec, number};
9
10alloc_freelist::define_freelist_allocator!(pub struct BumpAllocator; heap_size = 128 * 1024;);
11
12#[global_allocator]
13static GLOBAL_ALLOCATOR: BumpAllocator = BumpAllocator;
14
15#[alloc_error_handler]
16/// Implements alloc error.
17fn alloc_error(_layout: Layout) -> ! {
18    log("[telnetd] OOM\n");
19    call::exit(12)
20}
21
22#[panic_handler]
23fn panic(info: &PanicInfo) -> ! {
24    call::handle_panic("telnetd", info)
25}
26
27/// Implements log.
28fn log(msg: &str) {
29    let _ = call::debug_log(msg.as_bytes());
30}
31
32/// Implements sleep ms.
33fn sleep_ms(ms: u64) {
34    let req = TimeSpec {
35        tv_sec: (ms / 1000) as i64,
36        tv_nsec: ((ms % 1000) * 1_000_000) as i64,
37    };
38    let _ = unsafe {
39        strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
40    };
41}
42
43/// Writes all. Returns false on broken pipe or after 500 retries on EAGAIN.
44fn write_all(fd: usize, data: &[u8]) -> bool {
45    let mut off = 0usize;
46    let mut retries = 0u32;
47    while off < data.len() {
48        match call::write(fd, &data[off..]) {
49            Ok(0) => return false,
50            Ok(n) => {
51                off += n;
52                retries = 0;
53            }
54            Err(e) => {
55                if e.to_errno() == 11 {
56                    retries += 1;
57                    if retries > 500 {
58                        return false;
59                    }
60                    sleep_ms(10);
61                    continue;
62                }
63                return false;
64            }
65        }
66    }
67    true
68}
69
70/// Reads text file.
71fn read_text_file(path: &str, out: &mut [u8]) -> usize {
72    let fd = match call::openat(0, path, 0x0, 0) {
73        Ok(fd) => fd,
74        Err(_) => return 0,
75    };
76    let n = call::read(fd, out).unwrap_or(0);
77    let _ = call::close(fd);
78    n
79}
80
81/// Opens listener. Returns 0 on failure after 100 retries.
82fn open_listener() -> usize {
83    let mut retries = 0u32;
84    loop {
85        match call::openat(0, "/net/tcp/listen/23", 0x2, 0) {
86            Ok(fd) => return fd,
87            Err(_) => {
88                retries += 1;
89                if retries > 100 {
90                    log("[telnetd] FATAL: cannot open listener after 100 retries\n");
91                    return 0;
92                }
93                sleep_ms(200);
94            }
95        }
96    }
97}
98
99enum LineAction {
100    Continue,
101    Disconnect,
102}
103
104#[derive(Clone, Copy, PartialEq)]
105enum IacState {
106    Normal,
107    SkipOption,
108    SubNeg,
109}
110
111struct TelnetSession {
112    connected: bool,
113    line: [u8; 256],
114    line_len: usize,
115    iac_state: IacState,
116}
117
118impl TelnetSession {
119    const fn new() -> Self {
120        Self {
121            connected: false,
122            line: [0u8; 256],
123            line_len: 0,
124            iac_state: IacState::Normal,
125        }
126    }
127
128    fn reset(&mut self) {
129        self.connected = false;
130        self.line_len = 0;
131        self.iac_state = IacState::Normal;
132    }
133}
134
135/// Implements send prompt.
136fn send_prompt(fd: usize) {
137    let _ = write_all(fd, b"\r\nstrat9> ");
138}
139
140/// Implements handle command.
141fn handle_command(fd: usize, line: &str) -> LineAction {
142    let cmd = line.trim();
143    if cmd.is_empty() {
144        send_prompt(fd);
145        return LineAction::Continue;
146    }
147
148    if cmd == "help" {
149        let _ = write_all(
150            fd,
151            b"\r\nCommands: help, ip, net, echo <text>, clear, quit\r\n",
152        );
153        send_prompt(fd);
154        return LineAction::Continue;
155    }
156
157    if cmd == "ip" {
158        let mut buf = [0u8; 128];
159        let n = read_text_file("/net/address", &mut buf);
160        let _ = write_all(fd, b"\r\nIP: ");
161        if n > 0 {
162            let _ = write_all(fd, &buf[..n]);
163            let _ = write_all(fd, b"\r\n");
164        } else {
165            let _ = write_all(fd, b"n/a\r\n");
166        }
167        send_prompt(fd);
168        return LineAction::Continue;
169    }
170
171    if cmd == "net" {
172        let mut ip = [0u8; 128];
173        let mut gw = [0u8; 128];
174        let mut dns = [0u8; 128];
175        let mut route = [0u8; 128];
176        let nip = read_text_file("/net/ip", &mut ip);
177        let ngw = read_text_file("/net/gateway", &mut gw);
178        let ndns = read_text_file("/net/dns", &mut dns);
179        let nr = read_text_file("/net/route", &mut route);
180
181        let _ = write_all(fd, b"\r\nIP: ");
182        if nip > 0 {
183            let _ = write_all(fd, &ip[..nip]);
184            let _ = write_all(fd, b"\r\n");
185        } else {
186            let _ = write_all(fd, b"n/a\r\n");
187        }
188        let _ = write_all(fd, b"GW: ");
189        if ngw > 0 {
190            let _ = write_all(fd, &gw[..ngw]);
191            let _ = write_all(fd, b"\r\n");
192        } else {
193            let _ = write_all(fd, b"n/a\r\n");
194        }
195        let _ = write_all(fd, b"DNS: ");
196        if ndns > 0 {
197            let _ = write_all(fd, &dns[..ndns]);
198            let _ = write_all(fd, b"\r\n");
199        } else {
200            let _ = write_all(fd, b"n/a\r\n");
201        }
202        let _ = write_all(fd, b"ROUTE: ");
203        if nr > 0 {
204            let _ = write_all(fd, &route[..nr]);
205            let _ = write_all(fd, b"\r\n");
206        } else {
207            let _ = write_all(fd, b"n/a\r\n");
208        }
209        send_prompt(fd);
210        return LineAction::Continue;
211    }
212
213    if let Some(rest) = cmd.strip_prefix("echo ") {
214        let _ = write_all(fd, b"\r\n");
215        let _ = write_all(fd, rest.as_bytes());
216        let _ = write_all(fd, b"\r\n");
217        send_prompt(fd);
218        return LineAction::Continue;
219    }
220
221    if cmd == "clear" {
222        let _ = write_all(fd, b"\x1b[2J\x1b[H");
223        send_prompt(fd);
224        return LineAction::Continue;
225    }
226
227    if cmd == "quit" || cmd == "exit" {
228        let _ = write_all(fd, b"\r\nBye.\r\n");
229        return LineAction::Disconnect;
230    }
231
232    let _ = write_all(fd, b"\r\nUnknown command. Type 'help'.\r\n");
233    send_prompt(fd);
234    LineAction::Continue
235}
236
237/// Implements handle bytes.
238fn handle_bytes(fd: usize, session: &mut TelnetSession, bytes: &[u8]) -> LineAction {
239    for &b in bytes {
240        match session.iac_state {
241            IacState::SkipOption => {
242                session.iac_state = IacState::Normal;
243                continue;
244            }
245            IacState::SubNeg => {
246                if b == 240 {
247                    session.iac_state = IacState::Normal;
248                }
249                continue;
250            }
251            IacState::Normal => {}
252        }
253
254        if b == 255 {
255            session.iac_state = IacState::SkipOption;
256            continue;
257        }
258        if b == b'\r' {
259            continue;
260        }
261        if b == b'\n' {
262            let line = core::str::from_utf8(&session.line[..session.line_len]).unwrap_or("");
263            session.line_len = 0;
264            let action = handle_command(fd, line);
265            if matches!(action, LineAction::Disconnect) {
266                return action;
267            }
268            continue;
269        }
270        if session.line_len < session.line.len() {
271            session.line[session.line_len] = b;
272            session.line_len += 1;
273        }
274    }
275    LineAction::Continue
276}
277
278#[unsafe(no_mangle)]
279/// Implements start.
280pub extern "C" fn _start() -> ! {
281    log("[telnetd] Starting telnet server on /net/tcp/listen/23\n");
282    let mut fd = open_listener();
283    if fd == 0 {
284        log("[telnetd] Cannot open listener, exiting\n");
285        call::exit(1);
286    }
287    let mut session = TelnetSession::new();
288    let mut buf = [0u8; 512];
289    let mut idle_ticks: u64 = 0;
290
291    loop {
292        match call::read(fd, &mut buf) {
293            Ok(0) => {
294                if session.connected {
295                    log("[telnetd] client disconnected\n");
296                    let _ = call::close(fd);
297                    session.reset();
298                    fd = open_listener();
299                    if fd == 0 {
300                        call::exit(1);
301                    }
302                    idle_ticks = 0;
303                } else {
304                    sleep_ms(20);
305                    idle_ticks += 1;
306                    if idle_ticks > 750 {
307                        log("[telnetd] idle timeout on listener, reopening\n");
308                        let _ = call::close(fd);
309                        fd = open_listener();
310                        if fd == 0 {
311                            call::exit(1);
312                        }
313                        idle_ticks = 0;
314                    }
315                }
316            }
317            Ok(n) => {
318                idle_ticks = 0;
319                if !session.connected {
320                    session.connected = true;
321                    log("[telnetd] client connected\n");
322                    let _ = write_all(fd, b"\r\nStrat9 Telnet\r\nType 'help' for commands.\r\n");
323                    send_prompt(fd);
324                }
325                if matches!(
326                    handle_bytes(fd, &mut session, &buf[..n]),
327                    LineAction::Disconnect
328                ) {
329                    log("[telnetd] client quit\n");
330                    let _ = call::close(fd);
331                    session.reset();
332                    fd = open_listener();
333                    if fd == 0 {
334                        call::exit(1);
335                    }
336                    idle_ticks = 0;
337                }
338            }
339            Err(e) => {
340                if e.to_errno() == 11 {
341                    sleep_ms(10);
342                    idle_ticks += 1;
343                    if session.connected && idle_ticks > 1500 {
344                        log("[telnetd] client idle timeout, disconnecting\n");
345                        let _ = call::close(fd);
346                        session.reset();
347                        fd = open_listener();
348                        if fd == 0 {
349                            call::exit(1);
350                        }
351                        idle_ticks = 0;
352                    }
353                    continue;
354                }
355                log("[telnetd] read error, reconnecting\n");
356                let _ = call::close(fd);
357                session.reset();
358                sleep_ms(100);
359                fd = open_listener();
360                if fd == 0 {
361                    call::exit(1);
362                }
363                idle_ticks = 0;
364            }
365        }
366    }
367}