feat(net,gui): real internet access -- redirects, HTTPS guard, netcheck, and a live ONLINE indicator

The stack could already reach the real internet once the UDP-checksum and
scheduler fixes landed; this makes it work on real-world URLs and gives the
desktop a first-class "is the internet up?" answer.

HTTP client
  - Follows 3xx redirects (Location header, absolute or relative) up to five
    hops, so a bare host that 301s to a path or a canonical host resolves to
    actual content. Verified: `webget google.com/` follows the 301 to
    www.google.com and returns its HTTP 200 homepage.
  - Refuses https:// targets -- at the initial URL and on redirect -- with a
    clear message instead of speaking plaintext to port 443 and timing out.
  - Split the URL parser out so both the initial request and redirect targets
    go through the same path; added a case-insensitive header finder.

netcheck (CLI)
  One command that walks the whole chain -- link, address, gateway ping, DNS,
  and a real HTTP fetch -- and reports each PASS/FAIL, ending in ONLINE or
  DEGRADED. The one-shot answer to "is the internet working?".

Network Monitor (GUI)
  A live Internet: ONLINE/OFFLINE badge with the resolved IP, HTTP status and
  latency. It auto-probes once on open and re-checks on the Check button, all
  through the async HTTP worker so the window never blocks.

Verified against the real internet through QEMU's NAT: DNS resolved
example.com to a live Cloudflare address; `netcheck` passed all five layers
(Internet: ONLINE); the GUI badge showed "ONLINE 104.20.23.154 HTTP 200 240
ms"; redirect following and the HTTPS refusal both behaved. Builds clean on
x86_64, aarch64 and armv8i.

Known limit: no TLS, so HTTPS URLs are reported rather than fetched -- plain
HTTP only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
This commit is contained in:
subhobhai943andClaude Opus 4.8 committed 2026-08-27 02:44:53 +05:30
1 parent dd4922767a
commit f6e4511047
4 files changed
+308 -33

No files matched your search

+78 -2
View File
@@ -26,6 +26,7 @@
#include <net/tcp.h>
#include <net/udp.h>
#include <net/socket.h>
#include <net/http_client.h>
#include <drivers/e1000.h>
#define NETMON_HISTORY 64
@@ -46,8 +47,57 @@ typedef struct {
bool primed; // a baseline reading has been taken
int scroll; // first connection row shown
// Internet reachability probe (async, via the HTTP worker).
http_fetch_t* probe;
int inet_state; // 0 unknown, 1 checking, 2 online, 3 offline
char inet_detail[72];
bool auto_checked;
} netmon_data_t;
#define INET_UNKNOWN 0
#define INET_CHECKING 1
#define INET_ONLINE 2
#define INET_OFFLINE 3
#define NETMON_PROBE_HOST "example.com/" // a plain-HTTP host to reach for
// Advance the reachability probe: collect a finished result, or note that one
// is still in flight. Runs every paint; the fetch itself is on the worker.
static void netmon_poll_probe(netmon_data_t* nd) {
if (!nd->probe) return;
int st = nd->probe->state;
if (st == HTTP_STATE_RUNNING) { nd->inet_state = INET_CHECKING; return; }
if (st == HTTP_STATE_DONE && nd->probe->status_code > 0) {
char ipx[16];
ip_to_str(nd->probe->ip, ipx);
nd->inet_state = INET_ONLINE;
snprintf(nd->inet_detail, sizeof(nd->inet_detail), "%s HTTP %d %u ms",
ipx, nd->probe->status_code, nd->probe->elapsed_ms);
} else {
nd->inet_state = INET_OFFLINE;
snprintf(nd->inet_detail, sizeof(nd->inet_detail), "%s",
st == HTTP_STATE_ERROR ? nd->probe->err : "no response");
}
http_fetch_release(nd->probe);
nd->probe = NULL;
}
static void netmon_start_probe(netmon_data_t* nd) {
if (nd->probe) return; // one in flight already
nd->probe = http_fetch_start(NETMON_PROBE_HOST, 4096);
if (nd->probe) {
nd->inet_state = INET_CHECKING;
} else {
// The single worker slot is busy (e.g. the Web app is fetching).
nd->inet_state = INET_UNKNOWN;
snprintf(nd->inet_detail, sizeof(nd->inet_detail), "worker busy, try again");
}
}
// ===========================================================================
// Sampling
// ===========================================================================
@@ -217,8 +267,32 @@ static void netmon_paint(gui_window_t* win) {
(unsigned)tcp_get_connections_count(), tcp_get_listener_count());
gui_label_aligned(0, row + 12, w - 12, buf, GUI_THEME_TEXT_MUTED, GUI_ALIGN_RIGHT);
// --- internet reachability -------------------------------------------
// Kick off one probe automatically the first time the window paints, then
// let the user re-check on demand. The fetch runs on the worker thread, so
// this stays responsive while a check is in flight.
if (!nd->auto_checked) { nd->auto_checked = true; netmon_start_probe(nd); }
netmon_poll_probe(nd);
int irow = row + 28;
static const char* const inet_labels[] = { "UNKNOWN", "checking...", "ONLINE", "OFFLINE" };
uint32_t inet_col = (nd->inet_state == INET_ONLINE) ? GUI_THEME_SUCCESS
: (nd->inet_state == INET_OFFLINE) ? GUI_THEME_DANGER
: (nd->inet_state == INET_CHECKING)? GUI_THEME_WARNING
: GUI_THEME_TEXT_DIM;
gui_label(10, irow, "Internet:", GUI_THEME_TEXT_MUTED);
gui_badge(84, irow - 1, inet_labels[nd->inet_state],
inet_col, GUI_COLOR_BLACK);
if (nd->inet_detail[0]) {
gui_label(150, irow, nd->inet_detail, GUI_THEME_TEXT_DIM);
}
if (gui_button(5, w - 82, irow - 3, 72, 18,
nd->probe ? "checking" : "Check")) {
netmon_start_probe(nd);
}
// --- connection table -------------------------------------------------
int tbl_y = row + 30;
int tbl_y = row + 46;
gui_separator(10, tbl_y - 4, w - 20);
gui_label_bold(10, tbl_y, "PROTO LOCAL", GUI_THEME_TEXT_DIM);
gui_label(w / 2 + 6, tbl_y, "REMOTE", GUI_THEME_TEXT_DIM);
@@ -306,7 +380,9 @@ static void netmon_event(gui_window_t* win, const gui_event_t* ev) {
gui_widget_feed_event(win, ev);
if (ev->type == GUI_EVENT_CLOSE && win->user_data) {
kfree(win->user_data);
netmon_data_t* nd = (netmon_data_t*)win->user_data;
if (nd->probe) http_fetch_release(nd->probe); // safe mid-fetch
kfree(nd);
win->user_data = NULL;
}
}
+5 -3
View File
@@ -31,6 +31,7 @@ typedef struct http_fetch {
char host[128];
char path[256];
uint16_t port;
bool tls; // the URL asked for https, which we cannot speak
// Result. `state` is the only field the requester may read while the fetch
// is in flight; everything else is valid once state != RUNNING.
@@ -41,10 +42,11 @@ typedef struct http_fetch {
int len; // bytes stored
int cap; // buffer capacity
uint32_t ip; // resolved peer address
uint32_t ip; // resolved peer address (of the final hop)
int status_code; // parsed HTTP status, 0 if not found
uint32_t elapsed_ms; // wall time of the fetch
char err[64]; // human-readable failure reason
uint32_t elapsed_ms; // wall time of the whole fetch
int redirects; // number of 3xx hops followed
char err[96]; // human-readable failure reason
} http_fetch_t;
// Start the worker thread. Call once at boot, after the TCP stack is up.
+143 -28
View File
@@ -37,41 +37,119 @@ static http_fetch_t* volatile mailbox = NULL; // job awaiting the worker
// Anything it cannot make sense of leaves the job unusable and is reported as
// an error rather than guessed at.
// ===========================================================================
static void parse_url(const char* url, http_fetch_t* f) {
f->host[0] = '\0';
strcpy(f->path, "/");
f->port = 80;
// Parse an absolute URL into its parts. Returns false only when there is no
// host to be found. `is_https` reports the scheme so the caller can refuse a
// TLS target cleanly rather than speaking plaintext to port 443.
static bool split_url(const char* url, char* host, size_t host_cap,
char* path, size_t path_cap, uint16_t* port, bool* is_https) {
host[0] = '\0';
strcpy(path, "/");
*port = 80;
*is_https = false;
if (!url) return;
if (strncmp(url, "http://", 7) == 0) url += 7;
else if (strncmp(url, "https://", 8) == 0) url += 8; // no TLS; try plain
if (!url) return false;
if (strncmp(url, "http://", 7) == 0) {
url += 7;
} else if (strncmp(url, "https://", 8) == 0) {
url += 8;
*is_https = true;
*port = 443;
}
// Host (and optional :port) up to the first '/'.
int i = 0;
char hostport[192];
int i = 0;
while (url[i] && url[i] != '/' && i < (int)sizeof(hostport) - 1) {
hostport[i] = url[i];
i++;
}
hostport[i] = '\0';
const char* rest = url + i; // "" or "/path..."
// Split host:port.
char* colon = strchr(hostport, ':');
if (colon) {
*colon = '\0';
int p = 0;
for (const char* c = colon + 1; *c >= '0' && *c <= '9'; c++) p = p * 10 + (*c - '0');
if (p > 0 && p < 65536) f->port = (uint16_t)p;
if (p > 0 && p < 65536) *port = (uint16_t)p;
}
strncpy(f->host, hostport, sizeof(f->host) - 1);
f->host[sizeof(f->host) - 1] = '\0';
strncpy(host, hostport, host_cap - 1);
host[host_cap - 1] = '\0';
if (rest[0] == '/') {
strncpy(f->path, rest, sizeof(f->path) - 1);
f->path[sizeof(f->path) - 1] = '\0';
strncpy(path, rest, path_cap - 1);
path[path_cap - 1] = '\0';
}
return host[0] != '\0';
}
static void parse_url(const char* url, http_fetch_t* f) {
split_url(url, f->host, sizeof(f->host), f->path, sizeof(f->path), &f->port, &f->tls);
}
// Find a header's value in the response, case-insensitively. Copies the value
// (trimmed, up to the CRLF) into out. Returns false if the header is absent.
static bool find_header(const char* buf, int len, const char* name,
char* out, size_t out_cap) {
size_t nlen = strlen(name);
// Scan line by line; the status line is skipped naturally (no match).
for (int i = 0; i + (int)nlen < len; i++) {
if (i != 0 && buf[i - 1] != '\n') continue; // only at a line start
// Case-insensitive compare of the header name.
size_t k = 0;
while (k < nlen) {
char a = buf[i + k], b = name[k];
if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
if (a != b) break;
k++;
}
if (k != nlen || buf[i + k] != ':') continue;
const char* v = buf + i + nlen + 1;
const char* end = buf + len;
while (v < end && (*v == ' ' || *v == '\t')) v++;
size_t o = 0;
while (v < end && *v != '\r' && *v != '\n' && o < out_cap - 1) out[o++] = *v++;
out[o] = '\0';
return true;
}
return false;
}
// Resolve a Location value against the current request. Absolute URLs replace
// host/port/path; a path-only Location keeps the host. Returns false and sets
// f->err when the target is HTTPS, which this TLS-less client cannot follow.
static bool apply_redirect(http_fetch_t* f, const char* location) {
if (location[0] == '/' || location[0] == '\0') {
// Same host, new path.
strncpy(f->path, location[0] ? location : "/", sizeof(f->path) - 1);
f->path[sizeof(f->path) - 1] = '\0';
return true;
}
if (strncmp(location, "http://", 7) != 0 && strncmp(location, "https://", 8) != 0) {
// A bare relative reference; treat it as a rooted path.
snprintf(f->path, sizeof(f->path), "/%s", location);
return true;
}
bool https;
char host[128], path[256];
uint16_t port;
if (!split_url(location, host, sizeof(host), path, sizeof(path), &port, &https)) {
snprintf(f->err, sizeof(f->err), "redirect to a URL we cannot parse");
return false;
}
if (https) {
snprintf(f->err, sizeof(f->err), "redirect to HTTPS (TLS unsupported): %s", host);
return false;
}
strcpy(f->host, host);
strcpy(f->path, path);
f->port = port;
return true;
}
// Parse the numeric status out of "HTTP/1.x NNN ...", or 0 if absent.
@@ -96,22 +174,23 @@ static int parse_status(const char* buf, int len) {
// finalize would then read freed memory. Returning the state instead keeps the
// job private to the worker until the atomic hand-off.
// ===========================================================================
static int do_fetch(http_fetch_t* f) {
uint64_t t0 = pit_get_ticks();
#define HTTP_MAX_REDIRECTS 5
// Resolve: a dotted quad is taken as-is, otherwise the DNS resolver runs
// (itself bounded, so an unreachable server times out rather than hangs).
// Perform one request/response against the current host/path/port. Returns the
// HTTP status code, or a negative error code; the raw response lands in f->buf.
static int do_one_request(http_fetch_t* f) {
f->ip = ip_parse(f->host);
if (f->ip == 0) f->ip = dns_resolve(f->host);
if (f->ip == 0) {
snprintf(f->err, sizeof(f->err), "cannot resolve %s", f->host);
return HTTP_STATE_ERROR;
return -1;
}
tcp_conn_t* c = tcp_connect(f->ip, f->port, HTTP_CONNECT_TIMEOUT_MS);
if (!c) {
snprintf(f->err, sizeof(f->err), "connect to :%u refused/timed out", f->port);
return HTTP_STATE_ERROR;
snprintf(f->err, sizeof(f->err), "connect to %s:%u refused/timed out",
f->host, f->port);
return -1;
}
char req[512];
@@ -121,10 +200,9 @@ static int do_fetch(http_fetch_t* f) {
if (tcp_send(c, req, (uint16_t)rl) < 0) {
snprintf(f->err, sizeof(f->err), "send failed");
tcp_close(c);
return HTTP_STATE_ERROR;
return -1;
}
// Drain the response until the peer closes or the buffer fills.
f->len = 0;
while (f->len < f->cap - 1) {
int n = tcp_recv(c, f->buf + f->len, (uint16_t)(f->cap - 1 - f->len),
@@ -135,14 +213,51 @@ static int do_fetch(http_fetch_t* f) {
f->buf[f->len] = '\0';
tcp_close(c);
f->status_code = parse_status(f->buf, f->len);
f->elapsed_ms = (uint32_t)((pit_get_ticks() - t0) * 10);
if (f->len == 0) {
snprintf(f->err, sizeof(f->err), "no data received");
return -1;
}
return parse_status(f->buf, f->len);
}
// Fills in the result fields but deliberately does NOT touch f->state:
// publishing the terminal state is finalize's job, done under interrupts-off.
// Returning the state keeps the job private to the worker until that hand-off.
//
// A 3xx response with a Location header is followed, up to a bounded number of
// hops, so a real-world URL that redirects (a bare host to a path, http to a
// canonical host) resolves to actual content. An https target is reported
// rather than followed, since this client has no TLS.
static int do_fetch(http_fetch_t* f) {
uint64_t t0 = pit_get_ticks();
f->redirects = 0;
// The client has no TLS, so an https URL cannot be served. Say so plainly
// rather than sending a plaintext request to port 443 and reporting a
// confusing timeout.
if (f->tls) {
snprintf(f->err, sizeof(f->err), "%s is HTTPS; this client has no TLS", f->host);
return HTTP_STATE_ERROR;
}
return HTTP_STATE_DONE;
for (;;) {
int status = do_one_request(f);
if (status < 0) return HTTP_STATE_ERROR; // f->err already set
bool is_redirect = (status == 301 || status == 302 || status == 303 ||
status == 307 || status == 308);
char location[512];
if (is_redirect && f->redirects < HTTP_MAX_REDIRECTS &&
find_header(f->buf, f->len, "Location", location, sizeof(location))) {
if (!apply_redirect(f, location)) return HTTP_STATE_ERROR; // e.g. HTTPS
f->redirects++;
continue;
}
f->status_code = status;
f->elapsed_ms = (uint32_t)((pit_get_ticks() - t0) * 10);
return HTTP_STATE_DONE;
}
}
static void free_job(http_fetch_t* f) {
+82
View File
@@ -8,6 +8,7 @@
#include <net/udp.h>
#include <net/tcp.h>
#include <net/http_client.h>
#include <kernel/sched.h>
#include <net/socket.h>
#include <drivers/e1000.h>
#include <drivers/ata.h>
@@ -2488,6 +2489,86 @@ static int applet_webget(int argc, char** argv) {
return 0;
}
// End-to-end connectivity check: link, address, gateway, DNS, and a real HTTP
// fetch, each reported PASS/FAIL. This is the one-command answer to "is the
// internet working?" -- it exercises every layer the stack depends on.
static int applet_netcheck(int argc, char** argv) {
const char* probe = (argc >= 2) ? argv[1] : "example.com";
printk(ANSI_BRIGHT_CYAN "=== SUB-OS Connectivity Check ===\n" ANSI_RESET);
#define OKMARK ANSI_BRIGHT_GREEN "[ ok ]" ANSI_RESET
#define FAILMARK ANSI_BRIGHT_RED "[fail]" ANSI_RESET
int failures = 0;
// 1. Physical link.
bool link = e1000_is_link_up();
printk(" %s link eth0 %s\n", link ? OKMARK : FAILMARK,
link ? "UP" : "DOWN");
if (!link) failures++;
// 2. Interface address.
net_if_t* nif = net_get_primary_if();
bool has_ip = nif && nif->ip != 0;
char ips[16], gws[16], dnss[16];
ip_to_str(nif ? nif->ip : 0, ips);
ip_to_str(nif ? nif->gateway : 0, gws);
ip_to_str(nif ? nif->dns : 0, dnss);
printk(" %s address %s gw %s dns %s\n", has_ip ? OKMARK : FAILMARK,
ips, gws, dnss);
if (!has_ip) failures++;
// 3. Gateway reachability (one quiet ping).
int recv = (nif && nif->gateway) ? net_ping(nif->gateway, 1, 1000) : 0;
printk(" %s gateway %s %s\n", recv > 0 ? OKMARK : FAILMARK, gws,
recv > 0 ? "reachable" : "no reply");
if (recv <= 0) failures++;
// 4. DNS resolution.
uint32_t rip = dns_resolve(probe);
char rips[16];
ip_to_str(rip, rips);
printk(" %s dns %s -> %s\n", rip ? OKMARK : FAILMARK, probe,
rip ? rips : "no answer");
if (!rip) failures++;
// 5. A real HTTP fetch, on the async worker, polled to completion.
char url[160];
snprintf(url, sizeof(url), "%s/", probe);
http_fetch_t* f = http_fetch_start(url, 8192);
if (!f) {
printk(" %s http could not start fetch\n", FAILMARK);
failures++;
} else {
uint64_t t0 = pit_get_ticks();
while (f->state == HTTP_STATE_RUNNING && (pit_get_ticks() - t0) < 1500) {
sched_yield();
}
if (f->state == HTTP_STATE_DONE && f->status_code > 0) {
printk(" %s http GET %s -> %d, %d bytes, %u ms%s\n", OKMARK,
probe, f->status_code, f->len, f->elapsed_ms,
f->redirects ? " (redirected)" : "");
} else {
printk(" %s http %s\n", FAILMARK,
f->state == HTTP_STATE_ERROR ? f->err : "timed out");
failures++;
}
http_fetch_release(f);
}
if (failures == 0) {
printk(ANSI_BRIGHT_GREEN "\n Internet: ONLINE -- all checks passed.\n" ANSI_RESET);
} else {
printk(ANSI_YELLOW "\n Internet: DEGRADED -- %d check(s) failed.\n" ANSI_RESET,
failures);
}
#undef OKMARK
#undef FAILMARK
return failures == 0 ? 0 : 1;
}
// A minimal HTTP/1.0 client over the active-open path.
static int applet_httpget(int argc, char** argv) {
if (argc < 2) {
@@ -2860,6 +2941,7 @@ static const lazybox_applet_t applets[] = {
{"tcpconnect", applet_tcpconnect, "tcpconnect <host> <port> [text]", "Open an outbound TCP stream", "Network"},
{"httpget", applet_httpget, "httpget <host> [path] [port]", "Fetch a URL over TCP", "Network"},
{"webget", applet_webget, "webget <url>", "Fetch a URL via the async worker", "Network"},
{"netcheck", applet_netcheck, "netcheck [host]", "End-to-end internet connectivity test", "Network"},
{"tcpserve", applet_tcpserve, "tcpserve <port>", "Accept one inbound TCP stream", "Network"},
// Storage & Devices