diff --git a/vnet/conn.go b/vnet/conn.go index 9c92d11..7aad14c 100644 --- a/vnet/conn.go +++ b/vnet/conn.go @@ -181,7 +181,7 @@ loop: Op: "read", Net: c.locAddr.Network(), Addr: c.locAddr, - Err: newTimeoutError("i/o timeout"), + Err: errIOTimeout, } } } diff --git a/vnet/errors.go b/vnet/errors.go index 2dc3862..4d32cfa 100644 --- a/vnet/errors.go +++ b/vnet/errors.go @@ -7,11 +7,7 @@ type timeoutError struct { msg string } -func newTimeoutError(msg string) error { - return &timeoutError{ - msg: msg, - } -} +var errIOTimeout = &timeoutError{msg: "i/o timeout"} //nolint:gochecknoglobals func (e *timeoutError) Error() string { return e.msg diff --git a/vnet/nat.go b/vnet/nat.go index 4a2fb7b..9d2744f 100644 --- a/vnet/nat.go +++ b/vnet/nat.go @@ -14,7 +14,7 @@ import ( ) var ( - errNATRequiresMapping = errors.New("1:1 NAT requires at least one mapping") + errNATRequiresMapping = errors.New("1:1 NAT requires more than one mapping") errMismatchLengthIP = errors.New("length mismatch between mappedIPs and localIPs") errTranslationNotSupported = errors.New("translation is not supported for this protocol") errNoAssociatedLocalAddress = errors.New("no associated local address") diff --git a/vnet/net.go b/vnet/net.go index 78ea0af..5050860 100644 --- a/vnet/net.go +++ b/vnet/net.go @@ -14,6 +14,7 @@ import ( "strings" "sync" + "github.com/pion/logging" "github.com/pion/transport/v4" ) @@ -55,16 +56,38 @@ func newMACAddress() net.HardwareAddr { // Net represents a local network stack equivalent to a set of layers from NIC // up to the transport (UDP / TCP) layer. type Net struct { - interfaces []*transport.Interface // read-only - staticIPs []net.IP // read-only - router *Router // read-only - udpConns *udpConnMap // read-only - mutex sync.RWMutex + interfaces []*transport.Interface // read-only + staticIPs []net.IP // read-only + router *Router // read-only + udpConns *udpConnMap // read-only + tcpListeners *tcpListenerMap // read-only + tcpConns *tcpConnMap // read-only + mutex sync.RWMutex + log logging.LeveledLogger } // Compile-time assertion. var _ transport.Net = &Net{} +// Compile-time assertion: *Net satisfies listenerObserver. +var _ listenerObserver = &Net{} + +func (v *Net) insertTCPConn(conn *TCPConn) error { + return v.tcpConns.insert(conn) +} + +func (v *Net) deleteTCPConn(conn *TCPConn) error { + return v.tcpConns.deleteConn(conn) +} + +func (v *Net) deleteTCPListener(addr net.Addr) error { + return v.tcpListeners.delete(addr) +} + +func (v *Net) getLog() logging.LeveledLogger { + return v.log +} + func (v *Net) _getInterfaces() ([]*transport.Interface, error) { if len(v.interfaces) == 0 { return nil, errNoInterface @@ -210,14 +233,57 @@ func (v *Net) RemoveAddress(ifName string, ip net.IP) error { return nil } -func (v *Net) onInboundChunk(c Chunk) { - v.mutex.Lock() - defer v.mutex.Unlock() +func (v *Net) onInboundChunk(chunk Chunk) { + switch chunk.Network() { + case udp: + v.mutex.Lock() + conn, ok := v.udpConns.find(chunk.DestinationAddr()) + v.mutex.Unlock() + if ok { + conn.onInboundChunk(chunk) + } + + return + + case tcp: + tcpChunk, ok := chunk.(*chunkTCP) + if !ok { + return + } + + // Lookups must be protected, but handlers may re-enter Net via write(). + v.mutex.Lock() + conn, connOK := v.tcpConns.findByChunk(tcpChunk) + if connOK { + v.mutex.Unlock() + conn.onInboundChunk(tcpChunk) - if c.Network() == udp { - if conn, ok := v.udpConns.find(c.DestinationAddr()); ok { - conn.onInboundChunk(c) + return + } + + // New connection attempt (SYN) + if tcpChunk.flags&tcpSYN != 0 && tcpChunk.flags&tcpACK == 0 { + dstAddr := tcpChunk.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + l, ok := v.tcpListeners.find(dstAddr) + v.mutex.Unlock() + if ok { + l.onInboundSYN(tcpChunk) + + return + } + } else { + v.mutex.Unlock() } + + // No listener/conn for this tuple; send RST back. + dstAddr := tcpChunk.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + srcAddr := tcpChunk.SourceAddr().(*net.TCPAddr) //nolint:forcetypeassert + _ = v.write(newChunkTCP(dstAddr, srcAddr, tcpRST)) + + return + + default: + return } } @@ -250,7 +316,7 @@ func (v *Net) _dialUDP(network string, locAddr, remAddr *net.UDPAddr) (transport if locAddr.Port == 0 { // choose randomly from the range between 5000 and 5999 - port, err := v.assignPort(locAddr.IP, 5000, 5999) + port, err := v.assignUDPPort(locAddr.IP, 5000, 5999) if err != nil { return nil, &net.OpError{ Op: "listen", @@ -313,20 +379,38 @@ func (v *Net) DialUDP(network string, locAddr, remAddr *net.UDPAddr) (transport. // Dial connects to the address on the named network. func (v *Net) Dial(network string, address string) (net.Conn, error) { - v.mutex.Lock() - defer v.mutex.Unlock() + switch network { + case udp, udp4, udp6: + remAddr, err := v.ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } - remAddr, err := v.ResolveUDPAddr(network, address) - if err != nil { - return nil, err - } + v.mutex.Lock() + srcIP := v.determineSourceIP(nil, remAddr.IP) + v.mutex.Unlock() - // Determine source address - srcIP := v.determineSourceIP(nil, remAddr.IP) + locAddr := &net.UDPAddr{IP: srcIP, Port: 0} - locAddr := &net.UDPAddr{IP: srcIP, Port: 0} + return v.DialUDP(network, locAddr, remAddr) - return v._dialUDP(network, locAddr, remAddr) + case tcp, tcp4, tcp6: + remAddr, err := v.ResolveTCPAddr(network, address) + if err != nil { + return nil, err + } + + v.mutex.Lock() + srcIP := v.determineSourceIP(nil, remAddr.IP) + v.mutex.Unlock() + + locAddr := &net.TCPAddr{IP: srcIP, Port: 0} + + return v.DialTCP(network, locAddr, remAddr) + + default: + return nil, fmt.Errorf("%w %s", errUnknownNetwork, network) + } } // ResolveIPAddr returns an address of IP end point. @@ -438,13 +522,13 @@ func (v *Net) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) { return nil, errInvalidPortNumber } - udpAddr := &net.TCPAddr{ + tcpAddr := &net.TCPAddr{ IP: ipAddr.IP, Zone: ipAddr.Zone, Port: port, } - return udpAddr, nil + return tcpAddr, nil } func udpZeroIP(network string) net.IP { @@ -456,7 +540,8 @@ func udpZeroIP(network string) net.IP { } func (v *Net) write(chunk Chunk) error { - if chunk.Network() == udp { //nolint:nestif + switch chunk.Network() { + case udp: if udp, ok := chunk.(*chunkUDP); ok { if chunk.getDestinationIP().IsLoopback() { if conn, ok := v.udpConns.find(udp.DestinationAddr()); ok { @@ -468,6 +553,20 @@ func (v *Net) write(chunk Chunk) error { } else { return errUnexpectedTypeSwitchFailure } + + case tcp: + if tcp, ok := chunk.(*chunkTCP); ok { + if chunk.getDestinationIP().IsLoopback() { + v.onInboundChunk(tcp) + + return nil + } + } else { + return errUnexpectedTypeSwitchFailure + } + + default: + return fmt.Errorf("%w: %s", errUnexpectedNetwork, chunk.Network()) } if v.router == nil { @@ -480,9 +579,16 @@ func (v *Net) write(chunk Chunk) error { } func (v *Net) onClosed(addr net.Addr) { - if addr.Network() == udp { + switch addr.Network() { + case udp: //nolint:errcheck v.udpConns.delete(addr) // #nosec + case tcp: + pair := addr.(*tcpConnCloseAddr) //nolint:forcetypeassert + //nolint:errcheck + v.tcpConns.deleteByAddr(pair.laddr, pair.raddr) // #nosec + default: + // do nothing } } @@ -622,7 +728,7 @@ func (v *Net) allocateLocalAddr(ip net.IP, port int) error { } // caller must hold the mutex. -func (v *Net) assignPort(ip net.IP, start, end int) (int, error) { +func (v *Net) assignUDPPort(ip net.IP, start, end int) (int, error) { // choose randomly from the range between start and end (inclusive) if end < start { return -1, errEndPortLessThanStart @@ -642,6 +748,56 @@ func (v *Net) assignPort(ip net.IP, start, end int) (int, error) { return -1, errPortSpaceExhausted } +// caller must hold the mutex. +func (v *Net) assignTCPListenerPort(ip net.IP, start, end int) (int, error) { + if end < start { + return -1, errEndPortLessThanStart + } + + space := end + 1 - start + offset := rand.Intn(space) //nolint:gosec + for i := range space { + port := ((offset + i) % space) + start + addr := &net.TCPAddr{IP: ip, Port: port} + if _, ok := v.tcpListeners.find(addr); ok { + continue + } + // Also avoid if any outbound TCP connection is already using this local port. + if v.tcpConns.portInUse(ip, port) { + continue + } + + return port, nil + } + + return -1, errPortSpaceExhausted +} + +// caller must hold the mutex. +func (v *Net) assignTCPPort(ip net.IP, start, end int) (int, error) { + if end < start { + return -1, errEndPortLessThanStart + } + + space := end + 1 - start + offset := rand.Intn(space) //nolint:gosec + for i := range space { + port := ((offset + i) % space) + start + // For simplicity, don't reuse a local port if any listener exists on that port. + if _, ok := v.tcpListeners.find(&net.TCPAddr{IP: ip, Port: port}); ok { + continue + } + // Also avoid if any connection is already using this local port. + if v.tcpConns.portInUse(ip, port) { + continue + } + + return port, nil + } + + return -1, errPortSpaceExhausted +} + func (v *Net) getStaticIPs() []net.IP { return v.staticIPs } @@ -652,6 +808,9 @@ type NetConfig struct { // If no static IP address is given, the router will automatically assign // an IP address. StaticIPs []string + + // LoggerFactory is a factory for creating loggers. If nil, a default logger factory is used. + LoggerFactory logging.LoggerFactory } // NewNet creates an instance of a virtual network. @@ -691,21 +850,143 @@ func NewNet(config *NetConfig) (*Net, error) { } } + loggerFactory := config.LoggerFactory + if loggerFactory == nil { + loggerFactory = logging.NewDefaultLoggerFactory() + } + return &Net{ - interfaces: []*transport.Interface{lo0, eth0}, - staticIPs: staticIPs, - udpConns: newUDPConnMap(), + interfaces: []*transport.Interface{lo0, eth0}, + staticIPs: staticIPs, + udpConns: newUDPConnMap(), + tcpListeners: newTCPListenerMap(), + tcpConns: newTCPConnMap(), + log: loggerFactory.NewLogger("vnet"), }, nil } // DialTCP acts like Dial for TCP networks. -func (v *Net) DialTCP(string, *net.TCPAddr, *net.TCPAddr) (transport.TCPConn, error) { - return nil, transport.ErrNotSupported +func (v *Net) DialTCP(network string, locAddr, remAddr *net.TCPAddr) (transport.TCPConn, error) { //nolint:cyclop + // tcp6 is not yet supported; only "tcp" and "tcp4" are accepted. + if network != tcp && network != tcp4 { + return nil, fmt.Errorf("%w: %s", errUnexpectedNetwork, network) + } + + if remAddr == nil { + return nil, &net.OpError{Op: "dial", Net: network, Addr: nil, Err: errNoRemAddr} + } + if remAddr.IP == nil { + remAddr.IP = net.IPv4zero + } + + if locAddr == nil { + locAddr = &net.TCPAddr{IP: net.IPv4zero, Port: 0} + } else if locAddr.IP == nil { + locAddr.IP = net.IPv4zero + } + + v.mutex.Lock() + + // determine local IP if unspecified + locAddr.IP = v.determineSourceIP(locAddr.IP, remAddr.IP) + if locAddr.IP == nil { + v.mutex.Unlock() + + return nil, errLocAddr + } + + // validate address. do we have that address? + if !v.hasIPAddr(locAddr.IP) { + v.mutex.Unlock() + + return nil, &net.OpError{Op: "dial", Net: network, Addr: locAddr, Err: fmt.Errorf("bind: %w", + errCantAssignRequestedAddr)} + } + + if locAddr.Port == 0 { + // Ephemeral TCP port range: 5000–5999 (1000 ports), shared with UDP. + // Table-driven tests that open many parallel connections may exhaust + // this range and receive errPortSpaceExhausted. + port, err := v.assignTCPPort(locAddr.IP, 5000, 5999) + if err != nil { + v.mutex.Unlock() + + return nil, &net.OpError{Op: "dial", Net: network, Addr: locAddr, Err: err} + } + locAddr.Port = port + } + + conn, err := newTCPConn(locAddr, remAddr, v, v.log, nil) + if err != nil { + v.mutex.Unlock() + + return nil, err + } + + if err := v.tcpConns.insert(conn); err != nil { + v.mutex.Unlock() + + return nil, &net.OpError{Op: "dial", Net: network, Addr: locAddr, Err: fmt.Errorf("bind: %w", err)} + } + v.mutex.Unlock() + + if err := conn.startClientHandshake(); err != nil { + _ = v.tcpConns.deleteConn(conn) + + return nil, err + } + + if err := conn.waitEstablished(); err != nil { + _ = conn.Close() + + return nil, err + } + + return conn, nil } // ListenTCP acts like Listen for TCP networks. -func (v *Net) ListenTCP(string, *net.TCPAddr) (transport.TCPListener, error) { - return nil, transport.ErrNotSupported +func (v *Net) ListenTCP(network string, locAddr *net.TCPAddr) (transport.TCPListener, error) { //nolint:cyclop + v.mutex.Lock() + defer v.mutex.Unlock() + + // tcp6 is not yet supported; only "tcp" and "tcp4" are accepted. + if network != tcp && network != tcp4 { + return nil, fmt.Errorf("%w: %s", errUnexpectedNetwork, network) + } + + if locAddr == nil { + locAddr = &net.TCPAddr{IP: net.IPv4zero, Port: 0} + } else if locAddr.IP == nil { + locAddr.IP = net.IPv4zero + } + + if !v.hasIPAddr(locAddr.IP) { + return nil, &net.OpError{Op: "listen", Net: network, Addr: locAddr, Err: fmt.Errorf("bind: %w", + errCantAssignRequestedAddr)} + } + + if locAddr.Port == 0 { + // Ephemeral TCP listener port range: 5000–5999 (1000 ports), shared with UDP. + // Table-driven tests that spin up many parallel listeners may exhaust + // this range and receive errPortSpaceExhausted. + port, err := v.assignTCPListenerPort(locAddr.IP, 5000, 5999) + if err != nil { + return nil, &net.OpError{Op: "listen", Net: network, Addr: locAddr, Err: err} + } + locAddr.Port = port + } + + l, err := newTCPListener(locAddr, v) + if err != nil { + return nil, err + } + + if err := v.tcpListeners.insert(l); err != nil { + return nil, &net.OpError{Op: "listen", Net: network, Addr: locAddr, Err: fmt.Errorf("bind: %w", err)} + } + + return l, nil } // CreateDialer creates an instance of vnet.Dialer. diff --git a/vnet/net_test.go b/vnet/net_test.go index 4d6c5cb..c513296 100644 --- a/vnet/net_test.go +++ b/vnet/net_test.go @@ -6,6 +6,7 @@ package vnet import ( "context" "fmt" + "io" "net" "testing" "time" @@ -20,6 +21,8 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx loggerFactory := logging.NewDefaultLoggerFactory() log := logging.NewDefaultLoggerFactory().NewLogger("test") + const pingMsg = "PING!" + t.Run("tnet.Interfaces", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) if !assert.NoError(t, err, "should succeed") { @@ -186,7 +189,7 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Equal(t, "::1", ips[0].String(), "should match") }) - t.Run("assignPort()", func(t *testing.T) { + t.Run("assignUDPPort()", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) if !assert.NoError(t, err, "should succeed") { return @@ -211,11 +214,11 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx }) // attempt to assign port with start > end should fail - _, err = nw.assignPort(net.ParseIP(addr), 3000, 2999) + _, err = nw.assignUDPPort(net.ParseIP(addr), 3000, 2999) assert.NotNil(t, err, "should fail") for i := range space { - port, err2 := nw.assignPort(net.ParseIP(addr), start, end) + port, err2 := nw.assignUDPPort(net.ParseIP(addr), start, end) assert.NoError(t, err2, "should succeed") log.Debugf("[%d] got port: %d", i, port) @@ -231,10 +234,50 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Equal(t, space, nw.udpConns.size(), "should match") // attempt to assign again should fail - _, err = nw.assignPort(net.ParseIP(addr), start, end) + _, err = nw.assignUDPPort(net.ParseIP(addr), start, end) assert.NotNil(t, err, "should fail") }) + t.Run("Port allocation independent for UDP/TCP", func(t *testing.T) { + nw, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + + ip := net.ParseIP("127.0.0.1") + + // If TCP is already using a port, UDP allocation should still be able to pick it. + tcpL, err := nw.ListenTCP(tcp, &net.TCPAddr{IP: ip, Port: 5010}) + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = tcpL.Close() }() + + udpPort, err := nw.assignUDPPort(ip, 5010, 5010) + assert.NoError(t, err, "should succeed") + assert.Equal(t, 5010, udpPort, "should match") + + assert.NoError(t, tcpL.Close(), "should succeed") + + // If UDP is already using a port, TCP allocation should still be able to pick it. + udpC, err := nw.ListenPacket(udp, "127.0.0.1:5011") + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = udpC.Close() }() + + tcpPort, err := nw.assignTCPPort(ip, 5011, 5011) + assert.NoError(t, err, "should succeed") + assert.Equal(t, 5011, tcpPort, "should match") + + // And explicit binds should also be independent. + tcpL2, err := nw.ListenTCP(tcp, &net.TCPAddr{IP: ip, Port: 5011}) + assert.NoError(t, err, "should succeed") + if tcpL2 != nil { + _ = tcpL2.Close() + } + }) + t.Run("determineSourceIP()", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) if !assert.NoError(t, err, "should succeed") { @@ -361,6 +404,20 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Equal(t, 1234, udpAddr.Port, "should match") }) + t.Run("ResolveTCPAddr", func(t *testing.T) { + nw, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + + tcpAddr, err := nw.ResolveTCPAddr(tcp, "localhost:1234") + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Equal(t, "127.0.0.1", tcpAddr.IP.String(), "should match") + assert.Equal(t, 1234, tcpAddr.Port, "should match") + }) + t.Run("UDPLoopback", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) if !assert.NoError(t, err, "should succeed") { @@ -370,7 +427,7 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx conn, err := nw.ListenPacket(udp, "127.0.0.1:0") assert.NoError(t, err, "should succeed") laddr := conn.LocalAddr() - msg := "PING!" + msg := pingMsg n, err := conn.WriteTo([]byte(msg), laddr) assert.NoError(t, err, "should succeed") assert.Equal(t, len(msg), n, "should match") @@ -387,6 +444,101 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Empty(t, nw.udpConns.size(), "should match") }) + t.Run("TCPLoopback", func(t *testing.T) { + nw, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + + listener, err := nw.ListenTCP(tcp, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = listener.Close() }() + _ = listener.SetDeadline(time.Now().Add(2 * time.Second)) + + acceptedCh := make(chan net.Conn, 1) + go func() { + c, err2 := listener.Accept() + if err2 != nil { + close(acceptedCh) + + return + } + acceptedCh <- c + }() + + client, err := nw.Dial(tcp, listener.Addr().String()) + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = client.Close() }() + + var server net.Conn + select { + case server = <-acceptedCh: + case <-time.After(2 * time.Second): + assert.Fail(t, "accept timed out") + + return + } + if !assert.NotNil(t, server, "should accept") { + return + } + defer func() { _ = server.Close() }() + + _ = client.SetDeadline(time.Now().Add(2 * time.Second)) + _ = server.SetDeadline(time.Now().Add(2 * time.Second)) + + msg := pingMsg + + serverReadDone := make(chan error, 1) + go func() { + buf := make([]byte, len(msg)) + _, err2 := io.ReadFull(server, buf) + if err2 == nil && string(buf) != msg { + err2 = fmt.Errorf("unexpected payload: %q", string(buf)) // nolint:err113 + } + serverReadDone <- err2 + }() + + n, err := client.Write([]byte(msg)) + assert.NoError(t, err, "should succeed") + assert.Equal(t, len(msg), n, "should match") + + select { + case err2 := <-serverReadDone: + assert.NoError(t, err2, "should succeed") + case <-time.After(2 * time.Second): + assert.Fail(t, "server read timed out") + + return + } + + clientReadDone := make(chan error, 1) + go func() { + buf := make([]byte, len(msg)) + _, err2 := io.ReadFull(client, buf) + if err2 == nil && string(buf) != msg { + err2 = fmt.Errorf("unexpected payload: %q", string(buf)) // nolint:err113 + } + clientReadDone <- err2 + }() + + n, err = server.Write([]byte(msg)) + assert.NoError(t, err, "should succeed") + assert.Equal(t, len(msg), n, "should match") + + select { + case err2 := <-clientReadDone: + assert.NoError(t, err2, "should succeed") + case <-time.After(2 * time.Second): + assert.Fail(t, "client read timed out") + + return + } + }) + t.Run("ListenPacket random port", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) if !assert.NoError(t, err, "should succeed") { @@ -485,6 +637,57 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Empty(t, nw.udpConns.size(), "should match") }) + t.Run("Dial (TCP) lo0", func(t *testing.T) { + nw, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + + listener, err := nw.ListenTCP(tcp, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}) + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = listener.Close() }() + _ = listener.SetDeadline(time.Now().Add(2 * time.Second)) + + acceptedCh := make(chan net.Conn, 1) + go func() { + c, err2 := listener.Accept() + if err2 != nil { + close(acceptedCh) + + return + } + acceptedCh <- c + }() + + conn, err := nw.Dial(tcp, "127.0.0.1:1234") + assert.NoError(t, err, "should succeed") + defer func() { _ = conn.Close() }() + + var server net.Conn + select { + case server = <-acceptedCh: + case <-time.After(2 * time.Second): + assert.Fail(t, "accept timed out") + + return + } + if server != nil { + _ = server.Close() + } + + laddr := conn.LocalAddr() + log.Debugf("laddr: %s", laddr.String()) + + raddr := conn.RemoteAddr() + log.Debugf("raddr: %s", raddr.String()) + + assert.Equal(t, "127.0.0.1", laddr.(*net.TCPAddr).IP.String(), "should match") //nolint:forcetypeassert + assert.True(t, laddr.(*net.TCPAddr).Port != 0, "should match") //nolint:forcetypeassert + assert.Equal(t, "127.0.0.1:1234", raddr.String(), "should match") + }) + t.Run("Dial (UDP) eth0", func(t *testing.T) { wan, err := NewRouter(&RouterConfig{ CIDR: "1.2.3.0/24", @@ -516,6 +719,80 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Empty(t, nw.udpConns.size(), "should match") }) + t.Run("Dial (TCP) eth0", func(t *testing.T) { + wan, err := NewRouter(&RouterConfig{ + CIDR: "1.2.3.0/24", + LoggerFactory: loggerFactory, + }) + if !assert.NoError(t, err, "should succeed") { + return + } + + net1, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.NoError(t, wan.AddNet(net1), "should succeed") + ip1, err := getIPAddr(net1) + assert.NoError(t, err, "should succeed") + + net2, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.NoError(t, wan.AddNet(net2), "should succeed") + ip2, err := getIPAddr(net2) + assert.NoError(t, err, "should succeed") + + assert.NoError(t, wan.Start(), "should succeed") + defer func() { _ = wan.Stop() }() + + listener, err := net2.ListenTCP(tcp, &net.TCPAddr{IP: net.ParseIP(ip2), Port: 0}) + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = listener.Close() }() + _ = listener.SetDeadline(time.Now().Add(2 * time.Second)) + + acceptedCh := make(chan net.Conn, 1) + go func() { + c, err2 := listener.Accept() + if err2 != nil { + close(acceptedCh) + + return + } + acceptedCh <- c + }() + + conn, err := net1.Dial(tcp, listener.Addr().String()) + assert.NoError(t, err, "should succeed") + defer func() { _ = conn.Close() }() + + var server net.Conn + select { + case server = <-acceptedCh: + case <-time.After(2 * time.Second): + assert.Fail(t, "accept timed out") + + return + } + if server != nil { + _ = server.Close() + } + + laddr := conn.LocalAddr() + log.Debugf("laddr: %s", laddr.String()) + + raddr := conn.RemoteAddr() + log.Debugf("raddr: %s", raddr.String()) + + assert.Equal(t, ip1, laddr.(*net.TCPAddr).IP.String(), "should match") //nolint:forcetypeassert + assert.True(t, laddr.(*net.TCPAddr).Port != 0, "should match") //nolint:forcetypeassert + listenerPort := listener.Addr().(*net.TCPAddr).Port // nolint:forcetypeassert + assert.Equal(t, fmt.Sprintf("%s:%d", ip2, listenerPort), raddr.String(), "should match") + }) + t.Run("DialUDP", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) if !assert.NoError(t, err, "should succeed") { @@ -588,7 +865,7 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Equal(t, "::1", localAddr.IP.String(), "should match") assert.Equal(t, packetConn.LocalAddr().String(), conn.RemoteAddr().String(), "should match") - msg := "PING!" + msg := pingMsg n, err := conn.Write([]byte(msg)) assert.NoError(t, err, "should succeed") assert.Equal(t, len(msg), n, "should match") @@ -844,18 +1121,67 @@ func TestNetVirtual(t *testing.T) { //nolint:gocyclo,cyclop,maintidx assert.Empty(t, nw.udpConns.size(), "should match") }) + t.Run("Dialer (TCP)", func(t *testing.T) { + nw, err := NewNet(&NetConfig{}) + if !assert.NoError(t, err, "should succeed") { + return + } + + listener, err := nw.ListenTCP(tcp, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) + if !assert.NoError(t, err, "should succeed") { + return + } + defer func() { _ = listener.Close() }() + _ = listener.SetDeadline(time.Now().Add(2 * time.Second)) + + acceptedCh := make(chan net.Conn, 1) + go func() { + c, err2 := listener.Accept() + if err2 != nil { + close(acceptedCh) + + return + } + acceptedCh <- c + }() + + dialer := nw.CreateDialer(&net.Dialer{ + LocalAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}, + }) + + conn, err := dialer.Dial(tcp, listener.Addr().String()) + assert.NoError(t, err, "should succeed") + defer func() { _ = conn.Close() }() + + var server net.Conn + select { + case server = <-acceptedCh: + case <-time.After(2 * time.Second): + assert.Fail(t, "accept timed out") + + return + } + if server != nil { + _ = server.Close() + } + + laddr := conn.LocalAddr() + assert.Equal(t, "127.0.0.1", laddr.(*net.TCPAddr).IP.String(), "should match") //nolint:forcetypeassert + assert.True(t, laddr.(*net.TCPAddr).Port != 0, "should match") //nolint:forcetypeassert + }) + t.Run("Listen", func(t *testing.T) { nw, err := NewNet(&NetConfig{}) assert.Nil(t, err, "should succeed") listenConfig := nw.CreateListenConfig(&net.ListenConfig{}) - listener, err := listenConfig.Listen(context.Background(), "tcp4", "127.0.0.1:1234") + listener, err := listenConfig.Listen(context.Background(), tcp4, "127.0.0.1:1234") assert.NoError(t, err, "should succeed") laddr := listener.Addr() log.Debugf("laddr: %s", laddr.String()) - conn, err := net.Dial("tcp4", "127.0.0.1:1234") //nolint:noctx + conn, err := net.Dial(tcp4, "127.0.0.1:1234") //nolint:noctx assert.NoError(t, err, "should succeed") raddr := conn.RemoteAddr() diff --git a/vnet/tcp_conn.go b/vnet/tcp_conn.go index fb216ed..5cb8503 100644 --- a/vnet/tcp_conn.go +++ b/vnet/tcp_conn.go @@ -4,44 +4,502 @@ package vnet import ( + "errors" "io" "net" + "sync" "time" + "github.com/pion/logging" "github.com/pion/transport/v4" ) +type tcpState uint8 + +const ( + tcpStateInit tcpState = iota + tcpStateSynSent + tcpStateSynReceived + tcpStateEstablished + tcpStateClosed +) + +var ( + errConnectionReset = errors.New("connection reset") + errConnectionNotEstablished = errors.New("connection not established") +) + // TCPConn implements transport.TCPConn. type TCPConn struct { locAddr *net.TCPAddr remAddr *net.TCPAddr + obs connObserver + + mu sync.Mutex + state tcpState + inboundCh chan tcpSegment + curSeg *tcpSegment + curSegOffset int + readClosed bool // remote has closed + readChClosed bool + writeClosed bool + closed bool + readDeadline time.Time + writeDeadline time.Time + + nextSeq uint32 + pendingAcks map[uint32]chan struct{} + + // client connect flow + establishedCh chan struct{} + establishErr error + + // server side: notify listener once established + onEstablished func(*TCPConn) + + log logging.LeveledLogger } +type tcpSegment struct { + seq uint32 + data []byte +} + +// tcpConnCloseAddr carries both endpoints of a TCP connection. It is passed to +// connObserver.onClosed so that the observer can perform full 4-tuple matching +// when removing the connection from the map, rather than laddr-only matching. +type tcpConnCloseAddr struct { + laddr *net.TCPAddr + raddr *net.TCPAddr +} + +func (a *tcpConnCloseAddr) Network() string { return tcp } +func (a *tcpConnCloseAddr) String() string { return a.laddr.String() + "->" + a.raddr.String() } + +// tcpInboundQueueSize is the depth of the per-connection inbound segment channel. +// vnet has no retransmission, so a dropped segment means permanent data loss for +// that connection. 64 slots give burst-heavy tests and the duplication filter +// enough headroom without consuming significant memory. +const tcpInboundQueueSize = 64 + var _ transport.TCPConn = &TCPConn{} +func newTCPConn( + locAddr, remAddr *net.TCPAddr, obs connObserver, log logging.LeveledLogger, onEstablished func(*TCPConn), +) (*TCPConn, error) { + if obs == nil { + return nil, errObsCannotBeNil + } + + conn := &TCPConn{ + locAddr: locAddr, + remAddr: remAddr, + obs: obs, + log: log, + state: tcpStateInit, + inboundCh: make(chan tcpSegment, tcpInboundQueueSize), + establishedCh: make(chan struct{}), + onEstablished: onEstablished, + pendingAcks: map[uint32]chan struct{}{}, + } + + return conn, nil +} + +func (c *TCPConn) startClientHandshake() error { + c.mu.Lock() + if c.closed { + c.mu.Unlock() + + return errUseClosedNetworkConn + } + if c.locAddr.IP == nil { + c.mu.Unlock() + + return errLocAddr + } + + c.state = tcpStateSynSent + c.mu.Unlock() + + src := &net.TCPAddr{IP: c.locAddr.IP, Port: c.locAddr.Port} + dst := &net.TCPAddr{IP: c.remAddr.IP, Port: c.remAddr.Port} + + syn := newChunkTCP(src, dst, tcpSYN) + + return c.obs.write(syn) +} + +func (c *TCPConn) waitEstablished() error { + <-c.establishedCh + c.mu.Lock() + defer c.mu.Unlock() + + return c.establishErr +} + +func (c *TCPConn) onInboundChunk(chunk *chunkTCP) { // nolint:cyclop,gocognit + c.mu.Lock() + defer c.mu.Unlock() + + if c.closed { + return + } + + // RST aborts connection immediately + if chunk.flags&tcpRST != 0 { + c.establishErr = &net.OpError{Op: "dial", Net: tcp, Addr: c.remAddr, Err: errConnectionReset} + c.closed = true + c.state = tcpStateClosed + c.readClosed = true + c.closeReadChLocked() + c.closePendingAcksLocked() + select { + case <-c.establishedCh: + default: + close(c.establishedCh) + } + + return + } + + // handshake + if c.state == tcpStateSynSent { + if chunk.flags&(tcpSYN|tcpACK) == (tcpSYN | tcpACK) { + c.state = tcpStateEstablished + // reply ACK + src := &net.TCPAddr{IP: c.locAddr.IP, Port: c.locAddr.Port} + dst := &net.TCPAddr{IP: c.remAddr.IP, Port: c.remAddr.Port} + ack := newChunkTCP(src, dst, tcpACK) + go func() { _ = c.obs.write(ack) }() + select { + case <-c.establishedCh: + default: + close(c.establishedCh) + } + + return + } + } + + if c.state == tcpStateSynReceived { + if chunk.flags&tcpACK != 0 && chunk.flags&tcpSYN == 0 { + c.state = tcpStateEstablished + cb := c.onEstablished + if cb != nil { + go cb(c) + } + // Do not return here; the first ACK may also carry data (PSH). + } + } + + if chunk.flags&tcpFIN != 0 { + c.readClosed = true + select { + case <-c.establishedCh: + default: + // if the other side closed before connect completed + c.establishErr = io.EOF + close(c.establishedCh) + } + c.closeReadChLocked() + + return + } + + // Data ACK + // nextSeq is always incremented as nextSeq+1 before use, so assigned + // sequence numbers are always ≥ 1. The ackNum != 0 guard therefore never + // excludes a legitimate ACK in practice (seq 0 is never issued). After a + // uint32 wrap this would incorrectly skip an ACK for seq 0, but that + // requires ~4 billion writes and is unreachable in any real test workload. + if chunk.flags&tcpACK != 0 && chunk.ackNum != 0 { + if ch, ok := c.pendingAcks[chunk.ackNum]; ok { + delete(c.pendingAcks, chunk.ackNum) + close(ch) + } + // ACK may accompany other flags (e.g. PSH), so don't return. + } + + if chunk.flags&tcpPSH != 0 && len(chunk.userData) > 0 { + payload := make([]byte, len(chunk.userData)) + copy(payload, chunk.userData) + if !c.readChClosed { + seg := tcpSegment{seq: chunk.seqNum, data: payload} + select { + case c.inboundCh <- seg: + default: + // Receive queue is full — drop the segment and log so that data loss + // is observable during debugging. + c.log.Debugf("tcp: %s->%s: inbound segment seq=%d dropped (receive queue full)", + c.remAddr, c.locAddr, chunk.seqNum) + } + } + + return + } +} + +func (c *TCPConn) closeReadChLocked() { + if c.readChClosed { + return + } + c.readChClosed = true + close(c.inboundCh) +} + +func (c *TCPConn) closePendingAcksLocked() { + for _, ch := range c.pendingAcks { + close(ch) + } + clear(c.pendingAcks) +} + // Read reads data from the connection. -func (c *TCPConn) Read(b []byte) (int, error) { - return 0, transport.ErrNotSupported +func (c *TCPConn) Read(b []byte) (int, error) { // nolint:gocognit,cyclop + for { + var ack *chunkTCP + c.mu.Lock() + if c.closed { + c.mu.Unlock() + + return 0, &net.OpError{Op: "read", Net: tcp, Addr: c.locAddr, Err: errUseClosedNetworkConn} + } + // Serve current segment if present. + if c.curSeg != nil { + remaining := c.curSeg.data[c.curSegOffset:] + n := copy(b, remaining) + c.curSegOffset += n + if c.curSegOffset >= len(c.curSeg.data) { + // ACK after the segment has been read. + src := &net.TCPAddr{IP: c.locAddr.IP, Port: c.locAddr.Port} + dst := &net.TCPAddr{IP: c.remAddr.IP, Port: c.remAddr.Port} + ack = newChunkTCP(src, dst, tcpACK) + ack.ackNum = c.curSeg.seq + c.curSeg = nil + c.curSegOffset = 0 + } + c.mu.Unlock() + if ack != nil { + _ = c.obs.write(ack) + } + + return n, nil + } + + inboundCh := c.inboundCh + deadline := c.readDeadline + c.mu.Unlock() + + // Wait for the next segment. + if !deadline.IsZero() { // nolint:nestif + until := time.Until(deadline) + if until <= 0 { + return 0, &net.OpError{Op: "read", Net: tcp, Addr: c.locAddr, Err: errIOTimeout} + } + timer := time.NewTimer(until) + select { + case seg, ok := <-inboundCh: + if !timer.Stop() { + <-timer.C + } + if !ok { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return 0, &net.OpError{Op: "read", Net: tcp, Addr: c.locAddr, Err: errUseClosedNetworkConn} + } + + return 0, io.EOF + } + c.mu.Lock() + c.curSeg = &seg + c.curSegOffset = 0 + c.mu.Unlock() + + continue + case <-timer.C: + return 0, &net.OpError{Op: "read", Net: tcp, Addr: c.locAddr, Err: errIOTimeout} + } + } + + seg, ok := <-inboundCh + if !ok { + c.mu.Lock() + defer c.mu.Unlock() + if c.closed { + return 0, &net.OpError{Op: "read", Net: tcp, Addr: c.locAddr, Err: errUseClosedNetworkConn} + } + + return 0, io.EOF + } + c.mu.Lock() + c.curSeg = &seg + c.curSegOffset = 0 + c.mu.Unlock() + } } // Write writes data to the connection. -func (c *TCPConn) Write(b []byte) (int, error) { - return 0, transport.ErrNotSupported +func (c *TCPConn) Write(b []byte) (int, error) { // nolint:cyclop + c.mu.Lock() + if c.closed { + c.mu.Unlock() + + return 0, &net.OpError{Op: "write", Net: tcp, Addr: c.locAddr, Err: errUseClosedNetworkConn} + } + if c.writeClosed { + c.mu.Unlock() + + return 0, io.ErrClosedPipe + } + if c.state != tcpStateEstablished { + c.mu.Unlock() + + return 0, errConnectionNotEstablished + } + + seq := c.nextSeq + 1 + c.nextSeq = seq + ackCh := make(chan struct{}) + c.pendingAcks[seq] = ackCh + deadline := c.writeDeadline + + payload := make([]byte, len(b)) + copy(payload, b) + src := &net.TCPAddr{IP: c.locAddr.IP, Port: c.locAddr.Port} + dst := &net.TCPAddr{IP: c.remAddr.IP, Port: c.remAddr.Port} + chunk := newChunkTCP(src, dst, tcpPSH|tcpACK) + chunk.userData = payload + chunk.seqNum = seq + c.mu.Unlock() + + if err := c.obs.write(chunk); err != nil { + // obs.write failed — the chunk was never sent, so no ACK will arrive. + // Re-acquire c.mu and remove the pending entry before closing the channel + // to unblock any concurrent waiter. The ok-check guards against a + // concurrent closePendingAcksLocked() having already removed the entry; + // all accesses to pendingAcks are serialized through c.mu so no + // double-close is possible. + c.mu.Lock() + if ch, ok := c.pendingAcks[seq]; ok { + delete(c.pendingAcks, seq) + close(ch) + } + c.mu.Unlock() + + return 0, err + } + + if !deadline.IsZero() { + until := time.Until(deadline) + if until <= 0 { + return 0, &net.OpError{Op: "write", Net: tcp, Addr: c.locAddr, Err: errIOTimeout} + } + timer := time.NewTimer(until) + select { + case <-ackCh: + if !timer.Stop() { + <-timer.C + } + c.mu.Lock() + closed := c.closed + c.mu.Unlock() + if closed { + return 0, &net.OpError{Op: "write", Net: tcp, Addr: c.locAddr, Err: errUseClosedNetworkConn} + } + + return len(b), nil + case <-timer.C: + c.mu.Lock() + if ch, ok := c.pendingAcks[seq]; ok { + delete(c.pendingAcks, seq) + close(ch) + } + c.mu.Unlock() + + return 0, &net.OpError{Op: "write", Net: tcp, Addr: c.locAddr, Err: errIOTimeout} + } + } + + <-ackCh + c.mu.Lock() + closed := c.closed + c.mu.Unlock() + if closed { + return 0, &net.OpError{Op: "write", Net: tcp, Addr: c.locAddr, Err: errUseClosedNetworkConn} + } + + return len(b), nil } // Close closes the connection. func (c *TCPConn) Close() error { - return transport.ErrNotSupported + c.mu.Lock() + if c.closed { + c.mu.Unlock() + + return errAlreadyClosed + } + c.closed = true + c.state = tcpStateClosed + c.readClosed = true + // Capture writeClosed before setting it so we know whether to send the FIN. + // We cannot call CloseWrite() after this point because CloseWrite() guards on + // c.closed (which is now true) and would return immediately without sending + // the FIN segment. + alreadyWriteClosed := c.writeClosed + c.writeClosed = true + src := &net.TCPAddr{IP: c.locAddr.IP, Port: c.locAddr.Port} + dst := &net.TCPAddr{IP: c.remAddr.IP, Port: c.remAddr.Port} + c.closeReadChLocked() + c.closePendingAcksLocked() + c.mu.Unlock() + + // Send FIN so the remote peer's Read returns io.EOF. + if !alreadyWriteClosed { + fin := newChunkTCP(src, dst, tcpFIN|tcpACK) + _ = c.obs.write(fin) + } + c.obs.onClosed(&tcpConnCloseAddr{laddr: c.locAddr, raddr: c.remAddr}) + + return nil } // CloseRead closes the read side of the connection. func (c *TCPConn) CloseRead() error { - return transport.ErrNotSupported + c.mu.Lock() + defer c.mu.Unlock() + c.readClosed = true + c.closeReadChLocked() + + return nil } // CloseWrite closes the write side of the connection. func (c *TCPConn) CloseWrite() error { - return transport.ErrNotSupported + c.mu.Lock() + if c.closed { + c.mu.Unlock() + + return errUseClosedNetworkConn + } + if c.writeClosed { + c.mu.Unlock() + + return nil + } + c.writeClosed = true + src := &net.TCPAddr{IP: c.locAddr.IP, Port: c.locAddr.Port} + dst := &net.TCPAddr{IP: c.remAddr.IP, Port: c.remAddr.Port} + fin := newChunkTCP(src, dst, tcpFIN|tcpACK) + c.mu.Unlock() + + // writeClosed is already true at this point. If write(fin) fails, we do not + // roll it back: a subsequent CloseWrite() call would return nil (no-op) and + // the FIN would be permanently lost. For a vnet simulator this is acceptable + // — write() failures are rare and the connection is effectively broken anyway. + return c.obs.write(fin) } // LocalAddr returns the local network address. @@ -56,22 +514,34 @@ func (c *TCPConn) RemoteAddr() net.Addr { // SetDeadline sets the read and write deadlines associated with the connection. func (c *TCPConn) SetDeadline(t time.Time) error { - return transport.ErrNotSupported + if err := c.SetReadDeadline(t); err != nil { + return err + } + + return c.SetWriteDeadline(t) } // SetReadDeadline sets the deadline for future Read calls. func (c *TCPConn) SetReadDeadline(t time.Time) error { - return transport.ErrNotSupported + c.mu.Lock() + c.readDeadline = t + c.mu.Unlock() + + return nil } // SetWriteDeadline sets the deadline for future Write calls. func (c *TCPConn) SetWriteDeadline(t time.Time) error { - return transport.ErrNotSupported + c.mu.Lock() + c.writeDeadline = t + c.mu.Unlock() + + return nil } // ReadFrom reads data from r and writes it to the connection. func (c *TCPConn) ReadFrom(r io.Reader) (int64, error) { - return 0, transport.ErrNotSupported + return io.Copy(c, r) } // SetLinger sets the behavior of Close method on a connection with pending data. diff --git a/vnet/tcp_conn_test.go b/vnet/tcp_conn_test.go new file mode 100644 index 0000000..41d3d32 --- /dev/null +++ b/vnet/tcp_conn_test.go @@ -0,0 +1,737 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package vnet + +import ( + "bytes" + "errors" + "io" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/pion/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var errFailedToConvertToChunkTCP = errors.New("failed to convert chunk to chunkTCP") + +// tcpTestLogger is a shared logger used by newTCPConn calls in unit tests. +var tcpTestLogger = logging.NewDefaultLoggerFactory().NewLogger("test") //nolint:gochecknoglobals + +func newAckingEchoTCPObserver(connPtr **TCPConn) *dummyObserver { + return &dummyObserver{ + onWrite: func(c Chunk) error { + conn := *connPtr + if conn == nil { + return errors.New("tcp conn is nil") // nolint:err113 + } + + tc, ok := c.(*chunkTCP) + if !ok { + return errFailedToConvertToChunkTCP + } + + // Immediately ACK the sent segment as if the remote read it. + if tc.flags&tcpPSH != 0 && tc.seqNum != 0 { + dstAddr := tc.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + srcAddr := tc.SourceAddr().(*net.TCPAddr) //nolint:forcetypeassert + ack := newChunkTCP(dstAddr, srcAddr, tcpACK) + ack.ackNum = tc.seqNum + conn.onInboundChunk(ack) + } + + // Echo back payload as if it came from the remote. + dstAddr := tc.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + srcAddr := tc.SourceAddr().(*net.TCPAddr) //nolint:forcetypeassert + echo := newChunkTCP(dstAddr, srcAddr, tcpPSH|tcpACK) + echo.userData = make([]byte, len(tc.userData)) + copy(echo.userData, tc.userData) + conn.onInboundChunk(echo) + + return nil + }, + onOnClosed: func(net.Addr) {}, + } +} + +func TestTCPConn(t *testing.T) { //nolint:cyclop,maintidx,gocyclo + log := logging.NewDefaultLoggerFactory().NewLogger("test") + + t.Run("ReadFrom Read", func(t *testing.T) { + var conn *TCPConn + data := []byte("Hello") + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := newAckingEchoTCPObserver(&conn) + + var err error + conn, err = newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + rcvdCh := make(chan struct{}) + doneCh := make(chan struct{}) + + go func() { + buf := make([]byte, 1500) + + for { + n, err2 := conn.Read(buf) + if err2 != nil { + log.Debug("conn closed. exiting the read loop") + + break + } + log.Debug("read data") + assert.Equal(t, len(data), n, "should match") + assert.Equal(t, string(data), string(buf[:n]), "should match") + rcvdCh <- struct{}{} + } + + close(doneCh) + }() + + n, err := conn.ReadFrom(bytes.NewReader(data)) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Equal(t, int64(len(data)), n, "should match") + + loop: + for { + select { + case <-rcvdCh: + log.Debug("closing conn..") + err2 := conn.Close() + assert.Nil(t, err2, "should succeed") + case <-doneCh: + break loop + } + } + }) + + t.Run("Write Read", func(t *testing.T) { + var conn *TCPConn + var err error + data := []byte("Hello") + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := newAckingEchoTCPObserver(&conn) + + conn, err = newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + rcvdCh := make(chan struct{}) + doneCh := make(chan struct{}) + + go func() { + buf := make([]byte, 1500) + + for { + n, err2 := conn.Read(buf) + if err2 != nil { + log.Debug("conn closed. exiting the read loop") + + break + } + log.Debug("read data") + assert.Equal(t, len(data), n, "should match") + assert.Equal(t, string(data), string(buf[:n]), "should match") + rcvdCh <- struct{}{} + } + + close(doneCh) + }() + + var n int + n, err = conn.Write(data) + if !assert.Nil(t, err, "should succeed") { + return + } + assert.Equal(t, len(data), n, "should match") + + loop: + for { + select { + case <-rcvdCh: + log.Debug("closing conn..") + err = conn.Close() + assert.Nil(t, err, "should succeed") + case <-doneCh: + break loop + } + } + }) + + deadlineTest := func(t *testing.T, readOnly bool) { + t.Helper() + + var conn *TCPConn + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := &dummyObserver{ + onWrite: func(Chunk) error { return nil }, + onOnClosed: func(net.Addr) {}, + } + + var err error + conn, err = newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + doneCh := make(chan struct{}) + + if readOnly { + err = conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)) + } else { + err = conn.SetDeadline(time.Now().Add(50 * time.Millisecond)) + } + assert.Nil(t, err, "should succeed") + + go func() { + buf := make([]byte, 1500) + _, err2 := conn.Read(buf) + assert.NotNil(t, err2, "should return error") + var ne *net.OpError + if errors.As(err2, &ne) { + assert.True(t, ne.Timeout(), "should be a timeout") + } else { + assert.True(t, false, "should be an net.OpError") + } + + assert.Nil(t, conn.Close(), "should succeed") + close(doneCh) + }() + + <-doneCh + } + + t.Run("SetReadDeadline", func(t *testing.T) { + deadlineTest(t, true) + }) + + t.Run("SetDeadline", func(t *testing.T) { + deadlineTest(t, false) + }) + + t.Run("SetWriteDeadline", func(t *testing.T) { + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := &dummyObserver{ + onWrite: func(Chunk) error { return nil }, + onOnClosed: func(net.Addr) {}, + } + + conn, err := newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + err = conn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)) + assert.NoError(t, err, "should succeed") + + _, err = conn.Write([]byte("blocked")) + assert.Error(t, err, "should timeout") + var ne *net.OpError + if errors.As(err, &ne) { + assert.True(t, ne.Timeout(), "should be a timeout") + } else { + assert.True(t, false, "should be a net.OpError") + } + + assert.NoError(t, conn.Close(), "should succeed") + }) + + t.Run("Write blocks until peer reads", func(t *testing.T) { + msg := []byte("Hello") + addrA := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + addrB := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + var connA *TCPConn + var connB *TCPConn + + obsA := &dummyObserver{ + onWrite: func(c Chunk) error { + tc, ok := c.(*chunkTCP) + if !ok { + return errFailedToConvertToChunkTCP + } + // Deliver to peer. + connB.onInboundChunk(tc.Clone().(*chunkTCP)) //nolint:forcetypeassert + + return nil + }, + onOnClosed: func(net.Addr) {}, + } + + obsB := &dummyObserver{ + onWrite: func(c Chunk) error { + tc, ok := c.(*chunkTCP) + if !ok { + return errFailedToConvertToChunkTCP + } + // Deliver to peer. + connA.onInboundChunk(tc.Clone().(*chunkTCP)) //nolint:forcetypeassert + + return nil + }, + onOnClosed: func(net.Addr) {}, + } + + var err error + connA, err = newTCPConn(addrA, addrB, obsA, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + connB, err = newTCPConn(addrB, addrA, obsB, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + connA.mu.Lock() + connA.state = tcpStateEstablished + connA.mu.Unlock() + connB.mu.Lock() + connB.state = tcpStateEstablished + connB.mu.Unlock() + + writeDone := make(chan error, 1) + go func() { + _, err2 := connA.Write(msg) + writeDone <- err2 + }() + + // Should still be blocked (no read => no ACK). + select { + case err2 := <-writeDone: + assert.Fail(t, "Write returned before peer read", "%v", err2) + + return + case <-time.After(200 * time.Millisecond): + } + + _ = connB.SetReadDeadline(time.Now().Add(2 * time.Second)) + buf := make([]byte, len(msg)) + _, err = io.ReadFull(connB, buf) + assert.NoError(t, err, "should succeed") + assert.Equal(t, msg, buf, "should match") + + select { + case err2 := <-writeDone: + assert.NoError(t, err2, "should succeed") + case <-time.After(2 * time.Second): + assert.Fail(t, "Write did not unblock after peer read") + + return + } + + assert.NoError(t, connA.Close(), "should succeed") + assert.NoError(t, connB.Close(), "should succeed") + }) + + t.Run("RST", func(t *testing.T) { + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := &dummyObserver{ + onWrite: func(Chunk) error { return nil }, + onOnClosed: func(net.Addr) {}, + } + + conn, err := newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + writeDone := make(chan error, 1) + go func() { + _, err2 := conn.Write([]byte("data")) + writeDone <- err2 + }() + + select { + case err2 := <-writeDone: + assert.Fail(t, "Write returned before RST", "%v", err2) + + return + case <-time.After(100 * time.Millisecond): + } + + rst := newChunkTCP(dstAddr, srcAddr, tcpRST) + conn.onInboundChunk(rst) + + select { + case err2 := <-writeDone: + assert.Error(t, err2, "should error") + var ne *net.OpError + if errors.As(err2, &ne) { + assert.Equal(t, "write", ne.Op, "should match") + assert.Equal(t, errUseClosedNetworkConn, ne.Err, "should match") + } else { + assert.True(t, false, "should be a net.OpError") + } + case <-time.After(2 * time.Second): + assert.Fail(t, "Write did not unblock after RST") + + return + } + + buf := make([]byte, 10) + _, err = conn.Read(buf) + assert.Error(t, err, "should error") + _, err = conn.Write([]byte("x")) + assert.Error(t, err, "should error") + }) + + t.Run("ReadClosed (CloseRead)", func(t *testing.T) { + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + var conn *TCPConn + obs := &dummyObserver{ + onWrite: func(c Chunk) error { + tc, ok := c.(*chunkTCP) + if !ok { + return errFailedToConvertToChunkTCP + } + // ACK writes so Write doesn't block. + if tc.flags&tcpPSH != 0 && tc.seqNum != 0 { + dstAddr := tc.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + srcAddr := tc.SourceAddr().(*net.TCPAddr) //nolint:forcetypeassert + ack := newChunkTCP(dstAddr, srcAddr, tcpACK) + ack.ackNum = tc.seqNum + conn.onInboundChunk(ack) + } + + return nil + }, + onOnClosed: func(net.Addr) {}, + } + + var err error + conn, err = newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + assert.NoError(t, conn.CloseRead(), "should succeed") + + buf := make([]byte, 10) + _, err = conn.Read(buf) + assert.Equal(t, io.EOF, err, "should EOF") + + // Write side still usable until closed. + _, err = conn.Write([]byte("ok")) + assert.NoError(t, err, "should succeed") + assert.NoError(t, conn.Close(), "should succeed") + }) + + t.Run("ReadClosed (remote FIN)", func(t *testing.T) { + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := &dummyObserver{ + onWrite: func(Chunk) error { return nil }, + onOnClosed: func(net.Addr) {}, + } + + conn, err := newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + fin := newChunkTCP(dstAddr, srcAddr, tcpFIN|tcpACK) + conn.onInboundChunk(fin) + + buf := make([]byte, 10) + _, err = conn.Read(buf) + assert.Equal(t, io.EOF, err, "should EOF") + assert.NoError(t, conn.Close(), "should succeed") + }) + + t.Run("WriteClosed (CloseWrite)", func(t *testing.T) { + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := &dummyObserver{ + onWrite: func(Chunk) error { return nil }, + onOnClosed: func(net.Addr) {}, + } + + conn, err := newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + assert.NoError(t, conn.CloseWrite(), "should succeed") + _, err = conn.Write([]byte("nope")) + assert.Equal(t, io.ErrClosedPipe, err, "should match") + assert.NoError(t, conn.Close(), "should succeed") + }) + + t.Run("Inbound during close", func(t *testing.T) { + var nClosed int32 + var conn *TCPConn + srcAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + dstAddr := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + obs := &dummyObserver{ + onWrite: func(Chunk) error { return nil }, + onOnClosed: func(net.Addr) { + atomic.AddInt32(&nClosed, 1) + }, + } + + var err error + conn, err = newTCPConn(srcAddr, dstAddr, obs, tcpTestLogger, nil) + assert.NoError(t, err, "should succeed") + + conn.mu.Lock() + conn.state = tcpStateEstablished + conn.mu.Unlock() + + fin := newChunkTCP(dstAddr, srcAddr, tcpFIN|tcpACK) + psh := newChunkTCP(dstAddr, srcAddr, tcpPSH|tcpACK) + psh.userData = []byte("x") + + for range 1000 { // nolint:staticcheck // (false positive detection) + chDone := make(chan struct{}) + go func() { + time.Sleep(20 * time.Millisecond) + assert.NoError(t, conn.Close()) + close(chDone) + }() + tick := time.NewTicker(10 * time.Millisecond) + defer tick.Stop() + for { + select { + case <-chDone: + assert.Equal(t, int32(1), atomic.LoadInt32(&nClosed), "should invoke onClosed exactly once") + + return + case <-tick.C: + conn.onInboundChunk(psh) + conn.onInboundChunk(fin) + } + } + } + }) + + // Close() must send a FIN so the remote peer's Read returns io.EOF rather than blocking forever. + t.Run("Close sends FIN to peer", func(t *testing.T) { + addrA := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234} + addrB := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5678} + + var connA *TCPConn + var connB *TCPConn + + // obsA forwards outbound chunks from connA directly into connB. + obsA := &dummyObserver{ + onWrite: func(c Chunk) error { + tc, ok := c.(*chunkTCP) + if !ok { + return errFailedToConvertToChunkTCP + } + connB.onInboundChunk(tc.Clone().(*chunkTCP)) //nolint:forcetypeassert + + return nil + }, + onOnClosed: func(net.Addr) {}, + } + + // obsB forwards outbound chunks from connB directly into connA. + obsB := &dummyObserver{ + onWrite: func(c Chunk) error { + tc, ok := c.(*chunkTCP) + if !ok { + return errFailedToConvertToChunkTCP + } + connA.onInboundChunk(tc.Clone().(*chunkTCP)) //nolint:forcetypeassert + + return nil + }, + onOnClosed: func(net.Addr) {}, + } + + var err error + connA, err = newTCPConn(addrA, addrB, obsA, tcpTestLogger, nil) + require.NoError(t, err) + connB, err = newTCPConn(addrB, addrA, obsB, tcpTestLogger, nil) + require.NoError(t, err) + + connA.mu.Lock() + connA.state = tcpStateEstablished + connA.mu.Unlock() + connB.mu.Lock() + connB.state = tcpStateEstablished + connB.mu.Unlock() + + // Close connA — this must deliver a FIN to connB. + require.NoError(t, connA.Close()) + + // connB must observe the FIN as io.EOF on Read, not block forever. + readDone := make(chan error, 1) + go func() { + buf := make([]byte, 1) + _, err2 := connB.Read(buf) + readDone <- err2 + }() + + select { + case err2 := <-readDone: + assert.Equal(t, io.EOF, err2, "peer Read should return io.EOF after Close") + case <-time.After(2 * time.Second): + assert.Fail(t, "peer Read blocked forever — FIN was not delivered by Close") + } + + assert.NoError(t, connB.Close()) + }) +} + +func TestVNetTCPDialListen(t *testing.T) { + loggerFactory := logging.NewDefaultLoggerFactory() + + router, err := NewRouter(&RouterConfig{ + CIDR: "192.0.2.0/24", + LoggerFactory: loggerFactory, + }) + require.NoError(t, err) + require.NoError(t, router.Start()) + defer func() { _ = router.Stop() }() + + serverNet, err := NewNet(&NetConfig{}) + require.NoError(t, err) + clientNet, err := NewNet(&NetConfig{}) + require.NoError(t, err) + + require.NoError(t, router.AddNet(serverNet)) + require.NoError(t, router.AddNet(clientNet)) + + // Bind listener to server's assigned eth0 address so clients can route to it. + eth0, err := serverNet.InterfaceByName("eth0") + require.NoError(t, err) + addrs, err := eth0.Addrs() + require.NoError(t, err) + require.NotEmpty(t, addrs) + serverIP := addrs[0].(*net.IPNet).IP //nolint:forcetypeassert + + ln, err := serverNet.ListenTCP(tcp4, &net.TCPAddr{IP: serverIP, Port: 0}) + require.NoError(t, err) + defer func() { _ = ln.Close() }() + + srvAddr := ln.Addr().(*net.TCPAddr) //nolint:forcetypeassert + + serverDone := make(chan struct{}) + go func() { + defer close(serverDone) + c, err2 := ln.AcceptTCP() + require.NoError(t, err2) + defer func() { _ = c.Close() }() + + buf := make([]byte, 5) + _, err2 = io.ReadFull(c, buf) + require.NoError(t, err2) + require.Equal(t, []byte("hello"), buf) + + _, err2 = c.Write([]byte("world")) + require.NoError(t, err2) + }() + + conn, err := clientNet.DialTCP(tcp4, nil, &net.TCPAddr{IP: srvAddr.IP, Port: srvAddr.Port}) + require.NoError(t, err) + defer func() { _ = conn.Close() }() + + _, err = conn.Write([]byte("hello")) + require.NoError(t, err) + + buf := make([]byte, 5) + _, err = io.ReadFull(conn, buf) + require.NoError(t, err) + require.Equal(t, []byte("world"), buf) + + select { + case <-serverDone: + case <-time.After(2 * time.Second): + require.FailNow(t, "server did not finish") + } +} + +// When the accept queue is full the onEstablished goroutine must not block indefinitely +// (goroutine leak). Instead the connection is closed and the client observes an error +// on its next read. +func TestTCPListenerAcceptQueueFull(t *testing.T) { + loggerFactory := logging.NewDefaultLoggerFactory() + + router, err := NewRouter(&RouterConfig{ + CIDR: "192.0.2.0/24", + LoggerFactory: loggerFactory, + }) + require.NoError(t, err) + require.NoError(t, router.Start()) + defer func() { _ = router.Stop() }() + + serverNet, err := NewNet(&NetConfig{}) + require.NoError(t, err) + clientNet, err := NewNet(&NetConfig{}) + require.NoError(t, err) + + require.NoError(t, router.AddNet(serverNet)) + require.NoError(t, router.AddNet(clientNet)) + + eth0, err := serverNet.InterfaceByName("eth0") + require.NoError(t, err) + addrs, err := eth0.Addrs() + require.NoError(t, err) + require.NotEmpty(t, addrs) + serverIP := addrs[0].(*net.IPNet).IP //nolint:forcetypeassert + + ln, err := serverNet.ListenTCP(tcp4, &net.TCPAddr{IP: serverIP, Port: 0}) + require.NoError(t, err) + defer func() { _ = ln.Close() }() + + srvAddr := ln.Addr().(*net.TCPAddr) //nolint:forcetypeassert + + // Pre-fill the accept queue to capacity so the next completed handshake + // cannot enqueue its connection. + tln, ok := ln.(*TCPListener) + require.True(t, ok, "expected TCPListener") + queueCap := cap(tln.acceptCh) + for range queueCap { + tln.acceptCh <- &TCPConn{} + } + + // Dial one more connection. After the 3-way handshake completes on the + // server, onEstablished sees the full queue and must close the connection + // immediately (non-blocking select default branch) rather than blocking. + conn, err := clientNet.DialTCP(tcp4, nil, &net.TCPAddr{IP: srvAddr.IP, Port: srvAddr.Port}) + require.NoError(t, err) + defer func() { _ = conn.Close() }() + + // The server-side connection was closed (FIN sent), so Read must return an + // error and not block forever. + require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second))) + buf := make([]byte, 1) + _, err = conn.Read(buf) + require.Error(t, err, "Read must return an error — connection should have been rejected by server") +} diff --git a/vnet/tcp_listener.go b/vnet/tcp_listener.go index 21bacac..8bef6ee 100644 --- a/vnet/tcp_listener.go +++ b/vnet/tcp_listener.go @@ -4,32 +4,167 @@ package vnet import ( + "math" "net" + "sync" "time" + "github.com/pion/logging" "github.com/pion/transport/v4" ) +// listenerObserver is the interface that a TCPListener's host network must satisfy. +// It extends connObserver (so accepted connections can re-use the same observer) and +// adds the three map operations the listener needs during the handshake and close. +type listenerObserver interface { + connObserver + insertTCPConn(conn *TCPConn) error + deleteTCPConn(conn *TCPConn) error + deleteTCPListener(addr net.Addr) error + getLog() logging.LeveledLogger +} + // TCPListener implements transport.TCPListener. type TCPListener struct { locAddr *net.TCPAddr + obs listenerObserver + + acceptCh chan *TCPConn + mu sync.Mutex + closed bool + timer *time.Timer } var _ transport.TCPListener = &TCPListener{} +func newTCPListener(locAddr *net.TCPAddr, obs listenerObserver) (*TCPListener, error) { + if obs == nil { + return nil, errObsCannotBeNil + } + + return &TCPListener{ + locAddr: locAddr, + obs: obs, + acceptCh: make(chan *TCPConn, 64), + timer: time.NewTimer(time.Duration(math.MaxInt64)), + }, nil +} + +func (l *TCPListener) onInboundSYN(tcp *chunkTCP) { + l.mu.Lock() + if l.closed { + l.mu.Unlock() + + return + } + l.mu.Unlock() + // NOTE: the lock is released before the SYN-ACK is sent. A concurrent + // Close() can mark l.closed=true in this window, causing the SYN-ACK to be + // sent to a closing listener. This is harmless: the onEstablished callback + // below re-checks l.closed under the mutex and discards the connection if + // the listener has since closed, so no resource leak occurs. + + dst := tcp.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + src := tcp.SourceAddr().(*net.TCPAddr) //nolint:forcetypeassert + + // If listener is on 0.0.0.0, bind accepted conn to the destination IP. + loc := &net.TCPAddr{IP: dst.IP, Port: l.locAddr.Port} + rem := &net.TCPAddr{IP: src.IP, Port: src.Port} + + conn, err := newTCPConn(loc, rem, l.obs, l.obs.getLog(), func(c *TCPConn) { + l.mu.Lock() + defer l.mu.Unlock() + if l.closed { + _ = c.Close() + + return + } + select { + case l.acceptCh <- c: + default: + _ = c.Close() // accept queue full, reject the connection + } + }) + if err != nil { + return + } + + conn.mu.Lock() + conn.state = tcpStateSynReceived + conn.mu.Unlock() + + // Register early so ACK/data finds the conn. + if err := l.obs.insertTCPConn(conn); err != nil { + // Duplicate SYN (e.g. from vnet's duplication filter) — ignore. + return + } + + // Send SYN-ACK + synAck := newChunkTCP(loc, rem, tcpSYN|tcpACK) + if err := l.obs.write(synAck); err != nil { + // Failed to send SYN-ACK — remove the conn so it doesn't leak in tcpConns. + _ = l.obs.deleteTCPConn(conn) + + return + } +} + // Accept waits for and returns the next connection to the listener. func (l *TCPListener) Accept() (net.Conn, error) { - return nil, transport.ErrNotSupported + return l.AcceptTCP() } // AcceptTCP waits for and returns the next TCP connection to the listener. func (l *TCPListener) AcceptTCP() (transport.TCPConn, error) { - return nil, transport.ErrNotSupported + for { + l.mu.Lock() + if l.closed { + l.mu.Unlock() + + return nil, errUseClosedNetworkConn + } + l.mu.Unlock() + + select { + case c, ok := <-l.acceptCh: + if !ok { + return nil, errUseClosedNetworkConn + } + + return c, nil + case <-l.timer.C: + return nil, &net.OpError{Op: "accept", Net: tcp, Addr: l.locAddr, Err: errIOTimeout} + } + } } // Close closes the listener. Any blocked Accept operations will be unblocked and return errors. func (l *TCPListener) Close() error { - return transport.ErrNotSupported + l.mu.Lock() + if l.closed { + l.mu.Unlock() + + return errAlreadyClosed + } + l.closed = true + l.mu.Unlock() + + _ = l.obs.deleteTCPListener(l.locAddr) + close(l.acceptCh) + + // Drain any connections that were fully established and queued but never + // accepted. Without this they would remain open in the simulated network's + // tcpConns map and interfere with subsequent test cases that reuse the Net + // or Router. + // Guard on obs != nil: newTCPConn always sets obs (and returns an error if + // it is nil), so a nil obs means the entry is a test stub, not a real conn. + for c := range l.acceptCh { + if c.obs != nil { + _ = c.Close() + } + } + + return nil } // Addr returns the listener's network address. @@ -39,5 +174,13 @@ func (l *TCPListener) Addr() net.Addr { // SetDeadline sets the deadline for future Accept calls. func (l *TCPListener) SetDeadline(t time.Time) error { - return transport.ErrNotSupported + var d time.Duration + if t.IsZero() { + d = time.Duration(math.MaxInt64) + } else { + d = time.Until(t) + } + l.timer.Reset(d) + + return nil } diff --git a/vnet/udpproxy_direct_test.go b/vnet/udpproxy_direct_test.go index 736e364..a382796 100644 --- a/vnet/udpproxy_direct_test.go +++ b/vnet/udpproxy_direct_test.go @@ -310,7 +310,7 @@ func TestUDPProxyDirectDeliverBadCase(t *testing.T) { //nolint:cyclop } // BadCase: Invalid address, error and ignore. - tcpAddr, err := net.ResolveTCPAddr("tcp4", "192.168.1.10:8000") + tcpAddr, err := net.ResolveTCPAddr(tcp4, "192.168.1.10:8000") if err != nil { return err } @@ -320,7 +320,7 @@ func TestUDPProxyDirectDeliverBadCase(t *testing.T) { //nolint:cyclop } // BadCase: Invalid target address, ignore. - udpAddr, err := net.ResolveUDPAddr("udp4", "10.0.0.12:5788") + udpAddr, err := net.ResolveUDPAddr(udp4, "10.0.0.12:5788") if err != nil { return err }