From a2970b923d8bbed35db163d21922985978bdefb6 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Wed, 13 May 2026 15:31:18 -0300 Subject: [PATCH] add ipv6 to xnet.StackAsync (#107) * add ipv6 to xnet.StackAsync * dns improvements * improve DNS workings of StackAsync * add tentative ICMPv6 * work on prefixes and fix some small bugs, plan UDP/TCP6 * fix bugs in StackAsync and ipv4.Prefix.Contains * update arpsubtable * completely remove legacy internet.StackIP for StackIPv4/v6 * ipv4/ipv6 tcp/udp * add TCP6/UDP6 dialing APIs * add xnet.Stack6 interface * more ipv6 integration into StackAsync; various tweaks to lneto and documentation+TODOs * add stack6 tests * replace netip.Prefix with ipv4.Prefix where it makes sense --- .gitignore | 1 + README.md | 12 +- dhcpv4/client.go | 7 +- dhcpv4/dhcp_test.go | 4 +- dhcpv4/server.go | 42 +- dhcpv4/server_test.go | 9 +- dns/client.go | 19 +- dns/dns.go | 50 ++ dns/dns_test.go | 21 +- examples/berkeley-listener/berkeley_server.go | 6 +- examples/gen/gen-binary-bench/main.go | 189 ++++++ examples/httpserver/main.go | 6 +- examples/httptap/dhcp.go | 2 +- examples/httptap/main.go | 5 +- examples/min-working-example/main-mwe.go | 2 +- examples/xcurl/main.go | 4 +- internet/stack-ethernet.go | 2 +- internet/stack-ip.go | 68 -- internet/stack-ip4.go | 37 ++ internet/stack-ip6.go | 36 + internet/stack-ports.go | 20 +- internet/stackbasic_test.go | 26 +- internet/tcplistener_test.go | 28 +- ipv4/definitions.go | 4 +- ipv4/prefix .go | 130 ++++ ipv4/prefix_test.go | 228 +++++++ lneto_test.go | 16 + tcp/control.go | 14 +- tcp/listener.go | 1 - x/xnet/stack-async.go | 350 ++++++---- x/xnet/stack-go.go | 2 +- x/xnet/stack6.go | 212 ++++++ x/xnet/stack6_test.go | 618 ++++++++++++++++++ x/xnet/subnet-table.go | 87 ++- x/xnet/xnet_arp_test.go | 8 +- x/xnet/xnet_bench_test.go | 10 +- x/xnet/xnet_concurrent_test.go | 2 +- x/xnet/xnet_dns_test.go | 2 +- x/xnet/xnet_fuzz_test.go | 6 +- x/xnet/xnet_icmp_test.go | 4 +- x/xnet/xnet_listener_test.go | 8 +- x/xnet/xnet_mdns_test.go | 12 +- x/xnet/xnet_subnettable_test.go | 9 +- x/xnet/xnet_test.go | 14 +- 44 files changed, 1938 insertions(+), 395 deletions(-) create mode 100644 examples/gen/gen-binary-bench/main.go delete mode 100644 internet/stack-ip.go create mode 100644 ipv4/prefix .go create mode 100644 ipv4/prefix_test.go create mode 100644 x/xnet/stack6.go create mode 100644 x/xnet/stack6_test.go diff --git a/.gitignore b/.gitignore index 8354845..b8740dd 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ vendor/ **__debug_bin* # `__debug_bin` Debug binary generated in VSCode when using the built-in debugger. *bin +/gen-binary-bench /bridge # IDE diff --git a/README.md b/README.md index e87a362..42b657c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![go.dev reference](https://pkg.go.dev/badge/github.com/soypat/lneto)](https://pkg.go.dev/github.com/soypat/lneto) [![Go Report Card](https://goreportcard.com/badge/github.com/soypat/lneto)](https://goreportcard.com/report/github.com/soypat/lneto) [![codecov](https://codecov.io/gh/soypat/lneto/branch/main/graph/badge.svg)](https://codecov.io/gh/soypat/lneto) -[![Go](https://github.com/soypat/lneto/actions/workflows/go.yml/badge.svg)](https://github.com/soypat/lneto/actions/workflows/go.yml) +[![Go](https://github.com/soypat/lneto/actions/workflows/ci.yaml/badge.svg)](https://github.com/soypat/lneto/actions/workflows/ci.yaml) [![sourcegraph](https://sourcegraph.com/github.com/soypat/lneto/-/badge.svg)](https://sourcegraph.com/github.com/soypat/lneto?badge) Userspace networking primitives. @@ -29,12 +29,14 @@ Get a quick showcase of how lneto can be configured and how to get a TCP listeni ### Binary size comparisons All examples include IPv4, ARP, ICMP, TCP and UDP functionality. Go and TinyGo default build flags used. **DNC**= Does Not Compile. +```sh +go run ./examples/gen/gen-binary-bench # generate table +``` - -| Program | Extra Protocols | Packet capture printing | amd64 Go | WASM Go | amd64 TinyGo | WASM TinyGo | Pico TinyGo | +| Program | Extra Protocols | Packet capture printing | amd64 Go | WASM Go | amd64 TinyGo | WASM TinyGo | Pico TinyGo | |---|:---:|:---:|---|---|---|---|---| -| [Lneto MWE](./examples/min-working-example/) | DNS,NTP,DHCP | ✅ | 3.8MB | 4.3MB | 1.3MB | 934kB | 181kB | -| [Gvisor MWE w/ go-net](./examples/_import_examples/gvisor-mwe/)| None | ❌ | 6.6MB | 7.5MB | DNC | DNC | DNC | +| [Lneto MWE](./examples/min-working-example/) | DNS,NTP,DHCP | ✅ | 3.8MB | 4.3MB | 1.6MB | 1.2MB | 185kB | +| [Gvisor MWE w/ go-net](./examples/_import_examples/gvisor-mwe/) | None | ❌ | 6.6MB | 7.4MB | DNC | DNC | DNC | ## `xcurl` example You may try lneto out on linux with the [xcurl example](./examples/xcurl/) which gets an HTTP page by doing all the low-level networking part using absolutely no standard library. diff --git a/dhcpv4/client.go b/dhcpv4/client.go index e960ecb..e24d969 100644 --- a/dhcpv4/client.go +++ b/dhcpv4/client.go @@ -366,12 +366,11 @@ func (d *Client) DNSServerFirst() netip.Addr { return d.dns[0] } -func (d *Client) SubnetPrefix() netip.Prefix { +func (d *Client) SubnetPrefix() ipv4.Prefix { if !d.offer.valid { - return netip.Prefix{} + return ipv4.Prefix{} } - m, _ := netip.AddrFrom4(d.offer.addr).Prefix(int(d.SubnetCIDRBits())) - return m + return ipv4.PrefixFrom(d.offer.addr, d.SubnetCIDRBits()) } func (d *Client) SubnetCIDRBits() uint8 { diff --git a/dhcpv4/dhcp_test.go b/dhcpv4/dhcp_test.go index 2a87b97..c9bf10a 100644 --- a/dhcpv4/dhcp_test.go +++ b/dhcpv4/dhcp_test.go @@ -1,10 +1,10 @@ package dhcpv4 import ( - "net/netip" "testing" "github.com/soypat/lneto/internal" + "github.com/soypat/lneto/ipv4" ) func TestClientServer(t *testing.T) { @@ -29,7 +29,7 @@ func TestClientServer(t *testing.T) { } sv.Configure(ServerConfig{ ServerAddr: svAddr, - Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24), + Subnet: ipv4.PrefixFrom(svAddr, 24), }) // CLIENT DISCOVER. assertClState(StateInit) diff --git a/dhcpv4/server.go b/dhcpv4/server.go index ae7532b..16f260b 100644 --- a/dhcpv4/server.go +++ b/dhcpv4/server.go @@ -4,18 +4,18 @@ import ( "encoding/binary" "errors" "fmt" - "net/netip" "github.com/soypat/lneto" "github.com/soypat/lneto/internal" + "github.com/soypat/lneto/ipv4" ) var errOptionNotFit = errors.New("DHCPv4: options dont fit") type Server struct { connID uint64 - nextAddr netip.Addr - prefix netip.Prefix + nextAddr [4]byte + subnet ipv4.Prefix hosts map[[36]byte]serverEntry vld lneto.Validator pending int @@ -35,7 +35,7 @@ type ServerConfig struct { // DNS server address advertised to clients. Zero value omits the option. DNS [4]byte // Subnet defines the network prefix for address allocation and subnet mask responses. - Subnet netip.Prefix + Subnet ipv4.Prefix // LeaseSeconds is the lease duration. Zero defaults to 3600. LeaseSeconds uint32 // Port is the server listening port. Zero defaults to DefaultServerPort. @@ -63,10 +63,9 @@ type serverEntry struct { // The connection ID is incremented on each call to invalidate existing connections. // The hosts map is reused across calls to avoid reallocation. func (sv *Server) Configure(cfg ServerConfig) error { - svAddr := netip.AddrFrom4(cfg.ServerAddr) if !cfg.Subnet.IsValid() { return errors.New("dhcpv4 server: invalid subnet") - } else if !cfg.Subnet.Contains(svAddr) { + } else if !cfg.Subnet.Contains(cfg.ServerAddr) { return errors.New("dhcpv4 server: server address outside subnet") } port := cfg.Port @@ -90,10 +89,10 @@ func (sv *Server) Configure(cfg ServerConfig) error { siaddr: cfg.ServerAddr, gwaddr: cfg.Gateway, dns: cfg.DNS, - prefix: cfg.Subnet, + subnet: cfg.Subnet, port: port, leaseSeconds: lease, - nextAddr: svAddr, + nextAddr: cfg.Subnet.Next(cfg.ServerAddr), hosts: hosts, } return nil @@ -263,8 +262,8 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...) nopt += n } - if sv.prefix.IsValid() { - bits := uint(sv.prefix.Bits()) + if sv.subnet.IsValid() { + bits := uint(sv.subnet.Bits()) mask := ^uint32(0) << (32 - bits) var maskBuf [4]byte binary.BigEndian.PutUint32(maskBuf[:], mask) @@ -317,18 +316,18 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) // it is preferred. Returns false if the pool is exhausted. func (sv *Server) allocAddr(reqAddr []byte) ([4]byte, bool) { if len(reqAddr) == 4 { - candidate := netip.AddrFrom4([4]byte(reqAddr)) - if sv.prefix.Contains(candidate) && candidate.As4() != sv.siaddr && !sv.isAddrAssigned(candidate) { - return candidate.As4(), true + candidate := [4]byte(reqAddr) + if sv.subnet.Contains(candidate) && candidate != sv.siaddr && !sv.isAddrAssigned(candidate) { + return candidate, true } } - sv.nextAddr = sv.nextAddr.Next() - if !sv.prefix.Contains(sv.nextAddr) { - return [4]byte{}, false - } // Reject broadcast address (all host bits set). - a := sv.nextAddr.As4() - hostBits := uint(32 - sv.prefix.Bits()) + a := sv.nextAddr + sv.nextAddr = sv.subnet.Next(sv.nextAddr) + if sv.nextAddr == sv.siaddr { + sv.nextAddr = sv.subnet.Next(sv.nextAddr) + } + hostBits := uint(32 - sv.subnet.Bits()) hostMask := ^uint32(0) >> (32 - hostBits) if binary.BigEndian.Uint32(a[:])&hostMask == hostMask { return [4]byte{}, false @@ -336,10 +335,9 @@ func (sv *Server) allocAddr(reqAddr []byte) ([4]byte, bool) { return a, true } -func (sv *Server) isAddrAssigned(addr netip.Addr) bool { - a4 := addr.As4() +func (sv *Server) isAddrAssigned(addr [4]byte) bool { for _, v := range sv.hosts { - if v.addr == a4 { + if v.addr == addr { return true } } diff --git a/dhcpv4/server_test.go b/dhcpv4/server_test.go index 025b136..1eb95bb 100644 --- a/dhcpv4/server_test.go +++ b/dhcpv4/server_test.go @@ -1,14 +1,15 @@ package dhcpv4 import ( - "net/netip" "testing" + + "github.com/soypat/lneto/ipv4" ) func testServerConfig(svAddr [4]byte) ServerConfig { return ServerConfig{ ServerAddr: svAddr, - Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24), + Subnet: ipv4.PrefixFrom(svAddr, 24), } } @@ -180,7 +181,7 @@ func TestServerOfferContainsOptions(t *testing.T) { ServerAddr: svAddr, Gateway: gwAddr, DNS: dnsAddr, - Subnet: netip.PrefixFrom(netip.AddrFrom4(svAddr), 24), + Subnet: ipv4.PrefixFrom(svAddr, 24), LeaseSeconds: 7200, }) @@ -295,7 +296,7 @@ func TestServerConfigValidation(t *testing.T) { } err = sv.Configure(ServerConfig{ ServerAddr: [4]byte{10, 0, 0, 1}, - Subnet: netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 1, 0}), 24), + Subnet: ipv4.PrefixFrom([4]byte{192, 168, 1, 0}, 24), }) if err == nil { t.Error("expected error for server address outside subnet") diff --git a/dns/client.go b/dns/client.go index 2713125..8897066 100644 --- a/dns/client.go +++ b/dns/client.go @@ -4,6 +4,7 @@ import ( "log/slog" "math" "net" + "net/netip" "github.com/soypat/lneto" "github.com/soypat/lneto/internal" @@ -104,7 +105,7 @@ func (c *Client) isClosed() bool { return c.state == CQueryIdle || c.state == CQueryAborted } -func (c *Client) MessageCopyTo(dst *Message) (done bool, err error) { +func (c *Client) ResponseCopyTo(dst *Message) (done bool, err error) { if !c.respFlags.IsResponse() { return false, nil } @@ -116,11 +117,19 @@ func (c *Client) MessageCopyTo(dst *Message) (done bool, err error) { return true, nil } -func (c *Client) Answers() []Resource { - if c.state != CQueryDone { - return nil +func (c *Client) ResponseAnswerLookup(dst []netip.Addr, host string) (uint16, error) { + if !c.respFlags.IsResponse() { + return 0, nil } - return c.msg.Answers + rcode := c.respFlags.ResponseCode() + if rcode != 0 { + return 0, rcode + } + return c.msg.WriteAnswers(dst, host) +} + +func (c *Client) ResponseFlags() (HeaderFlags, bool) { + return c.respFlags, c.respFlags.IsResponse() } func (c *Client) Abort() { diff --git a/dns/dns.go b/dns/dns.go index 9c10fa6..db4e3e6 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "math" + "net/netip" "slices" "strconv" "strings" @@ -59,10 +60,38 @@ type ResourceHeader struct { Length uint16 } +// Name is a wire representation of a DNS name. type Name struct { data []byte } +// EqualString checks if the name receiver matches the strname string (non-wire formatted) name. +func (n Name) EqualString(strname string) bool { + data := n.data + for len(data) > 0 { + labelLen := int(data[0]) + if labelLen == 0 { + return strname == "" || strname == "." + } + if len(data) < 1+labelLen { + return false + } + label := data[1 : 1+labelLen] + var seg string + idx := strings.IndexByte(strname, '.') + if idx < 0 { + seg, strname = strname, "" + } else { + seg, strname = strname[:idx], strname[idx+1:] + } + if len(seg) != len(label) || seg != string(label) { + return false + } + data = data[1+labelLen:] + } + return false +} + // NamesEqual reports whether two DNS names are equal by comparing // their wire-format representations directly. This is case-sensitive; // for case-insensitive comparison use [NamesEqualFold]. @@ -270,6 +299,27 @@ func (m *Message) AppendTo(buf []byte, txid uint16, flags HeaderFlags) (_ []byte return buf, nil } +func (m *Message) WriteAnswers(dst []netip.Addr, host string) (n uint16, err error) { + for i := range m.Answers { + if int(n) >= len(dst) { + return n, lneto.ErrExhausted + } + ans := &m.Answers[i] + hdr := ans.Header() + if !hdr.Name.EqualString(host) { + continue + } + var ok bool + dst[n], ok = netip.AddrFromSlice(ans.RawData()) + if !ok { + err = lneto.ErrInvalidAddr + } else { + n++ + } + } + return n, err +} + func (m *Message) Len() uint16 { return SizeHeader + m.lenResources() } diff --git a/dns/dns_test.go b/dns/dns_test.go index cad0898..64bfc1b 100644 --- a/dns/dns_test.go +++ b/dns/dns_test.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + "net/netip" "strings" "testing" ) @@ -288,23 +289,23 @@ func TestClient_ReceivesDNSResponse(t *testing.T) { } // Check the client received the answer. - answers := client.Answers() - if len(answers) != 1 { - t.Fatalf("expected 1 answer, got %d", len(answers)) + var addrs [4]netip.Addr + answers, err := client.ResponseAnswerLookup(addrs[:], hostname) + if answers != 1 { + t.Fatalf("expected 1 answer, got %d", answers) } - - data := answers[0].RawData() - if len(data) != 4 { - t.Fatalf("expected 4 bytes in answer, got %d", len(data)) + addr := addrs[0] + if !addr.Is4() { + t.Fatalf("expected 4 bytes in answer, got %d", addr.BitLen()/8) } - if [4]byte(data) != wantIP { - t.Errorf("expected IP %v, got %v", wantIP, data) + if addr.As4() != wantIP { + t.Errorf("expected IP %v, got %v", wantIP, addr.String()) } // Test MessageCopyTo as well. var lookup Message lookup.LimitResourceDecoding(1, 1, 0, 0) - done, err := client.MessageCopyTo(&lookup) + done, err := client.ResponseCopyTo(&lookup) if err != nil { t.Fatal("MessageCopyTo error:", err) } diff --git a/examples/berkeley-listener/berkeley_server.go b/examples/berkeley-listener/berkeley_server.go index 194f340..f68d00a 100644 --- a/examples/berkeley-listener/berkeley_server.go +++ b/examples/berkeley-listener/berkeley_server.go @@ -137,7 +137,7 @@ func run() error { pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt) addr := stack.Addr4() pfbuf = bytes.ReplaceAll(pfbuf, ipv4.AppendFormatAddr(nil, addr), []byte("us")) - pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us")) + pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us")) pfbuf = append(pfbuf, ']', '\n') if err != nil { return err @@ -232,7 +232,7 @@ func run() error { return fmt.Errorf("ARP resolution of router failed: %w", err) } // Set gateway on the async stack (exported API). - stack.SetGateway6(routerHw) + stack.SetGatewayHardwareAddr(routerHw) // Create Berkeley listener via SocketNetip laddr := netip.AddrPortFrom(netip.IPv4Unspecified(), uint16(flagPort)) @@ -332,7 +332,7 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) { err := mockStack.Reset(xnet.StackConfig{ StaticAddress4: subnet.Addr().Next().As4(), MaxActiveTCPPorts: 1, - HardwareAddress: stack.Gateway6(), + HardwareAddress: stack.GatewayHardwareAddr(), Hostname: "the-other", MTU: uint16(stack.MTU()), RandSeed: int64(stack.Prand32()), diff --git a/examples/gen/gen-binary-bench/main.go b/examples/gen/gen-binary-bench/main.go new file mode 100644 index 0000000..51cc468 --- /dev/null +++ b/examples/gen/gen-binary-bench/main.go @@ -0,0 +1,189 @@ +//go:build !tinygo && linux + +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +type program struct { + Name string + Link string // relative path for README link + Dir string // directory to build from, relative to repo root + ExtraProtocols string + PacketCapture bool +} + +type buildTarget struct { + Name string + ext string // output file extension (determines format for tinygo) + build func(dir, outFile string) error +} + +type result struct { + BinarySize int64 + CompileTime time.Duration + DNC bool // Does Not Compile + Err error +} + +func (r result) sizeString() string { + if r.DNC { + return "DNC" + } + if r.Err != nil { + return "ERR" + } + return formatSize(r.BinarySize) +} + +func formatSize(n int64) string { + const mb = 1024 * 1024 + if n >= mb { + return fmt.Sprintf("%.1fMB", float64(n)/mb) + } + return fmt.Sprintf("%dkB", (n+512)/1024) +} + +func goBuild(goos, goarch string) func(dir, out string) error { + return func(dir, out string) error { + cmd := exec.Command("go", "build", "-o", out, ".") + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%w: %s", err, out) + } + return nil + } +} + +func tinygoBuild(target string) func(dir, out string) error { + return func(dir, out string) error { + args := []string{"build"} + if target != "" { + args = append(args, "-target="+target) + } + args = append(args, "-o", out, ".") + cmd := exec.Command("tinygo", args...) + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("%w: %s", err, out) + } + return nil + } +} + +var buildTargets = []buildTarget{ + {Name: "amd64 Go", ext: ".elf", build: goBuild("linux", "amd64")}, + {Name: "WASM Go", ext: ".wasm", build: goBuild("wasip1", "wasm")}, + {Name: "amd64 TinyGo", ext: ".elf", build: tinygoBuild("")}, + {Name: "WASM TinyGo", ext: ".wasm", build: tinygoBuild("wasm")}, + {Name: "Pico TinyGo", ext: ".bin", build: tinygoBuild("pico")}, +} + +var programs = []program{ + { + Name: "Lneto MWE", + Link: "./examples/min-working-example/", + Dir: "examples/min-working-example", + ExtraProtocols: "DNS,NTP,DHCP", + PacketCapture: true, + }, + { + Name: "Gvisor MWE w/ go-net", + Link: "./examples/_import_examples/gvisor-mwe/", + Dir: "examples/_import_examples/gvisor-mwe", + ExtraProtocols: "None", + PacketCapture: false, + }, +} + +func measure(dir, outFile string, fn func(dir, out string) error) result { + start := time.Now() + err := fn(dir, outFile) + elapsed := time.Since(start) + if err != nil { + return result{DNC: true, CompileTime: elapsed, Err: err} + } + fi, err := os.Stat(outFile) + if err != nil { + return result{Err: err, CompileTime: elapsed} + } + size := fi.Size() + os.Remove(outFile) + return result{BinarySize: size, CompileTime: elapsed} +} + +func main() { + root := flag.String("root", ".", "path to repository root") + flag.Parse() + + repoRoot, err := filepath.Abs(*root) + if err != nil { + panic(err) + } + + tmpDir, err := os.MkdirTemp("", "binbench-*") + if err != nil { + panic(err) + } + defer os.RemoveAll(tmpDir) + + type row struct { + prog program + results []result + } + rows := make([]row, len(programs)) + for i, prog := range programs { + dir := filepath.Join(repoRoot, prog.Dir) + results := make([]result, len(buildTargets)) + for j, bt := range buildTargets { + outFile := filepath.Join(tmpDir, fmt.Sprintf("p%d_t%d%s", i, j, bt.ext)) + fmt.Fprintf(os.Stderr, "building %s for %s...\n", prog.Name, bt.Name) + r := measure(dir, outFile, bt.build) + if r.DNC { + fmt.Fprintf(os.Stderr, " DNC: %v\n", r.Err) + } + results[j] = r + } + rows[i] = row{prog: prog, results: results} + } + + // Print markdown table. + headers := []string{"Program", "Extra Protocols", "Packet capture printing"} + for _, bt := range buildTargets { + headers = append(headers, bt.Name) + } + fmt.Printf("| %s |\n", strings.Join(headers, " | ")) + + aligns := make([]string, len(headers)) + aligns[0] = "---" + aligns[1] = ":---:" + aligns[2] = ":---:" + for i := 3; i < len(aligns); i++ { + aligns[i] = "---" + } + fmt.Printf("|%s|\n", strings.Join(aligns, "|")) + + for _, r := range rows { + pcap := "❌" + if r.prog.PacketCapture { + pcap = "✅" + } + cols := []string{ + fmt.Sprintf("[%s](%s)", r.prog.Name, r.prog.Link), + r.prog.ExtraProtocols, + pcap, + } + for _, res := range r.results { + cols = append(cols, res.sizeString()) + } + fmt.Printf("| %s |\n", strings.Join(cols, " | ")) + } +} diff --git a/examples/httpserver/main.go b/examples/httpserver/main.go index c5e0c40..d3527e9 100644 --- a/examples/httpserver/main.go +++ b/examples/httpserver/main.go @@ -137,7 +137,7 @@ func run() (err error) { pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt) pfbuf = bytes.ReplaceAll(pfbuf, ipv4.AppendFormatAddr(nil, stack.Addr4()), []byte("us")) - pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us")) + pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us")) pfbuf = append(pfbuf, ']', '\n') if err != nil { return err @@ -220,7 +220,7 @@ func run() (err error) { return fmt.Errorf("ARP resolution of router failed: %w", err) } timeResolveRouterHW() - stack.SetGateway6(routerHw) + stack.SetGatewayHardwareAddr(routerHw) svPort := uint16(flagPort) fmt.Printf("Listening on %s:%d\n", ipv4.AppendFormatAddr(nil, stack.Addr4()), svPort) @@ -233,7 +233,7 @@ func run() (err error) { TxBuf: make([]byte, mtu), TxPacketQueueSize: 3, }) - err = stack.ListenTCP(&conn, svPort) + err = stack.ListenTCP4(&conn, svPort) if err != nil { return fmt.Errorf("listen TCP: %w", err) } diff --git a/examples/httptap/dhcp.go b/examples/httptap/dhcp.go index 0f313c0..94505c7 100644 --- a/examples/httptap/dhcp.go +++ b/examples/httptap/dhcp.go @@ -55,7 +55,7 @@ type arpEntry struct { // newDHCPInterceptor creates a dhcpInterceptor that wraps iface and serves // DHCP from the given server address and subnet. -func newDHCPInterceptor(iface ltesto.Interface, svIP [4]byte, svMAC [6]byte, subnet netip.Prefix) (*dhcpInterceptor, error) { +func newDHCPInterceptor(iface ltesto.Interface, svIP [4]byte, svMAC [6]byte, subnet ipv4.Prefix) (*dhcpInterceptor, error) { d := &dhcpInterceptor{ inner: iface, svMAC: svMAC, diff --git a/examples/httptap/main.go b/examples/httptap/main.go index ab9eb29..86a99ba 100644 --- a/examples/httptap/main.go +++ b/examples/httptap/main.go @@ -18,6 +18,7 @@ import ( "github.com/soypat/lneto/internal" "github.com/soypat/lneto/internal/ltesto" "github.com/soypat/lneto/internet/pcap" + "github.com/soypat/lneto/ipv4" ) func main() { @@ -73,8 +74,10 @@ func run() error { if err != nil { return err } + svIP := ipMask.Addr().As4() - iface, err = newDHCPInterceptor(iface, svIP, hwaddr, ipMask.Masked()) + subnet := ipv4.PrefixFromNetip(ipMask) + iface, err = newDHCPInterceptor(iface, svIP, hwaddr, subnet.Masked()) if err != nil { return fmt.Errorf("DHCP interceptor: %w", err) } diff --git a/examples/min-working-example/main-mwe.go b/examples/min-working-example/main-mwe.go index 9229b7d..098f28d 100644 --- a/examples/min-working-example/main-mwe.go +++ b/examples/min-working-example/main-mwe.go @@ -92,7 +92,7 @@ func run(ctx context.Context, stack *xnet.StackAsync) error { if err != nil { return fmt.Errorf("resolving router MAC: %w", err) } - stack.SetGateway6(gateway) + stack.SetGatewayHardwareAddr(gateway) berkstack := stack.StackBlocking(stackBackoff).StackGo(xnet.StackGoConfig{ ListenerPoolConfig: xnet.TCPPoolConfig{ PoolSize: tcpConnPoolSize, diff --git a/examples/xcurl/main.go b/examples/xcurl/main.go index a7594d3..b3f721c 100644 --- a/examples/xcurl/main.go +++ b/examples/xcurl/main.go @@ -159,7 +159,7 @@ func run() (err error) { pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt) addr := stack.Addr4() pfbuf = bytes.ReplaceAll(pfbuf, addr[:], []byte("us")) - pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us")) + pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us")) pfbuf = append(pfbuf, ']', '\n') if err != nil { return err @@ -248,7 +248,7 @@ func run() (err error) { return fmt.Errorf("ARP resolution of router failed: %w", err) } timeResolveRouterHW() - stack.SetGateway6(routerHw) + stack.SetGatewayHardwareAddr(routerHw) if flagDoNTP { timeLookupNTP := timer("NTP IP lookup") const ntpHost = "pool.ntp.org" diff --git a/internet/stack-ethernet.go b/internet/stack-ethernet.go index 78052f9..3a6360e 100644 --- a/internet/stack-ethernet.go +++ b/internet/stack-ethernet.go @@ -128,7 +128,7 @@ func (ls *StackEthernet) LocalPort() uint16 { return 0 } func (ls *StackEthernet) Protocol() uint64 { return 1 } -func (ls *StackEthernet) Register(h lneto.StackNode) error { +func (ls *StackEthernet) RegisterEthernet(h lneto.StackNode) error { proto := h.Protocol() if proto > math.MaxUint16 || proto <= 1500 { return lneto.ErrInvalidConfig diff --git a/internet/stack-ip.go b/internet/stack-ip.go deleted file mode 100644 index 18bf2a2..0000000 --- a/internet/stack-ip.go +++ /dev/null @@ -1,68 +0,0 @@ -package internet - -import ( - "log/slog" - - "github.com/soypat/lneto" - "github.com/soypat/lneto/ethernet" -) - -var _ lneto.StackNode = (*StackIP)(nil) - -type StackIP struct { - connID uint64 - stackip4 - stackip6 -} - -func (stackip *StackIP) Reset(vld *lneto.Validator, maxNodes4, maxNodes6 int) error { - if maxNodes4 <= 0 && maxNodes6 <= 0 || vld == nil { - return lneto.ErrInvalidConfig - } - stackip.connID++ - stackip.reset4(vld, maxNodes4) - stackip.reset6(vld, maxNodes6) - return nil -} - -func (stackip *StackIP) ConnectionID() *uint64 { - return &stackip.connID -} - -func (stackip *StackIP) Protocol() uint64 { - return uint64(ethernet.TypeIPv4) // Only support ipv4 for now. -} - -func (stackip *StackIP) LocalPort() uint16 { return 0 } - -func (stackip *StackIP) SetLogger(logger *slog.Logger) { - stackip.stackip4.handlers.log = logger - stackip.stackip6.handlers.log = logger -} - -func (stackip *StackIP) Demux(carrierData []byte, offset int) error { - debugLog("ip:demux") - if len(carrierData) < 1 { - return lneto.ErrTruncatedFrame - } - version := carrierData[offset] >> 4 - switch version { - case 4: - return stackip.stackip4.demux4(carrierData, offset) - case 6: - return stackip.stackip6.demux6(carrierData, offset) - default: - return lneto.ErrUnsupported - } -} - -func (stackip *StackIP) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) { - if offsetToFrame != offsetToIP { - return 0, lneto.ErrBug - } - n, err = stackip.stackip4.encapsulate4(carrierData, offsetToIP) - if len(stackip.stackip6.handlers.nodes) > 0 && n == 0 { - n, err = stackip.stackip6.encapsulate6(carrierData, offsetToIP) - } - return n, err -} diff --git a/internet/stack-ip4.go b/internet/stack-ip4.go index 36c8900..b71cdee 100644 --- a/internet/stack-ip4.go +++ b/internet/stack-ip4.go @@ -5,6 +5,7 @@ import ( "log/slog" "github.com/soypat/lneto" + "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/internal" "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/tcp" @@ -15,6 +16,42 @@ import ( // It is meant to be embedded within StackNodes. // var _ lneto.StackNode = (*stackip4)(nil) +type StackIPv4 struct { + connID uint64 + stackip4 +} + +func (stackip4 *StackIPv4) Reset(vld *lneto.Validator, maxNodes int) error { + stackip4.reset4(vld, maxNodes) + return nil +} + +func (stackip *StackIPv4) ConnectionID() *uint64 { + return &stackip.connID +} + +func (stackip *StackIPv4) Protocol() uint64 { + return uint64(ethernet.TypeIPv4) +} + +func (stackip *StackIPv4) LocalPort() uint16 { return 0 } + +func (stackip *StackIPv4) SetLogger(logger *slog.Logger) { + stackip.stackip4.handlers.log = logger +} + +func (stackip *StackIPv4) Demux(carrierData []byte, offset int) error { + debugLog("ip:demux") + return stackip.stackip4.demux4(carrierData, offset) +} + +func (stackip *StackIPv4) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) { + if offsetToFrame != offsetToIP { + return 0, lneto.ErrBug + } + return stackip.stackip4.encapsulate4(carrierData, offsetToIP) +} + type stackip4 struct { handlers handlers vld *lneto.Validator diff --git a/internet/stack-ip6.go b/internet/stack-ip6.go index b219f69..2a386da 100644 --- a/internet/stack-ip6.go +++ b/internet/stack-ip6.go @@ -4,6 +4,7 @@ import ( "log/slog" "github.com/soypat/lneto" + "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/ipv6" "github.com/soypat/lneto/tcp" "github.com/soypat/lneto/udp" @@ -12,6 +13,41 @@ import ( // stackip6 is NOT a StackNode implementation. // It is meant to be embedded within StackNodes. // var _ lneto.StackNode = (*stackip6)(nil) +type StackIPv6 struct { + connID uint64 + stackip6 +} + +func (stackip4 *StackIPv6) Reset(vld *lneto.Validator, maxNodes int) error { + stackip4.reset6(vld, maxNodes) + return nil +} + +func (stackip *StackIPv6) ConnectionID() *uint64 { + return &stackip.connID +} + +func (stackip *StackIPv6) Protocol() uint64 { + return uint64(ethernet.TypeIPv6) +} + +func (stackip *StackIPv6) LocalPort() uint16 { return 0 } + +func (stackip *StackIPv6) SetLogger(logger *slog.Logger) { + stackip.stackip6.handlers.log = logger +} + +func (stackip *StackIPv6) Demux(carrierData []byte, offset int) error { + debugLog("ip:demux") + return stackip.stackip6.demux6(carrierData, offset) +} + +func (stackip *StackIPv6) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) { + if offsetToFrame != offsetToIP { + return 0, lneto.ErrBug + } + return stackip.stackip6.encapsulate6(carrierData, offsetToIP) +} type stackip6 struct { handlers handlers diff --git a/internet/stack-ports.go b/internet/stack-ports.go index c80df2f..368de6f 100644 --- a/internet/stack-ports.go +++ b/internet/stack-ports.go @@ -33,8 +33,6 @@ func (ps *StackPorts) ResetTCP(maxNodes uint16) error { func (ps *StackPorts) Reset(protocol uint64, dstPortOffset, maxNodes uint16) error { if protocol > math.MaxUint16 { return lneto.ErrInvalidConfig - } else if maxNodes <= 0 { - return lneto.ErrInvalidConfig } ps.handlers.reset("StackPorts(proto="+strconv.Itoa(int(protocol))+")", int(maxNodes)) *ps = StackPorts{ @@ -105,25 +103,29 @@ type StackPortsMACFiltered struct { sp StackPorts } -func (mfsp *StackPortsMACFiltered) Register(h lneto.StackNode, addr []byte) error { +func (mfsp *StackPortsMACFiltered) RegisterMACFiltered(h lneto.StackNode, macAddr []byte) error { + // TODO(soypat): We can likely constrain memory and the slice lifetime if StackPortsMACFiltered owns it + // or better yet, if the handlers node slice owns the memory. We need to think carefully of who has write access (the ARP and NDP handlers) + // and make sure that they never write after the connection has been terminated. Idea: + // RegisterMACFiltered(h lneto.StackNode, filterMAC bool) (macAddr *[6]byte, connIDthing *uint8, err error) port := h.LocalPort() proto := h.Protocol() if port <= 0 { return lneto.ErrZeroSource } else if proto != uint64(mfsp.sp.protocol) { return lneto.ErrInvalidConfig - } else if addr != nil && len(addr) != 6 { + } else if macAddr != nil && len(macAddr) != 6 { return lneto.ErrInvalidAddr } - return mfsp.sp.handlers.registerByPortProto(nodeFromStackNode(h, port, proto, addr)) + return mfsp.sp.handlers.registerByPortProto(nodeFromStackNode(h, port, proto, macAddr)) } -func (ps *StackPortsMACFiltered) ResetUDP(maxNodes uint16) error { - return ps.sp.ResetUDP(maxNodes) +func (ps *StackPortsMACFiltered) ResetUDP(maxNodes uint16) { + ps.sp.ResetUDP(maxNodes) // Can't error. } -func (ps *StackPortsMACFiltered) ResetTCP(maxNodes uint16) error { - return ps.sp.ResetTCP(maxNodes) +func (ps *StackPortsMACFiltered) ResetTCP(maxNodes uint16) { + ps.sp.ResetTCP(maxNodes) // Can't error. } func (ps *StackPortsMACFiltered) Reset(protocol uint64, dstPortOffset, maxNodes uint16) error { diff --git a/internet/stackbasic_test.go b/internet/stackbasic_test.go index a31f90e..67d529e 100644 --- a/internet/stackbasic_test.go +++ b/internet/stackbasic_test.go @@ -11,7 +11,7 @@ import ( func TestBasicStack(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var sbCl, sbSv StackIP + var sbCl, sbSv StackIPv4 var connCl, connSv tcp.Conn setupClientServer(t, rng, &sbCl, &sbSv, &connCl, &connSv) var buf [2048]byte @@ -37,13 +37,13 @@ func TestBasicStack(t *testing.T) { func TestBasicStack2(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var sbCl, sbSv StackIP + var sbCl, sbSv StackIPv4 var connCl, connSv tcp.Conn setupClientServerEstablished(t, rng, &sbCl, &sbSv, &connCl, &connSv) } -func expectExchange(t *testing.T, from, to *StackIP, buf []byte) { +func expectExchange(t *testing.T, from, to lneto.StackNode, buf []byte) { t.Helper() n, err := from.Encapsulate(buf, 0, 0) if err != nil { @@ -58,13 +58,13 @@ func expectExchange(t *testing.T, from, to *StackIP, buf []byte) { } } -func setupClientServerEstablished(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) { +func setupClientServerEstablished(t *testing.T, rng *rand.Rand, client, server *StackIPv4, connClient, connServer *tcp.Conn) { t.Helper() setupClientServer(t, rng, client, server, connClient, connServer) testClientServerEstablish(t, client, server, connClient, connServer) } -func testClientServerEstablish(t *testing.T, client, server *StackIP, connClient, connServer *tcp.Conn) { +func testClientServerEstablish(t *testing.T, client, server lneto.StackNode, connClient, connServer *tcp.Conn) { t.Helper() var buf [2048]byte nextToSend := client @@ -93,7 +93,7 @@ func testClientServerEstablish(t *testing.T, client, server *StackIP, connClient func TestBasicStack6(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var sbCl, sbSv StackIP + var sbCl, sbSv StackIPv6 var connCl, connSv tcp.Conn setupClientServer6(t, rng, &sbCl, &sbSv, &connCl, &connSv) var buf [2048]byte @@ -119,13 +119,13 @@ func TestBasicStack6(t *testing.T) { func TestBasicStack6Established(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var sbCl, sbSv StackIP + var sbCl, sbSv StackIPv6 var connCl, connSv tcp.Conn setupClientServer6(t, rng, &sbCl, &sbSv, &connCl, &connSv) testClientServerEstablish(t, &sbCl, &sbSv, &connCl, &connSv) } -func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) { +func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIPv6, connClient, connServer *tcp.Conn) { t.Helper() _ = rng const maxNodes = 1 @@ -134,10 +134,10 @@ func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIP, c clip6 := netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}) // 2001:db8::2 svip := netip.AddrPortFrom(svip6, 80) clip := netip.AddrPortFrom(clip6, 1337) - if err := server.Reset(new(lneto.Validator), 0, maxNodes); err != nil { + if err := server.Reset(new(lneto.Validator), maxNodes); err != nil { t.Fatal(err) } - if err := client.Reset(new(lneto.Validator), 0, maxNodes); err != nil { + if err := client.Reset(new(lneto.Validator), maxNodes); err != nil { t.Fatal(err) } server.SetAddr6(svip6.As16()) @@ -172,14 +172,14 @@ func setupClientServer6(t *testing.T, rng *rand.Rand, client, server *StackIP, c } } -func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) { +func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIPv4, connClient, connServer *tcp.Conn) { const maxNodes = 1 bufsize := 2048 // Ensure buffer sizes are OK with reused buffers. svip := netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 0}), 80) clip := netip.AddrPortFrom(netip.AddrFrom4([4]byte{192, 168, 1, 1}), 1337) - server.Reset(new(lneto.Validator), maxNodes, 0) - client.Reset(new(lneto.Validator), maxNodes, 0) + server.Reset(new(lneto.Validator), maxNodes) + client.Reset(new(lneto.Validator), maxNodes) server.SetAddr4(svip.Addr().As4()) client.SetAddr4(clip.Addr().As4()) err := connServer.Configure(tcp.ConnConfig{ diff --git a/internet/tcplistener_test.go b/internet/tcplistener_test.go index 0cfe57c..01bd63d 100644 --- a/internet/tcplistener_test.go +++ b/internet/tcplistener_test.go @@ -12,7 +12,7 @@ import ( func TestListener_SingleConnection(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var clientStack, serverStack StackIP + var clientStack, serverStack StackIPv4 var clientConn, serverConn tcp.Conn var listener tcp.Listener @@ -66,7 +66,7 @@ func TestListener_SingleConnection(t *testing.T) { func TestListener_AcceptAfterEstablished(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var client1Stack, serverStack StackIP + var client1Stack, serverStack StackIPv4 var client1Conn, serverConn tcp.Conn var listener tcp.Listener pool := newMockTCPPool(2, 3, 2048) @@ -104,7 +104,7 @@ func TestListener_AcceptAfterEstablished(t *testing.T) { } // Setup second client and verify we can still accept. - var client2Stack StackIP + var client2Stack StackIPv4 var client2Conn tcp.Conn setupClient(t, &client2Stack, &client2Conn, netip.AddrFrom4(serverStack.Addr4()), serverPort, 1338) @@ -131,13 +131,13 @@ func TestListener_AcceptAfterEstablished(t *testing.T) { func TestListener_MultiConn(t *testing.T) { const numClients = 5 rng := rand.New(rand.NewSource(1)) - var serverStack StackIP + var serverStack StackIPv4 var serverConn tcp.Conn var listener tcp.Listener pool := newMockTCPPool(numClients, 3, 2048) // Create slices for clients. - clientStacks := make([]StackIP, numClients) + clientStacks := make([]StackIPv4, numClients) clientConns := make([]tcp.Conn, numClients) acceptedConns := make([]*tcp.Conn, numClients) @@ -257,7 +257,7 @@ func TestListener_MultiConn(t *testing.T) { // Close connections, alternating between client-initiated and server-initiated. for i := range numClients { - var closer, responder *StackIP + var closer, responder *StackIPv4 var closerConn, responderConn *tcp.Conn var serverClosed bool whoCloses := "client" @@ -313,7 +313,7 @@ func TestListener_MultiConn(t *testing.T) { func TestListener_RSTOnPoolExhaustion(t *testing.T) { rng := rand.New(rand.NewSource(1)) - var client1Stack, client2Stack, serverStack StackIP + var client1Stack, client2Stack, serverStack StackIPv4 var client1Conn, client2Conn, serverConn tcp.Conn var listener tcp.Listener @@ -606,21 +606,11 @@ func TestStackPorts_ECN_SYN_RST(t *testing.T) { } } -// tryExchange attempts an exchange but doesn't fail if no data to send. -func tryExchange(t *testing.T, from, to *StackIP, buf []byte) { - t.Helper() - n, err := from.Encapsulate(buf, -1, 0) - if err != nil || n == 0 { - return // No data to send. - } - _ = to.Demux(buf[:n], 0) // Ignore errors during close. -} - -func setupClient(t *testing.T, client *StackIP, conn *tcp.Conn, serverAddr netip.Addr, serverPort, clientPort uint16) { +func setupClient(t *testing.T, client *StackIPv4, conn *tcp.Conn, serverAddr netip.Addr, serverPort, clientPort uint16) { t.Helper() bufsize := 2048 clientIP := netip.AddrFrom4([4]byte{192, 168, 1, byte(clientPort % 256)}) - client.Reset(new(lneto.Validator), 1, 0) + client.Reset(new(lneto.Validator), 1) client.SetAddr4(clientIP.As4()) err := conn.Configure(tcp.ConnConfig{ RxBuf: make([]byte, bufsize), diff --git a/ipv4/definitions.go b/ipv4/definitions.go index 7c025fa..44b5fce 100644 --- a/ipv4/definitions.go +++ b/ipv4/definitions.go @@ -1,6 +1,8 @@ package ipv4 -import "strconv" +import ( + "strconv" +) const ( // RFC791 defines the minimum MTU for an IPv4 packet as 68, meaning a payload of 48 bytes when no IPv4 options included. diff --git a/ipv4/prefix .go b/ipv4/prefix .go new file mode 100644 index 0000000..a43b8b3 --- /dev/null +++ b/ipv4/prefix .go @@ -0,0 +1,130 @@ +package ipv4 + +import ( + "encoding/binary" + "net/netip" +) + +// Prefix is a [netip.Prefix] equivalent specifically designed for IPv4. +type Prefix struct { + addr uint32 + bitsPlusOne uint8 +} + +func PrefixFromNetip(pfx netip.Prefix) Prefix { + addr := pfx.Addr() + if addr.Is4() { + return PrefixFrom(addr.As4(), uint8(pfx.Bits())) + } + return Prefix{} +} + +// PrefixFrom constructs a [Prefix] from an address and prefix bit length. +// +// It does not allocate and does not mask +// off the host bits of ip. +// +// If bits is less than zero or greater than 32, [Prefix.Bits] +// will return an invalid value 255. +func PrefixFrom(addr [4]byte, bits uint8) Prefix { + if bits > 32 { + bits = 0 + } + return Prefix{addr: addr2bits(addr), bitsPlusOne: bits + 1} +} + +// IsValid returns true if the [Prefix] is valid. +func (p Prefix) IsValid() bool { return p.bitsPlusOne != 0 } + +// Addr returns the IPv4 address. +func (p Prefix) Addr() [4]byte { return bits2addr(p.addr) } + +// Bits returns IPv4 prefix bits 0..32 or 255 for invalid prefixes. +func (p Prefix) Bits() uint8 { return p.bitsPlusOne - 1 } + +// NetipPrefix returns the equivalent [netip.Prefix]. +func (p Prefix) NetipPrefix() netip.Prefix { + return netip.PrefixFrom(netip.AddrFrom4(p.Addr()), int(p.Bits())) +} + +func (p Prefix) addrBitmasked() uint32 { return p.addr & p.bitmask() } +func (p Prefix) bitmask() uint32 { return ^uint32(0) << (32 - p.Bits()) } + +func addr2bits(addr [4]byte) uint32 { return binary.BigEndian.Uint32(addr[:]) } + +func bits2addr(addrbits uint32) (addr [4]byte) { + binary.BigEndian.PutUint32(addr[:], addrbits) + return addr +} + +// Contains reports whether the network p includes ip. +// +// A zero-value IP will not match any prefix. +func (p Prefix) Contains(addr [4]byte) bool { + if !p.IsValid() { + return false + } + mask := p.bitmask() + return p.addr&mask == addr2bits(addr)&mask +} + +// Masked returns the Prefix with address bits outside of the prefix masked to zero. +func (p Prefix) Masked() Prefix { + return Prefix{addr: p.addrBitmasked(), bitsPlusOne: p.bitsPlusOne} +} + +// IsSingleIP reports whether p contains exactly one IP address (i.e. a /32). +func (p Prefix) IsSingleIP() bool { return p.IsValid() && p.Bits() == 32 } + +// Overlaps reports whether p and o contain any IP addresses in common. +func (p Prefix) Overlaps(o Prefix) bool { + if !p.IsValid() || !o.IsValid() { + return false + } + mask := ^uint32(0) << (32 - min(p.Bits(), o.Bits())) + return p.addr&mask == o.addr&mask +} + +// Next returns the address following addr in the prefix mask with wrap around semantics. +func (p Prefix) Next(addr [4]byte) (next [4]byte) { + mask := p.bitmask() + host := addr2bits(addr) &^ mask + host = (host + 1) & ^mask + return bits2addr(p.addrBitmasked() | host) +} + +// Compare returns an integer comparing two prefixes. +// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. +// Prefixes sort first by validity (invalid before valid), then masked +// prefix address, then prefix length, then unmasked address. +func (p Prefix) Compare(p2 Prefix) int { + if p.IsValid() != p2.IsValid() { + if !p.IsValid() { + return -1 + } + return 1 + } + if !p.IsValid() { + return 0 + } + pm, p2m := p.addrBitmasked(), p2.addrBitmasked() + if pm != p2m { + if pm < p2m { + return -1 + } + return 1 + } + if p.bitsPlusOne != p2.bitsPlusOne { + if p.bitsPlusOne < p2.bitsPlusOne { + return -1 + } + return 1 + } + pa, p2a := p.addr, p2.addr + if pa < p2a { + return -1 + } else if pa > p2a { + return 1 + } + return 0 +} diff --git a/ipv4/prefix_test.go b/ipv4/prefix_test.go new file mode 100644 index 0000000..b1dd87e --- /dev/null +++ b/ipv4/prefix_test.go @@ -0,0 +1,228 @@ +package ipv4 + +import ( + "net/netip" + "testing" +) + +func TestPrefixFrom(t *testing.T) { + tests := []struct { + addr [4]byte + bits uint8 + wantValid bool + wantBits uint8 + wantAddr [4]byte + }{ + {[4]byte{192, 168, 1, 0}, 24, true, 24, [4]byte{192, 168, 1, 0}}, + {[4]byte{10, 0, 0, 0}, 8, true, 8, [4]byte{10, 0, 0, 0}}, + {[4]byte{0, 0, 0, 0}, 0, true, 0, [4]byte{0, 0, 0, 0}}, + {[4]byte{1, 2, 3, 4}, 32, true, 32, [4]byte{1, 2, 3, 4}}, + {[4]byte{1, 2, 3, 4}, 33, true, 0, [4]byte{1, 2, 3, 4}}, // >32 clamped to 0 + } + for _, tc := range tests { + p := PrefixFrom(tc.addr, tc.bits) + if p.IsValid() != tc.wantValid { + t.Errorf("PrefixFrom(%v, %d).IsValid() = %v, want %v", tc.addr, tc.bits, p.IsValid(), tc.wantValid) + } + if p.Bits() != tc.wantBits { + t.Errorf("PrefixFrom(%v, %d).Bits() = %d, want %d", tc.addr, tc.bits, p.Bits(), tc.wantBits) + } + if p.Addr() != tc.wantAddr { + t.Errorf("PrefixFrom(%v, %d).Addr() = %v, want %v", tc.addr, tc.bits, p.Addr(), tc.wantAddr) + } + } +} + +func TestPrefixZeroValue(t *testing.T) { + var p Prefix + if p.IsValid() { + t.Error("zero Prefix should be invalid") + } +} + +func TestPrefixFromNetip(t *testing.T) { + tests := []struct { + in string + wantValid bool + }{ + {"192.168.1.0/24", true}, + {"10.0.0.0/8", true}, + {"0.0.0.0/0", true}, + {"1.2.3.4/32", true}, + {"::1/128", false}, // IPv6 should yield invalid + } + for _, tc := range tests { + npfx, err := netip.ParsePrefix(tc.in) + if err != nil { + t.Fatalf("ParsePrefix(%q): %v", tc.in, err) + } + p := PrefixFromNetip(npfx) + if p.IsValid() != tc.wantValid { + t.Errorf("PrefixFromNetip(%q).IsValid() = %v, want %v", tc.in, p.IsValid(), tc.wantValid) + } + if !tc.wantValid { + continue + } + if p.NetipPrefix() != npfx { + t.Errorf("PrefixFromNetip(%q).NetipPrefix() = %v, want %v", tc.in, p.NetipPrefix(), npfx) + } + } +} + +func TestPrefixNetipRoundtrip(t *testing.T) { + inputs := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "0.0.0.0/0", "1.2.3.4/32"} + for _, s := range inputs { + npfx := netip.MustParsePrefix(s) + p := PrefixFromNetip(npfx) + if got := p.NetipPrefix(); got != npfx { + t.Errorf("roundtrip %q: got %v", s, got) + } + } +} + +func TestPrefixContains(t *testing.T) { + p := PrefixFrom([4]byte{192, 168, 1, 0}, 24) + tests := []struct { + addr [4]byte + want bool + }{ + {[4]byte{192, 168, 1, 0}, true}, + {[4]byte{192, 168, 1, 1}, true}, + {[4]byte{192, 168, 1, 255}, true}, + {[4]byte{192, 168, 2, 0}, false}, + {[4]byte{10, 0, 0, 1}, false}, + } + for _, tc := range tests { + if got := p.Contains(tc.addr); got != tc.want { + t.Errorf("%v.Contains(%v) = %v, want %v", p.NetipPrefix(), tc.addr, got, tc.want) + } + } + + var invalid Prefix + if invalid.Contains([4]byte{0, 0, 0, 0}) { + t.Error("invalid Prefix.Contains should return false") + } +} + +func TestPrefixMasked(t *testing.T) { + // Address with host bits set. + p := PrefixFrom([4]byte{192, 168, 1, 5}, 24) + m := p.Masked() + want := [4]byte{192, 168, 1, 0} + if m.Addr() != want { + t.Errorf("Masked().Addr() = %v, want %v", m.Addr(), want) + } + if m.Bits() != 24 { + t.Errorf("Masked().Bits() = %d, want 24", m.Bits()) + } +} + +func TestPrefixIsSingleIP(t *testing.T) { + if !PrefixFrom([4]byte{1, 2, 3, 4}, 32).IsSingleIP() { + t.Error("/32 should be single IP") + } + if PrefixFrom([4]byte{1, 2, 3, 4}, 31).IsSingleIP() { + t.Error("/31 should not be single IP") + } + var invalid Prefix + if invalid.IsSingleIP() { + t.Error("invalid Prefix.IsSingleIP should return false") + } +} + +func TestPrefixOverlaps(t *testing.T) { + tests := []struct { + a, b string + want bool + }{ + {"192.168.0.0/16", "192.168.1.0/24", true}, + {"10.0.0.0/8", "10.1.2.0/24", true}, + {"10.0.0.0/8", "192.168.0.0/16", false}, + {"0.0.0.0/0", "1.2.3.4/32", true}, + {"1.2.3.4/32", "1.2.3.4/32", true}, + {"1.2.3.4/32", "1.2.3.5/32", false}, + } + for _, tc := range tests { + a := PrefixFromNetip(netip.MustParsePrefix(tc.a)) + b := PrefixFromNetip(netip.MustParsePrefix(tc.b)) + if got := a.Overlaps(b); got != tc.want { + t.Errorf("%s.Overlaps(%s) = %v, want %v", tc.a, tc.b, got, tc.want) + } + // Symmetry. + if got := b.Overlaps(a); got != tc.want { + t.Errorf("%s.Overlaps(%s) [symmetric] = %v, want %v", tc.b, tc.a, got, tc.want) + } + } + + var invalid Prefix + valid := PrefixFrom([4]byte{10, 0, 0, 0}, 8) + if invalid.Overlaps(valid) || valid.Overlaps(invalid) { + t.Error("invalid Prefix.Overlaps should return false") + } +} + +func TestPrefixNext(t *testing.T) { + tests := []struct { + prefix string + addr [4]byte + want [4]byte + }{ + // Normal increment within /24. + {"192.168.1.0/24", [4]byte{192, 168, 1, 0}, [4]byte{192, 168, 1, 1}}, + {"192.168.1.0/24", [4]byte{192, 168, 1, 1}, [4]byte{192, 168, 1, 2}}, + {"192.168.1.0/24", [4]byte{192, 168, 1, 254}, [4]byte{192, 168, 1, 255}}, + // Wrap-around: last host addr in /24 wraps to first. + {"192.168.1.0/24", [4]byte{192, 168, 1, 255}, [4]byte{192, 168, 1, 0}}, + // /32: only one host, wraps to itself. + {"1.2.3.4/32", [4]byte{1, 2, 3, 4}, [4]byte{1, 2, 3, 4}}, + // /31: two hosts, wraps. + {"10.0.0.0/31", [4]byte{10, 0, 0, 0}, [4]byte{10, 0, 0, 1}}, + {"10.0.0.0/31", [4]byte{10, 0, 0, 1}, [4]byte{10, 0, 0, 0}}, + // /8: increment and wrap within the network. + {"10.0.0.0/8", [4]byte{10, 0, 0, 255}, [4]byte{10, 0, 1, 0}}, + {"10.0.0.0/8", [4]byte{10, 255, 255, 255}, [4]byte{10, 0, 0, 0}}, + } + for _, tc := range tests { + p := PrefixFromNetip(netip.MustParsePrefix(tc.prefix)) + got := p.Next(tc.addr) + if got != tc.want { + t.Errorf("%s.Next(%v) = %v, want %v", tc.prefix, tc.addr, got, tc.want) + } + } +} + +func TestPrefixCompare(t *testing.T) { + var invalid Prefix + a := PrefixFromNetip(netip.MustParsePrefix("10.0.0.0/8")) + b := PrefixFromNetip(netip.MustParsePrefix("192.168.0.0/16")) + + // invalid < valid + if invalid.Compare(a) != -1 { + t.Error("invalid.Compare(valid) should be -1") + } + if a.Compare(invalid) != 1 { + t.Error("valid.Compare(invalid) should be 1") + } + // two invalids are equal + if invalid.Compare(Prefix{}) != 0 { + t.Error("invalid.Compare(invalid) should be 0") + } + // reflexive + if a.Compare(a) != 0 { + t.Error("a.Compare(a) should be 0") + } + // ordering + if got := a.Compare(b); got >= 0 { + t.Errorf("10/8.Compare(192.168/16) should be negative, got %d", got) + } + if got := b.Compare(a); got <= 0 { + t.Errorf("192.168/16.Compare(10/8) should be positive, got %d", got) + } + + // shorter prefix < longer prefix when masked addr is equal + a8 := PrefixFromNetip(netip.MustParsePrefix("10.0.0.0/8")) + a16 := PrefixFromNetip(netip.MustParsePrefix("10.0.0.0/16")) + if a8.Compare(a16) >= 0 { + t.Error("10/8 should sort before 10/16") + } +} diff --git a/lneto_test.go b/lneto_test.go index f31d2da..f19d45b 100644 --- a/lneto_test.go +++ b/lneto_test.go @@ -1,7 +1,9 @@ package lneto_test import ( + "bytes" "math/rand" + "os" "testing" "github.com/soypat/lneto" @@ -156,3 +158,17 @@ func TestIPv4TCPChecksum(t *testing.T) { } } } + +func TestNoDeps(t *testing.T) { + data, err := os.ReadFile("go.mod") + if err != nil { + t.Fatal(err) + } + const expect = "module github.com/soypat/lneto\n\ngo 1.2" + if !bytes.HasPrefix(data, []byte(expect)) { + t.Fatalf("unexpected go.mod file:\nexpect:%sx\ngot:%s", expect, string(data)) + } + if bytes.Contains(data, []byte("require")) { + t.Fatal("no dependencies allowed in lneto") + } +} diff --git a/tcp/control.go b/tcp/control.go index 7e34937..e79023f 100644 --- a/tcp/control.go +++ b/tcp/control.go @@ -175,6 +175,13 @@ func (tcb *ControlBlock) MakeChallengeACK() Segment { } } +// recvSpace contains Receive Sequence Space data. Its sequence numbers correspond to remote data. +type recvSpace struct { + IRS Value // initial receive sequence number, defined by remote in SYN segment received. + NXT Value // receive next. seqs before this have been acked. this seq and up to NXT+WND-1 are allowed to be sent. Corresponds to remote data. + WND Size // receive window defined by local. Permitted number of remote unacked octets in flight. +} + // sendSpace contains Send Sequence Space data. Its sequence numbers correspond to local data. type sendSpace struct { ISS Value // initial send sequence number, defined locally on connection start @@ -201,13 +208,6 @@ func (snd *sendSpace) maxSend() Size { } } -// recvSpace contains Receive Sequence Space data. Its sequence numbers correspond to remote data. -type recvSpace struct { - IRS Value // initial receive sequence number, defined by remote in SYN segment received. - NXT Value // receive next. seqs before this have been acked. this seq and up to NXT+WND-1 are allowed to be sent. Corresponds to remote data. - WND Size // receive window defined by local. Permitted number of remote unacked octets in flight. -} - // Open implements a passive opening of a connection (wait for incoming packets from an unknown remote port). // Upon success [ControlBlock] enters LISTEN state, such as that of a server. // To open an active connection use [ControlBlock.Send] with a segment generated with [ClientSynSegment]. diff --git a/tcp/listener.go b/tcp/listener.go index 1991918..106c9b6 100644 --- a/tcp/listener.go +++ b/tcp/listener.go @@ -135,7 +135,6 @@ func (listener *Listener) Encapsulate(carrierData []byte, offsetToIP, offsetToFr if listener.isClosed() { return 0, net.ErrClosed } - //listener.trace("listener:encaps", slog.Uint64("port", uint64(listener.port))) // First try incoming connections (for handshake SYN-ACK). for i := range listener.incoming { conn := listener.incoming[i].conn diff --git a/x/xnet/stack-async.go b/x/xnet/stack-async.go index 88e0f8e..3c41992 100644 --- a/x/xnet/stack-async.go +++ b/x/xnet/stack-async.go @@ -15,6 +15,7 @@ import ( "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/internal" "github.com/soypat/lneto/internet" + "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/ipv4/icmpv4" "github.com/soypat/lneto/ntp" "github.com/soypat/lneto/tcp" @@ -23,6 +24,7 @@ import ( const ( minTCPBuffer = 256 + icmpEchoSize = 64 ) type StackAsync struct { @@ -30,9 +32,13 @@ type StackAsync struct { hostname string clientID string link internet.StackEthernet - ip internet.StackIP - arp arp.Handler - icmp icmpv4.Client + ip4 internet.StackIPv4 + + // ip6 internet.StackIPv6 + arp arp.Handler + icmp icmpv4.Client + // icmp6 icmpv6.Client + icmp6buf []byte udps internet.StackPortsMACFiltered tcps internet.StackPortsMACFiltered @@ -58,38 +64,51 @@ type StackAsync struct { prng uint32 - addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes. + addrBuf [6]byte // Temporary buffer for As4()/HardwareAddr6() results to avoid heap escapes. + addrbufnip [4]netip.Addr - totalsent uint64 - totalrecv uint64 + stats Statistics + + ipv6enabled bool + stack6 Stack6 } type StackConfig struct { - // StaticAddress6 [16]byte - StaticAddress4 [4]byte + HardwareAddress [6]byte + StaticAddress4 [4]byte + StaticAddress6 [16]byte + + IPv6Stack Stack6 DNSServer netip.Addr NTPServer netip.Addr RandSeed int64 - Hostname string - - // MaxActiveTCPPorts and MaxActiveUDPPorts are a memory guardrail to limit - // number of simultaneous open TCP/UDP ports. The memory impact at the stack level - // of a port corresponds to ~64 bytes excluding the registered StackNode i.e: [tcp.Conn] or [udp.Conn]. - MaxActiveTCPPorts, MaxActiveUDPPorts uint16 + // Hostname is used for DHCP hostname and ICMP ID. + Hostname string EthernetTxCRC32Update func(crc uint32, b []byte) uint32 - HardwareAddress [6]byte - MTU uint16 - // Accept multicast ethernet and IP packets. Needed for MDNS. - AcceptMulticast bool // ICMPQueueLimit sets maximum number of input/output packets queued for processing. // If set to zero ICMP cannot be enabled on the stack. ICMPQueueLimit int // PassivePeers limits how many subnet peers the stack passively learns MAC addresses for. // Passively learned entries skip ARP round-trips on the first DialTCP/DialUDP to that peer. PassivePeers int + + // MaxActiveTCPPorts and MaxActiveUDPPorts are a memory guardrail to limit + // number of simultaneous open TCP/UDP ports. The memory impact at the stack level + // of a port corresponds to ~64 bytes excluding the registered StackNode i.e: [tcp.Conn] or [udp.Conn]. + MaxActiveTCPPorts, MaxActiveUDPPorts uint16 + // MTU sets the maximum transmission unit, which is the maximum size of the Ethernet payload + // not including ethernet header, ethernet CRC. It is determined by the NIC hardware and the route the packets take over the network. + // By far the most common value for MTU is 1500 as specified by IEEE 802.3. + MTU uint16 + // Accept multicast ethernet and IP packets. Needed for MDNS. + AcceptMulticast bool +} + +func (cfg *StackConfig) id() uint16 { + return uint16(cfg.Hostname[len(cfg.Hostname)-1] - '0') } func (s *StackAsync) Hostname() string { @@ -100,7 +119,7 @@ func (s *StackAsync) Hostname() string { func (s *StackAsync) IngressEthernet(ethernetFrame []byte) error { s.mu.Lock() defer s.mu.Unlock() - s.totalrecv += uint64(len(ethernetFrame)) + s.stats.TotalReceived += uint64(len(ethernetFrame)) err := s.link.Demux(ethernetFrame, 0) if err == nil { s.arpt.learnFromIngressEthernet(ethernetFrame) @@ -114,16 +133,28 @@ func (s *StackAsync) EgressEthernet(dstEthernetFrame []byte) (int, error) { s.mu.Lock() defer s.mu.Unlock() n, err := s.link.Encapsulate(dstEthernetFrame, -1, 0) - s.totalsent += uint64(n) + s.stats.TotalSent += uint64(n) return n, err } // IngressIP processes an incoming IP frame through the stack and omits ethernet header processing. func (s *StackAsync) IngressIP(ipFrame []byte) error { + if len(ipFrame) < 1 { + return lneto.ErrTruncatedFrame + } + version := ipFrame[0] >> 4 s.mu.Lock() defer s.mu.Unlock() - s.totalrecv += uint64(len(ipFrame)) - return s.ip.Demux(ipFrame, 0) + s.stats.TotalReceived += uint64(len(ipFrame)) + switch version { + case 4: + return s.ip4.Demux(ipFrame, 0) + case 6: + if s.ipv6enabled { + return s.stack6.IngressIPv6(ipFrame) + } + } + return lneto.ErrPacketDrop } // EgressIP writes the next IP frame to send into dstIPFrame from the stack. The length of dstIPFrame should be at least MTU. @@ -133,8 +164,11 @@ func (s *StackAsync) EgressIP(dstIPFrame []byte) (int, error) { if len(dstIPFrame) < s.link.MTU() { return 0, lneto.ErrShortBuffer } - n, err := s.ip.Encapsulate(dstIPFrame, 0, 0) - s.totalsent += uint64(n) + n, err := s.ip4.Encapsulate(dstIPFrame, 0, 0) + if s.ipv6enabled && n == 0 { + n, err = s.stack6.EgressIPv6(dstIPFrame) + } + s.stats.TotalSent += uint64(n) return n, err } @@ -147,17 +181,33 @@ func (s *StackAsync) MTU() int { return s.link.MTU() } -func (s *StackAsync) Reset(cfg StackConfig) error { +func (s *StackAsync) Reset(cfg StackConfig) (err error) { + ipv6Enabled := cfg.IPv6Stack != nil if cfg.RandSeed == 0 || cfg.Hostname == "" || cfg.PassivePeers > 255 { return lneto.ErrInvalidConfig + } else if !internal.IsZeroed(cfg.StaticAddress6) && !ipv6Enabled { + return lneto.ErrBug // Forgot to EnableIPv6 after setting static IPv6 address. } mac := cfg.HardwareAddress s.mu.Lock() defer s.mu.Unlock() s.prng = uint32(cfg.RandSeed) s.hostname = cfg.Hostname - - const linkNodes = 2 // ARP and IP nodes + // Treat last character of hostname as number. + id := cfg.id() + linkNodes := 2 // ARP and IPv4 nodes + s.ipv6enabled = ipv6Enabled + s.stack6 = nil + if s.ipv6enabled { + linkNodes = 3 // IPv6 + s.Debug("ipv6 enabled") + err = cfg.IPv6Stack.Reset6(&cfg) + if err != nil { + s.ipv6enabled = false + return err + } + } + s.stack6 = cfg.IPv6Stack ecfg := internet.StackEthernetConfig{ MTU: int(cfg.MTU), MaxNodes: linkNodes, @@ -166,42 +216,37 @@ func (s *StackAsync) Reset(cfg StackConfig) error { AppendCRC32: cfg.EthernetTxCRC32Update != nil, CRC32Update: cfg.EthernetTxCRC32Update, } - err := s.link.Configure(ecfg) + err = s.link.Configure(ecfg) if err != nil { return err } - s.link.SetAcceptMulticast(cfg.AcceptMulticast) if cfg.PassivePeers == 0 { s.link.OnEncapsulate(nil) } else { s.link.OnEncapsulate(s.arpt.patchEgressMAC) } const ipNodes = 3 // 3 IP protocols possible: UDP, TCP, ICMP. - err = s.ip.Reset(&s.defaultValidator, ipNodes, 0) + err = s.ip4.Reset(&s.defaultValidator, ipNodes) if err != nil { return err } - s.ip.SetAddr4(cfg.StaticAddress4) - s.ip.SetAcceptMulticast4(cfg.AcceptMulticast) + s.ip4.SetAddr4(cfg.StaticAddress4) + s.setAcceptMulticast4(cfg.AcceptMulticast) + s.arpt.passivePeers = uint8(cfg.PassivePeers) err = s.resetARP() if err != nil { return err } udpConns := 3 + cfg.MaxActiveUDPPorts // DHCP, DNS, NTP + user-registered. - err = s.udps.ResetUDP(udpConns) - if err != nil { - return err - } + s.udps.ResetUDP(udpConns) + internal.SliceReuse(&s.userUDPs, int(cfg.MaxActiveUDPPorts)) // Enable TCP if connections present. if cfg.MaxActiveTCPPorts > 0 { - err = s.tcps.ResetTCP(cfg.MaxActiveTCPPorts) - if err != nil { - return err - } - err = s.ip.Register4(&s.tcps) + s.tcps.ResetTCP(cfg.MaxActiveTCPPorts) + err = s.ip4.Register4(&s.tcps) if err != nil { return err } @@ -209,20 +254,21 @@ func (s *StackAsync) Reset(cfg StackConfig) error { // Now setup stacks. // ARP registered in resetARP. - err = s.link.Register(&s.ip) // IPv4 | IPv6 + err = s.link.RegisterEthernet(&s.ip4) // IPv4 if err != nil { return err } - err = s.ip.Register4(&s.udps) + + err = s.ip4.Register4(&s.udps) if err != nil { return err } if cfg.ICMPQueueLimit > 0 { err = s.icmp.Configure(icmpv4.ClientConfig{ - ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*64), + ResponseQueueBuffer: make([]byte, cfg.ICMPQueueLimit*icmpEchoSize), ResponseQueueLimit: cfg.ICMPQueueLimit, HashSeed: s.prand32(), - ID: uint16(cfg.Hostname[len(cfg.Hostname)-1]) - '0', // Treat last character of hostname as number. + ID: id, }) if err != nil { return err @@ -233,17 +279,23 @@ func (s *StackAsync) Reset(cfg StackConfig) error { if s.clientID == "" { s.clientID = "lneto-" + s.hostname } - s.totalrecv = 0 - s.totalsent = 0 + s.stats = Statistics{} if cfg.DNSServer.IsValid() { s.dnssv = cfg.DNSServer } + if s.ipv6enabled { + s.Debug("registering IPv6 to ethernet") + err = s.link.RegisterEthernet(s.stack6.IPv6Stack()) + if err != nil { + return err + } + } return nil } func (s *StackAsync) resetARP() error { mac := s.link.HardwareAddr6() - addr := s.ip.Addr4() + addr := s.ip4.Addr4() proto := ethernet.TypeIPv4 err := s.arp.Reset(arp.HandlerConfig{ HardwareAddr: mac[:], @@ -258,7 +310,7 @@ func (s *StackAsync) resetARP() error { } s.arpt.reset(10, s.arpt.passivePeers) s.arp.SetOnResolveCallback(s.arpt.onResolve) - err = s.link.Register(&s.arp) + err = s.link.RegisterEthernet(&s.arp) if err != nil { return err } @@ -299,42 +351,42 @@ func (s *StackAsync) SetAddr4(addr [4]byte) error { } func (s *StackAsync) setIPAddr4(addr [4]byte) error { - s.ip.SetAddr4(addr) + s.ip4.SetAddr4(addr) return s.arp.UpdateProtoAddr(addr[:]) } func (s *StackAsync) Addr4() [4]byte { s.mu.Lock() defer s.mu.Unlock() - return s.ip.Addr4() + return s.ip4.Addr4() } -func (s *StackAsync) SetSubnet(subnetMask netip.Prefix) { +func (s *StackAsync) SetSubnet4(addr [4]byte, prefixBits uint8) { s.mu.Lock() defer s.mu.Unlock() - s.arpt.subnet = subnetMask + s.arpt.subnet4 = ipv4.PrefixFrom(addr, prefixBits) } -func (s *StackAsync) SetHardwareAddress(hw [6]byte) error { +func (s *StackAsync) SetHardwareAddr(hw [6]byte) error { s.mu.Lock() defer s.mu.Unlock() s.link.SetHardwareAddr6(hw) return s.resetARP() } -func (s *StackAsync) HardwareAddress() (hw [6]byte) { +func (s *StackAsync) HardwareAddr() (hw [6]byte) { s.mu.Lock() defer s.mu.Unlock() return s.link.HardwareAddr6() } -func (s *StackAsync) SetGateway6(gwhw [6]byte) { +func (s *StackAsync) SetGatewayHardwareAddr(gwhw [6]byte) { s.mu.Lock() defer s.mu.Unlock() s.link.SetGateway6(gwhw) } -func (s *StackAsync) Gateway6() [6]byte { +func (s *StackAsync) GatewayHardwareAddr() [6]byte { s.mu.Lock() defer s.mu.Unlock() return s.link.Gateway6() @@ -348,71 +400,56 @@ func (s *StackAsync) EnableICMP(enabled bool) (err error) { enabled = false // ensure aborted. } if enabled { - if s.ip.IsRegistered4(lneto.IPProtoICMP) { - return nil + if !s.ip4.IsRegistered4(lneto.IPProtoICMP) { + err = s.ip4.Register4(&s.icmp) } - err = s.ip.Register4(&s.icmp) } else { s.icmp.Abort() } + if s.ipv6enabled { + if err2 := s.stack6.EnableICMP6(enabled); err2 != nil { + err = err2 + } + } return err } func (s *StackAsync) DialUDP(conn *udp.Conn, localPort uint16, addrp netip.AddrPort) (err error) { - s.mu.Lock() - defer s.mu.Unlock() - var mac []byte - if s.arpt.subnet.Contains(addrp.Addr()) { - mac = make([]byte, 6) - ip := addrp.Addr().As4() - hw, err := s.arp.CacheLookup(ip[:]) - if err == nil { - // MAC already contained in results. - copy(mac, hw) - } else { - // StartQuery starts an ARP query for addresses in this network. - // On finishing query MAC is set and thus the StackPort will allow encapsulating - // data on that connection. - err = s.arpt.startQuery(mac, ip[:], &s.arp) - if err != nil { - return err - } - } + addr := addrp.Addr() + if addr.Is4() { + err = s.DialUDP4(conn, localPort, addrp.Addr().As4(), addrp.Port()) + } else if s.ipv6enabled && addr.Is6() { + err = s.stack6.DialUDP6(conn, localPort, addr.As16(), addrp.Port()) + } else { + err = lneto.ErrInvalidAddr } - err = conn.Open(localPort, addrp) - if err != nil { - return err - } - err = s.udps.Register(conn, mac) - return nil + return err } func (s *StackAsync) DialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrPort) (err error) { + addr := addrp.Addr() + if addr.Is4() { + err = s.DialTCP4(conn, localPort, addrp.Addr().As4(), addrp.Port()) + } else if s.ipv6enabled && addr.Is6() { + err = s.stack6.DialTCP6(conn, localPort, addr.As16(), addrp.Port(), tcp.Value(s.Prand32())) + } else { + err = lneto.ErrInvalidAddr + } + return err +} + +func (s *StackAsync) DialUDP4(conn *udp.Conn, localPort uint16, raddr [4]byte, rport uint16) (err error) { s.mu.Lock() defer s.mu.Unlock() - var mac []byte - if s.arpt.subnet.Contains(addrp.Addr()) { - ip := addrp.Addr().As4() - hw, err := s.arp.CacheLookup(ip[:]) - mac = make([]byte, 6) - if err == nil { - // Query exists, use pre-existing result. - copy(mac, hw) - } else { - // StartQuery starts an ARP query for addresses in this network. - // On finishing query MAC is set and thus the StackPort will allow encapsulating - // data on that connection. - err = s.arpt.startQuery(mac, ip[:], &s.arp) - if err != nil { - return err - } - } - } - err = conn.OpenActive(localPort, addrp, tcp.Value(s.prand32())) + mac, err := s.arpt.hwDynamicResolve(raddr, &s.arp) if err != nil { return err } - err = s.tcps.Register(conn, mac) // MAC is set later on by ARP response arriving to our network. + err = conn.Open(localPort, netip.AddrPortFrom(netip.AddrFrom4(raddr), rport)) + if err != nil { + return err + } + err = s.udps.RegisterMACFiltered(conn, mac) if err != nil { conn.Abort() return err @@ -420,14 +457,33 @@ func (s *StackAsync) DialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrP return nil } -func (s *StackAsync) ListenTCP(conn *tcp.Conn, localPort uint16) (err error) { +func (s *StackAsync) DialTCP4(conn *tcp.Conn, localPort uint16, raddr [4]byte, rport uint16) (err error) { + s.mu.Lock() + defer s.mu.Unlock() + mac, err := s.arpt.hwDynamicResolve(raddr, &s.arp) + if err != nil { + return err + } + err = conn.OpenActive(localPort, netip.AddrPortFrom(netip.AddrFrom4(raddr), rport), tcp.Value(s.prand32())) + if err != nil { + return err + } + err = s.tcps.RegisterMACFiltered(conn, mac) // MAC is set later on by ARP response arriving to our network. + if err != nil { + conn.Abort() + return err + } + return nil +} + +func (s *StackAsync) ListenTCP4(conn *tcp.Conn, localPort uint16) (err error) { s.mu.Lock() defer s.mu.Unlock() err = conn.OpenListen(localPort, tcp.Value(s.prand32())) if err != nil { return err } - err = s.tcps.Register(conn, nil) + err = s.tcps.RegisterMACFiltered(conn, nil) if err != nil { conn.Abort() return err @@ -436,19 +492,21 @@ func (s *StackAsync) ListenTCP(conn *tcp.Conn, localPort uint16) (err error) { } func (s *StackAsync) RegisterListener(listener *tcp.Listener) (err error) { + // TODO(pato): Possible to forward both IPv4 and IPv6 packets to the listener and have it selectively mux out correctly? + // Can try changing listener to inspect carrierData on demux and get the IPversion to know which tcp.Conns match the IP version. s.mu.Lock() defer s.mu.Unlock() lport := listener.LocalPort() if lport == 0 { return lneto.ErrZeroSource } - return s.tcps.Register(listener, nil) + return s.tcps.RegisterMACFiltered(listener, nil) } -// RegisterUDP registers a StackNode on a UDP port with the given remote address and port. +// RegisterUDP4 registers a StackNode on a UDP port with the given remote address and port. // The StackUDPPort wrapping is handled internally. The number of user-registered UDP ports // is limited by [StackConfig.MaxUDPConns]. -func (s *StackAsync) RegisterUDP(node lneto.StackNode, remoteAddr []byte, remotePort uint16) error { +func (s *StackAsync) RegisterUDP4(node lneto.StackNode, remoteAddr []byte, remotePort uint16) error { s.mu.Lock() defer s.mu.Unlock() idx := len(s.userUDPs) @@ -457,7 +515,7 @@ func (s *StackAsync) RegisterUDP(node lneto.StackNode, remoteAddr []byte, remote } s.userUDPs = s.userUDPs[:idx+1] s.userUDPs[idx].SetStackNode(node, remoteAddr, remotePort) - return s.udps.Register(&s.userUDPs[idx], nil) + return s.udps.RegisterMACFiltered(&s.userUDPs[idx], nil) } var errNoDNSServer = errors.New("no DNS server- did DHCP complete? You can set a predetermined DNS server in Stack configuration") @@ -495,38 +553,27 @@ func (s *StackAsync) StartLookupIP(host string) error { } *(*[4]byte)(s.addrBuf[:4]) = s.dnssv.As4() s.dnsUDP.SetStackNode(&s.dns, s.addrBuf[:4], dns.ServerPort) - err = s.udps.Register(&s.dnsUDP, nil) + err = s.udps.RegisterMACFiltered(&s.dnsUDP, nil) return err } -var errDNSNotDone = errors.New("DNS not done") +var ( + errDNSNotDone = errors.New("DNS not done") + errDNSNoAns = errors.New("no address in DNS answer") +) func (s *StackAsync) ResultLookupIP(host string) ([]netip.Addr, bool, error) { s.mu.Lock() defer s.mu.Unlock() - done, err := s.dns.MessageCopyTo(&s.lookup) - if err != nil { - return nil, done, err - } else if !done { - return nil, done, errDNSNotDone + _, ok := s.dns.ResponseFlags() + if !ok { + return nil, false, errDNSNotDone } - - var addrs []netip.Addr - ans := s.lookup.Answers - for i := range ans { - data := ans[i].RawData() - if len(data) == 4 { - addrs = append(addrs, netip.AddrFrom4([4]byte(data))) - } else if len(data) == 16 { - addrs = append(addrs, netip.AddrFrom16([16]byte(data))) - } else { - err = lneto.ErrInvalidAddr - } + n, err := s.dns.ResponseAnswerLookup(s.addrbufnip[:], host) + if n == 0 && err == nil { + err = errDNSNoAns } - if err == nil && len(addrs) == 0 { - err = errors.New("no address in DNS answer") - } - return addrs, done, err + return s.addrbufnip[:n], true, err } func (s *StackAsync) StartDHCPv4Request(request [4]byte) error { @@ -545,7 +592,7 @@ func (s *StackAsync) StartDHCPv4Request(request [4]byte) error { } s.dhcpUDP.SetStackNode(&s.dhcp, nil, dhcpv4.DefaultServerPort) - err = s.udps.Register(&s.dhcpUDP, nil) + err = s.udps.RegisterMACFiltered(&s.dhcpUDP, nil) if err != nil { return err } @@ -559,7 +606,7 @@ func (s *StackAsync) StartNTP(addr netip.Addr) error { *(*[4]byte)(s.addrBuf[:4]) = addr.As4() s.ntpUDP.SetStackNode(&s.ntp, s.addrBuf[:4], ntp.ServerPort) - err := s.udps.Register(&s.ntpUDP, nil) + err := s.udps.RegisterMACFiltered(&s.ntpUDP, nil) return err } @@ -611,6 +658,17 @@ func (s *StackAsync) DiscardResolveHardwareAddress6(ip netip.Addr) error { return s.arp.CacheRemove(addr[:]) } +func (s *StackAsync) SetAcceptMulticast4(enabled bool) { + s.mu.Lock() + defer s.mu.Unlock() + s.setAcceptMulticast4(enabled) +} + +func (s *StackAsync) setAcceptMulticast4(enabled bool) { + s.link.SetAcceptMulticast(enabled) + s.ip4.SetAcceptMulticast4(enabled) +} + type DHCPResults struct { DNSServers []netip.Addr Router netip.Addr @@ -640,8 +698,9 @@ type Statistics struct { } func (s *StackAsync) ReadStatistics(stats *Statistics) { - stats.TotalReceived = s.totalrecv - stats.TotalSent = s.totalsent + s.mu.Lock() + *stats = s.stats + s.mu.Unlock() } // AssimilateDHCPResults sets the stack's following parameters: @@ -651,8 +710,8 @@ func (s *StackAsync) ReadStatistics(stats *Statistics) { func (stack *StackAsync) AssimilateDHCPResults(results *DHCPResults) error { stack.mu.Lock() defer stack.mu.Unlock() - if results.Subnet.IsValid() { - stack.arpt.subnet = results.Subnet + if results.Subnet.IsValid() && results.Subnet.Addr().Is4() { + stack.arpt.subnet4 = ipv4.PrefixFromNetip(results.Subnet) } if !internal.IsZeroed(results.AssignedAddr4) { err := stack.setIPAddr4(results.AssignedAddr4) @@ -682,9 +741,10 @@ func (s *StackAsync) populateDHCPResults() error { return errors.New("no DHCP assigned address") } router := netip.AddrFrom4(router4) + subnet := s.dhcp.SubnetPrefix() s.dhcpResults = DHCPResults{ Router: router, - Subnet: s.dhcp.SubnetPrefix(), + Subnet: subnet.NetipPrefix(), AssignedAddr4: assigned4, ServerAddr: addr4(s.dhcp.ServerAddr()), BroadcastAddr: addr4(s.dhcp.BroadcastAddr()), @@ -712,8 +772,8 @@ func addr4(addr [4]byte, ok bool) netip.Addr { func (s *StackAsync) Debug(msg string) { internal.LogAttrs(slog.Default(), slog.LevelDebug, "stackasync", slog.String("umsg", msg), - slog.Uint64("sent", s.totalsent), - slog.Uint64("recv", s.totalrecv), + slog.Uint64("sent", s.stats.TotalSent), + slog.Uint64("recv", s.stats.TotalReceived), ) } @@ -725,7 +785,7 @@ func (s *StackAsync) DebugErr(msg, err string) { internal.LogAttrs(slog.Default(), slog.LevelError, "stackasync", slog.String("umsg", msg), slog.String("err", err), - slog.Uint64("sent", s.totalsent), - slog.Uint64("recv", s.totalrecv), + slog.Uint64("sent", s.stats.TotalSent), + slog.Uint64("recv", s.stats.TotalReceived), ) } diff --git a/x/xnet/stack-go.go b/x/xnet/stack-go.go index 5b567f2..9bb6902 100644 --- a/x/xnet/stack-go.go +++ b/x/xnet/stack-go.go @@ -76,7 +76,7 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype } if laddr.Addr() == netip.IPv4Unspecified() { // Specify address. - laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip.Addr4()), laddr.Port()) + laddr = netip.AddrPortFrom(netip.AddrFrom4(s.blk.async.ip4.Addr4()), laddr.Port()) } else if laddr.Addr().Is6() { return nil, lneto.ErrUnsupported } diff --git a/x/xnet/stack6.go b/x/xnet/stack6.go new file mode 100644 index 0000000..d037b6d --- /dev/null +++ b/x/xnet/stack6.go @@ -0,0 +1,212 @@ +package xnet + +import ( + "net/netip" + + "github.com/soypat/lneto" + "github.com/soypat/lneto/internal" + "github.com/soypat/lneto/internet" + "github.com/soypat/lneto/ipv6/icmpv6" + "github.com/soypat/lneto/tcp" + "github.com/soypat/lneto/udp" +) + +var _ Stack6 = (*stack6)(nil) + +func DefaultStack6() Stack6 { + return new(stack6) +} + +type Stack6 interface { + Reset6(cfg *StackConfig) error + Addr6() [16]byte + SetAddr6(addr [16]byte) + + EnableICMP6(enabled bool) error + Register6(node lneto.StackNode) error + DialUDP6(conn *udp.Conn, localPort uint16, raddr [16]byte, rport uint16) error + DialTCP6(conn *tcp.Conn, localPort uint16, raddr [16]byte, rport uint16, iss tcp.Value) error + IngressIPv6(ipframe []byte) error + EgressIPv6(ipframe []byte) (int, error) + IPv6Stack() lneto.StackNode +} + +type stack6 struct { + ip6 internet.StackIPv6 + udps6 internet.StackPortsMACFiltered + tcps6 internet.StackPortsMACFiltered + vld lneto.Validator + icmp6buf []byte + icmp6 icmpv6.Client + // ndpPending tracks in-flight NDP MAC resolves for outbound connections. + // macBuf is shared with the registered node so macResolve patches it in place. + ndpPending []struct { + addr [16]byte + macBuf []byte + } +} + +func (s *stack6) Register6(node lneto.StackNode) error { return s.ip6.Register6(node) } +func (s *stack6) Addr6() [16]byte { return s.ip6.Addr6() } +func (s *stack6) SetAddr6(addr [16]byte) { s.ip6.SetAddr6(addr) } + +func (s *stack6) IPv6Stack() lneto.StackNode { return &s.ip6 } + +func (s *stack6) Reset6(cfg *StackConfig) error { + const ipnodes = 3 // ICMP, TCP, UDP. + err := s.ip6.Reset(&s.vld, ipnodes) + if err != nil { + return err + } + s.ip6.SetAddr6(cfg.StaticAddress6) + s.ip6.SetAcceptMulticast6(true) // IPv6 needs multicast to work. + + s.tcps6.ResetTCP(cfg.MaxActiveTCPPorts) + if cfg.MaxActiveTCPPorts > 0 { + err = s.ip6.Register6(&s.tcps6) + if err != nil { + return err + } + } + s.udps6.ResetUDP(cfg.MaxActiveUDPPorts) + if cfg.MaxActiveUDPPorts > 0 { + err = s.ip6.Register6(&s.udps6) + if err != nil { + return err + } + } + + if cfg.ICMPQueueLimit > 0 { + minSize := cfg.ICMPQueueLimit * icmpEchoSize + internal.SliceReuse(&s.icmp6buf, minSize) + err = s.icmp6.Configure(icmpv6.ClientConfig{ + ResponseQueueBuffer: s.icmp6buf[:cap(s.icmp6buf)], + ResponseQueueLimit: cfg.ICMPQueueLimit, + HashSeed: uint32(cfg.RandSeed), + ID: cfg.id(), + OurAddr: cfg.StaticAddress6, + OurMAC: cfg.HardwareAddress, + NDPCache: 16, + }) + if err != nil { + return err + } + s.icmp6.SetNDPResolveCallback(s.macResolve) + ndpSlots := int(cfg.MaxActiveTCPPorts) + int(cfg.MaxActiveUDPPorts) + internal.SliceReuse(&s.ndpPending, ndpSlots) + s.ndpPending = s.ndpPending[:cap(s.ndpPending)] // all slots available for scan + } + return nil +} + +func (s *stack6) EnableICMP6(enabled bool) (err error) { + if s.icmp6.PingIncomingCapacity() == 0 { + err = lneto.ErrInvalidConfig + enabled = false // ensure aborted. + } + if enabled { + if !s.ip6.IsRegistered6(lneto.IPProtoIPv6ICMP) { + err = s.ip6.Register6(&s.icmp6) + } + } else { + s.icmp6.Abort() + } + return err +} + +func (s *stack6) IngressIPv6(ipFrame []byte) error { + return s.ip6.Demux(ipFrame, 0) +} + +func (s *stack6) EgressIPv6(ipFrame []byte) (int, error) { + return s.ip6.Encapsulate(ipFrame, 0, 0) +} + +// DialTCP6 opens an active TCP connection to raddr:rport. iss is the initial +// sequence number; the caller supplies a random value. NDP MAC resolution is +// attempted immediately; if the peer MAC is not yet cached a Neighbor +// Solicitation is queued and the connection is held until macResolve fires. +func (s *stack6) DialTCP6(conn *tcp.Conn, localPort uint16, raddr [16]byte, rport uint16, iss tcp.Value) error { + mac, err := s.ndpDynamicResolve(raddr) + if err != nil { + return err + } + err = conn.OpenActive(localPort, netip.AddrPortFrom(netip.AddrFrom16(raddr), rport), iss) + if err != nil { + return err + } + err = s.tcps6.RegisterMACFiltered(conn, mac) + if err != nil { + conn.Abort() + return err + } + return nil +} + +// DialUDP6 opens a UDP connection to raddr:rport. NDP MAC resolution follows +// the same deferred strategy as DialTCP6. +func (s *stack6) DialUDP6(conn *udp.Conn, localPort uint16, raddr [16]byte, rport uint16) error { + mac, err := s.ndpDynamicResolve(raddr) + if err != nil { + return err + } + err = conn.Open(localPort, netip.AddrPortFrom(netip.AddrFrom16(raddr), rport)) + if err != nil { + return err + } + err = s.udps6.RegisterMACFiltered(conn, mac) + if err != nil { + conn.Abort() + return err + } + return nil +} + +// macResolve is the NDP resolve callback. It patches the shared macBuf of any +// pending outbound connection to addr so StackPortsMACFiltered begins forwarding. +func (s *stack6) macResolve(mac [6]byte, addr [16]byte) { + for i := range s.ndpPending { + e := &s.ndpPending[i] + if e.addr == addr && e.macBuf != nil { + // macbuf is externally owned and expects it to be written to on resolve. + copy(e.macBuf, mac[:]) + e.macBuf = nil // free slot for future NDP resolution. + e.addr = [16]byte{} + } + } +} + +// ndpDynamicResolve mirrors hwDynamicResolve for IPv6. It returns a +// heap-allocated MAC slice shared with the ndpPending table so that macResolve +// can patch the destination MAC in place once NDP resolves, exactly as the ARP +// subnetTable does for IPv4. Returns nil (no MAC filtering) when NDP is not +// configured. +func (s *stack6) ndpDynamicResolve(raddr [16]byte) ([]byte, error) { + if !s.ip6.IsRegistered6(lneto.IPProtoIPv6ICMP) { + return nil, nil // NDP unavailable; routing layer handles MAC. + } + mac, err := s.icmp6.NDPCacheLookup(raddr) + macBuf := make([]byte, 6) + if err == nil { + copy(macBuf, mac[:]) + return macBuf, nil + } + if err = s.icmp6.NDPStartQuery(raddr, true); err != nil { + return nil, err + } + // Find a freed slot or grow the pending slice. + idx := -1 + for i := range s.ndpPending { + if s.ndpPending[i].macBuf == nil { + idx = i + break + } + } + if idx < 0 { + return nil, lneto.ErrExhausted + } + e := &s.ndpPending[idx] + e.addr = raddr + e.macBuf = macBuf + return macBuf, nil +} diff --git a/x/xnet/stack6_test.go b/x/xnet/stack6_test.go new file mode 100644 index 0000000..e0044f9 --- /dev/null +++ b/x/xnet/stack6_test.go @@ -0,0 +1,618 @@ +package xnet + +import ( + "bytes" + "testing" + + "github.com/soypat/lneto/tcp" + "github.com/soypat/lneto/udp" +) + +const ( + ipv6HeaderSize = 40 + mtu6Test = 1500 + maxFrame6 = ipv6HeaderSize + mtu6Test +) + +func stack6PairConfigs(seed int64, maxports, icmpQueue uint16) (cfg1, cfg2 StackConfig) { + var ( + testAddr6A = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 2001:db8::1 + testAddr6B = [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2} // 2001:db8::2 + testMAC6A = [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01} + testMAC6B = [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x02} + ) + cfg1 = StackConfig{ + Hostname: "test-s6-1", + RandSeed: seed, + StaticAddress6: testAddr6A, + HardwareAddress: testMAC6A, + MTU: mtu6Test, + MaxActiveUDPPorts: maxports, + MaxActiveTCPPorts: maxports, + ICMPQueueLimit: int(icmpQueue), + } + cfg2 = StackConfig{ + Hostname: "test-s6-2", + RandSeed: ^seed, + StaticAddress6: testAddr6B, + HardwareAddress: testMAC6B, + MTU: mtu6Test, + MaxActiveUDPPorts: maxports, + MaxActiveTCPPorts: maxports, + ICMPQueueLimit: int(icmpQueue), + } + return cfg1, cfg2 +} + +// newStack6Pair creates two stack6 instances with distinct IPv6 addresses and MACs. +// ICMPQueueLimit is zero so NDP is disabled; DialUDP6/DialTCP6 skip MAC filtering. +func newStack6Pair(t testing.TB, seed int64, maxports, icmpQueue uint16) (s1, s2 Stack6) { + t.Helper() + cfg1, cfg2 := stack6PairConfigs(seed, maxports, icmpQueue) + s1 = DefaultStack6() + s2 = DefaultStack6() + if err := s1.Reset6(&cfg1); err != nil { + t.Fatal("s1 Reset6:", err) + } + if err := s2.Reset6(&cfg2); err != nil { + t.Fatal("s2 Reset6:", err) + } + return s1, s2 +} + +// newUDPConn6 allocates and configures a udp.Conn for use with stack6. +func newUDPConn6(t testing.TB) *udp.Conn { + t.Helper() + const bufSize = 2048 + conn := new(udp.Conn) + if err := conn.Configure(udp.ConnConfig{ + RxBuf: make([]byte, bufSize), + TxBuf: make([]byte, bufSize), + RxQueueSize: 4, + TxQueueSize: 4, + }); err != nil { + t.Fatal("UDP Configure:", err) + } + return conn +} + +// newTCPConn6 allocates and configures a tcp.Conn for use with stack6. +func newTCPConn6(t testing.TB) *tcp.Conn { + t.Helper() + const bufSize = 2048 + conn := new(tcp.Conn) + if err := conn.Configure(tcp.ConnConfig{ + RxBuf: make([]byte, bufSize), + TxBuf: make([]byte, bufSize), + TxPacketQueueSize: 4, + }); err != nil { + t.Fatal("TCP Configure:", err) + } + return conn +} + +// exchangeIPv6Once encapsulates one IPv6 frame from src and delivers it to dst. +// Returns the number of bytes written (0 if src had nothing to send). +func exchangeIPv6Once(t testing.TB, src, dst Stack6, buf []byte) int { + t.Helper() + n, err := src.EgressIPv6(buf) + if err != nil { + t.Error("EgressIPv6:", err) + return 0 + } + if n == 0 { + return 0 + } + if err := dst.IngressIPv6(buf[:n]); err != nil { + t.Error("IngressIPv6:", err) + } + return n +} + +// listenTCP6 opens a passive TCP connection and registers it with a stack6 directly, +// mirroring what StackAsync.ListenTCP4 does for IPv4. +func listenTCP6(t testing.TB, s Stack6, conn *tcp.Conn, localPort uint16, iss tcp.Value) { + t.Helper() + if err := conn.OpenListen(localPort, iss); err != nil { + t.Fatal("OpenListen:", err) + } + if err := s.(*stack6).tcps6.RegisterMACFiltered(conn, nil); err != nil { + conn.Abort() + t.Fatal("RegisterMACFiltered:", err) + } +} + +// ===== Tests ===== + +func TestStack6Reset(t *testing.T) { + s := DefaultStack6() + testMAC6A := [6]byte{1, 2, 3} + testAddr6A := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + err := s.Reset6(&StackConfig{ + Hostname: "reset-test-1", + RandSeed: 42, + StaticAddress6: testAddr6A, + HardwareAddress: testMAC6A, + MTU: mtu6Test, + }) + if err != nil { + t.Fatal(err) + } + if got := s.Addr6(); got != testAddr6A { + t.Errorf("Addr6 = %v, want %v", got, testAddr6A) + } + testAddr2 := testAddr6A + testAddr2[15] = 2 + // SetAddr6 must update the returned address. + s.SetAddr6(testAddr2) + if got := s.Addr6(); got != testAddr2 { + t.Errorf("after SetAddr6: got %v, want %v", got, testAddr2) + } +} + +func TestStack6Reset_ICMPConfigured(t *testing.T) { + s := DefaultStack6() + err := s.Reset6(&StackConfig{ + Hostname: "icmp-cfg-1", + RandSeed: 1337, + MTU: mtu6Test, + ICMPQueueLimit: 4, + }) + if err != nil { + t.Fatal(err) + } + // EnableICMP6 should succeed since the client was configured. + if err := s.EnableICMP6(true); err != nil { + t.Fatal("EnableICMP6:", err) + } + // Disable should always succeed. + if err := s.EnableICMP6(false); err != nil { + t.Fatal("EnableICMP6(false):", err) + } +} + +// TestStack6UDP_DataExchange sends a datagram from stack A to stack B and reads it back. +// Both stacks are configured without ICMP so NDP/MAC-filtering is bypassed. +func TestStack6UDP_DataExchange(t *testing.T) { + const ( + rngseed = 100 + portA = 5001 + portB = 5002 + nports = 2 + ) + s1, s2 := newStack6Pair(t, rngseed, nports, 0) + buf := make([]byte, maxFrame6) + + connA := newUDPConn6(t) + connB := newUDPConn6(t) + + // Open A -> B direction. + if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil { + t.Fatal("DialUDP6 A:", err) + } + // Open B -> A direction so B's stack accepts datagrams from A. + if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil { + t.Fatal("DialUDP6 B:", err) + } + + want := []byte("hello ipv6 udp") + if _, err := connA.Write(want); err != nil { + t.Fatal("Write:", err) + } + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected packet from A to B") + } + + var rbuf [256]byte + n, err := connB.Read(rbuf[:]) + if err != nil { + t.Fatal("Read:", err) + } + if !bytes.Equal(rbuf[:n], want) { + t.Errorf("got %q, want %q", rbuf[:n], want) + } +} + +// TestStack6UDP_BidirectionalExchange verifies that both sides can send and receive. +func TestStack6UDP_BidirectionalExchange(t *testing.T) { + const ( + rngseed = 100 + nports = 1 + portA = 6001 + portB = 6002 + ) + s1, s2 := newStack6Pair(t, rngseed, nports, 0) + buf := make([]byte, maxFrame6) + + connA := newUDPConn6(t) + connB := newUDPConn6(t) + + if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil { + t.Fatal("DialUDP6 A:", err) + } + if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil { + t.Fatal("DialUDP6 B:", err) + } + + // A -> B + msgAtoB := []byte("A->B") + if _, err := connA.Write(msgAtoB); err != nil { + t.Fatal("Write A->B:", err) + } + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected packet A->B") + } + var rbuf [256]byte + n, err := connB.Read(rbuf[:]) + if err != nil { + t.Fatal("Read B:", err) + } + if !bytes.Equal(rbuf[:n], msgAtoB) { + t.Errorf("B received %q, want %q", rbuf[:n], msgAtoB) + } + + // B -> A + msgBtoA := []byte("B->A reply") + if _, err := connB.Write(msgBtoA); err != nil { + t.Fatal("Write B->A:", err) + } + if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 { + t.Fatal("expected packet B->A") + } + n, err = connA.Read(rbuf[:]) + if err != nil { + t.Fatal("Read A:", err) + } + if !bytes.Equal(rbuf[:n], msgBtoA) { + t.Errorf("A received %q, want %q", rbuf[:n], msgBtoA) + } +} + +// TestStack6ICMPv6_PingEcho verifies a full ICMPv6 echo request/reply exchange. +func TestStack6ICMPv6_PingEcho(t *testing.T) { + const ( + rngSeed = 42 + nports = 1 + icmpQueue = 2 + ) + s1, s2 := newStack6Pair(t, rngSeed, nports, icmpQueue) + buf := make([]byte, maxFrame6) + + if err := s1.EnableICMP6(true); err != nil { + t.Fatal("s1 EnableICMP6:", err) + } + if err := s2.EnableICMP6(true); err != nil { + t.Fatal("s2 EnableICMP6:", err) + } + + // Verify that no packets are pending before the ping. + if n, _ := s1.EgressIPv6(buf); n != 0 { + t.Fatal("unexpected egress from s1 before ping") + } + if n, _ := s2.EgressIPv6(buf); n != 0 { + t.Fatal("unexpected egress from s2 before ping") + } + + // Start the ping from s1 to s2. + pattern := []byte("ping6test") + key, err := s1.(*stack6).icmp6.PingStart(s2.Addr6(), pattern, 32) + if err != nil { + t.Fatal("PingStart:", err) + } + + // s1 sends echo request to s2. + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected ICMPv6 echo request from s1") + } + + // s2 sends echo reply back to s1. + if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 { + t.Fatal("expected ICMPv6 echo reply from s2") + } + + // No further packets should be needed. + if n, _ := s1.EgressIPv6(buf); n != 0 { + t.Error("unexpected extra egress from s1 after ping") + } + if n, _ := s2.EgressIPv6(buf); n != 0 { + t.Error("unexpected extra egress from s2 after ping") + } + + completed, ok := s1.(*stack6).icmp6.PingPop(key) + if !ok { + t.Fatal("ping key not found after exchange") + } + if !completed { + t.Fatal("expected ping to be completed") + } +} + +// TestStack6ICMPv6_MultiPing verifies multiple sequential pings work. +func TestStack6ICMPv6_MultiPing(t *testing.T) { + const ( + rngseed = 193213 + icmpqueue = 2 + ) + s1, s2 := newStack6Pair(t, rngseed, 0, icmpqueue) + buf := make([]byte, maxFrame6) + + if err := s1.EnableICMP6(true); err != nil { + t.Fatal("s1 EnableICMP6:", err) + } + if err := s2.EnableICMP6(true); err != nil { + t.Fatal("s2 EnableICMP6:", err) + } + + for i, pattern := range [][]byte{ + []byte("first"), + []byte("second"), + []byte("third"), + } { + key, err := s1.(*stack6).icmp6.PingStart(s2.Addr6(), pattern, uint16(len(pattern)+4)) + if err != nil { + t.Fatalf("PingStart [%d]: %v", i, err) + } + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatalf("[%d] expected echo request from s1", i) + } + if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 { + t.Fatalf("[%d] expected echo reply from s2", i) + } + completed, ok := s1.(*stack6).icmp6.PingPop(key) + if !ok { + t.Fatalf("[%d] ping key not found", i) + } + if !completed { + t.Fatalf("[%d] ping not completed", i) + } + } +} + +// TestStack6TCP_Handshake verifies that a TCP connection can be established over stack6. +func TestStack6TCP_Handshake(t *testing.T) { + const ( + rngseed = 213213 + svPort = 8080 + clPort = 12345 + nports = 1 + icmpqueue = 2 + ) + s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue) + buf := make([]byte, maxFrame6) + + svConn := newTCPConn6(t) + clConn := newTCPConn6(t) + + // Server listens on s2. + listenTCP6(t, s2, svConn, svPort, 200) + + // Client dials from s1 to s2. + if err := s1.DialTCP6(clConn, clPort, s2.Addr6(), svPort, 100); err != nil { + t.Fatal("DialTCP6:", err) + } + + // SYN: client -> server. + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected SYN from client") + } + if clConn.State() != tcp.StateSynSent { + t.Errorf("client state = %s, want SYN_SENT", clConn.State()) + } + if svConn.State() != tcp.StateSynRcvd { + t.Errorf("server state = %s, want SYN_RCVD", svConn.State()) + } + + // SYNACK: server -> client. + if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 { + t.Fatal("expected SYNACK from server") + } + if clConn.State() != tcp.StateEstablished { + t.Errorf("client state = %s, want ESTABLISHED", clConn.State()) + } + + // ACK: client -> server. + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected ACK from client") + } + if svConn.State() != tcp.StateEstablished { + t.Errorf("server state = %s, want ESTABLISHED", svConn.State()) + } +} + +// TestStack6TCP_DataExchange establishes a TCP connection and transfers data. +func TestStack6TCP_DataExchange(t *testing.T) { + const ( + rngseed = 400 + svPort = 9090 + clPort = 11111 + nports = 1 + icmpqueue = 1 + ) + s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue) + buf := make([]byte, maxFrame6) + + svConn := newTCPConn6(t) + clConn := newTCPConn6(t) + + listenTCP6(t, s2, svConn, svPort, 300) + if err := s1.DialTCP6(clConn, clPort, s2.Addr6(), svPort, 200); err != nil { + t.Fatal("DialTCP6:", err) + } + + // Three-way handshake. + tcp6Handshake(t, s1, s2, buf) + + // Send data from client to server. + payload := []byte("hello over tcp6") + if _, err := clConn.Write(payload); err != nil { + t.Fatal("Write:", err) + } + + // PSH+ACK: client -> server. + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected data packet from client") + } + // ACK: server -> client. + if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 { + t.Fatal("expected ACK from server") + } + // Drain any extra ACKs. + exchangeIPv6Once(t, s1, s2, buf) + exchangeIPv6Once(t, s2, s1, buf) + + var rbuf [256]byte + n, err := svConn.Read(rbuf[:]) + if err != nil { + t.Fatal("svConn.Read:", err) + } + if !bytes.Equal(rbuf[:n], payload) { + t.Errorf("server received %q, want %q", rbuf[:n], payload) + } +} + +// TestStack6_NDP_DialUDP verifies that DialUDP6 with ICMP enabled triggers NDP +// resolution and that seeding the NDP cache allows the connection to proceed. +func TestStack6_NDP_DialUDP(t *testing.T) { + const ( + portA = 7001 + portB = 7002 + rngseed = 132131 + nports = 1 + icmpqueue = 4 + ) + cfg1, cfg2 := stack6PairConfigs(rngseed, nports, icmpqueue) + s1, s2 := newStack6Pair(t, rngseed, nports, icmpqueue) + buf := make([]byte, maxFrame6) + + if err := s1.EnableICMP6(true); err != nil { + t.Fatal("s1 EnableICMP6:", err) + } + if err := s2.EnableICMP6(true); err != nil { + t.Fatal("s2 EnableICMP6:", err) + } + + // Seed s2's NDP cache so it knows s1's MAC (needed for NA reply). + if err := s2.(*stack6).icmp6.NDPCacheSeed(cfg1.StaticAddress6, cfg1.HardwareAddress); err != nil { + t.Fatal("NDPCacheSeed s2:", err) + } + + connA := newUDPConn6(t) + connB := newUDPConn6(t) + + // DialUDP6 on s1 queues an NS since s2's MAC is not yet in s1's NDP cache. + if err := s1.DialUDP6(connA, portA, cfg2.StaticAddress6, portB); err != nil { + t.Fatal("DialUDP6 A:", err) + } + // s2 already has s1's address seeded so DialUDP6 resolves immediately. + if err := s2.DialUDP6(connB, portB, cfg1.StaticAddress6, portA); err != nil { + t.Fatal("DialUDP6 B:", err) + } + + // NDP exchange: s1 sends Neighbor Solicitation, s2 replies with Neighbor Advertisement. + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected NDP Neighbor Solicitation from s1") + } + if n := exchangeIPv6Once(t, s2, s1, buf); n == 0 { + t.Fatal("expected NDP Neighbor Advertisement from s2") + } + + // After NDP resolved, connA's macBuf is patched; verify the cache now has s2's MAC. + mac, err := s1.(*stack6).icmp6.NDPCacheLookup(cfg2.StaticAddress6) + if err != nil { + t.Fatal("NDPCacheLookup after NDP exchange:", err) + } + if mac != cfg2.HardwareAddress { + t.Errorf("NDP resolved MAC = %v, want %v", mac, cfg2.HardwareAddress) + } + + // Now that NDP is resolved, data should flow. + want := []byte("ndp resolved udp") + if _, err := connA.Write(want); err != nil { + t.Fatal("Write:", err) + } + if n := exchangeIPv6Once(t, s1, s2, buf); n == 0 { + t.Fatal("expected UDP packet after NDP resolution") + } + var rbuf [256]byte + n, err := connB.Read(rbuf[:]) + if err != nil { + t.Fatal("Read:", err) + } + if !bytes.Equal(rbuf[:n], want) { + t.Errorf("got %q, want %q", rbuf[:n], want) + } +} + +// tcp6Handshake performs the SYN/SYNACK/ACK exchange between two stacks. +func tcp6Handshake(t testing.TB, client, server Stack6, buf []byte) { + t.Helper() + if n := exchangeIPv6Once(t, client, server, buf); n == 0 { + t.Fatal("handshake: expected SYN") + } + if n := exchangeIPv6Once(t, server, client, buf); n == 0 { + t.Fatal("handshake: expected SYNACK") + } + if n := exchangeIPv6Once(t, client, server, buf); n == 0 { + t.Fatal("handshake: expected ACK") + } +} + +// TestStack6_EgressNoData checks that EgressIPv6 returns 0 when there is nothing to send. +func TestStack6_EgressNoData(t *testing.T) { + s, _ := newStack6Pair(t, 13213, 2, 2) + buf := make([]byte, maxFrame6) + n, err := s.EgressIPv6(buf) + if err != nil { + t.Errorf("EgressIPv6 with no traffic: %v", err) + } + if n != 0 { + t.Errorf("expected 0 bytes, got %d", n) + } +} + +// TestStack6_IngressDropsWrongDst checks that packets destined for a different address are dropped. +func TestStack6_IngressDropsWrongDst(t *testing.T) { + s1, s2 := newStack6Pair(t, 2133213, 1, 1) + buf := make([]byte, maxFrame6) + + connA := newUDPConn6(t) + connB := newUDPConn6(t) + const ( + portA = 4001 + portB = 4002 + ) + if err := s1.DialUDP6(connA, portA, s2.Addr6(), portB); err != nil { + t.Fatal(err) + } + if err := s2.DialUDP6(connB, portB, s1.Addr6(), portA); err != nil { + t.Fatal(err) + } + + // Write and encapsulate from s1 (destination = testAddr6B). + if _, err := connA.Write([]byte("drop me")); err != nil { + t.Fatal(err) + } + n, err := s1.EgressIPv6(buf) + if err != nil || n == 0 { + t.Fatalf("expected encapsulated packet: n=%d err=%v", n, err) + } + + // Feed to s1 itself (wrong destination) – should be dropped (ErrPacketDrop or similar). + wrongDst := s1 + if err := wrongDst.IngressIPv6(buf[:n]); err == nil { + t.Error("expected error when delivering packet to wrong destination stack, got nil") + } + + // Deliver correctly to s2 – should succeed. + if err := s2.IngressIPv6(buf[:n]); err != nil { + t.Errorf("correct delivery to s2 failed: %v", err) + } + + // s2 should have received the datagram. + var rbuf [256]byte + rn, err := connB.Read(rbuf[:]) + if err != nil || rn == 0 { + t.Errorf("expected s2 to have received data: n=%d err=%v", rn, err) + } + +} diff --git a/x/xnet/subnet-table.go b/x/xnet/subnet-table.go index 8981dc5..019f2b1 100644 --- a/x/xnet/subnet-table.go +++ b/x/xnet/subnet-table.go @@ -2,11 +2,12 @@ package xnet import ( "encoding/binary" - "net/netip" + "github.com/soypat/lneto" "github.com/soypat/lneto/arp" "github.com/soypat/lneto/ethernet" "github.com/soypat/lneto/internal" + "github.com/soypat/lneto/ipv4" ) // subnetTable manages both passively learned peer MAC/IP tuples and in-flight async ARP resolves. @@ -16,10 +17,10 @@ import ( // [0 : passivePeers] — owned MAC+IP, permanently retained (learned passively from ingress) // [passivePeers : len] — externally-owned MAC, evicted by age (pending ARP queries) type subnetTable struct { - subnet netip.Prefix - resolves []struct { + subnet4 ipv4.Prefix + resolves4 []struct { mac []byte // externally owned for pending entries; owned for passive entries. - ip []byte // always owned by this struct. + ip [4]byte age uint16 } passivePeers uint8 @@ -27,12 +28,29 @@ type subnetTable struct { func (a *subnetTable) reset(arpentries int, passivePeers uint8) { a.passivePeers = passivePeers - if a.resolves == nil { - internal.SliceReuse(&a.resolves, arpentries+int(passivePeers)) - a.resolves = a.resolves[:cap(a.resolves)] + if a.resolves4 == nil { + internal.SliceReuse(&a.resolves4, arpentries+int(passivePeers)) + a.resolves4 = a.resolves4[:cap(a.resolves4)] } } +func (a *subnetTable) hwDynamicResolve(addr [4]byte, arph *arp.Handler) (mac []byte, err error) { + if !a.subnet4.Contains(addr) { + return nil, nil // not in subnet; caller uses gateway/default routing (nil MAC = no filtering) + } + var mac6 [6]byte + hw, err := arph.CacheLookup(addr[:]) + if err == nil { + copy(mac6[:], hw) + } else { + err = a.startQuery(mac6[:], addr[:], arph) + if err != nil { + return nil, err + } + } + return mac6[:], nil // mac6 escapes to heap via startQuery or return +} + func (a *subnetTable) learnFromIngressEthernet(ethernetFrame []byte) { if len(ethernetFrame) > 14+20 && binary.BigEndian.Uint16(ethernetFrame[12:14]) == uint16(ethernet.TypeIPv4) { @@ -44,21 +62,21 @@ func (a *subnetTable) learnFromIngressEthernet(ethernetFrame []byte) { // learnPassive stores or updates a passively observed MAC/IP tuple in the reserved slots. // It is a no-op if passivePeers is zero, src is not in the local subnet, or all slots are taken. func (a *subnetTable) learnPassive(src, mac []byte) { - if a.passivePeers == 0 { + if a.passivePeers == 0 || len(src) != 4 { return } - addr, _ := netip.AddrFromSlice(src) - if !a.subnet.Contains(addr) { + addr := [4]byte(src) + if !a.subnet4.Contains(addr) { return } for i := range a.passivePeers { - v := &a.resolves[i] - if internal.BytesEqual(v.ip, src) { + v := &a.resolves4[i] + if v.ip == addr { copy(v.mac, mac) // update in case MAC changed (e.g. NIC swap) return } - if len(v.ip) == 0 { - v.ip = append(v.ip, src...) + if v.ip == ([4]byte{}) { + v.ip = addr v.mac = append(v.mac, mac...) return } @@ -68,9 +86,13 @@ func (a *subnetTable) learnPassive(src, mac []byte) { // startQuery copies the MAC into mac immediately if the IP was passively learned, // otherwise issues an ARP query via h and registers mac as the externally-owned destination. func (a *subnetTable) startQuery(mac, ip []byte, h *arp.Handler) error { + if len(ip) != 4 { + return lneto.ErrUnsupported + } + addr := [4]byte(ip) for i := range a.passivePeers { - v := &a.resolves[i] - if internal.BytesEqual(v.ip, ip) { + v := &a.resolves4[i] + if v.ip == addr { copy(mac, v.mac) return nil } @@ -80,33 +102,38 @@ func (a *subnetTable) startQuery(mac, ip []byte, h *arp.Handler) error { } n := int(a.passivePeers) oldest := n - for i := n; i < len(a.resolves); i++ { - v := &a.resolves[i] + for i := n; i < len(a.resolves4); i++ { + v := &a.resolves4[i] if len(v.mac) == 0 { oldest = i break - } else if v.age > a.resolves[oldest].age { + } else if v.age > a.resolves4[oldest].age { oldest = i } } - for i := n; i < len(a.resolves); i++ { - a.resolves[i].age++ + for i := n; i < len(a.resolves4); i++ { + a.resolves4[i].age++ } - v := &a.resolves[oldest] + v := &a.resolves4[oldest] v.mac = mac - v.ip = append(v.ip[:0], ip...) - v.age = 0 + v.ip = addr + v.age = 1 return nil } // onResolve is the arp.Handler resolve callback; called when an ARP response arrives. func (a *subnetTable) onResolve(mac, ip []byte) { - for i := int(a.passivePeers); i < len(a.resolves); i++ { - v := &a.resolves[i] - if internal.BytesEqual(ip, v.ip) { + if len(ip) != 4 { + return + } + addr := [4]byte(ip) + for i := int(a.passivePeers); i < len(a.resolves4); i++ { + v := &a.resolves4[i] + if v.ip == addr { copy(v.mac, mac) v.mac = nil - v.ip = v.ip[:0] + v.ip = [4]byte{} + v.age = 0 return } } @@ -130,8 +157,8 @@ func (a *subnetTable) patchEgressMAC(frame []byte) { return } for i := range a.passivePeers { - v := &a.resolves[i] - if internal.BytesEqual(v.ip, dstIP) { + v := &a.resolves4[i] + if internal.BytesEqual(v.ip[:], dstIP) { *efrm.DestinationHardwareAddr() = [6]byte(v.mac) return } diff --git a/x/xnet/xnet_arp_test.go b/x/xnet/xnet_arp_test.go index d6a793a..56ddd0e 100644 --- a/x/xnet/xnet_arp_test.go +++ b/x/xnet/xnet_arp_test.go @@ -14,8 +14,8 @@ func TestARPLocal(t *testing.T) { s1, s2, c1, c2 := newTCPStacks(t, seed, mtu) routerHw := [6]byte{1, 2, 3, 4, 5, 6} // Most common case: we have a router in between computers. - s1.SetGateway6(routerHw) - s2.SetGateway6(routerHw) + s1.SetGatewayHardwareAddr(routerHw) + s2.SetGatewayHardwareAddr(routerHw) addr1 := netip.AddrPortFrom(netip.AddrFrom4(s1.Addr4()), 1024) // dialer, client. addr2 := netip.AddrPortFrom(netip.AddrFrom4(s2.Addr4()), 80) // listener, server. err := s1.AssimilateDHCPResults(&DHCPResults{ @@ -30,12 +30,12 @@ func TestARPLocal(t *testing.T) { if err != nil { t.Fatal(err) } - hw2 := s2.HardwareAddress() + hw2 := s2.HardwareAddr() err = s1.DialTCP(c1, addr1.Port(), addr2) // addr2 MAC address is unknown and must be resolved by stack. if err != nil { t.Fatal(err) } - err = s2.ListenTCP(c2, addr2.Port()) + err = s2.ListenTCP4(c2, addr2.Port()) if err != nil { t.Fatal(err) } diff --git a/x/xnet/xnet_bench_test.go b/x/xnet/xnet_bench_test.go index 99b9d0c..4f9eeec 100644 --- a/x/xnet/xnet_bench_test.go +++ b/x/xnet/xnet_bench_test.go @@ -35,8 +35,8 @@ func BenchmarkARPExchange(b *testing.B) { b.Fatal(err) } // Set gateways so ethernet frames are properly addressed. - c1.SetGateway6(c2.HardwareAddress()) - c2.SetGateway6(c1.HardwareAddress()) + c1.SetGatewayHardwareAddr(c2.HardwareAddr()) + c2.SetGatewayHardwareAddr(c1.HardwareAddr()) var buf [frameSize]byte @@ -104,8 +104,8 @@ func BenchmarkTCPHandshake(b *testing.B) { if err != nil { b.Fatal(err) } - sv.SetGateway6(client.HardwareAddress()) - client.SetGateway6(sv.HardwareAddress()) + sv.SetGatewayHardwareAddr(client.HardwareAddr()) + client.SetGatewayHardwareAddr(sv.HardwareAddr()) buf := make([]byte, MTU*4) err = clconn.Configure(tcp.ConnConfig{ @@ -130,7 +130,7 @@ func BenchmarkTCPHandshake(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { // Setup connections. - err = sv.ListenTCP(svconn, svPort) + err = sv.ListenTCP4(svconn, svPort) if err != nil { b.Fatal(err) } diff --git a/x/xnet/xnet_concurrent_test.go b/x/xnet/xnet_concurrent_test.go index f27187c..5bd4be7 100644 --- a/x/xnet/xnet_concurrent_test.go +++ b/x/xnet/xnet_concurrent_test.go @@ -88,7 +88,7 @@ func TestTCPListener_ConcurrentEcho(t *testing.T) { t.Fatalf("client %d reset: %v", i, err) } // Client gateway points to server. - clientStacks[i].SetGateway6(serverMAC) + clientStacks[i].SetGatewayHardwareAddr(serverMAC) // Configure client connection buffers. bufOff := i * tcpBufSize * 2 diff --git a/x/xnet/xnet_dns_test.go b/x/xnet/xnet_dns_test.go index 0665357..b6a6a8a 100644 --- a/x/xnet/xnet_dns_test.go +++ b/x/xnet/xnet_dns_test.go @@ -35,7 +35,7 @@ func TestDNS_QueryReceivesAnswer(t *testing.T) { if err != nil { t.Fatal("client Reset failed:", err) } - client.SetGateway6(dnsServerMAC) + client.SetGatewayHardwareAddr(dnsServerMAC) // The IP address we expect to receive from the DNS response. wantAddr := netip.MustParseAddr("93.184.216.34") // example.com's IP diff --git a/x/xnet/xnet_fuzz_test.go b/x/xnet/xnet_fuzz_test.go index a2e55fc..7fb4a8b 100644 --- a/x/xnet/xnet_fuzz_test.go +++ b/x/xnet/xnet_fuzz_test.go @@ -22,7 +22,7 @@ func FuzzStackPacketHTTP(f *testing.F) { var buf [ethernet.MaxFrameLength]byte s1, s2, c1, c2 := newTCPStacks(f, seed, MTU) var hdr httpraw.Header - err := s1.ListenTCP(c1, 80) + err := s1.ListenTCP4(c1, 80) if err != nil { f.Fatal(err) } @@ -88,7 +88,7 @@ func FuzzStackPacketHTTP(f *testing.F) { if err != nil { t.Fatal(err) } - err := s1.ListenTCP(c1, 80) + err := s1.ListenTCP4(c1, 80) if err != nil { t.Fatal(err) } @@ -364,7 +364,7 @@ func testStackSeeded(t *testing.T, seed1, seed2 int64) { if err != nil { t.Fatal(i, err) } - err = s2.ListenTCP(&tcp2, port2) + err = s2.ListenTCP4(&tcp2, port2) if err != nil { t.Fatal(i, err) } diff --git a/x/xnet/xnet_icmp_test.go b/x/xnet/xnet_icmp_test.go index 3cc0aad..c54da0b 100644 --- a/x/xnet/xnet_icmp_test.go +++ b/x/xnet/xnet_icmp_test.go @@ -33,8 +33,8 @@ func TestStackAsync_ICMPEcho(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sender.SetGateway6(receiver.HardwareAddress()) - receiver.SetGateway6(sender.HardwareAddress()) + sender.SetGatewayHardwareAddr(receiver.HardwareAddr()) + receiver.SetGatewayHardwareAddr(sender.HardwareAddr()) key, err := sender.icmp.PingStart(receiver.Addr4(), tt.pattern, tt.size) if err != nil { diff --git a/x/xnet/xnet_listener_test.go b/x/xnet/xnet_listener_test.go index 01398e0..7de522e 100644 --- a/x/xnet/xnet_listener_test.go +++ b/x/xnet/xnet_listener_test.go @@ -40,8 +40,8 @@ func TestStackAsyncListener_SingleConnection(t *testing.T) { if err != nil { t.Fatal(err) } - client.SetGateway6(sv.HardwareAddress()) - sv.SetGateway6(client.HardwareAddress()) + client.SetGatewayHardwareAddr(sv.HardwareAddr()) + sv.SetGatewayHardwareAddr(client.HardwareAddr()) // Create client connection. var clConn tcp.Conn @@ -166,7 +166,7 @@ func TestStackAsyncListener_MultiSequentialConn(t *testing.T) { } caddr := netip.AddrFrom4([4]byte{10, 0, 0, 1}) chw := [6]byte{0xbe, 0xef, 0, 0, 0, 1} - sv.SetGateway6(chw) + sv.SetGatewayHardwareAddr(chw) tst := testerFrom(t, MTU) doRequest := func(caddrp netip.AddrPort, sleep time.Duration, data []byte) { var client StackAsync @@ -181,7 +181,7 @@ func TestStackAsyncListener_MultiSequentialConn(t *testing.T) { if err != nil { panic(err) } - client.SetGateway6(sv.HardwareAddress()) + client.SetGatewayHardwareAddr(sv.HardwareAddr()) // Create client connection. var clConn tcp.Conn err = clConn.Configure(tcp.ConnConfig{ diff --git a/x/xnet/xnet_mdns_test.go b/x/xnet/xnet_mdns_test.go index 3dc0a5d..912f153 100644 --- a/x/xnet/xnet_mdns_test.go +++ b/x/xnet/xnet_mdns_test.go @@ -54,7 +54,7 @@ func TestMDNS_QueryResponse(t *testing.T) { if err != nil { t.Fatal("responder reset:", err) } - responderStack.SetGateway6(querierMAC) + responderStack.SetGatewayHardwareAddr(querierMAC) var responderClient mdns.Client err = responderClient.Configure(mdns.ClientConfig{ @@ -65,7 +65,7 @@ func TestMDNS_QueryResponse(t *testing.T) { if err != nil { t.Fatal("responder configure:", err) } - err = responderStack.RegisterUDP(&responderClient, mcastAddr, mdns.Port) + err = responderStack.RegisterUDP4(&responderClient, mcastAddr, mdns.Port) if err != nil { t.Fatal("responder register:", err) } @@ -84,7 +84,7 @@ func TestMDNS_QueryResponse(t *testing.T) { if err != nil { t.Fatal("querier reset:", err) } - querierStack.SetGateway6(responderMAC) + querierStack.SetGatewayHardwareAddr(responderMAC) var querierClient mdns.Client err = querierClient.Configure(mdns.ClientConfig{ @@ -105,7 +105,7 @@ func TestMDNS_QueryResponse(t *testing.T) { if err != nil { t.Fatal("start resolve:", err) } - err = querierStack.RegisterUDP(&querierClient, mcastAddr, mdns.Port) + err = querierStack.RegisterUDP4(&querierClient, mcastAddr, mdns.Port) if err != nil { t.Fatal("querier register:", err) } @@ -289,7 +289,7 @@ func newMDNSStack(t *testing.T, hostname string, seed int64, if err != nil { t.Fatal(hostname, "reset:", err) } - stack.SetGateway6(gatewayMAC) + stack.SetGatewayHardwareAddr(gatewayMAC) var client mdns.Client err = client.Configure(mdnsCfg) @@ -297,7 +297,7 @@ func newMDNSStack(t *testing.T, hostname string, seed int64, t.Fatal(hostname, "mdns configure:", err) } - err = stack.RegisterUDP(&client, mdnsCfg.MulticastAddr, mdns.Port) + err = stack.RegisterUDP4(&client, mdnsCfg.MulticastAddr, mdns.Port) if err != nil { t.Fatal(hostname, "register udp:", err) } diff --git a/x/xnet/xnet_subnettable_test.go b/x/xnet/xnet_subnettable_test.go index c13d8a2..28e8e38 100644 --- a/x/xnet/xnet_subnettable_test.go +++ b/x/xnet/xnet_subnettable_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/soypat/lneto/ethernet" + "github.com/soypat/lneto/ipv4" "github.com/soypat/lneto/tcp" ) @@ -21,7 +22,7 @@ func TestSubnetTable_PatchEgressMAC_WhenGatewayMAC(t *testing.T) { var st subnetTable st.reset(4, 2) - st.subnet = netip.MustParsePrefix("10.0.0.0/24") + st.subnet4 = ipv4.PrefixFrom(clientIP, 24) // Learn client MAC from a simulated ingress frame (client→server SYN). ingressFrame := makeMinimalIPv4Frame(serverMAC, clientMAC, clientIP, serverIP) @@ -62,8 +63,8 @@ func TestStackAsync_ListenerSynAckAddressedToClient(t *testing.T) { if err != nil { t.Fatal(err) } - sv.SetGateway6(routerMAC) - sv.SetSubnet(netip.MustParsePrefix("10.0.0.0/24")) + sv.SetGatewayHardwareAddr(routerMAC) + sv.SetSubnet4(sv.Addr4(), 24) pool, err := NewTCPPool(TCPPoolConfig{ PoolSize: 1, @@ -97,7 +98,7 @@ func TestStackAsync_ListenerSynAckAddressedToClient(t *testing.T) { if err != nil { t.Fatal(err) } - client.SetGateway6(serverMAC) + client.SetGatewayHardwareAddr(serverMAC) var clConn tcp.Conn if err = clConn.Configure(tcp.ConnConfig{ diff --git a/x/xnet/xnet_test.go b/x/xnet/xnet_test.go index baba791..be3e2a2 100644 --- a/x/xnet/xnet_test.go +++ b/x/xnet/xnet_test.go @@ -201,8 +201,8 @@ func newTCPStacks(t testing.TB, randSeed int64, mtu int) (s1, s2 *StackAsync, c1 if err != nil { t.Fatal(err) } - s1.SetGateway6(s2.HardwareAddress()) - s2.SetGateway6(s1.HardwareAddress()) + s1.SetGatewayHardwareAddr(s2.HardwareAddr()) + s2.SetGatewayHardwareAddr(s1.HardwareAddr()) buf := make([]byte, mtu*4) err = c1.Configure(tcp.ConnConfig{ RxBuf: buf[:mtu], @@ -253,7 +253,7 @@ func noExchange(source int) tcpExpectExchange { func (tst *tester) TestTCPSetupAndEstablish(svStack, clStack *StackAsync, svConn, clConn *tcp.Conn, svPort, clPort uint16) { t := tst.t // Attach server and client connections to stacks. - err := svStack.ListenTCP(svConn, svPort) + err := svStack.ListenTCP4(svConn, svPort) if err != nil { t.Fatal(err) } @@ -453,8 +453,8 @@ func (tst *tester) TCPExchange(expect tcpExpectExchange, stack1, stack2 *StackAs if err != nil { t.Fatal(err) } - srcEth := src.HardwareAddress() - dstEth := dst.HardwareAddress() + srcEth := src.HardwareAddr() + dstEth := dst.HardwareAddr() if !bytes.Equal(srcEth[:], tst.getData(protoEthernet, pcap.FieldClassSrc)) { t.Errorf("mismatched ethernet src addr %x", tst.getData(protoEthernet, pcap.FieldClassSrc)) } @@ -511,8 +511,8 @@ func (tst *tester) ARPExchangeOnly(querying, target *StackAsync) { } tst.buf = tst.buf[:n] - qHw := querying.HardwareAddress() - tgtHw := target.HardwareAddress() + qHw := querying.HardwareAddr() + tgtHw := target.HardwareAddr() broadcast := ethernet.BroadcastAddr() qIP := querying.Addr4() tgtIP := target.Addr4()