Repository: quic-go/quic-go
Pull request #4080 (2023-09-12 03:03:46)

ackhandler: detect ECN mangling
Mangling means that a path is re-marking all ECN-marked packets as CE.

This doesn't yet cover the corner case where some testing packets are lost, and all the others are CE-marked. I'm not sure how to properly log / trace that.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#4080](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (1b0868a) into [master](https://app.codecov.io/gh/quic-go/quic-go/commit/d6ac6300a443a636804399a1cd690ebafddc603d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d6ac630) will **increase** coverage by `0.02%`.
> Report is 2 commits behind head on master.
> The diff coverage is `68.52%`.

```diff
@@            Coverage Diff             @@
##           master    #4080      +/-   ##
==========================================
+ Coverage   83.49%   83.51%   +0.02%     
==========================================
  Files         148      148              
  Lines       15220    15238      +18     
==========================================
+ Hits        12707    12725      +18     
+ Misses       2010     2009       -1     
- Partials      503      504       +1     
```


| [Files Changed](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [internal/qtls/go120.go](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvcXRscy9nbzEyMC5nbw==) | `28.17% <0.00%> (ø)` | |
| [internal/qtls/go121.go](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvcXRscy9nbzEyMS5nbw==) | `56.84% <11.11%> (+1.74%)` | :arrow_up: |
| [internal/handshake/crypto\_setup.go](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvaGFuZHNoYWtlL2NyeXB0b19zZXR1cC5nbw==) | `57.73% <73.33%> (+0.55%)` | :arrow_up: |
| [internal/ackhandler/ecn.go](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvYWNraGFuZGxlci9lY24uZ28=) | `84.38% <100.00%> (+0.61%)` | :arrow_up: |
| [internal/handshake/session\_ticket.go](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvaGFuZHNoYWtlL3Nlc3Npb25fdGlja2V0Lmdv) | `100.00% <100.00%> (ø)` | |
| [qlog/types.go](https://app.codecov.io/gh/quic-go/quic-go/pull/4080?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cWxvZy90eXBlcy5nbw==) | `88.00% <100.00%> (+0.10%)` | :arrow_up: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/quic-go/quic-go/pull/4080/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)



Patch:
diff --git a/internal/ackhandler/ecn.go b/internal/ackhandler/ecn.go
index ff0dfe65..bb8b37f8 100644
--- a/internal/ackhandler/ecn.go
+++ b/internal/ackhandler/ecn.go
@@ -218,7 +218,21 @@ func (e *ecnTracker) HandleNewlyAcked(packets []*packet, ect0, ect1, ecnce int64
 		return false
 	}
 
+	// update our counters
+	e.numAckedECT0 = ect0
+	e.numAckedECT1 = ect1
+	e.numAckedECNCE = ecnce
+
 	if e.state == ecnStateTesting || e.state == ecnStateUnknown {
+		// Detect mangling (a path remarking all ECN-marked testing packets as CE).
+		if e.numSentECT0+e.numSentECT1 == e.numAckedECNCE && e.numAckedECNCE >= numECNTestingPackets {
+			if e.tracer != nil {
+				e.tracer.ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
+			}
+			e.state = ecnStateFailed
+			return false
+		}
+
 		var ackedTestingPacket bool
 		for _, p := range packets {
 			if e.isTestingPacket(p.PacketNumber) {
@@ -236,12 +250,9 @@ func (e *ecnTracker) HandleNewlyAcked(packets []*packet, ect0, ect1, ecnce int64
 		}
 	}
 
-	// update our counters
-	e.numAckedECT0 = ect0
-	e.numAckedECT1 = ect1
-	e.numAckedECNCE = ecnce
-
-	return newECNCE > 0
+	// Don't trust CE marks before having confirmed ECN capability of the path.
+	// Otherwise, mangling would be misinterpreted as actual congestion.
+	return e.state == ecnStateCapable && newECNCE > 0
 }
 
 func (e *ecnTracker) ecnMarking(pn protocol.PacketNumber) protocol.ECN {
diff --git a/logging/mockgen.go b/logging/mockgen.go
deleted file mode 100644
index e44c15e1..00000000
--- a/logging/mockgen.go
+++ /dev/null
@@ -1,4 +0,0 @@
-package logging
-
-//go:generate sh -c "go run go.uber.org/mock/mockgen -package logging -self_package github.com/quic-go/quic-go/logging -destination mock_connection_tracer_test.go github.com/quic-go/quic-go/logging ConnectionTracer"
-//go:generate sh -c "go run go.uber.org/mock/mockgen -package logging -self_package github.com/quic-go/quic-go/logging -destination mock_tracer_test.go github.com/quic-go/quic-go/logging Tracer"
diff --git a/logging/types.go b/logging/types.go
index bfee91c3..0d79b0a9 100644
--- a/logging/types.go
+++ b/logging/types.go
@@ -121,6 +121,8 @@ const (
 	ECNFailedLostAllTestingPackets
 	// ECNFailedMoreECNCountsThanSent is emitted when an ACK contains more ECN counts than ECN-marked packets were sent
 	ECNFailedMoreECNCountsThanSent
-	// ECNFailedTooFewECNCounts is emitted when contains fewer ECT(0) / ECT(1) counts than it acknowledges packets
+	// ECNFailedTooFewECNCounts is emitted when an ACK contains fewer ECN counts than it acknowledges packets
 	ECNFailedTooFewECNCounts
+	// ECNFailedManglingDetected is emitted when the path marks all ECN-marked packets as CE
+	ECNFailedManglingDetected
 )
diff --git a/qlog/types.go b/qlog/types.go
index 49b4853a..e0c0b243 100644
--- a/qlog/types.go
+++ b/qlog/types.go
@@ -364,6 +364,8 @@ func (e ecnStateTrigger) String() string {
 		return "ACK contains more ECN counts than ECN-marked packets sent"
 	case logging.ECNFailedTooFewECNCounts:
 		return "ACK contains fewer new ECN counts than acknowledged ECN-marked packets"
+	case logging.ECNFailedManglingDetected:
+		return "ECN mangling detected"
 	default:
 		return "unknown ECN state trigger"
 	}


Test patch:
diff --git a/internal/ackhandler/ecn_test.go b/internal/ackhandler/ecn_test.go
index 74fff094..984a97a9 100644
--- a/internal/ackhandler/ecn_test.go
+++ b/internal/ackhandler/ecn_test.go
@@ -175,6 +175,36 @@ var _ = Describe("ECN tracker", func() {
 		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 15), 3, 0, 2)).To(BeFalse())
 	})
 
+	It("detects ECN mangling", func() {
+		sendAllTestingPackets()
+		for i := 10; i < 20; i++ {
+			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
+			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+		}
+		// ECN capability not confirmed yet, therefore CE marks are not regarded as congestion events
+		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(0, 1, 2, 3), 0, 0, 4)).To(BeFalse())
+		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 10, 11, 12), 0, 0, 7)).To(BeFalse())
+		// With the next ACK, all testing packets will now have been marked CE.
+		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
+		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 13), 0, 0, 10)).To(BeFalse())
+	})
+
+	It("only detects ECN mangling after sending all testing packets", func() {
+		tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
+		for i := 0; i < 9; i++ {
+			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
+			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
+			Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(protocol.PacketNumber(i)), 0, 0, int64(i+1))).To(BeFalse())
+		}
+		// Send the last testing packet, and receive a
+		tracer.EXPECT().ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
+		Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
+		ecnTracker.SentPacket(9, protocol.ECT0)
+		// This ACK now reports the last testing packets as CE as well.
+		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
+		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(9), 0, 0, 10)).To(BeFalse())
+	})
+
 	It("declares congestion", func() {
 		sendAllTestingPackets()
 		for i := 10; i < 20; i++ {
diff --git a/logging/logging_suite_test.go b/logging/logging_suite_test.go
index d808adfe..e595313d 100644
--- a/logging/logging_suite_test.go
+++ b/logging/logging_suite_test.go
@@ -1,4 +1,4 @@
-package logging
+package logging_test
 
 import (
 	"testing"
diff --git a/logging/mock_connection_tracer_test.go b/logging/mock_connection_tracer_test.go
deleted file mode 100644
index 94722509..00000000
--- a/logging/mock_connection_tracer_test.go
+++ /dev/null
@@ -1,387 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/quic-go/quic-go/logging (interfaces: ConnectionTracer)
-
-// Package logging is a generated GoMock package.
-package logging
-
-import (
-	net "net"
-	reflect "reflect"
-	time "time"
-
-	protocol "github.com/quic-go/quic-go/internal/protocol"
-	utils "github.com/quic-go/quic-go/internal/utils"
-	wire "github.com/quic-go/quic-go/internal/wire"
-	gomock "go.uber.org/mock/gomock"
-)
-
-// MockConnectionTracer is a mock of ConnectionTracer interface.
-type MockConnectionTracer struct {
-	ctrl     *gomock.Controller
-	recorder *MockConnectionTracerMockRecorder
-}
-
-// MockConnectionTracerMockRecorder is the mock recorder for MockConnectionTracer.
-type MockConnectionTracerMockRecorder struct {
-	mock *MockConnectionTracer
-}
-
-// NewMockConnectionTracer creates a new mock instance.
-func NewMockConnectionTracer(ctrl *gomock.Controller) *MockConnectionTracer {
-	mock := &MockConnectionTracer{ctrl: ctrl}
-	mock.recorder = &MockConnectionTracerMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockConnectionTracer) EXPECT() *MockConnectionTracerMockRecorder {
-	return m.recorder
-}
-
-// AcknowledgedPacket mocks base method.
-func (m *MockConnectionTracer) AcknowledgedPacket(arg0 protocol.EncryptionLevel, arg1 protocol.PacketNumber) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "AcknowledgedPacket", arg0, arg1)
-}
-
-// AcknowledgedPacket indicates an expected call of AcknowledgedPacket.
-func (mr *MockConnectionTracerMockRecorder) AcknowledgedPacket(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcknowledgedPacket", reflect.TypeOf((*MockConnectionTracer)(nil).AcknowledgedPacket), arg0, arg1)
-}
-
-// BufferedPacket mocks base method.
-func (m *MockConnectionTracer) BufferedPacket(arg0 PacketType, arg1 protocol.ByteCount) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "BufferedPacket", arg0, arg1)
-}
-
-// BufferedPacket indicates an expected call of BufferedPacket.
-func (mr *MockConnectionTracerMockRecorder) BufferedPacket(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BufferedPacket", reflect.TypeOf((*MockConnectionTracer)(nil).BufferedPacket), arg0, arg1)
-}
-
-// Close mocks base method.
-func (m *MockConnectionTracer) Close() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Close")
-}
-
-// Close indicates an expected call of Close.
-func (mr *MockConnectionTracerMockRecorder) Close() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConnectionTracer)(nil).Close))
-}
-
-// ClosedConnection mocks base method.
-func (m *MockConnectionTracer) ClosedConnection(arg0 error) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ClosedConnection", arg0)
-}
-
-// ClosedConnection indicates an expected call of ClosedConnection.
-func (mr *MockConnectionTracerMockRecorder) ClosedConnection(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClosedConnection", reflect.TypeOf((*MockConnectionTracer)(nil).ClosedConnection), arg0)
-}
-
-// Debug mocks base method.
-func (m *MockConnectionTracer) Debug(arg0, arg1 string) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Debug", arg0, arg1)
-}
-
-// Debug indicates an expected call of Debug.
-func (mr *MockConnectionTracerMockRecorder) Debug(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockConnectionTracer)(nil).Debug), arg0, arg1)
-}
-
-// DroppedEncryptionLevel mocks base method.
-func (m *MockConnectionTracer) DroppedEncryptionLevel(arg0 protocol.EncryptionLevel) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "DroppedEncryptionLevel", arg0)
-}
-
-// DroppedEncryptionLevel indicates an expected call of DroppedEncryptionLevel.
-func (mr *MockConnectionTracerMockRecorder) DroppedEncryptionLevel(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DroppedEncryptionLevel", reflect.TypeOf((*MockConnectionTracer)(nil).DroppedEncryptionLevel), arg0)
-}
-
-// DroppedKey mocks base method.
-func (m *MockConnectionTracer) DroppedKey(arg0 protocol.KeyPhase) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "DroppedKey", arg0)
-}
-
-// DroppedKey indicates an expected call of DroppedKey.
-func (mr *MockConnectionTracerMockRecorder) DroppedKey(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DroppedKey", reflect.TypeOf((*MockConnectionTracer)(nil).DroppedKey), arg0)
-}
-
-// DroppedPacket mocks base method.
-func (m *MockConnectionTracer) DroppedPacket(arg0 PacketType, arg1 protocol.ByteCount, arg2 PacketDropReason) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "DroppedPacket", arg0, arg1, arg2)
-}
-
-// DroppedPacket indicates an expected call of DroppedPacket.
-func (mr *MockConnectionTracerMockRecorder) DroppedPacket(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DroppedPacket", reflect.TypeOf((*MockConnectionTracer)(nil).DroppedPacket), arg0, arg1, arg2)
-}
-
-// ECNStateUpdated mocks base method.
-func (m *MockConnectionTracer) ECNStateUpdated(arg0 ECNState, arg1 ECNStateTrigger) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ECNStateUpdated", arg0, arg1)
-}
-
-// ECNStateUpdated indicates an expected call of ECNStateUpdated.
-func (mr *MockConnectionTracerMockRecorder) ECNStateUpdated(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ECNStateUpdated", reflect.TypeOf((*MockConnectionTracer)(nil).ECNStateUpdated), arg0, arg1)
-}
-
-// LossTimerCanceled mocks base method.
-func (m *MockConnectionTracer) LossTimerCanceled() {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "LossTimerCanceled")
-}
-
-// LossTimerCanceled indicates an expected call of LossTimerCanceled.
-func (mr *MockConnectionTracerMockRecorder) LossTimerCanceled() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LossTimerCanceled", reflect.TypeOf((*MockConnectionTracer)(nil).LossTimerCanceled))
-}
-
-// LossTimerExpired mocks base method.
-func (m *MockConnectionTracer) LossTimerExpired(arg0 TimerType, arg1 protocol.EncryptionLevel) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "LossTimerExpired", arg0, arg1)
-}
-
-// LossTimerExpired indicates an expected call of LossTimerExpired.
-func (mr *MockConnectionTracerMockRecorder) LossTimerExpired(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LossTimerExpired", reflect.TypeOf((*MockConnectionTracer)(nil).LossTimerExpired), arg0, arg1)
-}
-
-// LostPacket mocks base method.
-func (m *MockConnectionTracer) LostPacket(arg0 protocol.EncryptionLevel, arg1 protocol.PacketNumber, arg2 PacketLossReason) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "LostPacket", arg0, arg1, arg2)
-}
-
-// LostPacket indicates an expected call of LostPacket.
-func (mr *MockConnectionTracerMockRecorder) LostPacket(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LostPacket", reflect.TypeOf((*MockConnectionTracer)(nil).LostPacket), arg0, arg1, arg2)
-}
-
-// NegotiatedVersion mocks base method.
-func (m *MockConnectionTracer) NegotiatedVersion(arg0 protocol.VersionNumber, arg1, arg2 []protocol.VersionNumber) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "NegotiatedVersion", arg0, arg1, arg2)
-}
-
-// NegotiatedVersion indicates an expected call of NegotiatedVersion.
-func (mr *MockConnectionTracerMockRecorder) NegotiatedVersion(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NegotiatedVersion", reflect.TypeOf((*MockConnectionTracer)(nil).NegotiatedVersion), arg0, arg1, arg2)
-}
-
-// ReceivedLongHeaderPacket mocks base method.
-func (m *MockConnectionTracer) ReceivedLongHeaderPacket(arg0 *wire.ExtendedHeader, arg1 protocol.ByteCount, arg2 protocol.ECN, arg3 []Frame) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ReceivedLongHeaderPacket", arg0, arg1, arg2, arg3)
-}
-
-// ReceivedLongHeaderPacket indicates an expected call of ReceivedLongHeaderPacket.
-func (mr *MockConnectionTracerMockRecorder) ReceivedLongHeaderPacket(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedLongHeaderPacket", reflect.TypeOf((*MockConnectionTracer)(nil).ReceivedLongHeaderPacket), arg0, arg1, arg2, arg3)
-}
-
-// ReceivedRetry mocks base method.
-func (m *MockConnectionTracer) ReceivedRetry(arg0 *wire.Header) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ReceivedRetry", arg0)
-}
-
-// ReceivedRetry indicates an expected call of ReceivedRetry.
-func (mr *MockConnectionTracerMockRecorder) ReceivedRetry(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedRetry", reflect.TypeOf((*MockConnectionTracer)(nil).ReceivedRetry), arg0)
-}
-
-// ReceivedShortHeaderPacket mocks base method.
-func (m *MockConnectionTracer) ReceivedShortHeaderPacket(arg0 *ShortHeader, arg1 protocol.ByteCount, arg2 protocol.ECN, arg3 []Frame) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ReceivedShortHeaderPacket", arg0, arg1, arg2, arg3)
-}
-
-// ReceivedShortHeaderPacket indicates an expected call of ReceivedShortHeaderPacket.
-func (mr *MockConnectionTracerMockRecorder) ReceivedShortHeaderPacket(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedShortHeaderPacket", reflect.TypeOf((*MockConnectionTracer)(nil).ReceivedShortHeaderPacket), arg0, arg1, arg2, arg3)
-}
-
-// ReceivedTransportParameters mocks base method.
-func (m *MockConnectionTracer) ReceivedTransportParameters(arg0 *wire.TransportParameters) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ReceivedTransportParameters", arg0)
-}
-
-// ReceivedTransportParameters indicates an expected call of ReceivedTransportParameters.
-func (mr *MockConnectionTracerMockRecorder) ReceivedTransportParameters(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedTransportParameters", reflect.TypeOf((*MockConnectionTracer)(nil).ReceivedTransportParameters), arg0)
-}
-
-// ReceivedVersionNegotiationPacket mocks base method.
-func (m *MockConnectionTracer) ReceivedVersionNegotiationPacket(arg0, arg1 protocol.ArbitraryLenConnectionID, arg2 []protocol.VersionNumber) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "ReceivedVersionNegotiationPacket", arg0, arg1, arg2)
-}
-
-// ReceivedVersionNegotiationPacket indicates an expected call of ReceivedVersionNegotiationPacket.
-func (mr *MockConnectionTracerMockRecorder) ReceivedVersionNegotiationPacket(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedVersionNegotiationPacket", reflect.TypeOf((*MockConnectionTracer)(nil).ReceivedVersionNegotiationPacket), arg0, arg1, arg2)
-}
-
-// RestoredTransportParameters mocks base method.
-func (m *MockConnectionTracer) RestoredTransportParameters(arg0 *wire.TransportParameters) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "RestoredTransportParameters", arg0)
-}
-
-// RestoredTransportParameters indicates an expected call of RestoredTransportParameters.
-func (mr *MockConnectionTracerMockRecorder) RestoredTransportParameters(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoredTransportParameters", reflect.TypeOf((*MockConnectionTracer)(nil).RestoredTransportParameters), arg0)
-}
-
-// SentLongHeaderPacket mocks base method.
-func (m *MockConnectionTracer) SentLongHeaderPacket(arg0 *wire.ExtendedHeader, arg1 protocol.ByteCount, arg2 protocol.ECN, arg3 *wire.AckFrame, arg4 []Frame) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SentLongHeaderPacket", arg0, arg1, arg2, arg3, arg4)
-}
-
-// SentLongHeaderPacket indicates an expected call of SentLongHeaderPacket.
-func (mr *MockConnectionTracerMockRecorder) SentLongHeaderPacket(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SentLongHeaderPacket", reflect.TypeOf((*MockConnectionTracer)(nil).SentLongHeaderPacket), arg0, arg1, arg2, arg3, arg4)
-}
-
-// SentShortHeaderPacket mocks base method.
-func (m *MockConnectionTracer) SentShortHeaderPacket(arg0 *ShortHeader, arg1 protocol.ByteCount, arg2 protocol.ECN, arg3 *wire.AckFrame, arg4 []Frame) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SentShortHeaderPacket", arg0, arg1, arg2, arg3, arg4)
-}
-
-// SentShortHeaderPacket indicates an expected call of SentShortHeaderPacket.
-func (mr *MockConnectionTracerMockRecorder) SentShortHeaderPacket(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SentShortHeaderPacket", reflect.TypeOf((*MockConnectionTracer)(nil).SentShortHeaderPacket), arg0, arg1, arg2, arg3, arg4)
-}
-
-// SentTransportParameters mocks base method.
-func (m *MockConnectionTracer) SentTransportParameters(arg0 *wire.TransportParameters) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SentTransportParameters", arg0)
-}
-
-// SentTransportParameters indicates an expected call of SentTransportParameters.
-func (mr *MockConnectionTracerMockRecorder) SentTransportParameters(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SentTransportParameters", reflect.TypeOf((*MockConnectionTracer)(nil).SentTransportParameters), arg0)
-}
-
-// SetLossTimer mocks base method.
-func (m *MockConnectionTracer) SetLossTimer(arg0 TimerType, arg1 protocol.EncryptionLevel, arg2 time.Time) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SetLossTimer", arg0, arg1, arg2)
-}
-
-// SetLossTimer indicates an expected call of SetLossTimer.
-func (mr *MockConnectionTracerMockRecorder) SetLossTimer(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLossTimer", reflect.TypeOf((*MockConnectionTracer)(nil).SetLossTimer), arg0, arg1, arg2)
-}
-
-// StartedConnection mocks base method.
-func (m *MockConnectionTracer) StartedConnection(arg0, arg1 net.Addr, arg2, arg3 protocol.ConnectionID) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "StartedConnection", arg0, arg1, arg2, arg3)
-}
-
-// StartedConnection indicates an expected call of StartedConnection.
-func (mr *MockConnectionTracerMockRecorder) StartedConnection(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartedConnection", reflect.TypeOf((*MockConnectionTracer)(nil).StartedConnection), arg0, arg1, arg2, arg3)
-}
-
-// UpdatedCongestionState mocks base method.
-func (m *MockConnectionTracer) UpdatedCongestionState(arg0 CongestionState) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "UpdatedCongestionState", arg0)
-}
-
-// UpdatedCongestionState indicates an expected call of UpdatedCongestionState.
-func (mr *MockConnectionTracerMockRecorder) UpdatedCongestionState(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedCongestionState", reflect.TypeOf((*MockConnectionTracer)(nil).UpdatedCongestionState), arg0)
-}
-
-// UpdatedKey mocks base method.
-func (m *MockConnectionTracer) UpdatedKey(arg0 protocol.KeyPhase, arg1 bool) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "UpdatedKey", arg0, arg1)
-}
-
-// UpdatedKey indicates an expected call of UpdatedKey.
-func (mr *MockConnectionTracerMockRecorder) UpdatedKey(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedKey", reflect.TypeOf((*MockConnectionTracer)(nil).UpdatedKey), arg0, arg1)
-}
-
-// UpdatedKeyFromTLS mocks base method.
-func (m *MockConnectionTracer) UpdatedKeyFromTLS(arg0 protocol.EncryptionLevel, arg1 protocol.Perspective) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "UpdatedKeyFromTLS", arg0, arg1)
-}
-
-// UpdatedKeyFromTLS indicates an expected call of UpdatedKeyFromTLS.
-func (mr *MockConnectionTracerMockRecorder) UpdatedKeyFromTLS(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedKeyFromTLS", reflect.TypeOf((*MockConnectionTracer)(nil).UpdatedKeyFromTLS), arg0, arg1)
-}
-
-// UpdatedMetrics mocks base method.
-func (m *MockConnectionTracer) UpdatedMetrics(arg0 *utils.RTTStats, arg1, arg2 protocol.ByteCount, arg3 int) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "UpdatedMetrics", arg0, arg1, arg2, arg3)
-}
-
-// UpdatedMetrics indicates an expected call of UpdatedMetrics.
-func (mr *MockConnectionTracerMockRecorder) UpdatedMetrics(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedMetrics", reflect.TypeOf((*MockConnectionTracer)(nil).UpdatedMetrics), arg0, arg1, arg2, arg3)
-}
-
-// UpdatedPTOCount mocks base method.
-func (m *MockConnectionTracer) UpdatedPTOCount(arg0 uint32) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "UpdatedPTOCount", arg0)
-}
-
-// UpdatedPTOCount indicates an expected call of UpdatedPTOCount.
-func (mr *MockConnectionTracerMockRecorder) UpdatedPTOCount(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedPTOCount", reflect.TypeOf((*MockConnectionTracer)(nil).UpdatedPTOCount), arg0)
-}
diff --git a/logging/mock_tracer_test.go b/logging/mock_tracer_test.go
deleted file mode 100644
index 61eb47ea..00000000
--- a/logging/mock_tracer_test.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/quic-go/quic-go/logging (interfaces: Tracer)
-
-// Package logging is a generated GoMock package.
-package logging
-
-import (
-	net "net"
-	reflect "reflect"
-
-	protocol "github.com/quic-go/quic-go/internal/protocol"
-	wire "github.com/quic-go/quic-go/internal/wire"
-	gomock "go.uber.org/mock/gomock"
-)
-
-// MockTracer is a mock of Tracer interface.
-type MockTracer struct {
-	ctrl     *gomock.Controller
-	recorder *MockTracerMockRecorder
-}
-
-// MockTracerMockRecorder is the mock recorder for MockTracer.
-type MockTracerMockRecorder struct {
-	mock *MockTracer
-}
-
-// NewMockTracer creates a new mock instance.
-func NewMockTracer(ctrl *gomock.Controller) *MockTracer {
-	mock := &MockTracer{ctrl: ctrl}
-	mock.recorder = &MockTracerMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockTracer) EXPECT() *MockTracerMockRecorder {
-	return m.recorder
-}
-
-// DroppedPacket mocks base method.
-func (m *MockTracer) DroppedPacket(arg0 net.Addr, arg1 PacketType, arg2 protocol.ByteCount, arg3 PacketDropReason) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "DroppedPacket", arg0, arg1, arg2, arg3)
-}
-
-// DroppedPacket indicates an expected call of DroppedPacket.
-func (mr *MockTracerMockRecorder) DroppedPacket(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DroppedPacket", reflect.TypeOf((*MockTracer)(nil).DroppedPacket), arg0, arg1, arg2, arg3)
-}
-
-// SentPacket mocks base method.
-func (m *MockTracer) SentPacket(arg0 net.Addr, arg1 *wire.Header, arg2 protocol.ByteCount, arg3 []Frame) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SentPacket", arg0, arg1, arg2, arg3)
-}
-
-// SentPacket indicates an expected call of SentPacket.
-func (mr *MockTracerMockRecorder) SentPacket(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SentPacket", reflect.TypeOf((*MockTracer)(nil).SentPacket), arg0, arg1, arg2, arg3)
-}
-
-// SentVersionNegotiationPacket mocks base method.
-func (m *MockTracer) SentVersionNegotiationPacket(arg0 net.Addr, arg1, arg2 protocol.ArbitraryLenConnectionID, arg3 []protocol.VersionNumber) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "SentVersionNegotiationPacket", arg0, arg1, arg2, arg3)
-}
-
-// SentVersionNegotiationPacket indicates an expected call of SentVersionNegotiationPacket.
-func (mr *MockTracerMockRecorder) SentVersionNegotiationPacket(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SentVersionNegotiationPacket", reflect.TypeOf((*MockTracer)(nil).SentVersionNegotiationPacket), arg0, arg1, arg2, arg3)
-}
diff --git a/logging/multiplex_test.go b/logging/multiplex_test.go
index 572ced6a..c0f784ec 100644
--- a/logging/multiplex_test.go
+++ b/logging/multiplex_test.go
@@ -1,12 +1,14 @@
-package logging
+package logging_test
 
 import (
 	"errors"
 	"net"
 	"time"
 
+	mocklogging "github.com/quic-go/quic-go/internal/mocks/logging"
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
+	. "github.com/quic-go/quic-go/logging"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -19,20 +21,20 @@ var _ = Describe("Tracing", func() {
 		})
 
 		It("returns the raw tracer if only one tracer is passed in", func() {
-			tr := NewMockTracer(mockCtrl)
+			tr := mocklogging.NewMockTracer(mockCtrl)
 			tracer := NewMultiplexedTracer(tr)
-			Expect(tracer).To(BeAssignableToTypeOf(&MockTracer{}))
+			Expect(tracer).To(BeAssignableToTypeOf(&mocklogging.MockTracer{}))
 		})
 
 		Context("tracing events", func() {
 			var (
 				tracer   Tracer
-				tr1, tr2 *MockTracer
+				tr1, tr2 *mocklogging.MockTracer
 			)
 
 			BeforeEach(func() {
-				tr1 = NewMockTracer(mockCtrl)
-				tr2 = NewMockTracer(mockCtrl)
+				tr1 = mocklogging.NewMockTracer(mockCtrl)
+				tr2 = mocklogging.NewMockTracer(mockCtrl)
 				tracer = NewMultiplexedTracer(tr1, tr2)
 			})
 
@@ -67,13 +69,13 @@ var _ = Describe("Tracing", func() {
 	Context("Connection Tracer", func() {
 		var (
 			tracer ConnectionTracer
-			tr1    *MockConnectionTracer
-			tr2    *MockConnectionTracer
+			tr1    *mocklogging.MockConnectionTracer
+			tr2    *mocklogging.MockConnectionTracer
 		)
 
 		BeforeEach(func() {
-			tr1 = NewMockConnectionTracer(mockCtrl)
-			tr2 = NewMockConnectionTracer(mockCtrl)
+			tr1 = mocklogging.NewMockConnectionTracer(mockCtrl)
+			tr2 = mocklogging.NewMockConnectionTracer(mockCtrl)
 			tracer = NewMultiplexedConnectionTracer(tr1, tr2)
 		})
 
diff --git a/logging/packet_header_test.go b/logging/packet_header_test.go
index 6df41925..66810a9c 100644
--- a/logging/packet_header_test.go
+++ b/logging/packet_header_test.go
@@ -1,8 +1,9 @@
-package logging
+package logging_test
 
 import (
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/wire"
+	. "github.com/quic-go/quic-go/logging"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
diff --git a/qlog/types_test.go b/qlog/types_test.go
index d08eb739..d6be3e68 100644
--- a/qlog/types_test.go
+++ b/qlog/types_test.go
@@ -151,6 +151,7 @@ var _ = Describe("Types", func() {
 		Expect(ecnStateTrigger(logging.ECNFailedLostAllTestingPackets).String()).To(Equal("all ECN testing packets declared lost"))
 		Expect(ecnStateTrigger(logging.ECNFailedMoreECNCountsThanSent).String()).To(Equal("ACK contains more ECN counts than ECN-marked packets sent"))
 		Expect(ecnStateTrigger(logging.ECNFailedTooFewECNCounts).String()).To(Equal("ACK contains fewer new ECN counts than acknowledged ECN-marked packets"))
+		Expect(ecnStateTrigger(logging.ECNFailedManglingDetected).String()).To(Equal("ECN mangling detected"))
 		Expect(ecnStateTrigger(42).String()).To(Equal("unknown ECN state trigger"))
 	})
 })