No description
  • Rust 96.3%
  • Shell 3.7%
Find a file
Jan De Landtsheer 0ccacca46e
Add infinite retry with exponential backoff for transient errors
mosnetd now retries run() internally on transient failures (DHCP timeout,
no carrier, netlink hiccup, OVS not ready) with exponential backoff
capped at 30s. Fatal errors (bad cmdline/config) exit immediately.
Reduces reliance on zinit restarts for transient boot-time failures.
2026-03-13 10:25:29 +01:00
docs Add infinite retry with exponential backoff for transient errors 2026-03-13 10:25:29 +01:00
examples Add dual-NIC topology support and rewrite ADR 015 2026-03-12 09:32:30 +01:00
packaging/alpine/rootfs-overlay/etc Guard OVS CLI commands at runtime, preserve logs across restarts 2026-03-12 12:40:30 +01:00
scripts Add dual-NIC topology support and rewrite ADR 015 2026-03-12 09:32:30 +01:00
src Add infinite retry with exponential backoff for transient errors 2026-03-13 10:25:29 +01:00
tests Add dual-NIC topology support and rewrite ADR 015 2026-03-12 09:32:30 +01:00
.gitignore Add out/ to .gitignore 2026-02-26 17:34:55 +01:00
.mcp.json Add dual-NIC topology support and rewrite ADR 015 2026-03-12 09:32:30 +01:00
Cargo.lock Add datapath and conntrack CLI subcommands via AppCtl 2026-03-11 13:42:40 +01:00
Cargo.toml Add infinite retry with exponential backoff for transient errors 2026-03-13 10:25:29 +01:00
CLAUDE.md Add infinite retry with exponential backoff for transient errors 2026-03-13 10:25:29 +01:00
Containerfile.test Implement OpenRPC IPC between mosnet CLI and mosnetd 2026-03-09 09:41:07 +01:00
README.md Add dual-NIC topology support and rewrite ADR 015 2026-03-12 09:32:30 +01:00
static-network-params.md Initial commit: mosnet with OVS bridge module 2026-02-05 00:57:20 +01:00

mosnet

A bare-metal network bootstrap tool for Linux systems, supporting both static IP configuration (with MAC NAT for MAC-restricted environments) and automatic DHCP configuration.

Features

  • Dual-mode operation: Automatically detects whether to use static or DHCP configuration
  • Dual bridge backends: Linux bridge (no dependencies) or OVS bridge (MAC NAT, OpenFlow)
  • MAC NAT: Transparent MAC address rewriting for MAC-restricted environments (e.g., Hetzner) — OVS only
  • ARP/NDP Proxy: Responds to ARP (IPv4) and NDP (IPv6) requests for configured IPs — OVS only
  • Built-in DHCP client: Full DHCP implementation with lease renewal
  • RFC1918 Priority: In DHCP mode, prefers private addresses for interface selection
  • IPv6 Support: Static configuration, SLAAC, or DHCPv6 in DHCP mode
  • nftables Firewall: Configurable firewall with named services or custom ports
  • NAT Support: Masquerading (SNAT) with auto-detection and port forwarding (DNAT)

Installation

Prerequisites

  • Rust 1.85+ (2024 edition)
  • Linux with netlink support
  • Open vSwitch (optional — only needed for mosbridge=ovs)

Building

cargo build --release                          # Default: OVS + Linux bridge
cargo build --release --no-default-features    # Linux bridge only (no OVS dependency)

This produces two binaries:

  • target/release/mosnetd — the daemon
  • target/release/mosnet — the CLI

Cargo Features

Feature Default Description
ovs yes OVS bridge, MAC NAT, ARP/NDP proxy controller (requires rovs crates)

A minimal build (--no-default-features) produces a binary with Linux bridge + firewall/NAT but no OVS.

Usage

mosnet consists of two binaries:

  • mosnetd — the daemon that configures networking, writes state to /var/cache/etc/mosnet.json, and parks (handling lease renewal in DHCP mode)
  • mosnet — a CLI for inspecting daemon state and live system networking

mosnetd reads configuration from the kernel command line (/proc/cmdline). The operating mode is determined automatically based on which parameters are present.

Static Mode

Use static mode when you have a fixed IP address, especially in MAC-restricted environments like Hetzner dedicated servers.

Kernel parameters:

mosip4=<address>,<gateway>,<netmask>[,<device>]
mosip6=<address>/<prefix>,<gateway>[,<device>]

Examples:

Standard network:

mosip4=192.168.1.10,192.168.1.1,255.255.255.0

Hetzner point-to-point configuration:

mosip4=136.243.10.20,136.243.10.1,255.255.255.255
mosip6=2a01:4f8:c17:1234::1/128,fe80::1

With explicit interface:

mosip4=192.168.1.10,192.168.1.1,255.255.255.0,enp3s0

Dual-stack:

mosip4=192.168.1.10,192.168.1.1,255.255.255.0 mosip6=2001:db8::1/64,2001:db8::ffff

DHCP Mode

Use DHCP mode when no static IP is needed. mosnet will probe all interfaces for DHCP offers and select the best one.

Trigger: Absence of both mosip4 and mosip6 parameters.

Optional parameters:

mosdhcp_timeout=<seconds>           # Probe timeout per interface (default: 10)
mosdhcp_prefer_private=yes|no       # Prefer RFC1918 addresses (default: yes)

Examples:

Default DHCP (no parameters needed):

# Just boot without mosip4/mosip6

Custom timeout:

mosdhcp_timeout=30

Prefer public IPs:

mosdhcp_prefer_private=no

Firewall

mosnet includes an nftables-based firewall that can be configured via kernel cmdline.

Parameter:

mosfirewall=<service>[,<service>]...

Named services:

  • ssh - TCP port 22
  • http - TCP port 80
  • https - TCP port 443
  • dns - UDP port 53
  • dhcp - UDP ports 67, 68

Custom ports:

  • <port>/tcp - Custom TCP port
  • <port>/udp - Custom UDP port

Special values:

  • none - Disable firewall (skip setup entirely)
  • permissive - Accept all traffic (creates table but default-accept)

Examples:

SSH only (secure default):

mosfirewall=ssh

Web server:

mosfirewall=ssh,http,https

Custom ports:

mosfirewall=ssh,8080/tcp,51820/udp

Permissive (accept all):

mosfirewall=permissive

The firewall creates an nftables inet table mosnet with:

  • Default drop policy for input (unless permissive)
  • Allow loopback
  • Allow established/related connections
  • Allow ICMP and ICMPv6 (ping)
  • Allow specified TCP/UDP ports

NAT

mosnet supports Source NAT (masquerading) and port forwarding via nftables.

Parameter:

mosnat=<interface>[,<port_forward>...]

Interface options:

  • snat - Auto-detect default gateway interface (recommended)
  • eth0 - Specific interface name
  • none - Disable NAT

Port forward format:

<external_port>:<internal_ip>:<internal_port>/<protocol>

Examples:

Auto-detect masquerade interface:

mosnat=snat

Masquerade on specific interface:

mosnat=eth0

Masquerade with port forwarding:

mosnat=snat,8080:192.168.1.100:80/tcp

Multiple port forwards:

mosnat=snat,8080:192.168.1.100:80/tcp,2222:192.168.1.100:22/tcp,53:192.168.1.53:53/udp

The NAT creates an nftables table mosnet-nat with:

  • Postrouting chain for masquerading outbound traffic
  • Prerouting chain for DNAT port forwarding

Bridge Backend

mosnet supports two bridge backends, selectable via kernel cmdline.

Parameter:

mosbridge=ovs|linux
  • ovs — OVS bridge with OpenFlow (requires ovs feature + running ovsdb-server/ovs-vswitchd)
  • linux — Linux bridge via netlink (no external daemons, always available)
  • When not specified: auto-selects OVS if compiled with ovs feature, otherwise Linux bridge

Examples:

mosbridge=linux                    # Force Linux bridge
mosbridge=ovs                      # Force OVS bridge
# (no parameter)                   # Auto-select based on compiled features

Running the Daemon

Execute mosnetd as root:

sudo ./mosnetd

mosnetd will:

  1. Parse the kernel command line
  2. Detect the operating mode (static or DHCP)
  3. Discover network interfaces
  4. Probe for connectivity (ARP for static, DHCP for dynamic)
  5. Select bridge backend (OVS or Linux, based on mosbridge= and compiled features)
  6. Create bridge and enslave the physical NIC
  7. Configure IP addresses and routes
  8. Configure firewall/NAT if specified
  9. Write state to /var/cache/etc/mosnet.json
  10. In static mode + OVS: run the ARP/NDP proxy controller
  11. In DHCP mode: handle lease renewal in the background (state file updated on each renewal)

Inspecting State with the CLI

Once mosnetd is running, use mosnet to inspect the system:

mosnet                    # Status overview (default)
mosnet status             # Same as above
mosnet config             # Show kernel parameters
mosnet lease              # DHCP lease details and timers
mosnet dns                # DNS configuration from /etc/resolv.conf
mosnet sysctl             # Kernel sysctl values (forwarding, proxy_arp, etc)
mosnet interfaces         # Network interfaces with addresses and link state
mosnet routes             # Routing table
mosnet neighbors          # ARP/NDP neighbor table (filtered; --all for everything)
mosnet bridge             # Bridge configuration and enslaved ports
mosnet firewall           # nftables rules
mosnet vlan              # VLAN filtering configuration
mosnet ovs                # OVS bridge state (ovs feature only)
mosnet flows              # OpenFlow flow table (ovs feature only)
mosnet prefix            # Delegated IPv6 prefix (DHCPv6-PD)
mosnet datapath          # Datapath flows (ovs feature only; --verbose for masks)
mosnet conntrack         # Conntrack entries (ovs feature only; --zone, --stats, --flush)
mosnet discover           # List available RPC methods (--json for full OpenRPC spec)
mosnet verify             # Cross-reference state file with live system
mosnet health             # Health check (exit 0=healthy, 1=degraded)

All subcommands support --json for machine-parseable output:

mosnet interfaces --json
mosnet discover --json    # Full OpenRPC specification
mosnet health --json

Environment Variables

  • RUST_LOG: Control logging verbosity (e.g., RUST_LOG=debug)
RUST_LOG=debug sudo ./mosnetd

Operating Modes Comparison

Aspect Static Mode DHCP Mode
Trigger mosip4 or mosip6 present No static IP params
Interface selection ARP probe to gateway DHCP probe (RFC1918 priority)
IPv4 source Kernel cmdline DHCP lease
IPv6 source Kernel cmdline SLAAC + DHCPv6
Lease renewal N/A Background task (T1/T2)
Use case Hetzner, MAC-restricted, fixed IP Standard networks

Architecture

Both bridge backends use the same physical pattern — the physical NIC is enslaved as a pure L2 port:

Physical NIC (enp3s0) ──► Bridge (mos)
     │                         │
     │                         ├── Host IP configured here
     │                         └── VM ports (future)
     └── No IP (pure L2 bridge port), MAC inherited by bridge

Linux Bridge (mosbridge=linux)

Created via rtnetlink — no external daemons needed. IP is configured directly on the bridge interface (mos). Always available, no feature gates required.

Host routing — In static mode, enable_host_routing() configures the kernel to forward traffic for VMs/containers behind the bridge:

Sysctl Value Purpose
net.ipv4.ip_forward 1 Enable IPv4 packet forwarding
net.ipv6.conf.all.forwarding 1 Enable IPv6 packet forwarding
net.ipv4.conf.<bridge>.proxy_arp 1 Bridge answers ARP requests on behalf of routed IPv4 addresses
net.ipv6.conf.<bridge>.proxy_ndp 1 Bridge answers NDP Neighbor Solicitations on behalf of routed IPv6 addresses
net.ipv6.conf.<bridge>.accept_ra 2 Accept Router Advertisements even with forwarding enabled (required for SLAAC)
net.ipv6.conf.<uplink>.disable_ipv6 1 Prevent SLAAC on the physical NIC (L2 port should not have its own IPv6 address)

Point-to-point routing — For /32 IPv4 (netmask 255.255.255.255) or /128 IPv6 configs where the gateway is not on the local subnet (e.g. Hetzner), a host route to the gateway is added before the default route so the kernel can reach it.

DHCP mode — IPv4/IPv6 forwarding is enabled (for VM/container routing) and accept_ra=2 is set on the bridge interface for SLAAC. Proxy ARP/NDP is not enabled (those are static-mode-only for Hetzner MAC restrictions).

Hetzner compatibility: Additional MAC-restricted IPs are fully supported. The host answers ARP/NDP using the physical MAC (proxy ARP/NDP), and VM traffic is routed through the host at L3 — so outbound packets always carry the physical MAC. VMs/containers must use the host's primary IP as their default gateway (routed, not L2-bridged).

OVS Bridge (mosbridge=ovs)

Created via rovs-ovsdb — requires running ovsdb-server and ovs-vswitchd. IP is configured on the bridge-local interface (mos) in both static and DHCP mode. Topology-specific extra internal ports may still be created for VLAN layouts. Requires the ovs Cargo feature (default).

┌─────────────────────────────────────────────┐
│             OVS Bridge: mos                 │
│                                             │
│  ┌─────────────────┐  ┌─────────────────┐  │
│  │ Uplink Port     │  │ Internal Port   │  │
│  │ (physical NIC)  │  │ "mos"           │  │
│  │ Type: system    │  │ Type: internal  │  │
│  │ No IP           │  │ Host IP here    │  │
│  └─────────────────┘  └─────────────────┘  │
│                                             │
│  OpenFlow: unix socket                      │
└─────────────────────────────────────────────┘

MAC NAT — OpenFlow rules rewrite source MACs to the physical NIC's MAC on egress, and rewrite destination MACs for incoming VM traffic on ingress. This allows VMs/containers to have their own MAC addresses while satisfying Hetzner's MAC filtering.

ARP/NDP proxy — In static mode, an OpenFlow controller listens on the bridge's management socket and:

  • Intercepts ARP requests and responds with the physical NIC's MAC for all configured IPv4 addresses
  • Intercepts NDP Neighbor Solicitations and responds with Neighbor Advertisements using the physical NIC's MAC for all configured IPv6 addresses

IPv6disable_ipv6=1 is set on the uplink port to prevent it from acquiring its own SLAAC address.

DHCP modeaccept_ra=2 is set on the internal port for SLAAC. The controller is not started in DHCP mode (no MAC NAT needed for standard networks).

Bridge Backend Comparison

Aspect Linux Bridge OVS Bridge
IP interface mos mos
Dependencies None (kernel only) ovsdb-server, ovs-vswitchd
MAC NAT No (not needed — L3 routing) Yes (OpenFlow, L2 rewrite)
IPv4 host routing kernel ip_forward + proxy ARP OpenFlow MAC NAT flows
IPv6 host routing kernel forwarding + NDP proxy + accept_ra=2 OpenFlow NDP proxy + accept_ra=2
Point-to-point (/32, /128) Host routes to gateway OpenFlow handles it
Hetzner MAC-restricted IPs Supported (VMs routed via host) Supported (L2 MAC rewrite)
Cargo feature Always available Requires ovs feature

Static Mode Flow

  1. Parse mosip4/mosip6 from kernel cmdline
  2. Discover physical interfaces via netlink
  3. Send ARP probes to find which interface can reach the gateway
  4. Select bridge backend (OVS or Linux)
  5. Create bridge, enslave physical NIC
  6. Configure IP address and routes on bridge interface
  7. Configure firewall/NAT if specified
  8. OVS only: install MAC NAT flows + run ARP/NDP proxy controller
  9. Linux bridge: enable kernel forwarding + proxy ARP/NDP

DHCP Mode Flow

  1. Check for absence of static IP parameters
  2. Discover physical interfaces via netlink
  3. Send DHCP DISCOVER on each interface (raw sockets)
  4. Collect DHCP OFFERs and select best interface:
    • Prefer RFC1918 addresses (10.x, 172.16-31.x, 192.168.x)
    • Fall back to global addresses
  5. Select bridge backend (OVS or Linux)
  6. Create bridge, enslave physical NIC
  7. Run full DHCP client (DISCOVER → OFFER → REQUEST → ACK)
  8. Configure IP/gateway from lease
  9. Configure DNS from lease (/etc/resolv.conf)
  10. Enable IPv4/IPv6 forwarding + IPv6 SLAAC (accept_ra=2)
  11. Try DHCPv6 for stateful IPv6 (fallback to SLAAC-only)
  12. Configure firewall/NAT if specified
  13. Spawn background task for lease renewal at T1/T2 timers

The Hetzner Problem

Hetzner dedicated servers have MAC-based filtering at the switch level. Any packet with a source MAC different from the physical NIC's MAC is dropped. This causes problems when:

  • Running VMs/containers with their own MAC addresses
  • Using virtual network interfaces

mosnet's solution:

  1. All traffic flows through an OVS bridge
  2. OpenFlow rules rewrite source MACs to the physical NIC's MAC (egress)
  3. OpenFlow rules rewrite destination MACs for incoming VM traffic (ingress)
  4. ARP/NDP proxy responds to requests for all configured IPs using the physical MAC

This allows VMs and containers to have their own MAC addresses internally while appearing to use the physical MAC externally.

Testing with QEMU

Build the Alpine image

./scripts/build-alpine-image.sh

Output: out/mosnet-alpine-x86_64.img (2 GB raw disk). The image is root-owned after build; fix permissions if needed:

sudo chown $(whoami) out/mosnet-alpine-x86_64.img

Set up the test network

Create a bridge with a tap device and DHCP/DNS server. Pick one of the two options below depending on whether you have OVS installed.

Option A: Linux bridge (no OVS required)

# Create bridge and tap
sudo ip link add mosnet type bridge
sudo ip addr add 10.0.100.1/24 dev mosnet
sudo ip -6 addr add fd00:dead:beef::1/64 dev mosnet
sudo ip link set mosnet up

sudo ip tuntap add dev tap-mosnet mode tap user $(whoami)
sudo ip link set tap-mosnet master mosnet
sudo ip link set tap-mosnet up

# Start dnsmasq as DHCP/DNS server
sudo dnsmasq \
  --conf-file=docs/dnsmasq.conf \
  --pid-file=/tmp/mosnet-dnsmasq.pid

Option B: OVS bridge

sudo ovs-vsctl add-br mosnet
sudo ip addr add 10.0.100.1/24 dev mosnet
sudo ip -6 addr add fd00:dead:beef::1/64 dev mosnet
sudo ip link set mosnet up

sudo ip tuntap add dev tap-mosnet mode tap user $(whoami)
sudo ip link set tap-mosnet up
sudo ovs-vsctl add-port mosnet tap-mosnet

sudo dnsmasq \
  --conf-file=docs/dnsmasq.conf \
  --pid-file=/tmp/mosnet-dnsmasq.pid

Boot the VM

qemu-system-x86_64 \
  -m 1024 \
  -drive file=out/mosnet-alpine-x86_64.img,format=raw \
  -netdev tap,id=net0,ifname=tap-mosnet,script=no,downscript=no \
  -device virtio-net-pci,netdev=net0 \
  -nographic

Login as root (no password). Check mosnet status:

rc-service mosnet status        # OpenRC service status
cat /var/log/mosnet.log         # mosnetd output
mosnet status                   # CLI overview
mosnet interfaces               # Network interfaces
mosnet lease                    # DHCP lease info
mosnet verify                   # Verify configuration

Tear down

Linux bridge:

sudo kill $(cat /tmp/mosnet-dnsmasq.pid)
sudo ip link set tap-mosnet nomaster
sudo ip tuntap del dev tap-mosnet mode tap
sudo ip link del mosnet

OVS bridge:

sudo kill $(cat /tmp/mosnet-dnsmasq.pid)
sudo ovs-vsctl del-port mosnet tap-mosnet
sudo ip tuntap del dev tap-mosnet mode tap
sudo ovs-vsctl del-br mosnet

Static mode testing

Rebuild the image with static IP kernel parameters:

MOSNET_KERNEL_APPEND="mosip4=10.0.100.10,10.0.100.1,255.255.255.0 mosfirewall=ssh" \
  ./scripts/build-alpine-image.sh

Troubleshooting

Inspect with the CLI

mosnet status             # Daemon overview
mosnet interfaces         # All interfaces, addresses, link state
mosnet routes             # Routing table
mosnet bridge             # Bridge ports and membership
mosnet firewall           # nftables rules
mosnet verify             # Cross-reference state with live system
mosnet health --json      # Machine-parseable health check

View logs

RUST_LOG=debug sudo ./mosnetd

Manual inspection (without CLI)

ip addr show mos         # Linux bridge or OVS host-facing interface
ip route show
nft list table inet mosnet       # Firewall rules
nft list table ip mosnet-nat     # NAT rules
cat /var/cache/etc/mosnet.json   # Raw daemon state

Common issues

"no working interface found"

  • Ensure at least one physical interface is up
  • Check that the gateway is reachable (static mode)
  • Verify DHCP server is available (DHCP mode)

"no DHCP offer received"

  • Increase timeout: mosdhcp_timeout=30
  • Check network connectivity
  • Verify DHCP server is running

OVS errors

  • Ensure Open vSwitch is installed and running: systemctl status openvswitch-switch
  • Check OVS logs: journalctl -u openvswitch-switch
  • Or switch to Linux bridge: mosbridge=linux

"mosbridge=ovs requested but 'ovs' feature is not compiled"

  • Rebuild with OVS support: cargo build --release (default features include ovs)
  • Or use Linux bridge: mosbridge=linux

Firewall blocking traffic

  • Check rules: nft list table inet mosnet
  • Temporarily disable: mosfirewall=none
  • Verify correct ports are open

NAT not working

  • Check masquerade interface: nft list table ip mosnet-nat
  • Verify IP forwarding: sysctl net.ipv4.ip_forward
  • Check routing: ip route

Development

Project Structure

src/
├── bin/
│   ├── mosnetd.rs     # Daemon: mode dispatch, state file writes
│   └── mosnet.rs      # CLI: 18 subcommands with --json support
├── lib.rs             # Library exports
└── mosnet/
    ├── backend.rs     # Bridge backend traits and implementations (Linux, OVS)
    ├── bridge.rs      # Linux bridge via netlink (always available)
    ├── cmdline.rs     # Kernel parameter parsing
    ├── config.rs      # Config file parser (mosnet.toml)
    ├── controller.rs  # ARP/NDP proxy (feature-gated: ovs)
    ├── dhcp/          # DHCP probe and client (modular)
    ├── dns.rs         # DNS from DHCP lease (/etc/resolv.conf)
    ├── error.rs       # Error types
    ├── firewall.rs    # nftables firewall/NAT setup + query
    ├── mode.rs        # Mode detection, bridge backend selection
    ├── netlink.rs     # IP/route/bridge management + query functions
    ├── ovs.rs         # OVS bridge + OVSDB/OpenFlow queries (feature-gated: ovs)
    ├── probe.rs       # Interface discovery, ARP probing
    ├── rdnss.rs       # SLAAC DNS listener (RDNSS/DNSSL)
    ├── rpc/           # JSON-RPC server and handlers (Unix socket)
    ├── serde_helpers.rs # Shared MAC format/parse, Duration serde helpers
    ├── state.rs       # DaemonState JSON state file (atomic read/write)
    └── sysctl.rs      # Sysctl write helpers (forwarding, proxy_arp) + read sysctl/DNS for CLI
scripts/
└── build-alpine-image.sh   # Alpine image builder
packaging/alpine/
└── rootfs-overlay/         # OpenRC init script for mosnet
docs/
├── dnsmasq.conf            # Test DHCP/DNS server config
└── adr/                    # Architecture decision records

Running Tests

cargo test

Linting

cargo clippy

License

See LICENSE file.

  • CLAUDE.md - Development guidance
  • static-network-params.md - Full kernel parameter specification
  • docs/alpine-image.md - Bootable Alpine image builder

Architecture Decision Records

ADR Title
000 Hetzner Static Config
001 DHCP Mode and Interface Probing
002 DHCP pnet Refactor
003 Feature Gates and Linux Bridge Mode
004 OVS Kernel Module Not Loaded
005 nftables Firewall and NAT
006 DHCP Security Hardening
007 OVS Secure Fail Mode
008 DHCPv6 with SLAAC Fallback
009 Netlink Shared Handle, Atomic DNS, Bridge MAC
010 Daemon Restart and State Recovery
011 Split mosnet into mosnetd (Daemon) + mosnet (CLI)
012 RDNSS/DNSSL Support via RTM_NEWNDUSEROPT
013 DHCPv6 Prefix Delegation (IA_PD)
014 Bridge Backend Trait
015 VLAN Filtering and Network Topology
016 TOML Config File for mosnetd
017 Codebase Duplication and Separation-of-Concerns Audit
018 Unified Bridge Device Name mos