patches and low-level development discussion
 help / color / mirror / code / Atom feed
* [PATCH v2 0/7] spectrum-router
@ 2025-11-28 22:30 Yureka Lilian
  2025-11-28 22:30 ` [PATCH v2 1/7] vm/sys/net: remove connman Yureka Lilian
                   ` (6 more replies)
  0 siblings, 7 replies; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

Changes since v1 (non-exhaustive):

- Use spectrum-router from pkgsMusl
- Refactor router into smaller modules
- Parse router advertisements from upstream interfaces
- Select active upstream interface based on router advertisements
- Assign guest mac addresses based hash of vm id
- Fix race condition when assigning driver-router interface
- Temporarily disable re-write of mac addresses which would be needed
  for wifi. It will be added back inside the xdp-forwarder in a future patch.

and completely new patches:
- Change nameserver & default route in app-vm template
- Fix integration tests
- Leave dbus in net-vm and add iwd for testing


Yureka Lilian (7):
  vm/sys/net: remove connman
  vm/sys/net: integrate xdp-forwarder
  vm/sys/net: add iwd
  tools: add spectrum-router
  host: integrate router
  img/app: change to ipv6 nameserver
  checks/integration: Adapt networking test for ipv6

 host/rootfs/default.nix                       |   4 +-
 host/rootfs/file-list.mk                      |   3 +
 .../data/service/spectrum-router/down         |   0
 .../template/data/service/spectrum-router/run |  13 +
 .../image/usr/bin/assign-driver-router-iface  |  11 +
 host/rootfs/image/usr/bin/run-vmm             |  12 +-
 host/rootfs/image/usr/bin/vm-import           |  13 -
 img/app/image/etc/mdev/iface                  |  17 -
 img/app/image/etc/resolv.conf                 |   2 +-
 pkgs/default.nix                              |   2 +
 pkgs/overlay.nix                              |   1 +
 release/checks/integration/default.nix        |   2 +-
 release/checks/integration/networking.c       |  32 +-
 tools/router/Cargo.lock                       | 807 ++++++++++++++++++
 tools/router/Cargo.lock.license               |   2 +
 tools/router/Cargo.toml                       |  21 +
 tools/router/default.nix                      |  18 +
 tools/router/src/main.rs                      |  73 ++
 tools/router/src/packet.rs                    | 187 ++++
 tools/router/src/protocol.rs                  |  65 ++
 tools/router/src/router.rs                    | 133 +++
 tools/router/src/upstream.rs                  | 170 ++++
 tools/start-vmm/ch.rs                         |  38 +-
 tools/start-vmm/lib.rs                        |  76 +-
 tools/start-vmm/meson.build                   |   2 +-
 tools/start-vmm/net-util.c                    |  39 -
 tools/start-vmm/net-util.h                    |   6 -
 tools/start-vmm/net.c                         |  55 --
 tools/start-vmm/net.rs                        |  11 -
 tools/start-vmm/tests/meson.build             |   5 -
 .../start-vmm/tests/tap_open-name-too-long.c  |  20 -
 tools/start-vmm/tests/tap_open.c              |  28 -
 vm/sys/net/Makefile                           |   2 +-
 vm/sys/net/default.nix                        |  15 +-
 vm/sys/net/file-list.mk                       |   3 -
 vm/sys/net/image/etc/fstab                    |   2 +
 vm/sys/net/image/etc/mdev/iface               |  28 +-
 vm/sys/net/image/etc/nftables.conf            |  16 +-
 vm/sys/net/image/etc/s6-rc/connman/run        |  19 -
 vm/sys/net/image/etc/s6-rc/connman/type       |   1 -
 .../net/image/etc/s6-rc/connman/type.license  |   2 -
 41 files changed, 1627 insertions(+), 329 deletions(-)
 rename vm/sys/net/image/etc/s6-rc/connman/dependencies.d/dbus => host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down (100%)
 create mode 100755 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
 create mode 100755 host/rootfs/image/usr/bin/assign-driver-router-iface
 create mode 100644 tools/router/Cargo.lock
 create mode 100644 tools/router/Cargo.lock.license
 create mode 100644 tools/router/Cargo.toml
 create mode 100644 tools/router/default.nix
 create mode 100644 tools/router/src/main.rs
 create mode 100644 tools/router/src/packet.rs
 create mode 100644 tools/router/src/protocol.rs
 create mode 100644 tools/router/src/router.rs
 create mode 100644 tools/router/src/upstream.rs
 delete mode 100644 tools/start-vmm/net-util.c
 delete mode 100644 tools/start-vmm/net-util.h
 delete mode 100644 tools/start-vmm/net.c
 delete mode 100644 tools/start-vmm/tests/tap_open-name-too-long.c
 delete mode 100644 tools/start-vmm/tests/tap_open.c
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/run
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/type
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/type.license

-- 
2.51.2


^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH v2 1/7] vm/sys/net: remove connman
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:06   ` Alyssa Ross
  2025-11-28 22:30 ` [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder Yureka Lilian
                   ` (5 subsequent siblings)
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

In preparation to integrating xdp-forwarder, making the net-vm a net-driver VM.
dbus is left, as it will likely be needed in the future for iwd.

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 vm/sys/net/Makefile                           |  2 +-
 vm/sys/net/default.nix                        |  8 +++-----
 vm/sys/net/file-list.mk                       |  3 ---
 .../etc/s6-rc/connman/dependencies.d/dbus     |  0
 vm/sys/net/image/etc/s6-rc/connman/run        | 19 -------------------
 vm/sys/net/image/etc/s6-rc/connman/type       |  1 -
 .../net/image/etc/s6-rc/connman/type.license  |  2 --
 7 files changed, 4 insertions(+), 31 deletions(-)
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/dependencies.d/dbus
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/run
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/type
 delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/type.license

diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile
index d71c232..7ad5e5c 100644
--- a/vm/sys/net/Makefile
+++ b/vm/sys/net/Makefile
@@ -29,7 +29,7 @@ $(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdis
 	    build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root
 	mv $@.tmp $@
 
-DIRS = dev etc/s6-linux-init/env proc run sys var/lib/connman
+DIRS = dev etc/s6-linux-init/env proc run sys
 
 BUILD_FILES = build/etc/s6-rc
 
diff --git a/vm/sys/net/default.nix b/vm/sys/net/default.nix
index de273e5..ae7fdc5 100644
--- a/vm/sys/net/default.nix
+++ b/vm/sys/net/default.nix
@@ -7,7 +7,7 @@ pkgsMusl.callPackage (
 
 { lib, stdenvNoCC, nixos, runCommand, writeClosure
 , erofs-utils, jq, s6-rc, util-linux, xorg
-, busybox, connmanMinimal, dbus, execline, kmod, linux_latest, mdevd, nftables
+, busybox, dbus, execline, kmod, linux_latest, mdevd, nftables
 , s6, s6-linux-init
 }:
 
@@ -51,10 +51,8 @@ let
     ];
   });
 
-  connman = connmanMinimal;
-
   packages = [
-    connman dbus execline kmod mdevd s6 s6-linux-init s6-rc
+    dbus execline kmod mdevd s6 s6-linux-init s6-rc
 
     (busybox.override {
       extraConfig = ''
@@ -73,7 +71,7 @@ let
 
   # Packages that should be fully linked into /usr,
   # (not just their bin/* files).
-  usrPackages = [ connman dbus firmware kernel.modules terminfo ];
+  usrPackages = [ dbus firmware kernel.modules terminfo ];
 
   packagesSysroot = runCommand "packages-sysroot" {
     inherit packages;
diff --git a/vm/sys/net/file-list.mk b/vm/sys/net/file-list.mk
index 15ccd24..7cc3520 100644
--- a/vm/sys/net/file-list.mk
+++ b/vm/sys/net/file-list.mk
@@ -20,9 +20,6 @@ LINKS = \
 	image/var/run
 
 S6_RC_FILES = \
-	image/etc/s6-rc/connman/dependencies.d/dbus \
-	image/etc/s6-rc/connman/run \
-	image/etc/s6-rc/connman/type \
 	image/etc/s6-rc/dbus/notification-fd \
 	image/etc/s6-rc/dbus/run \
 	image/etc/s6-rc/dbus/type \
diff --git a/vm/sys/net/image/etc/s6-rc/connman/dependencies.d/dbus b/vm/sys/net/image/etc/s6-rc/connman/dependencies.d/dbus
deleted file mode 100644
index e69de29..0000000
diff --git a/vm/sys/net/image/etc/s6-rc/connman/run b/vm/sys/net/image/etc/s6-rc/connman/run
deleted file mode 100644
index 058fc17..0000000
--- a/vm/sys/net/image/etc/s6-rc/connman/run
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/execlineb -P
-# SPDX-License-Identifier: EUPL-1.2+
-# SPDX-FileCopyrightText: 2020-2021 Alyssa Ross <hi@alyssa.is>
-
-if { modprobe af_packet }
-
-backtick -E HARDWARE_INTERFACES {
-  pipeline {
-    find -L /sys/class/net -mindepth 2 -maxdepth 2 -name address -print0
-  }
-
-  # Filter out other VMs and the loopback device.
-  pipeline { xargs -0 grep -iL ^\\(02:01:\\|00:00:00:00:00:00$\\) }
-
-  # Extract the interface names from the address file paths.
-  awk -F/ "{if (NR > 1) printf \",\"; printf \"%s\", $5}"
-}
-
-connmand -ni $HARDWARE_INTERFACES
diff --git a/vm/sys/net/image/etc/s6-rc/connman/type b/vm/sys/net/image/etc/s6-rc/connman/type
deleted file mode 100644
index 5883cff..0000000
--- a/vm/sys/net/image/etc/s6-rc/connman/type
+++ /dev/null
@@ -1 +0,0 @@
-longrun
diff --git a/vm/sys/net/image/etc/s6-rc/connman/type.license b/vm/sys/net/image/etc/s6-rc/connman/type.license
deleted file mode 100644
index 2b3b032..0000000
--- a/vm/sys/net/image/etc/s6-rc/connman/type.license
+++ /dev/null
@@ -1,2 +0,0 @@
-SPDX-License-Identifier: CC0-1.0
-SPDX-FileCopyrightText: 2020 Alyssa Ross <hi@alyssa.is>
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
  2025-11-28 22:30 ` [PATCH v2 1/7] vm/sys/net: remove connman Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:08   ` Alyssa Ross
  2025-11-28 22:30 ` [PATCH v2 3/7] vm/sys/net: add iwd Yureka Lilian
                   ` (4 subsequent siblings)
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 vm/sys/net/default.nix             | 11 ++++++++---
 vm/sys/net/image/etc/fstab         |  2 ++
 vm/sys/net/image/etc/mdev/iface    | 28 +++++++++-------------------
 vm/sys/net/image/etc/nftables.conf | 16 ++++++++++++----
 4 files changed, 31 insertions(+), 26 deletions(-)

diff --git a/vm/sys/net/default.nix b/vm/sys/net/default.nix
index ae7fdc5..f4e51dd 100644
--- a/vm/sys/net/default.nix
+++ b/vm/sys/net/default.nix
@@ -8,7 +8,7 @@ pkgsMusl.callPackage (
 { lib, stdenvNoCC, nixos, runCommand, writeClosure
 , erofs-utils, jq, s6-rc, util-linux, xorg
 , busybox, dbus, execline, kmod, linux_latest, mdevd, nftables
-, s6, s6-linux-init
+, s6, s6-linux-init, xdp-tools, spectrum-driver-tools
 }:
 
 let
@@ -52,7 +52,7 @@ let
   });
 
   packages = [
-    dbus execline kmod mdevd s6 s6-linux-init s6-rc
+    dbus execline kmod mdevd s6 s6-linux-init s6-rc xdp-tools
 
     (busybox.override {
       extraConfig = ''
@@ -71,7 +71,12 @@ let
 
   # Packages that should be fully linked into /usr,
   # (not just their bin/* files).
-  usrPackages = [ dbus firmware kernel.modules terminfo ];
+  usrPackages = [
+    dbus firmware kernel.modules terminfo
+
+    # for xdp-forwarder
+    spectrum-driver-tools
+  ];
 
   packagesSysroot = runCommand "packages-sysroot" {
     inherit packages;
diff --git a/vm/sys/net/image/etc/fstab b/vm/sys/net/image/etc/fstab
index 5dc9b2a..a0b62fa 100644
--- a/vm/sys/net/image/etc/fstab
+++ b/vm/sys/net/image/etc/fstab
@@ -1,6 +1,8 @@
 # SPDX-License-Identifier: CC0-1.0
 # SPDX-FileCopyrightText: 2020-2021 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 proc	/proc		proc	defaults	0	0
 devpts	/dev/pts	devpts	gid=5,mode=620	0	0
 tmpfs	/dev/shm	tmpfs	defaults	0	0
 sysfs	/sys		sysfs	defaults	0	0
+bpffs	/sys/fs/bpf	bpf	defaults	0	0
diff --git a/vm/sys/net/image/etc/mdev/iface b/vm/sys/net/image/etc/mdev/iface
index 2306575..da45ce9 100755
--- a/vm/sys/net/image/etc/mdev/iface
+++ b/vm/sys/net/image/etc/mdev/iface
@@ -1,36 +1,26 @@
 #!/bin/execlineb -P
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2020-2021 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 
 importas -Si INTERFACE
 
 ifte
 
 {
-  # This interface is connected to another VM.
-
-  # The other VM's IP is encoded in the NIC-specific portion of the
-  # interface's MAC address.
-  backtick -E CLIENT_IP {
-    awk -F: "{printf \"100.64.%d.%d\\n\", \"0x\" $5, \"0x\" $6}"
-    /sys/class/net/${INTERFACE}/address
-  }
-
-  if { ip address add 169.254.0.1/32 dev $INTERFACE }
-  if { ip link set $INTERFACE up }
-  ip route add $CLIENT_IP dev $INTERFACE
+  # This interface is connected to the router
+  if { xdp-loader load $INTERFACE /usr/lib/xdp/prog_router.o -m skb -p /sys/fs/bpf }
+  if { ip link set $INTERFACE promisc on }
+  if { set-router-iface $INTERFACE }
+  ip link set $INTERFACE up
 }
 
 {
   if { test $INTERFACE != lo }
   # This is a physical connection to a network device.
-  background { s6-rc -bu change connman }
-  if { s6-rc -bu change nftables }
-  if {
-    forx -pE module { nft_counter nft_masq }
-    modprobe $module
-  }
-  nft add rule ip nat postrouting oifname $INTERFACE counter masquerade
+  if { xdp-loader load $INTERFACE /usr/lib/xdp/prog_physical.o -m skb -p /sys/fs/bpf }
+  if { ip link set $INTERFACE promisc on }
+  ip link set $INTERFACE up
 }
 
 grep -iq ^02:01: /sys/class/net/${INTERFACE}/address
diff --git a/vm/sys/net/image/etc/nftables.conf b/vm/sys/net/image/etc/nftables.conf
index 296d92c..cc8e462 100644
--- a/vm/sys/net/image/etc/nftables.conf
+++ b/vm/sys/net/image/etc/nftables.conf
@@ -1,8 +1,16 @@
 # SPDX-License-Identifier: EUPL-1.2+
-# SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 
-table nat {
-	chain postrouting {
-		type nat hook postrouting priority 100;
+table driver-fw {
+	chain input {
+		type filter hook input priority filter; policy drop;
+	}
+
+	chain output {
+		type filter hook output priority filter; policy drop;
+	}
+
+	chain forward {
+		type filter hook forward priority filter; policy drop;
 	}
 }
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH v2 3/7] vm/sys/net: add iwd
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
  2025-11-28 22:30 ` [PATCH v2 1/7] vm/sys/net: remove connman Yureka Lilian
  2025-11-28 22:30 ` [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:09   ` Alyssa Ross
  2025-11-28 22:30 ` [PATCH v2 4/7] tools: add spectrum-router Yureka Lilian
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

This allows developers to manually start the iwd daemon and connect to a
wireless network, to confirm that the rest of the stack works correctly
when used with Wi-Fi.

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 vm/sys/net/default.nix | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/vm/sys/net/default.nix b/vm/sys/net/default.nix
index f4e51dd..889e5ec 100644
--- a/vm/sys/net/default.nix
+++ b/vm/sys/net/default.nix
@@ -8,7 +8,7 @@ pkgsMusl.callPackage (
 { lib, stdenvNoCC, nixos, runCommand, writeClosure
 , erofs-utils, jq, s6-rc, util-linux, xorg
 , busybox, dbus, execline, kmod, linux_latest, mdevd, nftables
-, s6, s6-linux-init, xdp-tools, spectrum-driver-tools
+, s6, s6-linux-init, xdp-tools, spectrum-driver-tools, iwd
 }:
 
 let
@@ -72,7 +72,7 @@ let
   # Packages that should be fully linked into /usr,
   # (not just their bin/* files).
   usrPackages = [
-    dbus firmware kernel.modules terminfo
+    dbus firmware kernel.modules terminfo iwd
 
     # for xdp-forwarder
     spectrum-driver-tools
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH v2 4/7] tools: add spectrum-router
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
                   ` (2 preceding siblings ...)
  2025-11-28 22:30 ` [PATCH v2 3/7] vm/sys/net: add iwd Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:18   ` Alyssa Ross
  2025-11-28 22:30 ` [PATCH v2 5/7] host: integrate router Yureka Lilian
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

The tokio-vhost & vhost-device-net crates which we also wrote and depend
on are left external in the outlook of becoming a rust-vmm project soon.

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 pkgs/default.nix                |   2 +
 tools/router/Cargo.lock         | 807 ++++++++++++++++++++++++++++++++
 tools/router/Cargo.lock.license |   2 +
 tools/router/Cargo.toml         |  21 +
 tools/router/default.nix        |  18 +
 tools/router/src/main.rs        |  73 +++
 tools/router/src/packet.rs      | 187 ++++++++
 tools/router/src/protocol.rs    |  65 +++
 tools/router/src/router.rs      | 133 ++++++
 tools/router/src/upstream.rs    | 170 +++++++
 10 files changed, 1478 insertions(+)
 create mode 100644 tools/router/Cargo.lock
 create mode 100644 tools/router/Cargo.lock.license
 create mode 100644 tools/router/Cargo.toml
 create mode 100644 tools/router/default.nix
 create mode 100644 tools/router/src/main.rs
 create mode 100644 tools/router/src/packet.rs
 create mode 100644 tools/router/src/protocol.rs
 create mode 100644 tools/router/src/router.rs
 create mode 100644 tools/router/src/upstream.rs

diff --git a/pkgs/default.nix b/pkgs/default.nix
index 0d159c9..c7311cb 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -52,6 +52,8 @@ let
     xdg-desktop-portal-spectrum-host =
       self.callSpectrumPackage ../tools/xdg-desktop-portal-spectrum-host {};
 
+    spectrum-router = self.callSpectrumPackage ../tools/router {};
+
     # Packages from the overlay, so it's possible to build them from
     # the CLI easily.
     inherit (pkgs) cloud-hypervisor dbus;
diff --git a/tools/router/Cargo.lock b/tools/router/Cargo.lock
new file mode 100644
index 0000000..4399532
--- /dev/null
+++ b/tools/router/Cargo.lock
@@ -0,0 +1,807 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+
+[[package]]
+name = "clap"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "env_filter"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "jiff"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "mio"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "regex"
+version = "1.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "socket2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "spectrum-router"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "clap",
+ "env_logger",
+ "futures-util",
+ "log",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "vhost-device-net",
+ "vm-memory",
+ "zerocopy",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.110"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio"
+version = "1.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-eventfd"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e66bd133670ac39baa1aca5c3a86709f4595c08ca4464a1e1400b83d62c0639"
+dependencies = [
+ "futures-lite",
+ "libc",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-vhost"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21d64b3e4d573da90b2bb040d69a9c2d754e8a3ab9d9ecf04a268748c99f1cd3"
+dependencies = [
+ "async-stream",
+ "bitvec",
+ "futures-util",
+ "libc",
+ "log",
+ "tokio",
+ "tokio-eventfd",
+ "virtio-queue",
+ "vm-memory",
+ "zerocopy",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "vhost-device-net"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac05caccd6d484f672551a187f7110ff9d32edd6a39bb16bb04f53017b1e6fd0"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tokio-vhost",
+ "vm-memory",
+]
+
+[[package]]
+name = "virtio-bindings"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "804f498a26d5a63be7bbb8bdcd3869c3f286c4c4a17108905276454da0caf8cb"
+
+[[package]]
+name = "virtio-queue"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0479158f863e59323771a1f684d843962f76960b86fecfec2bfa9c8f0f9180"
+dependencies = [
+ "log",
+ "virtio-bindings",
+ "vm-memory",
+ "vmm-sys-util",
+]
+
+[[package]]
+name = "vm-memory"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd5e56d48353c5f54ef50bd158a0452fc82f5383da840f7b8efc31695dd3b9d"
+dependencies = [
+ "libc",
+ "thiserror",
+ "winapi",
+]
+
+[[package]]
+name = "vmm-sys-util"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/tools/router/Cargo.lock.license b/tools/router/Cargo.lock.license
new file mode 100644
index 0000000..9b37611
--- /dev/null
+++ b/tools/router/Cargo.lock.license
@@ -0,0 +1,2 @@
+SPDX-License-Identifier: CC0-1.0
+SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
diff --git a/tools/router/Cargo.toml b/tools/router/Cargo.toml
new file mode 100644
index 0000000..0b96911
--- /dev/null
+++ b/tools/router/Cargo.toml
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: CC0-1.0
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+[package]
+name = "spectrum-router"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+anyhow = "1.0.100"
+clap = { version = "4.5.45", features = ["derive"] }
+env_logger = "0.11.8"
+log = { version = "0.4.27", features = ["release_max_level_debug"] }
+vhost-device-net = "0.1.0"
+tokio = { version = "1.48.0", features = ["macros", "rt"] }
+futures-util = "0.3.31"
+zerocopy = "0.8.27"
+tokio-stream = "0.1.17"
+arrayvec = "0.7.6"
+vm-memory = "0.16"
+tokio-util = "0.7.17"
diff --git a/tools/router/default.nix b/tools/router/default.nix
new file mode 100644
index 0000000..e70f9ec
--- /dev/null
+++ b/tools/router/default.nix
@@ -0,0 +1,18 @@
+# SPDX-FileCopyrightText: 2024 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+# SPDX-License-Identifier: MIT
+
+import ../../lib/call-package.nix (
+{ src, lib, rustPlatform }:
+
+rustPlatform.buildRustPackage {
+  name = "spectrum-router";
+
+  src = lib.fileset.toSource {
+    root = ../..;
+    fileset = lib.fileset.intersection src ./.;
+  };
+  sourceRoot = "source/tools/router";
+
+  cargoLock.lockFile = ./Cargo.lock;
+}) (_: {})
diff --git a/tools/router/src/main.rs b/tools/router/src/main.rs
new file mode 100644
index 0000000..e3aca65
--- /dev/null
+++ b/tools/router/src/main.rs
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+pub(crate) mod packet;
+pub(crate) mod protocol;
+mod router;
+mod upstream;
+
+use std::path::PathBuf;
+
+use packet::*;
+use router::{InterfaceId, Router};
+use upstream::Upstream;
+
+use clap::Parser;
+use futures_util::{SinkExt, TryStreamExt};
+use log::{error, info};
+use tokio::net::UnixListener;
+use vhost_device_net::{IncomingPacket, VhostDeviceNet};
+use vm_memory::GuestMemoryMmap;
+
+#[derive(Parser, Debug)]
+#[command()] //version = None, about = None, long_about = None)]
+struct Args {
+    #[arg(long)]
+    driver_listen_path: PathBuf,
+    #[arg(long)]
+    app_listen_path: PathBuf,
+}
+
+fn main() -> anyhow::Result<()> {
+    env_logger::init();
+    let args = Args::parse();
+
+    for path in [&args.driver_listen_path, &args.app_listen_path] {
+        let _ = std::fs::remove_file(path);
+    }
+
+    run_router(args)
+}
+#[tokio::main(flavor = "current_thread")]
+async fn run_router(args: Args) -> anyhow::Result<()> {
+    let app_listener = UnixListener::bind(&args.app_listen_path)?;
+    let driver_listener = UnixListener::bind(&args.driver_listen_path)?;
+
+    let mut router = Router::<GuestMemoryMmap>::new(InterfaceId::Upstream);
+
+    let (mut upstream, upstream_tx, upstream_rx) = Upstream::new(driver_listener);
+    router.add_iface(InterfaceId::Upstream, upstream_tx, upstream_rx);
+
+    tokio::spawn(async move { upstream.run().await });
+
+    let mut app_num = 0;
+
+    loop {
+        tokio::select! {
+            app_conn = app_listener.accept() => {
+                info!("app connected");
+                match app_conn {
+                    Ok((stream, _addr)) => {
+                        let device = VhostDeviceNet::from_unix_stream(stream).await?;
+                        let stream = Box::pin(device.tx().await?.map_ok(|buf| Packet::Incoming { buf: Some(buf), decap_vlan: false }));
+                        let sink = Box::pin(device.rx().await?.with(|packet: Packet<IncomingPacket<GuestMemoryMmap>>| async move { Ok(packet.out(None)?.into_reader()) }));
+                        router.add_iface(InterfaceId::App(app_num), stream, sink);
+                        app_num = app_num.checked_add(1).unwrap();
+                    }
+                    Err(e) => error!("app connection failed: {}", e),
+                }
+            }
+            _ = router.run() => {}
+        }
+    }
+}
diff --git a/tools/router/src/packet.rs b/tools/router/src/packet.rs
new file mode 100644
index 0000000..1f14ef1
--- /dev/null
+++ b/tools/router/src/packet.rs
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+use std::io::{self, Chain, Cursor, Read};
+
+use crate::protocol::*;
+
+use arrayvec::ArrayVec;
+use zerocopy::*;
+
+pub enum PacketData<R> {
+    Incoming(R),
+    Bytes(Cursor<Box<[u8]>>),
+}
+
+impl<R: Read> Read for PacketData<R> {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        match self {
+            PacketData::Incoming(r) => r.read(buf),
+            PacketData::Bytes(b) => b.read(buf),
+        }
+    }
+}
+
+impl<R: Read> PacketData<R> {
+    pub fn full_packet(&mut self) -> &[u8] {
+        match self {
+            PacketData::Bytes(b) => b.get_ref().as_ref(),
+            PacketData::Incoming(r) => {
+                let mut buf = vec![];
+                r.read_to_end(&mut buf).unwrap();
+                *self = PacketData::Bytes(Cursor::new(buf.into_boxed_slice()));
+                let PacketData::Bytes(b) = self else {
+                    unreachable!()
+                };
+                b.get_ref().as_ref()
+            }
+        }
+    }
+}
+
+pub enum Packet<R> {
+    /// The packet has not been looked at / read into our memory yet
+    Incoming { decap_vlan: bool, buf: Option<R> },
+    /// We've read the head of the packet to look at the headers.
+    Peek {
+        decap_vlan: bool,
+        peek: ArrayVec<u8, 64>,
+        buf: PacketData<R>,
+    },
+}
+
+pub struct PacketHeaders<'a, R> {
+    pub ether_frame: &'a mut EtherFrame,
+    pub vlan_tag: Option<&'a mut VlanTag>,
+    pub ether_type: &'a mut EtherType,
+    pub ipv6_hdr: Option<&'a mut Ipv6Header>,
+    pub peek_slice: &'a mut [u8],
+    pub buf: &'a mut PacketData<R>,
+}
+
+impl<R: Read> Packet<R> {
+    fn peek(
+        &mut self,
+    ) -> (
+        &mut ArrayVec<u8, 64>,
+        &mut PacketData<R>,
+        &mut bool, // decap_vlan
+    ) {
+        match self {
+            Packet::Incoming { buf, decap_vlan } => {
+                let mut buf = std::mem::take(buf).unwrap();
+                // A stack allocation which can keep all headers we are interested in
+                let mut peek = [0u8; 64];
+                // Read the first 64 bytes
+                // 64 >= 14 (ether) + 4 (vlan) + 40 (ipv6) + 4 (icmpv6)
+                let n = buf.read(&mut peek).unwrap();
+
+                let buf = PacketData::Incoming(buf);
+                let mut peek = ArrayVec::from(peek);
+                peek.truncate(n);
+                *self = Packet::Peek {
+                    peek,
+                    buf,
+                    decap_vlan: *decap_vlan,
+                };
+                let Packet::Peek {
+                    peek,
+                    buf,
+                    decap_vlan,
+                } = self
+                else {
+                    unreachable!()
+                };
+                (peek, buf, decap_vlan)
+            }
+            Packet::Peek {
+                peek,
+                buf,
+                decap_vlan,
+            } => (peek, buf, decap_vlan),
+        }
+    }
+    pub fn headers(&mut self) -> io::Result<PacketHeaders<'_, R>> {
+        let (peek, buf, decap_vlan) = self.peek();
+        let peek_slice = peek.as_mut_slice();
+        let (ether_frame, peek_slice) = EtherFrame::mut_from_prefix(peek_slice)
+            .map_err(|_| io::Error::other("packet with <12 bytes"))?;
+        let (ether_type, _) = EtherType::ref_from_prefix(peek_slice)
+            .map_err(|_| io::Error::other("packet with <14 bytes"))?;
+
+        let (vlan_tag, peek_slice) = if *decap_vlan && *ether_type == ETHER_TYPE_802_1Q {
+            let (vlan, peek_slice) = VlanTag::mut_from_prefix(peek_slice)
+                .map_err(|_| io::Error::other("packet with <16 bytes"))?;
+            (Some(vlan), peek_slice)
+        } else {
+            (None, peek_slice)
+        };
+        let (ether_type, peek_slice) = EtherType::mut_from_prefix(peek_slice)
+            .map_err(|_| io::Error::other("packet with <18 bytes"))?;
+
+        let (ipv6_hdr, peek_slice) = if *ether_type == ETHER_TYPE_IPV6 {
+            let (ipv6_hdr, peek_slice) = Ipv6Header::mut_from_prefix(peek_slice)
+                .map_err(|_| io::Error::other("short ipv6 header"))?;
+            (Some(ipv6_hdr), peek_slice)
+        } else {
+            (None, peek_slice)
+        };
+
+        Ok(PacketHeaders {
+            ether_frame,
+            vlan_tag,
+            ether_type,
+            ipv6_hdr,
+            peek_slice,
+            buf,
+        })
+    }
+    pub fn out(mut self, vlan_encap: Option<VlanTag>) -> io::Result<OutgoingPacket<R>> {
+        let PacketHeaders {
+            ether_frame,
+            ether_type,
+            ipv6_hdr,
+            peek_slice,
+            ..
+        } = self.headers()?;
+
+        let mut headers_out = ArrayVec::<u8, 128>::new();
+        headers_out
+            .try_extend_from_slice(ether_frame.as_bytes())
+            .unwrap();
+        if let Some(vlan_tag) = vlan_encap {
+            headers_out
+                .try_extend_from_slice(vlan_tag.as_bytes())
+                .unwrap();
+        }
+        headers_out
+            .try_extend_from_slice(ether_type.as_bytes())
+            .unwrap();
+        if let Some(ipv6_hdr) = ipv6_hdr {
+            headers_out
+                .try_extend_from_slice(ipv6_hdr.as_bytes())
+                .unwrap();
+        }
+        headers_out.try_extend_from_slice(peek_slice).unwrap();
+
+        let Packet::Peek {
+            peek: _peek, buf, ..
+        } = self
+        else {
+            unreachable!()
+        };
+        Ok(OutgoingPacket { headers_out, buf })
+    }
+}
+
+pub struct OutgoingPacket<R> {
+    /// This has extra space for added encapsulation / VLAN tags
+    headers_out: ArrayVec<u8, 128>,
+    buf: PacketData<R>,
+}
+
+impl<R: Read> OutgoingPacket<R> {
+    pub fn into_reader(self) -> Chain<Cursor<ArrayVec<u8, 128>>, PacketData<R>> {
+        Cursor::new(self.headers_out).chain(self.buf)
+    }
+}
diff --git a/tools/router/src/protocol.rs b/tools/router/src/protocol.rs
new file mode 100644
index 0000000..341b7ff
--- /dev/null
+++ b/tools/router/src/protocol.rs
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+use zerocopy::byteorder::network_endian::{U16, U32};
+use zerocopy::*;
+
+pub const ETHER_TYPE_IPV6: u16 = 0x86dd;
+pub const ETHER_TYPE_802_1Q: u16 = 0x8100;
+pub const IP_PROTO_ICMP6: u8 = 0x3a;
+pub const ICMP6_TYPE_R_ADV: u8 = 134;
+
+pub type MacAddr = [u8; 6];
+pub fn is_multicast(mac: &MacAddr) -> bool {
+    match mac {
+        [0xff, 0xff, 0xff, 0xff, 0xff, 0xff] => true,
+        [0x01, 0x80, 0xc2, _, _, _] => true, // 802 group
+        [0x33, 0x33, _, _, _, _] => true,    // IPv6 multicast
+        _ => false,
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
+#[repr(C)]
+pub struct EtherFrame {
+    pub dst_addr: MacAddr,
+    pub src_addr: MacAddr,
+}
+
+pub type EtherType = U16;
+
+#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
+#[repr(C)]
+pub struct VlanTag {
+    pub ether_type: U16,
+    pub tag_control_information: U16,
+}
+
+#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
+#[repr(C)]
+pub struct Ipv6Header {
+    pub version_traffic_class_flow_label: U32,
+    pub payload_length: U16,
+    pub next_header: u8,
+    pub hop_limit: u8,
+    pub src_addr: [u8; 16],
+    pub dst_addr: [u8; 16],
+}
+
+#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
+#[repr(C)]
+pub struct Icmpv6Header {
+    pub msg_type: u8,
+    pub code: u8,
+    pub checksum: U16,
+}
+
+#[derive(Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
+#[repr(C)]
+pub struct Icmpv6RouterAdvertisement {
+    pub hop_limit: u8,
+    pub flags: u8,
+    pub router_lifetime: U16,
+    pub reachable_time: U32,
+    pub retrans_timer: U32,
+}
diff --git a/tools/router/src/router.rs b/tools/router/src/router.rs
new file mode 100644
index 0000000..8676e4a
--- /dev/null
+++ b/tools/router/src/router.rs
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+use std::collections::HashMap;
+use std::io::{self, Cursor};
+use std::net::Ipv6Addr;
+use std::pin::Pin;
+
+use crate::packet::*;
+use crate::protocol::*;
+
+use futures_util::{Sink, SinkExt, Stream, StreamExt};
+use log::{debug, info, warn};
+use tokio_stream::StreamMap;
+use vhost_device_net::IncomingPacket;
+use vm_memory::GuestMemory;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InterfaceId {
+    Upstream,
+    App(usize),
+    Broadcast,
+}
+
+pub type PacketStream<M> = Pin<Box<dyn Stream<Item = io::Result<Packet<IncomingPacket<M>>>>>>;
+pub type PacketSink<M> = Pin<Box<dyn Sink<Packet<IncomingPacket<M>>, Error = io::Error>>>;
+
+pub struct Router<M: GuestMemory> {
+    streams: StreamMap<InterfaceId, PacketStream<M>>,
+    sinks: HashMap<InterfaceId, PacketSink<M>>,
+    fib: HashMap<Ipv6Addr, (MacAddr, InterfaceId)>,
+    default_out: InterfaceId,
+}
+
+impl<M: GuestMemory> Router<M> {
+    pub fn new(default_out: InterfaceId) -> Self {
+        Self {
+            streams: Default::default(),
+            sinks: Default::default(),
+            fib: Default::default(),
+            default_out,
+        }
+    }
+
+    pub fn add_iface(&mut self, id: InterfaceId, stream: PacketStream<M>, sink: PacketSink<M>) {
+        self.streams.insert(id.clone(), stream);
+        self.sinks.insert(id.clone(), sink);
+    }
+
+    pub async fn run(&mut self) -> io::Result<()> {
+        loop {
+            let next_res = self.streams.next().await;
+            let Some((in_iface, Ok(mut packet))) = next_res else {
+                info!("incoming err");
+                continue;
+            };
+
+            let PacketHeaders {
+                ether_frame,
+                ipv6_hdr,
+                ..
+            } = packet.headers()?;
+
+            let Some(ipv6_hdr) = ipv6_hdr else {
+                continue;
+            };
+            let src_addr = Ipv6Addr::from(ipv6_hdr.src_addr);
+            let dst_addr = Ipv6Addr::from(ipv6_hdr.dst_addr);
+
+            let out_iface = if is_multicast(&ether_frame.dst_addr) {
+                InterfaceId::Broadcast
+            } else if let Some((dst_mac, if_idx)) = self.fib.get(&dst_addr) {
+                ether_frame.dst_addr = *dst_mac;
+                if_idx.clone()
+            } else if in_iface != self.default_out {
+                self.default_out.clone()
+            } else {
+                warn!(
+                    "dropped incoming message to {} because no fib match",
+                    dst_addr
+                );
+                continue;
+            };
+
+            if in_iface != self.default_out
+                && !src_addr.is_unspecified()
+                && !src_addr.is_multicast()
+                && !self.fib.contains_key(&src_addr)
+            {
+                debug!(
+                    "adding fib entry for {} -> {:x?} {:?}",
+                    src_addr, ether_frame.src_addr, in_iface
+                );
+                self.fib
+                    .insert(src_addr, (ether_frame.src_addr, in_iface.clone()));
+            }
+
+            match out_iface {
+                InterfaceId::Broadcast => {
+                    let Packet::Peek {
+                        peek,
+                        mut buf,
+                        decap_vlan,
+                    } = packet
+                    else {
+                        unreachable!()
+                    };
+                    let buf = Box::<[u8]>::from(buf.full_packet());
+                    futures_util::future::try_join_all(
+                        self.sinks
+                            .iter_mut()
+                            .filter(|(id, _)| **id != in_iface)
+                            .map(|(_, sink)| {
+                                sink.send(Packet::Peek {
+                                    peek: peek.clone(),
+                                    buf: PacketData::Bytes(Cursor::new(buf.clone())),
+                                    decap_vlan,
+                                })
+                            }),
+                    )
+                    .await?;
+                }
+                ref unicast => {
+                    let Some(sink) = self.sinks.get_mut(unicast) else {
+                        warn!("dropped message because interface is not ready");
+                        continue;
+                    };
+                    sink.send(packet).await?;
+                }
+            }
+        }
+    }
+}
diff --git a/tools/router/src/upstream.rs b/tools/router/src/upstream.rs
new file mode 100644
index 0000000..ac3367a
--- /dev/null
+++ b/tools/router/src/upstream.rs
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: EUPL-1.2+
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+use std::io::{self, Cursor, Read};
+use std::pin::Pin;
+use std::time::{Duration, Instant};
+
+use crate::packet::*;
+use crate::protocol::*;
+use crate::router::{PacketSink, PacketStream};
+
+use futures_util::{Sink, SinkExt, Stream, StreamExt};
+use log::{debug, error, info, warn};
+use tokio::net::UnixListener;
+use tokio::sync::mpsc;
+use tokio_stream::wrappers::ReceiverStream;
+use tokio_util::sync::PollSender;
+use vhost_device_net::{IncomingPacket, VhostDeviceNet};
+use vm_memory::GuestMemoryMmap;
+use zerocopy::FromBytes;
+
+pub struct Upstream {
+    driver_listener: UnixListener,
+    active_interface: Option<u16>,
+    reevaluate_active_interface: Pin<Box<tokio::time::Sleep>>,
+    radv_valid_until: Vec<(u16, Instant)>,
+    tx_sender: mpsc::Sender<Packet<IncomingPacket<GuestMemoryMmap>>>,
+    rx_receiver: mpsc::Receiver<Packet<IncomingPacket<GuestMemoryMmap>>>,
+}
+
+impl Upstream {
+    pub fn new(
+        driver_listener: UnixListener,
+    ) -> (
+        Upstream,
+        PacketStream<GuestMemoryMmap>,
+        PacketSink<GuestMemoryMmap>,
+    ) {
+        let (tx_sender, tx_receiver) = mpsc::channel(64);
+        let (rx_sender, rx_receiver) = mpsc::channel(64);
+
+        (
+            Upstream {
+                driver_listener,
+                active_interface: None,
+                reevaluate_active_interface: Box::pin(tokio::time::sleep(Duration::from_hours(
+                    24 * 365,
+                ))),
+                radv_valid_until: Default::default(),
+                tx_sender,
+                rx_receiver,
+            },
+            Box::pin(ReceiverStream::new(tx_receiver).map(Ok)),
+            Box::pin(
+                PollSender::new(rx_sender)
+                    .sink_map_err(|_| io::Error::other("driver rx channel closed")),
+            ),
+        )
+    }
+    pub async fn run(&mut self) -> io::Result<()> {
+        let mut device_tx: Option<Pin<Box<dyn Stream<Item = _> + Send>>> = None;
+        let mut device_rx: Option<Pin<Box<dyn Sink<_, Error = _> + Send>>> = None;
+        loop {
+            tokio::select! {
+                driver_conn = self.driver_listener.accept() => {
+                    info!("driver connected");
+                    match driver_conn {
+                        Ok((stream, _addr)) => {
+                            self.radv_valid_until.clear();
+                            self.active_interface = None;
+                            self.reevaluate_active_interface.as_mut().reset((Instant::now() + Duration::from_hours(24 * 365)).into());
+
+                            let device = VhostDeviceNet::from_unix_stream(stream).await?;
+                            device_tx = Some(Box::pin(device.tx().await?));
+                            device_rx = Some(Box::pin(device.rx().await?));
+                        }
+                        Err(e) => error!("driver connection failed: {}", e),
+                    }
+                }
+                tx_res = async { device_tx.as_mut().unwrap().next().await }, if device_tx.is_some() => {
+                    let Some(Ok(buf)) = tx_res else {
+                        info!("driver tx err");
+                        continue;
+                    };
+
+                    let mut packet = Packet::Incoming { buf: Some(buf), decap_vlan: true };
+                    let PacketHeaders { ether_frame, vlan_tag: vlan_in, ipv6_hdr, peek_slice, buf, .. } = packet.headers()?;
+
+                    let Some(vlan_tag) = vlan_in else {
+                        warn!("untagged packet from driver");
+                        continue;
+                    };
+
+                    let vlan_id = u16::from(vlan_tag.tag_control_information) & 0xfff;
+
+                    if let Some(ref ipv6_hdr) = ipv6_hdr && ipv6_hdr.next_header == IP_PROTO_ICMP6 {
+                        let (icmpv6_hdr, icmpv6_data) = Icmpv6Header::ref_from_prefix(peek_slice).map_err(|_| io::Error::other("short icmpv6 header"))?;
+
+                        if icmpv6_hdr.msg_type == ICMP6_TYPE_R_ADV {
+                            let data = Cursor::new(icmpv6_data).chain(Cursor::new(buf.full_packet()));
+                            let r_adv = Icmpv6RouterAdvertisement::read_from_io(data)?;
+                            if r_adv.router_lifetime != 0 {
+                                let now = Instant::now();
+                                let r_adv_timeout = now + Duration::from_secs(u16::from(r_adv.router_lifetime).into());
+                                match self.radv_valid_until.binary_search_by_key(&vlan_id, |&(if_idx, _)| if_idx) {
+                                    Ok(pos) => self.radv_valid_until[pos] = (vlan_id, r_adv_timeout),
+                                    Err(insert_pos) => self.radv_valid_until.insert(insert_pos, (vlan_id, r_adv_timeout)),
+                                };
+                                debug!("router advertisement received on interface {}: {:x?} {:x?} {:?}", vlan_id, ether_frame, ipv6_hdr, r_adv);
+
+                                let prev_active_interface = self.active_interface.unwrap_or(u16::MAX);
+                                if vlan_id < prev_active_interface || self.reevaluate_active_interface.deadline() < now.into() {
+                                    self.active_interface = Some(vlan_id);
+                                    info!("set active interface to {}", vlan_id);
+                                    self.reevaluate_active_interface.as_mut().reset(r_adv_timeout.into());
+                                } else if vlan_id == prev_active_interface {
+                                    self.reevaluate_active_interface.as_mut().reset(r_adv_timeout.into());
+                                }
+                            }
+                        }
+                    }
+
+                    if Some(vlan_id) != self.active_interface {
+                        debug!("dropping packet from inactive interface {}", vlan_id);
+                        continue;
+                    }
+
+                    self.tx_sender.send(packet).await.map_err(io::Error::other)?;
+                }
+                rx_res = self.rx_receiver.recv() => {
+                    let Some(packet) = rx_res else {
+                        info!("driver rx err");
+                        continue;
+                    };
+
+                    let Some(sink) = device_rx.as_mut() else {
+                        warn!("dropped message because driver is not ready");
+                        continue;
+                    };
+
+                    let Some(active_interface) = &self.active_interface else {
+                        warn!("dropped packet because active interface is unknown");
+                        continue;
+                    };
+
+                    // Add active interface vlan
+                    let vlan_out = VlanTag {
+                        ether_type: ETHER_TYPE_802_1Q.into(),
+                        tag_control_information: (*active_interface).into(),
+                    };
+
+                    let packet = packet.out(Some(vlan_out))?;
+                    sink.send(packet.into_reader()).await?;
+                }
+                () = &mut self.reevaluate_active_interface => {
+                    let now = Instant::now();
+                    let prev_active_interface = self.active_interface.unwrap_or(u16::MAX);
+                    info!("router advertisement expired on interface {}", prev_active_interface);
+                    if let Some((if_idx, valid_until)) = self.radv_valid_until.iter().find(|(_, valid_until)| *valid_until > now) {
+                        self.active_interface = Some(*if_idx);
+                        info!("set active interface to {}", if_idx);
+                        self.reevaluate_active_interface.as_mut().reset((*valid_until).into());
+                    } else {
+                        self.reevaluate_active_interface.as_mut().reset((now + Duration::from_hours(24 * 365)).into());
+                    }
+                }
+            }
+        }
+    }
+}
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH v2 5/7] host: integrate router
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
                   ` (3 preceding siblings ...)
  2025-11-28 22:30 ` [PATCH v2 4/7] tools: add spectrum-router Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:46   ` Alyssa Ross
  2025-11-28 22:30 ` [PATCH v2 6/7] img/app: change to ipv6 nameserver Yureka Lilian
  2025-11-28 22:30 ` [PATCH v2 7/7] checks/integration: Adapt networking test for ipv6 Yureka Lilian
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

This removes the old host bridge + taps glue, and instead connects the
apps to their net provider's router instance.

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 host/rootfs/default.nix                       |  4 +-
 host/rootfs/file-list.mk                      |  3 +
 .../data/service/spectrum-router/down         |  0
 .../template/data/service/spectrum-router/run | 13 ++++
 .../image/usr/bin/assign-driver-router-iface  | 11 +++
 host/rootfs/image/usr/bin/run-vmm             | 12 +--
 host/rootfs/image/usr/bin/vm-import           | 13 ----
 pkgs/overlay.nix                              |  1 +
 tools/start-vmm/ch.rs                         | 38 ++--------
 tools/start-vmm/lib.rs                        | 76 +++++++++++++------
 tools/start-vmm/meson.build                   |  2 +-
 tools/start-vmm/net-util.c                    | 39 ----------
 tools/start-vmm/net-util.h                    |  6 --
 tools/start-vmm/net.c                         | 55 --------------
 tools/start-vmm/net.rs                        | 11 ---
 tools/start-vmm/tests/meson.build             |  5 --
 .../start-vmm/tests/tap_open-name-too-long.c  | 20 -----
 tools/start-vmm/tests/tap_open.c              | 28 -------
 18 files changed, 89 insertions(+), 248 deletions(-)
 create mode 100644 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down
 create mode 100755 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
 create mode 100755 host/rootfs/image/usr/bin/assign-driver-router-iface
 delete mode 100644 tools/start-vmm/net-util.c
 delete mode 100644 tools/start-vmm/net-util.h
 delete mode 100644 tools/start-vmm/net.c
 delete mode 100644 tools/start-vmm/tests/tap_open-name-too-long.c
 delete mode 100644 tools/start-vmm/tests/tap_open.c

diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix
index 4bbbe23..3b8557c 100644
--- a/host/rootfs/default.nix
+++ b/host/rootfs/default.nix
@@ -8,7 +8,7 @@ import ../../lib/call-package.nix (
 }:
 pkgsMusl.callPackage (
 
-{ spectrum-host-tools
+{ spectrum-host-tools, spectrum-router
 , lib, stdenvNoCC, nixos, runCommand, writeClosure, erofs-utils, s6-rc
 , busybox, cloud-hypervisor, cosmic-files, crosvm, cryptsetup
 , dejavu_fonts, dbus, execline, foot, fuse3, iproute2, inotify-tools
@@ -27,7 +27,7 @@ let
     cloud-hypervisor cosmic-files crosvm cryptsetup dbus execline
     fuse3 inotify-tools iproute2 jq kmod mdevd s6 s6-linux-init s6-rc
     socat spectrum-host-tools systemd util-linuxMinimal virtiofsd
-    xdg-desktop-portal-spectrum-host
+    xdg-desktop-portal-spectrum-host spectrum-router
 
     (foot.override { allowPgo = false; })
 
diff --git a/host/rootfs/file-list.mk b/host/rootfs/file-list.mk
index 613a9e7..95fb291 100644
--- a/host/rootfs/file-list.mk
+++ b/host/rootfs/file-list.mk
@@ -27,6 +27,8 @@ FILES = \
 	image/etc/s6-linux-init/run-image/service/vm-services/run \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/dbus/run \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down \
+	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/notification-fd \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-fs/run \
 	image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/vhost-user-gpu/notification-fd \
@@ -45,6 +47,7 @@ FILES = \
 	image/etc/xdg/weston/autolaunch \
 	image/etc/xdg/weston/weston.ini \
 	image/usr/bin/assign-devices \
+	image/usr/bin/assign-driver-router-iface \
 	image/usr/bin/create-vm-dependencies \
 	image/usr/bin/run-appimage \
 	image/usr/bin/run-vmm \
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down
new file mode 100644
index 0000000..e69de29
diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
new file mode 100755
index 0000000..fae9d9d
--- /dev/null
+++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
@@ -0,0 +1,13 @@
+#!/bin/execlineb -P
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+importas -i VM VM
+
+background {
+  assign-driver-router-iface ${VM}
+}
+
+export RUST_LOG debug
+spectrum-router --app-listen-path ${VM}/router-app.sock --driver-listen-path ${VM}/router-driver.sock
+
diff --git a/host/rootfs/image/usr/bin/assign-driver-router-iface b/host/rootfs/image/usr/bin/assign-driver-router-iface
new file mode 100755
index 0000000..c555fb6
--- /dev/null
+++ b/host/rootfs/image/usr/bin/assign-driver-router-iface
@@ -0,0 +1,11 @@
+#!/bin/execlineb -S1
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
+
+# This script is to be called once it is known that this VM is a driver VM
+# (net provider) AND the vmm endpoint is ready.
+# It add the interface between the router and the driver VM.
+# Note: This script is designed to be re-entrant
+redirfd -w 2 /dev/null
+ch-remote --api-socket ${1}/vmm add-net id=router,vhost_user=on,socket=${1}/router-driver.sock,mac=02:01:00:00:00:01
diff --git a/host/rootfs/image/usr/bin/run-vmm b/host/rootfs/image/usr/bin/run-vmm
index 5649674..b3865ff 100755
--- a/host/rootfs/image/usr/bin/run-vmm
+++ b/host/rootfs/image/usr/bin/run-vmm
@@ -37,17 +37,7 @@ background -d {
     test $router_id != $1
   }
 
-  backtick -E mac {
-    pipeline { ip -j link show client-${client_id} }
-    pipeline { jq -r ".[].ifindex" }
-    awk "{
-      printf \"02:01:%02X:%02X:%02X:%02X\", $0 / 256 ^ 3 % 256,
-	$0 / 256 ^ 2 % 256, $0 / 256 % 256, $0 % 256
-    }"
-  }
-
-  ch-remote --api-socket /run/vm/by-id/${router_id}/vmm add-net
-    id=router-${client_id},tap=router-${client_id},mac=${mac}
+  assign-driver-router-iface /run/vm/by-id/${router_id}
 }
 unexport !
 fdmove -c 3 0
diff --git a/host/rootfs/image/usr/bin/vm-import b/host/rootfs/image/usr/bin/vm-import
index de88f08..c1d1bbc 100755
--- a/host/rootfs/image/usr/bin/vm-import
+++ b/host/rootfs/image/usr/bin/vm-import
@@ -14,19 +14,6 @@ if { ln -s -- ${dir} /run/vm/by-name/${1}.${name} }
 if { ln -s -- ${2}/${name} ${dir}/config }
 if { ln -s -- /run/service/vmm/instance/${id} ${dir}/service }
 
-if {
-  if -t { elglob -0d " " providers ${name}/providers/net test -n $providers }
-
-  if { ip link add br-${id} type bridge }
-  if { ip link set br-${id} up }
-
-  if { ip tuntap add client-${id} mode tap }
-  if { ip link set client-${id} master br-${id} up }
-
-  if { ip tuntap add router-${id} mode tap }
-  ip link set router-${id} master br-${id} up
-}
-
 if { create-vm-dependencies $id }
 
 s6-instance-create -- /run/service/vmm $id
diff --git a/pkgs/overlay.nix b/pkgs/overlay.nix
index fdddae0..f894864 100644
--- a/pkgs/overlay.nix
+++ b/pkgs/overlay.nix
@@ -11,4 +11,5 @@
   );
 
   skawarePackages = import ./skaware-packages { inherit final super; };
+  mailutils = super.mailutils.overrideAttrs (_: { doCheck = false; });
 })
diff --git a/tools/start-vmm/ch.rs b/tools/start-vmm/ch.rs
index abe1742..56b18f4 100644
--- a/tools/start-vmm/ch.rs
+++ b/tools/start-vmm/ch.rs
@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: EUPL-1.2+
 // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 
-use std::convert::TryFrom;
 use std::ffi::OsStr;
 use std::fs::File;
 use std::io::Write;
@@ -10,7 +10,6 @@ use std::num::NonZeroI32;
 use std::os::unix::prelude::*;
 use std::path::Path;
 use std::process::{Command, Stdio};
-use std::string::FromUtf8Error;
 
 use miniserde::{Serialize, json};
 
@@ -46,7 +45,7 @@ pub struct GpuConfig {
 
 #[derive(Serialize)]
 pub struct NetConfig {
-    pub fd: RawFd,
+    pub vhost_user_sock: String,
     pub id: String,
     pub mac: MacAddress,
 }
@@ -137,7 +136,10 @@ pub fn create_vm(vm_dir: &Path, ready_fd: File, mut config: VmConfig) -> Result<
 
 pub fn add_net(vm_dir: &Path, net: &NetConfig) -> Result<(), NonZeroI32> {
     let mut ch_remote = command(vm_dir, "add-net")
-        .arg(format!("fd={},id={},mac={}", net.fd, net.id, net.mac))
+        .arg(format!(
+            "vhost_user=on,socket={},id={},mac={}",
+            net.vhost_user_sock, net.id, net.mac
+        ))
         .stdout(Stdio::piped())
         .spawn()
         .or(Err(EPERM))?;
@@ -150,31 +152,3 @@ pub fn add_net(vm_dir: &Path, net: &NetConfig) -> Result<(), NonZeroI32> {
 
     Err(EPROTO)
 }
-
-#[repr(C)]
-pub struct NetConfigC {
-    pub fd: RawFd,
-    pub id: [u8; 18],
-    pub mac: MacAddress,
-}
-
-impl<'a> TryFrom<&'a NetConfigC> for NetConfig {
-    type Error = FromUtf8Error;
-
-    fn try_from(c: &'a NetConfigC) -> Result<NetConfig, Self::Error> {
-        let nul_index = c.id.iter().position(|&c| c == 0).unwrap_or(c.id.len());
-        Ok(NetConfig {
-            fd: c.fd,
-            id: String::from_utf8(c.id[..nul_index].to_vec())?,
-            mac: c.mac,
-        })
-    }
-}
-
-impl TryFrom<NetConfigC> for NetConfig {
-    type Error = FromUtf8Error;
-
-    fn try_from(c: NetConfigC) -> Result<NetConfig, Self::Error> {
-        Self::try_from(&c)
-    }
-}
diff --git a/tools/start-vmm/lib.rs b/tools/start-vmm/lib.rs
index 0422d85..246dd6d 100644
--- a/tools/start-vmm/lib.rs
+++ b/tools/start-vmm/lib.rs
@@ -1,23 +1,24 @@
 // SPDX-License-Identifier: EUPL-1.2+
 // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 
 mod ch;
 mod net;
 mod s6;
 
 use std::borrow::Cow;
-use std::convert::TryInto;
 use std::env::args_os;
 use std::ffi::OsStr;
 use std::fs::File;
-use std::io::{self, ErrorKind};
+use std::hash::{Hash, Hasher};
+use std::io::ErrorKind;
 use std::path::Path;
 
 use ch::{
-    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, PayloadConfig,
-    VmConfig, VsockConfig,
+    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, NetConfig,
+    PayloadConfig, VmConfig, VsockConfig,
 };
-use net::net_setup;
+use net::MacAddress;
 
 pub fn prog_name() -> String {
     args_os()
@@ -40,8 +41,6 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
         return Err(format!("VM name may not contain a colon: {vm_name:?}"));
     }
 
-    let name_bytes = vm_name.as_bytes();
-
     let config_dir = vm_dir.join("config");
     let blk_dir = config_dir.join("blk");
     let kernel_path = config_dir.join("vmlinux");
@@ -97,24 +96,51 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
             shared: true,
         },
         net: match net_providers_dir.read_dir() {
-            Ok(_) => {
-                // SAFETY: we check the result.
-                let net = unsafe {
-                    net_setup(
-                        name_bytes.as_ptr().cast(),
-                        name_bytes
-                            .len()
-                            .try_into()
-                            .map_err(|e| format!("VM name too long: {e}"))?,
-                    )
-                };
-                if net.fd == -1 {
-                    let e = io::Error::last_os_error();
-                    return Err(format!("setting up networking failed: {e}"));
-                }
-
-                vec![net.try_into().unwrap()]
-            }
+            Ok(entries) => entries
+                .into_iter()
+                .map(|result| {
+                    Ok(result
+                        .map_err(|e| format!("examining directory entry: {e}"))?
+                        .path())
+                })
+                .map(|result: Result<_, String>| {
+                    let provider_name = result?.file_name().ok_or("unable to get net provider name".to_string())?.to_str().unwrap().to_string();
+
+                    if provider_name.contains(',') {
+                        return Err(format!("illegal ',' character in net provider name {provider_name:?}"));
+                    }
+
+                    let mut hasher = std::hash::DefaultHasher::new();
+                    vm_name.hash(&mut hasher);
+                    let id_hashed = hasher.finish();
+
+                    let mac = MacAddress::new([
+                        0x02, // IEEE 802c administratively assigned
+                        0x00, // Spectrum client
+                        (id_hashed >> 24) as u8,
+                        (id_hashed >> 16) as u8,
+                        (id_hashed >> 8) as u8,
+                        id_hashed as u8,
+                    ]);
+
+                    let provider_id = std::fs::read_link(format!("/run/vm/by-name/{provider_name}")).map_err(|e| format!("unable to get net provider id: {e}"))?.file_name().ok_or("unable to get net provider id".to_string())?.to_str().unwrap().to_string();
+
+                    let svc_dir = format!("/run/service/vm-services/instance/{provider_id}/data/service/spectrum-router");
+                    let svc_status = std::process::Command::new("s6-svc")
+                        .args(["-U", &svc_dir])
+                        .status()
+                        .expect("setting up the upstream router via s6-svc failed");
+                    if !svc_status.success() {
+                        return Err(format!("setting up the upstream router via s6-svc failed with exit code {svc_status}"));
+                    }
+
+                    Ok(NetConfig {
+                        vhost_user_sock: format!("/run/vm/by-name/{provider_name}/router-app.sock"),
+                        id: provider_name,
+                        mac,
+                    })
+                })
+                .collect::<Result<_, _>>()?,
             Err(e) if e.kind() == ErrorKind::NotFound => Default::default(),
             Err(e) => return Err(format!("reading directory {net_providers_dir:?}: {e}")),
         },
diff --git a/tools/start-vmm/meson.build b/tools/start-vmm/meson.build
index d07c5a0..aa9f6f3 100644
--- a/tools/start-vmm/meson.build
+++ b/tools/start-vmm/meson.build
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: EUPL-1.2+
 # SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
 
-c_lib = static_library('start-vmm', 'net.c', 'net-util.c',
+c_lib = static_library('start-vmm',
   c_args : '-D_GNU_SOURCE')
 
 rust_lib = static_library('start_vmm', 'lib.rs',
diff --git a/tools/start-vmm/net-util.c b/tools/start-vmm/net-util.c
deleted file mode 100644
index 49003e9..0000000
--- a/tools/start-vmm/net-util.c
+++ /dev/null
@@ -1,39 +0,0 @@
-// SPDX-License-Identifier: EUPL-1.2+
-// SPDX-FileCopyrightText: 2022, 2024 Alyssa Ross <hi@alyssa.is>
-
-#include "net-util.h"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <string.h>
-#include <unistd.h>
-
-#include <sys/ioctl.h>
-
-#include <linux/if_tun.h>
-
-int tap_open(char name[static IFNAMSIZ], int flags)
-{
-	struct ifreq ifr;
-	int fd, e;
-
-	if (strnlen(name, IFNAMSIZ) == IFNAMSIZ) {
-		errno = ENAMETOOLONG;
-		return -1;
-	}
-
-	strncpy(ifr.ifr_name, name, IFNAMSIZ - 1);
-	ifr.ifr_flags = IFF_TAP|flags;
-
-	if ((fd = open("/dev/net/tun", O_RDWR)) == -1)
-		return -1;
-	if (ioctl(fd, TUNSETIFF, &ifr) == -1) {
-		e = errno;
-		close(fd);
-		errno = e;
-		return -1;
-	}
-
-	strncpy(name, ifr.ifr_name, IFNAMSIZ);
-	return fd;
-}
diff --git a/tools/start-vmm/net-util.h b/tools/start-vmm/net-util.h
deleted file mode 100644
index 8f55206..0000000
--- a/tools/start-vmm/net-util.h
+++ /dev/null
@@ -1,6 +0,0 @@
-// SPDX-License-Identifier: EUPL-1.2+
-// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is>
-
-#include <net/if.h>
-
-int tap_open(char name[static IFNAMSIZ], int flags);
diff --git a/tools/start-vmm/net.c b/tools/start-vmm/net.c
deleted file mode 100644
index 78fe7f6..0000000
--- a/tools/start-vmm/net.c
+++ /dev/null
@@ -1,55 +0,0 @@
-// SPDX-License-Identifier: EUPL-1.2+
-// SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
-
-#include "ch.h"
-#include "net-util.h"
-
-#include <assert.h>
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include <arpa/inet.h>
-
-#include <linux/if_tun.h>
-
-static int get_tap_name(char tap_name[static IFNAMSIZ],
-                        const char tap_prefix[static 1],
-                        const char name[static 1], int name_len)
-{
-	int r = snprintf(tap_name, IFNAMSIZ, "%s-%*s", tap_prefix, name_len, name);
-	if (r >= IFNAMSIZ)
-		errno = ENAMETOOLONG;
-	return r < 0 || r >= IFNAMSIZ ? -1 : 0;
-}
-
-struct net_config net_setup(const char name[static 1], int name_len)
-{
-	int e;
-	unsigned int client_index;
-	struct net_config r = { .fd = -1, .mac = { 0 } };
-
-	if ((get_tap_name(r.id, "client", name, name_len)) == -1)
-		return r;
-
-	if (!(client_index = htonl(if_nametoindex(r.id))))
-		return r;
-
-	if ((r.fd = tap_open(r.id, IFF_NO_PI|IFF_VNET_HDR)) == -1)
-		goto fail_close;
-
-	r.mac[0] = 0x02; // IEEE 802c administratively assigned
-	r.mac[1] = 0x00; // Spectrum client
-	memcpy(&r.mac[2], &client_index, 4);
-
-	return r;
-
-fail_close:
-	e = errno;
-	close(r.fd);
-	errno = e;
-	r.fd = -1;
-	return r;
-}
diff --git a/tools/start-vmm/net.rs b/tools/start-vmm/net.rs
index ebfef7a..7b237df 100644
--- a/tools/start-vmm/net.rs
+++ b/tools/start-vmm/net.rs
@@ -2,14 +2,11 @@
 // SPDX-FileCopyrightText: 2022-2025 Alyssa Ross <hi@alyssa.is>
 
 use std::borrow::Cow;
-use std::ffi::{c_char, c_int};
 use std::fmt::{self, Display, Formatter};
 
 use miniserde::Serialize;
 use miniserde::ser::Fragment;
 
-use crate::ch::NetConfigC;
-
 #[repr(transparent)]
 #[derive(Copy, Clone)]
 pub struct MacAddress([u8; 6]);
@@ -36,14 +33,6 @@ impl Serialize for MacAddress {
     }
 }
 
-// SAFETY: declaration is compatible with C.
-unsafe extern "C" {
-    /// # Safety
-    ///
-    /// The rest of the result is only valid if the returned fd is not -1.
-    pub fn net_setup(name: *const c_char, len: c_int) -> NetConfigC;
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/tools/start-vmm/tests/meson.build b/tools/start-vmm/tests/meson.build
index bfdfc46..5538822 100644
--- a/tools/start-vmm/tests/meson.build
+++ b/tools/start-vmm/tests/meson.build
@@ -4,11 +4,6 @@
 rust_helper = static_library('test_helper', 'helper.rs',
   dependencies : rust_lib_dep)
 
-test('tap_open', executable('tap_open', 'tap_open.c', '../net-util.c',
-  c_args : '-D_GNU_SOURCE'))
-test('tap_open (name too long)', executable('tap_open-name-too-long',
-  'tap_open-name-too-long.c', '../net-util.c', c_args : '-D_GNU_SOURCE'))
-
 test('vm_command-basic', executable('vm_command-basic',
   'vm_command-basic.rs',
   dependencies : rust_lib_dep,
diff --git a/tools/start-vmm/tests/tap_open-name-too-long.c b/tools/start-vmm/tests/tap_open-name-too-long.c
deleted file mode 100644
index ba4ebd6..0000000
--- a/tools/start-vmm/tests/tap_open-name-too-long.c
+++ /dev/null
@@ -1,20 +0,0 @@
-// SPDX-License-Identifier: EUPL-1.2+
-// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is>
-
-#include "../net-util.h"
-
-#include <assert.h>
-#include <errno.h>
-#include <net/if.h>
-#include <string.h>
-
-int main(void)
-{
-	char name[IFNAMSIZ];
-	int fd;
-
-	memset(name, 'a', sizeof name);
-	fd = tap_open(name, 0);
-	assert(fd == -1);
-	assert(errno == ENAMETOOLONG);
-}
diff --git a/tools/start-vmm/tests/tap_open.c b/tools/start-vmm/tests/tap_open.c
deleted file mode 100644
index bf5d00c..0000000
--- a/tools/start-vmm/tests/tap_open.c
+++ /dev/null
@@ -1,28 +0,0 @@
-// SPDX-License-Identifier: EUPL-1.2+
-// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is>
-
-#include "../net-util.h"
-
-#include <assert.h>
-#include <errno.h>
-#include <sched.h>
-#include <string.h>
-
-#include <sys/ioctl.h>
-
-#include <linux/if_tun.h>
-
-int main(void)
-{
-	char name[IFNAMSIZ] = "tap%d";
-	struct ifreq ifr;
-	int fd;
-
-	unshare(CLONE_NEWUSER|CLONE_NEWNET);
-
-	fd = tap_open(name, 0);
-	if (fd == -1 && (errno == EPERM || errno == ENOENT))
-		return 77;
-	assert(!ioctl(fd, (unsigned)TUNGETIFF, &ifr));
-	assert(!strcmp(name, ifr.ifr_name));
-}
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH v2 6/7] img/app: change to ipv6 nameserver
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
                   ` (4 preceding siblings ...)
  2025-11-28 22:30 ` [PATCH v2 5/7] host: integrate router Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:20   ` Alyssa Ross
  2025-11-28 22:30 ` [PATCH v2 7/7] checks/integration: Adapt networking test for ipv6 Yureka Lilian
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

Also remove hard-coded ipv4 default route.
Eventually this will be changed to have both ipv4 and ipv6 nameserver,
and support IPv4 with the new router.

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 img/app/image/etc/mdev/iface  | 17 -----------------
 img/app/image/etc/resolv.conf |  2 +-
 2 files changed, 1 insertion(+), 18 deletions(-)

diff --git a/img/app/image/etc/mdev/iface b/img/app/image/etc/mdev/iface
index 95f2fa8..3241b4d 100755
--- a/img/app/image/etc/mdev/iface
+++ b/img/app/image/etc/mdev/iface
@@ -4,22 +4,5 @@
 
 background {
   importas -Si INTERFACE
-
-  # Our IP is encoded in the NIC-specific portion of the interface's MAC
-  # address.
-  backtick -E LOCAL_IP {
-    awk -PF: "
-      BEGIN { ex = 1 }
-      $1 == \"02\" && $2 == \"00\" {
-        printf \"100.64.%d.%d\\n\", \"0x\" $5, \"0x\" $6
-        ex = 0
-      }
-      END { exit ex }
-    " /sys/class/net/${INTERFACE}/address
-  }
-
-  if { ip address add ${LOCAL_IP}/32 dev $INTERFACE }
   if { ip link set $INTERFACE up }
-  if { ip route add 169.254.0.1 dev $INTERFACE }
-  ip route add default via 169.254.0.1 dev $INTERFACE
 }
diff --git a/img/app/image/etc/resolv.conf b/img/app/image/etc/resolv.conf
index 7fcdf3a..e82c3d0 100644
--- a/img/app/image/etc/resolv.conf
+++ b/img/app/image/etc/resolv.conf
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: CC0-1.0
 # SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>
 
-nameserver 1.1.1.1
+nameserver 2606:4700:4700::1111
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH v2 7/7] checks/integration: Adapt networking test for ipv6
  2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
                   ` (5 preceding siblings ...)
  2025-11-28 22:30 ` [PATCH v2 6/7] img/app: change to ipv6 nameserver Yureka Lilian
@ 2025-11-28 22:30 ` Yureka Lilian
  2025-11-29 13:26   ` Alyssa Ross
  6 siblings, 1 reply; 19+ messages in thread
From: Yureka Lilian @ 2025-11-28 22:30 UTC (permalink / raw)
  To: devel; +Cc: Yureka Lilian

Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
---
 release/checks/integration/default.nix  |  2 +-
 release/checks/integration/networking.c | 32 +++++++++++++++++++------
 2 files changed, 26 insertions(+), 8 deletions(-)

diff --git a/release/checks/integration/default.nix b/release/checks/integration/default.nix
index 30d047b..944aeff 100644
--- a/release/checks/integration/default.nix
+++ b/release/checks/integration/default.nix
@@ -20,7 +20,7 @@ let
     type = "nix";
     run = writeShellScript "run" ''
       set -x
-      while :; do echo hello | ${libressl.nc}/bin/nc -Nw 2 10.0.2.2 1234; done
+      while :; do echo hello | ${libressl.nc}/bin/nc -Nw 2 -6 fd00::2 1234; done
     '';
   };
 
diff --git a/release/checks/integration/networking.c b/release/checks/integration/networking.c
index 97d7895..6600385 100644
--- a/release/checks/integration/networking.c
+++ b/release/checks/integration/networking.c
@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: EUPL-1.2+
 // SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
+// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
 
 #include "lib.h"
 
@@ -12,21 +13,23 @@
 #include <net/if.h>
 
 #include <sys/ioctl.h>
+#include <linux/ipv6.h>
 
 static int setup_server(void)
 {
 	int fd;
 	struct ifreq ifr;
+	struct in6_ifreq ifr6;
 
-	struct sockaddr_in addr = {
-		.sin_family = AF_INET,
-		.sin_port = htons(1234),
-		.sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) },
+	struct sockaddr_in6 addr = {
+		.sin6_family = AF_INET6,
+		.sin6_port = htons(1234),
+		.sin6_addr = { .s6_addr = { 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 } },
 	};
 
 	sprintf(ifr.ifr_name, "lo");
 
-	if ((fd = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, 0)) == -1) {
+	if ((fd = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC, 0)) == -1) {
 		perror("socket");
 		exit(EXIT_FAILURE);
 	}
@@ -42,11 +45,26 @@ static int setup_server(void)
 		exit(EXIT_FAILURE);
 	}
 
-	if (bind(fd, &addr, sizeof addr) == -1) {
-		perror("bind");
+	ifr6.ifr6_ifindex = 1;
+	ifr6.ifr6_addr = addr.sin6_addr;
+	ifr6.ifr6_prefixlen = 128;
+	if (ioctl(fd, SIOCSIFADDR, &ifr6) == -1) {
+		perror("SIOCSIFADDR");
+		exit(EXIT_FAILURE);
+	}
+
+	if ((fd = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC, 0)) == -1) {
+		perror("socket");
 		exit(EXIT_FAILURE);
 	}
 
+	int tries = 0;
+	while (bind(fd, &addr, sizeof addr) == -1) {
+		perror("bind");
+		if (tries++ >= 5)
+			exit(EXIT_FAILURE);
+	}
+
 	if (listen(fd, 1) == -1) {
 		perror("listen");
 		exit(EXIT_FAILURE);
-- 
2.51.2


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 1/7] vm/sys/net: remove connman
  2025-11-28 22:30 ` [PATCH v2 1/7] vm/sys/net: remove connman Yureka Lilian
@ 2025-11-29 13:06   ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:06 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 1042 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> In preparation to integrating xdp-forwarder, making the net-vm a net-driver VM.
> dbus is left, as it will likely be needed in the future for iwd.
>
> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
> ---
>  vm/sys/net/Makefile                           |  2 +-
>  vm/sys/net/default.nix                        |  8 +++-----
>  vm/sys/net/file-list.mk                       |  3 ---
>  .../etc/s6-rc/connman/dependencies.d/dbus     |  0
>  vm/sys/net/image/etc/s6-rc/connman/run        | 19 -------------------
>  vm/sys/net/image/etc/s6-rc/connman/type       |  1 -
>  .../net/image/etc/s6-rc/connman/type.license  |  2 --
>  7 files changed, 4 insertions(+), 31 deletions(-)
>  delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/dependencies.d/dbus
>  delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/run
>  delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/type
>  delete mode 100644 vm/sys/net/image/etc/s6-rc/connman/type.license

Reviewed-by: Alyssa Ross <hi@alyssa.is>

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder
  2025-11-28 22:30 ` [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder Yureka Lilian
@ 2025-11-29 13:08   ` Alyssa Ross
  2025-11-29 13:15     ` Yureka
  0 siblings, 1 reply; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:08 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 1011 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> diff --git a/vm/sys/net/image/etc/nftables.conf b/vm/sys/net/image/etc/nftables.conf
> index 296d92c..cc8e462 100644
> --- a/vm/sys/net/image/etc/nftables.conf
> +++ b/vm/sys/net/image/etc/nftables.conf
> @@ -1,8 +1,16 @@
>  # SPDX-License-Identifier: EUPL-1.2+
> -# SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>
> +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>  
> -table nat {
> -	chain postrouting {
> -		type nat hook postrouting priority 100;
> +table driver-fw {
> +	chain input {
> +		type filter hook input priority filter; policy drop;
> +	}
> +
> +	chain output {
> +		type filter hook output priority filter; policy drop;
> +	}
> +
> +	chain forward {
> +		type filter hook forward priority filter; policy drop;
>  	}
>  }

Just checking: did you see my question about the formatting of this last
time?

https://spectrum-os.org/lists/archives/spectrum-devel/87see1t69a.fsf@alyssa.is/

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 3/7] vm/sys/net: add iwd
  2025-11-28 22:30 ` [PATCH v2 3/7] vm/sys/net: add iwd Yureka Lilian
@ 2025-11-29 13:09   ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:09 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 410 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> This allows developers to manually start the iwd daemon and connect to a
> wireless network, to confirm that the rest of the stack works correctly
> when used with Wi-Fi.
>
> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
> ---
>  vm/sys/net/default.nix | 4 ++--
>  1 file changed, 2 insertions(+), 2 deletions(-)

Reviewed-by: Alyssa Ross <hi@alyssa.is>

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder
  2025-11-29 13:08   ` Alyssa Ross
@ 2025-11-29 13:15     ` Yureka
  2025-11-29 13:17       ` Alyssa Ross
  0 siblings, 1 reply; 19+ messages in thread
From: Yureka @ 2025-11-29 13:15 UTC (permalink / raw)
  To: Alyssa Ross; +Cc: devel

On 11/29/25 14:08, Alyssa Ross wrote:
> Yureka Lilian <yureka@cyberchaos.dev> writes:
>
>> diff --git a/vm/sys/net/image/etc/nftables.conf b/vm/sys/net/image/etc/nftables.conf
>> index 296d92c..cc8e462 100644
>> --- a/vm/sys/net/image/etc/nftables.conf
>> +++ b/vm/sys/net/image/etc/nftables.conf
>> @@ -1,8 +1,16 @@
>>   # SPDX-License-Identifier: EUPL-1.2+
>> -# SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>
>> +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>>   
>> -table nat {
>> -	chain postrouting {
>> -		type nat hook postrouting priority 100;
>> +table driver-fw {
>> +	chain input {
>> +		type filter hook input priority filter; policy drop;
>> +	}
>> +
>> +	chain output {
>> +		type filter hook output priority filter; policy drop;
>> +	}
>> +
>> +	chain forward {
>> +		type filter hook forward priority filter; policy drop;
>>   	}
>>   }
> Just checking: did you see my question about the formatting of this last
> time?
>
> https://spectrum-os.org/lists/archives/spectrum-devel/87see1t69a.fsf@alyssa.is/

I saw the question but forgot to answer it. Thanks for reminding me.

At least inside nixpkgs all nftables chains are declared in this style, 
so yes it is a common way of writing it. I suppose because the policy is 
semantically part of the chain "declaration" as opposed to the rules 
following it.


^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder
  2025-11-29 13:15     ` Yureka
@ 2025-11-29 13:17       ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:17 UTC (permalink / raw)
  To: Yureka; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 1556 bytes --]

On Sat, Nov 29, 2025 at 02:15:23PM +0100, Yureka wrote:
> On 11/29/25 14:08, Alyssa Ross wrote:
> > Yureka Lilian <yureka@cyberchaos.dev> writes:
> >
> > > diff --git a/vm/sys/net/image/etc/nftables.conf b/vm/sys/net/image/etc/nftables.conf
> > > index 296d92c..cc8e462 100644
> > > --- a/vm/sys/net/image/etc/nftables.conf
> > > +++ b/vm/sys/net/image/etc/nftables.conf
> > > @@ -1,8 +1,16 @@
> > >   # SPDX-License-Identifier: EUPL-1.2+
> > > -# SPDX-FileCopyrightText: 2021 Alyssa Ross <hi@alyssa.is>
> > > +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
> > > -table nat {
> > > -	chain postrouting {
> > > -		type nat hook postrouting priority 100;
> > > +table driver-fw {
> > > +	chain input {
> > > +		type filter hook input priority filter; policy drop;
> > > +	}
> > > +
> > > +	chain output {
> > > +		type filter hook output priority filter; policy drop;
> > > +	}
> > > +
> > > +	chain forward {
> > > +		type filter hook forward priority filter; policy drop;
> > >   	}
> > >   }
> > Just checking: did you see my question about the formatting of this last
> > time?
> >
> > https://spectrum-os.org/lists/archives/spectrum-devel/87see1t69a.fsf@alyssa.is/
>
> I saw the question but forgot to answer it. Thanks for reminding me.
>
> At least inside nixpkgs all nftables chains are declared in this style, so
> yes it is a common way of writing it. I suppose because the policy is
> semantically part of the chain "declaration" as opposed to the rules
> following it.

Cool, then:

Reviewed-by: Alyssa Ross <hi@alyssa.is>

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 4/7] tools: add spectrum-router
  2025-11-28 22:30 ` [PATCH v2 4/7] tools: add spectrum-router Yureka Lilian
@ 2025-11-29 13:18   ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:18 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 1435 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> The tokio-vhost & vhost-device-net crates which we also wrote and depend
> on are left external in the outlook of becoming a rust-vmm project soon.
>
> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
> ---
>  pkgs/default.nix                |   2 +
>  tools/router/Cargo.lock         | 807 ++++++++++++++++++++++++++++++++
>  tools/router/Cargo.lock.license |   2 +
>  tools/router/Cargo.toml         |  21 +
>  tools/router/default.nix        |  18 +
>  tools/router/src/main.rs        |  73 +++
>  tools/router/src/packet.rs      | 187 ++++++++
>  tools/router/src/protocol.rs    |  65 +++
>  tools/router/src/router.rs      | 133 ++++++
>  tools/router/src/upstream.rs    | 170 +++++++
>  10 files changed, 1478 insertions(+)
>  create mode 100644 tools/router/Cargo.lock
>  create mode 100644 tools/router/Cargo.lock.license
>  create mode 100644 tools/router/Cargo.toml
>  create mode 100644 tools/router/default.nix
>  create mode 100644 tools/router/src/main.rs
>  create mode 100644 tools/router/src/packet.rs
>  create mode 100644 tools/router/src/protocol.rs
>  create mode 100644 tools/router/src/router.rs
>  create mode 100644 tools/router/src/upstream.rs

Acked-by: Alyssa Ross <hi@alyssa.is>

I think there's some stuff that would be nice to do here to better match
the rest of the Rust code in the Spectrum tree, but I think that'll all
happen naturally over time.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 6/7] img/app: change to ipv6 nameserver
  2025-11-28 22:30 ` [PATCH v2 6/7] img/app: change to ipv6 nameserver Yureka Lilian
@ 2025-11-29 13:20   ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:20 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 519 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> Also remove hard-coded ipv4 default route.
> Eventually this will be changed to have both ipv4 and ipv6 nameserver,
> and support IPv4 with the new router.

And eventually probably not hardcode the nameserver I guess?

> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
> ---
>  img/app/image/etc/mdev/iface  | 17 -----------------
>  img/app/image/etc/resolv.conf |  2 +-
>  2 files changed, 1 insertion(+), 18 deletions(-)

Reviewed-by: Alyssa Ross <hi@alyssa.is>

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 7/7] checks/integration: Adapt networking test for ipv6
  2025-11-28 22:30 ` [PATCH v2 7/7] checks/integration: Adapt networking test for ipv6 Yureka Lilian
@ 2025-11-29 13:26   ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:26 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 2659 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
> ---
>  release/checks/integration/default.nix  |  2 +-
>  release/checks/integration/networking.c | 32 +++++++++++++++++++------
>  2 files changed, 26 insertions(+), 8 deletions(-)

Reviewed-by: Alyssa Ross <hi@alyssa.is>

Presumably the IPv4 networking test stopped working at some point in
this series though, so we should disable it there at re-enable it here
so that we don't have commits that don't build.

> diff --git a/release/checks/integration/networking.c b/release/checks/integration/networking.c
> index 97d7895..6600385 100644
> --- a/release/checks/integration/networking.c
> +++ b/release/checks/integration/networking.c
> @@ -1,5 +1,6 @@
>  // SPDX-License-Identifier: EUPL-1.2+
>  // SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
> +// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>  
>  #include "lib.h"
>  
> @@ -12,21 +13,23 @@
>  #include <net/if.h>
>  
>  #include <sys/ioctl.h>
> +#include <linux/ipv6.h>
>  
>  static int setup_server(void)
>  {
>  	int fd;
>  	struct ifreq ifr;
> +	struct in6_ifreq ifr6;
>  
> -	struct sockaddr_in addr = {
> -		.sin_family = AF_INET,
> -		.sin_port = htons(1234),
> -		.sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) },
> +	struct sockaddr_in6 addr = {
> +		.sin6_family = AF_INET6,
> +		.sin6_port = htons(1234),
> +		.sin6_addr = { .s6_addr = { 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 } },
>  	};

Just a thought: could we declare and fill in ifr6 here, rather than
filling it in later?

>  	sprintf(ifr.ifr_name, "lo");
>  
> -	if ((fd = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, 0)) == -1) {
> +	if ((fd = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC, 0)) == -1) {
>  		perror("socket");
>  		exit(EXIT_FAILURE);
>  	}
> @@ -42,11 +45,26 @@ static int setup_server(void)
>  		exit(EXIT_FAILURE);
>  	}
>  
> -	if (bind(fd, &addr, sizeof addr) == -1) {
> -		perror("bind");
> +	ifr6.ifr6_ifindex = 1;
> +	ifr6.ifr6_addr = addr.sin6_addr;
> +	ifr6.ifr6_prefixlen = 128;
> +	if (ioctl(fd, SIOCSIFADDR, &ifr6) == -1) {
> +		perror("SIOCSIFADDR");
> +		exit(EXIT_FAILURE);
> +	}
> +
> +	if ((fd = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC, 0)) == -1) {
> +		perror("socket");
>  		exit(EXIT_FAILURE);
>  	}
>  
> +	int tries = 0;
> +	while (bind(fd, &addr, sizeof addr) == -1) {
> +		perror("bind");
> +		if (tries++ >= 5)
> +			exit(EXIT_FAILURE);
> +	}
> +

I really want to figure out why this happens, but not a blocker.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 5/7] host: integrate router
  2025-11-28 22:30 ` [PATCH v2 5/7] host: integrate router Yureka Lilian
@ 2025-11-29 13:46   ` Alyssa Ross
  2025-11-29 14:28     ` Yureka
  0 siblings, 1 reply; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 13:46 UTC (permalink / raw)
  To: Yureka Lilian; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 11759 bytes --]

Yureka Lilian <yureka@cyberchaos.dev> writes:

> This removes the old host bridge + taps glue, and instead connects the
> apps to their net provider's router instance.
>
> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
> ---
>  host/rootfs/default.nix                       |  4 +-
>  host/rootfs/file-list.mk                      |  3 +
>  .../data/service/spectrum-router/down         |  0
>  .../template/data/service/spectrum-router/run | 13 ++++
>  .../image/usr/bin/assign-driver-router-iface  | 11 +++
>  host/rootfs/image/usr/bin/run-vmm             | 12 +--
>  host/rootfs/image/usr/bin/vm-import           | 13 ----
>  pkgs/overlay.nix                              |  1 +
>  tools/start-vmm/ch.rs                         | 38 ++--------
>  tools/start-vmm/lib.rs                        | 76 +++++++++++++------
>  tools/start-vmm/meson.build                   |  2 +-
>  tools/start-vmm/net-util.c                    | 39 ----------
>  tools/start-vmm/net-util.h                    |  6 --
>  tools/start-vmm/net.c                         | 55 --------------
>  tools/start-vmm/net.rs                        | 11 ---
>  tools/start-vmm/tests/meson.build             |  5 --
>  .../start-vmm/tests/tap_open-name-too-long.c  | 20 -----
>  tools/start-vmm/tests/tap_open.c              | 28 -------
>  18 files changed, 89 insertions(+), 248 deletions(-)
>  create mode 100644 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down
>  create mode 100755 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
>  create mode 100755 host/rootfs/image/usr/bin/assign-driver-router-iface
>  delete mode 100644 tools/start-vmm/net-util.c
>  delete mode 100644 tools/start-vmm/net-util.h
>  delete mode 100644 tools/start-vmm/net.c
>  delete mode 100644 tools/start-vmm/tests/tap_open-name-too-long.c
>  delete mode 100644 tools/start-vmm/tests/tap_open.c
>
> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix
> index 4bbbe23..3b8557c 100644
> --- a/host/rootfs/default.nix
> +++ b/host/rootfs/default.nix
> @@ -8,7 +8,7 @@ import ../../lib/call-package.nix (
>  }:
>  pkgsMusl.callPackage (
>  
> -{ spectrum-host-tools
> +{ spectrum-host-tools, spectrum-router
>  , lib, stdenvNoCC, nixos, runCommand, writeClosure, erofs-utils, s6-rc
>  , busybox, cloud-hypervisor, cosmic-files, crosvm, cryptsetup
>  , dejavu_fonts, dbus, execline, foot, fuse3, iproute2, inotify-tools
> @@ -27,7 +27,7 @@ let
>      cloud-hypervisor cosmic-files crosvm cryptsetup dbus execline
>      fuse3 inotify-tools iproute2 jq kmod mdevd s6 s6-linux-init s6-rc
>      socat spectrum-host-tools systemd util-linuxMinimal virtiofsd
> -    xdg-desktop-portal-spectrum-host
> +    xdg-desktop-portal-spectrum-host spectrum-router

Usually I try to keep these sorted, but I can always fix that sort of
thing up myself.

> diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down
> new file mode 100644
> index 0000000..e69de29
> diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
> new file mode 100755
> index 0000000..fae9d9d
> --- /dev/null
> +++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
> @@ -0,0 +1,13 @@
> +#!/bin/execlineb -P
> +# SPDX-License-Identifier: EUPL-1.2+
> +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
> +
> +importas -i VM VM
> +
> +background {
> +  assign-driver-router-iface ${VM}

You can just write $VM here since it's a whole word on its own.

> +}
> +
> +export RUST_LOG debug

This intentioally still here?

> +spectrum-router --app-listen-path ${VM}/router-app.sock --driver-listen-path ${VM}/router-driver.sock
> +
> diff --git a/host/rootfs/image/usr/bin/assign-driver-router-iface b/host/rootfs/image/usr/bin/assign-driver-router-iface
> new file mode 100755
> index 0000000..c555fb6
> --- /dev/null
> +++ b/host/rootfs/image/usr/bin/assign-driver-router-iface
> @@ -0,0 +1,11 @@
> +#!/bin/execlineb -S1
> +# SPDX-License-Identifier: EUPL-1.2+
> +# SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
> +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
> +
> +# This script is to be called once it is known that this VM is a driver VM
> +# (net provider) AND the vmm endpoint is ready.
> +# It add the interface between the router and the driver VM.

add*s*

> diff --git a/tools/start-vmm/ch.rs b/tools/start-vmm/ch.rs
> index abe1742..56b18f4 100644
> --- a/tools/start-vmm/ch.rs
> +++ b/tools/start-vmm/ch.rs
> @@ -1,7 +1,7 @@
>  // SPDX-License-Identifier: EUPL-1.2+
>  // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
> +// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>  
> -use std::convert::TryFrom;
>  use std::ffi::OsStr;
>  use std::fs::File;
>  use std::io::Write;
> @@ -10,7 +10,6 @@ use std::num::NonZeroI32;
>  use std::os::unix::prelude::*;
>  use std::path::Path;
>  use std::process::{Command, Stdio};
> -use std::string::FromUtf8Error;
>  
>  use miniserde::{Serialize, json};
>  
> @@ -46,7 +45,7 @@ pub struct GpuConfig {
>  
>  #[derive(Serialize)]
>  pub struct NetConfig {
> -    pub fd: RawFd,
> +    pub vhost_user_sock: String,
>      pub id: String,
>      pub mac: MacAddress,
>  }
> @@ -137,7 +136,10 @@ pub fn create_vm(vm_dir: &Path, ready_fd: File, mut config: VmConfig) -> Result<
>  
>  pub fn add_net(vm_dir: &Path, net: &NetConfig) -> Result<(), NonZeroI32> {
>      let mut ch_remote = command(vm_dir, "add-net")
> -        .arg(format!("fd={},id={},mac={}", net.fd, net.id, net.mac))
> +        .arg(format!(
> +            "vhost_user=on,socket={},id={},mac={}",
> +            net.vhost_user_sock, net.id, net.mac
> +        ))
>          .stdout(Stdio::piped())
>          .spawn()
>          .or(Err(EPERM))?;

If we're not sending fds any more, I think we can just get rid of this,
and include network devices in the vm.create request.  (vhost_user_sock
will need to be changed to vhost_socket to match the Cloud Hypervisor
API.)

> diff --git a/tools/start-vmm/lib.rs b/tools/start-vmm/lib.rs
> index 0422d85..246dd6d 100644
> --- a/tools/start-vmm/lib.rs
> +++ b/tools/start-vmm/lib.rs
> @@ -1,23 +1,24 @@
>  // SPDX-License-Identifier: EUPL-1.2+
>  // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
> +// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>  
>  mod ch;
>  mod net;
>  mod s6;
>  
>  use std::borrow::Cow;
> -use std::convert::TryInto;
>  use std::env::args_os;
>  use std::ffi::OsStr;
>  use std::fs::File;
> -use std::io::{self, ErrorKind};
> +use std::hash::{Hash, Hasher};
> +use std::io::ErrorKind;
>  use std::path::Path;
>  
>  use ch::{
> -    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, PayloadConfig,
> -    VmConfig, VsockConfig,
> +    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, NetConfig,
> +    PayloadConfig, VmConfig, VsockConfig,
>  };
> -use net::net_setup;
> +use net::MacAddress;
>  
>  pub fn prog_name() -> String {
>      args_os()
> @@ -40,8 +41,6 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
>          return Err(format!("VM name may not contain a colon: {vm_name:?}"));
>      }
>  
> -    let name_bytes = vm_name.as_bytes();
> -
>      let config_dir = vm_dir.join("config");
>      let blk_dir = config_dir.join("blk");
>      let kernel_path = config_dir.join("vmlinux");
> @@ -97,24 +96,51 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
>              shared: true,
>          },
>          net: match net_providers_dir.read_dir() {
> -            Ok(_) => {
> -                // SAFETY: we check the result.
> -                let net = unsafe {
> -                    net_setup(
> -                        name_bytes.as_ptr().cast(),
> -                        name_bytes
> -                            .len()
> -                            .try_into()
> -                            .map_err(|e| format!("VM name too long: {e}"))?,
> -                    )
> -                };
> -                if net.fd == -1 {
> -                    let e = io::Error::last_os_error();
> -                    return Err(format!("setting up networking failed: {e}"));
> -                }
> -
> -                vec![net.try_into().unwrap()]
> -            }
> +            Ok(entries) => entries
> +                .into_iter()
> +                .map(|result| {
> +                    Ok(result
> +                        .map_err(|e| format!("examining directory entry: {e}"))?
> +                        .path())
> +                })
> +                .map(|result: Result<_, String>| {
> +                    let provider_name = result?.file_name().ok_or("unable to get net provider name".to_string())?.to_str().unwrap().to_string();
> +
> +                    if provider_name.contains(',') {
> +                        return Err(format!("illegal ',' character in net provider name {provider_name:?}"));
> +                    }
> +
> +                    let mut hasher = std::hash::DefaultHasher::new();
> +                    vm_name.hash(&mut hasher);
> +                    let id_hashed = hasher.finish();
> +
> +                    let mac = MacAddress::new([
> +                        0x02, // IEEE 802c administratively assigned
> +                        0x00, // Spectrum client
> +                        (id_hashed >> 24) as u8,
> +                        (id_hashed >> 16) as u8,
> +                        (id_hashed >> 8) as u8,
> +                        id_hashed as u8,
> +                    ]);
> +
> +                    let provider_id = std::fs::read_link(format!("/run/vm/by-name/{provider_name}")).map_err(|e| format!("unable to get net provider id: {e}"))?.file_name().ok_or("unable to get net provider id".to_string())?.to_str().unwrap().to_string();
> +
> +                    let svc_dir = format!("/run/service/vm-services/instance/{provider_id}/data/service/spectrum-router");
> +                    let svc_status = std::process::Command::new("s6-svc")
> +                        .args(["-U", &svc_dir])
> +                        .status()
> +                        .expect("setting up the upstream router via s6-svc failed");
> +                    if !svc_status.success() {
> +                        return Err(format!("setting up the upstream router via s6-svc failed with exit code {svc_status}"));
> +                    }

I'd prefer this was in run-vmm, since it's a bit surprising to stop in
the middle of constructing a Cloud Hypervisor API request to do service
management.

> diff --git a/tools/start-vmm/meson.build b/tools/start-vmm/meson.build
> index d07c5a0..aa9f6f3 100644
> --- a/tools/start-vmm/meson.build
> +++ b/tools/start-vmm/meson.build
> @@ -1,7 +1,7 @@
>  # SPDX-License-Identifier: EUPL-1.2+
>  # SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
>  
> -c_lib = static_library('start-vmm', 'net.c', 'net-util.c',
> +c_lib = static_library('start-vmm',
>    c_args : '-D_GNU_SOURCE')

C_lib is now completely empty, so can be removed.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 5/7] host: integrate router
  2025-11-29 13:46   ` Alyssa Ross
@ 2025-11-29 14:28     ` Yureka
  2025-11-29 14:44       ` Alyssa Ross
  0 siblings, 1 reply; 19+ messages in thread
From: Yureka @ 2025-11-29 14:28 UTC (permalink / raw)
  To: Alyssa Ross; +Cc: devel


On 11/29/25 14:46, Alyssa Ross wrote:
> Yureka Lilian <yureka@cyberchaos.dev> writes:
>
>> This removes the old host bridge + taps glue, and instead connects the
>> apps to their net provider's router instance.
>>
>> Signed-off-by: Yureka Lilian <yureka@cyberchaos.dev>
>> ---
>>   host/rootfs/default.nix                       |  4 +-
>>   host/rootfs/file-list.mk                      |  3 +
>>   .../data/service/spectrum-router/down         |  0
>>   .../template/data/service/spectrum-router/run | 13 ++++
>>   .../image/usr/bin/assign-driver-router-iface  | 11 +++
>>   host/rootfs/image/usr/bin/run-vmm             | 12 +--
>>   host/rootfs/image/usr/bin/vm-import           | 13 ----
>>   pkgs/overlay.nix                              |  1 +
>>   tools/start-vmm/ch.rs                         | 38 ++--------
>>   tools/start-vmm/lib.rs                        | 76 +++++++++++++------
>>   tools/start-vmm/meson.build                   |  2 +-
>>   tools/start-vmm/net-util.c                    | 39 ----------
>>   tools/start-vmm/net-util.h                    |  6 --
>>   tools/start-vmm/net.c                         | 55 --------------
>>   tools/start-vmm/net.rs                        | 11 ---
>>   tools/start-vmm/tests/meson.build             |  5 --
>>   .../start-vmm/tests/tap_open-name-too-long.c  | 20 -----
>>   tools/start-vmm/tests/tap_open.c              | 28 -------
>>   18 files changed, 89 insertions(+), 248 deletions(-)
>>   create mode 100644 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down
>>   create mode 100755 host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
>>   create mode 100755 host/rootfs/image/usr/bin/assign-driver-router-iface
>>   delete mode 100644 tools/start-vmm/net-util.c
>>   delete mode 100644 tools/start-vmm/net-util.h
>>   delete mode 100644 tools/start-vmm/net.c
>>   delete mode 100644 tools/start-vmm/tests/tap_open-name-too-long.c
>>   delete mode 100644 tools/start-vmm/tests/tap_open.c
>>
>> diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix
>> index 4bbbe23..3b8557c 100644
>> --- a/host/rootfs/default.nix
>> +++ b/host/rootfs/default.nix
>> @@ -8,7 +8,7 @@ import ../../lib/call-package.nix (
>>   }:
>>   pkgsMusl.callPackage (
>>   
>> -{ spectrum-host-tools
>> +{ spectrum-host-tools, spectrum-router
>>   , lib, stdenvNoCC, nixos, runCommand, writeClosure, erofs-utils, s6-rc
>>   , busybox, cloud-hypervisor, cosmic-files, crosvm, cryptsetup
>>   , dejavu_fonts, dbus, execline, foot, fuse3, iproute2, inotify-tools
>> @@ -27,7 +27,7 @@ let
>>       cloud-hypervisor cosmic-files crosvm cryptsetup dbus execline
>>       fuse3 inotify-tools iproute2 jq kmod mdevd s6 s6-linux-init s6-rc
>>       socat spectrum-host-tools systemd util-linuxMinimal virtiofsd
>> -    xdg-desktop-portal-spectrum-host
>> +    xdg-desktop-portal-spectrum-host spectrum-router
> Usually I try to keep these sorted, but I can always fix that sort of
> thing up myself.
>
>> diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/down
>> new file mode 100644
>> index 0000000..e69de29
>> diff --git a/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
>> new file mode 100755
>> index 0000000..fae9d9d
>> --- /dev/null
>> +++ b/host/rootfs/image/etc/s6-linux-init/run-image/service/vm-services/template/data/service/spectrum-router/run
>> @@ -0,0 +1,13 @@
>> +#!/bin/execlineb -P
>> +# SPDX-License-Identifier: EUPL-1.2+
>> +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>> +
>> +importas -i VM VM
>> +
>> +background {
>> +  assign-driver-router-iface ${VM}
> You can just write $VM here since it's a whole word on its own.
>
>> +}
>> +
>> +export RUST_LOG debug
> This intentioally still here?
>
>> +spectrum-router --app-listen-path ${VM}/router-app.sock --driver-listen-path ${VM}/router-driver.sock
>> +
>> diff --git a/host/rootfs/image/usr/bin/assign-driver-router-iface b/host/rootfs/image/usr/bin/assign-driver-router-iface
>> new file mode 100755
>> index 0000000..c555fb6
>> --- /dev/null
>> +++ b/host/rootfs/image/usr/bin/assign-driver-router-iface
>> @@ -0,0 +1,11 @@
>> +#!/bin/execlineb -S1
>> +# SPDX-License-Identifier: EUPL-1.2+
>> +# SPDX-FileCopyrightText: 2025 Alyssa Ross <hi@alyssa.is>
>> +# SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>> +
>> +# This script is to be called once it is known that this VM is a driver VM
>> +# (net provider) AND the vmm endpoint is ready.
>> +# It add the interface between the router and the driver VM.
> add*s*
>
>> diff --git a/tools/start-vmm/ch.rs b/tools/start-vmm/ch.rs
>> index abe1742..56b18f4 100644
>> --- a/tools/start-vmm/ch.rs
>> +++ b/tools/start-vmm/ch.rs
>> @@ -1,7 +1,7 @@
>>   // SPDX-License-Identifier: EUPL-1.2+
>>   // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
>> +// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>>   
>> -use std::convert::TryFrom;
>>   use std::ffi::OsStr;
>>   use std::fs::File;
>>   use std::io::Write;
>> @@ -10,7 +10,6 @@ use std::num::NonZeroI32;
>>   use std::os::unix::prelude::*;
>>   use std::path::Path;
>>   use std::process::{Command, Stdio};
>> -use std::string::FromUtf8Error;
>>   
>>   use miniserde::{Serialize, json};
>>   
>> @@ -46,7 +45,7 @@ pub struct GpuConfig {
>>   
>>   #[derive(Serialize)]
>>   pub struct NetConfig {
>> -    pub fd: RawFd,
>> +    pub vhost_user_sock: String,
>>       pub id: String,
>>       pub mac: MacAddress,
>>   }
>> @@ -137,7 +136,10 @@ pub fn create_vm(vm_dir: &Path, ready_fd: File, mut config: VmConfig) -> Result<
>>   
>>   pub fn add_net(vm_dir: &Path, net: &NetConfig) -> Result<(), NonZeroI32> {
>>       let mut ch_remote = command(vm_dir, "add-net")
>> -        .arg(format!("fd={},id={},mac={}", net.fd, net.id, net.mac))
>> +        .arg(format!(
>> +            "vhost_user=on,socket={},id={},mac={}",
>> +            net.vhost_user_sock, net.id, net.mac
>> +        ))
>>           .stdout(Stdio::piped())
>>           .spawn()
>>           .or(Err(EPERM))?;
> If we're not sending fds any more, I think we can just get rid of this,
> and include network devices in the vm.create request.  (vhost_user_sock
> will need to be changed to vhost_socket to match the Cloud Hypervisor
> API.)

Ah, I completely missed the reason it was originally structured like 
this! Will do.

>
>> diff --git a/tools/start-vmm/lib.rs b/tools/start-vmm/lib.rs
>> index 0422d85..246dd6d 100644
>> --- a/tools/start-vmm/lib.rs
>> +++ b/tools/start-vmm/lib.rs
>> @@ -1,23 +1,24 @@
>>   // SPDX-License-Identifier: EUPL-1.2+
>>   // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
>> +// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>>   
>>   mod ch;
>>   mod net;
>>   mod s6;
>>   
>>   use std::borrow::Cow;
>> -use std::convert::TryInto;
>>   use std::env::args_os;
>>   use std::ffi::OsStr;
>>   use std::fs::File;
>> -use std::io::{self, ErrorKind};
>> +use std::hash::{Hash, Hasher};
>> +use std::io::ErrorKind;
>>   use std::path::Path;
>>   
>>   use ch::{
>> -    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, PayloadConfig,
>> -    VmConfig, VsockConfig,
>> +    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, NetConfig,
>> +    PayloadConfig, VmConfig, VsockConfig,
>>   };
>> -use net::net_setup;
>> +use net::MacAddress;
>>   
>>   pub fn prog_name() -> String {
>>       args_os()
>> @@ -40,8 +41,6 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
>>           return Err(format!("VM name may not contain a colon: {vm_name:?}"));
>>       }
>>   
>> -    let name_bytes = vm_name.as_bytes();
>> -
>>       let config_dir = vm_dir.join("config");
>>       let blk_dir = config_dir.join("blk");
>>       let kernel_path = config_dir.join("vmlinux");
>> @@ -97,24 +96,51 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
>>               shared: true,
>>           },
>>           net: match net_providers_dir.read_dir() {
>> -            Ok(_) => {
>> -                // SAFETY: we check the result.
>> -                let net = unsafe {
>> -                    net_setup(
>> -                        name_bytes.as_ptr().cast(),
>> -                        name_bytes
>> -                            .len()
>> -                            .try_into()
>> -                            .map_err(|e| format!("VM name too long: {e}"))?,
>> -                    )
>> -                };
>> -                if net.fd == -1 {
>> -                    let e = io::Error::last_os_error();
>> -                    return Err(format!("setting up networking failed: {e}"));
>> -                }
>> -
>> -                vec![net.try_into().unwrap()]
>> -            }
>> +            Ok(entries) => entries
>> +                .into_iter()
>> +                .map(|result| {
>> +                    Ok(result
>> +                        .map_err(|e| format!("examining directory entry: {e}"))?
>> +                        .path())
>> +                })
>> +                .map(|result: Result<_, String>| {
>> +                    let provider_name = result?.file_name().ok_or("unable to get net provider name".to_string())?.to_str().unwrap().to_string();
>> +
>> +                    if provider_name.contains(',') {
>> +                        return Err(format!("illegal ',' character in net provider name {provider_name:?}"));
>> +                    }
>> +
>> +                    let mut hasher = std::hash::DefaultHasher::new();
>> +                    vm_name.hash(&mut hasher);
>> +                    let id_hashed = hasher.finish();
>> +
>> +                    let mac = MacAddress::new([
>> +                        0x02, // IEEE 802c administratively assigned
>> +                        0x00, // Spectrum client
>> +                        (id_hashed >> 24) as u8,
>> +                        (id_hashed >> 16) as u8,
>> +                        (id_hashed >> 8) as u8,
>> +                        id_hashed as u8,
>> +                    ]);
>> +
>> +                    let provider_id = std::fs::read_link(format!("/run/vm/by-name/{provider_name}")).map_err(|e| format!("unable to get net provider id: {e}"))?.file_name().ok_or("unable to get net provider id".to_string())?.to_str().unwrap().to_string();
>> +
>> +                    let svc_dir = format!("/run/service/vm-services/instance/{provider_id}/data/service/spectrum-router");
>> +                    let svc_status = std::process::Command::new("s6-svc")
>> +                        .args(["-U", &svc_dir])
>> +                        .status()
>> +                        .expect("setting up the upstream router via s6-svc failed");
>> +                    if !svc_status.success() {
>> +                        return Err(format!("setting up the upstream router via s6-svc failed with exit code {svc_status}"));
>> +                    }
> I'd prefer this was in run-vmm, since it's a bit surprising to stop in
> the middle of constructing a Cloud Hypervisor API request to do service
> management.

Is it by any chance even guaranteed that at the point when run-vmm for 
this VM runs, the vmm for the provider VM would already be up? That 
would simplify the process and make assign-driver-router-iface 
unnecessary because there would be one place where we can add the interface.

>
>> diff --git a/tools/start-vmm/meson.build b/tools/start-vmm/meson.build
>> index d07c5a0..aa9f6f3 100644
>> --- a/tools/start-vmm/meson.build
>> +++ b/tools/start-vmm/meson.build
>> @@ -1,7 +1,7 @@
>>   # SPDX-License-Identifier: EUPL-1.2+
>>   # SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
>>   
>> -c_lib = static_library('start-vmm', 'net.c', 'net-util.c',
>> +c_lib = static_library('start-vmm',
>>     c_args : '-D_GNU_SOURCE')
> C_lib is now completely empty, so can be removed.

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH v2 5/7] host: integrate router
  2025-11-29 14:28     ` Yureka
@ 2025-11-29 14:44       ` Alyssa Ross
  0 siblings, 0 replies; 19+ messages in thread
From: Alyssa Ross @ 2025-11-29 14:44 UTC (permalink / raw)
  To: Yureka; +Cc: devel

[-- Attachment #1: Type: text/plain, Size: 5426 bytes --]

Yureka <yuka@yuka.dev> writes:

> On 11/29/25 14:46, Alyssa Ross wrote:
>> Yureka Lilian <yureka@cyberchaos.dev> writes:
>>
>>> diff --git a/tools/start-vmm/lib.rs b/tools/start-vmm/lib.rs
>>> index 0422d85..246dd6d 100644
>>> --- a/tools/start-vmm/lib.rs
>>> +++ b/tools/start-vmm/lib.rs
>>> @@ -1,23 +1,24 @@
>>>   // SPDX-License-Identifier: EUPL-1.2+
>>>   // SPDX-FileCopyrightText: 2022-2024 Alyssa Ross <hi@alyssa.is>
>>> +// SPDX-FileCopyrightText: 2025 Yureka Lilian <yureka@cyberchaos.dev>
>>>   
>>>   mod ch;
>>>   mod net;
>>>   mod s6;
>>>   
>>>   use std::borrow::Cow;
>>> -use std::convert::TryInto;
>>>   use std::env::args_os;
>>>   use std::ffi::OsStr;
>>>   use std::fs::File;
>>> -use std::io::{self, ErrorKind};
>>> +use std::hash::{Hash, Hasher};
>>> +use std::io::ErrorKind;
>>>   use std::path::Path;
>>>   
>>>   use ch::{
>>> -    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, PayloadConfig,
>>> -    VmConfig, VsockConfig,
>>> +    ConsoleConfig, DiskConfig, FsConfig, GpuConfig, LandlockConfig, MemoryConfig, NetConfig,
>>> +    PayloadConfig, VmConfig, VsockConfig,
>>>   };
>>> -use net::net_setup;
>>> +use net::MacAddress;
>>>   
>>>   pub fn prog_name() -> String {
>>>       args_os()
>>> @@ -40,8 +41,6 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
>>>           return Err(format!("VM name may not contain a colon: {vm_name:?}"));
>>>       }
>>>   
>>> -    let name_bytes = vm_name.as_bytes();
>>> -
>>>       let config_dir = vm_dir.join("config");
>>>       let blk_dir = config_dir.join("blk");
>>>       let kernel_path = config_dir.join("vmlinux");
>>> @@ -97,24 +96,51 @@ pub fn vm_config(vm_dir: &Path) -> Result<VmConfig, String> {
>>>               shared: true,
>>>           },
>>>           net: match net_providers_dir.read_dir() {
>>> -            Ok(_) => {
>>> -                // SAFETY: we check the result.
>>> -                let net = unsafe {
>>> -                    net_setup(
>>> -                        name_bytes.as_ptr().cast(),
>>> -                        name_bytes
>>> -                            .len()
>>> -                            .try_into()
>>> -                            .map_err(|e| format!("VM name too long: {e}"))?,
>>> -                    )
>>> -                };
>>> -                if net.fd == -1 {
>>> -                    let e = io::Error::last_os_error();
>>> -                    return Err(format!("setting up networking failed: {e}"));
>>> -                }
>>> -
>>> -                vec![net.try_into().unwrap()]
>>> -            }
>>> +            Ok(entries) => entries
>>> +                .into_iter()
>>> +                .map(|result| {
>>> +                    Ok(result
>>> +                        .map_err(|e| format!("examining directory entry: {e}"))?
>>> +                        .path())
>>> +                })
>>> +                .map(|result: Result<_, String>| {
>>> +                    let provider_name = result?.file_name().ok_or("unable to get net provider name".to_string())?.to_str().unwrap().to_string();
>>> +
>>> +                    if provider_name.contains(',') {
>>> +                        return Err(format!("illegal ',' character in net provider name {provider_name:?}"));
>>> +                    }
>>> +
>>> +                    let mut hasher = std::hash::DefaultHasher::new();
>>> +                    vm_name.hash(&mut hasher);
>>> +                    let id_hashed = hasher.finish();
>>> +
>>> +                    let mac = MacAddress::new([
>>> +                        0x02, // IEEE 802c administratively assigned
>>> +                        0x00, // Spectrum client
>>> +                        (id_hashed >> 24) as u8,
>>> +                        (id_hashed >> 16) as u8,
>>> +                        (id_hashed >> 8) as u8,
>>> +                        id_hashed as u8,
>>> +                    ]);
>>> +
>>> +                    let provider_id = std::fs::read_link(format!("/run/vm/by-name/{provider_name}")).map_err(|e| format!("unable to get net provider id: {e}"))?.file_name().ok_or("unable to get net provider id".to_string())?.to_str().unwrap().to_string();
>>> +
>>> +                    let svc_dir = format!("/run/service/vm-services/instance/{provider_id}/data/service/spectrum-router");
>>> +                    let svc_status = std::process::Command::new("s6-svc")
>>> +                        .args(["-U", &svc_dir])
>>> +                        .status()
>>> +                        .expect("setting up the upstream router via s6-svc failed");
>>> +                    if !svc_status.success() {
>>> +                        return Err(format!("setting up the upstream router via s6-svc failed with exit code {svc_status}"));
>>> +                    }
>> I'd prefer this was in run-vmm, since it's a bit surprising to stop in
>> the middle of constructing a Cloud Hypervisor API request to do service
>> management.
>
> Is it by any chance even guaranteed that at the point when run-vmm for 
> this VM runs, the vmm for the provider VM would already be up? That 
> would simplify the process and make assign-driver-router-iface 
> unnecessary because there would be one place where we can add the interface.

No, but you could s6-svwait in there, in the existing background block.

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 227 bytes --]

^ permalink raw reply	[flat|nested] 19+ messages in thread

end of thread, other threads:[~2025-11-29 14:44 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-28 22:30 [PATCH v2 0/7] spectrum-router Yureka Lilian
2025-11-28 22:30 ` [PATCH v2 1/7] vm/sys/net: remove connman Yureka Lilian
2025-11-29 13:06   ` Alyssa Ross
2025-11-28 22:30 ` [PATCH v2 2/7] vm/sys/net: integrate xdp-forwarder Yureka Lilian
2025-11-29 13:08   ` Alyssa Ross
2025-11-29 13:15     ` Yureka
2025-11-29 13:17       ` Alyssa Ross
2025-11-28 22:30 ` [PATCH v2 3/7] vm/sys/net: add iwd Yureka Lilian
2025-11-29 13:09   ` Alyssa Ross
2025-11-28 22:30 ` [PATCH v2 4/7] tools: add spectrum-router Yureka Lilian
2025-11-29 13:18   ` Alyssa Ross
2025-11-28 22:30 ` [PATCH v2 5/7] host: integrate router Yureka Lilian
2025-11-29 13:46   ` Alyssa Ross
2025-11-29 14:28     ` Yureka
2025-11-29 14:44       ` Alyssa Ross
2025-11-28 22:30 ` [PATCH v2 6/7] img/app: change to ipv6 nameserver Yureka Lilian
2025-11-29 13:20   ` Alyssa Ross
2025-11-28 22:30 ` [PATCH v2 7/7] checks/integration: Adapt networking test for ipv6 Yureka Lilian
2025-11-29 13:26   ` Alyssa Ross

Code repositories for project(s) associated with this public inbox

	https://spectrum-os.org/git/crosvm
	https://spectrum-os.org/git/doc
	https://spectrum-os.org/git/mktuntap
	https://spectrum-os.org/git/nixpkgs
	https://spectrum-os.org/git/spectrum
	https://spectrum-os.org/git/ucspi-vsock
	https://spectrum-os.org/git/www

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).