Просмотр исходного кода

Merge pull request #767 from rod-hynes/master

in-proxy enhancements; bug fixes
Rod Hynes 4 недель назад
Родитель
Сommit
265fa9700d

+ 1 - 1
go.mod

@@ -44,6 +44,7 @@ require (
 	github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1
 	github.com/Psiphon-Labs/utls v0.0.0-20260129182755-24497d415a8d
 	github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f
+	github.com/axiomhq/hyperloglog v0.2.6
 	github.com/bifurcation/mint v0.0.0-20180306135233-198357931e61
 	github.com/bits-and-blooms/bloom/v3 v3.6.0
 	github.com/cespare/xxhash v1.1.0
@@ -105,7 +106,6 @@ require (
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 	github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
 	github.com/andybalholm/brotli v1.1.1 // indirect
-	github.com/axiomhq/hyperloglog v0.2.6 // indirect
 	github.com/bits-and-blooms/bitset v1.10.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cloudflare/circl v1.6.1 // indirect

+ 0 - 8
go.sum

@@ -16,12 +16,6 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e h1:NPfqIbzmijrl0VclX2t8eO5EPBhqe47LLGKpRrcVjXk=
 github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e/go.mod h1:ZdY5pBfat/WVzw3eXbIf7N1nZN0XD5H5+X8ZMDWbCs4=
-github.com/Psiphon-Inc/uds-ipc v0.0.0-20251003205000-ec2282b63bb4 h1:o6yYfSWOQ3A8GVd/9Pv8V2FwuVyDJ2gjsHSWnv2Igis=
-github.com/Psiphon-Inc/uds-ipc v0.0.0-20251003205000-ec2282b63bb4/go.mod h1:R8TGG+OXumorJdpAcSFE4SVTT2frMFEGY43Zc/44sNk=
-github.com/Psiphon-Inc/uds-ipc v0.0.0-20251003212312-ed8477de04f5 h1:ZR+pf49zi/729cdcMmaqKxtqpde04+QF3DaVFoE6xyM=
-github.com/Psiphon-Inc/uds-ipc v0.0.0-20251003212312-ed8477de04f5/go.mod h1:R8TGG+OXumorJdpAcSFE4SVTT2frMFEGY43Zc/44sNk=
-github.com/Psiphon-Inc/uds-ipc v0.0.0-20251007150957-166e89b034be h1:TDXrQ1eVlmc/eB3WofOXgYfDKYeiY19+ZCQCkH/6PcU=
-github.com/Psiphon-Inc/uds-ipc v0.0.0-20251007150957-166e89b034be/go.mod h1:R8TGG+OXumorJdpAcSFE4SVTT2frMFEGY43Zc/44sNk=
 github.com/Psiphon-Inc/uds-ipc v1.0.1 h1:K3Z0cS1XfzDdhxWTIwh/hiLrkRR83ZxUo2bqgBOGuZE=
 github.com/Psiphon-Inc/uds-ipc v1.0.1/go.mod h1:R8TGG+OXumorJdpAcSFE4SVTT2frMFEGY43Zc/44sNk=
 github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7 h1:Hx/NCZTnvoKZuIBwSmxE58KKoNLXIGG6hBJYN7pj9Ag=
@@ -34,8 +28,6 @@ github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378 h1:LqI8cx
 github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378/go.mod h1:7ZUnPnWT5z8J8hxfsVjKHYK77Zme/Y0If1b/zeziiJs=
 github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1 h1:zD7JvZCV8gjvtI0AZmE81Ffc/v7A+qwU1/YfUmN/Flk=
 github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1/go.mod h1:rONdWgPMbFjyyBai7gB1IBF4pT9r4l0GyiDst5XR1SY=
-github.com/Psiphon-Labs/utls v0.0.0-20250623193530-396869e9cd87 h1:h/OnQpPMwC7pKN9YQTJ+vQATjchta6kgumJNnkJBq1k=
-github.com/Psiphon-Labs/utls v0.0.0-20250623193530-396869e9cd87/go.mod h1:1vv0gVAzq9e2XYkW8HAKrmtuuZrBdDixQFx5H22KAjI=
 github.com/Psiphon-Labs/utls v0.0.0-20260129182755-24497d415a8d h1:PlKwrArEuQOVqEmThSs9KsXMiBduP8MSu9rlWmQ4jgE=
 github.com/Psiphon-Labs/utls v0.0.0-20260129182755-24497d415a8d/go.mod h1:1vv0gVAzq9e2XYkW8HAKrmtuuZrBdDixQFx5H22KAjI=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=

+ 6 - 0
psiphon/common/inproxy/api.go

@@ -299,12 +299,18 @@ type ClientMetrics struct {
 // overhead, proxies with multiple workers should designate just one worker
 // to set CheckTactics.
 //
+// When PreCheckTactics is set, the broker checks tactics as with
+// CheckTactics, but responds immediately without awaiting a match. This
+// option enables the proxy to quickly establish the shared Noise protocol
+// session and launch all workers.
+//
 // The proxy's session public key is an implicit and cryptographically
 // verified proxy ID.
 type ProxyAnnounceRequest struct {
 	PersonalCompartmentIDs []ID          `cbor:"1,keyasint,omitempty"`
 	Metrics                *ProxyMetrics `cbor:"2,keyasint,omitempty"`
 	CheckTactics           bool          `cbor:"3,keyasint,omitempty"`
+	PreCheckTactics        bool          `cbor:"4,keyasint,omitempty"`
 }
 
 // WebRTCSessionDescription is compatible with pion/webrtc.SessionDescription

+ 12 - 2
psiphon/common/inproxy/broker.go

@@ -729,16 +729,21 @@ func (b *Broker) handleProxyAnnounce(
 	// existing, cached tactics. In the case where tactics have changed,
 	// don't enqueue the proxy announcement and return no-match so that the
 	// proxy can store and apply the new tactics before announcing again.
+	//
+	// For PreCheckTactics requests, an immediate no-match response is
+	// returned even when there are no new tactics.
 
 	var tacticsPayload []byte
-	if announceRequest.CheckTactics {
+	if announceRequest.CheckTactics || announceRequest.PreCheckTactics {
 		tacticsPayload, newTacticsTag, err =
 			b.config.GetTacticsPayload(geoIPData, apiParams)
 		if err != nil {
 			return nil, errors.Trace(err)
 		}
 
-		if tacticsPayload != nil && newTacticsTag != "" {
+		if (tacticsPayload != nil && newTacticsTag != "") ||
+			announceRequest.PreCheckTactics {
+
 			responsePayload, err := MarshalProxyAnnounceResponse(
 				&ProxyAnnounceResponse{
 					TacticsPayload: tacticsPayload,
@@ -756,6 +761,11 @@ func (b *Broker) handleProxyAnnounce(
 	// such as censored locations, from announcing. Proxies with personal
 	// compartment IDs are always allowed, as they will be used only by
 	// clients specifically configured to use them.
+	//
+	// AllowProxy is not enforced until after CheckTactics/PreCheckTactics
+	// cases, which may return an immediate response. This allows proxies to
+	// download new tactics that may set AllowProxy, which well-behaved
+	// proxies can enforce locally as well.
 
 	if !hasPersonalCompartmentIDs &&
 		!b.config.AllowProxy(geoIPData) {

+ 45 - 15
psiphon/common/inproxy/proxy.go

@@ -371,14 +371,13 @@ func (p *Proxy) Run(ctx context.Context) {
 	//   session establisher to be a different worker than the no-delay worker.
 	//
 	// The first worker is the only proxy worker which sets
-	// ProxyAnnounceRequest.CheckTactics.
-	//
-	// Limitation: currently, the first proxy is always common (unless
-	// MaxCommonClients == 0). We might want to change this later
-	// so that the first message is just an announcement, and not a full
-	// proxy, so we don't have to decide its type.
+	// ProxyAnnounceRequest.CheckTactics/PreCheckTactics. PreCheckTactics is
+	// used on the first announcement so the request returns immediately
+	// without awaiting a match. This allows all workers to be launched
+	// quickly.
 
-	commonProxiesToCreate, personalProxiesToCreate := p.config.MaxCommonClients, p.config.MaxPersonalClients
+	commonProxiesToCreate, personalProxiesToCreate :=
+		p.config.MaxCommonClients, p.config.MaxPersonalClients
 
 	// Doing this outside of the go routine to avoid race conditions
 	firstWorkerIsPersonal := p.config.MaxCommonClients <= 0
@@ -450,8 +449,11 @@ func (p *Proxy) activityUpdate(period time.Duration) {
 	greaterThanSwapInt64(&p.peakBytesUp, bytesUp)
 	greaterThanSwapInt64(&p.peakBytesDown, bytesDown)
 
-	personalRegionActivity := p.snapshotAndResetRegionActivity(&p.personalStatsMutex, p.personalRegionActivity)
-	commonRegionActivity := p.snapshotAndResetRegionActivity(&p.commonStatsMutex, p.commonRegionActivity)
+	personalRegionActivity := p.snapshotAndResetRegionActivity(
+		&p.personalStatsMutex, p.personalRegionActivity)
+
+	commonRegionActivity := p.snapshotAndResetRegionActivity(
+		&p.commonStatsMutex, p.commonRegionActivity)
 
 	stateChanged := announcing != p.lastAnnouncing ||
 		connectingClients != p.lastConnectingClients ||
@@ -675,6 +677,8 @@ func (p *Proxy) proxyClients(
 		return false
 	}
 
+	preCheckTacticsDone := false
+
 	for ctx.Err() == nil {
 
 		if !p.config.WaitForNetworkConnectivity() {
@@ -714,7 +718,7 @@ func (p *Proxy) proxyClients(
 		}
 
 		backOff, err := p.proxyOneClient(
-			ctx, logAnnounce, signalAnnounceDone, isPersonal)
+			ctx, logAnnounce, &preCheckTacticsDone, signalAnnounceDone, isPersonal)
 
 		if !backOff || err == nil {
 			failureDelayFactor = 1
@@ -839,6 +843,7 @@ func (p *Proxy) doNetworkDiscovery(
 func (p *Proxy) proxyOneClient(
 	ctx context.Context,
 	logAnnounce func() bool,
+	preCheckTacticsDone *bool,
 	signalAnnounceDone func(),
 	isPersonal bool) (bool, error) {
 
@@ -914,7 +919,8 @@ func (p *Proxy) proxyOneClient(
 
 	// Only the first worker, which has signalAnnounceDone configured, checks
 	// for tactics.
-	checkTactics := signalAnnounceDone != nil
+	checkTactics := signalAnnounceDone != nil && *preCheckTacticsDone
+	preCheckTactics := signalAnnounceDone != nil && !*preCheckTacticsDone
 
 	maxCommonClients, maxPersonalClients, rateLimits := p.getLimits()
 
@@ -929,7 +935,12 @@ func (p *Proxy) proxyOneClient(
 	// with the original network ID.
 
 	metrics, tacticsNetworkID, compressTactics, err := p.getMetrics(
-		checkTactics, brokerCoordinator, webRTCCoordinator, maxCommonClients, maxPersonalClients, rateLimits)
+		checkTactics || preCheckTactics,
+		brokerCoordinator,
+		webRTCCoordinator,
+		maxCommonClients,
+		maxPersonalClients,
+		rateLimits)
 	if err != nil {
 		return backOff, errors.Trace(err)
 	}
@@ -997,6 +1008,7 @@ func (p *Proxy) proxyOneClient(
 			PersonalCompartmentIDs: personalCompartmentIDs,
 			Metrics:                metrics,
 			CheckTactics:           checkTactics,
+			PreCheckTactics:        preCheckTactics,
 		})
 	if logAnnounce() {
 		p.config.Logger.WithTraceFields(common.LogFields{
@@ -1029,12 +1041,16 @@ func (p *Proxy) proxyOneClient(
 		}
 	}
 
-	// Signal that the announce round trip is complete. At this point, the
-	// broker Noise session should be established and any fresh tactics
-	// applied.
+	// Signal that the announce round trip is complete, allowing other workers
+	// to launch. At this point, the broker Noise session should be established
+	// and any fresh tactics applied. Also toggle preCheckTacticsDone since
+	// there's no need to retry PreCheckTactics once a round trip succeeds.
 	if signalAnnounceDone != nil {
 		signalAnnounceDone()
 	}
+	if preCheckTactics {
+		*preCheckTacticsDone = true
+	}
 
 	// MustUpgrade has precedence over other cases, to ensure the callback is
 	// invoked. Trigger back-off back off when rate/entry limited or must
@@ -1055,10 +1071,24 @@ func (p *Proxy) proxyOneClient(
 
 	} else if announceResponse.NoMatch {
 
+		// No backoff for no-match.
+		//
+		// This is also the expected response for CheckTactics with a tactics
+		// payload and PreCheckTactics with or without a tactics payload,
+		// distinct cases which should not back off.
+
 		return backOff, errors.TraceNew("no match")
 
 	}
 
+	if preCheckTactics && !announceResponse.NoMatch {
+
+		// Sanity check: the broker should always respond with no-match for
+		// PreCheckTactics.
+
+		return backOff, errors.TraceNew("unexpected PreCheckTactics response")
+	}
+
 	if announceResponse.SelectedProtocolVersion < ProtocolVersion1 ||
 		(announceResponse.UseMediaStreams &&
 			announceResponse.SelectedProtocolVersion < ProtocolVersion2) ||

+ 2 - 0
psiphon/common/parameters/parameters.go

@@ -409,6 +409,7 @@ const (
 	InproxyAllBrokerSpecs                              = "InproxyAllBrokerSpecs"
 	InproxyBrokerSpecs                                 = "InproxyBrokerSpecs"
 	InproxyPersonalPairingBrokerSpecs                  = "InproxyPersonalPairingBrokerSpecs"
+	InproxyPersonalPairingMaxBrokerSpecCount           = "InproxyPersonalPairingMaxBrokerSpecCount"
 	InproxyProxyBrokerSpecs                            = "InproxyProxyBrokerSpecs"
 	InproxyProxyPersonalPairingBrokerSpecs             = "InproxyProxyPersonalPairingBrokerSpecs"
 	InproxyClientBrokerSpecs                           = "InproxyClientBrokerSpecs"
@@ -1041,6 +1042,7 @@ var defaultParameters = map[string]struct {
 	InproxyAllBrokerSpecs:                              {value: InproxyBrokerSpecsValue{}, flags: serverSideOnly},
 	InproxyBrokerSpecs:                                 {value: InproxyBrokerSpecsValue{}},
 	InproxyPersonalPairingBrokerSpecs:                  {value: InproxyBrokerSpecsValue{}},
+	InproxyPersonalPairingMaxBrokerSpecCount:           {value: 3, minimum: 0},
 	InproxyProxyBrokerSpecs:                            {value: InproxyBrokerSpecsValue{}},
 	InproxyProxyPersonalPairingBrokerSpecs:             {value: InproxyBrokerSpecsValue{}},
 	InproxyClientBrokerSpecs:                           {value: InproxyBrokerSpecsValue{}},

+ 9 - 0
psiphon/config.go

@@ -1114,6 +1114,7 @@ type Config struct {
 	InproxyPersonalPairingBrokerSpecs                       parameters.InproxyBrokerSpecsValue               `json:",omitempty"`
 	InproxyProxyBrokerSpecs                                 parameters.InproxyBrokerSpecsValue               `json:",omitempty"`
 	InproxyProxyPersonalPairingBrokerSpecs                  parameters.InproxyBrokerSpecsValue               `json:",omitempty"`
+	InproxyPersonalPairingMaxBrokerSpecCount                *int                                             `json:",omitempty"`
 	InproxyClientBrokerSpecs                                parameters.InproxyBrokerSpecsValue               `json:",omitempty"`
 	InproxyClientPersonalPairingBrokerSpecs                 parameters.InproxyBrokerSpecsValue               `json:",omitempty"`
 	InproxyReplayBrokerDialParametersTTLSeconds             *int                                             `json:",omitempty"`
@@ -2788,6 +2789,10 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.InproxyProxyPersonalPairingBrokerSpecs] = config.InproxyProxyPersonalPairingBrokerSpecs
 	}
 
+	if config.InproxyPersonalPairingMaxBrokerSpecCount != nil {
+		applyParameters[parameters.InproxyPersonalPairingMaxBrokerSpecCount] = *config.InproxyPersonalPairingMaxBrokerSpecCount
+	}
+
 	if len(config.InproxyClientBrokerSpecs) > 0 {
 		applyParameters[parameters.InproxyClientBrokerSpecs] = config.InproxyClientBrokerSpecs
 	}
@@ -3797,6 +3802,10 @@ func (config *Config) setDialParametersHash() {
 		hash.Write([]byte("InproxyProxyPersonalPairingBrokerSpecs"))
 		hash.Write([]byte(fmt.Sprintf("%+v", config.InproxyProxyPersonalPairingBrokerSpecs)))
 	}
+	if config.InproxyPersonalPairingMaxBrokerSpecCount != nil {
+		hash.Write([]byte("InproxyPersonalPairingMaxBrokerSpecCount"))
+		binary.Write(hash, binary.LittleEndian, int64(*config.InproxyPersonalPairingMaxBrokerSpecCount))
+	}
 	if len(config.InproxyClientBrokerSpecs) > 0 {
 		hash.Write([]byte("InproxyClientBrokerSpecs"))
 		hash.Write([]byte(fmt.Sprintf("%+v", config.InproxyClientBrokerSpecs)))

+ 22 - 0
psiphon/inproxy.go

@@ -434,6 +434,28 @@ func NewInproxyBrokerClientInstance(
 		PRNG := prng.NewPRNGWithSeed(&seed)
 
 		permutedIndexes := PRNG.Perm(len(brokerSpecs))
+
+		// Minimize rendezvous time by reducing the number of brokers this
+		// personal compartment ID maps over to. With a reduced number of
+		// possible brokers, the client and proxy have fewer brokers to check
+		// after fail overs.
+		//
+		// Given that permutedIndexes is a randomized shuffle, each personal
+		// compartment ID will map to a different set of reduced brokers,
+		// preserving overall broker load balancing.
+		//
+		// InproxyPersonalPairingMaxBrokerSpecCount will be configured high
+		// enough to also preserve reasonable availability when brokers fail.
+		// When InproxyPersonalPairingMaxBrokerSpecCount is 0, there is no max.
+		//
+		// This scheme depends on the len(personalCompartmentIDs) <= 1
+		// constraint checked above.
+
+		maxBrokerSpecs := p.Int(parameters.InproxyPersonalPairingMaxBrokerSpecCount)
+		if maxBrokerSpecs > 0 && len(permutedIndexes) > maxBrokerSpecs {
+			permutedIndexes = permutedIndexes[:maxBrokerSpecs]
+		}
+
 		selectedIndex := permutedIndexes[brokerSelectCount%len(permutedIndexes)]
 		brokerSpecs = brokerSpecs[selectedIndex : selectedIndex+1]
 

+ 7 - 2
psiphon/server/destBytes_test.go

@@ -79,7 +79,12 @@ func runTestDestBytes() error {
 	setLogCallback(logCallback)
 	defer setLogCallback(nil)
 
-	const logPeriod = 250 * time.Millisecond
+	// Test can fail if the following addBytes/Sleep loop isn't synchronzied
+	// with the destBytesLogger timer.
+	//
+	// TODO: use time/synctest in Go 1.25+
+
+	const logPeriod = 500 * time.Millisecond
 
 	destBytesLogger := newDestBytesLogger(&SupportServices{
 		Config: &Config{
@@ -236,7 +241,7 @@ func runTestDestBytes() error {
 		return nil
 	}
 
-	for i := 0; i < 3; i++ {
+	for i := 0; i < 2; i++ {
 
 		addBytes()
 

+ 14 - 7
psiphon/server/tunnelServer.go

@@ -2122,13 +2122,17 @@ type destinationBytesMetrics struct {
 }
 
 func (d *destinationBytesMetrics) UpdateProgress(
-	downstreamBytes, upstreamBytes, _ int64) {
+	bytesRead, bytesWritten, _ int64) {
 
 	// Concurrency: UpdateProgress may be called without holding the sshClient
-	// lock; all accesses to bytesUp/bytesDown must use atomic operations.
+	// lock; all accesses to d.bytesUp/bytesDown must use atomic operations.
+
+	// Bytes read from the egress destination become bytes sent down to the
+	// client and bytes written to the egress destination are bytes up from
+	// the client.
 
-	atomic.AddInt64(&d.bytesUp, upstreamBytes)
-	atomic.AddInt64(&d.bytesDown, downstreamBytes)
+	atomic.AddInt64(&d.bytesUp, bytesWritten)
+	atomic.AddInt64(&d.bytesDown, bytesRead)
 }
 
 func (d *destinationBytesMetrics) getBytesUp() int64 {
@@ -2225,7 +2229,7 @@ func newInproxyProxyQualityTracker(
 }
 
 func (t *inproxyProxyQualityTracker) UpdateProgress(
-	downstreamBytes, upstreamBytes, _ int64) {
+	bytesRead, bytesWritten, _ int64) {
 
 	// Concurrency: UpdateProgress may be called concurrently; all accesses to
 	// mutated fields use atomic operations.
@@ -2236,8 +2240,11 @@ func (t *inproxyProxyQualityTracker) UpdateProgress(
 		return
 	}
 
-	bytesUp := t.bytesUp.Add(upstreamBytes)
-	bytesDown := t.bytesDown.Add(downstreamBytes)
+	// Bytes read from the proxied tunnel are upstream and bytes written to
+	// the proxied tunnel are downstream.
+
+	bytesUp := t.bytesUp.Add(bytesRead)
+	bytesDown := t.bytesDown.Add(bytesWritten)
 
 	if (t.targetBytesUp == 0 || bytesUp >= t.targetBytesUp) &&
 		(t.targetBytesDown == 0 || bytesDown >= t.targetBytesDown) &&

+ 3 - 5
replace/dtls/pkg/crypto/ciphersuite/ccm.go

@@ -5,7 +5,6 @@ package ciphersuite
 
 import (
 	"crypto/aes"
-	"crypto/rand"
 	"encoding/binary"
 	"fmt"
 
@@ -65,10 +64,9 @@ func (c *CCM) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error)
 	payload := raw[recordlayer.HeaderSize:]
 	raw = raw[:recordlayer.HeaderSize]
 
-	nonce := append(append([]byte{}, c.localWriteIV[:4]...), make([]byte, 8)...)
-	if _, err := rand.Read(nonce[4:]); err != nil {
-		return nil, err
-	}
+	// [Psiphon]
+	// See comment in generateAEADNonce.
+	nonce := generateAEADNonce(c.localWriteIV, &pkt.Header)
 
 	additionalData := generateAEADAdditionalData(&pkt.Header, len(payload))
 	encryptedPayload := c.localCCM.Seal(nil, nonce, payload, additionalData)

+ 19 - 0
replace/dtls/pkg/crypto/ciphersuite/ciphersuite.go

@@ -33,6 +33,25 @@ func generateAEADAdditionalData(h *recordlayer.Header, payloadLen int) []byte {
 	return additionalData[:]
 }
 
+// [Psiphon]
+// Backport of https://github.com/pion/dtls/commit/61762dee8217991882c5eb79856b9e7a73ee349f. In
+// newer pion/dtls, AEAD nonce generation has moved to common code in aead.encrypt. In this older
+// fork, apply the fix to both CCM.Encrypt and GCM.Encrypt via this helper function.
+//
+// As in the upstream fix, nonce generation is changed from random bytes, which is more vulnerable
+// to reuse, to instead follow the scheme recommended in
+// https://www.rfc-editor.org/rfc/rfc9325#name-nonce-reuse-in-tls-12.
+func generateAEADNonce(writeIV []byte, h *recordlayer.Header) []byte {
+	if gcmNonceLength != 12 || ccmNonceLength != 12 {
+		panic("unexpected nonce length")
+	}
+	nonce := make([]byte, 12)
+	copy(nonce, writeIV[:4])
+	seq64 := (uint64(h.Epoch) << 48) | (h.SequenceNumber & 0x0000ffffffffffff)
+	binary.BigEndian.PutUint64(nonce[4:], seq64)
+	return nonce
+}
+
 // examinePadding returns, in constant time, the length of the padding to remove
 // from the end of payload. It also returns a byte which is equal to 255 if the
 // padding was valid and 0 otherwise. See RFC 2246, Section 6.2.3.2.

+ 3 - 6
replace/dtls/pkg/crypto/ciphersuite/gcm.go

@@ -6,7 +6,6 @@ package ciphersuite
 import (
 	"crypto/aes"
 	"crypto/cipher"
-	"crypto/rand"
 	"encoding/binary"
 	"fmt"
 
@@ -58,11 +57,9 @@ func (g *GCM) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error)
 	payload := raw[recordlayer.HeaderSize:]
 	raw = raw[:recordlayer.HeaderSize]
 
-	nonce := make([]byte, gcmNonceLength)
-	copy(nonce, g.localWriteIV[:4])
-	if _, err := rand.Read(nonce[4:]); err != nil {
-		return nil, err
-	}
+	// [Psiphon]
+	// See comment in generateAEADNonce.
+	nonce := generateAEADNonce(g.localWriteIV, &pkt.Header)
 
 	additionalData := generateAEADAdditionalData(&pkt.Header, len(payload))
 	encryptedPayload := g.localGCM.Seal(nil, nonce, payload, additionalData)

+ 3 - 5
vendor/github.com/pion/dtls/v2/pkg/crypto/ciphersuite/ccm.go

@@ -5,7 +5,6 @@ package ciphersuite
 
 import (
 	"crypto/aes"
-	"crypto/rand"
 	"encoding/binary"
 	"fmt"
 
@@ -65,10 +64,9 @@ func (c *CCM) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error)
 	payload := raw[recordlayer.HeaderSize:]
 	raw = raw[:recordlayer.HeaderSize]
 
-	nonce := append(append([]byte{}, c.localWriteIV[:4]...), make([]byte, 8)...)
-	if _, err := rand.Read(nonce[4:]); err != nil {
-		return nil, err
-	}
+	// [Psiphon]
+	// See comment in generateAEADNonce.
+	nonce := generateAEADNonce(c.localWriteIV, &pkt.Header)
 
 	additionalData := generateAEADAdditionalData(&pkt.Header, len(payload))
 	encryptedPayload := c.localCCM.Seal(nil, nonce, payload, additionalData)

+ 19 - 0
vendor/github.com/pion/dtls/v2/pkg/crypto/ciphersuite/ciphersuite.go

@@ -33,6 +33,25 @@ func generateAEADAdditionalData(h *recordlayer.Header, payloadLen int) []byte {
 	return additionalData[:]
 }
 
+// [Psiphon]
+// Backport of https://github.com/pion/dtls/commit/61762dee8217991882c5eb79856b9e7a73ee349f. In
+// newer pion/dtls, AEAD nonce generation has moved to common code in aead.encrypt. In this older
+// fork, apply the fix to both CCM.Encrypt and GCM.Encrypt via this helper function.
+//
+// As in the upstream fix, nonce generation is changed from random bytes, which is more vulnerable
+// to reuse, to instead follow the scheme recommended in
+// https://www.rfc-editor.org/rfc/rfc9325#name-nonce-reuse-in-tls-12.
+func generateAEADNonce(writeIV []byte, h *recordlayer.Header) []byte {
+	if gcmNonceLength != 12 || ccmNonceLength != 12 {
+		panic("unexpected nonce length")
+	}
+	nonce := make([]byte, 12)
+	copy(nonce, writeIV[:4])
+	seq64 := (uint64(h.Epoch) << 48) | (h.SequenceNumber & 0x0000ffffffffffff)
+	binary.BigEndian.PutUint64(nonce[4:], seq64)
+	return nonce
+}
+
 // examinePadding returns, in constant time, the length of the padding to remove
 // from the end of payload. It also returns a byte which is equal to 255 if the
 // padding was valid and 0 otherwise. See RFC 2246, Section 6.2.3.2.

+ 3 - 6
vendor/github.com/pion/dtls/v2/pkg/crypto/ciphersuite/gcm.go

@@ -6,7 +6,6 @@ package ciphersuite
 import (
 	"crypto/aes"
 	"crypto/cipher"
-	"crypto/rand"
 	"encoding/binary"
 	"fmt"
 
@@ -58,11 +57,9 @@ func (g *GCM) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error)
 	payload := raw[recordlayer.HeaderSize:]
 	raw = raw[:recordlayer.HeaderSize]
 
-	nonce := make([]byte, gcmNonceLength)
-	copy(nonce, g.localWriteIV[:4])
-	if _, err := rand.Read(nonce[4:]); err != nil {
-		return nil, err
-	}
+	// [Psiphon]
+	// See comment in generateAEADNonce.
+	nonce := generateAEADNonce(g.localWriteIV, &pkt.Header)
 
 	additionalData := generateAEADAdditionalData(&pkt.Header, len(payload))
 	encryptedPayload := g.localGCM.Seal(nil, nonce, payload, additionalData)