mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
[](https://pkg.go.dev/github.com/soypat/lneto)
|
||||
[](https://goreportcard.com/report/github.com/soypat/lneto)
|
||||
[](https://codecov.io/gh/soypat/lneto)
|
||||
[](https://github.com/soypat/lneto/actions/workflows/go.yml)
|
||||
[](https://github.com/soypat/lneto/actions/workflows/ci.yaml)
|
||||
[](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.
|
||||
|
||||
|
||||
+3
-4
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+20
-22
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
+14
-5
@@ -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() {
|
||||
|
||||
+50
@@ -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()
|
||||
}
|
||||
|
||||
+11
-10
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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, " | "))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-9
@@ -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 {
|
||||
|
||||
+13
-13
@@ -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{
|
||||
|
||||
@@ -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),
|
||||
|
||||
+3
-1
@@ -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.
|
||||
|
||||
+130
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -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].
|
||||
|
||||
@@ -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
|
||||
|
||||
+205
-145
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
+57
-30
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
+7
-7
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user