diff --git a/.travis.yml b/.travis.yml index b96a1ed..ea8b6fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ env: - GO111MODULE=on before_script: - - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.15.0 + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.17.1 script: - golangci-lint run ./... diff --git a/README.md b/README.md index e321224..c4a6714 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu * [Yutaka Takeda](https://github.com/enobufs) * [Atsushi Watanabe](https://github.com/at-wat) * [Robert Eperjesi](https://github.com/epes) +* [Sebastian Waisbrot](https://github.com/seppo0010) ### License MIT License - see [LICENSE](LICENSE) for full text diff --git a/agent.go b/agent.go index 11dc448..27abc58 100644 --- a/agent.go +++ b/agent.go @@ -85,6 +85,7 @@ type Agent struct { trickle bool tieBreaker uint64 + lite bool connectionState ConnectionState gatheringState GatheringState @@ -218,6 +219,9 @@ type AgentConfig struct { // or mark the connection as failed if no valid candidate is available CandidateSelectionTimeout *time.Duration + // Lite agents do not perform connectivity check and only provide host candidates. + Lite bool + // HostAcceptanceMinWait specify a minimum wait time before selecting host candidates HostAcceptanceMinWait *time.Duration // HostAcceptanceMinWait specify a minimum wait time before selecting srflx candidates @@ -232,6 +236,49 @@ type AgentConfig struct { Net *vnet.Net } +func containsCandidateType(candidateType CandidateType, candidateTypeList []CandidateType) bool { + if candidateTypeList == nil { + return false + } + for _, ct := range candidateTypeList { + if ct == candidateType { + return true + } + } + return false +} + +func createMulticastDNS(mDNSMode MulticastDNSMode, mDNSName string, log logging.LeveledLogger) (*mdns.Conn, MulticastDNSMode, error) { + if mDNSMode == MulticastDNSModeDisabled { + return nil, mDNSMode, nil + } + + addr, mdnsErr := net.ResolveUDPAddr("udp4", mdns.DefaultAddress) + if mdnsErr != nil { + return nil, mDNSMode, mdnsErr + } + + l, mdnsErr := net.ListenUDP("udp4", addr) + if mdnsErr != nil { + // If ICE fails to start MulticastDNS server just warn the user and continue + log.Errorf("Failed to enable mDNS, continuing in mDNS disabled mode: (%s)", mdnsErr) + return nil, MulticastDNSModeDisabled, nil + } + + switch mDNSMode { + case MulticastDNSModeQueryOnly: + conn, err := mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{}) + return conn, mDNSMode, err + case MulticastDNSModeQueryAndGather: + conn, err := mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{ + LocalNames: []string{mDNSName}, + }) + return conn, mDNSMode, err + default: + return nil, mDNSMode, nil + } +} + // NewAgent creates a new Agent func NewAgent(config *AgentConfig) (*Agent, error) { if config.PortMax < config.PortMin { @@ -255,41 +302,14 @@ func NewAgent(config *AgentConfig) (*Agent, error) { log := loggerFactory.NewLogger("ice") var mDNSConn *mdns.Conn - mDNSConn, err = func() (*mdns.Conn, error) { - if mDNSMode == MulticastDNSModeDisabled { - return nil, nil - } - - addr, mdnsErr := net.ResolveUDPAddr("udp4", mdns.DefaultAddress) - if mdnsErr != nil { - return nil, mdnsErr - } - - l, mdnsErr := net.ListenUDP("udp4", addr) - if mdnsErr != nil { - // If ICE fails to start MulticastDNS server just warn the user and continue - log.Errorf("Failed to enable mDNS, continuing in mDNS disabled mode: (%s)", mdnsErr) - mDNSMode = MulticastDNSModeDisabled - return nil, nil - } - - switch mDNSMode { - case MulticastDNSModeQueryOnly: - return mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{}) - case MulticastDNSModeQueryAndGather: - return mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{ - LocalNames: []string{mDNSName}, - }) - default: - return nil, nil - } - }() + mDNSConn, mDNSMode, err = createMulticastDNS(mDNSMode, mDNSName, log) if err != nil { return nil, err } a := &Agent{ tieBreaker: rand.New(rand.NewSource(time.Now().UnixNano())).Uint64(), + lite: config.Lite, gatheringState: GatheringStateNew, connectionState: ConnectionStateNew, localCandidates: make(map[NetworkType][]Candidate), @@ -395,6 +415,14 @@ func NewAgent(config *AgentConfig) (*Agent, error) { a.candidateTypes = config.CandidateTypes } + 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 + } + go a.taskLoop() // Initialize local candidates @@ -459,6 +487,10 @@ func (a *Agent) startConnectivityChecks(isControlling bool, remoteUfrag, remoteP a.selector = &controlledSelector{agent: a, log: a.log} } + if a.lite { + a.selector = &liteSelector{pairCandidateSelector: a.selector} + } + a.selector.Start() agent.updateConnectionState(ConnectionStateChecking) @@ -967,7 +999,7 @@ func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr) } remoteCandidate = prflxCandidate - a.log.Debugf("adding a new peer-reflexive candiate: %s ", remote) + a.log.Debugf("adding a new peer-reflexive candidate: %s ", remote) a.addRemoteCandidate(remoteCandidate) } diff --git a/agent_test.go b/agent_test.go index 93c130a..196ea72 100644 --- a/agent_test.go +++ b/agent_test.go @@ -432,6 +432,81 @@ func TestConnectivityOnStartup(t *testing.T) { } } +func TestConnectivityLite(t *testing.T) { + stunServerURL := &URL{ + Scheme: SchemeTypeSTUN, + Host: "1.2.3.4", + Port: 3478, + Proto: ProtoTypeUDP, + } + + v, err := buildVNet(&vnet.NATType{ + MappingBehavior: vnet.EndpointIndependent, + FilteringBehavior: vnet.EndpointIndependent, + }) + require.NoError(t, err, "should succeed") + defer v.close() + + aNotifier, aConnected := onConnected() + bNotifier, bConnected := onConnected() + + var wg sync.WaitGroup + wg.Add(2) + + cfg0 := &AgentConfig{ + Urls: []*URL{stunServerURL}, + Trickle: true, + NetworkTypes: supportedNetworkTypes, + MulticastDNSMode: MulticastDNSModeDisabled, + Net: v.net0, + + taskLoopInterval: time.Hour, + } + + aAgent, err := NewAgent(cfg0) + require.NoError(t, err) + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) + require.NoError(t, aAgent.OnCandidate(func(candidate Candidate) { + if candidate == nil { + wg.Done() + } + })) + require.NoError(t, aAgent.GatherCandidates()) + + cfg1 := &AgentConfig{ + Urls: []*URL{}, + Trickle: true, + Lite: true, + CandidateTypes: []CandidateType{CandidateTypeHost}, + NetworkTypes: supportedNetworkTypes, + MulticastDNSMode: MulticastDNSModeDisabled, + Net: v.net1, + taskLoopInterval: time.Hour, + } + + bAgent, err := NewAgent(cfg1) + require.NoError(t, err) + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) + require.NoError(t, bAgent.OnCandidate(func(candidate Candidate) { + if candidate == nil { + wg.Done() + } + })) + require.NoError(t, bAgent.GatherCandidates()) + + wg.Wait() + aConn, bConn := connectWithVNet(aAgent, bAgent) + + // Ensure pair selected + // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair + <-aConnected + <-bConnected + + if !closePipe(t, aConn, bConn) { + return + } +} + func TestInboundValidity(t *testing.T) { buildMsg := func(class stun.MessageClass, username, key string) *stun.Message { msg, err := stun.Build(stun.NewType(stun.MethodBinding, class), stun.TransactionID, diff --git a/candidatepair_state.go b/candidatepair_state.go index f76396c..28c7187 100644 --- a/candidatepair_state.go +++ b/candidatepair_state.go @@ -31,7 +31,7 @@ func (c CandidatePairState) String() string { case CandidatePairStateFailed: return "failed" case CandidatePairStateSucceeded: - return "succeded" + return "succeeded" } return "Unknown candidate pair state" } diff --git a/errors.go b/errors.go index 01e5e27..876d1cb 100644 --- a/errors.go +++ b/errors.go @@ -57,4 +57,11 @@ var ( // ErrAddressParseFailed indicates we were unable to parse a candidate address ErrAddressParseFailed = errors.New("failed to parse address") + + // ErrLiteUsingNonHostCandidates indicates non host candidates were selected for a lite agent + ErrLiteUsingNonHostCandidates = errors.New("lite agents must only use host candidates") + + // 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") ) diff --git a/selection.go b/selection.go index 8560a8e..c313829 100644 --- a/selection.go +++ b/selection.go @@ -298,3 +298,17 @@ func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote s.PingCandidate(local, remote) } } + +type liteSelector struct { + pairCandidateSelector +} + +// A lite selector should not contact candidates +func (s *liteSelector) ContactCandidates() { + if _, ok := s.pairCandidateSelector.(*controllingSelector); ok { + // pion/ice#96 + // TODO: implement lite controlling agent. For now falling back to full agent. + // This only happens if both peers are lite. See RFC 8445 S6.1.1 and S6.2 + s.pairCandidateSelector.ContactCandidates() + } +}