Skip to content

Commit 85890bb

Browse files
authored
Implement SCTP Negotiation Acceleration Protocol (SNAP) (#449)
This commit adds a createAssociationWithOutOfBandTokens method that creates the SCTP association with a local and remote sctp-init token as described in draft-hancke-tsvwg-snap-00. Tokens can be generated using the newly added GenerateOutOfBandToken method and set in the sctp.Config as LocalSctpInit and RemoteSctpInit. This allows the peer connection to generate and negotiate the sctp-init attribute in the SDP which skips two network round trips. See https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/
1 parent e3a1986 commit 85890bb

File tree

5 files changed

+253
-9
lines changed

5 files changed

+253
-9
lines changed

association.go

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ const defaultSCTPSrcDstPort = 5000
3131
// Use global random generator to properly seed by crypto grade random.
3232
var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:gochecknoglobals
3333

34+
// Generates a non-zero Initiate tag.
35+
func generateInitiateTag() uint32 {
36+
for {
37+
if u := globalMathRandomGenerator.Uint32(); u != 0 {
38+
return u
39+
}
40+
}
41+
}
42+
3443
// Association errors.
3544
var (
3645
ErrChunk = errors.New("abort chunk, with following errors")
@@ -315,6 +324,12 @@ type Association struct {
315324
tlrStartTime time.Time // time of first recovery RTT
316325
}
317326

327+
type snapConfig struct {
328+
// Local and remote SCTP init to use for SNAP
329+
localInit []byte
330+
remoteInit []byte
331+
}
332+
318333
// Config collects the arguments to createAssociation construction into
319334
// a single structure.
320335
type Config struct {
@@ -340,6 +355,9 @@ type Config struct {
340355

341356
// RACK config options
342357
rack rackSettings
358+
359+
// SNAP/sctp-init
360+
snapConfig *snapConfig
343361
}
344362

345363
// Server accepts a SCTP stream over a conn.
@@ -385,11 +403,37 @@ func createClientWithContext(ctx context.Context, config Config) (*Association,
385403
return createClientWithOptionsWithContext(ctx, config)
386404
}
387405

406+
func createSNAPAssociation(config *Config) (*Association, error) {
407+
// SNAP, aka sctp-init in the SDP.
408+
remote := &chunkInit{}
409+
err := remote.unmarshal(config.snapConfig.remoteInit)
410+
if err != nil {
411+
return nil, err
412+
}
413+
local := &chunkInit{}
414+
err = local.unmarshal(config.snapConfig.localInit)
415+
if err != nil {
416+
return nil, err
417+
}
418+
assoc := createAssociationFromConfigWithTsn(config, local.initialTSN)
419+
assoc.initWithOutOfBandTokens(local, remote)
420+
421+
return assoc, nil
422+
}
423+
388424
func createClientWithOptionsWithContext(ctx context.Context, opts ...ClientOption) (*Association, error) {
425+
config, err := buildClientConfig(opts...)
426+
if err != nil {
427+
return nil, err
428+
}
429+
if config.snapConfig != nil && len(config.snapConfig.remoteInit) != 0 && len(config.snapConfig.localInit) != 0 {
430+
return createSNAPAssociation(config)
431+
}
389432
assoc, err := createClientAssociation(opts...)
390433
if err != nil {
391434
return nil, err
392435
}
436+
393437
assoc.initClient()
394438

395439
select {
@@ -431,7 +475,7 @@ func createServerAssociation(opts ...ServerOption) (*Association, error) {
431475
return nil, err
432476
}
433477

434-
return createAssociationFromConfig(cfg), nil
478+
return createAssociationFromConfig(cfg)
435479
}
436480

437481
func (a *Association) initServer() {
@@ -512,7 +556,7 @@ func createClientAssociation(opts ...ClientOption) (*Association, error) {
512556
return nil, err
513557
}
514558

515-
return createAssociationFromConfig(cfg), nil
559+
return createAssociationFromConfig(cfg)
516560
}
517561

518562
func (a *Association) initClient() {
@@ -589,6 +633,8 @@ func (c Config) applyClient(cfg *Config) error { //nolint:dupl,cyclop
589633

590634
cfg.rack = c.rack
591635

636+
cfg.snapConfig = c.snapConfig
637+
592638
return nil
593639
}
594640

@@ -614,7 +660,13 @@ func buildClientConfig(opts ...ClientOption) (*Config, error) {
614660
return cfg, nil
615661
}
616662

617-
func createAssociationFromConfig(cfg *Config) *Association {
663+
func createAssociationFromConfig(cfg *Config) (*Association, error) {
664+
tsn := globalMathRandomGenerator.Uint32()
665+
666+
return createAssociationFromConfigWithTsn(cfg, tsn), nil
667+
}
668+
669+
func createAssociationFromConfigWithTsn(cfg *Config, tsn uint32) *Association {
618670
maxReceiveBufferSize := cfg.MaxReceiveBufferSize
619671
if maxReceiveBufferSize == 0 {
620672
maxReceiveBufferSize = initialRecvBufSize
@@ -632,7 +684,6 @@ func createAssociationFromConfig(cfg *Config) *Association {
632684

633685
rtoMax := cfg.RTOMax
634686

635-
tsn := globalMathRandomGenerator.Uint32()
636687
assoc := &Association{
637688
netConn: cfg.NetConn,
638689
maxReceiveBufferSize: maxReceiveBufferSize,
@@ -650,7 +701,7 @@ func createAssociationFromConfig(cfg *Config) *Association {
650701
controlQueue: newControlQueue(),
651702
mtu: mtu,
652703
maxPayloadSize: mtu - (commonHeaderSize + dataChunkHeaderSize),
653-
myVerificationTag: globalMathRandomGenerator.Uint32(),
704+
myVerificationTag: generateInitiateTag(),
654705
initialTSN: tsn,
655706
myNextTSN: tsn,
656707
myNextRSN: tsn,
@@ -717,6 +768,43 @@ func createAssociationFromConfig(cfg *Config) *Association {
717768
return assoc
718769
}
719770

771+
func (a *Association) initWithOutOfBandTokens(localInit *chunkInit, remoteInit *chunkInit) {
772+
a.lock.Lock()
773+
defer a.lock.Unlock()
774+
775+
go a.readLoop()
776+
go a.writeLoop()
777+
778+
a.payloadQueue.init(remoteInit.initialTSN - 1)
779+
a.myMaxNumInboundStreams = min16(localInit.numInboundStreams, remoteInit.numInboundStreams)
780+
a.myMaxNumOutboundStreams = min16(localInit.numOutboundStreams, remoteInit.numOutboundStreams)
781+
a.setRWND(remoteInit.advertisedReceiverWindowCredit)
782+
a.peerVerificationTag = remoteInit.initiateTag
783+
a.sourcePort = defaultSCTPSrcDstPort
784+
a.destinationPort = defaultSCTPSrcDstPort
785+
for _, param := range remoteInit.params {
786+
switch v := param.(type) { // nolint:gocritic
787+
case *paramSupportedExtensions:
788+
for _, t := range v.ChunkTypes {
789+
if t == ctForwardTSN {
790+
a.log.Debugf("[%s] use ForwardTSN (on init)", a.name)
791+
a.useForwardTSN = true
792+
}
793+
}
794+
case *paramZeroChecksumAcceptable:
795+
a.sendZeroChecksum = v.edmid == dtlsErrorDetectionMethod
796+
}
797+
}
798+
799+
if !a.useForwardTSN {
800+
a.log.Warnf("[%s] not using ForwardTSN (on init)", a.name)
801+
}
802+
803+
a.ssthresh = a.RWND()
804+
805+
a.setState(established)
806+
}
807+
720808
// caller must hold a.lock.
721809
func (a *Association) sendInit() error {
722810
a.log.Debugf("[%s] sending INIT", a.name)
@@ -1714,7 +1802,7 @@ func (a *Association) handleInitAck(pkt *packet, initChunkAck *chunkInitAck) err
17141802
a.setRWND(initChunkAck.advertisedReceiverWindowCredit)
17151803
a.log.Debugf("[%s] initial rwnd=%d", a.name, a.RWND())
17161804

1717-
// RFC 4690 Sec 7.2.1
1805+
// RFC 4960 Sec 7.2.1
17181806
// o The initial value of ssthresh MAY be arbitrarily high (for
17191807
// example, implementations MAY use the size of the receiver
17201808
// advertised window).
@@ -4222,3 +4310,39 @@ func (a *Association) sendActiveHeartbeatLocked() {
42224310
})
42234311
a.awakeWriteLoop()
42244312
}
4313+
4314+
// GenerateOutOfBandToken generates an out-of-band connection token (i.e. a
4315+
// serialized SCTP INIT chunk) for use with SNAP.
4316+
func GenerateOutOfBandToken(opts ...ClientOption) ([]byte, error) {
4317+
config := &Config{}
4318+
config.applyDefaults()
4319+
4320+
for _, opt := range opts {
4321+
if opt == nil {
4322+
continue
4323+
}
4324+
if err := opt.applyClient(config); err != nil {
4325+
return nil, err
4326+
}
4327+
}
4328+
4329+
config.applyDefaults()
4330+
4331+
init := &chunkInit{}
4332+
init.initialTSN = globalMathRandomGenerator.Uint32()
4333+
init.numOutboundStreams = math.MaxUint16
4334+
init.numInboundStreams = math.MaxUint16
4335+
init.initiateTag = generateInitiateTag()
4336+
init.advertisedReceiverWindowCredit = config.MaxReceiveBufferSize
4337+
setSupportedExtensions(&init.chunkInitCommon)
4338+
4339+
if config.EnableZeroChecksum {
4340+
init.params = append(init.params, &paramZeroChecksumAcceptable{edmid: dtlsErrorDetectionMethod})
4341+
}
4342+
_, err := init.check()
4343+
if err != nil {
4344+
return nil, err
4345+
}
4346+
4347+
return init.marshal()
4348+
}

association_options.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,18 @@ func WithCwndCAStep(cwndCAStep uint32) AssociationOption {
163163
return nil
164164
})
165165
}
166+
167+
// WithSNAP enables SNAP, https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/.
168+
func WithSNAP(localSctpInit []byte, remoteSctpInit []byte) AssociationOption {
169+
return sharedOption(func(c *Config) error {
170+
if len(localSctpInit) == 0 || len(remoteSctpInit) == 0 {
171+
return errInvalidSnapToken
172+
}
173+
c.snapConfig = &snapConfig{
174+
localInit: localSctpInit,
175+
remoteInit: remoteSctpInit,
176+
}
177+
178+
return nil
179+
})
180+
}

association_options_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ func TestAssociationOptions_Validation(t *testing.T) {
6868
err := WithRTOMax(0).applyServer(&cfg)
6969
assert.ErrorIs(t, err, errInvalidRTOMax)
7070
})
71+
72+
t.Run("snap nil arguments", func(t *testing.T) {
73+
var cfg Config
74+
err := WithSNAP(nil, nil).applyServer(&cfg)
75+
assert.ErrorIs(t, err, errInvalidSnapToken)
76+
err = WithSNAP([]byte{}, []byte{}).applyServer(&cfg)
77+
assert.ErrorIs(t, err, errInvalidSnapToken)
78+
})
7179
}
7280

7381
func TestClientWithOptions_ValidatesOptionValues(t *testing.T) {

association_test.go

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4842,7 +4842,8 @@ func TestAbortStillSendsWhenWriteLoopClosing(t *testing.T) {
48424842
EnableZeroChecksum: false,
48434843
}
48444844
cfg.applyDefaults()
4845-
assoc := createAssociationFromConfig(&cfg)
4845+
assoc, err := createAssociationFromConfig(&cfg)
4846+
assert.NoError(t, err)
48464847
assoc.initServer()
48474848

48484849
// Simulate the problematic timing: writeLoop is sitting in its select and
@@ -4921,7 +4922,8 @@ func TestAbort_WaitsForAbortWriteAttempt(t *testing.T) {
49214922
MaxMessageSize: 1200,
49224923
}
49234924
cfg.applyDefaults()
4924-
assoc := createAssociationFromConfig(&cfg)
4925+
assoc, err := createAssociationFromConfig(&cfg)
4926+
assert.NoError(t, err)
49254927
assoc.initServer()
49264928

49274929
assoc.Abort("test")
@@ -4944,7 +4946,8 @@ func TestAbortSentChClosedWhenAbortMarshalFails(t *testing.T) {
49444946
MaxMessageSize: 1200,
49454947
}
49464948
cfg.applyDefaults()
4947-
assoc := createAssociationFromConfig(&cfg)
4949+
assoc, err := createAssociationFromConfig(&cfg)
4950+
assert.NoError(t, err)
49484951
assoc.initServer()
49494952

49504953
assoc.lock.Lock()
@@ -4961,3 +4964,94 @@ func TestAbortSentChClosedWhenAbortMarshalFails(t *testing.T) {
49614964
require.Fail(t, "abortSentCh was not closed when ABORT marshaling failed")
49624965
}
49634966
}
4967+
4968+
func TestAssociationSNAPInvalidInit(t *testing.T) {
4969+
br := test.NewBridge()
4970+
tokenConfig := Config{
4971+
MaxReceiveBufferSize: 65535,
4972+
EnableZeroChecksum: false,
4973+
}
4974+
init, err := GenerateOutOfBandToken(tokenConfig)
4975+
assert.NoError(t, err)
4976+
4977+
_, err = ClientWithOptions(
4978+
WithNetConn(br.GetConn0()),
4979+
WithSNAP([]byte{1, 2}, init))
4980+
assert.ErrorIs(t, err, ErrChunkHeaderTooSmall)
4981+
4982+
_, err = ClientWithOptions(
4983+
WithNetConn(br.GetConn1()),
4984+
WithSNAP(init, []byte{1, 2}))
4985+
assert.ErrorIs(t, err, ErrChunkHeaderTooSmall)
4986+
}
4987+
4988+
func TestAssociationSNAP(t *testing.T) {
4989+
lim := test.TimeOut(time.Second * 10)
4990+
defer lim.Stop()
4991+
4992+
loggerFactory := logging.NewDefaultLoggerFactory()
4993+
br := test.NewBridge()
4994+
4995+
// Use GenerateOutOfBandToken to create the init chunks
4996+
tokenConfig := Config{
4997+
MaxReceiveBufferSize: 65535,
4998+
EnableZeroChecksum: false,
4999+
}
5000+
initA, err := GenerateOutOfBandToken(tokenConfig)
5001+
assert.NoError(t, err)
5002+
5003+
initB, err := GenerateOutOfBandToken(tokenConfig)
5004+
assert.NoError(t, err)
5005+
5006+
assocA, err := ClientWithOptions(
5007+
WithName("a"),
5008+
WithNetConn(br.GetConn0()),
5009+
WithLoggerFactory(loggerFactory),
5010+
WithSNAP(initA, initB))
5011+
assert.NoError(t, err)
5012+
assert.NotNil(t, assocA)
5013+
5014+
assocB, err := ClientWithOptions(
5015+
WithName("b"),
5016+
WithNetConn(br.GetConn1()),
5017+
WithLoggerFactory(loggerFactory),
5018+
WithSNAP(initB, initA))
5019+
assert.NoError(t, err)
5020+
assert.NotNil(t, assocB)
5021+
5022+
const si uint16 = 1
5023+
const msg = "SNAP is snappy"
5024+
5025+
streamA, err := assocA.OpenStream(si, PayloadTypeWebRTCBinary)
5026+
assert.NoError(t, err)
5027+
5028+
_, err = streamA.WriteSCTP([]byte(msg), PayloadTypeWebRTCBinary)
5029+
assert.NoError(t, err)
5030+
5031+
br.Process()
5032+
5033+
accepted := make(chan *Stream)
5034+
go func() {
5035+
s, errAccept := assocB.AcceptStream()
5036+
assert.NoError(t, errAccept)
5037+
accepted <- s
5038+
}()
5039+
5040+
flushBuffers(br, assocA, assocB)
5041+
5042+
var streamB *Stream
5043+
select {
5044+
case streamB = <-accepted:
5045+
case <-time.After(5 * time.Second):
5046+
assert.Fail(t, "timed out waiting for accept stream")
5047+
}
5048+
5049+
buf := make([]byte, 64)
5050+
n, ppi, err := streamB.ReadSCTP(buf)
5051+
assert.NoError(t, err, "ReadSCTP failed")
5052+
assert.Equal(t, len(msg), n, "unexpected length of received data")
5053+
assert.Equal(t, PayloadTypeWebRTCBinary, ppi, "unexpected ppi")
5054+
assert.Equal(t, msg, string(buf[:n]))
5055+
5056+
closeAssociationPair(br, assocA, assocB)
5057+
}

errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ var (
3333
// errInvalidRackWcDelAck indicates the receiver worst-case delayed-ACK for PTO when only 1 packet in flight
3434
// was set to < 0.
3535
errInvalidRackWcDelAck = errors.New("RackWcDelAck was set to <= 0")
36+
37+
// errInvalidSnapToken indicates a SNAP token that is not parseable.
38+
errInvalidSnapToken = errors.New("SNAP token is invalid")
3639
)

0 commit comments

Comments
 (0)