初版功能完成
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
// Package subnetgateway defines the Site userspace subnet backend boundary.
|
||||
package subnetgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type SessionConfig struct {
|
||||
SessionID uint64
|
||||
EngineerOverlayIP netip.Addr
|
||||
RemoteCIDRs []netip.Prefix
|
||||
}
|
||||
|
||||
type SubnetGateway interface {
|
||||
Prepare(context.Context, SessionConfig) error
|
||||
InjectIPv4(context.Context, uint64, []byte) error
|
||||
CloseSession(context.Context, uint64) error
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
// Package netstack implements the Site SubnetGateway with pinned gVisor netstack.
|
||||
package netstack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
xipv4 "golang.org/x/net/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
|
||||
"remlink/internal/subnetgateway"
|
||||
"remlink/internal/subnetgateway/pingrelay"
|
||||
"remlink/internal/subnetgateway/tcprelay"
|
||||
"remlink/internal/subnetgateway/udprelay"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTCPFlowLimit = 2048
|
||||
DefaultUDPFlowLimit = 4096
|
||||
DefaultUDPIdleTimeout = 60 * time.Second
|
||||
defaultQueueSize = 1024
|
||||
defaultMTU = 1280
|
||||
nicID tcpip.NICID = 1
|
||||
)
|
||||
|
||||
type Dialer interface {
|
||||
DialContext(context.Context, string, string) (net.Conn, error)
|
||||
}
|
||||
|
||||
type EgressHandler func(context.Context, uint64, []byte) error
|
||||
|
||||
type EchoProber interface {
|
||||
Echo(context.Context, netip.Addr, int, int, []byte) error
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
MTU int
|
||||
TCPFlowLimit int
|
||||
UDPFlowLimit int
|
||||
UDPIdleTimeout time.Duration
|
||||
Dialer Dialer
|
||||
Egress EgressHandler
|
||||
PingProber EchoProber
|
||||
}
|
||||
|
||||
type flowProtocol uint8
|
||||
|
||||
const (
|
||||
flowTCP flowProtocol = iota + 1
|
||||
flowUDP
|
||||
)
|
||||
|
||||
type flowKey struct {
|
||||
SessionID uint64
|
||||
Protocol flowProtocol
|
||||
EngineerIP netip.Addr
|
||||
EngineerPort uint16
|
||||
TargetIP netip.Addr
|
||||
TargetPort uint16
|
||||
}
|
||||
|
||||
type sessionState struct {
|
||||
config subnetgateway.SessionConfig
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Backend is one process-wide IPv4 stack shared by all Site Sessions.
|
||||
type Backend struct {
|
||||
mu sync.RWMutex
|
||||
stack *stack.Stack
|
||||
endpoint *channel.Endpoint
|
||||
dialer Dialer
|
||||
egress EgressHandler
|
||||
ping EchoProber
|
||||
udpIdle time.Duration
|
||||
sessions map[uint64]*sessionState
|
||||
byEngineer map[netip.Addr]uint64
|
||||
flows map[flowKey]context.CancelFunc
|
||||
tcpSlots chan struct{}
|
||||
udpSlots chan struct{}
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
closeOnce sync.Once
|
||||
egressError atomic.Value
|
||||
}
|
||||
|
||||
type errorBox struct{ err error }
|
||||
|
||||
func New(config Config) (*Backend, error) {
|
||||
if config.MTU == 0 {
|
||||
config.MTU = defaultMTU
|
||||
}
|
||||
if config.MTU < 576 || config.MTU > 65535 {
|
||||
return nil, errors.New("netstack MTU must be between 576 and 65535")
|
||||
}
|
||||
if config.TCPFlowLimit <= 0 {
|
||||
config.TCPFlowLimit = DefaultTCPFlowLimit
|
||||
}
|
||||
if config.UDPFlowLimit <= 0 {
|
||||
config.UDPFlowLimit = DefaultUDPFlowLimit
|
||||
}
|
||||
if config.UDPIdleTimeout <= 0 {
|
||||
config.UDPIdleTimeout = DefaultUDPIdleTimeout
|
||||
}
|
||||
if config.Dialer == nil {
|
||||
config.Dialer = &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
}
|
||||
if config.Egress == nil {
|
||||
return nil, errors.New("netstack Egress handler is required")
|
||||
}
|
||||
if config.PingProber == nil {
|
||||
config.PingProber = pingrelay.Relay{}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
backend := &Backend{
|
||||
dialer: config.Dialer, egress: config.Egress, ping: config.PingProber, udpIdle: config.UDPIdleTimeout,
|
||||
sessions: make(map[uint64]*sessionState), byEngineer: make(map[netip.Addr]uint64),
|
||||
flows: make(map[flowKey]context.CancelFunc), tcpSlots: make(chan struct{}, config.TCPFlowLimit),
|
||||
udpSlots: make(chan struct{}, config.UDPFlowLimit), ctx: ctx, cancel: cancel,
|
||||
}
|
||||
backend.stack = stack.New(stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol},
|
||||
})
|
||||
backend.endpoint = channel.New(defaultQueueSize, uint32(config.MTU), "")
|
||||
if err := tcpipError("create netstack NIC", backend.stack.CreateNIC(nicID, backend.endpoint)); err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
if err := tcpipError("enable netstack promiscuous mode", backend.stack.SetPromiscuousMode(nicID, true)); err != nil {
|
||||
backend.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := tcpipError("enable netstack spoofing", backend.stack.SetSpoofing(nicID, true)); err != nil {
|
||||
backend.Close()
|
||||
return nil, err
|
||||
}
|
||||
backend.stack.SetRouteTable([]tcpip.Route{{Destination: header.IPv4EmptySubnet, NIC: nicID}})
|
||||
tcpForwarder := tcp.NewForwarder(backend.stack, 0, config.TCPFlowLimit, backend.handleTCP)
|
||||
udpForwarder := udp.NewForwarder(backend.stack, func(request *udp.ForwarderRequest) { go backend.handleUDP(request) })
|
||||
backend.stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
|
||||
backend.stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
|
||||
go backend.runEgress()
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func (b *Backend) Prepare(_ context.Context, config subnetgateway.SessionConfig) error {
|
||||
if err := validateSession(config); err != nil {
|
||||
return err
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if existing := b.sessions[config.SessionID]; existing != nil {
|
||||
if existing.config.EngineerOverlayIP != config.EngineerOverlayIP || !samePrefixes(existing.config.RemoteCIDRs, config.RemoteCIDRs) {
|
||||
return errors.New("SessionID is already bound to another configuration")
|
||||
}
|
||||
// An exact retry is idempotent. Never mutate a published session config:
|
||||
// packet injection and flow lookup intentionally read it without holding
|
||||
// the map lock on their host-I/O paths.
|
||||
return nil
|
||||
}
|
||||
if existingID, exists := b.byEngineer[config.EngineerOverlayIP]; exists && existingID != config.SessionID {
|
||||
return errors.New("Engineer already has a prepared Site Session")
|
||||
}
|
||||
sessionContext, cancel := context.WithCancel(b.ctx)
|
||||
config.RemoteCIDRs = append([]netip.Prefix(nil), config.RemoteCIDRs...)
|
||||
b.sessions[config.SessionID] = &sessionState{config: config, ctx: sessionContext, cancel: cancel}
|
||||
b.byEngineer[config.EngineerOverlayIP] = config.SessionID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) InjectIPv4(ctx context.Context, sessionID uint64, packet []byte) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
session := b.sessionByID(sessionID)
|
||||
if session == nil {
|
||||
return errors.New("inject into unknown Session")
|
||||
}
|
||||
source, destination, err := rawIPv4Addresses(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if source != session.config.EngineerOverlayIP || !contains(session.config.RemoteCIDRs, destination) {
|
||||
return errors.New("injected IPv4 addresses do not match Session")
|
||||
}
|
||||
if packet[9] == uint8(header.ICMPv4ProtocolNumber) {
|
||||
return b.handleICMPEcho(session, packet, source, destination)
|
||||
}
|
||||
payload := append([]byte(nil), packet...)
|
||||
packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(payload)})
|
||||
b.endpoint.InjectInbound(ipv4.ProtocolNumber, packetBuffer)
|
||||
packetBuffer.DecRef()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) handleICMPEcho(session *sessionState, packet []byte, source, destination netip.Addr) error {
|
||||
headerLength := int(packet[0]&0x0F) * 4
|
||||
if len(packet) < headerLength+header.ICMPv4MinimumSize {
|
||||
return errors.New("ICMPv4 packet is too short")
|
||||
}
|
||||
message, err := icmp.ParseMessage(1, packet[headerLength:])
|
||||
if err != nil || message.Type != xipv4.ICMPTypeEcho || message.Code != 0 {
|
||||
return errors.New("only ICMPv4 Echo Request is supported")
|
||||
}
|
||||
echo, ok := message.Body.(*icmp.Echo)
|
||||
if !ok {
|
||||
return errors.New("ICMPv4 Echo body is invalid")
|
||||
}
|
||||
data := append([]byte(nil), echo.Data...)
|
||||
go func() {
|
||||
if err := b.ping.Echo(session.ctx, destination, echo.ID, echo.Seq, data); err != nil {
|
||||
return
|
||||
}
|
||||
replyMessage := icmp.Message{
|
||||
Type: xipv4.ICMPTypeEchoReply, Code: 0,
|
||||
Body: &icmp.Echo{ID: echo.ID, Seq: echo.Seq, Data: data},
|
||||
}
|
||||
encoded, err := replyMessage.Marshal(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
reply := buildIPv4Packet(destination, source, uint16(packet[4])<<8|uint16(packet[5]), uint8(header.ICMPv4ProtocolNumber), encoded)
|
||||
if err := b.egress(session.ctx, session.config.SessionID, reply); err != nil {
|
||||
b.egressError.Store(&errorBox{err: err})
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) CloseSession(_ context.Context, sessionID uint64) error {
|
||||
b.mu.Lock()
|
||||
session := b.sessions[sessionID]
|
||||
if session == nil {
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
delete(b.sessions, sessionID)
|
||||
delete(b.byEngineer, session.config.EngineerOverlayIP)
|
||||
session.cancel()
|
||||
for key, cancel := range b.flows {
|
||||
if key.SessionID == sessionID {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) Close() error {
|
||||
b.closeOnce.Do(func() {
|
||||
b.cancel()
|
||||
b.mu.Lock()
|
||||
for _, session := range b.sessions {
|
||||
session.cancel()
|
||||
}
|
||||
b.sessions = make(map[uint64]*sessionState)
|
||||
b.byEngineer = make(map[netip.Addr]uint64)
|
||||
b.mu.Unlock()
|
||||
if b.endpoint != nil {
|
||||
b.endpoint.Close()
|
||||
}
|
||||
if b.stack != nil {
|
||||
b.stack.Close()
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backend) Err() error {
|
||||
value := b.egressError.Load()
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value.(*errorBox).err
|
||||
}
|
||||
|
||||
func (b *Backend) FlowCounts() (tcpCount, udpCount int) {
|
||||
return len(b.tcpSlots), len(b.udpSlots)
|
||||
}
|
||||
|
||||
func (b *Backend) handleTCP(request *tcp.ForwarderRequest) {
|
||||
id := request.ID()
|
||||
key, session, ok := b.flowFromID(flowTCP, id)
|
||||
if !ok {
|
||||
request.Complete(true)
|
||||
return
|
||||
}
|
||||
flowContext, finish, ok := b.beginFlow(key, session, b.tcpSlots)
|
||||
if !ok {
|
||||
request.Complete(true)
|
||||
return
|
||||
}
|
||||
defer finish()
|
||||
hostConnection, err := b.dialer.DialContext(flowContext, "tcp4", targetAddress(key.TargetIP, key.TargetPort))
|
||||
if err != nil {
|
||||
request.Complete(true)
|
||||
return
|
||||
}
|
||||
var queue waiter.Queue
|
||||
endpoint, endpointErr := request.CreateEndpoint(&queue)
|
||||
if endpointErr != nil {
|
||||
request.Complete(true)
|
||||
_ = hostConnection.Close()
|
||||
return
|
||||
}
|
||||
request.Complete(false)
|
||||
engineerConnection := gonet.NewTCPConn(&queue, endpoint)
|
||||
_ = tcprelay.Relay(flowContext, engineerConnection, hostConnection)
|
||||
}
|
||||
|
||||
func (b *Backend) handleUDP(request *udp.ForwarderRequest) {
|
||||
id := request.ID()
|
||||
var queue waiter.Queue
|
||||
endpoint, endpointErr := request.CreateEndpoint(&queue)
|
||||
if endpointErr != nil {
|
||||
return
|
||||
}
|
||||
engineerConnection := gonet.NewUDPConn(&queue, endpoint)
|
||||
key, session, ok := b.flowFromID(flowUDP, id)
|
||||
if !ok {
|
||||
_ = engineerConnection.Close()
|
||||
return
|
||||
}
|
||||
flowContext, finish, ok := b.beginFlow(key, session, b.udpSlots)
|
||||
if !ok {
|
||||
_ = engineerConnection.Close()
|
||||
return
|
||||
}
|
||||
defer finish()
|
||||
hostConnection, err := b.dialer.DialContext(flowContext, "udp4", targetAddress(key.TargetIP, key.TargetPort))
|
||||
if err != nil {
|
||||
_ = engineerConnection.Close()
|
||||
return
|
||||
}
|
||||
_ = udprelay.Relay(flowContext, engineerConnection, hostConnection, b.udpIdle)
|
||||
}
|
||||
|
||||
func (b *Backend) runEgress() {
|
||||
for {
|
||||
packetBuffer := b.endpoint.ReadContext(b.ctx)
|
||||
if packetBuffer == nil {
|
||||
return
|
||||
}
|
||||
view := packetBuffer.ToView()
|
||||
packet := append([]byte(nil), view.AsSlice()...)
|
||||
view.Release()
|
||||
packetBuffer.DecRef()
|
||||
source, destination, err := rawIPv4Addresses(packet)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
session := b.sessionByEngineer(destination)
|
||||
if session == nil || !contains(session.config.RemoteCIDRs, source) {
|
||||
continue
|
||||
}
|
||||
if err := b.egress(session.ctx, session.config.SessionID, packet); err != nil {
|
||||
b.egressError.Store(&errorBox{err: err})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Backend) flowFromID(protocol flowProtocol, id stack.TransportEndpointID) (flowKey, *sessionState, bool) {
|
||||
engineerIP, ok := tcpipAddress(id.RemoteAddress)
|
||||
if !ok {
|
||||
return flowKey{}, nil, false
|
||||
}
|
||||
targetIP, ok := tcpipAddress(id.LocalAddress)
|
||||
if !ok {
|
||||
return flowKey{}, nil, false
|
||||
}
|
||||
session := b.sessionByEngineer(engineerIP)
|
||||
if session == nil || !contains(session.config.RemoteCIDRs, targetIP) {
|
||||
return flowKey{}, nil, false
|
||||
}
|
||||
return flowKey{
|
||||
SessionID: session.config.SessionID, Protocol: protocol,
|
||||
EngineerIP: engineerIP, EngineerPort: id.RemotePort, TargetIP: targetIP, TargetPort: id.LocalPort,
|
||||
}, session, true
|
||||
}
|
||||
|
||||
func (b *Backend) beginFlow(key flowKey, session *sessionState, slots chan struct{}) (context.Context, func(), bool) {
|
||||
select {
|
||||
case slots <- struct{}{}:
|
||||
default:
|
||||
return nil, nil, false
|
||||
}
|
||||
b.mu.Lock()
|
||||
if b.sessions[key.SessionID] != session {
|
||||
b.mu.Unlock()
|
||||
<-slots
|
||||
return nil, nil, false
|
||||
}
|
||||
if _, duplicate := b.flows[key]; duplicate {
|
||||
b.mu.Unlock()
|
||||
<-slots
|
||||
return nil, nil, false
|
||||
}
|
||||
ctx, cancel := context.WithCancel(session.ctx)
|
||||
b.flows[key] = cancel
|
||||
b.mu.Unlock()
|
||||
var once sync.Once
|
||||
finish := func() {
|
||||
once.Do(func() {
|
||||
cancel()
|
||||
b.mu.Lock()
|
||||
delete(b.flows, key)
|
||||
b.mu.Unlock()
|
||||
<-slots
|
||||
})
|
||||
}
|
||||
return ctx, finish, true
|
||||
}
|
||||
|
||||
func (b *Backend) sessionByID(sessionID uint64) *sessionState {
|
||||
b.mu.RLock()
|
||||
session := b.sessions[sessionID]
|
||||
b.mu.RUnlock()
|
||||
return session
|
||||
}
|
||||
|
||||
func (b *Backend) sessionByEngineer(address netip.Addr) *sessionState {
|
||||
b.mu.RLock()
|
||||
session := b.sessions[b.byEngineer[address]]
|
||||
b.mu.RUnlock()
|
||||
return session
|
||||
}
|
||||
|
||||
func validateSession(config subnetgateway.SessionConfig) error {
|
||||
if config.SessionID == 0 || !config.EngineerOverlayIP.Is4() || len(config.RemoteCIDRs) == 0 {
|
||||
return errors.New("netstack Session requires ID, Engineer IPv4, and Remote CIDRs")
|
||||
}
|
||||
for _, prefix := range config.RemoteCIDRs {
|
||||
if !prefix.Addr().Is4() || prefix != prefix.Masked() || prefix.Bits() == 0 || prefix.Contains(config.EngineerOverlayIP) {
|
||||
return errors.New("netstack Remote CIDRs must be canonical non-default IPv4 and exclude Engineer")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rawIPv4Addresses(packet []byte) (netip.Addr, netip.Addr, error) {
|
||||
if len(packet) < header.IPv4MinimumSize || packet[0]>>4 != 4 {
|
||||
return netip.Addr{}, netip.Addr{}, errors.New("invalid raw IPv4 packet")
|
||||
}
|
||||
headerLength := int(packet[0]&0x0F) * 4
|
||||
totalLength := int(packet[2])<<8 | int(packet[3])
|
||||
if headerLength < header.IPv4MinimumSize || totalLength != len(packet) || totalLength < headerLength {
|
||||
return netip.Addr{}, netip.Addr{}, errors.New("invalid raw IPv4 lengths")
|
||||
}
|
||||
return netip.AddrFrom4([4]byte{packet[12], packet[13], packet[14], packet[15]}),
|
||||
netip.AddrFrom4([4]byte{packet[16], packet[17], packet[18], packet[19]}), nil
|
||||
}
|
||||
|
||||
func contains(prefixes []netip.Prefix, address netip.Addr) bool {
|
||||
for _, prefix := range prefixes {
|
||||
if prefix.Contains(address) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func samePrefixes(left, right []netip.Prefix) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func tcpipAddress(address tcpip.Address) (netip.Addr, bool) {
|
||||
if address.Len() != 4 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return netip.AddrFrom4(address.As4()), true
|
||||
}
|
||||
|
||||
func targetAddress(address netip.Addr, port uint16) string {
|
||||
return net.JoinHostPort(address.String(), strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
func tcpipError(operation string, err tcpip.Error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s: %s", operation, err.String())
|
||||
}
|
||||
|
||||
func buildIPv4Packet(source, destination netip.Addr, identification uint16, protocol uint8, payload []byte) []byte {
|
||||
packet := make([]byte, header.IPv4MinimumSize+len(payload))
|
||||
packet[0] = 0x45
|
||||
totalLength := len(packet)
|
||||
packet[2], packet[3] = byte(totalLength>>8), byte(totalLength)
|
||||
packet[4], packet[5] = byte(identification>>8), byte(identification)
|
||||
packet[8] = 64
|
||||
packet[9] = protocol
|
||||
sourceBytes := source.As4()
|
||||
destinationBytes := destination.As4()
|
||||
copy(packet[12:16], sourceBytes[:])
|
||||
copy(packet[16:20], destinationBytes[:])
|
||||
checksum := ipv4HeaderChecksum(packet[:header.IPv4MinimumSize])
|
||||
packet[10], packet[11] = byte(checksum>>8), byte(checksum)
|
||||
copy(packet[header.IPv4MinimumSize:], payload)
|
||||
return packet
|
||||
}
|
||||
|
||||
func ipv4HeaderChecksum(headerBytes []byte) uint16 {
|
||||
var sum uint32
|
||||
for index := 0; index+1 < len(headerBytes); index += 2 {
|
||||
sum += uint32(headerBytes[index])<<8 | uint32(headerBytes[index+1])
|
||||
}
|
||||
for sum>>16 != 0 {
|
||||
sum = sum&0xFFFF + sum>>16
|
||||
}
|
||||
return ^uint16(sum)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package netstack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
xipv4 "golang.org/x/net/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/buffer"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
|
||||
"remlink/internal/subnetgateway"
|
||||
)
|
||||
|
||||
func TestTCPForwarderHostDialAndRoundTrip(t *testing.T) {
|
||||
targetIP := localTestIPv4(t)
|
||||
hostListener, err := net.Listen("tcp4", net.JoinHostPort(targetIP.String(), "0"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer hostListener.Close()
|
||||
go func() {
|
||||
connection, err := hostListener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
_, _ = io.Copy(connection, connection)
|
||||
}()
|
||||
|
||||
testNetwork := newTestNetwork(t, targetIP)
|
||||
defer testNetwork.close()
|
||||
port := uint16(hostListener.Addr().(*net.TCPAddr).Port)
|
||||
connection, err := gonet.DialTCP(testNetwork.clientStack, tcpip.FullAddress{
|
||||
Addr: tcpip.AddrFrom4(targetIP.As4()), Port: port,
|
||||
}, ipv4.ProtocolNumber)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
_ = connection.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
want := []byte("gVisor TCP forwarder")
|
||||
if _, err := connection.Write(want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(want))
|
||||
if _, err := io.ReadFull(connection, got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("TCP echo = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPForwarderHostSocketAndRoundTrip(t *testing.T) {
|
||||
targetIP := localTestIPv4(t)
|
||||
hostConnection, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP(targetIP.AsSlice())})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer hostConnection.Close()
|
||||
go func() {
|
||||
buffer := make([]byte, 2048)
|
||||
count, source, err := hostConnection.ReadFromUDP(buffer)
|
||||
if err == nil {
|
||||
_, _ = hostConnection.WriteToUDP(buffer[:count], source)
|
||||
}
|
||||
}()
|
||||
|
||||
testNetwork := newTestNetwork(t, targetIP)
|
||||
defer testNetwork.close()
|
||||
port := uint16(hostConnection.LocalAddr().(*net.UDPAddr).Port)
|
||||
connection, err := gonet.DialUDP(testNetwork.clientStack, nil, &tcpip.FullAddress{
|
||||
Addr: tcpip.AddrFrom4(targetIP.As4()), Port: port,
|
||||
}, ipv4.ProtocolNumber)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
_ = connection.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
want := []byte("gVisor UDP forwarder")
|
||||
if _, err := connection.Write(want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(want))
|
||||
count, err := connection.Read(got)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got[:count]) != string(want) {
|
||||
t.Fatalf("UDP echo = %q, want %q", got[:count], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestICMPEchoRelayPreservesIdentityAndBuildsRawReply(t *testing.T) {
|
||||
replies := make(chan []byte, 1)
|
||||
prober := &fakeEchoProber{}
|
||||
backend, err := New(Config{
|
||||
PingProber: prober,
|
||||
Egress: func(_ context.Context, sessionID uint64, packet []byte) error {
|
||||
if sessionID != 99 {
|
||||
t.Errorf("reply SessionID = %d", sessionID)
|
||||
}
|
||||
replies <- packet
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer backend.Close()
|
||||
engineer := netip.MustParseAddr("10.88.0.2")
|
||||
target := netip.MustParseAddr("192.168.13.10")
|
||||
if err := backend.Prepare(context.Background(), subnetgateway.SessionConfig{
|
||||
SessionID: 99, EngineerOverlayIP: engineer,
|
||||
RemoteCIDRs: []netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
echoRequest, err := (&icmp.Message{
|
||||
Type: xipv4.ICMPTypeEcho, Body: &icmp.Echo{ID: 0x1234, Seq: 77, Data: []byte("ping-data")},
|
||||
}).Marshal(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := buildIPv4Packet(engineer, target, 0xABCD, uint8(header.ICMPv4ProtocolNumber), echoRequest)
|
||||
if err := backend.InjectIPv4(context.Background(), 99, request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case reply := <-replies:
|
||||
source, destination, err := rawIPv4Addresses(reply)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if source != target || destination != engineer || uint16(reply[4])<<8|uint16(reply[5]) != 0xABCD {
|
||||
t.Fatalf("reply addresses/ID source=%s destination=%s id=%x", source, destination, reply[4:6])
|
||||
}
|
||||
message, err := icmp.ParseMessage(1, reply[header.IPv4MinimumSize:])
|
||||
if err != nil || message.Type != xipv4.ICMPTypeEchoReply {
|
||||
t.Fatalf("reply ICMP = %+v, %v", message, err)
|
||||
}
|
||||
echo := message.Body.(*icmp.Echo)
|
||||
if echo.ID != 0x1234 || echo.Seq != 77 || string(echo.Data) != "ping-data" {
|
||||
t.Fatalf("reply Echo = %+v", echo)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for ICMP Echo Reply")
|
||||
}
|
||||
if prober.target != target || prober.id != 0x1234 || prober.sequence != 77 {
|
||||
t.Fatalf("prober call = %+v", prober)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateRemoteCIDRsAreIsolatedBySessionAndEngineer(t *testing.T) {
|
||||
backend, err := New(Config{TCPFlowLimit: 4, UDPFlowLimit: 4, Egress: func(context.Context, uint64, []byte) error { return nil }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer backend.Close()
|
||||
remote := netip.MustParsePrefix("192.168.13.0/24")
|
||||
engineerA := netip.MustParseAddr("10.88.0.10")
|
||||
engineerB := netip.MustParseAddr("10.88.0.11")
|
||||
for _, session := range []subnetgateway.SessionConfig{
|
||||
{SessionID: 101, EngineerOverlayIP: engineerA, RemoteCIDRs: []netip.Prefix{remote}},
|
||||
{SessionID: 202, EngineerOverlayIP: engineerB, RemoteCIDRs: []netip.Prefix{remote}},
|
||||
} {
|
||||
if err := backend.Prepare(context.Background(), session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
target := tcpip.AddrFrom4([4]byte{192, 168, 13, 50})
|
||||
makeID := func(engineer netip.Addr) stack.TransportEndpointID {
|
||||
return stack.TransportEndpointID{
|
||||
RemoteAddress: tcpip.AddrFrom4(engineer.As4()), RemotePort: 41000,
|
||||
LocalAddress: target, LocalPort: 502,
|
||||
}
|
||||
}
|
||||
keyA, sessionA, okA := backend.flowFromID(flowTCP, makeID(engineerA))
|
||||
keyB, sessionB, okB := backend.flowFromID(flowTCP, makeID(engineerB))
|
||||
if !okA || !okB || keyA.SessionID != 101 || keyB.SessionID != 202 || sessionA == sessionB || keyA == keyB {
|
||||
t.Fatalf("flow isolation A=%+v/%p/%v B=%+v/%p/%v", keyA, sessionA, okA, keyB, sessionB, okB)
|
||||
}
|
||||
_, finishA, startedA := backend.beginFlow(keyA, sessionA, backend.tcpSlots)
|
||||
_, finishB, startedB := backend.beginFlow(keyB, sessionB, backend.tcpSlots)
|
||||
if !startedA || !startedB || len(backend.tcpSlots) != 2 {
|
||||
t.Fatalf("parallel flows started A=%v B=%v count=%d", startedA, startedB, len(backend.tcpSlots))
|
||||
}
|
||||
finishA()
|
||||
finishB()
|
||||
if len(backend.tcpSlots) != 0 {
|
||||
t.Fatalf("flow slots leaked: %d", len(backend.tcpSlots))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareRetryIsIdempotentButCannotMutatePublishedSession(t *testing.T) {
|
||||
backend, err := New(Config{Egress: func(context.Context, uint64, []byte) error { return nil }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer backend.Close()
|
||||
original := subnetgateway.SessionConfig{
|
||||
SessionID: 303, EngineerOverlayIP: netip.MustParseAddr("10.88.0.30"),
|
||||
RemoteCIDRs: []netip.Prefix{netip.MustParsePrefix("192.168.13.0/24")},
|
||||
}
|
||||
if err := backend.Prepare(context.Background(), original); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := backend.Prepare(context.Background(), original); err != nil {
|
||||
t.Fatalf("exact PREPARE retry failed: %v", err)
|
||||
}
|
||||
changed := original
|
||||
changed.RemoteCIDRs = []netip.Prefix{netip.MustParsePrefix("192.168.21.0/24")}
|
||||
if err := backend.Prepare(context.Background(), changed); err == nil {
|
||||
t.Fatal("PREPARE retry mutated an already published Session configuration")
|
||||
}
|
||||
if err := backend.InjectIPv4(context.Background(), original.SessionID,
|
||||
buildIPv4Packet(original.EngineerOverlayIP, netip.MustParseAddr("192.168.21.10"), 1, uint8(header.UDPProtocolNumber), []byte{0, 1})); err == nil {
|
||||
t.Fatal("changed Remote CIDR became visible after rejected PREPARE retry")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeEchoProber struct {
|
||||
target netip.Addr
|
||||
id int
|
||||
sequence int
|
||||
}
|
||||
|
||||
func (p *fakeEchoProber) Echo(_ context.Context, target netip.Addr, id, sequence int, _ []byte) error {
|
||||
p.target, p.id, p.sequence = target, id, sequence
|
||||
return nil
|
||||
}
|
||||
|
||||
type testNetwork struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
clientStack *stack.Stack
|
||||
clientEndpoint *channel.Endpoint
|
||||
backend *Backend
|
||||
}
|
||||
|
||||
func newTestNetwork(t *testing.T, targetIP netip.Addr) *testNetwork {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
network := &testNetwork{ctx: ctx, cancel: cancel}
|
||||
network.clientStack = stack.New(stack.Options{
|
||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
|
||||
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol},
|
||||
})
|
||||
network.clientEndpoint = channel.New(1024, 1280, "")
|
||||
if err := network.clientStack.CreateNIC(nicID, network.clientEndpoint); err != nil {
|
||||
cancel()
|
||||
t.Fatal(err.String())
|
||||
}
|
||||
engineer := tcpip.AddrFrom4([4]byte{10, 88, 0, 2})
|
||||
if err := network.clientStack.AddProtocolAddress(nicID, tcpip.ProtocolAddress{
|
||||
Protocol: ipv4.ProtocolNumber, AddressWithPrefix: engineer.WithPrefix(),
|
||||
}, stack.AddressProperties{}); err != nil {
|
||||
cancel()
|
||||
t.Fatal(err.String())
|
||||
}
|
||||
network.clientStack.SetRouteTable([]tcpip.Route{{Destination: header.IPv4EmptySubnet, NIC: nicID}})
|
||||
|
||||
var backend *Backend
|
||||
var err error
|
||||
backend, err = New(Config{
|
||||
TCPFlowLimit: 8, UDPFlowLimit: 8, UDPIdleTimeout: time.Second,
|
||||
Egress: func(_ context.Context, sessionID uint64, packet []byte) error {
|
||||
if sessionID != 7 {
|
||||
t.Errorf("egress SessionID = %d", sessionID)
|
||||
}
|
||||
packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(append([]byte(nil), packet...)),
|
||||
})
|
||||
network.clientEndpoint.InjectInbound(ipv4.ProtocolNumber, packetBuffer)
|
||||
packetBuffer.DecRef()
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
t.Fatal(err)
|
||||
}
|
||||
network.backend = backend
|
||||
if err := backend.Prepare(ctx, subnetgateway.SessionConfig{
|
||||
SessionID: 7, EngineerOverlayIP: netip.MustParseAddr("10.88.0.2"),
|
||||
RemoteCIDRs: []netip.Prefix{netip.PrefixFrom(targetIP, 32)},
|
||||
}); err != nil {
|
||||
network.close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
packetBuffer := network.clientEndpoint.ReadContext(ctx)
|
||||
if packetBuffer == nil {
|
||||
return
|
||||
}
|
||||
view := packetBuffer.ToView()
|
||||
packet := append([]byte(nil), view.AsSlice()...)
|
||||
view.Release()
|
||||
packetBuffer.DecRef()
|
||||
_ = backend.InjectIPv4(ctx, 7, packet)
|
||||
}
|
||||
}()
|
||||
return network
|
||||
}
|
||||
|
||||
func localTestIPv4(t *testing.T) netip.Addr {
|
||||
t.Helper()
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, networkInterface := range interfaces {
|
||||
if networkInterface.Flags&net.FlagUp == 0 || networkInterface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
addresses, err := networkInterface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, raw := range addresses {
|
||||
prefix, err := netip.ParsePrefix(raw.String())
|
||||
if err == nil && prefix.Addr().Is4() && !prefix.Addr().IsLoopback() {
|
||||
return prefix.Addr()
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Skip("no non-loopback IPv4 address available for host relay test")
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
func (n *testNetwork) close() {
|
||||
n.cancel()
|
||||
if n.backend != nil {
|
||||
_ = n.backend.Close()
|
||||
}
|
||||
if n.clientEndpoint != nil {
|
||||
n.clientEndpoint.Close()
|
||||
}
|
||||
if n.clientStack != nil {
|
||||
n.clientStack.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Package pingrelay probes a Site target and preserves Echo ID/Sequence.
|
||||
package pingrelay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 3 * time.Second
|
||||
|
||||
type Relay struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Echo sends one host ICMP Echo and waits for the matching target reply.
|
||||
func (r Relay) Echo(ctx context.Context, target netip.Addr, id, sequence int, data []byte) error {
|
||||
if !target.Is4() || id < 0 || id > 65535 || sequence < 0 || sequence > 65535 {
|
||||
return errors.New("PingRelay target, ID, or Sequence is invalid")
|
||||
}
|
||||
timeout := r.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultTimeout
|
||||
}
|
||||
connection, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("open host ICMP socket: %w", err)
|
||||
}
|
||||
defer connection.Close()
|
||||
deadline := time.Now().Add(timeout)
|
||||
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
||||
deadline = contextDeadline
|
||||
}
|
||||
if err := connection.SetDeadline(deadline); err != nil {
|
||||
return err
|
||||
}
|
||||
stopClose := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = connection.Close()
|
||||
case <-stopClose:
|
||||
}
|
||||
}()
|
||||
defer close(stopClose)
|
||||
message := icmp.Message{
|
||||
Type: ipv4.ICMPTypeEcho, Code: 0,
|
||||
Body: &icmp.Echo{ID: id, Seq: sequence, Data: append([]byte(nil), data...)},
|
||||
}
|
||||
encoded, err := message.Marshal(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal ICMP Echo: %w", err)
|
||||
}
|
||||
if _, err := connection.WriteTo(encoded, &net.IPAddr{IP: net.IP(target.AsSlice())}); err != nil {
|
||||
return fmt.Errorf("send ICMP Echo to %s: %w", target, err)
|
||||
}
|
||||
buffer := make([]byte, 1500)
|
||||
for {
|
||||
count, peer, err := connection.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("wait for ICMP Echo Reply from %s: %w", target, err)
|
||||
}
|
||||
peerIP, ok := netip.AddrFromSlice(peer.(*net.IPAddr).IP)
|
||||
if !ok || peerIP.Unmap() != target {
|
||||
continue
|
||||
}
|
||||
parsed, err := icmp.ParseMessage(1, buffer[:count])
|
||||
if err != nil || parsed.Type != ipv4.ICMPTypeEchoReply {
|
||||
continue
|
||||
}
|
||||
echo, ok := parsed.Body.(*icmp.Echo)
|
||||
if ok && echo.ID == id && echo.Seq == sequence {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Package tcprelay performs protocol-agnostic TCP byte forwarding.
|
||||
package tcprelay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const bufferSize = 32 * 1024
|
||||
|
||||
var bufferPool = sync.Pool{New: func() any { return make([]byte, bufferSize) }}
|
||||
|
||||
// Relay copies both directions until EOF, cancellation, or an I/O failure.
|
||||
func Relay(ctx context.Context, left, right net.Conn) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
results := make(chan error, 2)
|
||||
copyDirection := func(destination, source net.Conn) {
|
||||
buffer := bufferPool.Get().([]byte)
|
||||
_, err := io.CopyBuffer(destination, source, buffer)
|
||||
bufferPool.Put(buffer)
|
||||
if closeWriter, ok := destination.(interface{ CloseWrite() error }); ok {
|
||||
_ = closeWriter.CloseWrite()
|
||||
}
|
||||
results <- err
|
||||
}
|
||||
go copyDirection(left, right)
|
||||
go copyDirection(right, left)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = left.Close()
|
||||
_ = right.Close()
|
||||
}()
|
||||
first := <-results
|
||||
cancel()
|
||||
_ = left.Close()
|
||||
_ = right.Close()
|
||||
second := <-results
|
||||
if first != nil && !errors.Is(first, net.ErrClosed) {
|
||||
return first
|
||||
}
|
||||
if second != nil && !errors.Is(second, net.ErrClosed) {
|
||||
return second
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Package udprelay preserves UDP datagram boundaries across a host socket flow.
|
||||
package udprelay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Relay copies connected UDP datagrams until idle timeout or cancellation.
|
||||
func Relay(ctx context.Context, left, right net.Conn, idleTimeout time.Duration) error {
|
||||
parent := ctx
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
defer cancel()
|
||||
if idleTimeout <= 0 {
|
||||
idleTimeout = 60 * time.Second
|
||||
}
|
||||
var lastActivity atomic.Int64
|
||||
lastActivity.Store(time.Now().UnixNano())
|
||||
results := make(chan error, 2)
|
||||
copyDatagrams := func(destination, source net.Conn) {
|
||||
buffer := make([]byte, 65535)
|
||||
for {
|
||||
deadline := time.Now().Add(min(idleTimeout/2, time.Second))
|
||||
_ = source.SetReadDeadline(deadline)
|
||||
count, err := source.Read(buffer)
|
||||
if err != nil {
|
||||
if timeout, ok := err.(net.Error); ok && timeout.Timeout() {
|
||||
last := time.Unix(0, lastActivity.Load())
|
||||
if time.Since(last) < idleTimeout {
|
||||
continue
|
||||
}
|
||||
}
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
if _, err := destination.Write(buffer[:count]); err != nil {
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
lastActivity.Store(time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
go copyDatagrams(left, right)
|
||||
go copyDatagrams(right, left)
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = left.Close()
|
||||
_ = right.Close()
|
||||
}()
|
||||
err := <-results
|
||||
cancel()
|
||||
_ = left.Close()
|
||||
_ = right.Close()
|
||||
<-results
|
||||
if parent.Err() != nil {
|
||||
return parent.Err()
|
||||
}
|
||||
if timeout, ok := err.(net.Error); ok && timeout.Timeout() {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package udprelay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRelayIdleTimeoutIsNormalFlowCompletion(t *testing.T) {
|
||||
leftRelay, leftPeer := net.Pipe()
|
||||
rightRelay, rightPeer := net.Pipe()
|
||||
defer leftPeer.Close()
|
||||
defer rightPeer.Close()
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- Relay(context.Background(), leftRelay, rightRelay, 20*time.Millisecond) }()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("idle Relay returned %v, want nil", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("idle Relay did not reclaim the flow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelayReturnsParentCancellation(t *testing.T) {
|
||||
leftRelay, leftPeer := net.Pipe()
|
||||
rightRelay, rightPeer := net.Pipe()
|
||||
defer leftPeer.Close()
|
||||
defer rightPeer.Close()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- Relay(ctx, leftRelay, rightRelay, time.Hour) }()
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != context.Canceled {
|
||||
t.Fatalf("Relay cancellation = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("canceled Relay did not stop")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user