diff --git a/external_ip_mapper.go b/external_ip_mapper.go index 3d542fb1..58921f1d 100644 --- a/external_ip_mapper.go +++ b/external_ip_mapper.go @@ -18,9 +18,13 @@ func validateIPString(ipStr string) (net.IP, bool, error) { // 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) - valid bool // If not set any external IP, valid is false + // When non-nil, this is the sole external IP for one local IP assumed + ipSole net.IP + // Local-to-external IP mapping (k: local, v: []external). We allow an + // additional external IP if it matches the local IP. + ipMap map[string][]net.IP + // If not set any external IP, valid is false + valid bool } func (m *ipMapping) setSoleIP(ip net.IP) error { @@ -41,32 +45,47 @@ func (m *ipMapping) addIPMapping(locIP, extIP net.IP) error { locIPStr := locIP.String() - // Check if dup of local IP - if _, ok := m.ipMap[locIPStr]; ok { + extIPs, ok := m.ipMap[locIPStr] + // We only allow mapping a local address to either a single external address, + // itself or both. + if ok && len(extIPs) > 1 { return ErrInvalidNAT1To1IPMapping } - m.ipMap[locIPStr] = extIP + // De-duplication check. + for _, ip := range extIPs { + // If the address is external we only allow one. + if locIPStr != extIP.String() && ip.String() != locIPStr { + return ErrInvalidNAT1To1IPMapping + } + + // Otherwise the local IP can only map to itself once. + if ip.String() == extIP.String() { + return ErrInvalidNAT1To1IPMapping + } + } + + m.ipMap[locIPStr] = append(m.ipMap[locIPStr], extIP) m.valid = true return nil } -func (m *ipMapping) findExternalIP(locIP net.IP) (net.IP, error) { +func (m *ipMapping) findExternalIPs(locIP net.IP) ([]net.IP, error) { if !m.valid { - return locIP, nil + return []net.IP{locIP}, nil } if m.ipSole != nil { - return m.ipSole, nil + return []net.IP{m.ipSole}, nil } - extIP, ok := m.ipMap[locIP.String()] - if !ok { + extIPs, ok := m.ipMap[locIP.String()] + if !ok || len(extIPs) == 0 { return nil, ErrExternalMappedIPNotFound } - return extIP, nil + return extIPs, nil } type externalIPMapper struct { @@ -86,8 +105,8 @@ func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIP } m := &externalIPMapper{ - ipv4Mapping: ipMapping{ipMap: map[string]net.IP{}}, - ipv6Mapping: ipMapping{ipMap: map[string]net.IP{}}, + ipv4Mapping: ipMapping{ipMap: map[string][]net.IP{}}, + ipv6Mapping: ipMapping{ipMap: map[string][]net.IP{}}, candidateType: candidateType, } @@ -139,15 +158,15 @@ func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIP return m, nil } -func (m *externalIPMapper) findExternalIP(localIPStr string) (net.IP, error) { +func (m *externalIPMapper) findExternalIPs(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.ipv4Mapping.findExternalIPs(locIP) } - return m.ipv6Mapping.findExternalIP(locIP) + return m.ipv6Mapping.findExternalIPs(locIP) } diff --git a/external_ip_mapper_test.go b/external_ip_mapper_test.go index dbe39ad4..f57eb697 100644 --- a/external_ip_mapper_test.go +++ b/external_ip_mapper_test.go @@ -190,6 +190,22 @@ func TestExternalIPMapper(t *testing.T) { }) assert.Error(t, err, "should fail") assert.Nil(t, m, "should be nil") + + // Cannot have duplicates local IPv4 + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "10.0.0.1/10.0.0.1", + "10.0.0.1/10.0.0.1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") + + // Cannot have duplicates local IPv6 + m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + "2200::1/fe80::1", + "2200::1/fe80::1", + }) + assert.Error(t, err, "should fail") + assert.Nil(t, m, "should be nil") }) t.Run("newExternalIPMapper with implicit and explicit local IP", func(t *testing.T) { @@ -208,10 +224,10 @@ func TestExternalIPMapper(t *testing.T) { assert.Error(t, err, "should fail") }) - t.Run("findExternalIP without explicit local IP", func(t *testing.T) { + t.Run("findExternalIPs without explicit local IP", func(t *testing.T) { var m *externalIPMapper var err error - var extIP net.IP + var extIPs []net.IP // IPv4 with explicit local IP, defaults to CandidateTypeHost m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ @@ -224,24 +240,24 @@ func TestExternalIPMapper(t *testing.T) { assert.NotNil(t, m.ipv6Mapping.ipSole) // Find external IPv4 - extIP, err = m.findExternalIP("10.0.0.1") + extIPs, err = m.findExternalIPs("10.0.0.1") assert.NoError(t, err, "should succeed") - assert.Equal(t, "1.2.3.4", extIP.String(), "should match") + assert.Equal(t, "1.2.3.4", extIPs[0].String(), "should match") // Find external IPv6 - extIP, err = m.findExternalIP("fe80::0001") // Use '0001' instead of '1' on purpose + extIPs, err = m.findExternalIPs("fe80::0001") // Use '0001' instead of '1' on purpose assert.NoError(t, err, "should succeed") - assert.Equal(t, "2200::1", extIP.String(), "should match") + assert.Equal(t, "2200::1", extIPs[0].String(), "should match") // Bad local IP string - _, err = m.findExternalIP("really.bad") + _, err = m.findExternalIPs("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 + var extIPs []net.IP // IPv4 with explicit local IP, defaults to CandidateTypeHost m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ @@ -254,31 +270,31 @@ func TestExternalIPMapper(t *testing.T) { assert.NotNil(t, m, "should not be nil") // Find external IPv4 - extIP, err = m.findExternalIP("10.0.0.1") + extIPs, err = m.findExternalIPs("10.0.0.1") assert.NoError(t, err, "should succeed") - assert.Equal(t, "1.2.3.4", extIP.String(), "should match") + assert.Equal(t, "1.2.3.4", extIPs[0].String(), "should match") - extIP, err = m.findExternalIP("10.0.0.2") + extIPs, err = m.findExternalIPs("10.0.0.2") assert.NoError(t, err, "should succeed") - assert.Equal(t, "1.2.3.5", extIP.String(), "should match") + assert.Equal(t, "1.2.3.5", extIPs[0].String(), "should match") - _, err = m.findExternalIP("10.0.0.3") + _, err = m.findExternalIPs("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 purpose + extIPs, err = m.findExternalIPs("fe80::0001") // Use '0001' instead of '1' on purpose assert.NoError(t, err, "should succeed") - assert.Equal(t, "2200::1", extIP.String(), "should match") + assert.Equal(t, "2200::1", extIPs[0].String(), "should match") - extIP, err = m.findExternalIP("fe80::0002") // Use '0002' instead of '2' on purpose + extIPs, err = m.findExternalIPs("fe80::0002") // Use '0002' instead of '2' on purpose assert.NoError(t, err, "should succeed") - assert.Equal(t, "2200::2", extIP.String(), "should match") + assert.Equal(t, "2200::2", extIPs[0].String(), "should match") - _, err = m.findExternalIP("fe80::3") + _, err = m.findExternalIPs("fe80::3") assert.Error(t, err, "should fail") // Bad local IP string - _, err = m.findExternalIP("really.bad") + _, err = m.findExternalIPs("really.bad") assert.Error(t, err, "should fail") }) @@ -292,9 +308,9 @@ func TestExternalIPMapper(t *testing.T) { assert.NoError(t, err, "should succeed") // Attempt to find IPv6 that does not exist in the map - extIP, err := m.findExternalIP("fe80::1") + extIPs, err := m.findExternalIPs("fe80::1") assert.NoError(t, err, "should succeed") - assert.Equal(t, "fe80::1", extIP.String(), "should match") + assert.Equal(t, "fe80::1", extIPs[0].String(), "should match") m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "2200::1", @@ -302,8 +318,8 @@ func TestExternalIPMapper(t *testing.T) { assert.NoError(t, err, "should succeed") // Attempt to find IPv4 that does not exist in the map - extIP, err = m.findExternalIP("10.0.0.1") + extIPs, err = m.findExternalIPs("10.0.0.1") assert.NoError(t, err, "should succeed") - assert.Equal(t, "10.0.0.1", extIP.String(), "should match") + assert.Equal(t, "10.0.0.1", extIPs[0].String(), "should match") }) } diff --git a/gather.go b/gather.go index 8d2ce624..fb2ca65f 100644 --- a/gather.go +++ b/gather.go @@ -140,112 +140,114 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ } for _, ip := range localIPs { - mappedIP := ip + mappedIPs := []net.IP{ip} if a.mDNSMode != MulticastDNSModeQueryAndGather && a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost { - if _mappedIP, innerErr := a.extIPMapper.findExternalIP(ip.String()); innerErr == nil { - mappedIP = _mappedIP + if _mappedIPs, innerErr := a.extIPMapper.findExternalIPs(ip.String()); innerErr == nil { + mappedIPs = _mappedIPs } else { a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s", ip.String()) } } - address := mappedIP.String() - if a.mDNSMode == MulticastDNSModeQueryAndGather { - address = a.mDNSName - } - - for network := range networks { - type connAndPort struct { - conn net.PacketConn - port int + for _, mappedIP := range mappedIPs { + address := mappedIP.String() + if a.mDNSMode == MulticastDNSModeQueryAndGather { + address = a.mDNSName } - var ( - conns []connAndPort - tcpType TCPType - ) - switch network { - case tcp: - if a.tcpMux == nil { - continue + for network := range networks { + type connAndPort struct { + conn net.PacketConn + port int } + var ( + conns []connAndPort + tcpType TCPType + ) - // Handle ICE TCP passive mode - var muxConns []net.PacketConn - if multi, ok := a.tcpMux.(AllConnsGetter); ok { - a.log.Debugf("GetAllConns by ufrag: %s", a.localUfrag) - muxConns, err = multi.GetAllConns(a.localUfrag, mappedIP.To4() == nil, ip) - if err != nil { - a.log.Warnf("Failed to get all TCP connections by ufrag: %s %s %s", network, ip, a.localUfrag) + switch network { + case tcp: + if a.tcpMux == nil { continue } - } else { - a.log.Debugf("GetConn by ufrag: %s", a.localUfrag) - conn, err := a.tcpMux.GetConnByUfrag(a.localUfrag, mappedIP.To4() == nil, ip) + + // Handle ICE TCP passive mode + var muxConns []net.PacketConn + if multi, ok := a.tcpMux.(AllConnsGetter); ok { + a.log.Debugf("GetAllConns by ufrag: %s", a.localUfrag) + muxConns, err = multi.GetAllConns(a.localUfrag, mappedIP.To4() == nil, ip) + if err != nil { + a.log.Warnf("Failed to get all TCP connections by ufrag: %s %s %s", network, ip, a.localUfrag) + continue + } + } else { + a.log.Debugf("GetConn by ufrag: %s", a.localUfrag) + conn, err := a.tcpMux.GetConnByUfrag(a.localUfrag, mappedIP.To4() == nil, ip) + if err != nil { + a.log.Warnf("Failed to get TCP connections by ufrag: %s %s %s", network, ip, a.localUfrag) + continue + } + muxConns = []net.PacketConn{conn} + } + + // Extract the port for each PacketConn we got. + for _, conn := range muxConns { + if tcpConn, ok := conn.LocalAddr().(*net.TCPAddr); ok { + conns = append(conns, connAndPort{conn, tcpConn.Port}) + } else { + a.log.Warnf("Failed to get port of connection from TCPMux: %s %s %s", network, ip, a.localUfrag) + } + } + if len(conns) == 0 { + // Didn't succeed with any, try the next network. + continue + } + tcpType = TCPTypePassive + // 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 { - a.log.Warnf("Failed to get TCP connections by ufrag: %s %s %s", network, ip, a.localUfrag) + a.log.Warnf("Failed to listen %s %s", network, ip) continue } - muxConns = []net.PacketConn{conn} - } - // Extract the port for each PacketConn we got. - for _, conn := range muxConns { - if tcpConn, ok := conn.LocalAddr().(*net.TCPAddr); ok { - conns = append(conns, connAndPort{conn, tcpConn.Port}) + if udpConn, ok := conn.LocalAddr().(*net.UDPAddr); ok { + conns = append(conns, connAndPort{conn, udpConn.Port}) } else { - a.log.Warnf("Failed to get port of connection from TCPMux: %s %s %s", network, ip, a.localUfrag) + a.log.Warnf("Failed to get port of UDPAddr from ListenUDPInPortRange: %s %s %s", network, ip, a.localUfrag) + continue } } - if len(conns) == 0 { - // Didn't succeed with any, try the next network. - continue - } - tcpType = TCPTypePassive - // 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 { - a.log.Warnf("Failed to listen %s %s", network, ip) - continue - } - if udpConn, ok := conn.LocalAddr().(*net.UDPAddr); ok { - conns = append(conns, connAndPort{conn, udpConn.Port}) - } else { - a.log.Warnf("Failed to get port of UDPAddr from ListenUDPInPortRange: %s %s %s", network, ip, a.localUfrag) - continue - } - } - - for _, connAndPort := range conns { - hostConfig := CandidateHostConfig{ - Network: network, - Address: address, - Port: connAndPort.port, - Component: ComponentRTP, - TCPType: tcpType, - } - - c, err := NewCandidateHost(&hostConfig) - if err != nil { - closeConnAndLog(connAndPort.conn, a.log, "failed to create host candidate: %s %s %d: %v", network, mappedIP, connAndPort.port, err) - continue - } + for _, connAndPort := range conns { + hostConfig := CandidateHostConfig{ + Network: network, + Address: address, + Port: connAndPort.port, + Component: ComponentRTP, + TCPType: tcpType, + } - if a.mDNSMode == MulticastDNSModeQueryAndGather { - if err = c.setIP(ip); err != nil { + c, err := NewCandidateHost(&hostConfig) + if err != nil { closeConnAndLog(connAndPort.conn, a.log, "failed to create host candidate: %s %s %d: %v", network, mappedIP, connAndPort.port, err) continue } - } - if err := a.addCandidate(ctx, c, connAndPort.conn); err != nil { - if closeErr := c.close(); closeErr != nil { - a.log.Warnf("Failed to close candidate: %v", closeErr) + if a.mDNSMode == MulticastDNSModeQueryAndGather { + if err = c.setIP(ip); err != nil { + closeConnAndLog(connAndPort.conn, a.log, "failed to create host candidate: %s %s %d: %v", network, mappedIP, connAndPort.port, err) + continue + } + } + + if err := a.addCandidate(ctx, c, connAndPort.conn); err != nil { + if closeErr := c.close(); closeErr != nil { + a.log.Warnf("Failed to close candidate: %v", closeErr) + } + a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v", err) } - a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v", err) } } } @@ -265,53 +267,55 @@ func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolin if !ok { return errInvalidAddress } - candidateIP := udpAddr.IP + candidateIPs := []net.IP{udpAddr.IP} if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost { - mappedIP, err := a.extIPMapper.findExternalIP(candidateIP.String()) + mappedIPs, err := a.extIPMapper.findExternalIPs(candidateIPs[0].String()) if err != nil { - a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s", candidateIP.String()) + a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s", candidateIPs[0].String()) continue } - candidateIP = mappedIP + candidateIPs = mappedIPs } - hostConfig := CandidateHostConfig{ - Network: udp, - Address: candidateIP.String(), - Port: udpAddr.Port, - Component: ComponentRTP, - } + for _, candidateIP := range candidateIPs { + hostConfig := CandidateHostConfig{ + Network: udp, + Address: candidateIP.String(), + Port: udpAddr.Port, + Component: ComponentRTP, + } - // Detect a duplicate candidate before calling addCandidate(). - // otherwise, addCandidate() detects the duplicate candidate - // and close its connection, invalidating all candidates - // that share the same connection. - if _, ok := existingConfigs[hostConfig]; ok { - continue - } + // Detect a duplicate candidate before calling addCandidate(). + // otherwise, addCandidate() detects the duplicate candidate + // and close its connection, invalidating all candidates + // that share the same connection. + if _, ok := existingConfigs[hostConfig]; ok { + continue + } - conn, err := a.udpMux.GetConn(a.localUfrag, udpAddr) - if err != nil { - return err - } + conn, err := a.udpMux.GetConn(a.localUfrag, udpAddr) + if err != nil { + return err + } - c, err := NewCandidateHost(&hostConfig) - if err != nil { - closeConnAndLog(conn, a.log, "failed to create host mux candidate: %s %d: %v", candidateIP, udpAddr.Port, err) - continue - } + c, err := NewCandidateHost(&hostConfig) + if err != nil { + closeConnAndLog(conn, a.log, "failed to create host mux candidate: %s %d: %v", candidateIP, udpAddr.Port, err) + continue + } + + if err := a.addCandidate(ctx, c, conn); err != nil { + if closeErr := c.close(); closeErr != nil { + a.log.Warnf("Failed to close candidate: %v", closeErr) + } - if err := a.addCandidate(ctx, c, conn); err != nil { - if closeErr := c.close(); closeErr != nil { - a.log.Warnf("Failed to close candidate: %v", closeErr) + closeConnAndLog(conn, a.log, "failed to add candidate: %s %d: %v", candidateIP, udpAddr.Port, err) + continue } - closeConnAndLog(conn, a.log, "failed to add candidate: %s %d: %v", candidateIP, udpAddr.Port, err) - continue + existingConfigs[hostConfig] = struct{}{} } - - existingConfigs[hostConfig] = struct{}{} } return nil @@ -343,35 +347,37 @@ func (a *Agent) gatherCandidatesSrflxMapped(ctx context.Context, networkTypes [] return } - mappedIP, err := a.extIPMapper.findExternalIP(lAddr.IP.String()) + mappedIPs, err := a.extIPMapper.findExternalIPs(lAddr.IP.String()) if err != nil { closeConnAndLog(conn, a.log, "1:1 NAT mapping is enabled but no external IP is found for %s", lAddr.IP.String()) return } - srflxConfig := CandidateServerReflexiveConfig{ - Network: network, - Address: mappedIP.String(), - Port: lAddr.Port, - Component: ComponentRTP, - RelAddr: lAddr.IP.String(), - RelPort: lAddr.Port, - } - c, err := NewCandidateServerReflexive(&srflxConfig) - if err != nil { - closeConnAndLog(conn, a.log, "failed to create server reflexive candidate: %s %s %d: %v", - network, - mappedIP.String(), - lAddr.Port, - err) - return - } + for _, mappedIP := range mappedIPs { + srflxConfig := CandidateServerReflexiveConfig{ + Network: network, + Address: mappedIP.String(), + Port: lAddr.Port, + Component: ComponentRTP, + RelAddr: lAddr.IP.String(), + RelPort: lAddr.Port, + } + c, err := NewCandidateServerReflexive(&srflxConfig) + if err != nil { + closeConnAndLog(conn, a.log, "failed to create server reflexive candidate: %s %s %d: %v", + network, + mappedIP.String(), + lAddr.Port, + err) + return + } - if err := a.addCandidate(ctx, c, conn); err != nil { - if closeErr := c.close(); closeErr != nil { - a.log.Warnf("Failed to close candidate: %v", closeErr) + if err := a.addCandidate(ctx, c, conn); err != nil { + if closeErr := c.close(); closeErr != nil { + a.log.Warnf("Failed to close candidate: %v", closeErr) + } + a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v", err) } - a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v", err) } }() } diff --git a/gather_vnet_test.go b/gather_vnet_test.go index 5410bc2e..2d7b47d9 100644 --- a/gather_vnet_test.go +++ b/gather_vnet_test.go @@ -18,6 +18,7 @@ import ( "github.com/pion/transport/v3/test" "github.com/pion/transport/v3/vnet" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestVNetGather(t *testing.T) { @@ -357,6 +358,83 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { assert.NotNil(t, candiSrflx, "should not be nil") assert.Equal(t, "1.2.3.4", candiSrflx.Address(), "should match") }) + + t.Run("gather both local and overridden candidates", func(t *testing.T) { + localIP := "10.0.0.1" + externalIP := "1.1.1.1" + + // Local network setup + lan, err := vnet.NewRouter(&vnet.RouterConfig{ + CIDR: "10.0.0.0/24", + StaticIPs: []string{localIP}, + NATType: &vnet.NATType{ + Mode: vnet.NATModeNAT1To1, + }, + LoggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + localNet, err := vnet.NewNet(&vnet.NetConfig{ + StaticIPs: []string{localIP}, + }) + assert.NoError(t, err) + + err = lan.AddNet(localNet) + assert.NoError(t, err, "should succeed") + + natMap := []string{ + fmt.Sprintf("%s/%s", externalIP, localIP), + fmt.Sprintf("%s/%s", localIP, localIP), + } + + // ICE gathering + a, err := NewAgent(&AgentConfig{ + NetworkTypes: []NetworkType{ + NetworkTypeUDP4, + }, + NAT1To1IPs: natMap, + NAT1To1IPCandidateType: CandidateTypeHost, + Net: localNet, + }) + 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 until gathering is complete...") + <-done + log.Debug("Gathering is done") + + candidates, err := a.GetLocalCandidates() + assert.NoError(t, err, "should succeed") + + require.Len(t, candidates, 2) + + if candidates[0].Address() == externalIP { + require.Equal(t, localIP, candidates[1].Address()) + } else { + require.Equal(t, localIP, candidates[0].Address()) + require.Equal(t, externalIP, candidates[1].Address()) + } + + lAddr := [2]*net.UDPAddr{nil, nil} + for i, candi := range candidates { + lAddr[i] = candi.(*CandidateHost).conn.LocalAddr().(*net.UDPAddr) //nolint:forcetypeassert + require.Equal(t, lAddr[i].Port, candi.Port()) + } + + require.Equal(t, lAddr[0].IP.String(), lAddr[1].IP.String()) + require.NotEqual(t, lAddr[0].Port, lAddr[1].Port) + }) } func TestVNetGatherWithInterfaceFilter(t *testing.T) {