RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorTyler Fowler <tyler@localhost>2026-10-30 03:53:54 -0700
commit235b98b1bd5539477d1c2b19d811c5f18d4f1ecf
parentd478ebf57a05d03aaa6ac392ee6b3d12edc8c543
treebrowse files at this commit
Stage 12: networking (0.12.0)

- virtio-net driver (legacy, interrupt-driven via PIC IRQ 11) + smoltcp 0.14
  (ARP, IPv4, ICMP, UDP, TCP, DHCP, DNS); a netd thread polls on IRQs/timers
- BSD-style socket syscalls with Linux numbers (socket 41, connect 42,
  accept 43, sendto 44, recvfrom 45, bind 49, listen 50, setsockopt 54),
  plus clock_gettime 228 and our own resolve 503 / netinfo 504; blocking on
  wait queues with SO_RCVTIMEO deadlines
- programs: ifconfig (ip), ping, host (nslookup), fetch (wget), httpd
- spurious PIC IRQ 7/15 handlers (fixes a double fault)
- tests/net.rs (10 tests: DHCP, ICMP, UDP/TFTP, TCP echo + HTTP GET,
  ECONNREFUSED, recv timeout, programs, leak check, DNS or skip);
  self-test covers ifconfig/ping/fetch/wget to D:/host/httpd via hostfwd
- README networking chapter, roadmap; boot.png shows ifconfig/ping/fetch
Afetch.rs+136 -0
Ahost.rs+35 -0
Ahttpd.rs+132 -0
Aifconfig.rs+47 -0
Aping.rs+103 -0
Msh.rs+7 -1
Mulib.rs+161 -0

7 files changed, 621 insertions, 1 deletion

@@ -0,0 +1,136 @@+//! `fetch [-o file] http://host[:port]/path` (alias `wget`): download a+//! web page or file with HTTP/1.0 and print it, or save it with `-o`.+//!+//! HTTP is just text over a TCP connection. We send+//!     GET /path HTTP/1.0\r\n+//!     Host: example.com\r\n+//!     \r\n+//! and the server answers with a status line ("HTTP/1.0 200 OK"), header+//! lines, an empty line, then the body. With HTTP/1.0 the server closes+//! the connection when the body is done, so "read until EOF" is the whole+//! protocol on our side. (Plain HTTP only: HTTPS would need TLS.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use core::fmt::Write;++/// "http://host:port/path" -> (host, port, path)+fn parse_url(url: &str) -> Option<(&str, u16, &str)> {+    let rest = url.strip_prefix("http://")?;+    let (hostport, path) = match rest.find('/') {+        Some(i) => (&rest[..i], &rest[i..]),+        None => (rest, "/"),+    };+    let (host, port) = match hostport.split_once(':') {+        Some((h, p)) => (h, p.parse().ok()?),+        None => (hostport, 80),+    };+    if host.is_empty() { None } else { Some((host, port, path)) }+}++fn main() -> i64 {+    let (mut out_path, mut url) = (None, None);+    let mut i = 1;+    while let Some(a) = ulib::arg(i) {+        if a == "-o" || a == "-O" {+            i += 1;+            out_path = ulib::arg(i);+        } else {+            url = Some(a);+        }+        i += 1;+    }+    let Some((host, port, path)) = url.and_then(parse_url) else {+        eprintln!("usage: fetch [-o file] http://host[:port]/path");+        return 2;+    };++    // 1. Name -> address (DNS), 2. connect (TCP handshake).+    let ip = match ulib::resolve(host) {+        Ok(ip) => ip,+        Err(e) => {+            eprintln!("fetch: {}: {}", host, ulib::strerror(e));+            return 1;+        }+    };+    let start = ulib::now_us();+    let fd = ulib::socket(ulib::SOCK_STREAM, 0);+    let r = if fd < 0 { fd } else { ulib::connect(fd, ip, port) };+    if r < 0 {+        eprintln!("fetch: connect to {}:{}: {}", ip, port, ulib::strerror(r));+        return 1;+    }++    // 3. Send the request.+    let mut request = alloc::string::String::new();+    let _ = write!(request, "GET {} HTTP/1.0\r\nHost: {}\r\nUser-Agent: rustbelt-fetch/0.12\r\nConnection: close\r\n\r\n", path, host);+    if ulib::write_all(fd, request.as_bytes()) < 0 {+        eprintln!("fetch: send failed");+        return 1;+    }++    // 4. Read the response: headers first (up to the empty line)...+    let mut head = alloc::vec::Vec::new();+    let mut buf = [0u8; 4096];+    let body_start = loop {+        let n = ulib::read(fd, &mut buf);+        if n <= 0 {+            eprintln!("fetch: connection closed before the headers ended");+            return 1;+        }+        head.extend_from_slice(&buf[..n as usize]);+        if let Some(i) = head.windows(4).position(|w| w == b"\r\n\r\n") {+            break i + 4;+        }+        if head.len() > 16 * 1024 {+            eprintln!("fetch: headers too long");+            return 1;+        }+    };+    let status_line = head.split(|&b| b == b'\r').next().unwrap_or(b"");+    let status_line = core::str::from_utf8(status_line).unwrap_or("?");+    let code: u32 = status_line.split(' ').nth(1).and_then(|c| c.parse().ok()).unwrap_or(0);++    // ...then the body, to stdout or the file, until the server closes.+    let out = match out_path {+        Some(p) => ulib::open(p, ulib::O_CREAT | ulib::O_TRUNC),+        None => 1,+    };+    if out < 0 {+        eprintln!("fetch: {}: {}", out_path.unwrap(), ulib::strerror(out));+        return 1;+    }+    let mut total = head.len() - body_start;+    ulib::write_all(out, &head[body_start..]);+    loop {+        let n = ulib::read(fd, &mut buf);+        if n < 0 {+            eprintln!("fetch: read: {}", ulib::strerror(n));+            break;+        }+        if n == 0 {+            break; // the server closed: that's the end of the body+        }+        if ulib::write_all(out, &buf[..n as usize]) < 0 {+            eprintln!("fetch: write failed");+            return 1;+        }+        total += n as usize;+    }+    ulib::close(fd);+    let ms = ((ulib::now_us() - start) / 1000).max(1);+    if let Some(p) = out_path {+        ulib::close(out);+        println!(+            "fetch: {}, saved {} bytes to {} in {} ms ({} KiB/s)",+            status_line, total, p, ms, total as u64 * 1000 / 1024 / ms+        );+    }+    if (200..300).contains(&code) { 0 } else { 1 }+}++extern crate alloc;
@@ -0,0 +1,35 @@+//! `host name` (alias `nslookup`): look up a name's IPv4 addresses.+//!+//! DNS turns names into addresses. We send a UDP query to the DNS server+//! DHCP told us about (QEMU's is 10.0.2.3, which forwards to the host's+//! resolver), asking for the name's "A" (IPv4 address) records. smoltcp+//! builds and parses the DNS packets; the kernel's `resolve` syscall wraps+//! it (on Linux, DNS lives in the C library instead).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+    let Some(name) = ulib::arg(1) else {+        eprintln!("usage: host name");+        return 2;+    };+    let mut addrs = [[0u8; 4]; 8];+    let n = ulib::resolve_all(name, &mut addrs);+    if n < 0 {+        let why = if n == -ulib::ETIMEDOUT { "no answer from the DNS server" } else { ulib::strerror(n) };+        eprintln!("host: {}: {}", name, why);+        return 1;+    }+    if n == 0 {+        println!("{} has no IPv4 address", name);+        return 1;+    }+    for a in &addrs[..n as usize] {+        println!("{} has address {}", name, ulib::Ip(*a));+    }+    0+}
@@ -0,0 +1,132 @@+//! `httpd [-p port] [-n count] [-t seconds]`: a tiny web server.+//!+//! The server side of sockets: `bind` to a port, `listen`, then `accept`+//! connections one by one. Each request gets one answer and the connection+//! is closed (HTTP/1.0 style). `/` is a greeting page; any other path is a+//! file from the current directory (`cd B:` then `httpd` serves the FAT32+//! disk).+//!+//! QEMU's user-mode network hides the guest behind NAT, so to reach this+//! from the host QEMU must forward a port, e.g.+//!     -netdev user,id=net0,hostfwd=tcp:127.0.0.1:5580-:80+//! and then `curl http://127.0.0.1:5580/` on the host.+//!+//! `-n` stops after that many requests; `-t` gives up if no request comes+//! within that many seconds (both used by the self-test).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;+use alloc::format;++fn main() -> i64 {+    let (mut port, mut max_requests, mut timeout_s) = (80u16, u64::MAX, 0u64);+    let mut i = 1;+    while let Some(a) = ulib::arg(i) {+        let value = ulib::arg(i + 1).and_then(|v| v.parse::<u64>().ok());+        match (a, value) {+            ("-p", Some(v)) => port = v as u16,+            ("-n", Some(v)) => max_requests = v,+            ("-t", Some(v)) => timeout_s = v,+            _ => {+                eprintln!("usage: httpd [-p port] [-n count] [-t seconds]");+                return 2;+            }+        }+        i += 2;+    }++    let fd = ulib::socket(ulib::SOCK_STREAM, 0);+    for r in [fd, ulib::bind(fd, port), ulib::listen(fd, 4)] {+        if r < 0 {+            eprintln!("httpd: {}", ulib::strerror(r));+            return 1;+        }+    }+    ulib::set_recv_timeout(fd, timeout_s * 1000); // 0 = wait forever+    println!("[httpd] listening on port {}", port);++    let mut served = 0;+    while served < max_requests {+        let (conn, peer) = ulib::accept(fd);+        if conn < 0 {+            println!("[httpd] no request within {} s, giving up", timeout_s);+            return 1;+        }+        served += 1;+        handle(conn, peer, served);+        ulib::close(conn);+    }+    println!("[httpd] served {} request{}, bye", served, if served == 1 { "" } else { "s" });+    0+}++/// Read one request, send one response.+fn handle(conn: i64, peer: ulib::SockAddrIn, visitor: u64) {+    ulib::set_recv_timeout(conn, 5000);+    let mut req = [0u8; 2048];+    let mut len = 0;+    while len < req.len() && !req[..len].windows(4).any(|w| w == b"\r\n\r\n") {+        let n = ulib::read(conn, &mut req[len..]);+        if n <= 0 {+            return; // gave up or hung up+        }+        len += n as usize;+    }+    let line = req[..len].split(|&b| b == b'\r').next().unwrap_or(b"");+    let line = core::str::from_utf8(line).unwrap_or("");+    let mut words = line.split(' ');+    let (method, path) = (words.next().unwrap_or(""), words.next().unwrap_or("/"));++    let (status, bytes) = if method != "GET" {+        respond(conn, "405 Method Not Allowed", b"only GET, sorry\n")+    } else if path == "/" {+        let mut uts = [0u8; 6 * 65];+        let u = ulib::uname(&mut uts);+        let body = format!(+            "Hello from Rustbelt! {} {} ({})\nYou are visitor {}. Ask for /path to get a file from my current directory.\n",+            u[0], u[2], u[3], visitor+        );+        respond(conn, "200 OK", body.as_bytes())+    } else if path.contains("..") {+        respond(conn, "403 Forbidden", b"no ..\n")+    } else {+        send_file(conn, &path[1..])+    };+    println!("[httpd] {}:{} {} {} -> {} ({} bytes)", peer.ip(), peer.port(), method, path, status, bytes);+}++fn respond(conn: i64, status: &'static str, body: &[u8]) -> (&'static str, usize) {+    let head = format!("HTTP/1.0 {}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nServer: rustbelt-httpd\r\n\r\n", status, body.len());+    ulib::write_all(conn, head.as_bytes());+    ulib::write_all(conn, body);+    (&status[..3], body.len())+}++fn send_file(conn: i64, path: &str) -> (&'static str, usize) {+    let mut st = ulib::Stat::default();+    if ulib::stat(path, &mut st) < 0 || st.kind != ulib::KIND_FILE {+        return respond(conn, "404 Not Found", b"not found\n");+    }+    let fd = ulib::open(path, 0);+    if fd < 0 {+        return respond(conn, "404 Not Found", b"not found\n");+    }+    let head = format!("HTTP/1.0 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n", st.size);+    ulib::write_all(conn, head.as_bytes());+    let mut buf = [0u8; 4096];+    let mut sent = 0;+    loop {+        let n = ulib::read(fd, &mut buf);+        if n <= 0 || ulib::write_all(conn, &buf[..n as usize]) < 0 {+            break;+        }+        sent += n as usize;+    }+    ulib::close(fd);+    ("200", sent)+}
@@ -0,0 +1,47 @@+//! `ifconfig` (alias `ip`): show the network card and our IP address.+//!+//!     eth0: virtio-net  MAC 52:54:00:12:34:56  IRQ 11+//!           inet 10.0.2.15/24  gateway 10.0.2.2  DNS 10.0.2.3  (DHCP)+//!           RX 12 packets (2 KiB)  TX 9 packets (1 KiB)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::Ip;++fn main() -> i64 {+    let mut info = ulib::netinfo();+    if info.present == 0 {+        eprintln!("ifconfig: no network card (QEMU needs -device virtio-net-pci)");+        return 1;+    }+    // Right after boot DHCP may still be running: give it up to 3 s.+    for _ in 0..30 {+        if info.configured != 0 {+            break;+        }+        ulib::sleep_ms(100);+        info = ulib::netinfo();+    }+    let m = info.mac;+    println!(+        "eth0: virtio-net  MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}  IRQ {}",+        m[0], m[1], m[2], m[3], m[4], m[5], info.irq+    );+    if info.configured != 0 {+        println!(+            "      inet {}/{}  gateway {}  DNS {}  (DHCP)",+            Ip(info.address), info.prefix_len, Ip(info.gateway), Ip(info.dns)+        );+    } else {+        println!("      no address yet (waiting for DHCP)");+    }+    println!(+        "      RX {} packets ({} KiB)  TX {} packets ({} KiB)",+        info.rx_packets, info.rx_bytes / 1024, info.tx_packets, info.tx_bytes / 1024+    );+    0+}
@@ -0,0 +1,103 @@+//! `ping [-c count] [-i ms] host`: send ICMP echo requests, time the replies.+//!+//! ICMP is IP's "control message" protocol. An *echo request* (type 8) asks+//! a host to send the same bytes straight back as an *echo reply* (type 0).+//! The packet: type, code, checksum (2 bytes), identifier (2), sequence+//! number (2), then any payload. We use a Linux-style "ping socket"+//! (SOCK_DGRAM + IPPROTO_ICMP): the kernel fills in the identifier and+//! checksum and only gives us replies to *our* requests.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++const PAYLOAD: usize = 56; // like Linux's ping: 56 + 8 header = 64 bytes++fn main() -> i64 {+    let mut count = 4u16;+    let mut interval_ms = 1000;+    let mut host = None;+    let mut i = 1;+    while let Some(a) = ulib::arg(i) {+        match a {+            "-c" => { i += 1; count = ulib::arg(i).and_then(|v| v.parse().ok()).unwrap_or(4); }+            "-i" => { i += 1; interval_ms = ulib::arg(i).and_then(|v| v.parse().ok()).unwrap_or(1000); }+            _ => host = Some(a),+        }+        i += 1;+    }+    let Some(host) = host else {+        eprintln!("usage: ping [-c count] [-i interval_ms] host");+        return 2;+    };+    let ip = match ulib::resolve(host) {+        Ok(ip) => ip,+        Err(e) => {+            eprintln!("ping: {}: {}", host, ulib::strerror(e));+            return 2;+        }+    };+    let fd = ulib::socket(ulib::SOCK_DGRAM, ulib::IPPROTO_ICMP);+    if fd < 0 {+        eprintln!("ping: socket: {}", ulib::strerror(fd));+        return 2;+    }+    ulib::set_recv_timeout(fd, 1000); // a reply later than 1 s counts as lost+    println!("PING {} ({}): {} data bytes", host, ip, PAYLOAD);++    let (mut received, mut min, mut max, mut total) = (0u64, u64::MAX, 0u64, 0u64);+    for seq in 1..=count {+        // Echo request: type 8, code 0, checksum and id (kernel fills in), seq.+        let mut packet = [0u8; 8 + PAYLOAD];+        packet[0] = 8;+        packet[6..8].copy_from_slice(&seq.to_be_bytes());+        for (j, b) in packet[8..].iter_mut().enumerate() {+            *b = j as u8; // a recognizable payload+        }+        let sent_at = ulib::now_us();+        let n = ulib::sendto(fd, &packet, ip, 0);+        if n < 0 {+            eprintln!("ping: sendto: {}", ulib::strerror(n));+            return 2;+        }+        // Wait for *this* reply (a late one for an older seq is skipped).+        let mut reply = [0u8; 8 + PAYLOAD + 64];+        loop {+            let (n, from) = ulib::recvfrom(fd, &mut reply);+            if n < 0 {+                println!("Request timeout for icmp_seq={}", seq);+                break;+            }+            let n = n as usize;+            if n >= 8 && reply[0] == 0 && reply[6..8] == seq.to_be_bytes() {+                let us = ulib::now_us() - sent_at;+                let ok = reply[8..n] == packet[8..];+                println!(+                    "{} bytes from {}: icmp_seq={} time={}.{:02} ms{}",+                    n, from.ip(), seq, us / 1000, us % 1000 / 10,+                    if ok { "" } else { " (payload differs!)" }+                );+                received += 1;+                (min, max, total) = (min.min(us), max.max(us), total + us);+                break;+            }+        }+        if seq < count {+            ulib::sleep_ms(interval_ms);+        }+    }+    ulib::close(fd);+    let loss = (count as u64 - received) * 100 / count.max(1) as u64;+    println!("--- {} ping statistics ---", host);+    println!("{} packets transmitted, {} received, {}% packet loss", count, received, loss);+    if received > 0 {+        let avg = total / received;+        println!(+            "rtt min/avg/max = {}.{:02}/{}.{:02}/{}.{:02} ms",+            min / 1000, min % 1000 / 10, avg / 1000, avg % 1000 / 10, max / 1000, max % 1000 / 10+        );+    }+    if received > 0 { 0 } else { 1 }+}
@@ -30,13 +30,19 @@ use shparse::Drive; use ulib::*;  /// Built-in aliases (DOS names for the Unix-y programs).-const DEFAULT_ALIASES: &[(&str, &str)] = &[("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp")];+/// Plus Linux names for the network tools.+const DEFAULT_ALIASES: &[(&str, &str)] = &[+    ("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp"),+    ("ip", "ifconfig"), ("nslookup", "host"), ("wget", "fetch"),+];  const HELP: &str = "\ Rustbelt shell commands   builtins   cd [dir]  pwd  echo [text]  ver  drives  alias [name=value]              history  jobs  wait [pid]  cls / clear  help  exit [code]   programs   ls [-l]  cat  wc  grep [-i] [-v]  mkdir  rm  touch  cp  sleep  sysinfo+  network    ifconfig (ip)  ping [-c n] host  host name (nslookup)+             fetch [-o file] http://... (wget)  httpd [-p port] [-n count]   (bin dirs) hello  demo  evil  spin [rounds]  overflow  child  sh   run [--trace] prog args   run a program (--trace prints its syscalls)   start prog args           run it in the background (or: prog args &)
@@ -42,6 +42,17 @@ pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500; pub const SYS_MOUNTS: u64 = 501; pub const SYS_SYSINFO: u64 = 502;+pub const SYS_SOCKET: u64 = 41;+pub const SYS_CONNECT: u64 = 42;+pub const SYS_ACCEPT: u64 = 43;+pub const SYS_SENDTO: u64 = 44;+pub const SYS_RECVFROM: u64 = 45;+pub const SYS_BIND: u64 = 49;+pub const SYS_LISTEN: u64 = 50;+pub const SYS_SETSOCKOPT: u64 = 54;+pub const SYS_CLOCK_GETTIME: u64 = 228;+pub const SYS_RESOLVE: u64 = 503;+pub const SYS_NETINFO: u64 = 504;  pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000;@@ -64,6 +75,7 @@ pub const ENOTDIR: i64 = 20; pub const EISDIR: i64 = 21; pub const ESPIPE: i64 = 29; pub const ENOTEMPTY: i64 = 39;+pub const ETIMEDOUT: i64 = 110;  /// Same layout as the kernel's `fs::file::Stat`. #[repr(C)]@@ -365,10 +377,159 @@ pub fn strerror(errno: i64) -> &'static str {         39 => "Directory not empty",         40 => "Too many levels of symbolic links",         95 => "Operation not supported",+        88 => "Socket operation on non-socket",+        89 => "Destination address required",+        90 => "Message too long",+        93 => "Protocol not supported",+        97 => "Address family not supported",+        98 => "Address already in use",+        100 => "Network is down",+        101 => "Network is unreachable",+        104 => "Connection reset by peer",+        106 => "Already connected",+        107 => "Not connected",+        110 => "Connection timed out",+        111 => "Connection refused",         _ => "Unknown error",     } } +// --- Networking ------------------------------------------------------------------+//+// BSD sockets, the API every OS copied from 1983's 4.2BSD Unix: `socket()`+// makes an fd, `connect()` dials, read/write move data, `close()` hangs up.+// Servers `bind()` a port, `listen()`, and `accept()` each caller.++pub const AF_INET: u64 = 2;+pub const SOCK_STREAM: u64 = 1; // TCP+pub const SOCK_DGRAM: u64 = 2; // UDP (or ICMP with IPPROTO_ICMP)+pub const IPPROTO_ICMP: u64 = 1;++/// An IPv4 address that prints as "10.0.2.15".+#[derive(Clone, Copy, PartialEq, Eq, Default)]+pub struct Ip(pub [u8; 4]);++impl fmt::Display for Ip {+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {+        let [a, b, c, d] = self.0;+        write!(f, "{}.{}.{}.{}", a, b, c, d)+    }+}++/// "10.0.2.2" -> Ip, or None if it isn't a dotted-quad address.+pub fn parse_ip(s: &str) -> Option<Ip> {+    let mut ip = [0u8; 4];+    let mut parts = s.split('.');+    for b in ip.iter_mut() {+        *b = parts.next()?.parse().ok()?;+    }+    if parts.next().is_some() { None } else { Some(Ip(ip)) }+}++/// Linux's `struct sockaddr_in` (port and address big-endian).+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct SockAddrIn {+    pub family: u16,+    pub port_be: u16,+    pub addr: [u8; 4],+    pub zero: [u8; 8],+}++impl SockAddrIn {+    pub fn new(ip: Ip, port: u16) -> SockAddrIn {+        SockAddrIn { family: AF_INET as u16, port_be: port.to_be(), addr: ip.0, zero: [0; 8] }+    }+    pub fn ip(&self) -> Ip {+        Ip(self.addr)+    }+    pub fn port(&self) -> u16 {+        u16::from_be(self.port_be)+    }+}++pub fn socket(ty: u64, protocol: u64) -> i64 {+    syscall3(SYS_SOCKET, AF_INET, ty, protocol)+}+pub fn connect(fd: i64, ip: Ip, port: u16) -> i64 {+    let sa = SockAddrIn::new(ip, port);+    syscall3(SYS_CONNECT, fd as u64, &sa as *const _ as u64, 16)+}+pub fn bind(fd: i64, port: u16) -> i64 {+    let sa = SockAddrIn::new(Ip([0; 4]), port);+    syscall3(SYS_BIND, fd as u64, &sa as *const _ as u64, 16)+}+pub fn listen(fd: i64, backlog: u64) -> i64 {+    syscall3(SYS_LISTEN, fd as u64, backlog, 0)+}+/// Wait for a connection: (new fd or -errno, caller's address).+pub fn accept(fd: i64) -> (i64, SockAddrIn) {+    let mut sa = SockAddrIn::default();+    let fd = syscall3(SYS_ACCEPT, fd as u64, &mut sa as *mut _ as u64, 0);+    (fd, sa)+}+pub fn sendto(fd: i64, buf: &[u8], ip: Ip, port: u16) -> i64 {+    let sa = SockAddrIn::new(ip, port);+    syscall5(SYS_SENDTO, fd as u64, buf.as_ptr() as u64, buf.len() as u64, 0, &sa as *const _ as u64)+}+/// Receive one datagram (or some stream bytes): (length or -errno, sender).+pub fn recvfrom(fd: i64, buf: &mut [u8]) -> (i64, SockAddrIn) {+    let mut sa = SockAddrIn::default();+    let n = syscall5(SYS_RECVFROM, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, 0, &mut sa as *mut _ as u64);+    (n, sa)+}+/// SO_RCVTIMEO: recv/accept give up with -EAGAIN after `ms` (0 = never).+pub fn set_recv_timeout(fd: i64, ms: u64) -> i64 {+    let tv = [ms / 1000, (ms % 1000) * 1000]; // struct timeval+    syscall5(SYS_SETSOCKOPT, fd as u64, 1, 20, tv.as_ptr() as u64, 16)+}+/// Microseconds since boot (clock_gettime(CLOCK_MONOTONIC)).+pub fn now_us() -> u64 {+    let mut ts = [0u64; 2];+    syscall3(SYS_CLOCK_GETTIME, 1, ts.as_mut_ptr() as u64, 0);+    ts[0] * 1_000_000 + ts[1] / 1000+}+/// Look up a host name with DNS (or parse it if it's already an address).+pub fn resolve(name: &str) -> Result<Ip, i64> {+    if let Some(ip) = parse_ip(name) {+        return Ok(ip);+    }+    let mut out = [[0u8; 4]; 1];+    match syscall5(SYS_RESOLVE, name.as_ptr() as u64, name.len() as u64, out.as_mut_ptr() as u64, 1, 0) {+        n if n < 0 => Err(n),+        0 => Err(-2), // no A record: ENOENT+        _ => Ok(Ip(out[0])),+    }+}+/// All IPv4 addresses for `name` (up to `out.len()`); count or -errno.+pub fn resolve_all(name: &str, out: &mut [[u8; 4]]) -> i64 {+    syscall5(SYS_RESOLVE, name.as_ptr() as u64, name.len() as u64, out.as_mut_ptr() as u64, out.len() as u64, 0)+}++/// What `ifconfig` shows; same layout as the kernel's `net::NetInfo`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct NetInfo {+    pub present: u8,+    pub configured: u8,+    pub mac: [u8; 6],+    pub address: [u8; 4],+    pub prefix_len: u8,+    pub irq: u8,+    pub _pad: [u8; 2],+    pub gateway: [u8; 4],+    pub dns: [u8; 4],+    pub rx_packets: u64,+    pub rx_bytes: u64,+    pub tx_packets: u64,+    pub tx_bytes: u64,+}+pub fn netinfo() -> NetInfo {+    let mut info = NetInfo::default();+    syscall3(SYS_NETINFO, &mut info as *mut NetInfo as u64, 0, 0);+    info+}+ pub struct Stdout;  impl fmt::Write for Stdout {