diff --git a/association.go b/association.go index 838ae62b..e70f448d 100644 --- a/association.go +++ b/association.go @@ -30,6 +30,15 @@ const defaultSCTPSrcDstPort = 5000 // Use global random generator to properly seed by crypto grade random. var globalMathRandomGenerator = randutil.NewMathRandomGenerator() // nolint:gochecknoglobals +// Generates a non-zero Initiate tag. +func generateInitiateTag() uint32 { + for { + if u := globalMathRandomGenerator.Uint32(); u != 0 { + return u + } + } +} + // Association errors. var ( ErrChunk = errors.New("abort chunk, with following errors") @@ -321,7 +330,6 @@ type Config struct { Name string NetConn net.Conn MaxReceiveBufferSize uint32 - MaxMessageSize uint32 EnableZeroChecksum bool LoggerFactory logging.LoggerFactory BlockWrite bool @@ -347,9 +355,23 @@ type Config struct { rackWCDelAck time.Duration } +// SctpParameters represents negotiated (e.g. via SDP) SCTP parameters. +type SctpParameters struct { + // a=sctp-port + LocalSctpPort int16 + RemoteSctpPort int16 + + // a=max-message-size, negotiated. + MaxMessageSize uint32 + + // a=sctp-ini, decoded from base64. + LocalSctpInit []byte + RemoteSctpInit []byte +} + // Server accepts a SCTP stream over a conn. func Server(config Config) (*Association, error) { - a := createAssociation(config) + a := createAssociation(config, SctpParameters{}) a.init(false) select { @@ -365,12 +387,29 @@ func Server(config Config) (*Association, error) { } // Client opens a SCTP stream over a conn. -func Client(config Config) (*Association, error) { - return createClientWithContext(context.Background(), config) +func Client(config Config, options SctpParameters) (*Association, error) { + return createClientWithContext(context.Background(), config, options) } -func createClientWithContext(ctx context.Context, config Config) (*Association, error) { - assoc := createAssociation(config) +func createClientWithContext(ctx context.Context, config Config, options SctpParameters) (*Association, error) { + if len(options.RemoteSctpInit) != 0 && len(options.LocalSctpInit) != 0 { + // SNAP, aka sctp-init in the SDP. + remote := &chunkInit{} + err := remote.unmarshal(options.RemoteSctpInit) + if err != nil { + return nil, err + } + local := &chunkInit{} + err = local.unmarshal(options.LocalSctpInit) + if err != nil { + return nil, err + } + assoc := createAssociationWithTSN(config, options, local.initialTSN) + assoc.initWithOutOfBandTokens(local, remote) + + return assoc, nil + } + assoc := createAssociation(config, options) assoc.init(true) select { @@ -390,13 +429,19 @@ func createClientWithContext(ctx context.Context, config Config) (*Association, } } -func createAssociation(config Config) *Association { +func createAssociation(config Config, options SctpParameters) *Association { + tsn := globalMathRandomGenerator.Uint32() + + return createAssociationWithTSN(config, options, tsn) +} + +func createAssociationWithTSN(config Config, options SctpParameters, tsn uint32) *Association { maxReceiveBufferSize := config.MaxReceiveBufferSize if maxReceiveBufferSize == 0 { maxReceiveBufferSize = initialRecvBufSize } - maxMessageSize := config.MaxMessageSize + maxMessageSize := options.MaxMessageSize if maxMessageSize == 0 { maxMessageSize = defaultMaxMessageSize } @@ -406,7 +451,6 @@ func createAssociation(config Config) *Association { mtu = initialMTU } - tsn := globalMathRandomGenerator.Uint32() assoc := &Association{ netConn: config.NetConn, maxReceiveBufferSize: maxReceiveBufferSize, @@ -428,7 +472,7 @@ func createAssociation(config Config) *Association { controlQueue: newControlQueue(), mtu: mtu, maxPayloadSize: mtu - (commonHeaderSize + dataChunkHeaderSize), - myVerificationTag: globalMathRandomGenerator.Uint32(), + myVerificationTag: generateInitiateTag(), initialTSN: tsn, myNextTSN: tsn, myNextRSN: tsn, @@ -477,7 +521,7 @@ func createAssociation(config Config) *Association { assoc.name = fmt.Sprintf("%p", assoc) } - // RFC 4690 Sec 7.2.1 + // RFC 4960 Sec 7.2.1 // o The initial cwnd before DATA transmission or after a sufficiently // long idle period MUST be set to min(4*MTU, max (2*MTU, 4380 // bytes)). @@ -532,6 +576,43 @@ func (a *Association) init(isClient bool) { } } +func (a *Association) initWithOutOfBandTokens(localInit *chunkInit, remoteInit *chunkInit) { + a.lock.Lock() + defer a.lock.Unlock() + + go a.readLoop() + go a.writeLoop() + + a.payloadQueue.init(remoteInit.initialTSN - 1) + a.myMaxNumInboundStreams = min16(localInit.numInboundStreams, remoteInit.numInboundStreams) + a.myMaxNumOutboundStreams = min16(localInit.numOutboundStreams, remoteInit.numOutboundStreams) + a.setRWND(min32(localInit.advertisedReceiverWindowCredit, remoteInit.advertisedReceiverWindowCredit)) + a.peerVerificationTag = remoteInit.initiateTag + a.sourcePort = defaultSCTPSrcDstPort + a.destinationPort = defaultSCTPSrcDstPort + for _, param := range remoteInit.params { + switch v := param.(type) { // nolint:gocritic + case *paramSupportedExtensions: + for _, t := range v.ChunkTypes { + if t == ctForwardTSN { + a.log.Debugf("[%s] use ForwardTSN (on init)", a.name) + a.useForwardTSN = true + } + } + case *paramZeroChecksumAcceptable: + a.sendZeroChecksum = v.edmid == dtlsErrorDetectionMethod + } + } + + if !a.useForwardTSN { + a.log.Warnf("[%s] not using ForwardTSN (on init)", a.name) + } + + a.ssthresh = a.RWND() + + a.setState(established) +} + // caller must hold a.lock. func (a *Association) sendInit() error { a.log.Debugf("[%s] sending INIT", a.name) @@ -1500,7 +1581,7 @@ func (a *Association) handleInitAck(pkt *packet, initChunkAck *chunkInitAck) err a.setRWND(initChunkAck.advertisedReceiverWindowCredit) a.log.Debugf("[%s] initial rwnd=%d", a.name, a.RWND()) - // RFC 4690 Sec 7.2.1 + // RFC 4960 Sec 7.2.1 // o The initial value of ssthresh MAY be arbitrarily high (for // example, implementations MAY use the size of the receiver // advertised window). @@ -3997,3 +4078,21 @@ func (a *Association) sendActiveHeartbeatLocked() { }) a.awakeWriteLoop() } + +// GenerateOutOfBandToken generates an out-of-band connection token (i.e. a +// serialized SCTP INIT chunk) for use with SNAP. +func GenerateOutOfBandToken(config Config) ([]byte, error) { + init := &chunkInit{} + init.initialTSN = globalMathRandomGenerator.Uint32() + init.numOutboundStreams = math.MaxUint16 + init.numInboundStreams = math.MaxUint16 + init.initiateTag = generateInitiateTag() + init.advertisedReceiverWindowCredit = config.MaxReceiveBufferSize + setSupportedExtensions(&init.chunkInitCommon) + + if config.EnableZeroChecksum { + init.params = append(init.params, ¶mZeroChecksumAcceptable{edmid: dtlsErrorDetectionMethod}) + } + + return init.marshal() +} diff --git a/association_test.go b/association_test.go index d48f5b8f..20e56d1f 100644 --- a/association_test.go +++ b/association_test.go @@ -127,7 +127,7 @@ func association(t *testing.T, piper piperFunc) (*Association, *Association, err client, err := Client(Config{ NetConn: ca, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) resultCh <- result{client, err} }() @@ -267,7 +267,7 @@ func createNewAssociationPair( NetConn: br.GetConn0(), MaxReceiveBufferSize: recvBufSize, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) handshake0Ch <- true }() go func() { @@ -1171,7 +1171,7 @@ func TestInitVerificationTagIsZero(t *testing.T) { //nolint:cyclop NetConn: br.GetConn0(), MaxReceiveBufferSize: recvBufSize, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) handshake0Ch <- true }() @@ -1181,7 +1181,7 @@ func TestInitVerificationTagIsZero(t *testing.T) { //nolint:cyclop NetConn: br.GetConn1(), MaxReceiveBufferSize: recvBufSize, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) handshake1Ch <- true }() @@ -1248,7 +1248,7 @@ func TestCreateForwardTSN(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.cumulativeTSNAckPoint = 9 assoc.advancedPeerTSNAckPoint = 10 @@ -1275,7 +1275,7 @@ func TestCreateForwardTSN(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.cumulativeTSNAckPoint = 9 assoc.advancedPeerTSNAckPoint = 12 @@ -1341,7 +1341,7 @@ func TestHandleForwardTSN(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.useForwardTSN = true prevTSN := assoc.peerLastTSN() @@ -1366,7 +1366,7 @@ func TestHandleForwardTSN(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.useForwardTSN = true prevTSN := assoc.peerLastTSN() @@ -1396,7 +1396,7 @@ func TestHandleForwardTSN(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.useForwardTSN = true prevTSN := assoc.peerLastTSN() @@ -1425,7 +1425,7 @@ func TestHandleForwardTSN(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.useForwardTSN = true prevTSN := assoc.peerLastTSN() @@ -1453,7 +1453,7 @@ func TestHandleDataAckTriggering(t *testing.T) { newAssoc := func() *Association { assoc := createAssociation(Config{ LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.payloadQueue.init(0) return assoc @@ -1585,11 +1585,11 @@ func TestAssocT1InitTimer(t *testing.T) { //nolint:cyclop a0 := createAssociation(Config{ NetConn: br.GetConn0(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) a1 := createAssociation(Config{ NetConn: br.GetConn1(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) var err0, err1 error a0ReadyCh := make(chan bool) @@ -1645,11 +1645,11 @@ func TestAssocT1InitTimer(t *testing.T) { //nolint:cyclop a0 := createAssociation(Config{ NetConn: br.GetConn0(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) a1 := createAssociation(Config{ NetConn: br.GetConn1(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) var err0, err1 error a0ReadyCh := make(chan bool) @@ -1714,11 +1714,11 @@ func TestAssocT1CookieTimer(t *testing.T) { //nolint:cyclop a0 := createAssociation(Config{ NetConn: br.GetConn0(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) a1 := createAssociation(Config{ NetConn: br.GetConn1(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) var err0, err1 error a0ReadyCh := make(chan bool) @@ -1776,11 +1776,11 @@ func TestAssocT1CookieTimer(t *testing.T) { //nolint:cyclop a0 := createAssociation(Config{ NetConn: br.GetConn0(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) a1 := createAssociation(Config{ NetConn: br.GetConn1(), LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) var err0 error a0ReadyCh := make(chan bool) @@ -1847,7 +1847,7 @@ func TestAssocCreateNewStream(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) for i := 0; i < acceptChSize; i++ { s := assoc.createStream(uint16(i), true) //nolint:gosec @@ -2562,7 +2562,9 @@ func TestRoutineLeak(t *testing.T) { checkGoroutineLeaks(t) conn := newFakeEchoConn(io.EOF) - assoc, err := Client(Config{NetConn: conn, LoggerFactory: loggerFactory}) + assoc, err := Client(Config{ + NetConn: conn, LoggerFactory: loggerFactory, + }, SctpParameters{}) assert.Equal(t, nil, err, "errored to initialize Client") <-conn.done @@ -2582,7 +2584,7 @@ func TestRoutineLeak(t *testing.T) { checkGoroutineLeaks(t) conn := newFakeEchoConn(nil) - a, err := Client(Config{NetConn: conn, LoggerFactory: loggerFactory}) + a, err := Client(Config{NetConn: conn, LoggerFactory: loggerFactory}, SctpParameters{}) assert.Equal(t, nil, err, "errored to initialize Client") <-conn.done @@ -2605,7 +2607,7 @@ func TestStats(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() conn := newFakeEchoConn(nil) - assoc, err := Client(Config{NetConn: conn, LoggerFactory: loggerFactory}) + assoc, err := Client(Config{NetConn: conn, LoggerFactory: loggerFactory}, SctpParameters{}) assert.Equal(t, nil, err, "errored to initialize Client") <-conn.done @@ -2631,7 +2633,7 @@ func TestAssocHandleInit(t *testing.T) { assoc := createAssociation(Config{ NetConn: &dumbConn{}, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.setState(initialState) pkt := &packet{ sourcePort: 5001, @@ -2691,7 +2693,7 @@ func TestAssocMaxMessageSize(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() a := createAssociation(Config{ LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assert.NotNil(t, a, "should succeed") assert.Equal(t, uint32(65536), a.MaxMessageSize(), "should match") @@ -2710,8 +2712,9 @@ func TestAssocMaxMessageSize(t *testing.T) { t.Run("explicit", func(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() a := createAssociation(Config{ + LoggerFactory: loggerFactory, + }, SctpParameters{ MaxMessageSize: 30000, - LoggerFactory: loggerFactory, }) assert.NotNil(t, a, "should succeed") assert.Equal(t, uint32(30000), a.MaxMessageSize(), "should match") @@ -2732,7 +2735,7 @@ func TestAssocMaxMessageSize(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() a := createAssociation(Config{ LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assert.NotNil(t, a, "should succeed") assert.Equal(t, uint32(65536), a.MaxMessageSize(), "should match") a.SetMaxMessageSize(20000) @@ -2866,7 +2869,7 @@ func createAssocs() (*Association, *Association, error) { //nolint:cyclop a, err2 := createClientWithContext(ctx, Config{ NetConn: udp1, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if err2 != nil { a1Chan <- err2 } else { @@ -2878,7 +2881,7 @@ func createAssocs() (*Association, *Association, error) { //nolint:cyclop a, err2 := createClientWithContext(ctx, Config{ NetConn: udp2, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if err2 != nil { a2Chan <- err2 } else { @@ -2958,7 +2961,7 @@ func createAssociationPairWithConfig( cfg := config cfg.NetConn = udpConn1 cfg.LoggerFactory = loggerFactory - a, err2 := createClientWithContext(ctx, cfg) + a, err2 := createClientWithContext(ctx, cfg, SctpParameters{}) if err2 != nil { a1Chan <- err2 } else { @@ -2973,7 +2976,7 @@ func createAssociationPairWithConfig( if cfg.MaxReceiveBufferSize == 0 { cfg.MaxReceiveBufferSize = 100_000 } - a, err2 := createClientWithContext(ctx, cfg) + a, err2 := createClientWithContext(ctx, cfg, SctpParameters{}) if err2 != nil { a2Chan <- err2 } else { @@ -3070,7 +3073,7 @@ func TestAssociationAbortUnblocksStuckRead(t *testing.T) { assoc := createAssociation(Config{ NetConn: conn, LoggerFactory: logging.NewDefaultLoggerFactory(), - }) + }, SctpParameters{}) assoc.init(false) done := make(chan struct{}) @@ -3146,7 +3149,7 @@ func TestAssociationAbortSetsWriteDeadline(t *testing.T) { assoc := createAssociation(Config{ NetConn: conn, LoggerFactory: logging.NewDefaultLoggerFactory(), - }) + }, SctpParameters{}) assoc.init(false) done := make(chan struct{}) @@ -3641,7 +3644,7 @@ func TestAssociation_HandlePacketInCookieWaitState(t *testing.T) { NetConn: aConn, MaxReceiveBufferSize: 0, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) assoc.init(true) if !testCase.skipClose { @@ -3720,7 +3723,7 @@ func TestAssociation_createClientWithContext(t *testing.T) { _, err2 := createClientWithContext(ctx, Config{ NetConn: udp1, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if err2 != nil { errCh1 <- err2 } else { @@ -3732,7 +3735,7 @@ func TestAssociation_createClientWithContext(t *testing.T) { _, err2 := createClientWithContext(ctx, Config{ NetConn: udp2, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if err2 != nil { errCh2 <- err2 } else { @@ -3810,7 +3813,7 @@ func TestAssociation_ZeroChecksum(t *testing.T) { NetConn: udp1, LoggerFactory: &customLogger{testCase.expectChecksumEnabled, t}, EnableZeroChecksum: testCase.clientZeroChecksum, - }) + }, SctpParameters{}) assert.NoError(t, err) a1chan <- a1 }() @@ -3876,7 +3879,7 @@ func TestAssociation_ReconfigRequestsLimited(t *testing.T) { a1, err := Client(Config{ NetConn: udp1, LoggerFactory: logging.NewDefaultLoggerFactory(), - }) + }, SctpParameters{}) assert.NoError(t, err) a1chan <- a1 }() @@ -4093,7 +4096,7 @@ func newRackTestAssoc(t *testing.T) *Association { lg := logging.NewDefaultLoggerFactory() assoc := createAssociation(Config{ LoggerFactory: lg, - }) + }, SctpParameters{}) // Put the association into a sane "established" state with fresh queues. assoc.setState(established) @@ -4337,7 +4340,7 @@ func newTLRAssociationForTest(t *testing.T) (*Association, net.Conn) { MTU: 1200, LoggerFactory: logging.NewDefaultLoggerFactory(), RTOMax: 1000, - }) + }, SctpParameters{}) return a, c2 } @@ -4660,3 +4663,80 @@ func TestTLR_GetDataPacketsToRetransmit_RespectsBurstBudget_LaterRTT(t *testing. assert.Equal(t, 2, nChunks) assert.True(t, consumed) } + +func TestAssociationSnap(t *testing.T) { + lim := test.TimeOut(time.Second * 10) + defer lim.Stop() + + loggerFactory := logging.NewDefaultLoggerFactory() + br := test.NewBridge() + + // Use GenerateOutOfBandToken to create the init chunks + tokenConfig := Config{ + MaxReceiveBufferSize: 65535, + EnableZeroChecksum: false, + } + initA, err := GenerateOutOfBandToken(tokenConfig) + assert.NoError(t, err) + + initB, err := GenerateOutOfBandToken(tokenConfig) + assert.NoError(t, err) + + assocA, err := Client(Config{ + Name: "a", + NetConn: br.GetConn0(), + LoggerFactory: loggerFactory, + }, SctpParameters{ + LocalSctpInit: initA, + RemoteSctpInit: initB, + }) + assert.NoError(t, err) + assert.NotNil(t, assocA) + + assocB, err := Client(Config{ + Name: "b", + NetConn: br.GetConn1(), + LoggerFactory: loggerFactory, + }, SctpParameters{ + LocalSctpInit: initB, + RemoteSctpInit: initA, + }) + assert.NoError(t, err) + assert.NotNil(t, assocB) + + const si uint16 = 1 + const msg = "SNAP is snappy" + + streamA, err := assocA.OpenStream(si, PayloadTypeWebRTCBinary) + assert.NoError(t, err) + + _, err = streamA.WriteSCTP([]byte(msg), PayloadTypeWebRTCBinary) + assert.NoError(t, err) + + br.Process() + + accepted := make(chan *Stream) + go func() { + s, errAccept := assocB.AcceptStream() + assert.NoError(t, errAccept) + accepted <- s + }() + + flushBuffers(br, assocA, assocB) + + var streamB *Stream + select { + case streamB = <-accepted: + case <-time.After(5 * time.Second): + assert.Fail(t, "timed out waiting for accept stream") + } + + buf := make([]byte, 64) + n, ppi, err := streamB.ReadSCTP(buf) + assert.NoError(t, err, "ReadSCTP failed") + assert.Equal(t, len(msg), n, "unexpected length of received data") + assert.Equal(t, PayloadTypeWebRTCBinary, ppi, "unexpected ppi") + assert.Equal(t, msg, string(buf[:n])) + + closeAssociationPair(br, assocA, assocB) +} diff --git a/examples/ping-pong/ping/main.go b/examples/ping-pong/ping/main.go index a699b484..cfb3a80c 100644 --- a/examples/ping-pong/ping/main.go +++ b/examples/ping-pong/ping/main.go @@ -32,7 +32,7 @@ func main() { //nolint:cyclop NetConn: conn, LoggerFactory: logging.NewDefaultLoggerFactory(), } - a, err := sctp.Client(config) + a, err := sctp.Client(config, sctp.SctpParameters{}) if err != nil { log.Panic(err) } diff --git a/vnet_test.go b/vnet_test.go index 10c439f6..0ea850b8 100644 --- a/vnet_test.go +++ b/vnet_test.go @@ -293,7 +293,7 @@ func testRwndFull(t *testing.T, unordered bool) { //nolint:cyclop NetConn: conn, MaxReceiveBufferSize: maxReceiveBufferSize, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if !assert.NoError(t, err, "should succeed") { return } @@ -500,7 +500,7 @@ func TestStreamClose(t *testing.T) { //nolint:cyclop assoc, err := Client(Config{ NetConn: conn, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if !assert.NoError(t, err, "should succeed") { return } @@ -634,7 +634,7 @@ func TestCookieEchoRetransmission(t *testing.T) { NetConn: conn, MaxReceiveBufferSize: maxReceiveBufferSize, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if !assert.NoError(t, err, "should succeed") { return } @@ -662,7 +662,7 @@ func TestCookieEchoRetransmission(t *testing.T) { NetConn: conn, MaxReceiveBufferSize: maxReceiveBufferSize, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if !assert.NoError(t, err, "should succeed") { return } @@ -845,7 +845,7 @@ func TestRACK_RTTSwitch_Reordering_NoDrop(t *testing.T) { //nolint:gocyclo,cyclo assoc, err := Client(Config{ NetConn: conn, LoggerFactory: loggerFactory, - }) + }, SctpParameters{}) if err != nil { fail(fmt.Errorf("client assoc: %w", err)) clientStatsCh <- statsResult{ok: false}