05 · Embedded Networking¶
A desktop distribution hides networking behind NetworkManager and a GUI.
An embedded product usually has no GUI, no user to click "connect", and a
requirement that the link comes back by itself at 3 a.m. after the site's
switch reboots. This module covers the layer you actually configure —
ip, systemd-networkd, DHCP, DNS, wpa_supplicant and nftables — and the
failure modes that only show up in the field.
The one command that replaced five¶
ifconfig, route, arp and netstat are deprecated. Everything is now
ip (from iproute2) and ss:
root@target:~# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
link/ether 00:04:9f:06:1b:2c brd ff:ff:ff:ff:ff:ff
root@target:~# ip addr add 192.168.7.20/24 dev eth0
root@target:~# ip link set eth0 up
root@target:~# ip route add default via 192.168.7.1
root@target:~# ip -brief addr
lo UNKNOWN 127.0.0.1/8
eth0 UP 192.168.7.20/24
Read the flags carefully — they answer different questions:
UPmeans you administratively enabled the interface.LOWER_UPmeans the driver sees carrier: a cable is plugged in and the PHY has link. NoLOWER_UPis a hardware or cable problem, and no amount of IP configuration will fix it.NO-CARRIERappearing in the flags is the same message, stated explicitly.
root@target:~# ip -s link show eth0
RX: bytes packets errors dropped missed mcast
18829301 41208 0 0 0 1129
TX: bytes packets errors dropped carrier collsns
2910488 18822 0 0 0 0
$ cat /sys/class/net/eth0/carrier
1
Non-zero errors or carrier counters on a board that "works fine" are a
signal of a marginal PHY, bad magnetics or a RGMII clock-delay problem —
classic hardware bring-up findings.
Static configuration with systemd-networkd¶
For a systemd image, systemd-networkd is the right choice: small, no
D-Bus dependency for basic use, declarative files, and it handles
carrier-loss recovery for you. Files live in /etc/systemd/network/ and
are applied in lexical order; the first [Match] that matches wins.
# /etc/systemd/network/10-eth0-static.network
[Match]
Name=eth0
[Network]
Address=192.168.7.20/24
Gateway=192.168.7.1
DNS=192.168.7.1
DNS=9.9.9.9
[Link]
RequiredForOnline=yes
DHCP instead:
# /etc/systemd/network/20-eth0-dhcp.network
[Match]
Name=eth0
[Network]
DHCP=ipv4
[DHCPv4]
UseDNS=yes
UseNTP=yes
RouteMetric=100
SendHostname=yes
root@target:~# systemctl enable --now systemd-networkd
root@target:~# networkctl status eth0
● 2: eth0
Link File: /lib/systemd/network/99-default.link
Network File: /etc/systemd/network/20-eth0-dhcp.network
Type: ether
State: routable (configured)
Online: online
HW Address: 00:04:9f:06:1b:2c
Address: 192.168.7.20 (DHCP4 via 192.168.7.1)
Gateway: 192.168.7.1
DNS: 192.168.7.1
networkctl status is the first thing to run on any networking complaint.
State: configuring means DHCP has not completed; State: off means no
.network file matched at all.
DNS¶
Name resolution is a separate subsystem from addressing, and confusing the
two wastes a lot of debugging time. systemd-resolved manages
/etc/resolv.conf — usually as a symlink into /run:
root@target:~# ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Aug 4 09:02 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
root@target:~# resolvectl status
Link 2 (eth0)
Current Scopes: DNS
Protocols: +DefaultRoute
Current DNS Server: 192.168.7.1
DNS Servers: 192.168.7.1 9.9.9.9
On a read-only rootfs (module 8) that symlink is mandatory — a real
/etc/resolv.conf file cannot be rewritten when / is mounted read-only,
so DNS silently keeps stale servers forever.
Diagnose in the right order: ip route get 8.8.8.8 (routing), then
ping 8.8.8.8 (connectivity), then resolvectl query example.com (DNS).
If ping-by-IP works and by-name does not, stop looking at the network.
WiFi with wpa_supplicant¶
root@target:~# wpa_passphrase FactoryNet 'correct-horse-battery' >> \
/etc/wpa_supplicant/wpa_supplicant-wlan0.conf
# /etc/wpa_supplicant/wpa_supplicant-wlan0.conf
ctrl_interface=/run/wpa_supplicant
update_config=1
country=DE
network={
ssid="FactoryNet"
# psk="correct-horse-battery"
psk=6ae0f1e02b6d0e5c11f42d3f9b0a1d7c6f5e4a39821b0c7d6e5f4a3928170615
}
wpa_passphrase writes both the plaintext (commented) and the derived PSK.
Delete the commented plaintext line before shipping — it is a
cleartext credential in your image.
root@target:~# systemctl enable --now wpa_supplicant@wlan0
root@target:~# wpa_cli -i wlan0 status
wpa_state=COMPLETED
ssid=FactoryNet
ip_address=192.168.7.53
root@target:~# iw dev wlan0 link
signal: -62 dBm
rx bitrate: 65.0 MBit/s
The @wlan0 instance name is what makes systemd look for
wpa_supplicant-wlan0.conf; a mismatch between the two is the usual reason
the service starts and immediately exits.
Firewalling with nftables¶
iptables is legacy; nft is the current interface. A minimal
default-deny ruleset for an appliance:
# /etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 accept comment "ssh"
tcp dport 8080 accept comment "device web UI"
}
chain forward { type filter hook forward priority filter; policy drop; }
chain output { type filter hook output priority filter; policy accept; }
}
root@target:~# nft -c -f /etc/nftables.conf # check syntax, apply nothing
root@target:~# systemctl enable --now nftables
root@target:~# nft list ruleset | head -8
root@target:~# ss -tlnp
State Local Address:Port Process
LISTEN 0.0.0.0:22 users:(("dropbear",pid=201,fd=3))
LISTEN 0.0.0.0:8080 users:(("appd",pid=310,fd=6))
Note ct state established,related accept sitting first. Without it, a
default-drop input policy blocks the replies to your own outbound
connections and the board appears to have no network at all.
Traps¶
Networking traps that only bite in the field
- Locking yourself out. Applying a default-drop ruleset over SSH on a
remote board with no serial console is unrecoverable without a site
visit. Always stage it behind a timer that reverts after 5 minutes until
you have confirmed access, and always
nft -c -ffirst. - MAC address from a random pool. Many SoCs, i.MX included, fall back
to a random MAC when the fuse or EEPROM is unprogrammed. The board gets a
new DHCP lease on every boot, ARP caches go stale, and any
MAC-based licensing breaks. Check
ip linkfor the same MAC across two boots during bring-up. eth0is not guaranteed. Predictable interface names (enp1s0,end0) depend on udev rules and kernel version; a BSP upgrade can rename your interface and orphan every config file. Match on MAC or path in[Match]when it must be stable.- DHCP at boot as a hard dependency.
RequiredForOnline=yesplus a service ordered afternetwork-online.targetmeans a board with an unplugged cable hangs for the DHCP timeout on every boot. Decide deliberately whether the product should boot without a network. - Time before TLS. With no RTC, the clock starts at the epoch and every
certificate looks "not yet valid". NTP needs the network, the network's
TLS needs the time. Ship a build-time fallback timestamp
(
systemd-timesyncdmaintains one) or an RTC. /etc/resolv.confas a regular file on a read-only rootfs — DNS updates are silently discarded.
Cheat sheet¶
| Command / item | Purpose |
|---|---|
ip -brief addr / ip link |
Addresses / interfaces and carrier state |
LOWER_UP vs UP |
Carrier present vs administratively enabled |
ip route get 8.8.8.8 |
Which route and source IP would be used |
ip -s link show eth0 |
Error/drop/carrier counters — PHY health |
ss -tlnp / ss -tunap |
Listening sockets / all sockets with processes |
/etc/systemd/network/*.network |
Declarative address, DHCP, DNS, routes |
networkctl status <iface> |
Which file matched and what state it reached |
resolvectl status / resolvectl query <name> |
DNS servers / test resolution |
wpa_passphrase <ssid> <pass> |
Generate a PSK block (strip the plaintext!) |
wpa_supplicant@wlan0.service |
Instanced unit → wpa_supplicant-wlan0.conf |
wpa_cli -i wlan0 status / iw dev wlan0 link |
Association state / signal |
nft -c -f /etc/nftables.conf |
Syntax-check a ruleset without applying it |
nft list ruleset |
Dump the live ruleset |
ct state established,related accept |
The rule that must come first |
network-online.target |
Ordering target for "needs a working network" |
On verification
The .network, wpa_supplicant.conf and nftables.conf syntax here
follows the documented grammars for each, and the nftables ruleset is
written so nft -c -f can check it before it is ever applied. Live
hardware testing was not possible while writing this page — run the
check-only commands on your own target first.
How It Actually Works¶
ip/netlink replaced ifconfig/route because the kernel's network
state is exposed as a structured, queryable object model, not text.
ip addr/ip route speak NETLINK_ROUTE — a socket protocol where
requests and replies are binary nlmsghdr + rtattr structures
(RTM_NEWADDR, RTM_GETROUTE, ...), the same protocol the kernel uses
internally to notify listeners of changes. ifconfig was built on the
much older ioctl(SIOCSIFADDR, ...) interface, which can't represent
multiple addresses per interface or IPv6 scopes cleanly — that's the
actual reason it was retired, not just command-syntax fashion.
systemd-networkd itself is just another netlink client: it doesn't
patch files the kernel reads, it opens the same NETLINK_ROUTE socket
and issues the same RTM_NEWADDR/RTM_NEWLINK requests your ip
commands would.
DNS resolution order is decided by NSS, not by "the network stack."
/etc/nsswitch.conf's hosts: line tells glibc's Name Service Switch
which shared objects (libnss_files.so, libnss_dns.so,
libnss_resolve.so if systemd-resolved is in play) to dlopen() and
query, in order, for every getaddrinfo() call any program makes —
including ping, curl, and your own code. This is why editing
/etc/resolv.conf has zero effect on a box using systemd-resolved's
stub: NSS is pointed at libnss_resolve, which queries the resolved
daemon over D-Bus/a Unix socket instead of ever parsing that file.
wpa_supplicant is a state machine driving the kernel's nl80211
interface, and the 4-way handshake is real cryptographic protocol, not
config. After association, wpa_supplicant receives EAPOL frames from
the kernel's cfg80211/driver stack via a dedicated Ethertype
(0x888e), and executes RSN's 4-way handshake purely in userspace:
deriving the PTK from the PMK (itself from the PSK via PBKDF2 over the
passphrase+SSID) and nonces exchanged in messages 1–4, then installing
the resulting session keys into the kernel driver via nl80211
SET_KEY commands — only after that does the interface actually pass
traffic. wpa_cli status showing wpa_state=COMPLETED means that
exact key-installation step succeeded, not merely "associated to an AP."
nftables rules are compiled into a kernel bytecode VM, evaluated per
packet at defined hook points. nft translates your rule text into
the netlink NFT_MSG_NEWRULE binary format, which the kernel loads as a
sequence of expressions run by an in-kernel interpreter at Netfilter hook
points (NF_INET_PRE_ROUTING, NF_INET_LOCAL_IN, ...) baked into the
IP stack's packet-processing path itself — a DROP in a chain isn't
"redirecting" traffic, it's a hook callback returning NF_DROP before the
packet ever reaches the routing/socket-delivery code that would otherwise
run next.
Exercise¶
(1) In your QEMU image, bring eth0 up entirely by hand with ip (address,
route, and a nameserver in /etc/resolv.conf), confirm with
ip route get 8.8.8.8 and a ping, then tear it all down and reproduce the
same result declaratively with a single .network file. (2) Break DNS only
— keep addressing intact — and write down the exact sequence of commands
that lets you prove it is DNS and not routing. (3) Apply the nftables
ruleset above, verify with ss -tlnp that your service is still reachable,
then remove the ct state established,related accept line and describe
precisely what stops working and why. (4) One paragraph: a customer's
gateway ships with an unprogrammed MAC fuse and gets a new DHCP lease every
boot. Explain the three separate symptoms that will be reported by support
(intermittent connectivity, "the device changed IP", licence failures), and
where in the boot chain — fuse, U-Boot environment, device tree, or
.link file — you would fix it.