初版功能完成
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled

This commit is contained in:
qsc
2026-08-29 13:12:17 +08:00
commit 142e5dc7d6
217 changed files with 21313 additions and 0 deletions
+9
View File
@@ -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)
}
+115
View File
@@ -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()
}
}
+75
View File
@@ -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
}
+71
View File
@@ -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
}
+138
View File
@@ -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)
}
}
}
+42
View File
@@ -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)
}
}
}
+82
View File
@@ -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
}
+111
View File
@@ -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
}