初版功能完成
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
// Package subnet transports complete raw IPv4 packets inside Overlay UDP.
|
||||
package subnet
|
||||
|
||||
import "remlink/internal/protocol"
|
||||
|
||||
// EncodeDatagram uses the one authoritative v1 SessionHeader codec.
|
||||
func EncodeDatagram(sessionID uint64, packet []byte) ([]byte, error) {
|
||||
return protocol.EncodeIPv4Session(sessionID, packet)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type PacketHandler func(context.Context, uint64, []byte) error
|
||||
type RejectHandler func(error, netip.Addr)
|
||||
|
||||
// Listener binds only the configured Overlay IP and validates before dispatch.
|
||||
type Listener struct {
|
||||
conn *net.UDPConn
|
||||
registry *Registry
|
||||
handler PacketHandler
|
||||
onReject RejectHandler
|
||||
closeOnce sync.Once
|
||||
counterMu sync.RWMutex
|
||||
bySession map[uint64]listenerCounters
|
||||
accepted atomic.Uint64
|
||||
acceptedBytes atomic.Uint64
|
||||
rejected atomic.Uint64
|
||||
}
|
||||
|
||||
type listenerCounters struct {
|
||||
bytes uint64
|
||||
packets uint64
|
||||
}
|
||||
|
||||
func NewListener(localIP netip.Addr, port int, registry *Registry, handler PacketHandler, onReject RejectHandler) (*Listener, error) {
|
||||
if !localIP.Is4() || localIP.IsUnspecified() || port < 0 || port > 65535 || registry == nil || handler == nil {
|
||||
return nil, errors.New("Listener requires exact IPv4 local address, valid port, Registry, and handler")
|
||||
}
|
||||
connection, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP(localIP.AsSlice()), Port: port})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bind Session Listener to %s:%d: %w", localIP, port, err)
|
||||
}
|
||||
return &Listener{conn: connection, registry: registry, handler: handler, onReject: onReject, bySession: make(map[uint64]listenerCounters)}, nil
|
||||
}
|
||||
|
||||
func (l *Listener) Port() int { return l.conn.LocalAddr().(*net.UDPAddr).Port }
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
var err error
|
||||
l.closeOnce.Do(func() { err = l.conn.Close() })
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *Listener) Counters() (accepted, rejected uint64) {
|
||||
return l.accepted.Load(), l.rejected.Load()
|
||||
}
|
||||
|
||||
// DetailedCounters returns accepted payload bytes/packets and rejected datagrams.
|
||||
func (l *Listener) DetailedCounters() (bytes, packets, rejected uint64) {
|
||||
return l.acceptedBytes.Load(), l.accepted.Load(), l.rejected.Load()
|
||||
}
|
||||
|
||||
// SessionCounters returns accepted raw IPv4 payload bytes and packets for one SessionID.
|
||||
func (l *Listener) SessionCounters(sessionID uint64) (bytes, packets uint64) {
|
||||
l.counterMu.RLock()
|
||||
counters := l.bySession[sessionID]
|
||||
l.counterMu.RUnlock()
|
||||
return counters.bytes, counters.packets
|
||||
}
|
||||
|
||||
func (l *Listener) Run(ctx context.Context) error {
|
||||
stopClose := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = l.Close()
|
||||
case <-stopClose:
|
||||
}
|
||||
}()
|
||||
defer close(stopClose)
|
||||
buffer := make([]byte, 65535)
|
||||
for {
|
||||
count, source, err := l.conn.ReadFromUDPAddrPort(buffer)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("read Session UDP: %w", err)
|
||||
}
|
||||
sessionID, packet, err := ValidateDatagram(buffer[:count], source.Addr(), l.registry)
|
||||
if err != nil {
|
||||
l.rejected.Add(1)
|
||||
if l.onReject != nil {
|
||||
l.onReject(err, source.Addr())
|
||||
}
|
||||
continue
|
||||
}
|
||||
owned := append([]byte(nil), packet...)
|
||||
if err := l.handler(ctx, sessionID, owned); err != nil {
|
||||
l.rejected.Add(1)
|
||||
if l.onReject != nil {
|
||||
l.onReject(fmt.Errorf("handle Session packet: %w", err), source.Addr())
|
||||
}
|
||||
continue
|
||||
}
|
||||
l.acceptedBytes.Add(uint64(len(owned)))
|
||||
l.accepted.Add(1)
|
||||
l.counterMu.Lock()
|
||||
counters := l.bySession[sessionID]
|
||||
counters.bytes += uint64(len(owned))
|
||||
counters.packets++
|
||||
l.bySession[sessionID] = counters
|
||||
l.counterMu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestListenerContinuesAfterPacketHandlerFailure(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
peer := netip.MustParseAddr("127.0.0.1")
|
||||
if err := registry.Upsert(SessionBinding{
|
||||
SessionID: 41, PeerOverlayIP: peer, EngineerOverlayIP: peer,
|
||||
RemoteCIDRs: []netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")},
|
||||
Direction: EngineerToSite, Active: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var calls, rejects atomic.Int32
|
||||
listener, err := NewListener(peer, 0, registry, func(context.Context, uint64, []byte) error {
|
||||
if calls.Add(1) == 1 {
|
||||
return errors.New("injection failed")
|
||||
}
|
||||
return nil
|
||||
}, func(error, netip.Addr) { rejects.Add(1) })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- listener.Run(ctx) }()
|
||||
connection, err := net.DialUDP("udp4", nil, &net.UDPAddr{IP: net.IP(peer.AsSlice()), Port: listener.Port()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
packet := testListenerIPv4("127.0.0.1", "192.168.13.10")
|
||||
datagram, err := EncodeDatagram(41, packet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := connection.Write(datagram); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := connection.Write(datagram); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
accepted, rejected := listener.Counters()
|
||||
if accepted == 1 && rejected == 1 && rejects.Load() == 1 {
|
||||
cancel()
|
||||
<-done
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("listener counters accepted=%d rejected=%d callbacks=%d", func() uint64 { a, _ := listener.Counters(); return a }(), func() uint64 { _, r := listener.Counters(); return r }(), rejects.Load())
|
||||
}
|
||||
|
||||
func testListenerIPv4(sourceText, destinationText string) []byte {
|
||||
packet := make([]byte, 20)
|
||||
packet[0] = 0x45
|
||||
packet[3] = 20
|
||||
source := netip.MustParseAddr(sourceText).As4()
|
||||
destination := netip.MustParseAddr(destinationText).As4()
|
||||
copy(packet[12:16], source[:])
|
||||
copy(packet[16:20], destination[:])
|
||||
return packet
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Direction uint8
|
||||
|
||||
const (
|
||||
EngineerToSite Direction = iota + 1
|
||||
SiteToEngineer
|
||||
)
|
||||
|
||||
// SessionBinding contains the exact identities used by receive validation.
|
||||
type SessionBinding struct {
|
||||
SessionID uint64
|
||||
PeerOverlayIP netip.Addr
|
||||
EngineerOverlayIP netip.Addr
|
||||
RemoteCIDRs []netip.Prefix
|
||||
Direction Direction
|
||||
Active bool
|
||||
}
|
||||
|
||||
// Registry is the listener's concurrency-safe Active Session lookup.
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[uint64]SessionBinding
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry { return &Registry{sessions: make(map[uint64]SessionBinding)} }
|
||||
|
||||
func (r *Registry) Upsert(binding SessionBinding) error {
|
||||
if err := validateBinding(binding); err != nil {
|
||||
return err
|
||||
}
|
||||
binding.RemoteCIDRs = append([]netip.Prefix(nil), binding.RemoteCIDRs...)
|
||||
r.mu.Lock()
|
||||
r.sessions[binding.SessionID] = binding
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) Remove(sessionID uint64) {
|
||||
r.mu.Lock()
|
||||
delete(r.sessions, sessionID)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *Registry) Lookup(sessionID uint64) (SessionBinding, bool) {
|
||||
r.mu.RLock()
|
||||
binding, found := r.sessions[sessionID]
|
||||
r.mu.RUnlock()
|
||||
return binding, found
|
||||
}
|
||||
|
||||
func validateBinding(binding SessionBinding) error {
|
||||
if binding.SessionID == 0 || !binding.PeerOverlayIP.Is4() || !binding.EngineerOverlayIP.Is4() || len(binding.RemoteCIDRs) == 0 {
|
||||
return errors.New("Session binding requires nonzero ID, IPv4 peers, and Remote CIDRs")
|
||||
}
|
||||
if binding.Direction != EngineerToSite && binding.Direction != SiteToEngineer {
|
||||
return errors.New("Session binding direction is invalid")
|
||||
}
|
||||
for _, prefix := range binding.RemoteCIDRs {
|
||||
if !prefix.Addr().Is4() || prefix != prefix.Masked() || prefix.Bits() == 0 {
|
||||
return errors.New("Session Remote CIDRs must be canonical non-default IPv4 prefixes")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const DefaultQueueCapacity = 1024
|
||||
|
||||
type SenderConfig struct {
|
||||
SessionID uint64
|
||||
LocalIP netip.Addr
|
||||
PeerIP netip.Addr
|
||||
PeerPort int
|
||||
QueueCapacity int
|
||||
}
|
||||
|
||||
// Sender performs ordinary UDP socket I/O on a dedicated goroutine.
|
||||
type Sender struct {
|
||||
conn *net.UDPConn
|
||||
queue chan []byte
|
||||
enqueueMu sync.RWMutex
|
||||
closed bool
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
closeOnce sync.Once
|
||||
sessionID uint64
|
||||
sentBytes atomic.Uint64
|
||||
sentPackets atomic.Uint64
|
||||
dropped atomic.Uint64
|
||||
lastError atomic.Value
|
||||
}
|
||||
|
||||
type errorBox struct{ err error }
|
||||
|
||||
func NewSender(parent context.Context, config SenderConfig) (*Sender, error) {
|
||||
if err := validateSenderConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.QueueCapacity <= 0 {
|
||||
config.QueueCapacity = DefaultQueueCapacity
|
||||
}
|
||||
connection, err := net.DialUDP("udp4",
|
||||
&net.UDPAddr{IP: net.IP(config.LocalIP.AsSlice())},
|
||||
&net.UDPAddr{IP: net.IP(config.PeerIP.AsSlice()), Port: config.PeerPort},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bind Session Sender to %s and dial %s:%d: %w", config.LocalIP, config.PeerIP, config.PeerPort, err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
sender := &Sender{
|
||||
conn: connection, queue: make(chan []byte, config.QueueCapacity), cancel: cancel,
|
||||
done: make(chan struct{}), sessionID: config.SessionID,
|
||||
}
|
||||
go sender.run(ctx)
|
||||
return sender, nil
|
||||
}
|
||||
|
||||
func validateSenderConfig(config SenderConfig) error {
|
||||
if config.SessionID == 0 || !config.LocalIP.Is4() || !config.PeerIP.Is4() || config.PeerPort < 1 || config.PeerPort > 65535 {
|
||||
return errors.New("Sender requires SessionID, IPv4 local/peer addresses, and valid peer port")
|
||||
}
|
||||
if config.LocalIP.IsUnspecified() || config.PeerIP.IsUnspecified() || config.LocalIP.IsMulticast() || config.PeerIP.IsMulticast() || config.LocalIP == config.PeerIP {
|
||||
return errors.New("Sender local/peer addresses must be distinct unicast addresses")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enqueue never blocks PacketMux. packet ownership transfers to Sender on true.
|
||||
func (s *Sender) Enqueue(packet []byte) bool {
|
||||
s.enqueueMu.RLock()
|
||||
defer s.enqueueMu.RUnlock()
|
||||
if s.closed {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case s.queue <- packet:
|
||||
return true
|
||||
default:
|
||||
s.dropped.Add(1)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sender) Close() error {
|
||||
s.closeOnce.Do(func() {
|
||||
s.enqueueMu.Lock()
|
||||
s.closed = true
|
||||
s.enqueueMu.Unlock()
|
||||
s.cancel()
|
||||
_ = s.conn.Close()
|
||||
<-s.done
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sender) Counters() (bytes, packets, dropped uint64) {
|
||||
return s.sentBytes.Load(), s.sentPackets.Load(), s.dropped.Load()
|
||||
}
|
||||
|
||||
func (s *Sender) Err() error {
|
||||
value := s.lastError.Load()
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value.(*errorBox).err
|
||||
}
|
||||
|
||||
func (s *Sender) run(ctx context.Context) {
|
||||
defer close(s.done)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case packet := <-s.queue:
|
||||
datagram, err := EncodeDatagram(s.sessionID, packet)
|
||||
if err == nil {
|
||||
var written int
|
||||
written, err = s.conn.Write(datagram)
|
||||
if err == nil && written != len(datagram) {
|
||||
err = errors.New("short Session UDP write")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
s.lastError.Store(&errorBox{err: err})
|
||||
s.dropped.Add(1)
|
||||
continue
|
||||
}
|
||||
s.sentBytes.Add(uint64(len(packet)))
|
||||
s.sentPackets.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSenderRejectsEnqueueAfterClose(t *testing.T) {
|
||||
sender, err := NewSender(context.Background(), SenderConfig{
|
||||
SessionID: 55,
|
||||
LocalIP: netip.MustParseAddr("127.0.0.1"),
|
||||
PeerIP: netip.MustParseAddr("127.0.0.2"),
|
||||
PeerPort: 6200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sender.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sender.Enqueue([]byte{1, 2, 3}) {
|
||||
t.Fatal("closed Sender accepted a packet with no running consumer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSenderConfigRejectsInvalidAddressRoles(t *testing.T) {
|
||||
valid := SenderConfig{SessionID: 1, LocalIP: netip.MustParseAddr("127.0.0.1"), PeerIP: netip.MustParseAddr("127.0.0.2"), PeerPort: 6200}
|
||||
if err := validateSenderConfig(valid); err != nil {
|
||||
t.Fatalf("valid Sender config error = %v", err)
|
||||
}
|
||||
invalid := []SenderConfig{
|
||||
{SessionID: 1, LocalIP: netip.IPv4Unspecified(), PeerIP: valid.PeerIP, PeerPort: 6200},
|
||||
{SessionID: 1, LocalIP: valid.LocalIP, PeerIP: netip.MustParseAddr("224.0.0.1"), PeerPort: 6200},
|
||||
{SessionID: 1, LocalIP: valid.LocalIP, PeerIP: valid.LocalIP, PeerPort: 6200},
|
||||
}
|
||||
for _, current := range invalid {
|
||||
if err := validateSenderConfig(current); err == nil {
|
||||
t.Errorf("invalid Sender config accepted: %+v", current)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"remlink/internal/protocol"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownSession = errors.New("unknown or inactive Session")
|
||||
ErrOuterSourceMismatch = errors.New("Session outer source does not match peer Overlay IP")
|
||||
ErrInnerIPv4Invalid = errors.New("Session payload is not one complete IPv4 packet")
|
||||
ErrInnerSourceMismatch = errors.New("Session inner source is not allowed")
|
||||
ErrInnerTargetMismatch = errors.New("Session inner destination is not allowed")
|
||||
)
|
||||
|
||||
// ValidateDatagram verifies framing, Active Session identity, outer source,
|
||||
// IPv4 total length, and direction-specific inner addresses.
|
||||
func ValidateDatagram(data []byte, outerSource netip.Addr, registry *Registry) (uint64, []byte, error) {
|
||||
if registry == nil {
|
||||
return 0, nil, ErrUnknownSession
|
||||
}
|
||||
header, packet, err := protocol.DecodeIPv4Session(data)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
binding, found := registry.Lookup(header.SessionID)
|
||||
if !found || !binding.Active {
|
||||
return 0, nil, ErrUnknownSession
|
||||
}
|
||||
if outerSource.Unmap() != binding.PeerOverlayIP.Unmap() {
|
||||
return 0, nil, ErrOuterSourceMismatch
|
||||
}
|
||||
source, destination, err := ipv4Addresses(packet)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
switch binding.Direction {
|
||||
case EngineerToSite:
|
||||
if source != binding.EngineerOverlayIP {
|
||||
return 0, nil, fmt.Errorf("%w: got %s want %s", ErrInnerSourceMismatch, source, binding.EngineerOverlayIP)
|
||||
}
|
||||
if !containedBy(destination, binding.RemoteCIDRs) {
|
||||
return 0, nil, fmt.Errorf("%w: %s is outside Remote CIDRs", ErrInnerTargetMismatch, destination)
|
||||
}
|
||||
case SiteToEngineer:
|
||||
if !containedBy(source, binding.RemoteCIDRs) {
|
||||
return 0, nil, fmt.Errorf("%w: %s is outside Remote CIDRs", ErrInnerSourceMismatch, source)
|
||||
}
|
||||
if destination != binding.EngineerOverlayIP {
|
||||
return 0, nil, fmt.Errorf("%w: got %s want %s", ErrInnerTargetMismatch, destination, binding.EngineerOverlayIP)
|
||||
}
|
||||
default:
|
||||
return 0, nil, ErrUnknownSession
|
||||
}
|
||||
return header.SessionID, packet, nil
|
||||
}
|
||||
|
||||
func ipv4Addresses(packet []byte) (source, destination netip.Addr, err error) {
|
||||
if len(packet) < 20 || packet[0]>>4 != 4 {
|
||||
return netip.Addr{}, netip.Addr{}, ErrInnerIPv4Invalid
|
||||
}
|
||||
headerLength := int(packet[0]&0x0F) * 4
|
||||
totalLength := int(packet[2])<<8 | int(packet[3])
|
||||
if headerLength < 20 || headerLength > len(packet) || totalLength != len(packet) || totalLength < headerLength {
|
||||
return netip.Addr{}, netip.Addr{}, ErrInnerIPv4Invalid
|
||||
}
|
||||
source = netip.AddrFrom4([4]byte{packet[12], packet[13], packet[14], packet[15]})
|
||||
destination = netip.AddrFrom4([4]byte{packet[16], packet[17], packet[18], packet[19]})
|
||||
return source, destination, nil
|
||||
}
|
||||
|
||||
func containedBy(address netip.Addr, prefixes []netip.Prefix) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if prefix.Contains(address) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package subnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidateDatagramBothDirections(t *testing.T) {
|
||||
remoteCIDR := netip.MustParsePrefix("192.168.13.0/24")
|
||||
engineerIP := netip.MustParseAddr("10.88.0.2")
|
||||
siteIP := netip.MustParseAddr("10.88.0.3")
|
||||
|
||||
siteRegistry := NewRegistry()
|
||||
if err := siteRegistry.Upsert(SessionBinding{
|
||||
SessionID: 7, PeerOverlayIP: engineerIP, EngineerOverlayIP: engineerIP,
|
||||
RemoteCIDRs: []netip.Prefix{remoteCIDR}, Direction: EngineerToSite, Active: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
requestDatagram, _ := EncodeDatagram(7, testIPv4("10.88.0.2", "192.168.13.10"))
|
||||
if _, _, err := ValidateDatagram(requestDatagram, engineerIP, siteRegistry); err != nil {
|
||||
t.Fatalf("Engineer-to-Site validation: %v", err)
|
||||
}
|
||||
|
||||
engineerRegistry := NewRegistry()
|
||||
if err := engineerRegistry.Upsert(SessionBinding{
|
||||
SessionID: 7, PeerOverlayIP: siteIP, EngineerOverlayIP: engineerIP,
|
||||
RemoteCIDRs: []netip.Prefix{remoteCIDR}, Direction: SiteToEngineer, Active: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
replyDatagram, _ := EncodeDatagram(7, testIPv4("192.168.13.10", "10.88.0.2"))
|
||||
if _, _, err := ValidateDatagram(replyDatagram, siteIP, engineerRegistry); err != nil {
|
||||
t.Fatalf("Site-to-Engineer validation: %v", err)
|
||||
}
|
||||
if _, _, err := ValidateDatagram(replyDatagram, netip.MustParseAddr("10.88.0.4"), engineerRegistry); !errors.Is(err, ErrOuterSourceMismatch) {
|
||||
t.Fatalf("wrong outer source error = %v", err)
|
||||
}
|
||||
wrongDatagram, _ := EncodeDatagram(7, testIPv4("192.168.14.10", "10.88.0.2"))
|
||||
if _, _, err := ValidateDatagram(wrongDatagram, siteIP, engineerRegistry); !errors.Is(err, ErrInnerSourceMismatch) {
|
||||
t.Fatalf("wrong inner source error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSenderListenerRoundTripOverOrdinaryUDP(t *testing.T) {
|
||||
listenerIP := netip.MustParseAddr("127.0.0.1")
|
||||
senderIP := netip.MustParseAddr("127.0.0.2")
|
||||
engineerIP := netip.MustParseAddr("10.88.0.2")
|
||||
registry := NewRegistry()
|
||||
if err := registry.Upsert(SessionBinding{
|
||||
SessionID: 42, PeerOverlayIP: senderIP, EngineerOverlayIP: engineerIP,
|
||||
RemoteCIDRs: []netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")},
|
||||
Direction: EngineerToSite, Active: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
received := make(chan []byte, 1)
|
||||
listener, err := NewListener(listenerIP, 0, registry, func(_ context.Context, sessionID uint64, packet []byte) error {
|
||||
if sessionID != 42 {
|
||||
return errors.New("unexpected SessionID")
|
||||
}
|
||||
received <- packet
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- listener.Run(ctx) }()
|
||||
sender, err := NewSender(ctx, SenderConfig{
|
||||
SessionID: 42, LocalIP: senderIP, PeerIP: listenerIP, PeerPort: listener.Port(), QueueCapacity: 4,
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
t.Fatal(err)
|
||||
}
|
||||
packet := testIPv4("10.88.0.2", "192.168.13.10")
|
||||
if !sender.Enqueue(append([]byte(nil), packet...)) {
|
||||
t.Fatal("Sender queue unexpectedly full")
|
||||
}
|
||||
select {
|
||||
case got := <-received:
|
||||
if string(got) != string(packet) {
|
||||
t.Fatal("received packet differs")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for Session UDP")
|
||||
}
|
||||
_ = sender.Close()
|
||||
cancel()
|
||||
<-done
|
||||
accepted, rejected := listener.Counters()
|
||||
if accepted != 1 || rejected != 0 {
|
||||
t.Fatalf("listener counters accepted=%d rejected=%d", accepted, rejected)
|
||||
}
|
||||
}
|
||||
|
||||
func testIPv4(sourceText, destinationText string) []byte {
|
||||
packet := make([]byte, 20)
|
||||
packet[0] = 0x45
|
||||
packet[3] = byte(len(packet))
|
||||
source := netip.MustParseAddr(sourceText).As4()
|
||||
destination := netip.MustParseAddr(destinationText).As4()
|
||||
copy(packet[12:16], source[:])
|
||||
copy(packet[16:20], destination[:])
|
||||
return packet
|
||||
}
|
||||
Reference in New Issue
Block a user