@@ -1491,3 +1491,182 @@ func TestControllingSideRenomination(t *testing.T) {
14911491 "Controlling agent should NOT switch with standard nomination when pair already selected" )
14921492 })
14931493}
1494+
1495+ // ---------------------------------------------------------------------------
1496+ // Lite mode tests
1497+ // ---------------------------------------------------------------------------
1498+
1499+ // TestLiteControlledSelector_NoPingCandidate verifies that a lite controlled
1500+ // agent NEVER sends triggered connectivity checks (PingCandidate), regardless
1501+ // of the pair state. Per RFC 8445 §7, a lite implementation only acts as a
1502+ // STUN server and does not generate connectivity checks.
1503+ func TestLiteControlledSelector_NoPingCandidate (t * testing.T ) {
1504+ buildMsg := func (t * testing.T , a * Agent ) * stun.Message {
1505+ t .Helper ()
1506+ msg , err := stun .Build (stun .BindingRequest ,
1507+ stun .TransactionID ,
1508+ stun .NewUsername (a .localUfrag + ":" + a .remoteUfrag ),
1509+ stun .NewShortTermIntegrity (a .localPwd ),
1510+ stun .Fingerprint ,
1511+ )
1512+ require .NoError (t , err )
1513+
1514+ return msg
1515+ }
1516+
1517+ setupAgent := func (t * testing.T ) (* Agent , * pingNoIOCand , * pingNoIOCand , * CandidatePair ) {
1518+ t .Helper ()
1519+ liteAgent := bareAgentForPing ()
1520+ liteAgent .log = logging .NewDefaultLoggerFactory ().NewLogger ("test" )
1521+ liteAgent .remoteUfrag = selectionTestRemoteUfrag
1522+ liteAgent .localUfrag = selectionTestLocalUfrag
1523+ liteAgent .remotePwd = selectionTestPassword
1524+ liteAgent .localPwd = selectionTestPassword
1525+ liteAgent .tieBreaker = 1
1526+ liteAgent .lite = true
1527+ liteAgent .isControlling .Store (false )
1528+ liteAgent .onConnected = make (chan struct {})
1529+ liteAgent .setSelector ()
1530+
1531+ local := newPingNoIOCand ()
1532+ local .candidateBase .networkType = NetworkTypeUDP4
1533+ local .candidateBase .resolvedAddr = & net.UDPAddr {IP : net .ParseIP ("192.168.1.1" ), Port : 10000 }
1534+
1535+ remote := newPingNoIOCand ()
1536+ remote .candidateBase .networkType = NetworkTypeUDP4
1537+ remote .candidateBase .resolvedAddr = & net.UDPAddr {IP : net .ParseIP ("192.168.1.2" ), Port : 20000 }
1538+
1539+ pair := liteAgent .addPair (local , remote )
1540+
1541+ return liteAgent , local , remote , pair
1542+ }
1543+
1544+ t .Run ("NoTriggeredCheckWhileChecking" , func (t * testing.T ) {
1545+ // Pair is in Waiting state (ICE checking phase). A full controlled agent
1546+ // would send a triggered check here; a lite one must not.
1547+ agent , local , remote , pair := setupAgent (t )
1548+
1549+ ls , ok := agent .getSelector ().(* liteSelector )
1550+ require .True (t , ok , "expected liteSelector as top-level selector" )
1551+ _ , ok = ls .pairCandidateSelector .(* controlledSelector )
1552+ require .True (t , ok , "expected controlledSelector inside liteSelector" )
1553+
1554+ sentBefore := pair .RequestsSent ()
1555+ msg := buildMsg (t , agent )
1556+
1557+ for range 5 {
1558+ ls .HandleBindingRequest (msg , local , remote )
1559+ }
1560+
1561+ assert .Equal (t , sentBefore , pair .RequestsSent (),
1562+ "lite controlled agent must not send triggered checks during ICE checking" )
1563+ })
1564+
1565+ t .Run ("NoTriggeredCheckWhenSucceededAndSelected" , func (t * testing.T ) {
1566+ // Pair is Succeeded and selected. Even a full agent suppresses checks here,
1567+ // but we verify the lite path also stays clean.
1568+ agent , local , remote , pair := setupAgent (t )
1569+ pair .state = CandidatePairStateSucceeded
1570+ agent .setSelectedPair (pair )
1571+
1572+ ls , ok := agent .getSelector ().(* liteSelector )
1573+ require .True (t , ok )
1574+
1575+ sentBefore := pair .RequestsSent ()
1576+ msg := buildMsg (t , agent )
1577+ ls .HandleBindingRequest (msg , local , remote )
1578+
1579+ assert .Equal (t , sentBefore , pair .RequestsSent (),
1580+ "lite controlled agent must not send triggered checks when pair is connected" )
1581+ })
1582+
1583+ t .Run ("NominationStillAccepted" , func (t * testing.T ) {
1584+ // A lite agent must still accept USE-CANDIDATE and set the selected pair
1585+ // (RFC 8445 §7.3.2), even though it never sends its own checks.
1586+ agent , local , remote , pair := setupAgent (t )
1587+ pair .state = CandidatePairStateSucceeded
1588+
1589+ ls , ok := agent .getSelector ().(* liteSelector )
1590+ require .True (t , ok )
1591+
1592+ assert .Nil (t , agent .getSelectedPair (), "no pair selected yet" )
1593+
1594+ msg , err := stun .Build (stun .BindingRequest ,
1595+ stun .TransactionID ,
1596+ stun .NewUsername (agent .localUfrag + ":" + agent .remoteUfrag ),
1597+ UseCandidate (),
1598+ stun .NewShortTermIntegrity (agent .localPwd ),
1599+ stun .Fingerprint ,
1600+ )
1601+ require .NoError (t , err )
1602+
1603+ ls .HandleBindingRequest (msg , local , remote )
1604+
1605+ assert .Equal (t , pair , agent .getSelectedPair (),
1606+ "lite controlled agent must accept nomination and set selected pair" )
1607+ // Still no triggered check emitted
1608+ assert .Equal (t , uint64 (0 ), pair .RequestsSent ())
1609+ })
1610+ }
1611+
1612+ // TestLiteMode_FullToLite_Integration is an end-to-end test for the most common
1613+ // lite mode deployment: a full ICE agent (controlling) connects to a lite agent
1614+ // (controlled). The full agent performs connectivity checks and nominates; the
1615+ // lite agent responds to STUN but never generates checks of its own.
1616+ func TestLiteMode_FullToLite_Integration (t * testing.T ) {
1617+ defer test .CheckRoutines (t )()
1618+ defer test .TimeOut (time .Second * 30 ).Stop ()
1619+
1620+ oneHour := time .Hour
1621+ keepaliveInterval := time .Millisecond * 20
1622+
1623+ // Full agent — will become the controlling agent (Dial).
1624+ fullNotifier , fullConnected := onConnected ()
1625+ fullAgent , err := NewAgent (& AgentConfig {
1626+ NetworkTypes : []NetworkType {NetworkTypeUDP4 },
1627+ MulticastDNSMode : MulticastDNSModeDisabled ,
1628+ KeepaliveInterval : & keepaliveInterval ,
1629+ CheckInterval : & oneHour ,
1630+ })
1631+ require .NoError (t , err )
1632+ require .NoError (t , fullAgent .OnConnectionStateChange (fullNotifier ))
1633+ t .Cleanup (func () { require .NoError (t , fullAgent .Close ()) })
1634+
1635+ // Lite agent — will become the controlled agent (Accept).
1636+ liteNotifier , liteConnected := onConnected ()
1637+ liteAgent , err := NewAgent (& AgentConfig {
1638+ NetworkTypes : []NetworkType {NetworkTypeUDP4 },
1639+ MulticastDNSMode : MulticastDNSModeDisabled ,
1640+ KeepaliveInterval : & keepaliveInterval ,
1641+ CheckInterval : & oneHour ,
1642+ Lite : true ,
1643+ CandidateTypes : []CandidateType {CandidateTypeHost },
1644+ })
1645+ require .NoError (t , err )
1646+ require .NoError (t , liteAgent .OnConnectionStateChange (liteNotifier ))
1647+ t .Cleanup (func () { require .NoError (t , liteAgent .Close ()) })
1648+
1649+ // fullAgent.Accept / liteAgent.Dial => fullAgent=controlled, liteAgent=controlling.
1650+ // We want fullAgent=controlling, so we use connect() which calls
1651+ // aAgent.Accept (=fullAgent controlled) and bAgent.Dial (=liteAgent controlling).
1652+ // Swap: pass liteAgent as aAgent (Accept=controlled) and fullAgent as bAgent (Dial=controlling).
1653+ liteConn , fullConn := connect (t , liteAgent , fullAgent )
1654+
1655+ <- fullConnected
1656+ <- liteConnected
1657+
1658+ // Verify the lite agent never sent its own connectivity checks.
1659+ err = liteAgent .loop .Run (liteAgent .loop , func (_ context.Context ) {
1660+ for _ , pair := range liteAgent .checklist {
1661+ assert .Equal (t , uint64 (0 ), pair .RequestsSent (),
1662+ "lite agent must not send any connectivity checks" )
1663+ }
1664+ })
1665+ require .NoError (t , err )
1666+
1667+ // Both agents should be able to exchange data.
1668+ require .True (t , sendUntilDone (t , fullConn , liteConn , 100 ))
1669+ require .True (t , sendUntilDone (t , liteConn , fullConn , 100 ))
1670+
1671+ closePipe (t , liteConn , fullConn )
1672+ }
0 commit comments