| // Copyright 2025 The BoringSSL Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package runner | |
| import ( | |
| "slices" | |
| "strconv" | |
| "time" | |
| ) | |
| func addDTLSReplayTests() { | |
| for _, vers := range allVersions(dtls) { | |
| // Test that sequence number replays are detected. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Replay-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| }, | |
| messageCount: 200, | |
| replayWrites: true, | |
| }) | |
| // Test the incoming sequence number skipping by values larger | |
| // than the retransmit window. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Replay-LargeGaps-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| SequenceNumberMapping: func(in uint64) uint64 { | |
| return in * 1023 | |
| }, | |
| }, | |
| }, | |
| messageCount: 200, | |
| replayWrites: true, | |
| }) | |
| // Test the incoming sequence number changing non-monotonically. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Replay-NonMonotonic-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| SequenceNumberMapping: func(in uint64) uint64 { | |
| // This mapping has numbers counting backwards in groups | |
| // of 256, and then jumping forwards 511 numbers. | |
| return in ^ 255 | |
| }, | |
| }, | |
| }, | |
| // This messageCount is large enough to make sure that the SequenceNumberMapping | |
| // will reach the point where it jumps forwards after stepping backwards. | |
| messageCount: 500, | |
| replayWrites: true, | |
| }) | |
| } | |
| } | |
| // timeouts is the default retransmit schedule for BoringSSL. It doubles and | |
| // caps at 60 seconds. On the 13th timeout, it gives up. | |
| var timeouts = []time.Duration{ | |
| 400 * time.Millisecond, | |
| 800 * time.Millisecond, | |
| 1600 * time.Millisecond, | |
| 3200 * time.Millisecond, | |
| 6400 * time.Millisecond, | |
| 12800 * time.Millisecond, | |
| 25600 * time.Millisecond, | |
| 51200 * time.Millisecond, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| } | |
| // shortTimeouts is an alternate set of timeouts which would occur if the | |
| // initial timeout duration was set to 250ms. | |
| var shortTimeouts = []time.Duration{ | |
| 250 * time.Millisecond, | |
| 500 * time.Millisecond, | |
| 1 * time.Second, | |
| 2 * time.Second, | |
| 4 * time.Second, | |
| 8 * time.Second, | |
| 16 * time.Second, | |
| 32 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| 60 * time.Second, | |
| } | |
| // dtlsPrevEpochExpiration is how long before the shim releases old epochs. Add | |
| // an extra second to allow the shim to be less precise. | |
| const dtlsPrevEpochExpiration = 4*time.Minute + 1*time.Second | |
| func addDTLSRetransmitTests() { | |
| for _, shortTimeout := range []bool{false, true} { | |
| for _, vers := range allVersions(dtls) { | |
| suffix := "-" + vers.name | |
| flags := []string{"-async"} // Retransmit tests require async. | |
| useTimeouts := timeouts | |
| if shortTimeout { | |
| suffix += "-Short" | |
| flags = append(flags, "-initial-timeout-duration-ms", "250") | |
| useTimeouts = shortTimeouts | |
| } | |
| // Testing NewSessionTicket is tricky. First, BoringSSL sends two | |
| // tickets in a row. These are conceptually separate flights, but we | |
| // test them as one flight. Second, these tickets are sent | |
| // concurrently with the runner's first test message. The shim's | |
| // reply will come in before any retransmit challenges. | |
| // handleNewSessionTicket corrects for both effects. | |
| handleNewSessionTicket := func(f ACKFlightFunc) ACKFlightFunc { | |
| if vers.version < VersionTLS13 { | |
| return f | |
| } | |
| return func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| // BoringSSL sends two NewSessionTickets in a row. | |
| if received[0].Type == typeNewSessionTicket && len(received) < 2 { | |
| c.MergeIntoNextFlight() | |
| return | |
| } | |
| // NewSessionTicket is sent in parallel with the runner's | |
| // first application data. Consume the shim's reply. | |
| testMessage := makeTestMessage(0, 32) | |
| if received[0].Type == typeNewSessionTicket { | |
| c.ReadAppData(c.InEpoch(), expectedReply(testMessage)) | |
| } | |
| // Run the test, without any stray messages in the way. | |
| f(c, prev, received, records) | |
| // The test loop is expecting a reply to the first message. | |
| // Prime the shim to send it again. | |
| if received[0].Type == typeNewSessionTicket { | |
| c.WriteAppData(c.OutEpoch(), testMessage) | |
| } | |
| } | |
| } | |
| // In all versions, the sender will retransmit the whole flight if | |
| // it times out and hears nothing. | |
| writeFlightBasic := func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| // Exercise every timeout but the last one (which would fail the | |
| // connection). | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| c.ExpectNextTimeout(t) | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| } | |
| c.ExpectNextTimeout(useTimeouts[len(useTimeouts)-1]) | |
| } | |
| // Finally release the whole flight to the shim. | |
| c.WriteFlight(next) | |
| } | |
| ackFlightBasic := handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if vers.version >= VersionTLS13 { | |
| // In DTLS 1.3, final flights (either handshake or post-handshake) | |
| // are retransmitted until ACKed. Exercise every timeout but | |
| // the last one (which would fail the connection). | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| c.ExpectNextTimeout(t) | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| } | |
| c.ExpectNextTimeout(useTimeouts[len(useTimeouts)-1]) | |
| // Finally ACK the flight. | |
| c.WriteACK(c.OutEpoch(), records) | |
| return | |
| } | |
| // In DTLS 1.2, the final flight is retransmitted on receipt of | |
| // the previous flight. Test the peer is willing to retransmit | |
| // it several times. | |
| for range 5 { | |
| c.WriteFlight(prev) | |
| c.ReadRetransmit() | |
| } | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-Basic" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: writeFlightBasic, | |
| ACKFlightDTLS: ackFlightBasic, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-SetTimeout-Decrease" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| t := useTimeouts[0] | |
| tn := useTimeouts[0] / 2 | |
| c.ExpectNextTimeout(t) | |
| c.SetTimeout(tn) | |
| c.ExpectNextTimeout(tn) | |
| c.AdvanceClock(tn) | |
| c.ReadRetransmit() | |
| // The timer doubles after each retransmit. Timeout updates respect this, | |
| // so the effective timeout should be 2 * t. | |
| c.SetTimeout(t) | |
| c.ExpectNextTimeout(2 * t) | |
| } | |
| // Finally release the whole flight to the shim. | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: ackFlightBasic, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-SetTimeout-Fire" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| t := useTimeouts[0] | |
| tn := useTimeouts[0] / 2 | |
| c.ExpectNextTimeout(t) | |
| c.AdvanceClock(tn) | |
| c.SetTimeout(tn) | |
| // After rescaling the timer, the running timer immediately fires, | |
| // so the next timeout should be reported as zero. | |
| c.ExpectNextTimeout(0) | |
| // Triggering the shim to handle the timeouts will cause it to | |
| // retransmit. | |
| c.AdvanceClock(0) | |
| c.ReadRetransmit() | |
| // The next timeout is the rescaled second timeout. | |
| c.ExpectNextTimeout(useTimeouts[1] / 2) | |
| c.SetTimeout(t) | |
| } | |
| // Finally release the whole flight to the shim. | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: ackFlightBasic, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-SetTimeout-Increase" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| t := useTimeouts[0] | |
| tn := useTimeouts[0] * 2 | |
| c.ExpectNextTimeout(t) | |
| c.SetTimeout(tn) | |
| c.ExpectNextTimeout(tn) | |
| c.AdvanceClock(t) | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| // The timer doubles after each retransmit. Timeout updates respect this, | |
| // so the effective timeout should be 2 * t. | |
| c.SetTimeout(t) | |
| c.ExpectNextTimeout(2 * t) | |
| } | |
| // Finally release the whole flight to the shim. | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: ackFlightBasic, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-Basic" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: writeFlightBasic, | |
| ACKFlightDTLS: ackFlightBasic, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| if vers.version <= VersionTLS12 { | |
| // In DTLS 1.2, receiving a part of the next flight should not stop | |
| // the retransmission timer. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-PartialProgress" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| // Send a portion of the first message. The rest was lost. | |
| msg := next[0] | |
| split := len(msg.Data) / 2 | |
| c.WriteFragments([]DTLSFragment{msg.Fragment(0, split)}) | |
| // If we time out, the shim should still retransmit. It knows | |
| // we received the whole flight, but the shim should use a | |
| // retransmit to request the runner try again. | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| // "Retransmit" the rest of the flight. The shim should remember | |
| // the portion that was already sent. | |
| rest := []DTLSFragment{msg.Fragment(split, len(msg.Data)-split)} | |
| for _, m := range next[1:] { | |
| rest = append(rest, m.Fragment(0, len(m.Data))) | |
| } | |
| c.WriteFragments(rest) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| } else { | |
| // In DTLS 1.3, receiving a part of the next flight implicitly ACKs | |
| // the previous flight. | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-PartialProgress-Server" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| DefaultCurves: []CurveID{}, // Force HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) == 0 && next[0].Type == typeClientHello { | |
| // Send the initial ClientHello as-is. | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // Send a portion of the first message. The rest was lost. | |
| msg := next[0] | |
| split := len(msg.Data) / 2 | |
| c.WriteFragments([]DTLSFragment{msg.Fragment(0, split)}) | |
| // After waiting the current timeout, the shim should ACK | |
| // the partial flight. | |
| c.ExpectNextTimeout(useTimeouts[0] / 4) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(c.InEpoch()) | |
| // The partial flight is enough to ACK the previous flight. | |
| // The shim should stop retransmitting and even stop the | |
| // retransmit timer. | |
| c.ExpectNoNextTimeout() | |
| for _, t := range useTimeouts { | |
| c.AdvanceClock(t) | |
| } | |
| // "Retransmit" the rest of the flight. The shim should remember | |
| // the portion that was already sent. | |
| rest := []DTLSFragment{msg.Fragment(split, len(msg.Data)-split)} | |
| for _, m := range next[1:] { | |
| rest = append(rest, m.Fragment(0, len(m.Data))) | |
| } | |
| c.WriteFragments(rest) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // Same as 'DTLS-Retransmit-PartialProgress-Server' but modifying timeout affecting ACKs. | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-PartialProgress-Server-SetTimeout" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| DefaultCurves: []CurveID{}, // Force HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) == 0 && next[0].Type == typeClientHello { | |
| // Send the initial ClientHello as-is. | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // Send a portion of the first message. The rest was lost. | |
| // This makes the shim schedule an ACK timer. | |
| split := len(next[0].Data) / 2 | |
| c.WriteFragments([]DTLSFragment{next[0].Fragment(0, split)}) | |
| // Modify ongoing ACK timer. | |
| tn := useTimeouts[0] / 2 | |
| c.SetTimeout(tn) | |
| // The ACK timer should have been rescaled with the update. | |
| c.ExpectNextTimeout(tn / 4) | |
| c.AdvanceClock(tn / 4) | |
| c.ReadACK(c.InEpoch()) | |
| // Restore the timeout and continue the test. | |
| c.SetTimeout(useTimeouts[0]) | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // When the shim is a client, receiving fragments before the version is | |
| // known does not trigger this behavior. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-PartialProgress-Client" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| msg := next[0] | |
| if msg.Type != typeServerHello { | |
| // Post-handshake is tested separately. | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // Send a portion of the ServerHello. The rest was lost. | |
| split := len(msg.Data) / 2 | |
| c.WriteFragments([]DTLSFragment{msg.Fragment(0, split)}) | |
| // The shim did not know this was DTLS 1.3, so it still | |
| // retransmits ClientHello. | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| // Finish the ServerHello. The version is still not known, | |
| // at the time the ServerHello fragment is processed, This | |
| // is not as efficient as we could be; we could go back and | |
| // implicitly ACK once the version is known. But the last | |
| // byte of ServerHello will almost certainly be in the same | |
| // packet as EncryptedExtensions, which will trigger the case | |
| // below. | |
| c.WriteFragments([]DTLSFragment{msg.Fragment(split, len(msg.Data)-split)}) | |
| c.ExpectNextTimeout(useTimeouts[1]) | |
| c.AdvanceClock(useTimeouts[1]) | |
| c.ReadRetransmit() | |
| // Send EncryptedExtensions. The shim now knows the version. | |
| c.WriteFragments([]DTLSFragment{next[1].Fragment(0, len(next[1].Data))}) | |
| // The shim should ACK the partial flight. The shim hasn't | |
| // gotten to epoch 3 yet, so the ACK will come in epoch 2. | |
| c.AdvanceClock(useTimeouts[2] / 4) | |
| c.ReadACK(uint16(encryptionHandshake)) | |
| // This is enough to ACK the previous flight. The shim | |
| // should stop retransmitting and even stop the timer. | |
| c.ExpectNoNextTimeout() | |
| for _, t := range useTimeouts[2:] { | |
| c.AdvanceClock(t) | |
| } | |
| // "Retransmit" the rest of the flight. The shim should remember | |
| // the portion that was already sent. | |
| var rest []DTLSFragment | |
| for _, m := range next[2:] { | |
| rest = append(rest, m.Fragment(0, len(m.Data))) | |
| } | |
| c.WriteFragments(rest) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // Same as 'DTLS-Retransmit-PartialProgress-Client' but updating timeout while ACK timer is set. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-PartialProgress-Client-SetTimeout" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[0].Type != typeServerHello { | |
| // Post-handshake is tested separately. | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // Send the ServerHello and half of EncryptedExtensions. The rest was lost. | |
| // This makes the shim schedule an ACK timer. (A partial ServerHello will | |
| // not schedule an ACK timer because the shim has not learned the version.) | |
| c.WriteFlight(next[:1]) | |
| split := len(next[1].Data) / 2 | |
| c.WriteFragments([]DTLSFragment{next[1].Fragment(0, split)}) | |
| // Now modify timeout. | |
| tn := useTimeouts[0] / 2 | |
| c.SetTimeout(tn) | |
| // The ACK timer should have been rescaled with the update. | |
| // The shim hasn't gotten to epoch 3 yet, so the ACK will come in epoch 2. | |
| c.ExpectNextTimeout(tn / 4) | |
| c.AdvanceClock(tn / 4) | |
| c.ReadACK(uint16(encryptionHandshake)) | |
| // Restore the timeout and continue the test. | |
| c.SetTimeout(useTimeouts[0]) | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| } | |
| // Test that exceeding the timeout schedule hits a read | |
| // timeout. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Timeout" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| c.ExpectNextTimeout(t) | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| } | |
| c.ExpectNextTimeout(useTimeouts[len(useTimeouts)-1]) | |
| c.AdvanceClock(useTimeouts[len(useTimeouts)-1]) | |
| // The shim should give up at this point. | |
| }, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| shouldFail: true, | |
| expectedError: ":READ_TIMEOUT_EXPIRED:", | |
| }) | |
| // Test that timeout handling has a fudge factor, due to API | |
| // problems. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Fudge" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| c.AdvanceClock(useTimeouts[0] - 10*time.Millisecond) | |
| c.ReadRetransmit() | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| // Test attempting to handle a timeout in multiple steps. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-HandleTimeoutInSteps" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| c.AdvanceClock(0) | |
| part := useTimeouts[0] / 3 | |
| c.AdvanceClock(part) | |
| c.AdvanceClock(useTimeouts[0] - part) | |
| c.ReadRetransmit() | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| resumeSession: true, | |
| flags: flags, | |
| }) | |
| // Test that the shim can retransmit at different MTUs. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-ChangeMTU" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| // Request a client certificate, so the shim has more to send. | |
| ClientAuth: RequireAnyClientCert, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| for i, mtu := range []int{300, 301, 302, 303, 299, 298, 297} { | |
| c.SetMTU(mtu) | |
| c.AdvanceClock(useTimeouts[i]) | |
| c.ReadRetransmit() | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| shimCertificate: &rsaChainCertificate, | |
| flags: flags, | |
| }) | |
| // DTLS 1.3 uses explicit ACKs. | |
| if vers.version >= VersionTLS13 { | |
| // The two server flights (HelloRetryRequest and ServerHello..Finished) | |
| // happen after the shim has learned the version, so they are more | |
| // straightforward. In these tests, we trigger HelloRetryRequest, | |
| // and also use ML-KEM with rsaChainCertificate and a limited MTU, | |
| // to increase the number of records and exercise more complex | |
| // ACK patterns. | |
| // After ACKing everything, the shim should stop retransmitting. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKEverything" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Credential: &rsaChainCertificate, | |
| CurvePreferences: []CurveID{CurveX25519MLKEM768}, | |
| DefaultCurves: []CurveID{}, // Force HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| // Send smaller packets to exercise more ACK cases. | |
| MaxPacketLength: 512, | |
| MaxHandshakeRecordLength: 512, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| ackEpoch := received[len(received)-1].Epoch | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| c.WriteACK(ackEpoch, records) | |
| // After everything is ACKed, the shim should stop the timer | |
| // and wait for the next flight. | |
| c.ExpectNoNextTimeout() | |
| for _, t := range useTimeouts { | |
| c.AdvanceClock(t) | |
| } | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| ackEpoch := received[len(received)-1].Epoch | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| c.WriteACK(ackEpoch, records) | |
| // After everything is ACKed, the shim should stop the timer. | |
| c.ExpectNoNextTimeout() | |
| for _, t := range useTimeouts { | |
| c.AdvanceClock(t) | |
| } | |
| }), | |
| SequenceNumberMapping: func(in uint64) uint64 { | |
| // Perturb sequence numbers to test that ACKs are sorted. | |
| return in ^ 63 | |
| }, | |
| }, | |
| }, | |
| shimCertificate: &rsaChainCertificate, | |
| flags: slices.Concat(flags, []string{ | |
| "-mtu", "512", | |
| "-curves", strconv.Itoa(int(CurveX25519MLKEM768)), | |
| // Request a client certificate so the client final flight is | |
| // larger. | |
| "-require-any-client-certificate", | |
| }), | |
| }) | |
| // ACK packets one by one, in reverse. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKReverse" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| CurvePreferences: []CurveID{CurveX25519MLKEM768}, | |
| DefaultCurves: []CurveID{}, // Force HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| MaxPacketLength: 512, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| ackEpoch := received[len(received)-1].Epoch | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| c.WriteACK(ackEpoch, []DTLSRecordNumberInfo{records[len(records)-1]}) | |
| } | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| ackEpoch := received[len(received)-1].Epoch | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| c.WriteACK(ackEpoch, []DTLSRecordNumberInfo{records[len(records)-1]}) | |
| } | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| }), | |
| }, | |
| }, | |
| shimCertificate: &rsaChainCertificate, | |
| flags: slices.Concat(flags, []string{"-mtu", "512", "-curves", strconv.Itoa(int(CurveX25519MLKEM768))}), | |
| }) | |
| // ACK packets one by one, forwards. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKForwards" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| CurvePreferences: []CurveID{CurveX25519MLKEM768}, | |
| DefaultCurves: []CurveID{}, // Force HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| MaxPacketLength: 512, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| ackEpoch := received[len(received)-1].Epoch | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| c.WriteACK(ackEpoch, []DTLSRecordNumberInfo{records[0]}) | |
| } | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| ackEpoch := received[len(received)-1].Epoch | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| c.WriteACK(ackEpoch, []DTLSRecordNumberInfo{records[0]}) | |
| } | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| }), | |
| }, | |
| }, | |
| shimCertificate: &rsaChainCertificate, | |
| flags: slices.Concat(flags, []string{"-mtu", "512", "-curves", strconv.Itoa(int(CurveX25519MLKEM768))}), | |
| }) | |
| // ACK 1/3 the packets each time. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKIterate" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| CurvePreferences: []CurveID{CurveX25519MLKEM768}, | |
| DefaultCurves: []CurveID{}, // Force HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| MaxPacketLength: 512, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| ackEpoch := received[len(received)-1].Epoch | |
| for i, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| ack := make([]DTLSRecordNumberInfo, 0, (len(records)+2)/3) | |
| for i := 0; i < len(records); i += 3 { | |
| ack = append(ack, records[i]) | |
| } | |
| c.WriteACK(ackEpoch, ack) | |
| } | |
| // Change the MTU every iteration, to make the fragment | |
| // patterns more complex. | |
| c.SetMTU(512 + i) | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| ackEpoch := received[len(received)-1].Epoch | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| c.WriteACK(ackEpoch, []DTLSRecordNumberInfo{records[0]}) | |
| } | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| }), | |
| }, | |
| }, | |
| shimCertificate: &rsaChainCertificate, | |
| flags: slices.Concat(flags, []string{"-mtu", "512", "-curves", strconv.Itoa(int(CurveX25519MLKEM768))}), | |
| }) | |
| // ACKing packets that have already been ACKed is a no-op. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKDuplicate" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| SendHelloRetryRequestCookie: []byte("cookie"), | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| ackEpoch := received[len(received)-1].Epoch | |
| // Keep ACKing the same record over and over. | |
| c.WriteACK(ackEpoch, records[:1]) | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| c.WriteACK(ackEpoch, records[:1]) | |
| c.AdvanceClock(useTimeouts[1]) | |
| c.ReadRetransmit() | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| ackEpoch := received[len(received)-1].Epoch | |
| // Keep ACKing the same record over and over. | |
| c.WriteACK(ackEpoch, records[:1]) | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| c.WriteACK(ackEpoch, records[:1]) | |
| c.AdvanceClock(useTimeouts[1]) | |
| c.ReadRetransmit() | |
| // ACK everything to clear the timer. | |
| c.WriteACK(ackEpoch, records) | |
| }), | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // When ACKing ServerHello..Finished, the ServerHello might be | |
| // ACKed at epoch 0 or epoch 2, depending on how far the client | |
| // received. Test that epoch 0 is allowed by ACKing each packet | |
| // at the record it was received. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKMatchingEpoch" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| if len(records) > 0 { | |
| c.WriteACK(uint16(records[0].Epoch), []DTLSRecordNumberInfo{records[0]}) | |
| } | |
| c.AdvanceClock(t) | |
| records = c.ReadRetransmit() | |
| } | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // However, records in the handshake may not be ACKed at lower | |
| // epoch than they were received. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKBadEpoch" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) == 0 { | |
| // Send the ClientHello. | |
| c.WriteFlight(next) | |
| } else { | |
| // Try to ACK ServerHello..Finished at epoch 0. The shim should reject this. | |
| c.WriteACK(0, records) | |
| } | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| shouldFail: true, | |
| expectedError: ":DECODE_ERROR:", | |
| }) | |
| // The bad epoch check should notice when the epoch number | |
| // would overflow 2^16. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKEpochOverflow" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) == 0 { | |
| // Send the ClientHello. | |
| c.WriteFlight(next) | |
| } else { | |
| r := records[0] | |
| r.Epoch += 1 << 63 | |
| c.WriteACK(0, []DTLSRecordNumberInfo{r}) | |
| } | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| shouldFail: true, | |
| expectedError: ":DECODE_ERROR:", | |
| }) | |
| // ACK some records from the first transmission, trigger a | |
| // retransmit, but then ACK the rest of the first transmission. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKOldRecords" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| CurvePreferences: []CurveID{CurveX25519MLKEM768}, | |
| Bugs: ProtocolBugs{ | |
| MaxPacketLength: 512, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| ackEpoch := received[len(received)-1].Epoch | |
| c.WriteACK(ackEpoch, records[len(records)/2:]) | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| c.WriteACK(ackEpoch, records[:len(records)/2]) | |
| // Everything should be ACKed now. The shim should not | |
| // retransmit anything. | |
| c.AdvanceClock(useTimeouts[1]) | |
| c.AdvanceClock(useTimeouts[2]) | |
| c.AdvanceClock(useTimeouts[3]) | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: slices.Concat(flags, []string{"-mtu", "512", "-curves", strconv.Itoa(int(CurveX25519MLKEM768))}), | |
| }) | |
| // If the shim sends too many records, it will eventually forget them. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKForgottenRecords" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| CurvePreferences: []CurveID{CurveX25519MLKEM768}, | |
| Bugs: ProtocolBugs{ | |
| MaxPacketLength: 256, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) > 0 { | |
| // Make the peer retransmit many times, with a small MTU. | |
| for _, t := range useTimeouts[:len(useTimeouts)-2] { | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| } | |
| // ACK the first record the shim ever sent. It will have | |
| // fallen off the queue by now, so it is expected to not | |
| // impact the shim's retransmissions. | |
| c.WriteACK(c.OutEpoch(), []DTLSRecordNumberInfo{{DTLSRecordNumber: records[0].DTLSRecordNumber}}) | |
| c.AdvanceClock(useTimeouts[len(useTimeouts)-2]) | |
| c.ReadRetransmit() | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: slices.Concat(flags, []string{"-mtu", "256", "-curves", strconv.Itoa(int(CurveX25519MLKEM768))}), | |
| }) | |
| // The shim should ignore ACKs for a previous flight, and not get its | |
| // internal state confused. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKPreviousFlight" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| DefaultCurves: []CurveID{}, // Force a HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[len(next)-1].Type == typeFinished { | |
| // We are now sending client Finished, in response | |
| // to the shim's ServerHello. ACK the shim's first | |
| // record, which would have been part of | |
| // HelloRetryRequest. This should not impact retransmit. | |
| c.WriteACK(c.OutEpoch(), []DTLSRecordNumberInfo{{DTLSRecordNumber: DTLSRecordNumber{Epoch: 0, Sequence: 0}}}) | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| } | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // Records that contain a mix of discarded and processed fragments should | |
| // not be ACKed. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-DoNotACKDiscardedFragments" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| DefaultCurves: []CurveID{}, // Force a HelloRetryRequest. | |
| Bugs: ProtocolBugs{ | |
| PackHandshakeFragments: 4096, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| // Send the flight, but combine every fragment with a far future | |
| // fragment, which the shim will discard. During the handshake, | |
| // the shim has enough information to reject this entirely, but | |
| // that would require coordinating with the handshake state | |
| // machine. Instead, BoringSSL discards the fragment and skips | |
| // ACKing the packet. | |
| // | |
| // runner implicitly tests that the shim ACKs the Finished flight | |
| // (or, in case, that it is does not), so this exercises the final | |
| // ACK. | |
| for _, msg := range next { | |
| shouldDiscard := DTLSFragment{Epoch: msg.Epoch, Sequence: 1000, ShouldDiscard: true} | |
| c.WriteFragments([]DTLSFragment{shouldDiscard, msg.Fragment(0, len(msg.Data))}) | |
| // The shim has nothing to ACK and thus no ACK timer (which | |
| // would be 1/4 of this value). | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| } | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| // The server must continue to ACK the Finished flight even after | |
| // receiving application data from the client. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-Retransmit-Server-ACKFinishedAfterAppData" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| // WriteFlightDTLS will handle consuming ACKs. | |
| SkipImplicitACKRead: true, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[len(next)-1].Type != typeFinished { | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // Write Finished. The shim should ACK it immediately. | |
| c.WriteFlight(next) | |
| c.ReadACK(c.InEpoch()) | |
| // Exchange some application data. | |
| msg := []byte("hello") | |
| c.WriteAppData(c.OutEpoch(), msg) | |
| c.ReadAppData(c.InEpoch(), expectedReply(msg)) | |
| // Act as if the ACK was dropped and retransmit Finished. | |
| // The shim should process the retransmit from epoch 2 and | |
| // ACK, although it has already received data at epoch 3. | |
| c.WriteFlight(next) | |
| ackTimeout := useTimeouts[0] / 4 | |
| c.AdvanceClock(ackTimeout) | |
| c.ReadACK(c.InEpoch()) | |
| // Partially retransmit Finished. The shim should continue | |
| // to ACK. | |
| c.WriteFragments([]DTLSFragment{next[0].Fragment(0, 1)}) | |
| c.WriteFragments([]DTLSFragment{next[0].Fragment(1, 1)}) | |
| c.AdvanceClock(ackTimeout) | |
| c.ReadACK(c.InEpoch()) | |
| // Eventually, the shim assumes we have received the ACK | |
| // and drops epoch 2. Retransmits now go unanswered. | |
| c.AdvanceClock(dtlsPrevEpochExpiration) | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| // Disable tickets on the shim to avoid NewSessionTicket | |
| // interfering with the test callback. | |
| flags: slices.Concat(flags, []string{"-no-ticket"}), | |
| }) | |
| // As a client, the shim must tolerate ACKs in response to its | |
| // initial ClientHello, but it will not process them because the | |
| // version is not yet known. The second ClientHello, in response | |
| // to HelloRetryRequest, however, is ACKed. | |
| // | |
| // The shim must additionally process ACKs and retransmit its | |
| // Finished flight, possibly interleaved with application data. | |
| // (The server may send half-RTT data without Finished.) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| // Require a client certificate, so the Finished flight | |
| // is large. | |
| ClientAuth: RequireAnyClientCert, | |
| Bugs: ProtocolBugs{ | |
| SendHelloRetryRequestCookie: []byte("cookie"), // Send HelloRetryRequest | |
| MaxPacketLength: 512, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if len(received) == 0 || received[0].Type != typeClientHello { | |
| // We test post-handshake flights separately. | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // This is either HelloRetryRequest in response to ClientHello1, | |
| // or ServerHello..Finished in response to ClientHello2. | |
| first := records[0] | |
| if len(prev) == 0 { | |
| // This is HelloRetryRequest in response to ClientHello1. The client | |
| // will accept the ACK, but it will ignore it. Do not expect | |
| // retransmits to be impacted. | |
| first.MessageStartSequence = 0 | |
| first.MessageStartOffset = 0 | |
| first.MessageEndSequence = 0 | |
| first.MessageEndOffset = 0 | |
| } | |
| c.WriteACK(0, []DTLSRecordNumberInfo{first}) | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| c.WriteFlight(next) | |
| }, | |
| ACKFlightDTLS: func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| // The shim will process application data without an ACK. | |
| msg := []byte("hello") | |
| c.WriteAppData(c.OutEpoch(), msg) | |
| c.ReadAppData(c.InEpoch(), expectedReply(msg)) | |
| // After a timeout, the shim will retransmit Finished. | |
| c.AdvanceClock(useTimeouts[0]) | |
| c.ReadRetransmit() | |
| // Application data still flows. | |
| c.WriteAppData(c.OutEpoch(), msg) | |
| c.ReadAppData(c.InEpoch(), expectedReply(msg)) | |
| // ACK part of the flight and check that retransmits | |
| // are updated. | |
| c.WriteACK(c.OutEpoch(), records[len(records)/3:2*len(records)/3]) | |
| c.AdvanceClock(useTimeouts[1]) | |
| records = c.ReadRetransmit() | |
| // ACK the rest. Retransmits should stop. | |
| c.WriteACK(c.OutEpoch(), records) | |
| for _, t := range useTimeouts[2:] { | |
| c.AdvanceClock(t) | |
| } | |
| }, | |
| }, | |
| }, | |
| shimCertificate: &rsaChainCertificate, | |
| flags: slices.Concat(flags, []string{"-mtu", "512", "-curves", strconv.Itoa(int(CurveX25519MLKEM768))}), | |
| }) | |
| // If the client never receives an ACK for the Finished flight, it | |
| // is eventually fatal. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-FinishedTimeout" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| ACKFlightDTLS: func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| } | |
| c.AdvanceClock(useTimeouts[len(useTimeouts)-1]) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| shouldFail: true, | |
| expectedError: ":READ_TIMEOUT_EXPIRED:", | |
| }) | |
| // Neither post-handshake messages nor application data implicitly | |
| // ACK the Finished flight. The server may have sent either in | |
| // half-RTT data. Test that the client continues to retransmit | |
| // despite this. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-NoImplictACKFinished" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| ACKFlightDTLS: func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| // Merge the Finished flight into the NewSessionTicket. | |
| c.MergeIntoNextFlight() | |
| }, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[0].Type != typeNewSessionTicket { | |
| c.WriteFlight(next) | |
| return | |
| } | |
| if len(received) == 0 || received[0].Type != typeFinished { | |
| panic("Finished should be merged with NewSessionTicket") | |
| } | |
| // Merge NewSessionTicket into the KeyUpdate. | |
| if next[len(next)-1].Type != typeKeyUpdate { | |
| c.MergeIntoNextFlight() | |
| return | |
| } | |
| // Write NewSessionTicket and the KeyUpdate and | |
| // read the ACK. | |
| c.WriteFlight(next) | |
| ackTimeout := useTimeouts[0] / 4 | |
| c.AdvanceClock(ackTimeout) | |
| c.ReadACK(c.InEpoch()) | |
| // The retransmit timer is still running. | |
| c.AdvanceClock(useTimeouts[0] - ackTimeout) | |
| c.ReadRetransmit() | |
| // Application data can flow at the old epoch. | |
| msg := []byte("test") | |
| c.WriteAppData(c.OutEpoch()-1, msg) | |
| c.ReadAppData(c.InEpoch(), expectedReply(msg)) | |
| // The retransmit timer is still running. | |
| c.AdvanceClock(useTimeouts[1]) | |
| c.ReadRetransmit() | |
| // Advance the shim to the next epoch. | |
| c.WriteAppData(c.OutEpoch(), msg) | |
| c.ReadAppData(c.InEpoch(), expectedReply(msg)) | |
| // The retransmit timer is still running. The shim | |
| // actually could implicitly ACK at this point, but | |
| // RFC 9147 does not list this as an implicit ACK. | |
| c.AdvanceClock(useTimeouts[2]) | |
| c.ReadRetransmit() | |
| // Finally ACK the final flight. Now the shim will | |
| // stop the timer. | |
| c.WriteACK(c.OutEpoch(), records) | |
| c.ExpectNoNextTimeout() | |
| }, | |
| }, | |
| }, | |
| sendKeyUpdates: 1, | |
| keyUpdateRequest: keyUpdateNotRequested, | |
| flags: flags, | |
| }) | |
| // If the server never receives an ACK for NewSessionTicket, it | |
| // is eventually fatal. | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Server-NewSessionTicketTimeout" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| ACKFlightDTLS: handleNewSessionTicket(func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if received[0].Type != typeNewSessionTicket { | |
| c.WriteACK(c.OutEpoch(), records) | |
| return | |
| } | |
| // Time the peer out. | |
| for _, t := range useTimeouts[:len(useTimeouts)-1] { | |
| c.AdvanceClock(t) | |
| c.ReadRetransmit() | |
| } | |
| c.AdvanceClock(useTimeouts[len(useTimeouts)-1]) | |
| }), | |
| }, | |
| }, | |
| flags: flags, | |
| shouldFail: true, | |
| expectedError: ":READ_TIMEOUT_EXPIRED:", | |
| }) | |
| // If generating the reply to a flight takes time (generating a | |
| // CertificateVerify for a client certificate), the shim should | |
| // send an ACK. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-SlowReplyGeneration" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| ClientAuth: RequireAnyClientCert, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| c.WriteFlight(next) | |
| if next[0].Type == typeServerHello { | |
| // The shim will reply with Certificate..Finished, but | |
| // take time to do so. In that time, it should schedule | |
| // an ACK so the runner knows not to retransmit. | |
| c.ReadACK(c.InEpoch()) | |
| } | |
| }, | |
| }, | |
| }, | |
| shimCertificate: &rsaCertificate, | |
| // Simulate it taking time to generate the reply. | |
| flags: slices.Concat(flags, []string{"-private-key-delay-ms", strconv.Itoa(int(useTimeouts[0].Milliseconds()))}), | |
| }) | |
| // BoringSSL's ACK policy may schedule both retransmit and ACK | |
| // timers in parallel. | |
| // | |
| // TODO(crbug.com/42290594): This is only possible during the | |
| // handshake because we're willing to ACK old flights without | |
| // trying to distinguish these cases. However, post-handshake | |
| // messages will exercise this, so that may be a better version | |
| // of this test. In-handshake, it's kind of a waste to ACK this, | |
| // so maybe we should stop. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-BothTimers" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| // Arrange for there to be two server flights. | |
| SendHelloRetryRequestCookie: []byte("cookie"), | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[0].Sequence == 0 || next[0].Type != typeServerHello { | |
| // Send the first flight (HelloRetryRequest) as-is, | |
| // as well as any post-handshake flights. | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // The shim just send the ClientHello2 and is | |
| // waiting for ServerHello..Finished. If it hears | |
| // nothing, it will retransmit ClientHello2 on the | |
| // assumption the packet was lost. | |
| c.ExpectNextTimeout(useTimeouts[0]) | |
| // Retransmit a portion of HelloRetryRequest. | |
| c.WriteFragments([]DTLSFragment{prev[0].Fragment(0, 1)}) | |
| // The shim does not actually need to ACK this, | |
| // but BoringSSL does. Now both timers are active. | |
| // Fire the first... | |
| c.ExpectNextTimeout(useTimeouts[0] / 4) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(0) | |
| // ...followed by the second. | |
| c.ExpectNextTimeout(3 * useTimeouts[0] / 4) | |
| c.AdvanceClock(3 * useTimeouts[0] / 4) | |
| c.ReadRetransmit() | |
| // The shim is now set for the next retransmit. | |
| c.ExpectNextTimeout(useTimeouts[1]) | |
| // Start the ACK timer again. | |
| c.WriteFragments([]DTLSFragment{prev[0].Fragment(0, 1)}) | |
| c.ExpectNextTimeout(useTimeouts[1] / 4) | |
| // Expire both timers at once. | |
| c.AdvanceClock(useTimeouts[1]) | |
| c.ReadACK(0) | |
| c.ReadRetransmit() | |
| c.WriteFlight(next) | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-ACKPostHandshake" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[0].Type != typeNewSessionTicket { | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // The test should try to send two NewSessionTickets in a row. | |
| if len(next) != 2 { | |
| panic("unexpected message count") | |
| } | |
| // Send part of first ticket post-handshake message. | |
| first0, second0 := next[0].Split(len(next[0].Data) / 2) | |
| first1, second1 := next[1].Split(len(next[1].Data) / 2) | |
| c.WriteFragments([]DTLSFragment{first0}) | |
| // The shim should ACK on a timer. | |
| c.ExpectNextTimeout(useTimeouts[0] / 4) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(c.InEpoch()) | |
| // The shim is just waiting for us to retransmit. | |
| c.ExpectNoNextTimeout() | |
| // Send some more fragments. | |
| c.WriteFragments([]DTLSFragment{first0, second1}) | |
| // The shim should ACK, again on a timer. | |
| c.ExpectNextTimeout(useTimeouts[0] / 4) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(c.InEpoch()) | |
| c.ExpectNoNextTimeout() | |
| // Finish up both messages. We implicitly test if shim | |
| // processed these messages by checking that it returned a new | |
| // session. | |
| c.WriteFragments([]DTLSFragment{first1, second0}) | |
| // The shim should ACK again, once the timer expires. | |
| // | |
| // TODO(crbug.com/42290594): Should the shim ACK immediately? | |
| // Otherwise KeyUpdates are delayed, which will complicated | |
| // downstream testing. | |
| c.ExpectNextTimeout(useTimeouts[0] / 4) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(c.InEpoch()) | |
| c.ExpectNoNextTimeout() | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "DTLS-Retransmit-Client-ACKPostHandshakeTwice" + suffix, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| if next[0].Type != typeNewSessionTicket { | |
| c.WriteFlight(next) | |
| return | |
| } | |
| // The test should try to send two NewSessionTickets in a row. | |
| if len(next) != 2 { | |
| panic("unexpected message count") | |
| } | |
| // Send the flight. The shim should ACK it. | |
| c.WriteFlight(next) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(c.InEpoch()) | |
| c.ExpectNoNextTimeout() | |
| // Retransmit the flight, as if we lost the ACK. The shim should | |
| // ACK again. | |
| c.WriteFlight(next) | |
| c.AdvanceClock(useTimeouts[0] / 4) | |
| c.ReadACK(c.InEpoch()) | |
| c.ExpectNoNextTimeout() | |
| }, | |
| }, | |
| }, | |
| flags: flags, | |
| }) | |
| } | |
| } | |
| } | |
| // Test that the final Finished retransmitting isn't | |
| // duplicated if the peer badly fragments everything. | |
| testCases = append(testCases, testCase{ | |
| testType: serverTest, | |
| protocol: dtls, | |
| name: "DTLS-RetransmitFinished-Fragmented", | |
| config: Config{ | |
| MaxVersion: VersionTLS12, | |
| Bugs: ProtocolBugs{ | |
| MaxHandshakeRecordLength: 2, | |
| ACKFlightDTLS: func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| c.WriteFlight(prev) | |
| c.ReadRetransmit() | |
| }, | |
| }, | |
| }, | |
| flags: []string{"-async"}, | |
| }) | |
| // If the shim sends the last Finished (server full or client resume | |
| // handshakes), it must retransmit that Finished when it sees a | |
| // post-handshake penultimate Finished from the runner. The above tests | |
| // cover this. Conversely, if the shim sends the penultimate Finished | |
| // (client full or server resume), test that it does not retransmit. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: clientTest, | |
| name: "DTLS-StrayRetransmitFinished-ClientFull", | |
| config: Config{ | |
| MaxVersion: VersionTLS12, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| c.WriteFlight(next) | |
| for _, msg := range next { | |
| if msg.Type == typeFinished { | |
| c.WriteFlight([]DTLSMessage{msg}) | |
| } | |
| } | |
| }, | |
| }, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-StrayRetransmitFinished-ServerResume", | |
| config: Config{ | |
| MaxVersion: VersionTLS12, | |
| }, | |
| resumeConfig: &Config{ | |
| MaxVersion: VersionTLS12, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| c.WriteFlight(next) | |
| for _, msg := range next { | |
| if msg.Type == typeFinished { | |
| c.WriteFlight([]DTLSMessage{msg}) | |
| } | |
| } | |
| }, | |
| }, | |
| }, | |
| resumeSession: true, | |
| }) | |
| } | |
| func addDTLSReorderTests() { | |
| for _, vers := range allVersions(dtls) { | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "ReorderHandshakeFragments-Small-DTLS-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| ReorderHandshakeFragments: true, | |
| // Small enough that every handshake message is | |
| // fragmented. | |
| MaxHandshakeRecordLength: 2, | |
| }, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "ReorderHandshakeFragments-Large-DTLS-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| ReorderHandshakeFragments: true, | |
| // Large enough that no handshake message is | |
| // fragmented. | |
| MaxHandshakeRecordLength: 2048, | |
| }, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| name: "MixCompleteMessageWithFragments-DTLS-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| ReorderHandshakeFragments: true, | |
| MixCompleteMessageWithFragments: true, | |
| MaxHandshakeRecordLength: 2, | |
| }, | |
| }, | |
| }) | |
| } | |
| } | |
| func addDTLSFragmentWindowTests() { | |
| for _, vers := range allVersions(dtls) { | |
| // A handshake fragment with a sequence number exactly at the upper bound | |
| // of the sliding window (handshake_read_seq + SSL_MAX_HANDSHAKE_FLIGHT, | |
| // which is +7) should be silently discarded rather than triggering a | |
| // fatal internal error alert. | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: serverTest, | |
| name: "DTLS-FragmentWindow-UpperLimit-Discard-Server-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| for _, msg := range next { | |
| if msg.Sequence == 0 && !msg.IsChangeCipherSpec { | |
| // Injected future fragment with offset +7. | |
| // Since SSL_MAX_HANDSHAKE_FLIGHT is 7, sequence + 7 is at the upper limit. | |
| // This must be silently discarded by the shim. | |
| shouldDiscard := DTLSFragment{ | |
| Epoch: msg.Epoch, | |
| Type: msg.Type, | |
| Sequence: msg.Sequence + 7, | |
| Offset: 0, | |
| TotalLength: len(msg.Data), | |
| Data: msg.Data, | |
| ShouldDiscard: true, | |
| } | |
| c.WriteFragments([]DTLSFragment{shouldDiscard, msg.Fragment(0, len(msg.Data))}) | |
| } else { | |
| c.WriteFragments([]DTLSFragment{msg.Fragment(0, len(msg.Data))}) | |
| } | |
| } | |
| }, | |
| }, | |
| }, | |
| }) | |
| testCases = append(testCases, testCase{ | |
| protocol: dtls, | |
| testType: clientTest, | |
| name: "DTLS-FragmentWindow-UpperLimit-Discard-Client-" + vers.name, | |
| config: Config{ | |
| MaxVersion: vers.version, | |
| Bugs: ProtocolBugs{ | |
| PackHandshakeFragments: 4096, | |
| WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| for _, msg := range next { | |
| if msg.Sequence == 0 && !msg.IsChangeCipherSpec { | |
| // Injected future fragment with offset +7. | |
| // This must be silently discarded by the shim. | |
| shouldDiscard := DTLSFragment{ | |
| Epoch: msg.Epoch, | |
| Type: msg.Type, | |
| Sequence: msg.Sequence + 7, | |
| Offset: 0, | |
| TotalLength: len(msg.Data), | |
| Data: msg.Data, | |
| ShouldDiscard: true, | |
| } | |
| c.WriteFragments([]DTLSFragment{shouldDiscard, msg.Fragment(0, len(msg.Data))}) | |
| } else { | |
| c.WriteFragments([]DTLSFragment{msg.Fragment(0, len(msg.Data))}) | |
| } | |
| } | |
| }, | |
| }, | |
| }, | |
| }) | |
| } | |
| } |