IPv6 LPM¶
一台交换机上的两台仅 IPv6 主机,使用 ipv6_lpm 表按 128 位
目的地址匹配。表项在运行时通过 P4Runtime 编程下发。本例的看点是
<switch> table dump 把 IPv6 表项渲染为人类可读形式(fd00::1/128)
而非裸字节。
你将看到什么¶
pingall6 成功;s1 table dump MyIngress.ipv6_lpm 显示
fd00::1/128 与 fd00::2/128;每次 ping 后按端口的计数器都会
增加。
拓扑¶
examples/ipv6_lpm/topology.py:
"""Two IPv6 hosts forwarded by an `ipv6_lpm` table programmed at runtime.
Run with:
sudo p4net examples/ipv6_lpm/topology.py
Then in the shell:
pingall6
h1 ping6 h2
s1 table dump MyIngress.ipv6_lpm
s1 counter MyIngress.ipv6_pkts
"""
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", ip6="fd00::1/64", mac="00:00:00:00:00:01")
h2 = topology.add_host("h2", ip6="fd00::2/64", mac="00:00:00:00:00:02")
s1 = topology.add_switch("s1", p4_src=HERE / "ipv6_lpm.p4")
topology.add_link(h1, s1, port_b=1)
topology.add_link(h2, s1, port_b=2)
def setup(net: Network) -> None:
"""Seed static ND and install ipv6_lpm forwarding entries."""
h1 = net.host("h1")
h2 = net.host("h2")
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",
]
)
s1 = net.switch("s1")
s1.client.insert_table_entry(
table="MyIngress.ipv6_lpm",
match={"hdr.ipv6.dstAddr": "fd00::1/128"},
action="MyIngress.set_egress_port",
params={"port": 1},
)
s1.client.insert_table_entry(
table="MyIngress.ipv6_lpm",
match={"hdr.ipv6.dstAddr": "fd00::2/128"},
action="MyIngress.set_egress_port",
params={"port": 2},
)
if __name__ == "__main__":
from p4net.cli.main import main
raise SystemExit(main([__file__]))
Host.ip6 是唯一的 L3 地址——两台主机都仅 IPv6。编排器在拉起
接口之前调用 enable_ipv6(ns, iface) 并赋上 fd00::1/64 /
fd00::2/64。
P4 程序¶
examples/ipv6_lpm/ipv6_lpm.p4:
/* Minimal IPv6 LPM forwarding pipeline.
*
* Pairs with examples/ipv6_lpm/topology.py, which programs the table at
* runtime over P4Runtime. Both v6 endpoints are pre-seeded with static
* neighbor entries so ICMP unicast does not need to resolve at test time.
*/
#include <core.p4>
#include <v1model.p4>
const bit<16> ETHERTYPE_IPV6 = 0x86DD;
header ethernet_t {
bit<48> dstAddr;
bit<48> srcAddr;
bit<16> etherType;
}
header ipv6_t {
bit<4> version;
bit<8> trafficClass;
bit<20> flowLabel;
bit<16> payloadLen;
bit<8> nextHdr;
bit<8> hopLimit;
bit<128> srcAddr;
bit<128> dstAddr;
}
struct headers {
ethernet_t ethernet;
ipv6_t ipv6;
}
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 select(hdr.ethernet.etherType) {
ETHERTYPE_IPV6: parse_ipv6;
default: accept;
}
}
state parse_ipv6 {
pkt.extract(hdr.ipv6);
transition accept;
}
}
control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply {} }
control MyIngress(inout headers hdr, inout metadata meta,
inout standard_metadata_t std) {
counter(256, CounterType.packets) ipv6_pkts;
action drop() {
mark_to_drop(std);
}
action set_egress_port(bit<9> port) {
std.egress_spec = port;
ipv6_pkts.count((bit<32>) port);
}
table ipv6_lpm {
key = {
hdr.ipv6.dstAddr: lpm;
}
actions = {
drop;
set_egress_port;
NoAction;
}
default_action = NoAction();
size = 1024;
}
apply {
if (hdr.ipv6.isValid()) {
ipv6_lpm.apply();
}
}
}
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);
pkt.emit(hdr.ipv6);
}
}
V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(),
MyComputeChecksum(), MyDeparser()) main;
两个值得注意的点:
- 匹配键是
bit<128> dstAddr,类型lpm——运行时层存储的是 规范字节加上前缀长度,decode_match知道把 128 位字段渲染为 IPv6。 set_egress_port累加一个间接计数器,方便我们从控制器端 验证转发是否生效。
运行¶
p4net> hosts
name primary_ip primary_ip6 interfaces
h1 - fd00::1/64 h1-eth0
h2 - fd00::2/64 h2-eth0
p4net> s1 table dump MyIngress.ipv6_lpm
#0
table: MyIngress.ipv6_lpm
match: {'hdr.ipv6.dstAddr': 'fd00::1/128'}
action: MyIngress.set_egress_port
params: {'port': '1'}
#1
table: MyIngress.ipv6_lpm
match: {'hdr.ipv6.dstAddr': 'fd00::2/128'}
action: MyIngress.set_egress_port
params: {'port': '2'}
p4net> pingall6
H \ H h1 h2
h1 - 1
h2 1 -
2/2 succeeded
p4net> s1 counter MyIngress.ipv6_pkts 2
pkts=1 bytes=118
(上面的 table dump 输出来自 phase-13 集成测试的实际捕获。)
关键设计点¶
- 位宽感知的解码会为 128 位字段选择 IPv6 格式。同一个
decode_match把 32 位字段渲染为 IPv4,48 位字段渲染为 MAC, 128 位字段渲染为 IPv6 紧凑形式——P4Info 中无需任何按字段的 注解,位宽就是线索。 - 规范字节的往返干净。
encode_value("fd00::1", 128)产出b'\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'。 P4Runtime 通过去掉前导零来规范化,BMv2 存储控制器实际发出的 字节。读回时decode_ipv6在高位(最高位字节)一侧补零回到 16 字节,再交给ipaddress.IPv6Address.__str__渲染。 phase-13 集成测试已验证。
可尝试的变体¶
- 把一条
/128替换为/64(覆盖两台主机),同时再装一条/128,观察 LPM 在两条都安装时如何按更长前缀解析。 - 加一台
fd00::3/64的主机,运行时(无需修改 P4,无需重启) 从 Python 编程注入路由。 - 在 Python 控制器中用
client.read_counter("MyIngress.ipv6_pkts")周期性轮询计数器,观察流量。