diff --git a/icegatherer.go b/icegatherer.go index e78a61d5..7f0294be 100644 --- a/icegatherer.go +++ b/icegatherer.go @@ -363,6 +363,9 @@ func (g *ICEGatherer) renominationOptions() []ice.AgentOption { return generator() }), } + if renom.attributeType != nil { + opts = append(opts, ice.WithNominationAttribute(*renom.attributeType)) + } if renom.automatic { interval := time.Duration(0) diff --git a/icegatherer_test.go b/icegatherer_test.go index 107908f5..21a5e4ba 100644 --- a/icegatherer_test.go +++ b/icegatherer_test.go @@ -2128,6 +2128,16 @@ func TestICEGatherer_RenominationOptions(t *testing.T) { assert.NotNil(t, se.renomination.generator) } +func TestICEGatherer_RenominationInvalidAttributeFailsAtCreateAgent(t *testing.T) { + se := SettingEngine{} + assert.NoError(t, se.SetICERenomination(WithRenominationNominationAttribute(0x0000))) + + gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{}) + assert.NoError(t, err) + + assert.ErrorIs(t, gatherer.createAgent(), ice.ErrInvalidNominationAttribute) +} + func TestICEGatherer_RenominationOptionsDisabled(t *testing.T) { lim := test.TimeOut(time.Second * 10) defer lim.Stop() @@ -2769,6 +2779,7 @@ func makeSrflxCandidateInit(c ICECandidate) ICECandidateInit { func buildStagedRenominationPair( t *testing.T, bindingHandler func(*stun.Message, ice.Candidate, ice.Candidate, *ice.CandidatePair) bool, + renominationOptions ...RenominationOption, ) (*PeerConnection, *PeerConnection, *stagedCandidateSender, *stagedCandidateSender, func()) { t.Helper() @@ -2807,7 +2818,11 @@ func buildStagedRenominationPair( // prefer srflx/prflx nomination first so the test reliably observes the switch to host via renomination. offerSE.SetSrflxAcceptanceMinWait(0) offerSE.SetHostAcceptanceMinWait(3 * time.Second) - assert.NoError(t, offerSE.SetICERenomination(WithRenominationInterval(200*time.Millisecond))) + configuredRenominationOptions := append( + []RenominationOption{WithRenominationInterval(200 * time.Millisecond)}, + renominationOptions..., + ) + assert.NoError(t, offerSE.SetICERenomination(configuredRenominationOptions...)) answerSE := SettingEngine{} answerSE.SetNet(answerNet) @@ -2816,7 +2831,7 @@ func buildStagedRenominationPair( answerSE.SetICETimeouts(5*time.Second, 15*time.Second, 200*time.Millisecond) answerSE.SetSrflxAcceptanceMinWait(0) answerSE.SetHostAcceptanceMinWait(3 * time.Second) - assert.NoError(t, answerSE.SetICERenomination(WithRenominationInterval(200*time.Millisecond))) + assert.NoError(t, answerSE.SetICERenomination(configuredRenominationOptions...)) if bindingHandler != nil { answerSE.SetICEBindingRequestHandler(bindingHandler) } diff --git a/settingengine.go b/settingengine.go index 3f0b443f..61a30db0 100644 --- a/settingengine.go +++ b/settingengine.go @@ -123,6 +123,7 @@ type renominationSettings struct { generator ice.NominationValueGenerator automatic bool automaticInterval *time.Duration + attributeType *uint16 } // NominationValueGenerator generates nomination values for ICE renomination. @@ -151,9 +152,18 @@ func WithRenominationInterval(interval time.Duration) RenominationOption { } } +// WithRenominationNominationAttribute overrides the STUN attribute type used for ICE renomination. +// If unset, the underlying ICE agent default is used. +func WithRenominationNominationAttribute(attrType uint16) RenominationOption { + return func(cfg *renominationSettings) { + a := attrType + cfg.attributeType = &a + } +} + var errInvalidRenominationInterval = errors.New("renomination interval must be greater than zero") -// SetICERenomination configures ICE renomination using options for generator and scheduling. +// SetICERenomination configures ICE renomination using options for generator, scheduling, and attribute type. // Manual control is not exposed yet. This always enables automatic renomination with the default // generator unless a custom one is provided. func (e *SettingEngine) SetICERenomination(options ...RenominationOption) error { @@ -176,6 +186,7 @@ func (e *SettingEngine) SetICERenomination(options ...RenominationOption) error e.renomination.generator = cfg.generator e.renomination.automatic = true e.renomination.automaticInterval = cfg.automaticInterval + e.renomination.attributeType = cfg.attributeType return nil } diff --git a/settingengine_test.go b/settingengine_test.go index 47499dad..c0fb9c5d 100644 --- a/settingengine_test.go +++ b/settingengine_test.go @@ -98,11 +98,31 @@ func TestICERenomination(t *testing.T) { assert.NotNil(t, s.renomination.generator) }) + t.Run("CustomAttribute", func(t *testing.T) { + const customAttr = uint16(0x0042) + + s := SettingEngine{} + assert.NoError(t, s.SetICERenomination(WithRenominationNominationAttribute(customAttr))) + + assert.True(t, s.renomination.enabled) + if assert.NotNil(t, s.renomination.attributeType) { + assert.Equal(t, customAttr, *s.renomination.attributeType) + } + }) + t.Run("InvalidInterval", func(t *testing.T) { s := SettingEngine{} assert.ErrorIs(t, s.SetICERenomination(WithRenominationInterval(0)), errInvalidRenominationInterval) assert.ErrorIs(t, s.SetICERenomination(WithRenominationInterval(-1*time.Second)), errInvalidRenominationInterval) }) + + t.Run("InvalidAttribute", func(t *testing.T) { + s := SettingEngine{} + assert.NoError(t, s.SetICERenomination(WithRenominationNominationAttribute(0x0000))) + if assert.NotNil(t, s.renomination.attributeType) { + assert.Equal(t, uint16(0x0000), *s.renomination.attributeType) + } + }) } func TestDetachDataChannels(t *testing.T) {