diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 791e2f7..f1f7039 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,17 @@ jobs: files: coverage.out token: ${{ secrets.CODECOV_TOKEN }} + generate: + # The BPF programs are committed compiled, so that building sixup needs no clang; they have to + # be what bpf/*.c compiles to, with the toolchain AGENTS.md names. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: docker run --rm -v "$PWD:/src" -w /src golang:1.27-alpine3.24 sh -c 'apk add -q clang llvm libbpf-dev linux-headers && go generate ./...' + - name: Check the generated files + run: | + test -z "$(git status --porcelain)" || { git status --porcelain; echo "bpf/*.c changed: run go generate as AGENTS.md says and commit the result"; exit 1; } + lint: runs-on: ubuntu-latest steps: diff --git a/AGENTS.md b/AGENTS.md index 4ee2f52..b97ebfa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,3 +4,4 @@ - Write modern Go 1.27: generics, `slices`/`maps`/`cmp`, `errors.Is`/`%w`, `log/slog`, context. Compatibility with older versions is not a concern. - Do not rush to commit. After changing code, run `go build ./... && go vet ./... && go test ./...` and `GOOS=linux golangci-lint run` (the Linux target is the real one; on macOS the linux-only netlink constants are reported as unused), and wait for the user to confirm before committing. - Comments, log messages and command-line help are always in English. +- The BPF programs in `bpf/` are committed compiled. After changing them, regenerate with the toolchain CI checks against, and commit the result: `docker run --rm -v "$PWD:/src" -w /src golang:1.27-alpine3.24 sh -c 'apk add -q clang llvm libbpf-dev linux-headers && go generate ./...'`. diff --git a/bpf.go b/bpf.go index 3b69e18..0ce76aa 100644 --- a/bpf.go +++ b/bpf.go @@ -221,15 +221,43 @@ func (o *bpfObjects) close() { } } -// attachTCX attaches the program to the egress of a device; closing the descriptor it returns -// detaches it, and so does sixup ending. -func (o *bpfObjects) attachTCX(ifindex int) (int, error) { +// attachTCX attaches the program to a device, at unix.BPF_TCX_EGRESS or unix.BPF_TCX_INGRESS, and +// returns the link, which detaches it when closed, as it does when sixup ends. +func (o *bpfObjects) attachTCX(ifindex int, at uint32) (*os.File, error) { attr := struct { prog, ifindex, attachType, flags uint32 relative, _ uint32 expectedRevision uint64 - }{prog: uint32(o.prog), ifindex: uint32(ifindex), attachType: unix.BPF_TCX_EGRESS} - return bpfSys(unix.BPF_LINK_CREATE, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + }{prog: uint32(o.prog), ifindex: uint32(ifindex), attachType: at} + fd, err := bpfSys(unix.BPF_LINK_CREATE, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + if err != nil { + return nil, fmt.Errorf("attaching the program, which needs Linux 6.6 or later: %w", err) + } + return os.NewFile(uintptr(fd), "tcx"), nil +} + +// drops sums the counters of a program's stats map over the CPUs, by the names of those that +// count drops, and adds what its slow path dropped; those that are not zero. +func drops(stats *bpfMap, names []string, slow *slowPath) string { + var parts []string + per := make([]byte, stats.valueSize) + for i, name := range names { + if name == "" { + continue + } + stats.lookup(nativeEndian.AppendUint32(nil, uint32(i)), per) // every index of an array is there + var n uint64 + for c := 0; c < len(per); c += 8 { + n += nativeEndian.Uint64(per[c:]) + } + if n != 0 { + parts = append(parts, fmt.Sprintf("%s %d", name, n)) + } + } + if n := slow.dropped.Load(); n != 0 { + parts = append(parts, fmt.Sprintf("slow-path %d", n)) + } + return strings.Join(parts, ", ") } // errRingClosed is what a ring buffer reader returns once closed. diff --git a/bpf/clat.c b/bpf/clat.c new file mode 100644 index 0000000..d1caca9 --- /dev/null +++ b/bpf/clat.c @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: (MIT OR GPL-2.0) +// +// A CLAT (RFC 6877) for this host's own IPv4, through the NAT64 of the line's PREF64. IPv4 leaves +// by the low-priority default route on the device sixup-clat, from 192.0.0.4, and the program on +// that device's egress translates it to IPv6 from the host's CLAT address and sends it out of the +// WAN, the kernel filling in the link layer. On the WAN's ingress the same program takes the IPv6 +// from the NAT64 prefix to the CLAT address, translates it back and hands it to sixup-clat's +// ingress, where the host receives it, with the errors routers on the IPv6 path send the address. +// Everything else on the WAN passes untouched. The WAN is an Ethernet link. + +#include "xlat.h" + +volatile const __u8 clat6[16]; // the host's CLAT address +volatile const __be32 clat4; // 192.0.0.4 +volatile const __u32 clat_ifindex; // sixup-clat +volatile const __u8 clat_mac[6]; // and its address, which what it receives has to be for +volatile const __u32 wan_ifindex; // the WAN + +static __always_inline int is_clat6(const struct in6_addr *a) +{ +#pragma unroll + for (int i = 0; i < 16; i++) + if (a->s6_addr[i] != clat6[i]) + return 0; + return 1; +} + +// map6to4 extracts what the NAT64 prefix carries. Any other source is a router on the IPv6 path +// reporting an error, which becomes the IPv4 dummy address (RFC 7600). +static __always_inline int map6to4(const struct in6_addr *a, __be32 *out, int alloc) +{ + if (in_pref64(a)) + *out = extract4(a); + else if (is_clat6(a)) + *out = clat4; + else + *out = bpf_htonl(0xc0000008); // 192.0.0.8 + return 0; +} + +static __always_inline int map4to6(__be32 a, struct in6_addr *out) +{ + if (a != clat4) { + embed4(out, a); + return 0; + } +#pragma unroll + for (int i = 0; i < 16; i++) + out->s6_addr[i] = clat6[i]; + return 0; +} + +// On the WAN, the program's is what the NAT64 sends the CLAT address, and the errors any router on +// the way sends it; neighbour discovery and the rest of ICMPv6 for the address are the kernel's +static __always_inline int skip6(struct __sk_buff *skb, const struct ipv6hdr *ip6) +{ + if (!is_clat6(&ip6->daddr)) + return TC_ACT_OK; + if (in_pref64(&ip6->saddr)) + return XLAT; + __u8 type; + if (ip6->nexthdr == IPPROTO_ICMPV6 && !bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(*ip6), &type, 1) && type < 128) + return XLAT; + return TC_ACT_OK; +} + +// On sixup-clat, only what the host sends from 192.0.0.4; anything else has no way back +static __always_inline int skip4(struct __sk_buff *skb, const struct iphdr *ip) +{ + return ip->saddr == clat4 ? XLAT : TC_ACT_SHOT; +} + +static __always_inline int deliver(struct __sk_buff *skb, int to6) +{ + if (to6) + return bpf_redirect_neigh(wan_ifindex, NULL, 0, 0); + // Received on the WAN, the frame is addressed to it, which sixup-clat would not take for its own + if (bpf_skb_store_bytes(skb, 0, (const void *)clat_mac, sizeof(clat_mac), 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + return bpf_redirect(clat_ifindex, BPF_F_INGRESS); +} + +SEC("tc") +int clat(struct __sk_buff *skb) +{ + int v6 = skb->protocol == bpf_htons(ETH_P_IPV6); + if (skb->ifindex == wan_ifindex) + return v6 ? six_to_four(skb) : TC_ACT_OK; + // sixup-clat's egress. What sixup's slow path translated goes on like what the program does + if (skb->mark == slow_mark) { + skb->mark = 0; + return deliver(skb, v6); + } + if (skb->protocol == bpf_htons(ETH_P_IP)) + return four_to_six(skb); + return TC_ACT_SHOT; // the device's own MLD and router solicitations +} + +char LICENSE[] SEC("license") = "Dual MIT/GPL"; diff --git a/bpf/nat64.c b/bpf/nat64.c index 9dbf1ae..6fe2d2a 100644 --- a/bpf/nat64.c +++ b/bpf/nat64.c @@ -1,179 +1,25 @@ // SPDX-License-Identifier: (MIT OR GPL-2.0) // -// A stateless IPv6/IPv4 translator (RFC 7915) on the tc egress of sixup-nat64. Each IPv6 source is -// given an IPv4 address of its own from a private pool, so no ports are touched: the stateful part -// of NAT64, the sharing of the line's IPv4 address, is left to conntrack and the source NAT on the -// way out, as for any other IPv4 host behind the router. A translated packet is handed back to -// the same device's ingress and enters the stack as a new packet of the other family. -// -// TCP, UDP and ICMP are translated, ICMP errors with the packet they quote, and fragments one by -// one. Fragments reach the program as they were sent, or as conntrack's reassembly is cut again -// on the way out of the device, no larger than the pool route's MTU leaves room for. Two kinds -// need the whole datagram for their new checksum and go to sixup instead, through the ring buffer -// slow: fragmented ICMP, whose IPv6 checksum covers a length no single fragment shows, and IPv4 -// UDP without a checksum, which IPv6 requires. sixup sends them back translated through the same -// device, marked, and the program hands them to its ingress like the rest. Dropped and counted: IPv4 options, IPv6 extension -// headers other than a fragment header, and ICMP messages RFC 7915 does not translate. -// -// No packet reaches the program with CHECKSUM_COMPLETE: forwarding turns it into CHECKSUM_NONE -// (skb_forward_csum), and the router's own packets are never received ones. So only the checksum -// fields themselves are kept right, and with BPF_F_PSEUDO_HDR for CHECKSUM_PARTIAL. - -#include -#include -#include -#include -#include -#include -#include -#include -#include +// NAT64 on the tc egress of sixup-nat64. Each IPv6 source is given an IPv4 address of its own from +// a private pool, so no ports are touched: the stateful part of NAT64, the sharing of the line's +// IPv4 address, is left to conntrack and the source NAT on the way out, as for any other IPv4 +// host behind the router. A translated packet is handed back to the same device's ingress and +// enters the stack as a new packet of the other family. Fragments reach the program as they were +// sent, or as conntrack's reassembly is cut again on the way out of the device, no larger than +// the pool route's MTU leaves room for. -#define IP_MF 0x2000 -#define IP_DF 0x4000 -#define IP_OFFSET 0x1fff - -#define IPV6_MIN_MTU 1280 -#define IPV4_MIN_MTU 68 +#include "xlat.h" #define PROBES 8 // pool slots tried for a new source before giving up -// Set by the loader volatile const __u32 pool_base; // the first address of the pool, host order volatile const __u32 pool_mask; // the pool's size less one, a power of two less one -volatile const __u8 pref6[16]; // the NAT64 prefix, its host bits zero -volatile const __u8 mask6[16]; // ones over the prefix's length -volatile const __u8 v4pos[4]; // where RFC 6052 puts the bytes of an IPv4 address after it -volatile const __u32 slow_mark; // marks the packets sixup sends back translated - -// A map as sixup creates it: tools/bpfgen reads these from the section maps, so that sixup needs -// neither BTF nor an ELF parser -struct map_def { - __u32 type; - __u32 key_size; - __u32 value_size; - __u32 max_entries; - __u32 flags; -}; - -enum { - STAT_OUT, // IPv6 translated to IPv4 - STAT_IN, // IPv4 translated to IPv6 - STAT_DROP_PROTO, // not TCP, UDP or ICMP, or with IPv4 options or IPv6 extension headers - STAT_SLOW, // handed to sixup whole - STAT_DROP_ICMP, // an ICMP message RFC 7915 does not translate, or one too short to - STAT_DROP_POOL, // no free pool address for a new source - STAT_DROP_NOMAP, // for a pool address no source has, or quoting one - STAT_DROP_ERR, // a helper failed - STAT_MAX, -}; - -struct map_def stats SEC("maps") = {BPF_MAP_TYPE_PERCPU_ARRAY, sizeof(__u32), sizeof(__u64), STAT_MAX}; // The two directions of the address mapping, kept in step; sixup removes the pairs conntrack has // no flow for struct map_def v4_of SEC("maps") = {BPF_MAP_TYPE_HASH, sizeof(struct in6_addr), sizeof(__be32), 65536}; struct map_def v6_of SEC("maps") = {BPF_MAP_TYPE_HASH, sizeof(__be32), sizeof(struct in6_addr), 65536}; -#define SLOW_MAX 16384 // the largest frame handed to sixup, more than a jumbo LAN's; a power of two - -// The frames handed to sixup -struct map_def slow SEC("maps") = {BPF_MAP_TYPE_RINGBUF, 0, 0, 1 << 22}; - -// Where a frame is copied on its way there, as a ring buffer takes no packet directly -struct slow_frame { - __u8 b[SLOW_MAX]; -}; - -struct map_def slow_buf SEC("maps") = {BPF_MAP_TYPE_PERCPU_ARRAY, sizeof(__u32), sizeof(struct slow_frame), 1}; - -// The IPv4 UDP datagrams without a checksum whose first fragment went to sixup, so that the rest -// follows it there: the kernel cuts a reassembled datagram again first fragment first -struct frag4 { - __be32 saddr; - __be32 daddr; - __be16 id; - __u16 pad; -}; - -struct map_def slow_frags SEC("maps") = {BPF_MAP_TYPE_LRU_HASH, sizeof(struct frag4), sizeof(__u8), 1024}; - -// The IPv6 pseudo-header, which the ICMPv6 checksum covers and the ICMP one does not -struct pseudo6 { - struct in6_addr saddr; - struct in6_addr daddr; - __be32 len; - __be32 nexthdr; -}; - -struct frag6 { - __u8 nexthdr; - __u8 reserved; - __be16 frag_off; // offset in 8-byte units << 3, then two reserved bits and M - __be32 id; -}; - -// The fixed eight bytes of an ICMP or ICMPv6 header -union icmp8 { - __u8 b[8]; - __be16 h[4]; - __be32 w[2]; -}; - -// What an ICMP error quotes, header and all, as the checksum difference sees it -struct quote4 { - union icmp8 icmp; - struct iphdr ip; - __u8 l4[8]; -}; - -// A quoted IPv6 packet may carry a fragment header; without one, the eight bytes after the IPv6 -// header sit where it would, and the quote is 56 bytes long instead of 64 -struct quote6 { - union icmp8 icmp; - struct ipv6hdr ip; - struct frag6 fh; - __u8 l4[8]; -}; - -static __always_inline int count(__u32 stat, int verdict) -{ - __u64 *n = bpf_map_lookup_elem(&stats, &stat); - if (n) - *n += 1; - return verdict; -} - -// to_slow hands the packet to sixup, Ethernet header and all, and takes it from the stack. It is -// stolen rather than shot: a drop would fail the transmission, and the kernel cutting a datagram -// into fragments gives up on the rest at the first failure. -static __always_inline int to_slow(struct __sk_buff *skb) -{ - __u32 zero = 0, len = skb->len; - struct slow_frame *f = bpf_map_lookup_elem(&slow_buf, &zero); - if (!f || len < ETH_HLEN || len > SLOW_MAX) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - // The same length, in a form whose bounds the verifier can see; the barrier keeps the - // compiler from folding it back - asm volatile("" : "+r"(len)); - len = ((len - 1) & (SLOW_MAX - 1)) + 1; - if (bpf_skb_load_bytes(skb, 0, f->b, len) || bpf_ringbuf_output(&slow, f->b, len, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - return count(STAT_SLOW, TC_ACT_STOLEN); -} - -static __always_inline __u16 ip_checksum(const struct iphdr *ip) -{ - const __u16 *w = (const __u16 *)ip; - __u32 sum = 0; -#pragma unroll - for (int i = 0; i < (int)sizeof(*ip) / 2; i++) - sum += w[i]; - sum = (sum & 0xffff) + (sum >> 16); - sum = (sum & 0xffff) + (sum >> 16); - return ~sum; -} - static __always_inline __u32 hash6(const struct in6_addr *a) { __u32 h = a->s6_addr32[0] * 0x9e3779b1; @@ -183,603 +29,75 @@ static __always_inline __u32 hash6(const struct in6_addr *a) return h ^ (h >> 15); } -static __always_inline int in_pref64(const struct in6_addr *a) -{ -#pragma unroll - for (int i = 0; i < 16; i++) - if ((a->s6_addr[i] & mask6[i]) != pref6[i]) - return 0; - return 1; -} - -// embed4 is the IPv6 address of an IPv4 one behind the NAT64 prefix (RFC 6052 section 2.2). -static __always_inline void embed4(struct in6_addr *a, __be32 v4) +static __always_inline int in_pool(__be32 a) { - const __u8 *b = (const __u8 *)&v4; -#pragma unroll - for (int i = 0; i < 16; i++) - a->s6_addr[i] = pref6[i]; -#pragma unroll - for (int i = 0; i < 4; i++) - a->s6_addr[v4pos[i] & 15] = b[i]; -} - -// extract4 is the IPv4 address an address in the NAT64 prefix carries. -static __always_inline __be32 extract4(const struct in6_addr *a) -{ - __be32 v4; - __u8 *b = (__u8 *)&v4; -#pragma unroll - for (int i = 0; i < 4; i++) - b[i] = a->s6_addr[v4pos[i] & 15]; - return v4; + return (bpf_ntohl(a) & ~pool_mask) == pool_base; } -// map4 finds the pool address of an IPv6 source, giving it a free one the first time it is seen. -static __always_inline int map4(const struct in6_addr *src, __be32 *out) +// map6to4 extracts what the NAT64 prefix carries, and numbers an IPv6 source from the pool, +// giving it a free address the first time it is seen. +static __always_inline int map6to4(const struct in6_addr *a, __be32 *out, int alloc) { - __be32 *v = bpf_map_lookup_elem(&v4_of, src); + if (in_pref64(a)) { + *out = extract4(a); + return 0; + } + __be32 *v = bpf_map_lookup_elem(&v4_of, a); if (v) { *out = *v; return 0; } - __u32 h = hash6(src); + if (!alloc) + return STAT_DROP_NOMAP; + __u32 h = hash6(a); #pragma unroll for (int i = 0; i < PROBES; i++) { - __be32 a = bpf_htonl(pool_base + ((h + i) & pool_mask)); - if (bpf_map_update_elem(&v6_of, &a, src, BPF_NOEXIST)) + __be32 p = bpf_htonl(pool_base + ((h + i) & pool_mask)); + if (bpf_map_update_elem(&v6_of, &p, a, BPF_NOEXIST)) continue; - if (!bpf_map_update_elem(&v4_of, src, &a, BPF_NOEXIST)) { - *out = a; + if (!bpf_map_update_elem(&v4_of, a, &p, BPF_NOEXIST)) { + *out = p; return 0; } // Another CPU mapped the same source meanwhile - bpf_map_delete_elem(&v6_of, &a); - v = bpf_map_lookup_elem(&v4_of, src); + bpf_map_delete_elem(&v6_of, &p); + v = bpf_map_lookup_elem(&v4_of, a); if (!v) - return -1; + return STAT_DROP_POOL; *out = *v; return 0; } - return -1; -} - -// l4_csum_offset is where the checksum sits in a TCP or UDP header. -static __always_inline int l4_csum_offset(__u8 proto) -{ - return proto == IPPROTO_TCP ? 16 : 6; -} - -// The pointer of a Parameter Problem, from one header layout to the other (RFC 7915 figures 3 and 6) -static __always_inline int pointer_4to6(__u8 p) -{ - if (p <= 1) - return p; - if (p <= 3) - return 4; // Total Length: Payload Length - if (p == 8) - return 7; // TTL: Hop Limit - if (p == 9) - return 6; // Protocol: Next Header - if (p >= 12 && p <= 15) - return 8; - if (p >= 16 && p <= 19) - return 24; - return -1; -} - -static __always_inline int pointer_6to4(__u32 p) -{ - if (p <= 1) - return p; - if (p == 4 || p == 5) - return 2; - if (p == 6) - return 9; - if (p == 7) - return 8; - if (p >= 8 && p <= 23) - return 12; - if (p >= 24 && p <= 39) - return 16; - return -1; -} - -// icmp_6to4 turns an ICMPv6 header into its ICMP counterpart (RFC 7915 section 5.2). It returns 0 -// for an echo, 1 for an error quoting a packet, and -1 for a message that is not translated. -static __always_inline int icmp_6to4(union icmp8 *h) -{ - __u8 code = h->b[1]; - __u32 v = bpf_ntohl(h->w[1]); - int p; - switch (h->b[0]) { - case 128: // Echo Request - h->b[0] = 8; - return 0; - case 129: // Echo Reply - h->b[0] = 0; - return 0; - case 1: // Destination Unreachable - h->b[0] = 3; - switch (code) { - case 0: // no route - case 2: // beyond the scope of the source - case 3: // address unreachable - h->b[1] = 1; - break; - case 1: // administratively prohibited - h->b[1] = 10; - break; - case 4: // port unreachable - h->b[1] = 3; - break; - default: - return -1; - } - h->w[1] = 0; - return 1; - case 2: // Packet Too Big: Fragmentation Needed, for the 20 bytes the IPv4 header saves - if (v < IPV4_MIN_MTU + 20) - v = IPV4_MIN_MTU + 20; - v -= 20; - if (v > 0xffff) - v = 0xffff; - h->b[0] = 3; - h->b[1] = 4; - h->w[1] = bpf_htonl(v); - return 1; - case 3: // Time Exceeded, the codes the same - h->b[0] = 11; - h->w[1] = 0; - return 1; - case 4: // Parameter Problem - if (code == 1) { // unrecognised Next Header: Protocol Unreachable - h->b[0] = 3; - h->b[1] = 2; - h->w[1] = 0; - return 1; - } - if (code != 0) - return -1; - p = pointer_6to4(v); - if (p < 0) - return -1; - h->b[0] = 12; - h->w[1] = bpf_htonl((__u32)p << 24); - return 1; - } - return -1; + return STAT_DROP_POOL; } -// icmp_4to6 is the other way (RFC 7915 section 4.2). -static __always_inline int icmp_4to6(union icmp8 *h) +// map4to6 finds the source a pool address stands for, and embeds any other address in the prefix. +static __always_inline int map4to6(__be32 a, struct in6_addr *out) { - __u8 code = h->b[1]; - __u32 v; - int p; - switch (h->b[0]) { - case 8: // Echo Request - h->b[0] = 128; + if (!in_pool(a)) { + embed4(out, a); return 0; - case 0: // Echo Reply - h->b[0] = 129; - return 0; - case 3: // Destination Unreachable - switch (code) { - case 0: // net unreachable - case 1: // host unreachable - case 5: // source route failed - case 6: // destination network unknown - case 7: // destination host unknown - case 8: // source host isolated - case 11: // network unreachable for the TOS - case 12: // host unreachable for the TOS - h->b[0] = 1; - h->b[1] = 0; - h->w[1] = 0; - return 1; - case 9: // network administratively prohibited - case 10: // host administratively prohibited - case 13: // communication administratively prohibited - case 15: // precedence cutoff in effect - h->b[0] = 1; - h->b[1] = 1; - h->w[1] = 0; - return 1; - case 3: // port unreachable - h->b[0] = 1; - h->b[1] = 4; - h->w[1] = 0; - return 1; - case 2: // protocol unreachable: Parameter Problem at the Next Header - h->b[0] = 4; - h->b[1] = 1; - h->w[1] = bpf_htonl(6); - return 1; - case 4: // Fragmentation Needed: Packet Too Big, for the 20 bytes the IPv6 header adds - v = bpf_ntohs(h->h[3]); - v = v ? v + 20 : IPV6_MIN_MTU; - if (v < IPV6_MIN_MTU) - v = IPV6_MIN_MTU; - h->b[0] = 2; - h->b[1] = 0; - h->w[1] = bpf_htonl(v); - return 1; - } - return -1; - case 11: // Time Exceeded, the codes the same - h->b[0] = 3; - h->w[1] = 0; - return 1; - case 12: // Parameter Problem - if (code != 0 && code != 2) - return -1; - p = pointer_4to6(h->b[4]); - if (p < 0) - return -1; - h->b[0] = 4; - h->b[1] = 0; - h->w[1] = bpf_htonl(p); - return 1; - } - return -1; -} - -// icmp6_to_4 translates an ICMPv6 message whose IPv4 header ip is ready but for its length and -// checksum. An error's quote is translated too, back the way the packet it quotes came: from the -// NAT64 prefix to an IPv6 source the pool has an address for. The quote grows or shrinks by the -// difference of the headers, and the checksum is moved by what changed. -static __always_inline int icmp6_to_4(struct __sk_buff *skb, const struct ipv6hdr *ip6, struct iphdr *ip, __u16 len) -{ - const __u32 at = ETH_HLEN + sizeof(*ip6); - union icmp8 old, new; - if (len < sizeof(old) || bpf_skb_load_bytes(skb, at, &old, sizeof(old))) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - __be16 csum = old.h[1]; - old.h[1] = 0; - new = old; - int r = icmp_6to4(&new); - if (r < 0) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - struct pseudo6 ph = { - .saddr = ip6->saddr, - .daddr = ip6->daddr, - .len = bpf_htonl(len), - .nexthdr = bpf_htonl(IPPROTO_ICMPV6), - }; - __s64 seed = bpf_csum_diff((__be32 *)&ph, sizeof(ph), NULL, 0, 0); - ip->protocol = IPPROTO_ICMP; - - if (r == 0) { - ip->check = ip_checksum(ip); - __s64 diff = bpf_csum_diff(old.w, sizeof(old), new.w, sizeof(new), seed); - new.h[1] = csum; - if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __be16 ethertype = bpf_htons(ETH_P_IP); - if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN, ip, sizeof(*ip), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip), &new, sizeof(new), 0) || - bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip) + 2, 0, diff, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - count(STAT_OUT, 0); - return bpf_redirect(skb->ifindex, BPF_F_INGRESS); - } - - struct quote6 o; - struct quote4 n = {}; - if (len < sizeof(o) - sizeof(o.l4) || bpf_skb_load_bytes(skb, at + sizeof(o.icmp), &o.ip, sizeof(o.ip) + sizeof(o.fh))) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - o.icmp = old; - __u8 proto = o.ip.nexthdr; - __u16 ipl = bpf_ntohs(o.ip.payload_len); - int ifrag = proto == IPPROTO_FRAGMENT, first = 1; - n.ip.frag_off = bpf_htons(IP_DF); - if (ifrag) { - if (len < sizeof(o) || ipl < sizeof(o.fh) || bpf_skb_load_bytes(skb, at + sizeof(o) - sizeof(o.l4), o.l4, sizeof(o.l4))) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - proto = o.fh.nexthdr; - ipl -= sizeof(o.fh); - __u16 fo = bpf_ntohs(o.fh.frag_off); - first = fo >> 3 == 0; - n.ip.frag_off = bpf_htons(fo >> 3 | (fo & 1) << 13); - n.ip.id = bpf_htons(bpf_ntohl(o.fh.id)); - __builtin_memcpy(n.l4, o.l4, sizeof(n.l4)); - } else { - __builtin_memcpy(n.l4, &o.fh, sizeof(n.l4)); } - if (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMPV6) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - if (!in_pref64(&o.ip.saddr)) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - __be32 *dst = bpf_map_lookup_elem(&v4_of, &o.ip.daddr); - if (!dst) - return count(STAT_DROP_NOMAP, TC_ACT_SHOT); - n.icmp = new; - n.ip.version = 4; - n.ip.ihl = 5; - n.ip.tos = o.ip.priority << 4 | o.ip.flow_lbl[0] >> 4; - n.ip.tot_len = bpf_htons(ipl + sizeof(struct iphdr)); - n.ip.ttl = o.ip.hop_limit; - n.ip.protocol = proto == IPPROTO_ICMPV6 ? IPPROTO_ICMP : proto; - n.ip.saddr = extract4(&o.ip.saddr); - n.ip.daddr = *dst; - n.ip.check = ip_checksum(&n.ip); - // A quoted echo gets its own type; its checksum, like a quoted TCP or UDP one, is left alone - if (first && proto == IPPROTO_ICMPV6 && (n.l4[0] == 128 || n.l4[0] == 129)) - n.l4[0] = n.l4[0] == 128 ? 8 : 0; - __s64 diff = ifrag ? bpf_csum_diff((__be32 *)&o, sizeof(o), (__be32 *)&n, sizeof(n), seed) - : bpf_csum_diff((__be32 *)&o, sizeof(o) - sizeof(o.l4), (__be32 *)&n, sizeof(n), seed); - n.icmp.h[1] = csum; - - // The message shrinks by what the header does, and by the fragment header if the quote has one - int shrink = sizeof(struct ipv6hdr) - sizeof(struct iphdr) + (ifrag ? sizeof(struct frag6) : 0); - ip->tot_len = bpf_htons(len + sizeof(struct iphdr) - shrink); - ip->check = ip_checksum(ip); - if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0) || bpf_skb_adjust_room(skb, -shrink, BPF_ADJ_ROOM_NET, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __be16 ethertype = bpf_htons(ETH_P_IP); - if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN, ip, sizeof(*ip), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip), &n, sizeof(n), 0) || - bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip) + 2, 0, diff, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - count(STAT_OUT, 0); - return bpf_redirect(skb->ifindex, BPF_F_INGRESS); + struct in6_addr *v = bpf_map_lookup_elem(&v6_of, &a); + if (!v) + return STAT_DROP_NOMAP; + *out = *v; + return 0; } -static __always_inline int six_to_four(struct __sk_buff *skb) +// The device's own MLD and router solicitations are no one's loss, and nothing but the pool's +// should come its way +static __always_inline int skip6(struct __sk_buff *skb, const struct ipv6hdr *ip6) { - struct ipv6hdr ip6; - if (bpf_skb_load_bytes(skb, ETH_HLEN, &ip6, sizeof(ip6))) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - // The device's own MLD and router solicitations are no one's loss - if (!in_pref64(&ip6.daddr)) - return TC_ACT_SHOT; - __u8 proto = ip6.nexthdr; - __u16 len = bpf_ntohs(ip6.payload_len); - __be16 frag_off = bpf_htons(IP_DF), id = 0; - int frag = 0, first = 1; - if (proto == IPPROTO_FRAGMENT) { - struct frag6 fh; - if (len < sizeof(fh) || bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(ip6), &fh, sizeof(fh))) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - proto = fh.nexthdr; - if (proto == IPPROTO_ICMPV6) - return to_slow(skb); - len -= sizeof(fh); - __u16 fo = bpf_ntohs(fh.frag_off); - frag = 1; - first = fo >> 3 == 0; - frag_off = bpf_htons(fo >> 3 | (fo & 1) << 13); // DF clear: the pieces may be cut again - id = bpf_htons(bpf_ntohl(fh.id)); - } - if (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMPV6) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - if (len > 0xffff - sizeof(struct iphdr)) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - - __be32 saddr; - if (map4(&ip6.saddr, &saddr)) - return count(STAT_DROP_POOL, TC_ACT_SHOT); - struct iphdr ip = { - .version = 4, - .ihl = 5, - .tos = ip6.priority << 4 | ip6.flow_lbl[0] >> 4, - .tot_len = bpf_htons(len + sizeof(struct iphdr)), - .id = id, - .frag_off = frag_off, - .ttl = ip6.hop_limit, - .protocol = proto, - .saddr = saddr, - .daddr = extract4(&ip6.daddr), - }; - if (proto == IPPROTO_ICMPV6) - return icmp6_to_4(skb, &ip6, &ip, len); - ip.check = ip_checksum(&ip); - - if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - if (frag && bpf_skb_adjust_room(skb, -(int)sizeof(struct frag6), BPF_ADJ_ROOM_NET, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __be16 ethertype = bpf_htons(ETH_P_IP); - if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN, &ip, sizeof(ip), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - if (first) { - // The pseudo-header keeps its length and protocol; only the addresses change - __s64 diff = bpf_csum_diff((__be32 *)&ip6.saddr, 32, (__be32 *)&ip.saddr, 8, 0); - __u64 flags = BPF_F_PSEUDO_HDR | (proto == IPPROTO_UDP ? BPF_F_MARK_MANGLED_0 : 0); - if (bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(ip) + l4_csum_offset(proto), 0, diff, flags)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - } - count(STAT_OUT, 0); - return bpf_redirect(skb->ifindex, BPF_F_INGRESS); + return in_pref64(&ip6->daddr) ? XLAT : TC_ACT_SHOT; } -// icmp4_to_6 is icmp6_to_4 the other way: an error quotes a packet from a pool address to the -// NAT64 prefix. -static __always_inline int icmp4_to_6(struct __sk_buff *skb, const struct iphdr *ip, struct ipv6hdr *ip6, __u16 len) +static __always_inline int skip4(struct __sk_buff *skb, const struct iphdr *ip) { - const __u32 at = ETH_HLEN + sizeof(*ip); - union icmp8 old, new; - if (len < sizeof(old) || bpf_skb_load_bytes(skb, at, &old, sizeof(old))) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - __be16 csum = old.h[1]; - old.h[1] = 0; - new = old; - int r = icmp_4to6(&new); - if (r < 0) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - ip6->nexthdr = IPPROTO_ICMPV6; - - if (r == 0) { - struct pseudo6 ph = { - .saddr = ip6->saddr, - .daddr = ip6->daddr, - .len = bpf_htonl(len), - .nexthdr = bpf_htonl(IPPROTO_ICMPV6), - }; - __s64 seed = bpf_csum_diff(NULL, 0, (__be32 *)&ph, sizeof(ph), 0); - __s64 diff = bpf_csum_diff(old.w, sizeof(old), new.w, sizeof(new), seed); - new.h[1] = csum; - if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __be16 ethertype = bpf_htons(ETH_P_IPV6); - if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN, ip6, sizeof(*ip6), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &new, sizeof(new), 0) || - bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip6) + 2, 0, diff, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - count(STAT_IN, 0); - return bpf_redirect(skb->ifindex, BPF_F_INGRESS); - } - - struct quote4 o; - struct quote6 n = {}; - if (len < sizeof(o) || bpf_skb_load_bytes(skb, at + sizeof(o.icmp), &o.ip, sizeof(o.ip) + sizeof(o.l4))) - return count(STAT_DROP_ICMP, TC_ACT_SHOT); - o.icmp = old; - __u8 proto = o.ip.protocol; - if (o.ip.ihl != 5 || (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMP)) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - struct in6_addr *src = bpf_map_lookup_elem(&v6_of, &o.ip.saddr); - if (!src) - return count(STAT_DROP_NOMAP, TC_ACT_SHOT); - __u16 fo = bpf_ntohs(o.ip.frag_off); - int ifrag = (fo & (IP_MF | IP_OFFSET)) != 0; - __u8 nexthdr = proto == IPPROTO_ICMP ? IPPROTO_ICMPV6 : proto; - n.icmp = new; - n.ip.version = 6; - n.ip.priority = o.ip.tos >> 4; - n.ip.flow_lbl[0] = (o.ip.tos & 0xf) << 4; - n.ip.payload_len = bpf_htons(bpf_ntohs(o.ip.tot_len) - sizeof(struct iphdr) + (ifrag ? sizeof(n.fh) : 0)); - n.ip.nexthdr = nexthdr; - n.ip.hop_limit = o.ip.ttl; - n.ip.saddr = *src; - embed4(&n.ip.daddr, o.ip.daddr); - __u8 *l4 = (__u8 *)&n.fh; // right after the IPv6 header, unless a fragment header comes first - if (ifrag) { - n.ip.nexthdr = IPPROTO_FRAGMENT; - n.fh.nexthdr = nexthdr; - n.fh.frag_off = bpf_htons((fo & IP_OFFSET) << 3 | !!(fo & IP_MF)); - n.fh.id = bpf_htonl(bpf_ntohs(o.ip.id)); - l4 = n.l4; - } - __builtin_memcpy(l4, o.l4, sizeof(o.l4)); - if (!(fo & IP_OFFSET) && proto == IPPROTO_ICMP && (l4[0] == 8 || l4[0] == 0)) - l4[0] = l4[0] == 8 ? 128 : 129; - - int grow = sizeof(struct ipv6hdr) - sizeof(struct iphdr) + (ifrag ? sizeof(struct frag6) : 0); - len += grow; - ip6->payload_len = bpf_htons(len); - struct pseudo6 ph = { - .saddr = ip6->saddr, - .daddr = ip6->daddr, - .len = bpf_htonl(len), - .nexthdr = bpf_htonl(IPPROTO_ICMPV6), - }; - __s64 seed = bpf_csum_diff(NULL, 0, (__be32 *)&ph, sizeof(ph), 0); - __s64 diff = ifrag ? bpf_csum_diff((__be32 *)&o, sizeof(o), (__be32 *)&n, sizeof(n), seed) - : bpf_csum_diff((__be32 *)&o, sizeof(o), (__be32 *)&n, sizeof(n) - sizeof(n.l4), seed); - n.icmp.h[1] = csum; - - if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0) || bpf_skb_adjust_room(skb, grow, BPF_ADJ_ROOM_NET, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __be16 ethertype = bpf_htons(ETH_P_IPV6); - if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN, ip6, sizeof(*ip6), 0) || - (ifrag ? bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &n, sizeof(n), 0) - : bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &n, sizeof(n) - sizeof(n.l4), 0)) || - bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip6) + 2, 0, diff, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - count(STAT_IN, 0); - return bpf_redirect(skb->ifindex, BPF_F_INGRESS); + return in_pool(ip->daddr) ? XLAT : TC_ACT_SHOT; } -static __always_inline int four_to_six(struct __sk_buff *skb) +static __always_inline int deliver(struct __sk_buff *skb, int to6) { - struct iphdr ip; - if (bpf_skb_load_bytes(skb, ETH_HLEN, &ip, sizeof(ip))) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - if ((bpf_ntohl(ip.daddr) & ~pool_mask) != pool_base) - return TC_ACT_SHOT; // nothing of the pool's - if (ip.ihl != 5) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - __u8 proto = ip.protocol; - if (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMP) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - __u16 tot = bpf_ntohs(ip.tot_len); - if (tot < sizeof(ip)) - return count(STAT_DROP_PROTO, TC_ACT_SHOT); - __u16 len = tot - sizeof(ip); - __u16 fo = bpf_ntohs(ip.frag_off); - int frag = (fo & (IP_MF | IP_OFFSET)) != 0, first = (fo & IP_OFFSET) == 0; - if (frag && proto == IPPROTO_ICMP) - return to_slow(skb); - if (proto == IPPROTO_UDP) { - struct frag4 k = {.saddr = ip.saddr, .daddr = ip.daddr, .id = ip.id}; - if (first) { - __be16 check; - if (bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(ip) + 6, &check, sizeof(check))) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - if (!check) { - __u8 one = 1; - if (frag) - bpf_map_update_elem(&slow_frags, &k, &one, BPF_ANY); - return to_slow(skb); - } - } else if (bpf_map_lookup_elem(&slow_frags, &k)) { - return to_slow(skb); - } - } - - struct in6_addr *daddr = bpf_map_lookup_elem(&v6_of, &ip.daddr); - if (!daddr) - return count(STAT_DROP_NOMAP, TC_ACT_SHOT); - struct ipv6hdr ip6 = { - .version = 6, - .priority = ip.tos >> 4, - .flow_lbl = {(ip.tos & 0xf) << 4}, - .payload_len = bpf_htons(len), - .nexthdr = proto, - .hop_limit = ip.ttl, - .daddr = *daddr, - }; - embed4(&ip6.saddr, ip.saddr); - if (proto == IPPROTO_ICMP) - return icmp4_to_6(skb, &ip, &ip6, len); - - struct frag6 fh = { - .nexthdr = proto, - .frag_off = bpf_htons((fo & IP_OFFSET) << 3 | !!(fo & IP_MF)), - .id = bpf_htonl(bpf_ntohs(ip.id)), - }; - if (frag) { - ip6.nexthdr = IPPROTO_FRAGMENT; - ip6.payload_len = bpf_htons(len + sizeof(fh)); - } - if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - if (frag && bpf_skb_adjust_room(skb, sizeof(fh), BPF_ADJ_ROOM_NET, 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __be16 ethertype = bpf_htons(ETH_P_IPV6); - if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || - bpf_skb_store_bytes(skb, ETH_HLEN, &ip6, sizeof(ip6), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - __u32 l4 = ETH_HLEN + sizeof(ip6); - if (frag) { - if (bpf_skb_store_bytes(skb, l4, &fh, sizeof(fh), 0)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - l4 += sizeof(fh); - } - if (first) { - __s64 diff = bpf_csum_diff((__be32 *)&ip.saddr, 8, (__be32 *)&ip6.saddr, 32, 0); - __u64 flags = BPF_F_PSEUDO_HDR | (proto == IPPROTO_UDP ? BPF_F_MARK_MANGLED_0 : 0); - if (bpf_l4_csum_replace(skb, l4 + l4_csum_offset(proto), 0, diff, flags)) - return count(STAT_DROP_ERR, TC_ACT_SHOT); - } - count(STAT_IN, 0); return bpf_redirect(skb->ifindex, BPF_F_INGRESS); } @@ -789,7 +107,7 @@ int nat64(struct __sk_buff *skb) // What sixup's slow path translated comes in like what the program translates if (skb->mark == slow_mark) { skb->mark = 0; - return bpf_redirect(skb->ifindex, BPF_F_INGRESS); + return deliver(skb, skb->protocol == bpf_htons(ETH_P_IPV6)); } if (skb->protocol == bpf_htons(ETH_P_IPV6)) return six_to_four(skb); diff --git a/bpf/xlat.h b/bpf/xlat.h new file mode 100644 index 0000000..c6aa052 --- /dev/null +++ b/bpf/xlat.h @@ -0,0 +1,766 @@ +// SPDX-License-Identifier: (MIT OR GPL-2.0) +// +// The stateless IPv6/IPv4 translation of RFC 7915 that bpf/nat64.c and bpf/clat.c share. Each +// includes this and supplies how addresses map between the families and where a translated +// packet goes: +// +// map6to4 the IPv4 address of an IPv6 one, given a new one if alloc and the program has a +// pool; 0, or the counter to drop the packet under +// map4to6 the IPv6 address of an IPv4 one, likewise +// skip6 XLAT for an IPv6 packet to translate, else the verdict for one that is not the +// program's +// skip4 likewise for IPv4 +// deliver the verdict for a packet translated to IPv6 (to6) or to IPv4 +// +// TCP, UDP and ICMP are translated, ICMP errors with the packet they quote, and fragments one by +// one. Two kinds need the whole datagram for their new checksum and go to sixup instead, through +// the ring buffer slow: fragmented ICMP, whose IPv6 checksum covers a length no single fragment +// shows, and IPv4 UDP without a checksum, which IPv6 requires. sixup sends them back translated, +// marked slow_mark, and the program delivers them like the rest. Dropped and counted: IPv4 +// options, IPv6 extension headers other than a fragment header, and ICMP messages RFC 7915 does +// not translate. +// +// A packet received with CHECKSUM_COMPLETE, as the CLAT's are, keeps a right skb->csum: every +// write past the link layer header recomputes it, and every checksum field moves it along with +// the field (BPF_F_PSEUDO_HDR, which also serves CHECKSUM_PARTIAL). + +#ifndef XLAT_H +#define XLAT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define IP_MF 0x2000 +#define IP_DF 0x4000 +#define IP_OFFSET 0x1fff + +#define IPV6_MIN_MTU 1280 +#define IPV4_MIN_MTU 68 + +// Set by the loader +volatile const __u8 pref6[16]; // the NAT64 prefix, its host bits zero +volatile const __u8 mask6[16]; // ones over the prefix's length +volatile const __u8 v4pos[4]; // where RFC 6052 puts the bytes of an IPv4 address after it +volatile const __u32 slow_mark; // marks the packets sixup sends back translated + +// A map as sixup creates it: tools/bpfgen reads these from the section maps, so that sixup needs +// neither BTF nor an ELF parser +struct map_def { + __u32 type; + __u32 key_size; + __u32 value_size; + __u32 max_entries; + __u32 flags; +}; + +enum { + STAT_OUT, // IPv6 translated to IPv4 + STAT_IN, // IPv4 translated to IPv6 + STAT_DROP_PROTO, // not TCP, UDP or ICMP, or with IPv4 options or IPv6 extension headers + STAT_SLOW, // handed to sixup whole + STAT_DROP_ICMP, // an ICMP message RFC 7915 does not translate, or one too short to + STAT_DROP_POOL, // no free pool address for a new source + STAT_DROP_NOMAP, // for a pool address no source has, or quoting one + STAT_DROP_ERR, // a helper failed + STAT_MAX, +}; + +struct map_def stats SEC("maps") = {BPF_MAP_TYPE_PERCPU_ARRAY, sizeof(__u32), sizeof(__u64), STAT_MAX}; + +#define SLOW_MAX 16384 // the largest frame handed to sixup, more than a jumbo LAN's; a power of two + +// The frames handed to sixup +struct map_def slow SEC("maps") = {BPF_MAP_TYPE_RINGBUF, 0, 0, 1 << 22}; + +// Where a frame is copied on its way there, as a ring buffer takes no packet directly +struct slow_frame { + __u8 b[SLOW_MAX]; +}; + +struct map_def slow_buf SEC("maps") = {BPF_MAP_TYPE_PERCPU_ARRAY, sizeof(__u32), sizeof(struct slow_frame), 1}; + +// The IPv4 UDP datagrams without a checksum whose first fragment went to sixup, so that the rest +// follows it there: the kernel cuts a reassembled datagram again first fragment first +struct frag4 { + __be32 saddr; + __be32 daddr; + __be16 id; + __u16 pad; +}; + +struct map_def slow_frags SEC("maps") = {BPF_MAP_TYPE_LRU_HASH, sizeof(struct frag4), sizeof(__u8), 1024}; + +// The IPv6 pseudo-header, which the ICMPv6 checksum covers and the ICMP one does not +struct pseudo6 { + struct in6_addr saddr; + struct in6_addr daddr; + __be32 len; + __be32 nexthdr; +}; + +struct frag6 { + __u8 nexthdr; + __u8 reserved; + __be16 frag_off; // offset in 8-byte units << 3, then two reserved bits and M + __be32 id; +}; + +// The fixed eight bytes of an ICMP or ICMPv6 header +union icmp8 { + __u8 b[8]; + __be16 h[4]; + __be32 w[2]; +}; + +// What an ICMP error quotes, header and all, as the checksum difference sees it +struct quote4 { + union icmp8 icmp; + struct iphdr ip; + __u8 l4[8]; +}; + +// A quoted IPv6 packet may carry a fragment header; without one, the eight bytes after the IPv6 +// header sit where it would, and the quote is 56 bytes long instead of 64 +struct quote6 { + union icmp8 icmp; + struct ipv6hdr ip; + struct frag6 fh; + __u8 l4[8]; +}; + +#define XLAT -1 // what skip6 and skip4 return for a packet to translate + +static __always_inline int map6to4(const struct in6_addr *a, __be32 *out, int alloc); +static __always_inline int map4to6(__be32 a, struct in6_addr *out); +static __always_inline int skip6(struct __sk_buff *skb, const struct ipv6hdr *ip6); +static __always_inline int skip4(struct __sk_buff *skb, const struct iphdr *ip); +static __always_inline int deliver(struct __sk_buff *skb, int to6); + +static __always_inline int count(__u32 stat, int verdict) +{ + __u64 *n = bpf_map_lookup_elem(&stats, &stat); + if (n) + *n += 1; + return verdict; +} + +// to_slow hands the packet to sixup, Ethernet header and all, and takes it from the stack. It is +// stolen rather than shot: a drop would fail the transmission, and the kernel cutting a datagram +// into fragments gives up on the rest at the first failure. +static __always_inline int to_slow(struct __sk_buff *skb) +{ + __u32 zero = 0, len = skb->len; + struct slow_frame *f = bpf_map_lookup_elem(&slow_buf, &zero); + if (!f || len < ETH_HLEN || len > SLOW_MAX) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + // The same length, in a form whose bounds the verifier can see; the barrier keeps the + // compiler from folding it back + asm volatile("" : "+r"(len)); + len = ((len - 1) & (SLOW_MAX - 1)) + 1; + if (bpf_skb_load_bytes(skb, 0, f->b, len) || bpf_ringbuf_output(&slow, f->b, len, 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + return count(STAT_SLOW, TC_ACT_STOLEN); +} + +static __always_inline __u16 ip_checksum(const struct iphdr *ip) +{ + const __u16 *w = (const __u16 *)ip; + __u32 sum = 0; +#pragma unroll + for (int i = 0; i < (int)sizeof(*ip) / 2; i++) + sum += w[i]; + sum = (sum & 0xffff) + (sum >> 16); + sum = (sum & 0xffff) + (sum >> 16); + return ~sum; +} + +static __always_inline int in_pref64(const struct in6_addr *a) +{ +#pragma unroll + for (int i = 0; i < 16; i++) + if ((a->s6_addr[i] & mask6[i]) != pref6[i]) + return 0; + return 1; +} + +// embed4 is the IPv6 address of an IPv4 one behind the NAT64 prefix (RFC 6052 section 2.2). +static __always_inline void embed4(struct in6_addr *a, __be32 v4) +{ + const __u8 *b = (const __u8 *)&v4; +#pragma unroll + for (int i = 0; i < 16; i++) + a->s6_addr[i] = pref6[i]; +#pragma unroll + for (int i = 0; i < 4; i++) + a->s6_addr[v4pos[i] & 15] = b[i]; +} + +// extract4 is the IPv4 address an address in the NAT64 prefix carries. +static __always_inline __be32 extract4(const struct in6_addr *a) +{ + __be32 v4; + __u8 *b = (__u8 *)&v4; +#pragma unroll + for (int i = 0; i < 4; i++) + b[i] = a->s6_addr[v4pos[i] & 15]; + return v4; +} + +// l4_csum_offset is where the checksum sits in a TCP or UDP header. +static __always_inline int l4_csum_offset(__u8 proto) +{ + return proto == IPPROTO_TCP ? 16 : 6; +} + +// The pointer of a Parameter Problem, from one header layout to the other (RFC 7915 figures 3 and 6) +static __always_inline int pointer_4to6(__u8 p) +{ + if (p <= 1) + return p; + if (p <= 3) + return 4; // Total Length: Payload Length + if (p == 8) + return 7; // TTL: Hop Limit + if (p == 9) + return 6; // Protocol: Next Header + if (p >= 12 && p <= 15) + return 8; + if (p >= 16 && p <= 19) + return 24; + return -1; +} + +static __always_inline int pointer_6to4(__u32 p) +{ + if (p <= 1) + return p; + if (p == 4 || p == 5) + return 2; + if (p == 6) + return 9; + if (p == 7) + return 8; + if (p >= 8 && p <= 23) + return 12; + if (p >= 24 && p <= 39) + return 16; + return -1; +} + +// icmp_6to4 turns an ICMPv6 header into its ICMP counterpart (RFC 7915 section 5.2). It returns 0 +// for an echo, 1 for an error quoting a packet, and -1 for a message that is not translated. +static __always_inline int icmp_6to4(union icmp8 *h) +{ + __u8 code = h->b[1]; + __u32 v = bpf_ntohl(h->w[1]); + int p; + switch (h->b[0]) { + case 128: // Echo Request + h->b[0] = 8; + return 0; + case 129: // Echo Reply + h->b[0] = 0; + return 0; + case 1: // Destination Unreachable + h->b[0] = 3; + switch (code) { + case 0: // no route + case 2: // beyond the scope of the source + case 3: // address unreachable + h->b[1] = 1; + break; + case 1: // administratively prohibited + h->b[1] = 10; + break; + case 4: // port unreachable + h->b[1] = 3; + break; + default: + return -1; + } + h->w[1] = 0; + return 1; + case 2: // Packet Too Big: Fragmentation Needed, for the 20 bytes the IPv4 header saves + if (v < IPV4_MIN_MTU + 20) + v = IPV4_MIN_MTU + 20; + v -= 20; + if (v > 0xffff) + v = 0xffff; + h->b[0] = 3; + h->b[1] = 4; + h->w[1] = bpf_htonl(v); + return 1; + case 3: // Time Exceeded, the codes the same + h->b[0] = 11; + h->w[1] = 0; + return 1; + case 4: // Parameter Problem + if (code == 1) { // unrecognised Next Header: Protocol Unreachable + h->b[0] = 3; + h->b[1] = 2; + h->w[1] = 0; + return 1; + } + if (code != 0) + return -1; + p = pointer_6to4(v); + if (p < 0) + return -1; + h->b[0] = 12; + h->w[1] = bpf_htonl((__u32)p << 24); + return 1; + } + return -1; +} + +// icmp_4to6 is the other way (RFC 7915 section 4.2). +static __always_inline int icmp_4to6(union icmp8 *h) +{ + __u8 code = h->b[1]; + __u32 v; + int p; + switch (h->b[0]) { + case 8: // Echo Request + h->b[0] = 128; + return 0; + case 0: // Echo Reply + h->b[0] = 129; + return 0; + case 3: // Destination Unreachable + switch (code) { + case 0: // net unreachable + case 1: // host unreachable + case 5: // source route failed + case 6: // destination network unknown + case 7: // destination host unknown + case 8: // source host isolated + case 11: // network unreachable for the TOS + case 12: // host unreachable for the TOS + h->b[0] = 1; + h->b[1] = 0; + h->w[1] = 0; + return 1; + case 9: // network administratively prohibited + case 10: // host administratively prohibited + case 13: // communication administratively prohibited + case 15: // precedence cutoff in effect + h->b[0] = 1; + h->b[1] = 1; + h->w[1] = 0; + return 1; + case 3: // port unreachable + h->b[0] = 1; + h->b[1] = 4; + h->w[1] = 0; + return 1; + case 2: // protocol unreachable: Parameter Problem at the Next Header + h->b[0] = 4; + h->b[1] = 1; + h->w[1] = bpf_htonl(6); + return 1; + case 4: // Fragmentation Needed: Packet Too Big, for the 20 bytes the IPv6 header adds + v = bpf_ntohs(h->h[3]); + v = v ? v + 20 : IPV6_MIN_MTU; + if (v < IPV6_MIN_MTU) + v = IPV6_MIN_MTU; + h->b[0] = 2; + h->b[1] = 0; + h->w[1] = bpf_htonl(v); + return 1; + } + return -1; + case 11: // Time Exceeded, the codes the same + h->b[0] = 3; + h->w[1] = 0; + return 1; + case 12: // Parameter Problem + if (code != 0 && code != 2) + return -1; + p = pointer_4to6(h->b[4]); + if (p < 0) + return -1; + h->b[0] = 4; + h->b[1] = 0; + h->w[1] = bpf_htonl(p); + return 1; + } + return -1; +} + +// icmp6_to_4 translates an ICMPv6 message whose IPv4 header ip is ready but for its length and +// checksum. An error's quote is translated too, back the way the packet it quotes came: from the +// NAT64 prefix to an IPv6 source the pool has an address for. The quote grows or shrinks by the +// difference of the headers, and the checksum is moved by what changed. +static __always_inline int icmp6_to_4(struct __sk_buff *skb, const struct ipv6hdr *ip6, struct iphdr *ip, __u16 len) +{ + const __u32 at = ETH_HLEN + sizeof(*ip6); + union icmp8 old, new; + if (len < sizeof(old) || bpf_skb_load_bytes(skb, at, &old, sizeof(old))) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + __be16 csum = old.h[1]; + old.h[1] = 0; + new = old; + int r = icmp_6to4(&new); + if (r < 0) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + struct pseudo6 ph = { + .saddr = ip6->saddr, + .daddr = ip6->daddr, + .len = bpf_htonl(len), + .nexthdr = bpf_htonl(IPPROTO_ICMPV6), + }; + __s64 seed = bpf_csum_diff((__be32 *)&ph, sizeof(ph), NULL, 0, 0); + ip->protocol = IPPROTO_ICMP; + + if (r == 0) { + ip->check = ip_checksum(ip); + __s64 diff = bpf_csum_diff(old.w, sizeof(old), new.w, sizeof(new), seed); + new.h[1] = csum; + if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __be16 ethertype = bpf_htons(ETH_P_IP); + if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || + bpf_skb_store_bytes(skb, ETH_HLEN, ip, sizeof(*ip), BPF_F_RECOMPUTE_CSUM) || + bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip), &new, sizeof(new), BPF_F_RECOMPUTE_CSUM) || + bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip) + 2, 0, diff, BPF_F_PSEUDO_HDR)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + count(STAT_OUT, 0); + return deliver(skb, 0); + } + + struct quote6 o; + struct quote4 n = {}; + if (len < sizeof(o) - sizeof(o.l4) || bpf_skb_load_bytes(skb, at + sizeof(o.icmp), &o.ip, sizeof(o.ip) + sizeof(o.fh))) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + o.icmp = old; + __u8 proto = o.ip.nexthdr; + __u16 ipl = bpf_ntohs(o.ip.payload_len); + int ifrag = proto == IPPROTO_FRAGMENT, first = 1; + n.ip.frag_off = bpf_htons(IP_DF); + if (ifrag) { + if (len < sizeof(o) || ipl < sizeof(o.fh) || bpf_skb_load_bytes(skb, at + sizeof(o) - sizeof(o.l4), o.l4, sizeof(o.l4))) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + proto = o.fh.nexthdr; + ipl -= sizeof(o.fh); + __u16 fo = bpf_ntohs(o.fh.frag_off); + first = fo >> 3 == 0; + n.ip.frag_off = bpf_htons(fo >> 3 | (fo & 1) << 13); + n.ip.id = bpf_htons(bpf_ntohl(o.fh.id)); + __builtin_memcpy(n.l4, o.l4, sizeof(n.l4)); + } else { + __builtin_memcpy(n.l4, &o.fh, sizeof(n.l4)); + } + if (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMPV6) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + __be32 qsrc, qdst; + int st = map6to4(&o.ip.saddr, &qsrc, 0); + if (!st) + st = map6to4(&o.ip.daddr, &qdst, 0); + if (st) + return count(st, TC_ACT_SHOT); + n.icmp = new; + n.ip.version = 4; + n.ip.ihl = 5; + n.ip.tos = o.ip.priority << 4 | o.ip.flow_lbl[0] >> 4; + n.ip.tot_len = bpf_htons(ipl + sizeof(struct iphdr)); + n.ip.ttl = o.ip.hop_limit; + n.ip.protocol = proto == IPPROTO_ICMPV6 ? IPPROTO_ICMP : proto; + n.ip.saddr = qsrc; + n.ip.daddr = qdst; + n.ip.check = ip_checksum(&n.ip); + // A quoted echo gets its own type; its checksum, like a quoted TCP or UDP one, is left alone + if (first && proto == IPPROTO_ICMPV6 && (n.l4[0] == 128 || n.l4[0] == 129)) + n.l4[0] = n.l4[0] == 128 ? 8 : 0; + __s64 diff = ifrag ? bpf_csum_diff((__be32 *)&o, sizeof(o), (__be32 *)&n, sizeof(n), seed) + : bpf_csum_diff((__be32 *)&o, sizeof(o) - sizeof(o.l4), (__be32 *)&n, sizeof(n), seed); + n.icmp.h[1] = csum; + + // The message shrinks by what the header does, and by the fragment header if the quote has one + int shrink = sizeof(struct ipv6hdr) - sizeof(struct iphdr) + (ifrag ? sizeof(struct frag6) : 0); + ip->tot_len = bpf_htons(len + sizeof(struct iphdr) - shrink); + ip->check = ip_checksum(ip); + if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0) || bpf_skb_adjust_room(skb, -shrink, BPF_ADJ_ROOM_NET, 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __be16 ethertype = bpf_htons(ETH_P_IP); + if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || + bpf_skb_store_bytes(skb, ETH_HLEN, ip, sizeof(*ip), BPF_F_RECOMPUTE_CSUM) || + bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip), &n, sizeof(n), BPF_F_RECOMPUTE_CSUM) || + bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip) + 2, 0, diff, BPF_F_PSEUDO_HDR)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + count(STAT_OUT, 0); + return deliver(skb, 0); +} + +static __always_inline int six_to_four(struct __sk_buff *skb) +{ + struct ipv6hdr ip6; + if (bpf_skb_load_bytes(skb, ETH_HLEN, &ip6, sizeof(ip6))) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + int v = skip6(skb, &ip6); + if (v != XLAT) + return v; + __u8 proto = ip6.nexthdr; + __u16 len = bpf_ntohs(ip6.payload_len); + __be16 frag_off = bpf_htons(IP_DF), id = 0; + int frag = 0, first = 1; + if (proto == IPPROTO_FRAGMENT) { + struct frag6 fh; + if (len < sizeof(fh) || bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(ip6), &fh, sizeof(fh))) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + proto = fh.nexthdr; + if (proto == IPPROTO_ICMPV6) + return to_slow(skb); + len -= sizeof(fh); + __u16 fo = bpf_ntohs(fh.frag_off); + frag = 1; + first = fo >> 3 == 0; + frag_off = bpf_htons(fo >> 3 | (fo & 1) << 13); // DF clear: the pieces may be cut again + id = bpf_htons(bpf_ntohl(fh.id)); + } + if (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMPV6) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + if (len > 0xffff - sizeof(struct iphdr)) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + + __be32 saddr, daddr; + int st = map6to4(&ip6.saddr, &saddr, 1); + if (!st) + st = map6to4(&ip6.daddr, &daddr, 0); + if (st) + return count(st, TC_ACT_SHOT); + struct iphdr ip = { + .version = 4, + .ihl = 5, + .tos = ip6.priority << 4 | ip6.flow_lbl[0] >> 4, + .tot_len = bpf_htons(len + sizeof(struct iphdr)), + .id = id, + .frag_off = frag_off, + .ttl = ip6.hop_limit, + .protocol = proto, + .saddr = saddr, + .daddr = daddr, + }; + if (proto == IPPROTO_ICMPV6) + return icmp6_to_4(skb, &ip6, &ip, len); + ip.check = ip_checksum(&ip); + + if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IP), 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + if (frag && bpf_skb_adjust_room(skb, -(int)sizeof(struct frag6), BPF_ADJ_ROOM_NET, 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __be16 ethertype = bpf_htons(ETH_P_IP); + if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || + bpf_skb_store_bytes(skb, ETH_HLEN, &ip, sizeof(ip), BPF_F_RECOMPUTE_CSUM)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + if (first) { + // The pseudo-header keeps its length and protocol; only the addresses change + __s64 diff = bpf_csum_diff((__be32 *)&ip6.saddr, 32, (__be32 *)&ip.saddr, 8, 0); + __u64 flags = BPF_F_PSEUDO_HDR | (proto == IPPROTO_UDP ? BPF_F_MARK_MANGLED_0 : 0); + if (bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(ip) + l4_csum_offset(proto), 0, diff, flags)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + } + count(STAT_OUT, 0); + return deliver(skb, 0); +} + +// icmp4_to_6 is icmp6_to_4 the other way: an error quotes a packet from a pool address to the +// NAT64 prefix. +static __always_inline int icmp4_to_6(struct __sk_buff *skb, const struct iphdr *ip, struct ipv6hdr *ip6, __u16 len) +{ + const __u32 at = ETH_HLEN + sizeof(*ip); + union icmp8 old, new; + if (len < sizeof(old) || bpf_skb_load_bytes(skb, at, &old, sizeof(old))) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + __be16 csum = old.h[1]; + old.h[1] = 0; + new = old; + int r = icmp_4to6(&new); + if (r < 0) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + ip6->nexthdr = IPPROTO_ICMPV6; + + if (r == 0) { + struct pseudo6 ph = { + .saddr = ip6->saddr, + .daddr = ip6->daddr, + .len = bpf_htonl(len), + .nexthdr = bpf_htonl(IPPROTO_ICMPV6), + }; + __s64 seed = bpf_csum_diff(NULL, 0, (__be32 *)&ph, sizeof(ph), 0); + __s64 diff = bpf_csum_diff(old.w, sizeof(old), new.w, sizeof(new), seed); + new.h[1] = csum; + if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __be16 ethertype = bpf_htons(ETH_P_IPV6); + if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || + bpf_skb_store_bytes(skb, ETH_HLEN, ip6, sizeof(*ip6), BPF_F_RECOMPUTE_CSUM) || + bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &new, sizeof(new), BPF_F_RECOMPUTE_CSUM) || + bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip6) + 2, 0, diff, BPF_F_PSEUDO_HDR)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + count(STAT_IN, 0); + return deliver(skb, 1); + } + + struct quote4 o; + struct quote6 n = {}; + if (len < sizeof(o) || bpf_skb_load_bytes(skb, at + sizeof(o.icmp), &o.ip, sizeof(o.ip) + sizeof(o.l4))) + return count(STAT_DROP_ICMP, TC_ACT_SHOT); + o.icmp = old; + __u8 proto = o.ip.protocol; + if (o.ip.ihl != 5 || (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMP)) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + struct in6_addr qsrc, qdst; + int st = map4to6(o.ip.saddr, &qsrc); + if (!st) + st = map4to6(o.ip.daddr, &qdst); + if (st) + return count(st, TC_ACT_SHOT); + __u16 fo = bpf_ntohs(o.ip.frag_off); + int ifrag = (fo & (IP_MF | IP_OFFSET)) != 0; + __u8 nexthdr = proto == IPPROTO_ICMP ? IPPROTO_ICMPV6 : proto; + n.icmp = new; + n.ip.version = 6; + n.ip.priority = o.ip.tos >> 4; + n.ip.flow_lbl[0] = (o.ip.tos & 0xf) << 4; + n.ip.payload_len = bpf_htons(bpf_ntohs(o.ip.tot_len) - sizeof(struct iphdr) + (ifrag ? sizeof(n.fh) : 0)); + n.ip.nexthdr = nexthdr; + n.ip.hop_limit = o.ip.ttl; + n.ip.saddr = qsrc; + n.ip.daddr = qdst; + __u8 *l4 = (__u8 *)&n.fh; // right after the IPv6 header, unless a fragment header comes first + if (ifrag) { + n.ip.nexthdr = IPPROTO_FRAGMENT; + n.fh.nexthdr = nexthdr; + n.fh.frag_off = bpf_htons((fo & IP_OFFSET) << 3 | !!(fo & IP_MF)); + n.fh.id = bpf_htonl(bpf_ntohs(o.ip.id)); + l4 = n.l4; + } + __builtin_memcpy(l4, o.l4, sizeof(o.l4)); + if (!(fo & IP_OFFSET) && proto == IPPROTO_ICMP && (l4[0] == 8 || l4[0] == 0)) + l4[0] = l4[0] == 8 ? 128 : 129; + + int grow = sizeof(struct ipv6hdr) - sizeof(struct iphdr) + (ifrag ? sizeof(struct frag6) : 0); + len += grow; + ip6->payload_len = bpf_htons(len); + struct pseudo6 ph = { + .saddr = ip6->saddr, + .daddr = ip6->daddr, + .len = bpf_htonl(len), + .nexthdr = bpf_htonl(IPPROTO_ICMPV6), + }; + __s64 seed = bpf_csum_diff(NULL, 0, (__be32 *)&ph, sizeof(ph), 0); + __s64 diff = ifrag ? bpf_csum_diff((__be32 *)&o, sizeof(o), (__be32 *)&n, sizeof(n), seed) + : bpf_csum_diff((__be32 *)&o, sizeof(o), (__be32 *)&n, sizeof(n) - sizeof(n.l4), seed); + n.icmp.h[1] = csum; + + if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0) || bpf_skb_adjust_room(skb, grow, BPF_ADJ_ROOM_NET, 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __be16 ethertype = bpf_htons(ETH_P_IPV6); + if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || + bpf_skb_store_bytes(skb, ETH_HLEN, ip6, sizeof(*ip6), BPF_F_RECOMPUTE_CSUM) || + (ifrag ? bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &n, sizeof(n), BPF_F_RECOMPUTE_CSUM) + : bpf_skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &n, sizeof(n) - sizeof(n.l4), BPF_F_RECOMPUTE_CSUM)) || + bpf_l4_csum_replace(skb, ETH_HLEN + sizeof(*ip6) + 2, 0, diff, BPF_F_PSEUDO_HDR)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + count(STAT_IN, 0); + return deliver(skb, 1); +} + +static __always_inline int four_to_six(struct __sk_buff *skb) +{ + struct iphdr ip; + if (bpf_skb_load_bytes(skb, ETH_HLEN, &ip, sizeof(ip))) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + int v = skip4(skb, &ip); + if (v != XLAT) + return v; + if (ip.ihl != 5) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + __u8 proto = ip.protocol; + if (proto != IPPROTO_TCP && proto != IPPROTO_UDP && proto != IPPROTO_ICMP) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + __u16 tot = bpf_ntohs(ip.tot_len); + if (tot < sizeof(ip)) + return count(STAT_DROP_PROTO, TC_ACT_SHOT); + __u16 len = tot - sizeof(ip); + __u16 fo = bpf_ntohs(ip.frag_off); + int frag = (fo & (IP_MF | IP_OFFSET)) != 0, first = (fo & IP_OFFSET) == 0; + if (frag && proto == IPPROTO_ICMP) + return to_slow(skb); + if (proto == IPPROTO_UDP) { + struct frag4 k = {.saddr = ip.saddr, .daddr = ip.daddr, .id = ip.id}; + if (first) { + __be16 check; + if (bpf_skb_load_bytes(skb, ETH_HLEN + sizeof(ip) + 6, &check, sizeof(check))) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + if (!check) { + __u8 one = 1; + if (frag) + bpf_map_update_elem(&slow_frags, &k, &one, BPF_ANY); + return to_slow(skb); + } + } else if (bpf_map_lookup_elem(&slow_frags, &k)) { + return to_slow(skb); + } + } + + struct in6_addr saddr, daddr; + int st = map4to6(ip.saddr, &saddr); + if (!st) + st = map4to6(ip.daddr, &daddr); + if (st) + return count(st, TC_ACT_SHOT); + struct ipv6hdr ip6 = { + .version = 6, + .priority = ip.tos >> 4, + .flow_lbl = {(ip.tos & 0xf) << 4}, + .payload_len = bpf_htons(len), + .nexthdr = proto, + .hop_limit = ip.ttl, + .saddr = saddr, + .daddr = daddr, + }; + if (proto == IPPROTO_ICMP) + return icmp4_to_6(skb, &ip, &ip6, len); + + struct frag6 fh = { + .nexthdr = proto, + .frag_off = bpf_htons((fo & IP_OFFSET) << 3 | !!(fo & IP_MF)), + .id = bpf_htonl(bpf_ntohs(ip.id)), + }; + if (frag) { + ip6.nexthdr = IPPROTO_FRAGMENT; + ip6.payload_len = bpf_htons(len + sizeof(fh)); + } + if (bpf_skb_change_proto(skb, bpf_htons(ETH_P_IPV6), 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + if (frag && bpf_skb_adjust_room(skb, sizeof(fh), BPF_ADJ_ROOM_NET, 0)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __be16 ethertype = bpf_htons(ETH_P_IPV6); + if (bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_proto), ðertype, sizeof(ethertype), 0) || + bpf_skb_store_bytes(skb, ETH_HLEN, &ip6, sizeof(ip6), BPF_F_RECOMPUTE_CSUM)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + __u32 l4 = ETH_HLEN + sizeof(ip6); + if (frag) { + if (bpf_skb_store_bytes(skb, l4, &fh, sizeof(fh), BPF_F_RECOMPUTE_CSUM)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + l4 += sizeof(fh); + } + if (first) { + __s64 diff = bpf_csum_diff((__be32 *)&ip.saddr, 8, (__be32 *)&ip6.saddr, 32, 0); + __u64 flags = BPF_F_PSEUDO_HDR | (proto == IPPROTO_UDP ? BPF_F_MARK_MANGLED_0 : 0); + if (bpf_l4_csum_replace(skb, l4 + l4_csum_offset(proto), 0, diff, flags)) + return count(STAT_DROP_ERR, TC_ACT_SHOT); + } + count(STAT_IN, 0); + return deliver(skb, 1); +} + +#endif diff --git a/clat.go b/clat.go new file mode 100644 index 0000000..16a3fc5 --- /dev/null +++ b/clat.go @@ -0,0 +1,234 @@ +//go:build linux + +package main + +import ( + "context" + "errors" + "fmt" + "math" + "net" + "net/netip" + "os" + "slices" + "time" + + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +//go:generate go run ./tools/bpfgen bpf/clat.c clat + +var ( + clat4 = netip.MustParseAddr("192.0.0.4") // the host's IPv4 address behind the CLAT (RFC 7335) + clatDummy4 = netip.MustParseAddr("192.0.0.8") // the source of errors from the IPv6 path (RFC 7600) +) + +// clatMetric puts the CLAT's default route behind every other IPv4 one: it carries only what +// nothing else would. +const clatMetric = math.MaxUint32 + +// clatManager runs a CLAT (RFC 6877) for this host's own IPv4 while the upstream RA announces a +// PREF64 (RFC 8781), router or not. IPv4 that no other route takes leaves by the device +// sixup-clat from 192.0.0.4, bpf/clat.c translates it to the host's CLAT address, and the line's +// NAT64 does the rest. The CLAT address is one the WAN's address manager keeps on the WAN +// (clatAddr), so that the link delivers its packets here; the program on the WAN's ingress takes +// them before the stack does. The WAN has to be an Ethernet link. +type clatManager struct { + wan string + secret []byte + sysctl bool + warned bool // that the WAN is no Ethernet link + + // While running + cfg clatConfig + ifindex int + objs *bpfObjects + links []*os.File + slow *slowPath + reader *ringReader + served chan struct{} +} + +// clatConfig is what the CLAT is built from; another one rebuilds it. +type clatConfig struct { + pref64 netip.Prefix + addr netip.Addr // the host's CLAT address + wan int // the WAN's index + mtu int // and its MTU +} + +func (c *clatManager) run(ctx context.Context, ch <-chan Snapshot) { + defer c.stop() + tick := time.NewTicker(bpfTick) + defer tick.Stop() + var last string + for { + select { + case <-ctx.Done(): + return + case s := <-ch: + c.apply(s) + case <-tick.C: + if c.objs == nil { + continue + } + if d := drops(c.objs.maps[clatMapStats], bpfStatNames, c.slow); d != last { + last = d + infof("[clat] dropped so far: %s", d) + } + } + } +} + +// apply builds, rebuilds or takes down the CLAT for a snapshot. +func (c *clatManager) apply(s Snapshot) { + var want clatConfig + if a := s.clatAddr(c.secret); a.IsValid() { + ifi, err := net.InterfaceByName(c.wan) + switch { + case err != nil: + case len(ifi.HardwareAddr) != 6: + if !c.warned { + c.warned = true + warnf("[clat] the line announces the NAT64 prefix %s, but %s is no Ethernet link, which the CLAT needs", s.PREF64, c.wan) + } + default: + want = clatConfig{s.PREF64, a, ifi.Index, ifi.MTU} + } + } + if want == c.cfg { + return + } + if c.objs != nil { + c.stop() + infof("[clat] stopped") + } + c.cfg = want + if !want.addr.IsValid() { + return + } + if err := c.start(); err != nil { + warnf("[clat] %v", err) + return + } + infof("[clat] this host's IPv4 reaches the NAT64 prefix %s from %s through %s", want.pref64, want.addr, clatDev) +} + +// start builds the CLAT for c.cfg, and on failure undoes what it did. +func (c *clatManager) start() (err error) { + // Room for the IPv6 header and a fragment header once translated + ifi, err := dummyAdd(clatDev, c.cfg.mtu-28) + if err != nil { + return fmt.Errorf("cannot create %s: %w", clatDev, err) + } + c.ifindex = ifi.Index + defer func() { + if err != nil { + c.stop() + } + }() + // The device speaks no IPv6 of its own, and IPv4 from anywhere comes back on it while other + // routes lead elsewhere + if c.sysctl { + err = errors.Join(sysctlWrite("/proc/sys/net/ipv6/conf/"+clatDev+"/disable_ipv6", "1"), + sysctlWrite("/proc/sys/net/ipv4/conf/"+clatDev+"/rp_filter", "2")) + } + err = errors.Join(err, linkSetUp(ifi.Index), addr4Set(clatDev, netip.PrefixFrom(clat4, 32)), + routeOp(unix.RTM_NEWROUTE, netlink.Request|netlink.Acknowledge|netlink.Create|netlink.Replace, unix.RTN_UNICAST, 0, + ifi.Index, netip.PrefixFrom(netip.IPv4Unspecified(), 0), netip.Addr{}, clat4, clatMetric, 0)) + if err != nil { + return fmt.Errorf("setting up %s: %w", clatDev, err) + } + + ro := slices.Clone(clatRodata) + set := func(v bpfVar, b []byte) { copy(ro[v.off:v.off+v.size], b) } + var mask [16]byte + for i := range c.cfg.pref64.Bits() { + mask[i/8] |= 0x80 >> (i % 8) + } + pref, pos, a6, a4 := c.cfg.pref64.Masked().Addr().As16(), v4Positions(c.cfg.pref64.Bits()), c.cfg.addr.As16(), clat4.As4() + set(clatVarPref6, pref[:]) + set(clatVarMask6, mask[:]) + set(clatVarV4pos, pos[:]) + set(clatVarClat6, a6[:]) + set(clatVarClat4, a4[:]) + set(clatVarClatIfindex, nativeEndian.AppendUint32(nil, uint32(ifi.Index))) + set(clatVarClatMac, ifi.HardwareAddr) + set(clatVarWanIfindex, nativeEndian.AppendUint32(nil, uint32(c.cfg.wan))) + set(clatVarSlowMark, nativeEndian.AppendUint32(nil, bpfSlowMark)) + if c.objs, err = bpfLoad(bpfProgram{code: clatCode, maps: clatMaps, relocs: clatRelocs, rodata: ro, license: clatLicense}); err != nil { + return fmt.Errorf("loading the CLAT: %w", err) + } + for _, at := range []struct { + ifindex int + typ uint32 + }{{ifi.Index, unix.BPF_TCX_EGRESS}, {c.cfg.wan, unix.BPF_TCX_INGRESS}} { + l, err := c.objs.attachTCX(at.ifindex, at.typ) + if err != nil { + return fmt.Errorf("the CLAT: %w", err) + } + c.links = append(c.links, l) + } + if c.slow, err = newSlowPath(clatMapping(c.cfg.pref64, c.cfg.addr), ifi, c.cfg.mtu); err != nil { + return err + } + if c.reader, err = newRingReader(c.objs.maps[clatMapSlow]); err != nil { + return fmt.Errorf("reading the CLAT's slow path: %w", err) + } + c.served = make(chan struct{}) + go func() { + c.slow.serve(c.reader) + close(c.served) + }() + return nil +} + +// stop takes down whatever start set up; the device takes its address and route with it. +func (c *clatManager) stop() { + if c.reader != nil { + c.reader.close() + <-c.served + c.reader.free() + c.reader = nil + } + if c.slow != nil { + c.slow.close() + c.slow = nil + } + for _, l := range c.links { + l.Close() + } + c.links = nil + if c.objs != nil { + c.objs.close() + c.objs = nil + } + if c.ifindex != 0 { + linkDel(c.ifindex) + c.ifindex = 0 + } +} + +// clatMapping is bpf/clat.c's: the NAT64 prefix carries IPv4 addresses, the CLAT address stands +// for 192.0.0.4, and any other IPv6 address is a router on the path, 192.0.0.8. +func clatMapping(prefix netip.Prefix, addr netip.Addr) slowMapping { + a6 := addr.As16() + return slowMapping{ + v4of6: func(a [16]byte, _ bool) ([4]byte, error) { + switch { + case prefix.Contains(netip.AddrFrom16(a)): + return extract4(prefix, a), nil + case a == a6: + return clat4.As4(), nil + } + return clatDummy4.As4(), nil + }, + v6of4: func(a [4]byte) ([16]byte, error) { + if a == clat4.As4() { + return a6, nil + } + return embed4(prefix, a), nil + }, + } +} diff --git a/clat_bpf.go b/clat_bpf.go new file mode 100644 index 0000000..ee23011 --- /dev/null +++ b/clat_bpf.go @@ -0,0 +1,37 @@ +// Code generated by tools/bpfgen from bpf/clat.c; DO NOT EDIT. + +//go:build linux + +package main + +const clatLicense = "Dual MIT/GPL" + +// The maps, the read-only data last +const ( + clatMapStats = 0 + clatMapSlowBuf = 1 + clatMapSlow = 2 + clatMapSlowFrags = 3 + clatMapRodata = 4 +) + +var clatMaps = []bpfMapSpec{ + clatMapStats: {name: "stats", typ: 6, keySize: 4, valueSize: 8, maxEntries: 8, flags: 0x0}, + clatMapSlowBuf: {name: "slow_buf", typ: 6, keySize: 4, valueSize: 16384, maxEntries: 1, flags: 0x0}, + clatMapSlow: {name: "slow", typ: 27, keySize: 0, valueSize: 0, maxEntries: 4194304, flags: 0x0}, + clatMapSlowFrags: {name: "slow_frags", typ: 9, keySize: 12, valueSize: 1, maxEntries: 1024, flags: 0x0}, + clatMapRodata: {name: ".rodata", typ: 2, keySize: 4, valueSize: 74, maxEntries: 1, flags: 0x80}, +} + +// Where each variable sits in the read-only data, and its size +var ( + clatVarWanIfindex = bpfVar{off: 0, size: 4} + clatVarSlowMark = bpfVar{off: 4, size: 4} + clatVarPref6 = bpfVar{off: 8, size: 16} + clatVarMask6 = bpfVar{off: 24, size: 16} + clatVarV4pos = bpfVar{off: 40, size: 4} + clatVarClat6 = bpfVar{off: 44, size: 16} + clatVarClat4 = bpfVar{off: 60, size: 4} + clatVarClatIfindex = bpfVar{off: 64, size: 4} + clatVarClatMac = bpfVar{off: 68, size: 6} +) diff --git a/clat_bpfeb.bin b/clat_bpfeb.bin new file mode 100644 index 0000000..e08d1df Binary files /dev/null and b/clat_bpfeb.bin differ diff --git a/clat_bpfeb.go b/clat_bpfeb.go new file mode 100644 index 0000000..d322609 --- /dev/null +++ b/clat_bpfeb.go @@ -0,0 +1,211 @@ +// Code generated by tools/bpfgen from bpf/clat.c; DO NOT EDIT. + +//go:build linux && (mips || mips64 || ppc64 || s390x) + +package main + +import _ "embed" + +//go:embed clat_bpfeb.bin +var clatCode []byte + +var clatRodata = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} + +var clatRelocs = []bpfReloc{ + {insn: 2, m: clatMapRodata, off: 0}, + {insn: 19, m: clatMapStats, off: -1}, + {insn: 29, m: clatMapRodata, off: 4}, + {insn: 36, m: clatMapRodata, off: 0}, + {insn: 56, m: clatMapStats, off: -1}, + {insn: 62, m: clatMapRodata, off: 68}, + {insn: 72, m: clatMapStats, off: -1}, + {insn: 78, m: clatMapRodata, off: 44}, + {insn: 86, m: clatMapRodata, off: 44}, + {insn: 94, m: clatMapRodata, off: 44}, + {insn: 102, m: clatMapRodata, off: 44}, + {insn: 110, m: clatMapRodata, off: 44}, + {insn: 118, m: clatMapRodata, off: 44}, + {insn: 126, m: clatMapRodata, off: 44}, + {insn: 134, m: clatMapRodata, off: 44}, + {insn: 142, m: clatMapRodata, off: 24}, + {insn: 148, m: clatMapRodata, off: 8}, + {insn: 158, m: clatMapRodata, off: 24}, + {insn: 164, m: clatMapRodata, off: 8}, + {insn: 174, m: clatMapRodata, off: 24}, + {insn: 180, m: clatMapRodata, off: 8}, + {insn: 190, m: clatMapRodata, off: 24}, + {insn: 196, m: clatMapRodata, off: 8}, + {insn: 206, m: clatMapRodata, off: 24}, + {insn: 212, m: clatMapRodata, off: 8}, + {insn: 222, m: clatMapRodata, off: 24}, + {insn: 228, m: clatMapRodata, off: 8}, + {insn: 238, m: clatMapRodata, off: 24}, + {insn: 244, m: clatMapRodata, off: 8}, + {insn: 254, m: clatMapRodata, off: 24}, + {insn: 260, m: clatMapRodata, off: 8}, + {insn: 304, m: clatMapStats, off: -1}, + {insn: 310, m: clatMapRodata, off: 60}, + {insn: 322, m: clatMapStats, off: -1}, + {insn: 327, m: clatMapRodata, off: 64}, + {insn: 343, m: clatMapStats, off: -1}, + {insn: 354, m: clatMapStats, off: -1}, + {insn: 369, m: clatMapSlowBuf, off: -1}, + {insn: 380, m: clatMapStats, off: -1}, + {insn: 410, m: clatMapStats, off: -1}, + {insn: 425, m: clatMapSlow, off: -1}, + {insn: 436, m: clatMapStats, off: -1}, + {insn: 443, m: clatMapSlowFrags, off: -1}, + {insn: 452, m: clatMapSlowBuf, off: -1}, + {insn: 463, m: clatMapStats, off: -1}, + {insn: 478, m: clatMapSlow, off: -1}, + {insn: 489, m: clatMapStats, off: -1}, + {insn: 504, m: clatMapSlowFrags, off: -1}, + {insn: 513, m: clatMapSlowBuf, off: -1}, + {insn: 525, m: clatMapStats, off: -1}, + {insn: 532, m: clatMapRodata, off: 60}, + {insn: 537, m: clatMapRodata, off: 44}, + {insn: 572, m: clatMapRodata, off: 8}, + {insn: 606, m: clatMapRodata, off: 40}, + {insn: 638, m: clatMapRodata, off: 60}, + {insn: 643, m: clatMapRodata, off: 44}, + {insn: 678, m: clatMapRodata, off: 8}, + {insn: 712, m: clatMapRodata, off: 40}, + {insn: 792, m: clatMapStats, off: -1}, + {insn: 830, m: clatMapStats, off: -1}, + {insn: 844, m: clatMapStats, off: -1}, + {insn: 871, m: clatMapStats, off: -1}, + {insn: 880, m: clatMapStats, off: -1}, + {insn: 933, m: clatMapStats, off: -1}, + {insn: 947, m: clatMapSlow, off: -1}, + {insn: 958, m: clatMapStats, off: -1}, + {insn: 979, m: clatMapStats, off: -1}, + {insn: 1045, m: clatMapStats, off: -1}, + {insn: 1086, m: clatMapStats, off: -1}, + {insn: 1135, m: clatMapStats, off: -1}, + {insn: 1173, m: clatMapStats, off: -1}, + {insn: 1195, m: clatMapStats, off: -1}, + {insn: 1213, m: clatMapStats, off: -1}, + {insn: 1218, m: clatMapRodata, off: 60}, + {insn: 1223, m: clatMapRodata, off: 44}, + {insn: 1258, m: clatMapRodata, off: 8}, + {insn: 1292, m: clatMapRodata, off: 40}, + {insn: 1322, m: clatMapRodata, off: 60}, + {insn: 1327, m: clatMapRodata, off: 44}, + {insn: 1362, m: clatMapRodata, off: 8}, + {insn: 1396, m: clatMapRodata, off: 40}, + {insn: 1594, m: clatMapStats, off: -1}, + {insn: 1615, m: clatMapStats, off: -1}, + {insn: 1653, m: clatMapStats, off: -1}, + {insn: 1665, m: clatMapSlowBuf, off: -1}, + {insn: 1677, m: clatMapStats, off: -1}, + {insn: 1701, m: clatMapStats, off: -1}, + {insn: 1731, m: clatMapStats, off: -1}, + {insn: 1743, m: clatMapStats, off: -1}, + {insn: 1751, m: clatMapRodata, off: 24}, + {insn: 1756, m: clatMapRodata, off: 8}, + {insn: 1766, m: clatMapRodata, off: 24}, + {insn: 1772, m: clatMapRodata, off: 8}, + {insn: 1782, m: clatMapRodata, off: 24}, + {insn: 1788, m: clatMapRodata, off: 8}, + {insn: 1798, m: clatMapRodata, off: 24}, + {insn: 1804, m: clatMapRodata, off: 8}, + {insn: 1814, m: clatMapRodata, off: 24}, + {insn: 1820, m: clatMapRodata, off: 8}, + {insn: 1830, m: clatMapRodata, off: 24}, + {insn: 1836, m: clatMapRodata, off: 8}, + {insn: 1846, m: clatMapRodata, off: 24}, + {insn: 1852, m: clatMapRodata, off: 8}, + {insn: 1862, m: clatMapRodata, off: 24}, + {insn: 1868, m: clatMapRodata, off: 8}, + {insn: 1878, m: clatMapRodata, off: 40}, + {insn: 1891, m: clatMapRodata, off: 40}, + {insn: 1898, m: clatMapRodata, off: 40}, + {insn: 1914, m: clatMapRodata, off: 44}, + {insn: 1921, m: clatMapRodata, off: 44}, + {insn: 1929, m: clatMapRodata, off: 44}, + {insn: 1937, m: clatMapRodata, off: 44}, + {insn: 1945, m: clatMapRodata, off: 44}, + {insn: 1953, m: clatMapRodata, off: 44}, + {insn: 1961, m: clatMapRodata, off: 44}, + {insn: 1969, m: clatMapRodata, off: 44}, + {insn: 1977, m: clatMapRodata, off: 60}, + {insn: 1984, m: clatMapRodata, off: 24}, + {insn: 1989, m: clatMapRodata, off: 8}, + {insn: 1999, m: clatMapRodata, off: 24}, + {insn: 2005, m: clatMapRodata, off: 8}, + {insn: 2087, m: clatMapRodata, off: 40}, + {insn: 2106, m: clatMapRodata, off: 40}, + {insn: 2121, m: clatMapRodata, off: 44}, + {insn: 2128, m: clatMapRodata, off: 44}, + {insn: 2172, m: clatMapRodata, off: 60}, + {insn: 2238, m: clatMapStats, off: -1}, + {insn: 2254, m: clatMapStats, off: -1}, + {insn: 2263, m: clatMapStats, off: -1}, + {insn: 2272, m: clatMapStats, off: -1}, + {insn: 2279, m: clatMapRodata, off: 0}, + {insn: 2307, m: clatMapStats, off: -1}, + {insn: 2321, m: clatMapSlow, off: -1}, + {insn: 2332, m: clatMapStats, off: -1}, + {insn: 2362, m: clatMapStats, off: -1}, + {insn: 2371, m: clatMapStats, off: -1}, + {insn: 2384, m: clatMapStats, off: -1}, + {insn: 2393, m: clatMapRodata, off: 68}, + {insn: 2403, m: clatMapStats, off: -1}, + {insn: 2422, m: clatMapStats, off: -1}, + {insn: 2602, m: clatMapStats, off: -1}, + {insn: 2625, m: clatMapStats, off: -1}, + {insn: 2634, m: clatMapStats, off: -1}, + {insn: 2675, m: clatMapStats, off: -1}, + {insn: 2702, m: clatMapStats, off: -1}, + {insn: 2730, m: clatMapStats, off: -1}, + {insn: 2739, m: clatMapRodata, off: 24}, + {insn: 2744, m: clatMapRodata, off: 8}, + {insn: 2754, m: clatMapRodata, off: 24}, + {insn: 2760, m: clatMapRodata, off: 8}, + {insn: 2770, m: clatMapRodata, off: 24}, + {insn: 2776, m: clatMapRodata, off: 8}, + {insn: 2786, m: clatMapRodata, off: 24}, + {insn: 2792, m: clatMapRodata, off: 8}, + {insn: 2802, m: clatMapRodata, off: 24}, + {insn: 2808, m: clatMapRodata, off: 8}, + {insn: 2818, m: clatMapRodata, off: 24}, + {insn: 2824, m: clatMapRodata, off: 8}, + {insn: 2834, m: clatMapRodata, off: 24}, + {insn: 2840, m: clatMapRodata, off: 8}, + {insn: 2850, m: clatMapRodata, off: 24}, + {insn: 2856, m: clatMapRodata, off: 8}, + {insn: 2868, m: clatMapRodata, off: 40}, + {insn: 2897, m: clatMapRodata, off: 44}, + {insn: 2904, m: clatMapRodata, off: 44}, + {insn: 2912, m: clatMapRodata, off: 44}, + {insn: 2920, m: clatMapRodata, off: 44}, + {insn: 2928, m: clatMapRodata, off: 44}, + {insn: 2936, m: clatMapRodata, off: 44}, + {insn: 2944, m: clatMapRodata, off: 44}, + {insn: 2952, m: clatMapRodata, off: 44}, + {insn: 2960, m: clatMapRodata, off: 60}, + {insn: 2963, m: clatMapRodata, off: 24}, + {insn: 2968, m: clatMapRodata, off: 8}, + {insn: 2978, m: clatMapRodata, off: 24}, + {insn: 2984, m: clatMapRodata, off: 8}, + {insn: 3068, m: clatMapRodata, off: 40}, + {insn: 3086, m: clatMapRodata, off: 40}, + {insn: 3101, m: clatMapRodata, off: 44}, + {insn: 3108, m: clatMapRodata, off: 44}, + {insn: 3116, m: clatMapRodata, off: 44}, + {insn: 3124, m: clatMapRodata, off: 44}, + {insn: 3132, m: clatMapRodata, off: 44}, + {insn: 3140, m: clatMapRodata, off: 44}, + {insn: 3148, m: clatMapRodata, off: 44}, + {insn: 3156, m: clatMapRodata, off: 44}, + {insn: 3164, m: clatMapRodata, off: 60}, + {insn: 3298, m: clatMapStats, off: -1}, + {insn: 3364, m: clatMapStats, off: -1}, + {insn: 3373, m: clatMapStats, off: -1}, + {insn: 3382, m: clatMapRodata, off: 68}, + {insn: 3392, m: clatMapStats, off: -1}, + {insn: 3401, m: clatMapStats, off: -1}, + {insn: 3410, m: clatMapRodata, off: 68}, + {insn: 3420, m: clatMapStats, off: -1}, + {insn: 3425, m: clatMapRodata, off: 64}, +} diff --git a/clat_bpfel.bin b/clat_bpfel.bin new file mode 100644 index 0000000..be43240 Binary files /dev/null and b/clat_bpfel.bin differ diff --git a/clat_bpfel.go b/clat_bpfel.go new file mode 100644 index 0000000..9244824 --- /dev/null +++ b/clat_bpfel.go @@ -0,0 +1,207 @@ +// Code generated by tools/bpfgen from bpf/clat.c; DO NOT EDIT. + +//go:build linux && (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) + +package main + +import _ "embed" + +//go:embed clat_bpfel.bin +var clatCode []byte + +var clatRodata = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} + +var clatRelocs = []bpfReloc{ + {insn: 2, m: clatMapRodata, off: 0}, + {insn: 19, m: clatMapStats, off: -1}, + {insn: 29, m: clatMapRodata, off: 4}, + {insn: 36, m: clatMapRodata, off: 0}, + {insn: 56, m: clatMapStats, off: -1}, + {insn: 62, m: clatMapRodata, off: 68}, + {insn: 72, m: clatMapStats, off: -1}, + {insn: 78, m: clatMapRodata, off: 44}, + {insn: 86, m: clatMapRodata, off: 44}, + {insn: 94, m: clatMapRodata, off: 44}, + {insn: 102, m: clatMapRodata, off: 44}, + {insn: 110, m: clatMapRodata, off: 44}, + {insn: 118, m: clatMapRodata, off: 44}, + {insn: 126, m: clatMapRodata, off: 44}, + {insn: 134, m: clatMapRodata, off: 44}, + {insn: 142, m: clatMapRodata, off: 24}, + {insn: 148, m: clatMapRodata, off: 8}, + {insn: 158, m: clatMapRodata, off: 24}, + {insn: 164, m: clatMapRodata, off: 8}, + {insn: 174, m: clatMapRodata, off: 24}, + {insn: 180, m: clatMapRodata, off: 8}, + {insn: 190, m: clatMapRodata, off: 24}, + {insn: 196, m: clatMapRodata, off: 8}, + {insn: 206, m: clatMapRodata, off: 24}, + {insn: 212, m: clatMapRodata, off: 8}, + {insn: 222, m: clatMapRodata, off: 24}, + {insn: 228, m: clatMapRodata, off: 8}, + {insn: 238, m: clatMapRodata, off: 24}, + {insn: 244, m: clatMapRodata, off: 8}, + {insn: 254, m: clatMapRodata, off: 24}, + {insn: 260, m: clatMapRodata, off: 8}, + {insn: 305, m: clatMapStats, off: -1}, + {insn: 311, m: clatMapRodata, off: 60}, + {insn: 323, m: clatMapStats, off: -1}, + {insn: 328, m: clatMapRodata, off: 64}, + {insn: 344, m: clatMapStats, off: -1}, + {insn: 356, m: clatMapStats, off: -1}, + {insn: 372, m: clatMapSlowBuf, off: -1}, + {insn: 383, m: clatMapStats, off: -1}, + {insn: 413, m: clatMapStats, off: -1}, + {insn: 428, m: clatMapSlow, off: -1}, + {insn: 439, m: clatMapStats, off: -1}, + {insn: 446, m: clatMapSlowFrags, off: -1}, + {insn: 455, m: clatMapSlowBuf, off: -1}, + {insn: 466, m: clatMapStats, off: -1}, + {insn: 481, m: clatMapSlow, off: -1}, + {insn: 492, m: clatMapStats, off: -1}, + {insn: 507, m: clatMapSlowFrags, off: -1}, + {insn: 516, m: clatMapSlowBuf, off: -1}, + {insn: 528, m: clatMapStats, off: -1}, + {insn: 535, m: clatMapRodata, off: 60}, + {insn: 540, m: clatMapRodata, off: 44}, + {insn: 575, m: clatMapRodata, off: 8}, + {insn: 609, m: clatMapRodata, off: 40}, + {insn: 640, m: clatMapRodata, off: 60}, + {insn: 645, m: clatMapRodata, off: 44}, + {insn: 680, m: clatMapRodata, off: 8}, + {insn: 714, m: clatMapRodata, off: 40}, + {insn: 795, m: clatMapStats, off: -1}, + {insn: 836, m: clatMapStats, off: -1}, + {insn: 850, m: clatMapStats, off: -1}, + {insn: 877, m: clatMapStats, off: -1}, + {insn: 886, m: clatMapStats, off: -1}, + {insn: 940, m: clatMapStats, off: -1}, + {insn: 954, m: clatMapSlow, off: -1}, + {insn: 965, m: clatMapStats, off: -1}, + {insn: 986, m: clatMapStats, off: -1}, + {insn: 1054, m: clatMapStats, off: -1}, + {insn: 1095, m: clatMapStats, off: -1}, + {insn: 1146, m: clatMapStats, off: -1}, + {insn: 1184, m: clatMapStats, off: -1}, + {insn: 1206, m: clatMapStats, off: -1}, + {insn: 1224, m: clatMapStats, off: -1}, + {insn: 1229, m: clatMapRodata, off: 60}, + {insn: 1234, m: clatMapRodata, off: 44}, + {insn: 1269, m: clatMapRodata, off: 8}, + {insn: 1303, m: clatMapRodata, off: 40}, + {insn: 1332, m: clatMapRodata, off: 60}, + {insn: 1337, m: clatMapRodata, off: 44}, + {insn: 1372, m: clatMapRodata, off: 8}, + {insn: 1406, m: clatMapRodata, off: 40}, + {insn: 1613, m: clatMapStats, off: -1}, + {insn: 1634, m: clatMapStats, off: -1}, + {insn: 1672, m: clatMapStats, off: -1}, + {insn: 1684, m: clatMapSlowBuf, off: -1}, + {insn: 1696, m: clatMapStats, off: -1}, + {insn: 1720, m: clatMapStats, off: -1}, + {insn: 1753, m: clatMapStats, off: -1}, + {insn: 1765, m: clatMapStats, off: -1}, + {insn: 1775, m: clatMapRodata, off: 24}, + {insn: 1780, m: clatMapRodata, off: 8}, + {insn: 1790, m: clatMapRodata, off: 24}, + {insn: 1796, m: clatMapRodata, off: 8}, + {insn: 1806, m: clatMapRodata, off: 24}, + {insn: 1812, m: clatMapRodata, off: 8}, + {insn: 1822, m: clatMapRodata, off: 24}, + {insn: 1828, m: clatMapRodata, off: 8}, + {insn: 1838, m: clatMapRodata, off: 24}, + {insn: 1844, m: clatMapRodata, off: 8}, + {insn: 1854, m: clatMapRodata, off: 24}, + {insn: 1860, m: clatMapRodata, off: 8}, + {insn: 1870, m: clatMapRodata, off: 24}, + {insn: 1876, m: clatMapRodata, off: 8}, + {insn: 1886, m: clatMapRodata, off: 24}, + {insn: 1892, m: clatMapRodata, off: 8}, + {insn: 1902, m: clatMapRodata, off: 40}, + {insn: 1936, m: clatMapRodata, off: 44}, + {insn: 1943, m: clatMapRodata, off: 44}, + {insn: 1951, m: clatMapRodata, off: 44}, + {insn: 1959, m: clatMapRodata, off: 44}, + {insn: 1967, m: clatMapRodata, off: 44}, + {insn: 1975, m: clatMapRodata, off: 44}, + {insn: 1983, m: clatMapRodata, off: 44}, + {insn: 1991, m: clatMapRodata, off: 44}, + {insn: 1999, m: clatMapRodata, off: 60}, + {insn: 2009, m: clatMapRodata, off: 24}, + {insn: 2014, m: clatMapRodata, off: 8}, + {insn: 2024, m: clatMapRodata, off: 24}, + {insn: 2030, m: clatMapRodata, off: 8}, + {insn: 2112, m: clatMapRodata, off: 40}, + {insn: 2144, m: clatMapRodata, off: 44}, + {insn: 2151, m: clatMapRodata, off: 44}, + {insn: 2195, m: clatMapRodata, off: 60}, + {insn: 2260, m: clatMapStats, off: -1}, + {insn: 2276, m: clatMapStats, off: -1}, + {insn: 2285, m: clatMapStats, off: -1}, + {insn: 2294, m: clatMapStats, off: -1}, + {insn: 2301, m: clatMapRodata, off: 0}, + {insn: 2329, m: clatMapStats, off: -1}, + {insn: 2343, m: clatMapSlow, off: -1}, + {insn: 2354, m: clatMapStats, off: -1}, + {insn: 2384, m: clatMapStats, off: -1}, + {insn: 2393, m: clatMapStats, off: -1}, + {insn: 2406, m: clatMapStats, off: -1}, + {insn: 2415, m: clatMapRodata, off: 68}, + {insn: 2425, m: clatMapStats, off: -1}, + {insn: 2444, m: clatMapStats, off: -1}, + {insn: 2629, m: clatMapStats, off: -1}, + {insn: 2652, m: clatMapStats, off: -1}, + {insn: 2661, m: clatMapStats, off: -1}, + {insn: 2702, m: clatMapStats, off: -1}, + {insn: 2730, m: clatMapStats, off: -1}, + {insn: 2758, m: clatMapStats, off: -1}, + {insn: 2767, m: clatMapRodata, off: 24}, + {insn: 2772, m: clatMapRodata, off: 8}, + {insn: 2782, m: clatMapRodata, off: 24}, + {insn: 2788, m: clatMapRodata, off: 8}, + {insn: 2798, m: clatMapRodata, off: 24}, + {insn: 2804, m: clatMapRodata, off: 8}, + {insn: 2814, m: clatMapRodata, off: 24}, + {insn: 2820, m: clatMapRodata, off: 8}, + {insn: 2830, m: clatMapRodata, off: 24}, + {insn: 2836, m: clatMapRodata, off: 8}, + {insn: 2846, m: clatMapRodata, off: 24}, + {insn: 2852, m: clatMapRodata, off: 8}, + {insn: 2862, m: clatMapRodata, off: 24}, + {insn: 2868, m: clatMapRodata, off: 8}, + {insn: 2878, m: clatMapRodata, off: 24}, + {insn: 2884, m: clatMapRodata, off: 8}, + {insn: 2896, m: clatMapRodata, off: 40}, + {insn: 2925, m: clatMapRodata, off: 44}, + {insn: 2932, m: clatMapRodata, off: 44}, + {insn: 2940, m: clatMapRodata, off: 44}, + {insn: 2948, m: clatMapRodata, off: 44}, + {insn: 2956, m: clatMapRodata, off: 44}, + {insn: 2964, m: clatMapRodata, off: 44}, + {insn: 2972, m: clatMapRodata, off: 44}, + {insn: 2980, m: clatMapRodata, off: 44}, + {insn: 2988, m: clatMapRodata, off: 60}, + {insn: 2991, m: clatMapRodata, off: 24}, + {insn: 2996, m: clatMapRodata, off: 8}, + {insn: 3006, m: clatMapRodata, off: 24}, + {insn: 3012, m: clatMapRodata, off: 8}, + {insn: 3096, m: clatMapRodata, off: 40}, + {insn: 3127, m: clatMapRodata, off: 44}, + {insn: 3134, m: clatMapRodata, off: 44}, + {insn: 3142, m: clatMapRodata, off: 44}, + {insn: 3150, m: clatMapRodata, off: 44}, + {insn: 3158, m: clatMapRodata, off: 44}, + {insn: 3166, m: clatMapRodata, off: 44}, + {insn: 3174, m: clatMapRodata, off: 44}, + {insn: 3182, m: clatMapRodata, off: 44}, + {insn: 3190, m: clatMapRodata, off: 60}, + {insn: 3323, m: clatMapStats, off: -1}, + {insn: 3391, m: clatMapStats, off: -1}, + {insn: 3400, m: clatMapStats, off: -1}, + {insn: 3409, m: clatMapRodata, off: 68}, + {insn: 3419, m: clatMapStats, off: -1}, + {insn: 3428, m: clatMapStats, off: -1}, + {insn: 3437, m: clatMapRodata, off: 68}, + {insn: 3447, m: clatMapStats, off: -1}, + {insn: 3452, m: clatMapRodata, off: 64}, +} diff --git a/clat_integration_test.go b/clat_integration_test.go new file mode 100644 index 0000000..dfbc8af --- /dev/null +++ b/clat_integration_test.go @@ -0,0 +1,469 @@ +//go:build linux && integration + +package main + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "log" + "net" + "net/netip" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" + + "golang.org/x/sys/unix" +) + +// skbCtx is the start of a struct __sk_buff for a test run: the mark and the device. +func skbCtx(ifindex int, mark uint32) []byte { + b := make([]byte, 44) + nativeEndian.PutUint32(b[8:], mark) + nativeEndian.PutUint32(b[40:], uint32(ifindex)) + return b +} + +func ethFrame(pkt []byte) []byte { + proto := uint16(unix.ETH_P_IP) + if pkt[0]>>4 == 6 { + proto = unix.ETH_P_IPV6 + } + return append(binary.BigEndian.AppendUint16(make([]byte, 12), proto), pkt...) +} + +// The program translates the host's IPv4 out of sixup-clat and what the NAT64 sends back on the +// WAN, with the errors routers on the way send; on the WAN it leaves everything else to the stack. +func TestCLATProgram(t *testing.T) { + if !ownNetns(t) { + return + } + wan, err := dummyAdd("clatwan0", 0) + if err != nil { + t.Fatal(err) + } + defer linkDel(wan.Index) + clat6 := netip.MustParseAddr("2001:db8::c1a7") + c := &clatManager{wan: "clatwan0", cfg: clatConfig{nat64WKP, clat6, wan.Index, 1500}} + if err := c.start(); err != nil { + t.Fatal(err) + } + defer c.stop() + dev, err := net.InterfaceByName(clatDev) + if err != nil { + t.Fatal(err) + } + if dev.MTU != 1472 { + t.Errorf("%s should leave room for the IPv6 and fragment headers, has an MTU of %d", clatDev, dev.MTU) + } + var frame []byte + run := func(pkt []byte, ifindex int, mark uint32) (uint32, []byte) { + t.Helper() + ret, out, _ := c.objs.testRun(t, ethFrame(pkt), skbCtx(ifindex, mark)) + frame = out + return ret, out[14:] + } + server := "64:ff9b::c000:202" + udp := func(src, dst []byte) []byte { + u := []byte{0x30, 0x39, 0, 7, 0, 12, 0, 0, 'p', 'i', 'n', 'g'} + binary.BigEndian.PutUint16(u[6:], l4Checksum(src, dst, unix.IPPROTO_UDP, u)) + return u + } + + // Out: from 192.0.0.4, to the NAT64 from the CLAT address + ret, got := run(v4Packet("192.0.0.4", "192.0.2.2", unix.IPPROTO_UDP, 0, udp([]byte{192, 0, 0, 4}, []byte{192, 0, 2, 2})), dev.Index, 0) + if ret != 7 || netip.AddrFrom16([16]byte(got[8:24])) != clat6 || netip.AddrFrom16([16]byte(got[24:40])).String() != server || + l4Checksum(got[8:24], got[24:40], unix.IPPROTO_UDP, got[40:]) != 0 { + t.Fatalf("want UDP from %s to %s, got %d % x", clat6, server, ret, got) + } + // In: from the NAT64 to the CLAT address, back to 192.0.0.4 + src6, dst6 := netip.MustParseAddr(server).AsSlice(), clat6.AsSlice() + ret, got = run(v6Packet(server, clat6.String(), unix.IPPROTO_UDP, udp(src6, dst6)), wan.Index, 0) + if ret != 7 || netip.AddrFrom4([4]byte(got[12:16])).String() != "192.0.2.2" || netip.AddrFrom4([4]byte(got[16:20])) != clat4 || + l4Checksum(got[12:16], got[16:20], unix.IPPROTO_UDP, got[20:]) != 0 { + t.Fatalf("want UDP from 192.0.2.2 to 192.0.0.4, got %d % x", ret, got) + } + if !bytes.Equal(frame[:6], dev.HardwareAddr) { + t.Fatalf("the frame should be addressed to %s, got % x", clatDev, frame[:6]) + } + // A router on the path reports Packet Too Big about what the host sent: Fragmentation Needed + // from 192.0.0.8, 20 bytes smaller, about what 192.0.0.4 sent + quote := v6Packet(clat6.String(), server, unix.IPPROTO_UDP, udp(dst6, src6)) + ptb := append([]byte{2, 0, 0, 0, 0, 0, 0x05, 0x78}, quote...) // MTU 1400 + router := netip.MustParseAddr("2001:db8::1").AsSlice() + binary.BigEndian.PutUint16(ptb[2:], l4Checksum(router, dst6, unix.IPPROTO_ICMPV6, ptb)) + ret, got = run(v6Packet("2001:db8::1", clat6.String(), unix.IPPROTO_ICMPV6, ptb), wan.Index, 0) + if icmp := got[20:]; ret != 7 || netip.AddrFrom4([4]byte(got[12:16])) != clatDummy4 || icmp[0] != 3 || icmp[1] != 4 || + binary.BigEndian.Uint16(icmp[6:]) != 1380 || netip.AddrFrom4([4]byte(icmp[20:24])) != clat4 || l4Checksum(nil, nil, 0, icmp) != 0 { + t.Fatalf("want Fragmentation Needed for 1380 from 192.0.0.8, got %d % x", ret, got) + } + + // Left alone or dropped + ns := append([]byte{135, 0, 0, 0, 0, 0, 0, 0}, dst6...) + for name, c := range map[string]struct { + pkt []byte + ifindex int + want uint32 + }{ + "IPv6 for another address": {v6Packet(server, "2001:db8::2", unix.IPPROTO_UDP, udp(src6, src6)), wan.Index, 0}, + "IPv4 on the WAN": {v4Packet("192.0.2.2", "192.0.0.4", unix.IPPROTO_UDP, 0, udp(nil, nil)), wan.Index, 0}, + "a neighbour solicitation": {v6Packet("fe80::1", clat6.String(), unix.IPPROTO_ICMPV6, ns), wan.Index, 0}, + "an echo from outside the prefix": {v6Packet("2001:db8::1", clat6.String(), unix.IPPROTO_ICMPV6, []byte{128, 0, 0, 0, 0, 1, 0, 1}), wan.Index, 0}, + "IPv4 from another address": {v4Packet("10.0.0.1", "192.0.2.2", unix.IPPROTO_UDP, 0, udp(nil, nil)), dev.Index, 2}, + "the device's own IPv6": {v6Packet("fe80::1", "ff02::2", unix.IPPROTO_ICMPV6, []byte{133, 0, 0, 0, 0, 0, 0, 0}), dev.Index, 2}, + } { + if ret, _ := run(c.pkt, c.ifindex, 0); ret != c.want { + t.Errorf("%s: verdict %d, want %d", name, ret, c.want) + } + } + // What the slow path sends back goes on, either way, its mark cleared + for _, pkt := range [][]byte{v6Packet(clat6.String(), server, unix.IPPROTO_UDP, nil), v4Packet("192.0.2.2", "192.0.0.4", unix.IPPROTO_UDP, 0, nil)} { + ret, _, ctx := c.objs.testRun(t, ethFrame(pkt), skbCtx(dev.Index, bpfSlowMark)) + if ret != 7 || nativeEndian.Uint32(ctx[8:]) != 0 { + t.Errorf("a marked packet: verdict %d, mark %#x", ret, nativeEndian.Uint32(ctx[8:])) + } + } +} + +// A host with no IPv4 of its own reaches an IPv4-only server through its CLAT and the line's +// NAT64, sixup's own here, in a namespace that stands for the ISP. +func TestCLATAgainstANAT64(t *testing.T) { + if !ownNetns(t) { + return + } + ip := func(args ...string) { + t.Helper() + if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil { + t.Fatalf("ip %s: %v\n%s", strings.Join(args, " "), err, out) + } + } + nsOpen := func(name string) (string, int) { + ns := fmt.Sprintf("sx%d-%s", os.Getpid(), name) + ip("netns", "add", ns) + t.Cleanup(func() { exec.Command("ip", "netns", "del", ns).Run() }) + ip("-n", ns, "link", "set", "lo", "up") + fd, err := unix.Open("/var/run/netns/"+ns, unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { unix.Close(fd) }) + return ns, fd + } + isp, ispFD := nsOpen("isp") + srv, srvFD := nsOpen("srv") + ip("link", "add", "wan0", "type", "veth", "peer", "name", "eth0", "netns", isp) + ip("-n", isp, "link", "add", "up0", "type", "veth", "peer", "name", "eth0", "netns", srv) + ip("link", "set", "wan0", "up") + ip("addr", "add", "2001:db8:1::2/64", "dev", "wan0", "nodad") + ip("route", "add", "default", "via", "2001:db8:1::1") + ip("-n", isp, "link", "set", "eth0", "up") + ip("-n", isp, "link", "set", "up0", "up") + ip("-n", isp, "addr", "add", "2001:db8:1::1/64", "dev", "eth0", "nodad") + ip("-n", isp, "addr", "add", "192.0.2.1/24", "dev", "up0") + ip("-n", srv, "link", "set", "eth0", "up") + ip("-n", srv, "addr", "add", "192.0.2.2/24", "dev", "eth0") + ip("netns", "exec", isp, "sh", "-c", "echo 1 > /proc/sys/net/ipv6/conf/all/forwarding; echo 1 > /proc/sys/net/ipv4/ip_forward") + if out, err := exec.Command("ip", "netns", "exec", isp, "nft", "add table ip t; add chain ip t post { type nat hook postrouting priority 100; }; add rule ip t post oifname up0 masquerade").CombinedOutput(); err != nil { + t.Fatalf("nft: %v\n%s", err, out) + } + nat64 := &bpfNAT64{prefix: nat64WKP} + if err := inNetns(ispFD, nat64.start); err != nil { + t.Fatal(err) + } + defer inNetns(ispFD, func() error { nat64.stop(); return nil }) + + // The server echoes TCP and UDP + var tl net.Listener + var ul net.PacketConn + if err := inNetns(srvFD, func() (err error) { + if tl, err = net.Listen("tcp4", "192.0.2.2:7"); err != nil { + return err + } + ul, err = net.ListenPacket("udp4", "192.0.2.2:7") + return err + }); err != nil { + t.Fatal(err) + } + defer tl.Close() + defer ul.Close() + go func() { + for { + c, err := tl.Accept() + if err != nil { + return + } + go func() { io.Copy(c, c); c.Close() }() + } + }() + go func() { + buf := make([]byte, 4000) + for { + n, a, err := ul.ReadFrom(buf) + if err != nil { + return + } + ul.WriteTo(buf[:n], a) + } + }() + + // The line announces the NAT64's prefix; the WAN's address manager would add the CLAT address + var logged lockedBuffer + log.SetOutput(&logged) + defer log.SetOutput(os.Stderr) + bpfTick = 20 * time.Millisecond + defer func() { bpfTick = time.Minute }() + snap := Snapshot{PREF64: nat64WKP, WAN: []Prefix{{Prefix: netip.MustParsePrefix("2001:db8:1::/64"), Source: sourceRA, SLAAC: true, Valid: time.Now().Add(time.Hour)}}} + secret := []byte("secret") + a := snap.clatAddr(secret) + wan, _ := net.InterfaceByName("wan0") + if err := addrSet(wan.Index, a, 128, 0, 0, true, ifaFNodad); err != nil { + t.Fatal(err) + } + ch := make(chan Snapshot) + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan struct{}) + c := &clatManager{wan: "wan0", secret: secret, sysctl: true} + go func() { c.run(ctx, ch); close(done) }() + ch <- snap + ch <- snap // nothing new + ping := func(args ...string) ([]byte, error) { + return exec.Command("ping", append([]string{"-c", "1", "-W", "2"}, append(args, "192.0.2.2")...)...).CombinedOutput() + } + if out, err := ping(); err != nil { + t.Fatalf("ping through the CLAT: %v\n%s", err, out) + } + for path, want := range map[string]string{"/proc/sys/net/ipv4/conf/" + clatDev + "/rp_filter": "2", "/proc/sys/net/ipv6/conf/" + clatDev + "/disable_ipv6": "1"} { + if b, _ := os.ReadFile(path); strings.TrimSpace(string(b)) != want { + t.Errorf("%s = %q, want %s", path, b, want) + } + } + if out, _ := exec.Command("ip", "route", "show", "default").CombinedOutput(); !bytes.Contains(out, []byte("dev "+clatDev)) || !bytes.Contains(out, []byte("src 192.0.0.4 metric 4294967295")) { + t.Errorf("want a default route of last resort through %s:\n%s", clatDev, out) + } + + payload := bytes.Repeat([]byte("sixup clat "), 100000) + tc, err := net.DialTimeout("tcp4", "192.0.2.2:7", 3*time.Second) + if err != nil { + t.Fatal(err) + } + tc.SetDeadline(time.Now().Add(10 * time.Second)) + go func() { tc.Write(payload); tc.(*net.TCPConn).CloseWrite() }() + if got, err := io.ReadAll(tc); err != nil || !bytes.Equal(got, payload) { + t.Fatalf("TCP echoed %d bytes of %d: %v", len(got), len(payload), err) + } + tc.Close() + for _, size := range []int{5, 3000} { + uc, err := net.Dial("udp4", "192.0.2.2:7") + if err != nil { + t.Fatal(err) + } + uc.SetDeadline(time.Now().Add(2 * time.Second)) + uc.Write(make([]byte, size)) + buf := make([]byte, 4000) + if n, err := uc.Read(buf); err != nil || n != size { + t.Fatalf("UDP of %d bytes echoed %d: %v", size, n, err) + } + uc.Close() + } + // A fragmented ping takes the slow path out of the host and back into it + if out, err := ping("-s", "3000"); err != nil { + t.Fatalf("a fragmented ping: %v\n%s", err, out) + } + // The IPv4 path's MTU reaches the host through the NAT64, and that of the IPv6 path from the + // router that cannot forward + ip("-n", isp, "link", "set", "up0", "mtu", "1400") + ping("-s", "1400", "-M", "do") + if out, _ := exec.Command("ip", "route", "get", "192.0.2.2").CombinedOutput(); !bytes.Contains(out, []byte("mtu 1400")) { + t.Errorf("the host should have learnt an MTU of 1400:\n%s", out) + } + ip("-n", isp, "link", "set", joolOutside, "mtu", "1300") + ping("-s", "1300", "-M", "do") + if out, _ := exec.Command("ip", "route", "get", "192.0.2.2").CombinedOutput(); !bytes.Contains(out, []byte("mtu 1280")) { + t.Errorf("the host should have learnt an MTU of 1280 from the IPv6 path:\n%s", out) + } + // An IPv4 option the translation cannot carry is dropped and reported + ping("-R") + for deadline := time.Now().Add(2 * time.Second); !strings.Contains(logged.String(), "[clat] dropped so far: protocol 1"); { + if time.Now().After(deadline) { + t.Fatalf("the drop should be reported:\n%s", logged.String()) + } + time.Sleep(10 * time.Millisecond) + } + + // Without the PREF64 the CLAT goes, its device and route with it, and nothing is reported + ch <- Snapshot{} + time.Sleep(3 * bpfTick) + cancel() + <-done + if _, err := net.InterfaceByName(clatDev); err == nil { + t.Fatalf("%s should be gone", clatDev) + } + if !strings.Contains(logged.String(), "[clat] stopped") { + t.Errorf("the stop should be reported:\n%s", logged.String()) + } +} + +// The CLAT is built only on an Ethernet WAN that is there, and whatever fails part way, start +// leaves nothing behind. +func TestCLATWithoutItsNeeds(t *testing.T) { + if !ownNetns(t) { + return + } + var logged lockedBuffer + log.SetOutput(&logged) + defer log.SetOutput(os.Stderr) + snap := Snapshot{PREF64: nat64WKP, WAN: []Prefix{{Prefix: netip.MustParsePrefix("2001:db8:1::/64"), Source: sourceRA, SLAAC: true, Valid: time.Now().Add(time.Hour)}}} + loUp(t) + for _, wan := range []string{"lo", "absent0"} { + c := &clatManager{wan: wan} + c.apply(snap) + c.apply(snap) + if c.objs != nil { + t.Fatalf("%s: no CLAT should be built", wan) + } + } + if n := strings.Count(logged.String(), "no Ethernet link"); n != 1 { + t.Fatalf("a WAN without Ethernet should be reported once, %d times:\n%s", n, logged.String()) + } + + wan, err := dummyAdd("clatwan0", 0) + if err != nil { + t.Fatal(err) + } + c := &clatManager{wan: "clatwan0", cfg: clatConfig{nat64WKP, netip.MustParseAddr("2001:db8::c1a7"), wan.Index, 1500}} + if err := c.start(); err != nil { + t.Fatal(err) + } + c.stop() + for extra := 0; ; extra++ { + if extra > 500 { + t.Fatal("start never succeeded") + } + err := joolWithFDs(t, extra, c.start) + if err == nil { + break + } + if _, err := net.InterfaceByName(clatDev); err == nil { + t.Fatalf("with room for %d descriptors start failed and left %s", extra, clatDev) + } + } + c.stop() + // A start that fails is reported: without the right to create the device + c = &clatManager{wan: "clatwan0", secret: []byte("s")} + if err := syscall.Setresuid(-1, 65534, -1); err != nil { + t.Fatal(err) + } + c.apply(snap) + if err := syscall.Setresuid(-1, 0, -1); err != nil { + t.Fatal(err) + } + if c.objs != nil || !strings.Contains(logged.String(), "cannot create "+clatDev) { + t.Fatalf("the failure should be reported:\n%s", logged.String()) + } +} + +// Two sixups end to end: one routes a LAN and runs -nat64 bpf, whose RA announces the PREF64; the +// other, behind it without -lan, learns the prefix from that RA alone and reaches an IPv4-only +// server through its CLAT, the first's NAT64 and the first's IPv4 route. +func TestPLATAndCLATAgainstKernel(t *testing.T) { + needTools(t, "ip", "ping") + if os.Geteuid() != 0 { + t.Skip("network namespaces need root") + } + ip := func(args ...string) { + t.Helper() + if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil { + t.Fatalf("ip %s: %v\n%s", strings.Join(args, " "), err, out) + } + } + nsAdd := func(name string) (string, int) { + ns := fmt.Sprintf("sx%d-%s", os.Getpid(), name) + ip("netns", "add", ns) + t.Cleanup(func() { exec.Command("ip", "netns", "del", ns).Run() }) + ip("-n", ns, "link", "set", "lo", "up") + fd, err := unix.Open("/var/run/netns/"+ns, unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { unix.Close(fd) }) + return ns, fd + } + link := func(ns, name, peerNS, peer string) { + ip("link", "add", name, "netns", ns, "type", "veth", "peer", "name", peer, "netns", peerNS) + ip("-n", ns, "link", "set", name, "up") + ip("-n", peerNS, "link", "set", peer, "up") + } + isp, _ := nsAdd("isp") + plat, _ := nsAdd("plat") + host, hostFD := nsAdd("host") + srv, srvFD := nsAdd("srv") + link(plat, "wan0", isp, "isp0") + link(plat, "lan0", host, "wan0") + link(plat, "v4up", srv, "eth0") + ip("-n", plat, "addr", "add", "192.0.2.1/24", "dev", "v4up") + ip("-n", srv, "addr", "add", "192.0.2.2/24", "dev", "eth0") + ip("-n", srv, "route", "add", bpfPool.String(), "via", "192.0.2.1") + + var tl net.Listener + if err := inNetns(srvFD, func() (err error) { + tl, err = net.Listen("tcp4", "192.0.2.2:7") + return err + }); err != nil { + t.Fatal(err) + } + defer tl.Close() + go func() { + for { + c, err := tl.Accept() + if err != nil { + return + } + go func() { io.Copy(c, c); c.Close() }() + } + }() + + up := &treeRouter{ns: plat, state: t.TempDir()} + up.args = []string{"-wan", "wan0", "-lan", "lan0", "-state-dir", up.state, "-tunnel-dev", "", + "-dhcp6c-mode", "off", "-wan-ra=false", "-routed-prefix", "2001:db8::/48", "-nat64", "bpf", "-ra-min", "3s", "-ra-max", "4s"} + treeStart(t, up) + down := &treeRouter{ns: host, state: t.TempDir()} + down.args = []string{"-wan", "wan0", "-state-dir", down.state, "-tunnel-dev", "", "-dhcp6c-mode", "off"} + treeStart(t, down) + + eventually(t, 30*time.Second, "the host reaches 192.0.2.2 through its CLAT", func() bool { + return exec.Command("ip", "netns", "exec", host, "ping", "-c", "1", "-W", "1", "192.0.2.2").Run() == nil + }) + payload := bytes.Repeat([]byte("plat and clat "), 50000) + if err := inNetns(hostFD, func() error { + c, err := net.DialTimeout("tcp4", "192.0.2.2:7", 3*time.Second) + if err != nil { + return err + } + defer c.Close() + c.SetDeadline(time.Now().Add(10 * time.Second)) + go func() { c.Write(payload); c.(*net.TCPConn).CloseWrite() }() + got, err := io.ReadAll(c) + if err == nil && !bytes.Equal(got, payload) { + err = fmt.Errorf("echoed %d bytes of %d", len(got), len(payload)) + } + return err + }); err != nil { + t.Fatalf("TCP through the CLAT: %v", err) + } + if out, err := exec.Command("ip", "netns", "exec", host, "ping", "-c", "1", "-W", "2", "-s", "3000", "192.0.2.2").CombinedOutput(); err != nil { + t.Fatalf("a fragmented ping through both slow paths: %v\n%s", err, out) + } + + treeStop(down) + if !strings.Contains(down.stderr.String(), "[clat] this host's IPv4 reaches the NAT64 prefix 64:ff9b::/96") { + t.Errorf("the host should report its CLAT:\n%s", down.stderr) + } + if out, _ := exec.Command("ip", "-n", host, "link", "show", clatDev).CombinedOutput(); !bytes.Contains(out, []byte("does not exist")) { + t.Errorf("%s should go with sixup:\n%s", clatDev, out) + } +} diff --git a/clat_stub.go b/clat_stub.go new file mode 100644 index 0000000..0b88946 --- /dev/null +++ b/clat_stub.go @@ -0,0 +1,15 @@ +//go:build !linux + +package main + +import "context" + +// clatManager builds a CLAT with BPF, which only Linux has; the other platforms build for +// -dry-run, which configures nothing. +type clatManager struct { + wan string + secret []byte + sysctl bool +} + +func (c *clatManager) run(ctx context.Context, _ <-chan Snapshot) { <-ctx.Done() } diff --git a/docs/recipes.ja.md b/docs/recipes.ja.md index fbe2785..44761c8 100644 --- a/docs/recipes.ja.md +++ b/docs/recipes.ja.md @@ -30,6 +30,7 @@ sudo sixup -wan eth0 -lan eth1 | [下流のルーター](#下流のルーター) | 上流のルーターに `-dhcp6s-mode stateless` | | [IPv6 シングルスタックまたは IPv6-mostly の LAN](#ipv6-シングルスタックまたは-ipv6-mostly-の-lan) | `-nat64 bpf` | | [LAN のホストの後ろにある別のネットワーク](#lan-のホストの後ろにある別のネットワーク) | `-ra-route`、`-source-filter=false` | +| [このマシンだけで IPv6 を使う](#このマシンだけで-ipv6-を使う) | `-lan` なし | | [コンテナで動かす](#コンテナで動かす) | `-no-sysctl`、`-state-dir` | すべてのオプションとその既定値は `sixup -h` で一覧できます。このページでは用途ごとに紹介します。 @@ -50,6 +51,7 @@ sudo sixup -wan eth0 -dry-run - **PPPoE。** PPP デバイスを WAN に指定します(`-wan ppp0`)。ポイントツーポイントの WAN にはアドレス解決がないので、共有する /64 に代理応答は要りません。RA がなければ、デフォルトルートはデバイスに向けます。RA は回線の小さい MTU、1492 を LAN に伝えるので、ホストは経路 MTU 探索に頼らずに済みます。別の値は `-ra-mtu` で指定します。 - **MAP-E。** DHCPv6 に MAP-E のオプションがあるとき、またはプレフィックスが日本の IPoE 事業者(v6プラス、BIGLOBE、OCN、NURO)のルール表に合うとき、sixup はトンネル `sixup-ipv4` を作り、共有 IPv4 アドレスを設定し、メトリックの大きい IPv4 デフォルトルートを追加します。さらに nftables のテーブル `inet sixup` で送信元ポートを回線のポートセットに収め、TCP MSS を調整します。 - **DS-Lite。** DHCPv6 が AFTR の名前を示すとき、sixup はそれを解決してトンネルを作ります。変換は AFTR が行うので、ここでは MSS の調整だけを設定します。 +- **NAT64 で IPv4 を提供する回線。** 上流の RA が PREF64(RFC 8781)を広告すると、sixup はルーター自身の IPv4 のために CLAT(RFC 6877)を動かします。デバイス `sixup-clat` を通り `192.0.0.4` を送信元とする最も優先度の低いデフォルトルートで、その通信は WAN 上のルーター専用の IPv6 アドレスに変換されて回線の NAT64 に送られます。ほかの IPv4 ルートはすべてこれより優先されます。Linux 6.6 とイーサネットの WAN が必要です。LAN のホストは自分の CLAT を使います。[IPv6 シングルスタックまたは IPv6-mostly の LAN](#ipv6-シングルスタックまたは-ipv6-mostly-の-lan) を参照してください。 - **DHCPv6 が記述しないトンネル**(固定の 4in6 トンネルなど)。トンネルの通信が WAN に届くと、sixup はそれをキャプチャして両端のアドレスと IPv4 アドレスを推定します。 ## インターネットから LAN のホストに届くようにする @@ -323,6 +325,16 @@ sudo sixup -wan eth0 -lan eth1 -ra-route 2001:db8:200::/64 -source-filter=false 送信元フィルタは、LAN から外へは LAN 自身のプレフィックスと委任したプレフィックスを送信元とする通信だけを通し、それ以外の送信元には ICMPv6 code 5 を返します(BCP 38)。無効にすると、ルーティングしているプレフィックスも外へ出られます。その場合、上流でもそのプレフィックスをこのルーターへルーティングする必要があります。`-ra-onlink=false` は逆の方向にさらに進め、ホスト同士の通信もルーターを経由させます。 +## このマシンだけで IPv6 を使う + +`-lan` を指定しないと、sixup は自分が動いているマシンだけを扱います。回線が割り当てる WAN のアドレスとデフォルトルートを取り、MAP-E や DS-Lite の回線では IPv4 のトンネルと送信元 NAT を、NAT64 で IPv4 を提供する回線では CLAT を用意するので、マシン自身が IPv6 と IPv4 の両方に届きます。何も転送せず、RA、DHCPv6 サーバー、NDP プロキシ、PCP サーバー、境界のファイアウォールも動かしません。マシン自身の通信のフィルタリングはお任せします。 + +```sh +sudo sixup -wan eth0 +``` + +回線がプレフィックスを委任し、RA がリンク上にプレフィックスを載せないときは、委任プレフィックスのうち ISP がリンク用に取っておかない最初の /64 からアドレスを取ります。sixup は WAN でのカーネル自身の RA 処理を止めます。そうしないと、カーネルが sixup とは別にアドレスとルートを設定してしまうからです。回線が渡す DNS サーバーはシステムのリゾルバには書き込まれません。`sixup -dry-run` で確認できます。`-nat64` は LAN が必要なので、指定すると sixup は起動しません。 + ## コンテナで動かす sixup にはホストのネットワークが必要で、`forwarding`、`accept_ra`、`proxy_ndp`、`proxy_delay` を自分で設定します。`privileged` を付けないコンテナのように `/proc/sys` が読み取り専用のときは、ホスト側で設定して `-no-sysctl` を付けます。forwarding を 1、LAN の `accept_ra` を 0、LAN の `proxy_ndp` を 1、`proxy_delay` を 0 にします。状態ディレクトリはボリュームに置いてください。そうしないと新しいコンテナは新しい DUID を持ち、ISP が別のプレフィックスを割り当てることがあります。[`docker-compose.yml`](../docker-compose.yml) に両方の例があります。 diff --git a/docs/recipes.md b/docs/recipes.md index 608a591..01bacb3 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -32,6 +32,7 @@ both. | [A router behind this one](#a-router-behind-this-one) | `-dhcp6s-mode stateless` on the upstream router | | [An IPv6-only or IPv6-mostly LAN](#an-ipv6-only-or-ipv6-mostly-lan) | `-nat64 bpf` | | [Another network behind a LAN host](#another-network-behind-a-lan-host) | `-ra-route`, `-source-filter=false` | +| [IPv6 for this machine alone](#ipv6-for-this-machine-alone) | no `-lan` | | [Running in a container](#running-in-a-container) | `-no-sysctl`, `-state-dir` | Every option, with its default, is listed by `sixup -h`; this page shows them by need. @@ -75,6 +76,11 @@ sudo sixup -wan eth0 -dry-run and clamps the TCP MSS. - **DS-Lite.** When DHCPv6 names the AFTR, sixup resolves it and builds the tunnel. The AFTR translates, so only the MSS clamp is installed here. +- **A line with NAT64 for IPv4.** When the upstream RA announces a PREF64 (RFC 8781), sixup runs + a CLAT (RFC 6877) for the router's own IPv4: a default route of last resort through the device + `sixup-clat`, from `192.0.0.4`, whose traffic it translates to an IPv6 address of its own on the + WAN and sends to the line's NAT64. Any other IPv4 route comes first. It needs Linux 6.6 and an + Ethernet WAN; LAN hosts run their own CLAT, as in [An IPv6-only or IPv6-mostly LAN](#an-ipv6-only-or-ipv6-mostly-lan). - **A tunnel DHCPv6 does not describe**, such as a static 4in6 tunnel. When tunnel traffic reaches the WAN, sixup captures it and infers the endpoints and the IPv4 address. @@ -472,6 +478,25 @@ other sources with ICMPv6 code 5 (BCP 38). Turning it off lets the routed prefix to be routed to this router upstream too. `-ra-onlink=false` goes further the other way: hosts then send even traffic for each other through the router. +## IPv6 for this machine alone + +Without `-lan`, sixup looks after the machine it runs on and nothing else. It takes the WAN +addresses and the default route the line hands out, and on a MAP-E or DS-Lite line the IPv4 +tunnel with its source NAT, or the CLAT on a line with NAT64 for IPv4, so the machine itself +reaches both IPv6 and IPv4. It forwards +nothing and runs no RA, DHCPv6 server, NDP proxy, PCP server or border firewall; filtering the +machine's own traffic is left to you. + +```sh +sudo sixup -wan eth0 +``` + +When the line delegates a prefix and its RA puts none on the link, the machine takes its address +in the first /64 of the delegation that the ISP does not keep for the link. sixup turns off the +kernel's own handling of the RA on the WAN, which would otherwise configure addresses and routes +beside it. The DNS servers the line hands out are not written to the system's resolver; `sixup +-dry-run` shows them. `-nat64` needs a LAN and is refused. + ## Running in a container sixup needs the host network, and sets `forwarding`, `accept_ra`, `proxy_ndp` and `proxy_delay` diff --git a/docs/recipes.zh.md b/docs/recipes.zh.md index a12b420..0990ffd 100644 --- a/docs/recipes.zh.md +++ b/docs/recipes.zh.md @@ -28,6 +28,7 @@ sudo sixup -wan eth0 -lan eth1 | [下级路由器](#下级路由器) | 上级路由器加 `-dhcp6s-mode stateless` | | [IPv6 单栈或 IPv6-mostly 内网](#ipv6-单栈或-ipv6-mostly-内网) | `-nat64 bpf` | | [内网主机后面的另一个网络](#内网主机后面的另一个网络) | `-ra-route`、`-source-filter=false` | +| [只给本机用 IPv6](#只给本机用-ipv6) | 不指定 `-lan` | | [在容器里运行](#在容器里运行) | `-no-sysctl`、`-state-dir` | 每个参数及其默认值都可以用 `sixup -h` 查看,本页按需求介绍它们的用法。 @@ -48,6 +49,7 @@ sudo sixup -wan eth0 -dry-run - **PPPoE。** 把 PPP 设备作为 WAN,即 `-wan ppp0`。点对点的 WAN 没有地址解析,共享的 /64 不需要代理;收不到 RA 时,默认路由直接指向这个设备。RA 会把线路较小的 MTU 1492 告诉内网,主机不必依赖路径 MTU 发现;`-ra-mtu` 可以指定其他值。 - **MAP-E。** DHCPv6 带有 MAP-E 选项,或者前缀符合日本 IPoE 运营商(v6plus、BIGLOBE、OCN、NURO)的规则表时,sixup 建立隧道 `sixup-ipv4`,配置共享的 IPv4 地址,经隧道添加一条 metric 较高的 IPv4 默认路由,并维护 nftables 表 `inet sixup`,把源端口限制在线路分到的端口集内,同时钳制 TCP MSS。 - **DS-Lite。** DHCPv6 给出 AFTR 名称时,sixup 解析它并建立隧道。转换由 AFTR 完成,这里只设置 MSS 钳制。 +- **用 NAT64 提供 IPv4 的线路。** 上游 RA 通告 PREF64(RFC 8781)时,sixup 为路由器自身的 IPv4 运行 CLAT(RFC 6877):经设备 `sixup-clat`、源地址为 `192.0.0.4` 的一条优先级最低的默认路由,其流量被转换为路由器在 WAN 上的一个专用 IPv6 地址发出,交给线路的 NAT64。其他 IPv4 路由都优先于它。需要 Linux 6.6 和以太网 WAN;内网主机运行自己的 CLAT,见 [IPv6 单栈或 IPv6-mostly 内网](#ipv6-单栈或-ipv6-mostly-内网)。 - **DHCPv6 没有描述的隧道**,例如固定的 4in6 隧道。隧道流量到达 WAN 时,sixup 抓包推断出两端地址和 IPv4 地址。 ## 让外部访问内网主机 @@ -321,6 +323,16 @@ sudo sixup -wan eth0 -lan eth1 -ra-route 2001:db8:200::/64 -source-filter=false 源地址过滤只让内网用自己的前缀和委派出去的前缀向外发包,其他源地址会收到 ICMPv6 code 5(BCP 38)。关闭它,这个路由过来的前缀就能出去;此时上游也要把这个前缀路由到本路由器。`-ra-onlink=false` 则是另一个方向的做法:主机之间的流量也都经过路由器。 +## 只给本机用 IPv6 + +不指定 `-lan` 时,sixup 只管理它所在的这台机器。它取得线路分配的 WAN 地址和默认路由;在 MAP-E 或 DS-Lite 线路上,还会建立 IPv4 隧道和源地址转换,在用 NAT64 提供 IPv4 的线路上则运行 CLAT,所以本机既能用 IPv6,也能用 IPv4。它不转发任何流量,也不运行 RA、DHCPv6 服务器、NDP 代理、PCP 服务器或边界防火墙;本机自身流量的过滤由你自己负责。 + +```sh +sudo sixup -wan eth0 +``` + +如果线路委派了前缀,而 RA 没有在链路上给出前缀,本机就从委派前缀中运营商没有留给链路的第一个 /64 里取地址。sixup 会关闭内核自己对 WAN 上 RA 的处理,否则内核会在它之外另行配置地址和路由。线路给出的 DNS 服务器不会写入系统的解析配置,可以用 `sixup -dry-run` 查看。`-nat64` 需要内网,指定时 sixup 拒绝启动。 + ## 在容器里运行 sixup 需要使用主机网络,并自己设置 `forwarding`、`accept_ra`、`proxy_ndp` 和 `proxy_delay`。`/proc/sys` 只读时,比如容器没有 `privileged`,就在主机上设置这些值并加上 `-no-sysctl`:forwarding 为 1,内网的 `accept_ra` 为 0,内网的 `proxy_ndp` 为 1、`proxy_delay` 为 0。状态目录要放在卷上,否则新容器会得到新的 DUID,运营商可能分配另一个前缀。[`docker-compose.yml`](../docker-compose.yml) 演示了这两点。 diff --git a/docs/standards.md b/docs/standards.md index 792a7ef..5a77877 100644 --- a/docs/standards.md +++ b/docs/standards.md @@ -463,6 +463,9 @@ Tested by `TestNATPMP` and `TestPCPServerAgainstKernel`. - **RFC 8781, PREF64 in the RA.** Advertised while the NAT64 translates, with a lifetime of 3 × MaxRtrAdvInterval, and withdrawn with lifetime 0. - **RFC 6877, 464XLAT.** The CLAT is on the clients; PREF64 is what lets them find the prefix. + For the router's own IPv4, sixup runs one itself when the upstream announces a PREF64, in BPF, + from `192.0.0.4` (RFC 7335), with errors from the IPv6 path coming from `192.0.0.8` + (RFC 7600). - **RFC 8925, IPv6-Only Preferred.** A DHCPv4 option, left to the DHCPv4 server, since sixup runs none. diff --git a/flags.go b/flags.go index 926a5d5..04864fd 100644 --- a/flags.go +++ b/flags.go @@ -70,7 +70,7 @@ var ( // The options that need the address of an existing variable, or that accumulate repeated values. func init() { flag.BoolVar(&dryRun, "dry-run", false, "trial run: walk through the whole flow of requesting parameters from upstream, renewing and identifying tunnel parameters, printing continuously, but change no system configuration and send no RA to the LAN") - flag.Var(&lans, "lan", "LAN interface, repeatable, format name[:subnet-id], e.g. eth1:0; the id is the one this /64 takes out of the delegated prefix and has nothing to do with an IPv4 alias label of the same shape, which the kernel does not accept as an interface name anyway") + flag.Var(&lans, "lan", "LAN interface, repeatable, format name[:subnet-id], e.g. eth1:0; without any, sixup configures this host alone and forwards nothing; the id is the one this /64 takes out of the delegated prefix and has nothing to do with an IPv4 alias label of the same shape, which the kernel does not accept as an interface name anyway") flag.Var(&statics, "dhcp6s-static", "DHCPv6 static binding, repeatable: mac=..,addr=::100 or duid=hex,addr=2001:db8::5") flag.Var(&routes, "ra-route", "Route Information carried in the RA, repeatable") flag.Var(&ndStatic, "ndproxy-static", "static address or prefix for the NDP proxy, repeatable") @@ -175,7 +175,7 @@ var usageGroups = []struct { func printUsage() { w := flag.CommandLine.Output() - fmt.Fprintf(w, "usage: %s -wan -lan [options]\n", filepath.Base(os.Args[0])) + fmt.Fprintf(w, "usage: %s -wan [-lan ] [options]\n", filepath.Base(os.Args[0])) fmt.Fprintf(w, " %s -wan -dry-run\n", filepath.Base(os.Args[0])) listed := map[string]bool{} printFlag := func(f *flag.Flag) { diff --git a/main.go b/main.go index 6e3b0e0..2793952 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "runtime" "slices" "strconv" + "sync" "syscall" "time" ) @@ -80,12 +81,16 @@ func main() { if runtime.GOOS != "linux" && !dryRun { fatalf("only -dry-run is supported on %s: configuring addresses, routes, sysctls and tunnel devices needs Linux netlink, so run for real on Linux", runtime.GOOS) } - if len(lans) == 0 && !dryRun { - fatalf("at least one -lan is required") - } - if len(lans) == 0 { + // Without -lan sixup looks after this host alone: its WAN addresses and routes and the IPv4 + // tunnel, with no forwarding, no service to a LAN and no border firewall. A dry run still + // shows how a LAN would take the prefix. + if len(lans) == 0 && dryRun { lans = multiFlag{"lan"} } + hostOnly := len(lans) == 0 + if hostOnly && *nat64 != "off" { + fatalf("-nat64 translates for a LAN, and without -lan there is none") + } lanDefs := parseLans(lans) // The addresses configured on the WAN by hand are left as they are, and the first one is the // WAN address: no SLAAC or IA_NA address goes beside them @@ -280,7 +285,7 @@ func main() { go (&tunnelManager{dev: *tunDev, wan: *wan, mtu: *tunMTU, metric4: uint32(*tunMetric4), store: store}).run(ctx, store.Subscribe()) // A tunnel carries IPv4, which the kernel will not forward on a system that has never been a // router; the IPv6 switches alone leave the tunnel built and the LAN unable to use it. - if !*noSysctl && !dryRun { + if !*noSysctl && !hostOnly { if err := sysctlWrite("/proc/sys/net/ipv4/ip_forward", "1"); err != nil { warnf("[sysctl] ipv4/ip_forward=1 failed: %v", err) } @@ -294,6 +299,8 @@ func main() { for _, l := range lanDefs { names = append(names, l.iface) } + // The BPF translators leave devices and routes behind them, which have to go before sixup does + var teardown sync.WaitGroup if *nat64 == "jool" { // Jool's IPv4 output comes back through the veth and is forwarded out of this namespace if !*noSysctl { @@ -310,12 +317,16 @@ func main() { warnf("[sysctl] ipv4/ip_forward=1 failed: %v", err) } } - go (&bpfNAT64{prefix: nat64Pfx, lans: names, store: store}).run(ctx) + teardown.Go(func() { (&bpfNAT64{prefix: nat64Pfx, lans: names, store: store}).run(ctx) }) } - // The border of RFC 7084 is kept in every mode; allow only lets unsolicited traffic in - fw := &firewall{wan: *wan, lans: names, inbound: *unsolicited != "allow", source: *srcFilter, nat64: *nat64 != "off", holeIn: make(chan []portMapping, 1), delegIn: make(chan []netip.Prefix, 1)} - go fw.run(ctx, store.Subscribe()) - if *unsolicited != "deny" { + // The border of RFC 7084 is kept in every mode; allow only lets unsolicited traffic in. A host + // forwards nothing for it to filter. + var fw *firewall + if !hostOnly { + fw = &firewall{wan: *wan, lans: names, inbound: *unsolicited != "allow", source: *srcFilter, nat64: *nat64 != "off", holeIn: make(chan []portMapping, 1), delegIn: make(chan []netip.Prefix, 1)} + go fw.run(ctx, store.Subscribe()) + } + if *unsolicited != "deny" && !hostOnly { p := &pcpServer{lans: names, nat: nat} if *unsolicited == "request" { p.fw = fw // IPv6 mappings are the filter's pinholes @@ -339,10 +350,12 @@ func main() { // without SLAAC it still takes a /128 of the delegation and the tunnel endpoints wcfg := tcfg wcfg.enabled = *tEnable - go (&addrManager{ifname: *wan, secret: secret, cfg: wcfg, iids: iids, pick: Snapshot.wanStatic, tempPick: Snapshot.wanTemp, side: sideWAN, layout: layout, extra: Snapshot.tunnelEndpoints}).run(ctx, hub, store, store.Subscribe()) + go (&addrManager{ifname: *wan, secret: secret, cfg: wcfg, iids: iids, pick: Snapshot.wanStatic, tempPick: Snapshot.wanTemp, side: sideWAN, layout: layout, extra: wanEndpoints(secret)}).run(ctx, hub, store, store.Subscribe()) + // While the line announces a PREF64, this host's own IPv4 has a way out through its NAT64 + teardown.Go(func() { (&clatManager{wan: *wan, secret: secret, sysctl: !*noSysctl}).run(ctx, store.Subscribe()) }) poolStart, poolEnd := parsePool(*poolRange) var pd *pdPool - if *srvMode != "off" && !pdOff { + if *srvMode != "off" && !pdOff && !hostOnly { pd = newPDPool(srvPD, filepath.Join(*stateDir, "pd-leases.json"), fw) } for _, l := range lanDefs { @@ -368,7 +381,7 @@ func main() { } } var proxy *ndProxy - if *ndMode != "off" { + if *ndMode != "off" && !hostOnly { proxy = &ndProxy{mode: proxyMode(*ndMode), wanIf: *wan, wanPkts: pkts, lanIf: lanDefs[0].iface, ttl: ndProxyTTL, layout: layout} for _, s := range ndStatic { proxy.static = append(proxy.static, parsePrefixOrAddr(s)) @@ -380,12 +393,16 @@ func main() { } infof("[sixup] version %s", version) + if hostOnly { + infof("[sixup] no -lan: configuring this host only, forwarding nothing") + } infof("[sixup] starting: wan=%s lan=%v dhcpv6-client=%s dhcpv6-server=%s ndp-proxy=%s tempaddr=%t", *wan, lans, *dhcpMode, *srvMode, *ndMode, *tEnable) <-ctx.Done() infof("[sixup] got a termination signal, advertising RA with lifetime=0 and releasing the DHCPv6 bindings before exit") if dhcp != nil { dhcp.WaitDone() } + teardown.Wait() time.Sleep(500 * time.Millisecond) } diff --git a/main_integration_test.go b/main_integration_test.go index 12ba9e0..11e86af 100644 --- a/main_integration_test.go +++ b/main_integration_test.go @@ -13,6 +13,8 @@ import ( "syscall" "testing" "time" + + "github.com/google/nftables" ) // The daemon as a whole on a static prefix: it claims the interfaces, sets the sysctls, brings the @@ -119,6 +121,70 @@ func TestMainAgainstKernel(t *testing.T) { } } +// Without -lan the daemon numbers this host alone: the WAN takes an address of the routed prefix, +// the kernel no longer takes the RA itself, nothing is forwarded and no border firewall goes in. +func TestMainHostOnlyAgainstKernel(t *testing.T) { + if !ownNetns(t) { + return + } + loUp(t) + ns, err := os.Open("/proc/self/ns/net") + if err != nil { + t.Fatal(err) + } + defer ns.Close() + if err := vethAdd("wan-test0", "up-test0", int(ns.Fd())); err != nil { + t.Fatal(err) + } + wan, err := net.InterfaceByName("wan-test0") + if err != nil { + t.Fatal(err) + } + cmd, _, stderr := mainCmd(t, "-wan", "wan-test0", "-state-dir", t.TempDir(), + "-dhcp6c-mode", "off", "-wan-ra=false", "-routed-prefix", "2001:db8:1::/48", "-tunnel-dev", "") + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + want := netip.MustParsePrefix("2001:db8:1::/64") + numbered := func() bool { + list, _ := addrList(wan.Index) + return slices.ContainsFunc(list, func(ia ifAddr) bool { return want.Contains(ia.Addr) }) + } + for deadline := time.Now().Add(5 * time.Second); !numbered() && time.Now().Before(deadline); { + time.Sleep(50 * time.Millisecond) + } + if !numbered() { + t.Errorf("wan-test0 has no address in %s", want) + } + for path, val := range map[string]string{ + "/proc/sys/net/ipv6/conf/wan-test0/accept_ra": "0", + "/proc/sys/net/ipv6/conf/all/forwarding": "0", + "/proc/sys/net/ipv6/conf/wan-test0/forwarding": "0", + } { + if b, err := os.ReadFile(path); err != nil || strings.TrimSpace(string(b)) != val { + t.Errorf("%s = %q, %v; want %s", path, b, err, val) + } + } + c, err := nftables.New() + if err != nil { + t.Fatal(err) + } + tables, err := c.ListTables() + if err != nil { + t.Fatal(err) + } + if slices.ContainsFunc(tables, func(tb *nftables.Table) bool { return tb.Name == filterTable }) { + t.Error("a host should get no border firewall") + } + cmd.Process.Signal(syscall.SIGTERM) + if err := cmd.Wait(); err != nil { + t.Fatalf("%v:\n%s", err, stderr) + } + if !strings.Contains(stderr.String(), "configuring this host only") { + t.Errorf("the host mode should be reported:\n%s", stderr) + } +} + // A DHCPv6 server without the DHCPv6 client takes its DUID once the WAN appears, and gives up // waiting when sixup stops first. func TestMainServerAwaitsTheWANAgainstKernel(t *testing.T) { diff --git a/main_test.go b/main_test.go index 9cd81de..c94c468 100644 --- a/main_test.go +++ b/main_test.go @@ -95,16 +95,16 @@ func TestMainVersionAndLicense(t *testing.T) { } func TestMainRejectsBadOptions(t *testing.T) { - daemon := "at least one -lan is required" + hostNAT64 := "without -lan there is none" if runtime.GOOS != "linux" { - daemon = "only -dry-run is supported" + hostNAT64 = "only -dry-run is supported" } for _, c := range []struct { args []string want string }{ {nil, "-wan is required"}, - {[]string{"-wan", "x"}, daemon}, + {[]string{"-wan", "x", "-nat64", "bpf"}, hostNAT64}, {[]string{"-wan", "x", "-log-level", "loud"}, "-log-level must be"}, {[]string{"-dry-run", "-wan", "x", "-wan-prefix", "fd00::/64"}, "-wan-prefix:"}, {[]string{"-dry-run", "-wan", "x", "-wan-prefix", "2001:db8::/48"}, "-routed-prefix"}, diff --git a/nat64_bpf.go b/nat64_bpf.go index e04989b..2b43647 100644 --- a/nat64_bpf.go +++ b/nat64_bpf.go @@ -30,9 +30,9 @@ var nat64Maps = []bpfMapSpec{ // Where each variable sits in the read-only data, and its size var ( nat64VarSlowMark = bpfVar{off: 0, size: 4} - nat64VarPoolBase = bpfVar{off: 4, size: 4} - nat64VarPoolMask = bpfVar{off: 8, size: 4} - nat64VarPref6 = bpfVar{off: 12, size: 16} - nat64VarMask6 = bpfVar{off: 28, size: 16} - nat64VarV4pos = bpfVar{off: 44, size: 4} + nat64VarPref6 = bpfVar{off: 4, size: 16} + nat64VarMask6 = bpfVar{off: 20, size: 16} + nat64VarV4pos = bpfVar{off: 36, size: 4} + nat64VarPoolBase = bpfVar{off: 40, size: 4} + nat64VarPoolMask = bpfVar{off: 44, size: 4} ) diff --git a/nat64_bpfeb.bin b/nat64_bpfeb.bin index 5a3ec4b..2a3f42b 100644 Binary files a/nat64_bpfeb.bin and b/nat64_bpfeb.bin differ diff --git a/nat64_bpfeb.go b/nat64_bpfeb.go index 87d8077..f4bdc23 100644 --- a/nat64_bpfeb.go +++ b/nat64_bpfeb.go @@ -15,141 +15,206 @@ var nat64Relocs = []bpfReloc{ {insn: 1, m: nat64MapRodata, off: 0}, {insn: 26, m: nat64MapStats, off: -1}, {insn: 46, m: nat64MapStats, off: -1}, - {insn: 51, m: nat64MapRodata, off: 28}, - {insn: 58, m: nat64MapRodata, off: 12}, - {insn: 68, m: nat64MapRodata, off: 28}, - {insn: 74, m: nat64MapRodata, off: 12}, - {insn: 84, m: nat64MapRodata, off: 28}, - {insn: 90, m: nat64MapRodata, off: 12}, - {insn: 100, m: nat64MapRodata, off: 28}, - {insn: 106, m: nat64MapRodata, off: 12}, - {insn: 116, m: nat64MapRodata, off: 28}, - {insn: 122, m: nat64MapRodata, off: 12}, - {insn: 132, m: nat64MapRodata, off: 28}, - {insn: 138, m: nat64MapRodata, off: 12}, - {insn: 148, m: nat64MapRodata, off: 28}, - {insn: 154, m: nat64MapRodata, off: 12}, - {insn: 164, m: nat64MapRodata, off: 28}, - {insn: 170, m: nat64MapRodata, off: 12}, + {insn: 51, m: nat64MapRodata, off: 20}, + {insn: 58, m: nat64MapRodata, off: 4}, + {insn: 68, m: nat64MapRodata, off: 20}, + {insn: 74, m: nat64MapRodata, off: 4}, + {insn: 84, m: nat64MapRodata, off: 20}, + {insn: 90, m: nat64MapRodata, off: 4}, + {insn: 100, m: nat64MapRodata, off: 20}, + {insn: 106, m: nat64MapRodata, off: 4}, + {insn: 116, m: nat64MapRodata, off: 20}, + {insn: 122, m: nat64MapRodata, off: 4}, + {insn: 132, m: nat64MapRodata, off: 20}, + {insn: 138, m: nat64MapRodata, off: 4}, + {insn: 148, m: nat64MapRodata, off: 20}, + {insn: 154, m: nat64MapRodata, off: 4}, + {insn: 164, m: nat64MapRodata, off: 20}, + {insn: 170, m: nat64MapRodata, off: 4}, {insn: 204, m: nat64MapStats, off: -1}, - {insn: 210, m: nat64MapRodata, off: 8}, - {insn: 217, m: nat64MapRodata, off: 4}, + {insn: 210, m: nat64MapRodata, off: 44}, + {insn: 217, m: nat64MapRodata, off: 40}, {insn: 228, m: nat64MapStats, off: -1}, {insn: 243, m: nat64MapStats, off: -1}, {insn: 254, m: nat64MapStats, off: -1}, {insn: 269, m: nat64MapSlowBuf, off: -1}, {insn: 280, m: nat64MapStats, off: -1}, - {insn: 309, m: nat64MapStats, off: -1}, - {insn: 324, m: nat64MapSlow, off: -1}, - {insn: 335, m: nat64MapStats, off: -1}, - {insn: 342, m: nat64MapSlowFrags, off: -1}, - {insn: 351, m: nat64MapSlowBuf, off: -1}, - {insn: 362, m: nat64MapStats, off: -1}, - {insn: 377, m: nat64MapSlow, off: -1}, - {insn: 388, m: nat64MapStats, off: -1}, - {insn: 403, m: nat64MapSlowFrags, off: -1}, - {insn: 412, m: nat64MapSlowBuf, off: -1}, - {insn: 424, m: nat64MapStats, off: -1}, - {insn: 432, m: nat64MapV6Of, off: -1}, - {insn: 440, m: nat64MapStats, off: -1}, - {insn: 470, m: nat64MapRodata, off: 12}, - {insn: 509, m: nat64MapRodata, off: 44}, - {insn: 557, m: nat64MapStats, off: -1}, - {insn: 596, m: nat64MapStats, off: -1}, - {insn: 610, m: nat64MapStats, off: -1}, - {insn: 619, m: nat64MapStats, off: -1}, - {insn: 646, m: nat64MapStats, off: -1}, - {insn: 697, m: nat64MapStats, off: -1}, - {insn: 711, m: nat64MapSlow, off: -1}, - {insn: 722, m: nat64MapStats, off: -1}, - {insn: 743, m: nat64MapStats, off: -1}, - {insn: 808, m: nat64MapStats, off: -1}, - {insn: 849, m: nat64MapStats, off: -1}, - {insn: 898, m: nat64MapStats, off: -1}, - {insn: 911, m: nat64MapStats, off: -1}, - {insn: 944, m: nat64MapStats, off: -1}, - {insn: 966, m: nat64MapStats, off: -1}, - {insn: 982, m: nat64MapV6Of, off: -1}, - {insn: 990, m: nat64MapStats, off: -1}, - {insn: 1036, m: nat64MapRodata, off: 12}, - {insn: 1073, m: nat64MapRodata, off: 44}, - {insn: 1218, m: nat64MapStats, off: -1}, - {insn: 1237, m: nat64MapStats, off: -1}, - {insn: 1246, m: nat64MapStats, off: -1}, - {insn: 1287, m: nat64MapSlowBuf, off: -1}, - {insn: 1299, m: nat64MapStats, off: -1}, - {insn: 1332, m: nat64MapStats, off: -1}, - {insn: 1344, m: nat64MapStats, off: -1}, - {insn: 1351, m: nat64MapV4Of, off: -1}, - {insn: 1367, m: nat64MapSlow, off: -1}, - {insn: 1378, m: nat64MapStats, off: -1}, - {insn: 1402, m: nat64MapStats, off: -1}, - {insn: 1411, m: nat64MapStats, off: -1}, - {insn: 1420, m: nat64MapStats, off: -1}, - {insn: 1433, m: nat64MapStats, off: -1}, - {insn: 1456, m: nat64MapRodata, off: 4}, - {insn: 1459, m: nat64MapRodata, off: 8}, - {insn: 1467, m: nat64MapV6Of, off: -1}, - {insn: 1475, m: nat64MapV4Of, off: -1}, - {insn: 1484, m: nat64MapV6Of, off: -1}, - {insn: 1487, m: nat64MapV4Of, off: -1}, - {insn: 1518, m: nat64MapRodata, off: 44}, - {insn: 1586, m: nat64MapStats, off: -1}, - {insn: 1591, m: nat64MapRodata, off: 4}, - {insn: 1596, m: nat64MapRodata, off: 8}, - {insn: 1604, m: nat64MapV6Of, off: -1}, - {insn: 1610, m: nat64MapRodata, off: 4}, - {insn: 1615, m: nat64MapRodata, off: 8}, - {insn: 1623, m: nat64MapV6Of, off: -1}, - {insn: 1629, m: nat64MapRodata, off: 4}, - {insn: 1634, m: nat64MapRodata, off: 8}, - {insn: 1642, m: nat64MapV6Of, off: -1}, - {insn: 1648, m: nat64MapRodata, off: 4}, - {insn: 1653, m: nat64MapRodata, off: 8}, - {insn: 1661, m: nat64MapV6Of, off: -1}, - {insn: 1667, m: nat64MapRodata, off: 4}, - {insn: 1672, m: nat64MapRodata, off: 8}, - {insn: 1680, m: nat64MapV6Of, off: -1}, - {insn: 1686, m: nat64MapRodata, off: 4}, - {insn: 1691, m: nat64MapRodata, off: 8}, - {insn: 1699, m: nat64MapV6Of, off: -1}, - {insn: 1705, m: nat64MapRodata, off: 4}, - {insn: 1709, m: nat64MapRodata, off: 8}, - {insn: 1717, m: nat64MapV6Of, off: -1}, - {insn: 1727, m: nat64MapStats, off: -1}, - {insn: 1743, m: nat64MapStats, off: -1}, - {insn: 1770, m: nat64MapStats, off: -1}, - {insn: 1800, m: nat64MapStats, off: -1}, + {insn: 310, m: nat64MapStats, off: -1}, + {insn: 325, m: nat64MapSlow, off: -1}, + {insn: 336, m: nat64MapStats, off: -1}, + {insn: 343, m: nat64MapSlowFrags, off: -1}, + {insn: 353, m: nat64MapSlowBuf, off: -1}, + {insn: 364, m: nat64MapStats, off: -1}, + {insn: 379, m: nat64MapSlow, off: -1}, + {insn: 390, m: nat64MapStats, off: -1}, + {insn: 405, m: nat64MapSlowFrags, off: -1}, + {insn: 414, m: nat64MapSlowBuf, off: -1}, + {insn: 426, m: nat64MapStats, off: -1}, + {insn: 435, m: nat64MapStats, off: -1}, + {insn: 442, m: nat64MapRodata, off: 44}, + {insn: 449, m: nat64MapRodata, off: 40}, + {insn: 453, m: nat64MapRodata, off: 4}, + {insn: 488, m: nat64MapRodata, off: 36}, + {insn: 520, m: nat64MapV6Of, off: -1}, + {insn: 537, m: nat64MapRodata, off: 44}, + {insn: 543, m: nat64MapRodata, off: 40}, + {insn: 549, m: nat64MapRodata, off: 4}, + {insn: 583, m: nat64MapRodata, off: 36}, + {insn: 615, m: nat64MapV6Of, off: -1}, + {insn: 680, m: nat64MapStats, off: -1}, + {insn: 689, m: nat64MapStats, off: -1}, + {insn: 728, m: nat64MapStats, off: -1}, + {insn: 742, m: nat64MapStats, off: -1}, + {insn: 769, m: nat64MapStats, off: -1}, + {insn: 821, m: nat64MapStats, off: -1}, + {insn: 835, m: nat64MapSlow, off: -1}, + {insn: 846, m: nat64MapStats, off: -1}, + {insn: 866, m: nat64MapStats, off: -1}, + {insn: 932, m: nat64MapStats, off: -1}, + {insn: 973, m: nat64MapStats, off: -1}, + {insn: 1022, m: nat64MapStats, off: -1}, + {insn: 1035, m: nat64MapStats, off: -1}, + {insn: 1069, m: nat64MapStats, off: -1}, + {insn: 1089, m: nat64MapStats, off: -1}, + {insn: 1105, m: nat64MapRodata, off: 44}, + {insn: 1111, m: nat64MapRodata, off: 40}, + {insn: 1115, m: nat64MapRodata, off: 4}, + {insn: 1149, m: nat64MapRodata, off: 36}, + {insn: 1181, m: nat64MapV6Of, off: -1}, + {insn: 1197, m: nat64MapRodata, off: 44}, + {insn: 1203, m: nat64MapRodata, off: 40}, + {insn: 1207, m: nat64MapRodata, off: 4}, + {insn: 1241, m: nat64MapRodata, off: 36}, + {insn: 1273, m: nat64MapV6Of, off: -1}, + {insn: 1436, m: nat64MapStats, off: -1}, + {insn: 1449, m: nat64MapStats, off: -1}, + {insn: 1464, m: nat64MapStats, off: -1}, + {insn: 1502, m: nat64MapStats, off: -1}, + {insn: 1514, m: nat64MapSlowBuf, off: -1}, + {insn: 1526, m: nat64MapStats, off: -1}, + {insn: 1559, m: nat64MapStats, off: -1}, + {insn: 1571, m: nat64MapStats, off: -1}, + {insn: 1576, m: nat64MapRodata, off: 20}, + {insn: 1585, m: nat64MapRodata, off: 4}, + {insn: 1595, m: nat64MapRodata, off: 20}, + {insn: 1601, m: nat64MapRodata, off: 4}, + {insn: 1611, m: nat64MapRodata, off: 20}, + {insn: 1617, m: nat64MapRodata, off: 4}, + {insn: 1627, m: nat64MapRodata, off: 20}, + {insn: 1633, m: nat64MapRodata, off: 4}, + {insn: 1643, m: nat64MapRodata, off: 20}, + {insn: 1649, m: nat64MapRodata, off: 4}, + {insn: 1659, m: nat64MapRodata, off: 20}, + {insn: 1665, m: nat64MapRodata, off: 4}, + {insn: 1675, m: nat64MapRodata, off: 20}, + {insn: 1681, m: nat64MapRodata, off: 4}, + {insn: 1691, m: nat64MapRodata, off: 20}, + {insn: 1697, m: nat64MapRodata, off: 4}, + {insn: 1707, m: nat64MapRodata, off: 36}, + {insn: 1737, m: nat64MapV4Of, off: -1}, + {insn: 1753, m: nat64MapSlow, off: -1}, + {insn: 1764, m: nat64MapStats, off: -1}, + {insn: 1788, m: nat64MapStats, off: -1}, + {insn: 1797, m: nat64MapStats, off: -1}, + {insn: 1806, m: nat64MapStats, off: -1}, {insn: 1819, m: nat64MapStats, off: -1}, - {insn: 1994, m: nat64MapStats, off: -1}, - {insn: 2017, m: nat64MapStats, off: -1}, - {insn: 2026, m: nat64MapStats, off: -1}, - {insn: 2067, m: nat64MapStats, off: -1}, - {insn: 2095, m: nat64MapStats, off: -1}, - {insn: 2122, m: nat64MapStats, off: -1}, - {insn: 2131, m: nat64MapRodata, off: 28}, - {insn: 2137, m: nat64MapRodata, off: 12}, - {insn: 2147, m: nat64MapRodata, off: 28}, - {insn: 2153, m: nat64MapRodata, off: 12}, - {insn: 2163, m: nat64MapRodata, off: 28}, - {insn: 2169, m: nat64MapRodata, off: 12}, - {insn: 2179, m: nat64MapRodata, off: 28}, - {insn: 2185, m: nat64MapRodata, off: 12}, - {insn: 2195, m: nat64MapRodata, off: 28}, - {insn: 2201, m: nat64MapRodata, off: 12}, - {insn: 2211, m: nat64MapRodata, off: 28}, - {insn: 2217, m: nat64MapRodata, off: 12}, - {insn: 2227, m: nat64MapRodata, off: 28}, - {insn: 2233, m: nat64MapRodata, off: 12}, - {insn: 2243, m: nat64MapRodata, off: 28}, - {insn: 2249, m: nat64MapRodata, off: 12}, - {insn: 2261, m: nat64MapV4Of, off: -1}, - {insn: 2299, m: nat64MapRodata, off: 44}, - {insn: 2424, m: nat64MapStats, off: -1}, - {insn: 2433, m: nat64MapStats, off: -1}, - {insn: 2465, m: nat64MapStats, off: -1}, - {insn: 2474, m: nat64MapStats, off: -1}, - {insn: 2516, m: nat64MapStats, off: -1}, - {insn: 2525, m: nat64MapStats, off: -1}, - {insn: 2534, m: nat64MapStats, off: -1}, + {insn: 1842, m: nat64MapRodata, off: 40}, + {insn: 1845, m: nat64MapRodata, off: 44}, + {insn: 1853, m: nat64MapV6Of, off: -1}, + {insn: 1861, m: nat64MapV4Of, off: -1}, + {insn: 1870, m: nat64MapV6Of, off: -1}, + {insn: 1873, m: nat64MapV4Of, off: -1}, + {insn: 1883, m: nat64MapRodata, off: 20}, + {insn: 1889, m: nat64MapRodata, off: 4}, + {insn: 1899, m: nat64MapRodata, off: 20}, + {insn: 1905, m: nat64MapRodata, off: 4}, + {insn: 1915, m: nat64MapRodata, off: 20}, + {insn: 1921, m: nat64MapRodata, off: 4}, + {insn: 1931, m: nat64MapRodata, off: 20}, + {insn: 1937, m: nat64MapRodata, off: 4}, + {insn: 1947, m: nat64MapRodata, off: 20}, + {insn: 1953, m: nat64MapRodata, off: 4}, + {insn: 1963, m: nat64MapRodata, off: 20}, + {insn: 1969, m: nat64MapRodata, off: 4}, + {insn: 1979, m: nat64MapRodata, off: 20}, + {insn: 1985, m: nat64MapRodata, off: 4}, + {insn: 1995, m: nat64MapRodata, off: 20}, + {insn: 2001, m: nat64MapRodata, off: 4}, + {insn: 2011, m: nat64MapRodata, off: 36}, + {insn: 2041, m: nat64MapV4Of, off: -1}, + {insn: 2087, m: nat64MapStats, off: -1}, + {insn: 2136, m: nat64MapStats, off: -1}, + {insn: 2141, m: nat64MapRodata, off: 40}, + {insn: 2146, m: nat64MapRodata, off: 44}, + {insn: 2154, m: nat64MapV6Of, off: -1}, + {insn: 2160, m: nat64MapRodata, off: 40}, + {insn: 2165, m: nat64MapRodata, off: 44}, + {insn: 2173, m: nat64MapV6Of, off: -1}, + {insn: 2179, m: nat64MapRodata, off: 40}, + {insn: 2184, m: nat64MapRodata, off: 44}, + {insn: 2192, m: nat64MapV6Of, off: -1}, + {insn: 2198, m: nat64MapRodata, off: 40}, + {insn: 2203, m: nat64MapRodata, off: 44}, + {insn: 2211, m: nat64MapV6Of, off: -1}, + {insn: 2217, m: nat64MapRodata, off: 40}, + {insn: 2222, m: nat64MapRodata, off: 44}, + {insn: 2230, m: nat64MapV6Of, off: -1}, + {insn: 2236, m: nat64MapRodata, off: 40}, + {insn: 2241, m: nat64MapRodata, off: 44}, + {insn: 2249, m: nat64MapV6Of, off: -1}, + {insn: 2255, m: nat64MapRodata, off: 40}, + {insn: 2259, m: nat64MapRodata, off: 44}, + {insn: 2267, m: nat64MapV6Of, off: -1}, + {insn: 2277, m: nat64MapStats, off: -1}, + {insn: 2293, m: nat64MapStats, off: -1}, + {insn: 2320, m: nat64MapStats, off: -1}, + {insn: 2373, m: nat64MapStats, off: -1}, + {insn: 2521, m: nat64MapStats, off: -1}, + {insn: 2544, m: nat64MapStats, off: -1}, + {insn: 2553, m: nat64MapStats, off: -1}, + {insn: 2594, m: nat64MapStats, off: -1}, + {insn: 2622, m: nat64MapStats, off: -1}, + {insn: 2650, m: nat64MapStats, off: -1}, + {insn: 2659, m: nat64MapRodata, off: 20}, + {insn: 2667, m: nat64MapRodata, off: 4}, + {insn: 2677, m: nat64MapRodata, off: 20}, + {insn: 2683, m: nat64MapRodata, off: 4}, + {insn: 2693, m: nat64MapRodata, off: 20}, + {insn: 2699, m: nat64MapRodata, off: 4}, + {insn: 2709, m: nat64MapRodata, off: 20}, + {insn: 2715, m: nat64MapRodata, off: 4}, + {insn: 2725, m: nat64MapRodata, off: 20}, + {insn: 2731, m: nat64MapRodata, off: 4}, + {insn: 2741, m: nat64MapRodata, off: 20}, + {insn: 2747, m: nat64MapRodata, off: 4}, + {insn: 2757, m: nat64MapRodata, off: 20}, + {insn: 2763, m: nat64MapRodata, off: 4}, + {insn: 2773, m: nat64MapRodata, off: 20}, + {insn: 2779, m: nat64MapRodata, off: 4}, + {insn: 2789, m: nat64MapRodata, off: 36}, + {insn: 2818, m: nat64MapV4Of, off: -1}, + {insn: 2824, m: nat64MapRodata, off: 20}, + {insn: 2832, m: nat64MapRodata, off: 4}, + {insn: 2842, m: nat64MapRodata, off: 20}, + {insn: 2848, m: nat64MapRodata, off: 4}, + {insn: 2858, m: nat64MapRodata, off: 20}, + {insn: 2864, m: nat64MapRodata, off: 4}, + {insn: 2874, m: nat64MapRodata, off: 20}, + {insn: 2880, m: nat64MapRodata, off: 4}, + {insn: 2890, m: nat64MapRodata, off: 20}, + {insn: 2896, m: nat64MapRodata, off: 4}, + {insn: 2906, m: nat64MapRodata, off: 20}, + {insn: 2912, m: nat64MapRodata, off: 4}, + {insn: 2922, m: nat64MapRodata, off: 20}, + {insn: 2928, m: nat64MapRodata, off: 4}, + {insn: 2938, m: nat64MapRodata, off: 20}, + {insn: 2944, m: nat64MapRodata, off: 4}, + {insn: 2954, m: nat64MapRodata, off: 36}, + {insn: 2982, m: nat64MapV4Of, off: -1}, + {insn: 3119, m: nat64MapStats, off: -1}, + {insn: 3128, m: nat64MapStats, off: -1}, + {insn: 3161, m: nat64MapStats, off: -1}, + {insn: 3203, m: nat64MapStats, off: -1}, + {insn: 3212, m: nat64MapStats, off: -1}, + {insn: 3221, m: nat64MapStats, off: -1}, } diff --git a/nat64_bpfel.bin b/nat64_bpfel.bin index 52a7e1a..3a697ce 100644 Binary files a/nat64_bpfel.bin and b/nat64_bpfel.bin differ diff --git a/nat64_bpfel.go b/nat64_bpfel.go index fe999fd..9f31b35 100644 --- a/nat64_bpfel.go +++ b/nat64_bpfel.go @@ -15,34 +15,34 @@ var nat64Relocs = []bpfReloc{ {insn: 1, m: nat64MapRodata, off: 0}, {insn: 26, m: nat64MapStats, off: -1}, {insn: 46, m: nat64MapStats, off: -1}, - {insn: 51, m: nat64MapRodata, off: 28}, - {insn: 58, m: nat64MapRodata, off: 12}, - {insn: 68, m: nat64MapRodata, off: 28}, - {insn: 74, m: nat64MapRodata, off: 12}, - {insn: 84, m: nat64MapRodata, off: 28}, - {insn: 90, m: nat64MapRodata, off: 12}, - {insn: 100, m: nat64MapRodata, off: 28}, - {insn: 106, m: nat64MapRodata, off: 12}, - {insn: 116, m: nat64MapRodata, off: 28}, - {insn: 122, m: nat64MapRodata, off: 12}, - {insn: 132, m: nat64MapRodata, off: 28}, - {insn: 138, m: nat64MapRodata, off: 12}, - {insn: 148, m: nat64MapRodata, off: 28}, - {insn: 154, m: nat64MapRodata, off: 12}, - {insn: 164, m: nat64MapRodata, off: 28}, - {insn: 170, m: nat64MapRodata, off: 12}, - {insn: 206, m: nat64MapStats, off: -1}, - {insn: 211, m: nat64MapRodata, off: 8}, - {insn: 220, m: nat64MapRodata, off: 4}, - {insn: 231, m: nat64MapStats, off: -1}, - {insn: 246, m: nat64MapStats, off: -1}, - {insn: 258, m: nat64MapStats, off: -1}, - {insn: 274, m: nat64MapSlowBuf, off: -1}, - {insn: 285, m: nat64MapStats, off: -1}, - {insn: 314, m: nat64MapStats, off: -1}, - {insn: 329, m: nat64MapSlow, off: -1}, - {insn: 340, m: nat64MapStats, off: -1}, - {insn: 347, m: nat64MapSlowFrags, off: -1}, + {insn: 51, m: nat64MapRodata, off: 20}, + {insn: 58, m: nat64MapRodata, off: 4}, + {insn: 68, m: nat64MapRodata, off: 20}, + {insn: 74, m: nat64MapRodata, off: 4}, + {insn: 84, m: nat64MapRodata, off: 20}, + {insn: 90, m: nat64MapRodata, off: 4}, + {insn: 100, m: nat64MapRodata, off: 20}, + {insn: 106, m: nat64MapRodata, off: 4}, + {insn: 116, m: nat64MapRodata, off: 20}, + {insn: 122, m: nat64MapRodata, off: 4}, + {insn: 132, m: nat64MapRodata, off: 20}, + {insn: 138, m: nat64MapRodata, off: 4}, + {insn: 148, m: nat64MapRodata, off: 20}, + {insn: 154, m: nat64MapRodata, off: 4}, + {insn: 164, m: nat64MapRodata, off: 20}, + {insn: 170, m: nat64MapRodata, off: 4}, + {insn: 204, m: nat64MapStats, off: -1}, + {insn: 209, m: nat64MapRodata, off: 44}, + {insn: 218, m: nat64MapRodata, off: 40}, + {insn: 229, m: nat64MapStats, off: -1}, + {insn: 244, m: nat64MapStats, off: -1}, + {insn: 256, m: nat64MapStats, off: -1}, + {insn: 272, m: nat64MapSlowBuf, off: -1}, + {insn: 283, m: nat64MapStats, off: -1}, + {insn: 313, m: nat64MapStats, off: -1}, + {insn: 328, m: nat64MapSlow, off: -1}, + {insn: 339, m: nat64MapStats, off: -1}, + {insn: 346, m: nat64MapSlowFrags, off: -1}, {insn: 356, m: nat64MapSlowBuf, off: -1}, {insn: 367, m: nat64MapStats, off: -1}, {insn: 382, m: nat64MapSlow, off: -1}, @@ -50,100 +50,171 @@ var nat64Relocs = []bpfReloc{ {insn: 408, m: nat64MapSlowFrags, off: -1}, {insn: 417, m: nat64MapSlowBuf, off: -1}, {insn: 429, m: nat64MapStats, off: -1}, - {insn: 437, m: nat64MapV6Of, off: -1}, - {insn: 445, m: nat64MapStats, off: -1}, - {insn: 477, m: nat64MapRodata, off: 12}, - {insn: 516, m: nat64MapRodata, off: 44}, - {insn: 563, m: nat64MapStats, off: -1}, - {insn: 605, m: nat64MapStats, off: -1}, - {insn: 619, m: nat64MapStats, off: -1}, - {insn: 628, m: nat64MapStats, off: -1}, - {insn: 655, m: nat64MapStats, off: -1}, - {insn: 706, m: nat64MapStats, off: -1}, - {insn: 720, m: nat64MapSlow, off: -1}, - {insn: 731, m: nat64MapStats, off: -1}, - {insn: 752, m: nat64MapStats, off: -1}, - {insn: 820, m: nat64MapStats, off: -1}, - {insn: 861, m: nat64MapStats, off: -1}, - {insn: 912, m: nat64MapStats, off: -1}, - {insn: 925, m: nat64MapStats, off: -1}, - {insn: 958, m: nat64MapStats, off: -1}, - {insn: 980, m: nat64MapStats, off: -1}, - {insn: 996, m: nat64MapV6Of, off: -1}, - {insn: 1004, m: nat64MapStats, off: -1}, - {insn: 1050, m: nat64MapRodata, off: 12}, - {insn: 1087, m: nat64MapRodata, off: 44}, - {insn: 1239, m: nat64MapStats, off: -1}, - {insn: 1258, m: nat64MapStats, off: -1}, - {insn: 1267, m: nat64MapStats, off: -1}, - {insn: 1309, m: nat64MapSlowBuf, off: -1}, - {insn: 1321, m: nat64MapStats, off: -1}, - {insn: 1359, m: nat64MapStats, off: -1}, - {insn: 1371, m: nat64MapStats, off: -1}, - {insn: 1378, m: nat64MapV4Of, off: -1}, - {insn: 1394, m: nat64MapSlow, off: -1}, - {insn: 1405, m: nat64MapStats, off: -1}, - {insn: 1429, m: nat64MapStats, off: -1}, - {insn: 1438, m: nat64MapStats, off: -1}, - {insn: 1447, m: nat64MapStats, off: -1}, - {insn: 1460, m: nat64MapStats, off: -1}, - {insn: 1483, m: nat64MapRodata, off: 4}, - {insn: 1486, m: nat64MapRodata, off: 8}, - {insn: 1495, m: nat64MapV6Of, off: -1}, - {insn: 1503, m: nat64MapV4Of, off: -1}, - {insn: 1512, m: nat64MapV6Of, off: -1}, - {insn: 1515, m: nat64MapV4Of, off: -1}, - {insn: 1551, m: nat64MapRodata, off: 44}, - {insn: 1616, m: nat64MapStats, off: -1}, - {insn: 1621, m: nat64MapRodata, off: 4}, - {insn: 1633, m: nat64MapV6Of, off: -1}, - {insn: 1639, m: nat64MapRodata, off: 4}, - {insn: 1644, m: nat64MapRodata, off: 8}, - {insn: 1653, m: nat64MapV6Of, off: -1}, - {insn: 1662, m: nat64MapRodata, off: 8}, - {insn: 1671, m: nat64MapV6Of, off: -1}, - {insn: 1680, m: nat64MapRodata, off: 8}, - {insn: 1689, m: nat64MapV6Of, off: -1}, - {insn: 1698, m: nat64MapRodata, off: 8}, - {insn: 1707, m: nat64MapV6Of, off: -1}, - {insn: 1716, m: nat64MapRodata, off: 8}, - {insn: 1725, m: nat64MapV6Of, off: -1}, - {insn: 1733, m: nat64MapRodata, off: 8}, - {insn: 1742, m: nat64MapV6Of, off: -1}, - {insn: 1752, m: nat64MapStats, off: -1}, - {insn: 1768, m: nat64MapStats, off: -1}, - {insn: 1795, m: nat64MapStats, off: -1}, - {insn: 1827, m: nat64MapStats, off: -1}, + {insn: 438, m: nat64MapStats, off: -1}, + {insn: 444, m: nat64MapRodata, off: 44}, + {insn: 453, m: nat64MapRodata, off: 40}, + {insn: 457, m: nat64MapRodata, off: 4}, + {insn: 492, m: nat64MapRodata, off: 36}, + {insn: 523, m: nat64MapV6Of, off: -1}, + {insn: 538, m: nat64MapRodata, off: 44}, + {insn: 547, m: nat64MapRodata, off: 40}, + {insn: 553, m: nat64MapRodata, off: 4}, + {insn: 587, m: nat64MapRodata, off: 36}, + {insn: 618, m: nat64MapV6Of, off: -1}, + {insn: 686, m: nat64MapStats, off: -1}, + {insn: 695, m: nat64MapStats, off: -1}, + {insn: 737, m: nat64MapStats, off: -1}, + {insn: 751, m: nat64MapStats, off: -1}, + {insn: 778, m: nat64MapStats, off: -1}, + {insn: 829, m: nat64MapStats, off: -1}, + {insn: 843, m: nat64MapSlow, off: -1}, + {insn: 854, m: nat64MapStats, off: -1}, + {insn: 874, m: nat64MapStats, off: -1}, + {insn: 942, m: nat64MapStats, off: -1}, + {insn: 983, m: nat64MapStats, off: -1}, + {insn: 1034, m: nat64MapStats, off: -1}, + {insn: 1047, m: nat64MapStats, off: -1}, + {insn: 1081, m: nat64MapStats, off: -1}, + {insn: 1101, m: nat64MapStats, off: -1}, + {insn: 1115, m: nat64MapRodata, off: 44}, + {insn: 1124, m: nat64MapRodata, off: 40}, + {insn: 1128, m: nat64MapRodata, off: 4}, + {insn: 1162, m: nat64MapRodata, off: 36}, + {insn: 1193, m: nat64MapV6Of, off: -1}, + {insn: 1207, m: nat64MapRodata, off: 44}, + {insn: 1216, m: nat64MapRodata, off: 40}, + {insn: 1220, m: nat64MapRodata, off: 4}, + {insn: 1254, m: nat64MapRodata, off: 36}, + {insn: 1285, m: nat64MapV6Of, off: -1}, + {insn: 1459, m: nat64MapStats, off: -1}, + {insn: 1472, m: nat64MapStats, off: -1}, + {insn: 1487, m: nat64MapStats, off: -1}, + {insn: 1525, m: nat64MapStats, off: -1}, + {insn: 1538, m: nat64MapSlowBuf, off: -1}, + {insn: 1550, m: nat64MapStats, off: -1}, + {insn: 1586, m: nat64MapStats, off: -1}, + {insn: 1598, m: nat64MapStats, off: -1}, + {insn: 1603, m: nat64MapRodata, off: 20}, + {insn: 1612, m: nat64MapRodata, off: 4}, + {insn: 1622, m: nat64MapRodata, off: 20}, + {insn: 1628, m: nat64MapRodata, off: 4}, + {insn: 1638, m: nat64MapRodata, off: 20}, + {insn: 1644, m: nat64MapRodata, off: 4}, + {insn: 1654, m: nat64MapRodata, off: 20}, + {insn: 1660, m: nat64MapRodata, off: 4}, + {insn: 1670, m: nat64MapRodata, off: 20}, + {insn: 1676, m: nat64MapRodata, off: 4}, + {insn: 1686, m: nat64MapRodata, off: 20}, + {insn: 1692, m: nat64MapRodata, off: 4}, + {insn: 1702, m: nat64MapRodata, off: 20}, + {insn: 1708, m: nat64MapRodata, off: 4}, + {insn: 1718, m: nat64MapRodata, off: 20}, + {insn: 1724, m: nat64MapRodata, off: 4}, + {insn: 1734, m: nat64MapRodata, off: 36}, + {insn: 1764, m: nat64MapV4Of, off: -1}, + {insn: 1780, m: nat64MapSlow, off: -1}, + {insn: 1791, m: nat64MapStats, off: -1}, + {insn: 1815, m: nat64MapStats, off: -1}, + {insn: 1824, m: nat64MapStats, off: -1}, + {insn: 1833, m: nat64MapStats, off: -1}, {insn: 1846, m: nat64MapStats, off: -1}, - {insn: 2027, m: nat64MapStats, off: -1}, - {insn: 2050, m: nat64MapStats, off: -1}, - {insn: 2059, m: nat64MapStats, off: -1}, - {insn: 2100, m: nat64MapStats, off: -1}, - {insn: 2130, m: nat64MapStats, off: -1}, - {insn: 2157, m: nat64MapStats, off: -1}, - {insn: 2166, m: nat64MapRodata, off: 28}, - {insn: 2172, m: nat64MapRodata, off: 12}, - {insn: 2182, m: nat64MapRodata, off: 28}, - {insn: 2188, m: nat64MapRodata, off: 12}, - {insn: 2198, m: nat64MapRodata, off: 28}, - {insn: 2204, m: nat64MapRodata, off: 12}, - {insn: 2214, m: nat64MapRodata, off: 28}, - {insn: 2220, m: nat64MapRodata, off: 12}, - {insn: 2230, m: nat64MapRodata, off: 28}, - {insn: 2236, m: nat64MapRodata, off: 12}, - {insn: 2246, m: nat64MapRodata, off: 28}, - {insn: 2252, m: nat64MapRodata, off: 12}, - {insn: 2262, m: nat64MapRodata, off: 28}, - {insn: 2268, m: nat64MapRodata, off: 12}, - {insn: 2278, m: nat64MapRodata, off: 28}, - {insn: 2284, m: nat64MapRodata, off: 12}, - {insn: 2296, m: nat64MapV4Of, off: -1}, - {insn: 2332, m: nat64MapRodata, off: 44}, - {insn: 2457, m: nat64MapStats, off: -1}, - {insn: 2466, m: nat64MapStats, off: -1}, - {insn: 2501, m: nat64MapStats, off: -1}, - {insn: 2510, m: nat64MapStats, off: -1}, - {insn: 2552, m: nat64MapStats, off: -1}, - {insn: 2561, m: nat64MapStats, off: -1}, - {insn: 2570, m: nat64MapStats, off: -1}, + {insn: 1869, m: nat64MapRodata, off: 40}, + {insn: 1872, m: nat64MapRodata, off: 44}, + {insn: 1881, m: nat64MapV6Of, off: -1}, + {insn: 1889, m: nat64MapV4Of, off: -1}, + {insn: 1898, m: nat64MapV6Of, off: -1}, + {insn: 1901, m: nat64MapV4Of, off: -1}, + {insn: 1911, m: nat64MapRodata, off: 20}, + {insn: 1917, m: nat64MapRodata, off: 4}, + {insn: 1927, m: nat64MapRodata, off: 20}, + {insn: 1933, m: nat64MapRodata, off: 4}, + {insn: 1943, m: nat64MapRodata, off: 20}, + {insn: 1949, m: nat64MapRodata, off: 4}, + {insn: 1959, m: nat64MapRodata, off: 20}, + {insn: 1965, m: nat64MapRodata, off: 4}, + {insn: 1975, m: nat64MapRodata, off: 20}, + {insn: 1981, m: nat64MapRodata, off: 4}, + {insn: 1991, m: nat64MapRodata, off: 20}, + {insn: 1997, m: nat64MapRodata, off: 4}, + {insn: 2007, m: nat64MapRodata, off: 20}, + {insn: 2013, m: nat64MapRodata, off: 4}, + {insn: 2023, m: nat64MapRodata, off: 20}, + {insn: 2029, m: nat64MapRodata, off: 4}, + {insn: 2039, m: nat64MapRodata, off: 36}, + {insn: 2069, m: nat64MapV4Of, off: -1}, + {insn: 2120, m: nat64MapStats, off: -1}, + {insn: 2166, m: nat64MapStats, off: -1}, + {insn: 2171, m: nat64MapRodata, off: 40}, + {insn: 2176, m: nat64MapRodata, off: 44}, + {insn: 2185, m: nat64MapV6Of, off: -1}, + {insn: 2191, m: nat64MapRodata, off: 40}, + {insn: 2196, m: nat64MapRodata, off: 44}, + {insn: 2205, m: nat64MapV6Of, off: -1}, + {insn: 2211, m: nat64MapRodata, off: 40}, + {insn: 2216, m: nat64MapRodata, off: 44}, + {insn: 2225, m: nat64MapV6Of, off: -1}, + {insn: 2231, m: nat64MapRodata, off: 40}, + {insn: 2236, m: nat64MapRodata, off: 44}, + {insn: 2245, m: nat64MapV6Of, off: -1}, + {insn: 2251, m: nat64MapRodata, off: 40}, + {insn: 2256, m: nat64MapRodata, off: 44}, + {insn: 2265, m: nat64MapV6Of, off: -1}, + {insn: 2271, m: nat64MapRodata, off: 40}, + {insn: 2276, m: nat64MapRodata, off: 44}, + {insn: 2285, m: nat64MapV6Of, off: -1}, + {insn: 2291, m: nat64MapRodata, off: 40}, + {insn: 2295, m: nat64MapRodata, off: 44}, + {insn: 2304, m: nat64MapV6Of, off: -1}, + {insn: 2314, m: nat64MapStats, off: -1}, + {insn: 2330, m: nat64MapStats, off: -1}, + {insn: 2357, m: nat64MapStats, off: -1}, + {insn: 2415, m: nat64MapStats, off: -1}, + {insn: 2569, m: nat64MapStats, off: -1}, + {insn: 2592, m: nat64MapStats, off: -1}, + {insn: 2601, m: nat64MapStats, off: -1}, + {insn: 2642, m: nat64MapStats, off: -1}, + {insn: 2672, m: nat64MapStats, off: -1}, + {insn: 2699, m: nat64MapStats, off: -1}, + {insn: 2708, m: nat64MapRodata, off: 20}, + {insn: 2716, m: nat64MapRodata, off: 4}, + {insn: 2726, m: nat64MapRodata, off: 20}, + {insn: 2732, m: nat64MapRodata, off: 4}, + {insn: 2742, m: nat64MapRodata, off: 20}, + {insn: 2748, m: nat64MapRodata, off: 4}, + {insn: 2758, m: nat64MapRodata, off: 20}, + {insn: 2764, m: nat64MapRodata, off: 4}, + {insn: 2774, m: nat64MapRodata, off: 20}, + {insn: 2780, m: nat64MapRodata, off: 4}, + {insn: 2790, m: nat64MapRodata, off: 20}, + {insn: 2796, m: nat64MapRodata, off: 4}, + {insn: 2806, m: nat64MapRodata, off: 20}, + {insn: 2812, m: nat64MapRodata, off: 4}, + {insn: 2822, m: nat64MapRodata, off: 20}, + {insn: 2828, m: nat64MapRodata, off: 4}, + {insn: 2838, m: nat64MapRodata, off: 36}, + {insn: 2867, m: nat64MapV4Of, off: -1}, + {insn: 2873, m: nat64MapRodata, off: 20}, + {insn: 2881, m: nat64MapRodata, off: 4}, + {insn: 2891, m: nat64MapRodata, off: 20}, + {insn: 2897, m: nat64MapRodata, off: 4}, + {insn: 2907, m: nat64MapRodata, off: 20}, + {insn: 2913, m: nat64MapRodata, off: 4}, + {insn: 2923, m: nat64MapRodata, off: 20}, + {insn: 2929, m: nat64MapRodata, off: 4}, + {insn: 2939, m: nat64MapRodata, off: 20}, + {insn: 2945, m: nat64MapRodata, off: 4}, + {insn: 2955, m: nat64MapRodata, off: 20}, + {insn: 2961, m: nat64MapRodata, off: 4}, + {insn: 2971, m: nat64MapRodata, off: 20}, + {insn: 2977, m: nat64MapRodata, off: 4}, + {insn: 2987, m: nat64MapRodata, off: 20}, + {insn: 2993, m: nat64MapRodata, off: 4}, + {insn: 3003, m: nat64MapRodata, off: 36}, + {insn: 3031, m: nat64MapV4Of, off: -1}, + {insn: 3165, m: nat64MapStats, off: -1}, + {insn: 3174, m: nat64MapStats, off: -1}, + {insn: 3210, m: nat64MapStats, off: -1}, + {insn: 3252, m: nat64MapStats, off: -1}, + {insn: 3261, m: nat64MapStats, off: -1}, + {insn: 3270, m: nat64MapStats, off: -1}, } diff --git a/nat64bpf.go b/nat64bpf.go index d14062e..1559a43 100644 --- a/nat64bpf.go +++ b/nat64bpf.go @@ -11,7 +11,6 @@ import ( "net/netip" "os" "slices" - "strings" "time" "github.com/mdlayher/netlink" @@ -80,7 +79,7 @@ func (b *bpfNAT64) run(ctx context.Context) { // start sets the translator up, and on failure undoes what it did. func (b *bpfNAT64) start() (err error) { - ifi, err := dummyAdd(joolOutside) + ifi, err := dummyAdd(joolOutside, 0) if err != nil { return fmt.Errorf("cannot create %s: %w", joolOutside, err) } @@ -96,7 +95,7 @@ func (b *bpfNAT64) start() (err error) { if err := b.route(ifi); err != nil { return err } - if b.slow, err = newSlowPath(b.prefix, b.objs.maps[nat64MapV4Of], b.objs.maps[nat64MapV6Of], ifi, b.lanMTU()); err != nil { + if b.slow, err = newSlowPath(nat64Mapping(b.prefix, b.objs.maps[nat64MapV4Of], b.objs.maps[nat64MapV6Of]), ifi, b.lanMTU()); err != nil { return err } if b.reader, err = newRingReader(b.objs.maps[nat64MapSlow]); err != nil { @@ -155,11 +154,9 @@ func (b *bpfNAT64) load() error { return fmt.Errorf("loading the BPF translator: %w", err) } b.objs = objs - fd, err := objs.attachTCX(b.ifindex) - if err != nil { - return fmt.Errorf("attaching the BPF translator, which needs Linux 6.6 or later: %w", err) + if b.link, err = objs.attachTCX(b.ifindex, unix.BPF_TCX_EGRESS); err != nil { + return fmt.Errorf("the BPF translator: %w", err) } - b.link = os.NewFile(uintptr(fd), "tcx") return nil } @@ -292,33 +289,12 @@ func route4MTU(ifi int, dst netip.Prefix, mtu int) error { return linkRequest(unix.RTM_NEWROUTE, netlink.Create|netlink.Replace, hdr, attrs) } -// drops sums the drop counters over the CPUs, naming those that are not zero, and adds what the -// slow path dropped. -func (b *bpfNAT64) drops() string { - var parts []string - stats := b.objs.maps[nat64MapStats] - per := make([]byte, stats.valueSize) - for i, name := range bpfStatNames { - if name == "" { - continue - } - stats.lookup(nativeEndian.AppendUint32(nil, uint32(i)), per) // every index of an array is there - var n uint64 - for c := 0; c < len(per); c += 8 { - n += nativeEndian.Uint64(per[c:]) - } - if n != 0 { - parts = append(parts, fmt.Sprintf("%s %d", name, n)) - } - } - if n := b.slow.dropped.Load(); n != 0 { - parts = append(parts, fmt.Sprintf("slow-path %d", n)) - } - return strings.Join(parts, ", ") -} +// drops names the program's drop counters that are not zero, and what the slow path dropped. +func (b *bpfNAT64) drops() string { return drops(b.objs.maps[nat64MapStats], bpfStatNames, b.slow) } -// dummyAdd creates a dummy device, replacing one of the same name. -func dummyAdd(name string) (*net.Interface, error) { +// dummyAdd creates a dummy device, with an MTU of its own unless mtu is 0, replacing one of the +// same name. +func dummyAdd(name string, mtu int) (*net.Interface, error) { if ifi, err := net.InterfaceByName(name); err == nil { if err := linkDel(ifi.Index); err != nil { return nil, fmt.Errorf("removing the old %s: %w", name, err) @@ -326,6 +302,9 @@ func dummyAdd(name string) (*net.Interface, error) { } ae := netlink.NewAttributeEncoder() ae.String(unix.IFLA_IFNAME, name) + if mtu > 0 { + ae.Uint32(unix.IFLA_MTU, uint32(mtu)) + } ae.Nested(unix.IFLA_LINKINFO, func(ae *netlink.AttributeEncoder) error { ae.String(unix.IFLA_INFO_KIND, "dummy") return nil diff --git a/nat64slow.go b/nat64slow.go index e7dfe33..235548e 100644 --- a/nat64slow.go +++ b/nat64slow.go @@ -16,8 +16,8 @@ import ( "golang.org/x/sys/unix" ) -// The slow path of the BPF translator takes the datagrams bpf/nat64.c cannot give a right -// checksum one fragment at a time: fragmented ICMP, and IPv4 UDP without a checksum. It puts +// The slow path of the BPF translators takes the datagrams bpf/nat64.c and bpf/clat.c cannot give +// a right checksum one fragment at a time: fragmented ICMP, and IPv4 UDP without a checksum. It puts // them together, translates them whole, cuts them into fragments again where they are too large // for the device, or for the smallest LAN, and sends them back out of the device marked // bpfSlowMark. The program hands them to the device's ingress, where they enter the stack like @@ -31,9 +31,7 @@ const ( ) type slowPath struct { - prefix netip.Prefix - v4of *bpfMap - v6of *bpfMap + slowMapping dev *net.Interface mtu6 int // what an IPv6 packet sent on may be at most fd int // a packet socket, sending marked @@ -43,6 +41,45 @@ type slowPath struct { now func() time.Time } +// slowMapping maps addresses between the families as the program the slow path serves does. +type slowMapping struct { + v4of6 func(a [16]byte, alloc bool) ([4]byte, error) // a new pool address for a source if alloc + v6of4 func(a [4]byte) ([16]byte, error) +} + +// nat64Mapping is bpf/nat64.c's: the NAT64 prefix carries IPv4 addresses, and the pool stands for +// IPv6 sources. +func nat64Mapping(prefix netip.Prefix, v4of, v6of *bpfMap) slowMapping { + return slowMapping{ + v4of6: func(a [16]byte, alloc bool) ([4]byte, error) { + var v [4]byte + switch { + case prefix.Contains(netip.AddrFrom16(a)): + return extract4(prefix, a), nil + case alloc: + v, ok := map4(v4of, v6of, a) + if !ok { + return v, errors.New("the pool is full") + } + return v, nil + case v4of.lookup(a[:], v[:]) != nil: + return v, fmt.Errorf("%s has no pool address", netip.AddrFrom16(a)) + } + return v, nil + }, + v6of4: func(a [4]byte) ([16]byte, error) { + var v [16]byte + if !bpfPool.Contains(netip.AddrFrom4(a)) { + return embed4(prefix, a), nil + } + if v6of.lookup(a[:], v[:]) != nil { + return v, fmt.Errorf("no source has %s", netip.AddrFrom4(a)) + } + return v, nil + }, + } +} + // slowKey tells the fragments of one datagram from those of others (RFC 791, RFC 8200). type slowKey struct { src, dst netip.Addr @@ -57,13 +94,13 @@ type slowGroup struct { expires time.Time } -func newSlowPath(prefix netip.Prefix, v4of, v6of *bpfMap, dev *net.Interface, mtu6 int) (*slowPath, error) { +func newSlowPath(m slowMapping, dev *net.Interface, mtu6 int) (*slowPath, error) { fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0) if err != nil { return nil, fmt.Errorf("packet socket: %w", err) } unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_MARK, bpfSlowMark) // CAP_NET_RAW, which the socket took, allows it - return &slowPath{prefix: prefix, v4of: v4of, v6of: v6of, dev: dev, mtu6: min(mtu6, dev.MTU), fd: fd, groups: map[slowKey]*slowGroup{}, now: time.Now}, nil + return &slowPath{slowMapping: m, dev: dev, mtu6: min(mtu6, dev.MTU), fd: fd, groups: map[slowKey]*slowGroup{}, now: time.Now}, nil } func (p *slowPath) close() { unix.Close(p.fd) } @@ -208,13 +245,16 @@ func (p *slowPath) whole(hdr []byte, proto uint8, payload []byte) error { return p.to4(hdr, proto, payload) } -// to6 translates IPv4 from the pool's side: ICMP echo, or UDP whose checksum it computes. +// to6 translates IPv4: ICMP echo, or UDP whose checksum it computes. func (p *slowPath) to6(hdr []byte, proto uint8, payload []byte) error { - var dst [16]byte - if err := p.v6of.lookup(hdr[16:20], dst[:]); err != nil { - return fmt.Errorf("no source has %s", netip.AddrFrom4([4]byte(hdr[16:20]))) + src, err := p.v6of4([4]byte(hdr[12:16])) + if err != nil { + return err + } + dst, err := p.v6of4([4]byte(hdr[16:20])) + if err != nil { + return err } - src := embed4(p.prefix, [4]byte(hdr[12:16])) msg := slices.Clone(payload) var csum int switch { @@ -241,18 +281,18 @@ func (p *slowPath) to6(hdr []byte, proto uint8, payload []byte) error { return p.send6(h, msg) } -// to4 translates IPv6 ICMP echo from the LAN side. +// to4 translates IPv6 ICMP echo. func (p *slowPath) to4(hdr []byte, proto uint8, payload []byte) error { - src, dst := [16]byte(hdr[8:24]), [16]byte(hdr[24:40]) - if !p.prefix.Contains(netip.AddrFrom16(dst)) { - return fmt.Errorf("%s is outside %s", netip.AddrFrom16(dst), p.prefix) - } if proto != unix.IPPROTO_ICMPV6 || len(payload) < 8 || (payload[0] != 128 && payload[0] != 129) { return fmt.Errorf("cannot translate protocol %d whole", proto) } - a4, ok := map4(p.v4of, p.v6of, src) - if !ok { - return errors.New("the pool is full") + dst, err := p.v4of6([16]byte(hdr[24:40]), false) + if err != nil { + return err + } + src, err := p.v4of6([16]byte(hdr[8:24]), true) + if err != nil { + return err } msg := slices.Clone(payload) msg[0] = map[byte]byte{128: 8, 129: 0}[msg[0]] @@ -262,9 +302,8 @@ func (p *slowPath) to4(hdr []byte, proto uint8, payload []byte) error { h[0] = 4<<4 | 5 h[1] = hdr[0]<<4 | hdr[1]>>4 h[8], h[9] = hdr[7], unix.IPPROTO_ICMP - copy(h[12:], a4[:]) - a := extract4(p.prefix, dst) - copy(h[16:], a[:]) + copy(h[12:], src[:]) + copy(h[16:], dst[:]) return p.send4(h, msg) } diff --git a/nat64slow_integration_test.go b/nat64slow_integration_test.go index 2d3253f..5e8dcfc 100644 --- a/nat64slow_integration_test.go +++ b/nat64slow_integration_test.go @@ -21,13 +21,14 @@ import ( // slowFixture is a slow path sending through a dummy device of its own, with maps of its own, // and a socket that sees what leaves the device. type slowFixture struct { - p *slowPath - cap int + p *slowPath + v4of, v6of *bpfMap + cap int } func newSlowFixture(t *testing.T, mtu int) *slowFixture { t.Helper() - ifi, err := dummyAdd("slowtest0") + ifi, err := dummyAdd("slowtest0", 0) if err != nil { t.Fatal(err) } @@ -37,7 +38,7 @@ func newSlowFixture(t *testing.T, mtu int) *slowFixture { } ifi.MTU = mtu v4of, v6of := testMaps(t, 64, 64) - p, err := newSlowPath(nat64WKP, v4of, v6of, ifi, 1500) + p, err := newSlowPath(nat64Mapping(nat64WKP, v4of, v6of), ifi, 1500) if err != nil { t.Fatal(err) } @@ -51,7 +52,7 @@ func newSlowFixture(t *testing.T, mtu int) *slowFixture { t.Fatal(err) } unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Usec: 200000}) - return &slowFixture{p: p, cap: fd} + return &slowFixture{p: p, v4of: v4of, v6of: v6of, cap: fd} } // sent returns the IP packets that left the device since the last call. @@ -136,7 +137,7 @@ func TestSlowPathTranslates(t *testing.T) { p := f.p host := netip.MustParseAddr("2001:db8::5").As16() pool := [4]byte{198, 18, 0, 5} - if err := p.v6of.update(pool[:], host[:], 0); err != nil { + if err := f.v6of.update(pool[:], host[:], 0); err != nil { t.Fatal(err) } @@ -207,7 +208,7 @@ func TestSlowPathTranslates(t *testing.T) { } hdr, msg = reassembled(t, f.sent(), 600) var a4 [4]byte - if err := p.v4of.lookup(src.AsSlice(), a4[:]); err != nil { + if err := f.v4of.lookup(src.AsSlice(), a4[:]); err != nil { t.Fatal(err) } base, mask := binary.BigEndian.Uint32(bpfPool.Addr().AsSlice()), uint32(1)<<(32-bpfPool.Bits())-1 @@ -218,14 +219,20 @@ func TestSlowPathTranslates(t *testing.T) { t.Fatalf("want a 2000-byte echo reply with a right checksum, TOS and TTL, got % x", hdr) } + // A source the pool already numbers is found again without a new address + if got, err := p.v4of6(src.As16(), false); err != nil || got != a4 { + t.Fatalf("want %v again, got %v: %v", a4, got, err) + } + // What cannot be translated for name, pkt := range map[string][]byte{ - "IPv4 to an address no source has": v4Packet("192.0.2.2", "198.18.0.6", unix.IPPROTO_UDP, 0, udp), - "IPv4 TCP": v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_TCP, 0, udp), - "an IPv4 error": v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_ICMP, 0, []byte{3, 3, 0, 0, 0, 0, 0, 0}), - "IPv6 outside the prefix": v6Packet("2001:db8::77", "2001:db8::1", unix.IPPROTO_ICMPV6, reply[:8]), - "IPv6 UDP": v6Packet("2001:db8::77", "64:ff9b::1", unix.IPPROTO_UDP, udp), - "an IPv6 error": v6Packet("2001:db8::77", "64:ff9b::1", unix.IPPROTO_ICMPV6, []byte{1, 4, 0, 0, 0, 0, 0, 0}), + "IPv4 from a pool address no source has": v4Packet("198.18.0.99", "198.18.0.5", unix.IPPROTO_UDP, 0, udp), + "IPv4 to an address no source has": v4Packet("192.0.2.2", "198.18.0.6", unix.IPPROTO_UDP, 0, udp), + "IPv4 TCP": v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_TCP, 0, udp), + "an IPv4 error": v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_ICMP, 0, []byte{3, 3, 0, 0, 0, 0, 0, 0}), + "IPv6 outside the prefix": v6Packet("2001:db8::77", "2001:db8::1", unix.IPPROTO_ICMPV6, reply[:8]), + "IPv6 UDP": v6Packet("2001:db8::77", "64:ff9b::1", unix.IPPROTO_UDP, udp), + "an IPv6 error": v6Packet("2001:db8::77", "64:ff9b::1", unix.IPPROTO_ICMPV6, []byte{1, 4, 0, 0, 0, 0, 0, 0}), } { if err := p.packet(pkt); err == nil { t.Errorf("%s: want an error", name) @@ -236,7 +243,7 @@ func TestSlowPathTranslates(t *testing.T) { for i := range uint32(bpfProbes) { var slot [4]byte binary.BigEndian.PutUint32(slot[:], base+((hash6(busy)+i)&mask)) - p.v6of.update(slot[:], host[:], 0) + f.v6of.update(slot[:], host[:], 0) } if err := p.packet(v6Packet("2001:db8::99", "64:ff9b::1", unix.IPPROTO_ICMPV6, reply[:8])); err == nil || !strings.Contains(err.Error(), "full") { t.Fatalf("want the pool full, got %v", err) @@ -282,7 +289,7 @@ func TestBPFProgram(t *testing.T) { return } b := &bpfNAT64{prefix: nat64WKP} - ifi, err := dummyAdd("progtest0") + ifi, err := dummyAdd("progtest0", 0) if err != nil { t.Fatal(err) } @@ -349,10 +356,10 @@ func TestBPFNAT64StartUndoes(t *testing.T) { } b.stop() // A device of the same name, as a crash would leave, is replaced - if _, err := dummyAdd("reptest0"); err != nil { + if _, err := dummyAdd("reptest0", 0); err != nil { t.Fatal(err) } - ifi, err := dummyAdd("reptest0") + ifi, err := dummyAdd("reptest0", 0) if err != nil { t.Fatalf("the old device should be replaced: %v", err) } @@ -377,7 +384,7 @@ func TestBPFNAT64WithoutPrivileges(t *testing.T) { if err := syscall.Setresuid(-1, 65534, -1); err != nil { t.Fatal(err) } - _, addErr := dummyAdd(joolOutside) + _, addErr := dummyAdd(joolOutside, 0) b.sweep() b.sweep() if err := syscall.Setresuid(-1, 0, -1); err != nil { @@ -431,7 +438,7 @@ func TestSlowPathWithoutDescriptors(t *testing.T) { if err != nil { t.Fatal(err) } - if err := joolWithFDs(t, 0, func() error { _, err := newSlowPath(nat64WKP, nil, nil, lo, 1500); return err }); !errors.Is(err, unix.EMFILE) { + if err := joolWithFDs(t, 0, func() error { _, err := newSlowPath(slowMapping{}, lo, 1500); return err }); !errors.Is(err, unix.EMFILE) { t.Fatalf("want the socket refused, got %v", err) } } @@ -443,7 +450,7 @@ func TestBPFICMPTranslation(t *testing.T) { return } b := &bpfNAT64{prefix: nat64WKP} - ifi, err := dummyAdd("icmptest0") + ifi, err := dummyAdd("icmptest0", 0) if err != nil { t.Fatal(err) } @@ -594,7 +601,7 @@ func TestBPFRFC6052(t *testing.T) { udp := []byte{0, 7, 0x30, 0x39, 0, 8, 0xab, 0xcd} for prefix, want := range rfc6052Examples { b := &bpfNAT64{prefix: netip.MustParsePrefix(prefix)} - ifi, err := dummyAdd("rfctest0") + ifi, err := dummyAdd("rfctest0", 0) if err != nil { t.Fatal(err) } diff --git a/nat64slow_test.go b/nat64slow_test.go index 67109c7..249d9b5 100644 --- a/nat64slow_test.go +++ b/nat64slow_test.go @@ -175,3 +175,20 @@ func TestRFC6052(t *testing.T) { } } } + +// The CLAT's slow path maps as its program does: the NAT64 prefix both ways, the CLAT address and +// 192.0.0.4, and a router on the IPv6 path to 192.0.0.8. +func TestCLATMapping(t *testing.T) { + addr := netip.MustParseAddr("2001:db8::c1a7") + m := clatMapping(nat64WKP, addr) + for in, want := range map[string]string{"64:ff9b::c000:202": "192.0.2.2", addr.String(): "192.0.0.4", "2001:db8::1": "192.0.0.8"} { + if got, err := m.v4of6(netip.MustParseAddr(in).As16(), false); err != nil || netip.AddrFrom4(got).String() != want { + t.Errorf("%s: got %v %v, want %s", in, netip.AddrFrom4(got), err, want) + } + } + for in, want := range map[string]netip.Addr{"192.0.2.2": netip.MustParseAddr("64:ff9b::c000:202"), "192.0.0.4": addr} { + if got, err := m.v6of4(netip.MustParseAddr(in).As4()); err != nil || netip.AddrFrom16(got) != want { + t.Errorf("%s: got %v %v, want %s", in, netip.AddrFrom16(got), err, want) + } + } +} diff --git a/prefix.go b/prefix.go index d50f0ed..4b2ec21 100644 --- a/prefix.go +++ b/prefix.go @@ -184,6 +184,34 @@ func (s Snapshot) wanSelf(iid iidPolicy, secret []byte, ifi *net.Interface) (net return netip.Addr{}, false } +// clatDev is the device the CLAT takes this host's IPv4 from. +const clatDev = "sixup-clat" + +// clatAddr is this host's CLAT address (RFC 6877) while the upstream announces a PREF64: stable in +// the WAN subnet, else in the SLAAC prefix, and apart from every other address of the host. +func (s Snapshot) clatAddr(secret []byte) netip.Addr { + if !s.PREF64.IsValid() { + return netip.Addr{} + } + prefixes := append([]Prefix{s.WANSubnet}, s.wanSLAAC()...) + i := slices.IndexFunc(prefixes, func(p Prefix) bool { return p.Prefix.IsValid() && !p.Deprecated }) + if i < 0 { + return netip.Addr{} + } + return stableIID(secret, prefixes[i].Prefix, clatDev, 0) +} + +// wanEndpoints is what the WAN holds beside the addresses of its prefixes: the tunnel endpoints +// and the CLAT address, neither ever a source of its own. +func wanEndpoints(secret []byte) func(Snapshot) []netip.Addr { + return func(s Snapshot) []netip.Addr { + if a := s.clatAddr(secret); a.IsValid() { + return append(s.tunnelEndpoints(), a) + } + return s.tunnelEndpoints() + } +} + // selfAddr returns the address of this router in prefix by the policy iid, after any DAD failure. func (s Snapshot) selfAddr(iid iidPolicy, secret []byte, prefix netip.Prefix, ifi *net.Interface) netip.Addr { if a, ok := s.Self[prefix]; ok { @@ -800,7 +828,8 @@ func (s *Store) recompute(now time.Time) { if change == changeNone && !tunnelEqual(s.cur.Tunnel, next.Tunnel) { change = changeRenew } - if change == changeNone && (s.cur.WANAddr != next.WANAddr || s.cur.PDPending != next.PDPending) { + // A host's subnet follows the delegation alone, so it moves without any LAN prefix moving + if change == changeNone && (s.cur.WANAddr != next.WANAddr || s.cur.WANSubnet.Prefix != next.WANSubnet.Prefix || s.cur.PDPending != next.PDPending) { change = changeRenew } // DNS, search list, PREF64 and MTU reach hosts through the RA and the DHCPv6 server, so a @@ -999,21 +1028,40 @@ func (s *Store) spareSubnet(p Prefix) (netip.Prefix, bool) { } // wanSubnet picks the first LAN's delegated /64: routed to this router, so the upstream resolves -// none of its addresses, and taken from no downstream delegation (RFC 7084 WAA-7). One also -// on-link on the WAN is skipped. +// none of its addresses, and taken from no downstream delegation (RFC 7084 WAA-7). A host with no +// LAN takes the first /64 of the delegation the ISP does not keep for the WAN link (RFC 6603). +// One also on-link on the WAN is skipped. func (s Snapshot) wanSubnet(lans []lanDef) Prefix { - for _, deprecated := range []bool{false, true} { + candidates := func(yield func(Prefix) bool) { for _, l := range lans { for _, p := range s.LAN[l.iface] { - if p.Source != sourcePD || p.Stale || p.Deprecated != deprecated || - slices.ContainsFunc(s.WAN, func(w Prefix) bool { return w.Source == sourceRA && w.Prefix.Overlaps(p.Prefix) }) { - continue + if !yield(p) { + return + } + } + } + for _, p := range s.WAN { + for i := 0; len(lans) == 0 && i < 2; i++ { + if sub, ok := splitLAN(p.Prefix, i); ok && !p.Exclude.Overlaps(sub) { + p.Prefix = sub + if !yield(p) { + return + } + break } - p.OffLink = true - return p } } } + for _, deprecated := range []bool{false, true} { + for p := range candidates { + if p.Source != sourcePD || p.Stale || p.Deprecated != deprecated || + slices.ContainsFunc(s.WAN, func(w Prefix) bool { return w.Source == sourceRA && w.Prefix.Overlaps(p.Prefix) }) { + continue + } + p.OffLink = true + return p + } + } return Prefix{} } diff --git a/prefix_test.go b/prefix_test.go index d2f635b..6039d26 100644 --- a/prefix_test.go +++ b/prefix_test.go @@ -855,6 +855,62 @@ func TestStoreWANSubnet(t *testing.T) { } } +// Without a LAN, the host's subnet is the first /64 of the delegation that the ISP does not keep +// for the WAN link; a delegated /64 the RA also announces on-link is left to SLAAC. +func TestStoreWANSubnetWithoutALAN(t *testing.T) { + st := newStore("pd", nil, time.Second, nil, false, 0, 0, "") + ch := st.Subscribe() + recv(t, ch) + now := time.Now() + pd := Prefix{Prefix: netip.MustParsePrefix("2001:db8:100::/56"), Preferred: now.Add(time.Hour), Valid: now.Add(2 * time.Hour), Source: "pd"} + st.Set("pd", SourceUpdate{Prefixes: []Prefix{pd}}) + s := recv(t, ch) + if w := s.WANSubnet; w.Prefix != netip.MustParsePrefix("2001:db8:100::/64") || !w.OffLink || !slices.Equal(s.wanStatic(), []Prefix{w}) { + t.Fatalf("the first /64 should be the host's: %+v", w) + } + pd.Exclude = netip.MustParsePrefix("2001:db8:100::/64") + st.Set("pd", SourceUpdate{Prefixes: []Prefix{pd}}) + if s = recv(t, ch); s.WANSubnet.Prefix != netip.MustParsePrefix("2001:db8:100:1::/64") { + t.Fatalf("the excluded /64 should be passed over: %+v", s.WANSubnet) + } + shared := netip.MustParsePrefix("2001:db8:200::/64") + st.Set("pd", SourceUpdate{Prefixes: []Prefix{{Prefix: shared, Preferred: now.Add(time.Hour), Valid: now.Add(2 * time.Hour), Source: "pd"}}}) + recv(t, ch) + st.Set("ra", SourceUpdate{Prefixes: []Prefix{{Prefix: shared, Preferred: now.Add(time.Hour), Valid: now.Add(2 * time.Hour), Source: "ra", SLAAC: true}}}) + if s = recv(t, ch); s.WANSubnet.Prefix.IsValid() || len(s.wanStatic()) != 1 || s.wanStatic()[0].Source != sourceRA { + t.Fatalf("an on-link /64 is SLAAC's: subnet %+v, static %v", s.WANSubnet, s.wanStatic()) + } +} + +// The CLAT address exists while the upstream announces a PREF64: in the WAN subnet, else in the +// SLAAC prefix, stable and apart from the host's own; the WAN holds it beside the tunnel endpoints. +func TestCLATAddr(t *testing.T) { + secret := []byte("secret") + now := time.Now() + sub := Prefix{Prefix: netip.MustParsePrefix("2001:db8:100::/64"), Source: sourcePD, Valid: now.Add(time.Hour)} + ra := Prefix{Prefix: netip.MustParsePrefix("2001:db8:1::/64"), Source: sourceRA, SLAAC: true, Valid: now.Add(time.Hour)} + s := Snapshot{WANSubnet: sub, WAN: []Prefix{ra}} + if a := s.clatAddr(secret); a.IsValid() || len(wanEndpoints(secret)(s)) != 0 { + t.Fatalf("no PREF64, no CLAT address: %s", a) + } + s.PREF64 = nat64WKP + a := s.clatAddr(secret) + if !sub.Prefix.Contains(a) || a != s.clatAddr(secret) || a == stableIID(secret, sub.Prefix, "wan0", 0) { + t.Fatalf("want a stable address of its own in the WAN subnet, got %s", a) + } + if got := wanEndpoints(secret)(s); !slices.Equal(got, []netip.Addr{a}) { + t.Fatalf("the WAN should hold the CLAT address, got %v", got) + } + s.WANSubnet.Deprecated = true + if a := s.clatAddr(secret); !ra.Prefix.Contains(a) { + t.Fatalf("with the subnet deprecated, want the SLAAC prefix, got %s", a) + } + s.WAN = nil + if a := s.clatAddr(secret); a.IsValid() { + t.Fatalf("with no live prefix, want none, got %s", a) + } +} + // An expired prefix has no time left; without a WAN subnet the temporary addresses come from the // SLAAC prefix; and only a MAP-E CE is a tunnel endpoint to configure. func TestSnapshotHelpers(t *testing.T) { diff --git a/sysctl.go b/sysctl.go index a9ac0ad..8d62c48 100644 --- a/sysctl.go +++ b/sysctl.go @@ -2,12 +2,19 @@ package main // applySysctl turns on the kernel switches a bare system needs: global forwarding, and no RA on the LAN interfaces. // IPv6 forwarding is decided by conf/all/forwarding, so a per-interface setting alone has no effect. +// Without a LAN there is nothing to forward, and only the WAN's own RA handling is turned off. func applySysctl(wan string, lans []lanDef) { set := func(iface, key, val string) { if err := sysctlSet(iface, key, val); err != nil { warnf("[sysctl] %s/%s=%s failed: %v", iface, key, val, err) } } + // A host that forwards nothing would take the RA itself beside sixup, addresses and default + // route alike; a router ignores it already + if len(lans) == 0 { + set(wan, "accept_ra", "0") + return + } set("all", "forwarding", "1") for _, l := range lans { set(l.iface, "accept_ra", "0")