Add IPv6 inside the PPP session and use enp2s0 in the example
- wan.ipv6.transport = "ppp": IPv6CP in pppd, router advertisements and DHCPv6 prefix delegation on the PPP device, DHCPv6 accepted on the PPP device, IPv6 MSS clamped to MTU-60. Allowed on a shared single-NIC link. A build-time check requires a PPPoE uplink. - IPv4 MSS clamping now only matches IPv4 packets. - The example uses the new box's NIC name (enp2s0). Covered by the onestick VM test (radvd and a minimal DHCPv6-PD responder as the ISP) and assertion cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -940,6 +940,45 @@ let
|
||||
};
|
||||
expect = [ "not a valid hostname" ];
|
||||
};
|
||||
pppTransportValid = {
|
||||
router = {
|
||||
wan = {
|
||||
ipv4 = {
|
||||
type = "pppoe";
|
||||
pppoe = {
|
||||
vlan = 1001;
|
||||
username = "u";
|
||||
passwordFile = "/var/lib/pw";
|
||||
};
|
||||
};
|
||||
ipv6 = {
|
||||
enable = true;
|
||||
transport = "ppp";
|
||||
};
|
||||
};
|
||||
lans.main = lan {
|
||||
interface = "eth0";
|
||||
ipv6.pd = true;
|
||||
};
|
||||
};
|
||||
expect = [ ];
|
||||
};
|
||||
pppTransportNeedsPppoe = {
|
||||
router = {
|
||||
wan = {
|
||||
ipv4.type = "dhcp";
|
||||
ipv6 = {
|
||||
enable = true;
|
||||
transport = "ppp";
|
||||
};
|
||||
};
|
||||
lans.main = lan { interface = "eth0"; };
|
||||
};
|
||||
expect = [
|
||||
"needs a PPPoE uplink"
|
||||
"also used by a LAN"
|
||||
];
|
||||
};
|
||||
nonOverlappingLans = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
@@ -988,6 +1027,7 @@ let
|
||||
"wgValid"
|
||||
"wanValid"
|
||||
"sharedLinkValid"
|
||||
"pppTransportValid"
|
||||
"dnsValid"
|
||||
];
|
||||
|
||||
|
||||
+101
-1
@@ -1,5 +1,6 @@
|
||||
# Router on a stick: one NIC carries the untagged LAN and, as VLAN 7, the
|
||||
# PPPoE uplink (IPv4 only), like a single-port box behind a VLAN-tagged ISP.
|
||||
# PPPoE uplink, like a single-port box behind a VLAN-tagged ISP. IPv6 runs
|
||||
# inside the PPP session: the ISP sends RAs and delegates a /64 on the PPP link.
|
||||
#
|
||||
# upstream (ISP, PPPoE on vlan 7) -- switch -- router (eth1) -- client, peer
|
||||
#
|
||||
@@ -68,6 +69,62 @@ pkgs.testers.runNixOSTest {
|
||||
matchConfig.Name = "eth1.${toString vlan}";
|
||||
networkConfig.LinkLocalAddressing = "no";
|
||||
};
|
||||
# The server's global address, reachable through the session.
|
||||
systemd.network.networks."05-lo" = {
|
||||
matchConfig.Name = "lo";
|
||||
address = [ "2001:db8:1::1/128" ];
|
||||
};
|
||||
# RAs and DHCPv6 prefix delegation on the (dynamic) PPP device. Kea
|
||||
# cannot serve a point-to-point link (it only sees the peer's address),
|
||||
# so a minimal responder stands in for the ISP's DHCPv6 server. The test
|
||||
# starts both once the session exists.
|
||||
environment.systemPackages = [
|
||||
pkgs.radvd
|
||||
pkgs.python3
|
||||
];
|
||||
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
|
||||
environment.etc."dhcp6-pd.py".text = ''
|
||||
import socket, struct, subprocess, sys
|
||||
|
||||
PREFIX = socket.inet_pton(socket.AF_INET6, "2001:db8:2::")
|
||||
DEV = sys.argv[1]
|
||||
ifindex = socket.if_nametoindex(DEV)
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setsockopt(socket.SOL_SOCKET, 25, DEV.encode() + b"\0") # SO_BINDTODEVICE
|
||||
sock.bind(("::", 547))
|
||||
group = socket.inet_pton(socket.AF_INET6, "ff02::1:2")
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group + struct.pack("@I", ifindex))
|
||||
SERVER_ID = struct.pack("!HH", 3, 0) + b"\x00\x03\x00\x01" + bytes(6)
|
||||
|
||||
def options(data):
|
||||
out, i = {}, 0
|
||||
while i + 4 <= len(data):
|
||||
code, length = struct.unpack("!HH", data[i:i + 4])
|
||||
out[code] = data[i + 4:i + 4 + length]
|
||||
i += 4 + length
|
||||
return out
|
||||
|
||||
def opt(code, body):
|
||||
return struct.pack("!HH", code, len(body)) + body
|
||||
|
||||
print("dhcp6-pd: listening on", DEV, flush=True)
|
||||
while True:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
mtype, xid, opts = data[0], data[1:4], options(data[4:])
|
||||
if mtype not in (1, 3) or 25 not in opts: # Solicit, Request with IA_PD
|
||||
continue
|
||||
iaid = opts[25][:4]
|
||||
prefix = opt(26, struct.pack("!IIB", 3000, 4000, 64) + PREFIX)
|
||||
ia_pd = opt(25, iaid + struct.pack("!II", 1800, 2700) + prefix)
|
||||
rapid = mtype == 1 and 14 in opts
|
||||
reply = bytes([7 if (mtype == 3 or rapid) else 2]) + xid
|
||||
reply += opt(1, opts[1]) + opt(2, SERVER_ID[4:]) + ia_pd
|
||||
if rapid:
|
||||
reply += opt(14, b"")
|
||||
sock.sendto(reply, addr)
|
||||
print("dhcp6-pd: answered", mtype, "from", addr, flush=True)
|
||||
'';
|
||||
environment.etc = {
|
||||
"ppp/options".text = "";
|
||||
"ppp/chap-secrets".text = ''
|
||||
@@ -75,6 +132,7 @@ pkgs.testers.runNixOSTest {
|
||||
'';
|
||||
"ppp/pppoe-server-options".text = ''
|
||||
require-chap
|
||||
+ipv6
|
||||
noccp
|
||||
mtu 1492
|
||||
mru 1492
|
||||
@@ -116,6 +174,10 @@ pkgs.testers.runNixOSTest {
|
||||
enable = true;
|
||||
wan = {
|
||||
interface = "eth1";
|
||||
ipv6 = {
|
||||
enable = true;
|
||||
transport = "ppp";
|
||||
};
|
||||
ipv4 = {
|
||||
type = "pppoe";
|
||||
pppoe = {
|
||||
@@ -129,6 +191,7 @@ pkgs.testers.runNixOSTest {
|
||||
interface = "eth1";
|
||||
internet = true;
|
||||
ipv4.address = "192.168.10.1";
|
||||
ipv6.pd = true;
|
||||
dhcp.pool = {
|
||||
start = "192.168.10.100";
|
||||
end = "192.168.10.199";
|
||||
@@ -213,5 +276,42 @@ pkgs.testers.runNixOSTest {
|
||||
client.fail("curl -sf --max-time 3 http://192.168.10.1:8080/")
|
||||
router.succeed("nft list ruleset | grep -q 'iifname \"pppoe-wan\"'")
|
||||
assert 'iifname { "eth1"' not in router.succeed("nft list ruleset"), "eth1 treated as an uplink"
|
||||
|
||||
with subtest("IPv6 comes up inside the PPP session"):
|
||||
upstream.wait_until_succeeds("ip link show ppp0")
|
||||
upstream.wait_until_succeeds("ip -6 -o addr show dev ppp0 scope link | grep -q inet6")
|
||||
upstream.succeed("systemd-run --unit=dhcp6pd python3 /etc/dhcp6-pd.py ppp0")
|
||||
upstream.succeed("printf 'interface ppp0 {\\n AdvSendAdvert on;\\n AdvDefaultLifetime 1800;\\n};\\n' > /tmp/radvd.conf")
|
||||
upstream.succeed("systemd-run --unit=radvd6 radvd -n -C /tmp/radvd.conf")
|
||||
router.wait_until_succeeds("ip -6 route show default | grep -q 'dev pppoe-wan'")
|
||||
router.wait_until_succeeds("ip -6 addr show dev eth1 scope global | grep -q 2001:db8:2:")
|
||||
# Nothing IPv6 on the physical link or the PPPoE VLAN.
|
||||
router.fail("ip -6 addr show dev eth1.${toString vlan} | grep -q inet6")
|
||||
ll = router.succeed("ip -6 -o addr show dev pppoe-wan scope link | awk '{print $4}' | cut -d/ -f1 | head -1").strip()
|
||||
assert ll.startswith("fe80:"), ll
|
||||
# The ISP routes the delegated prefix to the router's link-local address.
|
||||
upstream.succeed(f"ip -6 route replace 2001:db8:2::/48 via {ll} dev ppp0")
|
||||
|
||||
with subtest("the client autoconfigures from the delegated prefix, without NAT"):
|
||||
client.wait_until_succeeds("ip -6 addr show dev eth1 scope global | grep -q 2001:db8:2:")
|
||||
client_v6 = client.succeed("ip -6 -o addr show dev eth1 scope global | awk '{print $4}' | cut -d/ -f1 | head -1").strip()
|
||||
client.wait_until_succeeds("ping -6 -c 1 -W 2 2001:db8:1::1")
|
||||
out = client.succeed("curl -sf -g --max-time 5 'http://[2001:db8:1::1]/'")
|
||||
assert out.strip() == client_v6, f"expected {client_v6}, got {out!r}"
|
||||
|
||||
with subtest("IPv6 MSS is clamped to the PPP MTU"):
|
||||
router.succeed("tcpdump -nn -i pppoe-wan -c 1 -w /tmp/syn6.pcap 'ip6 and ip6[6] == 6 and ip6[53] & 0x12 == 0x02' >/dev/null 2>&1 &")
|
||||
router.wait_until_succeeds("pgrep tcpdump")
|
||||
router.sleep(1)
|
||||
client.succeed("curl -sf -g --max-time 5 'http://[2001:db8:1::1]/'")
|
||||
mss = router.wait_until_succeeds("tcpdump -nn -v -r /tmp/syn6.pcap 2>&1 | grep -o 'mss [0-9]*'")
|
||||
assert mss.strip() == "mss 1432", f"expected mss 1432, got {mss!r}"
|
||||
|
||||
with subtest("the ISP cannot initiate IPv6 to the router or the LAN"):
|
||||
# Positive control: the ISP really has a route to the client.
|
||||
upstream.succeed(f"ip -6 route get {client_v6} | grep -q 'dev ppp0'")
|
||||
upstream.fail(f"ping -6 -c 1 -W 2 {client_v6}")
|
||||
upstream.fail(f"nc -6 -z -w 2 {client_v6} 22")
|
||||
upstream.fail(f"nc -6 -z -w 2 {ll}%ppp0 22")
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user