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]
16fn 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
27fn log(msg: &str) {
29 let _ = call::debug_log(msg.as_bytes());
30}
31
32fn 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
43fn 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
70fn 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
81fn 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
135fn send_prompt(fd: usize) {
137 let _ = write_all(fd, b"\r\nstrat9> ");
138}
139
140fn 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
237fn 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)]
279pub 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}