Linux Networking with ip, nftables, and tc
There is a class of Linux administrator who still reaches for ifconfig out of muscle memory, types netstat -tulnp without thinking, and has a collection of gnarly iptables -A one-liners saved in a notes file from 2013. If that describes you, this post is not a lecture — it is a practical migration and upgrade guide. The old tools still work on many systems, but they are reading stale data, they are unmaintained, and in the case of iptables they are being quietly replaced underneath you whether you notice or not. On a modern Debian 12 or Ubuntu 24.04 host, the iptables binary you invoke is a compatibility shim that translates your rules into nftables behind the scenes.
This post covers the modern Linux networking toolkit end to end: the ip command from iproute2 (version 7.0.0 was released upstream in April 2026), network namespaces as the isolation primitive underneath every container runtime, nftables as the unified packet filtering framework that ships as the default on all major distributions, traffic control with tc for rate shaping and network emulation, and ss as the netstat replacement. Every command shown here is real and runs on a current kernel. Nothing is pseudocode.
Why the Old Tools Are Dead
The net-tools package — which provides ifconfig, route, arp, netstat, and nameif — was formally declared unmaintained around 2009. It has received sporadic maintenance patches from distro vendors since then, but no upstream development has occurred. More importantly, it reads network state from /proc/net files that are themselves a compatibility layer on top of the kernel’s actual data structures. The kernel’s preferred interface for network configuration has been the Netlink socket API for over a decade. iproute2’s ip command speaks Netlink natively.
The iproute2 suite replaces net-tools completely. The mapping is straightforward:
| Old command (net-tools) | New command (iproute2) | Notes |
|---|---|---|
ifconfig |
ip addr, ip link |
ip addr shows addresses; ip link shows interface state |
ifconfig eth0 up |
ip link set eth0 up |
|
ifconfig eth0 192.168.1.10 netmask 255.255.255.0 |
ip addr add 192.168.1.10/24 dev eth0 |
|
route -n |
ip route show |
|
route add default gw 10.0.0.1 |
ip route add default via 10.0.0.1 |
|
arp -n |
ip neigh show |
Covers both IPv4 ARP and IPv6 NDP |
netstat -tulnp |
ss -tlnp |
ss reads from kernel via Netlink, not /proc |
netstat -s |
nstat or ss -s |
|
netstat -rn |
ip route show |
For packet filtering, the transition is:
| Old tool | New tool | Scope |
|---|---|---|
iptables |
nft (nftables) |
IPv4 packet filtering |
ip6tables |
nft (nftables) |
IPv6 packet filtering |
arptables |
nft (nftables, arp family) |
ARP filtering |
ebtables |
nft (nftables, bridge family) |
Ethernet bridge filtering |
ipset |
nftables sets | Efficient set-based matching |
On Debian 12 and Ubuntu 22.04+, iptables is iptables-nft — it calls nftables under the hood. You can verify this yourself: update-alternatives --display iptables will show the symlink pointing to xtables-nft-multi. Your iptables -L output is actually rendered by reading the nftables ruleset. This means there is zero performance cost to switching — you are already using nftables.
The ip Command
The ip command is organized around objects: link, addr, route, neigh, rule, netns, and several others. The general form is ip [options] OBJECT COMMAND. Many subcommands can be abbreviated: ip a for ip addr, ip r for ip route, ip l for ip link.
ip link — Interface Management
ip link show lists all interfaces with their state, MTU, MAC address, and flags. The output is denser than ifconfig but more complete:
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:ab:cd:ef brd ff:ff:ff:ff:ff:ff
The flags in angle brackets tell you the interface state. UP means administratively up; LOWER_UP means the physical layer has a carrier. You can filter by state: ip link show up shows only active interfaces.
Bringing interfaces up and down:
|
|
Setting MTU (necessary for tunnels or jumbo frames):
|
|
Renaming an interface (useful before the udev persistent naming rules kick in, or for scripting):
|
|
Creating a VLAN subinterface on eth0 for VLAN 100:
|
|
Creating a software bridge (for VM or container networking):
|
|
Creating a dummy interface (useful for testing, loopback-like interfaces, or anchoring addresses):
|
|
Creating a VXLAN tunnel interface (common in overlay networks):
|
|
ip addr — Address Management
ip addr show (or ip addr show dev eth0 for a specific interface) displays all assigned addresses, their scope, and their broadcast addresses.
Adding and removing addresses:
|
|
Addresses have a scope: global (reachable from other hosts), link (link-local, fe80::/10 for IPv6 or 169.254.0.0/16 for IPv4), or host (loopback only). You can set this explicitly:
|
|
Labels allow you to tag secondary addresses, which is handy for scripts that need to find specific addresses:
|
|
To flush all addresses from an interface:
|
|
ip route — Routing Table Management
ip route show prints the main routing table. The output includes the destination, gateway (via), interface (dev), and source address selection hint (src):
default via 10.0.0.1 dev eth0 proto dhcp src 10.0.0.50 metric 100
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.50
Adding and removing routes:
|
|
You can add a route with a specific metric (lower is preferred) and a source address hint:
|
|
One of the most useful diagnostic commands in the entire networking toolbox is ip route get, which performs a kernel route lookup for a specific destination and tells you exactly which route would be used, which interface, and what source address would be selected:
|
|
This is invaluable for debugging routing asymmetry. It accounts for policy routing rules, marks, and VRFs — it shows what the kernel will actually do, not just what is in the table.
Policy Routing with ip rule and Multiple Tables
Linux maintains multiple routing tables, numbered 1–252, plus three special ones: local (255), main (254), and default (253). The main table is what ip route show displays by default.
Policy routing allows you to route traffic based on source address, fwmark, incoming interface, or TOS, rather than only on destination. The mechanism is a set of ordered rules (ip rule) that map packets to routing tables.
A classic use case is a dual-ISP setup where you want traffic from one IP to leave via ISP1 and traffic from another IP to leave via ISP2:
|
|
ip rule show displays all rules in priority order. The kernel evaluates them top to bottom and uses the first matching rule’s table.
ip neigh — ARP and NDP Table
ip neigh show displays the neighbor cache — both IPv4 ARP entries and IPv6 NDP entries. The state field shows whether each entry is REACHABLE, STALE, DELAY, PROBE, or FAILED.
|
|
You can manually add a static ARP entry (useful for pinning gateway MACs in certain environments):
|
|
Flushing stale entries forces a re-ARP of all neighbors:
|
|
Persistence: ip Commands Are Not Persistent
Every ip command is transient — a reboot wipes everything. This is intentional: the kernel’s network state is runtime state, not configuration. For persistence you need a configuration layer on top:
- systemd-networkd:
.networkand.netdevfiles in/etc/systemd/network/. Clean, well-structured, good for servers. - NetworkManager: Preferred on desktops. Manages connections via profiles.
- Debian
/etc/network/interfaces: The classic Debian approach, still works, usesipcommands inpre-up/post-upstanzas on modern systems. - ip-up scripts or rc.local: Brittle but simple for one-off configurations.
A minimal systemd-networkd config for a static address:
|
|
Network Namespaces
Network namespaces are one of the six Linux namespace types (the others being UTS, IPC, PID, mount, and user). A network namespace is a complete isolated copy of the network stack: its own set of interfaces, its own routing tables, its own iptables/nftables rules, its own sockets, and its own port number space. Two processes in different network namespaces can both bind to port 80 simultaneously without conflict. This is the fundamental isolation mechanism underneath Docker, podman, Kubernetes, LXC, and every other Linux container runtime.
Creating and Managing Namespaces
|
|
When you first create a namespace, it has only a loopback interface and that interface is down:
|
|
Connecting Namespaces with veth Pairs
A veth (virtual Ethernet) pair is a linked pair of virtual network interfaces — a virtual patch cable. Anything sent into one end comes out the other. You create both ends at once and then move one end into a namespace:
|
|
Now the two namespaces can communicate:
|
|
The topology looks like this:
+-----------------------+ +-----------------------+
| namespace: red | | namespace: blue |
| | | |
| lo: 127.0.0.1/8 | | lo: 127.0.0.1/8 |
| veth-red: | | veth-blue: |
| 192.168.100.1/24 | | 192.168.100.2/24 |
| [veth-red]====+========+=====[veth-blue] |
| | | |
| route: 192.168.100.0 | | route: 192.168.100.0 |
| via veth-red | | via veth-blue |
| | | |
+-----------------------+ +-----------------------+
Each namespace has its own isolated routing table. There is no shared state between them except the physical veth cable.
NAT from a Namespace to the Host Network
If you want a namespace to reach the internet, you need to either route to it properly or use NAT. The minimal approach: give the host end of a veth an address, set a default route in the namespace pointing to the host, enable IP forwarding on the host, and add a masquerade rule:
|
|
Traffic from the namespace now exits through the host’s eth0 with the host’s source IP, exactly as Docker containers do.
How Docker Uses Namespaces
When Docker starts a container, it calls unshare(CLONE_NEWNET) to create a new network namespace, creates a veth pair, keeps one end in the host namespace as vethXXXXXX, moves the other end into the container’s namespace as eth0, and configures it via the docker0 bridge. When you run docker exec on a running container, it uses nsenter --net=/proc/<pid>/ns/net to enter the container’s network namespace. You can do the same thing manually:
|
|
nftables — The Unified Packet Filter
nftables was merged into the mainline kernel in 3.13 (2014) and has been production-ready since kernel 4.x. It replaces iptables, ip6tables, arptables, and ebtables with a single coherent framework. On Debian 12 and Ubuntu 24.04, nftables is the default packet filtering backend. The iptables command is a compatibility wrapper (iptables-nft) that translates iptables syntax into nftables rules.
Why nftables Is Better Than iptables
The structural problem with iptables is that every rule is evaluated linearly. If you have 500 rules blocking known-bad IP addresses, every packet that arrives goes through all 500 comparisons. nftables sets use hash tables and interval trees internally — matching a packet against a set of 10,000 IP addresses takes the same time as matching against 10. This alone justifies the migration for anyone running any kind of dynamic blocklist.
Beyond performance, nftables offers:
- A single tool for all protocol families (ip, ip6, inet, arp, bridge, netdev)
- Atomic ruleset replacement —
nft -f ruleset.nftreplaces the entire ruleset in one kernel transaction, with no window where the old rules are gone but new rules are not yet in place - Cleaner syntax with explicit chain and table types
- Native verdict maps for dispatch tables
- Interval set support for CIDR ranges without
ipset
Families, Tables, and Chains
Every nftables object lives in a table, which belongs to a family:
| Family | Scope |
|---|---|
ip |
IPv4 only |
ip6 |
IPv6 only |
inet |
IPv4 and IPv6 (most common for dual-stack servers) |
arp |
ARP packets |
bridge |
Bridged Ethernet frames |
netdev |
Per-device ingress hook (before routing) |
A chain within a table is either a base chain (attached to a Netfilter hook, evaluated automatically) or a regular chain (called explicitly via jump or goto from another chain).
Base chains have a type, a hook, and a priority:
- Type:
filter,nat, orroute - Hook: where in the packet path the chain is evaluated
- Priority: lower number = evaluated first among multiple chains on the same hook
Netfilter Hook Points
The packet path through the kernel’s Netfilter framework, showing where chains attach:
Incoming packet
|
v
[PREROUTING hook] <-- nat (DNAT), mangle, raw
|
+---> Routing decision
| |
| destination=local?
| |
| YES | NO
| | | |
v v | v
[INPUT hook] | | [FORWARD hook]
(local proc) | | (routed fwd)
| | | |
v | | v
process | | [POSTROUTING hook]
| | <-- nat (SNAT/masq), mangle
| | |
+----+-------+---> Out to network
^
|
[OUTPUT hook]
(locally generated)
<-- nat (DNAT), mangle, filter
The five hook points are: prerouting, input, forward, output, postrouting. The nat chain type should only be attached to prerouting (for DNAT) and postrouting (for SNAT/masquerade). Filter chains can be attached to any hook.
Rule Syntax
Rules have a left-hand side (the match) and a right-hand side (the verdict or statement):
|
|
Sets — The Performance Multiplier
A set is a named collection of values (IPs, ports, prefixes, etc.) that can be matched against in O(1) time. You define the set and reference it in rules:
table inet filter {
set TRUSTED_MGMT {
type ipv4_addr
flags interval
elements = {
10.0.0.0/8,
172.16.0.0/12,
192.168.0.0/16
}
}
chain input {
type filter hook input priority 0; policy drop;
ip saddr @TRUSTED_MGMT tcp dport 22 accept
}
}
The flags interval annotation is required for CIDR ranges. Without it, the set only supports exact addresses. Sets can also be dynamic (updated at runtime via nft set add element) which is the basis for dynamic blocklists.
Named sets can be created and modified independently of the ruleset:
|
|
Verdict Maps
A verdict map (vmap) dispatches a packet to different verdicts or chains based on a key. This replaces long chains of individual rules checking the same field:
# Instead of three separate rules for ports 22, 80, 443:
tcp dport vmap {
22 : accept,
80 : jump http_chain,
443 : jump https_chain
}
Maps generalize this to arbitrary keys and values:
# Dispatch by input interface
iifname vmap {
"eth0" : jump wan_input,
"eth1" : jump lan_input,
"tun0" : jump vpn_input
}
A Complete Production Server Ruleset
This is a real, complete nftables configuration for a server with a public interface. Save it as /etc/nftables.conf:
#!/usr/sbin/nft -f
# LunarOps production server firewall
# Reload atomically: nft -f /etc/nftables.conf
flush ruleset
table inet filter {
# Set of management source addresses
set MGMT_NETS {
type ipv4_addr
flags interval
elements = {
10.0.0.0/8,
172.16.0.0/12,
192.168.0.0/16
}
}
# Rate limiting set for SSH brute-force mitigation
# Tracks recent connection sources using a meter
set SSH_RATELIMIT {
type ipv4_addr
size 65536
flags dynamic
}
chain input {
type filter hook input priority filter; policy drop;
# Loopback is always allowed
iif lo accept
# Drop invalid conntrack state immediately
ct state invalid drop
# Allow established and related (stateful)
ct state { established, related } accept
# ICMP: allow ping and path MTU discovery
ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded } accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, echo-reply, destination-unreachable, packet-too-big, time-exceeded, nd-neighbor-solicit, nd-neighbor-advert } accept
# SSH: restrict to management networks, rate-limit to 5 new connections/minute per source
ip saddr @MGMT_NETS tcp dport 22 \
ct state new limit rate 5/minute \
accept
# HTTP and HTTPS: open to the world
tcp dport { 80, 443 } accept
# DNS (if this is a resolver)
# udp dport 53 accept
# tcp dport 53 accept
# Log everything that reaches the default drop policy
log prefix "nft-input-drop: " flags all counter
}
chain forward {
type filter hook forward priority filter; policy drop;
# This server does not forward packets by default.
# For a router, add rules here.
ct state { established, related } accept
}
chain output {
type filter hook output priority filter; policy accept;
# Outbound is unrestricted by default.
# Tighten by changing policy to drop and adding explicit allows.
}
}
Load it atomically:
|
|
Verify the loaded ruleset:
|
|
Enable at boot:
|
|
iptables vs nftables Syntax Comparison
| Purpose | iptables | nftables |
|---|---|---|
| Allow established | iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT |
ct state established,related accept |
| Allow SSH | iptables -A INPUT -p tcp --dport 22 -j ACCEPT |
tcp dport 22 accept |
| Drop source IP | iptables -A INPUT -s 1.2.3.4 -j DROP |
ip saddr 1.2.3.4 drop |
| Log and drop | iptables -A INPUT -j LOG --log-prefix "DROP: "; -j DROP |
log prefix "DROP: " drop |
| Match IP set | ipset create BADIPS hash:ip; iptables -A INPUT -m set --match-set BADIPS src -j DROP |
set BADIPS { type ipv4_addr; }; ip saddr @BADIPS drop |
| DNAT port forward | iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-dest 192.168.1.50:80 |
iif eth0 tcp dport 8080 dnat to 192.168.1.50:80 |
| Masquerade | iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE |
oif eth0 masquerade |
| Rate limit | iptables -A INPUT -p tcp --dport 22 -m limit --limit 5/min -j ACCEPT |
tcp dport 22 limit rate 5/minute accept |
| Save ruleset | iptables-save > /etc/iptables/rules.v4 |
nft -f /etc/nftables.conf (atomic) |
Practical nftables Examples
Port Forwarding / DNAT
Forward incoming TCP connections on port 8080 of the host to an internal host at 192.168.1.50:80. This requires a nat table with a prerouting chain:
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif "eth0" tcp dport 8080 dnat to 192.168.1.50:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
# If the forwarded host is on a different interface, masquerade
oif "eth1" masquerade
}
}
Also ensure forwarding is allowed in the filter table and IP forwarding is enabled:
|
|
Masquerade for a Home Router or Container Host
The canonical NAT rule for a Linux router with WAN on eth0 and LAN on eth1:
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oif "eth0" masquerade
}
}
masquerade is a special form of SNAT that automatically uses the outgoing interface’s current IP address. It is slightly less efficient than explicit SNAT (because it must look up the interface address for each connection) but correct when the WAN IP is dynamic.
Rate Limiting and Connection Limits
Rate limit new SSH connections per source IP using a dynamic set (meter):
chain input {
type filter hook input priority filter; policy drop;
tcp dport 22 ct state new \
add @SSH_RATELIMIT { ip saddr limit rate over 3/minute } \
drop
tcp dport 22 ct state new accept
}
The first rule adds the source IP to a set with a per-element rate limit, and drops if the rate is exceeded. The second rule accepts the connection if the first rule did not drop it.
Conntrack: The Stateful Foundation
Every production firewall should start with a conntrack rule. The ct state established,related accept rule is what makes stateful firewalling work — you can set your input policy to drop, allow only specific new connections, and all the return traffic for those connections is automatically permitted by the conntrack rule. The related state covers protocols like FTP passive mode where the data connection is distinct from the control connection.
The invalid state drop is equally important. Packets in ct state invalid have failed conntrack validation — they may be fragmented incorrectly, have out-of-window TCP sequence numbers, or be attempts at connection tracking evasion. Drop them immediately, before any other processing.
Traffic Control with tc
tc (traffic control) manages how packets are queued before transmission. It operates on egress (outbound) traffic only, at the interface level. It cannot directly shape ingress (inbound) traffic, though the standard workaround is to use an ifb (Intermediate Functional Block) virtual device that redirects ingress traffic through an egress qdisc.
The Queueing Discipline (qdisc)
Every network interface has a root qdisc — the algorithm that decides which packets to transmit next and when. The default qdisc on modern Linux (kernel 5.15+, systemd-networkd default) is fq_codel (Fair Queuing with Controlled Delay). Older defaults were pfifo_fast (simple three-band FIFO). fq_codel is generally the right default for most servers: it eliminates bufferbloat by actively dropping packets that have been queued too long, and it provides per-flow fair queuing so one large flow cannot starve others.
See the current qdisc on an interface:
|
|
qdiscs are organized into two categories:
| Category | Examples | Description |
|---|---|---|
| Classless | fq_codel, tbf, netem, pfifo, sfq |
Apply a single policy to all traffic |
| Classful | htb, hfsc, cbq |
Divide traffic into classes with per-class policies |
HTB — Hierarchical Token Bucket
HTB (Hierarchical Token Bucket) is the standard classful qdisc for bandwidth shaping. It lets you define a hierarchy of classes, assign guaranteed rates and maximum rates (ceil) to each class, and attach leaf qdiscs to each class for the actual packet queuing.
Scenario: a server with 100 Mbit uplink. We want to guarantee 50 Mbit/s to interactive SSH and DNS traffic, guarantee 10 Mbit/s to bulk transfers, and allow both to burst up to the full 100 Mbit when the other class is idle.
|
|
The class hierarchy looks like this:
1: root HTB qdisc
|
+-- 1:1 root class (100mbit ceiling)
|
+-- 1:10 high-priority (rate 50mbit, ceil 100mbit, prio 1)
| |
| +-- 10: fq_codel leaf (SSH, DNS traffic)
|
+-- 1:30 default/bulk (rate 10mbit, ceil 100mbit, prio 2)
|
+-- 30: fq_codel leaf (all other traffic)
When only bulk traffic is running, it can borrow bandwidth from the high-priority class and use the full 100mbit. When SSH traffic arrives, HTB reclaims the bandwidth for the 1:10 class immediately, up to its ceil value.
TBF — Simple Token Bucket Filter
For straightforward rate limiting without class hierarchy, TBF is simpler and slightly more efficient:
|
|
burst is the maximum number of bytes that can be transmitted instantaneously (the “bucket size”). latency bounds the maximum time a packet can wait in the queue before being dropped. For most rate-limiting use cases TBF is all you need.
netem — Network Emulation for Testing
netem is where tc becomes genuinely indispensable for development and operations work. It allows you to inject artificial delay, jitter, packet loss, packet reordering, packet corruption, and packet duplication into a network interface. This is the correct way to test how your application behaves on degraded network connections.
Add 100ms of fixed delay to all traffic:
|
|
Add 100ms delay with ±10ms jitter (uniformly distributed):
|
|
Add delay with jitter following a normal distribution (more realistic):
|
|
Simulate 1% random packet loss:
|
|
Simulate a flaky transatlantic connection — 200ms delay with 20ms jitter, 0.5% packet loss, and 0.1% packet corruption:
|
|
Simulate packet reordering (5% of packets are delayed by 10ms and may arrive out of order):
|
|
Remove all tc rules (restore default qdisc):
|
|
Changing netem parameters on a running interface (replace, do not add):
|
|
Show the current qdisc state including packet counts and drop statistics:
|
|
ss — The Correct netstat
ss reads socket state directly from the kernel via Netlink, without reading from /proc/net. On a host with thousands of connections, netstat can take seconds because it sequentially reads large /proc/net/tcp files. ss is nearly instantaneous regardless of connection count.
The most common invocation, equivalent to netstat -tulnp:
|
|
The flags: -t TCP, -u UDP, -l listening, -n numeric (no DNS lookup), -p process name and PID. Add -6 or just use -a for all sockets including IPv6.
Show all established TCP connections with process info:
|
|
Filter by remote port:
|
|
Show socket statistics summary:
|
|
Show Unix domain sockets:
|
|
Show extended socket information including TCP timer state (useful for debugging connection hangs):
|
|
The i flag shows internal TCP info including congestion window, send buffer size, retransmits, and RTT. This is information netstat simply cannot provide.
Practical End-to-End Examples
Example 1: A Simple Two-NIC Linux Router
Configure a Linux box with two NICs (eth0 = WAN, eth1 = LAN 192.168.10.0/24) as a NAT router:
|
|
LAN hosts set 192.168.10.1 as their default gateway. Outbound traffic from the LAN is NATted through the WAN IP and return traffic is forwarded back by conntrack.
Example 2: Network Namespace for an Isolated Development Service
You are running a development database that should only be able to reach your internal monitoring server at 10.0.0.100, and nothing else. Put it in a namespace with a restrictive policy:
|
|
The database process cannot establish connections to anything except 10.0.0.100 and the host-side veth, regardless of any misconfiguration in the database software itself.
Example 3: Simulate a 200ms Transatlantic Connection for Load Testing
You have a load test suite that needs to simulate a client in Europe connecting to a US server. On a test machine that is co-located with your server, add netem to simulate the latency before running the test:
|
|
If you need to apply the delay only to traffic going to a specific host (rather than all traffic), combine HTB with netem as leaf qdiscs — create a class for that destination IP, attach netem to it, and use a filter to route matching traffic into that class.
An alternative for per-connection emulation without affecting the whole interface is to use network namespaces: run the load generator in a namespace, attach netem to the veth interface inside that namespace, and all traffic from the load generator goes through the artificial impairment without affecting other processes on the host.
Tying It Together
The tools covered in this post form a complete, coherent stack. ip manages the runtime network configuration — interfaces, addresses, routes, neighbors, namespaces. nftables provides stateful packet filtering, NAT, and set-based matching for all protocol families in a single tool. tc shapes, prioritizes, and can degrade traffic for testing. ss gives you accurate, fast visibility into socket state. They all speak Netlink; they all work with the kernel’s actual data structures; they are all actively maintained.
The migration from net-tools and iptables is not a future project — on any modern distribution it has already happened, whether you have acknowledged it or not. The iptables binary on your Debian 12 server is writing nftables rules. The ifconfig output on Ubuntu 24.04 is reading from /proc/net/if_inet6 and /proc/net/dev, which are themselves synthesized from kernel structures that ip addr reads directly.
Learning these tools pays dividends beyond familiarity with the commands. Network namespaces are the substrate of containers. nftables sets and verdict maps are the mental model behind every modern firewall and load balancer decision engine. tc netem is the only reliable way to test network resilience without a physical WAN emulator. The investment in understanding this stack pays back every time you debug a container networking issue, write a firewall policy that does not inadvertently block IPv6, or discover that your application silently hangs on a connection with 5% packet loss.
Comments