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, flag, 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]
16fn alloc_error(_layout: Layout) -> ! {
17    log("[telnetd] OOM\n");
18    call::exit(12)
19}
20
21#[panic_handler]
22fn panic(info: &PanicInfo) -> ! {
23    call::handle_panic("telnetd", info)
24}
25
26const EAGAIN: usize = 11;
27const ECONNRESET: usize = 104;
28const EADDRINUSE: usize = 98;
29const TELNET_PORT_PATH: &str = "/net/tcp/listen/23";
30const LISTENERS_PATH: &str = "/net/tcp/listeners";
31const IP_PATH: &str = "/net/ip";
32
33fn log(msg: &str) {
34    let _ = call::debug_log(msg.as_bytes());
35}
36
37fn sleep_ms(ms: u64) {
38    let req = TimeSpec {
39        tv_sec: (ms / 1000) as i64,
40        tv_nsec: ((ms % 1000) * 1_000_000) as i64,
41    };
42    let _ = unsafe {
43        strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
44    };
45}
46
47fn write_all(fd: usize, data: &[u8]) -> bool {
48    let mut off = 0usize;
49    let mut retries = 0u32;
50    while off < data.len() {
51        match call::write(fd, &data[off..]) {
52            Ok(0) => return false,
53            Ok(n) => {
54                off += n;
55                retries = 0;
56            }
57            Err(e) => {
58                if e.to_errno() == EAGAIN {
59                    retries += 1;
60                    if retries > 500 {
61                        return false;
62                    }
63                    sleep_ms(10);
64                    continue;
65                }
66                return false;
67            }
68        }
69    }
70    true
71}
72
73fn read_text_file(path: &str, out: &mut [u8]) -> usize {
74    let fd = match call::openat(
75        0,
76        path,
77        flag::OpenFlags::RDONLY.bits() as usize,
78        0,
79    ) {
80        Ok(fd) => fd,
81        Err(_) => return 0,
82    };
83    let n = call::read(fd, out).unwrap_or(0);
84    let _ = call::close(fd);
85    n
86}
87
88fn network_configured() -> bool {
89    let mut buf = [0u8; 64];
90    let n = read_text_file(IP_PATH, &mut buf);
91    if n == 0 {
92        return false;
93    }
94    let s = core::str::from_utf8(&buf[..n]).unwrap_or("").trim();
95    if s.is_empty() {
96        return false;
97    }
98    if s.starts_with("0.0.0.0") || s.starts_with("169.254.") || s == "(unavailable)" {
99        return false;
100    }
101    true
102}
103
104fn wait_for_network() {
105    log("[telnetd] waiting for /net IP configuration\n");
106    let mut retries = 0u32;
107    loop {
108        if network_configured() {
109            log("[telnetd] network ready\n");
110            return;
111        }
112        retries += 1;
113        if retries % 25 == 0 {
114            log("[telnetd] still waiting for DHCP/static IPv4...\n");
115        }
116        sleep_ms(200);
117    }
118}
119
120fn listener_established() -> bool {
121    let mut buf = [0u8; 512];
122    let n = read_text_file(LISTENERS_PATH, &mut buf);
123    if n == 0 {
124        return false;
125    }
126    let s = core::str::from_utf8(&buf[..n]).unwrap_or("");
127    for line in s.lines() {
128        let has_port = line.contains("port=23");
129        let has_est = line.contains("state=ESTABLISHED");
130        if has_port && has_est {
131            return true;
132        }
133    }
134    false
135}
136
137fn open_listener() -> Option<usize> {
138    let mut retries = 0u32;
139    loop {
140        match call::openat(
141            0,
142            TELNET_PORT_PATH,
143            flag::OpenFlags::RDWR.bits() as usize,
144            0,
145        ) {
146            Ok(fd) => return Some(fd),
147            Err(e) => {
148                let err = e.to_errno();
149                if err == EADDRINUSE {
150                    log("[telnetd] port 23 busy, retrying\n");
151                } else if retries == 0 {
152                    log("[telnetd] /net/tcp/listen not ready yet\n");
153                }
154                retries += 1;
155                if retries > 200 {
156                    log("[telnetd] FATAL: cannot open /net/tcp/listen/23\n");
157                    return None;
158                }
159                sleep_ms(200);
160            }
161        }
162    }
163}
164
165enum LineAction {
166    Continue,
167    Disconnect,
168}
169
170#[derive(Clone, Copy, PartialEq)]
171enum IacState {
172    Normal,
173    SkipOption,
174    SubNeg,
175}
176
177struct TelnetSession {
178    connected: bool,
179    line: [u8; 256],
180    line_len: usize,
181    iac_state: IacState,
182}
183
184impl TelnetSession {
185    const fn new() -> Self {
186        Self {
187            connected: false,
188            line: [0u8; 256],
189            line_len: 0,
190            iac_state: IacState::Normal,
191        }
192    }
193
194    fn reset(&mut self) {
195        self.connected = false;
196        self.line_len = 0;
197        self.iac_state = IacState::Normal;
198    }
199}
200
201fn send_prompt(fd: usize) {
202    let _ = write_all(fd, b"\r\nstrat9> ");
203}
204
205fn send_banner(fd: usize) {
206    let _ = write_all(fd, b"\r\nStrat9 Telnet\r\nType 'help' for commands.\r\n");
207    send_prompt(fd);
208}
209
210fn handle_command(fd: usize, line: &str) -> LineAction {
211    let cmd = line.trim();
212    if cmd.is_empty() {
213        send_prompt(fd);
214        return LineAction::Continue;
215    }
216
217    if cmd == "help" {
218        let _ = write_all(
219            fd,
220            b"\r\nCommands: help, ip, net, echo <text>, clear, quit\r\n",
221        );
222        send_prompt(fd);
223        return LineAction::Continue;
224    }
225
226    if cmd == "ip" {
227        let mut buf = [0u8; 128];
228        let n = read_text_file("/net/address", &mut buf);
229        let _ = write_all(fd, b"\r\nIP: ");
230        if n > 0 {
231            let _ = write_all(fd, &buf[..n]);
232            let _ = write_all(fd, b"\r\n");
233        } else {
234            let _ = write_all(fd, b"n/a\r\n");
235        }
236        send_prompt(fd);
237        return LineAction::Continue;
238    }
239
240    if cmd == "net" {
241        let mut ip = [0u8; 128];
242        let mut gw = [0u8; 128];
243        let mut dns = [0u8; 128];
244        let mut route = [0u8; 128];
245        let nip = read_text_file("/net/ip", &mut ip);
246        let ngw = read_text_file("/net/gateway", &mut gw);
247        let ndns = read_text_file("/net/dns", &mut dns);
248        let nr = read_text_file("/net/route", &mut route);
249
250        let _ = write_all(fd, b"\r\nIP: ");
251        if nip > 0 {
252            let _ = write_all(fd, &ip[..nip]);
253            let _ = write_all(fd, b"\r\n");
254        } else {
255            let _ = write_all(fd, b"n/a\r\n");
256        }
257        let _ = write_all(fd, b"GW: ");
258        if ngw > 0 {
259            let _ = write_all(fd, &gw[..ngw]);
260            let _ = write_all(fd, b"\r\n");
261        } else {
262            let _ = write_all(fd, b"n/a\r\n");
263        }
264        let _ = write_all(fd, b"DNS: ");
265        if ndns > 0 {
266            let _ = write_all(fd, &dns[..ndns]);
267            let _ = write_all(fd, b"\r\n");
268        } else {
269            let _ = write_all(fd, b"n/a\r\n");
270        }
271        let _ = write_all(fd, b"ROUTE: ");
272        if nr > 0 {
273            let _ = write_all(fd, &route[..nr]);
274            let _ = write_all(fd, b"\r\n");
275        } else {
276            let _ = write_all(fd, b"n/a\r\n");
277        }
278        send_prompt(fd);
279        return LineAction::Continue;
280    }
281
282    if let Some(rest) = cmd.strip_prefix("echo ") {
283        let _ = write_all(fd, b"\r\n");
284        let _ = write_all(fd, rest.as_bytes());
285        let _ = write_all(fd, b"\r\n");
286        send_prompt(fd);
287        return LineAction::Continue;
288    }
289
290    if cmd == "clear" {
291        let _ = write_all(fd, b"\x1b[2J\x1b[H");
292        send_prompt(fd);
293        return LineAction::Continue;
294    }
295
296    if cmd == "quit" || cmd == "exit" {
297        let _ = write_all(fd, b"\r\nBye.\r\n");
298        return LineAction::Disconnect;
299    }
300
301    let _ = write_all(fd, b"\r\nUnknown command. Type 'help'.\r\n");
302    send_prompt(fd);
303    LineAction::Continue
304}
305
306fn handle_bytes(fd: usize, session: &mut TelnetSession, bytes: &[u8]) -> LineAction {
307    for &b in bytes {
308        match session.iac_state {
309            IacState::SkipOption => {
310                session.iac_state = IacState::Normal;
311                continue;
312            }
313            IacState::SubNeg => {
314                if b == 240 {
315                    session.iac_state = IacState::Normal;
316                }
317                continue;
318            }
319            IacState::Normal => {}
320        }
321
322        if b == 255 {
323            session.iac_state = IacState::SkipOption;
324            continue;
325        }
326        if b == b'\r' {
327            continue;
328        }
329        if b == b'\n' {
330            let line = core::str::from_utf8(&session.line[..session.line_len]).unwrap_or("");
331            session.line_len = 0;
332            let action = handle_command(fd, line);
333            if matches!(action, LineAction::Disconnect) {
334                return action;
335            }
336            continue;
337        }
338        if session.line_len < session.line.len() {
339            session.line[session.line_len] = b;
340            session.line_len += 1;
341        }
342    }
343    LineAction::Continue
344}
345
346fn reopen_listener(session: &mut TelnetSession) -> Option<usize> {
347    if let Some(fd) = open_listener() {
348        session.reset();
349        return Some(fd);
350    }
351    None
352}
353
354#[unsafe(no_mangle)]
355pub extern "C" fn _start() -> ! {
356    log("[telnetd] Starting on /net/tcp/listen/23\n");
357    wait_for_network();
358    let mut fd = match open_listener() {
359        Some(fd) => fd,
360        None => {
361            log("[telnetd] Cannot open listener, exiting\n");
362            call::exit(1);
363        }
364    };
365    let mut session = TelnetSession::new();
366    let mut buf = [0u8; 512];
367    let mut idle_ticks: u64 = 0;
368    let mut announced = false;
369
370    loop {
371        if !session.connected && listener_established() {
372            session.connected = true;
373            announced = false;
374            idle_ticks = 0;
375            log("[telnetd] client connected\n");
376        }
377
378        if session.connected && !announced {
379            send_banner(fd);
380            announced = true;
381        }
382
383        match call::read(fd, &mut buf) {
384            Ok(0) => {
385                if session.connected {
386                    log("[telnetd] client disconnected\n");
387                    let _ = call::close(fd);
388                    fd = match reopen_listener(&mut session) {
389                        Some(fd) => fd,
390                        None => {
391                            call::exit(1);
392                        }
393                    };
394                    announced = false;
395                    idle_ticks = 0;
396                } else {
397                    sleep_ms(20);
398                    idle_ticks += 1;
399                    if idle_ticks > 750 {
400                        log("[telnetd] idle timeout on listener, reopening\n");
401                        let _ = call::close(fd);
402                        fd = match reopen_listener(&mut session) {
403                            Some(fd) => fd,
404                            None => {
405                                call::exit(1);
406                            }
407                        };
408                        announced = false;
409                        idle_ticks = 0;
410                    }
411                }
412            }
413            Ok(n) => {
414                idle_ticks = 0;
415                if !session.connected && listener_established() {
416                    session.connected = true;
417                    announced = false;
418                    log("[telnetd] client connected\n");
419                    if !announced {
420                        send_banner(fd);
421                        announced = true;
422                    }
423                }
424                if matches!(
425                    handle_bytes(fd, &mut session, &buf[..n]),
426                    LineAction::Disconnect
427                ) {
428                    log("[telnetd] client quit\n");
429                    let _ = call::close(fd);
430                    fd = match reopen_listener(&mut session) {
431                        Some(fd) => fd,
432                        None => {
433                            call::exit(1);
434                        }
435                    };
436                    announced = false;
437                    idle_ticks = 0;
438                }
439            }
440            Err(e) => {
441                let err = e.to_errno();
442                if err == EAGAIN {
443                    if session.connected && !listener_established() {
444                        log("[telnetd] peer closed, reconnecting\n");
445                        let _ = call::close(fd);
446                        fd = match reopen_listener(&mut session) {
447                            Some(fd) => fd,
448                            None => {
449                                call::exit(1);
450                            }
451                        };
452                        announced = false;
453                        idle_ticks = 0;
454                        continue;
455                    }
456                    sleep_ms(10);
457                    idle_ticks += 1;
458                    if session.connected && idle_ticks > 1500 {
459                        log("[telnetd] client idle timeout, disconnecting\n");
460                        let _ = call::close(fd);
461                        fd = match reopen_listener(&mut session) {
462                            Some(fd) => fd,
463                            None => {
464                                call::exit(1);
465                            }
466                        };
467                        announced = false;
468                        idle_ticks = 0;
469                    }
470                    continue;
471                }
472                if err == ECONNRESET {
473                    log("[telnetd] connection reset\n");
474                } else {
475                    log("[telnetd] read error, reconnecting\n");
476                }
477                let _ = call::close(fd);
478                session.reset();
479                sleep_ms(100);
480                fd = match open_listener() {
481                    Some(fd) => fd,
482                    None => {
483                        call::exit(1);
484                    }
485                };
486                announced = false;
487                idle_ticks = 0;
488            }
489        }
490    }
491}