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}