diff --git a/agent.go b/agent.go index 7ecb086..1a938f7 100644 --- a/agent.go +++ b/agent.go @@ -83,9 +83,10 @@ type Agent struct { // force candidate to be contacted immediately (instead of waiting for connectivityTicker) forceCandidateContact chan bool - trickle bool - tieBreaker uint64 - lite bool + trickle bool + tieBreaker uint64 + lite bool + connectionState ConnectionState gatheringState GatheringState @@ -142,6 +143,9 @@ type Agent struct { // LRU of outbound Binding request Transaction IDs pendingBindingRequests []bindingRequest + // 1:1 D-NAT IP address mapping + extIPMapper *externalIPMapper + // State for closing done chan struct{} err atomicError @@ -222,6 +226,20 @@ type AgentConfig struct { // Lite agents do not perform connectivity check and only provide host candidates. Lite bool + // NAT1To1IPCandidateType is used along with NAT1To1IPs to specify which candidate type + // the 1:1 NAT IP addresses should be mapped to. + // If unspecified or CandidateTypeHost, NAT1To1IPs are used to replace host candidate IPs. + // If CandidateTypeServerReflexive, it will insert a srflx candidate (as if it was dervied + // from a STUN server) with its port number being the one for the actual host candidate. + // Other values will result in an error. + NAT1To1IPCandidateType CandidateType + + // NAT1To1IPs contains a list of public IP addresses that are to be used as a host + // candidate or srflx candidate. This is used typically for servers that are behind + // 1:1 D-NAT (e.g. AWS EC2 instances) and to eliminate the need of server reflexisive + // candidate gathering. + NAT1To1IPs []string + // HostAcceptanceMinWait specify a minimum wait time before selecting host candidates HostAcceptanceMinWait *time.Duration // HostAcceptanceMinWait specify a minimum wait time before selecting srflx candidates @@ -342,13 +360,43 @@ func NewAgent(config *AgentConfig) (*Agent, error) { if a.net == nil { a.net = vnet.NewNet(nil) - } else { + } else if a.net.IsVirtual() { a.log.Warn("vnet is enabled") if a.mDNSMode != MulticastDNSModeDisabled { a.log.Warn("vnet does not support mDNS yet") } } + a.initWithDefaults(config) + + // Make sure the buffer doesn't grow indefinitely. + // NOTE: We actually won't get anywhere close to this limit. + // SRTP will constantly read from the endpoint and drop packets if it's full. + a.buffer.SetLimitSize(maxBufferSize) + + if a.lite && (len(a.candidateTypes) != 1 || a.candidateTypes[0] != CandidateTypeHost) { + return nil, ErrLiteUsingNonHostCandidates + } + + if config.Urls != nil && len(config.Urls) > 0 && !containsCandidateType(CandidateTypeServerReflexive, a.candidateTypes) && !containsCandidateType(CandidateTypeRelay, a.candidateTypes) { + return nil, ErrUselessUrlsProvided + } + + if err = a.initExtIPMapping(config); err != nil { + return nil, err + } + + go a.taskLoop() + + // Initialize local candidates + if !a.trickle { + a.gatherCandidates() + } + return a, nil +} + +// a sSeparate init routine called by NewAgent() to overcome gocyclo error with golangci-lint +func (a *Agent) initWithDefaults(config *AgentConfig) { if config.MaxBindingRequests == nil { a.maxBindingRequests = defaultMaxBindingRequests } else { @@ -385,11 +433,6 @@ func NewAgent(config *AgentConfig) (*Agent, error) { a.relayAcceptanceMinWait = *config.RelayAcceptanceMinWait } - // Make sure the buffer doesn't grow indefinitely. - // NOTE: We actually won't get anywhere close to this limit. - // SRTP will constantly read from the endpoint and drop packets if it's full. - a.buffer.SetLimitSize(maxBufferSize) - // connectionTimeout used to declare a connection dead if config.ConnectionTimeout == nil { a.connectionTimeout = defaultConnectionTimeout @@ -414,22 +457,45 @@ func NewAgent(config *AgentConfig) (*Agent, error) { } else { a.candidateTypes = config.CandidateTypes } +} - if a.lite && (len(a.candidateTypes) != 1 || a.candidateTypes[0] != CandidateTypeHost) { - return nil, ErrLiteUsingNonHostCandidates +func (a *Agent) initExtIPMapping(config *AgentConfig) error { + var err error + a.extIPMapper, err = newExternalIPMapper(config.NAT1To1IPCandidateType, config.NAT1To1IPs) + if err != nil { + return err } - - if config.Urls != nil && len(config.Urls) > 0 && !containsCandidateType(CandidateTypeServerReflexive, a.candidateTypes) && !containsCandidateType(CandidateTypeRelay, a.candidateTypes) { - return nil, ErrUselessUrlsProvided + if a.extIPMapper == nil { + return nil // this may happen when config.NAT1To1IPs is an empty array } + if a.extIPMapper.candidateType == CandidateTypeHost { + if a.mDNSMode == MulticastDNSModeQueryAndGather { + return ErrMulticastDNSWithNAT1To1IPMapping + } + candiHostEnabled := false + for _, candiType := range a.candidateTypes { + if candiType == CandidateTypeHost { + candiHostEnabled = true + break + } + } + if !candiHostEnabled { + return ErrIneffectiveNAT1To1IPMappingHost + } - go a.taskLoop() - - // Initialize local candidates - if !a.trickle { - a.gatherCandidates() + } else if a.extIPMapper.candidateType == CandidateTypeServerReflexive { + candiSrflxEnabled := false + for _, candiType := range a.candidateTypes { + if candiType == CandidateTypeServerReflexive { + candiSrflxEnabled = true + break + } + } + if !candiSrflxEnabled { + return ErrIneffectiveNAT1To1IPMappingSrflx + } } - return a, nil + return nil } // OnConnectionStateChange sets a handler that is fired when the connection state changes diff --git a/agent_test.go b/agent_test.go index 196ea72..6f7e524 100644 --- a/agent_test.go +++ b/agent_test.go @@ -367,10 +367,11 @@ func TestConnectivityOnStartup(t *testing.T) { Proto: ProtoTypeUDP, } - v, err := buildVNet(&vnet.NATType{ + natType := &vnet.NATType{ MappingBehavior: vnet.EndpointIndependent, FilteringBehavior: vnet.EndpointIndependent, - }) + } + v, err := buildVNet(natType, natType) require.NoError(t, err, "should succeed") defer v.close() @@ -440,10 +441,11 @@ func TestConnectivityLite(t *testing.T) { Proto: ProtoTypeUDP, } - v, err := buildVNet(&vnet.NATType{ + natType := &vnet.NATType{ MappingBehavior: vnet.EndpointIndependent, FilteringBehavior: vnet.EndpointIndependent, - }) + } + v, err := buildVNet(natType, natType) require.NoError(t, err, "should succeed") defer v.close() @@ -1113,3 +1115,88 @@ func TestRemoteCandidateStats(t *testing.T) { t.Fatalf("Error on agent.Close(): %s", err) } } + +func TestInitExtIPMapping(t *testing.T) { + var a *Agent + var err error + + var closeAgent = func() { + if a != nil { + if err = a.Close(); err != nil { + t.Fatalf("failed to close agent: %v", err) + } + } + } + + // a.extIPMapper should be nil by default + a, err = NewAgent(&AgentConfig{ + Trickle: true, // to avoid starting gathering candidates + }) + if err != nil { + t.Fatalf("Failed to create agent: %v", err) + } + if a.extIPMapper != nil { + t.Fatal("a.extIPMapper should be nil by default") + } + closeAgent() + + // a.extIPMapper should be nil when NAT1To1IPs is a non-nil empty array + a, err = NewAgent(&AgentConfig{ + NAT1To1IPs: []string{}, + NAT1To1IPCandidateType: CandidateTypeHost, + Trickle: true, // to avoid starting gathering candidates + }) + if err != nil { + t.Fatalf("Failed to create agent: %v", err) + } + if a.extIPMapper != nil { + t.Fatal("a.extIPMapper should be nil by default") + } + closeAgent() + + // NewAgent should return an error when 1:1 NAT for host candidate is enabled + // but the candidate type does not appear in the CandidateTypes. + _, err = NewAgent(&AgentConfig{ + NAT1To1IPs: []string{"1.2.3.4"}, + NAT1To1IPCandidateType: CandidateTypeHost, + CandidateTypes: []CandidateType{CandidateTypeRelay}, + Trickle: true, // to avoid starting gathering candidates + }) + if err != ErrIneffectiveNAT1To1IPMappingHost { + t.Fatalf("Unexpected error: %v", err) + } + + // NewAgent should return an error when 1:1 NAT for srflx candidate is enabled + // but the candidate type does not appear in the CandidateTypes. + _, err = NewAgent(&AgentConfig{ + NAT1To1IPs: []string{"1.2.3.4"}, + NAT1To1IPCandidateType: CandidateTypeServerReflexive, + CandidateTypes: []CandidateType{CandidateTypeRelay}, + Trickle: true, // to avoid starting gathering candidates + }) + if err != ErrIneffectiveNAT1To1IPMappingSrflx { + t.Fatalf("Unexpected error: %v", err) + } + + // NewAgent should return an error when 1:1 NAT for host candidate is enabled + // along with mDNS with MulticastDNSModeQueryAndGather + _, err = NewAgent(&AgentConfig{ + NAT1To1IPs: []string{"1.2.3.4"}, + NAT1To1IPCandidateType: CandidateTypeHost, + MulticastDNSMode: MulticastDNSModeQueryAndGather, + Trickle: true, // to avoid starting gathering candidates + }) + if err != ErrMulticastDNSWithNAT1To1IPMapping { + t.Fatalf("Unexpected error: %v", err) + } + + // NewAgent should return if newExternalIPMapper() returns an error. + _, err = NewAgent(&AgentConfig{ + NAT1To1IPs: []string{"bad.2.3.4"}, // bad IP + NAT1To1IPCandidateType: CandidateTypeHost, + Trickle: true, // to avoid starting gathering candidates + }) + if err != ErrInvalidNAT1To1IPMapping { + t.Fatalf("Unexpected error: %v", err) + } +} diff --git a/candidatetype.go b/candidatetype.go index dd58d89..6adadfa 100644 --- a/candidatetype.go +++ b/candidatetype.go @@ -5,7 +5,8 @@ type CandidateType byte // CandidateType enum const ( - CandidateTypeHost CandidateType = iota + 1 + CandidateTypeUnspecified CandidateType = iota + CandidateTypeHost CandidateTypeServerReflexive CandidateTypePeerReflexive CandidateTypeRelay diff --git a/connectivity_vnet_test.go b/connectivity_vnet_test.go index 46f7a13..06aa921 100644 --- a/connectivity_vnet_test.go +++ b/connectivity_vnet_test.go @@ -25,7 +25,7 @@ func (v *virtualNet) close() { v.wan.Stop() // nolint:errcheck,gosec } -func buildVNet(natType *vnet.NATType) (*virtualNet, error) { +func buildVNet(natType0, natType1 *vnet.NATType) (*virtualNet, error) { loggerFactory := logging.NewDefaultLoggerFactory() // WAN @@ -48,16 +48,27 @@ func buildVNet(natType *vnet.NATType) (*virtualNet, error) { // LAN 0 lan0, err := vnet.NewRouter(&vnet.RouterConfig{ - StaticIP: "27.1.1.1", // this router's external IP on eth0 + StaticIPs: func() []string { + if natType0.Mode == vnet.NATModeNAT1To1 { + return []string{ + "27.1.1.1/192.168.0.1", + } + } + return []string{ + "27.1.1.1", + } + }(), CIDR: "192.168.0.0/24", - NATType: natType, + NATType: natType0, LoggerFactory: loggerFactory, }) if err != nil { return nil, err } - net0 := vnet.NewNet(&vnet.NetConfig{}) + net0 := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{"192.168.0.1"}, + }) err = lan0.AddNet(net0) if err != nil { return nil, err @@ -70,16 +81,27 @@ func buildVNet(natType *vnet.NATType) (*virtualNet, error) { // LAN 1 lan1, err := vnet.NewRouter(&vnet.RouterConfig{ - StaticIP: "28.1.1.1", // this router's external IP on eth0 + StaticIPs: func() []string { + if natType1.Mode == vnet.NATModeNAT1To1 { + return []string{ + "28.1.1.1/10.2.0.1", + } + } + return []string{ + "28.1.1.1", + } + }(), CIDR: "10.2.0.0/24", - NATType: natType, + NATType: natType1, LoggerFactory: loggerFactory, }) if err != nil { return nil, err } - net1 := vnet.NewNet(&vnet.NetConfig{}) + net1 := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{"10.2.0.1"}, + }) err = lan1.AddNet(net1) if err != nil { return nil, err @@ -168,19 +190,33 @@ func connectWithVNet(aAgent, bAgent *Agent) (*Conn, *Conn) { return aConn, bConn } -func pipeWithVNet(v *virtualNet, urls0, urls1 []*URL) (*Conn, *Conn) { +type agentTestConfig struct { + urls []*URL + nat1To1IPCandidateType CandidateType +} + +func pipeWithVNet(v *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (*Conn, *Conn) { aNotifier, aConnected := onConnected() bNotifier, bConnected := onConnected() var wg sync.WaitGroup wg.Add(2) + var nat1To1IPs []string + if a0TestConfig.nat1To1IPCandidateType != CandidateTypeUnspecified { + nat1To1IPs = []string{ + "27.1.1.1", + } + } + cfg0 := &AgentConfig{ - Urls: urls0, - Trickle: true, - NetworkTypes: supportedNetworkTypes, - MulticastDNSMode: MulticastDNSModeDisabled, - Net: v.net0, + Urls: a0TestConfig.urls, + Trickle: true, + NetworkTypes: supportedNetworkTypes, + MulticastDNSMode: MulticastDNSModeDisabled, + NAT1To1IPs: nat1To1IPs, + NAT1To1IPCandidateType: a0TestConfig.nat1To1IPCandidateType, + Net: v.net0, } aAgent, err := NewAgent(cfg0) @@ -204,12 +240,19 @@ func pipeWithVNet(v *virtualNet, urls0, urls1 []*URL) (*Conn, *Conn) { panic(err) } + if a1TestConfig.nat1To1IPCandidateType != CandidateTypeUnspecified { + nat1To1IPs = []string{ + "28.1.1.1", + } + } cfg1 := &AgentConfig{ - Urls: urls1, - Trickle: true, - NetworkTypes: supportedNetworkTypes, - MulticastDNSMode: MulticastDNSModeDisabled, - Net: v.net1, + Urls: a1TestConfig.urls, + Trickle: true, + NetworkTypes: supportedNetworkTypes, + MulticastDNSMode: MulticastDNSModeDisabled, + NAT1To1IPs: nat1To1IPs, + NAT1To1IPCandidateType: a1TestConfig.nat1To1IPCandidateType, + Net: v.net1, } bAgent, err := NewAgent(cfg1) @@ -270,15 +313,16 @@ func TestConnectivityVNet(t *testing.T) { Proto: ProtoTypeUDP, } - t.Run("Full-cone NATs", func(t *testing.T) { + t.Run("Full-cone NATs on both ends", func(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") - // buildVNet with Full-cone NATs - v, err := buildVNet(&vnet.NATType{ + // buildVNet with a Full-cone NATs both LANs + natType := &vnet.NATType{ MappingBehavior: vnet.EndpointIndependent, FilteringBehavior: vnet.EndpointIndependent, - }) + } + v, err := buildVNet(natType, natType) if !assert.NoError(t, err, "should succeed") { return @@ -286,14 +330,17 @@ func TestConnectivityVNet(t *testing.T) { defer v.close() log.Debug("Connecting...") - urls0 := []*URL{ - stunServerURL, + a0TestConfig := &agentTestConfig{ + urls: []*URL{ + stunServerURL, + }, } - - urls1 := []*URL{ - stunServerURL, + a1TestConfig := &agentTestConfig{ + urls: []*URL{ + stunServerURL, + }, } - ca, cb := pipeWithVNet(v, urls0, urls1) + ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) time.Sleep(1 * time.Second) @@ -303,15 +350,16 @@ func TestConnectivityVNet(t *testing.T) { } }) - t.Run("Symmetric NATs", func(t *testing.T) { + t.Run("Symmetric NATs on both ends", func(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") - // buildVNet with Symmetric NATs - v, err := buildVNet(&vnet.NATType{ + // buildVNet with a Symmetric NATs for both LANs + natType := &vnet.NATType{ MappingBehavior: vnet.EndpointAddrPortDependent, FilteringBehavior: vnet.EndpointAddrPortDependent, - }) + } + v, err := buildVNet(natType, natType) if !assert.NoError(t, err, "should succeed") { return @@ -319,15 +367,90 @@ func TestConnectivityVNet(t *testing.T) { defer v.close() log.Debug("Connecting...") - urls0 := []*URL{ - stunServerURL, - turnServerURL, + a0TestConfig := &agentTestConfig{ + urls: []*URL{ + stunServerURL, + turnServerURL, + }, } + a1TestConfig := &agentTestConfig{ + urls: []*URL{ + stunServerURL, + }, + } + ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) - urls1 := []*URL{ - stunServerURL, + log.Debug("Closing...") + if !closePipe(t, ca, cb) { + return } - ca, cb := pipeWithVNet(v, urls0, urls1) + }) + + t.Run("1:1 NAT with host candidate vs Symmetric NATs", func(t *testing.T) { + loggerFactory := logging.NewDefaultLoggerFactory() + log := loggerFactory.NewLogger("test") + + // Agent0 is behind 1:1 NAT + natType0 := &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + } + // Agent1 is behind a symmetric NAT + natType1 := &vnet.NATType{ + MappingBehavior: vnet.EndpointAddrPortDependent, + FilteringBehavior: vnet.EndpointAddrPortDependent, + } + v, err := buildVNet(natType0, natType1) + + if !assert.NoError(t, err, "should succeed") { + return + } + defer v.close() + + log.Debug("Connecting...") + a0TestConfig := &agentTestConfig{ + urls: []*URL{}, + nat1To1IPCandidateType: CandidateTypeHost, // Use 1:1 NAT IP as a host candidate + } + a1TestConfig := &agentTestConfig{ + urls: []*URL{}, + } + ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) + + log.Debug("Closing...") + if !closePipe(t, ca, cb) { + return + } + }) + + t.Run("1:1 NAT with srflx candidate vs Symmetric NATs", func(t *testing.T) { + loggerFactory := logging.NewDefaultLoggerFactory() + log := loggerFactory.NewLogger("test") + + // Agent0 is behind 1:1 NAT + natType0 := &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + } + // Agent1 is behind a symmetric NAT + natType1 := &vnet.NATType{ + MappingBehavior: vnet.EndpointAddrPortDependent, + FilteringBehavior: vnet.EndpointAddrPortDependent, + } + v, err := buildVNet(natType0, natType1) + + if !assert.NoError(t, err, "should succeed") { + return + } + defer v.close() + + log.Debug("Connecting...") + a0TestConfig := &agentTestConfig{ + urls: []*URL{}, + nat1To1IPCandidateType: CandidateTypeServerReflexive, // Use 1:1 NAT IP as a srflx candidate + } + a1TestConfig := &agentTestConfig{ + urls: []*URL{}, + } + ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) log.Debug("Closing...") if !closePipe(t, ca, cb) { diff --git a/errors.go b/errors.go index 876d1cb..ae90de9 100644 --- a/errors.go +++ b/errors.go @@ -64,4 +64,26 @@ var ( // ErrUselessUrlsProvided indicates that one or more URL was provided to the agent but no host // candidate required them ErrUselessUrlsProvided = errors.New("agent does not need URL with selected candidate types") + + // ErrUnsupportedNAT1To1IPCandidateType indicates that the specified NAT1To1IPCandidateType is + // unsupported + ErrUnsupportedNAT1To1IPCandidateType = errors.New("unsupported 1:1 NAT IP candidate type") + + // ErrInvalidNAT1To1IPMapping indicates that the given 1:1 NAT IP mapping is invalid + ErrInvalidNAT1To1IPMapping = errors.New("invalid 1:1 NAT IP mapping") + + // ErrExternalMappedIPNotFound in NAT1To1IPMapping + ErrExternalMappedIPNotFound = errors.New("external mapped IP not found") + + // ErrMulticastDNSWithNAT1To1IPMapping indicates that the mDNS gathering cannot be used along + // with 1:1 NAT IP mapping for host candidate. + ErrMulticastDNSWithNAT1To1IPMapping = errors.New("mDNS gathering cannot be used with 1:1 NAT IP mapping for host candidate") + + // ErrIneffectiveNAT1To1IPMappingHost indicates that 1:1 NAT IP mapping for host candidate is + // requested, but the host candidate type is disabled. + ErrIneffectiveNAT1To1IPMappingHost = errors.New("1:1 NAT IP mapping for host candidate ineffective") + + // ErrIneffectiveNAT1To1IPMappingSrflx indicates that 1:1 NAT IP mapping for srflx candidate is + // requested, but the srflx candidate type is disabled. + ErrIneffectiveNAT1To1IPMappingSrflx = errors.New("1:1 NAT IP mapping for srflx candidate ineffective") ) diff --git a/external_ip_mapper.go b/external_ip_mapper.go new file mode 100644 index 0000000..fdb1c95 --- /dev/null +++ b/external_ip_mapper.go @@ -0,0 +1,143 @@ +package ice + +import ( + "net" + "strings" +) + +func validateIPString(ipStr string) (net.IP, bool, error) { + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, false, ErrInvalidNAT1To1IPMapping + } + return ip, (ip.To4() != nil), nil +} + +// ipMapping holds the mapping of local and external IP address for a particular IP family +type ipMapping struct { + ipSole net.IP // when non-nil, this is the sole external IP for one local IP assumed + ipMap map[string]net.IP // local-to-external IP mapping (k: local, v: external) +} + +func (m *ipMapping) setSoleIP(ip net.IP) error { + if m.ipSole != nil || len(m.ipMap) > 0 { + return ErrInvalidNAT1To1IPMapping + } + + m.ipSole = ip + + return nil +} + +func (m *ipMapping) addIPMapping(locIP, extIP net.IP) error { + if m.ipSole != nil { + return ErrInvalidNAT1To1IPMapping + } + + locIPStr := locIP.String() + + // check if dup of local IP + if _, ok := m.ipMap[locIPStr]; ok { + return ErrInvalidNAT1To1IPMapping + } + + m.ipMap[locIPStr] = extIP + + return nil +} + +func (m *ipMapping) findExternalIP(locIP net.IP) (net.IP, error) { + if m.ipSole != nil { + return m.ipSole, nil + } + + extIP, ok := m.ipMap[locIP.String()] + if !ok { + return nil, ErrExternalMappedIPNotFound + } + + return extIP, nil +} + +type externalIPMapper struct { + ipv4Mapping ipMapping + ipv6Mapping ipMapping + candidateType CandidateType +} + +func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIPMapper, error) { + if len(ips) == 0 { + return nil, nil + } + if candidateType == CandidateTypeUnspecified { + candidateType = CandidateTypeHost // defaults to host + } else if candidateType != CandidateTypeHost && candidateType != CandidateTypeServerReflexive { + return nil, ErrUnsupportedNAT1To1IPCandidateType + } + + m := &externalIPMapper{ + ipv4Mapping: ipMapping{ipMap: map[string]net.IP{}}, + ipv6Mapping: ipMapping{ipMap: map[string]net.IP{}}, + candidateType: candidateType, + } + + for _, extIPStr := range ips { + ipPair := strings.Split(extIPStr, "/") + if len(ipPair) == 0 || len(ipPair) > 2 { + return nil, ErrInvalidNAT1To1IPMapping + } + + extIP, isExtIPv4, err := validateIPString(ipPair[0]) + if err != nil { + return nil, err + } + if len(ipPair) == 1 { + if isExtIPv4 { + if err := m.ipv4Mapping.setSoleIP(extIP); err != nil { + return nil, err + } + } else { + if err := m.ipv6Mapping.setSoleIP(extIP); err != nil { + return nil, err + } + } + } else { + locIP, isLocIPv4, err := validateIPString(ipPair[1]) + if err != nil { + return nil, err + } + if isExtIPv4 { + if !isLocIPv4 { + return nil, ErrInvalidNAT1To1IPMapping + } + + if err := m.ipv4Mapping.addIPMapping(locIP, extIP); err != nil { + return nil, err + } + } else { + if isLocIPv4 { + return nil, ErrInvalidNAT1To1IPMapping + } + + if err := m.ipv6Mapping.addIPMapping(locIP, extIP); err != nil { + return nil, err + } + } + } + } + + return m, nil +} + +func (m *externalIPMapper) findExternalIP(localIPStr string) (net.IP, error) { + locIP, isLocIPv4, err := validateIPString(localIPStr) + if err != nil { + return nil, err + } + + if isLocIPv4 { + return m.ipv4Mapping.findExternalIP(locIP) + } + + return m.ipv6Mapping.findExternalIP(locIP) +} diff --git a/external_ip_mapper_test.go b/external_ip_mapper_test.go new file mode 100644 index 0000000..aa7f2e2 --- /dev/null +++ b/external_ip_mapper_test.go @@ -0,0 +1,304 @@ +package ice + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExternalIPMapper(t *testing.T) { + t.Run("validateIPString", func(t *testing.T) { + var ip net.IP + var isIPv4 bool + var err error + + ip, isIPv4, err = validateIPString("1.2.3.4") + assert.NoError(t, err, "should succeed") + assert.True(t, isIPv4, "should be true") + assert.Equal(t, "1.2.3.4", ip.String(), "should be true") + + ip, isIPv4, err = validateIPString("2601:4567::5678") + assert.NoError(t, err, "should succeed") + assert.False(t, isIPv4, "should be false") + assert.Equal(t, "2601:4567::5678", ip.String(), "should be true") + + _, _, err = validateIPString("bad.6.6.6") + assert.Error(t, err, "should fail") + }) + + t.Run("newExternalIPMapper", func(t *testing.T) { + var m *externalIPMapper + var err error + + // ips being nil should succeed but mapper will be nil also + m, err = newExternalIPMapper(CandidateTypeUnspecified, nil) + assert.NoError(t, err, "should succeed") + assert.Nil(t, m, "should be nil") + + // ips being empty should succeed but mapper will still be nil + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{}) + assert.NoError(t, err, "should succeed") + assert.Nil(t, m, "should be nil") + + // IPv4 with no explicit local IP, defaults to CandidateTypeHost + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") + assert.NotNil(t, m.ipv4Mapping.ipSole) + assert.Nil(t, m.ipv6Mapping.ipSole) + assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") + assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + + // IPv4 with no explicit local IP, using CandidateTypeServerReflexive + m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + "1.2.3.4", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + assert.Equal(t, CandidateTypeServerReflexive, m.candidateType, "should match") + assert.NotNil(t, m.ipv4Mapping.ipSole) + assert.Nil(t, m.ipv6Mapping.ipSole) + assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") + assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + + // IPv4 with no explicit local IP, defaults to CandidateTypeHost + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "2601:4567::5678", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") + assert.Nil(t, m.ipv4Mapping.ipSole) + assert.NotNil(t, m.ipv6Mapping.ipSole) + assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") + assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + + // IPv4 and IPv6 in the mix + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4", + "2601:4567::5678", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") + assert.NotNil(t, m.ipv4Mapping.ipSole) + assert.NotNil(t, m.ipv6Mapping.ipSole) + assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") + assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + + // Unsupported candidate type - CandidateTypePeerReflexive + m, err = newExternalIPMapper(CandidateTypePeerReflexive, []string{ + "1.2.3.4", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Unsupported candidate type - CandidateTypeRelay + m, err = newExternalIPMapper(CandidateTypePeerReflexive, []string{ + "1.2.3.4", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Cannot duplicate mapping IPv4 family + m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + "1.2.3.4", + "5.6.7.8", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Cannot duplicate mapping IPv6 family + m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + "2201::1", + "2201::0002", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Invalide external IP string + m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + "bad.2.3.4", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Invalide local IP string + m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + "1.2.3.4/10.0.0.bad", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + }) + + t.Run("newExternalIPMapper with explicit local IP", func(t *testing.T) { + var m *externalIPMapper + var err error + + // IPv4 with explicit local IP, defaults to CandidateTypeHost + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4/10.0.0.1", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") + assert.Nil(t, m.ipv4Mapping.ipSole) + assert.Nil(t, m.ipv6Mapping.ipSole) + assert.Equal(t, 1, len(m.ipv4Mapping.ipMap), "should match") + assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + + // Cannot assign two ext IPs for one local IPv4 + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4/10.0.0.1", + "1.2.3.5/10.0.0.1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Cannot assign two ext IPs for one local IPv6 + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "2200::1/fe80::1", + "2200::0002/fe80::1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Cannot mix different IP family in a pair (1) + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "2200::1/10.0.0.1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Cannot mix different IP family in a pair (2) + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4/fe80::1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Invalid pair + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4/192.168.0.2/10.0.0.1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + }) + + t.Run("newExternalIPMapper with inplicit and explicit local IP", func(t *testing.T) { + // Mixing inpicit and explicit local IPs not allowed + _, err := newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4", + "1.2.3.5/10.0.0.1", + }) + assert.Error(t, err, "should fail") + + // Mixing inpicit and explicit local IPs not allowed + _, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.5/10.0.0.1", + "1.2.3.4", + }) + assert.Error(t, err, "should fail") + }) + + t.Run("findExternalIP without explicit local IP", func(t *testing.T) { + var m *externalIPMapper + var err error + var extIP net.IP + + // IPv4 with explicit local IP, defaults to CandidateTypeHost + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4", + "2200::1", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + assert.NotNil(t, m.ipv4Mapping.ipSole) + assert.NotNil(t, m.ipv6Mapping.ipSole) + + // find external IPv4 + extIP, err = m.findExternalIP("10.0.0.1") + assert.NoError(t, err, "should succeed") + assert.Equal(t, "1.2.3.4", extIP.String(), "should match") + + // find external IPv6 + extIP, err = m.findExternalIP("fe80::0001") // use '0001' instead of '1' on purpse + assert.NoError(t, err, "should succeed") + assert.Equal(t, "2200::1", extIP.String(), "should match") + + // Bad local IP string + _, err = m.findExternalIP("really.bad") + assert.Error(t, err, "should fail") + }) + + t.Run("findExternalIP with explicit local IP", func(t *testing.T) { + var m *externalIPMapper + var err error + var extIP net.IP + + // IPv4 with explicit local IP, defaults to CandidateTypeHost + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4/10.0.0.1", + "1.2.3.5/10.0.0.2", + "2200::1/fe80::1", + "2200::2/fe80::2", + }) + assert.NoError(t, err, "should succeed") + assert.NotNil(t, m, "should not be nil") + + // find external IPv4 + extIP, err = m.findExternalIP("10.0.0.1") + assert.NoError(t, err, "should succeed") + assert.Equal(t, "1.2.3.4", extIP.String(), "should match") + + extIP, err = m.findExternalIP("10.0.0.2") + assert.NoError(t, err, "should succeed") + assert.Equal(t, "1.2.3.5", extIP.String(), "should match") + + _, err = m.findExternalIP("10.0.0.3") + assert.Error(t, err, "should fail") + + // find external IPv6 + extIP, err = m.findExternalIP("fe80::0001") // use '0001' instead of '1' on purpse + assert.NoError(t, err, "should succeed") + assert.Equal(t, "2200::1", extIP.String(), "should match") + + extIP, err = m.findExternalIP("fe80::0002") // use '0002' instead of '2' on purpse + assert.NoError(t, err, "should succeed") + assert.Equal(t, "2200::2", extIP.String(), "should match") + + _, err = m.findExternalIP("fe80::3") + assert.Error(t, err, "should fail") + + // Bad local IP string + _, err = m.findExternalIP("really.bad") + assert.Error(t, err, "should fail") + }) + + t.Run("findExternalIP with empty map", func(t *testing.T) { + var m *externalIPMapper + var err error + + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "1.2.3.4", + }) + assert.NoError(t, err, "should succeed") + + // attempt to find IPv6 that does not exist in the map + _, err = m.findExternalIP("fe80::1") + assert.Error(t, err, "should fail") + + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "2200::1", + }) + assert.NoError(t, err, "should succeed") + + // attempt to find IPv4 that does not exist in the map + _, err = m.findExternalIP("10.0.0.1") + assert.Error(t, err, "should fail") + }) +} diff --git a/gather.go b/gather.go index a540c3b..b6c8808 100644 --- a/gather.go +++ b/gather.go @@ -176,8 +176,17 @@ func (a *Agent) gatherCandidatesLocal(networkTypes []NetworkType) { wg.Add(len(localIPs) * len(supportedNetworks)) for _, ip := range localIPs { + mappedIP := ip + if a.mDNSMode != MulticastDNSModeQueryAndGather && a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost { + if _mappedIP, err := a.extIPMapper.findExternalIP(ip.String()); err == nil { + mappedIP = _mappedIP + } else { + a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s\n", ip.String()) + } + } + for _, network := range supportedNetworks { - go func(network string, ip net.IP) { + go func(network string, ip, mappedIP net.IP) { defer wg.Done() conn, err := a.listenUDP(int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: ip, Port: 0}) if err != nil { @@ -185,7 +194,7 @@ func (a *Agent) gatherCandidatesLocal(networkTypes []NetworkType) { return } - address := ip.String() + address := mappedIP.String() if a.mDNSMode == MulticastDNSModeQueryAndGather { address = a.mDNSName } @@ -201,13 +210,13 @@ func (a *Agent) gatherCandidatesLocal(networkTypes []NetworkType) { c, err := NewCandidateHost(&hostConfig) if err != nil { - a.log.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, ip, port, err) + a.log.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err) return } if a.mDNSMode == MulticastDNSModeQueryAndGather { if err = c.setIP(ip); err != nil { - a.log.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, ip, port, err) + a.log.Warnf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err) return } } @@ -222,55 +231,106 @@ func (a *Agent) gatherCandidatesLocal(networkTypes []NetworkType) { }); err != nil { a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err) } - }(network, ip) + }(network, ip, mappedIP) } } } func (a *Agent) gatherCandidatesSrflx(urls []*URL, networkTypes []NetworkType) { + var stunURLs []*URL + for _, url := range urls { + if url.Scheme == SchemeTypeSTUN { + stunURLs = append(stunURLs, url) + } + } + for _, networkType := range networkTypes { network := networkType.String() - for _, url := range urls { - if url.Scheme != SchemeTypeSTUN { - continue - } + if len(stunURLs) > 0 { + for _, url := range stunURLs { + if url.Scheme != SchemeTypeSTUN { + continue + } - hostPort := fmt.Sprintf("%s:%d", url.Host, url.Port) - serverAddr, err := a.net.ResolveUDPAddr(network, hostPort) - if err != nil { - a.log.Warnf("failed to resolve stun host: %s: %v", hostPort, err) - continue - } + hostPort := fmt.Sprintf("%s:%d", url.Host, url.Port) + serverAddr, err := a.net.ResolveUDPAddr(network, hostPort) + if err != nil { + a.log.Warnf("failed to resolve stun host: %s: %v", hostPort, err) + continue + } + conn, err := a.listenUDP(int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0}) + if err != nil { + a.log.Warnf("Failed to listen for %s: %v\n", serverAddr.String(), err) + continue + } + + xoraddr, err := getXORMappedAddr(conn, serverAddr, stunGatherTimeout) + if err != nil { + a.log.Warnf("could not get server reflexive address %s %s: %v\n", network, url, err) + continue + } + + laddr := conn.LocalAddr().(*net.UDPAddr) + ip := xoraddr.IP + port := xoraddr.Port + relIP := laddr.IP.String() + relPort := laddr.Port + + srflxConfig := CandidateServerReflexiveConfig{ + Network: network, + Address: ip.String(), + Port: port, + Component: ComponentRTP, + RelAddr: relIP, + RelPort: relPort, + } + c, err := NewCandidateServerReflexive(&srflxConfig) + if err != nil { + a.log.Warnf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err) + continue + } + + if err := a.run(func(agent *Agent) { + c.start(a, conn) + a.addCandidate(c) + + if a.onCandidateHdlr != nil { + go a.onCandidateHdlr(c) + } + }); err != nil { + a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err) + } + } + } else if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeServerReflexive { conn, err := a.listenUDP(int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0}) if err != nil { - a.log.Warnf("Failed to listen for %s: %v\n", serverAddr.String(), err) - continue - } - - xoraddr, err := getXORMappedAddr(conn, serverAddr, stunGatherTimeout) - if err != nil { - a.log.Warnf("could not get server reflexive address %s %s: %v\n", network, url, err) + a.log.Warnf("Failed to listen %s: %v\n", network, err) continue } laddr := conn.LocalAddr().(*net.UDPAddr) - ip := xoraddr.IP - port := xoraddr.Port - relIP := laddr.IP.String() - relPort := laddr.Port + mappedIP, err := a.extIPMapper.findExternalIP(laddr.IP.String()) + if err != nil { + a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s\n", laddr.IP.String()) + continue + } srflxConfig := CandidateServerReflexiveConfig{ Network: network, - Address: ip.String(), - Port: port, + Address: mappedIP.String(), + Port: laddr.Port, Component: ComponentRTP, - RelAddr: relIP, - RelPort: relPort, + RelAddr: laddr.IP.String(), + RelPort: laddr.Port, } c, err := NewCandidateServerReflexive(&srflxConfig) if err != nil { - a.log.Warnf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err) + a.log.Warnf("Failed to create server reflexive candidate: %s %s %d: %v\n", + network, + mappedIP.String(), + laddr.Port, + err) continue } diff --git a/gather_vnet_test.go b/gather_vnet_test.go index 67356e2..8fc24e3 100644 --- a/gather_vnet_test.go +++ b/gather_vnet_test.go @@ -1,11 +1,13 @@ package ice import ( + "fmt" "net" "testing" "github.com/pion/logging" "github.com/pion/transport/vnet" + "github.com/stretchr/testify/assert" ) func TestVNetGather(t *testing.T) { @@ -116,6 +118,10 @@ func TestVNetGather(t *testing.T) { } else if conn == nil { t.Fatalf("listenUDP error with no port restriction return a nil conn") } + err = conn.Close() + if err != nil { + t.Fatalf("failed to close conn") + } _, err = a.listenUDP(4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) if err != ErrPort { @@ -135,5 +141,205 @@ func TestVNetGather(t *testing.T) { } else if port != "5000" { t.Fatalf("listenUDP with port restriction of 5000 listened on incorrect port (%s)", port) } + err = conn.Close() + if err != nil { + t.Fatalf("failed to close conn") + } + }) +} + +func TestVNetGatherWithNAT1To1(t *testing.T) { + loggerFactory := logging.NewDefaultLoggerFactory() + log := loggerFactory.NewLogger("test") + + t.Run("gather 1:1 NAT external IPs as host candidates", func(t *testing.T) { + externalIP0 := "1.2.3.4" + externalIP1 := "1.2.3.5" + localIP0 := "10.0.0.1" + localIP1 := "10.0.0.2" + map0 := fmt.Sprintf("%s/%s", externalIP0, localIP0) + map1 := fmt.Sprintf("%s/%s", externalIP1, localIP1) + + wan, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "1.2.3.0/24", + LoggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + lan, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "10.0.0.0/24", + StaticIPs: []string{map0, map1}, + NATType: &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + }, + LoggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + err = wan.AddRouter(lan) + assert.NoError(t, err, "should succeed") + + nw := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{localIP0, localIP1}, + }) + if nw == nil { + t.Fatalf("Failed to create a Net: %s", err) + } + + err = lan.AddNet(nw) + assert.NoError(t, err, "should succeed") + + a, err := NewAgent(&AgentConfig{ + NetworkTypes: []NetworkType{ + NetworkTypeUDP4, + }, + NAT1To1IPs: []string{map0, map1}, + Trickle: true, + Net: nw, + }) + assert.NoError(t, err, "should succeed") + defer a.Close() // nolint:errcheck + + done := make(chan struct{}) + err = a.OnCandidate(func(c Candidate) { + if c == nil { + close(done) + } + }) + assert.NoError(t, err, "should succeed") + + err = a.GatherCandidates() + assert.NoError(t, err, "should succeed") + + log.Debug("wait for gathering is done...") + <-done + log.Debug("gathering is done") + + candidates, err := a.GetLocalCandidates() + assert.NoError(t, err, "should succeed") + + if len(candidates) != 2 { + t.Fatal("There must be two candidates") + } + + laddr := [2]*net.UDPAddr{nil, nil} + for i, candi := range candidates { + laddr[i] = candi.(*CandidateHost).conn.LocalAddr().(*net.UDPAddr) + if candi.Port() != laddr[i].Port { + t.Fatalf("Unexpected candidate port: %d", candi.Port()) + } + } + + if candidates[0].Address() == externalIP0 { + if candidates[1].Address() != externalIP1 { + t.Fatalf("Unexpected candidate IP: %s", candidates[1].Address()) + } + if laddr[0].IP.String() != localIP0 { + t.Fatalf("Unexpected listen IP: %s", laddr[0].IP.String()) + } + if laddr[1].IP.String() != localIP1 { + t.Fatalf("Unexpected listen IP: %s", laddr[1].IP.String()) + } + } else if candidates[0].Address() == externalIP1 { + if candidates[1].Address() != externalIP0 { + t.Fatalf("Unexpected candidate IP: %s", candidates[1].Address()) + } + if laddr[0].IP.String() != localIP1 { + t.Fatalf("Unexpected listen IP: %s", laddr[0].IP.String()) + } + if laddr[1].IP.String() != localIP0 { + t.Fatalf("Unexpected listen IP: %s", laddr[1].IP.String()) + } + } + }) + + t.Run("gather 1:1 NAT external IPs as srflx candidates", func(t *testing.T) { + wan, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "1.2.3.0/24", + LoggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + lan, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "10.0.0.0/24", + StaticIPs: []string{ + "1.2.3.4/10.0.0.1", + }, + NATType: &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + }, + LoggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + err = wan.AddRouter(lan) + assert.NoError(t, err, "should succeed") + + nw := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{ + "10.0.0.1", + }, + }) + if nw == nil { + t.Fatalf("Failed to create a Net: %s", err) + } + + err = lan.AddNet(nw) + assert.NoError(t, err, "should succeed") + + a, err := NewAgent(&AgentConfig{ + NetworkTypes: []NetworkType{ + NetworkTypeUDP4, + }, + NAT1To1IPs: []string{ + "1.2.3.4", + }, + NAT1To1IPCandidateType: CandidateTypeServerReflexive, + Trickle: true, + Net: nw, + }) + assert.NoError(t, err, "should succeed") + defer a.Close() // nolint:errcheck + + done := make(chan struct{}) + err = a.OnCandidate(func(c Candidate) { + if c == nil { + close(done) + } + }) + assert.NoError(t, err, "should succeed") + + err = a.GatherCandidates() + assert.NoError(t, err, "should succeed") + + log.Debug("wait for gathering is done...") + <-done + log.Debug("gathering is done") + + candidates, err := a.GetLocalCandidates() + assert.NoError(t, err, "should succeed") + + if len(candidates) != 2 { + t.Fatalf("Expected two candidates. actually %d", len(candidates)) + } + + var candiHost *CandidateHost + var candiSrflx *CandidateServerReflexive + + for _, candidate := range candidates { + switch candi := candidate.(type) { + case *CandidateHost: + candiHost = candi + case *CandidateServerReflexive: + candiSrflx = candi + default: + t.Fatal("Unexpected candidate type") + } + } + + assert.NotNil(t, candiHost, "should not be nil") + assert.Equal(t, "10.0.0.1", candiHost.Address(), "should match") + assert.NotNil(t, candiSrflx, "should not be nil") + assert.Equal(t, "1.2.3.4", candiSrflx.Address(), "should match") }) } diff --git a/go.mod b/go.mod index e2a0f21..a5930aa 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.12 require ( github.com/pion/logging v0.2.2 github.com/pion/mdns v0.0.3 - github.com/pion/stun v0.3.2 - github.com/pion/transport v0.8.7 - github.com/pion/turn v1.3.6 - github.com/stretchr/testify v1.3.0 + github.com/pion/stun v0.3.3 + github.com/pion/transport v0.8.9 + github.com/pion/turn v1.3.7 + github.com/stretchr/testify v1.4.0 golang.org/x/net v0.0.0-20190628185345-da137c7871d7 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect ) diff --git a/go.sum b/go.sum index dd8f447..25836cc 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,11 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk= github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -10,12 +15,13 @@ github.com/pion/mdns v0.0.3 h1:DxdOYd0pgwLKiDlIIxfU0qdG5iWh1Xn6CsS9vc6cMAY= github.com/pion/mdns v0.0.3/go.mod h1:VrN3wefVgtfL8QgpEblPUC46ag1reLIfpqekCnKunLE= github.com/pion/stun v0.3.2 h1:Vsy6C+bTbJKEC2TH4vHYOnRKmozNPi5FpeKv9/bX16k= github.com/pion/stun v0.3.2/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= -github.com/pion/transport v0.8.6 h1:xHQq2mxAjB+UrFs90aUBaXwlmIACfQAZnOiVAX3uqMw= -github.com/pion/transport v0.8.6/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno= -github.com/pion/transport v0.8.7 h1:t7uYhWOoljd82rnkLH+H2Lw7/IGA5kV9Bl5sWrmcYSc= -github.com/pion/transport v0.8.7/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8= -github.com/pion/turn v1.3.6 h1:N49o5g3SRI5g5Sg8WVft1RDAFxIHOi5roHHdbC39g7g= -github.com/pion/turn v1.3.6/go.mod h1:D8XaX/CVKLkRozV9baRvhZmwOyKAJCTHrj23DdwMI4g= +github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= +github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/transport v0.8.8/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8= +github.com/pion/transport v0.8.9 h1:3PUZULb0WZd/QNfXKKMwcUHzLR+XfNem6lF2M9UrxSU= +github.com/pion/transport v0.8.9/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8= +github.com/pion/turn v1.3.7 h1:/nyM2XrlZILD7KKfnh0oYEBTRG5JlbH21ibjluRoCeo= +github.com/pion/turn v1.3.7/go.mod h1:js0LBFqMcKAlaWAXoYqNjefGI7kfJCrkCBfHGuTToXE= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -23,6 +29,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b h1:lkjdUzSyJ5P1+eal9fxXX9Xg2BTfswsonKUse48C0uE= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -33,3 +41,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=