DUT Network Driver
jumpstarter-driver-dut-network provides network isolation for DUTs (Devices Under Test) by configuring a dedicated network interface with NAT, DHCP, and nftables-based firewall rules on the exporter host.
This enables scenarios where multiple DUTs share the same static IP configuration (common in automotive/embedded labs) by isolating each DUT behind its own NAT interface on the exporter.
Installation
pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-dut-network
System Dependencies
The following must be available on the exporter host:
ip(iproute2) - for interface managementnft(nftables) - for NAT and firewall rulesdnsmasq- for DHCP serving
Optional:
nmcli(NetworkManager) - only needed if NM is running; the driver marks its interfaces as unmanaged
Configuration
Masquerade NAT (recommended for most use cases)
DUTs share the exporter's upstream IP when accessing the network:
export:
dut-network:
type: jumpstarter_driver_dut_network.driver.DutNetwork
config:
interface: "eth2"
subnet: "192.168.100.0/24"
gateway_ip: "192.168.100.1"
nat_mode: "masquerade"
dhcp_enabled: true
dhcp_range_start: "192.168.100.100"
dhcp_range_end: "192.168.100.200"
addresses:
- mac: "8a:12:4e:25:f4:8e"
ip: "192.168.100.10"
hostname: "sa8775p"
dns_servers: ["8.8.8.8", "8.8.4.4"]
1:1 NAT
Each DUT gets a dedicated public IP alias via a per-entry public_ip field, enabling inbound connections from the LAN. Entries without a public_ip fall back to masquerade for outbound traffic. Entries without a mac are used for 1:1 NAT mappings only and are excluded from DHCP static lease generation.
export:
dut-network:
type: jumpstarter_driver_dut_network.driver.DutNetwork
config:
interface: "eth2"
subnet: "192.168.100.0/24"
gateway_ip: "192.168.100.1"
upstream_interface: "enp2s0"
nat_mode: "1to1"
addresses:
- mac: "8a:12:4e:25:f4:8e"
ip: "192.168.100.10"
hostname: "sa8775p-1"
public_ip: "10.26.28.84"
- mac: "8a:12:4e:25:f4:8f"
ip: "192.168.100.11"
hostname: "sa8775p-2"
public_ip: "10.26.28.85"
# Entry without MAC: 1:1 NAT mapping only, no DHCP static lease
- ip: "192.168.100.12"
hostname: "nxp-board-03"
public_ip: "10.26.28.86"
Disabled NAT (DHCP only)
DHCP works normally but no NAT rules or IP forwarding are configured. Useful for pure L2 isolation or when routing is handled externally:
export:
dut-network:
type: jumpstarter_driver_dut_network.driver.DutNetwork
config:
interface: "enx00e04c683af1"
nat_mode: "disabled" # also accepts "none"
dhcp_enabled: true
Custom DNS Entries
Register custom DNS records that dnsmasq will respond to. Useful for pointing DUTs at local services without a full DNS infrastructure:
export:
dut-network:
type: jumpstarter_driver_dut_network.driver.DutNetwork
config:
interface: "eth2"
nat_mode: "masquerade"
dns_entries:
- hostname: "controller.lab.local"
ip: "10.26.28.1"
- hostname: "registry.lab.local"
ip: "10.26.28.2"
Reference
| Parameter | Type | Default | Description |
|---|---|---|---|
interface |
str | required | Physical NIC for DUT connectivity (e.g., USB NIC name) |
subnet |
str | 192.168.100.0/24 |
Private subnet for DUTs |
gateway_ip |
str | 192.168.100.1 |
IP assigned to the interface (acts as gateway for DUTs) |
upstream_interface |
str | auto-detect | Interface for outbound NAT traffic |
dhcp_enabled |
bool | true |
Whether to run DHCP on the interface |
dhcp_range_start |
str | 192.168.100.100 |
DHCP dynamic range start |
dhcp_range_end |
str | 192.168.100.200 |
DHCP dynamic range end |
addresses |
list | [] |
Address entries: {ip, mac?, hostname?, public_ip?}. Entries with mac generate DHCP static leases; entries without mac are used for 1:1 NAT only. |
dns_servers |
list | [8.8.8.8, 8.8.4.4] |
DNS servers for DHCP clients |
dns_entries |
list | [] |
Custom DNS records: {hostname, ip} |
state_dir |
str | /var/lib/jumpstarter/dut-network-{interface}/ |
Directory for dnsmasq state files |
nat_mode |
str | masquerade |
NAT mode: masquerade, 1to1, disabled, or none |
public_interface |
str | None | Interface for IP alias (defaults to upstream) |
Address Entry Fields
| Field | Required | Description |
|---|---|---|
ip |
yes | Private IP to assign |
mac |
no | MAC address of the DUT. Required for DHCP static lease; omit for 1:1 NAT-only entries |
hostname |
no | Hostname for DHCP |
public_ip |
no | Public IP for 1:1 NAT (per-entry). At least one entry must have public_ip when nat_mode=1to1 |
Usage
CLI
Inside a jmp shell session:
# Show full network status
j dut-network status
# List DHCP leases
j dut-network leases
# Look up DUT IP by MAC
j dut-network get-ip 8a:12:4e:25:f4:8e
# Add an address entry with a MAC (creates a DHCP static lease)
j dut-network add-address 192.168.100.50 --mac 02:00:00:aa:bb:cc --hostname my-dut
# Add an address entry without MAC (1:1 NAT mapping only, no DHCP lease)
j dut-network add-address 192.168.100.51 --public-ip 10.26.28.90
# Remove an address entry by IP
j dut-network remove-address 192.168.100.50
# Show nftables NAT rules
j dut-network nat-rules
# List configured DNS entries
j dut-network dns-entries
# Add a custom DNS entry
j dut-network add-dns controller.lab.local 10.26.28.1
# Remove a DNS entry
j dut-network remove-dns controller.lab.local
Python
from jumpstarter.common.utils import env
with env() as client:
# Get network status
status = client.dut_network.status()
print(status["interface_status"]["name"])
# Get all DHCP leases
leases = client.dut_network.get_leases()
for lease in leases:
print(f"{lease['mac']} -> {lease['ip']}")
# Look up DUT IP
ip = client.dut_network.get_dut_ip("8a:12:4e:25:f4:8e")
# Manage address entries at runtime
# With MAC: creates a DHCP static lease + optional 1:1 NAT mapping
client.dut_network.add_address("192.168.100.50", mac="02:00:00:aa:bb:cc", hostname="new-dut")
# Without MAC: 1:1 NAT mapping only (no DHCP lease)
client.dut_network.add_address("192.168.100.51", public_ip="10.26.28.90")
client.dut_network.remove_address("192.168.100.50")
# Manage DNS entries at runtime
client.dut_network.add_dns_entry("myhost.lab.local", "10.0.0.99")
entries = client.dut_network.get_dns_entries()
client.dut_network.remove_dns_entry("myhost.lab.local")
Architecture
The driver configures an isolated network for the DUT:
- Takes over a dedicated Ethernet interface (e.g., USB NIC) and assigns a gateway IP directly to it
- Runs dnsmasq to provide DHCP to DUTs connected to that interface
- Configures nftables rules for NAT (masquerade or 1:1)
- Enables IP forwarding so DUT traffic routes through the exporter
When NetworkManager is detected, the driver marks managed interfaces as unmanaged to prevent interference. On cleanup, existing addresses are flushed and the interface is restored to NetworkManager control.
Exporter Host
┌─────────┐ ┌──────────────────────────────────────┐ ┌─────────┐
│ DUT │ │ │ │ LAN │
│ │ eth │ eth2 ┌──────────┐ │ │ │
│ DHCP │◄──────►│ 192.168.100.1/24 │ dnsmasq │ │ │ │
│ client │ │ (gateway) │ DHCP+DNS │ │ │ │
│ │ │ │ └──────────┘ │ │ │
│ 192.168.│ │ │ forwarding │ eth │ │
│ 100.10 │ │ ▼ ┌──────────┐ │ │ │
│ │ │ ┌─────────┐ │ nftables │ │ enp2s0 │ 10.26. │
└─────────┘ │ │ ip_fwd │───────►│ NAT │────►│◄──────► │ 28.0/24 │
│ └─────────┘ │ │ │(upstream)│ │
│ │masq/1:1 │ │ └─────────┘
│ └──────────┘ │
└──────────────────────────────────────┘
─── Masquerade: DUT traffic appears as exporter's upstream IP
─── 1:1 NAT: DUT gets a dedicated public IP on the upstream interface
Disabled NAT (DHCP-only isolation)
Exporter Host
┌─────────┐ ┌──────────────────────────────┐
│ DUT │ │ │
│ │ eth │ eth2 ┌──────────┐ │
│ DHCP │◄──────►│ 192.168.100.1 │ dnsmasq │ │
│ client │ │ (gateway) │ DHCP+DNS │ │
│ │ │ └──────────┘ │
│ 192.168.│ │ │
│ 100.10 │ │ No forwarding, no NAT. │
│ │ │ L2-isolated network only. │
└─────────┘ └──────────────────────────────┘
The DUT can reach the exporter on 192.168.100.1 but has
no route to the LAN or internet. Useful for pure L2
isolation or when routing is handled externally.
API Reference
.. autoclass:: jumpstarter_driver_dut_network.driver.DutNetwork()
The driver uses a dedicated nftables table (named after the interface) that
does not conflict with firewalld or other nftables users.
Troubleshooting
NAT traffic not forwarding (Docker hosts)
On hosts running Docker, the default iptables policy is often set to
iptables -P FORWARD DROP to isolate container networks. Since modern
Linux translates iptables rules into nftables under the hood, this creates
a table ip filter { chain FORWARD { policy drop } } base chain that
all forwarded packets must pass - including traffic routed through
the DUT interface.
The driver automatically detects this situation using native nftables:
when NAT is enabled, it checks if the ip filter table's FORWARD chain
has policy drop. If so, targeted accept rules are inserted directly
into that chain for the DUT and upstream interfaces on startup, and
removed by handle on cleanup. No manual intervention or iptables
binary is required.
Per-interface IP forwarding
The driver enables IPv4 forwarding only on the DUT and upstream
interfaces (net.ipv4.conf.<iface>.forwarding=1) rather than the global
net.ipv4.ip_forward sysctl. This avoids turning a multi-homed host
into a full router on every interface. If forwarding still does not work,
verify with:
sysctl net.ipv4.conf.<interface>.forwarding
sysctl net.ipv4.conf.<upstream>.forwarding
Release files for jumpstarter-driver-dut-network 0.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jumpstarter_driver_dut_network-0.9.0.tar.gz | 42.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jumpstarter_driver_dut_network-0.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.0 kB
Release files / jumpstarter_driver_dut_network-0.9.0.tar.gz
| Download URL | jumpstarter_driver_dut_network-0.9.0.tar.gz |
|---|---|
| Size | 42.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
592986e562120ee706cc250857e11a1fe3f7f547ad2bc3c7b3b031ea36f4477a
|
|
BLAKE2b-256 checksum How to use checksums |
f596350cebe1bccfc666b0684c8a29c90cf0e29c9d3f94aaf734b833e9b26246
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.22
|
Release files / jumpstarter_driver_dut_network-0.9.0-py3-none-any.whl
| Download URL | jumpstarter_driver_dut_network-0.9.0-py3-none-any.whl |
|---|---|
| Size | 50.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8e8651b5e591ca2c9ba335e4b8d2f6e28613cf28e9f6ccfa4c1670a06246ab73
|
|
BLAKE2b-256 checksum How to use checksums |
9509b424fd140b4432dafbf5f2245b5665b1bfc19eeda86e43e9766282125bf6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.22
|