mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b46b4c7bc5 | |||
| 8f9d765cb0 | |||
| 6f5acd1948 | |||
| e0ef681085 | |||
| bef2137bad | |||
| 059f761856 | |||
| 5a674a4ad1 | |||
| 60098ad8d0 | |||
| 813b7b5e57 | |||
| 5f8ca45859 | |||
| 856ac373d5 | |||
| 82f9461548 | |||
| 6ba06acdcb | |||
| 3b82cefec3 | |||
| 24d5d303bc | |||
| 46d4c06b9f | |||
| edf302baf8 | |||
| 8efd810610 | |||
| d010e6a7e9 | |||
| 9fcb7e9b52 | |||
| b95eb03aa5 | |||
| a2970b923d | |||
| 7d5830d7ab | |||
| bdbd38ab44 | |||
| a430f6c40a | |||
| bba913751a | |||
| cf94767133 | |||
| aa77403a2b | |||
| 67c42aa136 | |||
| 198db3789b | |||
| b2c3d11c4c | |||
| a46e40580c | |||
| 34bbaa6eb4 | |||
| 363bec6793 | |||
| 6d0ffcf0ad | |||
| c020795303 | |||
| f87761f777 | |||
| 9d436c1d86 | |||
| 16c2c3de36 | |||
| 75a812a8d7 | |||
| 5bde7a9979 | |||
| 191b8ee4cc |
@@ -0,0 +1,4 @@
|
||||
ignore:
|
||||
- "examples/**"
|
||||
- "**/stringers.go"
|
||||
- "**/*_stringers.go"
|
||||
@@ -0,0 +1,137 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
unformatted=$(gofmt -l .)
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "::error::Files not formatted with gofmt:"
|
||||
echo "$unformatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: go vet (tinygo tags)
|
||||
run: go vet -tags=tinygo ./...
|
||||
|
||||
- name: go fix
|
||||
run: |
|
||||
diff=$(go fix -diff ./... 2>&1) || true
|
||||
if [ -n "$diff" ]; then
|
||||
echo "$diff"
|
||||
echo "::error::Files need go tool fix. Run: go tool fix ./..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
test:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
mkdir -p profiles
|
||||
go test -v -shuffle=on -count=1 \
|
||||
-coverprofile=profiles/coverage.profile -covermode=atomic -coverpkg=./... \
|
||||
-timeout=10m \
|
||||
./...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@3f20e214133d0983f9a10f3d63b0faf9241a3daa # v6.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: profiles/coverage.profile
|
||||
slug: soypat/lneto
|
||||
|
||||
- name: Upload profiles
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: test-profiles
|
||||
path: profiles/
|
||||
retention-days: 14
|
||||
|
||||
race:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Test (race + shuffle)
|
||||
run: go test -race -shuffle=on -count=1 -timeout=10m ./...
|
||||
|
||||
benchmark:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "1.26"
|
||||
|
||||
- name: Run benchmarks
|
||||
run: go test -bench=. -benchmem -count=5 -shuffle=on -run='^$' -timeout=15m ./... | tee bench-results.txt
|
||||
|
||||
# Fails for a weird reason, will be investigated in the future
|
||||
# - name: Report benchmark regressions
|
||||
# uses: benchmark-action/github-action-benchmark@a887eba2af2000fda74e733fcf952aa823b65916 # v1.21.0
|
||||
# with:
|
||||
# tool: go
|
||||
# output-file-path: bench-results.txt
|
||||
# summary-always: true
|
||||
# comment-on-alert: true
|
||||
# alert-threshold: "120%"
|
||||
# fail-on-alert: false
|
||||
# auto-push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload benchmark results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: bench-results
|
||||
path: bench-results.txt
|
||||
retention-days: 30
|
||||
@@ -1,43 +0,0 @@
|
||||
# This workflow will build a golang project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.25
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: TinyGo vet
|
||||
run: go vet -tags=tinygo ./...
|
||||
# - name: govulncheck # Getting false positives all the time.
|
||||
# uses: golang/govulncheck-action@v1
|
||||
# with:
|
||||
# go-version-input: 1.21
|
||||
# go-package: ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -race ./...
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: soypat/lneto
|
||||
|
||||
@@ -3,9 +3,15 @@ go.work
|
||||
go.work.sum
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
# Test coverage profiles.
|
||||
profiles/
|
||||
|
||||
# Dependency directories after running `go mod vendor`
|
||||
vendor/
|
||||
|
||||
# Wifi credentials
|
||||
*.credentials
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.elf
|
||||
*.uf2
|
||||
@@ -15,6 +21,7 @@ vendor/
|
||||
*.so
|
||||
*.dylib
|
||||
*.hex
|
||||
*.wasm
|
||||
|
||||
# Profiling
|
||||
*.pprof
|
||||
@@ -31,6 +38,7 @@ vendor/
|
||||
**__debug_bin*
|
||||
# `__debug_bin` Debug binary generated in VSCode when using the built-in debugger.
|
||||
*bin
|
||||
/gen-binary-bench
|
||||
|
||||
/bridge
|
||||
# IDE
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
[](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://sourcegraph.com/github.com/soypat/lneto?badge)
|
||||
[](https://github.com/soypat/lneto/actions/workflows/ci.yaml)
|
||||
[](https://github.com/soypat/lneto/network/dependents)
|
||||
|
||||
Userspace networking primitives.
|
||||
Userspace networking primitives.
|
||||
|
||||
`lneto` is pronounced "L-net-oh", a.k.a. "El Neto"; a.k.a. "Don Networkio"; a.k.a "Neto, connector of worlds".
|
||||
|
||||
## Features
|
||||
`lneto` provides the following features:
|
||||
- Zero Operating System required. ***Zero***. All protocols (including TCP, excepting NTP) are time-independent.
|
||||
- Explicit HAL needed. Lneto performs no `time.Sleep` nor `time.Now` calls unless configured by user.
|
||||
- Zero scheduling required. No goroutines/channels use in Lneto. Can be run in event loop.
|
||||
- Heapless packet processing
|
||||
- [`httpraw`](https://github.com/soypat/lneto/tree/main/http/httpraw) is likely the most performant HTTP/1.1 processing package in the Go ecosystem. Based on [`fasthttp`](https://github.com/valyala/fasthttp) but simpler and more thoughtful memory use.
|
||||
- Lean memory footprint
|
||||
- HTTP header struct is 80 bytes with no runtime usage nor heap usage other than buffer
|
||||
- Entire Ethernet+IPv4+UDP+DHCP+DNS+NTP stack in ~1kB.
|
||||
- Entire Ethernet+IPv4+UDP+DHCP+DNS+NTP stack in ~2kB RAM.
|
||||
- Empty go.mod file. No dependencies except for basic standard library packages such as `bytes`, `errors`, `io`.
|
||||
- `net` only imported for `net.ErrClosed`.
|
||||
- Can produce **very** small binaries. Ideal for embedded systems.
|
||||
@@ -27,15 +30,25 @@ Userspace networking primitives.
|
||||
## [`min-working-example`](./examples/min-working-example/) - Quick lneto showcase
|
||||
Get a quick showcase of how lneto can be configured and how to get a TCP listening server up and running.
|
||||
|
||||
### 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 |
|
||||
|---|:---:|:---:|---|---|---|---|---|
|
||||
| [Lneto MWE](./examples/min-working-example/) | DNS,NTP,DHCP | ✅ | 3.9MB | 4.4MB | 1.6MB | 1.2MB | 189kB |
|
||||
| [Gvisor MWE w/ go-net](./examples/_import_examples/gvisor-mwe/) | None | ❌ | 6.6MB | 7.5MB | 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.
|
||||
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.
|
||||
|
||||
- DHCP client address lease
|
||||
- ARP address resolution
|
||||
- DNS address resolution of requested host
|
||||
- HTTP over TCP/IPv4/Ethernet connection using
|
||||
- NTP time check (optional)
|
||||
- Print packet captures using lneto's [internet/pcap](./internet/pcap) package
|
||||
- Print packet captures using lneto's [internet/pcap](./internet/pcap) package
|
||||
|
||||
See Developing section below for more information.
|
||||
|
||||
@@ -43,7 +56,7 @@ See Developing section below for more information.
|
||||
lneto is currently being used primarily by embedded developers and those who need a lighter alternative to gVisor in terms of memory usage and binary size.
|
||||
|
||||
- [**soypat/cyw43439**](https://github.com/soypat/cyw43439): Enabling internet access on [Raspberry Pi Pico W](https://www.raspberrypi.com/products/raspberry-pi-pico/). See [`examples`](https://github.com/soypat/cyw43439/tree/main/examples).
|
||||
- [**tinygo-org/espradio**](https://github.com/tinygo-rog/espradio): Enabling internet access on [Espressif's ESP32s](https://www.espressif.com/en/products/socs/esp32)
|
||||
- [**tinygo-org/espradio**](https://github.com/tinygo-org/espradio): Enabling internet access on [Espressif's ESP32s](https://www.espressif.com/en/products/socs/esp32).
|
||||
- [**TamaGo**](https://github.com/usbarmory/tamago): Enabling networking on Go baremetal projects. See [go-net project](https://github.com/usbarmory/go-net).
|
||||
- [**netbird.io**](https://netbird.io/)(planned): Secure remote P2P access.
|
||||
- [**soypat/lan8720**](https://github.com/soypat/lan8720): Enabling internet access with 100M ethernet PHY devices.
|
||||
@@ -71,26 +84,60 @@ PASS
|
||||
ok github.com/soypat/lneto/x/xnet 2.926s
|
||||
```
|
||||
|
||||
## Protocol Support Matrix
|
||||
|
||||
> **Generated 2026-04-27.** This table may not always reflect the current state of the codebase.
|
||||
> Allocation figures are derived from benchmark tests run on amd64.
|
||||
> `—` denotes no standalone benchmark exists for that protocol; those packages follow the same zero-allocation design principle documented in the codebase.
|
||||
|
||||
| Protocol | RFC / Spec | Status | Package | Allocs/op | Notes |
|
||||
|----------|-----------|:------:|---------|:---------:|-------|
|
||||
| Ethernet II | IEEE 802.3 | ✅ | `ethernet` | 0 ¹ | Frame parsing, CRC-32 |
|
||||
| ARP | RFC 826 | ✅ | `arp` | 0 ¹ | Request/reply, hardware address cache |
|
||||
| IPv4 | RFC 791 | ✅ | `ipv4` | 0 ² | Frame parsing, ones-complement checksum |
|
||||
| ICMPv4 | RFC 792 | ✅ | `ipv4/icmpv4` | — | Echo (ping) client handler |
|
||||
| UDP | RFC 768 | ✅ | `udp` | — | Handler + thread-safe `Conn` |
|
||||
| TCP | RFC 9293 | ✅ | `tcp` | 0 ²³ | Full state machine, SYN cookies, retransmit queue, `Conn`/`Listener` |
|
||||
| DNS | RFC 1035 | ✅ | `dns` | — | Client (A/AAAA query) |
|
||||
| DHCPv4 | RFC 2131 | ✅ | `dhcp/dhcpv4` | — | Client + Server |
|
||||
| IPv4 Link-Local (APIPA) | RFC 3927 | ✅ | `ipv4/linklocal4` | 0 | Heapless claim-and-defend state machine for 169.254.x.x |
|
||||
| NTP | RFC 5905 | ✅ | `ntp` | — | Client |
|
||||
| NTS | RFC 8915 | ✅ | `x/nts` | — | Client + Server; key exchange over TLS 1.3, authenticated NTP. Requires caller-supplied AEAD_AES_SIV_CMAC_256 |
|
||||
| mDNS | RFC 6762 | ✅ | `dns/mdns` | — | Client (service announcement + query) |
|
||||
| HTTP/1.1 headers | RFC 9110, RFC 9112 | ✅ | `http/httpraw` | 2 ⁴ | Header parse/format; no field normalization |
|
||||
| Ethernet PHY/MDIO | IEEE 802.3 cl.22/45 | ✅ | `phy` | — | Bare-metal PHY management via MDIO |
|
||||
| IPv6 | RFC 8200 | ✅ | `ipv6` | — | Frame parsing and stack handling |
|
||||
| ICMPv6 | RFC 4443 | ✅ | `ipv6/icmpv6` | — | Echo+NDP frame parsing and stack handling |
|
||||
| DHCPv6 | RFC 8415 | 🟡 | `dhcp/dhcpv6` | — | Frame parsing and standalone handling |
|
||||
| TLS 1.3 | RFC 8446 | ❌ | — | — | Not implemented |
|
||||
|
||||
¹ `BenchmarkARPExchange` — full ARP request/response exchange over Ethernet: **0 B/op, 0 allocs/op**
|
||||
² `BenchmarkTCPHandshake` — TCP 3-way handshake over Ethernet/IPv4: **0 B/op, 0 allocs/op**
|
||||
³ `BenchmarkSYNCookie_Generate` / `BenchmarkSYNCookie_Validate` — SYN cookie generation and validation: **0 B/op, 0 allocs/op each**
|
||||
⁴ `BenchmarkParseBytes` — HTTP/1.1 request header + body parsing: **240 B/op, 2 allocs/op**
|
||||
|
||||
## Packages
|
||||
- `lneto`: Low-level Networking Operations, or "El Neto", the networking package. Zero copy network frame marshalling and unmarshalling.
|
||||
- [`lneto/validation.go`](./validation.go): Packet validation utilities
|
||||
- [`lneto/internet`](./internet): Userspace IP/TCP networking stack. This is where the magic happens. Integrates many of the listed packages.
|
||||
- [`lneto/internet/pcap`](./internal/pcap): Packet capture and field breakdown utilities. Wireshark in the making.
|
||||
- [`lneto/http/httpraw`](./http/httpraw/): Heapless HTTP header processing and validation. Does no implement header normalization.
|
||||
- [`lneto/tcp`](./ntp): TCP implementation and low level logic.
|
||||
- [`lneto/tcp`](./tcp): TCP implementation and low level logic.
|
||||
- [`lneto/ipv4/linklocal4`](./ipv4/linklocal4): RFC 3927 IPv4 link-local (APIPA) address autoconfiguration. Heapless claim-and-defend state machine for self-assigned 169.254.x.x addresses when no DHCP server is available.
|
||||
- [`lneto/dhcpv4`](./dhcpv4): DHCP version 4 protocol implementation and low level logic.
|
||||
- [`lneto/dns`](./dns): DNS protocol implementation and low level logic.
|
||||
- [`lneto/ntp`](./ntp): NTP implementation and low level logic. Includes NTP time primitives manipulation and conversion to Go native types.
|
||||
- [`lneto/internal`](./internal): Lightweight and flexible ring buffer implementation and debugging primitives.
|
||||
- [`lneto/x`](./x): Experimental packages.
|
||||
- [`lneto/x/xnet`](./x/xnet/): `net` package like abstractions of stack implementations for ease of reuse. Still in testing phase and likely subject to breaking API change.
|
||||
- [`lneto/x/nts`](./x/nts/): Network Time Security (RFC 8915). NTS-KE key exchange over TLS 1.3 and AEAD-authenticated NTP client/server. Ships no cryptography itself; the caller supplies the mandated `AEAD_AES_SIV_CMAC_256` `cipher.AEAD` implementation.
|
||||
|
||||
### Abstractions
|
||||
The following interface is implemented by networking stack nodes and the stack themselves.
|
||||
|
||||
```go
|
||||
type StackNode interface {
|
||||
// Encapsulate receives a buffer the receiver must fill with data.
|
||||
// Encapsulate receives a buffer the receiver must fill with data.
|
||||
// The receiver's start byte is at carrierData[offsetToFrame].
|
||||
Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error)
|
||||
// Demux receives a buffer the receiver must decode and pass on to corresponding child StackNode(s).
|
||||
@@ -116,11 +163,19 @@ go mod download github.com/soypat/lneto@latest
|
||||
|
||||
## Developing (linux)
|
||||
|
||||
When testing lneto through a Linux network interface, disable receive offloading if packets are unexpectedly dropped with checksum errors. Generic receive offload (GRO) can merge TCP frames before delivery to raw sockets without recalculating the full checksum.
|
||||
|
||||
```sh
|
||||
sudo ethtool -K <network-interface> gro off
|
||||
```
|
||||
|
||||
Depending on the interface and driver, other offloading features may also need to be disabled.
|
||||
|
||||
- [`examples/httptap`](./examples/httptap) (linux only, root privilidges required) Program opens a TAP interface and assigns an IP address to it and exposes the interface via a HTTP interface. This program is run with root privilidges to facilitate debugging of lneto since no root privilidges are required to interact with the HTTP interface exposed.
|
||||
- `POST http://127.0.0.1:7070/send`: Receives a POST with request body containing JSON string of data to send over TAP interface. Response contains only status code.
|
||||
- `GET http://127.0.0.1:7070/recv`: Receives a GET request. Response contains a JSON string of oldest unread TAP interface packet. If string is empty then there is no more data to read.
|
||||
|
||||
- [`xcurl`](./examples/xcurl) Contains example of a application that uses lneto and can attach to a linux tap/bridge interface or a [httptap](./examples/httptap)(with -ihttp flag) to work. When using httptap can be run as non-root user to be debugged comfortably.
|
||||
- [`xcurl`](./examples/xcurl) Contains example of a application that uses lneto and can attach to a linux tap/bridge interface or a [httptap](./examples/httptap)(with -ihttp flag) to work. When using httptap can be run as non-root user to be debugged comfortably.
|
||||
- Example: `go run ./examples/xcurl -host google.com -ihttp`
|
||||
|
||||
### LLM Policy / AI Policy
|
||||
@@ -218,4 +273,4 @@ The document has moved
|
||||
<A HREF="http://www.google.com/">here</A>.
|
||||
</BODY></HTML>
|
||||
success
|
||||
```
|
||||
```
|
||||
|
||||
+5
-86
@@ -2,8 +2,6 @@ package arp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
@@ -49,9 +47,9 @@ func TestHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
// Perform ARP exchange.
|
||||
expectHWAddr := c2.ourHWAddr
|
||||
expectHWAddr := c2.ourHWAddr[:]
|
||||
queryAddr := c2.ourProtoAddr
|
||||
err = c1.StartQuery(nil, queryAddr)
|
||||
err = c1.StartQuery(queryAddr, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -85,11 +83,11 @@ func TestHandler(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hwaddr, err := c1.QueryResult(queryAddr)
|
||||
hwaddr, err := c1.CacheLookup(queryAddr)
|
||||
if err != nil {
|
||||
log.Fatal("expected query result:", err)
|
||||
t.Fatal("expected query result:", err)
|
||||
} else if !bytes.Equal(hwaddr, expectHWAddr) {
|
||||
log.Fatalf("expected to get hwaddr %x!=%x", hwaddr, expectHWAddr)
|
||||
t.Fatalf("expected to get hwaddr %x!=%x", hwaddr, expectHWAddr)
|
||||
}
|
||||
n, err = c1.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
@@ -105,85 +103,6 @@ func TestHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryCompaction(t *testing.T) {
|
||||
var h Handler
|
||||
|
||||
startQuery := func(addr []byte) {
|
||||
err := h.StartQuery(nil, addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
err := h.Reset(HandlerConfig{
|
||||
HardwareAddr: []byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x00},
|
||||
ProtocolAddr: []byte{192, 168, 1, 1},
|
||||
MaxQueries: 5,
|
||||
MaxPending: 1,
|
||||
HardwareType: 1,
|
||||
ProtocolType: ethernet.TypeIPv4,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create multiple queries
|
||||
addr1 := []byte{192, 168, 1, 10}
|
||||
addr2 := []byte{192, 168, 1, 20}
|
||||
addr3 := []byte{192, 168, 1, 30}
|
||||
|
||||
// Start 3 queries
|
||||
startQuery(addr1)
|
||||
startQuery(addr2)
|
||||
startQuery(addr3)
|
||||
|
||||
if len(h.queries) != 3 {
|
||||
t.Fatalf("expected 3 queries, got %d", len(h.queries))
|
||||
}
|
||||
|
||||
// Discard the middle query (addr2)
|
||||
if err := h.DiscardQuery(addr2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify addr2 is marked as invalid
|
||||
hasAddr2 := slices.ContainsFunc(h.queries, func(q queryResult) bool {
|
||||
return bytes.Equal(q.protoaddr, addr2)
|
||||
})
|
||||
if hasAddr2 {
|
||||
t.Fatal("addr2 query found after discard")
|
||||
}
|
||||
|
||||
// Start new queries to trigger compaction
|
||||
addr4 := []byte{192, 168, 1, 40}
|
||||
addr5 := []byte{192, 168, 1, 50}
|
||||
addr6 := []byte{192, 168, 1, 60}
|
||||
|
||||
startQuery(addr4)
|
||||
startQuery(addr5)
|
||||
startQuery(addr6)
|
||||
|
||||
// After compaction we should be left with 5 queries
|
||||
expectedAddrs := [][]byte{addr1, addr3, addr4, addr5, addr6}
|
||||
|
||||
if len(h.queries) != len(expectedAddrs) {
|
||||
t.Fatalf("after compaction: expected %d queries, got %d", len(expectedAddrs), len(h.queries))
|
||||
}
|
||||
|
||||
gotAddrs := [][]byte{}
|
||||
for _, q := range h.queries {
|
||||
if !q.isInvalid() {
|
||||
gotAddrs = append(gotAddrs, q.protoaddr)
|
||||
} else {
|
||||
t.Fatalf("invalid query %v should have been removed during compaction", q.protoaddr)
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.EqualFunc(gotAddrs, expectedAddrs, bytes.Equal) {
|
||||
t.Fatalf("expected %v, got %v", expectedAddrs, gotAddrs)
|
||||
}
|
||||
}
|
||||
|
||||
func validateARP(t *testing.T, buf []byte) {
|
||||
t.Helper()
|
||||
afrm, err := NewFrame(buf)
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package arp
|
||||
|
||||
import (
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
entries []entry
|
||||
}
|
||||
|
||||
// entry is designed for compactness. size=class=24 bytes, same as a slice header on x86.
|
||||
type entry struct {
|
||||
addr [16]byte
|
||||
mac [6]byte
|
||||
age uint8
|
||||
flags eflags
|
||||
}
|
||||
|
||||
func (e *entry) use(mac [6]byte, proto []byte, flags eflags) {
|
||||
e.flags = eflagInUse | flags
|
||||
if copy(e.addr[:], proto) == 16 {
|
||||
e.flags |= eflagIPv6
|
||||
}
|
||||
e.age = 0
|
||||
e.mac = mac
|
||||
}
|
||||
|
||||
func (e *entry) destroy() { *e = entry{} }
|
||||
|
||||
func (e *entry) put(frame, ourAddr []byte, ourMAC [6]byte, op Operation) (int, error) {
|
||||
if len(ourMAC) != 6 {
|
||||
return 0, lneto.ErrInvalidAddr
|
||||
}
|
||||
f, err := NewFrame(frame)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
f.SetHardware(1, 6)
|
||||
f.SetOperation(op)
|
||||
var n int
|
||||
if e.flags&eflagIPv6 != 0 {
|
||||
if len(ourAddr) != 16 {
|
||||
return 0, lneto.ErrInvalidAddr
|
||||
} else if len(frame) < sizeHeaderv6 {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
f.SetProtocol(ethernet.TypeIPv6, 16)
|
||||
hw, addr := f.Sender16()
|
||||
*hw = ourMAC
|
||||
copy(addr[:], ourAddr)
|
||||
hw, addr = f.Target16()
|
||||
copy(hw[:], e.mac[:])
|
||||
copy(addr[:], e.addr[:])
|
||||
n = sizeHeaderv6
|
||||
} else {
|
||||
if len(ourAddr) != 4 {
|
||||
return 0, lneto.ErrInvalidAddr
|
||||
}
|
||||
f.SetProtocol(ethernet.TypeIPv4, 4)
|
||||
hw, addr := f.Sender4()
|
||||
*hw = ourMAC
|
||||
copy(addr[:], ourAddr)
|
||||
hw, addr = f.Target4()
|
||||
copy(hw[:], e.mac[:])
|
||||
copy(addr[:], e.addr[:])
|
||||
n = sizeHeaderv4
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (flags eflags) hasAny(bits eflags) bool { return flags&bits != 0 }
|
||||
|
||||
type eflags uint8
|
||||
|
||||
// unset eflagInUse to signal the entry can be acquired for a new query.
|
||||
const (
|
||||
// eflagInUse set when in use. Discarded/unused entries have this bit unset.
|
||||
eflagInUse eflags = 1 << iota
|
||||
// set for IPv6 addressed entries.
|
||||
eflagIPv6
|
||||
// network device queried our address and we must respond to it.
|
||||
// Both MAC and IP are valid in this case.
|
||||
eflagPendingResponse
|
||||
// user asked to query this address and query has yet to be answered. May or may not be sent.
|
||||
eflagIncomplete
|
||||
// user asked to query this address and the query has not been sent out yet.
|
||||
// The MAC address is invalid in this case.
|
||||
eflagIncompletePendingQuery
|
||||
// eflagPriority set for prioritized cache entries. These entries are discarded last.
|
||||
// i.e: set for user created queries, unset for external incoming network queries.
|
||||
eflagPriority
|
||||
// trigger callback, you know the drill.
|
||||
eflagResolveTriggersCallback
|
||||
)
|
||||
|
||||
func (c *cache) age() {
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&eflagInUse != 0 && c.entries[i].age < 255 {
|
||||
c.entries[i].age++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) reset(size int) {
|
||||
internal.SliceReuse(&c.entries, size)
|
||||
c.entries = c.entries[:cap(c.entries)] // maximize queries given allocation.
|
||||
}
|
||||
|
||||
func (c *cache) getNextFlagged(entryHasFlags eflags) *entry {
|
||||
for i := range c.entries {
|
||||
flags := c.entries[i].flags
|
||||
if flags&eflagInUse != 0 && flags.hasAny(entryHasFlags) {
|
||||
return &c.entries[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cache) clearFlags(entryHasFlags, clrTheseFlagsIfMatch eflags) {
|
||||
for i := range c.entries {
|
||||
// Can clear flags on unused too, simpler.
|
||||
if c.entries[i].flags&entryHasFlags != 0 {
|
||||
c.entries[i].flags &^= clrTheseFlagsIfMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) Lookup(addr []byte) *entry {
|
||||
n := len(addr)
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&eflagInUse != 0 && internal.BytesEqual(c.entries[i].addr[:n], addr) {
|
||||
return &c.entries[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// acquireNext gets next available entry for use. If all are in use evicts
|
||||
// the oldest passive entry (learned from incoming requests) before touching
|
||||
// active user queries or pending responses.
|
||||
func (c *cache) acquireNext() *entry {
|
||||
const priorityFlags = eflagPendingResponse | eflagIncomplete | eflagPriority
|
||||
oldest, oldestPassive := 0, -1
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&eflagInUse == 0 {
|
||||
oldest = i
|
||||
break
|
||||
}
|
||||
if !c.entries[i].flags.hasAny(priorityFlags) {
|
||||
if oldestPassive < 0 || c.entries[i].age > c.entries[oldestPassive].age {
|
||||
oldestPassive = i
|
||||
}
|
||||
}
|
||||
if c.entries[i].age > c.entries[oldest].age {
|
||||
oldest = i
|
||||
}
|
||||
}
|
||||
if oldestPassive >= 0 && c.entries[oldest].flags&eflagInUse != 0 {
|
||||
oldest = oldestPassive
|
||||
}
|
||||
c.age()
|
||||
e := &c.entries[oldest]
|
||||
e.destroy()
|
||||
return e
|
||||
}
|
||||
+109
-180
@@ -1,21 +1,21 @@
|
||||
package arp
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
connID uint64
|
||||
ourHWAddr []byte
|
||||
ourProtoAddr []byte
|
||||
htype uint16
|
||||
protoType ethernet.Type
|
||||
pendingResponse [][sizeHeaderv6]byte
|
||||
queries []queryResult
|
||||
connID uint64
|
||||
cache cache
|
||||
vld lneto.Validator
|
||||
ourProtoAddr []byte
|
||||
onresolve func(hw, proto []byte)
|
||||
|
||||
htype uint16
|
||||
protoType ethernet.Type
|
||||
ourHWAddr [6]byte
|
||||
}
|
||||
|
||||
type HandlerConfig struct {
|
||||
@@ -41,6 +41,10 @@ func (h *Handler) UpdateProtoAddr(protoAddr []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) SetOnResolveCallback(cb func(hwAddr, protoAddr []byte)) {
|
||||
h.onresolve = cb
|
||||
}
|
||||
|
||||
func (h *Handler) Reset(cfg HandlerConfig) error {
|
||||
if len(cfg.HardwareAddr) == 0 || len(cfg.HardwareAddr) > 255 ||
|
||||
len(cfg.ProtocolAddr) == 0 || len(cfg.ProtocolAddr) > 255 {
|
||||
@@ -48,197 +52,120 @@ func (h *Handler) Reset(cfg HandlerConfig) error {
|
||||
} else if cfg.MaxQueries <= 0 || cfg.MaxPending <= 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
if cfg.HardwareType != 1 || cfg.ProtocolType != ethernet.TypeIPv4 && cfg.ProtocolType != ethernet.TypeIPv6 {
|
||||
return lneto.ErrUnsupported // We only support common for now.
|
||||
}
|
||||
*h = Handler{
|
||||
connID: h.connID + 1,
|
||||
ourHWAddr: h.ourHWAddr[:0],
|
||||
ourProtoAddr: h.ourProtoAddr[:0],
|
||||
htype: cfg.HardwareType,
|
||||
protoType: cfg.ProtocolType,
|
||||
pendingResponse: h.pendingResponse[:0],
|
||||
queries: h.queries[:0],
|
||||
connID: h.connID + 1,
|
||||
ourHWAddr: h.ourHWAddr,
|
||||
ourProtoAddr: h.ourProtoAddr[:0],
|
||||
htype: cfg.HardwareType,
|
||||
protoType: cfg.ProtocolType,
|
||||
cache: h.cache,
|
||||
}
|
||||
h.ourHWAddr = append(h.ourHWAddr, cfg.HardwareAddr...)
|
||||
h.cache.reset(cfg.MaxPending + cfg.MaxQueries)
|
||||
|
||||
h.ourHWAddr = [6]byte(cfg.HardwareAddr)
|
||||
h.ourProtoAddr = append(h.ourProtoAddr, cfg.ProtocolAddr...)
|
||||
if cap(h.pendingResponse) < cfg.MaxPending {
|
||||
h.pendingResponse = make([][52]byte, cfg.MaxPending)[:0]
|
||||
}
|
||||
if cap(h.queries) < cfg.MaxQueries {
|
||||
h.queries = make([]queryResult, cfg.MaxQueries)[:0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type queryResult struct {
|
||||
protoaddr []byte
|
||||
hwaddr []byte
|
||||
dstHw []byte
|
||||
querysent bool
|
||||
// inc tracks amount of times the query survived compaction.
|
||||
// The queries with higher inc will be discarded first.
|
||||
inc uint16
|
||||
}
|
||||
|
||||
func (qr *queryResult) destroy() {
|
||||
*qr = queryResult{protoaddr: qr.protoaddr[:0], hwaddr: qr.hwaddr[:0]}
|
||||
}
|
||||
|
||||
func (qr *queryResult) response() []byte {
|
||||
if len(qr.hwaddr) == 0 {
|
||||
return nil
|
||||
}
|
||||
return qr.hwaddr[:]
|
||||
}
|
||||
func (qr *queryResult) isInvalid() bool { return len(qr.protoaddr) == 0 }
|
||||
|
||||
// AbortPending drops pending queries and incoming requests.
|
||||
func (h *Handler) AbortPending() {
|
||||
h.pendingResponse = h.pendingResponse[:0]
|
||||
h.queries = h.queries[:0]
|
||||
h.cache.clearFlags(eflagPendingResponse|eflagIncomplete, eflagInUse)
|
||||
}
|
||||
|
||||
func (h *Handler) expectSize() int {
|
||||
return sizeHeader + 2*len(h.ourHWAddr) + 2*len(h.ourProtoAddr)
|
||||
// CacheSeed pre-populates the cache with a known proto→hardware mapping, making it
|
||||
// immediately resolvable via [Handler.CacheLookup] without an ARP exchange.
|
||||
// Seeded entries are evicted before active user queries when the cache is full.
|
||||
func (h *Handler) CacheSeed(protoAddr, hwAddr []byte) error {
|
||||
if len(hwAddr) != 6 {
|
||||
return lneto.ErrUnsupported
|
||||
}
|
||||
e := h.cache.acquireNext()
|
||||
e.use([6]byte(hwAddr), protoAddr, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) QueryResult(protoAddr []byte) (hwAddr []byte, err error) {
|
||||
for i := range h.queries {
|
||||
if internal.BytesEqual(protoAddr, h.queries[i].protoaddr) {
|
||||
if !h.queries[i].querysent {
|
||||
return nil, errQueryPending
|
||||
}
|
||||
mac := h.queries[i].response()
|
||||
if mac == nil {
|
||||
return nil, errQueryPending
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
// CacheLookup returns the hardware address for protoAddr if it is resolved in the cache.
|
||||
// Returns [errQueryPending] if a query is in flight, [errQueryNotFound] if no entry exists.
|
||||
func (h *Handler) CacheLookup(protoAddr []byte) (hwAddr []byte, err error) {
|
||||
e := h.cache.Lookup(protoAddr)
|
||||
if e == nil {
|
||||
return nil, errQueryNotFound
|
||||
} else if e.flags.hasAny(eflagIncomplete) {
|
||||
return nil, errQueryPending
|
||||
}
|
||||
return nil, errQueryNotFound
|
||||
return e.mac[:], nil
|
||||
}
|
||||
|
||||
func (h *Handler) DiscardQuery(protoAddr []byte) error {
|
||||
for i := range h.queries {
|
||||
q := &h.queries[i]
|
||||
if internal.BytesEqual(protoAddr, q.protoaddr) {
|
||||
q.destroy()
|
||||
return nil
|
||||
}
|
||||
// CacheRemove cancels a pending query or evicts a cached entry for protoAddr.
|
||||
func (h *Handler) CacheRemove(protoAddr []byte) error {
|
||||
e := h.cache.Lookup(protoAddr)
|
||||
if e == nil {
|
||||
return errQueryNotFound
|
||||
}
|
||||
return errQueryNotFound
|
||||
}
|
||||
|
||||
func (h *Handler) compactQueries() {
|
||||
validOff := 0
|
||||
maxIdx := -1
|
||||
maxInc := uint16(0)
|
||||
for i := 0; i < len(h.queries); i++ {
|
||||
discard := h.queries[i].isInvalid()
|
||||
if !discard {
|
||||
h.queries[i].inc++
|
||||
if h.queries[i].inc > maxInc {
|
||||
maxInc = h.queries[i].inc
|
||||
maxIdx = i
|
||||
}
|
||||
if i != validOff {
|
||||
// We swap the queries here so that when `StartQuery` extends
|
||||
// queries slice, we don't have sharing of the internal structures.
|
||||
// An alternative would be to zero things, however that would incur
|
||||
// an allocation cost.
|
||||
h.queries[validOff], h.queries[i] = h.queries[i], h.queries[validOff]
|
||||
}
|
||||
validOff++
|
||||
}
|
||||
}
|
||||
if validOff == len(h.queries) && maxIdx >= 0 {
|
||||
h.queries[maxIdx].destroy() // Destroy oldest query if unable to compact.
|
||||
}
|
||||
h.queries = h.queries[:validOff]
|
||||
e.destroy()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartQuery queues a query to perform over ARP for the protocol address `proto`.
|
||||
// The user can additionally specify an dstHWAddr to write query result to on completion.
|
||||
// If dstHWAddr is nil then query still occurs but no external buffer is written on query completion.
|
||||
// dstHWAddr must be zeroed out (invalid MAC).
|
||||
func (h *Handler) StartQuery(dstHWAddr, proto []byte) error {
|
||||
if len(h.queries) == cap(h.queries) {
|
||||
h.compactQueries()
|
||||
if len(h.queries) == cap(h.queries) {
|
||||
return lneto.ErrExhausted // Should never fail.
|
||||
}
|
||||
}
|
||||
// Use [Handler.SetOnResolveCallback] to asynchronously set an ARP request result.
|
||||
func (h *Handler) StartQuery(proto []byte, triggerCallback bool) error {
|
||||
if len(proto) != len(h.ourProtoAddr) {
|
||||
return lneto.ErrMismatchLen
|
||||
} else if dstHWAddr != nil && len(dstHWAddr) != len(h.ourHWAddr) {
|
||||
return lneto.ErrMismatchLen
|
||||
} else if dstHWAddr != nil && !internal.IsZeroed(dstHWAddr...) {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
q := internal.SliceReclaim(&h.queries)
|
||||
*q = queryResult{
|
||||
protoaddr: append(q.protoaddr[:0], proto...),
|
||||
hwaddr: q.hwaddr[:0],
|
||||
dstHw: dstHWAddr,
|
||||
e := h.cache.acquireNext()
|
||||
e.use([6]byte{}, proto, eflagIncomplete|eflagIncompletePendingQuery|eflagPriority)
|
||||
if triggerCallback {
|
||||
e.flags |= eflagResolveTriggersCallback
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||
func (h *Handler) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, error) {
|
||||
b := carrierData[offsetToFrame:]
|
||||
n := h.expectSize()
|
||||
if len(b) < n {
|
||||
return 0, errShortARP
|
||||
afrm, err := h.newframe(b)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(h.pendingResponse) > 0 {
|
||||
// pop frame.
|
||||
afrm, _ := NewFrame(h.pendingResponse[len(h.pendingResponse)-1][:])
|
||||
h.pendingResponse = h.pendingResponse[:len(h.pendingResponse)-1]
|
||||
afrm.SetOperation(OpReply)
|
||||
afrm.SwapTargetSender()
|
||||
hwsender, _ := afrm.Sender()
|
||||
copy(hwsender, h.ourHWAddr)
|
||||
n := copy(b, afrm.Clip().RawData())
|
||||
tgt, _ := afrm.Target()
|
||||
trySetEthernetDst(carrierData[:offsetToFrame], tgt)
|
||||
return n, nil
|
||||
op := OpReply
|
||||
e := h.cache.getNextFlagged(eflagPendingResponse) // Prioritize responses.
|
||||
if e == nil {
|
||||
e = h.cache.getNextFlagged(eflagIncompletePendingQuery)
|
||||
if e == nil {
|
||||
return 0, nil // No action to perform
|
||||
}
|
||||
e.flags &^= eflagIncompletePendingQuery
|
||||
op = OpRequest
|
||||
} else {
|
||||
e.flags &^= eflagPendingResponse
|
||||
}
|
||||
for i := range h.queries {
|
||||
if h.queries[i].isInvalid() || h.queries[i].querysent {
|
||||
continue
|
||||
}
|
||||
h.queries[i].querysent = true
|
||||
afrm, _ := NewFrame(b)
|
||||
afrm.SetHardware(h.htype, uint8(len(h.ourHWAddr)))
|
||||
afrm.SetProtocol(h.protoType, uint8(len(h.ourProtoAddr)))
|
||||
afrm.SetOperation(OpRequest)
|
||||
hwSender, protoSender := afrm.Sender()
|
||||
copy(hwSender, h.ourHWAddr)
|
||||
copy(protoSender, h.ourProtoAddr)
|
||||
hwTarget, protoTarget := afrm.Target()
|
||||
copy(protoTarget, h.queries[i].protoaddr)
|
||||
for j := range hwTarget {
|
||||
hwTarget[j] = 0
|
||||
}
|
||||
// Write Request or Reply, depending on which entry we got.
|
||||
n, err := e.put(b, h.ourProtoAddr, h.ourHWAddr, op)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch op {
|
||||
case OpRequest:
|
||||
broadcast := ethernet.BroadcastAddr()
|
||||
trySetEthernetDst(carrierData[:offsetToFrame], broadcast[:])
|
||||
return n, nil
|
||||
case OpReply:
|
||||
tgt, _ := afrm.Target()
|
||||
trySetEthernetDst(carrierData[:offsetToFrame], tgt)
|
||||
}
|
||||
return 0, nil
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (h *Handler) Demux(ethFrame []byte, frameOffset int) error {
|
||||
if len(h.pendingResponse) == cap(h.pendingResponse) {
|
||||
return lneto.ErrExhausted
|
||||
}
|
||||
|
||||
b := ethFrame[frameOffset:]
|
||||
afrm, err := NewFrame(b)
|
||||
afrm, err := h.newframe(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var vld lneto.Validator
|
||||
afrm.ValidateSize(&vld)
|
||||
if vld.HasError() {
|
||||
return vld.ErrPop()
|
||||
afrm.ValidateSize(&h.vld)
|
||||
if h.vld.HasError() {
|
||||
return h.vld.ErrPop()
|
||||
}
|
||||
htype, hlen := afrm.Hardware()
|
||||
if htype != h.htype || int(hlen) != len(h.ourHWAddr) {
|
||||
@@ -254,35 +181,37 @@ func (h *Handler) Demux(ethFrame []byte, frameOffset int) error {
|
||||
if !internal.BytesEqual(protoaddr, h.ourProtoAddr) {
|
||||
return nil // Not for us.
|
||||
}
|
||||
h.pendingResponse = h.pendingResponse[:len(h.pendingResponse)+1] // Extend pending buffer.
|
||||
copy(h.pendingResponse[len(h.pendingResponse)-1][:], afrm.buf) // Set pending buffer.
|
||||
hw, proto := afrm.Sender()
|
||||
e := h.cache.acquireNext()
|
||||
e.use([6]byte(hw), proto, eflagPendingResponse)
|
||||
|
||||
case OpReply:
|
||||
hwaddr, protoaddr := afrm.Sender()
|
||||
for i := range h.queries {
|
||||
q := &h.queries[i]
|
||||
mac := q.response()
|
||||
if mac == nil && internal.BytesEqual(q.protoaddr, protoaddr) {
|
||||
q.hwaddr = append(q.hwaddr, hwaddr...)
|
||||
if q.dstHw != nil {
|
||||
if !internal.IsZeroed(q.dstHw...) {
|
||||
internal.LogAttrs(nil, slog.LevelError, "race-condition:ARP-reused-buffer")
|
||||
}
|
||||
// External write to user buffer.
|
||||
// Copy data and free up this memory.
|
||||
copy(q.dstHw, hwaddr)
|
||||
q.inc = 10000
|
||||
}
|
||||
return nil
|
||||
}
|
||||
e := h.cache.Lookup(protoaddr)
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
copy(e.mac[:], hwaddr)
|
||||
e.flags &^= eflagIncomplete | eflagIncompletePendingQuery
|
||||
if e.flags.hasAny(eflagResolveTriggersCallback) && h.onresolve != nil {
|
||||
h.onresolve(e.mac[:], protoaddr)
|
||||
}
|
||||
|
||||
default:
|
||||
return errARPUnsupported
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) newframe(b []byte) (Frame, error) {
|
||||
f, err := NewFrame(b)
|
||||
if err != nil {
|
||||
return f, err
|
||||
} else if h.protoType == ethernet.TypeIPv6 && len(b) < sizeHeaderv6 {
|
||||
return f, lneto.ErrMismatch
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func trySetEthernetDst(ethFrame []byte, dst []byte) {
|
||||
if len(ethFrame) >= 14 {
|
||||
copy(ethFrame[:6], dst)
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package lneto
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Flag return values for a BackoffStrategy.
|
||||
const (
|
||||
BackoffFlagGosched = time.Duration(-1)
|
||||
BackoffFlagNop = time.Duration(-2)
|
||||
)
|
||||
|
||||
// BackoffStrategy is the abstraction of a backoff strategy for retrying an operation.
|
||||
// It returns the amount of time to sleep for or a flag value:
|
||||
// - Returns [BackoffFlagNop]: Signal no yielding function should be called.
|
||||
// Useful for when the BackoffStrategy implements its own yield.
|
||||
// - Returns [BackoffFlagGosched]: Signal [runtime.Gosched] should be called.
|
||||
//
|
||||
// Despite the name(changes welcome) consecutiveBackoffs starts at 0 and
|
||||
// increments by 1 every time the operation is retried.
|
||||
// See internal/backoff.go for a implementation example.
|
||||
type BackoffStrategy func(consecutiveBackoffs uint) (sleepOrFlag time.Duration)
|
||||
|
||||
// Do applies the backoff strategy by calling backoff(consecutiveBackoffs)
|
||||
// and then the corresponding yield function for the returned value. See [BackoffStrategy].
|
||||
func (backoff BackoffStrategy) Do(consecutiveBackoffs uint) {
|
||||
sleep := backoff(consecutiveBackoffs)
|
||||
switch sleep {
|
||||
case BackoffFlagNop:
|
||||
// No yield. Yield implemented by backoff.
|
||||
case BackoffFlagGosched:
|
||||
runtime.Gosched()
|
||||
default:
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -37,12 +37,12 @@ type StackNode interface {
|
||||
// SetFlagPending(flagPending func(numPendingEncapsulations int))
|
||||
}
|
||||
|
||||
//go:generate stringer -type=IPProto,errGeneric -linecomment -output stringers.go .
|
||||
|
||||
// IPProto represents the IP protocol number.
|
||||
type IPProto uint8
|
||||
|
||||
// IP protocol numbers.
|
||||
//
|
||||
//go:generate stringer -type=IPProto,errGeneric -linecomment -output stringers.go .
|
||||
const (
|
||||
IPProtoHopByHop IPProto = 0 // IPv6 Hop-by-Hop Option [RFC8200]
|
||||
IPProtoICMP IPProto = 1 // ICMP [RFC792]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package dhcp
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// Request is the decoded, allocation-relevant view of an inbound client message
|
||||
// passed to an [Allocator]. All slice fields alias the caller's receive buffer
|
||||
// and are only valid for the duration of the call; an implementation that needs
|
||||
// to persist them must copy.
|
||||
type Request struct {
|
||||
// ClientID identifies the client. For DHCPv4 this is the client identifier
|
||||
// option when present, otherwise the client hardware address. For DHCPv6 it
|
||||
// is the client DUID.
|
||||
ClientID []byte
|
||||
// Requested is the address the client asked for (DHCPv4 "Requested IP
|
||||
// Address" option), or the zero value when the client expressed no
|
||||
// preference.
|
||||
Requested netip.Addr
|
||||
// Subnet is the server's configured allocation prefix. It is the zero value
|
||||
// when the server does not constrain allocation to a prefix.
|
||||
Subnet netip.Prefix
|
||||
// Hostname is the client-supplied hostname, or nil.
|
||||
Hostname []byte
|
||||
// ParamReqList is the client's parameter request list, or nil.
|
||||
ParamReqList []byte
|
||||
}
|
||||
|
||||
// Allocator owns a DHCP server's lease database: the address pool, the
|
||||
// persisted client-to-address bindings, and the lease lifetime / expiration
|
||||
// policy. A server delegates address assignment to an Allocator so that it only
|
||||
// has to drive the protocol state machine.
|
||||
//
|
||||
// Implementations that expire leases must obtain time from a caller-injected
|
||||
// clock rather than calling time.Now directly, in keeping with lneto's
|
||||
// time-independent design.
|
||||
//
|
||||
// Offer and Commit model the two phases of acquisition: Offer makes a tentative
|
||||
// reservation in response to a DHCPv4 DISCOVER (or DHCPv6 SOLICIT), and Commit
|
||||
// binds it in response to a REQUEST. Implementations may treat Offer
|
||||
// idempotently so that a repeated DISCOVER for the same client returns the same
|
||||
// reservation.
|
||||
type Allocator interface {
|
||||
// Offer tentatively reserves a binding for the requesting client and
|
||||
// returns it. It is called when the server receives a DISCOVER/SOLICIT.
|
||||
Offer(Request) (Binding, error)
|
||||
// Commit binds a previously offered reservation, finalizing the lease, and
|
||||
// returns the committed binding. It is called when the server receives a
|
||||
// REQUEST.
|
||||
Commit(Request) (Binding, error)
|
||||
// Release frees the binding held for the given client identity. It is
|
||||
// called when the server receives a RELEASE. Releasing an unknown client is
|
||||
// not an error.
|
||||
Release(clientID []byte) error
|
||||
// Decline marks the addresses in the request as unusable, typically because
|
||||
// the client detected an address conflict. It is called when the server
|
||||
// receives a DECLINE.
|
||||
Decline(Request) error
|
||||
|
||||
// AppendOptions lets the allocator customize the option bytes the server is
|
||||
// about to send. dst already contains the options the server derived from
|
||||
// its own configuration (server identifier, router, subnet mask, DNS, lease
|
||||
// times, ...) for the given client and binding. The implementation may
|
||||
// append further options, or rewrite the existing ones, and must return the
|
||||
// resulting slice. The returned slice must not exceed dst's capacity.
|
||||
AppendOptions(dst []byte, clientID []byte, b Binding) ([]byte, error)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/dhcp"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// MapAllocator is the built-in [dhcp.Allocator] used by [Server] when no
|
||||
// allocator is supplied. It keeps the lease database in a map and allocates
|
||||
// addresses sequentially from the configured subnet, preferring a client's
|
||||
// requested address when it is free and in-subnet.
|
||||
//
|
||||
// When configured with a clock (see [AllocatorConfig.Now]) it reclaims expired
|
||||
// leases back into the pool; with no clock leases never expire, matching the
|
||||
// behaviour of a server that does not track time.
|
||||
//
|
||||
// The map makes MapAllocator unsuitable for the most memory-constrained
|
||||
// targets; such deployments should provide their own [dhcp.Allocator]. This
|
||||
// mirrors the existing allowance for the DHCP server to use a map on capable
|
||||
// hardware.
|
||||
type MapAllocator struct {
|
||||
subnet ipv4.Prefix
|
||||
serverAddr [4]byte
|
||||
nextAddr [4]byte
|
||||
leaseSeconds uint32
|
||||
now func() time.Time
|
||||
leases map[[36]byte]mapLease
|
||||
declined map[[4]byte]struct{}
|
||||
}
|
||||
|
||||
type mapLease struct {
|
||||
addr [4]byte
|
||||
expiry time.Time // zero means no expiry (no clock configured).
|
||||
committed bool
|
||||
}
|
||||
|
||||
// AllocatorConfig configures a [MapAllocator].
|
||||
type AllocatorConfig struct {
|
||||
// ServerAddr is the server's own address, excluded from the pool.
|
||||
ServerAddr [4]byte
|
||||
// Subnet is the allocation prefix.
|
||||
Subnet ipv4.Prefix
|
||||
// LeaseSeconds is the lease duration handed to clients. Zero defaults to 3600.
|
||||
LeaseSeconds uint32
|
||||
// Now, when non-nil, is used to expire and reclaim leases. When nil leases
|
||||
// never expire.
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// NewMapAllocator returns a configured [MapAllocator].
|
||||
func NewMapAllocator(cfg AllocatorConfig) (*MapAllocator, error) {
|
||||
if !cfg.Subnet.IsValid() {
|
||||
return nil, errors.New("dhcpv4 allocator: invalid subnet")
|
||||
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
|
||||
return nil, errors.New("dhcpv4 allocator: server address outside subnet")
|
||||
}
|
||||
lease := cfg.LeaseSeconds
|
||||
if lease == 0 {
|
||||
lease = 3600
|
||||
}
|
||||
return &MapAllocator{
|
||||
subnet: cfg.Subnet,
|
||||
serverAddr: cfg.ServerAddr,
|
||||
nextAddr: cfg.Subnet.Next(cfg.ServerAddr),
|
||||
leaseSeconds: lease,
|
||||
now: cfg.Now,
|
||||
leases: make(map[[36]byte]mapLease),
|
||||
declined: make(map[[4]byte]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ dhcp.Allocator = (*MapAllocator)(nil)
|
||||
|
||||
// Offer implements [dhcp.Allocator]. It returns the binding already reserved for
|
||||
// the client if one exists, otherwise it reserves a new address.
|
||||
func (a *MapAllocator) Offer(req dhcp.Request) (dhcp.Binding, error) {
|
||||
a.sweepExpired()
|
||||
key, ok := keyOf(req.ClientID)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: client id too long")
|
||||
}
|
||||
if existing, ok := a.leases[key]; ok {
|
||||
existing.expiry = a.expiry()
|
||||
a.leases[key] = existing
|
||||
return a.binding(existing.addr), nil
|
||||
}
|
||||
addr, ok := a.allocAddr(req.Requested)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: address pool exhausted")
|
||||
}
|
||||
a.leases[key] = mapLease{addr: addr, expiry: a.expiry()}
|
||||
return a.binding(addr), nil
|
||||
}
|
||||
|
||||
// Commit implements [dhcp.Allocator], finalizing the client's lease.
|
||||
func (a *MapAllocator) Commit(req dhcp.Request) (dhcp.Binding, error) {
|
||||
a.sweepExpired()
|
||||
key, ok := keyOf(req.ClientID)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: client id too long")
|
||||
}
|
||||
lease, ok := a.leases[key]
|
||||
if !ok {
|
||||
// Reservation expired or never made; allocate afresh.
|
||||
addr, ok := a.allocAddr(req.Requested)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: address pool exhausted")
|
||||
}
|
||||
lease = mapLease{addr: addr}
|
||||
}
|
||||
lease.committed = true
|
||||
lease.expiry = a.expiry()
|
||||
a.leases[key] = lease
|
||||
return a.binding(lease.addr), nil
|
||||
}
|
||||
|
||||
// Release implements [dhcp.Allocator], freeing the client's lease.
|
||||
func (a *MapAllocator) Release(clientID []byte) error {
|
||||
key, ok := keyOf(clientID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
delete(a.leases, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decline implements [dhcp.Allocator], marking the client's address unusable.
|
||||
func (a *MapAllocator) Decline(req dhcp.Request) error {
|
||||
key, ok := keyOf(req.ClientID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if lease, ok := a.leases[key]; ok {
|
||||
a.declined[lease.addr] = struct{}{}
|
||||
delete(a.leases, key)
|
||||
} else if req.Requested.Is4() {
|
||||
a.declined[req.Requested.As4()] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppendOptions implements [dhcp.Allocator]. The server already wrote the
|
||||
// configuration-derived options, so the default allocator leaves them as-is.
|
||||
func (a *MapAllocator) AppendOptions(dst []byte, _ []byte, _ dhcp.Binding) ([]byte, error) {
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func (a *MapAllocator) binding(addr [4]byte) dhcp.Binding {
|
||||
t1, t2 := dhcp.DefaultT1T2(a.leaseSeconds)
|
||||
return dhcp.Binding{
|
||||
T1: t1,
|
||||
T2: t2,
|
||||
Leases: []dhcp.Lease{{
|
||||
Addr: netip.AddrFrom4(addr),
|
||||
Preferred: a.leaseSeconds,
|
||||
Valid: a.leaseSeconds,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *MapAllocator) expiry() time.Time {
|
||||
if a.now == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return a.now().Add(time.Duration(a.leaseSeconds) * time.Second)
|
||||
}
|
||||
|
||||
func (a *MapAllocator) sweepExpired() {
|
||||
if a.now == nil {
|
||||
return
|
||||
}
|
||||
now := a.now()
|
||||
for k, v := range a.leases {
|
||||
if !v.expiry.IsZero() && now.After(v.expiry) {
|
||||
delete(a.leases, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allocAddr allocates the next available address from the pool, preferring a
|
||||
// valid in-subnet requested address that is free.
|
||||
func (a *MapAllocator) allocAddr(requested netip.Addr) ([4]byte, bool) {
|
||||
if requested.Is4() {
|
||||
candidate := requested.As4()
|
||||
if a.subnet.Contains(candidate) && candidate != a.serverAddr && !a.isAssigned(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
}
|
||||
addr := a.nextAddr
|
||||
a.nextAddr = a.subnet.Next(a.nextAddr)
|
||||
if a.nextAddr == a.serverAddr {
|
||||
a.nextAddr = a.subnet.Next(a.nextAddr)
|
||||
}
|
||||
// Reject the broadcast address (all host bits set).
|
||||
hostBits := uint(32 - a.subnet.Bits())
|
||||
hostMask := ^uint32(0) >> (32 - hostBits)
|
||||
if binary.BigEndian.Uint32(addr[:])&hostMask == hostMask {
|
||||
return [4]byte{}, false
|
||||
}
|
||||
return addr, true
|
||||
}
|
||||
|
||||
func (a *MapAllocator) isAssigned(addr [4]byte) bool {
|
||||
if _, ok := a.declined[addr]; ok {
|
||||
return true
|
||||
}
|
||||
for _, v := range a.leases {
|
||||
if v.addr == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func keyOf(clientID []byte) ([36]byte, bool) {
|
||||
var key [36]byte
|
||||
if len(clientID) > len(key) {
|
||||
return key, false
|
||||
}
|
||||
copy(key[:], clientID)
|
||||
return key, true
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/dhcp"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// recordingAllocator is a test [dhcp.Allocator] that hands out a fixed address,
|
||||
// records whether the server-derived options reached its AppendOptions hook, and
|
||||
// appends a marker option of its own.
|
||||
type recordingAllocator struct {
|
||||
addr netip.Addr
|
||||
sawRouter bool
|
||||
appended bool
|
||||
}
|
||||
|
||||
func (a *recordingAllocator) binding() dhcp.Binding {
|
||||
return dhcp.Binding{
|
||||
T1: 30,
|
||||
T2: 52,
|
||||
Leases: []dhcp.Lease{{Addr: a.addr, Preferred: 60, Valid: 60}},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *recordingAllocator) Offer(dhcp.Request) (dhcp.Binding, error) { return a.binding(), nil }
|
||||
func (a *recordingAllocator) Commit(dhcp.Request) (dhcp.Binding, error) { return a.binding(), nil }
|
||||
func (a *recordingAllocator) Release([]byte) error { return nil }
|
||||
func (a *recordingAllocator) Decline(dhcp.Request) error { return nil }
|
||||
|
||||
func (a *recordingAllocator) AppendOptions(dst []byte, _ []byte, _ dhcp.Binding) ([]byte, error) {
|
||||
// dst already contains the server-derived options. Confirm the router
|
||||
// option the server was configured to send is present.
|
||||
for i := 0; i+1 < len(dst); {
|
||||
op := OptNum(dst[i])
|
||||
if op == OptEnd {
|
||||
break
|
||||
}
|
||||
if op == OptRouter {
|
||||
a.sawRouter = true
|
||||
}
|
||||
i += 2 + int(dst[i+1])
|
||||
}
|
||||
// Append our own option into the remaining capacity.
|
||||
tail := dst[len(dst):cap(dst)]
|
||||
n, err := EncodeOptionString(tail, OptDomainName, "example")
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
a.appended = true
|
||||
return dst[:len(dst)+n], nil
|
||||
}
|
||||
|
||||
// TestServerCustomAllocator verifies the server delegates address assignment to
|
||||
// an injected allocator, that the allocator's AppendOptions hook sees the
|
||||
// server-derived options, and that options it appends reach the client.
|
||||
func TestServerCustomAllocator(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 1, 1}
|
||||
want := netip.AddrFrom4([4]byte{10, 9, 8, 7})
|
||||
alloc := &recordingAllocator{addr: want}
|
||||
|
||||
var sv Server
|
||||
err := sv.Configure(ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Gateway: [4]byte{192, 168, 1, 254},
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
Allocator: alloc,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(42, RequestConfig{ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf [1024]byte
|
||||
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n == 0 {
|
||||
t.Fatal("no offer emitted")
|
||||
}
|
||||
|
||||
frm, _ := NewFrame(buf[:n])
|
||||
if got := *frm.YIAddr(); got != want.As4() {
|
||||
t.Errorf("offered address: got %v want %v", got, want)
|
||||
}
|
||||
if !alloc.sawRouter {
|
||||
t.Error("allocator AppendOptions did not see the server-derived router option")
|
||||
}
|
||||
var foundDomain bool
|
||||
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
if opt == OptDomainName && string(data) == "example" {
|
||||
foundDomain = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if !foundDomain {
|
||||
t.Error("allocator-appended domain option did not reach the emitted frame")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapAllocatorExpiration verifies that the default allocator reclaims an
|
||||
// expired lease back into the pool once its valid lifetime has elapsed.
|
||||
func TestMapAllocatorExpiration(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 1, 1}
|
||||
now := time.Unix(1_000_000, 0)
|
||||
clock := func() time.Time { return now }
|
||||
|
||||
a, err := NewMapAllocator(AllocatorConfig{
|
||||
ServerAddr: svAddr,
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
LeaseSeconds: 10,
|
||||
Now: clock,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Client 1 acquires and commits an address.
|
||||
b1, err := a.Offer(dhcp.Request{ClientID: []byte{1}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr1, _ := b1.Addr()
|
||||
if _, err := a.Commit(dhcp.Request{ClientID: []byte{1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// While client 1 holds the lease, another client requesting the same
|
||||
// address must not be granted it.
|
||||
b2, err := a.Offer(dhcp.Request{ClientID: []byte{2}, Requested: addr1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if addr2, _ := b2.Addr(); addr2 == addr1 {
|
||||
t.Fatalf("addr %v handed out while still leased to client 1", addr1)
|
||||
}
|
||||
if err := a.Release([]byte{2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Advance time past client 1's lease; its address must be reclaimable.
|
||||
now = now.Add(20 * time.Second)
|
||||
b3, err := a.Offer(dhcp.Request{ClientID: []byte{3}, Requested: addr1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if addr3, _ := b3.Addr(); addr3 != addr1 {
|
||||
t.Errorf("expired address not reclaimed: got %v want %v", addr3, addr1)
|
||||
}
|
||||
}
|
||||
@@ -153,10 +153,7 @@ func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
||||
numOpts += n
|
||||
n, _ = EncodeOption(opts[numOpts:], OptParameterRequestList, defaultParamReqList...)
|
||||
numOpts += n
|
||||
maxlen := len(dst)
|
||||
if maxlen > math.MaxUint16 {
|
||||
maxlen = math.MaxUint16
|
||||
}
|
||||
maxlen := min(len(dst), math.MaxUint16)
|
||||
n, _ = EncodeOption16(opts[numOpts:], OptMaximumMessageSize, uint16(maxlen))
|
||||
numOpts += n
|
||||
if c.reqIP.valid {
|
||||
@@ -268,11 +265,11 @@ func (c *Client) getMessageType(frm Frame) MessageType {
|
||||
func (c *Client) setOptions(frm Frame) error {
|
||||
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
switch opt {
|
||||
case OptRenewTimeValue:
|
||||
case OptT1Renewal:
|
||||
c.tRenew = maybeU32(data)
|
||||
case OptIPAddressLeaseTime:
|
||||
c.tIPLease = maybeU32(data)
|
||||
case OptRebindingTimeValue:
|
||||
case OptT2Rebinding:
|
||||
c.tRebind = maybeU32(data)
|
||||
case OptServerIdentification:
|
||||
c.svip.setmaybe(data)
|
||||
@@ -369,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 {
|
||||
@@ -128,8 +128,8 @@ const (
|
||||
OptParameterRequestList OptNum = 55 // Parameter request list
|
||||
OptMessage OptNum = 56 // DHCP error message
|
||||
OptMaximumMessageSize OptNum = 57 // DHCP maximum message size
|
||||
OptRenewTimeValue OptNum = 58 // DHCP renewal (T1) time
|
||||
OptRebindingTimeValue OptNum = 59 // DHCP rebinding (T2) time
|
||||
OptT1Renewal OptNum = 58 // DHCP renewal (T1) time
|
||||
OptT2Rebinding OptNum = 59 // DHCP rebinding (T2) time
|
||||
OptClientIdentifier OptNum = 60 // Client identifier
|
||||
OptClientIdentifier1 OptNum = 61 // Client identifier(1)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,447 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/dhcp"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
var errOptionNotFit = errors.New("DHCPv4: options dont fit")
|
||||
|
||||
const defaultMaxPending = 8
|
||||
|
||||
// Server implements the DHCPv4 server state machine (RFC 2131). It drives the
|
||||
// DISCOVER->OFFER->REQUEST->ACK exchange and delegates address assignment,
|
||||
// lease lifetime and per-client option customization to a [dhcp.Allocator].
|
||||
//
|
||||
// The Server itself holds no lease database: it keeps only a bounded table of
|
||||
// in-flight transactions. The persistent client-to-address bindings live in the
|
||||
// allocator, which may be supplied through [ServerConfig.Allocator] or, when
|
||||
// omitted, defaults to a [MapAllocator] built from the configuration.
|
||||
type Server struct {
|
||||
connID uint64
|
||||
subnet ipv4.Prefix
|
||||
alloc dhcp.Allocator
|
||||
txns []txn
|
||||
vld lneto.Validator
|
||||
maxPending int
|
||||
port uint16
|
||||
siaddr [4]byte
|
||||
gwaddr [4]byte
|
||||
dns [4]byte
|
||||
}
|
||||
|
||||
// ServerConfig contains configuration parameters for [Server.Configure].
|
||||
type ServerConfig struct {
|
||||
// ServerAddr is the DHCP server's own IPv4 address.
|
||||
ServerAddr [4]byte
|
||||
// Gateway advertised to clients as default router. Zero value omits the option.
|
||||
Gateway [4]byte
|
||||
// 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 ipv4.Prefix
|
||||
// LeaseSeconds is the lease duration. Zero defaults to 3600. Only used when
|
||||
// building the default allocator (Allocator is nil).
|
||||
LeaseSeconds uint32
|
||||
// Port is the server listening port. Zero defaults to DefaultServerPort.
|
||||
Port uint16
|
||||
// Allocator delegates address assignment and lease management. When nil a
|
||||
// [MapAllocator] is built from ServerAddr, Subnet, LeaseSeconds and Now.
|
||||
Allocator dhcp.Allocator
|
||||
// MaxPending bounds the number of simultaneous in-flight transactions the
|
||||
// server tracks. Zero defaults to a small built-in value.
|
||||
MaxPending int
|
||||
// Now, when non-nil, is passed to the default allocator to enable lease
|
||||
// expiration. Ignored when Allocator is non-nil.
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// txn is an in-flight DORA transaction awaiting a response or a follow-up
|
||||
// request. The committed lease lives in the allocator; txn only carries what
|
||||
// the server needs to emit OFFER/ACK frames.
|
||||
type txn struct {
|
||||
binding dhcp.Binding
|
||||
clientID [36]byte
|
||||
xid uint32
|
||||
port uint16
|
||||
offered [4]byte
|
||||
hwaddr [6]byte
|
||||
clientLen uint8
|
||||
state ClientState // StateSelecting (offer pending/sent) or StateRequesting (ack pending).
|
||||
respond MessageType // MsgOffer, MsgAck, or 0 when nothing is queued to send.
|
||||
}
|
||||
|
||||
// Configure resets and configures the server with the given configuration.
|
||||
// The connection ID is incremented on each call to invalidate existing connections.
|
||||
func (sv *Server) Configure(cfg ServerConfig) error {
|
||||
alloc := cfg.Allocator
|
||||
if alloc == nil {
|
||||
if !cfg.Subnet.IsValid() {
|
||||
return errors.New("dhcpv4 server: invalid subnet")
|
||||
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
|
||||
return errors.New("dhcpv4 server: server address outside subnet")
|
||||
}
|
||||
a, err := NewMapAllocator(AllocatorConfig{
|
||||
ServerAddr: cfg.ServerAddr,
|
||||
Subnet: cfg.Subnet,
|
||||
LeaseSeconds: cfg.LeaseSeconds,
|
||||
Now: cfg.Now,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alloc = a
|
||||
}
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = DefaultServerPort
|
||||
}
|
||||
maxPending := cfg.MaxPending
|
||||
if maxPending <= 0 {
|
||||
maxPending = defaultMaxPending
|
||||
}
|
||||
txns := sv.txns
|
||||
if cap(txns) < maxPending {
|
||||
txns = make([]txn, 0, maxPending)
|
||||
} else {
|
||||
txns = txns[:0]
|
||||
}
|
||||
*sv = Server{
|
||||
connID: sv.connID + 1,
|
||||
siaddr: cfg.ServerAddr,
|
||||
gwaddr: cfg.Gateway,
|
||||
dns: cfg.DNS,
|
||||
subnet: cfg.Subnet,
|
||||
port: port,
|
||||
alloc: alloc,
|
||||
txns: txns,
|
||||
maxPending: maxPending,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
|
||||
func (sv *Server) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
||||
func (sv *Server) LocalPort() uint16 { return sv.port }
|
||||
|
||||
func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
||||
isIPLayer := frameOffset >= 28
|
||||
dhcpData := carrierData[frameOffset:]
|
||||
dfrm, err := NewFrame(dhcpData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dfrm.ValidateSize(&sv.vld)
|
||||
if sv.vld.HasError() {
|
||||
return sv.vld.ErrPop()
|
||||
}
|
||||
|
||||
var msgType MessageType
|
||||
var clientID []byte
|
||||
var reqlist []byte
|
||||
var reqAddr []byte
|
||||
var hostname []byte
|
||||
err = dfrm.ForEachOption(func(off int, op OptNum, data []byte) error {
|
||||
switch op {
|
||||
case OptMessageType:
|
||||
if len(data) == 1 {
|
||||
msgType = MessageType(data[0])
|
||||
}
|
||||
case OptHostName:
|
||||
if len(data) <= 36 {
|
||||
hostname = data
|
||||
}
|
||||
case OptClientIdentifier:
|
||||
if len(data) <= 36 {
|
||||
clientID = data
|
||||
}
|
||||
case OptParameterRequestList:
|
||||
if len(data) > 36 {
|
||||
return errors.New("too many request options")
|
||||
}
|
||||
reqlist = data
|
||||
case OptRequestedIPaddress:
|
||||
if len(data) == 4 {
|
||||
reqAddr = data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve client identity: the client identifier option when present,
|
||||
// otherwise the hardware address.
|
||||
if len(clientID) == 0 {
|
||||
clientID = dfrm.CHAddrAs6()[:]
|
||||
}
|
||||
|
||||
req := dhcp.Request{
|
||||
ClientID: clientID,
|
||||
Hostname: hostname,
|
||||
ParamReqList: reqlist,
|
||||
}
|
||||
if sv.subnet.IsValid() {
|
||||
req.Subnet = sv.subnet.NetipPrefix()
|
||||
}
|
||||
if len(reqAddr) == 4 {
|
||||
req.Requested = netip.AddrFrom4([4]byte(reqAddr))
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case MsgDiscover:
|
||||
binding, err := sv.alloc.Offer(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server offer: %w", err)
|
||||
}
|
||||
offered, ok := binding.Addr()
|
||||
if !ok || !offered.Is4() {
|
||||
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||
}
|
||||
t := sv.upsertTxn(clientID)
|
||||
if t == nil {
|
||||
return errors.New("dhcpv4 server: too many pending transactions")
|
||||
}
|
||||
t.binding = binding
|
||||
t.offered = offered.As4()
|
||||
t.xid = dfrm.XID()
|
||||
t.hwaddr = *dfrm.CHAddrAs6()
|
||||
if isIPLayer {
|
||||
_, t.port, _ = getSrcIPPort(carrierData)
|
||||
}
|
||||
t.state = StateSelecting
|
||||
t.respond = MsgOffer
|
||||
|
||||
case MsgRequest:
|
||||
t := sv.findTxn(clientID)
|
||||
if t == nil {
|
||||
return errors.New("request for non existing client")
|
||||
} else if dfrm.XID() != t.xid {
|
||||
return errors.New("unexpected XID for client")
|
||||
} else if t.state != StateSelecting && t.state != StateRequesting {
|
||||
return errors.New("DHCP request unexpected state")
|
||||
}
|
||||
binding, err := sv.alloc.Commit(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server commit: %w", err)
|
||||
}
|
||||
offered, ok := binding.Addr()
|
||||
if !ok || !offered.Is4() {
|
||||
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||
}
|
||||
t.binding = binding
|
||||
t.offered = offered.As4()
|
||||
t.state = StateRequesting
|
||||
t.respond = MsgAck
|
||||
|
||||
case MsgRelease:
|
||||
err = sv.alloc.Release(clientID)
|
||||
sv.dropTxn(clientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server release: %w", err)
|
||||
}
|
||||
|
||||
case MsgDecline:
|
||||
err = sv.alloc.Decline(req)
|
||||
sv.dropTxn(clientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server decline: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unhandled message type %s", msgType.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||
carrierIsIP := offsetToIP >= 0
|
||||
dfrm, err := NewFrame(carrierData[offsetToFrame:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
optBuf := dfrm.OptionsPayload()
|
||||
if len(optBuf) < 255 {
|
||||
return 0, errOptionNotFit
|
||||
}
|
||||
|
||||
t := sv.nextPending()
|
||||
if t == nil {
|
||||
return 0, nil // No pending outgoing frames.
|
||||
}
|
||||
|
||||
var nopt int
|
||||
n, err := EncodeOption(optBuf[nopt:], OptMessageType, byte(t.respond))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nopt += n
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||
nopt += n
|
||||
if sv.gwaddr != [4]byte{} {
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if sv.subnet.IsValid() {
|
||||
bits := uint(sv.subnet.Bits())
|
||||
mask := ^uint32(0) << (32 - bits)
|
||||
var maskBuf [4]byte
|
||||
binary.BigEndian.PutUint32(maskBuf[:], mask)
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptSubnetMask, maskBuf[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if sv.dns != [4]byte{} {
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptDNSServers, sv.dns[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if lease, ok := leaseOf(t.binding); ok && lease.Valid > 0 {
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, lease.Valid)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptT1Renewal, t.binding.T1)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptT2Rebinding, t.binding.T2)
|
||||
nopt += n
|
||||
}
|
||||
|
||||
// Let the allocator append or rewrite options. dst already holds the
|
||||
// server-derived options for this client and binding.
|
||||
optBuf, err = sv.alloc.AppendOptions(optBuf[:nopt], t.clientID[:t.clientLen], t.binding)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nopt = len(optBuf)
|
||||
optBuf = dfrm.OptionsPayload()
|
||||
if nopt >= len(optBuf) {
|
||||
return 0, errOptionNotFit
|
||||
}
|
||||
optBuf[nopt] = byte(OptEnd)
|
||||
nopt++
|
||||
|
||||
dfrm.ClearHeader()
|
||||
dfrm.SetOp(OpReply)
|
||||
dfrm.SetHardware(1, 6, 0)
|
||||
dfrm.SetXID(t.xid)
|
||||
dfrm.SetSecs(0)
|
||||
dfrm.SetFlags(0)
|
||||
*dfrm.YIAddr() = t.offered
|
||||
if t.respond == MsgAck {
|
||||
*dfrm.CIAddr() = t.offered
|
||||
}
|
||||
*dfrm.SIAddr() = sv.siaddr
|
||||
*dfrm.GIAddr() = sv.gwaddr
|
||||
copy(dfrm.CHAddrAs6()[:], t.hwaddr[:])
|
||||
dfrm.SetMagicCookie(MagicCookie)
|
||||
if carrierIsIP {
|
||||
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], t.offered[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if t.respond == MsgAck {
|
||||
// Transaction complete; the lease now lives in the allocator.
|
||||
sv.dropTxnAt(t)
|
||||
} else {
|
||||
t.respond = 0 // Offer sent, await the client's REQUEST.
|
||||
}
|
||||
return OptionsOffset + nopt, nil
|
||||
}
|
||||
|
||||
// leaseOf returns the first lease of a binding.
|
||||
func leaseOf(b dhcp.Binding) (dhcp.Lease, bool) {
|
||||
if len(b.Leases) == 0 {
|
||||
return dhcp.Lease{}, false
|
||||
}
|
||||
return b.Leases[0], true
|
||||
}
|
||||
|
||||
// findTxn returns the in-flight transaction for clientID, or nil.
|
||||
func (sv *Server) findTxn(clientID []byte) *txn {
|
||||
for i := range sv.txns {
|
||||
if sv.txns[i].matches(clientID) {
|
||||
return &sv.txns[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// upsertTxn returns the existing transaction for clientID, reusing its slot, or
|
||||
// appends a new one. It returns nil if the table is full.
|
||||
func (sv *Server) upsertTxn(clientID []byte) *txn {
|
||||
if t := sv.findTxn(clientID); t != nil {
|
||||
t.binding = dhcp.Binding{}
|
||||
t.respond = 0
|
||||
return t
|
||||
}
|
||||
if len(sv.txns) >= sv.maxPending {
|
||||
return nil
|
||||
}
|
||||
var t txn
|
||||
t.clientLen = uint8(copy(t.clientID[:], clientID))
|
||||
sv.txns = append(sv.txns, t)
|
||||
return &sv.txns[len(sv.txns)-1]
|
||||
}
|
||||
|
||||
// nextPending returns the next transaction with a queued response, or nil.
|
||||
func (sv *Server) nextPending() *txn {
|
||||
for i := range sv.txns {
|
||||
if sv.txns[i].respond != 0 {
|
||||
return &sv.txns[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropTxn removes the transaction for clientID if present.
|
||||
func (sv *Server) dropTxn(clientID []byte) {
|
||||
for i := range sv.txns {
|
||||
if sv.txns[i].matches(clientID) {
|
||||
sv.removeAt(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropTxnAt removes the transaction pointed to by t.
|
||||
func (sv *Server) dropTxnAt(t *txn) {
|
||||
for i := range sv.txns {
|
||||
if &sv.txns[i] == t {
|
||||
sv.removeAt(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *Server) removeAt(i int) {
|
||||
last := len(sv.txns) - 1
|
||||
sv.txns[i] = sv.txns[last]
|
||||
sv.txns[last] = txn{}
|
||||
sv.txns = sv.txns[:last]
|
||||
}
|
||||
|
||||
func (t *txn) matches(clientID []byte) bool {
|
||||
if int(t.clientLen) != len(clientID) {
|
||||
return false
|
||||
}
|
||||
return string(t.clientID[:t.clientLen]) == string(clientID)
|
||||
}
|
||||
|
||||
func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
|
||||
srcaddr, _, _, off, err := internal.GetIPAddr(ipCarrier)
|
||||
if err != nil {
|
||||
return srcaddr, port, err
|
||||
} else if len(ipCarrier[off:]) < 2 {
|
||||
return srcaddr, port, errors.New("getSrcIPPort got only IP layer")
|
||||
}
|
||||
port = binary.BigEndian.Uint16(ipCarrier[off:]) // TCP and UDP share same port offsets.
|
||||
return srcaddr, port, nil
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +107,7 @@ func TestServerMultipleClients(t *testing.T) {
|
||||
}
|
||||
|
||||
// All assigned addresses must be unique.
|
||||
for i := 0; i < nClients; i++ {
|
||||
for i := range nClients {
|
||||
for j := i + 1; j < nClients; j++ {
|
||||
if assignedAddrs[i] == assignedAddrs[j] {
|
||||
t.Errorf("clients %d and %d got same address %v", i, j, assignedAddrs[i])
|
||||
@@ -123,7 +124,7 @@ func TestServerSequentialAddressAllocation(t *testing.T) {
|
||||
sv.Configure(testServerConfig(svAddr))
|
||||
|
||||
// Build raw DISCOVER frames for two clients.
|
||||
for i := byte(0); i < 2; i++ {
|
||||
for i := range byte(2) {
|
||||
var buf [512]byte
|
||||
frm, _ := NewFrame(buf[:])
|
||||
frm.ClearHeader()
|
||||
@@ -147,7 +148,7 @@ func TestServerSequentialAddressAllocation(t *testing.T) {
|
||||
|
||||
// Encapsulate both OFFERs and verify addresses are in expected range.
|
||||
var seen [2][4]byte
|
||||
for i := byte(0); i < 2; i++ {
|
||||
for i := range byte(2) {
|
||||
var buf [512]byte
|
||||
n, err := sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -238,9 +239,9 @@ func TestServerOfferContainsOptions(t *testing.T) {
|
||||
foundLease = true
|
||||
gotLease = maybeU32(data)
|
||||
}
|
||||
case OptRenewTimeValue:
|
||||
case OptT1Renewal:
|
||||
gotRenew = maybeU32(data)
|
||||
case OptRebindingTimeValue:
|
||||
case OptT2Rebinding:
|
||||
gotRebind = maybeU32(data)
|
||||
}
|
||||
return nil
|
||||
@@ -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")
|
||||
@@ -67,8 +67,8 @@ func _() {
|
||||
_ = x[OptParameterRequestList-55]
|
||||
_ = x[OptMessage-56]
|
||||
_ = x[OptMaximumMessageSize-57]
|
||||
_ = x[OptRenewTimeValue-58]
|
||||
_ = x[OptRebindingTimeValue-59]
|
||||
_ = x[OptT1Renewal-58]
|
||||
_ = x[OptT2Rebinding-59]
|
||||
_ = x[OptClientIdentifier-60]
|
||||
_ = x[OptClientIdentifier1-61]
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
// RequestConfig holds the parameters for starting a DHCPv6 exchange.
|
||||
type RequestConfig struct {
|
||||
// ClientHardwareAddr is the client's Ethernet MAC address.
|
||||
// It is used to construct the client DUID-LL and IAID.
|
||||
ClientHardwareAddr [6]byte
|
||||
}
|
||||
|
||||
// Client is a stateful DHCPv6 client implementing the [lneto.StackNode] interface.
|
||||
// It manages the Solicit→Advertise→Request→Reply exchange (RFC 8415 §18).
|
||||
//
|
||||
// Typical usage:
|
||||
//
|
||||
// var cl Client
|
||||
// cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: mac})
|
||||
// // drive Encapsulate / Demux calls via the network stack
|
||||
type Client struct {
|
||||
connID uint64
|
||||
state ClientState
|
||||
xid uint32 // lower 24 bits used
|
||||
|
||||
// duid is the client's DUID-LL. Client owns the backing array; it is set
|
||||
// once from the MAC in BeginRequest and carried across resets unchanged.
|
||||
duid []byte
|
||||
// serverDUID is the selected server's DUID. Client owns the backing array;
|
||||
// it is cleared (len=0) on reset so capacity is reused without allocation.
|
||||
serverDUID []byte
|
||||
|
||||
// dns accumulates DNS recursive name server addresses (OptDNSServers).
|
||||
// Client owns the backing array; cleared on reset, capacity reused.
|
||||
dns []netip.Addr
|
||||
|
||||
assignedAddr [16]byte
|
||||
assignedAddrValid bool
|
||||
|
||||
// iaid is derived from the first 4 bytes of the client MAC.
|
||||
iaid [4]byte
|
||||
|
||||
// IA_NA timers from the server's Advertise/Reply.
|
||||
t1, t2 uint32
|
||||
preferredLifetime uint32
|
||||
validLifetime uint32
|
||||
|
||||
clientMAC [6]byte
|
||||
|
||||
// auxbuf is a scratch buffer used during Encapsulate to avoid allocations.
|
||||
auxbuf [128]byte
|
||||
}
|
||||
|
||||
// BeginRequest initialises a new DHCPv6 exchange with the given 24-bit transaction ID.
|
||||
// It must be called before any Encapsulate or Demux calls.
|
||||
func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error {
|
||||
if xid == 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
} else if c.state != StateInit && c.state != 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
} else if internal.IsZeroed(cfg.ClientHardwareAddr[:]...) {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
c.clientMAC = cfg.ClientHardwareAddr
|
||||
c.iaid = [4]byte(cfg.ClientHardwareAddr[:4])
|
||||
c.xid = xid & 0xFFFFFF
|
||||
c.reset()
|
||||
c.duid = AppendDUIDLL(c.duid[:0], cfg.ClientHardwareAddr)
|
||||
c.state = StateInit
|
||||
return nil
|
||||
}
|
||||
|
||||
// reset clears exchange state while preserving slice backing arrays and the
|
||||
// connection ID is incremented to invalidate any existing stack registrations.
|
||||
func (c *Client) reset() {
|
||||
*c = Client{
|
||||
connID: c.connID + 1,
|
||||
xid: c.xid,
|
||||
clientMAC: c.clientMAC,
|
||||
iaid: c.iaid,
|
||||
duid: c.duid,
|
||||
serverDUID: c.serverDUID[:0],
|
||||
dns: c.dns[:0],
|
||||
}
|
||||
}
|
||||
|
||||
// Encapsulate writes the next outgoing DHCPv6 message into carrierData[offsetToFrame:].
|
||||
// Returns the number of bytes written or 0 if there is nothing to send in the current state.
|
||||
// Implements [lneto.StackNode].
|
||||
func (c *Client) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, error) {
|
||||
if c.isClosed() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
dst := carrierData[offsetToFrame:]
|
||||
if len(dst) < OptionsOffset+128 {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
frm, err := NewFrame(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var numOpts int
|
||||
var nextState ClientState
|
||||
|
||||
switch c.state {
|
||||
case StateInit:
|
||||
frm.SetMsgType(MsgSolicit)
|
||||
frm.SetTransactionID(c.xid)
|
||||
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
||||
numOpts += n
|
||||
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, nil)
|
||||
numOpts += n
|
||||
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptORO, defaultOptRequestList...)
|
||||
numOpts += n
|
||||
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
||||
numOpts += n
|
||||
nextState = StateSoliciting
|
||||
|
||||
case StateRequesting:
|
||||
frm.SetMsgType(MsgRequest)
|
||||
frm.SetTransactionID(c.xid)
|
||||
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
||||
numOpts += n
|
||||
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptServerID, c.serverDUID...)
|
||||
numOpts += n
|
||||
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
||||
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
||||
numOpts += n
|
||||
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptORO, defaultOptRequestList...)
|
||||
numOpts += n
|
||||
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
||||
numOpts += n
|
||||
nextState = StateRequesting // retransmittable; Demux(Reply) advances to Bound
|
||||
|
||||
case StateRenewing:
|
||||
frm.SetMsgType(MsgRenew)
|
||||
frm.SetTransactionID(c.xid)
|
||||
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
||||
numOpts += n
|
||||
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptServerID, c.serverDUID...)
|
||||
numOpts += n
|
||||
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
||||
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
||||
numOpts += n
|
||||
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
||||
numOpts += n
|
||||
nextState = StateRenewing
|
||||
|
||||
case StateRebinding:
|
||||
frm.SetMsgType(MsgRebind)
|
||||
frm.SetTransactionID(c.xid)
|
||||
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
||||
numOpts += n
|
||||
// No OptServerID in Rebind (RFC 8415 §18.2.5).
|
||||
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
||||
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
||||
numOpts += n
|
||||
n, _ = EncodeOption16(dst[OptionsOffset+numOpts:], OptElapsedTime, 0)
|
||||
numOpts += n
|
||||
nextState = StateRebinding
|
||||
|
||||
default:
|
||||
return 0, nil // StateSoliciting, StateBound, or uninitialised.
|
||||
}
|
||||
|
||||
c.state = nextState
|
||||
return OptionsOffset + numOpts, nil
|
||||
}
|
||||
|
||||
// Demux processes an incoming DHCPv6 message at carrierData[frameOffset:].
|
||||
// It validates the transaction ID and advances the client state machine on success.
|
||||
// Implements [lneto.StackNode].
|
||||
func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||
if c.isClosed() {
|
||||
return net.ErrClosed
|
||||
}
|
||||
frm, err := NewFrame(carrierData[frameOffset:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if frm.TransactionID() != c.xid {
|
||||
return lneto.ErrMismatch
|
||||
}
|
||||
|
||||
msgType := frm.MsgType()
|
||||
var nextState ClientState
|
||||
switch c.state {
|
||||
case StateSoliciting:
|
||||
if msgType != MsgAdvertise {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
nextState = StateRequesting
|
||||
case StateRequesting, StateRenewing, StateRebinding:
|
||||
if msgType != MsgReply {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
nextState = StateBound
|
||||
default:
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
|
||||
if err := c.setOptions(frm); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = nextState
|
||||
return nil
|
||||
}
|
||||
|
||||
// setOptions parses all DHCPv6 options in frm and stores relevant values.
|
||||
func (c *Client) setOptions(frm Frame) error {
|
||||
return frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
||||
switch code {
|
||||
case OptServerID:
|
||||
c.serverDUID = append(c.serverDUID[:0], data...)
|
||||
case OptIANA:
|
||||
c.parseIANA(data)
|
||||
case OptDNSServers:
|
||||
if len(c.dns) > 0 || len(data)%16 != 0 {
|
||||
break // skip if already populated or malformed
|
||||
}
|
||||
for i := 0; i+16 <= len(data); i += 16 {
|
||||
c.dns = append(c.dns, netip.AddrFrom16([16]byte(data[i:i+16])))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// parseIANA processes the payload of an OptIANA option, extracting the
|
||||
// assigned address and lease timers from any embedded OptIAAddr sub-option.
|
||||
func (c *Client) parseIANA(data []byte) {
|
||||
if len(data) < 12 {
|
||||
return
|
||||
}
|
||||
if [4]byte(data[:4]) != c.iaid {
|
||||
return // not our Identity Association
|
||||
}
|
||||
t1 := binary.BigEndian.Uint32(data[4:8])
|
||||
t2 := binary.BigEndian.Uint32(data[8:12])
|
||||
|
||||
// Iterate sub-options manually (same 4-byte TLV format).
|
||||
ptr := 12
|
||||
for ptr+4 <= len(data) {
|
||||
subCode := OptCode(binary.BigEndian.Uint16(data[ptr:]))
|
||||
subLen := int(binary.BigEndian.Uint16(data[ptr+2:]))
|
||||
if ptr+4+subLen > len(data) {
|
||||
break // malformed sub-option; stop safely
|
||||
}
|
||||
if subCode == OptIAAddr && subLen >= 24 {
|
||||
sub := data[ptr+4 : ptr+4+subLen]
|
||||
c.assignedAddr = [16]byte(sub[:16])
|
||||
c.assignedAddrValid = true
|
||||
c.preferredLifetime = binary.BigEndian.Uint32(sub[16:20])
|
||||
c.validLifetime = binary.BigEndian.Uint32(sub[20:24])
|
||||
}
|
||||
ptr += 4 + subLen
|
||||
}
|
||||
if c.assignedAddrValid {
|
||||
c.t1 = t1
|
||||
c.t2 = t2
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool { return c.state == 0 || c.xid == 0 }
|
||||
|
||||
// State returns the current client state.
|
||||
func (c *Client) State() ClientState { return c.state }
|
||||
|
||||
// AssignedAddr returns the IPv6 address assigned by the server and whether it is valid.
|
||||
func (c *Client) AssignedAddr() ([16]byte, bool) { return c.assignedAddr, c.assignedAddrValid }
|
||||
|
||||
// AppendDNSServers appends the DNS server addresses received from the server to dst.
|
||||
func (c *Client) AppendDNSServers(dst []netip.Addr) []netip.Addr { return append(dst, c.dns...) }
|
||||
|
||||
// NumDNSServers returns the number of DNS server addresses received.
|
||||
func (c *Client) NumDNSServers() int { return len(c.dns) }
|
||||
|
||||
// ConnectionID returns a pointer to the client's connection ID.
|
||||
// The value increments on each reset; callers should discard registrations when it changes.
|
||||
// Implements [lneto.StackNode].
|
||||
func (c *Client) ConnectionID() *uint64 { return &c.connID }
|
||||
|
||||
// LocalPort returns the DHCPv6 client port (546).
|
||||
// Implements [lneto.StackNode].
|
||||
func (c *Client) LocalPort() uint16 { return ClientPort }
|
||||
|
||||
// Protocol returns the IP protocol number for UDP.
|
||||
// Implements [lneto.StackNode].
|
||||
func (c *Client) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
||||
|
||||
// defaultOptRequestList is the ORO payload (RFC 8415 §21.7) listing the options
|
||||
// the client wants the server to include in its reply.
|
||||
var defaultOptRequestList = []byte{
|
||||
byte(OptDNSServers >> 8), byte(OptDNSServers), // 23
|
||||
byte(OptDomainList >> 8), byte(OptDomainList), // 24
|
||||
byte(OptNTPServer >> 8), byte(OptNTPServer), // 56
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// writeOpt6 encodes a single DHCPv6 option into dst using the 4-byte TLV header
|
||||
// (2-byte code + 2-byte length) and returns the total bytes written.
|
||||
// Used by tests to build server frames without depending on the stub EncodeOption.
|
||||
func writeOpt6(dst []byte, code OptCode, data ...byte) int {
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(code))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(len(data)))
|
||||
copy(dst[4:], data)
|
||||
return 4 + len(data)
|
||||
}
|
||||
|
||||
// buildServerFrame constructs a minimal DHCPv6 Advertise or Reply frame
|
||||
// containing OptServerID, and an OptIANA with an embedded OptIAAddr.
|
||||
func buildServerFrame(msgType MsgType, xid uint32, serverDUID []byte, iaid [4]byte, addr [16]byte) []byte {
|
||||
// IAAddr payload: addr(16) + preferred(4) + valid(4)
|
||||
iaAddrPayload := make([]byte, 24)
|
||||
copy(iaAddrPayload[:16], addr[:])
|
||||
binary.BigEndian.PutUint32(iaAddrPayload[16:20], 3600)
|
||||
binary.BigEndian.PutUint32(iaAddrPayload[20:24], 7200)
|
||||
|
||||
// Encode IAAddr as an option.
|
||||
iaAddrOpt := make([]byte, 4+len(iaAddrPayload))
|
||||
writeOpt6(iaAddrOpt, OptIAAddr, iaAddrPayload...)
|
||||
|
||||
// IA_NA payload: IAID(4) + T1(4) + T2(4) + IAAddr option.
|
||||
iaNAPayload := make([]byte, 12+len(iaAddrOpt))
|
||||
copy(iaNAPayload[:4], iaid[:])
|
||||
binary.BigEndian.PutUint32(iaNAPayload[4:8], 1800)
|
||||
binary.BigEndian.PutUint32(iaNAPayload[8:12], 3600)
|
||||
copy(iaNAPayload[12:], iaAddrOpt)
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
buf[0] = byte(msgType)
|
||||
buf[1] = byte(xid >> 16)
|
||||
buf[2] = byte(xid >> 8)
|
||||
buf[3] = byte(xid)
|
||||
n := OptionsOffset
|
||||
n += writeOpt6(buf[n:], OptServerID, serverDUID...)
|
||||
n += writeOpt6(buf[n:], OptIANA, iaNAPayload...)
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
// TestFrameForEachOption verifies that ForEachOption correctly delivers
|
||||
// the option code and data to the callback for a hand-built frame.
|
||||
func TestFrameForEachOption(t *testing.T) {
|
||||
buf := make([]byte, OptionsOffset+8)
|
||||
buf[0] = byte(MsgSolicit)
|
||||
buf[3] = 42 // XID low byte
|
||||
|
||||
n := writeOpt6(buf[OptionsOffset:], OptClientID, 'A', 'B')
|
||||
frm, err := NewFrame(buf[:OptionsOffset+n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var gotCode OptCode
|
||||
var gotData []byte
|
||||
err = frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
||||
gotCode = code
|
||||
gotData = append(gotData[:0], data...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("ForEachOption:", err)
|
||||
}
|
||||
if gotCode != OptClientID {
|
||||
t.Errorf("option code: want %d (OptClientID), got %d", OptClientID, gotCode)
|
||||
}
|
||||
if string(gotData) != "AB" {
|
||||
t.Errorf("option data: want %q, got %q", "AB", gotData)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFrameValidateSize verifies that ValidateSize returns an error when an option's
|
||||
// declared length extends past the end of the buffer.
|
||||
func TestFrameValidateSize(t *testing.T) {
|
||||
buf := make([]byte, OptionsOffset+6)
|
||||
buf[0] = byte(MsgSolicit)
|
||||
// Option code = OptClientID, claimed length = 100, actual data = 2 bytes.
|
||||
binary.BigEndian.PutUint16(buf[OptionsOffset:], uint16(OptClientID))
|
||||
binary.BigEndian.PutUint16(buf[OptionsOffset+2:], 100)
|
||||
buf[OptionsOffset+4] = 'A'
|
||||
buf[OptionsOffset+5] = 'B'
|
||||
|
||||
frm, err := NewFrame(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := frm.ValidateSize(); err == nil {
|
||||
t.Error("ValidateSize: want error for truncated option, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientSolicitRequest exercises the full four-step DHCPv6 exchange:
|
||||
// Solicit → (fabricated) Advertise → Request → (fabricated) Reply → Bound.
|
||||
func TestClientSolicitRequest(t *testing.T) {
|
||||
const xid = 0x112233
|
||||
clientMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||
assignedAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
iaid := [4]byte{clientMAC[0], clientMAC[1], clientMAC[2], clientMAC[3]}
|
||||
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: clientMAC}); err != nil {
|
||||
t.Fatal("BeginRequest:", err)
|
||||
}
|
||||
if cl.State() != StateInit {
|
||||
t.Fatalf("initial state: want StateInit, got %v", cl.State())
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
|
||||
// CLIENT: send Solicit.
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Encapsulate (Solicit):", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("Encapsulate (Solicit): wrote 0 bytes")
|
||||
}
|
||||
if cl.State() != StateSoliciting {
|
||||
t.Fatalf("after Solicit: want StateSoliciting, got %v", cl.State())
|
||||
}
|
||||
|
||||
// SERVER: fabricated Advertise.
|
||||
advFrame := buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, assignedAddr)
|
||||
if err := cl.Demux(advFrame, 0); err != nil {
|
||||
t.Fatal("Demux (Advertise):", err)
|
||||
}
|
||||
if cl.State() != StateRequesting {
|
||||
t.Fatalf("after Advertise: want StateRequesting, got %v", cl.State())
|
||||
}
|
||||
|
||||
// CLIENT: send Request.
|
||||
n, err = cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("Encapsulate (Request):", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("Encapsulate (Request): wrote 0 bytes")
|
||||
}
|
||||
|
||||
// SERVER: fabricated Reply.
|
||||
replyFrame := buildServerFrame(MsgReply, xid, serverDUID, iaid, assignedAddr)
|
||||
if err := cl.Demux(replyFrame, 0); err != nil {
|
||||
t.Fatal("Demux (Reply):", err)
|
||||
}
|
||||
if cl.State() != StateBound {
|
||||
t.Fatalf("after Reply: want StateBound, got %v", cl.State())
|
||||
}
|
||||
|
||||
addr, valid := cl.AssignedAddr()
|
||||
if !valid {
|
||||
t.Fatal("AssignedAddr: not valid after bound")
|
||||
}
|
||||
if addr != assignedAddr {
|
||||
t.Errorf("AssignedAddr: got %v, want %v", addr, assignedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientEncapsulateSolicit verifies that the first Encapsulate call
|
||||
// writes a Solicit message with at least OptClientID and OptIANA.
|
||||
func TestClientEncapsulateSolicit(t *testing.T) {
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(0xABCDEF, RequestConfig{
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("Encapsulate: wrote 0 bytes")
|
||||
}
|
||||
|
||||
frm, err := NewFrame(buf[:n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if frm.MsgType() != MsgSolicit {
|
||||
t.Errorf("MsgType: want MsgSolicit, got %v", frm.MsgType())
|
||||
}
|
||||
if frm.TransactionID() != 0xABCDEF {
|
||||
t.Errorf("TransactionID: want 0xABCDEF, got 0x%X", frm.TransactionID())
|
||||
}
|
||||
|
||||
var hasClientID, hasIANA bool
|
||||
err = frm.ForEachOption(func(_ int, code OptCode, _ []byte) error {
|
||||
switch code {
|
||||
case OptClientID:
|
||||
hasClientID = true
|
||||
case OptIANA:
|
||||
hasIANA = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("ForEachOption:", err)
|
||||
}
|
||||
if !hasClientID {
|
||||
t.Error("Solicit: missing OptClientID")
|
||||
}
|
||||
if !hasIANA {
|
||||
t.Error("Solicit: missing OptIANA")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClientDoubleTapEncapsulate verifies that calling Encapsulate twice in the
|
||||
// same state returns 0 bytes on the second call (idempotent, no duplicate messages).
|
||||
func TestClientDoubleTapEncapsulate(t *testing.T) {
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(1, RequestConfig{
|
||||
ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 512)
|
||||
n, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("first Encapsulate:", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("first Encapsulate: wrote 0 bytes")
|
||||
}
|
||||
|
||||
n2, err := cl.Encapsulate(buf, -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("second Encapsulate:", err)
|
||||
}
|
||||
if n2 != 0 {
|
||||
t.Errorf("second Encapsulate: want 0 bytes (idempotent), got %d", n2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
// ClientState transition table during request:
|
||||
//
|
||||
// StateInit -> | Send out Solicit | -> StateSoliciting
|
||||
// StateSoliciting -> | Accept Advertise | -> StateRequesting
|
||||
// StateRequesting -> | Receive Reply | -> StateBound
|
||||
|
||||
//go:generate stringer -type=MsgType,ClientState,OptCode,StatusCode,DUIDType -linecomment -output stringers.go
|
||||
|
||||
const (
|
||||
// ClientPort is the UDP port DHCPv6 clients listen on.
|
||||
ClientPort = 546
|
||||
// ServerPort is the UDP port DHCPv6 servers listen on.
|
||||
ServerPort = 547
|
||||
// OptionsOffset is the byte offset where DHCPv6 options begin in a client-server message.
|
||||
// Layout: MsgType(1) + TransactionID(3).
|
||||
OptionsOffset = 4
|
||||
)
|
||||
|
||||
// MsgType is the DHCPv6 message type (RFC 8415 §7.3).
|
||||
type MsgType uint8
|
||||
|
||||
const (
|
||||
MsgSolicit MsgType = 1 // solicit
|
||||
MsgAdvertise MsgType = 2 // advertise
|
||||
MsgRequest MsgType = 3 // request
|
||||
MsgConfirm MsgType = 4 // confirm
|
||||
MsgRenew MsgType = 5 // renew
|
||||
MsgRebind MsgType = 6 // rebind
|
||||
MsgReply MsgType = 7 // reply
|
||||
MsgRelease MsgType = 8 // release
|
||||
MsgDecline MsgType = 9 // decline
|
||||
MsgReconfigure MsgType = 10 // reconfigure
|
||||
MsgInformRequest MsgType = 11 // inform-request
|
||||
MsgRelayForw MsgType = 12 // relay-forw
|
||||
MsgRelayRepl MsgType = 13 // relay-repl
|
||||
)
|
||||
|
||||
// ClientState is the DHCPv6 client DORA state machine state.
|
||||
type ClientState uint8
|
||||
|
||||
const (
|
||||
_ ClientState = iota
|
||||
StateInit // init
|
||||
StateSoliciting // soliciting
|
||||
StateRequesting // requesting
|
||||
StateBound // bound
|
||||
StateRenewing // renewing
|
||||
StateRebinding // rebinding
|
||||
)
|
||||
|
||||
// HasIP returns true if the state indicates the Client has an IPv6 address assigned.
|
||||
func (s ClientState) HasIP() bool {
|
||||
return s == StateBound || s == StateRenewing || s == StateRebinding
|
||||
}
|
||||
|
||||
// OptCode is a DHCPv6 option code (RFC 8415 §21), encoded as a 2-byte big-endian value.
|
||||
type OptCode uint16
|
||||
|
||||
const (
|
||||
OptClientID OptCode = 1 // client-id
|
||||
OptServerID OptCode = 2 // server-id
|
||||
OptIANA OptCode = 3 // ia-na
|
||||
OptIATA OptCode = 4 // ia-ta
|
||||
OptIAAddr OptCode = 5 // iaaddr
|
||||
OptORO OptCode = 6 // oro
|
||||
OptPreference OptCode = 7 // preference
|
||||
OptElapsedTime OptCode = 8 // elapsed-time
|
||||
OptRelayMsg OptCode = 9 // relay-msg
|
||||
OptAuth OptCode = 11 // auth
|
||||
OptUnicast OptCode = 12 // unicast
|
||||
OptStatusCode OptCode = 13 // status-code
|
||||
OptRapidCommit OptCode = 14 // rapid-commit
|
||||
OptUserClass OptCode = 15 // user-class
|
||||
OptVendorClass OptCode = 16 // vendor-class
|
||||
OptVendorOpts OptCode = 17 // vendor-opts
|
||||
OptInterfaceID OptCode = 18 // interface-id
|
||||
OptReconfMsg OptCode = 19 // reconf-msg
|
||||
OptReconfAccept OptCode = 20 // reconf-accept
|
||||
OptDNSServers OptCode = 23 // dns-servers
|
||||
OptDomainList OptCode = 24 // domain-list
|
||||
OptIAPD OptCode = 25 // ia-pd
|
||||
OptIAPrefix OptCode = 26 // iaprefix
|
||||
OptNTPServer OptCode = 56 // ntp-server
|
||||
)
|
||||
|
||||
// DUIDType is the DHCP Unique Identifier type (RFC 8415 §11).
|
||||
type DUIDType uint16
|
||||
|
||||
const (
|
||||
DUIDTypeLLT DUIDType = 1 // duid-llt
|
||||
DUIDTypeEN DUIDType = 2 // duid-en
|
||||
DUIDTypeLL DUIDType = 3 // duid-ll
|
||||
)
|
||||
|
||||
// StatusCode is the DHCPv6 status code value (RFC 8415 §21.13).
|
||||
type StatusCode uint16
|
||||
|
||||
const (
|
||||
StatusSuccess StatusCode = 0 // success
|
||||
StatusUnspecFail StatusCode = 1 // unspec-fail
|
||||
StatusNoAddrsAvail StatusCode = 2 // no-addrs-avail
|
||||
StatusNoBinding StatusCode = 3 // no-binding
|
||||
StatusNotOnLink StatusCode = 4 // not-on-link
|
||||
StatusUseMulticast StatusCode = 5 // use-multicast
|
||||
)
|
||||
|
||||
// EncodeOption writes a DHCPv6 TLV option into dst.
|
||||
// Format: code(2) + length(2) + data.
|
||||
func EncodeOption(dst []byte, code OptCode, data ...byte) (int, error) {
|
||||
if len(data) > 0xffff {
|
||||
return 0, lneto.ErrInvalidLengthField
|
||||
} else if len(dst) < 4+len(data) {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(code))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(len(data)))
|
||||
copy(dst[4:], data)
|
||||
return 4 + len(data), nil
|
||||
}
|
||||
|
||||
// EncodeOption16 encodes a single uint16 value as a DHCPv6 option.
|
||||
func EncodeOption16(dst []byte, code OptCode, v uint16) (int, error) {
|
||||
return EncodeOption(dst, code, byte(v>>8), byte(v))
|
||||
}
|
||||
|
||||
// EncodeOption32 encodes a single uint32 value as a DHCPv6 option.
|
||||
func EncodeOption32(dst []byte, code OptCode, v uint32) (int, error) {
|
||||
return EncodeOption(dst, code, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
||||
}
|
||||
|
||||
// EncodeOptionIANA encodes an IA_NA option (RFC 8415 §21.4).
|
||||
// Layout: code(2) + len(2) + IAID(4) + T1(4) + T2(4) + subOpts.
|
||||
func EncodeOptionIANA(dst []byte, iaid [4]byte, t1, t2 uint32, subOpts []byte) (int, error) {
|
||||
const fixedLen = 12 // IAID(4) + T1(4) + T2(4)
|
||||
dataLen := fixedLen + len(subOpts)
|
||||
if len(dst) < 4+dataLen {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(OptIANA))
|
||||
binary.BigEndian.PutUint16(dst[2:4], uint16(dataLen))
|
||||
copy(dst[4:8], iaid[:])
|
||||
binary.BigEndian.PutUint32(dst[8:12], t1)
|
||||
binary.BigEndian.PutUint32(dst[12:16], t2)
|
||||
copy(dst[16:], subOpts)
|
||||
return 4 + dataLen, nil
|
||||
}
|
||||
|
||||
// EncodeOptionIAAddr encodes an IAADDR option (RFC 8415 §21.6).
|
||||
// Layout: code(2) + len(2) + addr(16) + preferred(4) + valid(4).
|
||||
func EncodeOptionIAAddr(dst []byte, addr [16]byte, preferred, valid uint32) (int, error) {
|
||||
const dataLen = 24 // addr(16) + preferred(4) + valid(4)
|
||||
if len(dst) < 4+dataLen {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
binary.BigEndian.PutUint16(dst[0:2], uint16(OptIAAddr))
|
||||
binary.BigEndian.PutUint16(dst[2:4], dataLen)
|
||||
copy(dst[4:20], addr[:])
|
||||
binary.BigEndian.PutUint32(dst[20:24], preferred)
|
||||
binary.BigEndian.PutUint32(dst[24:28], valid)
|
||||
return 4 + dataLen, nil
|
||||
}
|
||||
|
||||
// AppendDUIDLL appends a DUID-LL (type 3) for an Ethernet MAC to dst.
|
||||
// Format: DUIDType(2) + hwtype=1(2) + mac(6).
|
||||
func AppendDUIDLL(dst []byte, mac [6]byte) []byte {
|
||||
return append(dst,
|
||||
byte(DUIDTypeLL>>8), byte(DUIDTypeLL), // type 3
|
||||
0, 1, // hardware type 1 = Ethernet
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package dhcpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
// NewFrame returns a Frame backed by buf.
|
||||
// Returns an error if buf is shorter than [OptionsOffset] bytes.
|
||||
func NewFrame(buf []byte) (Frame, error) {
|
||||
if len(buf) < OptionsOffset {
|
||||
return Frame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return Frame{buf: buf}, nil
|
||||
}
|
||||
|
||||
// Frame encapsulates the raw bytes of a DHCPv6 client-server message (RFC 8415 §8)
|
||||
// and provides methods for accessing and modifying its fields.
|
||||
//
|
||||
// Layout:
|
||||
//
|
||||
// Byte 0: msg-type
|
||||
// Bytes 1-3: transaction-id (24-bit big-endian)
|
||||
// Bytes 4+: options (code(2) + length(2) + data)
|
||||
type Frame struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// MsgType returns the message type field.
|
||||
func (frm Frame) MsgType() MsgType { return MsgType(frm.buf[0]) }
|
||||
|
||||
// SetMsgType sets the message type field.
|
||||
func (frm Frame) SetMsgType(t MsgType) { frm.buf[0] = byte(t) }
|
||||
|
||||
// TransactionID returns the 24-bit transaction ID as a uint32 (upper byte is always zero).
|
||||
func (frm Frame) TransactionID() uint32 {
|
||||
return uint32(frm.buf[1])<<16 | uint32(frm.buf[2])<<8 | uint32(frm.buf[3])
|
||||
}
|
||||
|
||||
// SetTransactionID writes the lower 24 bits of id into bytes 1–3.
|
||||
func (frm Frame) SetTransactionID(id uint32) {
|
||||
frm.buf[1] = byte(id >> 16)
|
||||
frm.buf[2] = byte(id >> 8)
|
||||
frm.buf[3] = byte(id)
|
||||
}
|
||||
|
||||
// Options returns the options section of the frame (bytes from [OptionsOffset] onward).
|
||||
func (frm Frame) Options() []byte { return frm.buf[OptionsOffset:] }
|
||||
|
||||
// ForEachOption iterates over all DHCPv6 options in the frame's options section.
|
||||
// Each option is passed to fn as (byteOffset, optionCode, optionData).
|
||||
// Iteration stops early if fn returns [io.EOF]; any other non-nil error is returned directly.
|
||||
// If fn is nil, the function only validates the structure.
|
||||
func (frm Frame) ForEachOption(fn func(off int, code OptCode, data []byte) error) error {
|
||||
buf := frm.buf
|
||||
ptr := OptionsOffset
|
||||
for ptr+4 <= len(buf) {
|
||||
code := OptCode(binary.BigEndian.Uint16(buf[ptr:]))
|
||||
optlen := int(binary.BigEndian.Uint16(buf[ptr+2:]))
|
||||
if ptr+4+optlen > len(buf) {
|
||||
return lneto.ErrInvalidLengthField
|
||||
}
|
||||
if fn != nil {
|
||||
err := fn(ptr, code, buf[ptr+4:ptr+4+optlen])
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ptr += 4 + optlen
|
||||
}
|
||||
if ptr != len(buf) {
|
||||
// 1–3 trailing bytes that cannot form a valid option header.
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSize validates the structure of the options section without invoking a callback.
|
||||
func (frm Frame) ValidateSize() error { return frm.ForEachOption(nil) }
|
||||
@@ -0,0 +1,161 @@
|
||||
// Code generated by "stringer -type=MsgType,ClientState,OptCode,StatusCode,DUIDType -linecomment -output stringers.go"; DO NOT EDIT.
|
||||
|
||||
package dhcpv6
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[MsgSolicit-1]
|
||||
_ = x[MsgAdvertise-2]
|
||||
_ = x[MsgRequest-3]
|
||||
_ = x[MsgConfirm-4]
|
||||
_ = x[MsgRenew-5]
|
||||
_ = x[MsgRebind-6]
|
||||
_ = x[MsgReply-7]
|
||||
_ = x[MsgRelease-8]
|
||||
_ = x[MsgDecline-9]
|
||||
_ = x[MsgReconfigure-10]
|
||||
_ = x[MsgInformRequest-11]
|
||||
_ = x[MsgRelayForw-12]
|
||||
_ = x[MsgRelayRepl-13]
|
||||
}
|
||||
|
||||
const _MsgType_name = "solicitadvertiserequestconfirmrenewrebindreplyreleasedeclinereconfigureinform-requestrelay-forwrelay-repl"
|
||||
|
||||
var _MsgType_index = [...]uint8{0, 7, 16, 23, 30, 35, 41, 46, 53, 60, 71, 85, 95, 105}
|
||||
|
||||
func (i MsgType) String() string {
|
||||
i -= 1
|
||||
if i >= MsgType(len(_MsgType_index)-1) {
|
||||
return "MsgType(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _MsgType_name[_MsgType_index[i]:_MsgType_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[StateInit-1]
|
||||
_ = x[StateSoliciting-2]
|
||||
_ = x[StateRequesting-3]
|
||||
_ = x[StateBound-4]
|
||||
_ = x[StateRenewing-5]
|
||||
_ = x[StateRebinding-6]
|
||||
}
|
||||
|
||||
const _ClientState_name = "initsolicitingrequestingboundrenewingrebinding"
|
||||
|
||||
var _ClientState_index = [...]uint8{0, 4, 14, 24, 29, 37, 46}
|
||||
|
||||
func (i ClientState) String() string {
|
||||
i -= 1
|
||||
if i >= ClientState(len(_ClientState_index)-1) {
|
||||
return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[OptClientID-1]
|
||||
_ = x[OptServerID-2]
|
||||
_ = x[OptIANA-3]
|
||||
_ = x[OptIATA-4]
|
||||
_ = x[OptIAAddr-5]
|
||||
_ = x[OptORO-6]
|
||||
_ = x[OptPreference-7]
|
||||
_ = x[OptElapsedTime-8]
|
||||
_ = x[OptRelayMsg-9]
|
||||
_ = x[OptAuth-11]
|
||||
_ = x[OptUnicast-12]
|
||||
_ = x[OptStatusCode-13]
|
||||
_ = x[OptRapidCommit-14]
|
||||
_ = x[OptUserClass-15]
|
||||
_ = x[OptVendorClass-16]
|
||||
_ = x[OptVendorOpts-17]
|
||||
_ = x[OptInterfaceID-18]
|
||||
_ = x[OptReconfMsg-19]
|
||||
_ = x[OptReconfAccept-20]
|
||||
_ = x[OptDNSServers-23]
|
||||
_ = x[OptDomainList-24]
|
||||
_ = x[OptIAPD-25]
|
||||
_ = x[OptIAPrefix-26]
|
||||
_ = x[OptNTPServer-56]
|
||||
}
|
||||
|
||||
const (
|
||||
_OptCode_name_0 = "client-idserver-idia-naia-taiaaddroropreferenceelapsed-timerelay-msg"
|
||||
_OptCode_name_1 = "authunicaststatus-coderapid-commituser-classvendor-classvendor-optsinterface-idreconf-msgreconf-accept"
|
||||
_OptCode_name_2 = "dns-serversdomain-listia-pdiaprefix"
|
||||
_OptCode_name_3 = "ntp-server"
|
||||
)
|
||||
|
||||
var (
|
||||
_OptCode_index_0 = [...]uint8{0, 9, 18, 23, 28, 34, 37, 47, 59, 68}
|
||||
_OptCode_index_1 = [...]uint8{0, 4, 11, 22, 34, 44, 56, 67, 79, 89, 102}
|
||||
_OptCode_index_2 = [...]uint8{0, 11, 22, 27, 35}
|
||||
)
|
||||
|
||||
func (i OptCode) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 9:
|
||||
i -= 1
|
||||
return _OptCode_name_0[_OptCode_index_0[i]:_OptCode_index_0[i+1]]
|
||||
case 11 <= i && i <= 20:
|
||||
i -= 11
|
||||
return _OptCode_name_1[_OptCode_index_1[i]:_OptCode_index_1[i+1]]
|
||||
case 23 <= i && i <= 26:
|
||||
i -= 23
|
||||
return _OptCode_name_2[_OptCode_index_2[i]:_OptCode_index_2[i+1]]
|
||||
case i == 56:
|
||||
return _OptCode_name_3
|
||||
default:
|
||||
return "OptCode(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[StatusSuccess-0]
|
||||
_ = x[StatusUnspecFail-1]
|
||||
_ = x[StatusNoAddrsAvail-2]
|
||||
_ = x[StatusNoBinding-3]
|
||||
_ = x[StatusNotOnLink-4]
|
||||
_ = x[StatusUseMulticast-5]
|
||||
}
|
||||
|
||||
const _StatusCode_name = "successunspec-failno-addrs-availno-bindingnot-on-linkuse-multicast"
|
||||
|
||||
var _StatusCode_index = [...]uint8{0, 7, 18, 32, 42, 53, 66}
|
||||
|
||||
func (i StatusCode) String() string {
|
||||
if i >= StatusCode(len(_StatusCode_index)-1) {
|
||||
return "StatusCode(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[DUIDTypeLLT-1]
|
||||
_ = x[DUIDTypeEN-2]
|
||||
_ = x[DUIDTypeLL-3]
|
||||
}
|
||||
|
||||
const _DUIDType_name = "duid-lltduid-enduid-ll"
|
||||
|
||||
var _DUIDType_index = [...]uint8{0, 8, 15, 22}
|
||||
|
||||
func (i DUIDType) String() string {
|
||||
i -= 1
|
||||
if i >= DUIDType(len(_DUIDType_index)-1) {
|
||||
return "DUIDType(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _DUIDType_name[_DUIDType_index[i]:_DUIDType_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Package dhcp holds protocol-version-independent types shared by the DHCPv4
|
||||
// and DHCPv6 implementations. In particular it defines the [Allocator]
|
||||
// interface a DHCP server delegates address assignment and lease lifetime
|
||||
// management to, decoupling the higher-level state machine from the backing
|
||||
// lease database.
|
||||
//
|
||||
// The lease lifetime model follows the common shape described by RFC 2131
|
||||
// (DHCPv4) and RFC 8415 (DHCPv6): a per-client [Binding] carries renewal (T1)
|
||||
// and rebind (T2) times and holds one or more [Lease]s, each with a preferred
|
||||
// and valid lifetime. DHCPv4 assigns a single address whose preferred and
|
||||
// valid lifetimes equal the lease time; DHCPv6 may assign several addresses
|
||||
// (and prefixes) through identity associations, each with its own lifetimes.
|
||||
package dhcp
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// Lease is a single address binding offered or assigned to a client.
|
||||
type Lease struct {
|
||||
// Addr is the assigned address. For DHCPv4 this is an IPv4 address.
|
||||
Addr netip.Addr
|
||||
// Preferred is the preferred lifetime in seconds (RFC 8415 §7.1). For
|
||||
// DHCPv4, where there is no distinct preferred lifetime, set it equal to
|
||||
// Valid.
|
||||
Preferred uint32
|
||||
// Valid is the valid lifetime in seconds. This is the DHCPv4 "IP Address
|
||||
// Lease Time" (RFC 2132 option 51).
|
||||
Valid uint32
|
||||
}
|
||||
|
||||
// Binding groups the leases assigned to a single client identity association
|
||||
// together with the renewal and rebind times that govern when the client
|
||||
// should attempt to extend them.
|
||||
type Binding struct {
|
||||
// T1 is the renewal time in seconds: when the client should contact the
|
||||
// allocating server to extend its leases. RFC 2131 §4.4.5 / RFC 8415 §14.2
|
||||
// default this to half the (shortest) lease time.
|
||||
T1 uint32
|
||||
// T2 is the rebinding time in seconds: when the client should broadcast to
|
||||
// any server to extend its leases. The RFCs default this to 0.875 of the
|
||||
// (shortest) lease time.
|
||||
T2 uint32
|
||||
// Leases holds the addresses bound to the client. DHCPv4 uses exactly one
|
||||
// lease; DHCPv6 may use more than one (IA_NA / IA_PD).
|
||||
Leases []Lease
|
||||
}
|
||||
|
||||
// Addr returns the first lease address and whether the binding holds any lease.
|
||||
// It is a convenience for single-address (DHCPv4) callers.
|
||||
func (b Binding) Addr() (netip.Addr, bool) {
|
||||
if len(b.Leases) == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return b.Leases[0].Addr, true
|
||||
}
|
||||
|
||||
// DefaultT1T2 returns the RFC 2131 §4.4.5 default renewal (T1) and rebind (T2)
|
||||
// times for a given lease duration: T1 = 0.5·lease and T2 = 0.875·lease.
|
||||
func DefaultT1T2(leaseSeconds uint32) (t1Renewal, t2Rebinding uint32) {
|
||||
return leaseSeconds / 2, leaseSeconds * 7 / 8
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
var errOptionNotFit = errors.New("DHCPv4: options dont fit")
|
||||
|
||||
type Server struct {
|
||||
connID uint64
|
||||
nextAddr netip.Addr
|
||||
prefix netip.Prefix
|
||||
hosts map[[36]byte]serverEntry
|
||||
vld lneto.Validator
|
||||
pending int
|
||||
leaseSeconds uint32
|
||||
port uint16
|
||||
siaddr [4]byte
|
||||
gwaddr [4]byte
|
||||
dns [4]byte
|
||||
}
|
||||
|
||||
// ServerConfig contains configuration parameters for [Server.Configure].
|
||||
type ServerConfig struct {
|
||||
// ServerAddr is the DHCP server's own IPv4 address.
|
||||
ServerAddr [4]byte
|
||||
// Gateway advertised to clients as default router. Zero value omits the option.
|
||||
Gateway [4]byte
|
||||
// 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
|
||||
// LeaseSeconds is the lease duration. Zero defaults to 3600.
|
||||
LeaseSeconds uint32
|
||||
// Port is the server listening port. Zero defaults to DefaultServerPort.
|
||||
Port uint16
|
||||
}
|
||||
|
||||
type serverEntry struct {
|
||||
hostname string
|
||||
xid uint32
|
||||
port uint16
|
||||
addr [4]byte
|
||||
requestlist [10]byte
|
||||
hwaddr [6]byte
|
||||
clientIdlen uint8
|
||||
// Possible states:
|
||||
// - 0: No entry/uninitialized
|
||||
// - Init: Server received discover, pending Offer sent out.
|
||||
// - Selecting: Server sent out offer, request not received.
|
||||
// - Requesting: Request received, pending Ack sent out.
|
||||
// - Bound: Ack sent out, no more pending data to be sent.
|
||||
state ClientState
|
||||
}
|
||||
|
||||
// Configure resets and configures the server with the given configuration.
|
||||
// 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) {
|
||||
return errors.New("dhcpv4 server: server address outside subnet")
|
||||
}
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = DefaultServerPort
|
||||
}
|
||||
lease := cfg.LeaseSeconds
|
||||
if lease == 0 {
|
||||
lease = 3600
|
||||
}
|
||||
hosts := sv.hosts
|
||||
if hosts == nil {
|
||||
hosts = make(map[[36]byte]serverEntry)
|
||||
} else {
|
||||
for k := range hosts {
|
||||
delete(hosts, k)
|
||||
}
|
||||
}
|
||||
*sv = Server{
|
||||
connID: sv.connID + 1,
|
||||
siaddr: cfg.ServerAddr,
|
||||
gwaddr: cfg.Gateway,
|
||||
dns: cfg.DNS,
|
||||
prefix: cfg.Subnet,
|
||||
port: port,
|
||||
leaseSeconds: lease,
|
||||
nextAddr: svAddr,
|
||||
hosts: hosts,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sv *Server) ConnectionID() *uint64 { return &sv.connID }
|
||||
func (sv *Server) Protocol() uint64 { return uint64(lneto.IPProtoUDP) }
|
||||
func (sv *Server) LocalPort() uint16 { return sv.port }
|
||||
|
||||
func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
||||
isIPLayer := frameOffset >= 28
|
||||
dhcpData := carrierData[frameOffset:]
|
||||
dfrm, err := NewFrame(dhcpData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dfrm.ValidateSize(&sv.vld)
|
||||
if sv.vld.HasError() {
|
||||
return sv.vld.ErrPop()
|
||||
}
|
||||
|
||||
var msgType MessageType
|
||||
var clientID []byte
|
||||
var reqlist []byte
|
||||
var reqAddr []byte
|
||||
var hostname []byte
|
||||
err = dfrm.ForEachOption(func(off int, op OptNum, data []byte) error {
|
||||
switch op {
|
||||
case OptMessageType:
|
||||
if len(data) == 1 {
|
||||
msgType = MessageType(data[0])
|
||||
}
|
||||
case OptHostName:
|
||||
if len(data) <= 36 {
|
||||
hostname = data
|
||||
}
|
||||
case OptClientIdentifier:
|
||||
if len(data) <= 36 {
|
||||
clientID = data
|
||||
}
|
||||
case OptParameterRequestList:
|
||||
if len(data) > 36 {
|
||||
return errors.New("too many request options")
|
||||
}
|
||||
reqlist = data
|
||||
case OptRequestedIPaddress:
|
||||
if len(data) == 4 {
|
||||
reqAddr = data
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var clientIDRaw [36]byte
|
||||
var client serverEntry
|
||||
var clientExists bool
|
||||
if len(clientID) == 0 {
|
||||
client, clientIDRaw, clientExists = sv.getClientByIP(*dfrm.CIAddr())
|
||||
} else {
|
||||
copy(clientIDRaw[:], clientID)
|
||||
client, clientExists = sv.getClient(clientIDRaw)
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case MsgDiscover:
|
||||
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
|
||||
sv.pending-- // Cancel unfulfilled pending response.
|
||||
}
|
||||
if !clientExists {
|
||||
addr, ok := sv.allocAddr(reqAddr)
|
||||
if !ok {
|
||||
return errors.New("dhcpv4 server: address pool exhausted")
|
||||
}
|
||||
client.addr = addr
|
||||
}
|
||||
copy(client.requestlist[:], reqlist)
|
||||
client.state = StateInit
|
||||
client.hostname = string(hostname)
|
||||
client.xid = dfrm.XID()
|
||||
client.hwaddr = *dfrm.CHAddrAs6()
|
||||
if isIPLayer {
|
||||
_, client.port, _ = getSrcIPPort(carrierData)
|
||||
}
|
||||
client.clientIdlen = uint8(len(clientID))
|
||||
sv.pending++
|
||||
|
||||
case MsgRequest:
|
||||
if !clientExists {
|
||||
err = errors.New("request for non existing client")
|
||||
} else if dfrm.XID() != client.xid {
|
||||
err = errors.New("unexpected XID for client")
|
||||
} else if client.state != StateSelecting && client.state != StateRequesting {
|
||||
err = errors.New("DHCP request unexpected state")
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if client.state == StateSelecting {
|
||||
client.state = StateRequesting
|
||||
sv.pending++
|
||||
}
|
||||
|
||||
case MsgRelease:
|
||||
if clientExists {
|
||||
if client.state == StateInit || client.state == StateRequesting {
|
||||
sv.pending--
|
||||
}
|
||||
delete(sv.hosts, clientIDRaw)
|
||||
return nil
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unhandled message type %s", msgType.String())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
|
||||
}
|
||||
sv.hosts[clientIDRaw] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||
carrierIsIP := offsetToIP >= 0
|
||||
dfrm, err := NewFrame(carrierData[offsetToFrame:])
|
||||
optBuf := dfrm.OptionsPayload()[:]
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if len(optBuf) < 255 {
|
||||
return 0, errOptionNotFit
|
||||
}
|
||||
if sv.pending == 0 {
|
||||
return 0, nil // No pending outgoing frames.
|
||||
}
|
||||
|
||||
var client serverEntry
|
||||
var clientID [36]byte
|
||||
for k, v := range sv.hosts {
|
||||
pending := v.state == StateInit || v.state == StateRequesting
|
||||
if pending {
|
||||
client = v
|
||||
clientID = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if client.state == 0 {
|
||||
return 0, nil // Nothing to do.
|
||||
}
|
||||
futureState := ClientState(0)
|
||||
var nopt int
|
||||
switch client.state {
|
||||
case StateInit:
|
||||
futureState = StateSelecting
|
||||
nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgOffer))
|
||||
case StateRequesting:
|
||||
futureState = StateBound
|
||||
nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgAck))
|
||||
*dfrm.CIAddr() = client.addr
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||
nopt += n
|
||||
if sv.gwaddr != [4]byte{} {
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if sv.prefix.IsValid() {
|
||||
bits := uint(sv.prefix.Bits())
|
||||
mask := ^uint32(0) << (32 - bits)
|
||||
var maskBuf [4]byte
|
||||
binary.BigEndian.PutUint32(maskBuf[:], mask)
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptSubnetMask, maskBuf[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if sv.dns != [4]byte{} {
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptDNSServers, sv.dns[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if sv.leaseSeconds > 0 {
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, sv.leaseSeconds)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptRenewTimeValue, sv.leaseSeconds/2)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptRebindingTimeValue, sv.leaseSeconds*7/8)
|
||||
nopt += n
|
||||
}
|
||||
optBuf[nopt] = byte(OptEnd)
|
||||
nopt++
|
||||
|
||||
dfrm.ClearHeader()
|
||||
dfrm.SetOp(OpReply)
|
||||
dfrm.SetHardware(1, 6, 0)
|
||||
dfrm.SetXID(client.xid)
|
||||
dfrm.SetSecs(0)
|
||||
dfrm.SetFlags(0)
|
||||
*dfrm.YIAddr() = client.addr // Offer here.
|
||||
*dfrm.SIAddr() = sv.siaddr
|
||||
*dfrm.GIAddr() = sv.gwaddr
|
||||
copy(dfrm.CHAddrAs6()[:], client.hwaddr[:])
|
||||
dfrm.SetMagicCookie(MagicCookie)
|
||||
if carrierIsIP {
|
||||
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], client.addr[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
client.state = futureState
|
||||
|
||||
// Set server state.
|
||||
sv.hosts[clientID] = client
|
||||
sv.pending--
|
||||
return OptionsOffset + nopt, nil
|
||||
}
|
||||
|
||||
// allocAddr allocates the next available address from the pool.
|
||||
// If reqAddr is a valid 4-byte address within the subnet and not already assigned,
|
||||
// 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
|
||||
}
|
||||
}
|
||||
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())
|
||||
hostMask := ^uint32(0) >> (32 - hostBits)
|
||||
if binary.BigEndian.Uint32(a[:])&hostMask == hostMask {
|
||||
return [4]byte{}, false
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
func (sv *Server) isAddrAssigned(addr netip.Addr) bool {
|
||||
a4 := addr.As4()
|
||||
for _, v := range sv.hosts {
|
||||
if v.addr == a4 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
|
||||
entry, ok := sv.hosts[clientID]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func (sv *Server) getClientByIP(ip [4]byte) (serverEntry, [36]byte, bool) {
|
||||
for k, v := range sv.hosts {
|
||||
if v.addr == ip {
|
||||
return v, k, true
|
||||
}
|
||||
}
|
||||
return serverEntry{}, [36]byte{}, false
|
||||
}
|
||||
|
||||
func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
|
||||
srcaddr, _, _, off, err := internal.GetIPAddr(ipCarrier)
|
||||
if err != nil {
|
||||
return srcaddr, port, err
|
||||
} else if len(ipCarrier[off:]) < 2 {
|
||||
return srcaddr, port, errors.New("getSrcIPPort got only IP layer")
|
||||
}
|
||||
port = binary.BigEndian.Uint16(ipCarrier[off:]) // TCP and UDP share same port offsets.
|
||||
return srcaddr, port, nil
|
||||
}
|
||||
+23
-24
@@ -4,6 +4,7 @@ import (
|
||||
"log/slog"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
@@ -15,7 +16,7 @@ type Client struct {
|
||||
lport uint16
|
||||
msg Message
|
||||
respFlags HeaderFlags
|
||||
state clientState
|
||||
state StateClientQuery
|
||||
enableRecursion bool
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ func (c *Client) StartResolve(localPort, txid uint16, cfg ResolveConfig) error {
|
||||
if nd > math.MaxUint16 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
c.reset(localPort, txid, dnsSendQuery, cfg.EnableRecursion)
|
||||
c.reset(localPort, txid, CQueryPending, cfg.EnableRecursion)
|
||||
c.msg.LimitResourceDecoding(uint16(nd), uint16(nd), 0, 0)
|
||||
c.msg.AddQuestions(cfg.Questions)
|
||||
c.msg.AddAdditionals(cfg.Additional)
|
||||
@@ -46,7 +47,7 @@ func (c *Client) StartResolve(localPort, txid uint16, cfg ResolveConfig) error {
|
||||
func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||
if c.isClosed() {
|
||||
return 0, net.ErrClosed
|
||||
} else if c.state != dnsSendQuery {
|
||||
} else if c.state != CQueryPending {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
||||
internal.LogAttrs(nil, slog.LevelError, "dns:unexpected-write", slog.Int("got", len(data)), slog.Int("want", int(msglen)))
|
||||
return 0, lneto.ErrBug
|
||||
}
|
||||
c.state = dnsAwaitResponse
|
||||
c.state = CQueryOutstanding
|
||||
// Unset don't frag since DNS requests go through LOTS of nodes.
|
||||
// if frameOffset >= 28 {
|
||||
// version := carrierData[0] >> 4
|
||||
@@ -78,7 +79,7 @@ func (c *Client) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
||||
func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||
if c.isClosed() {
|
||||
return net.ErrClosed
|
||||
} else if c.state != dnsAwaitResponse {
|
||||
} else if c.state != CQueryOutstanding {
|
||||
return nil
|
||||
}
|
||||
frame := carrierData[frameOffset:]
|
||||
@@ -91,7 +92,7 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||
return nil // Not meant for our client.
|
||||
}
|
||||
c.respFlags = flags
|
||||
c.state = dnsDone
|
||||
c.state = CQueryDone
|
||||
msg := &c.msg
|
||||
_, incompleteButOK, err := msg.Decode(frame)
|
||||
if err != nil && !incompleteButOK {
|
||||
@@ -101,10 +102,10 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool {
|
||||
return c.state == dnsClosed || c.state == dnsAborted
|
||||
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,18 +117,26 @@ func (c *Client) MessageCopyTo(dst *Message) (done bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Client) Answers() []Resource {
|
||||
if c.state != dnsDone {
|
||||
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() {
|
||||
c.reset(0, 0, 0, false)
|
||||
c.reset(0, 0, CQueryAborted, false)
|
||||
}
|
||||
|
||||
func (c *Client) reset(lport, txid uint16, state clientState, enableRecursion bool) {
|
||||
func (c *Client) reset(lport, txid uint16, state StateClientQuery, enableRecursion bool) {
|
||||
*c = Client{
|
||||
connID: c.connID + 1,
|
||||
lport: lport,
|
||||
@@ -138,13 +147,3 @@ func (c *Client) reset(lport, txid uint16, state clientState, enableRecursion bo
|
||||
}
|
||||
c.msg.Reset()
|
||||
}
|
||||
|
||||
type clientState uint8
|
||||
|
||||
const (
|
||||
dnsClosed clientState = iota
|
||||
dnsSendQuery
|
||||
dnsAwaitResponse
|
||||
dnsDone
|
||||
dnsAborted
|
||||
)
|
||||
|
||||
@@ -238,6 +238,17 @@ const (
|
||||
RCodeRefused RCode = 5 // refused
|
||||
)
|
||||
|
||||
// StateClientQuery is the lifecycle state of a single DNS query.
|
||||
type StateClientQuery uint8
|
||||
|
||||
const (
|
||||
CQueryIdle StateClientQuery = iota // no active query (zero value)
|
||||
CQueryPending // query built, not yet transmitted
|
||||
CQueryOutstanding // transmitted; awaiting response (RFC 7766 §9.3)
|
||||
CQueryDone // response received and decoded
|
||||
CQueryAborted // query abandoned (connection error or caller abort)
|
||||
)
|
||||
|
||||
func b2u8(b bool) uint8 {
|
||||
if b {
|
||||
return 1
|
||||
|
||||
+129
-48
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,6 +27,11 @@ const (
|
||||
MaxSizeUDP = 512
|
||||
)
|
||||
|
||||
// Message is a convenience type for decoding DNS messages and storing results in a single object.
|
||||
// Message is designed for ease of memory reuse. All internal buffers in a Message are reused in methods:
|
||||
// - [Message.Decode]: Limited in decode size by [Message.LimitResourceDecoding] which must be called beforehand.
|
||||
// - [Message.CopyFrom]
|
||||
// - [Message.AddQuestions]
|
||||
type Message struct {
|
||||
Questions []Question
|
||||
Answers []Resource
|
||||
@@ -54,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
|
||||
before, after, ok := strings.Cut(strname, ".")
|
||||
if !ok {
|
||||
seg, strname = strname, ""
|
||||
} else {
|
||||
seg, strname = before, after
|
||||
}
|
||||
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].
|
||||
@@ -265,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()
|
||||
}
|
||||
@@ -346,6 +401,8 @@ func (r *Resource) Reset() {
|
||||
r.data = r.data[:0]
|
||||
}
|
||||
|
||||
func (r *Resource) Header() ResourceHeader { return r.header }
|
||||
|
||||
func (r *Resource) RawData() []byte {
|
||||
length := r.header.Length
|
||||
if int(length) > len(r.data) {
|
||||
@@ -489,7 +546,7 @@ func NewName(domain string) (Name, error) {
|
||||
// Returns an empty Name if n exceeds the number of labels.
|
||||
func (n Name) TrimLabels(skip int) Name {
|
||||
off := 0
|
||||
for i := 0; i < skip; i++ {
|
||||
for range skip {
|
||||
if off >= len(n.data) {
|
||||
return Name{}
|
||||
}
|
||||
@@ -596,8 +653,6 @@ func append32(b []byte, v uint32) []byte {
|
||||
}
|
||||
|
||||
func visitAllLabels(msg []byte, off uint16, fn func(b []byte), allowCompression bool) (uint16, error) {
|
||||
// currOff is the current working offset.
|
||||
currOff := off
|
||||
if len(msg) > math.MaxUint16 {
|
||||
return off, errResTooLong
|
||||
}
|
||||
@@ -608,60 +663,86 @@ func visitAllLabels(msg []byte, off uint16, fn func(b []byte), allowCompression
|
||||
// the usage of this name.
|
||||
var newOff = off
|
||||
|
||||
LOOP:
|
||||
for {
|
||||
if currOff >= uint16(len(msg)) {
|
||||
return off, lneto.ErrTruncatedFrame
|
||||
}
|
||||
c := uint16(msg[currOff])
|
||||
currOff++
|
||||
switch c & 0xc0 {
|
||||
case 0x00: // String label (segment).
|
||||
if c == 0x00 {
|
||||
break LOOP // Nominal end of name, always ends with null terminator.
|
||||
}
|
||||
endOff := currOff + c
|
||||
if endOff > uint16(len(msg)) {
|
||||
return off, errCalcLen
|
||||
}
|
||||
|
||||
// Reject names containing dots. See issue golang/go#56246
|
||||
if bytes.IndexByte(msg[currOff:endOff], '.') >= 0 {
|
||||
return off, errInvalidName
|
||||
}
|
||||
|
||||
fn(msg[currOff:endOff])
|
||||
currOff = endOff
|
||||
|
||||
case 0xc0: // Pointer.
|
||||
// https://cs.opensource.google/go/x/net/+/refs/tags/v0.19.0:dns/dnsmessage/message.go;l=2078
|
||||
if !allowCompression {
|
||||
return off, errCompressedSRV
|
||||
}
|
||||
if currOff >= uint16(len(msg)) {
|
||||
return off, errInvalidPtr
|
||||
}
|
||||
c1 := msg[currOff]
|
||||
currOff++
|
||||
start, end, isPtr, err := NextLabel(msg[off:])
|
||||
if err != nil {
|
||||
return off, err
|
||||
} else if start == end {
|
||||
if ptr == 0 {
|
||||
newOff = currOff
|
||||
newOff = off + 1 // advance past the null terminator byte
|
||||
}
|
||||
// Don't follow too many pointers, maybe there's a loop.
|
||||
if ptr++; ptr > 10 {
|
||||
return off, errTooManyPtr
|
||||
break
|
||||
} else if isPtr {
|
||||
if !allowCompression {
|
||||
return newOff, errCompressedSRV
|
||||
}
|
||||
currOff = (c^0xC0)<<8 | uint16(c1)
|
||||
default:
|
||||
// Prefixes 0x80 and 0x40 are reserved.
|
||||
return off, errReserved
|
||||
if ptr == 0 {
|
||||
newOff = off + 2 // next record follows the 2-byte pointer
|
||||
}
|
||||
off = start
|
||||
if int(off) >= len(msg) {
|
||||
return newOff, errInvalidPtr
|
||||
} else if ptr++; ptr > 10 {
|
||||
return newOff, errTooManyPtr
|
||||
}
|
||||
} else {
|
||||
// Is normal label; start/end are relative to msg[off:].
|
||||
fn(msg[off+start : off+end])
|
||||
off += end
|
||||
}
|
||||
}
|
||||
if ptr == 0 {
|
||||
newOff = currOff
|
||||
}
|
||||
return newOff, nil
|
||||
}
|
||||
|
||||
// NextLabel parses the first control byte of data and returns the position and extent of next DNS label.
|
||||
//
|
||||
// For a normal string label (RFC 1035 §3.1), isPointer==false and start/end are
|
||||
// byte indices into data: data[start:end] holds the raw label bytes.
|
||||
// A null terminator (c==0) signals the end of the name: start==end==1, err==nil.
|
||||
//
|
||||
// For a compression pointer (RFC 1035 §4.1.4), isPointer==true:
|
||||
// - start is the absolute target offset within the full DNS message to jump to.
|
||||
// - end==0 (sentinel; not a data range).
|
||||
//
|
||||
// Returns [lneto.ErrTruncatedFrame] if data is too short to read the full label or pointer.
|
||||
// Returns errReserved for the 0x40 and 0x80 reserved prefix classes.
|
||||
func NextLabel(data []byte) (start_RelOrAbs, endRel uint16, isAbsPointer bool, err error) {
|
||||
// Default invalid values
|
||||
start_RelOrAbs, endRel = 0, 0
|
||||
if len(data) == 0 {
|
||||
return start_RelOrAbs, endRel, false, lneto.ErrTruncatedFrame
|
||||
}
|
||||
c := uint16(data[0])
|
||||
switch c & 0xc0 {
|
||||
case 0:
|
||||
start_RelOrAbs = 1
|
||||
// String label segment.
|
||||
if c == 0 {
|
||||
return start_RelOrAbs, start_RelOrAbs, false, nil // Null terminator. String ended.
|
||||
}
|
||||
endRel = start_RelOrAbs + c
|
||||
if int(endRel) > len(data) {
|
||||
return start_RelOrAbs, endRel, false, lneto.ErrTruncatedFrame
|
||||
}
|
||||
// Reject names containing dots. See issue golang/go#56246
|
||||
if bytes.IndexByte(data[start_RelOrAbs:endRel], '.') >= 0 {
|
||||
return start_RelOrAbs, endRel, false, errInvalidName
|
||||
}
|
||||
// Correct label!
|
||||
case 0xc0:
|
||||
// Pointer. Start is absolute index in DNS message.
|
||||
isAbsPointer = true
|
||||
if len(data) < 2 {
|
||||
return start_RelOrAbs, endRel, isAbsPointer, lneto.ErrTruncatedFrame // Need more data to fully read pointer.
|
||||
}
|
||||
c1 := uint16(data[1])
|
||||
start_RelOrAbs = (c^0xC0)<<8 | c1
|
||||
default:
|
||||
err = errReserved
|
||||
}
|
||||
return start_RelOrAbs, endRel, isAbsPointer, err
|
||||
}
|
||||
|
||||
func (dst *Message) CopyFrom(m Message) {
|
||||
internal.SliceReuse(&dst.Questions, len(m.Questions))
|
||||
internal.SliceReuse(&dst.Answers, len(m.Answers))
|
||||
|
||||
+21
-20
@@ -2,6 +2,7 @@ package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -195,32 +196,32 @@ func TestMessageAppendEncodeIncompleteOK(t *testing.T) {
|
||||
|
||||
func (m *Message) String() string {
|
||||
// s := fmt.Sprintf("Message: %#v\n", &m.Header)
|
||||
var s string
|
||||
var s strings.Builder
|
||||
if len(m.Questions) > 0 {
|
||||
s += "-- Questions\n"
|
||||
s.WriteString("-- Questions\n")
|
||||
for _, q := range m.Questions {
|
||||
s += fmt.Sprintf("%#v\n", q)
|
||||
s.WriteString(fmt.Sprintf("%#v\n", q))
|
||||
}
|
||||
}
|
||||
if len(m.Answers) > 0 {
|
||||
s += "-- Answers\n"
|
||||
s.WriteString("-- Answers\n")
|
||||
for _, a := range m.Answers {
|
||||
s += fmt.Sprintf("%#v\n", a)
|
||||
s.WriteString(fmt.Sprintf("%#v\n", a))
|
||||
}
|
||||
}
|
||||
if len(m.Authorities) > 0 {
|
||||
s += "-- Authorities\n"
|
||||
s.WriteString("-- Authorities\n")
|
||||
for _, ns := range m.Authorities {
|
||||
s += fmt.Sprintf("%#v\n", ns)
|
||||
s.WriteString(fmt.Sprintf("%#v\n", ns))
|
||||
}
|
||||
}
|
||||
if len(m.Additionals) > 0 {
|
||||
s += "-- Additionals\n"
|
||||
s.WriteString("-- Additionals\n")
|
||||
for _, e := range m.Additionals {
|
||||
s += fmt.Sprintf("%#v\n", e)
|
||||
s.WriteString(fmt.Sprintf("%#v\n", e))
|
||||
}
|
||||
}
|
||||
return s
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func TestDecodeMessage(t *testing.T) {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -17,16 +17,18 @@ const (
|
||||
ErrMismatch // mismatch
|
||||
ErrMismatchLen // mismatched length
|
||||
ErrInvalidConfig // invalid configuration
|
||||
_ // little stitious
|
||||
ErrInvalidField // invalid field
|
||||
ErrInvalidLengthField // invalid length field
|
||||
ErrExhausted // resource exhausted
|
||||
ErrAlreadyRegistered // protocol already registered
|
||||
ErrTruncatedFrame // truncated frame
|
||||
|
||||
ErrMissingHALConfig // missing HAL configuration
|
||||
// Below are potentially good future error additions
|
||||
// based on one or two encountered use cases, example use case included.
|
||||
/*
|
||||
- ErrUnregistered/ErrAborted // connection unregistered. i.e: ICMP client aborted during active ping, ping process returns this.
|
||||
- ErrInvalidArgs // invalid func arguments i.e: different from Config since this refers to non-config arguments. usually nil values.
|
||||
*/
|
||||
)
|
||||
|
||||
|
||||
+13
-2
@@ -5,13 +5,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxMTU defines the maximum payload of a standard ethernet frame. Does NOT include ethernet header, FCS and VLAN tag.
|
||||
// Ethernet frames can be larger but this is out of the 802.3 standard and going into jumbo frame territory.
|
||||
MaxMTU = 1500
|
||||
// MaxFrameLength (1522) defines the maximum length of a standard ethernet frame including headers, FCS and VLAN if present.
|
||||
MaxFrameLength = MaxMTU + MaxOverheadSize
|
||||
// MaxOverheadSize is the maximum overhead a packet can incur
|
||||
// from transmitting an ethernet frame. Includes:
|
||||
// - 14 bytes of Ethernet header containing MAC addresses and ethernet type, always present
|
||||
// - 4 bytes of VLAN tag, if present.
|
||||
// - 4 bytes of VLAN tag, if present. See 802.1Q.
|
||||
// - 4 bytes of the 32 bit trailing CRC, if required by PHY.
|
||||
MaxOverheadSize = 14 + 4 + 4
|
||||
MaxOverheadSize = sizeHeaderNoVLAN + fcsOverhead + vlanOverhead
|
||||
vlanOverhead = 4
|
||||
fcsOverhead = 4
|
||||
sizeHeaderNoVLAN = 14
|
||||
// MinimumFrameLength as defined by IEEE 802.3 standards. Includes ethernet header.
|
||||
MinimumFrameLength = 64
|
||||
// Does not include VLAN/FCS for more conservative minimum MTU.
|
||||
MinimumMTU = MinimumFrameLength - sizeHeaderNoVLAN
|
||||
)
|
||||
|
||||
// AppendAddr appends the text representation of the hardware address to the destination buffer.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# import-examples
|
||||
This folder contains examples that need external imports to work.
|
||||
|
||||
This is done to avoid adding dependencies to lneto's go.mod file.
|
||||
- Trivial Auditing
|
||||
- No dependency analysis needed for vulnerability scanning
|
||||
- Eliminate a whole set of attack vectors for lneto importers
|
||||
- Ensures nothing outside Lneto gets compiled maintaining binary sizes small
|
||||
@@ -0,0 +1,14 @@
|
||||
module espradio-netdev
|
||||
|
||||
go 1.25.7
|
||||
|
||||
// These replace directives point to local checkouts during development.
|
||||
// Remove them when using as a standalone program with published releases.
|
||||
replace github.com/soypat/lneto => ../../../.
|
||||
|
||||
replace tinygo.org/x/espradio => ../../../../espradio
|
||||
|
||||
require (
|
||||
github.com/soypat/lneto v0.1.1-0.20260425023453-aa77403a2b32
|
||||
tinygo.org/x/espradio v0.1.0
|
||||
)
|
||||
@@ -0,0 +1,213 @@
|
||||
// Example showing how to use the ESP32 WiFi radio through lneto's netdev
|
||||
// package. This mirrors the picow-netdev example and demonstrates the
|
||||
// standardised DevEthernet+Netlink interface that works across hardware targets.
|
||||
//
|
||||
// Build and flash:
|
||||
//
|
||||
// tinygo flash -target xiao-esp32c3 \
|
||||
// -ldflags="-X main.ssid=YourSSID -X main.password=YourPassword" \
|
||||
// -monitor ./examples/esp32-netdev
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/x/netdev"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
"tinygo.org/x/espradio"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed wifi.credentials
|
||||
credentials string
|
||||
// remove windows CRLF "\r\n" and trailing newline.
|
||||
credentialsNormalized = strings.TrimSuffix(strings.ReplaceAll(credentials, "\r\n", "\n"), "\n")
|
||||
|
||||
globConnectParams espradio.STAConfig
|
||||
|
||||
poolCfg = xnet.TCPPoolConfig{
|
||||
PoolSize: 4,
|
||||
QueueSize: 4,
|
||||
TxBufSize: 2048,
|
||||
RxBufSize: 512,
|
||||
NewBackoff: func() lneto.BackoffStrategy { return backoff },
|
||||
NanoTime: func() int64 { return time.Now().UnixNano() },
|
||||
EstablishedTimeout: 5 * time.Second,
|
||||
ClosingTimeout: 3 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(time.Second)
|
||||
ssid, password, ok := strings.Cut(credentialsNormalized, "\n")
|
||||
if !ok {
|
||||
fail("must write newline separated ssid/password in wifi.credentials", nil)
|
||||
}
|
||||
globConnectParams.SSID = ssid
|
||||
globConnectParams.Password = password
|
||||
var dev EspDev
|
||||
dev.radioConfig = espradio.Config{Logging: espradio.LogLevelError}
|
||||
|
||||
// LinkConnect runs Enable+Start+Connect+StartNetDev. It must complete
|
||||
// before HardwareAddr6 is called because the MAC is only readable after
|
||||
// Enable() initialises the WiFi hardware.
|
||||
err := dev.LinkConnect(globConnectParams)
|
||||
failIfErr("wifi connect", err)
|
||||
|
||||
hw, err := dev.HardwareAddr6()
|
||||
failIfErr("hardware addr", err)
|
||||
|
||||
var stack xnet.Netstack
|
||||
err = stack.Reset(xnet.StackConfig{
|
||||
RandSeed: time.Now().UnixNano() | 1,
|
||||
Hostname: "esp32-lneto",
|
||||
MaxActiveTCPPorts: 4,
|
||||
MaxActiveUDPPorts: 4,
|
||||
ICMPQueueLimit: 1,
|
||||
MTU: 1500,
|
||||
PassivePeers: 4,
|
||||
HardwareAddress: hw,
|
||||
}, backoff, poolCfg)
|
||||
failIfErr("stack reset", err)
|
||||
err = stack.EnableICMP(true)
|
||||
failIfErr("icmp enable", err)
|
||||
dev.LinkNotify(userNotify)
|
||||
var iface netdev.Interface[espradio.STAConfig]
|
||||
err = iface.Init(&dev, &dev, netdev.InterfaceConfig{})
|
||||
failIfErr("init iface", err)
|
||||
|
||||
var runner netdev.Runner[espradio.STAConfig]
|
||||
go func() {
|
||||
if err := runner.Run(context.Background(), iface, &stack, backoff); err != nil {
|
||||
failIfErr("runner", err)
|
||||
}
|
||||
}()
|
||||
|
||||
assigned, gatewayRt, subnetBits, err := stack.EnableDHCP(context.Background(), true, netip.Addr{})
|
||||
failIfErr("enable dhcp", err)
|
||||
println("assigned=", assigned.String(), "gateway=", gatewayRt.String(), "subnet=", subnetBits)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// compile-time interface checks.
|
||||
var _ lneto.BackoffStrategy = backoff
|
||||
var _ netdev.Stack = (*xnet.Netstack)(nil)
|
||||
var _ netdev.DevEthernet = (*EspDev)(nil)
|
||||
var _ netdev.Netlink[espradio.STAConfig] = (*EspDev)(nil)
|
||||
|
||||
func backoff(consecutiveBackoffs uint) time.Duration {
|
||||
return 5 * time.Millisecond
|
||||
}
|
||||
|
||||
func userNotify(connected bool) (retries int, reconnectParams espradio.STAConfig) {
|
||||
if !connected {
|
||||
return 1, globConnectParams
|
||||
}
|
||||
return 0, espradio.STAConfig{}
|
||||
}
|
||||
|
||||
// EspDev adapts [espradio.NetDev] to [netdev.DevEthernet] and wraps the
|
||||
// ESP32 WiFi bring-up sequence (Enable/Start/Connect/StartNetDev) as [netdev.Netlink].
|
||||
//
|
||||
// The same struct implements both interfaces so a single pointer can be passed
|
||||
// to [netdev.Interface.Init] for both the netlink and device arguments, matching
|
||||
// the pattern used by the picow-netdev example.
|
||||
type EspDev struct {
|
||||
nd *espradio.NetDev
|
||||
radioConfig espradio.Config
|
||||
notifyCb netdev.NotifyCallback[espradio.STAConfig]
|
||||
}
|
||||
|
||||
// LinkConnect implements [netdev.Netlink].
|
||||
// Runs Enable→Start→Connect→StartNetDev. nd is nil until this returns successfully.
|
||||
func (d *EspDev) LinkConnect(cfg espradio.STAConfig) error {
|
||||
if err := espradio.Enable(d.radioConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := espradio.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := espradio.Connect(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
nd, err := espradio.StartNetDev()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nd = nd
|
||||
return nil
|
||||
}
|
||||
|
||||
// LinkDisconnect implements [netdev.Netlink].
|
||||
func (d *EspDev) LinkDisconnect() {}
|
||||
|
||||
// LinkNotify implements [netdev.Netlink].
|
||||
func (d *EspDev) LinkNotify(cb netdev.NotifyCallback[espradio.STAConfig]) {
|
||||
d.notifyCb = cb
|
||||
}
|
||||
|
||||
// HardwareAddr6 implements [netdev.DevEthernet].
|
||||
func (d *EspDev) HardwareAddr6() ([6]byte, error) {
|
||||
return d.nd.HardwareAddr6()
|
||||
}
|
||||
|
||||
// SendOffsetEthFrame implements [netdev.DevEthernet].
|
||||
// ESP32 has no frame prefix offset, so buf is passed directly to [espradio.NetDev.SendEthFrame].
|
||||
func (d *EspDev) SendOffsetEthFrame(buf []byte) error {
|
||||
return d.nd.SendEthFrame(buf)
|
||||
}
|
||||
|
||||
// SetEthRecvHandler implements [netdev.DevEthernet].
|
||||
// Adapts the error-less netdev handler signature to espradio's error-returning one.
|
||||
func (d *EspDev) SetEthRecvHandler(handler func(rxEthframe []byte)) {
|
||||
if handler == nil {
|
||||
d.nd.SetEthRecvHandler(nil)
|
||||
return
|
||||
}
|
||||
d.nd.SetEthRecvHandler(func(pkt []byte) error {
|
||||
handler(pkt)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// EthPoll implements [netdev.DevEthernet].
|
||||
//
|
||||
// espradio.NetDev.EthPoll both pops a frame from the C ring buffer into buf
|
||||
// AND synchronously calls the registered rxHandler with that frame. Returning
|
||||
// the non-zero byte count here as well would trigger the Runner's "device uses
|
||||
// both paths" guard. We therefore drain the ring (calling the handler) and
|
||||
// return (0, 0, err) so the Runner sees only the handler-based receive path.
|
||||
func (d *EspDev) EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error) {
|
||||
_, err = d.nd.EthPoll(buf)
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// MaxFrameSizeAndOffset implements [netdev.DevEthernet].
|
||||
// ESP32 transmits frames with no prefix offset.
|
||||
func (d *EspDev) MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int) {
|
||||
return d.nd.MaxFrameSize(), 0
|
||||
}
|
||||
|
||||
func failIfErr(msg string, err error) {
|
||||
if err != nil {
|
||||
fail(msg, err)
|
||||
}
|
||||
println(msg, "PASS")
|
||||
}
|
||||
|
||||
func fail(msg string, err error) {
|
||||
var errstr string
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
for {
|
||||
println("FAIL:", msg, errstr)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
module gvisormwe
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/usbarmory/go-net v0.0.0-20260416163630-1078311e0956 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250911055229-61a46406f068 // indirect
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/usbarmory/go-net v0.0.0-20260416163630-1078311e0956 h1:ZjhVXLMT/Ogxs1GIy1g8UJk7yi9G8kt90D0ElAPbaCc=
|
||||
github.com/usbarmory/go-net v0.0.0-20260416163630-1078311e0956/go.mod h1:+6WiKCFJtJQZdNM2VpwQsYGo/aBJ39pN7nWx6Td3Z8s=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gvisor.dev/gvisor v0.0.0-20250911055229-61a46406f068 h1:95kdltF/maTDk/Wulj7V81cSLgjB/Mg/6eJmOKsey4U=
|
||||
gvisor.dev/gvisor v0.0.0-20250911055229-61a46406f068/go.mod h1:K16uJjZ+hSqDVsXhU2Rg2FpMN7kBvjZp/Ibt5BYZJjw=
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
gnet "github.com/usbarmory/go-net"
|
||||
)
|
||||
|
||||
const pollTime = 5 * time.Millisecond
|
||||
|
||||
var networkDevice NetworkDevice
|
||||
|
||||
// NetworkDevice implements gnet.NetworkDevice for bridging with raw Ethernet I/O.
|
||||
type NetworkDevice struct {
|
||||
send func([]byte) error
|
||||
recv func([]byte) (int, error)
|
||||
}
|
||||
|
||||
func (d *NetworkDevice) Transmit(buf []byte) error { return d.send(buf) }
|
||||
func (d *NetworkDevice) Receive(buf []byte) (int, error) { return d.recv(buf) }
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
if err := run(ctx); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
// Create gVisor-based networking stack.
|
||||
stack := gnet.NewGVisorStack(1)
|
||||
|
||||
// Configure stack with MAC, IP prefix, and gateway.
|
||||
err := stack.Configure(
|
||||
"aa:bb:cc:dd:ee:ff",
|
||||
netip.MustParsePrefix("192.168.1.10/24"),
|
||||
netip.MustParseAddr("192.168.1.1"),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuring stack: %w", err)
|
||||
}
|
||||
|
||||
err = stack.EnableICMP()
|
||||
if err != nil {
|
||||
return fmt.Errorf("enabling ICMP: %w", err)
|
||||
}
|
||||
|
||||
// Bridge the stack with a network device for packet I/O.
|
||||
iface := &gnet.Interface{
|
||||
Stack: stack,
|
||||
NetworkDevice: &networkDevice,
|
||||
HandleStackErr: func(err error, tx bool) {
|
||||
dir := "rx"
|
||||
if tx {
|
||||
dir = "tx"
|
||||
}
|
||||
fmt.Printf("stack %s err: %v\n", dir, err)
|
||||
},
|
||||
}
|
||||
// Start the packet processing loop in a goroutine.
|
||||
go iface.Start()
|
||||
|
||||
// Create a TCP listener on port 80 using the gVisor stack.
|
||||
const sockStream = 0x1
|
||||
laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.MustParseAddr("192.168.1.10"), 80))
|
||||
c, err := stack.Socket(ctx, "tcp", syscall.AF_INET, sockStream, laddr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating AF_INET stream socket: %w", err)
|
||||
}
|
||||
listener := c.(net.Listener)
|
||||
for ctx.Err() == nil {
|
||||
time.Sleep(pollTime)
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
fmt.Println("conn failed:", err)
|
||||
continue
|
||||
}
|
||||
go handleConn(conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConn(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
conn.Write([]byte("Hello from gVisor stack!"))
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
module piconetdev
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require github.com/soypat/cyw43439 v0.1.1
|
||||
|
||||
require (
|
||||
github.com/soypat/lneto v0.1.0 // indirect
|
||||
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
|
||||
github.com/tinygo-org/pio v0.2.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
)
|
||||
|
||||
// This is an example taken grom github.com/soypat/lneto
|
||||
// Remove this replace directive when using as own program.
|
||||
replace github.com/soypat/lneto => ../../../.
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/soypat/cyw43439 v0.1.1 h1:vcaTiVzfuz3keK7lJpVxStZ6tV8HCw7Ugzsh1k4mneE=
|
||||
github.com/soypat/cyw43439 v0.1.1/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc=
|
||||
github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I=
|
||||
github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U=
|
||||
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
|
||||
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
|
||||
github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU=
|
||||
github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/cyw43439"
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/x/netdev"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed wifi.credentials
|
||||
credentials string
|
||||
// remove windows CRLF "\r\n" and trailing newline.
|
||||
credentialsNormalized = strings.TrimSuffix(strings.ReplaceAll(credentials, "\r\n", "\n"), "\n")
|
||||
globConnectParams ConnectParams
|
||||
poolCfg = xnet.TCPPoolConfig{
|
||||
PoolSize: 5,
|
||||
QueueSize: 5,
|
||||
TxBufSize: 2048,
|
||||
RxBufSize: 2048,
|
||||
NewBackoff: func() lneto.BackoffStrategy { return backoff },
|
||||
NanoTime: func() int64 { return time.Now().UnixNano() },
|
||||
EstablishedTimeout: 5 * time.Second,
|
||||
ClosingTimeout: 3 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(time.Second)
|
||||
ssid, password, ok := strings.Cut(credentialsNormalized, "\n")
|
||||
if !ok {
|
||||
fail("must write newline separated ssid/password in wifi.credentials", nil)
|
||||
}
|
||||
globConnectParams.SSID = ssid
|
||||
globConnectParams.Passphrase = password
|
||||
|
||||
dev := Netdev{
|
||||
dev: cyw43439.NewPicoWDevice(),
|
||||
}
|
||||
err := dev.dev.Init(cyw43439.DefaultWifiConfig())
|
||||
failIfErr("init cyw43439", err)
|
||||
hw, _ := dev.HardwareAddr6()
|
||||
var stack xnet.Netstack
|
||||
err = stack.Reset(xnet.StackConfig{
|
||||
RandSeed: time.Now().UnixNano() | 1,
|
||||
Hostname: "lneto-pico",
|
||||
MaxActiveTCPPorts: 4,
|
||||
MaxActiveUDPPorts: 4,
|
||||
ICMPQueueLimit: 1,
|
||||
MTU: 1500,
|
||||
HardwareAddress: hw,
|
||||
}, backoff, poolCfg)
|
||||
failIfErr("stack reset", err)
|
||||
err = stack.EnableICMP(true)
|
||||
failIfErr("icmp enable", err)
|
||||
err = dev.LinkConnect(globConnectParams)
|
||||
failIfErr("wifi join", err)
|
||||
|
||||
var iface netdev.Interface[ConnectParams]
|
||||
var runner netdev.Runner[ConnectParams]
|
||||
dev.LinkNotify(userNotify)
|
||||
err = iface.Init(&dev, &dev, netdev.InterfaceConfig{})
|
||||
failIfErr("init iface", err)
|
||||
|
||||
go func() {
|
||||
if err := runner.Run(context.Background(), iface, &stack, backoff); err != nil {
|
||||
failIfErr("runner", err)
|
||||
}
|
||||
}()
|
||||
assigned, gatewayRt, subnetBits, err := stack.EnableDHCP(context.Background(), true, netip.Addr{})
|
||||
failIfErr("enable dhcp", err)
|
||||
println("assigned=", assigned.String(), "gateway=", gatewayRt.String(), "subnet", subnetBits)
|
||||
select {}
|
||||
}
|
||||
|
||||
// compile-time guarantee of interface implementation.
|
||||
var _ lneto.BackoffStrategy = backoff
|
||||
var _ netdev.Stack = (*xnet.Netstack)(nil)
|
||||
|
||||
func backoff(consecutiveBackoffs uint) (sleepOrFlag time.Duration) {
|
||||
return 5 * time.Millisecond
|
||||
}
|
||||
|
||||
func userNotify(connected bool) (retries int, connectParams ConnectParams) {
|
||||
if !connected {
|
||||
return 1, globConnectParams
|
||||
}
|
||||
return 0, ConnectParams{}
|
||||
}
|
||||
|
||||
var _ netdev.DevEthernet = (*Netdev)(nil)
|
||||
var _ netdev.Netlink[ConnectParams] = (*Netdev)(nil)
|
||||
|
||||
type Netdev struct {
|
||||
dev *cyw43439.Device
|
||||
notifyCb netdev.NotifyCallback[ConnectParams]
|
||||
}
|
||||
|
||||
type ConnectParams struct {
|
||||
SSID string
|
||||
cyw43439.JoinOptions
|
||||
}
|
||||
|
||||
func (nl *Netdev) Netflags() (flags net.Flags) {
|
||||
flags |= net.FlagUp
|
||||
if nl.dev.IsLinkUp() {
|
||||
flags |= net.FlagRunning
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// LinkConnect implements [netdev.Netlink].
|
||||
func (nl *Netdev) LinkConnect(connectParams ConnectParams) error {
|
||||
return nl.dev.Join(connectParams.SSID, connectParams.JoinOptions)
|
||||
}
|
||||
|
||||
// LinkDisconnect implements [netdev.Netlink].
|
||||
func (nl *Netdev) LinkDisconnect() {
|
||||
// Not implemented by cyw43439 package.
|
||||
}
|
||||
|
||||
// LinkNotify implements [netdev.Netlink].
|
||||
func (nl *Netdev) LinkNotify(cb netdev.NotifyCallback[ConnectParams]) {
|
||||
nl.notifyCb = cb
|
||||
}
|
||||
|
||||
// HardwareAddr6 implements [netdev.DevEthernet].
|
||||
func (d *Netdev) HardwareAddr6() ([6]byte, error) {
|
||||
return d.dev.HardwareAddr6()
|
||||
}
|
||||
|
||||
// SendEthFrameOffset implements [netdev.DevEthernet].
|
||||
func (d *Netdev) SendOffsetEthFrame(offsetTxEthFrame []byte) error {
|
||||
return d.dev.SendEth(offsetTxEthFrame)
|
||||
}
|
||||
|
||||
// SetRecvHandler implements [netdev.DevEthernet].
|
||||
func (d *Netdev) SetEthRecvHandler(handler func(rxEthframe []byte)) {
|
||||
d.dev.RecvEthHandle(func(pkt []byte) error {
|
||||
handler(pkt)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// EthPoll implements [netdev.DevEthernet].
|
||||
func (d *Netdev) EthPoll(buf []byte) (ethFrameOff, ethernetBytes int, err error) {
|
||||
_, err = d.dev.PollOne()
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// MaxFrameSizeAndOffset implements [netdev.DevEthernet].
|
||||
func (d *Netdev) MaxFrameSizeAndOffset() (maxFrameSize int, frameOff int) {
|
||||
return cyw43439.MaxFrameSize, 0
|
||||
}
|
||||
func failIfErr(msg string, err error) {
|
||||
if err != nil {
|
||||
fail(msg, err)
|
||||
}
|
||||
println("PASS", msg)
|
||||
}
|
||||
func fail(msg string, err error) {
|
||||
var errstr string
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
for {
|
||||
println(msg, errstr)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
"github.com/soypat/lneto/internet/pcap"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
)
|
||||
@@ -134,8 +135,9 @@ func run() error {
|
||||
pfbuf = fmt.Appendf(pfbuf[:0], "%-3s %3d", context, len(pkt))
|
||||
pfbuf = append(pfbuf, ' ', '[')
|
||||
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, stack.Addr().AppendTo(nil), []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us"))
|
||||
addr := stack.Addr4()
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ipv4.AppendFormatAddr(nil, addr), []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us"))
|
||||
pfbuf = append(pfbuf, ']', '\n')
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -158,11 +160,11 @@ func run() error {
|
||||
} else if n != nwrite {
|
||||
log.Fatalf("mismatch written bytes %d!=%d", nwrite, n)
|
||||
}
|
||||
if flagMockClient && mockStack.Addr().IsValid() {
|
||||
if flagMockClient && mockStack.Addr4() != ([4]byte{}) {
|
||||
mockStack.IngressEthernet(buf[:nwrite])
|
||||
}
|
||||
}
|
||||
if flagMockClient && mockStack.Addr().IsValid() {
|
||||
if flagMockClient && mockStack.Addr4() != ([4]byte{}) {
|
||||
n, _ := mockStack.EgressEthernet(buf[:])
|
||||
if n > 0 {
|
||||
stack.IngressEthernet(buf[:n])
|
||||
@@ -199,7 +201,7 @@ func run() error {
|
||||
}()
|
||||
|
||||
// Create blocking + Berkeley stack
|
||||
blocking := stack.StackBlocking(5 * time.Millisecond)
|
||||
blocking := stack.StackBlocking(stackBackoff)
|
||||
berkeley := blocking.StackGo(xnet.StackGoConfig{
|
||||
ListenerPoolConfig: xnet.TCPPoolConfig{
|
||||
PoolSize: uint16(flagPoolSize),
|
||||
@@ -208,11 +210,12 @@ func run() error {
|
||||
RxBufSize: mtu,
|
||||
EstablishedTimeout: 5 * time.Second,
|
||||
ClosingTimeout: 5 * time.Second,
|
||||
NewBackoff: func() lneto.BackoffStrategy { return tcpBackoff },
|
||||
},
|
||||
})
|
||||
|
||||
// Perform DHCP to get address.
|
||||
rstack := stack.StackRetrying(5 * time.Millisecond)
|
||||
rstack := stack.StackRetrying(stackBackoff)
|
||||
const dhcpTimeout = 6 * time.Second
|
||||
const dhcpRetries = 2
|
||||
results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries)
|
||||
@@ -222,7 +225,7 @@ func run() error {
|
||||
if err = stack.AssimilateDHCPResults(results); err != nil {
|
||||
return fmt.Errorf("assimilating DHCP results: %w", err)
|
||||
}
|
||||
slog.Info("dhcp-complete", slog.String("assignedIP", results.AssignedAddr.String()), slog.String("routerIP", results.Router.String()))
|
||||
slog.Info("dhcp-complete", slog.String("assignedIP", string(ipv4.AppendFormatAddr(nil, results.AssignedAddr4))), slog.String("routerIP", results.Router.String()))
|
||||
|
||||
// Resolve router HW and set gateway
|
||||
routerHw, err := rstack.DoResolveHardwareAddress6(results.Router, 2*time.Second, 2)
|
||||
@@ -230,7 +233,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))
|
||||
@@ -246,7 +249,7 @@ func run() error {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
fmt.Printf("Listening (Berkeley) on %s:%d\n", stack.Addr().String(), flagPort)
|
||||
fmt.Printf("Listening (Berkeley) on %s:%d\n", string(ipv4.AppendFormatAddr(nil, stack.Addr4())), flagPort)
|
||||
|
||||
// Optionally run an in-memory mock client that dials the berkeley listener
|
||||
if flagMockClient {
|
||||
@@ -326,11 +329,11 @@ func tryPoll(iface ltesto.Interface, poll time.Duration) (dataMayBeReady bool, _
|
||||
}
|
||||
|
||||
func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) {
|
||||
target := netip.AddrPortFrom(stack.Addr(), port)
|
||||
target := netip.AddrPortFrom(netip.AddrFrom4(stack.Addr4()), port)
|
||||
err := mockStack.Reset(xnet.StackConfig{
|
||||
StaticAddress: subnet.Addr().Next(),
|
||||
StaticAddress4: subnet.Addr().Next().As4(),
|
||||
MaxActiveTCPPorts: 1,
|
||||
HardwareAddress: stack.Gateway6(),
|
||||
HardwareAddress: stack.GatewayHardwareAddr(),
|
||||
Hostname: "the-other",
|
||||
MTU: uint16(stack.MTU()),
|
||||
RandSeed: int64(stack.Prand32()),
|
||||
@@ -344,6 +347,7 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) {
|
||||
TxBuf: make([]byte, 2048),
|
||||
TxPacketQueueSize: 4,
|
||||
Logger: slog.Default(),
|
||||
RWBackoff: tcpBackoff,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
@@ -368,7 +372,7 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) {
|
||||
hdr.SetMethod("GET")
|
||||
hdr.SetRequestURI("/")
|
||||
hdr.SetProtocol("HTTP/1.1")
|
||||
hdr.Set("Host", stack.Addr().String())
|
||||
hdr.Set("Host", string(ipv4.AppendFormatAddr(nil, stack.Addr4())))
|
||||
hdr.Set("User-Agent", "lneto-mock")
|
||||
hdr.Set("Connection", "close")
|
||||
req, err := hdr.AppendRequest(nil)
|
||||
@@ -399,3 +403,22 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) {
|
||||
fmt.Println("mockclient: received response:\n", string(page))
|
||||
mockConn.Close()
|
||||
}
|
||||
|
||||
func stackBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
if consecutiveBackoffs < 10 {
|
||||
return time.Millisecond
|
||||
}
|
||||
return 10 * time.Millisecond
|
||||
}
|
||||
|
||||
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
const (
|
||||
minWait = uint32(time.Microsecond)
|
||||
maxWait = 5 * uint32(time.Millisecond)
|
||||
maxShift = 22
|
||||
_overflowCheck = minWait << maxShift
|
||||
)
|
||||
shifted := minWait << min(consecutiveBackoffs, maxShift)
|
||||
wait := min(shifted, maxWait)
|
||||
return time.Duration(wait)
|
||||
}
|
||||
|
||||
@@ -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, " | "))
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
"github.com/soypat/lneto/internet/pcap"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
)
|
||||
@@ -134,8 +135,9 @@ func run() (err error) {
|
||||
pfbuf = fmt.Appendf(pfbuf[:0], "%-3s %3d", context, len(pkt))
|
||||
pfbuf = append(pfbuf, ' ', '[')
|
||||
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, stack.Addr().AppendTo(nil), []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us"))
|
||||
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ipv4.AppendFormatAddr(nil, stack.Addr4()), []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us"))
|
||||
pfbuf = append(pfbuf, ']', '\n')
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -190,7 +192,7 @@ func run() (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
rstack := stack.StackRetrying(5 * time.Millisecond)
|
||||
rstack := stack.StackRetrying(stackBackoff)
|
||||
|
||||
const (
|
||||
dhcpTimeout = 6 * time.Second
|
||||
@@ -206,7 +208,7 @@ func run() (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("assimilating DHCP results: %w", err)
|
||||
}
|
||||
slog.Info("dhcp-complete", slog.String("assignedIP", results.AssignedAddr.String()), slog.String("routerIP", results.Router.String()))
|
||||
slog.Info("dhcp-complete", slog.String("assignedIP", string(ipv4.AppendFormatAddr(nil, results.AssignedAddr4))), slog.String("routerIP", results.Router.String()))
|
||||
|
||||
const (
|
||||
arpTimeout = 2 * time.Second
|
||||
@@ -218,10 +220,10 @@ 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", stack.Addr().String(), svPort)
|
||||
fmt.Printf("Listening on %s:%d\n", ipv4.AppendFormatAddr(nil, stack.Addr4()), svPort)
|
||||
|
||||
// Serve connections in a loop.
|
||||
for {
|
||||
@@ -230,8 +232,9 @@ func run() (err error) {
|
||||
RxBuf: make([]byte, mtu),
|
||||
TxBuf: make([]byte, mtu),
|
||||
TxPacketQueueSize: 3,
|
||||
RWBackoff: tcpBackoff,
|
||||
})
|
||||
err = stack.ListenTCP(&conn, svPort)
|
||||
err = stack.ListenTCP4(&conn, svPort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen TCP: %w", err)
|
||||
}
|
||||
@@ -354,3 +357,22 @@ func tryPoll(iface ltesto.Interface, poll time.Duration) (dataMayBeReady bool, _
|
||||
dataMayBeReady = true
|
||||
return dataMayBeReady, nil
|
||||
}
|
||||
|
||||
func stackBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
if consecutiveBackoffs < 10 {
|
||||
return time.Millisecond
|
||||
}
|
||||
return 10 * time.Millisecond
|
||||
}
|
||||
|
||||
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
const (
|
||||
minWait = uint32(time.Microsecond)
|
||||
maxWait = 5 * uint32(time.Millisecond)
|
||||
maxShift = 22
|
||||
_overflowCheck = minWait << maxShift
|
||||
)
|
||||
shifted := minWait << min(consecutiveBackoffs, maxShift)
|
||||
wait := min(shifted, maxWait)
|
||||
return time.Duration(wait)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ package main
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/dhcpv4"
|
||||
"github.com/soypat/lneto/dhcp/dhcpv4"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/udp"
|
||||
@@ -53,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,
|
||||
@@ -89,6 +91,12 @@ func (d *dhcpInterceptor) Write(b []byte) (int, error) {
|
||||
return len(b), nil // Consumed by DHCP server, don't forward.
|
||||
}
|
||||
d.rewriteEthernetDst(b)
|
||||
if len(b) >= sizeEthernet+sizeIPv4 &&
|
||||
*(*[6]byte)(b[0:6]) == d.svMAC &&
|
||||
binary.BigEndian.Uint16(b[12:14]) == uint16(ethernet.TypeIPv4) {
|
||||
dstIP := *(*[4]byte)(b[sizeEthernet+16 : sizeEthernet+20])
|
||||
internal.LogAttrs(slog.Default(), slog.LevelWarn, "ethernet-self-loop", internal.SlogAddr4("dstIP", &dstIP))
|
||||
}
|
||||
return d.inner.Write(b)
|
||||
}
|
||||
|
||||
@@ -308,6 +316,8 @@ func (d *dhcpInterceptor) rewriteEthernetDst(b []byte) {
|
||||
d.mu.Unlock()
|
||||
if ok {
|
||||
copy(b[0:6], mac[:])
|
||||
} else {
|
||||
internal.LogAttrs(slog.Default(), slog.LevelWarn, "gateway-rewrite-miss", internal.SlogAddr4("dstIP", &dstIP))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -85,8 +88,9 @@ func run() error {
|
||||
}
|
||||
defer sv.Close()
|
||||
var cap pcap.PacketBreakdown
|
||||
cap.SubfieldLimit = 30 // For big DNS or DHCP.
|
||||
pf := pcap.Formatter{
|
||||
FilterClasses: []pcap.FieldClass{pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassSize, pcap.FieldClassFlags},
|
||||
FilterClasses: []pcap.FieldClass{pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassSize, pcap.FieldClassFlags, pcap.FieldClassDNSName},
|
||||
}
|
||||
var pfbuf []byte
|
||||
sv.OnTransfer(func(channel int, pkt []byte) {
|
||||
|
||||
@@ -79,7 +79,7 @@ func run(ctx context.Context, stack *xnet.StackAsync) error {
|
||||
// Other option is to instead use async API which leads to more verbose
|
||||
// and more stateful code.
|
||||
go stackLoop(ctx, stack)
|
||||
rstack := stack.StackRetrying(pollTime)
|
||||
rstack := stack.StackRetrying(stackBackoff)
|
||||
results, err := rstack.DoDHCPv4([4]byte{}, protoTimeout, protoRetries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("doing DHCP: %w", err)
|
||||
@@ -92,8 +92,8 @@ func run(ctx context.Context, stack *xnet.StackAsync) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving router MAC: %w", err)
|
||||
}
|
||||
stack.SetGateway6(gateway)
|
||||
berkstack := stack.StackBlocking(pollTime).StackGo(xnet.StackGoConfig{
|
||||
stack.SetGatewayHardwareAddr(gateway)
|
||||
berkstack := stack.StackBlocking(stackBackoff).StackGo(xnet.StackGoConfig{
|
||||
ListenerPoolConfig: xnet.TCPPoolConfig{
|
||||
PoolSize: tcpConnPoolSize,
|
||||
QueueSize: tcpPacketQueueSize,
|
||||
@@ -102,10 +102,11 @@ func run(ctx context.Context, stack *xnet.StackAsync) error {
|
||||
NanoTime: nanotime,
|
||||
EstablishedTimeout: tcpEstablishedTimeout,
|
||||
ClosingTimeout: tcpCloseTimeout,
|
||||
NewBackoff: func() lneto.BackoffStrategy { return tcpBackoff },
|
||||
},
|
||||
})
|
||||
|
||||
laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(results.AssignedAddr, 80))
|
||||
laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4(results.AssignedAddr4), 80))
|
||||
// raddr := net.TCPAddr{} // If active (client) connection then set raddr in which case a net.Conn type is returned.
|
||||
const sockstream = 0x1
|
||||
c, err := berkstack.Socket(ctx, "tcp", syscall.AF_INET, sockstream, laddr, nil)
|
||||
@@ -147,7 +148,7 @@ func stackLoop(ctx context.Context, stack *xnet.StackAsync) {
|
||||
fmt.Println("encaps err:", err)
|
||||
} else if nwrite > 0 {
|
||||
network.SendEth(buf[:nwrite])
|
||||
cap.PrintPacket("OUT", buf[:nwrite])
|
||||
cap.PrintEthernet("OUT", buf[:nwrite])
|
||||
}
|
||||
nread, err := network.RecvEth(buf[:])
|
||||
if err != nil {
|
||||
@@ -157,7 +158,7 @@ func stackLoop(ctx context.Context, stack *xnet.StackAsync) {
|
||||
if err != nil && err != lneto.ErrPacketDrop {
|
||||
fmt.Println("demux err:", err)
|
||||
} else {
|
||||
cap.PrintPacket("IN ", buf[:nread])
|
||||
cap.PrintEthernet("IN ", buf[:nread])
|
||||
}
|
||||
}
|
||||
if nwrite == 0 && nread == 0 {
|
||||
@@ -171,3 +172,22 @@ func must(err error) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func stackBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
if consecutiveBackoffs < 10 {
|
||||
return time.Millisecond
|
||||
}
|
||||
return 10 * time.Millisecond
|
||||
}
|
||||
|
||||
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
const (
|
||||
minWait = uint32(time.Microsecond)
|
||||
maxWait = 5 * uint32(time.Millisecond)
|
||||
maxShift = 22
|
||||
_overflowCheck = minWait << maxShift
|
||||
)
|
||||
shifted := minWait << min(consecutiveBackoffs, maxShift)
|
||||
wait := min(shifted, maxWait)
|
||||
return time.Duration(wait)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Command ntp-client performs a two-exchange NTP clock synchronization against
|
||||
// a remote server and prints the corrected time, clock offset, and round-trip
|
||||
// delay.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./examples/ntp-client/ -server pool.ntp.org:123
|
||||
// go run ./examples/ntp-client/ -server 127.0.0.1:10123 -debug
|
||||
//
|
||||
// This tool uses the standard library net package for UDP transport instead of
|
||||
// lneto's own networking stack. These examples exercise one protocol layer at a
|
||||
// time in isolation, keeping the transport concern separate so failures are
|
||||
// clearly attributable to the NTP codec and state machine rather than the
|
||||
// full-stack IP/UDP path.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/ntp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
addr := flag.String("server", "pool.ntp.org:123", "NTP server address (host:port)")
|
||||
debug := flag.Bool("debug", false, "enable debug logging")
|
||||
flag.Parse()
|
||||
|
||||
conn, err := net.DialTimeout("udp", *addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var precBuf [64]int64
|
||||
sysprec := ntp.CalculateSystemPrecision(nil, precBuf[:])
|
||||
|
||||
var client ntp.Client
|
||||
client.Reset(sysprec, time.Now)
|
||||
if *debug {
|
||||
client.SetLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||
}
|
||||
|
||||
const maxRetries = 10
|
||||
var buf [1500]byte
|
||||
for attempt := 0; !client.IsDone() && attempt < maxRetries; attempt++ {
|
||||
n, err := client.Encapsulate(buf[:ntp.SizeHeader], 0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encapsulate: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("encapsulate returned 0 bytes unexpectedly")
|
||||
}
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err = conn.Write(buf[:n]); err != nil {
|
||||
return fmt.Errorf("write: %w", err)
|
||||
}
|
||||
|
||||
rn, err := conn.Read(buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read: %w", err)
|
||||
}
|
||||
|
||||
if err = client.Demux(buf[:rn], 0); err != nil {
|
||||
return fmt.Errorf("demux: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !client.IsDone() {
|
||||
return fmt.Errorf("NTP exchange did not complete within %d attempts", maxRetries)
|
||||
}
|
||||
|
||||
fmt.Printf("NTP time: %s\n", client.Now().Format(time.RFC3339Nano))
|
||||
fmt.Printf("Offset: %s\n", client.Offset())
|
||||
fmt.Printf("RTD: %s\n", client.RoundTripDelay())
|
||||
fmt.Printf("Stratum: %s\n", client.ServerStratum())
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Command ntp-server is a minimal NTP server that listens for client requests
|
||||
// on a UDP socket and responds with the current system time. It serves as an
|
||||
// integration test target for the ntp-client example.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./examples/ntp-server/ -addr :10123
|
||||
//
|
||||
// The listen address defaults to :123 (requires root).
|
||||
//
|
||||
// This tool uses the standard library net package for UDP transport instead of
|
||||
// lneto's own networking stack. These examples exercise one protocol layer at a
|
||||
// time in isolation, keeping the transport concern separate so failures are
|
||||
// clearly attributable to the NTP codec and state machine rather than the
|
||||
// full-stack IP/UDP path.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/ntp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
listenAddr := flag.String("addr", ":123", "UDP listen address (host:port)")
|
||||
flag.Parse()
|
||||
|
||||
pc, err := net.ListenPacket("udp", *listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
defer pc.Close()
|
||||
fmt.Printf("NTP server listening on %s\n", pc.LocalAddr())
|
||||
|
||||
var precBuf [64]int64
|
||||
sysprec := ntp.CalculateSystemPrecision(nil, precBuf[:])
|
||||
|
||||
var handler ntp.Server
|
||||
err = handler.Reset(ntp.ServerConfig{
|
||||
Now: time.Now,
|
||||
Stratum: ntp.StratumPrimary,
|
||||
Precision: sysprec,
|
||||
RefID: [4]byte{'G', 'O', 'L', 'N'},
|
||||
MaxPending: 16,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("server reset: %w", err)
|
||||
}
|
||||
|
||||
var buf [1500]byte
|
||||
var backoff uint
|
||||
for {
|
||||
pc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
n, raddr, err := pc.ReadFrom(buf[:])
|
||||
if err != nil {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
backoff++
|
||||
if backoff > 10 {
|
||||
time.Sleep(time.Duration(backoff) * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
fmt.Printf("read error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
backoff = 0
|
||||
|
||||
if err = handler.Demux(buf[:n], 0); err != nil {
|
||||
fmt.Printf("demux error from %s: %v\n", raddr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var resp [ntp.SizeHeader]byte
|
||||
rn, err := handler.Encapsulate(resp[:], 0, 0)
|
||||
if err != nil {
|
||||
fmt.Printf("encapsulate error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if rn > 0 {
|
||||
if _, err = pc.WriteTo(resp[:rn], raddr); err != nil {
|
||||
fmt.Printf("write error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-6
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
"github.com/soypat/lneto/internet/pcap"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/x/xnet"
|
||||
)
|
||||
@@ -136,9 +137,11 @@ func run() (err error) {
|
||||
lastAction := time.Now()
|
||||
buf := make([]byte, math.MaxUint16) // Generic-receive Offload (GRO) can aggregate packets.
|
||||
var cap pcap.PacketBreakdown
|
||||
cap.SubfieldLimit = 30 // For chunky DNS.
|
||||
var frames []pcap.Frame
|
||||
pf := pcap.Formatter{
|
||||
FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress, pcap.FieldClassTimestamp},
|
||||
FilterClasses: []pcap.FieldClass{pcap.FieldClassFlags, pcap.FieldClassOperation, pcap.FieldClassDst, pcap.FieldClassSrc, pcap.FieldClassAddress, pcap.FieldClassTimestamp, pcap.FieldClassDNSName},
|
||||
SubfieldLimit: 30,
|
||||
}
|
||||
var pfbuf []byte
|
||||
logFrames := func(context string, pkt []byte) error {
|
||||
@@ -154,8 +157,9 @@ func run() (err error) {
|
||||
pfbuf = fmt.Appendf(pfbuf[:0], "%-3s %3d", context, len(pkt))
|
||||
pfbuf = append(pfbuf, ' ', '[')
|
||||
pfbuf, err = pf.FormatFrames(pfbuf, frames, pkt)
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, stack.Addr().AppendTo(nil), []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddress()), []byte("us"))
|
||||
addr := stack.Addr4()
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, addr[:], []byte("us"))
|
||||
pfbuf = bytes.ReplaceAll(pfbuf, ethernet.AppendAddr(nil, stack.HardwareAddr()), []byte("us"))
|
||||
pfbuf = append(pfbuf, ']', '\n')
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -212,7 +216,7 @@ func run() (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
rstack := stack.StackRetrying(5 * time.Millisecond)
|
||||
rstack := stack.StackRetrying(stackBackoff)
|
||||
|
||||
const (
|
||||
dhcpTimeout = 6 * time.Second
|
||||
@@ -229,7 +233,7 @@ func run() (err error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("assimilating DHCP results: %w", err)
|
||||
}
|
||||
slog.Info("dhcp-complete", slog.String("assignedIP", results.AssignedAddr.String()), slog.String("routerIP", results.Router.String()), slog.Any("DNS", results.DNSServers), slog.Any("subnet", results.Subnet.String()))
|
||||
slog.Info("dhcp-complete", slog.String("assignedIP", string(ipv4.AppendFormatAddr(nil, results.AssignedAddr4))), slog.String("routerIP", results.Router.String()), slog.Any("DNS", results.DNSServers), slog.Any("subnet", results.Subnet.String()))
|
||||
const (
|
||||
arpTimeout = 2 * time.Second
|
||||
arpRetries = 2
|
||||
@@ -244,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"
|
||||
@@ -277,6 +281,7 @@ func run() (err error) {
|
||||
RxBuf: make([]byte, mtu),
|
||||
TxBuf: make([]byte, mtu),
|
||||
TxPacketQueueSize: 3,
|
||||
RWBackoff: tcpBackoff,
|
||||
})
|
||||
|
||||
timeHTTPCreate := timer("create HTTP GET request")
|
||||
@@ -382,3 +387,21 @@ func tryPoll(iface ltesto.Interface, poll time.Duration) (dataMayBeReady bool, _
|
||||
dataMayBeReady = true
|
||||
return dataMayBeReady, nil
|
||||
}
|
||||
|
||||
func stackBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
if consecutiveBackoffs < 10 {
|
||||
return time.Millisecond
|
||||
}
|
||||
return 10 * time.Millisecond
|
||||
}
|
||||
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
|
||||
const (
|
||||
minWait = uint32(time.Microsecond)
|
||||
maxWait = 5 * uint32(time.Millisecond)
|
||||
maxShift = 22
|
||||
_overflowCheck = minWait << maxShift
|
||||
)
|
||||
shifted := minWait << min(consecutiveBackoffs, maxShift)
|
||||
wait := min(shifted, maxWait)
|
||||
return time.Duration(wait)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (c *Cookie) Parse() error {
|
||||
|
||||
func (c *Cookie) ForEach(cb func(key, value []byte) error) error {
|
||||
nc := len(c.kvs)
|
||||
for i := 0; i < nc; i++ {
|
||||
for i := range nc {
|
||||
kv := c.kvs[i]
|
||||
key := tok2bytes(c.buf, kv.key)
|
||||
value := tok2bytes(c.buf, kv.value)
|
||||
@@ -91,7 +91,7 @@ func (c *Cookie) ForEach(cb func(key, value []byte) error) error {
|
||||
// Get gets a cookie's value from its key. Use HasValueOrKey to check if a key or single-valued cookie is present in the cookie.
|
||||
func (c *Cookie) Get(key string) []byte {
|
||||
nc := len(c.kvs)
|
||||
for i := 0; i < nc; i++ {
|
||||
for i := range nc {
|
||||
kv := c.kvs[i]
|
||||
if b2s(tok2bytes(c.buf, kv.key)) == key {
|
||||
return tok2bytes(c.buf, kv.value)
|
||||
@@ -102,7 +102,7 @@ func (c *Cookie) Get(key string) []byte {
|
||||
|
||||
func (c *Cookie) HasKeyOrSingleValue(keyOrSingleValue string) bool {
|
||||
nc := len(c.kvs)
|
||||
for i := 0; i < nc; i++ {
|
||||
for i := range nc {
|
||||
kv := c.kvs[i]
|
||||
if kv.key.len == 0 && b2s(tok2bytes(c.buf, kv.value)) == keyOrSingleValue ||
|
||||
b2s(tok2bytes(c.buf, kv.key)) == keyOrSingleValue {
|
||||
@@ -159,7 +159,7 @@ func (c *Cookie) String() string {
|
||||
// AppendKeyValues appends the HTTP header value of the cookie expected after the "Cookie:" string. Does not include trailing \r\n's.
|
||||
func (c *Cookie) AppendKeyValues(dst []byte) []byte {
|
||||
nc := len(c.kvs)
|
||||
for i := 0; i < nc; i++ {
|
||||
for i := range nc {
|
||||
kv := c.kvs[i]
|
||||
key := tok2bytes(c.buf, kv.key)
|
||||
value := tok2bytes(c.buf, kv.value)
|
||||
|
||||
@@ -199,7 +199,7 @@ func (h *Header) ForEach(cb func(key, value []byte) error) error {
|
||||
|
||||
func (hb *headerBuf) forEach(cb func(key, value []byte) error) error {
|
||||
nh := len(hb.headers)
|
||||
for i := 0; i < nh; i++ {
|
||||
for i := range nh {
|
||||
kv := hb.headers[i]
|
||||
if !kv.isValid() {
|
||||
continue
|
||||
|
||||
@@ -112,13 +112,19 @@ func BenchmarkParseBytes(b *testing.B) {
|
||||
asRequest = false
|
||||
)
|
||||
req, _ := http.NewRequest(wantMethod, wantURI, strings.NewReader(wantMessage))
|
||||
var buf bytes.Buffer
|
||||
req.Write(&buf)
|
||||
data := buf.Bytes()
|
||||
var rawBuf bytes.Buffer
|
||||
req.Write(&rawBuf)
|
||||
data := rawBuf.Bytes()
|
||||
|
||||
// hdr is declared outside the loop so that ParseBytes can reuse the
|
||||
// backing arrays on Reset (headers slice and data buffer) without
|
||||
// allocating on every iteration. Declaring it inside the loop causes
|
||||
// two allocs per iteration: one for the headers slice (make in reset)
|
||||
// and one for the data buffer (append in readFromBytes).
|
||||
var hdr Header
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var hdr Header
|
||||
err := hdr.ParseBytes(asRequest, data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
||||
@@ -120,7 +120,13 @@ func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) }
|
||||
func (hb *headerBuf) parseNextHeaders(ss *scannerState) {
|
||||
debuglog("http:nexthdr:loop")
|
||||
for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) {
|
||||
hb.headers = append(hb.headers, kv) // TODO(HEAP): inc=16B slice growth when capacity exceeded
|
||||
if len(hb.headers) == cap(hb.headers) {
|
||||
// Refuse to grow the headers slice: caller must pre-allocate
|
||||
// sufficient capacity via reset or use a larger initial size.
|
||||
ss.err = errOOM
|
||||
return
|
||||
}
|
||||
hb.headers = append(hb.headers, kv)
|
||||
}
|
||||
debuglog("http:nexthdr:done")
|
||||
}
|
||||
|
||||
@@ -533,10 +533,7 @@ func TestParseRequest_InvalidHeaderSpaceBeforeColon(t *testing.T) {
|
||||
func splitInto(s string, n int) []string {
|
||||
var chunks []string
|
||||
for len(s) > 0 {
|
||||
end := n
|
||||
if end > len(s) {
|
||||
end = len(s)
|
||||
}
|
||||
end := min(n, len(s))
|
||||
chunks = append(chunks, s[:end])
|
||||
s = s[end:]
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package internal
|
||||
|
||||
import "time"
|
||||
|
||||
type BackoffFlags uint8
|
||||
|
||||
const (
|
||||
BackoffHasPriority BackoffFlags = 1 << iota
|
||||
BackoffCriticalPath
|
||||
BackoffTCPConn
|
||||
)
|
||||
|
||||
const backoffMinWait = time.Microsecond
|
||||
|
||||
func backoffMaxWait(priority BackoffFlags) time.Duration {
|
||||
switch {
|
||||
case priority&BackoffCriticalPath != 0:
|
||||
return 1 * time.Millisecond
|
||||
case priority&BackoffTCPConn != 0:
|
||||
return 5 * time.Millisecond
|
||||
default:
|
||||
return time.Second >> (priority & BackoffHasPriority)
|
||||
}
|
||||
}
|
||||
|
||||
func NewBackoff(priority BackoffFlags) Backoff {
|
||||
return Backoff{
|
||||
wait: uint32(backoffMinWait),
|
||||
maxWait: uint32(backoffMaxWait(priority)),
|
||||
startWait: uint32(backoffMinWait),
|
||||
}
|
||||
}
|
||||
|
||||
// A Backoff with a non-zero MaxWait is ready for use.
|
||||
type Backoff struct {
|
||||
// wait defines the amount of time that Miss will wait on next call.
|
||||
wait uint32
|
||||
// Maximum allowable value for Wait.
|
||||
maxWait uint32
|
||||
// startWait is the intial Wait value, as well as the value that Wait takes after a call to Hit.
|
||||
startWait uint32
|
||||
}
|
||||
|
||||
// Hit sets eb.Wait to the StartWait value.
|
||||
func (eb *Backoff) Hit() {
|
||||
if eb.maxWait == 0 {
|
||||
panic("MaxWait cannot be zero")
|
||||
}
|
||||
eb.wait = eb.startWait
|
||||
}
|
||||
|
||||
// Miss sleeps for eb.Wait and increases eb.Wait exponentially.
|
||||
func (eb *Backoff) Miss() {
|
||||
if eb.maxWait == 0 {
|
||||
panic("MaxWait cannot be zero")
|
||||
}
|
||||
time.Sleep(time.Duration(eb.wait))
|
||||
eb.wait *= 2
|
||||
if eb.wait > eb.maxWait {
|
||||
eb.wait = eb.maxWait
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,18 @@ func GetIPAddr(buf []byte) (src, dst []byte, id, ipEndOff uint16, err error) {
|
||||
return src, dst, id, ipEndOff, err
|
||||
}
|
||||
|
||||
// IsMulticastIPAddr reports whether addr is an IPv4 or IPv6 multicast address.
|
||||
func IsMulticastIPAddr(addr []byte) bool {
|
||||
switch len(addr) {
|
||||
case 4:
|
||||
return addr[0]&0xf0 == 0xe0
|
||||
case 16:
|
||||
return addr[0] == 0xff
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func SetIPAddrs(buf []byte, id uint16, src, dst []byte) (err error) {
|
||||
var dstaddr, srcaddr []byte
|
||||
version := buf[0] >> 4
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package internal
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsMulticastIPAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
addr []byte
|
||||
want bool
|
||||
}{
|
||||
{"ipv4 multicast", []byte{224, 0, 0, 1}, true},
|
||||
{"ipv4 multicast upper", []byte{239, 255, 255, 255}, true},
|
||||
{"ipv4 unicast", []byte{192, 0, 2, 1}, false},
|
||||
{"ipv6 multicast", []byte{0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, true},
|
||||
{"ipv6 unicast", []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, false},
|
||||
{"invalid length", []byte{224}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsMulticastIPAddr(tt.addr); got != tt.want {
|
||||
t.Fatalf("got %t; want %t", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -489,11 +489,8 @@ func mutateTCPOptions(opts []byte, seed int64, variant int64) int64 {
|
||||
}
|
||||
case 4: // SACK with garbage block data.
|
||||
if len(opts) >= 10 {
|
||||
opts[0] = byte(tcp.OptSACK) // kind=5
|
||||
sackLen := len(opts)
|
||||
if sackLen > 34 {
|
||||
sackLen = 34 // max 4 SACK blocks
|
||||
}
|
||||
opts[0] = byte(tcp.OptSACK) // kind=5
|
||||
sackLen := min(len(opts), 34) // max 4 SACK blocks
|
||||
opts[1] = byte(sackLen)
|
||||
for i := 2; i < sackLen; i++ {
|
||||
opts[i] = byte(seed)
|
||||
|
||||
+2
-16
@@ -69,7 +69,7 @@ func (r *Ring) Write(b []byte) (int, error) {
|
||||
n := copy(r.Buf[r.End:r.Off], b)
|
||||
r.End += n
|
||||
if r.End <= 0 {
|
||||
panic("zero end after write") // TODO: remove panics after validation.
|
||||
panic("zero end after write") // invariant: End must be >0 after writing into midFree region
|
||||
}
|
||||
return n, nil
|
||||
} else if r.End == 0 {
|
||||
@@ -87,7 +87,7 @@ func (r *Ring) Write(b []byte) (int, error) {
|
||||
n += n2
|
||||
}
|
||||
if r.End <= 0 {
|
||||
panic("zero end after write")
|
||||
panic("zero end after write") // invariant: End must be >0 after appending to the tail region
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
@@ -271,20 +271,6 @@ func (r *Ring) addOff(a, b int) int {
|
||||
return result
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (r *Ring) string() string {
|
||||
var b bytes.Buffer
|
||||
r2 := *r
|
||||
|
||||
+13
-13
@@ -20,7 +20,7 @@ func TestRing(t *testing.T) {
|
||||
}
|
||||
const data = "hello"
|
||||
// Set random data and write some more and read it back.
|
||||
for i := 0; i < 32; i++ {
|
||||
for i := range 32 {
|
||||
nfirst := max(1, rng.Intn(bufSize)/2)
|
||||
nsecond := max(1, rng.Intn(bufSize)/2)
|
||||
if nfirst+nsecond > bufSize {
|
||||
@@ -59,7 +59,7 @@ func TestRing(t *testing.T) {
|
||||
var zeros [bufSize]byte
|
||||
|
||||
// Set random data and write some more and read it back with ReadAt and ReadPeek and ReadDiscard.
|
||||
for i := 0; i < 32; i++ {
|
||||
for range 32 {
|
||||
nfirst := rng.Intn(len(data))/2 + 1 // write garbage data first.
|
||||
nsecond := rng.Intn(len(data))/2 + 1
|
||||
if nfirst+nsecond > bufSize {
|
||||
@@ -72,7 +72,7 @@ func TestRing(t *testing.T) {
|
||||
content = append(content, data[:nsecond]...)
|
||||
setRingData(t, r, randOff, content)
|
||||
// Two-tap ReadPeek to make sure pointer not advanced.
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
n, err = r.ReadPeek(readback[:])
|
||||
if err != nil && err != io.EOF {
|
||||
t.Fatal("read failed", err)
|
||||
@@ -87,7 +87,7 @@ func TestRing(t *testing.T) {
|
||||
}
|
||||
|
||||
// Two-tap ReadAt to make sure pointer not advanced.
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
off := rng.Intn(nfirst + nsecond)
|
||||
n, err = r.ReadAt(readback[:nfirst+nsecond-off], int64(off))
|
||||
|
||||
@@ -135,7 +135,7 @@ func TestRing2(t *testing.T) {
|
||||
ringbuf := make([]byte, maxsize)
|
||||
auxbuf := make([]byte, maxsize)
|
||||
rng.Read(data)
|
||||
for i := 0; i < ntests; i++ {
|
||||
for i := range ntests {
|
||||
dsize := max(rng.Intn(len(data)), 1)
|
||||
if !testRing1_loopback(t, rng, ringbuf, data[:dsize], auxbuf) {
|
||||
t.Fatalf("failed test %d", i)
|
||||
@@ -156,7 +156,7 @@ func TestRingEmpty(t *testing.T) {
|
||||
for _, isonReadEndCalled := range []bool{false, true} {
|
||||
name := fmt.Sprintf("reset=%v readend=%v", isResetCalled, isonReadEndCalled)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for off := 0; off < bufSize+1; off++ {
|
||||
for off := range bufSize + 1 {
|
||||
r.End = 0
|
||||
r.Off = off
|
||||
if isResetCalled {
|
||||
@@ -201,7 +201,7 @@ func TestRingNonEmpty(t *testing.T) {
|
||||
name := fmt.Sprintf("readend=%v checkWrite=%v checkRead=%v", isonReadEndCalled, checkWrite, checkRead)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for end := 1; end < bufSize+1; end++ {
|
||||
for off := 0; off < bufSize+1; off++ {
|
||||
for off := range bufSize + 1 {
|
||||
r.End = end
|
||||
r.Off = off
|
||||
buf := r.Buffered()
|
||||
@@ -251,7 +251,7 @@ func TestRing_OffWrite(t *testing.T) {
|
||||
var rawbuf, auxbuf, readback [bufSize]byte
|
||||
r := &Ring{Buf: rawbuf[:]}
|
||||
for n := 1; n < bufSize+1; n++ {
|
||||
for off := 0; off < bufSize+1; off++ {
|
||||
for off := range bufSize + 1 {
|
||||
r.Off = off // Start write at off.
|
||||
r.End = 0 // Reset to use no data.
|
||||
for i := 0; i < n; i++ {
|
||||
@@ -288,7 +288,7 @@ func TestRing_TwoWrite(t *testing.T) {
|
||||
var rawbuf, auxbuf, readback [bufSize]byte
|
||||
r := &Ring{Buf: rawbuf[:]}
|
||||
|
||||
for i := 0; i < 1024; i++ {
|
||||
for range 1024 {
|
||||
n1 := rng.Intn(bufSize-1) + 1 // leave space for one more write
|
||||
n2 := rng.Intn(bufSize-n1) + 1
|
||||
off := rng.Intn(bufSize + 1)
|
||||
@@ -319,8 +319,8 @@ func TestRingOverwrite(t *testing.T) {
|
||||
const bufSize = 8
|
||||
var rawbuf, auxbuf [bufSize]byte
|
||||
r := &Ring{Buf: rawbuf[:]}
|
||||
for off := 0; off < bufSize+1; off++ {
|
||||
for buf := 0; buf < bufSize+1; buf++ {
|
||||
for off := range bufSize + 1 {
|
||||
for buf := range bufSize + 1 {
|
||||
setRingData(t, r, off, rawbuf[:buf])
|
||||
// Select write size overwriting data.
|
||||
for osz := bufSize - buf + 1; osz < bufSize+1; osz++ {
|
||||
@@ -403,7 +403,7 @@ func TestRing_findcrash(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(0))
|
||||
data := make([]byte, maxsize)
|
||||
|
||||
for i := 0; i < ntests; i++ {
|
||||
for i := range ntests {
|
||||
free := r.Free()
|
||||
if free < 0 {
|
||||
t.Fatal("free < 0")
|
||||
@@ -448,7 +448,7 @@ func TestRingFreeLimited(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
buffer := make([]byte, n)
|
||||
r := Ring{Buf: buffer}
|
||||
for itest := 0; itest < 100000; itest++ {
|
||||
for itest := range 100000 {
|
||||
r.Off = rng.Intn(n)
|
||||
r.End = rng.Intn(n + 1)
|
||||
limit := rng.Intn(n)
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ func DeleteZeroed[T comparable](a []T) []T {
|
||||
var z T
|
||||
off := 0
|
||||
deleted := false
|
||||
for i := 0; i < len(a); i++ {
|
||||
for i := range a {
|
||||
if a[i] != z {
|
||||
if deleted {
|
||||
a[off] = a[i]
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
// node is a concrete StackNode as stored in Stacks. Methods are devirtualized for performance benefits, especially on TinyGo.
|
||||
@@ -159,6 +160,8 @@ func (h *handlers) encapsulateNode(node *node, buf []byte, offsetIP, offsetThisF
|
||||
err = nil // CLOSE error handled gracefully by deleting node.
|
||||
node = nil // Node is destroyed in tryHandleError and invalidated.
|
||||
}
|
||||
// TODO(soypat): We have fuzz tests in place, maybe we can start returning the error up the chain to catch invalid settings at application level so that users don't have to have logs in place to understand invalid config/buffer size.
|
||||
// Encapsulate should only fail with error on programmer errors.
|
||||
if n > 0 {
|
||||
return n, err
|
||||
} else if err != nil {
|
||||
@@ -229,3 +232,31 @@ func incLim(v, max int) int {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (l logger) error(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelError, msg, attrs...)
|
||||
}
|
||||
func (l logger) info(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelInfo, msg, attrs...)
|
||||
}
|
||||
func (l logger) warn(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelWarn, msg, attrs...)
|
||||
}
|
||||
func (l logger) debug(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelDebug, msg, attrs...)
|
||||
}
|
||||
func (l logger) trace(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, internal.LevelTrace, msg, attrs...)
|
||||
}
|
||||
|
||||
const enableAllocLog = internal.HeapAllocDebugging
|
||||
|
||||
func debugLog(msg string) {
|
||||
if enableAllocLog {
|
||||
internal.LogAllocs(msg)
|
||||
}
|
||||
}
|
||||
|
||||
+197
-15
@@ -11,15 +11,15 @@ import (
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/dhcpv4"
|
||||
"github.com/soypat/lneto/dhcp/dhcpv4"
|
||||
"github.com/soypat/lneto/dns"
|
||||
"github.com/soypat/lneto/dns/mdns"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/http/httpraw"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/ipv4/icmpv4"
|
||||
"github.com/soypat/lneto/ipv6"
|
||||
"github.com/soypat/lneto/mdns"
|
||||
"github.com/soypat/lneto/ntp"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/udp"
|
||||
@@ -384,14 +384,14 @@ func (pc *PacketBreakdown) CaptureICMPv4(dst []Frame, pkt []byte, bitOffset int)
|
||||
}
|
||||
|
||||
finfo := reclaimFrame(&dst, "ICMP", bitOffset, baseICMPv4Fields[:])
|
||||
|
||||
tp := ifrm.Type()
|
||||
// Add type-specific fields.
|
||||
switch ifrm.Type() {
|
||||
switch tp {
|
||||
case icmpv4.TypeEcho, icmpv4.TypeEchoReply:
|
||||
finfo.Fields = append(finfo.Fields, icmpv4EchoFields[:]...)
|
||||
if len(icmpData) > 8 {
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: "Data",
|
||||
Name: tp.String(),
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: 8 * octet,
|
||||
BitLength: (len(icmpData) - 8) * octet,
|
||||
@@ -439,26 +439,202 @@ func (pc *PacketBreakdown) CaptureDNS(dst []Frame, pkt []byte, bitOffset int) ([
|
||||
}
|
||||
dnsData := pkt[bitOffset/8:]
|
||||
pc.dmsg.LimitResourceDecoding(4, 4, 4, 4)
|
||||
off, incomplete, err := pc.dmsg.Decode(dnsData)
|
||||
_, incomplete, err := pc.dmsg.Decode(dnsData)
|
||||
if err != nil && !incomplete {
|
||||
return dst, err
|
||||
}
|
||||
debuglog("pcap:dns-decode")
|
||||
finfo := reclaimFrame(&dst, "DNS", bitOffset, nil)
|
||||
hdr, _ := dns.NewFrame(dnsData)
|
||||
finfo := reclaimFrame(&dst, "DNS", bitOffset, baseDNSFields[:])
|
||||
if incomplete {
|
||||
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||
}
|
||||
field := internal.SliceReclaim(&finfo.Fields)
|
||||
*field = FrameField{
|
||||
Name: "Data",
|
||||
FrameBitOffset: 0,
|
||||
BitLength: int(off) * octet,
|
||||
SubFields: field.SubFields[:0], // Reuse subfields.
|
||||
if pc.SubfieldLimit <= 0 {
|
||||
debuglog("pcap:dns-done")
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
wireOff := dns.SizeHeader
|
||||
|
||||
// Questions section: walk all QDCount wire records to keep wireOff correct,
|
||||
// but only emit SubFields for the decoded ones.
|
||||
nq := int(hdr.QDCount())
|
||||
if nq > 0 {
|
||||
sectionStart := wireOff
|
||||
qfield := internal.SliceReclaim(&finfo.Fields)
|
||||
*qfield = FrameField{Name: "Questions", Class: fieldClassDNSResource, SubFields: qfield.SubFields[:0], Flags: FlagContainer}
|
||||
decoded := pc.dmsg.Questions
|
||||
for i := range nq {
|
||||
nameStart := wireOff
|
||||
wireOff, err = dnsSkipName(dnsData, wireOff)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
nameEnd := wireOff
|
||||
wireOff += 4 // Type(2) + Class(2)
|
||||
if i < len(decoded) && len(qfield.SubFields)+3 <= pc.SubfieldLimit {
|
||||
qfield.SubFields = append(qfield.SubFields, FrameField{
|
||||
Name: "Name",
|
||||
Class: FieldClassDNSName,
|
||||
FrameBitOffset: nameStart * octet,
|
||||
BitLength: (nameEnd - nameStart) * octet,
|
||||
}, FrameField{
|
||||
Name: "Type",
|
||||
Class: FieldClassOperation,
|
||||
FrameBitOffset: nameEnd * octet,
|
||||
BitLength: 2 * octet,
|
||||
}, FrameField{
|
||||
Name: "Class",
|
||||
Class: FieldClassOperation,
|
||||
FrameBitOffset: (nameEnd + 2) * octet,
|
||||
BitLength: 2 * octet,
|
||||
})
|
||||
}
|
||||
}
|
||||
qfield.FrameBitOffset = sectionStart * octet
|
||||
qfield.BitLength = (wireOff - sectionStart) * octet
|
||||
}
|
||||
|
||||
wireOff = pc.appendDNSResources(finfo, "Answers", dnsData, pc.dmsg.Answers, int(hdr.ANCount()), wireOff)
|
||||
wireOff = pc.appendDNSResources(finfo, "Authorities", dnsData, pc.dmsg.Authorities, int(hdr.NSCount()), wireOff)
|
||||
wireOff = pc.appendDNSResources(finfo, "Additionals", dnsData, pc.dmsg.Additionals, int(hdr.ARCount()), wireOff)
|
||||
_ = wireOff
|
||||
|
||||
debuglog("pcap:dns-done")
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// appendDNSResources adds a section FrameField with per-record SubFields to finfo.
|
||||
// It walks all `total` wire records to keep wireOff accurate, but only emits SubFields
|
||||
// for the decoded slice entries while nFields < pc.SubfieldLimit.
|
||||
func (pc *PacketBreakdown) appendDNSResources(finfo *Frame, name string, dnsData []byte, decoded []dns.Resource, total, wireOff int) int {
|
||||
if total == 0 {
|
||||
return wireOff
|
||||
}
|
||||
var err error
|
||||
sectionStart := wireOff
|
||||
rfield := internal.SliceReclaim(&finfo.Fields)
|
||||
*rfield = FrameField{Name: name, Class: fieldClassDNSResource, SubFields: rfield.SubFields[:0], Flags: FlagContainer}
|
||||
for i := range total {
|
||||
nameStart := wireOff
|
||||
wireOff, err = dnsSkipName(dnsData, wireOff)
|
||||
if err != nil || wireOff+10 > len(dnsData) {
|
||||
break
|
||||
}
|
||||
nameEnd := wireOff
|
||||
dataLen := int(binary.BigEndian.Uint16(dnsData[nameEnd+8:]))
|
||||
wireOff += 10 + dataLen // Type(2)+Class(2)+TTL(4)+Length(2)+Data
|
||||
if wireOff > len(dnsData) {
|
||||
break
|
||||
}
|
||||
if i < len(decoded) && len(rfield.SubFields)+6 <= pc.SubfieldLimit {
|
||||
rfield.SubFields = append(rfield.SubFields, FrameField{
|
||||
Name: "Name",
|
||||
Class: FieldClassDNSName,
|
||||
FrameBitOffset: nameStart * octet,
|
||||
BitLength: (nameEnd - nameStart) * octet,
|
||||
}, FrameField{
|
||||
Name: "Type",
|
||||
Class: FieldClassOperation,
|
||||
FrameBitOffset: nameEnd * octet,
|
||||
BitLength: 2 * octet,
|
||||
}, FrameField{
|
||||
Name: "Class",
|
||||
Class: FieldClassOperation,
|
||||
FrameBitOffset: (nameEnd + 2) * octet,
|
||||
BitLength: 2 * octet,
|
||||
}, FrameField{
|
||||
Name: "TTL",
|
||||
Class: FieldClassID,
|
||||
FrameBitOffset: (nameEnd + 4) * octet,
|
||||
BitLength: 4 * octet,
|
||||
}, FrameField{
|
||||
Name: "Length",
|
||||
Class: FieldClassSize,
|
||||
FrameBitOffset: (nameEnd + 8) * octet,
|
||||
BitLength: 2 * octet,
|
||||
}, FrameField{
|
||||
Name: "Data",
|
||||
Class: FieldClassAddress,
|
||||
FrameBitOffset: (nameEnd + 10) * octet,
|
||||
BitLength: dataLen * octet,
|
||||
})
|
||||
}
|
||||
}
|
||||
rfield.FrameBitOffset = sectionStart * octet
|
||||
rfield.BitLength = (wireOff - sectionStart) * octet
|
||||
if err != nil {
|
||||
finfo.Errors = append(finfo.Errors, err)
|
||||
}
|
||||
return wireOff
|
||||
}
|
||||
|
||||
// dnsAppendDottedName walks a DNS name in wire format starting at off within
|
||||
// dnsMsg, follows compression pointers, and appends the dotted representation
|
||||
// (e.g. "example.com") to dst. Returns dst unchanged on error.
|
||||
func dnsAppendDottedName(dst, dnsMsg []byte, off int) []byte {
|
||||
// TODO: somehow move this to dns package. dns.Name.Append ? dns.Name is the wire format...
|
||||
first := true
|
||||
for ptr := 0; off < len(dnsMsg) && ptr <= 10; {
|
||||
c := dnsMsg[off]
|
||||
off++
|
||||
switch c & 0xc0 {
|
||||
case 0x00:
|
||||
if c == 0 {
|
||||
return dst // null terminator: done
|
||||
}
|
||||
end := off + int(c)
|
||||
if end > len(dnsMsg) {
|
||||
return dst
|
||||
}
|
||||
if !first {
|
||||
dst = append(dst, '.')
|
||||
}
|
||||
dst = append(dst, dnsMsg[off:end]...)
|
||||
off = end
|
||||
first = false
|
||||
case 0xc0:
|
||||
if off >= len(dnsMsg) {
|
||||
return dst
|
||||
}
|
||||
off = int(c&0x3f)<<8 | int(dnsMsg[off])
|
||||
ptr++ // guard against pointer loops
|
||||
default:
|
||||
return dst // reserved label type
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// dnsSkipName advances off past a DNS name in wire format without allocating.
|
||||
// Compression pointers are followed and consume 2 bytes, terminating the name.
|
||||
func dnsSkipName(b []byte, off int) (int, error) {
|
||||
for {
|
||||
if off >= len(b) {
|
||||
return off, lneto.ErrTruncatedFrame
|
||||
}
|
||||
c := b[off]
|
||||
off++
|
||||
switch c & 0xc0 {
|
||||
case 0x00:
|
||||
if c == 0 {
|
||||
return off, nil // null terminator
|
||||
}
|
||||
off += int(c) // skip label bytes
|
||||
if off > len(b) {
|
||||
return off, lneto.ErrTruncatedFrame
|
||||
}
|
||||
case 0xc0:
|
||||
if off >= len(b) {
|
||||
return off, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return off + 1, nil // compression pointer: 2 bytes total, always terminal
|
||||
default:
|
||||
return off, lneto.ErrInvalidField // reserved label type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PacketBreakdown) CaptureNTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
|
||||
if bitOffset%8 != 0 {
|
||||
return dst, errNotByteAligned
|
||||
@@ -521,7 +697,7 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
|
||||
field.Class = FieldClassSize
|
||||
|
||||
// Time/duration options (seconds).
|
||||
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptRenewTimeValue, dhcpv4.OptRebindingTimeValue,
|
||||
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptT1Renewal, dhcpv4.OptT2Rebinding,
|
||||
dhcpv4.OptTimeOffset, dhcpv4.OptARPCacheTimeout, dhcpv4.OptPathMTUAgingTimeout,
|
||||
dhcpv4.OptTCPKeepaliveInterval, dhcpv4.OptDefaultIPTTL, dhcpv4.OptDefaultTCPTimetoLive:
|
||||
field.Class = FieldClassTimestamp
|
||||
@@ -656,6 +832,9 @@ type Flags uint32
|
||||
const (
|
||||
FlagRightAligned Flags = 1 << iota
|
||||
FlagLegacy
|
||||
// FlagContainer is used for [FrameField]s whose SubFields represent
|
||||
// the entirety of the FrameField's data. i.e: DNS Questions/Answers.
|
||||
FlagContainer
|
||||
)
|
||||
|
||||
func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
|
||||
@@ -771,7 +950,7 @@ func appendField(dst, pkt []byte, fieldBitStart, bitlen int, rightAligned bool)
|
||||
if octets+octetsStart+1 > len(pkt) {
|
||||
return dst, lneto.ErrShortBuffer
|
||||
}
|
||||
for i := 0; i < octets; i++ {
|
||||
for i := range octets {
|
||||
b := (pkt[octetsStart+i] & mask) << (8 - firstBitOffset)
|
||||
b |= pkt[octetsStart+i+1] >> firstBitOffset
|
||||
dst = append(dst, b)
|
||||
@@ -855,10 +1034,13 @@ const (
|
||||
FieldClassBinaryText // binary-text
|
||||
FieldClassOperation // op
|
||||
FieldClassTimestamp // timestamp
|
||||
FieldClassDNSName // dns name
|
||||
)
|
||||
|
||||
const octet = 8
|
||||
|
||||
const fieldClassDNSResource = fieldClassUndefined
|
||||
|
||||
var baseEthernetFields = [...]FrameField{
|
||||
{
|
||||
Class: FieldClassDst,
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/dhcpv4"
|
||||
"github.com/soypat/lneto/dhcp/dhcpv4"
|
||||
"github.com/soypat/lneto/dns"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/http/httpraw"
|
||||
"github.com/soypat/lneto/internal/ltesto"
|
||||
@@ -35,7 +36,7 @@ func makeHttpPayload(body string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func TestCap(t *testing.T) {
|
||||
const mtu = 1500
|
||||
const mtu = ethernet.MaxMTU
|
||||
const httpBody = "{200,ok}"
|
||||
var buf [mtu]byte
|
||||
var gen ltesto.PacketGen
|
||||
@@ -486,9 +487,91 @@ func ExampleFormatter_dhcp() {
|
||||
// (Hostname string)="myhost"
|
||||
}
|
||||
|
||||
func writeOpt(dst []byte, opt dhcpv4.OptNum, data ...byte) int {
|
||||
dst[0] = byte(opt)
|
||||
dst[1] = byte(len(data))
|
||||
copy(dst[2:], data)
|
||||
return 2 + len(data)
|
||||
func ExampleFormatter_dns() {
|
||||
const (
|
||||
ethSize = 14
|
||||
ipv4Size = 20
|
||||
udpSize = 8
|
||||
)
|
||||
|
||||
// Build a DNS query for "example.com" A record.
|
||||
var msg dns.Message
|
||||
msg.Questions = []dns.Question{
|
||||
{Name: dns.MustNewName("example.com"), Type: dns.TypeA, Class: dns.ClassINET},
|
||||
{Name: dns.MustNewName("temu.com"), Type: dns.TypeAAAA, Class: dns.ClassANY},
|
||||
}
|
||||
msg.Answers = []dns.Resource{
|
||||
dns.NewResource(dns.MustNewName("abc.com"), dns.TypeALL, dns.ClassANY, 64, []byte{10, 0, 11, 1}),
|
||||
dns.NewResource(dns.MustNewName("123.com"), dns.TypeA, dns.ClassINET, 64, []byte{20, 0, 22, 2}),
|
||||
}
|
||||
|
||||
dnsPayload, err := msg.AppendTo(nil, 0x1234, dns.NewClientHeaderFlags(dns.OpCodeQuery, true))
|
||||
if err != nil {
|
||||
fmt.Println("dns encode error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
pkt := make([]byte, ethSize+ipv4Size+udpSize+len(dnsPayload))
|
||||
|
||||
efrm, _ := ethernet.NewFrame(pkt)
|
||||
*efrm.DestinationHardwareAddr() = [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
|
||||
*efrm.SourceHardwareAddr() = [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x02}
|
||||
efrm.SetEtherType(ethernet.TypeIPv4)
|
||||
|
||||
ifrm, _ := ipv4.NewFrame(pkt[ethSize:])
|
||||
ifrm.SetVersionAndIHL(4, 5)
|
||||
ifrm.SetTTL(64)
|
||||
ifrm.SetProtocol(lneto.IPProtoUDP)
|
||||
ifrm.SetTotalLength(uint16(ipv4Size + udpSize + len(dnsPayload)))
|
||||
ifrm.SetCRC(ifrm.CalculateHeaderCRC())
|
||||
|
||||
ufrm, _ := udp.NewFrame(pkt[ethSize+ipv4Size:])
|
||||
ufrm.SetSourcePort(58200)
|
||||
ufrm.SetDestinationPort(dns.ServerPort)
|
||||
ufrm.SetLength(uint16(udpSize + len(dnsPayload)))
|
||||
|
||||
copy(pkt[ethSize+ipv4Size+udpSize:], dnsPayload)
|
||||
|
||||
var cap PacketBreakdown
|
||||
cap.SubfieldLimit = 20
|
||||
frames, err := cap.CaptureEthernet(nil, pkt, 0)
|
||||
if err != nil {
|
||||
fmt.Println("capture error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
var fmtr Formatter
|
||||
fmtr.SubfieldLimit = cap.SubfieldLimit
|
||||
fmtr.FrameSep = "\n"
|
||||
fmtr.FieldSep = "; "
|
||||
fmtr.SubfieldSep = "\n\t"
|
||||
out, err := fmtr.FormatFrames(nil, frames, pkt)
|
||||
if err != nil {
|
||||
fmt.Println("format error:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(string(out))
|
||||
// Output:
|
||||
// Ethernet len=14; destination=00:00:00:00:00:01; source=00:00:00:00:00:02; protocol=0x0800
|
||||
// IPv4 len=20; version=0x04; (Header Length)=5; (Type of Service)=0x00; (Total Length)=117; identification=0x0000; flags=0x0000; (Time to live)=0x40; protocol=0x11; checksum=0x7a79; source=0.0.0.0; destination=0.0.0.0
|
||||
// UDP len=8; (Source port)=58200; (Destination port)=53; size=97; checksum=0x0000
|
||||
// DNS len=89; identification=0x1234; flags=0x0100; Questions=2; Answers=2; Authorities=0; Additionals=0; Questions
|
||||
// Name=example.com
|
||||
// Type=1
|
||||
// Class=1
|
||||
// Name=temu.com
|
||||
// Type=28
|
||||
// Class=255; Answers
|
||||
// Name=abc.com
|
||||
// Type=255
|
||||
// Class=255
|
||||
// TTL=0x00000040
|
||||
// Length=4
|
||||
// Data=10.0.11.1
|
||||
// Name=123.com
|
||||
// Type=1
|
||||
// Class=1
|
||||
// TTL=0x00000040
|
||||
// Length=4
|
||||
// Data=20.0.22.2
|
||||
}
|
||||
|
||||
+17
-4
@@ -105,12 +105,12 @@ func (f *Formatter) FormatFrame(dst []byte, frm Frame, pkt []byte) (_ []byte, er
|
||||
}
|
||||
|
||||
func (f *Formatter) filterField(field FrameField) bool {
|
||||
return f.FilterClasses != nil && !slices.Contains(f.FilterClasses, field.Class) ||
|
||||
return f.FilterClasses != nil && field.Flags&FlagContainer == 0 && !slices.Contains(f.FilterClasses, field.Class) ||
|
||||
(field.Flags.IsLegacy() && !f.DisableLegacyFilter)
|
||||
}
|
||||
|
||||
func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, pkt []byte) (_ []byte, err error) {
|
||||
printOnlySubfields := field.Class == FieldClassOptions && len(field.SubFields) > 0
|
||||
printOnlySubfields := (field.Flags&FlagContainer != 0 || field.Class == FieldClassOptions) && len(field.SubFields) > 0
|
||||
if !printOnlySubfields {
|
||||
dst, err = f.formatField(dst, pktStartOff, field, pkt)
|
||||
} else {
|
||||
@@ -119,7 +119,10 @@ func (f *Formatter) FormatField(dst []byte, pktStartOff int, field FrameField, p
|
||||
if f.SubfieldLimit > 0 && len(field.SubFields) > 0 {
|
||||
sep := f.subfieldSep()
|
||||
lim := min(len(field.SubFields), f.SubfieldLimit)
|
||||
for i := 0; err == nil && i < lim && !f.filterField(field.SubFields[i]); i++ {
|
||||
for i := 0; err == nil && i < lim; i++ {
|
||||
if f.filterField(field.SubFields[i]) {
|
||||
continue
|
||||
}
|
||||
dst = append(dst, sep...)
|
||||
// Notice we only format subfields one level low
|
||||
dst, err = f.formatField(dst, pktStartOff, field.SubFields[i], pkt)
|
||||
@@ -143,9 +146,13 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
|
||||
dst = append(dst, ')')
|
||||
}
|
||||
if field.BitLength == 0 {
|
||||
// We do not print empty nor container fields.
|
||||
return dst, nil
|
||||
}
|
||||
dst = append(dst, '=')
|
||||
if field.Flags&FlagContainer != 0 {
|
||||
return dst, nil
|
||||
}
|
||||
f.mubuf.Lock()
|
||||
defer f.mubuf.Unlock()
|
||||
f.buf, err = appendField(f.buf[:0], pkt, field.FrameBitOffset+pktStartOff, field.BitLength, field.Flags.IsRightAligned())
|
||||
@@ -177,6 +184,12 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
|
||||
dst = strconv.AppendQuote(dst, unsafe.String(&f.buf[0], len(f.buf)))
|
||||
}
|
||||
debuglog("pcap:fmtfield:text-done")
|
||||
case FieldClassDNSName:
|
||||
// Walk the DNS message from pktStartOff to resolve the name, following
|
||||
// compression pointers that reference earlier bytes in the DNS message.
|
||||
dnsMsg := pkt[pktStartOff/8:]
|
||||
nameOff := field.FrameBitOffset / 8
|
||||
dst = dnsAppendDottedName(dst, dnsMsg, nameOff)
|
||||
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
|
||||
// IP, MAC addresses and ports.
|
||||
if field.BitLength <= 16 {
|
||||
@@ -221,7 +234,7 @@ func (f *Formatter) fieldSep() string {
|
||||
func (f *Formatter) subfieldSep() string {
|
||||
sep := f.SubfieldSep
|
||||
if sep == "" {
|
||||
sep = "_" // default sub-field separator
|
||||
sep = "," // default sub-field separator
|
||||
}
|
||||
return sep
|
||||
}
|
||||
|
||||
@@ -25,11 +25,12 @@ func _() {
|
||||
_ = x[FieldClassBinaryText-14]
|
||||
_ = x[FieldClassOperation-15]
|
||||
_ = x[FieldClassTimestamp-16]
|
||||
_ = x[FieldClassDNSName-17]
|
||||
}
|
||||
|
||||
const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypesizeflagsidentificationchecksumoptionspayloadtextaddressbinary-textoptimestamp"
|
||||
const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypesizeflagsidentificationchecksumoptionspayloadtextaddressbinary-textoptimestampdns name"
|
||||
|
||||
var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 49, 54, 68, 76, 83, 90, 94, 101, 112, 114, 123}
|
||||
var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 49, 54, 68, 76, 83, 90, 94, 101, 112, 114, 123, 131}
|
||||
|
||||
func (i FieldClass) String() string {
|
||||
if i >= FieldClass(len(_FieldClass_index)-1) {
|
||||
|
||||
@@ -39,6 +39,8 @@ type StackEthernet struct {
|
||||
gwmac [6]byte
|
||||
mtu uint16
|
||||
acceptMulticast bool
|
||||
|
||||
onSend func(p []byte)
|
||||
// crcupdate set when crc32 has been configured to be appended.
|
||||
crcupdate func(crc uint32, p []byte) uint32
|
||||
}
|
||||
@@ -63,6 +65,10 @@ func (ls *StackEthernet) HardwareAddr6() [6]byte {
|
||||
return ls.mac
|
||||
}
|
||||
|
||||
func (ls *StackEthernet) OnEncapsulate(cb func([]byte)) {
|
||||
ls.onSend = cb
|
||||
}
|
||||
|
||||
// Reset6 resets the stack with the given parameters.
|
||||
//
|
||||
// Deprecated: Use [StackEthernet.Configure] instead.
|
||||
@@ -79,7 +85,7 @@ func (ls *StackEthernet) Reset6(mac, gateway [6]byte, mtu, maxNodes int) error {
|
||||
// It validates the configuration parameters and resets internal state.
|
||||
// The connection ID is incremented on each call to invalidate existing connections.
|
||||
func (ls *StackEthernet) Configure(cfg StackEthernetConfig) error {
|
||||
if cfg.MTU > (math.MaxUint16-ethernet.MaxOverheadSize) || cfg.MTU < 256 {
|
||||
if cfg.MTU > ethernet.MaxMTU || cfg.MTU < ethernet.MinimumMTU {
|
||||
return lneto.ErrInvalidConfig
|
||||
} else if cfg.MaxNodes <= 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
@@ -122,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
|
||||
@@ -194,6 +200,9 @@ func (ls *StackEthernet) Encapsulate(carrierData []byte, offsetToIP, offsetToFra
|
||||
dst[n] = 0
|
||||
n++
|
||||
}
|
||||
if ls.onSend != nil {
|
||||
ls.onSend(dst[:n])
|
||||
}
|
||||
if ls.crcupdate != nil {
|
||||
crc := ls.crcupdate(0, carrierData[offsetToFrame:offsetToFrame+n])
|
||||
binary.LittleEndian.PutUint32(carrierData[offsetToFrame+n:], crc)
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
package internet
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/udp"
|
||||
)
|
||||
|
||||
var _ lneto.StackNode = (*StackIP)(nil)
|
||||
|
||||
type StackIP struct {
|
||||
connID uint64
|
||||
ipID uint16
|
||||
ip [4]byte
|
||||
acceptMulticast bool
|
||||
validator lneto.Validator
|
||||
handlers handlers
|
||||
}
|
||||
|
||||
func (sb *StackIP) Reset(addr netip.Addr, maxNodes int) error {
|
||||
if maxNodes <= 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
err := sb.SetAddr(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.handlers.reset("StackIP", maxNodes)
|
||||
*sb = StackIP{
|
||||
connID: sb.connID + 1,
|
||||
validator: sb.validator,
|
||||
handlers: sb.handlers,
|
||||
ip: sb.ip,
|
||||
acceptMulticast: sb.acceptMulticast,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sb *StackIP) SetAddr(addr netip.Addr) error {
|
||||
if !addr.IsValid() {
|
||||
return lneto.ErrInvalidAddr
|
||||
} else if !addr.Is4() {
|
||||
return lneto.ErrUnsupported
|
||||
}
|
||||
sb.ip = addr.As4()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sb *StackIP) ConnectionID() *uint64 {
|
||||
return &sb.connID
|
||||
}
|
||||
|
||||
func (sb *StackIP) Protocol() uint64 {
|
||||
return uint64(ethernet.TypeIPv4) // Only support ipv4 for now.
|
||||
}
|
||||
|
||||
func (sb *StackIP) LocalPort() uint16 { return 0 }
|
||||
|
||||
func (sb *StackIP) Addr() netip.Addr {
|
||||
return netip.AddrFrom4(sb.ip)
|
||||
}
|
||||
|
||||
func (sb *StackIP) SetAcceptMulticast(accept bool) {
|
||||
sb.acceptMulticast = accept
|
||||
}
|
||||
|
||||
func (sb *StackIP) SetLogger(logger *slog.Logger) {
|
||||
sb.handlers.log = logger
|
||||
}
|
||||
|
||||
func (sb *StackIP) Demux(carrierData []byte, offset int) error {
|
||||
debugLog("ip:demux")
|
||||
sb.handlers.info("StackIP.Demux:start")
|
||||
frame := carrierData[offset:] // we don't care about carrier data in IP.
|
||||
ifrm, err := ipv4.NewFrame(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst := ifrm.DestinationAddr()
|
||||
if sb.ip != ([4]byte{}) && *dst != sb.ip {
|
||||
if !sb.acceptMulticast || dst[0]&0xF0 != 0xE0 {
|
||||
sb.handlers.debug("ip:not-for-us")
|
||||
return lneto.ErrPacketDrop // Not meant for us.
|
||||
}
|
||||
}
|
||||
|
||||
sb.validator.ResetErr()
|
||||
ifrm.ValidateExceptCRC(&sb.validator)
|
||||
if err = sb.validator.ErrPop(); err != nil {
|
||||
sb.handlers.error("ip:Demux.validate")
|
||||
return err
|
||||
}
|
||||
|
||||
if ifrm.CalculateHeaderCRC() != 0 {
|
||||
sb.handlers.error("ip:demux.crc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
off := ifrm.HeaderLength()
|
||||
totalLen := ifrm.TotalLength()
|
||||
proto := ifrm.Protocol()
|
||||
node := sb.handlers.nodeByProto(uint16(proto))
|
||||
// nodeIdx := getNodeByProto(sb.handlers, uint16(proto))
|
||||
if node == nil {
|
||||
// Drop packet.
|
||||
sb.handlers.info("ip:demux.drop", internal.SlogAddr4("dstaddr", ifrm.DestinationAddr()), slog.String("proto", ifrm.Protocol().String()))
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
// Incoming CRC Validation of common IP Protocols.
|
||||
var crc lneto.CRC791
|
||||
switch proto {
|
||||
case lneto.IPProtoTCP:
|
||||
ifrm.CRCWriteTCPPseudo(&crc)
|
||||
if crc.PayloadSum16(ifrm.Payload()) != 0 {
|
||||
sb.handlers.error("ip:demux.tcpcrc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
case lneto.IPProtoUDP:
|
||||
ufrm, err := udp.NewFrame(ifrm.Payload())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ufrm.ValidateSize(&sb.validator)
|
||||
if err = sb.validator.ErrPop(); err != nil {
|
||||
sb.handlers.error("ip:demux.udpvalidatesize")
|
||||
return err
|
||||
}
|
||||
frameLen := ufrm.Length()
|
||||
ifrm.CRCWriteUDPPseudo(&crc, frameLen)
|
||||
if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 {
|
||||
sb.handlers.error("ip:demux.udpcrc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
}
|
||||
sb.handlers.info("ipDemux", slog.String("ipproto", proto.String()), slog.Int("plen", int(totalLen)))
|
||||
err = node.callbacks.Demux(frame[:totalLen], off)
|
||||
if sb.handlers.tryHandleError(node, err) {
|
||||
sb.handlers.info("ipclose", slog.String("proto", proto.String()))
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (sb *StackIP) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||
frame := carrierData[offsetToFrame:]
|
||||
if len(frame) < 256 {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
ifrm, _ := ipv4.NewFrame(frame)
|
||||
const ihl = 5
|
||||
const headerlen = ihl * 4
|
||||
const dontFrag = 0x4000
|
||||
ifrm.SetVersionAndIHL(4, ihl)
|
||||
ifrm.SetToS(0)
|
||||
seed := sb.ipID + uint16(sb.connID)
|
||||
id := internal.Prand16(seed)
|
||||
ifrm.SetID(id)
|
||||
ifrm.SetFlags(dontFrag)
|
||||
ifrm.SetTTL(64)
|
||||
*ifrm.SourceAddr() = sb.ip
|
||||
sb.ipID = id
|
||||
// Children (TCP/UDP) start at offset headerlen (20 bytes after IP header start).
|
||||
// offsetToIP is 0 relative to this slice (frame), children's frame starts at headerlen.
|
||||
node, n, err := sb.handlers.encapsulateAny(carrierData, offsetToFrame, offsetToFrame+headerlen)
|
||||
if n == 0 {
|
||||
return n, err
|
||||
}
|
||||
proto := lneto.IPProto(node.proto)
|
||||
totalLen := n + headerlen
|
||||
ifrm.SetTotalLength(uint16(totalLen))
|
||||
ifrm.SetProtocol(proto)
|
||||
// Zero the CRC field so its value does not add to the final result.
|
||||
ifrm.SetCRC(0)
|
||||
crcValue := ifrm.CalculateHeaderCRC()
|
||||
ifrm.SetCRC(crcValue)
|
||||
// Calculate CRC for our newly generated packet.
|
||||
var crc lneto.CRC791
|
||||
payload := ifrm.Payload()
|
||||
switch proto {
|
||||
case lneto.IPProtoTCP:
|
||||
ifrm.CRCWriteTCPPseudo(&crc)
|
||||
tfrm, _ := tcp.NewFrame(payload)
|
||||
// Zero the CRC field so its value does not add to the final result.
|
||||
tfrm.SetCRC(0)
|
||||
crcValue = crc.PayloadSum16(payload)
|
||||
tfrm.SetCRC(crcValue)
|
||||
case lneto.IPProtoUDP:
|
||||
ufrm, _ := udp.NewFrame(payload)
|
||||
ifrm.CRCWriteUDPPseudo(&crc, uint16(n))
|
||||
ufrm.SetLength(uint16(n))
|
||||
// Zero the CRC field so its value does not add to the final result.
|
||||
ufrm.SetCRC(0)
|
||||
crcValue = lneto.NeverZeroSum(crc.PayloadSum16(payload))
|
||||
ufrm.SetCRC(crcValue)
|
||||
}
|
||||
return totalLen, err
|
||||
}
|
||||
|
||||
func (sb *StackIP) Register(h lneto.StackNode) error {
|
||||
proto := h.Protocol()
|
||||
if proto > 255 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
return sb.handlers.registerByPortProto(nodeFromStackNode(h, h.LocalPort(), proto, nil))
|
||||
}
|
||||
|
||||
func (sb *StackIP) IsRegistered(proto lneto.IPProto) bool {
|
||||
return sb.handlers.nodeByProto(uint16(proto)) != nil
|
||||
}
|
||||
|
||||
func (sb *StackIP) recvicmp(icmpData []byte) error {
|
||||
var crc lneto.CRC791
|
||||
if crc.PayloadSum16(icmpData) != 0 {
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (l logger) error(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelError, msg, attrs...)
|
||||
}
|
||||
func (l logger) info(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelInfo, msg, attrs...)
|
||||
}
|
||||
func (l logger) warn(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelWarn, msg, attrs...)
|
||||
}
|
||||
func (l logger) debug(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, slog.LevelDebug, msg, attrs...)
|
||||
}
|
||||
func (l logger) trace(msg string, attrs ...slog.Attr) {
|
||||
internal.LogAttrs(l.log, internal.LevelTrace, msg, attrs...)
|
||||
}
|
||||
|
||||
const enableAllocLog = internal.HeapAllocDebugging
|
||||
|
||||
func debugLog(msg string) {
|
||||
if enableAllocLog {
|
||||
internal.LogAllocs(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package internet
|
||||
|
||||
import (
|
||||
"io"
|
||||
"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"
|
||||
"github.com/soypat/lneto/udp"
|
||||
)
|
||||
|
||||
// stackip4 is NOT a StackNode implementation.
|
||||
// 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
|
||||
ipID uint16
|
||||
ip4 [4]byte
|
||||
acceptMulticast bool
|
||||
}
|
||||
|
||||
func (si4 *stackip4) reset4(vld *lneto.Validator, maxNodes int) {
|
||||
*si4 = stackip4{
|
||||
ip4: [4]byte{},
|
||||
ipID: 1,
|
||||
acceptMulticast: false,
|
||||
handlers: si4.handlers,
|
||||
vld: vld,
|
||||
}
|
||||
si4.handlers.reset("stackip4", maxNodes)
|
||||
}
|
||||
|
||||
func (si4 *stackip4) Register4(h lneto.StackNode) error {
|
||||
proto := h.Protocol()
|
||||
if proto > 255 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
return si4.handlers.registerByPortProto(nodeFromStackNode(h, h.LocalPort(), proto, nil))
|
||||
}
|
||||
|
||||
func (si4 *stackip4) IsRegistered4(proto lneto.IPProto) bool {
|
||||
return si4.handlers.nodeByProto(uint16(proto)) != nil
|
||||
}
|
||||
|
||||
func (si4 *stackip4) SetAcceptMulticast4(accept bool) {
|
||||
si4.acceptMulticast = accept
|
||||
}
|
||||
func (si4 *stackip4) Addr4() [4]byte { return si4.ip4 }
|
||||
func (si4 *stackip4) SetAddr4(ip4 [4]byte) {
|
||||
si4.ip4 = ip4
|
||||
}
|
||||
|
||||
func (si4 *stackip4) demux4(carrierData []byte, offset int) error {
|
||||
debugLog("ip4:demux")
|
||||
si4.handlers.info("demux:start")
|
||||
frame := carrierData[offset:] // we don't care about carrier data in IP.
|
||||
ifrm, err := ipv4.NewFrame(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst := ifrm.DestinationAddr()
|
||||
if si4.ip4 != ([4]byte{}) && *dst != si4.ip4 {
|
||||
if !si4.acceptMulticast || !internal.IsMulticastIPAddr(dst[:]) {
|
||||
si4.handlers.debug("ip:not-for-us")
|
||||
return lneto.ErrPacketDrop // Not meant for us.
|
||||
}
|
||||
}
|
||||
|
||||
si4.vld.ResetErr()
|
||||
ifrm.ValidateExceptCRC(si4.vld)
|
||||
if err = si4.vld.ErrPop(); err != nil {
|
||||
si4.handlers.error("ip:Demux.validate")
|
||||
return err
|
||||
}
|
||||
|
||||
if ifrm.CalculateHeaderCRC() != 0 {
|
||||
si4.handlers.error("ip:demux.crc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
off := ifrm.HeaderLength()
|
||||
|
||||
proto := ifrm.Protocol()
|
||||
node := si4.handlers.nodeByProto(uint16(proto))
|
||||
// nodeIdx := getNodeByProto(sb.handlers, uint16(proto))
|
||||
if node == nil {
|
||||
// Drop packet.
|
||||
si4.handlers.info("ip:demux.drop", internal.SlogAddr4("dstaddr", ifrm.DestinationAddr()), slog.String("proto", ifrm.Protocol().String()))
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
// Incoming CRC Validation of common IP Protocols.
|
||||
var crc lneto.CRC791
|
||||
switch proto {
|
||||
case lneto.IPProtoTCP:
|
||||
ifrm.CRCWriteTCPPseudo(&crc)
|
||||
if crc.PayloadSum16(ifrm.Payload()) != 0 {
|
||||
si4.handlers.error("ip:demux.tcpcrc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
case lneto.IPProtoUDP:
|
||||
ufrm, err := udp.NewFrame(ifrm.Payload())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ufrm.ValidateSize(si4.vld)
|
||||
if err = si4.vld.ErrPop(); err != nil {
|
||||
si4.handlers.error("ip:demux.udpvalidatesize")
|
||||
return err
|
||||
}
|
||||
frameLen := ufrm.Length()
|
||||
ifrm.CRCWriteUDPPseudo(&crc, frameLen)
|
||||
if crc.PayloadSum16(ufrm.RawData()[:frameLen]) != 0 {
|
||||
si4.handlers.error("ip:demux.udpcrc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
}
|
||||
totalLen := ifrm.TotalLength()
|
||||
si4.handlers.info("ipDemux", slog.String("ipproto", proto.String()), slog.Int("tlen", int(totalLen)))
|
||||
err = node.callbacks.Demux(frame[:totalLen], off)
|
||||
if si4.handlers.tryHandleError(node, err) {
|
||||
si4.handlers.info("ipclose", slog.String("proto", proto.String()))
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (si4 *stackip4) encapsulate4(carrierData []byte, offsetToIP int) (int, error) {
|
||||
frame := carrierData[offsetToIP:]
|
||||
if len(frame) < ipv4.MinimumMTU {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
ifrm, _ := ipv4.NewFrame(frame)
|
||||
const ihl = 5
|
||||
const headerlen = ihl * 4
|
||||
const dontFrag = 0x4000
|
||||
ifrm.SetVersionAndIHL(4, ihl)
|
||||
ifrm.SetToS(0)
|
||||
seed := (si4.ipID + 1) ^ uint16(si4.ip4[0])
|
||||
id := internal.Prand16(seed)
|
||||
ifrm.SetID(id)
|
||||
ifrm.SetFlags(dontFrag)
|
||||
ifrm.SetTTL(64)
|
||||
*ifrm.SourceAddr() = si4.ip4
|
||||
si4.ipID = id
|
||||
// Children (TCP/UDP) start at offset headerlen (20 bytes after IP header start).
|
||||
// offsetToIP is 0 relative to this slice (frame), children's frame starts at headerlen.
|
||||
node, n, err := si4.handlers.encapsulateAny(carrierData, offsetToIP, offsetToIP+headerlen)
|
||||
if n == 0 {
|
||||
return n, err
|
||||
}
|
||||
proto := lneto.IPProto(node.proto)
|
||||
totalLen := n + headerlen
|
||||
ifrm.SetTotalLength(uint16(totalLen))
|
||||
ifrm.SetProtocol(proto)
|
||||
// Zero the CRC field so its value does not add to the final result.
|
||||
ifrm.SetCRC(0)
|
||||
crcValue := ifrm.CalculateHeaderCRC()
|
||||
ifrm.SetCRC(crcValue)
|
||||
// Calculate CRC for our newly generated packet.
|
||||
var crc lneto.CRC791
|
||||
payload := ifrm.Payload()
|
||||
switch proto {
|
||||
case lneto.IPProtoTCP:
|
||||
ifrm.CRCWriteTCPPseudo(&crc)
|
||||
tfrm, _ := tcp.NewFrame(payload)
|
||||
// Zero the CRC field so its value does not add to the final result.
|
||||
tfrm.SetCRC(0)
|
||||
crcValue = crc.PayloadSum16(payload)
|
||||
tfrm.SetCRC(crcValue)
|
||||
case lneto.IPProtoUDP:
|
||||
ufrm, _ := udp.NewFrame(payload)
|
||||
ifrm.CRCWriteUDPPseudo(&crc, uint16(n))
|
||||
ufrm.SetLength(uint16(n))
|
||||
// Zero the CRC field so its value does not add to the final result.
|
||||
ufrm.SetCRC(0)
|
||||
crcValue = lneto.NeverZeroSum(crc.PayloadSum16(payload))
|
||||
ufrm.SetCRC(crcValue)
|
||||
}
|
||||
return totalLen, err
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package internet
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv6"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
"github.com/soypat/lneto/udp"
|
||||
)
|
||||
|
||||
// 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
|
||||
vld *lneto.Validator
|
||||
ip6 [16]byte
|
||||
acceptMulticast bool
|
||||
}
|
||||
|
||||
func (si6 *stackip6) Register6(h lneto.StackNode) error {
|
||||
proto := h.Protocol()
|
||||
if proto > 255 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
return si6.handlers.registerByPortProto(nodeFromStackNode(h, h.LocalPort(), proto, nil))
|
||||
}
|
||||
|
||||
func (si6 *stackip6) IsRegistered6(proto lneto.IPProto) bool {
|
||||
return si6.handlers.nodeByProto(uint16(proto)) != nil
|
||||
}
|
||||
|
||||
func (si6 *stackip6) SetAcceptMulticast6(accept bool) { si6.acceptMulticast = accept }
|
||||
func (si6 *stackip6) Addr6() [16]byte { return si6.ip6 }
|
||||
func (si6 *stackip6) SetAddr6(ip6 [16]byte) { si6.ip6 = ip6 }
|
||||
|
||||
func (si6 *stackip6) reset6(vld *lneto.Validator, maxNodes int) {
|
||||
*si6 = stackip6{
|
||||
handlers: si6.handlers,
|
||||
vld: vld,
|
||||
}
|
||||
si6.handlers.reset("stackip6", maxNodes)
|
||||
}
|
||||
|
||||
func (si6 *stackip6) demux6(carrierData []byte, offset int) error {
|
||||
debugLog("ip6:demux")
|
||||
si6.handlers.info("StackIP6.Demux:start")
|
||||
ifrm, err := ipv6.NewFrame(carrierData[offset:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst := ifrm.DestinationAddr()
|
||||
if si6.ip6 != ([16]byte{}) && *dst != si6.ip6 {
|
||||
if !si6.acceptMulticast || !internal.IsMulticastIPAddr(dst[:]) {
|
||||
si6.handlers.debug("ip6:not-for-us")
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
}
|
||||
|
||||
si6.vld.ResetErr()
|
||||
ifrm.ValidateSize(si6.vld)
|
||||
if err = si6.vld.ErrPop(); err != nil {
|
||||
si6.handlers.error("ip6:Demux.validate")
|
||||
return err
|
||||
}
|
||||
|
||||
proto := ifrm.NextHeader()
|
||||
node := si6.handlers.nodeByProto(uint16(proto))
|
||||
if node == nil {
|
||||
si6.handlers.info("ip6:demux.drop", slog.String("proto", proto.String()))
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
payload := ifrm.Payload()
|
||||
var crc lneto.CRC791
|
||||
switch proto {
|
||||
case lneto.IPProtoTCP:
|
||||
ifrm.CRCWritePseudo(&crc)
|
||||
if crc.PayloadSum16(payload) != 0 {
|
||||
si6.handlers.error("ip6:demux.tcpcrc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
case lneto.IPProtoUDP:
|
||||
ufrm, err := udp.NewFrame(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ufrm.ValidateSize(si6.vld)
|
||||
if err = si6.vld.ErrPop(); err != nil {
|
||||
si6.handlers.error("ip6:demux.udpvalidatesize")
|
||||
return err
|
||||
}
|
||||
ifrm.CRCWritePseudo(&crc)
|
||||
if crc.PayloadSum16(payload) != 0 {
|
||||
si6.handlers.error("ip6:demux.udpcrc")
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
}
|
||||
const headerlen = 40
|
||||
plen := ifrm.PayloadLength()
|
||||
si6.handlers.info("ip6Demux", slog.String("ipproto", proto.String()), slog.Int("plen", int(plen)))
|
||||
err = node.callbacks.Demux(carrierData[offset:offset+headerlen+int(plen)], headerlen)
|
||||
if si6.handlers.tryHandleError(node, err) {
|
||||
si6.handlers.info("ip6close", slog.String("proto", proto.String()))
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (si6 *stackip6) encapsulate6(carrierData []byte, offsetToIP int) (int, error) {
|
||||
ifrm, err := ipv6.NewFrame(carrierData[offsetToIP:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Set default parameters which node is free to change.
|
||||
ifrm.SetVersionTrafficAndFlow(6, 0, 0)
|
||||
ifrm.SetHopLimit(64)
|
||||
*ifrm.SourceAddr() = si6.ip6
|
||||
const headerlen = 40
|
||||
node, n, err := si6.handlers.encapsulateAny(carrierData, offsetToIP, offsetToIP+headerlen)
|
||||
if n == 0 {
|
||||
return n, err
|
||||
}
|
||||
proto := lneto.IPProto(node.proto)
|
||||
ifrm.SetNextHeader(proto)
|
||||
ifrm.SetPayloadLength(uint16(n))
|
||||
var crc lneto.CRC791
|
||||
payload := ifrm.Payload()
|
||||
switch proto {
|
||||
case lneto.IPProtoTCP:
|
||||
ifrm.CRCWritePseudo(&crc)
|
||||
tfrm, _ := tcp.NewFrame(payload)
|
||||
tfrm.SetCRC(0)
|
||||
tfrm.SetCRC(crc.PayloadSum16(payload))
|
||||
case lneto.IPProtoUDP:
|
||||
ufrm, _ := udp.NewFrame(payload)
|
||||
ufrm.SetLength(uint16(n))
|
||||
ifrm.CRCWritePseudo(&crc)
|
||||
ufrm.SetCRC(0)
|
||||
ufrm.SetCRC(lneto.NeverZeroSum(crc.PayloadSum16(payload)))
|
||||
}
|
||||
return headerlen + n, err
|
||||
}
|
||||
+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 {
|
||||
|
||||
@@ -45,7 +45,15 @@ func (sudp *StackUDPPort) Demux(carrierData []byte, frameOffset int) error {
|
||||
if dst != sudp.h.lport {
|
||||
return lneto.ErrPacketDrop // Not meant for us.
|
||||
}
|
||||
// TODO remote ip address handling.
|
||||
if len(sudp.raddr) > 0 && !internal.IsMulticastIPAddr(sudp.raddr) {
|
||||
srcIP, _, _, _, err := internal.GetIPAddr(carrierData[:frameOffset])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !internal.BytesEqual(srcIP, sudp.raddr) {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
}
|
||||
|
||||
src := ufrm.SourcePort()
|
||||
if sudp.rmport != 0 && src != sudp.rmport {
|
||||
|
||||
+104
-12
@@ -4,13 +4,15 @@ import (
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
)
|
||||
|
||||
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
|
||||
@@ -36,15 +38,15 @@ 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, -1, 0)
|
||||
n, err := from.Encapsulate(buf, 0, 0)
|
||||
if err != nil {
|
||||
t.Error("expectExchange:encapsulate:", err)
|
||||
} else if n == 0 {
|
||||
@@ -57,13 +59,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
|
||||
@@ -90,20 +92,105 @@ func testClientServerEstablish(t *testing.T, client, server *StackIP, connClient
|
||||
}
|
||||
}
|
||||
|
||||
func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIP, connClient, connServer *tcp.Conn) {
|
||||
func TestBasicStack6(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
var sbCl, sbSv StackIPv6
|
||||
var connCl, connSv tcp.Conn
|
||||
setupClientServer6(t, rng, &sbCl, &sbSv, &connCl, &connSv)
|
||||
var buf [2048]byte
|
||||
nextToSend := &sbCl
|
||||
nextToRecv := &sbSv
|
||||
exchangeAndExpectStates := func(clState, svState tcp.State) {
|
||||
t.Helper()
|
||||
expectExchange(t, nextToSend, nextToRecv, buf[:])
|
||||
gotCl := connCl.State()
|
||||
gotSv := connSv.State()
|
||||
if gotCl != clState {
|
||||
t.Errorf("want client state %s, got %s", clState, gotCl)
|
||||
}
|
||||
if gotSv != svState {
|
||||
t.Errorf("want server state %s, got %s", svState, gotSv)
|
||||
}
|
||||
nextToSend, nextToRecv = nextToRecv, nextToSend
|
||||
}
|
||||
exchangeAndExpectStates(tcp.StateSynSent, tcp.StateSynRcvd)
|
||||
exchangeAndExpectStates(tcp.StateEstablished, tcp.StateSynRcvd)
|
||||
exchangeAndExpectStates(tcp.StateEstablished, tcp.StateEstablished)
|
||||
}
|
||||
|
||||
func TestBasicStack6Established(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
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 *StackIPv6, connClient, connServer *tcp.Conn) {
|
||||
t.Helper()
|
||||
_ = rng
|
||||
const maxNodes = 1
|
||||
bufsize := 2048
|
||||
svip6 := netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) // 2001:db8::1
|
||||
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), maxNodes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.Reset(new(lneto.Validator), maxNodes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server.SetAddr6(svip6.As16())
|
||||
client.SetAddr6(clip6.As16())
|
||||
err := connServer.Configure(tcp.ConnConfig{
|
||||
RxBuf: make([]byte, bufsize),
|
||||
TxBuf: make([]byte, bufsize),
|
||||
TxPacketQueueSize: 3,
|
||||
RWBackoff: backoffYield,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = connClient.Configure(tcp.ConnConfig{
|
||||
RxBuf: make([]byte, bufsize),
|
||||
TxBuf: make([]byte, bufsize),
|
||||
TxPacketQueueSize: 3,
|
||||
RWBackoff: backoffYield,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = connServer.OpenListen(svip.Port(), 200); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = connClient.OpenActive(clip.Port(), svip, 100); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = server.Register6(connServer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = client.Register6(connClient); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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(svip.Addr(), maxNodes)
|
||||
client.Reset(clip.Addr(), maxNodes)
|
||||
|
||||
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{
|
||||
RxBuf: make([]byte, bufsize),
|
||||
TxBuf: make([]byte, bufsize),
|
||||
TxPacketQueueSize: 3,
|
||||
Logger: nil,
|
||||
RWBackoff: backoffYield,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -113,6 +200,7 @@ func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIP, co
|
||||
TxBuf: make([]byte, bufsize),
|
||||
TxPacketQueueSize: 3,
|
||||
Logger: nil,
|
||||
RWBackoff: backoffYield,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -127,12 +215,16 @@ func setupClientServer(t *testing.T, rng *rand.Rand, client, server *StackIP, co
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = server.Register(connServer)
|
||||
err = server.Register4(connServer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = client.Register(connClient)
|
||||
err = client.Register4(connClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func backoffYield(consecutiveBackoffs uint) time.Duration {
|
||||
return lneto.BackoffFlagGosched
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/tcp"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -24,7 +25,7 @@ func TestListener_SingleConnection(t *testing.T) {
|
||||
if err := listener.Reset(serverPort, pool); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := serverStack.Register(&listener); err != nil {
|
||||
if err := serverStack.Register4(&listener); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -65,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)
|
||||
@@ -77,7 +78,7 @@ func TestListener_AcceptAfterEstablished(t *testing.T) {
|
||||
if err := listener.Reset(serverPort, pool); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := serverStack.Register(&listener); err != nil {
|
||||
if err := serverStack.Register4(&listener); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -103,9 +104,9 @@ 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, serverStack.Addr(), serverPort, 1338)
|
||||
setupClient(t, &client2Stack, &client2Conn, netip.AddrFrom4(serverStack.Addr4()), serverPort, 1338)
|
||||
|
||||
// Complete full handshake for client2.
|
||||
expectExchange(t, &client2Stack, &serverStack, buf[:]) // SYN
|
||||
@@ -130,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)
|
||||
|
||||
@@ -147,20 +148,20 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
if err := listener.Reset(serverPort, pool); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := serverStack.Register(&listener); err != nil {
|
||||
if err := serverStack.Register4(&listener); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Setup remaining clients.
|
||||
for i := 1; i < numClients; i++ {
|
||||
clientPort := uint16(1337 + i)
|
||||
setupClient(t, &clientStacks[i], &clientConns[i], serverStack.Addr(), serverPort, clientPort)
|
||||
setupClient(t, &clientStacks[i], &clientConns[i], netip.AddrFrom4(serverStack.Addr4()), serverPort, clientPort)
|
||||
}
|
||||
|
||||
var buf [2048]byte
|
||||
|
||||
// Complete full handshakes for all clients.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
expectExchange(t, &clientStacks[i], &serverStack, buf[:]) // SYN
|
||||
expectExchange(t, &serverStack, &clientStacks[i], buf[:]) // SYN-ACK
|
||||
expectExchange(t, &clientStacks[i], &serverStack, buf[:]) // ACK
|
||||
@@ -173,7 +174,7 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Accept all connections.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
var err error
|
||||
acceptedConns[i], _, err = listener.TryAccept()
|
||||
if err != nil {
|
||||
@@ -185,7 +186,7 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify all connections established.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
if clientConns[i].State() != tcp.StateEstablished {
|
||||
t.Errorf("client %d: expected StateEstablished, got %s", i, clientConns[i].State())
|
||||
}
|
||||
@@ -195,7 +196,7 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test data exchange: client -> server.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
msg := []byte("hello from client " + string('0'+byte(i)))
|
||||
n, err := clientConns[i].Write(msg)
|
||||
if err != nil {
|
||||
@@ -207,12 +208,12 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Exchange data packets from all clients to server.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
expectExchange(t, &clientStacks[i], &serverStack, buf[:])
|
||||
}
|
||||
|
||||
// Read data on server side and verify.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
expected := "hello from client " + string('0'+byte(i))
|
||||
var readBuf [64]byte
|
||||
n, err := acceptedConns[i].Read(readBuf[:])
|
||||
@@ -225,7 +226,7 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test data exchange: server -> client.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
msg := []byte("reply to client " + string('0'+byte(i)))
|
||||
n, err := acceptedConns[i].Write(msg)
|
||||
if err != nil {
|
||||
@@ -237,12 +238,12 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Exchange data packets from server to all clients.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
expectExchange(t, &serverStack, &clientStacks[i], buf[:])
|
||||
}
|
||||
|
||||
// Read responses on client side and verify.
|
||||
for i := 0; i < numClients; i++ {
|
||||
for i := range numClients {
|
||||
expected := "reply to client " + string('0'+byte(i))
|
||||
var readBuf [64]byte
|
||||
n, err := clientConns[i].Read(readBuf[:])
|
||||
@@ -255,8 +256,8 @@ func TestListener_MultiConn(t *testing.T) {
|
||||
}
|
||||
|
||||
// Close connections, alternating between client-initiated and server-initiated.
|
||||
for i := 0; i < numClients; i++ {
|
||||
var closer, responder *StackIP
|
||||
for i := range numClients {
|
||||
var closer, responder *StackIPv4
|
||||
var closerConn, responderConn *tcp.Conn
|
||||
var serverClosed bool
|
||||
whoCloses := "client"
|
||||
@@ -312,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
|
||||
|
||||
@@ -324,7 +325,7 @@ func TestListener_RSTOnPoolExhaustion(t *testing.T) {
|
||||
if err := listener.Reset(serverPort, pool); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := serverStack.Register(&listener); err != nil {
|
||||
if err := serverStack.Register4(&listener); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -340,10 +341,10 @@ func TestListener_RSTOnPoolExhaustion(t *testing.T) {
|
||||
|
||||
// Setup client2 and send its SYN — pool is full, server should queue RST.
|
||||
const client2Port = uint16(1338)
|
||||
setupClient(t, &client2Stack, &client2Conn, serverStack.Addr(), serverPort, client2Port)
|
||||
setupClient(t, &client2Stack, &client2Conn, netip.AddrFrom4(serverStack.Addr4()), serverPort, client2Port)
|
||||
|
||||
// Client2 sends SYN.
|
||||
n, err := client2Stack.Encapsulate(buf[:], -1, 0)
|
||||
n, err := client2Stack.Encapsulate(buf[:], 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal("client2 encapsulate:", err)
|
||||
} else if n == 0 {
|
||||
@@ -356,7 +357,7 @@ func TestListener_RSTOnPoolExhaustion(t *testing.T) {
|
||||
}
|
||||
|
||||
// Server encapsulates — should produce RST (no connection data pending).
|
||||
n, err = serverStack.Encapsulate(buf[:], -1, 0)
|
||||
n, err = serverStack.Encapsulate(buf[:], 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal("server encapsulate RST:", err)
|
||||
} else if n == 0 {
|
||||
@@ -605,25 +606,17 @@ 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(clientIP, 1)
|
||||
client.Reset(new(lneto.Validator), 1)
|
||||
client.SetAddr4(clientIP.As4())
|
||||
err := conn.Configure(tcp.ConnConfig{
|
||||
RxBuf: make([]byte, bufsize),
|
||||
TxBuf: make([]byte, bufsize),
|
||||
TxPacketQueueSize: 3,
|
||||
RWBackoff: backoffYield,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -633,7 +626,7 @@ func setupClient(t *testing.T, client *StackIP, conn *tcp.Conn, serverAddr netip
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = client.Register(conn)
|
||||
err = client.Register4(conn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -657,6 +650,7 @@ func newMockTCPPool(n, queuesize, bufsize int) *mockTCPPool {
|
||||
RxBuf: make([]byte, bufsize),
|
||||
TxBuf: make([]byte, bufsize),
|
||||
TxPacketQueueSize: queuesize,
|
||||
RWBackoff: backoffYield,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
+13
-1
@@ -1,8 +1,12 @@
|
||||
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.
|
||||
MinimumMTU = 68
|
||||
sizeHeader = 20
|
||||
)
|
||||
|
||||
@@ -14,6 +18,14 @@ func IsMulticast(addr [4]byte) bool {
|
||||
return addr[0]&0xf0 == 0xe0
|
||||
}
|
||||
|
||||
// IsLinkLocal reports whether addr is within the IPv4 link-local prefix
|
||||
// 169.254.0.0/16 reserved for dynamic link-local configuration as defined in [RFC3927].
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927
|
||||
func IsLinkLocal(addr [4]byte) bool {
|
||||
return addr[0] == 169 && addr[1] == 254
|
||||
}
|
||||
|
||||
// ToS represents the Traffic Class (a.k.a Type of Service). It is 8 bits long. 6 MSB are Differentiated Services; 2 LSB are Explicit Congenstion Notification.
|
||||
type ToS uint8
|
||||
|
||||
|
||||
@@ -31,6 +31,27 @@ func TestAppendFormatAddr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLinkLocal(t *testing.T) {
|
||||
tests := []struct {
|
||||
addr [4]byte
|
||||
want bool
|
||||
}{
|
||||
{addr: [4]byte{169, 254, 0, 0}, want: true},
|
||||
{addr: [4]byte{169, 254, 1, 1}, want: true},
|
||||
{addr: [4]byte{169, 254, 254, 255}, want: true},
|
||||
{addr: [4]byte{169, 254, 255, 255}, want: true},
|
||||
{addr: [4]byte{169, 253, 1, 1}, want: false},
|
||||
{addr: [4]byte{169, 255, 1, 1}, want: false},
|
||||
{addr: [4]byte{192, 168, 1, 1}, want: false},
|
||||
{addr: [4]byte{0, 0, 0, 0}, want: false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := IsLinkLocal(tc.addr); got != tc.want {
|
||||
t.Errorf("IsLinkLocal(%v): got %v, want %v", tc.addr, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendFormatAddr_noAllocs(t *testing.T) {
|
||||
var buf [24]byte
|
||||
addr := [4]byte{192, 168, 1, 1}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ func TestFrame(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
const wantVersion = 4
|
||||
v := new(lneto.Validator)
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
// SET VALUES:
|
||||
wantIHL := uint8(5 + rng.Intn(10))
|
||||
wantToS := ToS(rng.Intn(4))
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=Type,CodeDestinationUnreachable,CodeRedirect -linecomment -output stringers.go
|
||||
|
||||
const (
|
||||
sizeHeader = 8
|
||||
)
|
||||
@@ -33,7 +35,7 @@ const (
|
||||
type CodeTimeExceeded uint8
|
||||
|
||||
const (
|
||||
CodeExceededInTransit CodeTimeExceeded = iota // TTL exceeded in transit
|
||||
CodeExceededInTransit CodeTimeExceeded = iota // TTL exceeded
|
||||
CodeFragmentReassembly // fragment reassembly time exceeded
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Code generated by "stringer -type=Type,CodeDestinationUnreachable,CodeRedirect -linecomment -output stringers.go"; DO NOT EDIT.
|
||||
|
||||
package icmpv4
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[TypeEchoReply-0]
|
||||
_ = x[TypeEcho-8]
|
||||
_ = x[TypeDestinationUnreachable-3]
|
||||
_ = x[TypeSourceQuench-4]
|
||||
_ = x[TypeRedirect-5]
|
||||
_ = x[TypeTimeExceeded-11]
|
||||
_ = x[TypeParameterProblem-12]
|
||||
_ = x[TypeTimestamp-13]
|
||||
_ = x[TypeTimestampReply-14]
|
||||
_ = x[TypeInfoRequest-15]
|
||||
_ = x[TypeInfoRequestReply-16]
|
||||
}
|
||||
|
||||
const (
|
||||
_Type_name_0 = "echo reply"
|
||||
_Type_name_1 = "destination unreachablesource quenchredirect"
|
||||
_Type_name_2 = "echo"
|
||||
_Type_name_3 = "time exceededparameter problemtimestamptimestamp replyinformation requestinformation request reply"
|
||||
)
|
||||
|
||||
var (
|
||||
_Type_index_1 = [...]uint8{0, 23, 36, 44}
|
||||
_Type_index_3 = [...]uint8{0, 13, 30, 39, 54, 73, 98}
|
||||
)
|
||||
|
||||
func (i Type) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Type_name_0
|
||||
case 3 <= i && i <= 5:
|
||||
i -= 3
|
||||
return _Type_name_1[_Type_index_1[i]:_Type_index_1[i+1]]
|
||||
case i == 8:
|
||||
return _Type_name_2
|
||||
case 11 <= i && i <= 16:
|
||||
i -= 11
|
||||
return _Type_name_3[_Type_index_3[i]:_Type_index_3[i+1]]
|
||||
default:
|
||||
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CodeNetUnreachable-0]
|
||||
_ = x[CodeHostUnreachable-1]
|
||||
_ = x[CodeProtoUnreachable-2]
|
||||
_ = x[CodePortUnreachable-3]
|
||||
_ = x[CodeFragNeededAndDFSet-4]
|
||||
_ = x[CodeSourceRouteFailed-5]
|
||||
}
|
||||
|
||||
const _CodeDestinationUnreachable_name = "net unreachablehost unreachableprotocol unreachableport unreachablefragmentation needed and DF setsource route failed"
|
||||
|
||||
var _CodeDestinationUnreachable_index = [...]uint8{0, 15, 31, 51, 67, 98, 117}
|
||||
|
||||
func (i CodeDestinationUnreachable) String() string {
|
||||
if i >= CodeDestinationUnreachable(len(_CodeDestinationUnreachable_index)-1) {
|
||||
return "CodeDestinationUnreachable(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[i]:_CodeDestinationUnreachable_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CodeRedirectForNetwork-0]
|
||||
_ = x[CodeRedirectForHost-1]
|
||||
_ = x[CodeRedirectForToSAndNetwork-2]
|
||||
_ = x[CodeRedirectToSAndHost-3]
|
||||
}
|
||||
|
||||
const _CodeRedirect_name = "redirect for networkredirect for hostredirect for ToS+networkredirect for ToS+host"
|
||||
|
||||
var _CodeRedirect_index = [...]uint8{0, 20, 37, 61, 82}
|
||||
|
||||
func (i CodeRedirect) String() string {
|
||||
if i >= CodeRedirect(len(_CodeRedirect_index)-1) {
|
||||
return "CodeRedirect(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _CodeRedirect_name[_CodeRedirect_index[i]:_CodeRedirect_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Package linklocal4 implements dynamic configuration of IPv4 link-local addresses
|
||||
// (a.k.a. APIPA, "self-assigned" 169.254.x.x addresses) as specified in [RFC3927].
|
||||
//
|
||||
// The [Handler] is a heapless state machine that claims and defends a link-local
|
||||
// address using ARP probes and announcements. It performs no allocations during
|
||||
// steady-state operation and holds no buffers of its own; the caller provides the
|
||||
// scratch buffer on each call. This makes it suitable for memory constrained and
|
||||
// bare-metal targets.
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927
|
||||
package linklocal4
|
||||
|
||||
import "time"
|
||||
|
||||
// Protocol constants as defined in [RFC3927] section 9.
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927#section-9
|
||||
const (
|
||||
// probeWait is the initial random delay before sending the first probe.
|
||||
probeWait = 1 * time.Second
|
||||
// probeNum is the number of ARP probes to send.
|
||||
probeNum = 3
|
||||
// probeMin and probeMax bound the random spacing between probes.
|
||||
probeMin = 1 * time.Second
|
||||
probeMax = 2 * time.Second
|
||||
// announceWait is the delay after the last probe before announcing.
|
||||
announceWait = 2 * time.Second
|
||||
// announceNum is the number of ARP announcements to send.
|
||||
announceNum = 2
|
||||
// announceInterval is the spacing between announcements.
|
||||
announceInterval = 2 * time.Second
|
||||
// maxConflicts is the number of conflicts after which probing is rate limited.
|
||||
maxConflicts = 10
|
||||
// rateLimitInterval bounds the rate of address claiming once maxConflicts is exceeded.
|
||||
rateLimitInterval = 60 * time.Second
|
||||
// defendInterval is the minimum spacing between defensive announcements before
|
||||
// the address is abandoned to break an endless defense loop.
|
||||
defendInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
// arpIPv4Size is the size of an ARP-over-Ethernet IPv4 packet (RFC826):
|
||||
// 8 byte fixed header + 2*(6 byte hardware addr + 4 byte protocol addr).
|
||||
const arpIPv4Size = 28
|
||||
|
||||
// State represents the stage of the link-local address autoconfiguration
|
||||
// state machine. The transition order during a successful claim is:
|
||||
//
|
||||
// StateWaiting -> StateProbing -> StateAnnouncing -> StateBound
|
||||
type State uint8
|
||||
|
||||
const (
|
||||
// StateInvalid is the zero value; the handler has not been configured.
|
||||
StateInvalid State = iota
|
||||
// StateWaiting is the initial random delay before the first probe is sent.
|
||||
StateWaiting
|
||||
// StateProbing sends ARP probes to detect whether the candidate is in use.
|
||||
StateProbing
|
||||
// StateAnnouncing has claimed the candidate and is broadcasting announcements.
|
||||
StateAnnouncing
|
||||
// StateBound owns the address and defends it against conflicts.
|
||||
StateBound
|
||||
// StateRateLimited is waiting out rateLimitInterval after too many conflicts.
|
||||
StateRateLimited
|
||||
)
|
||||
|
||||
// IsBound reports whether the state machine has successfully claimed an address.
|
||||
func (s State) IsBound() bool { return s == StateBound }
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateInvalid:
|
||||
return "invalid"
|
||||
case StateWaiting:
|
||||
return "waiting"
|
||||
case StateProbing:
|
||||
return "probing"
|
||||
case StateAnnouncing:
|
||||
return "announcing"
|
||||
case StateBound:
|
||||
return "bound"
|
||||
case StateRateLimited:
|
||||
return "rate-limited"
|
||||
default:
|
||||
return "linklocal4.State(?)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package linklocal4
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// Handler implements the [RFC3927] IPv4 link-local address autoconfiguration
|
||||
// state machine. It is a [lneto.StackNode] over the ARP EtherType: it produces
|
||||
// ARP probes and announcements on [Handler.Encapsulate] and inspects incoming
|
||||
// ARP traffic for conflicts on [Handler.Demux].
|
||||
//
|
||||
// Handler is heapless and performs zero allocations after [Handler.Reset];
|
||||
// the only allocation is the one-time capture of the clock function. It holds
|
||||
// no internal buffers and operates entirely on the caller-supplied scratch
|
||||
// buffer, making it suitable for memory constrained targets.
|
||||
//
|
||||
// A Handler claims and defends a single address. Normal "who-has" ARP
|
||||
// resolution of the claimed address is the responsibility of the ARP layer
|
||||
// (see [arp.Handler]); this Handler only manages the claim-and-defend protocol.
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927
|
||||
type Handler struct {
|
||||
connID uint64
|
||||
now func() time.Time
|
||||
|
||||
// nextActionAt is the time at which the next probe/announcement is due.
|
||||
nextActionAt time.Time
|
||||
// lastDefend is the time the most recent defensive announcement was sent.
|
||||
lastDefend time.Time
|
||||
|
||||
prng uint32
|
||||
candidate [4]byte
|
||||
firstCandidate [4]byte
|
||||
hw [6]byte
|
||||
|
||||
state State
|
||||
probesSent uint8
|
||||
announceSent uint8
|
||||
conflicts uint8
|
||||
|
||||
haveFirst bool
|
||||
defendDue bool
|
||||
defendValid bool
|
||||
vld lneto.Validator
|
||||
}
|
||||
|
||||
var _ lneto.StackNode = (*Handler)(nil)
|
||||
|
||||
// linkLocalNet is the RFC3927 IPv4 link-local prefix (169.254.0.0/16). The first
|
||||
// and last /24 within it (169.254.0.x and 169.254.255.x) are reserved per
|
||||
// section 2.1, so the usable host range is 169.254.1.0-169.254.254.255.
|
||||
var linkLocalNet = ipv4.PrefixFrom([4]byte{169, 254, 0, 0}, 16)
|
||||
|
||||
// Config configures a [Handler] for link-local address acquisition.
|
||||
type Config struct {
|
||||
// HardwareAddr is the interface MAC address used as the ARP sender hardware address.
|
||||
HardwareAddr [6]byte
|
||||
// Now is the monotonic clock source used to schedule probes and announcements.
|
||||
// It is required.
|
||||
Now func() time.Time
|
||||
// Seed seeds the pseudo-random address generator. It is required and must be
|
||||
// non-zero. Per RFC3927 section 2.1 it SHOULD be derived from a persistent
|
||||
// per-host value such as the MAC address so that different hosts pick different
|
||||
// sequences and a host tends to reuse the same address across reboots.
|
||||
Seed uint64
|
||||
// FirstCandidate, if within 169.254.1.0-169.254.254.255, is tried before any
|
||||
// random address. Use it to retry a previously recorded address.
|
||||
FirstCandidate [4]byte
|
||||
}
|
||||
|
||||
// Reset configures the handler and begins link-local address acquisition,
|
||||
// transitioning to [StateWaiting]. It increments the connection ID, invalidating
|
||||
// any prior registration.
|
||||
func (h *Handler) Reset(cfg Config) error {
|
||||
if cfg.Now == nil || internal.IsZeroed(cfg.HardwareAddr[:]...) || cfg.Seed == 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
first := cfg.FirstCandidate
|
||||
haveFirst := linkLocalNet.Contains(first) && first[2] >= 1 && first[2] <= 254
|
||||
*h = Handler{
|
||||
connID: h.connID + 1,
|
||||
now: cfg.Now,
|
||||
prng: uint32(cfg.Seed) ^ uint32(cfg.Seed>>32),
|
||||
hw: cfg.HardwareAddr,
|
||||
firstCandidate: first,
|
||||
haveFirst: haveFirst,
|
||||
}
|
||||
if h.prng == 0 {
|
||||
h.prng = 1 // Fold of a non-zero seed can still be zero; xorshift cannot escape the zero state.
|
||||
}
|
||||
h.beginProbing(cfg.Now(), randDelay(h.prand(), probeWait))
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalPort implements [lneto.StackNode]. It always returns 0.
|
||||
func (h *Handler) LocalPort() uint16 { return 0 }
|
||||
|
||||
// Protocol implements [lneto.StackNode], returning the ARP EtherType.
|
||||
func (h *Handler) Protocol() uint64 { return uint64(ethernet.TypeARP) }
|
||||
|
||||
// ConnectionID implements [lneto.StackNode].
|
||||
func (h *Handler) ConnectionID() *uint64 { return &h.connID }
|
||||
|
||||
// State returns the current autoconfiguration state.
|
||||
func (h *Handler) State() State { return h.state }
|
||||
|
||||
// Addr returns the claimed link-local address. ok is true only once the address
|
||||
// has been successfully claimed (state [StateBound]).
|
||||
func (h *Handler) Addr() (addr [4]byte, ok bool) {
|
||||
return h.candidate, h.state == StateBound
|
||||
}
|
||||
|
||||
// Candidate returns the address currently being probed, announced or defended.
|
||||
func (h *Handler) Candidate() [4]byte { return h.candidate }
|
||||
|
||||
// Conflicts returns the number of address conflicts encountered so far.
|
||||
func (h *Handler) Conflicts() int { return int(h.conflicts) }
|
||||
|
||||
// Encapsulate implements [lneto.StackNode]. It writes the next ARP probe or
|
||||
// announcement into carrierData at offsetToFrame when one is due, returning the
|
||||
// number of bytes written, or 0 when no action is pending. The Ethernet
|
||||
// destination, if present before offsetToFrame, is set to broadcast.
|
||||
func (h *Handler) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, error) {
|
||||
if offsetToFrame < 0 || len(carrierData)-offsetToFrame < arpIPv4Size {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
now := h.now()
|
||||
b := carrierData[offsetToFrame:]
|
||||
var senderProto [4]byte // zero = ARP probe; candidate = ARP announcement.
|
||||
switch h.state {
|
||||
case StateWaiting, StateProbing:
|
||||
if now.Before(h.nextActionAt) {
|
||||
return 0, nil
|
||||
}
|
||||
if h.probesSent < probeNum {
|
||||
h.state = StateProbing
|
||||
h.probesSent++
|
||||
if h.probesSent < probeNum {
|
||||
h.nextActionAt = now.Add(randInterval(h.prand(), probeMin, probeMax))
|
||||
} else {
|
||||
h.nextActionAt = now.Add(announceWait)
|
||||
}
|
||||
// senderProto stays zero: this is a probe.
|
||||
} else {
|
||||
// announceWait elapsed with no conflict: claim the address.
|
||||
h.state = StateAnnouncing
|
||||
h.announceSent = 1
|
||||
h.nextActionAt = now.Add(announceInterval)
|
||||
senderProto = h.candidate
|
||||
}
|
||||
|
||||
case StateAnnouncing:
|
||||
if now.Before(h.nextActionAt) {
|
||||
return 0, nil
|
||||
}
|
||||
h.announceSent++
|
||||
senderProto = h.candidate
|
||||
if h.announceSent >= announceNum {
|
||||
h.state = StateBound
|
||||
} else {
|
||||
h.nextActionAt = now.Add(announceInterval)
|
||||
}
|
||||
|
||||
case StateBound:
|
||||
if !h.defendDue {
|
||||
return 0, nil
|
||||
}
|
||||
h.defendDue = false
|
||||
senderProto = h.candidate
|
||||
|
||||
case StateRateLimited:
|
||||
if now.Before(h.nextActionAt) {
|
||||
return 0, nil
|
||||
}
|
||||
// onConflict already selected a fresh candidate; resume probing it.
|
||||
h.beginProbing(now, randDelay(h.prand(), probeWait))
|
||||
return 0, nil
|
||||
|
||||
default:
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
h.putARP(b, senderProto)
|
||||
if offsetToFrame >= 14 {
|
||||
// TODO: Support VLAN-tagged Ethernet headers when setting the broadcast destination.
|
||||
broadcast := ethernet.BroadcastAddr()
|
||||
copy(carrierData[offsetToFrame-14:offsetToFrame-8], broadcast[:])
|
||||
}
|
||||
return arpIPv4Size, nil
|
||||
}
|
||||
|
||||
// Demux implements [lneto.StackNode]. It inspects an incoming ARP frame for
|
||||
// address conflicts per RFC3927 sections 2.2.1 and 2.5, updating the state
|
||||
// machine to reconfigure or defend as required.
|
||||
func (h *Handler) Demux(carrierData []byte, frameOffset int) error {
|
||||
if h.state == StateInvalid {
|
||||
return nil
|
||||
}
|
||||
afrm, err := arp.NewFrame(carrierData[frameOffset:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.vld.ResetErr()
|
||||
afrm.ValidateSize(&h.vld)
|
||||
if h.vld.HasError() {
|
||||
return h.vld.ErrPop()
|
||||
}
|
||||
ptype, plen := afrm.Protocol()
|
||||
if ptype != ethernet.TypeIPv4 || plen != 4 {
|
||||
return nil // Not IPv4 ARP; irrelevant to link-local conflict detection.
|
||||
}
|
||||
senderHW, senderProto := afrm.Sender4()
|
||||
_, targetProto := afrm.Target4()
|
||||
now := h.now()
|
||||
|
||||
switch h.state {
|
||||
case StateWaiting, StateProbing:
|
||||
// Conflict if anyone else uses the candidate as a sender address, or
|
||||
// is probing for the same candidate from a different hardware address.
|
||||
conflict := *senderProto == h.candidate ||
|
||||
(afrm.Operation() == arp.OpRequest && internal.IsZeroed(senderProto[:]...) &&
|
||||
*targetProto == h.candidate && *senderHW != h.hw)
|
||||
if conflict {
|
||||
h.onConflict(now)
|
||||
}
|
||||
|
||||
case StateAnnouncing, StateBound:
|
||||
// We own the address; a conflicting sender hardware address means another
|
||||
// host claims it too.
|
||||
if *senderProto == h.candidate && *senderHW != h.hw {
|
||||
h.onDefend(now)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onConflict handles a conflict detected while probing: pick a new candidate and
|
||||
// restart, rate limiting once maxConflicts is exceeded.
|
||||
func (h *Handler) onConflict(now time.Time) {
|
||||
if h.conflicts < 255 {
|
||||
h.conflicts++
|
||||
}
|
||||
h.selectCandidate()
|
||||
if h.conflicts > maxConflicts {
|
||||
h.state = StateRateLimited
|
||||
h.nextActionAt = now.Add(rateLimitInterval)
|
||||
return
|
||||
}
|
||||
h.beginProbing(now, randDelay(h.prand(), probeWait))
|
||||
}
|
||||
|
||||
// onDefend handles a conflict on an address we own per RFC3927 section 2.5(b):
|
||||
// defend once with a single announcement, but abandon the address if conflicts
|
||||
// recur within defendInterval to avoid an endless defense loop.
|
||||
func (h *Handler) onDefend(now time.Time) {
|
||||
if !h.defendValid || now.Sub(h.lastDefend) >= defendInterval {
|
||||
h.lastDefend = now
|
||||
h.defendValid = true
|
||||
h.defendDue = true
|
||||
return
|
||||
}
|
||||
// Second conflict within defendInterval: give up and reconfigure.
|
||||
h.onConflict(now)
|
||||
}
|
||||
|
||||
// beginProbing resets probe/announce counters and schedules the first probe after delay.
|
||||
func (h *Handler) beginProbing(now time.Time, delay time.Duration) {
|
||||
if internal.IsZeroed(h.candidate[:]...) {
|
||||
h.selectCandidate()
|
||||
}
|
||||
h.state = StateWaiting
|
||||
h.probesSent = 0
|
||||
h.announceSent = 0
|
||||
h.defendDue = false
|
||||
h.defendValid = false
|
||||
h.nextActionAt = now.Add(delay)
|
||||
}
|
||||
|
||||
// selectCandidate picks the next address to try. It uses FirstCandidate once if
|
||||
// provided, otherwise a uniform pseudo-random address in 169.254.1.0-169.254.254.255
|
||||
// per RFC3927 section 2.1 (the first and last /24 are reserved).
|
||||
func (h *Handler) selectCandidate() {
|
||||
if h.haveFirst {
|
||||
h.haveFirst = false
|
||||
h.candidate = h.firstCandidate
|
||||
return
|
||||
}
|
||||
// 254*256 = 65024 usable addresses; offset by one /24 to skip 169.254.0.x.
|
||||
low := 256 + h.prand()%65024
|
||||
h.candidate = [4]byte{169, 254, byte(low >> 8), byte(low)}
|
||||
}
|
||||
|
||||
// putARP marshals an ARP request (probe or announcement) into dst. A probe has
|
||||
// an all-zero sender protocol address; an announcement repeats the candidate.
|
||||
func (h *Handler) putARP(dst []byte, senderProto [4]byte) {
|
||||
f, _ := arp.NewFrame(dst)
|
||||
f.SetHardware(1, 6)
|
||||
f.SetProtocol(ethernet.TypeIPv4, 4)
|
||||
f.SetOperation(arp.OpRequest)
|
||||
shw, sproto := f.Sender4()
|
||||
*shw = h.hw
|
||||
*sproto = senderProto
|
||||
thw, tproto := f.Target4()
|
||||
*thw = [6]byte{} // Target hardware address ignored; set to zero per RFC3927 section 2.2.1.
|
||||
*tproto = h.candidate
|
||||
}
|
||||
|
||||
func (h *Handler) prand() uint32 {
|
||||
h.prng = internal.Prand32(h.prng)
|
||||
return h.prng
|
||||
}
|
||||
|
||||
// randDelay returns a duration uniformly in [0, max] derived from r.
|
||||
func randDelay(r uint32, max time.Duration) time.Duration {
|
||||
return time.Duration(uint64(r) % uint64(max+1))
|
||||
}
|
||||
|
||||
// randInterval returns a duration uniformly in [min, max] derived from r.
|
||||
func randInterval(r uint32, min, max time.Duration) time.Duration {
|
||||
span := max - min
|
||||
if span <= 0 {
|
||||
return min
|
||||
}
|
||||
return min + time.Duration(uint64(r)%uint64(span+1))
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package linklocal4
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
type fakeClock struct{ t time.Time }
|
||||
|
||||
func (c *fakeClock) now() time.Time { return c.t }
|
||||
func (c *fakeClock) advance(d time.Duration) { c.t = c.t.Add(d) }
|
||||
|
||||
var (
|
||||
ourHW = [6]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}
|
||||
otherHW = [6]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x02}
|
||||
)
|
||||
|
||||
const frameOff = 14 // pretend there is an ethernet header before the ARP frame.
|
||||
|
||||
func newHandler(t *testing.T, clk *fakeClock) *Handler {
|
||||
t.Helper()
|
||||
var h Handler
|
||||
err := h.Reset(Config{
|
||||
HardwareAddr: ourHW,
|
||||
Now: clk.now,
|
||||
Seed: 0xC0FFEE,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.State() != StateWaiting {
|
||||
t.Fatalf("expected StateWaiting after Reset, got %s", h.State())
|
||||
}
|
||||
return &h
|
||||
}
|
||||
|
||||
// step advances the clock past any pending interval and runs one Encapsulate.
|
||||
func step(t *testing.T, h *Handler, clk *fakeClock, buf []byte) (arp.Frame, int) {
|
||||
t.Helper()
|
||||
clk.advance(3 * time.Second) // larger than any RFC3927 probe/announce interval.
|
||||
n, err := h.Encapsulate(buf, -1, frameOff)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n == 0 {
|
||||
return arp.Frame{}, 0
|
||||
}
|
||||
f, err := arp.NewFrame(buf[frameOff : frameOff+n])
|
||||
if err != nil {
|
||||
t.Fatalf("invalid arp produced: %v", err)
|
||||
}
|
||||
var vld lneto.Validator
|
||||
f.ValidateSize(&vld)
|
||||
if vld.HasError() {
|
||||
t.Fatalf("invalid arp size: %v", vld.ErrPop())
|
||||
}
|
||||
if f.Operation() != arp.OpRequest {
|
||||
t.Fatalf("link-local ARP must be a request, got %s", f.Operation())
|
||||
}
|
||||
// Ethernet destination must be broadcast.
|
||||
bc := ethernet.BroadcastAddr()
|
||||
for i := range 6 {
|
||||
if buf[i] != bc[i] {
|
||||
t.Fatalf("ethernet destination not broadcast: %x", buf[:6])
|
||||
}
|
||||
}
|
||||
return f, n
|
||||
}
|
||||
|
||||
func TestClaim(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
|
||||
cand := h.Candidate()
|
||||
if !ipv4.IsLinkLocal(cand) || cand[2] < 1 || cand[2] > 254 {
|
||||
t.Fatalf("candidate %v not a valid link-local address", cand)
|
||||
}
|
||||
|
||||
var probes, announces int
|
||||
for i := 0; i < 10 && h.State() != StateBound; i++ {
|
||||
f, n := step(t, h, clk, buf)
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
_, sproto := f.Sender4()
|
||||
_, tproto := f.Target4()
|
||||
shw, _ := f.Sender4()
|
||||
if *shw != ourHW {
|
||||
t.Fatalf("sender hardware address mismatch: %x", *shw)
|
||||
}
|
||||
if *tproto != cand {
|
||||
t.Fatalf("target proto must be candidate %v, got %v", cand, *tproto)
|
||||
}
|
||||
if *sproto == ([4]byte{}) {
|
||||
probes++
|
||||
} else if *sproto == cand {
|
||||
announces++
|
||||
} else {
|
||||
t.Fatalf("unexpected sender proto %v", *sproto)
|
||||
}
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("expected StateBound, got %s", h.State())
|
||||
}
|
||||
if probes != probeNum {
|
||||
t.Errorf("expected %d probes, got %d", probeNum, probes)
|
||||
}
|
||||
if announces != announceNum {
|
||||
t.Errorf("expected %d announcements, got %d", announceNum, announces)
|
||||
}
|
||||
addr, ok := h.Addr()
|
||||
if !ok || addr != cand {
|
||||
t.Fatalf("Addr()=%v,%v want %v,true", addr, ok, cand)
|
||||
}
|
||||
}
|
||||
|
||||
// makeARP builds an ARP IPv4 frame in buf for conflict-detection tests.
|
||||
func makeARP(t *testing.T, buf []byte, op arp.Operation, senderHW [6]byte, senderProto, targetProto [4]byte) []byte {
|
||||
t.Helper()
|
||||
f, err := arp.NewFrame(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.SetHardware(1, 6)
|
||||
f.SetProtocol(ethernet.TypeIPv4, 4)
|
||||
f.SetOperation(op)
|
||||
shw, sp := f.Sender4()
|
||||
*shw = senderHW
|
||||
*sp = senderProto
|
||||
thw, tp := f.Target4()
|
||||
*thw = [6]byte{}
|
||||
*tp = targetProto
|
||||
return buf[:arpIPv4Size]
|
||||
}
|
||||
|
||||
func TestConflictDuringProbe(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
|
||||
// Send the first probe.
|
||||
_, n := step(t, h, clk, buf)
|
||||
if n == 0 {
|
||||
t.Fatal("expected first probe")
|
||||
}
|
||||
cand := h.Candidate()
|
||||
|
||||
// Another host replies/uses the candidate as its sender address: conflict.
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpReply, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Conflicts() != 1 {
|
||||
t.Fatalf("expected 1 conflict, got %d", h.Conflicts())
|
||||
}
|
||||
if h.Candidate() == cand {
|
||||
t.Fatal("expected a new candidate after conflict")
|
||||
}
|
||||
if h.State() != StateWaiting {
|
||||
t.Fatalf("expected restart in StateWaiting, got %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeConflictFromSimultaneousProbe(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
step(t, h, clk, buf) // first probe
|
||||
cand := h.Candidate()
|
||||
|
||||
// Another host probes for the same candidate (zero sender proto, different HW).
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, otherHW, [4]byte{}, cand)
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Conflicts() != 1 || h.Candidate() == cand {
|
||||
t.Fatalf("simultaneous probe should cause conflict: conflicts=%d cand=%v", h.Conflicts(), h.Candidate())
|
||||
}
|
||||
}
|
||||
|
||||
func driveToBound(t *testing.T, h *Handler, clk *fakeClock, buf []byte) {
|
||||
t.Helper()
|
||||
for i := 0; i < 10 && h.State() != StateBound; i++ {
|
||||
step(t, h, clk, buf)
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("failed to reach StateBound, stuck at %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefense(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
driveToBound(t, h, clk, buf)
|
||||
cand := h.Candidate()
|
||||
|
||||
// A conflicting ARP from another host: handler should defend with one announcement.
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := h.Encapsulate(buf, -1, frameOff)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("expected defensive announcement, n=%d err=%v", n, err)
|
||||
}
|
||||
f, _ := arp.NewFrame(buf[frameOff : frameOff+n])
|
||||
_, sproto := f.Sender4()
|
||||
if *sproto != cand {
|
||||
t.Fatalf("defensive announcement must use candidate as sender, got %v", *sproto)
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("should remain bound after a single defense, got %s", h.State())
|
||||
}
|
||||
// Subsequent Encapsulate yields nothing more.
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("expected only a single defensive announcement")
|
||||
}
|
||||
|
||||
// A second conflict within defendInterval forces reconfiguration.
|
||||
clk.advance(defendInterval / 2)
|
||||
frame = makeARP(t, arpbuf[:], arp.OpReply, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.State() == StateBound {
|
||||
t.Fatal("expected reconfiguration after repeated conflict within defendInterval")
|
||||
}
|
||||
if h.Candidate() == cand {
|
||||
t.Fatal("expected new candidate after giving up address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoSelfConflict(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
driveToBound(t, h, clk, buf)
|
||||
cand := h.Candidate()
|
||||
|
||||
// Our own announcement (same hardware address) must not be treated as a conflict.
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, ourHW, cand, cand)
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("self-sent ARP must not trigger a defense")
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("state changed on self ARP: %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstCandidate(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
want := [4]byte{169, 254, 42, 7}
|
||||
var h Handler
|
||||
err := h.Reset(Config{
|
||||
HardwareAddr: ourHW,
|
||||
Now: clk.now,
|
||||
Seed: 0xC0FFEE,
|
||||
FirstCandidate: want,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Candidate() != want {
|
||||
t.Fatalf("expected FirstCandidate %v, got %v", want, h.Candidate())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
var arpbuf [64]byte
|
||||
// Force more than maxConflicts conflicts during probing.
|
||||
for i := 0; i <= maxConflicts; i++ {
|
||||
step(t, h, clk, buf) // emit a probe for the current candidate.
|
||||
cand := h.Candidate()
|
||||
frame := makeARP(t, arpbuf[:], arp.OpReply, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if h.State() != StateRateLimited {
|
||||
t.Fatalf("expected StateRateLimited after %d conflicts, got %s", h.Conflicts(), h.State())
|
||||
}
|
||||
// Before rateLimitInterval elapses no probe is emitted.
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("must not probe while rate limited")
|
||||
}
|
||||
// After the interval the machine resumes probing.
|
||||
clk.advance(rateLimitInterval + time.Second)
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("rate-limit recovery should reschedule, not emit immediately")
|
||||
}
|
||||
if h.State() != StateWaiting {
|
||||
t.Fatalf("expected StateWaiting after rate-limit recovery, got %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroAlloc(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
driveToBound(t, h, clk, buf)
|
||||
cand := h.Candidate()
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, otherHW, cand, [4]byte{1, 2, 3, 4})
|
||||
|
||||
if n := testing.AllocsPerRun(100, func() {
|
||||
_, _ = h.Encapsulate(buf, -1, frameOff)
|
||||
}); n != 0 {
|
||||
t.Errorf("Encapsulate allocated %g times, want 0", n)
|
||||
}
|
||||
if n := testing.AllocsPerRun(100, func() {
|
||||
_ = h.Demux(frame, 0)
|
||||
}); n != 0 {
|
||||
t.Errorf("Demux allocated %g times, want 0", n)
|
||||
}
|
||||
}
|
||||
+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
-1
@@ -17,7 +17,7 @@ func AppendFormatAddr(dst []byte, addr [16]byte) []byte {
|
||||
// Find the longest run of consecutive all-zero 16-bit groups for :: compression.
|
||||
bestStart, bestLen := 0, 0
|
||||
curStart := -1
|
||||
for i := 0; i < 8; i++ {
|
||||
for i := range 8 {
|
||||
if addr[i*2] == 0 && addr[i*2+1] == 0 {
|
||||
if curStart < 0 {
|
||||
curStart = i
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
var _ lneto.StackNode = (*Client)(nil)
|
||||
|
||||
const (
|
||||
keyHashCompletedBit = 1 << 31
|
||||
keyHashSentBit = 1 << 30
|
||||
keyHashBits = (1 << 30) - 1
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
// Echo (ping) fields.
|
||||
ResponseQueueBuffer []byte
|
||||
ResponseQueueLimit int
|
||||
HashSeed uint32
|
||||
ID uint16
|
||||
// NDP fields; NDP is disabled when NDPCache == 0.
|
||||
OurAddr [16]byte
|
||||
OurMAC [6]byte
|
||||
NDPCache int
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
connid uint64
|
||||
magic uint32
|
||||
_seq uint16
|
||||
id uint16
|
||||
|
||||
outgoingEcho []struct {
|
||||
pattern []byte
|
||||
key uint32
|
||||
size uint16
|
||||
raddr [16]byte
|
||||
}
|
||||
|
||||
// responseLengths stores the length of responses received.
|
||||
// together they should add up to the written length of responseRing.
|
||||
incomingEcho []struct {
|
||||
length uint16
|
||||
id uint16
|
||||
seq uint16
|
||||
raddr [16]byte
|
||||
}
|
||||
responseRing internal.Ring
|
||||
|
||||
// NDP address resolution fields.
|
||||
ndpCache ndpCache
|
||||
onresolve func(mac [6]byte, addr [16]byte)
|
||||
ourMAC [6]byte
|
||||
ourIP [16]byte
|
||||
}
|
||||
|
||||
func (client *Client) Configure(cfg ClientConfig) error {
|
||||
echoOK := cfg.HashSeed != 0 && len(cfg.ResponseQueueBuffer) >= 16 && cfg.ResponseQueueLimit > 0
|
||||
ndpOK := cfg.NDPCache > 0
|
||||
if !echoOK && !ndpOK {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
client.connid++
|
||||
if echoOK {
|
||||
internal.SliceReuse(&client.outgoingEcho, cfg.ResponseQueueLimit)
|
||||
internal.SliceReuse(&client.incomingEcho, cfg.ResponseQueueLimit)
|
||||
client.responseRing = internal.Ring{Buf: cfg.ResponseQueueBuffer}
|
||||
client.magic = cfg.HashSeed
|
||||
client.id = cfg.ID
|
||||
}
|
||||
if ndpOK {
|
||||
client.ourIP = cfg.OurAddr
|
||||
client.ourMAC = cfg.OurMAC
|
||||
client.ndpCache.reset(cfg.NDPCache)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (client *Client) SetAddr6(addr [16]byte) {
|
||||
client.ourIP = addr
|
||||
client.Reset()
|
||||
}
|
||||
|
||||
func (client *Client) Protocol() uint64 { return uint64(lneto.IPProtoIPv6ICMP) }
|
||||
func (client *Client) LocalPort() uint16 { return 0 }
|
||||
func (client *Client) ConnectionID() *uint64 { return &client.connid }
|
||||
|
||||
func (client *Client) Abort() {
|
||||
client.Reset()
|
||||
client.connid++
|
||||
}
|
||||
|
||||
func (client *Client) Reset() {
|
||||
client.incomingEcho = client.incomingEcho[:0]
|
||||
client.outgoingEcho = client.outgoingEcho[:0]
|
||||
client.responseRing.Reset()
|
||||
client.ndpCache.reset(0)
|
||||
}
|
||||
|
||||
func (client *Client) Demux(carrierData []byte, frameOffset int) error {
|
||||
rawdata := carrierData[frameOffset:]
|
||||
ifrm, err := NewFrame(rawdata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tp := ifrm.Type()
|
||||
ipEnabled := frameOffset >= 40
|
||||
var crc lneto.CRC791
|
||||
if ipEnabled {
|
||||
crc.WriteEven(carrierData[8:40]) // IPv6 src(16B)+dst(16B) pseudo-header
|
||||
crc.AddUint32(uint32(len(rawdata)))
|
||||
crc.AddUint32(uint32(lneto.IPProtoIPv6ICMP))
|
||||
}
|
||||
if crc.PayloadSum16(rawdata) != 0 {
|
||||
return lneto.ErrBadCRC
|
||||
}
|
||||
switch tp {
|
||||
case TypeEchoRequest, TypeEchoReply:
|
||||
return client.demuxEcho(carrierData, frameOffset)
|
||||
case TypeNeighborSolicitation, TypeNeighborAdvertisement:
|
||||
return client.demuxNDP(carrierData, frameOffset)
|
||||
default:
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) Encapsulate(carrierData []byte, ipOffset, frameOffset int) (int, error) {
|
||||
n, dst, err := client.encapsEcho(carrierData, frameOffset)
|
||||
if n == 0 && err == nil {
|
||||
n, dst, err = client.encapsNDP(carrierData, frameOffset)
|
||||
}
|
||||
if n == 0 || err != nil {
|
||||
return n, err
|
||||
}
|
||||
ifrm, _ := NewFrame(carrierData[frameOffset : frameOffset+n])
|
||||
ifrm.SetCRC(0)
|
||||
if ipOffset >= 0 {
|
||||
if err = internal.SetIPAddrs(carrierData[ipOffset:], 0, nil, dst[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var crc lneto.CRC791
|
||||
crc.WriteEven(carrierData[ipOffset+8 : ipOffset+40])
|
||||
crc.AddUint32(uint32(n))
|
||||
crc.AddUint32(uint32(lneto.IPProtoIPv6ICMP))
|
||||
ifrm.SetCRC(crc.PayloadSum16(carrierData[frameOffset : frameOffset+n]))
|
||||
} else {
|
||||
var crc lneto.CRC791
|
||||
ifrm.SetCRC(crc.PayloadSum16(carrierData[frameOffset : frameOffset+n]))
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// NDP public API — mirrors NDPHandler but on the unified Client.
|
||||
|
||||
func (client *Client) SetNDPResolveCallback(cb func(mac [6]byte, addr [16]byte)) {
|
||||
client.onresolve = cb
|
||||
}
|
||||
|
||||
func (client *Client) NDPStartQuery(addr [16]byte, triggerCallback bool) error {
|
||||
if addr == ([16]byte{}) {
|
||||
return lneto.ErrZeroDestination
|
||||
}
|
||||
e := client.ndpCache.acquireNext()
|
||||
e.use([6]byte{}, addr, ndpFlagIncomplete|ndpFlagIncompletePendingQuery|ndpFlagPriority)
|
||||
if triggerCallback {
|
||||
e.flags |= ndpFlagResolveTriggersCallback
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) NDPCacheLookup(addr [16]byte) ([6]byte, error) {
|
||||
e := client.ndpCache.Lookup(addr)
|
||||
if e == nil {
|
||||
return [6]byte{}, errNDPQueryNotFound
|
||||
} else if e.flags.hasAny(ndpFlagIncomplete) {
|
||||
return [6]byte{}, errNDPQueryPending
|
||||
}
|
||||
return e.mac, nil
|
||||
}
|
||||
|
||||
func (client *Client) NDPCacheSeed(addr [16]byte, mac [6]byte) error {
|
||||
if addr == ([16]byte{}) {
|
||||
return lneto.ErrZeroDestination
|
||||
}
|
||||
e := client.ndpCache.acquireNext()
|
||||
e.use(mac, addr, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) NDPCacheRemove(addr [16]byte) error {
|
||||
e := client.ndpCache.Lookup(addr)
|
||||
if e == nil {
|
||||
return errNDPQueryNotFound
|
||||
}
|
||||
e.destroy()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
const (
|
||||
ndpOptSourceLinkAddr = 1 // RFC 4861 §4.6.1
|
||||
ndpOptTargetLinkAddr = 2 // RFC 4861 §4.6.2
|
||||
)
|
||||
|
||||
var (
|
||||
errNDPQueryPending = errors.New("icmpv6: NDP query pending")
|
||||
errNDPQueryNotFound = errors.New("icmpv6: NDP query not found")
|
||||
)
|
||||
|
||||
func (client *Client) demuxNDP(carrierData []byte, frameOffset int) error {
|
||||
rawdata := carrierData[frameOffset:]
|
||||
if len(rawdata) < sizeNDPBase {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
ifrm, _ := NewFrame(rawdata) // already validated by Demux
|
||||
tp := ifrm.Type()
|
||||
targetAddr := (*[16]byte)(rawdata[8:24])
|
||||
options := rawdata[24:]
|
||||
ipEnabled := frameOffset >= 40
|
||||
switch tp {
|
||||
case TypeNeighborSolicitation:
|
||||
if *targetAddr != client.ourIP {
|
||||
return nil // Not for us.
|
||||
}
|
||||
mac, ok := parseLinkLayerOption(options, ndpOptSourceLinkAddr)
|
||||
if !ok {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
var senderAddr [16]byte
|
||||
if ipEnabled {
|
||||
copy(senderAddr[:], carrierData[8:24]) // IPv6 source address
|
||||
}
|
||||
e := client.ndpCache.acquireNext()
|
||||
e.use(mac, senderAddr, ndpFlagPendingResponse)
|
||||
|
||||
case TypeNeighborAdvertisement:
|
||||
mac, ok := parseLinkLayerOption(options, ndpOptTargetLinkAddr)
|
||||
if !ok {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
e := client.ndpCache.Lookup(*targetAddr)
|
||||
if e == nil {
|
||||
return nil // Unsolicited or already evicted.
|
||||
}
|
||||
e.mac = mac
|
||||
e.flags &^= ndpFlagIncomplete | ndpFlagIncompletePendingQuery
|
||||
if e.flags.hasAny(ndpFlagResolveTriggersCallback) && client.onresolve != nil {
|
||||
client.onresolve(mac, *targetAddr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) encapsNDP(carrierData []byte, frameOffset int) (n int, dst [16]byte, err error) {
|
||||
buf := carrierData[frameOffset:]
|
||||
if len(buf) < sizeNDP {
|
||||
return 0, [16]byte{}, lneto.ErrShortBuffer
|
||||
}
|
||||
tp := TypeNeighborAdvertisement
|
||||
e := client.ndpCache.getNextFlagged(ndpFlagPendingResponse) // Prioritize responses.
|
||||
if e == nil {
|
||||
e = client.ndpCache.getNextFlagged(ndpFlagIncompletePendingQuery)
|
||||
if e == nil {
|
||||
return 0, [16]byte{}, nil
|
||||
}
|
||||
e.flags &^= ndpFlagIncompletePendingQuery
|
||||
tp = TypeNeighborSolicitation
|
||||
} else {
|
||||
e.flags &^= ndpFlagPendingResponse
|
||||
}
|
||||
n, err = e.put(buf, client.ourIP, client.ourMAC, tp)
|
||||
if err != nil {
|
||||
return 0, [16]byte{}, err
|
||||
}
|
||||
switch tp {
|
||||
case TypeNeighborSolicitation:
|
||||
dst = solicitedNodeMulticast(e.addr)
|
||||
case TypeNeighborAdvertisement:
|
||||
dst = e.addr
|
||||
}
|
||||
return n, dst, nil
|
||||
}
|
||||
|
||||
// solicitedNodeMulticast returns the solicited-node multicast address for addr (RFC 4291 §2.7.1).
|
||||
func solicitedNodeMulticast(addr [16]byte) [16]byte {
|
||||
return [16]byte{
|
||||
0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0xff,
|
||||
addr[13], addr[14], addr[15],
|
||||
}
|
||||
}
|
||||
|
||||
// parseLinkLayerOption scans NDP options for the first option of optType
|
||||
// and returns the embedded 6-byte Ethernet address.
|
||||
func parseLinkLayerOption(options []byte, optType byte) ([6]byte, bool) {
|
||||
for len(options) >= sizeNDPOption {
|
||||
t := options[0]
|
||||
l := int(options[1]) * 8
|
||||
if l == 0 || l > len(options) {
|
||||
break
|
||||
}
|
||||
if t == optType && l >= sizeNDPOption {
|
||||
return [6]byte(options[2:8]), true
|
||||
}
|
||||
options = options[l:]
|
||||
}
|
||||
return [6]byte{}, false
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
func (client *Client) PingIncomingCapacity() int {
|
||||
return cap(client.incomingEcho)
|
||||
}
|
||||
|
||||
func (client *Client) PingStart(remoteAddr [16]byte, pattern []byte, size uint16) (key uint32, err error) {
|
||||
if int(size) < len(pattern) || len(pattern) == 0 {
|
||||
return 0, lneto.ErrInvalidConfig
|
||||
} else if remoteAddr == [16]byte{} {
|
||||
return 0, lneto.ErrZeroDestination
|
||||
}
|
||||
free := cap(client.outgoingEcho) - len(client.outgoingEcho)
|
||||
if free == 0 {
|
||||
return 0, lneto.ErrExhausted
|
||||
}
|
||||
key = client.magichash(pattern, int(size)) & keyHashBits
|
||||
v := internal.SliceReclaim(&client.outgoingEcho)
|
||||
v.key = key
|
||||
v.size = size
|
||||
v.pattern = append(v.pattern[:0], pattern...)
|
||||
v.raddr = remoteAddr
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (client *Client) PingPeek(key uint32) (completed, ok bool) {
|
||||
idx := client.pingidx(key)
|
||||
if idx >= 0 {
|
||||
return client.outgoingEcho[idx].key&keyHashCompletedBit != 0, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (client *Client) PingPop(key uint32) (completed, ok bool) {
|
||||
idx := client.pingidx(key)
|
||||
if idx >= 0 {
|
||||
completed := client.outgoingEcho[idx].key&keyHashCompletedBit != 0
|
||||
client.outgoingEcho = slices.Delete(client.outgoingEcho, idx, idx+1)
|
||||
return completed, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (client *Client) pingidx(key uint32) int {
|
||||
for i := range client.outgoingEcho {
|
||||
if client.outgoingEcho[i].key&keyHashBits == key {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (client *Client) seq() uint16 {
|
||||
client._seq++
|
||||
return client._seq
|
||||
}
|
||||
|
||||
func (client *Client) magichash(pattern []byte, size int) (hash uint32) {
|
||||
hash = client.magic
|
||||
i := 0
|
||||
n := size / len(pattern)
|
||||
for i < n {
|
||||
for _, b := range pattern {
|
||||
hash = hash*31 + uint32(b)
|
||||
}
|
||||
i++
|
||||
}
|
||||
n = size % len(pattern)
|
||||
for i = 0; i < n; i++ {
|
||||
hash = hash*31 + uint32(pattern[i])
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// demuxEcho handles TypeEchoRequest and TypeEchoReply frames.
|
||||
// CRC has already been verified by the caller (Client.Demux).
|
||||
func (client *Client) demuxEcho(carrierData []byte, frameOffset int) error {
|
||||
rawdata := carrierData[frameOffset:]
|
||||
ifrm, _ := NewFrame(rawdata) // already validated by Demux
|
||||
tp := ifrm.Type()
|
||||
ipEnabled := frameOffset >= 40
|
||||
var raddr [16]byte
|
||||
if ipEnabled {
|
||||
src, _, _, _, _ := internal.GetIPAddr(carrierData)
|
||||
if len(src) == 16 {
|
||||
raddr = [16]byte(src)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
switch tp {
|
||||
case TypeEchoRequest:
|
||||
free := cap(client.incomingEcho) - len(client.incomingEcho)
|
||||
if free == 0 {
|
||||
return lneto.ErrExhausted
|
||||
}
|
||||
efrm := FrameEcho{Frame: ifrm}
|
||||
data := efrm.Data()
|
||||
if len(data) == 0 {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
n, werr := client.responseRing.Write(data)
|
||||
if werr != nil {
|
||||
err = werr
|
||||
break
|
||||
}
|
||||
v := internal.SliceReclaim(&client.incomingEcho)
|
||||
v.length = uint16(n)
|
||||
v.id = efrm.Identifier()
|
||||
v.seq = efrm.SequenceNumber()
|
||||
v.raddr = raddr
|
||||
|
||||
case TypeEchoReply:
|
||||
efrm := FrameEcho{Frame: ifrm}
|
||||
data := efrm.Data()
|
||||
if len(data) == 0 {
|
||||
return lneto.ErrPacketDrop
|
||||
}
|
||||
hash := client.magichash(data, len(data)) & keyHashBits
|
||||
idx := client.pingidx(hash)
|
||||
if idx < 0 || (ipEnabled && client.outgoingEcho[idx].raddr != raddr) {
|
||||
err = lneto.ErrPacketDrop
|
||||
break
|
||||
}
|
||||
client.outgoingEcho[idx].key |= keyHashCompletedBit
|
||||
|
||||
default:
|
||||
err = lneto.ErrPacketDrop
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// encapsEcho writes an echo reply or request frame into carrierData[frameOffset:].
|
||||
// CRC and SetIPAddrs are handled by the caller (Client.Encapsulate).
|
||||
func (client *Client) encapsEcho(carrierData []byte, frameOffset int) (n int, dst [16]byte, err error) {
|
||||
if len(client.incomingEcho) == 0 && len(client.outgoingEcho) == 0 {
|
||||
return 0, [16]byte{}, nil
|
||||
}
|
||||
ifrm, err := NewFrame(carrierData[frameOffset:])
|
||||
if err != nil {
|
||||
return 0, [16]byte{}, err
|
||||
}
|
||||
if len(client.incomingEcho) > 0 {
|
||||
// Priority: send echo reply.
|
||||
inc := client.incomingEcho[0]
|
||||
efrm := FrameEcho{Frame: ifrm}
|
||||
efrm.SetType(TypeEchoReply)
|
||||
efrm.SetIdentifier(inc.id)
|
||||
efrm.SetSequenceNumber(inc.seq)
|
||||
dataLen := int(inc.length)
|
||||
_, rerr := client.responseRing.Read(efrm.Data()[:dataLen])
|
||||
if rerr != nil {
|
||||
return 0, [16]byte{}, rerr
|
||||
}
|
||||
client.incomingEcho = slices.Delete(client.incomingEcho, 0, 1)
|
||||
n = sizeHeader + dataLen
|
||||
dst = inc.raddr
|
||||
} else {
|
||||
idx := 0
|
||||
for idx < len(client.outgoingEcho) && client.outgoingEcho[idx].key&keyHashSentBit != 0 {
|
||||
idx++
|
||||
}
|
||||
if idx >= len(client.outgoingEcho) {
|
||||
return 0, [16]byte{}, nil // No pending packet to send.
|
||||
}
|
||||
out := &client.outgoingEcho[idx]
|
||||
efrm := FrameEcho{Frame: ifrm}
|
||||
efrm.SetType(TypeEchoRequest)
|
||||
efrm.SetIdentifier(client.id)
|
||||
efrm.SetSequenceNumber(client.seq())
|
||||
pattern := out.pattern
|
||||
data := efrm.Data()
|
||||
size := int(out.size)
|
||||
written := 0
|
||||
for written+len(pattern) <= size && written+len(pattern) <= len(data) {
|
||||
copy(data[written:], pattern)
|
||||
written += len(pattern)
|
||||
}
|
||||
copy(data[written:written+size%len(pattern)], pattern)
|
||||
n = sizeHeader + size
|
||||
out.key |= keyHashSentBit
|
||||
dst = out.raddr
|
||||
}
|
||||
ifrm.buf = carrierData[frameOffset : frameOffset+n]
|
||||
ifrm.SetCode(0)
|
||||
return n, dst, nil
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
testHashSeed = 0xdeadbeef
|
||||
)
|
||||
|
||||
func TestClients(t *testing.T) {
|
||||
const sizebuffer = 64
|
||||
const queuesize = 2
|
||||
var sender, responder Client
|
||||
err := sender.Configure(ClientConfig{
|
||||
ResponseQueueBuffer: make([]byte, sizebuffer),
|
||||
ResponseQueueLimit: queuesize,
|
||||
HashSeed: testHashSeed,
|
||||
ID: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = responder.Configure(ClientConfig{
|
||||
ResponseQueueBuffer: make([]byte, sizebuffer),
|
||||
ResponseQueueLimit: queuesize,
|
||||
HashSeed: testHashSeed,
|
||||
ID: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pattern := []byte("ab12")
|
||||
size := 8
|
||||
var buf [64]byte
|
||||
key1 := testSingleExchange(t, &sender, &responder, buf[:], pattern, uint16(size))
|
||||
completed, ok := sender.PingPop(key1)
|
||||
if !completed || !ok {
|
||||
t.Fatal("ping did not complete or not exist")
|
||||
}
|
||||
n, err := sender.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if n > 0 {
|
||||
t.Error("sender: expected no more data to be sent")
|
||||
}
|
||||
n, err = responder.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if n > 0 {
|
||||
t.Error("responder: expected no more data to be sent")
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleExchange(t *testing.T, sender, responder *Client, buf []byte, pattern []byte, size uint16) (senderKey uint32) {
|
||||
const frameOff = 0
|
||||
const ipOff = -1
|
||||
n, err := responder.Encapsulate(buf, ipOff, frameOff)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n > 0 {
|
||||
t.Fatal("expected no data pending to be sent from responder")
|
||||
}
|
||||
senderKey, n = testSendEcho(t, sender, buf, pattern, size)
|
||||
completed, ok := sender.PingPeek(senderKey)
|
||||
if !ok {
|
||||
t.Error("ping key not exist")
|
||||
} else if completed {
|
||||
t.Error("ping completed before response")
|
||||
}
|
||||
ifrm, _ := NewFrame(buf[frameOff : frameOff+n])
|
||||
efrm := FrameEcho{Frame: ifrm}
|
||||
id, seq := efrm.Identifier(), efrm.SequenceNumber()
|
||||
err1 := responder.Demux(buf[:frameOff+n], frameOff)
|
||||
if err1 != nil {
|
||||
t.Error("responder demux during single", err1)
|
||||
}
|
||||
n, err = responder.Encapsulate(buf, ipOff, frameOff)
|
||||
if err != nil {
|
||||
t.Error("responder encaps during single", err)
|
||||
return
|
||||
} else if n == 0 && err1 == nil {
|
||||
t.Error("responder wrote no data")
|
||||
return
|
||||
}
|
||||
ifrm, err = NewFrame(buf[frameOff : frameOff+n])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ifrm.Type() != TypeEchoReply {
|
||||
t.Fatalf("expected echo reply %d", ifrm.Type())
|
||||
}
|
||||
efrm = FrameEcho{Frame: ifrm}
|
||||
if efrm.Identifier() != id {
|
||||
t.Error("mismatched identifier want/got:", id, efrm.Identifier())
|
||||
}
|
||||
if efrm.SequenceNumber() != seq {
|
||||
t.Error("mismatched sequence number want/got:", seq, efrm.SequenceNumber())
|
||||
}
|
||||
data := efrm.Data()
|
||||
testPatternMatch(t, data, pattern, int(size))
|
||||
err = sender.Demux(buf[:frameOff+n], frameOff)
|
||||
if err != nil {
|
||||
t.Error("sender demuxed response", err)
|
||||
}
|
||||
completed, ok = sender.PingPeek(senderKey)
|
||||
if !completed {
|
||||
t.Error("expected ping to have completed")
|
||||
}
|
||||
if !ok {
|
||||
t.Error("ping key not exist after completion")
|
||||
}
|
||||
if completed2, ok2 := sender.PingPeek(senderKey); completed != completed2 || ok != ok2 {
|
||||
t.Error("change in status after peek")
|
||||
}
|
||||
n, err = sender.Encapsulate(buf, ipOff, frameOff)
|
||||
if err != nil {
|
||||
t.Error("error after done")
|
||||
}
|
||||
if n > 0 {
|
||||
t.Error("expected no data to be sent after ping completion", n)
|
||||
}
|
||||
return senderKey
|
||||
}
|
||||
|
||||
func testSendEcho(t *testing.T, sender *Client, buf []byte, pattern []byte, size uint16) (key uint32, n int) {
|
||||
t.Helper()
|
||||
const frameOff = 0
|
||||
const ipOff = -1
|
||||
n, err := sender.Encapsulate(buf, ipOff, frameOff)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n > 0 {
|
||||
t.Fatal("expected no data pending to send on testSendEcho start")
|
||||
}
|
||||
key, err = sender.PingStart([16]byte{1}, pattern, size)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n, err = sender.Encapsulate(buf[:], ipOff, frameOff)
|
||||
if err != nil {
|
||||
t.Errorf("sender encapsulate: %v", err)
|
||||
}
|
||||
ifrm, err := NewFrame(buf[:n])
|
||||
if err != nil {
|
||||
t.Fatal(err) // only fails in short frame case.
|
||||
}
|
||||
if ifrm.Type() != TypeEchoRequest {
|
||||
t.Errorf("not echo request type on send: %d", ifrm.Type())
|
||||
}
|
||||
efrm := FrameEcho{Frame: ifrm}
|
||||
data := efrm.Data()
|
||||
testPatternMatch(t, data, pattern, int(size))
|
||||
return key, n
|
||||
}
|
||||
|
||||
func testPatternMatch(t *testing.T, data []byte, pattern []byte, size int) {
|
||||
t.Helper()
|
||||
if len(data) != size {
|
||||
t.Errorf("pattern size mismatch, want %d, got %d", size, len(data))
|
||||
}
|
||||
for i := 0; i < size; i += len(pattern) {
|
||||
got := data[i:min(len(data), i+len(pattern))]
|
||||
want := pattern[:len(got)]
|
||||
if !internal.BytesEqual(got, want) {
|
||||
t.Errorf("pattern data mismatch at %d, got %s, want %s", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=Type,CodeDestinationUnreachable,CodeParameterProblem -linecomment -output stringers.go
|
||||
|
||||
const (
|
||||
sizeHeader = 8
|
||||
sizeNDPBase = sizeHeader + 16 // 24: ICMPv6 header + 16-byte target address
|
||||
sizeNDPOption = 8 // 1 type + 1 len + 6 MAC (Ethernet link-layer option, RFC 4861 §4.6.1)
|
||||
sizeNDP = sizeNDPBase + sizeNDPOption
|
||||
)
|
||||
|
||||
type Type uint8
|
||||
|
||||
const (
|
||||
TypeDestinationUnreachable Type = 1 // destination unreachable
|
||||
TypePacketTooBig Type = 2 // packet too big
|
||||
TypeTimeExceeded Type = 3 // time exceeded
|
||||
TypeParameterProblem Type = 4 // parameter problem
|
||||
|
||||
TypeEchoRequest Type = 128 // echo request
|
||||
TypeEchoReply Type = 129 // echo reply
|
||||
|
||||
TypeRouterSolicitation Type = 133 // router solicitation
|
||||
TypeRouterAdvertisement Type = 134 // router advertisement
|
||||
TypeNeighborSolicitation Type = 135 // neighbor solicitation
|
||||
TypeNeighborAdvertisement Type = 136 // neighbor advertisement
|
||||
TypeRedirectMessage Type = 137 // redirect message
|
||||
)
|
||||
|
||||
type CodeTimeExceeded uint8
|
||||
|
||||
const (
|
||||
CodeHopLimitExceeded CodeTimeExceeded = iota // hop limit exceeded in transit
|
||||
CodeFragmentReassembly // fragment reassembly time exceeded
|
||||
)
|
||||
|
||||
type CodeDestinationUnreachable uint8
|
||||
|
||||
const (
|
||||
CodeNoRoute CodeDestinationUnreachable = iota // no route to destination
|
||||
CodeAdminProhibited // communication administratively prohibited
|
||||
CodeBeyondScope // beyond scope of source address
|
||||
CodeAddressUnreachable // address unreachable
|
||||
CodePortUnreachable // port unreachable
|
||||
CodeIngressEgressPolicy // source address failed ingress/egress policy
|
||||
CodeRejectRoute // reject route to destination
|
||||
)
|
||||
|
||||
type CodeParameterProblem uint8
|
||||
|
||||
const (
|
||||
CodeErroneousHeaderField CodeParameterProblem = iota // erroneous header field encountered
|
||||
CodeUnrecognizedNextHeader // unrecognized next header type encountered
|
||||
CodeUnrecognizedIPv6Option // unrecognized IPv6 option encountered
|
||||
)
|
||||
|
||||
func NewFrame(buf []byte) (Frame, error) {
|
||||
if len(buf) < sizeHeader {
|
||||
return Frame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return Frame{buf: buf}, nil
|
||||
}
|
||||
|
||||
type Frame struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (frm Frame) Type() Type { return Type(frm.buf[0]) }
|
||||
|
||||
func (frm Frame) SetType(t Type) { frm.buf[0] = uint8(t) }
|
||||
|
||||
func (frm Frame) Code() uint8 { return frm.buf[1] }
|
||||
|
||||
func (frm Frame) SetCode(code uint8) { frm.buf[1] = code }
|
||||
|
||||
// CRC returns the checksum field of the frame.
|
||||
func (frm Frame) CRC() uint16 {
|
||||
return binary.BigEndian.Uint16(frm.buf[2:4])
|
||||
}
|
||||
|
||||
// SetCRC sets the checksum field of the frame.
|
||||
func (frm Frame) SetCRC(crc uint16) {
|
||||
binary.BigEndian.PutUint16(frm.buf[2:4], crc)
|
||||
}
|
||||
|
||||
func (frm Frame) payload() []byte {
|
||||
return frm.buf[4:]
|
||||
}
|
||||
|
||||
type FrameDestinationUnreachable struct {
|
||||
Frame
|
||||
}
|
||||
|
||||
func (frm FrameDestinationUnreachable) Code() CodeDestinationUnreachable {
|
||||
return CodeDestinationUnreachable(frm.Frame.Code())
|
||||
}
|
||||
|
||||
func (frm FrameDestinationUnreachable) SetCode(code CodeDestinationUnreachable) {
|
||||
frm.Frame.SetCode(uint8(code))
|
||||
}
|
||||
|
||||
type FramePacketTooBig struct {
|
||||
Frame
|
||||
}
|
||||
|
||||
func (frm FramePacketTooBig) MTU() uint32 {
|
||||
return binary.BigEndian.Uint32(frm.buf[4:8])
|
||||
}
|
||||
|
||||
func (frm FramePacketTooBig) SetMTU(mtu uint32) {
|
||||
binary.BigEndian.PutUint32(frm.buf[4:8], mtu)
|
||||
}
|
||||
|
||||
type FrameParameterProblem struct {
|
||||
Frame
|
||||
}
|
||||
|
||||
func (frm FrameParameterProblem) Code() CodeParameterProblem {
|
||||
return CodeParameterProblem(frm.Frame.Code())
|
||||
}
|
||||
|
||||
func (frm FrameParameterProblem) SetCode(code CodeParameterProblem) {
|
||||
frm.Frame.SetCode(uint8(code))
|
||||
}
|
||||
|
||||
// Pointer identifies the octet offset within the invoking packet where the error was detected.
|
||||
func (frm FrameParameterProblem) Pointer() uint32 {
|
||||
return binary.BigEndian.Uint32(frm.buf[4:8])
|
||||
}
|
||||
|
||||
func (frm FrameParameterProblem) SetPointer(ptr uint32) {
|
||||
binary.BigEndian.PutUint32(frm.buf[4:8], ptr)
|
||||
}
|
||||
|
||||
type FrameEcho struct {
|
||||
Frame
|
||||
}
|
||||
|
||||
func (frm FrameEcho) Identifier() uint16 {
|
||||
return binary.BigEndian.Uint16(frm.buf[4:6])
|
||||
}
|
||||
|
||||
func (frm FrameEcho) SetIdentifier(id uint16) {
|
||||
binary.BigEndian.PutUint16(frm.buf[4:6], id)
|
||||
}
|
||||
|
||||
func (frm FrameEcho) SequenceNumber() uint16 {
|
||||
return binary.BigEndian.Uint16(frm.buf[6:8])
|
||||
}
|
||||
|
||||
func (frm FrameEcho) SetSequenceNumber(seq uint16) {
|
||||
binary.BigEndian.PutUint16(frm.buf[6:8], seq)
|
||||
}
|
||||
|
||||
func (frm FrameEcho) Data() []byte {
|
||||
return frm.buf[8:]
|
||||
}
|
||||
|
||||
func (frm FrameEcho) RawData() []byte {
|
||||
return frm.buf
|
||||
}
|
||||
|
||||
// FrameNeighborSolicitation accesses a Neighbor Solicitation message (RFC 4861 §4.3).
|
||||
// Layout after ICMPv6 base header: Reserved(4B) | TargetAddr(16B) | Options.
|
||||
type FrameNeighborSolicitation struct {
|
||||
Frame
|
||||
}
|
||||
|
||||
// TargetAddr returns the IPv6 address being queried.
|
||||
func (frm FrameNeighborSolicitation) TargetAddr() *[16]byte {
|
||||
return (*[16]byte)(frm.buf[8:24])
|
||||
}
|
||||
|
||||
// Options returns the bytes following the fixed header for parsing NDP options.
|
||||
func (frm FrameNeighborSolicitation) Options() []byte {
|
||||
return frm.buf[24:]
|
||||
}
|
||||
|
||||
// FrameNeighborAdvertisement accesses a Neighbor Advertisement message (RFC 4861 §4.4).
|
||||
// Layout after ICMPv6 base header: R|S|O|Reserved(4B) | TargetAddr(16B) | Options.
|
||||
type FrameNeighborAdvertisement struct {
|
||||
Frame
|
||||
}
|
||||
|
||||
// Flags returns the R (router), S (solicited), O (override) flag bits.
|
||||
func (frm FrameNeighborAdvertisement) Flags() (router, solicited, override bool) {
|
||||
b := frm.buf[4]
|
||||
return b&0x80 != 0, b&0x40 != 0, b&0x20 != 0
|
||||
}
|
||||
|
||||
// SetFlags sets the R, S, O flags and zeroes the reserved bits.
|
||||
func (frm FrameNeighborAdvertisement) SetFlags(router, solicited, override bool) {
|
||||
var b byte
|
||||
if router {
|
||||
b |= 0x80
|
||||
}
|
||||
if solicited {
|
||||
b |= 0x40
|
||||
}
|
||||
if override {
|
||||
b |= 0x20
|
||||
}
|
||||
frm.buf[4] = b
|
||||
frm.buf[5] = 0
|
||||
frm.buf[6] = 0
|
||||
frm.buf[7] = 0
|
||||
}
|
||||
|
||||
// TargetAddr returns the IPv6 address being announced.
|
||||
func (frm FrameNeighborAdvertisement) TargetAddr() *[16]byte {
|
||||
return (*[16]byte)(frm.buf[8:24])
|
||||
}
|
||||
|
||||
// Options returns the bytes following the fixed header for parsing NDP options.
|
||||
func (frm FrameNeighborAdvertisement) Options() []byte {
|
||||
return frm.buf[24:]
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
type ndpCache struct {
|
||||
entries []ndpEntry
|
||||
}
|
||||
|
||||
// ndpEntry maps an IPv6 address to a MAC. Designed for compactness: 24 bytes.
|
||||
type ndpEntry struct {
|
||||
addr [16]byte
|
||||
mac [6]byte
|
||||
age uint8
|
||||
flags ndpFlags
|
||||
}
|
||||
|
||||
type ndpFlags uint8
|
||||
|
||||
// unset ndpFlagInUse to signal the entry can be acquired for a new query.
|
||||
const (
|
||||
ndpFlagInUse ndpFlags = 1 << iota
|
||||
ndpFlagPendingResponse // received a NS for our addr; must send NA
|
||||
ndpFlagIncomplete // query in flight; MAC not yet resolved
|
||||
ndpFlagIncompletePendingQuery // NS not yet transmitted
|
||||
ndpFlagPriority // user query; evicted last
|
||||
ndpFlagResolveTriggersCallback // call onresolve when MAC is learned
|
||||
)
|
||||
|
||||
func (f ndpFlags) hasAny(bits ndpFlags) bool { return f&bits != 0 }
|
||||
|
||||
func (e *ndpEntry) use(mac [6]byte, addr [16]byte, flags ndpFlags) {
|
||||
e.flags = ndpFlagInUse | flags
|
||||
e.addr = addr
|
||||
e.age = 0
|
||||
e.mac = mac
|
||||
}
|
||||
|
||||
func (e *ndpEntry) destroy() { *e = ndpEntry{} }
|
||||
|
||||
func (e *ndpEntry) put(buf []byte, ourAddr [16]byte, ourMAC [6]byte, tp Type) (int, error) {
|
||||
if len(buf) < sizeNDP {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
buf[0] = uint8(tp)
|
||||
buf[1] = 0
|
||||
buf[2], buf[3] = 0, 0 // checksum zeroed; caller computes it
|
||||
switch tp {
|
||||
case TypeNeighborSolicitation:
|
||||
buf[4], buf[5], buf[6], buf[7] = 0, 0, 0, 0
|
||||
copy(buf[8:24], e.addr[:]) // target = address being queried
|
||||
buf[24] = ndpOptSourceLinkAddr
|
||||
buf[25] = 1 // length in units of 8 bytes
|
||||
copy(buf[26:32], ourMAC[:])
|
||||
case TypeNeighborAdvertisement:
|
||||
buf[4] = 0x60 // S=1 (solicited), O=1 (override)
|
||||
buf[5], buf[6], buf[7] = 0, 0, 0
|
||||
copy(buf[8:24], ourAddr[:]) // target = our address being announced
|
||||
buf[24] = ndpOptTargetLinkAddr
|
||||
buf[25] = 1
|
||||
copy(buf[26:32], ourMAC[:])
|
||||
default:
|
||||
return 0, lneto.ErrUnsupported
|
||||
}
|
||||
return sizeNDP, nil
|
||||
}
|
||||
|
||||
func (c *ndpCache) ageEntries() {
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&ndpFlagInUse != 0 && c.entries[i].age < 255 {
|
||||
c.entries[i].age++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ndpCache) reset(maxLimit int) {
|
||||
internal.SliceReuse(&c.entries, maxLimit)
|
||||
c.entries = c.entries[:cap(c.entries)]
|
||||
}
|
||||
|
||||
func (c *ndpCache) getNextFlagged(flags ndpFlags) *ndpEntry {
|
||||
for i := range c.entries {
|
||||
f := c.entries[i].flags
|
||||
if f&ndpFlagInUse != 0 && f.hasAny(flags) {
|
||||
return &c.entries[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ndpCache) clearFlags(entryHasFlags, clrTheseFlagsIfMatch ndpFlags) {
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&entryHasFlags != 0 {
|
||||
c.entries[i].flags &^= clrTheseFlagsIfMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ndpCache) Lookup(addr [16]byte) *ndpEntry {
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&ndpFlagInUse != 0 && c.entries[i].addr == addr {
|
||||
return &c.entries[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// acquireNext returns the next available entry, evicting the oldest passive
|
||||
// (passively-learned) entry before touching active user queries or pending responses.
|
||||
func (c *ndpCache) acquireNext() *ndpEntry {
|
||||
const priorityFlags = ndpFlagPendingResponse | ndpFlagIncomplete | ndpFlagPriority
|
||||
oldest, oldestPassive := 0, -1
|
||||
for i := range c.entries {
|
||||
if c.entries[i].flags&ndpFlagInUse == 0 {
|
||||
oldest = i
|
||||
break
|
||||
}
|
||||
if !c.entries[i].flags.hasAny(priorityFlags) {
|
||||
if oldestPassive < 0 || c.entries[i].age > c.entries[oldestPassive].age {
|
||||
oldestPassive = i
|
||||
}
|
||||
}
|
||||
if c.entries[i].age > c.entries[oldest].age {
|
||||
oldest = i
|
||||
}
|
||||
}
|
||||
if oldestPassive >= 0 && c.entries[oldest].flags&ndpFlagInUse != 0 {
|
||||
oldest = oldestPassive
|
||||
}
|
||||
c.ageEntries()
|
||||
e := &c.entries[oldest]
|
||||
e.destroy()
|
||||
return e
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package icmpv6
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientNDP(t *testing.T) {
|
||||
addr1 := [16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
addr2 := [16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
|
||||
mac1 := [6]byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x01}
|
||||
mac2 := [6]byte{0xc0, 0xff, 0xee, 0xc0, 0xff, 0x02}
|
||||
|
||||
var h1, h2 Client
|
||||
if err := h1.Configure(ClientConfig{OurAddr: addr1, OurMAC: mac1, NDPCache: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := h2.Configure(ClientConfig{OurAddr: addr2, OurMAC: mac2, NDPCache: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf [64]byte
|
||||
|
||||
// No pending work: both clients should be silent.
|
||||
n, err := h1.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n > 0 {
|
||||
t.Fatal("expected no data before query:", err, n)
|
||||
}
|
||||
n, err = h2.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n > 0 {
|
||||
t.Fatal("expected no data before query:", err, n)
|
||||
}
|
||||
|
||||
// h1 queries h2's MAC.
|
||||
if err = h1.NDPStartQuery(addr2, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// h1 sends a Neighbor Solicitation.
|
||||
n, err = h1.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("h1 encapsulate NS:", err)
|
||||
} else if n == 0 {
|
||||
t.Fatal("expected NS to be written")
|
||||
}
|
||||
validateNDP(t, buf[:n], TypeNeighborSolicitation)
|
||||
|
||||
// Verify NS target address is addr2.
|
||||
ifrm, _ := NewFrame(buf[:n])
|
||||
nsfrm := FrameNeighborSolicitation{Frame: ifrm}
|
||||
if *nsfrm.TargetAddr() != addr2 {
|
||||
t.Errorf("NS target addr mismatch: want %x, got %x", addr2, *nsfrm.TargetAddr())
|
||||
}
|
||||
|
||||
// h2 receives the NS.
|
||||
if err = h2.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatal("h2 demux NS:", err)
|
||||
}
|
||||
|
||||
// h2 sends a Neighbor Advertisement.
|
||||
n, err = h2.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal("h2 encapsulate NA:", err)
|
||||
} else if n == 0 {
|
||||
t.Fatal("expected NA to be written")
|
||||
}
|
||||
validateNDP(t, buf[:n], TypeNeighborAdvertisement)
|
||||
|
||||
// Verify NA target address is addr2 (h2's own address).
|
||||
ifrm, _ = NewFrame(buf[:n])
|
||||
nafrm := FrameNeighborAdvertisement{Frame: ifrm}
|
||||
if *nafrm.TargetAddr() != addr2 {
|
||||
t.Errorf("NA target addr mismatch: want %x, got %x", addr2, *nafrm.TargetAddr())
|
||||
}
|
||||
_, solicited, _ := nafrm.Flags()
|
||||
if !solicited {
|
||||
t.Error("expected solicited flag set in NA")
|
||||
}
|
||||
|
||||
// Double-tap: h2 should have nothing left to send.
|
||||
n2, err := h2.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n2 > 0 {
|
||||
t.Fatal("double tap: expected no more data from h2:", err, n2)
|
||||
}
|
||||
|
||||
// h1 receives the NA and resolves h2's MAC.
|
||||
if err = h1.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatal("h1 demux NA:", err)
|
||||
}
|
||||
mac, err := h1.NDPCacheLookup(addr2)
|
||||
if err != nil {
|
||||
t.Fatal("cache lookup after resolution:", err)
|
||||
}
|
||||
if !bytes.Equal(mac[:], mac2[:]) {
|
||||
t.Errorf("resolved MAC mismatch: want %x, got %x", mac2, mac)
|
||||
}
|
||||
|
||||
// h1 should have nothing left to send.
|
||||
n, err = h1.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n > 0 {
|
||||
t.Fatal("expected no data after completion:", err, n)
|
||||
}
|
||||
}
|
||||
|
||||
func validateNDP(t *testing.T, buf []byte, wantType Type) {
|
||||
t.Helper()
|
||||
if len(buf) < sizeNDP {
|
||||
t.Errorf("NDP frame too short: %d < %d", len(buf), sizeNDP)
|
||||
return
|
||||
}
|
||||
ifrm, err := NewFrame(buf)
|
||||
if err != nil {
|
||||
t.Error("NewFrame:", err)
|
||||
return
|
||||
}
|
||||
if ifrm.Type() != wantType {
|
||||
t.Errorf("type mismatch: want %s, got %s", wantType, ifrm.Type())
|
||||
}
|
||||
if ifrm.Code() != 0 {
|
||||
t.Errorf("code must be zero, got %d", ifrm.Code())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Code generated by "stringer -type=Type,CodeDestinationUnreachable,CodeParameterProblem -linecomment -output stringers.go"; DO NOT EDIT.
|
||||
|
||||
package icmpv6
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[TypeDestinationUnreachable-1]
|
||||
_ = x[TypePacketTooBig-2]
|
||||
_ = x[TypeTimeExceeded-3]
|
||||
_ = x[TypeParameterProblem-4]
|
||||
_ = x[TypeEchoRequest-128]
|
||||
_ = x[TypeEchoReply-129]
|
||||
_ = x[TypeRouterSolicitation-133]
|
||||
_ = x[TypeRouterAdvertisement-134]
|
||||
_ = x[TypeNeighborSolicitation-135]
|
||||
_ = x[TypeNeighborAdvertisement-136]
|
||||
_ = x[TypeRedirectMessage-137]
|
||||
}
|
||||
|
||||
const (
|
||||
_Type_name_0 = "destination unreachablepacket too bigtime exceededparameter problem"
|
||||
_Type_name_1 = "echo requestecho reply"
|
||||
_Type_name_2 = "router solicitationrouter advertisementneighbor solicitationneighbor advertisementredirect message"
|
||||
)
|
||||
|
||||
var (
|
||||
_Type_index_0 = [...]uint8{0, 23, 37, 50, 67}
|
||||
_Type_index_1 = [...]uint8{0, 12, 22}
|
||||
_Type_index_2 = [...]uint8{0, 19, 39, 60, 82, 98}
|
||||
)
|
||||
|
||||
func (i Type) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 4:
|
||||
i -= 1
|
||||
return _Type_name_0[_Type_index_0[i]:_Type_index_0[i+1]]
|
||||
case 128 <= i && i <= 129:
|
||||
i -= 128
|
||||
return _Type_name_1[_Type_index_1[i]:_Type_index_1[i+1]]
|
||||
case 133 <= i && i <= 137:
|
||||
i -= 133
|
||||
return _Type_name_2[_Type_index_2[i]:_Type_index_2[i+1]]
|
||||
default:
|
||||
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CodeNoRoute-0]
|
||||
_ = x[CodeAdminProhibited-1]
|
||||
_ = x[CodeBeyondScope-2]
|
||||
_ = x[CodeAddressUnreachable-3]
|
||||
_ = x[CodePortUnreachable-4]
|
||||
_ = x[CodeIngressEgressPolicy-5]
|
||||
_ = x[CodeRejectRoute-6]
|
||||
}
|
||||
|
||||
const _CodeDestinationUnreachable_name = "no route to destinationcommunication administratively prohibitedbeyond scope of source addressaddress unreachableport unreachablesource address failed ingress/egress policyreject route to destination"
|
||||
|
||||
var _CodeDestinationUnreachable_index = [...]uint8{0, 23, 64, 94, 113, 129, 172, 199}
|
||||
|
||||
func (i CodeDestinationUnreachable) String() string {
|
||||
if i >= CodeDestinationUnreachable(len(_CodeDestinationUnreachable_index)-1) {
|
||||
return "CodeDestinationUnreachable(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[i]:_CodeDestinationUnreachable_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[CodeErroneousHeaderField-0]
|
||||
_ = x[CodeUnrecognizedNextHeader-1]
|
||||
_ = x[CodeUnrecognizedIPv6Option-2]
|
||||
}
|
||||
|
||||
const _CodeParameterProblem_name = "erroneous header field encounteredunrecognized next header type encounteredunrecognized IPv6 option encountered"
|
||||
|
||||
var _CodeParameterProblem_index = [...]uint8{0, 34, 75, 111}
|
||||
|
||||
func (i CodeParameterProblem) String() string {
|
||||
if i >= CodeParameterProblem(len(_CodeParameterProblem_index)-1) {
|
||||
return "CodeParameterProblem(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _CodeParameterProblem_name[_CodeParameterProblem_index[i]:_CodeParameterProblem_index[i+1]]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user