summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-09-28 10:45:49 +0300
committerPetri Hienonen <petri.hienonen@gmail.com>2025-09-28 10:45:49 +0300
commit650138aae6a2d6ae26f57a22056bdf0ea5fa8c77 (patch)
tree6b90cc177702c24b399b078b0f0014fc4e3af614
downloadnetwork-650138aae6a2d6ae26f57a22056bdf0ea5fa8c77.tar.zst
Initial commit
-rw-r--r--index.html80
-rw-r--r--main.go72
-rw-r--r--network-ui.service18
-rw-r--r--requirements.json62
4 files changed, 232 insertions, 0 deletions
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..618f523
--- /dev/null
+++ b/index.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <title>Systemd-networkd Control Panel</title>
+ <script type="module" src="https://unpkg.com/@fluentui/web-components@2.6.1/dist/web-components.min.js"></script>
+ <style>
+ body {
+ font-family: Arial, sans-serif;
+ margin: 2em;
+ }
+
+ section {
+ margin-bottom: 2em;
+ }
+
+ pre {
+ background: #f5f5f5;
+ padding: 1em;
+ border-radius: 6px;
+ overflow-x: auto;
+ }
+ </style>
+</head>
+
+<body>
+
+ <h1>Systemd-networkd Control Panel</h1>
+ <p>This page shows network status, logs, and allows you to restart network services or reboot the device.
+ For more details see <a href="https://www.freedesktop.org/software/systemd/man/systemd-networkd.service.html"
+ target="_blank">systemd-networkd documentation</a>.</p>
+
+ <section>
+ <h2>1. Network Status</h2>
+ <p>The command <code>networkctl status --json=short</code> shows current interfaces, addresses, and routes.</p>
+ <fluent-button appearance="accent" onclick="loadStatus()">Refresh Status</fluent-button>
+ <pre id="status">[Waiting for data]</pre>
+ </section>
+
+ <section>
+ <h2>2. Network Logs</h2>
+ <p>Logs come from <code>journalctl -u systemd-networkd.service</code>. This helps diagnose DHCP and link issues.
+ </p>
+ <fluent-button onclick="loadLogs()">Show Logs</fluent-button>
+ <pre id="logs">[Waiting for logs]</pre>
+ </section>
+
+ <section>
+ <h2>3. Manage Services</h2>
+ <p>
+ - Reload networkd after configuration changes.<br>
+ - Reboot device if required.
+ </p>
+ <fluent-button appearance="accent" onclick="reloadNetworkd()">Restart networkd</fluent-button>
+ <fluent-button appearance="accent" onclick="rebootDevice()">Reboot device</fluent-button>
+ </section>
+
+ <script>
+ async function loadStatus() {
+ let res = await fetch('/api/status');
+ document.getElementById('status').textContent = await res.text();
+ }
+ async function loadLogs() {
+ let res = await fetch('/api/logs');
+ document.getElementById('logs').textContent = await res.text();
+ }
+ async function reloadNetworkd() {
+ let res = await fetch('/api/reload', {method: 'POST'});
+ alert(await res.text());
+ }
+ async function rebootDevice() {
+ let res = await fetch('/api/reboot', {method: 'POST'});
+ alert(await res.text());
+ }
+ </script>
+
+</body>
+
+</html>
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..046ad91
--- /dev/null
+++ b/main.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "encoding/json"
+ "html/template"
+ "log"
+ "net/http"
+ "os/exec"
+)
+
+// Templates
+var tpl = template.Must(template.ParseFiles("index.html"))
+
+// Handlers
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+ tpl.Execute(w, nil)
+}
+
+// Run `networkctl status --json=short`
+func statusHandler(w http.ResponseWriter, r *http.Request) {
+ cmd := exec.Command("networkctl", "status", "--json=short")
+ out, err := cmd.Output()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.Write(out)
+}
+
+// Run `journalctl -u systemd-networkd.service --no-pager -n 200`
+func logsHandler(w http.ResponseWriter, r *http.Request) {
+ cmd := exec.Command("journalctl", "-u", "systemd-networkd.service", "--no-pager", "-n", "200")
+ out, err := cmd.Output()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write(out)
+}
+
+// Reload systemd-networkd
+func reloadHandler(w http.ResponseWriter, r *http.Request) {
+ cmd := exec.Command("systemctl", "restart", "systemd-networkd")
+ if err := cmd.Run(); err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ json.NewEncoder(w).Encode(map[string]string{"status": "restarted"})
+}
+
+// Reboot the system
+func rebootHandler(w http.ResponseWriter, r *http.Request) {
+ cmd := exec.Command("systemctl", "reboot")
+ if err := cmd.Start(); err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ json.NewEncoder(w).Encode(map[string]string{"status": "rebooting"})
+}
+
+func main() {
+ http.HandleFunc("/", indexHandler)
+ http.HandleFunc("/api/status", statusHandler)
+ http.HandleFunc("/api/logs", logsHandler)
+ http.HandleFunc("/api/reload", reloadHandler)
+ http.HandleFunc("/api/reboot", rebootHandler)
+
+ log.Println("Listening on :8080")
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
diff --git a/network-ui.service b/network-ui.service
new file mode 100644
index 0000000..6b3f0f8
--- /dev/null
+++ b/network-ui.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=Web UI for systemd-networkd
+After=network.target
+
+[Service]
+ExecStart=/usr/local/bin/network-ui
+WorkingDirectory=/var/lib/network-ui
+User=network-ui
+Group=network-ui
+ProtectSystem=strict
+ProtectHome=yes
+NoNewPrivileges=yes
+PrivateTmp=yes
+AmbientCapabilities=CAP_SYS_BOOT CAP_NET_ADMIN
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/requirements.json b/requirements.json
new file mode 100644
index 0000000..3e0b728
--- /dev/null
+++ b/requirements.json
@@ -0,0 +1,62 @@
+{
+ "project": "systemd-networkd Web UI",
+ "version": "0.1",
+ "requirements": {
+ "functional": [
+ {
+ "id": "F-001",
+ "description": "The system shall provide a web-based dashboard to display current network status (interfaces, addresses, routes) by invoking `networkctl status --json=short`."
+ },
+ {
+ "id": "F-002",
+ "description": "The system shall provide access to logs of `systemd-networkd.service` via `journalctl`."
+ },
+ {
+ "id": "F-003",
+ "description": "The system shall provide controls to restart `systemd-networkd` without rebooting the device."
+ },
+ {
+ "id": "F-004",
+ "description": "The system shall provide a control to reboot the device."
+ },
+ {
+ "id": "F-005",
+ "description": "The system shall present contextual teaching information (e.g., IPv4 basics, logs explanation, restart rationale) directly within the web interface."
+ },
+ {
+ "id": "F-006",
+ "description": "The system shall use a single-page design with minimal dependencies (HTML + Fluent UI web components from CDN)."
+ }
+ ],
+ "technical": [
+ {
+ "id": "T-001",
+ "description": "The backend shall be implemented in Go, exposing HTTP endpoints on port 80."
+ },
+ {
+ "id": "T-002",
+ "description": "The system shall run as a `systemd` service with restricted privileges (User=network-ui, ProtectSystem=strict, NoNewPrivileges=yes)."
+ },
+ {
+ "id": "T-003",
+ "description": "The backend shall execute only specific whitelisted commands: `networkctl`, `journalctl`, `systemctl restart systemd-networkd`, and `systemctl reboot`."
+ },
+ {
+ "id": "T-004",
+ "description": "The frontend shall consume backend endpoints via REST-style APIs returning UTF8-SON"
+ },
+ {
+ "id": "T-005",
+ "description": "The system shall require no external database or file storage beyond access to `systemd` configuration and logs."
+ },
+ {
+ "id": "T-006",
+ "description": "The service shall start automatically at boot and remain persistent under `systemd` supervision."
+ },
+ {
+ "id": "T-007",
+ "description": "The design shall ensure sandboxing: no direct shell access beyond intended commands, no write access to arbitrary filesystem locations."
+ }
+ ]
+ }
+}