1#![no_std]
2#![no_main]
3#![feature(alloc_error_handler)]
4
5extern crate alloc;
6
7use core::{alloc::Layout, fmt::Write, 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) -> ! {
25 log("[telnetd] PANiK: ");
26 let mut buf = [0u8; 256];
27 let n = {
28 let mut w = BufWriter {
29 buf: &mut buf,
30 pos: 0,
31 };
32 let _ = write!(w, "{}", info.message());
33 w.pos
34 };
35 if let Ok(s) = core::str::from_utf8(&buf[..n]) {
36 log(s);
37 }
38 log("\n");
39 call::exit(255)
40}
41
42struct BufWriter<'a> {
43 buf: &'a mut [u8],
44 pos: usize,
45}
46
47impl core::fmt::Write for BufWriter<'_> {
48 fn write_str(&mut self, s: &str) -> core::fmt::Result {
50 let bytes = s.as_bytes();
51 let avail = self.buf.len().saturating_sub(self.pos);
52 let n = bytes.len().min(avail);
53 self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]);
54 self.pos += n;
55 Ok(())
56 }
57}
58
59fn log(msg: &str) {
61 let _ = call::debug_log(msg.as_bytes());
62}
63
64fn sleep_ms(ms: u64) {
66 let req = TimeSpec {
67 tv_sec: (ms / 1000) as i64,
68 tv_nsec: ((ms % 1000) * 1_000_000) as i64,
69 };
70 let _ = unsafe {
71 strat9_syscall::syscall2(number::SYS_NANOSLEEP, &req as *const TimeSpec as usize, 0)
72 };
73}
74
75fn write_all(fd: usize, data: &[u8]) -> bool {
77 let mut off = 0usize;
78 while off < data.len() {
79 match call::write(fd, &data[off..]) {
80 Ok(0) => return false,
81 Ok(n) => off += n,
82 Err(e) => {
83 if e.to_errno() == 11 {
84 sleep_ms(10);
85 continue;
86 }
87 return false;
88 }
89 }
90 }
91 true
92}
93
94fn read_text_file(path: &str, out: &mut [u8]) -> usize {
96 let fd = match call::openat(0, path, 0x0, 0) {
97 Ok(fd) => fd as usize,
98 Err(_) => return 0,
99 };
100 let n = call::read(fd, out).unwrap_or(0);
101 let _ = call::close(fd);
102 n
103}
104
105fn open_listener() -> usize {
107 loop {
108 match call::openat(0, "/net/tcp/listen/23", 0x2, 0) {
109 Ok(fd) => return fd as usize,
110 Err(_) => sleep_ms(200),
111 }
112 }
113}
114
115enum LineAction {
116 Continue,
117 Disconnect,
118}
119
120struct TelnetSession {
121 connected: bool,
122 line: [u8; 256],
123 line_len: usize,
124 iac_skip: u8,
125}
126
127impl TelnetSession {
128 const fn new() -> Self {
130 Self {
131 connected: false,
132 line: [0u8; 256],
133 line_len: 0,
134 iac_skip: 0,
135 }
136 }
137
138 fn reset(&mut self) {
140 self.connected = false;
141 self.line_len = 0;
142 self.iac_skip = 0;
143 }
144}
145
146fn send_prompt(fd: usize) {
148 let _ = write_all(fd, b"\r\nstrat9> ");
149}
150
151fn handle_command(fd: usize, line: &str) -> LineAction {
153 let cmd = line.trim();
154 if cmd.is_empty() {
155 send_prompt(fd);
156 return LineAction::Continue;
157 }
158
159 if cmd == "help" {
160 let _ = write_all(
161 fd,
162 b"\r\nCommands: help, ip, net, echo <text>, clear, quit\r\n",
163 );
164 send_prompt(fd);
165 return LineAction::Continue;
166 }
167
168 if cmd == "ip" {
169 let mut buf = [0u8; 128];
170 let n = read_text_file("/net/address", &mut buf);
171 let _ = write_all(fd, b"\r\nIP: ");
172 if n > 0 {
173 let _ = write_all(fd, &buf[..n]);
174 } else {
175 let _ = write_all(fd, b"n/a\r\n");
176 }
177 send_prompt(fd);
178 return LineAction::Continue;
179 }
180
181 if cmd == "net" {
182 let mut ip = [0u8; 128];
183 let mut gw = [0u8; 128];
184 let mut dns = [0u8; 128];
185 let mut route = [0u8; 128];
186 let nip = read_text_file("/net/ip", &mut ip);
187 let ngw = read_text_file("/net/gateway", &mut gw);
188 let ndns = read_text_file("/net/dns", &mut dns);
189 let nr = read_text_file("/net/route", &mut route);
190
191 let _ = write_all(fd, b"\r\nIP: ");
192 let _ = write_all(fd, if nip > 0 { &ip[..nip] } else { b"n/a\r\n" });
193 let _ = write_all(fd, b"GW: ");
194 let _ = write_all(fd, if ngw > 0 { &gw[..ngw] } else { b"n/a\r\n" });
195 let _ = write_all(fd, b"DNS: ");
196 let _ = write_all(fd, if ndns > 0 { &dns[..ndns] } else { b"n/a\r\n" });
197 let _ = write_all(fd, b"ROUTE: ");
198 let _ = write_all(fd, if nr > 0 { &route[..nr] } else { b"n/a\r\n" });
199 send_prompt(fd);
200 return LineAction::Continue;
201 }
202
203 if let Some(rest) = cmd.strip_prefix("echo ") {
204 let _ = write_all(fd, b"\r\n");
205 let _ = write_all(fd, rest.as_bytes());
206 let _ = write_all(fd, b"\r\n");
207 send_prompt(fd);
208 return LineAction::Continue;
209 }
210
211 if cmd == "clear" {
212 let _ = write_all(fd, b"\x1b[2J\x1b[H");
213 send_prompt(fd);
214 return LineAction::Continue;
215 }
216
217 if cmd == "quit" || cmd == "exit" {
218 let _ = write_all(fd, b"\r\nBye.\r\n");
219 return LineAction::Disconnect;
220 }
221
222 let _ = write_all(fd, b"\r\nUnknown command. Type 'help'.\r\n");
223 send_prompt(fd);
224 LineAction::Continue
225}
226
227fn handle_bytes(fd: usize, session: &mut TelnetSession, bytes: &[u8]) -> LineAction {
229 for &b in bytes {
230 if session.iac_skip > 0 {
231 session.iac_skip -= 1;
232 continue;
233 }
234 if b == 255 {
235 session.iac_skip = 2;
236 continue;
237 }
238 if b == b'\r' {
239 continue;
240 }
241 if b == b'\n' {
242 let line = core::str::from_utf8(&session.line[..session.line_len]).unwrap_or("");
243 session.line_len = 0;
244 let action = handle_command(fd, line);
245 if matches!(action, LineAction::Disconnect) {
246 return action;
247 }
248 continue;
249 }
250 if session.line_len < session.line.len() {
251 session.line[session.line_len] = b;
252 session.line_len += 1;
253 }
254 }
255 LineAction::Continue
256}
257
258#[unsafe(no_mangle)]
259pub extern "C" fn _start() -> ! {
261 log("[telnetd] Starting telnet server on /net/tcp/listen/23\n");
262 let mut fd = open_listener();
263 let mut session = TelnetSession::new();
264 let mut buf = [0u8; 128];
265
266 loop {
267 match call::read(fd, &mut buf) {
268 Ok(0) => {
269 if session.connected {
270 let _ = call::close(fd);
271 session.reset();
272 fd = open_listener();
273 } else {
274 sleep_ms(20);
275 }
276 }
277 Ok(n) => {
278 if !session.connected {
279 session.connected = true;
280 let _ = write_all(fd, b"\r\nStrat9 Telnet\r\nType 'help' for commands.\r\n");
281 send_prompt(fd);
282 }
283 if matches!(
284 handle_bytes(fd, &mut session, &buf[..n]),
285 LineAction::Disconnect
286 ) {
287 let _ = call::close(fd);
288 session.reset();
289 fd = open_listener();
290 }
291 }
292 Err(e) => {
293 if e.to_errno() == 11 {
294 sleep_ms(10);
295 continue;
296 }
297 let _ = call::close(fd);
298 session.reset();
299 sleep_ms(100);
300 fd = open_listener();
301 }
302 }
303 }
304}