mirror of
https://github.com/soypat/lneto.git
synced 2026-08-02 14:07:53 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa609ea54f | |||
| a42f0b404c | |||
| ef279863a9 | |||
| d5ea2efdf4 | |||
| a0a5336108 | |||
| 28addf329a | |||
| 860d6d00bb |
@@ -20,9 +20,9 @@ jobs:
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
unformatted=$(gofmt -l .)
|
||||
unformatted=$(gofmt -l -d . 2>&1) || true
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "::error::Files not formatted with gofmt:"
|
||||
echo "::error::File(s) not formatted with gofmt:"
|
||||
echo "$unformatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -49,6 +49,7 @@ You may try lneto out on linux with the [xcurl example](./examples/xcurl/) which
|
||||
- HTTP over TCP/IPv4/Ethernet connection using
|
||||
- NTP time check (optional)
|
||||
- Print packet captures using lneto's [internet/pcap](./internet/pcap) package
|
||||
- Optional fixed TCP source port with `-port`, useful when debugging raw-socket interactions with the host OS
|
||||
|
||||
See Developing section below for more information.
|
||||
|
||||
@@ -171,6 +172,19 @@ sudo ethtool -K <network-interface> gro off
|
||||
|
||||
Depending on the interface and driver, other offloading features may also need to be disabled.
|
||||
|
||||
When running `xcurl` directly on a normal Linux interface (for example `-i wlan0` or `-i eth0`), lneto shares the host interface IP and MAC address. The Linux TCP stack also sees incoming packets for that IP. If xcurl's TCP handshake succeeds but the HTTP request is followed by a TCP RST from the server, the host kernel may have sent its own outbound RST for xcurl's raw TCP flow because no kernel socket owns that source port.
|
||||
|
||||
For a short validation test, use a fixed xcurl source port and temporarily drop kernel-generated outbound RSTs for that port:
|
||||
|
||||
```sh
|
||||
go build -o examples/xcurl/xcurl ./examples/xcurl
|
||||
sudo iptables -I OUTPUT -p tcp --sport 2300 --tcp-flags RST RST -j DROP
|
||||
sudo ./examples/xcurl/xcurl -i <network-interface> -port 2300 -host example.com
|
||||
sudo iptables -D OUTPUT -p tcp --sport 2300 --tcp-flags RST RST -j DROP
|
||||
```
|
||||
|
||||
This is a debugging workaround, not the preferred operating mode. Prefer `-ihttp`, a TAP interface, a network namespace, or an otherwise isolated interface/IP when testing xcurl without host TCP stack interference.
|
||||
|
||||
- [`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.
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -265,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 OptT1Renewal:
|
||||
case OptRenewTimeValue:
|
||||
c.tRenew = maybeU32(data)
|
||||
case OptIPAddressLeaseTime:
|
||||
c.tIPLease = maybeU32(data)
|
||||
case OptT2Rebinding:
|
||||
case OptRebindingTimeValue:
|
||||
c.tRebind = maybeU32(data)
|
||||
case OptServerIdentification:
|
||||
c.svip.setmaybe(data)
|
||||
|
||||
@@ -128,8 +128,8 @@ const (
|
||||
OptParameterRequestList OptNum = 55 // Parameter request list
|
||||
OptMessage OptNum = 56 // DHCP error message
|
||||
OptMaximumMessageSize OptNum = 57 // DHCP maximum message size
|
||||
OptT1Renewal OptNum = 58 // DHCP renewal (T1) time
|
||||
OptT2Rebinding OptNum = 59 // DHCP rebinding (T2) time
|
||||
OptRenewTimeValue OptNum = 58 // DHCP renewal (T1) time
|
||||
OptRebindingTimeValue OptNum = 59 // DHCP rebinding (T2) time
|
||||
OptClientIdentifier OptNum = 60 // Client identifier
|
||||
OptClientIdentifier1 OptNum = 61 // Client identifier(1)
|
||||
|
||||
|
||||
+190
-246
@@ -4,38 +4,26 @@ 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
|
||||
connID uint64
|
||||
nextAddr [4]byte
|
||||
subnet ipv4.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].
|
||||
@@ -48,82 +36,64 @@ type ServerConfig struct {
|
||||
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 is the lease duration. Zero defaults to 3600.
|
||||
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.
|
||||
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 {
|
||||
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
|
||||
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")
|
||||
}
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = DefaultServerPort
|
||||
}
|
||||
maxPending := cfg.MaxPending
|
||||
if maxPending <= 0 {
|
||||
maxPending = defaultMaxPending
|
||||
lease := cfg.LeaseSeconds
|
||||
if lease == 0 {
|
||||
lease = 3600
|
||||
}
|
||||
txns := sv.txns
|
||||
if cap(txns) < maxPending {
|
||||
txns = make([]txn, 0, maxPending)
|
||||
hosts := sv.hosts
|
||||
if hosts == nil {
|
||||
hosts = make(map[[36]byte]serverEntry)
|
||||
} else {
|
||||
txns = txns[:0]
|
||||
for k := range hosts {
|
||||
delete(hosts, k)
|
||||
}
|
||||
}
|
||||
*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,
|
||||
connID: sv.connID + 1,
|
||||
siaddr: cfg.ServerAddr,
|
||||
gwaddr: cfg.Gateway,
|
||||
dns: cfg.DNS,
|
||||
subnet: cfg.Subnet,
|
||||
port: port,
|
||||
leaseSeconds: lease,
|
||||
nextAddr: cfg.Subnet.Next(cfg.ServerAddr),
|
||||
hosts: hosts,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -178,114 +148,128 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve client identity: the client identifier option when present,
|
||||
// otherwise the hardware address.
|
||||
var clientIDRaw [36]byte
|
||||
var client serverEntry
|
||||
var clientExists bool
|
||||
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))
|
||||
// No explicit client identifier: use chaddr as the stable lookup key.
|
||||
// This lets the server correlate Discover→Offer→Request even when
|
||||
// ciaddr=0.0.0.0 (client has no IP yet), which is the normal case
|
||||
// for first-time lease acquisition (RFC 2131 §4.3.1).
|
||||
chaddr := *dfrm.CHAddrAs6()
|
||||
copy(clientIDRaw[:], chaddr[:])
|
||||
client, clientExists = sv.getClient(clientIDRaw)
|
||||
if !clientExists {
|
||||
// Fallback: look up by ciaddr for clients that did send ciaddr.
|
||||
ciaddr := *dfrm.CIAddr()
|
||||
if ciaddr != ([4]byte{}) {
|
||||
client, clientIDRaw, clientExists = sv.getClientByIP(ciaddr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copy(clientIDRaw[:], clientID)
|
||||
client, clientExists = sv.getClient(clientIDRaw)
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case MsgDiscover:
|
||||
binding, err := sv.alloc.Offer(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server offer: %w", err)
|
||||
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
|
||||
sv.pending-- // Cancel unfulfilled pending response.
|
||||
}
|
||||
offered, ok := binding.Addr()
|
||||
if !ok || !offered.Is4() {
|
||||
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||
if !clientExists {
|
||||
addr, ok := sv.allocAddr(reqAddr)
|
||||
if !ok {
|
||||
return errors.New("dhcpv4 server: address pool exhausted")
|
||||
}
|
||||
client.addr = addr
|
||||
}
|
||||
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()
|
||||
copy(client.requestlist[:], reqlist)
|
||||
client.state = StateInit
|
||||
client.hostname = string(hostname)
|
||||
client.xid = dfrm.XID()
|
||||
client.hwaddr = *dfrm.CHAddrAs6()
|
||||
if isIPLayer {
|
||||
_, t.port, _ = getSrcIPPort(carrierData)
|
||||
_, client.port, _ = getSrcIPPort(carrierData)
|
||||
}
|
||||
t.state = StateSelecting
|
||||
t.respond = MsgOffer
|
||||
client.clientIdlen = uint8(len(clientID))
|
||||
sv.pending++
|
||||
|
||||
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")
|
||||
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")
|
||||
}
|
||||
binding, err := sv.alloc.Commit(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server commit: %w", err)
|
||||
break
|
||||
}
|
||||
offered, ok := binding.Addr()
|
||||
if !ok || !offered.Is4() {
|
||||
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||
if client.state == StateSelecting {
|
||||
client.state = StateRequesting
|
||||
sv.pending++
|
||||
}
|
||||
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)
|
||||
if clientExists {
|
||||
if client.state == StateInit || client.state == StateRequesting {
|
||||
sv.pending--
|
||||
}
|
||||
delete(sv.hosts, clientIDRaw)
|
||||
return nil
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unhandled message type %s", msgType.String())
|
||||
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
|
||||
}
|
||||
optBuf := dfrm.OptionsPayload()
|
||||
if len(optBuf) < 255 {
|
||||
} else if len(optBuf) < 255 {
|
||||
return 0, errOptionNotFit
|
||||
}
|
||||
|
||||
t := sv.nextPending()
|
||||
if t == nil {
|
||||
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
|
||||
n, err := EncodeOption(optBuf[nopt:], OptMessageType, byte(t.respond))
|
||||
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
|
||||
}
|
||||
nopt += n
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||
n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||
nopt += n
|
||||
if sv.gwaddr != [4]byte{} {
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
||||
@@ -303,136 +287,96 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
||||
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)
|
||||
if sv.leaseSeconds > 0 {
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, sv.leaseSeconds)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptT1Renewal, t.binding.T1)
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptRenewTimeValue, sv.leaseSeconds/2)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptT2Rebinding, t.binding.T2)
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptRebindingTimeValue, sv.leaseSeconds*7/8)
|
||||
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.SetXID(client.xid)
|
||||
dfrm.SetSecs(0)
|
||||
dfrm.SetFlags(0)
|
||||
*dfrm.YIAddr() = t.offered
|
||||
if t.respond == MsgAck {
|
||||
*dfrm.CIAddr() = t.offered
|
||||
}
|
||||
*dfrm.YIAddr() = client.addr // Offer here.
|
||||
*dfrm.SIAddr() = sv.siaddr
|
||||
*dfrm.GIAddr() = sv.gwaddr
|
||||
copy(dfrm.CHAddrAs6()[:], t.hwaddr[:])
|
||||
copy(dfrm.CHAddrAs6()[:], client.hwaddr[:])
|
||||
dfrm.SetMagicCookie(MagicCookie)
|
||||
if carrierIsIP {
|
||||
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], t.offered[:])
|
||||
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], client.addr[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Per RFC 2131 §4.1: unicast Offer/Ack to chaddr because the client
|
||||
// does not yet have the offered IP, so no ARP entry exists.
|
||||
// The Ethernet layer sets dst=gwmac before calling us; overwrite it
|
||||
// here so the frame reaches the client via its hardware address.
|
||||
// offsetToIP==14 means the Ethernet header is at carrierData[0:14].
|
||||
if offsetToIP >= 14 {
|
||||
copy(carrierData[offsetToIP-14:offsetToIP-8], client.hwaddr[:])
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
client.state = futureState
|
||||
|
||||
// Set server state.
|
||||
sv.hosts[clientID] = client
|
||||
sv.pending--
|
||||
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]
|
||||
// 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 := [4]byte(reqAddr)
|
||||
if sv.subnet.Contains(candidate) && candidate != sv.siaddr && !sv.isAddrAssigned(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
// Reject broadcast address (all host bits set).
|
||||
a := sv.nextAddr
|
||||
sv.nextAddr = sv.subnet.Next(sv.nextAddr)
|
||||
if sv.nextAddr == sv.siaddr {
|
||||
sv.nextAddr = sv.subnet.Next(sv.nextAddr)
|
||||
}
|
||||
hostBits := uint(32 - sv.subnet.Bits())
|
||||
hostMask := ^uint32(0) >> (32 - hostBits)
|
||||
if binary.BigEndian.Uint32(a[:])&hostMask == hostMask {
|
||||
return [4]byte{}, false
|
||||
}
|
||||
return a, true
|
||||
}
|
||||
|
||||
// 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]
|
||||
func (sv *Server) isAddrAssigned(addr [4]byte) bool {
|
||||
for _, v := range sv.hosts {
|
||||
if v.addr == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
return serverEntry{}, [36]byte{}, false
|
||||
}
|
||||
|
||||
func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package dhcpv4
|
||||
|
||||
// Tests covering the AP-mode DHCP server fixes:
|
||||
// 1. Client lookup by chaddr when no ClientID option is present.
|
||||
// 2. Ciaddr fallback lookup when chaddr lookup finds nothing.
|
||||
// 3. Ethernet dst is patched to chaddr on Offer/Ack when Encapsulate is
|
||||
// called with offsetToIP >= 14.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// doraNoClientID runs a complete DORA exchange using raw DHCP frames
|
||||
// (no ClientID option) with the given chaddr. The client uses only chaddr
|
||||
// as its identity, matching normal AP-mode behaviour where mobile clients
|
||||
// don't send OptClientIdentifier.
|
||||
func doraNoClientID(t *testing.T, sv *Server, chaddr [6]byte, xid uint32) (assignedIP [4]byte) {
|
||||
t.Helper()
|
||||
var cl Client
|
||||
err := cl.BeginRequest(xid, RequestConfig{
|
||||
ClientHardwareAddr: chaddr,
|
||||
// Intentionally no ClientID – forces server to key on chaddr.
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BeginRequest: %v", err)
|
||||
}
|
||||
|
||||
var buf [1024]byte
|
||||
|
||||
// DISCOVER
|
||||
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("discover encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("discover demux: %v", err)
|
||||
}
|
||||
|
||||
// OFFER
|
||||
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("offer encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
frm, _ := NewFrame(buf[:n])
|
||||
assignedIP = *frm.YIAddr()
|
||||
if err = cl.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("offer demux: %v", err)
|
||||
}
|
||||
|
||||
// REQUEST
|
||||
n, err = cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("request encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("request demux: %v", err)
|
||||
}
|
||||
|
||||
// ACK
|
||||
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("ack encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = cl.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("ack demux: %v", err)
|
||||
}
|
||||
|
||||
if cl.State() != StateBound {
|
||||
t.Errorf("want StateBound, got %s", cl.State())
|
||||
}
|
||||
return assignedIP
|
||||
}
|
||||
|
||||
// TestServerChaddrLookup_NoClientID verifies the server can complete a full DORA
|
||||
// cycle when the client sends no OptClientIdentifier option. Before the fix the
|
||||
// MsgRequest Demux call would fail with "request for non existing client" because
|
||||
// the server tried to look up the entry by ciaddr (0.0.0.0) instead of chaddr.
|
||||
func TestServerChaddrLookup_NoClientID(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 4, 1}
|
||||
var sv Server
|
||||
sv.Configure(ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Gateway: svAddr,
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
})
|
||||
|
||||
chaddr := [6]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
|
||||
ip := doraNoClientID(t, &sv, chaddr, 0x12345678)
|
||||
if ip[0] != 192 || ip[1] != 168 || ip[2] != 4 {
|
||||
t.Errorf("unexpected subnet in assigned IP %v", ip)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerChaddrLookup_MultipleClientsNoClientID verifies that multiple clients
|
||||
// without ClientID each get a unique address and successfully reach StateBound.
|
||||
func TestServerChaddrLookup_MultipleClientsNoClientID(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 4, 1}
|
||||
var sv Server
|
||||
sv.Configure(ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Gateway: svAddr,
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
})
|
||||
|
||||
addrs := make([][4]byte, 4)
|
||||
for i := range addrs {
|
||||
chaddr := [6]byte{0x00, 0x11, 0x22, 0x33, 0x44, byte(i + 1)}
|
||||
addrs[i] = doraNoClientID(t, &sv, chaddr, uint32(i+1))
|
||||
}
|
||||
|
||||
for i := range addrs {
|
||||
for j := i + 1; j < len(addrs); j++ {
|
||||
if addrs[i] == addrs[j] {
|
||||
t.Errorf("clients %d and %d got same address %v", i, j, addrs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerCiaddrFallback exercises the ciaddr fallback path in Demux.
|
||||
// An entry is registered under an explicit non-MAC ClientID. A subsequent
|
||||
// RELEASE arrives with no ClientID (so chaddr lookup misses) but with
|
||||
// ciaddr=assignedIP, which should let the server find and remove the entry.
|
||||
func TestServerCiaddrFallback(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 4, 1}
|
||||
chaddr := [6]byte{0xca, 0xfe, 0x00, 0x00, 0x01, 0x01}
|
||||
const xid = uint32(0xaabbccdd)
|
||||
var sv Server
|
||||
sv.Configure(ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
})
|
||||
|
||||
// DORA with an explicit ClientID — server keys the entry by that string.
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(xid, RequestConfig{
|
||||
ClientHardwareAddr: chaddr,
|
||||
ClientID: "device-id",
|
||||
}); err != nil {
|
||||
t.Fatalf("BeginRequest: %v", err)
|
||||
}
|
||||
assignedIP := doraWithClient(t, &sv, &cl)
|
||||
|
||||
// RELEASE: no ClientID, ciaddr = assignedIP.
|
||||
// chaddr lookup fails (entry is keyed by "device-id"), so server must
|
||||
// fall back to getClientByIP.
|
||||
var relBuf [512]byte
|
||||
rfrm, _ := NewFrame(relBuf[:])
|
||||
rfrm.ClearHeader()
|
||||
rfrm.SetOp(OpRequest)
|
||||
rfrm.SetHardware(1, 6, 0)
|
||||
rfrm.SetXID(xid)
|
||||
*rfrm.CIAddr() = assignedIP
|
||||
copy(rfrm.CHAddrAs6()[:], chaddr[:])
|
||||
rfrm.SetMagicCookie(MagicCookie)
|
||||
opts := rfrm.OptionsPayload()
|
||||
n, _ := EncodeOption(opts, OptMessageType, byte(MsgRelease))
|
||||
opts[n] = byte(OptEnd)
|
||||
n++
|
||||
if err := sv.Demux(relBuf[:OptionsOffset+n], 0); err != nil {
|
||||
t.Fatalf("release demux: %v", err)
|
||||
}
|
||||
if len(sv.hosts) != 0 {
|
||||
t.Errorf("expected 0 hosts after release, got %d", len(sv.hosts))
|
||||
}
|
||||
}
|
||||
|
||||
// TestServerOfferPatchesEthernetDst verifies that Encapsulate overwrites the
|
||||
// first 6 bytes of the carrier (Ethernet dst) with the client's chaddr when
|
||||
// offsetToIP >= 14.
|
||||
func TestServerOfferPatchesEthernetDst(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 4, 1}
|
||||
clChaddr := [6]byte{0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
|
||||
var sv Server
|
||||
sv.Configure(ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
})
|
||||
|
||||
var cl Client
|
||||
cl.BeginRequest(0x11223344, RequestConfig{ClientHardwareAddr: clChaddr})
|
||||
|
||||
var buf [1024]byte
|
||||
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("discover encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("discover demux: %v", err)
|
||||
}
|
||||
|
||||
// Carrier: 14 bytes Ethernet header, then a minimal IPv4 header marker.
|
||||
var carrier [1024]byte
|
||||
carrier[14] = 0x45 // IPv4 version+IHL so SetIPAddrs succeeds.
|
||||
offerN, err := sv.Encapsulate(carrier[:], 14, 14+20+8)
|
||||
if err != nil || offerN == 0 {
|
||||
t.Fatalf("offer encapsulate: n=%d err=%v", offerN, err)
|
||||
}
|
||||
|
||||
var got [6]byte
|
||||
copy(got[:], carrier[0:6])
|
||||
if got != clChaddr {
|
||||
t.Errorf("Ethernet dst: got %v, want %v", got, clChaddr)
|
||||
}
|
||||
}
|
||||
|
||||
// doraWithClient runs a complete DORA exchange using the given pre-configured
|
||||
// Client and returns the assigned IP.
|
||||
func doraWithClient(t *testing.T, sv *Server, cl *Client) (assignedIP [4]byte) {
|
||||
t.Helper()
|
||||
var buf [1024]byte
|
||||
|
||||
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("discover encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("discover demux: %v", err)
|
||||
}
|
||||
|
||||
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("offer encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
frm, _ := NewFrame(buf[:n])
|
||||
assignedIP = *frm.YIAddr()
|
||||
if err = cl.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("offer demux: %v", err)
|
||||
}
|
||||
|
||||
n, err = cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("request encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("request demux: %v", err)
|
||||
}
|
||||
|
||||
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("ack encapsulate: n=%d err=%v", n, err)
|
||||
}
|
||||
if err = cl.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatalf("ack demux: %v", err)
|
||||
}
|
||||
if cl.State() != StateBound {
|
||||
t.Errorf("want StateBound, got %s", cl.State())
|
||||
}
|
||||
return assignedIP
|
||||
}
|
||||
@@ -239,9 +239,9 @@ func TestServerOfferContainsOptions(t *testing.T) {
|
||||
foundLease = true
|
||||
gotLease = maybeU32(data)
|
||||
}
|
||||
case OptT1Renewal:
|
||||
case OptRenewTimeValue:
|
||||
gotRenew = maybeU32(data)
|
||||
case OptT2Rebinding:
|
||||
case OptRebindingTimeValue:
|
||||
gotRebind = maybeU32(data)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -67,8 +67,8 @@ func _() {
|
||||
_ = x[OptParameterRequestList-55]
|
||||
_ = x[OptMessage-56]
|
||||
_ = x[OptMaximumMessageSize-57]
|
||||
_ = x[OptT1Renewal-58]
|
||||
_ = x[OptT2Rebinding-59]
|
||||
_ = x[OptRenewTimeValue-58]
|
||||
_ = x[OptRebindingTimeValue-59]
|
||||
_ = x[OptClientIdentifier-60]
|
||||
_ = x[OptClientIdentifier1-61]
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
+27
-2
@@ -50,6 +50,7 @@ func run() (err error) {
|
||||
flagUseHTTP = false
|
||||
flagHostToResolve = ""
|
||||
flagRequestedIP = ""
|
||||
flagLocalTCPPort = 0
|
||||
flagDoNTP = false
|
||||
flagNoPcap = false
|
||||
flagPprof = false
|
||||
@@ -58,6 +59,7 @@ func run() (err error) {
|
||||
flag.BoolVar(&flagUseHTTP, "ihttp", flagUseHTTP, "Use HTTP tap interface.")
|
||||
flag.StringVar(&flagHostToResolve, "host", flagHostToResolve, "Hostname to resolve via DNS.")
|
||||
flag.StringVar(&flagRequestedIP, "addr", flagRequestedIP, "IP address to request via DHCP.")
|
||||
flag.IntVar(&flagLocalTCPPort, "port", flagLocalTCPPort, "Local TCP source port to use. Zero chooses a pseudo-random port.")
|
||||
flag.BoolVar(&flagDoNTP, "ntp", flagDoNTP, "Do NTP round and print result time")
|
||||
flag.BoolVar(&flagNoPcap, "nopcap", flagNoPcap, "Disable pcap logging.")
|
||||
flag.BoolVar(&flagPprof, "pprof", flagPprof, "Enable CPU profiling.")
|
||||
@@ -79,6 +81,21 @@ func run() (err error) {
|
||||
flag.Usage()
|
||||
return err
|
||||
}
|
||||
reqAddr := [4]byte{192, 168, 1, 96}
|
||||
requestedAddrSet := false
|
||||
if flagRequestedIP != "" {
|
||||
addr, err := netip.ParseAddr(flagRequestedIP)
|
||||
if err != nil || !addr.Is4() {
|
||||
flag.Usage()
|
||||
return fmt.Errorf("invalid requested IPv4 address %q", flagRequestedIP)
|
||||
}
|
||||
reqAddr = addr.As4()
|
||||
requestedAddrSet = true
|
||||
}
|
||||
if flagLocalTCPPort < 0 || flagLocalTCPPort > math.MaxUint16 {
|
||||
flag.Usage()
|
||||
return fmt.Errorf("invalid local TCP port %d", flagLocalTCPPort)
|
||||
}
|
||||
fmt.Println("softrand", softRand)
|
||||
var iface ltesto.Interface
|
||||
if flagUseHTTP {
|
||||
@@ -223,11 +240,14 @@ func run() (err error) {
|
||||
dhcpRetries = 2
|
||||
)
|
||||
timeDHCP := timer("DHCP request completed")
|
||||
results, err := rstack.DoDHCPv4([4]byte{192, 168, 1, 96}, dhcpTimeout, dhcpRetries)
|
||||
results, err := rstack.DoDHCPv4(reqAddr, dhcpTimeout, dhcpRetries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DHCP failed: %w", err)
|
||||
}
|
||||
timeDHCP()
|
||||
if requestedAddrSet && results.AssignedAddr4 != reqAddr {
|
||||
return fmt.Errorf("DHCP assigned %s, not requested %s", netip.AddrFrom4(results.AssignedAddr4), netip.AddrFrom4(reqAddr))
|
||||
}
|
||||
|
||||
err = stack.AssimilateDHCPResults(results)
|
||||
if err != nil {
|
||||
@@ -302,7 +322,12 @@ func run() (err error) {
|
||||
timeTCPDial := timer("TCP dial (handshake)")
|
||||
const tcpDialTimeout = 8 * time.Second // Was 60 * time.Minute causing 3.6s sleep per iteration!
|
||||
target := netip.AddrPortFrom(addrs[0], 80)
|
||||
err = rstack.DoDialTCP(&conn, uint16(softRand&0xefff)+1024, target, tcpDialTimeout, internetRetries)
|
||||
localPort := uint16(flagLocalTCPPort)
|
||||
if localPort == 0 {
|
||||
localPort = uint16(softRand&0xefff) + 1024
|
||||
}
|
||||
fmt.Printf("TCP target %s local-port=%d\n", target, localPort)
|
||||
err = rstack.DoDialTCP(&conn, localPort, target, tcpDialTimeout, internetRetries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TCP failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package internet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/tcp"
|
||||
)
|
||||
|
||||
// TestConn_ConcurrentCloseDoesNotDropWrite is a regression test for issue #82:
|
||||
// when one goroutine has a Write in progress (blocked because the TX buffer is
|
||||
// full) and another goroutine calls Close, the in-flight write data must not be
|
||||
// silently dropped. With the atomic write-lock, Close waits for the in-progress
|
||||
// write to finish queueing all of its data before tearing the connection down.
|
||||
//
|
||||
// The scenario is made deterministic by filling the TX buffer before any packets
|
||||
// are exchanged: the writer blocks holding the write lock, Close is then issued
|
||||
// (and must wait), and only afterwards is the packet pump started so the buffer
|
||||
// can drain and the writer can run to completion.
|
||||
func TestConn_ConcurrentCloseDoesNotDropWrite(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
var sbCl, sbSv StackIPv4
|
||||
var connCl, connSv tcp.Conn
|
||||
setupClientServerEstablished(t, rng, &sbCl, &sbSv, &connCl, &connSv)
|
||||
|
||||
// Payload several times larger than the 2048-byte TX buffer so the writer
|
||||
// must block waiting for the buffer to drain across many segments.
|
||||
payload := make([]byte, 4*2048)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i)
|
||||
}
|
||||
|
||||
// Watchdog: break any deadlock so a regression fails fast instead of hanging.
|
||||
watchdog := time.AfterFunc(10*time.Second, func() {
|
||||
t.Error("test timed out: Close likely blocked waiting on a stuck write")
|
||||
connCl.Abort()
|
||||
connSv.Abort()
|
||||
})
|
||||
defer watchdog.Stop()
|
||||
|
||||
// Writer (G1): writes the whole payload. Blocks once the TX buffer fills.
|
||||
type writeResult struct {
|
||||
n int
|
||||
err error
|
||||
}
|
||||
writeDone := make(chan writeResult, 1)
|
||||
go func() {
|
||||
n, err := connCl.Write(payload)
|
||||
writeDone <- writeResult{n, err}
|
||||
}()
|
||||
|
||||
// Wait until the writer has filled the TX buffer and is blocked. At this
|
||||
// point it owns the write lock, reproducing "Write in progress".
|
||||
for connCl.FreeOutput() != 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
// Close (G2): issued while the write is in progress. Must wait for the
|
||||
// writer to drain instead of dropping its data.
|
||||
closeDone := make(chan error, 1)
|
||||
go func() {
|
||||
closeDone <- connCl.Close()
|
||||
}()
|
||||
|
||||
// Reader: drains the server side until EOF, accumulating everything received.
|
||||
readDone := make(chan []byte, 1)
|
||||
go func() {
|
||||
got := make([]byte, 0, len(payload))
|
||||
rbuf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := connSv.Read(rbuf)
|
||||
got = append(got, rbuf[:n]...)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
readDone <- got
|
||||
}()
|
||||
|
||||
// Packet pump: only now do we move packets between the two stacks, letting
|
||||
// the buffer drain so the blocked writer can finish.
|
||||
var stop atomic.Bool
|
||||
pumpStopped := make(chan struct{})
|
||||
go func() {
|
||||
defer close(pumpStopped)
|
||||
var buf [2048]byte
|
||||
for !stop.Load() {
|
||||
progressed := false
|
||||
if n, err := sbCl.Encapsulate(buf[:], 0, 0); err == nil && n > 0 {
|
||||
_ = sbSv.Demux(buf[:n], 0)
|
||||
progressed = true
|
||||
}
|
||||
if n, err := sbSv.Encapsulate(buf[:], 0, 0); err == nil && n > 0 {
|
||||
_ = sbCl.Demux(buf[:n], 0)
|
||||
progressed = true
|
||||
}
|
||||
if !progressed {
|
||||
runtime.Gosched()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wr := <-writeDone
|
||||
if wr.err != nil {
|
||||
t.Errorf("issue #82: Write returned error %v; concurrent Close dropped in-flight data", wr.err)
|
||||
}
|
||||
if wr.n != len(payload) {
|
||||
t.Errorf("issue #82: Write wrote %d of %d bytes; concurrent Close truncated the write", wr.n, len(payload))
|
||||
}
|
||||
if err := <-closeDone; err != nil {
|
||||
t.Errorf("Close returned error: %v", err)
|
||||
}
|
||||
got := <-readDone
|
||||
stop.Store(true)
|
||||
<-pumpStopped
|
||||
|
||||
if len(got) != len(payload) {
|
||||
t.Fatalf("server received %d of %d bytes; data was dropped by concurrent Close", len(got), len(payload))
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatal("server received corrupted data")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConn_ConcurrentWritesSerialize verifies that the atomic write-lock
|
||||
// serializes concurrent Write calls on the same connection so their payloads are
|
||||
// not interleaved on the wire, and that all bytes from both writers are delivered.
|
||||
func TestConn_ConcurrentWritesSerialize(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(2))
|
||||
var sbCl, sbSv StackIPv4
|
||||
var connCl, connSv tcp.Conn
|
||||
setupClientServerEstablished(t, rng, &sbCl, &sbSv, &connCl, &connSv)
|
||||
|
||||
const chunk = 1500
|
||||
a := bytes.Repeat([]byte{0xAA}, chunk)
|
||||
b := bytes.Repeat([]byte{0xBB}, chunk)
|
||||
|
||||
watchdog := time.AfterFunc(10*time.Second, func() {
|
||||
t.Error("test timed out")
|
||||
connCl.Abort()
|
||||
connSv.Abort()
|
||||
})
|
||||
defer watchdog.Stop()
|
||||
|
||||
var stop atomic.Bool
|
||||
pumpStopped := make(chan struct{})
|
||||
go func() {
|
||||
defer close(pumpStopped)
|
||||
var buf [2048]byte
|
||||
for !stop.Load() {
|
||||
progressed := false
|
||||
if n, err := sbCl.Encapsulate(buf[:], 0, 0); err == nil && n > 0 {
|
||||
_ = sbSv.Demux(buf[:n], 0)
|
||||
progressed = true
|
||||
}
|
||||
if n, err := sbSv.Encapsulate(buf[:], 0, 0); err == nil && n > 0 {
|
||||
_ = sbCl.Demux(buf[:n], 0)
|
||||
progressed = true
|
||||
}
|
||||
if !progressed {
|
||||
runtime.Gosched()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
writeErr := make(chan error, 2)
|
||||
writer := func(b []byte) {
|
||||
n, err := connCl.Write(b)
|
||||
if err == nil && n != len(b) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
writeErr <- err
|
||||
}
|
||||
go writer(a)
|
||||
go writer(b)
|
||||
|
||||
got := make([]byte, 0, 2*chunk)
|
||||
rbuf := make([]byte, 1024)
|
||||
for len(got) < 2*chunk {
|
||||
n, err := connSv.Read(rbuf)
|
||||
got = append(got, rbuf[:n]...)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for range 2 {
|
||||
if err := <-writeErr; err != nil {
|
||||
t.Errorf("concurrent write failed: %v", err)
|
||||
}
|
||||
}
|
||||
stop.Store(true)
|
||||
<-pumpStopped
|
||||
|
||||
if len(got) != 2*chunk {
|
||||
t.Fatalf("received %d bytes, want %d", len(got), 2*chunk)
|
||||
}
|
||||
// Serialized writes mean one chunk's bytes appear fully before the other's;
|
||||
// the boundary is a single transition, never interleaved.
|
||||
transitions := 0
|
||||
for i := 1; i < len(got); i++ {
|
||||
if got[i] != got[i-1] {
|
||||
transitions++
|
||||
}
|
||||
}
|
||||
if transitions != 1 {
|
||||
t.Fatalf("expected exactly one A/B boundary (serialized writes), got %d transitions", transitions)
|
||||
}
|
||||
}
|
||||
@@ -697,7 +697,7 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
|
||||
field.Class = FieldClassSize
|
||||
|
||||
// Time/duration options (seconds).
|
||||
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptT1Renewal, dhcpv4.OptT2Rebinding,
|
||||
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptRenewTimeValue, dhcpv4.OptRebindingTimeValue,
|
||||
dhcpv4.OptTimeOffset, dhcpv4.OptARPCacheTimeout, dhcpv4.OptPathMTUAgingTimeout,
|
||||
dhcpv4.OptTCPKeepaliveInterval, dhcpv4.OptDefaultIPTTL, dhcpv4.OptDefaultTCPTimetoLive:
|
||||
field.Class = FieldClassTimestamp
|
||||
|
||||
+23
-12
@@ -22,34 +22,35 @@ type StackIPv4 struct {
|
||||
}
|
||||
|
||||
func (stackip4 *StackIPv4) Reset(vld *lneto.Validator, maxNodes int) error {
|
||||
stackip4.connID++
|
||||
stackip4.reset4(vld, maxNodes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (stackip *StackIPv4) ConnectionID() *uint64 {
|
||||
return &stackip.connID
|
||||
func (stackip4 *StackIPv4) ConnectionID() *uint64 {
|
||||
return &stackip4.connID
|
||||
}
|
||||
|
||||
func (stackip *StackIPv4) Protocol() uint64 {
|
||||
func (stackip4 *StackIPv4) Protocol() uint64 {
|
||||
return uint64(ethernet.TypeIPv4)
|
||||
}
|
||||
|
||||
func (stackip *StackIPv4) LocalPort() uint16 { return 0 }
|
||||
func (stackip4 *StackIPv4) LocalPort() uint16 { return 0 }
|
||||
|
||||
func (stackip *StackIPv4) SetLogger(logger *slog.Logger) {
|
||||
stackip.stackip4.handlers.log = logger
|
||||
func (stackip4 *StackIPv4) SetLogger(logger *slog.Logger) {
|
||||
stackip4.stackip4.handlers.log = logger
|
||||
}
|
||||
|
||||
func (stackip *StackIPv4) Demux(carrierData []byte, offset int) error {
|
||||
func (stackip4 *StackIPv4) Demux(carrierData []byte, offset int) error {
|
||||
debugLog("ip:demux")
|
||||
return stackip.stackip4.demux4(carrierData, offset)
|
||||
return stackip4.stackip4.demux4(carrierData, offset)
|
||||
}
|
||||
|
||||
func (stackip *StackIPv4) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
|
||||
func (stackip4 *StackIPv4) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
|
||||
if offsetToFrame != offsetToIP {
|
||||
return 0, lneto.ErrBug
|
||||
}
|
||||
return stackip.stackip4.encapsulate4(carrierData, offsetToIP)
|
||||
return stackip4.stackip4.encapsulate4(carrierData, offsetToIP)
|
||||
}
|
||||
|
||||
type stackip4 struct {
|
||||
@@ -58,6 +59,7 @@ type stackip4 struct {
|
||||
ipID uint16
|
||||
ip4 [4]byte
|
||||
acceptMulticast bool
|
||||
acceptBroadcast bool
|
||||
}
|
||||
|
||||
func (si4 *stackip4) reset4(vld *lneto.Validator, maxNodes int) {
|
||||
@@ -86,6 +88,10 @@ func (si4 *stackip4) IsRegistered4(proto lneto.IPProto) bool {
|
||||
func (si4 *stackip4) SetAcceptMulticast4(accept bool) {
|
||||
si4.acceptMulticast = accept
|
||||
}
|
||||
func (si4 *stackip4) SetAcceptBroadcast4(accept bool) {
|
||||
si4.acceptBroadcast = accept
|
||||
}
|
||||
|
||||
func (si4 *stackip4) Addr4() [4]byte { return si4.ip4 }
|
||||
func (si4 *stackip4) SetAddr4(ip4 [4]byte) {
|
||||
si4.ip4 = ip4
|
||||
@@ -100,8 +106,13 @@ func (si4 *stackip4) demux4(carrierData []byte, offset int) error {
|
||||
return err
|
||||
}
|
||||
dst := ifrm.DestinationAddr()
|
||||
if si4.ip4 != ([4]byte{}) && *dst != si4.ip4 {
|
||||
if !si4.acceptMulticast || !internal.IsMulticastIPAddr(dst[:]) {
|
||||
if si4.ip4 != ([4]byte{}) && *dst != si4.ip4 { // Accept all packets when IP zeroed.
|
||||
switch {
|
||||
case si4.acceptMulticast && ipv4.IsMulticast(*dst):
|
||||
// accept multicast.
|
||||
case si4.acceptBroadcast && ipv4.IsBroadcast(*dst):
|
||||
// accept broadcast.
|
||||
default:
|
||||
si4.handlers.debug("ip:not-for-us")
|
||||
return lneto.ErrPacketDrop // Not meant for us.
|
||||
}
|
||||
|
||||
+14
-12
@@ -19,35 +19,36 @@ type StackIPv6 struct {
|
||||
stackip6
|
||||
}
|
||||
|
||||
func (stackip4 *StackIPv6) Reset(vld *lneto.Validator, maxNodes int) error {
|
||||
stackip4.reset6(vld, maxNodes)
|
||||
func (stackip6 *StackIPv6) Reset(vld *lneto.Validator, maxNodes int) error {
|
||||
stackip6.connID++
|
||||
stackip6.reset6(vld, maxNodes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (stackip *StackIPv6) ConnectionID() *uint64 {
|
||||
return &stackip.connID
|
||||
func (stackip6 *StackIPv6) ConnectionID() *uint64 {
|
||||
return &stackip6.connID
|
||||
}
|
||||
|
||||
func (stackip *StackIPv6) Protocol() uint64 {
|
||||
func (stackip6 *StackIPv6) Protocol() uint64 {
|
||||
return uint64(ethernet.TypeIPv6)
|
||||
}
|
||||
|
||||
func (stackip *StackIPv6) LocalPort() uint16 { return 0 }
|
||||
func (stackip6 *StackIPv6) LocalPort() uint16 { return 0 }
|
||||
|
||||
func (stackip *StackIPv6) SetLogger(logger *slog.Logger) {
|
||||
stackip.stackip6.handlers.log = logger
|
||||
func (stackip6 *StackIPv6) SetLogger(logger *slog.Logger) {
|
||||
stackip6.stackip6.handlers.log = logger
|
||||
}
|
||||
|
||||
func (stackip *StackIPv6) Demux(carrierData []byte, offset int) error {
|
||||
func (stackip6 *StackIPv6) Demux(carrierData []byte, offset int) error {
|
||||
debugLog("ip:demux")
|
||||
return stackip.stackip6.demux6(carrierData, offset)
|
||||
return stackip6.stackip6.demux6(carrierData, offset)
|
||||
}
|
||||
|
||||
func (stackip *StackIPv6) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
|
||||
func (stackip6 *StackIPv6) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (n int, err error) {
|
||||
if offsetToFrame != offsetToIP {
|
||||
return 0, lneto.ErrBug
|
||||
}
|
||||
return stackip.stackip6.encapsulate6(carrierData, offsetToIP)
|
||||
return stackip6.stackip6.encapsulate6(carrierData, offsetToIP)
|
||||
}
|
||||
|
||||
type stackip6 struct {
|
||||
@@ -55,6 +56,7 @@ type stackip6 struct {
|
||||
vld *lneto.Validator
|
||||
ip6 [16]byte
|
||||
acceptMulticast bool
|
||||
acceptBroadcast bool
|
||||
}
|
||||
|
||||
func (si6 *stackip6) Register6(h lneto.StackNode) error {
|
||||
|
||||
@@ -18,6 +18,17 @@ func IsMulticast(addr [4]byte) bool {
|
||||
return addr[0]&0xf0 == 0xe0
|
||||
}
|
||||
|
||||
// IsBroadcast reports whether addr is the limited broadcast address
|
||||
// 255.255.255.255 used to address all hosts on the local network segment
|
||||
// as defined in [RFC919]. It does not detect directed (subnet) broadcast
|
||||
// addresses, which depend on the network mask and cannot be determined from
|
||||
// the address alone.
|
||||
//
|
||||
// [RFC919]: https://datatracker.ietf.org/doc/html/rfc919
|
||||
func IsBroadcast(addr [4]byte) bool {
|
||||
return addr == [4]byte{255, 255, 255, 255}
|
||||
}
|
||||
|
||||
// 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].
|
||||
//
|
||||
|
||||
+59
-10
@@ -8,6 +8,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
@@ -29,6 +30,13 @@ type Conn struct {
|
||||
h Handler
|
||||
remoteAddr []byte
|
||||
|
||||
// writeLock holds the connection ID ([Handler.connid]) of the goroutine
|
||||
// that currently owns the write side via [Conn.Write] or [Conn.Flush].
|
||||
// A zero value means no write is in progress. It serializes concurrent
|
||||
// writers and lets [Conn.Close] acquire the write side before closing so it
|
||||
// does not drop in-flight data (see issue #82).
|
||||
writeLock atomic.Uint64
|
||||
|
||||
_backoff lneto.BackoffStrategy
|
||||
rdead time.Time
|
||||
wdead time.Time
|
||||
@@ -49,6 +57,7 @@ func (conn *Conn) reset(h Handler) {
|
||||
conn.wdead = time.Time{}
|
||||
conn.abortErr = nil
|
||||
conn.ipID = 0
|
||||
conn.writeLock.Store(0)
|
||||
}
|
||||
|
||||
// ConnConfig provides configuration parameters for [Conn].
|
||||
@@ -99,6 +108,22 @@ func (conn *Conn) RemotePort() uint16 {
|
||||
return conn.h.RemotePort()
|
||||
}
|
||||
|
||||
// IsAwaitingControl reports whether the connection is waiting for a response to
|
||||
// a control segment that can be retransmitted to advance connection state.
|
||||
func (conn *Conn) IsAwaitingControl() bool {
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
return conn.h.IsAwaitingControl()
|
||||
}
|
||||
|
||||
// RequeueControl asks the next packet emission to retransmit the outstanding
|
||||
// control segment, if the connection is waiting for one.
|
||||
func (conn *Conn) RequeueControl() {
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
conn.h.RequeueControl()
|
||||
}
|
||||
|
||||
// RemoteAddr returns the address of the peer Conn is exchanging data with.
|
||||
func (conn *Conn) RemoteAddr() []byte {
|
||||
conn.mu.Lock()
|
||||
@@ -213,6 +238,11 @@ func (conn *Conn) CloseRead() error {
|
||||
|
||||
// Close will initiate TCP close sequence. After Close is called future [Conn.Write] calls will fail with [net.ErrClosed].
|
||||
func (conn *Conn) Close() error {
|
||||
connid, err := conn.acquireWriteLock(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.releaseWriteLock(connid)
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
conn.trace("TCPConn.Close", slog.Uint64("lport", uint64(conn.h.localPort)), slog.Uint64("rport", uint64(conn.h.remotePort)))
|
||||
@@ -237,10 +267,11 @@ func (conn *Conn) InternalHandler() *Handler {
|
||||
|
||||
// Write writes argument data to the TCPConns's output buffer which is queued to be sent.
|
||||
func (conn *Conn) Write(b []byte) (int, error) {
|
||||
connid, err := conn.lockPipeConnID()
|
||||
connid, err := conn.acquireWriteLock(&conn.wdead)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer conn.releaseWriteLock(connid)
|
||||
rport := conn.RemotePort()
|
||||
plen := len(b)
|
||||
lport := conn.LocalPort()
|
||||
@@ -286,10 +317,11 @@ func (conn *Conn) Write(b []byte) (int, error) {
|
||||
|
||||
// Flush blocks until all buffered TCP data has been sent.
|
||||
func (conn *Conn) Flush() error {
|
||||
connid, err := conn.lockPipeConnID()
|
||||
connid, err := conn.acquireWriteLock(&conn.wdead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.releaseWriteLock(connid)
|
||||
var backoffs uint
|
||||
for conn.BufferedUnsent() != 0 {
|
||||
if err := conn.checkPipe(connid, &conn.wdead); err != nil {
|
||||
@@ -350,14 +382,31 @@ func (conn *Conn) Read(b []byte) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (conn *Conn) lockPipeConnID() (uint64, error) {
|
||||
// acquireWriteLock validates the connection is open and acquires the write lock
|
||||
// for the current connection, returning its connection ID. It serializes
|
||||
// concurrent writers: while another goroutine owns the write side it blocks with
|
||||
// backoff until that writer releases, the connection closes, or deadline elapses.
|
||||
// The returned connID must be released with [Conn.releaseWriteLock].
|
||||
func (conn *Conn) acquireWriteLock(deadline *time.Time) (connID uint64, err error) {
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
err := conn.checkPipeOpen()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
connID = conn.h.connid
|
||||
conn.mu.Unlock()
|
||||
var backoffs uint
|
||||
for {
|
||||
if err := conn.checkPipe(connID, deadline); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if conn.writeLock.CompareAndSwap(0, connID) {
|
||||
return connID, nil
|
||||
}
|
||||
conn.backoff(backoffs)
|
||||
backoffs++
|
||||
}
|
||||
return conn.h.connid, nil
|
||||
}
|
||||
|
||||
// releaseWriteLock releases a write lock previously acquired by [Conn.acquireWriteLock].
|
||||
func (conn *Conn) releaseWriteLock(connID uint64) {
|
||||
conn.writeLock.CompareAndSwap(connID, 0)
|
||||
}
|
||||
|
||||
func (conn *Conn) checkPipe(connID uint64, deadline *time.Time) (err error) {
|
||||
@@ -365,9 +414,9 @@ func (conn *Conn) checkPipe(connID uint64, deadline *time.Time) (err error) {
|
||||
defer conn.mu.Unlock()
|
||||
if conn.abortErr != nil {
|
||||
err = conn.abortErr
|
||||
} else if connID != conn.h.connid {
|
||||
} else if connID != conn.h.connid || conn.h.State().IsClosed() {
|
||||
err = net.ErrClosed
|
||||
} else if !deadline.IsZero() && time.Since(*deadline) > 0 {
|
||||
} else if deadline != nil && !deadline.IsZero() && time.Since(*deadline) > 0 {
|
||||
err = errDeadlineExceeded
|
||||
}
|
||||
return err
|
||||
|
||||
+36
-3
@@ -32,7 +32,8 @@ type Handler struct {
|
||||
closing bool
|
||||
shutdownRx bool
|
||||
// nRetransmit stores the number of times the oldest packet was retransmit.
|
||||
nRetransmit uint8
|
||||
nRetransmit uint8
|
||||
requeueControl bool
|
||||
}
|
||||
|
||||
// SetLoggers sets the [slog.Logger] for the Handler and internal [ControlBlock].
|
||||
@@ -271,6 +272,7 @@ func (h *Handler) Send(b []byte) (int, error) {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
awaitingSyn := h.AwaitingSynSend()
|
||||
requeueControl := h.requeueControl
|
||||
buffered := h.bufTx.BufferedUnsent()
|
||||
if h.scb.State() == StateCloseWait && !h.closing && buffered == 0 && !h.scb.HasPending() {
|
||||
// Remote closed with no application data left to send: initiate our own close.
|
||||
@@ -278,7 +280,7 @@ func (h *Handler) Send(b []byte) (int, error) {
|
||||
// before Send is called, implementing the half-close per RFC 9293 §3.5.
|
||||
h.closing = true
|
||||
}
|
||||
if !awaitingSyn && buffered == 0 && !h.closing && !h.scb.HasPending() {
|
||||
if !awaitingSyn && !requeueControl && buffered == 0 && !h.closing && !h.scb.HasPending() {
|
||||
// Early nop short circuit.
|
||||
return 0, nil
|
||||
}
|
||||
@@ -301,11 +303,27 @@ func (h *Handler) Send(b []byte) (int, error) {
|
||||
offset := uint8(5)
|
||||
mss := uint16(len(b) - sizeHeaderTCP)
|
||||
var segment Segment
|
||||
if awaitingSyn {
|
||||
if awaitingSyn || requeueControl && h.scb.State() == StateSynSent {
|
||||
// Handling init syn segment.
|
||||
segment = ClientSynSegment(h.bufTx.iss, Size(h.bufRx.Size()))
|
||||
h.optcodec.PutOption16(b[sizeHeaderTCP:], OptMaxSegmentSize, mss)
|
||||
offset++
|
||||
if requeueControl {
|
||||
h.info("tcp.Handler:requeue-syn", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort)))
|
||||
}
|
||||
} else if requeueControl && h.scb.State() == StateSynRcvd {
|
||||
segment = Segment{
|
||||
SEQ: h.scb.snd.UNA,
|
||||
ACK: h.scb.rcv.NXT,
|
||||
WND: Size(h.bufRx.Free()),
|
||||
Flags: synack,
|
||||
}
|
||||
h.optcodec.PutOption16(b[sizeHeaderTCP:], OptMaxSegmentSize, mss)
|
||||
offset++
|
||||
h.info("tcp.Handler:requeue-synack", slog.Uint64("port", uint64(h.localPort)), slog.Uint64("rport", uint64(h.remotePort)))
|
||||
} else if requeueControl {
|
||||
h.requeueControl = false
|
||||
return 0, nil
|
||||
} else {
|
||||
var ok bool
|
||||
maxPayload := len(b) - sizeHeaderTCP
|
||||
@@ -335,6 +353,7 @@ func (h *Handler) Send(b []byte) (int, error) {
|
||||
} else if prevState != h.scb.State() && h.logenabled(slog.LevelInfo) {
|
||||
h.info("tcp.Handler:tx-statechange", slog.Uint64("port", uint64(h.localPort)), slog.String("oldState", prevState.String()), slog.String("newState", h.scb.State().String()), slog.String("txflags", segment.Flags.String()))
|
||||
}
|
||||
h.requeueControl = false
|
||||
tfrm.SetSourcePort(h.localPort)
|
||||
tfrm.SetDestinationPort(h.remotePort)
|
||||
tfrm.SetSegment(segment, offset)
|
||||
@@ -443,6 +462,20 @@ func (h *Handler) AwaitingSynResponse() bool {
|
||||
return h.remotePort != 0 && h.scb.State() == StateSynSent
|
||||
}
|
||||
|
||||
// IsAwaitingControl reports whether the connection is waiting for a response to
|
||||
// a control segment that can be retransmitted to advance connection state.
|
||||
func (h *Handler) IsAwaitingControl() bool {
|
||||
return h.AwaitingSynResponse() || h.scb.State() == StateSynRcvd
|
||||
}
|
||||
|
||||
// RequeueControl asks the next Send call to retransmit the outstanding control
|
||||
// segment, if the connection is waiting for one.
|
||||
func (h *Handler) RequeueControl() {
|
||||
if h.IsAwaitingControl() {
|
||||
h.requeueControl = true
|
||||
}
|
||||
}
|
||||
|
||||
// AwaitingSynAck returns true if the Handler is a passive server opened with [Handler.OpenListen] and not yet received a valid SYN remote packet.
|
||||
func (h *Handler) AwaitingSynAck() bool {
|
||||
return h.remotePort == 0 && h.scb.State() == StateListen
|
||||
|
||||
@@ -20,6 +20,103 @@ func TestHandler(t *testing.T) {
|
||||
sendDataFull(t, client, server, []byte("hello"), rawbuf[:])
|
||||
}
|
||||
|
||||
func TestHandler_RequeueControlRetransmitsSYN(t *testing.T) {
|
||||
const mtu = ethernet.MaxMTU
|
||||
const maxpackets = 3
|
||||
rng := rand.New(rand.NewSource(1))
|
||||
client, server := newHandler(t, mtu, maxpackets), newHandler(t, mtu, maxpackets)
|
||||
setupClientServer(t, rng, client, server)
|
||||
|
||||
var rawbuf [mtu]byte
|
||||
n, err := client.Send(rawbuf[:])
|
||||
if err != nil {
|
||||
t.Fatal("client sending initial SYN:", err)
|
||||
} else if n < sizeHeaderTCP {
|
||||
t.Fatalf("initial SYN size=%d, want at least %d", n, sizeHeaderTCP)
|
||||
}
|
||||
initial := mustSegment(t, rawbuf[:n], 0)
|
||||
if initial.Flags != FlagSYN {
|
||||
t.Fatalf("initial flags=%s, want SYN", initial.Flags)
|
||||
}
|
||||
if client.State() != StateSynSent {
|
||||
t.Fatalf("client state=%s, want SYN-SENT", client.State())
|
||||
}
|
||||
|
||||
clear(rawbuf[:])
|
||||
n, err = client.Send(rawbuf[:])
|
||||
if err != nil {
|
||||
t.Fatal("client sending before RequeueControl:", err)
|
||||
} else if n != 0 {
|
||||
t.Fatalf("Send before RequeueControl wrote %d bytes, want 0", n)
|
||||
}
|
||||
|
||||
client.RequeueControl()
|
||||
clear(rawbuf[:])
|
||||
n, err = client.Send(rawbuf[:])
|
||||
if err != nil {
|
||||
t.Fatal("client retransmitting SYN:", err)
|
||||
} else if n < sizeHeaderTCP {
|
||||
t.Fatalf("retransmitted SYN size=%d, want at least %d", n, sizeHeaderTCP)
|
||||
}
|
||||
retransmit := mustSegment(t, rawbuf[:n], 0)
|
||||
if retransmit.Flags != FlagSYN {
|
||||
t.Fatalf("retransmit flags=%s, want SYN", retransmit.Flags)
|
||||
}
|
||||
if retransmit.SEQ != initial.SEQ {
|
||||
t.Fatalf("retransmit SEQ=%d, want initial SEQ=%d", retransmit.SEQ, initial.SEQ)
|
||||
}
|
||||
|
||||
if err := server.Recv(rawbuf[:n]); err != nil {
|
||||
t.Fatal("server receiving retransmitted SYN:", err)
|
||||
}
|
||||
clear(rawbuf[:])
|
||||
n, err = server.Send(rawbuf[:])
|
||||
if err != nil {
|
||||
t.Fatal("server sending SYN-ACK:", err)
|
||||
}
|
||||
segSynAck := mustSegment(t, rawbuf[:n], 0)
|
||||
if segSynAck.Flags != synack {
|
||||
t.Fatalf("server flags=%s, want SYN-ACK", segSynAck.Flags)
|
||||
}
|
||||
if err := client.Recv(rawbuf[:n]); err != nil {
|
||||
t.Fatal("client receiving SYN-ACK:", err)
|
||||
}
|
||||
if client.State() != StateEstablished {
|
||||
t.Fatalf("client state=%s, want ESTABLISHED", client.State())
|
||||
}
|
||||
|
||||
clear(rawbuf[:])
|
||||
n, err = client.Send(rawbuf[:]) // final ACK.
|
||||
if err != nil {
|
||||
t.Fatal("client sending final ACK:", err)
|
||||
}
|
||||
if ack := mustSegment(t, rawbuf[:n], 0); ack.Flags != FlagACK {
|
||||
t.Fatalf("client final flags=%s, want ACK", ack.Flags)
|
||||
}
|
||||
if err := server.Recv(rawbuf[:n]); err != nil {
|
||||
t.Fatal("server receiving final ACK:", err)
|
||||
}
|
||||
|
||||
client.RequeueControl()
|
||||
clear(rawbuf[:])
|
||||
n, err = client.Send(rawbuf[:])
|
||||
if err != nil {
|
||||
t.Fatal("client sending after establishment:", err)
|
||||
} else if n != 0 {
|
||||
seg := mustSegment(t, rawbuf[:n], 0)
|
||||
t.Fatalf("Send after establishment wrote %d bytes (%s), want 0", n, seg.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
func mustSegment(t *testing.T, b []byte, payloadLen int) Segment {
|
||||
t.Helper()
|
||||
frame, err := NewFrame(b)
|
||||
if err != nil {
|
||||
t.Fatal("parse TCP frame:", err)
|
||||
}
|
||||
return frame.Segment(payloadLen)
|
||||
}
|
||||
|
||||
func sendDataFull(t *testing.T, client, server *Handler, data, packetBuf []byte) {
|
||||
n, err := client.Write(data)
|
||||
if err != nil {
|
||||
|
||||
@@ -103,6 +103,8 @@ type StackConfig struct {
|
||||
MTU uint16
|
||||
// Accept multicast ethernet and IP packets. Needed for MDNS.
|
||||
AcceptMulticast bool
|
||||
// Accept broadcast IPv4 packets. Needed for managing access points and DHCPv4 servers.
|
||||
AcceptIPv4Broadcast bool
|
||||
}
|
||||
|
||||
func (cfg *StackConfig) id() uint16 {
|
||||
@@ -234,7 +236,7 @@ func (s *StackAsync) Reset(cfg StackConfig) (err error) {
|
||||
}
|
||||
s.ip4.SetAddr4(cfg.StaticAddress4)
|
||||
s.setAcceptMulticast4(cfg.AcceptMulticast)
|
||||
|
||||
s.ip4.SetAcceptBroadcast4(cfg.AcceptIPv4Broadcast)
|
||||
s.arpt.passivePeers = uint8(cfg.PassivePeers)
|
||||
err = s.resetARP()
|
||||
if err != nil {
|
||||
|
||||
+29
-11
@@ -31,8 +31,20 @@ func (s *StackAsync) StackBlocking(stackProtoBackoff lneto.BackoffStrategy) Stac
|
||||
}
|
||||
|
||||
type StackBlocking struct {
|
||||
async *StackAsync
|
||||
_backoff lneto.BackoffStrategy
|
||||
async *StackAsync
|
||||
_backoff lneto.BackoffStrategy
|
||||
_nanotime func() int64
|
||||
}
|
||||
|
||||
func (s StackBlocking) nanotime() int64 {
|
||||
if s._nanotime != nil {
|
||||
return s._nanotime()
|
||||
}
|
||||
return time.Now().UnixNano()
|
||||
}
|
||||
|
||||
func (s StackBlocking) deadlineTO(timeout time.Duration) int64 {
|
||||
return int64(timeout) + s.nanotime()
|
||||
}
|
||||
|
||||
func (s StackBlocking) DoDHCPv4(reqAddr [4]byte, timeout time.Duration) (*DHCPResults, error) {
|
||||
@@ -41,7 +53,7 @@ func (s StackBlocking) DoDHCPv4(reqAddr [4]byte, timeout time.Duration) (*DHCPRe
|
||||
return nil, err
|
||||
}
|
||||
var backoffs uint
|
||||
deadline := time.Now().Add(timeout)
|
||||
deadline := s.deadlineTO(timeout)
|
||||
requested := false
|
||||
var lastState dhcpv4.ClientState
|
||||
for range maxIter {
|
||||
@@ -108,7 +120,7 @@ func (s StackBlocking) DoNTP(hostAddr netip.Addr, timeout time.Duration) (offset
|
||||
return -1, err
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
deadline := s.deadlineTO(timeout)
|
||||
var done bool
|
||||
var backoffs uint
|
||||
for range maxIter {
|
||||
@@ -130,7 +142,7 @@ func (s StackBlocking) DoResolveHardwareAddress6(addr netip.Addr, timeout time.D
|
||||
return hw, err
|
||||
}
|
||||
var backoffs uint
|
||||
deadline := time.Now().Add(timeout)
|
||||
deadline := s.deadlineTO(timeout)
|
||||
for range maxIter {
|
||||
hw, err = s.async.ResultResolveHardwareAddress6(addr)
|
||||
if err == nil {
|
||||
@@ -159,7 +171,7 @@ func (s StackBlocking) DoLookupIPType(host string, timeout time.Duration, qtype
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
deadline := s.deadlineTO(timeout)
|
||||
var backoffs uint
|
||||
for range maxIter {
|
||||
addrs, completed, err := s.async.ResultLookupIP(host)
|
||||
@@ -181,7 +193,15 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
err = s.waitDialTCP(conn, timeout)
|
||||
if err != nil {
|
||||
conn.Abort()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s StackBlocking) waitDialTCP(conn *tcp.Conn, timeout time.Duration) (err error) {
|
||||
deadline := s.deadlineTO(timeout)
|
||||
var backoffs uint
|
||||
for range maxIter {
|
||||
state := conn.State()
|
||||
@@ -189,12 +209,10 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A
|
||||
return nil
|
||||
} else if state == tcp.StateSynSent || state == tcp.StateSynRcvd || conn.AwaitingSynSend() {
|
||||
if err = s.checkDeadline(deadline); err != nil {
|
||||
conn.Abort()
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Unexpected state, abort and terminate connection.
|
||||
conn.Abort()
|
||||
return errTCPFailedToConnect
|
||||
}
|
||||
s.backoff(backoffs)
|
||||
@@ -203,8 +221,8 @@ func (s StackBlocking) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.A
|
||||
return errDeadlineExceed
|
||||
}
|
||||
|
||||
func (s StackBlocking) checkDeadline(deadline time.Time) error {
|
||||
if time.Since(deadline) > 0 {
|
||||
func (s StackBlocking) checkDeadline(deadline int64) error {
|
||||
if s.nanotime() > deadline {
|
||||
return errDeadlineExceed
|
||||
}
|
||||
return nil
|
||||
|
||||
+21
-5
@@ -18,10 +18,15 @@ import (
|
||||
const (
|
||||
sockSTREAM = 0x1
|
||||
sockDGRAM = 0x2
|
||||
|
||||
defaultTCPDialTimeout = 2 * time.Second
|
||||
defaultTCPDialRetries = 1
|
||||
)
|
||||
|
||||
type StackGoConfig struct {
|
||||
ListenerPoolConfig TCPPoolConfig
|
||||
TCPDialTimeout time.Duration
|
||||
TCPDialRetries int
|
||||
}
|
||||
|
||||
func (s *StackAsync) StackGo(stackProtoBackoff lneto.BackoffStrategy, cfg StackGoConfig) StackGo {
|
||||
@@ -32,16 +37,27 @@ func (s *StackAsync) StackGo(stackProtoBackoff lneto.BackoffStrategy, cfg StackG
|
||||
}
|
||||
|
||||
func (s StackBlocking) StackGo(cfg StackGoConfig) StackGo {
|
||||
if cfg.TCPDialRetries <= 0 {
|
||||
cfg.TCPDialRetries = defaultTCPDialRetries
|
||||
}
|
||||
if cfg.TCPDialTimeout <= 0 {
|
||||
cfg.TCPDialTimeout = defaultTCPDialTimeout
|
||||
}
|
||||
|
||||
sg := StackGo{
|
||||
blk: s,
|
||||
plcfg: cfg.ListenerPoolConfig,
|
||||
blk: s,
|
||||
plcfg: cfg.ListenerPoolConfig,
|
||||
tcpDialTimeout: cfg.TCPDialTimeout,
|
||||
tcpDialRetries: cfg.TCPDialRetries,
|
||||
}
|
||||
return sg
|
||||
}
|
||||
|
||||
type StackGo struct {
|
||||
blk StackBlocking
|
||||
plcfg TCPPoolConfig
|
||||
blk StackBlocking
|
||||
plcfg TCPPoolConfig
|
||||
tcpDialTimeout time.Duration
|
||||
tcpDialRetries int
|
||||
}
|
||||
|
||||
func (s StackGo) Socket(ctx context.Context, network string, family, sotype int, laddr, raddr net.Addr) (c any, err error) {
|
||||
@@ -171,7 +187,7 @@ func (s StackGo) SocketNetip(ctx context.Context, network string, family, sotype
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = s.blk.async.DialTCP(&conn, laddr.Port(), raddr)
|
||||
err = s.blk.StackRetrying().DoDialTCP(&conn, laddr.Port(), raddr, s.tcpDialTimeout, s.tcpDialRetries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ func (s *StackAsync) StackRetrying(stackProtoBackoff lneto.BackoffStrategy) Stac
|
||||
if stackProtoBackoff == nil {
|
||||
panic("nil backoff to StackRetrying")
|
||||
}
|
||||
return StackRetrying{
|
||||
block: s.StackBlocking(stackProtoBackoff),
|
||||
}
|
||||
return s.StackBlocking(stackProtoBackoff).StackRetrying()
|
||||
}
|
||||
|
||||
func (s StackBlocking) StackRetrying() StackRetrying {
|
||||
return StackRetrying{block: s}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -93,13 +95,28 @@ func (s StackRetrying) DoResolveHardwareAddress6(addr netip.Addr, timeout time.D
|
||||
func (s StackRetrying) DoDialTCP(conn *tcp.Conn, localPort uint16, addrp netip.AddrPort, timeout time.Duration, retries int) (err error) {
|
||||
expectEnd := time.Now().Add(timeout * time.Duration(retries))
|
||||
var firstErr error
|
||||
for range retries {
|
||||
err = s.block.DoDialTCP(conn, localPort, addrp, timeout)
|
||||
for i := range retries {
|
||||
if i == 0 || conn.State().IsClosed() {
|
||||
err = s.block.async.DialTCP(conn, localPort, addrp)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else if conn.IsAwaitingControl() {
|
||||
conn.RequeueControl()
|
||||
}
|
||||
|
||||
err = s.block.waitDialTCP(conn, timeout)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if !conn.IsAwaitingControl() {
|
||||
conn.Abort()
|
||||
}
|
||||
}
|
||||
if time.Now().Before(expectEnd) {
|
||||
if err != firstErr {
|
||||
|
||||
@@ -2,10 +2,12 @@ package xnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -28,6 +30,57 @@ const (
|
||||
finack = tcp.FlagFIN | tcp.FlagACK
|
||||
)
|
||||
|
||||
func newstackTestScheduler(t testing.TB) stackTestScheduler {
|
||||
return stackTestScheduler{
|
||||
t: t,
|
||||
stackBackoffSignal: make(chan struct{}),
|
||||
stackContinueSignal: make(chan struct{}),
|
||||
timeout: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
type stackTestScheduler struct {
|
||||
t testing.TB
|
||||
// when stack backs off it signals here and waits until channel read or timeout.
|
||||
stackBackoffSignal chan struct{}
|
||||
// when main goroutine is ready for more information this channel is written to to signal waiting on stack activity.
|
||||
stackContinueSignal chan struct{}
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (ss *stackTestScheduler) backoffStack(consecutiveBackoffs uint) time.Duration {
|
||||
timeout := time.After(ss.timeout)
|
||||
select {
|
||||
case ss.stackBackoffSignal <- struct{}{}:
|
||||
case <-timeout:
|
||||
ss.t.Fatal("timeout backing off, possible race condition? Multiple stacks using same backoff is unexpected pattern")
|
||||
}
|
||||
select {
|
||||
case <-ss.stackContinueSignal:
|
||||
case <-timeout:
|
||||
ss.t.Fatal("timeout waiting for continue")
|
||||
}
|
||||
return lneto.BackoffFlagNop // backoff yield implemented on our side.
|
||||
}
|
||||
|
||||
func (ss *stackTestScheduler) mainGoroutineWaitForStackYield() {
|
||||
timeout := time.After(ss.timeout)
|
||||
select {
|
||||
case <-ss.stackBackoffSignal:
|
||||
case <-timeout:
|
||||
ss.t.Fatal("timeout waiting for stack to backoff")
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *stackTestScheduler) mainGoroutineYieldToStack() {
|
||||
timeout := time.After(ss.timeout)
|
||||
select {
|
||||
case ss.stackContinueSignal <- struct{}{}:
|
||||
case <-timeout:
|
||||
ss.t.Fatal("timeout while trying to yield to stack")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPConn_ReadBlocksUntilDataAvailable(t *testing.T) {
|
||||
const seed = 5678
|
||||
const MTU = ethernet.MaxMTU
|
||||
@@ -105,6 +158,78 @@ func TestTCPConn_ReadBlocksUntilDataAvailable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackGoTCPDialRetriesPendingControl(t *testing.T) {
|
||||
const seed = 5678
|
||||
const MTU = ethernet.MaxMTU
|
||||
const tcptimeout = time.Second
|
||||
const yield = 1 * time.Millisecond
|
||||
client, sv, _, _ := newTCPStacks(t, seed, MTU)
|
||||
tbackoffer := newstackTestScheduler(t)
|
||||
|
||||
sg := client.StackBlocking(tbackoffer.backoffStack).StackGo(StackGoConfig{
|
||||
ListenerPoolConfig: TCPPoolConfig{
|
||||
QueueSize: 4,
|
||||
TxBufSize: MTU,
|
||||
RxBufSize: MTU,
|
||||
NewBackoff: func() lneto.BackoffStrategy {
|
||||
return backoffYield
|
||||
},
|
||||
},
|
||||
TCPDialTimeout: tcptimeout,
|
||||
TCPDialRetries: 2,
|
||||
})
|
||||
t.Log("start")
|
||||
// closure to simulate time.
|
||||
var now time.Duration
|
||||
sg.blk._nanotime = func() int64 { return int64(now) }
|
||||
|
||||
laddr := netip.AddrPortFrom(netip.AddrFrom4(client.Addr4()), 1234)
|
||||
raddr := netip.AddrPortFrom(netip.AddrFrom4(sv.Addr4()), 22)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := sg.SocketNetip(context.Background(), "tcp", syscall.AF_INET, sockSTREAM, laddr, raddr)
|
||||
done <- err
|
||||
}()
|
||||
npacket := 0
|
||||
ntcppacket := 0
|
||||
var buf [ethernet.MaxMTU + ethernet.MaxOverheadSize]byte
|
||||
for !t.Failed() { // Tinygo does not implement failnow.
|
||||
tbackoffer.mainGoroutineWaitForStackYield()
|
||||
n, err := client.EgressEthernet(buf[:])
|
||||
now += tcptimeout / 100
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n == 0 {
|
||||
t.Fatal("expected packet from socketnetip", npacket, ntcppacket)
|
||||
}
|
||||
npacket++
|
||||
frm, ok := getTCPFrame(buf[:])
|
||||
if !ok {
|
||||
tbackoffer.mainGoroutineYieldToStack()
|
||||
continue
|
||||
}
|
||||
ntcppacket++
|
||||
_, flags := frm.OffsetAndFlags()
|
||||
if flags != tcp.FlagSYN {
|
||||
t.Fatal("expected SYN packet")
|
||||
}
|
||||
switch ntcppacket {
|
||||
case 1:
|
||||
now += 2 * tcptimeout
|
||||
tbackoffer.mainGoroutineYieldToStack()
|
||||
case 2:
|
||||
now += 2 * tcptimeout
|
||||
tbackoffer.mainGoroutineYieldToStack()
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("SocketNetip hanging")
|
||||
case <-done:
|
||||
return // Test success.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackAsyncTCP_multipacket(t *testing.T) {
|
||||
const seed = 1234
|
||||
const MTU = 512
|
||||
|
||||
Reference in New Issue
Block a user