mirror of
https://github.com/netbirdio/ice.git
synced 2026-05-22 17:10:58 -07:00
Remove global state for ICE TCP
This addresses a few points issue of #245: - Take a net.Listener instead of having global state - Expose a net.TCPMux based API Also, the unused closeChannel was removed from tcp_mux.go Closes #253.
This commit is contained in:
@@ -120,8 +120,8 @@ type Agent struct {
|
||||
loggerFactory logging.LoggerFactory
|
||||
log logging.LeveledLogger
|
||||
|
||||
net *vnet.Net
|
||||
tcp *tcpIPMux
|
||||
net *vnet.Net
|
||||
tcpMux TCPMux
|
||||
|
||||
interfaceFilter func(string) bool
|
||||
|
||||
@@ -306,11 +306,10 @@ func NewAgent(config *AgentConfig) (*Agent, error) {
|
||||
insecureSkipVerify: config.InsecureSkipVerify,
|
||||
}
|
||||
|
||||
a.tcp = newTCPIPMux(tcpIPMuxParams{
|
||||
ListenPort: config.TCPListenPort,
|
||||
Logger: log,
|
||||
ReadBufferSize: 8,
|
||||
})
|
||||
a.tcpMux = config.TCPMux
|
||||
if a.tcpMux == nil {
|
||||
a.tcpMux = newInvalidTCPMux()
|
||||
}
|
||||
|
||||
if a.net == nil {
|
||||
a.net = vnet.NewNet(nil)
|
||||
@@ -887,7 +886,9 @@ func (a *Agent) Close() error {
|
||||
|
||||
a.gatherCandidateCancel()
|
||||
a.err.Store(ErrClosed)
|
||||
a.tcp.RemoveUfrag(a.localUfrag)
|
||||
|
||||
a.tcpMux.RemoveConnByUfrag(a.localUfrag)
|
||||
|
||||
close(a.done)
|
||||
|
||||
<-done
|
||||
|
||||
+4
-4
@@ -139,10 +139,10 @@ type AgentConfig struct {
|
||||
// to TURN servers via TLS or DTLS
|
||||
InsecureSkipVerify bool
|
||||
|
||||
// TCPListenPort will be used to start a TCP listener on all allowed interfaces for
|
||||
// ICE TCP. Currently only passive candidates are supported. This functionality is
|
||||
// experimental and this API will likely change in the future.
|
||||
TCPListenPort int
|
||||
// TCPMux will be used for multiplexing incoming TCP connections for ICE TCP.
|
||||
// Currently only passive candidates are supported. This functionality is
|
||||
// experimental and the API might change in the future.
|
||||
TCPMux TCPMux
|
||||
}
|
||||
|
||||
// initWithDefaults populates an agent and falls back to defaults if fields are unset
|
||||
|
||||
@@ -103,6 +103,9 @@ var (
|
||||
// ErrRunCanceled indicates a run operation was canceled by its individual done
|
||||
ErrRunCanceled = errors.New("run was canceled by done")
|
||||
|
||||
// ErrTCPMuxNotInitialized indicates TCPMux is not initialized and that invalidTCPMux is used.
|
||||
ErrTCPMuxNotInitialized = errors.New("TCPMux is not initialized")
|
||||
|
||||
// ErrTCPRemoteAddrAlreadyExists indicates we already have the connection with same remote addr.
|
||||
ErrTCPRemoteAddrAlreadyExists = errors.New("conn with same remote addr already exists")
|
||||
)
|
||||
|
||||
@@ -161,28 +161,22 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ
|
||||
var tcpType TCPType
|
||||
switch network {
|
||||
case tcp:
|
||||
if a.tcp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// below is for passive mode
|
||||
// Handle ICE TCP passive mode
|
||||
// TODO active mode
|
||||
// TODO S-O mode
|
||||
|
||||
mux, muxErr := a.tcp.Listen(ip)
|
||||
if muxErr != nil {
|
||||
a.log.Warnf("could not listen %s %s\n", network, ip)
|
||||
continue
|
||||
}
|
||||
|
||||
a.log.Debugf("GetConn by ufrag: %s\n", a.localUfrag)
|
||||
conn, err = mux.GetConn(a.localUfrag)
|
||||
conn, err = a.tcpMux.GetConnByUfrag(a.localUfrag)
|
||||
if err != nil {
|
||||
a.log.Warnf("error getting tcp conn by ufrag: %s %s\n", network, ip, a.localUfrag)
|
||||
if err != ErrTCPMuxNotInitialized {
|
||||
a.log.Warnf("error getting tcp conn by ufrag: %s %s\n", network, ip, a.localUfrag)
|
||||
}
|
||||
continue
|
||||
}
|
||||
port = conn.LocalAddr().(*net.TCPAddr).Port
|
||||
tcpType = TCPTypePassive
|
||||
// TODO is there a way to verify that the listen address is even
|
||||
// accessible from the current interface.
|
||||
case udp:
|
||||
conn, err = listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: ip, Port: 0})
|
||||
if err != nil {
|
||||
|
||||
+17
-1
@@ -14,9 +14,11 @@ import (
|
||||
|
||||
"github.com/pion/dtls/v2"
|
||||
"github.com/pion/dtls/v2/pkg/crypto/selfsign"
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/transport/test"
|
||||
"github.com/pion/turn/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListenUDP(t *testing.T) {
|
||||
@@ -116,11 +118,25 @@ func TestSTUNConcurrency(t *testing.T) {
|
||||
Port: serverPort,
|
||||
})
|
||||
|
||||
listener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IP{127, 0, 0, 1},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = listener.Close()
|
||||
}()
|
||||
|
||||
a, err := NewAgent(&AgentConfig{
|
||||
NetworkTypes: supportedNetworkTypes,
|
||||
Urls: urls,
|
||||
CandidateTypes: []CandidateType{CandidateTypeHost, CandidateTypeServerReflexive},
|
||||
TCPListenPort: 9999,
|
||||
TCPMux: NewTCPMuxDefault(
|
||||
TCPMuxParams{
|
||||
Listener: listener,
|
||||
Logger: logging.NewDefaultLoggerFactory().NewLogger("ice"),
|
||||
ReadBufferSize: 8,
|
||||
},
|
||||
),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
package ice
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/pion/logging"
|
||||
)
|
||||
|
||||
// tcpMuxes is a map of local addr listeners to tcpMux
|
||||
var tcpMuxes map[string]*tcpMux
|
||||
var tcpMuxesMu sync.Mutex
|
||||
|
||||
type tcpIPMux struct {
|
||||
params *tcpIPMuxParams
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type tcpIPMuxParams struct {
|
||||
ListenPort int
|
||||
ReadBufferSize int
|
||||
Logger logging.LeveledLogger
|
||||
}
|
||||
|
||||
func newTCPIPMux(params tcpIPMuxParams) *tcpIPMux {
|
||||
m := &tcpIPMux{
|
||||
params: ¶ms,
|
||||
}
|
||||
|
||||
tcpMuxesMu.Lock()
|
||||
|
||||
if tcpMuxes == nil {
|
||||
tcpMuxes = map[string]*tcpMux{}
|
||||
}
|
||||
|
||||
tcpMuxesMu.Unlock()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *tcpIPMux) Remove(key string) {
|
||||
tcpMuxesMu.Lock()
|
||||
defer tcpMuxesMu.Unlock()
|
||||
|
||||
if tcpMux, ok := tcpMuxes[key]; ok {
|
||||
err := tcpMux.Close()
|
||||
if err != nil {
|
||||
m.params.Logger.Errorf("Error closing tcpMux for key: %s: %s", key, err)
|
||||
}
|
||||
delete(tcpMuxes, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tcpIPMux) RemoveUfrag(ufrag string) {
|
||||
tcpMuxesMu.Lock()
|
||||
defer tcpMuxesMu.Unlock()
|
||||
|
||||
for _, tcpMux := range tcpMuxes {
|
||||
tcpMux.RemoveConn(ufrag)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tcpIPMux) Listen(ip net.IP) (*tcpMux, error) {
|
||||
tcpMuxesMu.Lock()
|
||||
defer tcpMuxesMu.Unlock()
|
||||
|
||||
key := net.JoinHostPort(ip.String(), strconv.Itoa(m.params.ListenPort))
|
||||
|
||||
tcpMux, ok := tcpMuxes[key]
|
||||
if ok {
|
||||
return tcpMux, nil
|
||||
}
|
||||
|
||||
listener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: ip,
|
||||
Port: m.params.ListenPort,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key = net.JoinHostPort(ip.String(), strconv.Itoa(listener.Addr().(*net.TCPAddr).Port))
|
||||
|
||||
tcpMux = newTCPMux(tcpMuxParams{
|
||||
Listener: listener,
|
||||
Logger: m.params.Logger,
|
||||
ReadBufferSize: m.params.ReadBufferSize,
|
||||
})
|
||||
|
||||
tcpMuxes[key] = tcpMux
|
||||
|
||||
m.wg.Add(1)
|
||||
go func() {
|
||||
defer m.wg.Done()
|
||||
<-tcpMux.CloseChannel()
|
||||
m.Remove(key)
|
||||
}()
|
||||
|
||||
return tcpMux, nil
|
||||
}
|
||||
+72
-37
@@ -11,31 +11,69 @@ import (
|
||||
"github.com/pion/stun"
|
||||
)
|
||||
|
||||
type tcpMux struct {
|
||||
params *tcpMuxParams
|
||||
// TCPMux is allows grouping multiple TCP net.Conns and using them like UDP
|
||||
// net.PacketConns. The main implementation of this is TCPMuxDefault, and this
|
||||
// interface exists to:
|
||||
// 1. prevent SEGV panics when TCPMuxDefault is not initialized by using the
|
||||
// invalidTCPMux implementation, and
|
||||
// 2. allow mocking in tests.
|
||||
type TCPMux interface {
|
||||
io.Closer
|
||||
GetConnByUfrag(ufrag string) (net.PacketConn, error)
|
||||
RemoveConnByUfrag(ufrag string)
|
||||
}
|
||||
|
||||
// invalidTCPMux is an implementation of TCPMux that always returns ErroTCPMuxNotInitialized.
|
||||
type invalidTCPMux struct {
|
||||
}
|
||||
|
||||
func newInvalidTCPMux() *invalidTCPMux {
|
||||
return &invalidTCPMux{}
|
||||
}
|
||||
|
||||
// Close implements TCPMux interface.
|
||||
func (m *invalidTCPMux) Close() error {
|
||||
return ErrTCPMuxNotInitialized
|
||||
}
|
||||
|
||||
// GetConnByUfrag implements TCPMux interface.
|
||||
func (m *invalidTCPMux) GetConnByUfrag(ufrag string) (net.PacketConn, error) {
|
||||
return nil, ErrTCPMuxNotInitialized
|
||||
}
|
||||
|
||||
// RemoveConnByUfrag implements TCPMux interface.
|
||||
func (m *invalidTCPMux) RemoveConnByUfrag(ufrag string) {}
|
||||
|
||||
// TCPMuxDefault muxes TCP net.Conns into net.PacketConns and groups them by
|
||||
// Ufrag. It is a default implementation of TCPMux interface.
|
||||
type TCPMuxDefault struct {
|
||||
params *TCPMuxParams
|
||||
closed bool
|
||||
|
||||
// conns is a map of all tcpPacketConns indexed by ufrag
|
||||
conns map[string]*tcpPacketConn
|
||||
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
closedChan chan struct{}
|
||||
closeOnce sync.Once
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type tcpMuxParams struct {
|
||||
// TCPMuxParams are parameters for TCPMux.
|
||||
type TCPMuxParams struct {
|
||||
Listener net.Listener
|
||||
Logger logging.LeveledLogger
|
||||
ReadBufferSize int
|
||||
}
|
||||
|
||||
func newTCPMux(params tcpMuxParams) *tcpMux {
|
||||
m := &tcpMux{
|
||||
// NewTCPMuxDefault creates a new instance of TCPMuxDefault.
|
||||
func NewTCPMuxDefault(params TCPMuxParams) *TCPMuxDefault {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice")
|
||||
}
|
||||
|
||||
m := &TCPMuxDefault{
|
||||
params: ¶ms,
|
||||
|
||||
conns: map[string]*tcpPacketConn{},
|
||||
|
||||
closedChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
m.wg.Add(1)
|
||||
@@ -47,7 +85,7 @@ func newTCPMux(params tcpMuxParams) *tcpMux {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *tcpMux) start() {
|
||||
func (m *TCPMuxDefault) start() {
|
||||
m.params.Logger.Infof("Listening TCP on %s\n", m.params.Listener.Addr())
|
||||
for {
|
||||
conn, err := m.params.Listener.Accept()
|
||||
@@ -66,14 +104,20 @@ func (m *tcpMux) start() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tcpMux) LocalAddr() net.Addr {
|
||||
// LocalAddr returns the listening address of this TCPMuxDefault.
|
||||
func (m *TCPMuxDefault) LocalAddr() net.Addr {
|
||||
return m.params.Listener.Addr()
|
||||
}
|
||||
|
||||
func (m *tcpMux) GetConn(ufrag string) (net.PacketConn, error) {
|
||||
// GetConnByUfrag retrieves an existing or creates a new net.PacketConn.
|
||||
func (m *TCPMuxDefault) GetConnByUfrag(ufrag string) (net.PacketConn, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.closed {
|
||||
return nil, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
conn, ok := m.conns[ufrag]
|
||||
|
||||
if ok {
|
||||
@@ -86,7 +130,7 @@ func (m *tcpMux) GetConn(ufrag string) (net.PacketConn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (m *tcpMux) createConn(ufrag string, localAddr net.Addr) *tcpPacketConn {
|
||||
func (m *TCPMuxDefault) createConn(ufrag string, localAddr net.Addr) *tcpPacketConn {
|
||||
conn := newTCPPacketConn(tcpPacketParams{
|
||||
ReadBuffer: m.params.ReadBufferSize,
|
||||
LocalAddr: localAddr,
|
||||
@@ -98,20 +142,20 @@ func (m *tcpMux) createConn(ufrag string, localAddr net.Addr) *tcpPacketConn {
|
||||
go func() {
|
||||
defer m.wg.Done()
|
||||
<-conn.CloseChannel()
|
||||
m.RemoveConn(ufrag)
|
||||
m.RemoveConnByUfrag(ufrag)
|
||||
}()
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (m *tcpMux) closeAndLogError(closer io.Closer) {
|
||||
func (m *TCPMuxDefault) closeAndLogError(closer io.Closer) {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
m.params.Logger.Warnf("Error closing connection: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tcpMux) handleConn(conn net.Conn) {
|
||||
func (m *TCPMuxDefault) handleConn(conn net.Conn) {
|
||||
buf := make([]byte, receiveMTU)
|
||||
|
||||
n, err := readStreamingPacket(conn, buf)
|
||||
@@ -169,28 +213,27 @@ func (m *tcpMux) handleConn(conn net.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tcpMux) Close() error {
|
||||
// Close closes the listener and waits for all goroutines to exit.
|
||||
func (m *TCPMuxDefault) Close() error {
|
||||
m.mu.Lock()
|
||||
m.closed = true
|
||||
|
||||
m.closeOnce.Do(func() {
|
||||
close(m.closedChan)
|
||||
})
|
||||
|
||||
for _, conn := range m.conns {
|
||||
m.closeAndLogError(conn)
|
||||
}
|
||||
m.conns = map[string]*tcpPacketConn{}
|
||||
m.mu.Unlock()
|
||||
|
||||
err := m.params.Listener.Close()
|
||||
|
||||
m.mu.Unlock()
|
||||
|
||||
m.wg.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *tcpMux) CloseChannel() <-chan struct{} {
|
||||
return m.closedChan
|
||||
}
|
||||
|
||||
func (m *tcpMux) RemoveConn(ufrag string) {
|
||||
// RemoveConnByUfrag closes and removes a net.PacketConn by Ufrag.
|
||||
func (m *TCPMuxDefault) RemoveConnByUfrag(ufrag string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -198,14 +241,6 @@ func (m *tcpMux) RemoveConn(ufrag string) {
|
||||
m.closeAndLogError(conn)
|
||||
delete(m.conns, ufrag)
|
||||
}
|
||||
|
||||
if len(m.conns) == 0 {
|
||||
m.closeOnce.Do(func() {
|
||||
close(m.closedChan)
|
||||
})
|
||||
|
||||
m.closeAndLogError(m.params.Listener)
|
||||
}
|
||||
}
|
||||
|
||||
const streamingPacketHeaderLen = 2
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ice
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
@@ -11,20 +12,30 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTCP_Recv(t *testing.T) {
|
||||
var _ TCPMux = &TCPMuxDefault{}
|
||||
var _ TCPMux = &invalidTCPMux{}
|
||||
|
||||
func TestTCPMux_Recv(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
loggerFactory := logging.NewDefaultLoggerFactory()
|
||||
|
||||
tim := newTCPIPMux(tcpIPMuxParams{
|
||||
ListenPort: 8080,
|
||||
listener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IP{127, 0, 0, 1},
|
||||
Port: 0,
|
||||
})
|
||||
require.NoError(t, err, "error starting listener")
|
||||
defer func() {
|
||||
_ = listener.Close()
|
||||
}()
|
||||
|
||||
tcpMux := NewTCPMuxDefault(TCPMuxParams{
|
||||
Listener: listener,
|
||||
Logger: loggerFactory.NewLogger("ice"),
|
||||
ReadBufferSize: 20,
|
||||
})
|
||||
|
||||
tcpMux, err := tim.Listen(net.IP{127, 0, 0, 1})
|
||||
require.NoError(t, err, "error starting listener")
|
||||
defer func() {
|
||||
_ = tcpMux.Close()
|
||||
}()
|
||||
@@ -42,7 +53,7 @@ func TestTCP_Recv(t *testing.T) {
|
||||
n, err := writeStreamingPacket(conn, msg.Raw)
|
||||
require.NoError(t, err, "error writing tcp stun packet")
|
||||
|
||||
pktConn, err := tcpMux.GetConn("myufrag")
|
||||
pktConn, err := tcpMux.GetConnByUfrag("myufrag")
|
||||
require.NoError(t, err, "error retrieving muxed connection for ufrag")
|
||||
defer func() {
|
||||
_ = pktConn.Close()
|
||||
@@ -55,3 +66,34 @@ func TestTCP_Recv(t *testing.T) {
|
||||
assert.Equal(t, n, n2, "received byte size mismatch")
|
||||
assert.Equal(t, msg.Raw, recv, "received bytes mismatch")
|
||||
}
|
||||
|
||||
func TestTCPMux_NoDeadlockWhenClosingUnusedPacketConn(t *testing.T) {
|
||||
report := test.CheckRoutines(t)
|
||||
defer report()
|
||||
|
||||
loggerFactory := logging.NewDefaultLoggerFactory()
|
||||
|
||||
listener, err := net.ListenTCP("tcp", &net.TCPAddr{
|
||||
IP: net.IP{127, 0, 0, 1},
|
||||
Port: 0,
|
||||
})
|
||||
require.NoError(t, err, "error starting listener")
|
||||
defer func() {
|
||||
_ = listener.Close()
|
||||
}()
|
||||
|
||||
tcpMux := NewTCPMuxDefault(TCPMuxParams{
|
||||
Listener: listener,
|
||||
Logger: loggerFactory.NewLogger("ice"),
|
||||
ReadBufferSize: 20,
|
||||
})
|
||||
|
||||
_, err = tcpMux.GetConnByUfrag("test")
|
||||
require.NoError(t, err, "error getting conn by ufrag")
|
||||
|
||||
require.NoError(t, tcpMux.Close(), "error closing tcpMux")
|
||||
|
||||
conn, err := tcpMux.GetConnByUfrag("test")
|
||||
assert.Nil(t, conn, "should receive nil because mux is closed")
|
||||
assert.Equal(t, io.ErrClosedPipe, err, "should receive error because mux is closed")
|
||||
}
|
||||
+4
-5
@@ -62,17 +62,16 @@ func (t *tcpPacketConn) AddConn(conn net.Conn, firstPacketData []byte) error {
|
||||
}
|
||||
|
||||
if _, ok := t.conns[conn.RemoteAddr().String()]; ok {
|
||||
return ErrTCPRemoteAddrAlreadyExists
|
||||
return fmt.Errorf("connection with same remote address already exists: %s", conn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
t.conns[conn.RemoteAddr().String()] = conn
|
||||
|
||||
if firstPacketData != nil {
|
||||
t.recvChan <- streamingPacket{firstPacketData, conn.RemoteAddr(), nil}
|
||||
}
|
||||
|
||||
t.wg.Add(1)
|
||||
go func() {
|
||||
if firstPacketData != nil {
|
||||
t.recvChan <- streamingPacket{firstPacketData, conn.RemoteAddr(), nil}
|
||||
}
|
||||
defer t.wg.Done()
|
||||
t.startReading(conn)
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user