mirror of
https://github.com/netbirdio/ice.git
synced 2026-05-22 17:10:58 -07:00
@@ -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
|
||||
|
||||
+91
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ type CandidateType byte
|
||||
|
||||
// CandidateType enum
|
||||
const (
|
||||
CandidateTypeHost CandidateType = iota + 1
|
||||
CandidateTypeUnspecified CandidateType = iota
|
||||
CandidateTypeHost
|
||||
CandidateTypeServerReflexive
|
||||
CandidateTypePeerReflexive
|
||||
CandidateTypeRelay
|
||||
|
||||
+161
-38
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user