Dual stack¶
Two hosts on a single switch, each carrying both an IPv4 /24 and an
IPv6 /64 address. The pipeline is the same port-swap forwarder as
Quick start — L2 swapping treats v4 and v6
identically. What's interesting is the address management.
What you'll see¶
pingall (IPv4) and pingall6 (IPv6) both succeed. The hosts'
interfaces carry exactly the addresses we asked for — no fe80::
link-local clutter, no SLAAC-derived addresses.
Topology¶
examples/dual_stack/topology.py:
"""Two hosts plus one switch carrying both IPv4 and IPv6.
The pipeline is L3-agnostic (it just swaps ports 1 and 2), so both v4 and
v6 traverse identically. ``setup(net)`` seeds static ARP and ND so the
hosts don't have to resolve neighbours at run time.
Run with:
sudo p4net examples/dual_stack/topology.py
Then in the shell:
pingall
h1 ping h2
h1 ping6 h2
"""
from __future__ import annotations
from pathlib import Path
from p4net import Network
from p4net.topo import Topology
HERE = Path(__file__).resolve().parent
topology = Topology()
h1 = topology.add_host(
"h1",
ip="10.0.0.1/24",
mac="00:00:00:00:00:01",
ip6="fd00::1/64",
)
h2 = topology.add_host(
"h2",
ip="10.0.0.2/24",
mac="00:00:00:00:00:02",
ip6="fd00::2/64",
)
s1 = topology.add_switch("s1", p4_src=HERE / "dual_stack.p4")
topology.add_link(h1, s1, port_b=1)
topology.add_link(h2, s1, port_b=2)
def setup(net: Network) -> None:
"""Pre-seed static ARP and ND so ICMP unicast doesn't have to resolve."""
h1 = net.host("h1")
h2 = net.host("h2")
h1.exec(
[
"ip",
"neigh",
"replace",
"10.0.0.2",
"lladdr",
"00:00:00:00:00:02",
"dev",
"h1-eth0",
"nud",
"permanent",
]
)
h2.exec(
[
"ip",
"neigh",
"replace",
"10.0.0.1",
"lladdr",
"00:00:00:00:00:01",
"dev",
"h2-eth0",
"nud",
"permanent",
]
)
h1.exec(
[
"ip",
"-6",
"neigh",
"replace",
"fd00::2",
"lladdr",
"00:00:00:00:00:02",
"dev",
"h1-eth0",
"nud",
"permanent",
]
)
h2.exec(
[
"ip",
"-6",
"neigh",
"replace",
"fd00::1",
"lladdr",
"00:00:00:00:00:01",
"dev",
"h2-eth0",
"nud",
"permanent",
]
)
if __name__ == "__main__":
from p4net.cli.main import main
raise SystemExit(main([__file__]))
Both Host.ip and Host.ip6 are set. The orchestrator detects this
and runs enable_ipv6(ns, iface) (with accept_ra=0, autoconf=0)
before bringing the interface up, then assigns both addresses.
P4 program¶
examples/dual_stack/dual_stack.p4:
#include <core.p4>
#include <v1model.p4>
header ethernet_t {
bit<48> dstAddr;
bit<48> srcAddr;
bit<16> etherType;
}
struct headers {
ethernet_t ethernet;
}
struct metadata {}
parser MyParser(packet_in pkt, out headers hdr, inout metadata meta,
inout standard_metadata_t std) {
state start {
pkt.extract(hdr.ethernet);
transition accept;
}
}
control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyIngress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) {
apply {
if (std.ingress_port == 1) {
std.egress_spec = 2;
} else if (std.ingress_port == 2) {
std.egress_spec = 1;
} else {
mark_to_drop(std);
}
}
}
control MyEgress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) { apply {} }
control MyComputeChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyDeparser(packet_out pkt, in headers hdr) {
apply {
pkt.emit(hdr.ethernet);
}
}
V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(),
MyComputeChecksum(), MyDeparser()) main;
The pipeline is L3-agnostic — it only swaps ports. v4 and v6 traffic take the same path.
Run it¶
p4net> hosts
name primary_ip primary_ip6 interfaces
h1 10.0.0.1/24 fd00::1/64 h1-eth0
h2 10.0.0.2/24 fd00::2/64 h2-eth0
p4net> h1 cmd ip -6 addr show dev h1-eth0
3: h1-eth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
inet6 fd00::1/64 scope global
valid_lft forever preferred_lft forever
p4net> pingall
2/2 succeeded
p4net> pingall6
2/2 succeeded
Note: only fd00::1/64. No fe80:: link-local. That's the sysctl
gate doing its job.
What's interesting¶
accept_ra=0andautoconf=0are written along withdisable_ipv6=0so the kernel doesn't silently auto-configure additional addresses from a Router Advertisement (which there are none of here, but still).- Static ND in
setup(net)— withaccept_raoff, IPv6 neighbor solicitation still works, but doing it for every cold-start ping wastes time. Pre-seeded entries keep the latency measurements clean.
Variations to try¶
- Drop the
ip6arguments from one host and confirmpingall6excludes it from the matrix (it filters onprimary_ip6). - Set
accept_ra=Truein a manualenable_ipv6(...)call and observe what addresses appear. (Requires bypassing the orchestrator.) - Add a
loss_pct=10.0link parameter and observe that v4 and v6 pings get the same loss rate (the qdisc is L3-agnostic).