@@ -51,6 +51,30 @@ type blockingWritePacketConn struct {
5151 closeOnce sync.Once
5252}
5353
54+ type localCandidatePacketConn struct {
55+ addr net.Addr
56+ closeCount atomic.Int32
57+ }
58+
59+ func (c * localCandidatePacketConn ) ReadFrom ([]byte ) (int , net.Addr , error ) {
60+ return 0 , c .addr , io .EOF
61+ }
62+
63+ func (c * localCandidatePacketConn ) WriteTo (payload []byte , _ net.Addr ) (int , error ) {
64+ return len (payload ), nil
65+ }
66+
67+ func (c * localCandidatePacketConn ) Close () error {
68+ c .closeCount .Add (1 )
69+
70+ return nil
71+ }
72+
73+ func (c * localCandidatePacketConn ) LocalAddr () net.Addr { return c .addr }
74+ func (c * localCandidatePacketConn ) SetDeadline (time.Time ) error { return nil }
75+ func (c * localCandidatePacketConn ) SetReadDeadline (time.Time ) error { return nil }
76+ func (c * localCandidatePacketConn ) SetWriteDeadline (time.Time ) error { return nil }
77+
5478func newBlockingWritePacketConn () * blockingWritePacketConn {
5579 return & blockingWritePacketConn {
5680 writeStarted : make (chan struct {}),
@@ -2711,6 +2735,143 @@ func TestAddRemoteCandidateHonorsRemoteIPFilter(t *testing.T) {
27112735 }, time .Second , 10 * time .Millisecond )
27122736}
27132737
2738+ func TestAddLocalCandidateRegistersExternalRelay (t * testing.T ) {
2739+ agent , err := NewAgentWithOptions (
2740+ WithCandidateTypes ([]CandidateType {}),
2741+ WithMulticastDNSMode (MulticastDNSModeDisabled ),
2742+ )
2743+ require .NoError (t , err )
2744+ defer func () { require .NoError (t , agent .Close ()) }()
2745+
2746+ gatheringComplete := make (chan struct {})
2747+ candidates := make (chan Candidate , 1 )
2748+ require .NoError (t , agent .OnCandidate (func (candidate Candidate ) {
2749+ if candidate == nil {
2750+ close (gatheringComplete )
2751+
2752+ return
2753+ }
2754+ candidates <- candidate
2755+ }))
2756+
2757+ candidate , err := NewCandidateRelay (& CandidateRelayConfig {
2758+ Network : NetworkTypeUDP4 .String (),
2759+ Address : "192.0.2.10" ,
2760+ Port : 5000 ,
2761+ Component : ComponentRTP ,
2762+ RelayProtocol : "custom" ,
2763+ })
2764+ require .NoError (t , err )
2765+ packetConn := & localCandidatePacketConn {
2766+ addr : & net.UDPAddr {IP : net .IPv4 (192 , 0 , 2 , 10 ), Port : 5000 },
2767+ }
2768+
2769+ require .NoError (t , agent .AddLocalCandidate (candidate , packetConn ))
2770+ localCandidates , err := agent .GetLocalCandidates ()
2771+ require .NoError (t , err )
2772+ require .Len (t , localCandidates , 1 )
2773+ relayCandidate , ok := localCandidates [0 ].(* CandidateRelay )
2774+ require .True (t , ok )
2775+ require .Equal (t , "custom" , relayCandidate .RelayProtocol ())
2776+
2777+ select {
2778+ case got := <- candidates :
2779+ require .Equal (t , candidate , got )
2780+ case <- time .After (time .Second ):
2781+ require .FailNow (t , "timed out waiting for local candidate callback" )
2782+ }
2783+
2784+ require .NoError (t , agent .GatherCandidates ())
2785+ select {
2786+ case <- gatheringComplete :
2787+ case <- time .After (time .Second ):
2788+ require .FailNow (t , "timed out waiting for gathering completion" )
2789+ }
2790+ }
2791+
2792+ func TestAddLocalCandidateRejectsNilPacketConn (t * testing.T ) {
2793+ agent , err := NewAgentWithOptions (WithMulticastDNSMode (MulticastDNSModeDisabled ))
2794+ require .NoError (t , err )
2795+ defer func () { require .NoError (t , agent .Close ()) }()
2796+
2797+ candidate , err := NewCandidateRelay (& CandidateRelayConfig {
2798+ Network : NetworkTypeUDP4 .String (),
2799+ Address : "192.0.2.11" ,
2800+ Port : 5001 ,
2801+ Component : ComponentRTP ,
2802+ })
2803+ require .NoError (t , err )
2804+ require .ErrorIs (t , agent .AddLocalCandidate (candidate , nil ), ErrCandidatePacketConnNil )
2805+ }
2806+
2807+ func TestAddLocalCandidateRejectsDuplicateWithoutClosing (t * testing.T ) {
2808+ agent , err := NewAgentWithOptions (WithMulticastDNSMode (MulticastDNSModeDisabled ))
2809+ require .NoError (t , err )
2810+ defer func () { require .NoError (t , agent .Close ()) }()
2811+
2812+ require .NoError (t , agent .OnCandidate (func (Candidate ) {}))
2813+ var candidateCloseCount atomic.Int32
2814+ candidate , err := NewCandidateRelay (& CandidateRelayConfig {
2815+ Network : NetworkTypeUDP4 .String (),
2816+ Address : "192.0.2.12" ,
2817+ Port : 5002 ,
2818+ Component : ComponentRTP ,
2819+ OnClose : func () error {
2820+ candidateCloseCount .Add (1 )
2821+
2822+ return nil
2823+ },
2824+ })
2825+ require .NoError (t , err )
2826+ packetConn := & localCandidatePacketConn {
2827+ addr : & net.UDPAddr {IP : net .IPv4 (192 , 0 , 2 , 12 ), Port : 5002 },
2828+ }
2829+
2830+ require .NoError (t , agent .AddLocalCandidate (candidate , packetConn ))
2831+ require .ErrorIs (t , agent .AddLocalCandidate (candidate , packetConn ), ErrDuplicateCandidate )
2832+ require .Zero (t , candidateCloseCount .Load ())
2833+ require .Zero (t , packetConn .closeCount .Load ())
2834+
2835+ localCandidates , err := agent .GetLocalCandidates ()
2836+ require .NoError (t , err )
2837+ require .Equal (t , []Candidate {candidate }, localCandidates )
2838+ }
2839+
2840+ func TestAddCandidateClosesDuplicate (t * testing.T ) {
2841+ agent , err := NewAgentWithOptions (WithMulticastDNSMode (MulticastDNSModeDisabled ))
2842+ require .NoError (t , err )
2843+ defer func () { require .NoError (t , agent .Close ()) }()
2844+
2845+ require .NoError (t , agent .OnCandidate (func (Candidate ) {}))
2846+ config := CandidateRelayConfig {
2847+ Network : NetworkTypeUDP4 .String (),
2848+ Address : "192.0.2.13" ,
2849+ Port : 5003 ,
2850+ Component : ComponentRTP ,
2851+ }
2852+ first , err := NewCandidateRelay (& config )
2853+ require .NoError (t , err )
2854+ var duplicateCloseCount atomic.Int32
2855+ config .OnClose = func () error {
2856+ duplicateCloseCount .Add (1 )
2857+
2858+ return nil
2859+ }
2860+ duplicate , err := NewCandidateRelay (& config )
2861+ require .NoError (t , err )
2862+ firstConn := & localCandidatePacketConn {
2863+ addr : & net.UDPAddr {IP : net .IPv4 (192 , 0 , 2 , 13 ), Port : 5003 },
2864+ }
2865+ duplicateConn := & localCandidatePacketConn {
2866+ addr : & net.UDPAddr {IP : net .IPv4 (192 , 0 , 2 , 13 ), Port : 5003 },
2867+ }
2868+
2869+ require .NoError (t , agent .addCandidate (context .Background (), first , firstConn , false ))
2870+ require .NoError (t , agent .addCandidate (context .Background (), duplicate , duplicateConn , false ))
2871+ require .Equal (t , int32 (1 ), duplicateCloseCount .Load ())
2872+ require .Equal (t , int32 (1 ), duplicateConn .closeCount .Load ())
2873+ }
2874+
27142875func TestGetLocalCandidates (t * testing.T ) {
27152876 var config AgentConfig
27162877
@@ -2736,7 +2897,7 @@ func TestGetLocalCandidates(t *testing.T) {
27362897
27372898 expectedCandidates = append (expectedCandidates , cand )
27382899
2739- err = agent .addCandidate (context .Background (), cand , dummyConn )
2900+ err = agent .addCandidate (context .Background (), cand , dummyConn , false )
27402901 require .NoError (t , err )
27412902 }
27422903
@@ -3376,7 +3537,7 @@ func TestSetCandidatesUfrag(t *testing.T) {
33763537 cand , errCand := NewCandidateHost (& cfg )
33773538 require .NoError (t , errCand )
33783539
3379- err = agent .addCandidate (context .Background (), cand , dummyConn )
3540+ err = agent .addCandidate (context .Background (), cand , dummyConn , false )
33803541 require .NoError (t , err )
33813542 }
33823543
0 commit comments