- Rust 96.8%
- Shell 3.2%
Busybox login requires a valid /etc/shadow entry. Using passwd -d properly clears the password hash while keeping the entry intact. |
||
|---|---|---|
| docs | ||
| examples | ||
| packaging/alpine/rootfs-overlay/etc | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| README.md | ||
| static-network-params.md | ||
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
- MAC NAT: Transparent MAC address rewriting for MAC-restricted environments (e.g., Hetzner)
- ARP/NDP Proxy: Responds to ARP (IPv4) and NDP (IPv6) requests for configured IPs
- Built-in DHCP client: Full DHCP implementation with lease renewal
- RFC1918 Priority: In DHCP mode, prefers private addresses for interface selection
- OVS Integration: Creates Open vSwitch bridge for VM/container networking
- IPv6 Support: Static configuration, SLAAC, or DHCPv6 in DHCP mode
- nftables Firewall: Configurable firewall with presets (ssh, permissive) or custom ports
- NAT Support: Masquerading (SNAT) with auto-detection and port forwarding (DNAT)
Installation
Prerequisites
- Rust 1.85+ (2024 edition)
- Open vSwitch installed and running
- Linux with netlink support
Building
cargo build --release
The binary will be at target/release/mosnet.
Usage
mosnet 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=<preset>[,<tcp_ports>][,<udp_ports>]
Presets:
ssh- Allow SSH (22/tcp) and ICMP onlypermissive- Allow SSH, HTTP/HTTPS (22,80,443/tcp) and ICMPnone- Disable firewall
Custom ports:
- TCP ports:
tcp:22,80,443 - UDP ports:
udp:53,123
Examples:
SSH only (secure default):
mosfirewall=ssh
Web server:
mosfirewall=permissive
Custom ports:
mosfirewall=ssh,tcp:8080,udp:51820
Full custom:
mosfirewall=none,tcp:22,80,443,8080,udp:53
The firewall creates an nftables table mosnet-firewall with:
- Default drop policy for input
- Allow established/related connections
- Allow ICMP (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 namenone- 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
Running
Simply execute the binary as root:
sudo ./mosnet
mosnet will:
- Parse the kernel command line
- Detect the operating mode
- Discover network interfaces
- Probe for connectivity (ARP for static, DHCP for dynamic)
- Create an OVS bridge
- Configure IP addresses
- In static mode: run the ARP/NDP proxy controller
- In DHCP mode: handle lease renewal in the background
Environment Variables
RUST_LOG: Control logging verbosity (e.g.,RUST_LOG=debug)
RUST_LOG=debug sudo ./mosnet
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) |
| Internal OVS port | public |
mos |
| OpenFlow rules | MAC NAT + ARP/NDP intercept | Basic forwarding |
| Controller | ARP/NDP proxy (runs) | Not needed |
| IPv4 source | Kernel cmdline | DHCP lease |
| IPv6 source | Kernel cmdline | SLAAC |
| Lease renewal | N/A | Background task (T1/T2) |
| Use case | Hetzner, MAC-restricted | Standard networks |
Architecture
OVS Bridge Structure
Physical NIC (e.g., enp3s0)
│
│ (L2 only, no IP configured)
│
▼
┌─────────────────────────────────────────────┐
│ OVS Bridge: br-mosnet │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Uplink Port │ │ Internal Port │ │
│ │ (physical NIC) │ │ "public"/"mos" │ │
│ │ │ │ │ │
│ │ Type: system │ │ Type: internal │ │
│ │ No IP │ │ Host IP here │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ OpenFlow Controller: ptcp:6653 │
└─────────────────────────────────────────────┘
The internal port inherits the physical NIC's MAC address, ensuring all outgoing packets have the correct source MAC.
Static Mode Flow
- Parse
mosip4/mosip6from kernel cmdline - Discover physical interfaces via netlink
- Send ARP probes to find which interface can reach the gateway
- Create OVS bridge with "public" internal port
- Configure IP address and routes on "public"
- Install MAC NAT OpenFlow rules
- Run ARP proxy (responds to ARP requests for our IP)
- Run NDP proxy (responds to Neighbor Solicitations for our IPv6)
DHCP Mode Flow
- Check for absence of static IP parameters
- Discover physical interfaces via netlink
- Send DHCP DISCOVER on each interface (raw sockets)
- Collect DHCP OFFERs and select best interface:
- Prefer RFC1918 addresses (10.x, 172.16-31.x, 192.168.x)
- Fall back to global addresses
- Create OVS bridge with "mos" internal port
- Run full DHCP client (DISCOVER → OFFER → REQUEST → ACK)
- Configure IP/gateway from lease
- Enable IPv6 SLAAC (
accept_ra=2) - 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:
- All traffic flows through an OVS bridge
- OpenFlow rules rewrite source MACs to the physical NIC's MAC (egress)
- OpenFlow rules rewrite destination MACs for incoming VM traffic (ingress)
- 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 # mosnet output
ip addr show # Verify IP from DHCP
ip route show # Verify default route
cat /etc/resolv.conf # Verify DNS
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
Check OVS bridge status
ovs-vsctl show
View OpenFlow rules
ovs-ofctl dump-flows br-mosnet
Check interface configuration
ip addr show public # Static mode
ip addr show mos # DHCP mode
ip route show
View logs
RUST_LOG=debug sudo ./mosnet
Check firewall rules
nft list table ip mosnet-firewall
Check NAT rules
nft list table ip mosnet-nat
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
Firewall blocking traffic
- Check rules:
nft list table ip mosnet-firewall - 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/
├── main.rs # Entry point, mode dispatch
├── lib.rs # Library exports
└── mosnet/
├── bridge.rs # Linux bridge via netlink (always available)
├── cmdline.rs # Kernel parameter parsing
├── controller.rs # ARP/NDP proxy (feature-gated: ovs)
├── dhcp/ # DHCP probe and client (modular)
├── dns.rs # DNS from DHCP lease (/etc/resolv.conf)
├── firewall.rs # nftables firewall and NAT
├── mode.rs # Mode detection, bridge backend selection
├── netlink.rs # IP/route/bridge configuration
├── ovs.rs # OVS bridge management (feature-gated: ovs)
├── probe.rs # Interface discovery, ARP probing
└── error.rs # Error types
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.
Related Documentation
CLAUDE.md- Development guidancedocs/adr/001-dhcp-mode-and-interface-probing.md- DHCP mode designdocs/adr-hetzner-static-config.md- Static mode kernel parametersstatic-network-params.md- Full parameter specification