diff --git a/Dockerfile b/Dockerfile index 1aba5f1..7826e23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG TARGETARCH TARGETVARIANT VERSION=dev WORKDIR /src COPY go.mod go.sum ./ RUN go mod download -COPY *.go LICENSE NOTICE ./ +COPY *.go *.bin LICENSE NOTICE ./ RUN CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" GOARM="${TARGETVARIANT#v}" \ go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /out/sixup . diff --git a/bpf.go b/bpf.go new file mode 100644 index 0000000..3b69e18 --- /dev/null +++ b/bpf.go @@ -0,0 +1,321 @@ +//go:build linux + +package main + +import ( + "bytes" + "errors" + "fmt" + "os" + "runtime" + "strconv" + "strings" + "sync/atomic" + "unsafe" + + "golang.org/x/sys/unix" +) + +// A small loader for the programs tools/bpfgen prepares: the bpf() system call and nothing else, +// so that sixup carries no ELF or BTF parser. The attribute layouts are those of union bpf_attr +// in linux/bpf.h. + +type bpfMapSpec struct { + name string + typ, keySize, valueSize, maxEntries, flags uint32 +} + +// bpfReloc points a 64-bit load of the code at a map, or at an offset into the read-only data. +type bpfReloc struct { + insn, m int + off int64 // -1 for the map itself +} + +type bpfVar struct{ off, size int } + +// bpfProgram is what tools/bpfgen writes for one program, with the read-only data filled in. +type bpfProgram struct { + code []byte + maps []bpfMapSpec + relocs []bpfReloc + rodata []byte + license string +} + +// bpfObjects is a program loaded with its maps. +type bpfObjects struct { + maps []*bpfMap + prog int +} + +// srcShift is where the source register sits in the second byte of an instruction: its high +// nibble on a little-endian machine, its low one on a big-endian one. +var srcShift = 4 * (nativeEndian.Uint16([]byte{1, 0}) & 1) + +func bpfSys(cmd int, attr unsafe.Pointer, size uintptr) (int, error) { + r, _, errno := unix.Syscall(unix.SYS_BPF, uintptr(cmd), uintptr(attr), size) + runtime.KeepAlive(attr) + if errno != 0 { + return -1, errno + } + return int(r), nil +} + +func ptr(b []byte) uint64 { + if len(b) == 0 { + return 0 + } + return uint64(uintptr(unsafe.Pointer(&b[0]))) +} + +type bpfMap struct { + fd int + spec bpfMapSpec + valueSize int // per CPU too for the per-CPU ones +} + +func bpfMapCreate(spec bpfMapSpec) (*bpfMap, error) { + attr := struct { + typ, keySize, valueSize, maxEntries, flags, innerMapFd, numaNode uint32 + name [16]byte + }{typ: spec.typ, keySize: spec.keySize, valueSize: spec.valueSize, maxEntries: spec.maxEntries, flags: spec.flags} + copy(attr.name[:15], strings.TrimPrefix(spec.name, ".")) + fd, err := bpfSys(unix.BPF_MAP_CREATE, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + if err != nil { + return nil, fmt.Errorf("creating the map %s: %w", spec.name, err) + } + m := &bpfMap{fd: fd, spec: spec, valueSize: int(spec.valueSize)} + if spec.typ == unix.BPF_MAP_TYPE_PERCPU_ARRAY || spec.typ == unix.BPF_MAP_TYPE_PERCPU_HASH { + m.valueSize = ((m.valueSize + 7) &^ 7) * possibleCPUs() // each CPU's rounded up to 8 bytes + } + return m, nil +} + +func (m *bpfMap) close() { unix.Close(m.fd) } + +// elem is one of the map element commands, with key, and value or next key. +func (m *bpfMap) elem(cmd int, key, value []byte, flags uint64) error { + attr := struct { + fd, _ uint32 + key, value uint64 + flags uint64 + }{fd: uint32(m.fd), key: ptr(key), value: ptr(value), flags: flags} + _, err := bpfSys(cmd, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + runtime.KeepAlive(key) + runtime.KeepAlive(value) + return err +} + +// lookup fills value, which for a per-CPU map holds every CPU's, and fails with ENOENT for a key +// the map does not have. +func (m *bpfMap) lookup(key, value []byte) error { + return m.elem(unix.BPF_MAP_LOOKUP_ELEM, key, value, 0) +} + +func (m *bpfMap) update(key, value []byte, flags uint64) error { + return m.elem(unix.BPF_MAP_UPDATE_ELEM, key, value, flags) +} + +func (m *bpfMap) delete(key []byte) error { return m.elem(unix.BPF_MAP_DELETE_ELEM, key, nil, 0) } + +// keys returns the keys in the map, as many as it lists in one pass. +func (m *bpfMap) keys() [][]byte { + var keys [][]byte + var key []byte // nil asks for the first + for { + next := make([]byte, m.spec.keySize) + if m.elem(unix.BPF_MAP_GET_NEXT_KEY, key, next, 0) != nil { + return keys + } + keys = append(keys, next) + key = next + } +} + +// possibleCPUs counts the CPUs a per-CPU map has a value for: /sys/devices/system/cpu/possible, +// such as 0-3 or 0,2-5. +func possibleCPUs() int { + b, _ := os.ReadFile("/sys/devices/system/cpu/possible") // sysfs is there wherever BPF is + n := 0 + for r := range strings.SplitSeq(strings.TrimSpace(string(b)), ",") { + lo, hi, ok := strings.Cut(r, "-") + first, _ := strconv.Atoi(lo) + last := first + if ok { + last, _ = strconv.Atoi(hi) + } + n += last - first + 1 + } + return n +} + +// bpfLoad creates the maps, fills in and freezes the read-only data, points the code at both +// and loads it as a tc program. A program the verifier refuses comes back with the end of its log. +func bpfLoad(p bpfProgram) (_ *bpfObjects, err error) { + objs := &bpfObjects{prog: -1} + defer func() { + if err != nil { + objs.close() + } + }() + for _, spec := range p.maps { + m, err := bpfMapCreate(spec) + if err != nil { + return nil, err + } + objs.maps = append(objs.maps, m) + } + ro := objs.maps[len(objs.maps)-1] + freeze := func() error { + attr := struct{ fd uint32 }{uint32(ro.fd)} + _, err := bpfSys(unix.BPF_MAP_FREEZE, unsafe.Pointer(&attr), unsafe.Sizeof(attr)) + return err + } + if err := errors.Join(ro.update(make([]byte, 4), p.rodata, 0), freeze()); err != nil { + return nil, fmt.Errorf("filling in and freezing the read-only data: %w", err) + } + + code := append([]byte(nil), p.code...) + for _, r := range p.relocs { + insn := code[r.insn*8:] + src := byte(unix.BPF_PSEUDO_MAP_FD) + if r.off >= 0 { + src = unix.BPF_PSEUDO_MAP_VALUE + nativeEndian.PutUint32(insn[12:], uint32(r.off)) // the second half of the 64-bit load + } + insn[1] = insn[1]&^(0xf<= 0 { + unix.Close(o.prog) + } +} + +// 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) { + 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)) +} + +// errRingClosed is what a ring buffer reader returns once closed. +var errRingClosed = errors.New("ring buffer closed") + +// ringReader reads the records of a BPF ring buffer: the consumer position on a page it writes, +// the producer position on the next, and the data mapped twice after it, so that a record that +// wraps around reads straight through. An eventfd in the same epoll set wakes it to stop. +type ringReader struct { + cons, prod []byte + mask uint64 + epfd, evfd int +} + +func newRingReader(m *bpfMap) (_ *ringReader, err error) { + page := os.Getpagesize() + r := &ringReader{mask: uint64(m.spec.maxEntries) - 1, epfd: -1, evfd: -1} + defer func() { + if err != nil { + r.free() + } + }() + if r.cons, err = unix.Mmap(m.fd, 0, page, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED); err != nil { + return nil, err + } + if r.prod, err = unix.Mmap(m.fd, int64(page), page+2*int(m.spec.maxEntries), unix.PROT_READ, unix.MAP_SHARED); err != nil { + return nil, err + } + if r.epfd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil { + return nil, err + } + if r.evfd, err = unix.Eventfd(0, unix.EFD_CLOEXEC); err != nil { + return nil, err + } + for _, fd := range []int{m.fd, r.evfd} { + // A new descriptor in a new set, which fails only for want of memory + unix.EpollCtl(r.epfd, unix.EPOLL_CTL_ADD, fd, &unix.EpollEvent{Events: unix.EPOLLIN, Fd: int32(fd)}) + } + return r, nil +} + +// read returns the next record, waiting for one, or errRingClosed once close is called. +func (r *ringReader) read() ([]byte, error) { + page := uint64(os.Getpagesize()) + cons := (*uint64)(unsafe.Pointer(&r.cons[0])) + prod := (*uint64)(unsafe.Pointer(&r.prod[0])) + events := make([]unix.EpollEvent, 2) + for { + // A record the program is still writing is waited for like none: committing it wakes + // the reader that has not consumed it. The program discards none. + if c := atomic.LoadUint64(cons); c < atomic.LoadUint64(prod) { + off := page + c&r.mask + if hdr := atomic.LoadUint32((*uint32)(unsafe.Pointer(&r.prod[off]))); hdr&unix.BPF_RINGBUF_BUSY_BIT == 0 { + n := uint64(hdr) + rec := append([]byte(nil), r.prod[off+unix.BPF_RINGBUF_HDR_SZ:][:n]...) + atomic.StoreUint64(cons, c+(n+unix.BPF_RINGBUF_HDR_SZ+7)&^7) + return rec, nil + } + } + n, err := unix.EpollWait(r.epfd, events, -1) + if errors.Is(err, unix.EINTR) { + continue + } + if err != nil { + return nil, err + } + for _, e := range events[:n] { + if int(e.Fd) == r.evfd { + return nil, errRingClosed + } + } + } +} + +// close makes read return; free releases the reader once nothing reads. +func (r *ringReader) close() { unix.Write(r.evfd, []byte{1, 0, 0, 0, 0, 0, 0, 0}) } + +func (r *ringReader) free() { + for _, b := range [][]byte{r.cons, r.prod} { + if b != nil { + unix.Munmap(b) + } + } + for _, fd := range []int{r.epfd, r.evfd} { + if fd >= 0 { + unix.Close(fd) + } + } +} diff --git a/bpf/nat64.c b/bpf/nat64.c new file mode 100644 index 0000000..9dbf1ae --- /dev/null +++ b/bpf/nat64.c @@ -0,0 +1,801 @@ +// 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 + +#define IP_MF 0x2000 +#define IP_DF 0x4000 +#define IP_OFFSET 0x1fff + +#define IPV6_MIN_MTU 1280 +#define IPV4_MIN_MTU 68 + +#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; + h = (h ^ a->s6_addr32[1]) * 0x85ebca6b; + h = (h ^ a->s6_addr32[2]) * 0xc2b2ae35; + h = (h ^ a->s6_addr32[3]) * 0x27d4eb2f; + 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) +{ + 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; +} + +// 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) +{ + __be32 *v = bpf_map_lookup_elem(&v4_of, src); + if (v) { + *out = *v; + return 0; + } + __u32 h = hash6(src); +#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)) + continue; + if (!bpf_map_update_elem(&v4_of, src, &a, BPF_NOEXIST)) { + *out = a; + return 0; + } + // Another CPU mapped the same source meanwhile + bpf_map_delete_elem(&v6_of, &a); + v = bpf_map_lookup_elem(&v4_of, src); + if (!v) + return -1; + *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; +} + +// 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), 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); +} + +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); + // 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); +} + +// 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), 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); +} + +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); + 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); +} + +SEC("tc") +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); + } + if (skb->protocol == bpf_htons(ETH_P_IPV6)) + return six_to_four(skb); + if (skb->protocol == bpf_htons(ETH_P_IP)) + return four_to_six(skb); + return TC_ACT_SHOT; +} + +char LICENSE[] SEC("license") = "Dual MIT/GPL"; diff --git a/bpf_integration_test.go b/bpf_integration_test.go new file mode 100644 index 0000000..87653f9 --- /dev/null +++ b/bpf_integration_test.go @@ -0,0 +1,54 @@ +//go:build linux && integration + +package main + +import ( + "strings" + "testing" + + "golang.org/x/sys/unix" +) + +// A program whose read-only data cannot be written is refused before it is loaded. +func TestBPFLoadRefusesUnwritableData(t *testing.T) { + _, err := bpfLoad(bpfProgram{ + maps: []bpfMapSpec{{name: ".rodata", typ: unix.BPF_MAP_TYPE_ARRAY, keySize: 4, valueSize: 4, maxEntries: 1, flags: unix.BPF_F_RDONLY}}, + rodata: make([]byte, 4), + }) + if err == nil || !strings.Contains(err.Error(), "read-only data") { + t.Fatalf("want the data refused, got %v", err) + } +} + +// A ring buffer reader needs a map it can map, twice over past the first page; once its epoll +// set is gone, reading fails. +func TestRingReaderFailures(t *testing.T) { + for name, spec := range map[string]bpfMapSpec{ + "a hash map": {typ: unix.BPF_MAP_TYPE_HASH, keySize: 4, valueSize: 4, maxEntries: 1}, + "a one-page array": {typ: unix.BPF_MAP_TYPE_ARRAY, keySize: 4, valueSize: 8, maxEntries: 1, flags: unix.BPF_F_MMAPABLE}, + } { + m, err := bpfMapCreate(spec) + if err != nil { + t.Fatal(err) + } + if _, err := newRingReader(m); err == nil { + t.Errorf("%s: want the reader refused", name) + } + m.close() + } + m, err := bpfMapCreate(bpfMapSpec{typ: unix.BPF_MAP_TYPE_RINGBUF, maxEntries: 1 << 12}) + if err != nil { + t.Fatal(err) + } + defer m.close() + r, err := newRingReader(m) + if err != nil { + t.Fatal(err) + } + unix.Close(r.epfd) + if _, err := r.read(); err == nil { + t.Fatal("want reading without an epoll set to fail") + } + r.epfd = -1 + r.free() +} diff --git a/docs/recipes.ja.md b/docs/recipes.ja.md index 4ab7790..fbe2785 100644 --- a/docs/recipes.ja.md +++ b/docs/recipes.ja.md @@ -28,7 +28,7 @@ sudo sixup -wan eth0 -lan eth1 | [DNS サーバーを指定する](#dns-サーバーを指定する) | `-ra-dns self`、またはサーバーのアドレス | | [DHCPv6 でアドレスを配布する](#dhcpv6-でアドレスを配布する) | `-dhcp6s-mode stateful` | | [下流のルーター](#下流のルーター) | 上流のルーターに `-dhcp6s-mode stateless` | -| [IPv6 シングルスタックまたは IPv6-mostly の LAN](#ipv6-シングルスタックまたは-ipv6-mostly-の-lan) | `-nat64 jool` | +| [IPv6 シングルスタックまたは IPv6-mostly の LAN](#ipv6-シングルスタックまたは-ipv6-mostly-の-lan) | `-nat64 bpf` | | [LAN のホストの後ろにある別のネットワーク](#lan-のホストの後ろにある別のネットワーク) | `-ra-route`、`-source-filter=false` | | [コンテナで動かす](#コンテナで動かす) | `-no-sysctl`、`-state-dir` | @@ -274,13 +274,15 @@ sudo sixup -wan eth0 -lan eth1 LAN は IPv6 だけで運用できます。IPv4 アドレスも DHCPv4 も要らず、二つのプロトコルを並行して保守する必要もありません。クライアントは NAT64 を通じて引き続き IPv4 のインターネットに届き、その IPv6 パケットはルーターで IPv4 に変換されます。IPv6 シングルスタックへの移行は、ネットワークが IPv6 のためにできる最も意味のあることの一つです。そうしたネットワークが一つ増えるたびに、誰かが IPv4 を残し続ける理由が一つ減るからです。そうするつもりなら、この節はあなたのためのものです。 -Jool のモジュールをインストールし、NAT64 を有効にして sixup を起動します。モジュールが読み込まれていなければ、sixup が `/sbin/modprobe jool` で読み込みます。名前空間とは IPv4 の /31 `192.168.255.254/31` でつなぎます。これがホストのアドレスやルートと重なると sixup は起動しないので、`-jool-ipv4` で別のものを指定します。 +NAT64 を有効にして sixup を起動します。`-nat64 bpf` は sixup 自身の変換器で、何もインストールする必要がなく、Linux 6.6 以降だけが必要です。 ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool +sudo sixup -wan eth0 -lan eth1 -nat64 bpf ``` -Jool は veth `sixup-nat64` の向こうの専用のネットワーク名前空間で動き、`64:ff9b::/96` を LAN とルーター自身のために変換します。変換している間、RA はそのプレフィックスを広告します(PREF64、RFC 8781)。変換後の IPv4 は、ルーターの IPv4 ルートに従って出ていきます。MAP-E のトンネルを通るときはほかの通信と回線のポートセットを共有し、トンネルがなければ設定済みのほかの IPv4 の出口を使います。ファイアウォールは `sixup-nat64` を通る通信を許可する必要があります。 +ダミーデバイス `sixup-nat64` 上で `64:ff9b::/96` を LAN とルーター自身のためにカーネル内で変換します。IPv6 のクライアントはそれぞれ `198.18.0.0/15` から自分の IPv4 アドレスを割り当てられ、ルーターの IPv4 の送信元 NAT が、背後のほかのホストと同じようにそれを隠します。MAP-E のトンネルを通るときは、変換後の通信がほかの通信と回線のポートセットを共有し、トンネルがなければ設定済みのほかの IPv4 の出口を使います。このプールがホストのアドレスやルートと重なると sixup は起動しません。変換している間、RA はそのプレフィックスを広告します(PREF64、RFC 8781)。ファイアウォールは `sixup-nat64` を通る通信を許可する必要があります。 + +古いカーネルでは、代わりに `-nat64 jool` で Jool のカーネルモジュール(4.1 以降)を使います。同じ名前の veth の向こうの専用のネットワーク名前空間で動きます。モジュールが読み込まれていなければ、sixup が `/sbin/modprobe jool` で読み込みます。名前空間とは IPv4 の /31 `192.168.255.254/31` でつなぎます。これがホストのアドレスやルートと重なると sixup は起動しないので、`-jool-ipv4` で別のものを指定します。 クライアントが IPv4 に届く方法は二つあります。 @@ -301,13 +303,13 @@ RA の PREF64 に従って CLAT を有効にする組み込みの対応状況で NAT64 を通してプライベートな IPv4 アドレスに届くには、ネットワーク固有のプレフィックスを使います。Well-Known Prefix ではプライベートアドレスを扱えない場合があるからです(RFC 6052)。 ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool -nat64-prefix fd00:64::/96 +sudo sixup -wan eth0 -lan eth1 -nat64 bpf -nat64-prefix fd00:64::/96 ``` クライアントに CLAT を有効にさせず、DNS64 だけで NAT64 を提供するには、プレフィックスを RA に載せないようにします。 ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool -ra-pref64 off +sudo sixup -wan eth0 -lan eth1 -nat64 bpf -ra-pref64 off ``` ## LAN のホストの後ろにある別のネットワーク diff --git a/docs/recipes.md b/docs/recipes.md index f8f95de..608a591 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -30,7 +30,7 @@ both. | [Choosing the DNS servers](#choosing-the-dns-servers) | `-ra-dns self`, or the servers' addresses | | [Addresses handed out by DHCPv6](#addresses-handed-out-by-dhcpv6) | `-dhcp6s-mode stateful` | | [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 jool` | +| [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` | | [Running in a container](#running-in-a-container) | `-no-sysctl`, `-state-dir` | @@ -398,19 +398,26 @@ packets at the router. Going IPv6-only is one of the most useful things a networ IPv6, since every such network is one less reason for anyone to keep IPv4 around; if that is your plan, this section is for you. -Install the Jool module and start sixup with NAT64. If the module is not loaded, sixup loads it with `/sbin/modprobe jool`. -The namespace is reached through the IPv4 /31 `192.168.255.254/31`; when that overlaps an address -or a route of the host, sixup refuses to start, and `-jool-ipv4` gives another. +Start sixup with NAT64. `-nat64 bpf` is sixup's own translator: it needs nothing installed, only +Linux 6.6 or later. ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool +sudo sixup -wan eth0 -lan eth1 -nat64 bpf ``` -Jool runs in a network namespace of its own behind the veth `sixup-nat64` and translates -`64:ff9b::/96`, for the LAN and for the router itself. While it translates, the RA announces the -prefix (PREF64, RFC 8781). The translated IPv4 leaves by the router's IPv4 route. Through a MAP-E -tunnel it shares the line's port set with everything else, and without a tunnel it takes -whatever IPv4 uplink is configured. A firewall has to let traffic through `sixup-nat64`. +It translates `64:ff9b::/96` on the dummy device `sixup-nat64`, for the LAN and for the router +itself, in the kernel. Each IPv6 client gets an IPv4 address of its own from `198.18.0.0/15`, and +the router's IPv4 source NAT hides it like any other host behind it: through a MAP-E tunnel the +translated traffic shares the line's port set with everything else, and without a tunnel it +takes whatever IPv4 uplink is configured. When that pool overlaps an address or a route of the +host, sixup refuses to start. While it translates, the RA announces the prefix (PREF64, +RFC 8781). A firewall has to let traffic through `sixup-nat64`. + +On an older kernel, `-nat64 jool` runs the Jool kernel module (4.1 or later) instead, in a +network namespace of its own behind a veth of the same name. If the module is not loaded, sixup +loads it with `/sbin/modprobe jool`. The namespace is reached through the IPv4 /31 +`192.168.255.254/31`; when that overlaps an address or a route of the host, sixup refuses to +start, and `-jool-ipv4` gives another. Clients reach IPv4 in one of two ways. @@ -439,14 +446,14 @@ To reach private IPv4 addresses through NAT64, which the well-known prefix may n (RFC 6052), use a network-specific prefix. ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool -nat64-prefix fd00:64::/96 +sudo sixup -wan eth0 -lan eth1 -nat64 bpf -nat64-prefix fd00:64::/96 ``` To offer NAT64 through DNS64 only, without clients turning on their CLAT, keep the prefix out of the RA. ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool -ra-pref64 off +sudo sixup -wan eth0 -lan eth1 -nat64 bpf -ra-pref64 off ``` ## Another network behind a LAN host diff --git a/docs/recipes.zh.md b/docs/recipes.zh.md index d1a2336..a12b420 100644 --- a/docs/recipes.zh.md +++ b/docs/recipes.zh.md @@ -26,7 +26,7 @@ sudo sixup -wan eth0 -lan eth1 | [指定 DNS 服务器](#指定-dns-服务器) | `-ra-dns self`,或服务器的地址 | | [用 DHCPv6 分配地址](#用-dhcpv6-分配地址) | `-dhcp6s-mode stateful` | | [下级路由器](#下级路由器) | 上级路由器加 `-dhcp6s-mode stateless` | -| [IPv6 单栈或 IPv6-mostly 内网](#ipv6-单栈或-ipv6-mostly-内网) | `-nat64 jool` | +| [IPv6 单栈或 IPv6-mostly 内网](#ipv6-单栈或-ipv6-mostly-内网) | `-nat64 bpf` | | [内网主机后面的另一个网络](#内网主机后面的另一个网络) | `-ra-route`、`-source-filter=false` | | [在容器里运行](#在容器里运行) | `-no-sysctl`、`-state-dir` | @@ -272,13 +272,15 @@ sudo sixup -wan eth0 -lan eth1 内网可以只用 IPv6,没有 IPv4 地址,没有 DHCPv4,也不用同时维护两套协议。客户端仍然可以通过 NAT64 访问 IPv4 互联网,由路由器把它们的 IPv6 包转换成 IPv4。改用 IPv6 单栈,是一个网络能为 IPv6 做的最有意义的事情之一,因为每多一个这样的网络,大家继续保留 IPv4 的理由就少一个。如果你打算这样做,这一节就是为你写的。 -安装 Jool 模块,再启用 NAT64。如果模块没有加载,sixup 会用 `/sbin/modprobe jool` 加载它。与命名空间之间通过 IPv4 /31 `192.168.255.254/31` 相连;它与本机的地址或路由重叠时,sixup 拒绝启动,用 `-jool-ipv4` 指定另一个。 +启动 sixup 时启用 NAT64。`-nat64 bpf` 是 sixup 自带的转换器,不需要安装任何东西,只要求 Linux 6.6 及以上。 ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool +sudo sixup -wan eth0 -lan eth1 -nat64 bpf ``` -Jool 运行在独立的网络命名空间里,经 veth `sixup-nat64` 相连,转换 `64:ff9b::/96`,内网和路由器自身都能使用。转换期间,RA 会通告这个前缀(PREF64,RFC 8781)。转换出的 IPv4 按路由器的 IPv4 路由出去。走 MAP-E 隧道时,它和其他流量共用线路的端口集;没有隧道时,从已配置的其他 IPv4 出口出去。防火墙需要放行经过 `sixup-nat64` 的流量。 +它在 dummy 设备 `sixup-nat64` 上转换 `64:ff9b::/96`,内网和路由器自身都能使用,转换在内核中完成。每个 IPv6 客户端从 `198.18.0.0/15` 分到一个自己的 IPv4 地址,再由路由器的 IPv4 源地址转换隐藏,和路由器后面的其他主机一样。走 MAP-E 隧道时,转换后的流量和其他流量共用线路的端口集;没有隧道时,从已配置的其他 IPv4 出口出去。这个地址池与本机的地址或路由重叠时,sixup 拒绝启动。转换期间,RA 会通告这个前缀(PREF64,RFC 8781)。防火墙需要放行经过 `sixup-nat64` 的流量。 + +内核较旧时,可以改用 `-nat64 jool`,运行 Jool 内核模块(4.1 及以后),它在独立的网络命名空间里,经同名的 veth 相连。如果模块没有加载,sixup 会用 `/sbin/modprobe jool` 加载它。与命名空间之间通过 IPv4 /31 `192.168.255.254/31` 相连;它与本机的地址或路由重叠时,sixup 拒绝启动,用 `-jool-ipv4` 指定另一个。 客户端访问 IPv4 有两种方式。 @@ -299,13 +301,13 @@ Jool 运行在独立的网络命名空间里,经 veth `sixup-nat64` 相连, 要通过 NAT64 访问私有 IPv4 地址,需要用网络专用前缀,因为知名前缀可能不能用于私有地址(RFC 6052)。 ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool -nat64-prefix fd00:64::/96 +sudo sixup -wan eth0 -lan eth1 -nat64 bpf -nat64-prefix fd00:64::/96 ``` 只想通过 DNS64 提供 NAT64,不让客户端启用 CLAT,就不在 RA 中通告前缀。 ```sh -sudo sixup -wan eth0 -lan eth1 -nat64 jool -ra-pref64 off +sudo sixup -wan eth0 -lan eth1 -nat64 bpf -ra-pref64 off ``` ## 内网主机后面的另一个网络 diff --git a/docs/standards.md b/docs/standards.md index baf2548..792a7ef 100644 --- a/docs/standards.md +++ b/docs/standards.md @@ -455,7 +455,11 @@ Tested by `TestNATPMP` and `TestPCPServerAgainstKernel`. ports are kept inside the line's port set by a port restricted NAT, as a MAP-E CE must. - **RFC 6052, NAT64 prefixes.** The well-known `64:ff9b::/96` by default, or a network specific prefix with `-nat64-prefix`. -- **RFC 6146, stateful NAT64.** Done by Jool with `-nat64 jool`. +- **RFC 7915, IP/ICMP translation, and RFC 6146, stateful NAT64.** `-nat64 bpf` translates + statelessly in BPF, with an IPv4 address from a private pool for each IPv6 source, and leaves + the state to conntrack and the IPv4 source NAT, which together make a stateful NAT64. Fragmented + ICMP and IPv4 UDP without a checksum, which need the whole datagram, are reassembled and + translated by sixup itself. `-nat64 jool` uses Jool's stateful NAT64 instead. - **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. diff --git a/firewall.go b/firewall.go index 6f383f8..11dd4a7 100644 --- a/firewall.go +++ b/firewall.go @@ -70,7 +70,7 @@ type firewall struct { lans []string // the interfaces sixup serves inbound bool // filter unsolicited traffic from the WAN source bool // refuse LAN sources outside ours (-source-filter) - nat64 bool // Jool translates behind joolOutside + nat64 bool // a translator sits behind joolOutside holeIn chan []portMapping // PCP's pinholes, the latest set replacing the last delegIn chan []netip.Prefix // the prefixes delegated to downstream routers, likewise holes *nftables.Set diff --git a/flags.go b/flags.go index 27bdaa8..926a5d5 100644 --- a/flags.go +++ b/flags.go @@ -54,9 +54,9 @@ var ( tunMTU = flag.Int("tunnel-mtu", 0, "MTU of the tunnel device, 0 means the WAN interface MTU minus 40") tunMetric4 = flag.Uint("tunnel-route4-metric", 4096, "metric of the IPv4 default route added through the tunnel device once its IPv4 is known; the metric is high on purpose, so an existing IPv4 default route keeps winning and the tunnel only takes over when there is none. 0 adds no route") tunCap = flag.Bool("tunnel-capture", true, "when running as a daemon and DHCPv6 sends no tunnel option, capture tunnel traffic to infer the parameters") - nat64 = flag.String("nat64", "off", "which NAT64 implementation to configure: jool uses the Jool kernel module (https://jool.mx, 4.1 or later; when it is not loaded, sixup runs /sbin/modprobe jool once), run in a network namespace of its own behind the veth sixup-nat64, translating -nat64-prefix for the LAN and this router alike. The namespace exists, and the prefix is advertised in the RA unless -ra-pref64 is off, only while the module is loaded, which is checked every minute. The IPv4 output leaves by this host's IPv4 route, through the tunnel's source NAT or any other, so a MAP-E line's ports have one allocator. A firewall has to let traffic through sixup-nat64: the LAN's, forwarded, and the answers to this router's own, arriving on it. off configures none. DNS64 is not part of this and is left to a resolver of your choosing") + nat64 = flag.String("nat64", "off", "which NAT64 to run, translating -nat64-prefix for the LAN and this router behind the device sixup-nat64, which a firewall has to let traffic through: bpf, recommended, is sixup's own translator and needs only Linux 6.6 or later; it numbers IPv6 sources from 198.18.0.0/15, which must be unused on this host, and leaves the rest to the IPv4 source NAT, so a MAP-E line's ports have one allocator. jool uses the Jool kernel module (https://jool.mx, 4.1 or later, loaded with /sbin/modprobe jool when it is not) in a network namespace of its own, for older kernels; it runs, and the prefix is advertised, only while the module is loaded. off runs none. DNS64 is left to a resolver of your choosing") nat64Prefix = flag.String("nat64-prefix", nat64WKP.String(), "prefix the NAT64 translates (RFC 6052), advertised in the RA while it does (see -ra-pref64). Reaching private IPv4 addresses needs a network-specific prefix such as fd00:64::/96, and one carved from the delegated prefix would change with it") - joolIPv4 = flag.String("jool-ipv4", "192.168.255.254/31", "the IPv4 /31 between this namespace and Jool's: the lower address is the gateway on this side, the upper one Jool's pool4. It must not overlap any address or route of this host") + joolIPv4 = flag.String("jool-ipv4", "192.168.255.254/31", "with -nat64 jool, the IPv4 /31 between this namespace and Jool's: the lower address is the gateway on this side, the upper one Jool's pool4. It must not overlap any address or route of this host") unsolicited = flag.String("unsolicited", "request", "unsolicited IPv6 from the WAN to the LAN (RFC 6092): request lets in only what the LAN started, endpoints that recently sent out, ports opened with PCP (UDP 5351), and the ICMPv6 and IPsec that must pass; deny does the same without PCP; allow lets it all in. PCP, and NAT-PMP for programs that know only it, also forward ports of the tunnel's IPv4 while sixup does its NAT. The RFC 7084 border for ULA and LAN prefixes holds in every mode") srcFilter = flag.Bool("source-filter", true, "let the LAN send to the WAN only from its own and the delegated prefixes, answering others with ICMPv6 code 5 (BCP 38), and the WAN reach only those; turn it off when another prefix is routed into the LAN") tunNAT = flag.String("tunnel-nat", "auto", "maintain the nftables table sixup for traffic leaving the tunnel device: auto configures the port-restricted source NAT a MAP-E customer edge is required to have (RFC 7597), an ordinary source NAT on a line with its own public IPv4, none on DS-Lite where the AFTR translates, and in every case an MSS clamp to the tunnel MTU; off writes no rules. Nothing outside that table is read or changed, and it is removed on exit") diff --git a/jool.go b/jool.go index 5be6e8b..833f4d7 100644 --- a/jool.go +++ b/jool.go @@ -691,11 +691,12 @@ func linkRequest(typ netlink.HeaderType, flags netlink.HeaderFlags, hdr, attrs [ return err } -// checkJoolIPv4 refuses a /31 that an address or a route of this host already covers: the two -// hosts behind it would become unreachable, or Jool's traffic would go to them. The default route -// covers everything and does not count, and neither does the veth of an earlier run, which the -// kernel may still be tearing down when sixup starts again. -func checkJoolIPv4(link netip.Prefix) error { +// checkIPv4Unused refuses a prefix that an address or a route of this host already covers: Jool's +// /31 or the BPF translator's pool, whose addresses would become unreachable, or whose traffic +// would go to them. The default route covers everything and does not count, and neither does +// sixup-nat64 of an earlier run, which the kernel may still be tearing down when sixup starts +// again. +func checkIPv4Unused(link netip.Prefix) error { ifaces, err := net.Interfaces() if err != nil { return err diff --git a/jool_integration_test.go b/jool_integration_test.go index 12847a9..fd4d982 100644 --- a/jool_integration_test.go +++ b/jool_integration_test.go @@ -30,7 +30,7 @@ func TestJoolNamespaceAgainstKernel(t *testing.T) { enterNetNS(t) loUp(t) link := netip.MustParsePrefix("192.168.255.254/31") - if err := checkJoolIPv4(link); err != nil { + if err := checkIPv4Unused(link); err != nil { t.Fatalf("an empty namespace has nothing to overlap: %v", err) } m := &joolManager{prefix: nat64WKP, link: link} @@ -92,7 +92,7 @@ func TestJoolNamespaceAgainstKernel(t *testing.T) { // A second run may start while the kernel still tears down the first one's namespace: its veth // is no conflict, and building a new pair replaces it - if err := checkJoolIPv4(link); err != nil { + if err := checkIPv4Unused(link); err != nil { t.Fatalf("the veth of an earlier run must not count as a conflict: %v", err) } srv.Close() @@ -114,7 +114,7 @@ func TestJoolNamespaceAgainstKernel(t *testing.T) { if err := routeSet(1, netip.MustParsePrefix("192.168.255.0/24"), netip.Addr{}, 0, 0); err != nil { t.Fatal(err) } - if err := checkJoolIPv4(link); err == nil { + if err := checkIPv4Unused(link); err == nil { t.Fatal("a /31 inside another route must be refused") } routeDel(1, netip.MustParsePrefix("192.168.255.0/24"), netip.Addr{}, 0) @@ -218,7 +218,7 @@ func TestJoolStartWithoutTheModule(t *testing.T) { joolSkipIfLoaded(t) enterNetNS(t) loUp(t) - if err := checkJoolIPv4(netip.MustParsePrefix("127.0.0.0/31")); err == nil || !strings.Contains(err.Error(), "overlaps the address") { + if err := checkIPv4Unused(netip.MustParsePrefix("127.0.0.0/31")); err == nil || !strings.Contains(err.Error(), "overlaps the address") { t.Fatalf("a /31 inside lo's 127.0.0.0/8 must be refused, got %v", err) } m := &joolManager{prefix: nat64WKP, link: netip.MustParsePrefix("192.168.255.254/31")} @@ -263,7 +263,7 @@ func TestJoolStartWithoutTheModule(t *testing.T) { t.Fatalf("with room for %d descriptors, want EMFILE, got %v", extra, err) } } - if err := joolWithFDs(t, 0, func() error { return checkJoolIPv4(m.link) }); !errors.Is(err, unix.EMFILE) { + if err := joolWithFDs(t, 0, func() error { return checkIPv4Unused(m.link) }); !errors.Is(err, unix.EMFILE) { t.Fatalf("listing the interfaces needs a socket, got %v", err) } if err := joolWithFDs(t, 0, func() error { _, err := routes4(); return err }); !errors.Is(err, unix.EMFILE) { diff --git a/jool_stub.go b/jool_stub.go index 6c0d2be..1c9cb0c 100644 --- a/jool_stub.go +++ b/jool_stub.go @@ -17,4 +17,4 @@ type joolManager struct { func (m *joolManager) run(ctx context.Context) { <-ctx.Done() } -func checkJoolIPv4(netip.Prefix) error { return nil } +func checkIPv4Unused(netip.Prefix) error { return nil } diff --git a/main.go b/main.go index cb9029f..6e3b0e0 100644 --- a/main.go +++ b/main.go @@ -48,6 +48,10 @@ var licenseText string //go:embed NOTICE var noticeText string +// bpfPool is the private IPv4 pool -nat64 bpf numbers IPv6 sources from. The source NAT on the +// way out hides these addresses like those of any other IPv4 host behind the router. +var bpfPool = netip.MustParsePrefix("198.18.0.0/15") + // nat64WKP is the well-known prefix of RFC 6052, the default of -nat64-prefix. Carving one out of // the delegated prefix would tie the translator to a prefix that changes, and every client would // have to be told again. @@ -135,8 +139,8 @@ func main() { if *tunNAT != "auto" && *tunNAT != "off" { fatalf("-tunnel-nat must be auto / off") } - if *nat64 != "jool" && *nat64 != "off" { - fatalf("-nat64 must be jool / off") + if *nat64 != "jool" && *nat64 != "bpf" && *nat64 != "off" { + fatalf("-nat64 must be jool / bpf / off") } joolLink, err := netip.ParsePrefix(*joolIPv4) if err != nil || !joolLink.Addr().Is4() || joolLink.Bits() != 31 { @@ -161,10 +165,15 @@ func main() { } // Refused before anything is configured, so a conflict leaves the host as it was if *nat64 == "jool" && !dryRun { - if err := checkJoolIPv4(joolLink); err != nil { + if err := checkIPv4Unused(joolLink); err != nil { fatalf("-jool-ipv4: %v", err) } } + if *nat64 == "bpf" && !dryRun { + if err := checkIPv4Unused(bpfPool); err != nil { + fatalf("-nat64 bpf numbers IPv6 sources from %s, but %v", bpfPool, err) + } + } layout := shared64Layout(*shared64) switch layout { case "wan", "lan", "split": @@ -281,6 +290,10 @@ func main() { go nat.run(ctx, store.Subscribe()) } } + var names []string + for _, l := range lanDefs { + names = append(names, l.iface) + } if *nat64 == "jool" { // Jool's IPv4 output comes back through the veth and is forwarded out of this namespace if !*noSysctl { @@ -290,12 +303,17 @@ func main() { } go (&joolManager{prefix: nat64Pfx, link: joolLink, store: store}).run(ctx) } - var names []string - for _, l := range lanDefs { - names = append(names, l.iface) + if *nat64 == "bpf" { + // The translated IPv4 comes in on the dummy device and is forwarded out of it + if !*noSysctl { + if err := sysctlWrite("/proc/sys/net/ipv4/ip_forward", "1"); err != nil { + warnf("[sysctl] ipv4/ip_forward=1 failed: %v", err) + } + } + go (&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 == "jool", holeIn: make(chan []portMapping, 1), delegIn: make(chan []netip.Prefix, 1)} + 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" { p := &pcpServer{lans: names, nat: nat} diff --git a/nat64_bpf.go b/nat64_bpf.go new file mode 100644 index 0000000..e04989b --- /dev/null +++ b/nat64_bpf.go @@ -0,0 +1,38 @@ +// Code generated by tools/bpfgen from bpf/nat64.c; DO NOT EDIT. + +//go:build linux + +package main + +const nat64License = "Dual MIT/GPL" + +// The maps, the read-only data last +const ( + nat64MapStats = 0 + nat64MapSlowBuf = 1 + nat64MapSlow = 2 + nat64MapSlowFrags = 3 + nat64MapV6Of = 4 + nat64MapV4Of = 5 + nat64MapRodata = 6 +) + +var nat64Maps = []bpfMapSpec{ + nat64MapStats: {name: "stats", typ: 6, keySize: 4, valueSize: 8, maxEntries: 8, flags: 0x0}, + nat64MapSlowBuf: {name: "slow_buf", typ: 6, keySize: 4, valueSize: 16384, maxEntries: 1, flags: 0x0}, + nat64MapSlow: {name: "slow", typ: 27, keySize: 0, valueSize: 0, maxEntries: 4194304, flags: 0x0}, + nat64MapSlowFrags: {name: "slow_frags", typ: 9, keySize: 12, valueSize: 1, maxEntries: 1024, flags: 0x0}, + nat64MapV6Of: {name: "v6_of", typ: 1, keySize: 4, valueSize: 16, maxEntries: 65536, flags: 0x0}, + nat64MapV4Of: {name: "v4_of", typ: 1, keySize: 16, valueSize: 4, maxEntries: 65536, flags: 0x0}, + nat64MapRodata: {name: ".rodata", typ: 2, keySize: 4, valueSize: 48, maxEntries: 1, flags: 0x80}, +} + +// 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} +) diff --git a/nat64_bpfeb.bin b/nat64_bpfeb.bin new file mode 100644 index 0000000..5a3ec4b Binary files /dev/null and b/nat64_bpfeb.bin differ diff --git a/nat64_bpfeb.go b/nat64_bpfeb.go new file mode 100644 index 0000000..87d8077 --- /dev/null +++ b/nat64_bpfeb.go @@ -0,0 +1,155 @@ +// Code generated by tools/bpfgen from bpf/nat64.c; DO NOT EDIT. + +//go:build linux && (mips || mips64 || ppc64 || s390x) + +package main + +import _ "embed" + +//go:embed nat64_bpfeb.bin +var nat64Code []byte + +var nat64Rodata = []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} + +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: 204, m: nat64MapStats, off: -1}, + {insn: 210, m: nat64MapRodata, off: 8}, + {insn: 217, m: nat64MapRodata, off: 4}, + {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: 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}, +} diff --git a/nat64_bpfel.bin b/nat64_bpfel.bin new file mode 100644 index 0000000..52a7e1a Binary files /dev/null and b/nat64_bpfel.bin differ diff --git a/nat64_bpfel.go b/nat64_bpfel.go new file mode 100644 index 0000000..fe999fd --- /dev/null +++ b/nat64_bpfel.go @@ -0,0 +1,149 @@ +// Code generated by tools/bpfgen from bpf/nat64.c; DO NOT EDIT. + +//go:build linux && (386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64) + +package main + +import _ "embed" + +//go:embed nat64_bpfel.bin +var nat64Code []byte + +var nat64Rodata = []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} + +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: 356, m: nat64MapSlowBuf, off: -1}, + {insn: 367, m: nat64MapStats, off: -1}, + {insn: 382, m: nat64MapSlow, off: -1}, + {insn: 393, m: nat64MapStats, off: -1}, + {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: 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}, +} diff --git a/nat64bpf.go b/nat64bpf.go new file mode 100644 index 0000000..d14062e --- /dev/null +++ b/nat64bpf.go @@ -0,0 +1,338 @@ +//go:build linux + +package main + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "net" + "net/netip" + "os" + "slices" + "strings" + "time" + + "github.com/mdlayher/netlink" + "golang.org/x/sys/unix" +) + +//go:generate go run ./tools/bpfgen bpf/nat64.c nat64 + +// The counters of bpf/nat64.c, in its order; the unnamed ones count no drops +var bpfStatNames = []string{"", "", "protocol", "", "icmp", "pool-full", "unmapped", "error"} + +// bpfNAT64 translates the NAT64 prefix with the program in bpf/nat64.c, attached to the egress of +// a dummy device that the prefix and the pool are routed to. Translated packets come back in on +// the same device as the other family, so conntrack and the source NAT see an ordinary IPv4 host. +// What the program cannot translate a fragment at a time goes through slowPath. The program and +// the device go when sixup does: the tcx link belongs to its descriptor. +type bpfNAT64 struct { + prefix netip.Prefix + lans []string // whose smallest MTU bounds what the pool route lets through whole + store *Store + idle map[[4]byte]int // pool addresses conntrack had no flow for, by how many sweeps in a row + + // While translating + ifindex int + objs *bpfObjects + link *os.File // the tcx link, which detaches the program when closed + slow *slowPath + reader *ringReader + served chan struct{} // closed when the slow path stops reading +} + +// bpfTick is how often the mappings are swept and the drops reported. +var bpfTick = time.Minute + +// bpfIdleSweeps is how many sweeps in a row a pool address has to go without a flow before its +// mapping is removed, so that a flow conntrack has not confirmed yet is not cut. +const bpfIdleSweeps = 2 + +func (b *bpfNAT64) run(ctx context.Context) { + if err := b.start(); err != nil { + warnf("[nat64] %v", err) + return + } + defer func() { + b.stop() + b.store.SetNAT64(netip.Prefix{}) + }() + b.store.SetNAT64(b.prefix) + infof("[nat64] translating %s through %s with BPF, IPv6 sources numbered from %s", b.prefix, joolOutside, bpfPool) + tick := time.NewTicker(bpfTick) + defer tick.Stop() + var last string + for { + select { + case <-ctx.Done(): + return + case <-tick.C: + } + b.sweep() + if s := b.drops(); s != last { + last = s + infof("[nat64] dropped so far: %s", s) + } + } +} + +// start sets the translator up, and on failure undoes what it did. +func (b *bpfNAT64) start() (err error) { + ifi, err := dummyAdd(joolOutside) + if err != nil { + return fmt.Errorf("cannot create %s: %w", joolOutside, err) + } + b.ifindex = ifi.Index + defer func() { + if err != nil { + b.stop() + } + }() + if err := b.load(); err != nil { + return err + } + 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 { + return err + } + if b.reader, err = newRingReader(b.objs.maps[nat64MapSlow]); err != nil { + return fmt.Errorf("reading the slow path: %w", err) + } + b.served = make(chan struct{}) + go func() { + b.slow.serve(b.reader) + close(b.served) + }() + return nil +} + +// stop takes down whatever start set up. +func (b *bpfNAT64) stop() { + if b.reader != nil { + b.reader.close() + <-b.served + b.reader.free() + b.reader = nil + } + if b.slow != nil { + b.slow.close() + b.slow = nil + } + if b.link != nil { + b.link.Close() + b.link = nil + } + if b.objs != nil { + b.objs.close() + b.objs = nil + } + if b.ifindex != 0 { + linkDel(b.ifindex) + b.ifindex = 0 + } +} + +func (b *bpfNAT64) load() error { + ro := slices.Clone(nat64Rodata) + set := func(v bpfVar, b []byte) { copy(ro[v.off:v.off+v.size], b) } + var mask [16]byte + for i := range b.prefix.Bits() { + mask[i/8] |= 0x80 >> (i % 8) + } + pref, pos := b.prefix.Masked().Addr().As16(), v4Positions(b.prefix.Bits()) + set(nat64VarPoolBase, nativeEndian.AppendUint32(nil, binary.BigEndian.Uint32(bpfPool.Addr().AsSlice()))) + set(nat64VarPoolMask, nativeEndian.AppendUint32(nil, uint32(1)<<(32-bpfPool.Bits())-1)) + set(nat64VarPref6, pref[:]) + set(nat64VarMask6, mask[:]) + set(nat64VarV4pos, pos[:]) + set(nat64VarSlowMark, nativeEndian.AppendUint32(nil, bpfSlowMark)) + objs, err := bpfLoad(bpfProgram{code: nat64Code, maps: nat64Maps, relocs: nat64Relocs, rodata: ro, license: nat64License}) + if err != nil { + 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) + } + b.link = os.NewFile(uintptr(fd), "tcx") + return nil +} + +// route brings the device up and routes the NAT64 prefix and the pool to it. The device takes no +// address: it never answers for one, and NOARP spares the neighbour lookups. +// +// The pool's route carries an MTU 28 bytes below the smallest LAN's, the IPv6 header's 20 more +// and a fragment header's 8. IPv4 that conntrack reassembled is cut again to it on the way to the +// program, so each piece, translated, still crosses the LAN; a whole packet with DF that would +// not fit earns its sender a Fragmentation Needed from the router. +func (b *bpfNAT64) route(ifi *net.Interface) error { + err := errors.Join(linkSetUp(ifi.Index), routeSet(ifi.Index, b.prefix, netip.Addr{}, 0, 0), route4MTU(ifi.Index, bpfPool, b.lanMTU()-28)) + if err != nil { + return fmt.Errorf("bringing %s up and routing %s and %s to it: %w", ifi.Name, b.prefix, bpfPool, err) + } + return nil +} + +// lanMTU is the smallest MTU of the LANs, 1500 when none can be read. +func (b *bpfNAT64) lanMTU() int { + mtu := 0 + for _, name := range b.lans { + if ifi, err := net.InterfaceByName(name); err == nil && (mtu == 0 || ifi.MTU < mtu) { + mtu = ifi.MTU + } + } + if mtu < 1280 { + return 1500 + } + return mtu +} + +// sweep removes the mappings of pool addresses conntrack has had no flow for over bpfIdleSweeps +// sweeps, so that the pool and the maps do not fill with sources long gone. Without conntrack to +// ask, nothing is removed. +func (b *bpfNAT64) sweep() { + active, err := conntrackSources4(bpfPool) + if err != nil { + return + } + if b.idle == nil { + b.idle = map[[4]byte]int{} + } + v4of, v6of := b.objs.maps[nat64MapV4Of], b.objs.maps[nat64MapV6Of] + for _, key := range v6of.keys() { + a4 := [4]byte(key) + if active[netip.AddrFrom4(a4)] { + delete(b.idle, a4) + continue + } + if b.idle[a4]++; b.idle[a4] >= bpfIdleSweeps { + a6 := make([]byte, 16) + if v6of.lookup(key, a6) == nil { + v4of.delete(a6) + } + v6of.delete(key) + delete(b.idle, a4) + } + } +} + +// conntrackSources4 dumps conntrack's IPv4 flows and returns the original sources within pool. +func conntrackSources4(pool netip.Prefix) (map[netip.Addr]bool, error) { + c, err := nfDial() + if err != nil { + return nil, err + } + defer c.Close() + msgs, err := c.Execute(netlink.Message{ + Header: netlink.Header{Type: netlink.HeaderType(unix.NFNL_SUBSYS_CTNETLINK<>4 == 6 { + proto = unix.ETH_P_IPV6 + } + to := &unix.SockaddrLinklayer{Protocol: proto<<8 | proto>>8, Ifindex: p.dev.Index, Halen: 6} + copy(to.Addr[:], p.dev.HardwareAddr) + return unix.Sendto(p.fd, pkt, 0, to) +} + +// serve reads what the program hands over until the reader is closed. +func (p *slowPath) serve(rd *ringReader) { + for { + rec, err := rd.read() + if err != nil { + return + } + p.handle(rec) + } +} + +// handle takes one frame the program handed over. +func (p *slowPath) handle(frame []byte) { + if len(frame) < 14 { + p.dropped.Add(1) + return + } + if err := p.packet(frame[14:]); err != nil { + p.dropped.Add(1) + debugf("[nat64] slow path: %v", err) + } +} + +// packet takes an IP packet, a whole one or a fragment, and sends on what it completes. +func (p *slowPath) packet(pkt []byte) error { + var k slowKey + var hdr, payload []byte + var off int + var more bool + switch { + case len(pkt) >= 20 && pkt[0]>>4 == 4: + tot := int(binary.BigEndian.Uint16(pkt[2:4])) + if pkt[0]&0xf != 5 || tot < 20 || tot > len(pkt) { + return errors.New("malformed IPv4") + } + fo := binary.BigEndian.Uint16(pkt[6:8]) + hdr, payload = pkt[:20], pkt[20:tot] + off, more = int(fo&0x1fff)*8, fo&0x2000 != 0 + k = slowKey{netip.AddrFrom4([4]byte(pkt[12:16])), netip.AddrFrom4([4]byte(pkt[16:20])), uint32(binary.BigEndian.Uint16(pkt[4:6])), pkt[9]} + case len(pkt) >= 40 && pkt[0]>>4 == 6: + end := 40 + int(binary.BigEndian.Uint16(pkt[4:6])) + if end > len(pkt) { + return errors.New("malformed IPv6") + } + hdr, payload = pkt[:40], pkt[40:end] + k = slowKey{netip.AddrFrom16([16]byte(pkt[8:24])), netip.AddrFrom16([16]byte(pkt[24:40])), 0, pkt[6]} + if pkt[6] == unix.IPPROTO_FRAGMENT { + if len(payload) < 8 { + return errors.New("malformed fragment header") + } + fo := binary.BigEndian.Uint16(payload[2:4]) + off, more = int(fo>>3)*8, fo&1 != 0 + k.proto, k.id = payload[0], binary.BigEndian.Uint32(payload[4:8]) + payload = payload[8:] + } + default: + return errors.New("not IP") + } + if off == 0 && !more { + return p.whole(hdr, k.proto, payload) + } + hdr, whole, err := p.reassemble(k, hdr, off, more, payload) + if whole == nil || err != nil { + return err + } + return p.whole(hdr, k.proto, whole) +} + +// reassemble adds a fragment to its datagram, and returns the first fragment's header and the +// payload once all of it is there. Overlapping fragments drop the datagram (RFC 5722). +func (p *slowPath) reassemble(k slowKey, hdr []byte, off int, more bool, data []byte) ([]byte, []byte, error) { + now := p.now() + for key, g := range p.groups { + if now.After(g.expires) { + delete(p.groups, key) + p.dropped.Add(1) + } + } + g := p.groups[k] + if g == nil { + if len(p.groups) >= slowGroups { + return nil, nil, errors.New("too many datagrams being reassembled") + } + g = &slowGroup{parts: map[int][]byte{}, total: -1, expires: now.Add(slowTimeout)} + p.groups[k] = g + } + if off == 0 { + g.hdr = slices.Clone(hdr) + if hdr[0]>>4 == 6 { + g.hdr[6] = k.proto // the header the datagram has without its fragment header + } + } + if !more { + g.total = off + len(data) + } + if (more && len(data)%8 != 0) || off+len(data) > 0xffff || (g.total >= 0 && off+len(data) > g.total) { + delete(p.groups, k) + return nil, nil, errors.New("malformed fragment") + } + for o, d := range g.parts { + if off < o+len(d) && o < off+len(data) { + delete(p.groups, k) + return nil, nil, errors.New("overlapping fragments") + } + } + g.parts[off] = slices.Clone(data) + if g.total < 0 || g.hdr == nil { + return nil, nil, nil + } + whole := make([]byte, 0, g.total) + for _, o := range slices.Sorted(maps.Keys(g.parts)) { + if o != len(whole) { + return nil, nil, nil // a hole yet + } + whole = append(whole, g.parts[o]...) + } + // Whole now: the last piece is among the parts, and nothing lies beyond it + delete(p.groups, k) + return g.hdr, whole, nil +} + +// whole translates a complete datagram and sends it on. +func (p *slowPath) whole(hdr []byte, proto uint8, payload []byte) error { + if hdr[0]>>4 == 4 { + return p.to6(hdr, proto, payload) + } + return p.to4(hdr, proto, payload) +} + +// to6 translates IPv4 from the pool's side: 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 := embed4(p.prefix, [4]byte(hdr[12:16])) + msg := slices.Clone(payload) + var csum int + switch { + case proto == unix.IPPROTO_ICMP && len(msg) >= 8 && (msg[0] == 8 || msg[0] == 0): + msg[0] = map[byte]byte{8: 128, 0: 129}[msg[0]] + proto, csum = unix.IPPROTO_ICMPV6, 2 + case proto == unix.IPPROTO_UDP && len(msg) >= 8: + csum = 6 + default: + return fmt.Errorf("cannot translate protocol %d whole", proto) + } + msg[csum], msg[csum+1] = 0, 0 + c := l4Checksum(src[:], dst[:], proto, msg) + if c == 0 && proto == unix.IPPROTO_UDP { + c = 0xffff + } + binary.BigEndian.PutUint16(msg[csum:], c) + h := make([]byte, 40) + h[0] = 6<<4 | hdr[1]>>4 + h[1] = hdr[1] << 4 + h[6], h[7] = proto, hdr[8] + copy(h[8:], src[:]) + copy(h[24:], dst[:]) + return p.send6(h, msg) +} + +// to4 translates IPv6 ICMP echo from the LAN side. +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") + } + msg := slices.Clone(payload) + msg[0] = map[byte]byte{128: 8, 129: 0}[msg[0]] + msg[2], msg[3] = 0, 0 + binary.BigEndian.PutUint16(msg[2:], l4Checksum(nil, nil, 0, msg)) + h := make([]byte, 20) + 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[:]) + return p.send4(h, msg) +} + +// send4 sends an IPv4 datagram, in fragments when it is larger than the device takes. +func (p *slowPath) send4(h, msg []byte) error { + p.id++ + binary.BigEndian.PutUint16(h[4:6], uint16(p.id)) + chunk := (p.dev.MTU - 20) &^ 7 + for off := 0; off < len(msg); off += chunk { + part := msg[off:min(off+chunk, len(msg))] + fo := off / 8 + if off+len(part) < len(msg) { + fo |= 0x2000 + } + binary.BigEndian.PutUint16(h[2:4], uint16(20+len(part))) + binary.BigEndian.PutUint16(h[6:8], uint16(fo)) + h[10], h[11] = 0, 0 + binary.BigEndian.PutUint16(h[10:], l4Checksum(nil, nil, 0, h)) + if err := p.send(slices.Concat(h, part)); err != nil { + return err + } + } + return nil +} + +// send6 sends an IPv6 datagram, in fragments when it is larger than mtu6. +func (p *slowPath) send6(h, msg []byte) error { + if 40+len(msg) <= p.mtu6 { + binary.BigEndian.PutUint16(h[4:6], uint16(len(msg))) + return p.send(slices.Concat(h, msg)) + } + p.id++ + proto := h[6] + h[6] = unix.IPPROTO_FRAGMENT + chunk := (p.mtu6 - 48) &^ 7 + for off := 0; off < len(msg); off += chunk { + part := msg[off:min(off+chunk, len(msg))] + fo := off + if off+len(part) < len(msg) { + fo |= 1 + } + fh := []byte{proto, 0, byte(fo >> 8), byte(fo), 0, 0, 0, 0} + binary.BigEndian.PutUint32(fh[4:], p.id) + binary.BigEndian.PutUint16(h[4:6], uint16(8+len(part))) + if err := p.send(slices.Concat(h, fh, part)); err != nil { + return err + } + } + return nil +} + +// v4Positions is where RFC 6052 puts the four bytes of an IPv4 address after a prefix of bits: +// right after it, but never in byte 8, the u octet, which stays zero. +func v4Positions(bits int) [4]byte { + var pos [4]byte + for i, b := 0, bits/8; i < 4; b++ { + if b != 8 { + pos[i] = byte(b) + i++ + } + } + return pos +} + +// embed4 is the IPv6 address of an IPv4 one behind the NAT64 prefix (RFC 6052 section 2.2). +func embed4(prefix netip.Prefix, v4 [4]byte) [16]byte { + a := prefix.Masked().Addr().As16() + for i, pos := range v4Positions(prefix.Bits()) { + a[pos] = v4[i] + } + return a +} + +// extract4 is the IPv4 address an address in the NAT64 prefix carries. +func extract4(prefix netip.Prefix, a [16]byte) [4]byte { + var v4 [4]byte + for i, pos := range v4Positions(prefix.Bits()) { + v4[i] = a[pos] + } + return v4 +} + +// l4Checksum is the Internet checksum of msg, after an IPv6 pseudo-header when src is given. +func l4Checksum(src, dst []byte, proto uint8, msg []byte) uint16 { + var sum uint32 + add := func(b []byte) { + for i := 0; i+1 < len(b); i += 2 { + sum += uint32(binary.BigEndian.Uint16(b[i:])) + } + if len(b)%2 == 1 { + sum += uint32(b[len(b)-1]) << 8 + } + } + if src != nil { + add(src) + add(dst) + sum += uint32(len(msg)) + uint32(proto) + } + add(msg) + for sum > 0xffff { + sum = sum>>16 + sum&0xffff + } + return ^uint16(sum) +} + +// map4 is map4 of bpf/nat64.c: the pool address of an IPv6 source, a free one the first time. +func map4(v4of, v6of *bpfMap, src [16]byte) ([4]byte, bool) { + var a [4]byte + if v4of.lookup(src[:], a[:]) == nil { + return a, true + } + h := hash6(src) + base := binary.BigEndian.Uint32(bpfPool.Addr().AsSlice()) + mask := uint32(1)<<(32-bpfPool.Bits()) - 1 + for i := range uint32(bpfProbes) { + binary.BigEndian.PutUint32(a[:], base+((h+i)&mask)) + if v6of.update(a[:], src[:], unix.BPF_NOEXIST) != nil { + continue + } + if v4of.update(src[:], a[:], unix.BPF_NOEXIST) == nil { + return a, true + } + v6of.delete(a[:]) // the program mapped the same source meanwhile + return a, v4of.lookup(src[:], a[:]) == nil + } + return a, false +} + +// hash6 is hash6 of bpf/nat64.c, which reads the address as words in the host's byte order. +func hash6(a [16]byte) uint32 { + w := func(i int) uint32 { return binary.NativeEndian.Uint32(a[4*i:]) } + h := w(0) * 0x9e3779b1 + h = (h ^ w(1)) * 0x85ebca6b + h = (h ^ w(2)) * 0xc2b2ae35 + h = (h ^ w(3)) * 0x27d4eb2f + return h ^ h>>15 +} diff --git a/nat64slow_integration_test.go b/nat64slow_integration_test.go new file mode 100644 index 0000000..2d3253f --- /dev/null +++ b/nat64slow_integration_test.go @@ -0,0 +1,663 @@ +//go:build linux && integration + +package main + +import ( + "bytes" + "encoding/binary" + "errors" + "net" + "net/netip" + "runtime" + "slices" + "strings" + "syscall" + "testing" + "unsafe" + + "golang.org/x/sys/unix" +) + +// 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 +} + +func newSlowFixture(t *testing.T, mtu int) *slowFixture { + t.Helper() + ifi, err := dummyAdd("slowtest0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { linkDel(ifi.Index) }) + if err := linkSetUp(ifi.Index); err != nil { + t.Fatal(err) + } + ifi.MTU = mtu + v4of, v6of := testMaps(t, 64, 64) + p, err := newSlowPath(nat64WKP, v4of, v6of, ifi, 1500) + if err != nil { + t.Fatal(err) + } + t.Cleanup(p.close) + fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM, int(htons(unix.ETH_P_ALL))) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { unix.Close(fd) }) + if err := unix.Bind(fd, &unix.SockaddrLinklayer{Protocol: htons(unix.ETH_P_ALL), Ifindex: ifi.Index}); err != nil { + t.Fatal(err) + } + unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Usec: 200000}) + return &slowFixture{p: p, cap: fd} +} + +// sent returns the IP packets that left the device since the last call. +func (f *slowFixture) sent() [][]byte { + var pkts [][]byte + for { + buf := make([]byte, 2000) + n, _, err := unix.Recvfrom(f.cap, buf, 0) + if err != nil { + return pkts + } + if buf[0]>>4 == 4 || buf[0]>>4 == 6 { + pkts = append(pkts, buf[:n]) + } + } +} + +// v4Packet builds an IPv4 packet whose header checksum is right. +func v4Packet(src, dst string, proto uint8, fo uint16, payload []byte) []byte { + h := make([]byte, 20) + h[0], h[1], h[8], h[9] = 0x45, 0xb8, 64, proto + binary.BigEndian.PutUint16(h[2:], uint16(20+len(payload))) + binary.BigEndian.PutUint16(h[4:], 0x4242) + binary.BigEndian.PutUint16(h[6:], fo) + copy(h[12:], netip.MustParseAddr(src).AsSlice()) + copy(h[16:], netip.MustParseAddr(dst).AsSlice()) + binary.BigEndian.PutUint16(h[10:], l4Checksum(nil, nil, 0, h)) + return append(h, payload...) +} + +func v6Packet(src, dst string, next uint8, payload []byte) []byte { + h := make([]byte, 40) + h[0], h[1], h[6], h[7] = 0x6b, 0x80, next, 64 + binary.BigEndian.PutUint16(h[4:], uint16(len(payload))) + copy(h[8:], netip.MustParseAddr(src).AsSlice()) + copy(h[24:], netip.MustParseAddr(dst).AsSlice()) + return append(h, payload...) +} + +// reassembled puts IPv4 or IPv6 fragments back together, checking each against mtu. +func reassembled(t *testing.T, pkts [][]byte, mtu int) (hdr, payload []byte) { + t.Helper() + whole := map[int][]byte{} + for _, p := range pkts { + if len(p) > mtu { + t.Fatalf("a %d-byte packet is over the MTU of %d", len(p), mtu) + } + if p[0]>>4 == 4 { + hdr = p[:20] + whole[int(binary.BigEndian.Uint16(p[6:])&0x1fff)*8] = p[20:] + continue + } + hdr = p[:40] + off := 0 + data := p[40:] + if p[6] == unix.IPPROTO_FRAGMENT { + off, data = int(binary.BigEndian.Uint16(p[42:])&^7), p[48:] + } + whole[off] = data + } + for _, o := range slices.Sorted(func(yield func(int) bool) { + for o := range whole { + if !yield(o) { + return + } + } + }) { + if o != len(payload) { + t.Fatalf("a hole at %d", len(payload)) + } + payload = append(payload, whole[o]...) + } + return hdr, payload +} + +// The slow path's translations, whole and in fragments, with checksums a receiver accepts. +func TestSlowPathTranslates(t *testing.T) { + if !ownNetns(t) { + return + } + f := newSlowFixture(t, 600) + 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 { + t.Fatal(err) + } + + // IPv4 UDP without a checksum gets one + udp := []byte{0, 53, 0, 99, 0, 12, 0, 0, 'h', 'e', 'y', '!'} + if err := p.packet(v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_UDP, 0, udp)); err != nil { + t.Fatal(err) + } + pkts := f.sent() + if len(pkts) != 1 { + t.Fatalf("want one packet, got %d", len(pkts)) + } + got := pkts[0] + if src := netip.AddrFrom16([16]byte(got[8:24])); src != netip.MustParseAddr("64:ff9b::c000:202") || [16]byte(got[24:40]) != host || got[6] != unix.IPPROTO_UDP { + t.Fatalf("want UDP from 64:ff9b::c000:202 to 2001:db8::5, got % x", got[:40]) + } + if got[0] != 0x6b || got[1] != 0x80 || got[7] != 64 { + t.Fatalf("the traffic class and hop limit should carry over, got % x", got[:8]) + } + if l4Checksum(got[8:24], got[24:40], unix.IPPROTO_UDP, got[40:]) != 0 { + t.Fatal("the UDP checksum should be right") + } + // A checksum that computes to zero is sent as all ones + zero := slices.Clone(udp) + binary.BigEndian.PutUint16(zero[8:], 0) + binary.BigEndian.PutUint16(zero[10:], 0) + pfx := netip.MustParseAddr("64:ff9b::c000:202").As16() + binary.BigEndian.PutUint16(zero[8:], l4Checksum(pfx[:], host[:], unix.IPPROTO_UDP, zero)) + p.packet(v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_UDP, 0, zero)) + if pkts := f.sent(); len(pkts) != 1 || binary.BigEndian.Uint16(pkts[0][46:]) != 0xffff { + t.Fatalf("want a checksum of 0xffff, got %v", pkts) + } + + // A fragmented echo comes out in fragments for an MTU of 1280 + p.mtu6 = 1280 + echo := append([]byte{8, 0, 0, 0, 0x12, 0x34, 0, 1}, bytes.Repeat([]byte{7}, 2992)...) + binary.BigEndian.PutUint16(echo[2:], l4Checksum(nil, nil, 0, echo)) + for _, part := range []struct { + fo uint16 + data []byte + }{{0x2000 | 1480/8, echo[1480:2960]}, {2960 / 8, echo[2960:]}, {0x2000, echo[:1480]}} { + if err := p.packet(v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_ICMP, part.fo, part.data)); err != nil { + t.Fatal(err) + } + } + hdr, msg := reassembled(t, f.sent(), 1280) + if msg[0] != 128 || len(msg) != len(echo) || l4Checksum(hdr[8:24], hdr[24:40], unix.IPPROTO_ICMPV6, msg) != 0 { + t.Fatalf("want a 3000-byte echo request with a right checksum, got type %d, %d bytes", msg[0], len(msg)) + } + + // An IPv6 echo from a new source gets the pool address the program would give it, and goes + // out in fragments the device takes + reply := append([]byte{129, 0, 0, 0, 0x12, 0x34, 0, 1}, bytes.Repeat([]byte{9}, 1992)...) + src := netip.MustParseAddr("2001:db8::77") + binary.BigEndian.PutUint16(reply[2:], l4Checksum(src.AsSlice(), pfx[:], unix.IPPROTO_ICMPV6, reply)) + frag := func(off int, more bool, data []byte) []byte { + fh := []byte{unix.IPPROTO_ICMPV6, 0, 0, 0, 0, 0, 0, 9} + fo := uint16(off) + if more { + fo |= 1 + } + binary.BigEndian.PutUint16(fh[2:], fo) + return v6Packet(src.String(), "64:ff9b::c000:202", unix.IPPROTO_FRAGMENT, append(fh, data...)) + } + p.packet(frag(0, true, reply[:1024])) + if err := p.packet(frag(1024, false, reply[1024:])); err != nil { + t.Fatal(err) + } + hdr, msg = reassembled(t, f.sent(), 600) + var a4 [4]byte + if err := p.v4of.lookup(src.AsSlice(), a4[:]); err != nil { + t.Fatal(err) + } + base, mask := binary.BigEndian.Uint32(bpfPool.Addr().AsSlice()), uint32(1)<<(32-bpfPool.Bits())-1 + if want := base + hash6(src.As16())&mask; binary.BigEndian.Uint32(a4[:]) != want || [4]byte(hdr[12:16]) != a4 { + t.Fatalf("want the source numbered %#x, got %v and a packet from %v", want, a4, hdr[12:16]) + } + if msg[0] != 0 || len(msg) != len(reply) || l4Checksum(nil, nil, 0, msg) != 0 || hdr[1] != 0xb8 || hdr[8] != 64 { + t.Fatalf("want a 2000-byte echo reply with a right checksum, TOS and TTL, got % x", hdr) + } + + // 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}), + } { + if err := p.packet(pkt); err == nil { + t.Errorf("%s: want an error", name) + } + } + // A source whose every slot is taken + busy := netip.MustParseAddr("2001:db8::99").As16() + for i := range uint32(bpfProbes) { + var slot [4]byte + binary.BigEndian.PutUint32(slot[:], base+((hash6(busy)+i)&mask)) + p.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) + } + + // Without the socket nothing goes out, whole or in fragments + unix.Close(p.fd) + p.fd = -1 + p.mtu6 = 1500 + for name, pkt := range map[string][]byte{ + "whole IPv6": v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_UDP, 0, udp), + "fragments IPv6": v4Packet("192.0.2.2", "198.18.0.5", unix.IPPROTO_ICMP, 0, echo), + "IPv4": v6Packet(src.String(), "64:ff9b::1", unix.IPPROTO_ICMPV6, reply[:8]), + } { + if strings.HasPrefix(name, "fragments") { + p.mtu6 = 1280 + } + if err := p.packet(pkt); err == nil { + t.Errorf("%s: want the send refused", name) + } + } +} + +// map4 gives up when the map of sources has no room, after taking back the slot it claimed. +func TestMap4WithoutRoom(t *testing.T) { + if !ownNetns(t) { + return + } + v4of, v6of := testMaps(t, 1, 4) + v4of.update(netip.MustParseAddr("2001:db8::1").AsSlice(), []byte{198, 18, 0, 1}, 0) + if _, ok := map4(v4of, v6of, netip.MustParseAddr("2001:db8::2").As16()); ok { + t.Fatal("a full map should refuse a new source") + } + if keys := v6of.keys(); len(keys) != 0 { + t.Fatalf("the claimed slot %v should be given back", keys) + } +} + +// The program numbers a new source the way map4 does, and passes what the slow path sends back +// to the device's ingress untouched. +func TestBPFProgram(t *testing.T) { + if !ownNetns(t) { + return + } + b := &bpfNAT64{prefix: nat64WKP} + ifi, err := dummyAdd("progtest0") + if err != nil { + t.Fatal(err) + } + defer linkDel(ifi.Index) + b.ifindex = ifi.Index + if err := b.load(); err != nil { + t.Fatal(err) + } + defer b.stop() + frame := func(pkt []byte, proto uint16) []byte { + return append(binary.BigEndian.AppendUint16(make([]byte, 12), proto), pkt...) + } + src := netip.MustParseAddr("2001:db8::abcd") + udp := []byte{0, 1, 0, 2, 0, 8, 0x12, 0x34} + if ret, _, _ := b.objs.testRun(t, frame(v6Packet(src.String(), "64:ff9b::c000:202", unix.IPPROTO_UDP, udp), unix.ETH_P_IPV6), nil); ret != 7 { // TC_ACT_REDIRECT + t.Fatalf("want the packet redirected, got %d", ret) + } + var a [4]byte + if err := b.objs.maps[nat64MapV4Of].lookup(src.AsSlice(), a[:]); err != nil { + t.Fatal(err) + } + base, mask := binary.BigEndian.Uint32(bpfPool.Addr().AsSlice()), uint32(1)<<(32-bpfPool.Bits())-1 + if want := base + hash6(src.As16())&mask; binary.BigEndian.Uint32(a[:]) != want { + t.Fatalf("the program numbered %v, map4 would give %#x", a, want) + } + // Attaching to a device that is not there fails, naming what tcx needs + gone := &bpfNAT64{prefix: nat64WKP, ifindex: 1 << 30} + if err := gone.load(); err == nil || !strings.Contains(err.Error(), "6.6") { + t.Fatalf("want the attachment refused, got %v", err) + } + gone.ifindex = 0 + gone.stop() + + // struct __sk_buff from its start to mark, the third word + ctx := nativeEndian.AppendUint32(make([]byte, 8), bpfSlowMark) + ret, _, out := b.objs.testRun(t, frame(make([]byte, 40), unix.ETH_P_IPV6), ctx) + if mark := nativeEndian.Uint32(out[8:]); ret != 7 || mark != 0 { + t.Fatalf("want a marked packet redirected with its mark cleared, got %d, mark %#x", ret, mark) + } +} + +// Whatever fails part way, start leaves no device behind; given enough descriptors it succeeds. +func TestBPFNAT64StartUndoes(t *testing.T) { + if !ownNetns(t) { + return + } + b := &bpfNAT64{prefix: nat64WKP} + // Once with room, which also gets the runtime's poller its descriptor beforehand + if err := b.start(); err != nil { + t.Fatal(err) + } + b.stop() + for extra := 0; ; extra++ { + if extra > 500 { + t.Fatal("start never succeeded") + } + err := joolWithFDs(t, extra, b.start) + if err == nil { + break + } + if _, err := net.InterfaceByName(joolOutside); err == nil { + t.Fatalf("with room for %d descriptors start failed and left %s", extra, joolOutside) + } + } + b.stop() + // A device of the same name, as a crash would leave, is replaced + if _, err := dummyAdd("reptest0"); err != nil { + t.Fatal(err) + } + ifi, err := dummyAdd("reptest0") + if err != nil { + t.Fatalf("the old device should be replaced: %v", err) + } + linkDel(ifi.Index) + if _, err := net.InterfaceByName(joolOutside); err == nil { + t.Fatalf("%s should be gone", joolOutside) + } +} + +// Without the right to change links, the old device cannot be removed; without the right to ask +// conntrack, the sweep leaves every mapping alone; without descriptors, likewise. +func TestBPFNAT64WithoutPrivileges(t *testing.T) { + if !ownNetns(t) { + return + } + b := &bpfNAT64{prefix: nat64WKP} + if err := b.start(); err != nil { + t.Fatal(err) + } + defer b.stop() + b.objs.maps[nat64MapV6Of].update([]byte{198, 18, 0, 1}, netip.MustParseAddr("2001:db8::1").AsSlice(), 0) + if err := syscall.Setresuid(-1, 65534, -1); err != nil { + t.Fatal(err) + } + _, addErr := dummyAdd(joolOutside) + b.sweep() + b.sweep() + if err := syscall.Setresuid(-1, 0, -1); err != nil { + t.Fatal(err) + } + if addErr == nil || !strings.Contains(addErr.Error(), "removing the old") { + t.Fatalf("want the old device's removal refused, got %v", addErr) + } + joolWithFDs(t, 0, func() error { b.sweep(); b.sweep(); return nil }) + if len(b.objs.maps[nat64MapV6Of].keys()) == 0 { + t.Fatal("a sweep that cannot ask conntrack should remove nothing") + } + // The slow path's drops are reported with the program's + b.slow.dropped.Add(2) + if d := b.drops(); d != "slow-path 2" { + t.Fatalf("want the slow path's drops, got %q", d) + } +} + +// Without CAP_NET_RAW start gets as far as the slow path's packet socket, and takes down what it +// set up. +func TestBPFNAT64WithoutRawSockets(t *testing.T) { + if !ownNetns(t) { + return + } + b := &bpfNAT64{prefix: nat64WKP} + ch := make(chan error) + go func() { + runtime.LockOSThread() // never unlocked: the thread leaves with its capabilities + hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3} + var data [2]unix.CapUserData + unix.Capget(&hdr, &data[0]) + data[0].Effective &^= 1 << unix.CAP_NET_RAW + if err := unix.Capset(&hdr, &data[0]); err != nil { + ch <- err + return + } + ch <- b.start() + }() + if err := <-ch; err == nil || !strings.Contains(err.Error(), "packet socket") { + t.Fatalf("want the packet socket refused, got %v", err) + } + if _, err := net.InterfaceByName(joolOutside); err == nil { + t.Fatalf("%s should be gone", joolOutside) + } +} + +// Without descriptors there is no packet socket. +func TestSlowPathWithoutDescriptors(t *testing.T) { + lo, err := net.InterfaceByName("lo") + 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) { + t.Fatalf("want the socket refused, got %v", err) + } +} + +// The program's ICMP translation against RFC 7915 sections 4.2 and 5.2, one message at a time: +// type, code and the rest of the header, both checksums, and the quote turned around. +func TestBPFICMPTranslation(t *testing.T) { + if !ownNetns(t) { + return + } + b := &bpfNAT64{prefix: nat64WKP} + ifi, err := dummyAdd("icmptest0") + if err != nil { + t.Fatal(err) + } + b.ifindex = ifi.Index + if err := b.load(); err != nil { + t.Fatal(err) + } + defer b.stop() + host := netip.MustParseAddr("2001:db8::5") + b.objs.maps[nat64MapV6Of].update([]byte{198, 18, 0, 5}, host.AsSlice(), 0) + b.objs.maps[nat64MapV4Of].update(host.AsSlice(), []byte{198, 18, 0, 5}, 0) + run := func(pkt []byte, proto uint16) ([]byte, bool) { + t.Helper() + ret, out, _ := b.objs.testRun(t, append(binary.BigEndian.AppendUint16(make([]byte, 12), proto), pkt...), nil) + return out[14:], ret == 7 + } + udp := []byte{0, 7, 0x30, 0x39, 0, 8, 0xab, 0xcd} + + // From IPv4: an error from 192.0.2.9 about what 198.18.0.5 sent 192.0.2.2 + quote4 := v4Packet("198.18.0.5", "192.0.2.2", unix.IPPROTO_UDP, 0x4000, udp) + for _, c := range []struct { + typ, code byte + rest uint32 + want []uint32 // type, code, rest; none to be dropped + }{ + {3, 0, 0, []uint32{1, 0, 0}}, + {3, 1, 0, []uint32{1, 0, 0}}, + {3, 2, 0, []uint32{4, 1, 6}}, + {3, 3, 0, []uint32{1, 4, 0}}, + {3, 4, 1400, []uint32{2, 0, 1420}}, + {3, 4, 1000, []uint32{2, 0, 1280}}, + {3, 4, 0, []uint32{2, 0, 1280}}, + {3, 5, 0, []uint32{1, 0, 0}}, + {3, 6, 0, []uint32{1, 0, 0}}, + {3, 7, 0, []uint32{1, 0, 0}}, + {3, 8, 0, []uint32{1, 0, 0}}, + {3, 9, 0, []uint32{1, 1, 0}}, + {3, 10, 0, []uint32{1, 1, 0}}, + {3, 11, 0, []uint32{1, 0, 0}}, + {3, 12, 0, []uint32{1, 0, 0}}, + {3, 13, 0, []uint32{1, 1, 0}}, + {3, 14, 0, nil}, + {3, 15, 0, []uint32{1, 1, 0}}, + {3, 16, 0, nil}, + {11, 0, 0, []uint32{3, 0, 0}}, + {11, 1, 0, []uint32{3, 1, 0}}, + {12, 0, 0 << 24, []uint32{4, 0, 0}}, + {12, 0, 1 << 24, []uint32{4, 0, 1}}, + {12, 0, 2 << 24, []uint32{4, 0, 4}}, + {12, 0, 3 << 24, []uint32{4, 0, 4}}, + {12, 0, 4 << 24, nil}, + {12, 0, 8 << 24, []uint32{4, 0, 7}}, + {12, 0, 9 << 24, []uint32{4, 0, 6}}, + {12, 0, 10 << 24, nil}, + {12, 2, 13 << 24, []uint32{4, 0, 8}}, + {12, 0, 17 << 24, []uint32{4, 0, 24}}, + {12, 0, 20 << 24, nil}, + {12, 1, 0, nil}, + {5, 0, 0, nil}, // Redirect + {13, 0, 0, nil}, // Timestamp + } { + msg := binary.BigEndian.AppendUint32([]byte{c.typ, c.code, 0, 0}, c.rest) + msg = append(msg, quote4...) + binary.BigEndian.PutUint16(msg[2:], l4Checksum(nil, nil, 0, msg)) + got, ok := run(v4Packet("192.0.2.9", "198.18.0.5", unix.IPPROTO_ICMP, 0, msg), unix.ETH_P_IP) + if ok != (c.want != nil) { + t.Errorf("ICMP %d/%d: translated %v, want %v", c.typ, c.code, ok, c.want != nil) + continue + } + if !ok { + continue + } + icmp := got[40:] + if g := []uint32{uint32(icmp[0]), uint32(icmp[1]), binary.BigEndian.Uint32(icmp[4:])}; !slices.Equal(g, c.want) { + t.Errorf("ICMP %d/%d: got %v, want %v", c.typ, c.code, g, c.want) + } + if l4Checksum(got[8:24], got[24:40], unix.IPPROTO_ICMPV6, icmp) != 0 { + t.Errorf("ICMP %d/%d: the ICMPv6 checksum is wrong", c.typ, c.code) + } + if q := icmp[8:]; [16]byte(q[8:24]) != host.As16() || netip.AddrFrom16([16]byte(q[24:40])) != netip.MustParseAddr("64:ff9b::c000:202") || q[6] != unix.IPPROTO_UDP || binary.BigEndian.Uint16(q[4:]) != 8 || !bytes.Equal(q[40:48], udp) { + t.Errorf("ICMP %d/%d: the quote should be the UDP from 2001:db8::5 to 64:ff9b::c000:202, got % x", c.typ, c.code, q) + } + } + + // From IPv6: an error from 2001:db8::5 about what 64:ff9b::c000:209 sent it + quote6 := v6Packet("64:ff9b::c000:209", host.String(), unix.IPPROTO_UDP, udp) + src, dst := host.As16(), netip.MustParseAddr("64:ff9b::c000:209").As16() + for _, c := range []struct { + typ, code byte + rest uint32 + want []uint32 + }{ + {1, 0, 0, []uint32{3, 1, 0}}, + {1, 1, 0, []uint32{3, 10, 0}}, + {1, 2, 0, []uint32{3, 1, 0}}, + {1, 3, 0, []uint32{3, 1, 0}}, + {1, 4, 0, []uint32{3, 3, 0}}, + {1, 5, 0, nil}, + {2, 0, 1500, []uint32{3, 4, 1480}}, + {2, 0, 70, []uint32{3, 4, 68}}, + {2, 0, 100000, []uint32{3, 4, 0xffff}}, + {3, 0, 0, []uint32{11, 0, 0}}, + {3, 1, 0, []uint32{11, 1, 0}}, + {4, 0, 0, []uint32{12, 0, 0}}, + {4, 0, 1, []uint32{12, 0, 1 << 24}}, + {4, 0, 2, nil}, + {4, 0, 4, []uint32{12, 0, 2 << 24}}, + {4, 0, 6, []uint32{12, 0, 9 << 24}}, + {4, 0, 7, []uint32{12, 0, 8 << 24}}, + {4, 0, 9, []uint32{12, 0, 12 << 24}}, + {4, 0, 30, []uint32{12, 0, 16 << 24}}, + {4, 0, 40, nil}, + {4, 1, 0, []uint32{3, 2, 0}}, + {4, 2, 0, nil}, + {133, 0, 0, nil}, // Router Solicitation + } { + msg := binary.BigEndian.AppendUint32([]byte{c.typ, c.code, 0, 0}, c.rest) + msg = append(msg, quote6...) + binary.BigEndian.PutUint16(msg[2:], l4Checksum(src[:], dst[:], unix.IPPROTO_ICMPV6, msg)) + got, ok := run(v6Packet(host.String(), "64:ff9b::c000:209", unix.IPPROTO_ICMPV6, msg), unix.ETH_P_IPV6) + if ok != (c.want != nil) { + t.Errorf("ICMPv6 %d/%d: translated %v, want %v", c.typ, c.code, ok, c.want != nil) + continue + } + if !ok { + continue + } + icmp := got[20:binary.BigEndian.Uint16(got[2:])] + if g := []uint32{uint32(icmp[0]), uint32(icmp[1]), binary.BigEndian.Uint32(icmp[4:])}; !slices.Equal(g, c.want) { + t.Errorf("ICMPv6 %d/%d: got %v, want %v", c.typ, c.code, g, c.want) + } + if l4Checksum(nil, nil, 0, icmp) != 0 || l4Checksum(nil, nil, 0, got[:20]) != 0 { + t.Errorf("ICMPv6 %d/%d: a checksum is wrong", c.typ, c.code) + } + if q := icmp[8:]; l4Checksum(nil, nil, 0, q[:20]) != 0 || [4]byte(q[12:16]) != [4]byte{192, 0, 2, 9} || [4]byte(q[16:20]) != [4]byte{198, 18, 0, 5} || !bytes.Equal(q[20:28], udp) { + t.Errorf("ICMPv6 %d/%d: the quote should be the UDP from 192.0.2.9 to 198.18.0.5, got % x", c.typ, c.code, q) + } + } +} + +// The program embeds and extracts IPv4 addresses the way RFC 6052 does behind a prefix of every +// length, and leaves alone what is outside the prefix. +func TestBPFRFC6052(t *testing.T) { + if !ownNetns(t) { + return + } + host := netip.MustParseAddr("2001:db8::5") + 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") + if err != nil { + t.Fatal(err) + } + b.ifindex = ifi.Index + if err := b.load(); err != nil { + t.Fatal(err) + } + b.objs.maps[nat64MapV6Of].update([]byte{198, 18, 0, 5}, host.AsSlice(), 0) + run := func(pkt []byte, proto uint16) ([]byte, bool) { + ret, out, _ := b.objs.testRun(t, append(binary.BigEndian.AppendUint16(make([]byte, 12), proto), pkt...), nil) + return out[14:], ret == 7 + } + got, ok := run(v4Packet("192.0.2.33", "198.18.0.5", unix.IPPROTO_UDP, 0, udp), unix.ETH_P_IP) + if src := netip.AddrFrom16([16]byte(got[8:24])); !ok || src != netip.MustParseAddr(want) { + t.Errorf("%s: 192.0.2.33 became %s, want %s", prefix, src, want) + } + got, ok = run(v6Packet(host.String(), want, unix.IPPROTO_UDP, udp), unix.ETH_P_IPV6) + if dst := netip.AddrFrom4([4]byte(got[16:20])); !ok || dst != netip.MustParseAddr("192.0.2.33") { + t.Errorf("%s: %s became %s, want 192.0.2.33", prefix, want, dst) + } + if _, ok := run(v6Packet(host.String(), "2001:db9::1", unix.IPPROTO_UDP, udp), unix.ETH_P_IPV6); ok { + t.Errorf("%s: an address outside the prefix should not be translated", prefix) + } + b.stop() + } +} + +// testMaps creates the two maps of the address mapping, with room for so many sources and pool +// addresses. +func testMaps(t *testing.T, sources, pool uint32) (v4of, v6of *bpfMap) { + t.Helper() + v4of, err := bpfMapCreate(bpfMapSpec{name: "v4_of", typ: unix.BPF_MAP_TYPE_HASH, keySize: 16, valueSize: 4, maxEntries: sources}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(v4of.close) + v6of, err = bpfMapCreate(bpfMapSpec{name: "v6_of", typ: unix.BPF_MAP_TYPE_HASH, keySize: 4, valueSize: 16, maxEntries: pool}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(v6of.close) + return v4of, v6of +} + +// testRun runs the program once on a frame (BPF_PROG_TEST_RUN), with ctx as the start of its +// struct __sk_buff, and returns the verdict, the frame and the whole context after. +func (o *bpfObjects) testRun(t *testing.T, frame, ctx []byte) (uint32, []byte, []byte) { + t.Helper() + out, ctxOut := make([]byte, 2000), make([]byte, 192) + attr := struct { + prog, retval, dataSizeIn, dataSizeOut uint32 + dataIn, dataOut uint64 + repeat, duration uint32 + ctxSizeIn, ctxSizeOut uint32 + ctxIn, ctxOut uint64 + }{prog: uint32(o.prog), dataSizeIn: uint32(len(frame)), dataSizeOut: uint32(len(out)), dataIn: ptr(frame), dataOut: ptr(out)} + if ctx != nil { + attr.ctxSizeIn, attr.ctxSizeOut, attr.ctxIn, attr.ctxOut = uint32(len(ctx)), uint32(len(ctxOut)), ptr(ctx), ptr(ctxOut) + } + if _, err := bpfSys(unix.BPF_PROG_TEST_RUN, unsafe.Pointer(&attr), unsafe.Sizeof(attr)); err != nil { + t.Fatal(err) + } + runtime.KeepAlive(frame) + runtime.KeepAlive(ctx) + return attr.retval, out[:attr.dataSizeOut], ctxOut +} diff --git a/nat64slow_test.go b/nat64slow_test.go new file mode 100644 index 0000000..67109c7 --- /dev/null +++ b/nat64slow_test.go @@ -0,0 +1,177 @@ +//go:build linux + +package main + +import ( + "encoding/binary" + "net/netip" + "testing" + "time" + + "github.com/mdlayher/netlink" +) + +// A frame too short for an Ethernet header, or with no IP after it, is dropped. +func TestSlowPathFrames(t *testing.T) { + p := &slowPath{} + p.handle(make([]byte, 10)) + p.handle(make([]byte, 20)) + if n := p.dropped.Load(); n != 2 { + t.Fatalf("want both frames dropped, got %d", n) + } +} + +// What is not a well-formed IPv4 or IPv6 packet is refused before anything is reassembled. +func TestSlowPathRefusesMalformed(t *testing.T) { + p := &slowPath{groups: map[slowKey]*slowGroup{}, now: time.Now} + v4 := func(ihl byte, tot uint16, n int) []byte { + b := make([]byte, n) + b[0] = 4<<4 | ihl + binary.BigEndian.PutUint16(b[2:], tot) + return b + } + v6 := func(plen uint16, next byte, n int) []byte { + b := make([]byte, n) + b[0], b[6] = 6<<4, next + binary.BigEndian.PutUint16(b[4:], plen) + return b + } + for name, pkt := range map[string][]byte{ + "empty": nil, + "IPv4 options": v4(6, 24, 24), + "IPv4 length short": v4(5, 10, 20), + "IPv4 length long": v4(5, 40, 20), + "IPv6 length long": v6(10, 17, 40), + "IPv6 fragment short": v6(4, 44, 44), + } { + if err := p.packet(pkt); err == nil { + t.Errorf("%s: want an error", name) + } + } +} + +// Fragments come together in any order; a hole waits, an overlap or a malformed piece drops the +// datagram, and so do too many datagrams at once or one that takes too long. +func TestSlowPathReassembly(t *testing.T) { + now := time.Unix(1000, 0) + p := &slowPath{groups: map[slowKey]*slowGroup{}, now: func() time.Time { return now }} + hdr := make([]byte, 40) + hdr[0], hdr[6] = 6<<4, 44 + key := func(id uint32) slowKey { + return slowKey{netip.MustParseAddr("2001:db8::1"), netip.MustParseAddr("64:ff9b::1"), id, 58} + } + add := func(id uint32, off int, more bool, n int) ([]byte, []byte, error) { + return p.reassemble(key(id), hdr, off, more, make([]byte, n)) + } + + if _, w, err := add(1, 16, false, 4); w != nil || err != nil { + t.Fatalf("the last piece alone completes nothing: %v %v", w, err) + } + if _, w, err := add(1, 0, true, 8); w != nil || err != nil { + t.Fatalf("a hole is left at 8: %v %v", w, err) + } + h, w, err := add(1, 8, true, 8) + if err != nil || len(w) != 20 || h[6] != 58 { + t.Fatalf("want 20 bytes and the header naming ICMPv6, got %d, %v, %v", len(w), h, err) + } + if len(p.groups) != 0 { + t.Fatal("a datagram put together should be forgotten") + } + + for name, step := range map[string]func() error{ + "a piece with more that is not a multiple of 8": func() error { _, _, err := add(2, 0, true, 7); return err }, + "a piece beyond 65535": func() error { _, _, err := add(3, 65528, false, 16); return err }, + "a piece beyond the end": func() error { + add(4, 16, false, 8) + _, _, err := add(4, 16, true, 16) + return err + }, + "overlapping pieces": func() error { + add(5, 0, true, 16) + _, _, err := add(5, 8, true, 16) + return err + }, + } { + if err := step(); err == nil { + t.Errorf("%s: want an error", name) + } + } + if len(p.groups) != 0 { + t.Fatalf("each bad datagram should be dropped, %d left", len(p.groups)) + } + + for id := range uint32(slowGroups) { + add(100+id, 0, true, 8) + } + if _, _, err := add(999, 0, true, 8); err == nil { + t.Fatal("a datagram over the limit should be refused") + } + dropped := p.dropped.Load() + now = now.Add(slowTimeout + time.Second) + if _, _, err := add(999, 0, true, 8); err != nil { + t.Fatalf("the expired datagrams should make room: %v", err) + } + if n := p.dropped.Load() - dropped; n != slowGroups { + t.Fatalf("want the %d expired datagrams counted, got %d", slowGroups, n) + } +} + +// Only the original sources within the pool count, and what does not parse is passed over. +func TestPoolSources(t *testing.T) { + flow := func(src [4]byte) netlink.Message { + ae := netlink.NewAttributeEncoder() + ae.Nested(ctaTupleOrig, func(ae *netlink.AttributeEncoder) error { + ae.Uint8(9, 0) // not the IP tuple + ae.Nested(ctaTupleIP, func(ae *netlink.AttributeEncoder) error { + ae.Bytes(1, src[:]) + ae.Bytes(2, []byte{192, 0, 2, 2}) + return nil + }) + return nil + }) + ae.Nested(ctaTupleReply, func(ae *netlink.AttributeEncoder) error { + ae.Nested(ctaTupleIP, func(ae *netlink.AttributeEncoder) error { + ae.Bytes(1, []byte{198, 18, 9, 9}) + return nil + }) + return nil + }) + b, _ := ae.Encode() + return netlink.Message{Data: append(make([]byte, nfgenmsgLen), b...)} + } + got := poolSources([]netlink.Message{ + flow([4]byte{198, 18, 0, 1}), + flow([4]byte{10, 0, 0, 1}), + {Data: []byte{0, 0}}, // shorter than nfgenmsg + {Data: []byte{0, 0, 0, 0, 9, 0}}, // an attribute cut short + }, bpfPool) + if len(got) != 1 || !got[netip.MustParseAddr("198.18.0.1")] { + t.Fatalf("want only 198.18.0.1, got %v", got) + } +} + +const ctaTupleReply = 2 // CTA_TUPLE_REPLY + +// The examples of RFC 6052 section 2.4: 192.0.2.33 behind a prefix of each length. +var rfc6052Examples = map[string]string{ + "2001:db8::/32": "2001:db8:c000:221::", + "2001:db8:100::/40": "2001:db8:1c0:2:21::", + "2001:db8:122::/48": "2001:db8:122:c000:2:2100::", + "2001:db8:122:300::/56": "2001:db8:122:3c0:0:221::", + "2001:db8:122:344::/64": "2001:db8:122:344:c0:2:2100:0", + "2001:db8:122:344::/96": "2001:db8:122:344::c000:221", +} + +func TestRFC6052(t *testing.T) { + v4 := [4]byte{192, 0, 2, 33} + for prefix, want := range rfc6052Examples { + p := netip.MustParsePrefix(prefix) + a := embed4(p, v4) + if got := netip.AddrFrom16(a); got != netip.MustParseAddr(want) { + t.Errorf("%s: embedded %s, want %s", prefix, got, want) + } + if got := extract4(p, a); got != v4 { + t.Errorf("%s: extracted %v", prefix, got) + } + } +} diff --git a/tools/bpfgen/main.go b/tools/bpfgen/main.go new file mode 100644 index 0000000..4d228c2 --- /dev/null +++ b/tools/bpfgen/main.go @@ -0,0 +1,238 @@ +// Command bpfgen compiles a BPF program for either byte order and writes what sixup loads it with, +// so that sixup itself parses no ELF and no BTF: the instructions, the maps, the read-only data and +// the relocations that tie them together. It wants clang and the libbpf headers. +// +// Usage: go run ./tools/bpfgen bpf/nat64.c nat64 +// +// The program is one function in one section. Its maps are struct map_def values in the section +// maps, and its variables are volatile const ones in .rodata, which sixup fills in and freezes. +package main + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "go/format" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" +) + +// The architectures Go builds for, by byte order +var orders = []struct { + target string + order binary.ByteOrder + arches string +}{ + {"bpfel", binary.LittleEndian, "386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64"}, + {"bpfeb", binary.BigEndian, "mips || mips64 || ppc64 || s390x"}, +} + +const rBPF6464 = 1 // R_BPF_64_64, the relocation of a 64-bit load, which debug/elf does not name + +type mapDef struct { + name string + typ, keySize, valueSize, max, flags uint32 +} + +type varDef struct { + name string + off, size uint64 +} + +type reloc struct { + insn, m int + off int64 // where in the read-only data, or -1 for the map itself +} + +type object struct { + code []byte + maps []mapDef // the read-only data last + rodata []byte + vars []varDef + relocs []reloc + license string +} + +func main() { + if len(os.Args) != 3 { + fmt.Fprintln(os.Stderr, "usage: bpfgen ") + os.Exit(2) + } + if err := run(os.Args[1], os.Args[2]); err != nil { + fmt.Fprintln(os.Stderr, "bpfgen:", err) + os.Exit(1) + } +} + +func run(src, ident string) error { + dir, err := os.MkdirTemp("", "bpfgen") + if err != nil { + return err + } + defer os.RemoveAll(dir) + var first *object + for _, o := range orders { + path := filepath.Join(dir, o.target+".o") + cmd := exec.Command("clang", "-O2", "-Wall", "-Werror", "-target", o.target, "-mcpu=v3", "-c", src, "-o", path) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("clang -target %s: %w", o.target, err) + } + obj, err := parse(path, o.order) + if err != nil { + return fmt.Errorf("%s: %w", o.target, err) + } + if first == nil { + first = obj + } else if !slices.Equal(first.maps, obj.maps) || !slices.Equal(first.vars, obj.vars) { + return errors.New("the byte orders disagree on the maps or the variables") + } + base := ident + "_" + o.target + if err := os.WriteFile(base+".bin", obj.code, 0o644); err != nil { + return err + } + if err := write(base+".go", perOrder(src, ident, base, o.arches, obj)); err != nil { + return err + } + } + return write(ident+"_bpf.go", shared(src, ident, first)) +} + +// parse reads the program, its maps, its read-only data and its relocations from an object. +func parse(path string, order binary.ByteOrder) (*object, error) { + f, err := elf.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var prog *elf.Section + section := map[string]elf.SectionIndex{} + for i, s := range f.Sections { + section[s.Name] = elf.SectionIndex(i) + if s.Flags&elf.SHF_EXECINSTR != 0 && s.Size > 0 { + if prog != nil { + return nil, fmt.Errorf("one program section expected, found %s and %s", prog.Name, s.Name) + } + prog = s + } + } + if prog == nil { + return nil, errors.New("no program") + } + obj := &object{} + if obj.code, err = prog.Data(); err != nil { + return nil, err + } + data := func(name string) []byte { + if s := f.Section(name); s != nil { + b, _ := s.Data() + return b + } + return nil + } + obj.license = strings.TrimRight(string(data("license")), "\x00") + obj.rodata = data(".rodata") + syms, err := f.Symbols() + if err != nil { + return nil, err + } + maps := data("maps") + var mapOffs []uint64 + for _, s := range syms { + switch { + case s.Section == section["maps"] && len(maps) > 0: + b := maps[s.Value:] + obj.maps = append(obj.maps, mapDef{s.Name, order.Uint32(b), order.Uint32(b[4:]), order.Uint32(b[8:]), order.Uint32(b[12:]), order.Uint32(b[16:])}) + mapOffs = append(mapOffs, s.Value) + case s.Section == section[".rodata"] && obj.rodata != nil && elf.ST_TYPE(s.Info) == elf.STT_OBJECT: + obj.vars = append(obj.vars, varDef{s.Name, s.Value, s.Size}) + } + } + slices.SortFunc(obj.vars, func(a, b varDef) int { return int(a.off) - int(b.off) }) + rodataMap := len(obj.maps) + obj.maps = append(obj.maps, mapDef{".rodata", 2, 4, uint32(len(obj.rodata)), 1, 0x80}) // BPF_MAP_TYPE_ARRAY, BPF_F_RDONLY_PROG + + rels := data(".rel" + prog.Name) + for b := rels; len(b) >= 16; b = b[16:] { + off, info := order.Uint64(b), order.Uint64(b[8:]) + if info&0xffffffff != rBPF6464 || info>>32 == 0 || int(info>>32) > len(syms) { + return nil, fmt.Errorf("unsupported relocation %#x at %#x", info, off) + } + s := syms[info>>32-1] // Symbols leaves out the null symbol + r := reloc{insn: int(off / 8), off: -1} + switch s.Section { + case section["maps"]: + r.m = slices.Index(mapOffs, s.Value) + case section[".rodata"]: + r.m, r.off = rodataMap, int64(s.Value)+int64(int32(order.Uint32(obj.code[off+4:]))) + default: + return nil, fmt.Errorf("relocation against %s, which is neither a map nor read-only data", s.Name) + } + obj.relocs = append(obj.relocs, r) + } + return obj, nil +} + +func header(src, tags string) *bytes.Buffer { + b := new(bytes.Buffer) + fmt.Fprintf(b, "// Code generated by tools/bpfgen from %s; DO NOT EDIT.\n\n//go:build %s\n\npackage main\n\n", src, tags) + return b +} + +// perOrder is what differs between the byte orders: the code, its relocations and the read-only +// data as compiled. +func perOrder(src, ident, base, arches string, obj *object) []byte { + b := header(src, "linux && ("+arches+")") + fmt.Fprintf(b, "import _ \"embed\"\n\n//go:embed %s.bin\nvar %sCode []byte\n\n", base, ident) + fmt.Fprintf(b, "var %sRodata = %#v\n\n", ident, obj.rodata) + fmt.Fprintf(b, "var %sRelocs = []bpfReloc{\n", ident) + for _, r := range obj.relocs { + fmt.Fprintf(b, "\t{insn: %d, m: %s, off: %d},\n", r.insn, mapConst(ident, obj.maps[r.m].name), r.off) + } + b.WriteString("}\n") + return b.Bytes() +} + +// shared is what both byte orders have alike: the maps, the variables and the license. +func shared(src, ident string, obj *object) []byte { + b := header(src, "linux") + fmt.Fprintf(b, "const %sLicense = %q\n\n", ident, obj.license) + fmt.Fprintf(b, "// The maps, the read-only data last\nconst (\n") + for i, m := range obj.maps { + fmt.Fprintf(b, "\t%s = %d\n", mapConst(ident, m.name), i) + } + fmt.Fprintf(b, ")\n\nvar %sMaps = []bpfMapSpec{\n", ident) + for _, m := range obj.maps { + fmt.Fprintf(b, "\t%s: {name: %q, typ: %d, keySize: %d, valueSize: %d, maxEntries: %d, flags: %#x},\n", mapConst(ident, m.name), m.name, m.typ, m.keySize, m.valueSize, m.max, m.flags) + } + fmt.Fprintf(b, "}\n\n// Where each variable sits in the read-only data, and its size\nvar (\n") + for _, v := range obj.vars { + fmt.Fprintf(b, "\t%sVar%s = bpfVar{off: %d, size: %d}\n", ident, camel(v.name), v.off, v.size) + } + b.WriteString(")\n") + return b.Bytes() +} + +func mapConst(ident, name string) string { return ident + "Map" + camel(name) } + +// camel turns a C name into the rest of a Go one: v4_of becomes V4Of, .rodata Rodata. +func camel(name string) string { + var b strings.Builder + for w := range strings.FieldsFuncSeq(name, func(r rune) bool { return r == '_' || r == '.' }) { + b.WriteString(strings.ToUpper(w[:1]) + w[1:]) + } + return b.String() +} + +func write(path string, src []byte) error { + out, err := format.Source(src) + if err != nil { + return fmt.Errorf("%s: %w", path, err) + } + return os.WriteFile(path, out, 0o644) +}