Skip to main content

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 management
  • nft (nftables) - for NAT and firewall rules
  • dnsmasq - for DHCP serving

Optional:

  • nmcli (NetworkManager) - only needed if NM is running; the driver marks its interfaces as unmanaged

Configuration

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:

  1. Takes over a dedicated Ethernet interface (e.g., USB NIC) and assigns a gateway IP directly to it
  2. Runs dnsmasq to provide DHCP to DUTs connected to that interface
  3. Configures nftables rules for NAT (masquerade or 1:1)
  4. 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)

Source distribution for jumpstarter-driver-dut-network 0.9.0
File Size Uploaded
jumpstarter_driver_dut_network-0.9.0.tar.gz 42.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jumpstarter-driver-dut-network 0.9.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.9.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page