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

Merge pull request #762 from rod-hynes/destination-bytes

Aggregated destination bytes
Rod Hynes 1 месяц назад
Родитель
Сommit
7d00deb2e0

+ 1 - 1
go.mod

@@ -36,7 +36,7 @@ require (
 	github.com/Jigsaw-Code/outline-sdk v0.0.16
 	github.com/Jigsaw-Code/outline-ss-server v1.8.0
 	github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e
-	github.com/Psiphon-Inc/uds-ipc v0.0.0-20251007150957-166e89b034be
+	github.com/Psiphon-Inc/uds-ipc v1.0.1
 	github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7
 	github.com/Psiphon-Labs/consistent v0.0.0-20240322131436-20aaa4e05737
 	github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464

+ 2 - 0
go.sum

@@ -22,6 +22,8 @@ github.com/Psiphon-Inc/uds-ipc v0.0.0-20251003212312-ed8477de04f5 h1:ZR+pf49zi/7
 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=
 github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7/go.mod h1:alTtZBo3j4AWFvUrAH6F5ZaHcTj4G5Y01nHz8dkU6vU=
 github.com/Psiphon-Labs/consistent v0.0.0-20240322131436-20aaa4e05737 h1:QTMy7Uc2Xc7fz6O/Khy1xi0VBND13GqzLUE2mHw6HUU=

+ 1 - 1
psiphon/common/parameters/parameters.go

@@ -343,7 +343,6 @@ const (
 	RestrictInproxyProviderIDsServerProbability        = "RestrictInproxyProviderIDsServerProbability"
 	RestrictInproxyProviderIDsClientProbability        = "RestrictInproxyProviderIDsClientProbability"
 	UpstreamProxyAllowAllServerEntrySources            = "UpstreamProxyAllowAllServerEntrySources"
-	DestinationBytesMetricsASN                         = "DestinationBytesMetricsASN"
 	DestinationBytesMetricsASNs                        = "DestinationBytesMetricsASNs"
 	DNSResolverAttemptsPerServer                       = "DNSResolverAttemptsPerServer"
 	DNSResolverAttemptsPerPreferredServer              = "DNSResolverAttemptsPerPreferredServer"
@@ -566,6 +565,7 @@ const (
 	InproxyAllBrokerPublicKeys                = "InproxyAllBrokerPublicKeys"
 	InproxyTunnelProtocolSelectionProbability = "InproxyTunnelProtocolSelectionProbability"
 	ReplayIgnoreChangedConfigState            = "ReplayIgnoreChangedConfigState"
+	DestinationBytesMetricsASN                = "DestinationBytesMetricsASN"
 )
 
 const (

+ 10 - 14
psiphon/server/api.go

@@ -46,6 +46,7 @@ const (
 	CLIENT_PLATFORM_ANDROID = "Android"
 	CLIENT_PLATFORM_WINDOWS = "Windows"
 	CLIENT_PLATFORM_IOS     = "iOS"
+	CLIENT_PLATFORM_OTHER   = "Other"
 
 	SPONSOR_ID_LENGTH = 16
 )
@@ -796,20 +797,13 @@ func statusAPIRequestHandler(
 			return nil, errors.Trace(err)
 		}
 		for domain, bytes := range hostBytes {
-
-			domainBytesFields := getRequestLogFields(
-				"domain_bytes",
-				"",
-				sshClient.sessionID,
+			// Limitation: only TCP bytes are reported.
+			support.destBytesLogger.AddDomainBytes(
+				domain,
 				sshClient.getClientGeoIPData(),
-				authorizedAccessTypes,
-				params,
-				statusRequestParams)
-
-			domainBytesFields["domain"] = domain
-			domainBytesFields["bytes"] = bytes
-
-			logQueue = append(logQueue, domainBytesFields)
+				sshClient.handshakeState.apiParams,
+				bytes,
+				0)
 		}
 	}
 
@@ -1869,9 +1863,11 @@ func normalizeClientPlatform(clientPlatform string) string {
 		return CLIENT_PLATFORM_ANDROID
 	} else if strings.HasPrefix(clientPlatform, CLIENT_PLATFORM_IOS) {
 		return CLIENT_PLATFORM_IOS
+	} else if strings.HasPrefix(clientPlatform, CLIENT_PLATFORM_WINDOWS) {
+		return CLIENT_PLATFORM_WINDOWS
 	}
 
-	return CLIENT_PLATFORM_WINDOWS
+	return CLIENT_PLATFORM_OTHER
 }
 
 func isMobileClientPlatform(clientPlatform string) bool {

+ 17 - 0
psiphon/server/config.go

@@ -66,6 +66,7 @@ const (
 	METRIC_WRITER_SHUTDOWN_DELAY                        = 10 * time.Second
 	STOP_ESTABLISH_TUNNELS_ESTABLISHED_CLIENT_THRESHOLD = 20
 	DEFAULT_LOG_FILE_REOPEN_RETRIES                     = 25
+	DEFAULT_DESTINATION_BYTES_PERIOD                    = 5 * time.Minute
 )
 
 // Config specifies the configuration and behavior of a Psiphon
@@ -358,6 +359,11 @@ type Config struct {
 	// The default, 0, disables load logging.
 	LoadMonitorPeriodSeconds int `json:",omitempty"`
 
+	// DestinationBytesPeriodSeconds indicates how frequently to log
+	// aggregated destination bytes metrics. Set to 0 to disable. When not
+	// specified, the default period DEFAULT_DESTINATION_BYTES_PERIOD is used.
+	DestinationBytesPeriodSeconds *int `json:",omitempty"`
+
 	// PeakUpstreamFailureRateMinimumSampleSize specifies the minimum number
 	// of samples (e.g., upstream port forward attempts) that are required
 	// before taking a failure rate snapshot which may be recorded as
@@ -588,6 +594,7 @@ type Config struct {
 	serverEntryTag                                 string
 	runningProtocols                               []string
 	runningOnlyInproxyBroker                       bool
+	destinationBytesPeriod                         time.Duration
 }
 
 // GetLogFileReopenConfig gets the reopen retries, and create/mode inputs for
@@ -628,6 +635,11 @@ func (config *Config) RunPeriodicGarbageCollection() bool {
 	return config.periodicGarbageCollection > 0
 }
 
+// RunDestBytesLogger indicates whether aggregate and log destination bytes.
+func (config *Config) RunDestBytesLogger() bool {
+	return config.destinationBytesPeriod > 0
+}
+
 // DumpProfilesOnStopEstablishTunnels indicates whether dump profiles due to
 // an unexpectedly low number of established clients during high load.
 func (config *Config) DumpProfilesOnStopEstablishTunnels(establishedClientsCount int) bool {
@@ -951,6 +963,11 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 		}
 	}
 
+	config.destinationBytesPeriod = DEFAULT_DESTINATION_BYTES_PERIOD
+	if config.DestinationBytesPeriodSeconds != nil {
+		config.destinationBytesPeriod = time.Duration(*config.DestinationBytesPeriodSeconds) * time.Second
+	}
+
 	return &config, nil
 }
 

+ 329 - 0
psiphon/server/destBytes.go

@@ -0,0 +1,329 @@
+/*
+ * Copyright (c) 2026, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package server
+
+import (
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+)
+
+const (
+	destBytesSoftMaxEntries = 100000
+	destBytesHardMaxEntries = 1000000
+)
+
+// destBytesLogger accumulates ASN and domain destination bytes metrics,
+// aggregates into coarse-grained buckets, and periodically logs destination
+// byte events.
+type destBytesLogger struct {
+	support *SupportServices
+
+	runMutex      sync.Mutex
+	running       bool
+	stopBroadcast chan struct{}
+	waitGroup     *sync.WaitGroup
+
+	asnBytesMutex sync.Mutex
+	asnBytes      map[destBytesBucket]destBytesCounters
+
+	domainBytesMutex sync.Mutex
+	domainBytes      map[destBytesBucket]destBytesCounters
+
+	signalLogASNBytes    chan struct{}
+	signalLogDomainBytes chan struct{}
+	loggedHardMax        atomic.Bool
+}
+
+type destBytesBucket struct {
+	destination    string
+	clientRegion   string
+	clientASN      string
+	sponsorID      string
+	clientPlatform string
+	deviceRegion   string
+}
+
+type destBytesCounters struct {
+	TCP int64
+	UDP int64
+}
+
+// newDestBytesLogger initializes a new destBytesLogger.
+func newDestBytesLogger(support *SupportServices) *destBytesLogger {
+	return &destBytesLogger{
+		support:              support,
+		asnBytes:             make(map[destBytesBucket]destBytesCounters),
+		domainBytes:          make(map[destBytesBucket]destBytesCounters),
+		signalLogASNBytes:    make(chan struct{}, 1),
+		signalLogDomainBytes: make(chan struct{}, 1),
+	}
+}
+
+// Start begins the periodic logging worker.
+func (d *destBytesLogger) Start() error {
+
+	d.runMutex.Lock()
+	defer d.runMutex.Unlock()
+
+	if d.running {
+		return errors.TraceNew("already running")
+	}
+
+	d.running = true
+	d.stopBroadcast = make(chan struct{})
+	d.waitGroup = new(sync.WaitGroup)
+
+	d.waitGroup.Add(1)
+	go func() {
+		defer d.waitGroup.Done()
+		d.run()
+	}()
+
+	return nil
+}
+
+// Stop halts the periodic logging worker. Any remaining aggregated metrics
+// will be logged before Stop returns.
+func (d *destBytesLogger) Stop() {
+
+	d.runMutex.Lock()
+	defer d.runMutex.Unlock()
+
+	if !d.running {
+		return
+	}
+
+	close(d.stopBroadcast)
+	d.waitGroup.Wait()
+
+	d.running = false
+	d.stopBroadcast = nil
+	d.waitGroup = nil
+}
+
+// AddASNBytes adds ASN destination bytes to the aggregation.
+func (d *destBytesLogger) AddASNBytes(
+	destination string,
+	clientGeoIPData GeoIPData,
+	apiParams common.APIParameters,
+	bytesTCP int64,
+	bytesUDP int64) {
+
+	if d == nil {
+		// !RunDestBytesLogger case.
+		return
+	}
+
+	d.addBytes(
+		true,
+		destination,
+		clientGeoIPData,
+		apiParams,
+		bytesTCP,
+		bytesUDP)
+}
+
+// AddDomainBytes adds domain destination bytes to the aggregation.
+func (d *destBytesLogger) AddDomainBytes(
+	destination string,
+	clientGeoIPData GeoIPData,
+	apiParams common.APIParameters,
+	bytesTCP int64,
+	bytesUDP int64) {
+
+	if d == nil {
+		// !RunDestBytesLogger case.
+		return
+	}
+
+	d.addBytes(
+		false,
+		destination,
+		clientGeoIPData,
+		apiParams,
+		bytesTCP,
+		bytesUDP)
+}
+
+func (d *destBytesLogger) run() {
+
+	ticker := time.NewTicker(d.support.Config.destinationBytesPeriod)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			d.logAccumulatedASNDestBytes()
+			d.logAccumulatedDomainDestBytes()
+		case <-d.signalLogASNBytes:
+			d.logAccumulatedASNDestBytes()
+		case <-d.signalLogDomainBytes:
+			d.logAccumulatedDomainDestBytes()
+		case <-d.stopBroadcast:
+			// Log on stop to record metrics accumulated since the last
+			// periodic logging.
+			d.logAccumulatedASNDestBytes()
+			d.logAccumulatedDomainDestBytes()
+			return
+		}
+	}
+}
+
+func (d *destBytesLogger) logAccumulatedASNDestBytes() {
+
+	// Take a snapshot of the aggregation, and then unlock immediately to
+	// avoid blocking addBytes calls while logging.
+	//
+	// Resetting the aggregation here also frees memory associated with rarer
+	// buckets that don't reoccur often.
+
+	d.asnBytesMutex.Lock()
+	asnBytes := d.asnBytes
+	d.asnBytes = make(map[destBytesBucket]destBytesCounters)
+	d.asnBytesMutex.Unlock()
+
+	for bucket, counters := range asnBytes {
+		logFields := make(LogFields)
+		logFields["event_name"] = "asn_dest_bytes"
+		logFields["asn"] = bucket.destination
+		d.addLogFields(logFields, bucket, counters)
+		log.LogRawFieldsWithTimestamp(logFields)
+	}
+}
+
+func (d *destBytesLogger) logAccumulatedDomainDestBytes() {
+
+	// See snapshot comment in logAccumulatedDomainDestBytes.
+
+	d.domainBytesMutex.Lock()
+	domainBytes := d.domainBytes
+	d.domainBytes = make(map[destBytesBucket]destBytesCounters)
+	d.domainBytesMutex.Unlock()
+
+	for bucket, counters := range domainBytes {
+		logFields := make(LogFields)
+		logFields["event_name"] = "domain_dest_bytes"
+		logFields["domain"] = bucket.destination
+		d.addLogFields(logFields, bucket, counters)
+		log.LogRawFieldsWithTimestamp(logFields)
+	}
+}
+
+func (d *destBytesLogger) addLogFields(
+	logFields LogFields,
+	bucket destBytesBucket,
+	counters destBytesCounters) {
+
+	logFields["client_region"] = bucket.clientRegion
+	logFields["client_asn"] = bucket.clientASN
+	logFields["sponsor_id"] = bucket.sponsorID
+	logFields["client_platform"] = bucket.clientPlatform
+	logFields["device_region"] = bucket.deviceRegion
+
+	logFields["bytes_tcp"] = counters.TCP
+	logFields["bytes_udp"] = counters.UDP
+	logFields["bytes"] = counters.TCP + counters.UDP
+}
+
+func (d *destBytesLogger) addBytes(
+	isASN bool,
+	destination string,
+	clientGeoIPData GeoIPData,
+	apiParams common.APIParameters,
+	bytesTCP int64,
+	bytesUDP int64) {
+
+	if bytesTCP == 0 && bytesUDP == 0 {
+		// Some cases, such as client submitted domain bytes, may report all 0
+		// bytes. Skip this data.
+		return
+	}
+
+	sponsorID, _ := getOptionalStringRequestParam(apiParams, "sponsor_id")
+	clientPlatform, _ := getOptionalStringRequestParam(apiParams, "client_platform")
+	deviceRegion, _ := getOptionalStringRequestParam(apiParams, "device_region")
+
+	bucket := destBytesBucket{
+		destination:    destination,
+		clientRegion:   clientGeoIPData.Country,
+		clientASN:      clientGeoIPData.ASN,
+		sponsorID:      sponsorID,
+		clientPlatform: normalizeClientPlatform(clientPlatform),
+		deviceRegion:   deviceRegion,
+	}
+
+	// The map key is a comparable struct of strings. The non-pointer struct
+	// types used for the map keys and values avoids allocations.
+
+	var destBytes map[destBytesBucket]destBytesCounters
+	var logSignal chan struct{}
+
+	if isASN {
+		d.asnBytesMutex.Lock()
+		defer d.asnBytesMutex.Unlock()
+
+		destBytes = d.asnBytes
+		logSignal = d.signalLogASNBytes
+
+	} else {
+		d.domainBytesMutex.Lock()
+		defer d.domainBytesMutex.Unlock()
+
+		destBytes = d.domainBytes
+		logSignal = d.signalLogDomainBytes
+	}
+
+	counters, ok := destBytes[bucket]
+
+	if !ok {
+
+		// A new aggregation map entry will be added. To avoid the map getting
+		// too large, signal an immediate log dump without awaiting the next
+		// period.
+		//
+		// When the soft limit is reached, logging is signaled. If the hard
+		// limit is reached, the new data is dropped.
+
+		count := len(destBytes)
+
+		if count >= destBytesSoftMaxEntries {
+			select {
+			case logSignal <- struct{}{}:
+			default:
+			}
+		}
+
+		if count >= destBytesHardMaxEntries {
+			if d.loggedHardMax.CompareAndSwap(false, true) {
+				log.WithTrace().Warning("destBytesLogger hard max exceeded")
+			}
+			return
+		}
+	}
+
+	counters.TCP += bytesTCP
+	counters.UDP += bytesUDP
+
+	destBytes[bucket] = counters
+}

+ 261 - 0
psiphon/server/destBytes_test.go

@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2026, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package server
+
+import (
+	"encoding/json"
+	"io"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+)
+
+func TestDestBytes(t *testing.T) {
+	err := runTestDestBytes()
+	if err != nil {
+		t.Error(errors.Trace(err).Error())
+	}
+}
+
+func runTestDestBytes() error {
+
+	var logsMutex sync.Mutex
+	var asnDestBytesLogs []map[string]interface{}
+	var domainDestBytesLogs []map[string]interface{}
+
+	// Discard logs, skipping InitLogging. Force disable useProtobufLogging,
+	// which would otherwise require the udsipc.Reader/handler scheme in
+	// server_test. Both asn_dest_bytes and domain_dest_bytes contents are
+	// checked in server_test in PSIPHON_RUN_PROTOBUF_LOGGING_TEST mode.
+
+	logWriter := log.Logger.Out
+	protobufLogging := useProtobufLogging
+	defer func() {
+		log.Logger.Out = logWriter
+		useProtobufLogging = protobufLogging
+	}()
+	log.Logger.Out = io.Discard
+
+	logCallback := func(log []byte) {
+
+		logFields := make(map[string]interface{})
+		err := json.Unmarshal(log, &logFields)
+		if err != nil {
+			panic(err.Error())
+		}
+
+		logsMutex.Lock()
+		defer logsMutex.Unlock()
+
+		switch logFields["event_name"].(string) {
+		case "asn_dest_bytes":
+			asnDestBytesLogs = append(asnDestBytesLogs, logFields)
+		case "domain_dest_bytes":
+			domainDestBytesLogs = append(domainDestBytesLogs, logFields)
+		}
+	}
+
+	setLogCallback(logCallback)
+	defer setLogCallback(nil)
+
+	const logPeriod = 250 * time.Millisecond
+
+	destBytesLogger := newDestBytesLogger(&SupportServices{
+		Config: &Config{
+			destinationBytesPeriod: logPeriod,
+		},
+	})
+
+	err := destBytesLogger.Start()
+	if err != nil {
+		return errors.Trace(err)
+	}
+	defer destBytesLogger.Stop()
+
+	destASNs := []string{"00001", "00002"}
+	destDomains := []string{"example.com", "example.org"}
+	clientRegions := []string{"R1", "R2"}
+	clientASNs := []string{"00003", "00004"}
+	clientPlatformPrefixes := []string{"iOS", "Android"}
+	sponsorIDs := []string{prng.HexString(SPONSOR_ID_LENGTH)}
+	bytesTCP := int64(2048)
+	bytesUDP := int64(1024)
+	eventCount := 10
+
+	addBytes := func() {
+		for i := 0; i < eventCount; i++ {
+			for _, clientRegion := range clientRegions {
+				for _, clientASN := range clientASNs {
+
+					geoIPData := GeoIPData{
+						Country: clientRegion,
+						ASN:     clientASN,
+					}
+
+					for _, clientPlatformPrefix := range clientPlatformPrefixes {
+
+						clientPlatform := clientPlatformPrefix + prng.DefaultPRNG().HexString(4)
+
+						apiParams := common.APIParameters{
+							"client_platform": clientPlatform,
+							"sponsor_id":      sponsorIDs[0],
+						}
+
+						for _, destASN := range destASNs {
+							destBytesLogger.AddASNBytes(destASN, geoIPData, apiParams, bytesTCP, bytesUDP)
+						}
+
+						for _, destDomain := range destDomains {
+							destBytesLogger.AddDomainBytes(destDomain, geoIPData, apiParams, bytesTCP, bytesUDP)
+						}
+
+					}
+				}
+			}
+		}
+	}
+
+	checkLogs := func() error {
+
+		logsMutex.Lock()
+		defer logsMutex.Unlock()
+
+		for i, logs := range [][]map[string]interface{}{asnDestBytesLogs, domainDestBytesLogs} {
+
+			destCount := len(destASNs)
+			if i != 0 {
+				destCount = len(destDomains)
+			}
+			if len(logs) !=
+				destCount*len(clientRegions)*len(clientASNs)*len(clientPlatformPrefixes)*len(sponsorIDs) {
+
+				return errors.Tracef("unexpected log count: %d", len(logs))
+			}
+
+			loggedDestASNs := make(map[string]struct{})
+			loggedDestDomains := make(map[string]struct{})
+			loggedClientRegions := make(map[string]struct{})
+			loggedClientASNs := make(map[string]struct{})
+			loggedClientPlatforms := make(map[string]struct{})
+			loggedSponsorIDs := make(map[string]struct{})
+
+			sumBytesTCP := int64(0)
+			sumBytesUDP := int64(0)
+			sumBytes := int64(0)
+
+			for _, logFields := range logs {
+				if i == 0 {
+					loggedDestASNs[logFields["asn"].(string)] = struct{}{}
+				} else {
+					loggedDestDomains[logFields["domain"].(string)] = struct{}{}
+				}
+				loggedClientRegions[logFields["client_region"].(string)] = struct{}{}
+				loggedClientASNs[logFields["client_asn"].(string)] = struct{}{}
+				loggedClientPlatforms[logFields["client_platform"].(string)] = struct{}{}
+				loggedSponsorIDs[logFields["sponsor_id"].(string)] = struct{}{}
+				sumBytesTCP += int64(logFields["bytes_tcp"].(float64))
+				sumBytesUDP += int64(logFields["bytes_udp"].(float64))
+				sumBytes += int64(logFields["bytes"].(float64))
+			}
+
+			checkFields := func(logged map[string]struct{}, expected []string) error {
+				if len(logged) != len(expected) {
+					return errors.Tracef("unexpected length: %d", len(logged))
+				}
+				for _, key := range expected {
+					if _, ok := logged[key]; !ok {
+						return errors.Tracef("missing %v", key)
+					}
+				}
+				return nil
+			}
+
+			if i == 0 {
+				err := checkFields(loggedDestASNs, destASNs)
+				if err != nil {
+					return errors.Trace(err)
+				}
+			} else {
+				err = checkFields(loggedDestDomains, destDomains)
+				if err != nil {
+					return errors.Trace(err)
+				}
+			}
+			err := checkFields(loggedClientRegions, clientRegions)
+			if err != nil {
+				return errors.Trace(err)
+			}
+			err = checkFields(loggedClientASNs, clientASNs)
+			if err != nil {
+				return errors.Trace(err)
+			}
+			err = checkFields(loggedClientPlatforms, clientPlatformPrefixes)
+			if err != nil {
+				return errors.Trace(err)
+			}
+			err = checkFields(loggedSponsorIDs, sponsorIDs)
+			if err != nil {
+				return errors.Trace(err)
+			}
+
+			if sumBytesTCP != int64(len(logs)*eventCount)*bytesTCP {
+				return errors.Tracef("unexpected TCP bytes: %d", sumBytesTCP)
+			}
+			if sumBytesUDP != int64(len(logs)*eventCount)*bytesUDP {
+				return errors.Tracef("unexpected UDP bytes: %d", sumBytesUDP)
+			}
+			if sumBytes != int64(len(logs)*eventCount)*(bytesTCP+bytesUDP) {
+				return errors.Tracef("unexpected bytes: %d", sumBytes)
+			}
+		}
+
+		asnDestBytesLogs = nil
+		domainDestBytesLogs = nil
+
+		return nil
+	}
+
+	for i := 0; i < 3; i++ {
+
+		addBytes()
+
+		time.Sleep(logPeriod * 2)
+
+		err := checkLogs()
+		if err != nil {
+			return errors.Trace(err)
+		}
+	}
+
+	addBytes()
+
+	destBytesLogger.Stop()
+
+	err = checkLogs()
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	return nil
+}

+ 2 - 0
psiphon/server/log.go

@@ -127,6 +127,7 @@ func (logger *TraceLogger) WithTraceFields(fields LogFields) *logrus.Entry {
 // Note that any existing "trace"/"host_id"/"provider"/"build_rev" field will
 // be renamed to "field.<name>".
 func (logger *TraceLogger) LogRawFieldsWithTimestamp(fields LogFields) {
+
 	if ShouldLogJSON() {
 		renameLogFields(fields)
 		fields["host_id"] = logHostID
@@ -410,6 +411,7 @@ func init() {
 
 	// Set default format
 	logFormat = "json"
+	shouldLogJSON = true
 
 	log = &TraceLogger{
 		&logrus.Logger{

+ 170 - 0
psiphon/server/pb/psiphond/asn_dest_bytes.pb.go

@@ -0,0 +1,170 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.10
+// 	protoc        v6.33.0
+// source: ca.psiphon.psiphond/asn_dest_bytes.proto
+
+package psiphond
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type AsnDestBytes struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	DestParams    *DestParams            `protobuf:"bytes,1,opt,name=dest_params,json=destParams,proto3,oneof" json:"dest_params,omitempty"`
+	Asn           *string                `protobuf:"bytes,100,opt,name=asn,proto3,oneof" json:"asn,omitempty"`
+	BytesTcp      *int64                 `protobuf:"varint,101,opt,name=bytes_tcp,json=bytesTcp,proto3,oneof" json:"bytes_tcp,omitempty"`
+	BytesUdp      *int64                 `protobuf:"varint,102,opt,name=bytes_udp,json=bytesUdp,proto3,oneof" json:"bytes_udp,omitempty"`
+	Bytes         *int64                 `protobuf:"varint,103,opt,name=bytes,proto3,oneof" json:"bytes,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *AsnDestBytes) Reset() {
+	*x = AsnDestBytes{}
+	mi := &file_ca_psiphon_psiphond_asn_dest_bytes_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *AsnDestBytes) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AsnDestBytes) ProtoMessage() {}
+
+func (x *AsnDestBytes) ProtoReflect() protoreflect.Message {
+	mi := &file_ca_psiphon_psiphond_asn_dest_bytes_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AsnDestBytes.ProtoReflect.Descriptor instead.
+func (*AsnDestBytes) Descriptor() ([]byte, []int) {
+	return file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *AsnDestBytes) GetDestParams() *DestParams {
+	if x != nil {
+		return x.DestParams
+	}
+	return nil
+}
+
+func (x *AsnDestBytes) GetAsn() string {
+	if x != nil && x.Asn != nil {
+		return *x.Asn
+	}
+	return ""
+}
+
+func (x *AsnDestBytes) GetBytesTcp() int64 {
+	if x != nil && x.BytesTcp != nil {
+		return *x.BytesTcp
+	}
+	return 0
+}
+
+func (x *AsnDestBytes) GetBytesUdp() int64 {
+	if x != nil && x.BytesUdp != nil {
+		return *x.BytesUdp
+	}
+	return 0
+}
+
+func (x *AsnDestBytes) GetBytes() int64 {
+	if x != nil && x.Bytes != nil {
+		return *x.Bytes
+	}
+	return 0
+}
+
+var File_ca_psiphon_psiphond_asn_dest_bytes_proto protoreflect.FileDescriptor
+
+const file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDesc = "" +
+	"\n" +
+	"(ca.psiphon.psiphond/asn_dest_bytes.proto\x12\x13ca.psiphon.psiphond\x1a%ca.psiphon.psiphond/dest_params.proto\"\x89\x02\n" +
+	"\fAsnDestBytes\x12E\n" +
+	"\vdest_params\x18\x01 \x01(\v2\x1f.ca.psiphon.psiphond.DestParamsH\x00R\n" +
+	"destParams\x88\x01\x01\x12\x15\n" +
+	"\x03asn\x18d \x01(\tH\x01R\x03asn\x88\x01\x01\x12 \n" +
+	"\tbytes_tcp\x18e \x01(\x03H\x02R\bbytesTcp\x88\x01\x01\x12 \n" +
+	"\tbytes_udp\x18f \x01(\x03H\x03R\bbytesUdp\x88\x01\x01\x12\x19\n" +
+	"\x05bytes\x18g \x01(\x03H\x04R\x05bytes\x88\x01\x01B\x0e\n" +
+	"\f_dest_paramsB\x06\n" +
+	"\x04_asnB\f\n" +
+	"\n" +
+	"_bytes_tcpB\f\n" +
+	"\n" +
+	"_bytes_udpB\b\n" +
+	"\x06_bytesBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
+
+var (
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescOnce sync.Once
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescData []byte
+)
+
+func file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescGZIP() []byte {
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescOnce.Do(func() {
+		file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDesc), len(file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDesc)))
+	})
+	return file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDescData
+}
+
+var file_ca_psiphon_psiphond_asn_dest_bytes_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_ca_psiphon_psiphond_asn_dest_bytes_proto_goTypes = []any{
+	(*AsnDestBytes)(nil), // 0: ca.psiphon.psiphond.AsnDestBytes
+	(*DestParams)(nil),   // 1: ca.psiphon.psiphond.DestParams
+}
+var file_ca_psiphon_psiphond_asn_dest_bytes_proto_depIdxs = []int32{
+	1, // 0: ca.psiphon.psiphond.AsnDestBytes.dest_params:type_name -> ca.psiphon.psiphond.DestParams
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_ca_psiphon_psiphond_asn_dest_bytes_proto_init() }
+func file_ca_psiphon_psiphond_asn_dest_bytes_proto_init() {
+	if File_ca_psiphon_psiphond_asn_dest_bytes_proto != nil {
+		return
+	}
+	file_ca_psiphon_psiphond_dest_params_proto_init()
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDesc), len(file_ca_psiphon_psiphond_asn_dest_bytes_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_ca_psiphon_psiphond_asn_dest_bytes_proto_goTypes,
+		DependencyIndexes: file_ca_psiphon_psiphond_asn_dest_bytes_proto_depIdxs,
+		MessageInfos:      file_ca_psiphon_psiphond_asn_dest_bytes_proto_msgTypes,
+	}.Build()
+	File_ca_psiphon_psiphond_asn_dest_bytes_proto = out.File
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_goTypes = nil
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_depIdxs = nil
+}

+ 167 - 0
psiphon/server/pb/psiphond/dest_params.pb.go

@@ -0,0 +1,167 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.10
+// 	protoc        v6.33.0
+// source: ca.psiphon.psiphond/dest_params.proto
+
+package psiphond
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type DestParams struct {
+	state          protoimpl.MessageState `protogen:"open.v1"`
+	ClientAsn      *string                `protobuf:"bytes,1,opt,name=client_asn,json=clientAsn,proto3,oneof" json:"client_asn,omitempty"`
+	ClientPlatform *string                `protobuf:"bytes,2,opt,name=client_platform,json=clientPlatform,proto3,oneof" json:"client_platform,omitempty"`
+	ClientRegion   *string                `protobuf:"bytes,3,opt,name=client_region,json=clientRegion,proto3,oneof" json:"client_region,omitempty"`
+	DeviceRegion   *string                `protobuf:"bytes,4,opt,name=device_region,json=deviceRegion,proto3,oneof" json:"device_region,omitempty"`
+	SponsorId      *string                `protobuf:"bytes,5,opt,name=sponsor_id,json=sponsorId,proto3,oneof" json:"sponsor_id,omitempty"`
+	unknownFields  protoimpl.UnknownFields
+	sizeCache      protoimpl.SizeCache
+}
+
+func (x *DestParams) Reset() {
+	*x = DestParams{}
+	mi := &file_ca_psiphon_psiphond_dest_params_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DestParams) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DestParams) ProtoMessage() {}
+
+func (x *DestParams) ProtoReflect() protoreflect.Message {
+	mi := &file_ca_psiphon_psiphond_dest_params_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DestParams.ProtoReflect.Descriptor instead.
+func (*DestParams) Descriptor() ([]byte, []int) {
+	return file_ca_psiphon_psiphond_dest_params_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *DestParams) GetClientAsn() string {
+	if x != nil && x.ClientAsn != nil {
+		return *x.ClientAsn
+	}
+	return ""
+}
+
+func (x *DestParams) GetClientPlatform() string {
+	if x != nil && x.ClientPlatform != nil {
+		return *x.ClientPlatform
+	}
+	return ""
+}
+
+func (x *DestParams) GetClientRegion() string {
+	if x != nil && x.ClientRegion != nil {
+		return *x.ClientRegion
+	}
+	return ""
+}
+
+func (x *DestParams) GetDeviceRegion() string {
+	if x != nil && x.DeviceRegion != nil {
+		return *x.DeviceRegion
+	}
+	return ""
+}
+
+func (x *DestParams) GetSponsorId() string {
+	if x != nil && x.SponsorId != nil {
+		return *x.SponsorId
+	}
+	return ""
+}
+
+var File_ca_psiphon_psiphond_dest_params_proto protoreflect.FileDescriptor
+
+const file_ca_psiphon_psiphond_dest_params_proto_rawDesc = "" +
+	"\n" +
+	"%ca.psiphon.psiphond/dest_params.proto\x12\x13ca.psiphon.psiphond\"\xac\x02\n" +
+	"\n" +
+	"DestParams\x12\"\n" +
+	"\n" +
+	"client_asn\x18\x01 \x01(\tH\x00R\tclientAsn\x88\x01\x01\x12,\n" +
+	"\x0fclient_platform\x18\x02 \x01(\tH\x01R\x0eclientPlatform\x88\x01\x01\x12(\n" +
+	"\rclient_region\x18\x03 \x01(\tH\x02R\fclientRegion\x88\x01\x01\x12(\n" +
+	"\rdevice_region\x18\x04 \x01(\tH\x03R\fdeviceRegion\x88\x01\x01\x12\"\n" +
+	"\n" +
+	"sponsor_id\x18\x05 \x01(\tH\x04R\tsponsorId\x88\x01\x01B\r\n" +
+	"\v_client_asnB\x12\n" +
+	"\x10_client_platformB\x10\n" +
+	"\x0e_client_regionB\x10\n" +
+	"\x0e_device_regionB\r\n" +
+	"\v_sponsor_idBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
+
+var (
+	file_ca_psiphon_psiphond_dest_params_proto_rawDescOnce sync.Once
+	file_ca_psiphon_psiphond_dest_params_proto_rawDescData []byte
+)
+
+func file_ca_psiphon_psiphond_dest_params_proto_rawDescGZIP() []byte {
+	file_ca_psiphon_psiphond_dest_params_proto_rawDescOnce.Do(func() {
+		file_ca_psiphon_psiphond_dest_params_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_dest_params_proto_rawDesc), len(file_ca_psiphon_psiphond_dest_params_proto_rawDesc)))
+	})
+	return file_ca_psiphon_psiphond_dest_params_proto_rawDescData
+}
+
+var file_ca_psiphon_psiphond_dest_params_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_ca_psiphon_psiphond_dest_params_proto_goTypes = []any{
+	(*DestParams)(nil), // 0: ca.psiphon.psiphond.DestParams
+}
+var file_ca_psiphon_psiphond_dest_params_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_ca_psiphon_psiphond_dest_params_proto_init() }
+func file_ca_psiphon_psiphond_dest_params_proto_init() {
+	if File_ca_psiphon_psiphond_dest_params_proto != nil {
+		return
+	}
+	file_ca_psiphon_psiphond_dest_params_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_dest_params_proto_rawDesc), len(file_ca_psiphon_psiphond_dest_params_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_ca_psiphon_psiphond_dest_params_proto_goTypes,
+		DependencyIndexes: file_ca_psiphon_psiphond_dest_params_proto_depIdxs,
+		MessageInfos:      file_ca_psiphon_psiphond_dest_params_proto_msgTypes,
+	}.Build()
+	File_ca_psiphon_psiphond_dest_params_proto = out.File
+	file_ca_psiphon_psiphond_dest_params_proto_goTypes = nil
+	file_ca_psiphon_psiphond_dest_params_proto_depIdxs = nil
+}

+ 170 - 0
psiphon/server/pb/psiphond/domain_dest_bytes.pb.go

@@ -0,0 +1,170 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.36.10
+// 	protoc        v6.33.0
+// source: ca.psiphon.psiphond/domain_dest_bytes.proto
+
+package psiphond
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+	unsafe "unsafe"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type DomainDestBytes struct {
+	state         protoimpl.MessageState `protogen:"open.v1"`
+	DestParams    *DestParams            `protobuf:"bytes,1,opt,name=dest_params,json=destParams,proto3,oneof" json:"dest_params,omitempty"`
+	Domain        *string                `protobuf:"bytes,100,opt,name=domain,proto3,oneof" json:"domain,omitempty"`
+	BytesTcp      *int64                 `protobuf:"varint,101,opt,name=bytes_tcp,json=bytesTcp,proto3,oneof" json:"bytes_tcp,omitempty"`
+	BytesUdp      *int64                 `protobuf:"varint,102,opt,name=bytes_udp,json=bytesUdp,proto3,oneof" json:"bytes_udp,omitempty"`
+	Bytes         *int64                 `protobuf:"varint,103,opt,name=bytes,proto3,oneof" json:"bytes,omitempty"`
+	unknownFields protoimpl.UnknownFields
+	sizeCache     protoimpl.SizeCache
+}
+
+func (x *DomainDestBytes) Reset() {
+	*x = DomainDestBytes{}
+	mi := &file_ca_psiphon_psiphond_domain_dest_bytes_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *DomainDestBytes) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DomainDestBytes) ProtoMessage() {}
+
+func (x *DomainDestBytes) ProtoReflect() protoreflect.Message {
+	mi := &file_ca_psiphon_psiphond_domain_dest_bytes_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DomainDestBytes.ProtoReflect.Descriptor instead.
+func (*DomainDestBytes) Descriptor() ([]byte, []int) {
+	return file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *DomainDestBytes) GetDestParams() *DestParams {
+	if x != nil {
+		return x.DestParams
+	}
+	return nil
+}
+
+func (x *DomainDestBytes) GetDomain() string {
+	if x != nil && x.Domain != nil {
+		return *x.Domain
+	}
+	return ""
+}
+
+func (x *DomainDestBytes) GetBytesTcp() int64 {
+	if x != nil && x.BytesTcp != nil {
+		return *x.BytesTcp
+	}
+	return 0
+}
+
+func (x *DomainDestBytes) GetBytesUdp() int64 {
+	if x != nil && x.BytesUdp != nil {
+		return *x.BytesUdp
+	}
+	return 0
+}
+
+func (x *DomainDestBytes) GetBytes() int64 {
+	if x != nil && x.Bytes != nil {
+		return *x.Bytes
+	}
+	return 0
+}
+
+var File_ca_psiphon_psiphond_domain_dest_bytes_proto protoreflect.FileDescriptor
+
+const file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDesc = "" +
+	"\n" +
+	"+ca.psiphon.psiphond/domain_dest_bytes.proto\x12\x13ca.psiphon.psiphond\x1a%ca.psiphon.psiphond/dest_params.proto\"\x95\x02\n" +
+	"\x0fDomainDestBytes\x12E\n" +
+	"\vdest_params\x18\x01 \x01(\v2\x1f.ca.psiphon.psiphond.DestParamsH\x00R\n" +
+	"destParams\x88\x01\x01\x12\x1b\n" +
+	"\x06domain\x18d \x01(\tH\x01R\x06domain\x88\x01\x01\x12 \n" +
+	"\tbytes_tcp\x18e \x01(\x03H\x02R\bbytesTcp\x88\x01\x01\x12 \n" +
+	"\tbytes_udp\x18f \x01(\x03H\x03R\bbytesUdp\x88\x01\x01\x12\x19\n" +
+	"\x05bytes\x18g \x01(\x03H\x04R\x05bytes\x88\x01\x01B\x0e\n" +
+	"\f_dest_paramsB\t\n" +
+	"\a_domainB\f\n" +
+	"\n" +
+	"_bytes_tcpB\f\n" +
+	"\n" +
+	"_bytes_udpB\b\n" +
+	"\x06_bytesBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
+
+var (
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescOnce sync.Once
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescData []byte
+)
+
+func file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescGZIP() []byte {
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescOnce.Do(func() {
+		file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDesc), len(file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDesc)))
+	})
+	return file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDescData
+}
+
+var file_ca_psiphon_psiphond_domain_dest_bytes_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_ca_psiphon_psiphond_domain_dest_bytes_proto_goTypes = []any{
+	(*DomainDestBytes)(nil), // 0: ca.psiphon.psiphond.DomainDestBytes
+	(*DestParams)(nil),      // 1: ca.psiphon.psiphond.DestParams
+}
+var file_ca_psiphon_psiphond_domain_dest_bytes_proto_depIdxs = []int32{
+	1, // 0: ca.psiphon.psiphond.DomainDestBytes.dest_params:type_name -> ca.psiphon.psiphond.DestParams
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_ca_psiphon_psiphond_domain_dest_bytes_proto_init() }
+func file_ca_psiphon_psiphond_domain_dest_bytes_proto_init() {
+	if File_ca_psiphon_psiphond_domain_dest_bytes_proto != nil {
+		return
+	}
+	file_ca_psiphon_psiphond_dest_params_proto_init()
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_msgTypes[0].OneofWrappers = []any{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDesc), len(file_ca_psiphon_psiphond_domain_dest_bytes_proto_rawDesc)),
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_ca_psiphon_psiphond_domain_dest_bytes_proto_goTypes,
+		DependencyIndexes: file_ca_psiphon_psiphond_domain_dest_bytes_proto_depIdxs,
+		MessageInfos:      file_ca_psiphon_psiphond_domain_dest_bytes_proto_msgTypes,
+	}.Build()
+	File_ca_psiphon_psiphond_domain_dest_bytes_proto = out.File
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_goTypes = nil
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_depIdxs = nil
+}

+ 77 - 76
psiphon/server/pb/psiphond/psiphond.pb.go

@@ -31,7 +31,6 @@ type Psiphond struct {
 	Provider     string                 `protobuf:"bytes,5,opt,name=provider,proto3" json:"provider,omitempty"`
 	// Types that are valid to be assigned to Metric:
 	//
-	//	*Psiphond_DomainBytes
 	//	*Psiphond_FailedTunnel
 	//	*Psiphond_InproxyBroker
 	//	*Psiphond_IrregularTunnel
@@ -44,10 +43,11 @@ type Psiphond struct {
 	//	*Psiphond_ServerPanic
 	//	*Psiphond_ServerPacket
 	//	*Psiphond_ServerTunnel
-	//	*Psiphond_ServerTunnelAsnDestBytes
 	//	*Psiphond_Tactics
 	//	*Psiphond_TacticsSpeedtest
 	//	*Psiphond_UniqueUser
+	//	*Psiphond_AsnDestBytes
+	//	*Psiphond_DomainDestBytes
 	//	*Psiphond_DslRelayGetServerEntries
 	Metric        isPsiphond_Metric `protobuf_oneof:"metric"`
 	unknownFields protoimpl.UnknownFields
@@ -126,15 +126,6 @@ func (x *Psiphond) GetMetric() isPsiphond_Metric {
 	return nil
 }
 
-func (x *Psiphond) GetDomainBytes() *DomainBytes {
-	if x != nil {
-		if x, ok := x.Metric.(*Psiphond_DomainBytes); ok {
-			return x.DomainBytes
-		}
-	}
-	return nil
-}
-
 func (x *Psiphond) GetFailedTunnel() *FailedTunnel {
 	if x != nil {
 		if x, ok := x.Metric.(*Psiphond_FailedTunnel); ok {
@@ -243,15 +234,6 @@ func (x *Psiphond) GetServerTunnel() *ServerTunnel {
 	return nil
 }
 
-func (x *Psiphond) GetServerTunnelAsnDestBytes() *ServerTunnelASNDestBytes {
-	if x != nil {
-		if x, ok := x.Metric.(*Psiphond_ServerTunnelAsnDestBytes); ok {
-			return x.ServerTunnelAsnDestBytes
-		}
-	}
-	return nil
-}
-
 func (x *Psiphond) GetTactics() *Tactics {
 	if x != nil {
 		if x, ok := x.Metric.(*Psiphond_Tactics); ok {
@@ -279,6 +261,24 @@ func (x *Psiphond) GetUniqueUser() *UniqueUser {
 	return nil
 }
 
+func (x *Psiphond) GetAsnDestBytes() *AsnDestBytes {
+	if x != nil {
+		if x, ok := x.Metric.(*Psiphond_AsnDestBytes); ok {
+			return x.AsnDestBytes
+		}
+	}
+	return nil
+}
+
+func (x *Psiphond) GetDomainDestBytes() *DomainDestBytes {
+	if x != nil {
+		if x, ok := x.Metric.(*Psiphond_DomainDestBytes); ok {
+			return x.DomainDestBytes
+		}
+	}
+	return nil
+}
+
 func (x *Psiphond) GetDslRelayGetServerEntries() *DslRelayGetServerEntries {
 	if x != nil {
 		if x, ok := x.Metric.(*Psiphond_DslRelayGetServerEntries); ok {
@@ -292,10 +292,6 @@ type isPsiphond_Metric interface {
 	isPsiphond_Metric()
 }
 
-type Psiphond_DomainBytes struct {
-	DomainBytes *DomainBytes `protobuf:"bytes,101,opt,name=domain_bytes,json=domainBytes,proto3,oneof"`
-}
-
 type Psiphond_FailedTunnel struct {
 	FailedTunnel *FailedTunnel `protobuf:"bytes,102,opt,name=failed_tunnel,json=failedTunnel,proto3,oneof"`
 }
@@ -344,10 +340,6 @@ type Psiphond_ServerTunnel struct {
 	ServerTunnel *ServerTunnel `protobuf:"bytes,113,opt,name=server_tunnel,json=serverTunnel,proto3,oneof"`
 }
 
-type Psiphond_ServerTunnelAsnDestBytes struct {
-	ServerTunnelAsnDestBytes *ServerTunnelASNDestBytes `protobuf:"bytes,114,opt,name=server_tunnel_asn_dest_bytes,json=serverTunnelAsnDestBytes,proto3,oneof"`
-}
-
 type Psiphond_Tactics struct {
 	Tactics *Tactics `protobuf:"bytes,115,opt,name=tactics,proto3,oneof"`
 }
@@ -360,12 +352,18 @@ type Psiphond_UniqueUser struct {
 	UniqueUser *UniqueUser `protobuf:"bytes,117,opt,name=unique_user,json=uniqueUser,proto3,oneof"`
 }
 
+type Psiphond_AsnDestBytes struct {
+	AsnDestBytes *AsnDestBytes `protobuf:"bytes,118,opt,name=asn_dest_bytes,json=asnDestBytes,proto3,oneof"`
+}
+
+type Psiphond_DomainDestBytes struct {
+	DomainDestBytes *DomainDestBytes `protobuf:"bytes,119,opt,name=domain_dest_bytes,json=domainDestBytes,proto3,oneof"`
+}
+
 type Psiphond_DslRelayGetServerEntries struct {
 	DslRelayGetServerEntries *DslRelayGetServerEntries `protobuf:"bytes,120,opt,name=dsl_relay_get_server_entries,json=dslRelayGetServerEntries,proto3,oneof"`
 }
 
-func (*Psiphond_DomainBytes) isPsiphond_Metric() {}
-
 func (*Psiphond_FailedTunnel) isPsiphond_Metric() {}
 
 func (*Psiphond_InproxyBroker) isPsiphond_Metric() {}
@@ -390,28 +388,29 @@ func (*Psiphond_ServerPacket) isPsiphond_Metric() {}
 
 func (*Psiphond_ServerTunnel) isPsiphond_Metric() {}
 
-func (*Psiphond_ServerTunnelAsnDestBytes) isPsiphond_Metric() {}
-
 func (*Psiphond_Tactics) isPsiphond_Metric() {}
 
 func (*Psiphond_TacticsSpeedtest) isPsiphond_Metric() {}
 
 func (*Psiphond_UniqueUser) isPsiphond_Metric() {}
 
+func (*Psiphond_AsnDestBytes) isPsiphond_Metric() {}
+
+func (*Psiphond_DomainDestBytes) isPsiphond_Metric() {}
+
 func (*Psiphond_DslRelayGetServerEntries) isPsiphond_Metric() {}
 
 var File_ca_psiphon_psiphond_psiphond_proto protoreflect.FileDescriptor
 
 const file_ca_psiphon_psiphond_psiphond_proto_rawDesc = "" +
 	"\n" +
-	"\"ca.psiphon.psiphond/psiphond.proto\x12\x13ca.psiphon.psiphond\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&ca.psiphon.psiphond/domain_bytes.proto\x1a'ca.psiphon.psiphond/failed_tunnel.proto\x1a(ca.psiphon.psiphond/inproxy_broker.proto\x1a*ca.psiphon.psiphond/irregular_tunnel.proto\x1a'ca.psiphon.psiphond/orphan_packet.proto\x1a,ca.psiphon.psiphond/remote_server_list.proto\x1a*ca.psiphon.psiphond/server_blocklist.proto\x1a%ca.psiphon.psiphond/server_load.proto\x1a&ca.psiphon.psiphond/server_panic.proto\x1a'ca.psiphon.psiphond/server_packet.proto\x1a'ca.psiphon.psiphond/server_tunnel.proto\x1a!ca.psiphon.psiphond/tactics.proto\x1a%ca.psiphon.psiphond/unique_user.proto\x1a#ca.psiphon.psiphond/dsl_relay.proto\"\xee\f\n" +
+	"\"ca.psiphon.psiphond/psiphond.proto\x12\x13ca.psiphon.psiphond\x1a\x1fgoogle/protobuf/timestamp.proto\x1a(ca.psiphon.psiphond/asn_dest_bytes.proto\x1a+ca.psiphon.psiphond/domain_dest_bytes.proto\x1a'ca.psiphon.psiphond/failed_tunnel.proto\x1a(ca.psiphon.psiphond/inproxy_broker.proto\x1a*ca.psiphon.psiphond/irregular_tunnel.proto\x1a'ca.psiphon.psiphond/orphan_packet.proto\x1a,ca.psiphon.psiphond/remote_server_list.proto\x1a*ca.psiphon.psiphond/server_blocklist.proto\x1a%ca.psiphon.psiphond/server_load.proto\x1a&ca.psiphon.psiphond/server_panic.proto\x1a'ca.psiphon.psiphond/server_packet.proto\x1a'ca.psiphon.psiphond/server_tunnel.proto\x1a!ca.psiphon.psiphond/tactics.proto\x1a%ca.psiphon.psiphond/unique_user.proto\x1a#ca.psiphon.psiphond/dsl_relay.proto\"\xe1\f\n" +
 	"\bPsiphond\x128\n" +
 	"\ttimestamp\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x17\n" +
 	"\ahost_id\x18\x02 \x01(\tR\x06hostId\x12\x1b\n" +
 	"\thost_type\x18\x03 \x01(\tR\bhostType\x12$\n" +
 	"\x0ehost_build_rev\x18\x04 \x01(\tR\fhostBuildRev\x12\x1a\n" +
-	"\bprovider\x18\x05 \x01(\tR\bprovider\x12E\n" +
-	"\fdomain_bytes\x18e \x01(\v2 .ca.psiphon.psiphond.DomainBytesH\x00R\vdomainBytes\x12H\n" +
+	"\bprovider\x18\x05 \x01(\tR\bprovider\x12H\n" +
 	"\rfailed_tunnel\x18f \x01(\v2!.ca.psiphon.psiphond.FailedTunnelH\x00R\ffailedTunnel\x12K\n" +
 	"\x0einproxy_broker\x18g \x01(\v2\".ca.psiphon.psiphond.InproxyBrokerH\x00R\rinproxyBroker\x12Q\n" +
 	"\x10irregular_tunnel\x18h \x01(\v2$.ca.psiphon.psiphond.IrregularTunnelH\x00R\x0firregularTunnel\x12H\n" +
@@ -424,14 +423,15 @@ const file_ca_psiphon_psiphond_psiphond_proto_rawDesc = "" +
 	"\x14server_load_protocol\x18n \x01(\v2'.ca.psiphon.psiphond.ServerLoadProtocolH\x00R\x12serverLoadProtocol\x12E\n" +
 	"\fserver_panic\x18o \x01(\v2 .ca.psiphon.psiphond.ServerPanicH\x00R\vserverPanic\x12H\n" +
 	"\rserver_packet\x18p \x01(\v2!.ca.psiphon.psiphond.ServerPacketH\x00R\fserverPacket\x12H\n" +
-	"\rserver_tunnel\x18q \x01(\v2!.ca.psiphon.psiphond.ServerTunnelH\x00R\fserverTunnel\x12o\n" +
-	"\x1cserver_tunnel_asn_dest_bytes\x18r \x01(\v2-.ca.psiphon.psiphond.ServerTunnelASNDestBytesH\x00R\x18serverTunnelAsnDestBytes\x128\n" +
+	"\rserver_tunnel\x18q \x01(\v2!.ca.psiphon.psiphond.ServerTunnelH\x00R\fserverTunnel\x128\n" +
 	"\atactics\x18s \x01(\v2\x1c.ca.psiphon.psiphond.TacticsH\x00R\atactics\x12T\n" +
 	"\x11tactics_speedtest\x18t \x01(\v2%.ca.psiphon.psiphond.TacticsSpeedTestH\x00R\x10tacticsSpeedtest\x12B\n" +
 	"\vunique_user\x18u \x01(\v2\x1f.ca.psiphon.psiphond.UniqueUserH\x00R\n" +
-	"uniqueUser\x12o\n" +
+	"uniqueUser\x12I\n" +
+	"\x0easn_dest_bytes\x18v \x01(\v2!.ca.psiphon.psiphond.AsnDestBytesH\x00R\fasnDestBytes\x12R\n" +
+	"\x11domain_dest_bytes\x18w \x01(\v2$.ca.psiphon.psiphond.DomainDestBytesH\x00R\x0fdomainDestBytes\x12o\n" +
 	"\x1cdsl_relay_get_server_entries\x18x \x01(\v2-.ca.psiphon.psiphond.DslRelayGetServerEntriesH\x00R\x18dslRelayGetServerEntriesB\b\n" +
-	"\x06metricBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
+	"\x06metricJ\x04\be\x10fJ\x04\br\x10sBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
 
 var (
 	file_ca_psiphon_psiphond_psiphond_proto_rawDescOnce sync.Once
@@ -449,44 +449,44 @@ var file_ca_psiphon_psiphond_psiphond_proto_msgTypes = make([]protoimpl.MessageI
 var file_ca_psiphon_psiphond_psiphond_proto_goTypes = []any{
 	(*Psiphond)(nil),                 // 0: ca.psiphon.psiphond.Psiphond
 	(*timestamppb.Timestamp)(nil),    // 1: google.protobuf.Timestamp
-	(*DomainBytes)(nil),              // 2: ca.psiphon.psiphond.DomainBytes
-	(*FailedTunnel)(nil),             // 3: ca.psiphon.psiphond.FailedTunnel
-	(*InproxyBroker)(nil),            // 4: ca.psiphon.psiphond.InproxyBroker
-	(*IrregularTunnel)(nil),          // 5: ca.psiphon.psiphond.IrregularTunnel
-	(*OrphanPacket)(nil),             // 6: ca.psiphon.psiphond.OrphanPacket
-	(*RemoteServerList)(nil),         // 7: ca.psiphon.psiphond.RemoteServerList
-	(*ServerBlocklistHit)(nil),       // 8: ca.psiphon.psiphond.ServerBlocklistHit
-	(*ServerLoad)(nil),               // 9: ca.psiphon.psiphond.ServerLoad
-	(*ServerLoadDNS)(nil),            // 10: ca.psiphon.psiphond.ServerLoadDNS
-	(*ServerLoadProtocol)(nil),       // 11: ca.psiphon.psiphond.ServerLoadProtocol
-	(*ServerPanic)(nil),              // 12: ca.psiphon.psiphond.ServerPanic
-	(*ServerPacket)(nil),             // 13: ca.psiphon.psiphond.ServerPacket
-	(*ServerTunnel)(nil),             // 14: ca.psiphon.psiphond.ServerTunnel
-	(*ServerTunnelASNDestBytes)(nil), // 15: ca.psiphon.psiphond.ServerTunnelASNDestBytes
-	(*Tactics)(nil),                  // 16: ca.psiphon.psiphond.Tactics
-	(*TacticsSpeedTest)(nil),         // 17: ca.psiphon.psiphond.TacticsSpeedTest
-	(*UniqueUser)(nil),               // 18: ca.psiphon.psiphond.UniqueUser
+	(*FailedTunnel)(nil),             // 2: ca.psiphon.psiphond.FailedTunnel
+	(*InproxyBroker)(nil),            // 3: ca.psiphon.psiphond.InproxyBroker
+	(*IrregularTunnel)(nil),          // 4: ca.psiphon.psiphond.IrregularTunnel
+	(*OrphanPacket)(nil),             // 5: ca.psiphon.psiphond.OrphanPacket
+	(*RemoteServerList)(nil),         // 6: ca.psiphon.psiphond.RemoteServerList
+	(*ServerBlocklistHit)(nil),       // 7: ca.psiphon.psiphond.ServerBlocklistHit
+	(*ServerLoad)(nil),               // 8: ca.psiphon.psiphond.ServerLoad
+	(*ServerLoadDNS)(nil),            // 9: ca.psiphon.psiphond.ServerLoadDNS
+	(*ServerLoadProtocol)(nil),       // 10: ca.psiphon.psiphond.ServerLoadProtocol
+	(*ServerPanic)(nil),              // 11: ca.psiphon.psiphond.ServerPanic
+	(*ServerPacket)(nil),             // 12: ca.psiphon.psiphond.ServerPacket
+	(*ServerTunnel)(nil),             // 13: ca.psiphon.psiphond.ServerTunnel
+	(*Tactics)(nil),                  // 14: ca.psiphon.psiphond.Tactics
+	(*TacticsSpeedTest)(nil),         // 15: ca.psiphon.psiphond.TacticsSpeedTest
+	(*UniqueUser)(nil),               // 16: ca.psiphon.psiphond.UniqueUser
+	(*AsnDestBytes)(nil),             // 17: ca.psiphon.psiphond.AsnDestBytes
+	(*DomainDestBytes)(nil),          // 18: ca.psiphon.psiphond.DomainDestBytes
 	(*DslRelayGetServerEntries)(nil), // 19: ca.psiphon.psiphond.DslRelayGetServerEntries
 }
 var file_ca_psiphon_psiphond_psiphond_proto_depIdxs = []int32{
 	1,  // 0: ca.psiphon.psiphond.Psiphond.timestamp:type_name -> google.protobuf.Timestamp
-	2,  // 1: ca.psiphon.psiphond.Psiphond.domain_bytes:type_name -> ca.psiphon.psiphond.DomainBytes
-	3,  // 2: ca.psiphon.psiphond.Psiphond.failed_tunnel:type_name -> ca.psiphon.psiphond.FailedTunnel
-	4,  // 3: ca.psiphon.psiphond.Psiphond.inproxy_broker:type_name -> ca.psiphon.psiphond.InproxyBroker
-	5,  // 4: ca.psiphon.psiphond.Psiphond.irregular_tunnel:type_name -> ca.psiphon.psiphond.IrregularTunnel
-	6,  // 5: ca.psiphon.psiphond.Psiphond.orphan_packet:type_name -> ca.psiphon.psiphond.OrphanPacket
-	7,  // 6: ca.psiphon.psiphond.Psiphond.remote_server_list:type_name -> ca.psiphon.psiphond.RemoteServerList
-	8,  // 7: ca.psiphon.psiphond.Psiphond.server_blocklist:type_name -> ca.psiphon.psiphond.ServerBlocklistHit
-	9,  // 8: ca.psiphon.psiphond.Psiphond.server_load:type_name -> ca.psiphon.psiphond.ServerLoad
-	10, // 9: ca.psiphon.psiphond.Psiphond.server_load_dns:type_name -> ca.psiphon.psiphond.ServerLoadDNS
-	11, // 10: ca.psiphon.psiphond.Psiphond.server_load_protocol:type_name -> ca.psiphon.psiphond.ServerLoadProtocol
-	12, // 11: ca.psiphon.psiphond.Psiphond.server_panic:type_name -> ca.psiphon.psiphond.ServerPanic
-	13, // 12: ca.psiphon.psiphond.Psiphond.server_packet:type_name -> ca.psiphon.psiphond.ServerPacket
-	14, // 13: ca.psiphon.psiphond.Psiphond.server_tunnel:type_name -> ca.psiphon.psiphond.ServerTunnel
-	15, // 14: ca.psiphon.psiphond.Psiphond.server_tunnel_asn_dest_bytes:type_name -> ca.psiphon.psiphond.ServerTunnelASNDestBytes
-	16, // 15: ca.psiphon.psiphond.Psiphond.tactics:type_name -> ca.psiphon.psiphond.Tactics
-	17, // 16: ca.psiphon.psiphond.Psiphond.tactics_speedtest:type_name -> ca.psiphon.psiphond.TacticsSpeedTest
-	18, // 17: ca.psiphon.psiphond.Psiphond.unique_user:type_name -> ca.psiphon.psiphond.UniqueUser
+	2,  // 1: ca.psiphon.psiphond.Psiphond.failed_tunnel:type_name -> ca.psiphon.psiphond.FailedTunnel
+	3,  // 2: ca.psiphon.psiphond.Psiphond.inproxy_broker:type_name -> ca.psiphon.psiphond.InproxyBroker
+	4,  // 3: ca.psiphon.psiphond.Psiphond.irregular_tunnel:type_name -> ca.psiphon.psiphond.IrregularTunnel
+	5,  // 4: ca.psiphon.psiphond.Psiphond.orphan_packet:type_name -> ca.psiphon.psiphond.OrphanPacket
+	6,  // 5: ca.psiphon.psiphond.Psiphond.remote_server_list:type_name -> ca.psiphon.psiphond.RemoteServerList
+	7,  // 6: ca.psiphon.psiphond.Psiphond.server_blocklist:type_name -> ca.psiphon.psiphond.ServerBlocklistHit
+	8,  // 7: ca.psiphon.psiphond.Psiphond.server_load:type_name -> ca.psiphon.psiphond.ServerLoad
+	9,  // 8: ca.psiphon.psiphond.Psiphond.server_load_dns:type_name -> ca.psiphon.psiphond.ServerLoadDNS
+	10, // 9: ca.psiphon.psiphond.Psiphond.server_load_protocol:type_name -> ca.psiphon.psiphond.ServerLoadProtocol
+	11, // 10: ca.psiphon.psiphond.Psiphond.server_panic:type_name -> ca.psiphon.psiphond.ServerPanic
+	12, // 11: ca.psiphon.psiphond.Psiphond.server_packet:type_name -> ca.psiphon.psiphond.ServerPacket
+	13, // 12: ca.psiphon.psiphond.Psiphond.server_tunnel:type_name -> ca.psiphon.psiphond.ServerTunnel
+	14, // 13: ca.psiphon.psiphond.Psiphond.tactics:type_name -> ca.psiphon.psiphond.Tactics
+	15, // 14: ca.psiphon.psiphond.Psiphond.tactics_speedtest:type_name -> ca.psiphon.psiphond.TacticsSpeedTest
+	16, // 15: ca.psiphon.psiphond.Psiphond.unique_user:type_name -> ca.psiphon.psiphond.UniqueUser
+	17, // 16: ca.psiphon.psiphond.Psiphond.asn_dest_bytes:type_name -> ca.psiphon.psiphond.AsnDestBytes
+	18, // 17: ca.psiphon.psiphond.Psiphond.domain_dest_bytes:type_name -> ca.psiphon.psiphond.DomainDestBytes
 	19, // 18: ca.psiphon.psiphond.Psiphond.dsl_relay_get_server_entries:type_name -> ca.psiphon.psiphond.DslRelayGetServerEntries
 	19, // [19:19] is the sub-list for method output_type
 	19, // [19:19] is the sub-list for method input_type
@@ -500,7 +500,8 @@ func file_ca_psiphon_psiphond_psiphond_proto_init() {
 	if File_ca_psiphon_psiphond_psiphond_proto != nil {
 		return
 	}
-	file_ca_psiphon_psiphond_domain_bytes_proto_init()
+	file_ca_psiphon_psiphond_asn_dest_bytes_proto_init()
+	file_ca_psiphon_psiphond_domain_dest_bytes_proto_init()
 	file_ca_psiphon_psiphond_failed_tunnel_proto_init()
 	file_ca_psiphon_psiphond_inproxy_broker_proto_init()
 	file_ca_psiphon_psiphond_irregular_tunnel_proto_init()
@@ -515,7 +516,6 @@ func file_ca_psiphon_psiphond_psiphond_proto_init() {
 	file_ca_psiphon_psiphond_unique_user_proto_init()
 	file_ca_psiphon_psiphond_dsl_relay_proto_init()
 	file_ca_psiphon_psiphond_psiphond_proto_msgTypes[0].OneofWrappers = []any{
-		(*Psiphond_DomainBytes)(nil),
 		(*Psiphond_FailedTunnel)(nil),
 		(*Psiphond_InproxyBroker)(nil),
 		(*Psiphond_IrregularTunnel)(nil),
@@ -528,10 +528,11 @@ func file_ca_psiphon_psiphond_psiphond_proto_init() {
 		(*Psiphond_ServerPanic)(nil),
 		(*Psiphond_ServerPacket)(nil),
 		(*Psiphond_ServerTunnel)(nil),
-		(*Psiphond_ServerTunnelAsnDestBytes)(nil),
 		(*Psiphond_Tactics)(nil),
 		(*Psiphond_TacticsSpeedtest)(nil),
 		(*Psiphond_UniqueUser)(nil),
+		(*Psiphond_AsnDestBytes)(nil),
+		(*Psiphond_DomainDestBytes)(nil),
 		(*Psiphond_DslRelayGetServerEntries)(nil),
 	}
 	type x struct{}

+ 98 - 281
psiphon/server/pb/psiphond/server_tunnel.pb.go

@@ -27,7 +27,6 @@ type ServerTunnel struct {
 	BaseParams                                    *BaseParams            `protobuf:"bytes,1,opt,name=base_params,json=baseParams,proto3,oneof" json:"base_params,omitempty"`
 	DialParams                                    *DialParams            `protobuf:"bytes,2,opt,name=dial_params,json=dialParams,proto3,oneof" json:"dial_params,omitempty"`
 	InproxyDialParams                             *InproxyDialParams     `protobuf:"bytes,3,opt,name=inproxy_dial_params,json=inproxyDialParams,proto3,oneof" json:"inproxy_dial_params,omitempty"`
-	TunnelId                                      *string                `protobuf:"bytes,100,opt,name=tunnel_id,json=tunnelId,proto3,oneof" json:"tunnel_id,omitempty"`
 	BurstDownstreamFirstBytes                     *int64                 `protobuf:"varint,101,opt,name=burst_downstream_first_bytes,json=burstDownstreamFirstBytes,proto3,oneof" json:"burst_downstream_first_bytes,omitempty"`
 	BurstDownstreamFirstDuration                  *int64                 `protobuf:"varint,102,opt,name=burst_downstream_first_duration,json=burstDownstreamFirstDuration,proto3,oneof" json:"burst_downstream_first_duration,omitempty"`
 	BurstDownstreamFirstOffset                    *int64                 `protobuf:"varint,103,opt,name=burst_downstream_first_offset,json=burstDownstreamFirstOffset,proto3,oneof" json:"burst_downstream_first_offset,omitempty"`
@@ -103,12 +102,6 @@ type ServerTunnel struct {
 	RandomStreamReceivedUpstreamBytes             *int64                 `protobuf:"varint,173,opt,name=random_stream_received_upstream_bytes,json=randomStreamReceivedUpstreamBytes,proto3,oneof" json:"random_stream_received_upstream_bytes,omitempty"`
 	RandomStreamDownstreamBytes                   *int64                 `protobuf:"varint,174,opt,name=random_stream_downstream_bytes,json=randomStreamDownstreamBytes,proto3,oneof" json:"random_stream_downstream_bytes,omitempty"`
 	RandomStreamSentDownstreamBytes               *int64                 `protobuf:"varint,175,opt,name=random_stream_sent_downstream_bytes,json=randomStreamSentDownstreamBytes,proto3,oneof" json:"random_stream_sent_downstream_bytes,omitempty"`
-	DestBytesAsn                                  *string                `protobuf:"bytes,176,opt,name=dest_bytes_asn,json=destBytesAsn,proto3,oneof" json:"dest_bytes_asn,omitempty"`
-	DestBytes                                     *int64                 `protobuf:"varint,177,opt,name=dest_bytes,json=destBytes,proto3,oneof" json:"dest_bytes,omitempty"`
-	DestBytesUpTcp                                *int64                 `protobuf:"varint,178,opt,name=dest_bytes_up_tcp,json=destBytesUpTcp,proto3,oneof" json:"dest_bytes_up_tcp,omitempty"`
-	DestBytesDownTcp                              *int64                 `protobuf:"varint,179,opt,name=dest_bytes_down_tcp,json=destBytesDownTcp,proto3,oneof" json:"dest_bytes_down_tcp,omitempty"`
-	DestBytesUpUdp                                *int64                 `protobuf:"varint,180,opt,name=dest_bytes_up_udp,json=destBytesUpUdp,proto3,oneof" json:"dest_bytes_up_udp,omitempty"`
-	DestBytesDownUdp                              *int64                 `protobuf:"varint,181,opt,name=dest_bytes_down_udp,json=destBytesDownUdp,proto3,oneof" json:"dest_bytes_down_udp,omitempty"`
 	RelayedSteeringIp                             *string                `protobuf:"bytes,182,opt,name=relayed_steering_ip,json=relayedSteeringIp,proto3,oneof" json:"relayed_steering_ip,omitempty"`
 	RequestCheckServerEntryTags                   *int64                 `protobuf:"varint,183,opt,name=request_check_server_entry_tags,json=requestCheckServerEntryTags,proto3,oneof" json:"request_check_server_entry_tags,omitempty"`
 	CheckedServerEntryTags                        *int64                 `protobuf:"varint,184,opt,name=checked_server_entry_tags,json=checkedServerEntryTags,proto3,oneof" json:"checked_server_entry_tags,omitempty"`
@@ -170,13 +163,6 @@ func (x *ServerTunnel) GetInproxyDialParams() *InproxyDialParams {
 	return nil
 }
 
-func (x *ServerTunnel) GetTunnelId() string {
-	if x != nil && x.TunnelId != nil {
-		return *x.TunnelId
-	}
-	return ""
-}
-
 func (x *ServerTunnel) GetBurstDownstreamFirstBytes() int64 {
 	if x != nil && x.BurstDownstreamFirstBytes != nil {
 		return *x.BurstDownstreamFirstBytes
@@ -702,48 +688,6 @@ func (x *ServerTunnel) GetRandomStreamSentDownstreamBytes() int64 {
 	return 0
 }
 
-func (x *ServerTunnel) GetDestBytesAsn() string {
-	if x != nil && x.DestBytesAsn != nil {
-		return *x.DestBytesAsn
-	}
-	return ""
-}
-
-func (x *ServerTunnel) GetDestBytes() int64 {
-	if x != nil && x.DestBytes != nil {
-		return *x.DestBytes
-	}
-	return 0
-}
-
-func (x *ServerTunnel) GetDestBytesUpTcp() int64 {
-	if x != nil && x.DestBytesUpTcp != nil {
-		return *x.DestBytesUpTcp
-	}
-	return 0
-}
-
-func (x *ServerTunnel) GetDestBytesDownTcp() int64 {
-	if x != nil && x.DestBytesDownTcp != nil {
-		return *x.DestBytesDownTcp
-	}
-	return 0
-}
-
-func (x *ServerTunnel) GetDestBytesUpUdp() int64 {
-	if x != nil && x.DestBytesUpUdp != nil {
-		return *x.DestBytesUpUdp
-	}
-	return 0
-}
-
-func (x *ServerTunnel) GetDestBytesDownUdp() int64 {
-	if x != nil && x.DestBytesDownUdp != nil {
-		return *x.DestBytesDownUdp
-	}
-	return 0
-}
-
 func (x *ServerTunnel) GetRelayedSteeringIp() string {
 	if x != nil && x.RelayedSteeringIp != nil {
 		return *x.RelayedSteeringIp
@@ -786,207 +730,105 @@ func (x *ServerTunnel) GetSshProtocolBytesOverhead() int64 {
 	return 0
 }
 
-type ServerTunnelASNDestBytes struct {
-	state            protoimpl.MessageState `protogen:"open.v1"`
-	TunnelId         *string                `protobuf:"bytes,100,opt,name=tunnel_id,json=tunnelId,proto3,oneof" json:"tunnel_id,omitempty"`
-	DestAsn          *string                `protobuf:"bytes,101,opt,name=dest_asn,json=destAsn,proto3,oneof" json:"dest_asn,omitempty"`
-	DestBytes        *int64                 `protobuf:"varint,102,opt,name=dest_bytes,json=destBytes,proto3,oneof" json:"dest_bytes,omitempty"`
-	DestBytesUpTcp   *int64                 `protobuf:"varint,103,opt,name=dest_bytes_up_tcp,json=destBytesUpTcp,proto3,oneof" json:"dest_bytes_up_tcp,omitempty"`
-	DestBytesDownTcp *int64                 `protobuf:"varint,104,opt,name=dest_bytes_down_tcp,json=destBytesDownTcp,proto3,oneof" json:"dest_bytes_down_tcp,omitempty"`
-	DestBytesUpUdp   *int64                 `protobuf:"varint,105,opt,name=dest_bytes_up_udp,json=destBytesUpUdp,proto3,oneof" json:"dest_bytes_up_udp,omitempty"`
-	DestBytesDownUdp *int64                 `protobuf:"varint,106,opt,name=dest_bytes_down_udp,json=destBytesDownUdp,proto3,oneof" json:"dest_bytes_down_udp,omitempty"`
-	unknownFields    protoimpl.UnknownFields
-	sizeCache        protoimpl.SizeCache
-}
-
-func (x *ServerTunnelASNDestBytes) Reset() {
-	*x = ServerTunnelASNDestBytes{}
-	mi := &file_ca_psiphon_psiphond_server_tunnel_proto_msgTypes[1]
-	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-	ms.StoreMessageInfo(mi)
-}
-
-func (x *ServerTunnelASNDestBytes) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ServerTunnelASNDestBytes) ProtoMessage() {}
-
-func (x *ServerTunnelASNDestBytes) ProtoReflect() protoreflect.Message {
-	mi := &file_ca_psiphon_psiphond_server_tunnel_proto_msgTypes[1]
-	if x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ServerTunnelASNDestBytes.ProtoReflect.Descriptor instead.
-func (*ServerTunnelASNDestBytes) Descriptor() ([]byte, []int) {
-	return file_ca_psiphon_psiphond_server_tunnel_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *ServerTunnelASNDestBytes) GetTunnelId() string {
-	if x != nil && x.TunnelId != nil {
-		return *x.TunnelId
-	}
-	return ""
-}
-
-func (x *ServerTunnelASNDestBytes) GetDestAsn() string {
-	if x != nil && x.DestAsn != nil {
-		return *x.DestAsn
-	}
-	return ""
-}
-
-func (x *ServerTunnelASNDestBytes) GetDestBytes() int64 {
-	if x != nil && x.DestBytes != nil {
-		return *x.DestBytes
-	}
-	return 0
-}
-
-func (x *ServerTunnelASNDestBytes) GetDestBytesUpTcp() int64 {
-	if x != nil && x.DestBytesUpTcp != nil {
-		return *x.DestBytesUpTcp
-	}
-	return 0
-}
-
-func (x *ServerTunnelASNDestBytes) GetDestBytesDownTcp() int64 {
-	if x != nil && x.DestBytesDownTcp != nil {
-		return *x.DestBytesDownTcp
-	}
-	return 0
-}
-
-func (x *ServerTunnelASNDestBytes) GetDestBytesUpUdp() int64 {
-	if x != nil && x.DestBytesUpUdp != nil {
-		return *x.DestBytesUpUdp
-	}
-	return 0
-}
-
-func (x *ServerTunnelASNDestBytes) GetDestBytesDownUdp() int64 {
-	if x != nil && x.DestBytesDownUdp != nil {
-		return *x.DestBytesDownUdp
-	}
-	return 0
-}
-
 var File_ca_psiphon_psiphond_server_tunnel_proto protoreflect.FileDescriptor
 
 const file_ca_psiphon_psiphond_server_tunnel_proto_rawDesc = "" +
 	"\n" +
-	"'ca.psiphon.psiphond/server_tunnel.proto\x12\x13ca.psiphon.psiphond\x1a%ca.psiphon.psiphond/base_params.proto\x1a%ca.psiphon.psiphond/dial_params.proto\x1a-ca.psiphon.psiphond/inproxy_dial_params.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8dF\n" +
+	"'ca.psiphon.psiphond/server_tunnel.proto\x12\x13ca.psiphon.psiphond\x1a%ca.psiphon.psiphond/base_params.proto\x1a%ca.psiphon.psiphond/dial_params.proto\x1a-ca.psiphon.psiphond/inproxy_dial_params.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xd0B\n" +
 	"\fServerTunnel\x12E\n" +
 	"\vbase_params\x18\x01 \x01(\v2\x1f.ca.psiphon.psiphond.BaseParamsH\x00R\n" +
 	"baseParams\x88\x01\x01\x12E\n" +
 	"\vdial_params\x18\x02 \x01(\v2\x1f.ca.psiphon.psiphond.DialParamsH\x01R\n" +
 	"dialParams\x88\x01\x01\x12[\n" +
-	"\x13inproxy_dial_params\x18\x03 \x01(\v2&.ca.psiphon.psiphond.InproxyDialParamsH\x02R\x11inproxyDialParams\x88\x01\x01\x12 \n" +
-	"\ttunnel_id\x18d \x01(\tH\x03R\btunnelId\x88\x01\x01\x12D\n" +
-	"\x1cburst_downstream_first_bytes\x18e \x01(\x03H\x04R\x19burstDownstreamFirstBytes\x88\x01\x01\x12J\n" +
-	"\x1fburst_downstream_first_duration\x18f \x01(\x03H\x05R\x1cburstDownstreamFirstDuration\x88\x01\x01\x12F\n" +
-	"\x1dburst_downstream_first_offset\x18g \x01(\x03H\x06R\x1aburstDownstreamFirstOffset\x88\x01\x01\x12B\n" +
-	"\x1bburst_downstream_first_rate\x18h \x01(\x03H\aR\x18burstDownstreamFirstRate\x88\x01\x01\x12B\n" +
-	"\x1bburst_downstream_last_bytes\x18i \x01(\x03H\bR\x18burstDownstreamLastBytes\x88\x01\x01\x12H\n" +
-	"\x1eburst_downstream_last_duration\x18j \x01(\x03H\tR\x1bburstDownstreamLastDuration\x88\x01\x01\x12D\n" +
-	"\x1cburst_downstream_last_offset\x18k \x01(\x03H\n" +
-	"R\x19burstDownstreamLastOffset\x88\x01\x01\x12@\n" +
-	"\x1aburst_downstream_last_rate\x18l \x01(\x03H\vR\x17burstDownstreamLastRate\x88\x01\x01\x12@\n" +
-	"\x1aburst_downstream_max_bytes\x18m \x01(\x03H\fR\x17burstDownstreamMaxBytes\x88\x01\x01\x12F\n" +
-	"\x1dburst_downstream_max_duration\x18n \x01(\x03H\rR\x1aburstDownstreamMaxDuration\x88\x01\x01\x12B\n" +
-	"\x1bburst_downstream_max_offset\x18o \x01(\x03H\x0eR\x18burstDownstreamMaxOffset\x88\x01\x01\x12>\n" +
-	"\x19burst_downstream_max_rate\x18p \x01(\x03H\x0fR\x16burstDownstreamMaxRate\x88\x01\x01\x12@\n" +
-	"\x1aburst_downstream_min_bytes\x18q \x01(\x03H\x10R\x17burstDownstreamMinBytes\x88\x01\x01\x12F\n" +
-	"\x1dburst_downstream_min_duration\x18r \x01(\x03H\x11R\x1aburstDownstreamMinDuration\x88\x01\x01\x12B\n" +
-	"\x1bburst_downstream_min_offset\x18s \x01(\x03H\x12R\x18burstDownstreamMinOffset\x88\x01\x01\x12>\n" +
-	"\x19burst_downstream_min_rate\x18t \x01(\x03H\x13R\x16burstDownstreamMinRate\x88\x01\x01\x12@\n" +
-	"\x1aburst_upstream_first_bytes\x18u \x01(\x03H\x14R\x17burstUpstreamFirstBytes\x88\x01\x01\x12F\n" +
-	"\x1dburst_upstream_first_duration\x18v \x01(\x03H\x15R\x1aburstUpstreamFirstDuration\x88\x01\x01\x12B\n" +
-	"\x1bburst_upstream_first_offset\x18w \x01(\x03H\x16R\x18burstUpstreamFirstOffset\x88\x01\x01\x12>\n" +
-	"\x19burst_upstream_first_rate\x18x \x01(\x03H\x17R\x16burstUpstreamFirstRate\x88\x01\x01\x12>\n" +
-	"\x19burst_upstream_last_bytes\x18y \x01(\x03H\x18R\x16burstUpstreamLastBytes\x88\x01\x01\x12D\n" +
-	"\x1cburst_upstream_last_duration\x18z \x01(\x03H\x19R\x19burstUpstreamLastDuration\x88\x01\x01\x12@\n" +
-	"\x1aburst_upstream_last_offset\x18{ \x01(\x03H\x1aR\x17burstUpstreamLastOffset\x88\x01\x01\x12<\n" +
-	"\x18burst_upstream_last_rate\x18| \x01(\x03H\x1bR\x15burstUpstreamLastRate\x88\x01\x01\x12<\n" +
-	"\x18burst_upstream_max_bytes\x18} \x01(\x03H\x1cR\x15burstUpstreamMaxBytes\x88\x01\x01\x12B\n" +
-	"\x1bburst_upstream_max_duration\x18~ \x01(\x03H\x1dR\x18burstUpstreamMaxDuration\x88\x01\x01\x12>\n" +
-	"\x19burst_upstream_max_offset\x18\x7f \x01(\x03H\x1eR\x16burstUpstreamMaxOffset\x88\x01\x01\x12;\n" +
-	"\x17burst_upstream_max_rate\x18\x80\x01 \x01(\x03H\x1fR\x14burstUpstreamMaxRate\x88\x01\x01\x12=\n" +
-	"\x18burst_upstream_min_bytes\x18\x81\x01 \x01(\x03H R\x15burstUpstreamMinBytes\x88\x01\x01\x12C\n" +
-	"\x1bburst_upstream_min_duration\x18\x82\x01 \x01(\x03H!R\x18burstUpstreamMinDuration\x88\x01\x01\x12?\n" +
-	"\x19burst_upstream_min_offset\x18\x83\x01 \x01(\x03H\"R\x16burstUpstreamMinOffset\x88\x01\x01\x12;\n" +
-	"\x17burst_upstream_min_rate\x18\x84\x01 \x01(\x03H#R\x14burstUpstreamMinRate\x88\x01\x01\x12\x1a\n" +
-	"\x05bytes\x18\x85\x01 \x01(\x03H$R\x05bytes\x88\x01\x01\x12*\n" +
-	"\x0ebytes_down_tcp\x18\x86\x01 \x01(\x03H%R\fbytesDownTcp\x88\x01\x01\x12*\n" +
-	"\x0ebytes_down_udp\x18\x87\x01 \x01(\x03H&R\fbytesDownUdp\x88\x01\x01\x12&\n" +
-	"\fbytes_up_tcp\x18\x88\x01 \x01(\x03H'R\n" +
+	"\x13inproxy_dial_params\x18\x03 \x01(\v2&.ca.psiphon.psiphond.InproxyDialParamsH\x02R\x11inproxyDialParams\x88\x01\x01\x12D\n" +
+	"\x1cburst_downstream_first_bytes\x18e \x01(\x03H\x03R\x19burstDownstreamFirstBytes\x88\x01\x01\x12J\n" +
+	"\x1fburst_downstream_first_duration\x18f \x01(\x03H\x04R\x1cburstDownstreamFirstDuration\x88\x01\x01\x12F\n" +
+	"\x1dburst_downstream_first_offset\x18g \x01(\x03H\x05R\x1aburstDownstreamFirstOffset\x88\x01\x01\x12B\n" +
+	"\x1bburst_downstream_first_rate\x18h \x01(\x03H\x06R\x18burstDownstreamFirstRate\x88\x01\x01\x12B\n" +
+	"\x1bburst_downstream_last_bytes\x18i \x01(\x03H\aR\x18burstDownstreamLastBytes\x88\x01\x01\x12H\n" +
+	"\x1eburst_downstream_last_duration\x18j \x01(\x03H\bR\x1bburstDownstreamLastDuration\x88\x01\x01\x12D\n" +
+	"\x1cburst_downstream_last_offset\x18k \x01(\x03H\tR\x19burstDownstreamLastOffset\x88\x01\x01\x12@\n" +
+	"\x1aburst_downstream_last_rate\x18l \x01(\x03H\n" +
+	"R\x17burstDownstreamLastRate\x88\x01\x01\x12@\n" +
+	"\x1aburst_downstream_max_bytes\x18m \x01(\x03H\vR\x17burstDownstreamMaxBytes\x88\x01\x01\x12F\n" +
+	"\x1dburst_downstream_max_duration\x18n \x01(\x03H\fR\x1aburstDownstreamMaxDuration\x88\x01\x01\x12B\n" +
+	"\x1bburst_downstream_max_offset\x18o \x01(\x03H\rR\x18burstDownstreamMaxOffset\x88\x01\x01\x12>\n" +
+	"\x19burst_downstream_max_rate\x18p \x01(\x03H\x0eR\x16burstDownstreamMaxRate\x88\x01\x01\x12@\n" +
+	"\x1aburst_downstream_min_bytes\x18q \x01(\x03H\x0fR\x17burstDownstreamMinBytes\x88\x01\x01\x12F\n" +
+	"\x1dburst_downstream_min_duration\x18r \x01(\x03H\x10R\x1aburstDownstreamMinDuration\x88\x01\x01\x12B\n" +
+	"\x1bburst_downstream_min_offset\x18s \x01(\x03H\x11R\x18burstDownstreamMinOffset\x88\x01\x01\x12>\n" +
+	"\x19burst_downstream_min_rate\x18t \x01(\x03H\x12R\x16burstDownstreamMinRate\x88\x01\x01\x12@\n" +
+	"\x1aburst_upstream_first_bytes\x18u \x01(\x03H\x13R\x17burstUpstreamFirstBytes\x88\x01\x01\x12F\n" +
+	"\x1dburst_upstream_first_duration\x18v \x01(\x03H\x14R\x1aburstUpstreamFirstDuration\x88\x01\x01\x12B\n" +
+	"\x1bburst_upstream_first_offset\x18w \x01(\x03H\x15R\x18burstUpstreamFirstOffset\x88\x01\x01\x12>\n" +
+	"\x19burst_upstream_first_rate\x18x \x01(\x03H\x16R\x16burstUpstreamFirstRate\x88\x01\x01\x12>\n" +
+	"\x19burst_upstream_last_bytes\x18y \x01(\x03H\x17R\x16burstUpstreamLastBytes\x88\x01\x01\x12D\n" +
+	"\x1cburst_upstream_last_duration\x18z \x01(\x03H\x18R\x19burstUpstreamLastDuration\x88\x01\x01\x12@\n" +
+	"\x1aburst_upstream_last_offset\x18{ \x01(\x03H\x19R\x17burstUpstreamLastOffset\x88\x01\x01\x12<\n" +
+	"\x18burst_upstream_last_rate\x18| \x01(\x03H\x1aR\x15burstUpstreamLastRate\x88\x01\x01\x12<\n" +
+	"\x18burst_upstream_max_bytes\x18} \x01(\x03H\x1bR\x15burstUpstreamMaxBytes\x88\x01\x01\x12B\n" +
+	"\x1bburst_upstream_max_duration\x18~ \x01(\x03H\x1cR\x18burstUpstreamMaxDuration\x88\x01\x01\x12>\n" +
+	"\x19burst_upstream_max_offset\x18\x7f \x01(\x03H\x1dR\x16burstUpstreamMaxOffset\x88\x01\x01\x12;\n" +
+	"\x17burst_upstream_max_rate\x18\x80\x01 \x01(\x03H\x1eR\x14burstUpstreamMaxRate\x88\x01\x01\x12=\n" +
+	"\x18burst_upstream_min_bytes\x18\x81\x01 \x01(\x03H\x1fR\x15burstUpstreamMinBytes\x88\x01\x01\x12C\n" +
+	"\x1bburst_upstream_min_duration\x18\x82\x01 \x01(\x03H R\x18burstUpstreamMinDuration\x88\x01\x01\x12?\n" +
+	"\x19burst_upstream_min_offset\x18\x83\x01 \x01(\x03H!R\x16burstUpstreamMinOffset\x88\x01\x01\x12;\n" +
+	"\x17burst_upstream_min_rate\x18\x84\x01 \x01(\x03H\"R\x14burstUpstreamMinRate\x88\x01\x01\x12\x1a\n" +
+	"\x05bytes\x18\x85\x01 \x01(\x03H#R\x05bytes\x88\x01\x01\x12*\n" +
+	"\x0ebytes_down_tcp\x18\x86\x01 \x01(\x03H$R\fbytesDownTcp\x88\x01\x01\x12*\n" +
+	"\x0ebytes_down_udp\x18\x87\x01 \x01(\x03H%R\fbytesDownUdp\x88\x01\x01\x12&\n" +
+	"\fbytes_up_tcp\x18\x88\x01 \x01(\x03H&R\n" +
 	"bytesUpTcp\x88\x01\x01\x12&\n" +
-	"\fbytes_up_udp\x18\x89\x01 \x01(\x03H(R\n" +
+	"\fbytes_up_udp\x18\x89\x01 \x01(\x03H'R\n" +
 	"bytesUpUdp\x88\x01\x01\x12 \n" +
-	"\bduration\x18\x8a\x01 \x01(\x03H)R\bduration\x88\x01\x01\x12;\n" +
-	"\x16establishment_duration\x18\x8b\x01 \x01(\x03H*R\x15establishmentDuration\x88\x01\x01\x125\n" +
-	"\x13handshake_completed\x18\x8c\x01 \x01(\bH+R\x12handshakeCompleted\x88\x01\x01\x12@\n" +
-	"\x1ais_first_tunnel_in_session\x18\x8d\x01 \x01(\bH,R\x16isFirstTunnelInSession\x88\x01\x01\x12P\n" +
-	"\"meek_cached_response_miss_position\x18\x8e\x01 \x01(\x03H-R\x1emeekCachedResponseMissPosition\x88\x01\x01\x124\n" +
-	"\x13meek_client_retries\x18\x8f\x01 \x01(\x03H.R\x11meekClientRetries\x88\x01\x01\x12O\n" +
-	"\"meek_peak_cached_response_hit_size\x18\x90\x01 \x01(\x03H/R\x1dmeekPeakCachedResponseHitSize\x88\x01\x01\x12H\n" +
-	"\x1emeek_peak_cached_response_size\x18\x91\x01 \x01(\x03H0R\x1ameekPeakCachedResponseSize\x88\x01\x01\x12;\n" +
-	"\x17meek_peak_response_size\x18\x92\x01 \x01(\x03H1R\x14meekPeakResponseSize\x88\x01\x01\x12M\n" +
-	" meek_underlying_connection_count\x18\x93\x01 \x01(\x03H2R\x1dmeekUnderlyingConnectionCount\x88\x01\x01\x12=\n" +
-	"\x18meek_server_http_version\x18\x94\x01 \x01(\tH3R\x15meekServerHttpVersion\x88\x01\x01\x12,\n" +
-	"\x0fnew_tactics_tag\x18\x95\x01 \x01(\tH4R\rnewTacticsTag\x88\x01\x01\x12f\n" +
-	".peak_concurrent_dialing_port_forward_count_tcp\x18\x96\x01 \x01(\x03H5R(peakConcurrentDialingPortForwardCountTcp\x88\x01\x01\x12W\n" +
-	"&peak_concurrent_port_forward_count_tcp\x18\x97\x01 \x01(\x03H6R!peakConcurrentPortForwardCountTcp\x88\x01\x01\x12W\n" +
-	"&peak_concurrent_port_forward_count_udp\x18\x98\x01 \x01(\x03H7R!peakConcurrentPortForwardCountUdp\x88\x01\x01\x12`\n" +
-	"*peak_concurrent_proximate_accepted_clients\x18\x99\x01 \x01(\x03H8R&peakConcurrentProximateAcceptedClients\x88\x01\x01\x12f\n" +
-	"-peak_concurrent_proximate_established_clients\x18\x9a\x01 \x01(\x03H9R)peakConcurrentProximateEstablishedClients\x88\x01\x01\x127\n" +
-	"\x15peak_dns_failure_rate\x18\x9b\x01 \x01(\x01H:R\x12peakDnsFailureRate\x88\x01\x01\x12M\n" +
-	"!peak_dns_failure_rate_sample_size\x18\x9c\x01 \x01(\x03H;R\x1cpeakDnsFailureRateSampleSize\x88\x01\x01\x12O\n" +
-	"\"peak_tcp_port_forward_failure_rate\x18\x9d\x01 \x01(\x01H<R\x1dpeakTcpPortForwardFailureRate\x88\x01\x01\x12e\n" +
-	".peak_tcp_port_forward_failure_rate_sample_size\x18\x9e\x01 \x01(\x03H=R'peakTcpPortForwardFailureRateSampleSize\x88\x01\x01\x12N\n" +
-	"!pre_handshake_random_stream_count\x18\x9f\x01 \x01(\x03H>R\x1dpreHandshakeRandomStreamCount\x88\x01\x01\x12c\n" +
-	",pre_handshake_random_stream_downstream_bytes\x18\xa0\x01 \x01(\x03H?R'preHandshakeRandomStreamDownstreamBytes\x88\x01\x01\x12p\n" +
-	"3pre_handshake_random_stream_received_upstream_bytes\x18\xa1\x01 \x01(\x03H@R-preHandshakeRandomStreamReceivedUpstreamBytes\x88\x01\x01\x12l\n" +
-	"1pre_handshake_random_stream_sent_downstream_bytes\x18\xa2\x01 \x01(\x03HAR+preHandshakeRandomStreamSentDownstreamBytes\x88\x01\x01\x12_\n" +
-	"*pre_handshake_random_stream_upstream_bytes\x18\xa3\x01 \x01(\x03HBR%preHandshakeRandomStreamUpstreamBytes\x88\x01\x01\x12'\n" +
-	"\fsplit_tunnel\x18\xa4\x01 \x01(\bHCR\vsplitTunnel\x88\x01\x01\x12?\n" +
-	"\n" +
-	"start_time\x18\xa5\x01 \x01(\v2\x1a.google.protobuf.TimestampHDR\tstartTime\x88\x01\x01\x122\n" +
-	"\x12station_ip_address\x18\xa6\x01 \x01(\tHER\x10stationIpAddress\x88\x01\x01\x12N\n" +
-	"!total_packet_tunnel_channel_count\x18\xa7\x01 \x01(\x03HFR\x1dtotalPacketTunnelChannelCount\x88\x01\x01\x12D\n" +
-	"\x1ctotal_port_forward_count_tcp\x18\xa8\x01 \x01(\x03HGR\x18totalPortForwardCountTcp\x88\x01\x01\x12D\n" +
-	"\x1ctotal_port_forward_count_udp\x18\xa9\x01 \x01(\x03HHR\x18totalPortForwardCountUdp\x88\x01\x01\x12?\n" +
-	"\x19total_udpgw_channel_count\x18\xaa\x01 \x01(\x03HIR\x16totalUdpgwChannelCount\x88\x01\x01\x124\n" +
-	"\x13random_stream_count\x18\xab\x01 \x01(\x03HJR\x11randomStreamCount\x88\x01\x01\x12E\n" +
-	"\x1crandom_stream_upstream_bytes\x18\xac\x01 \x01(\x03HKR\x19randomStreamUpstreamBytes\x88\x01\x01\x12V\n" +
-	"%random_stream_received_upstream_bytes\x18\xad\x01 \x01(\x03HLR!randomStreamReceivedUpstreamBytes\x88\x01\x01\x12I\n" +
-	"\x1erandom_stream_downstream_bytes\x18\xae\x01 \x01(\x03HMR\x1brandomStreamDownstreamBytes\x88\x01\x01\x12R\n" +
-	"#random_stream_sent_downstream_bytes\x18\xaf\x01 \x01(\x03HNR\x1frandomStreamSentDownstreamBytes\x88\x01\x01\x12*\n" +
-	"\x0edest_bytes_asn\x18\xb0\x01 \x01(\tHOR\fdestBytesAsn\x88\x01\x01\x12#\n" +
+	"\bduration\x18\x8a\x01 \x01(\x03H(R\bduration\x88\x01\x01\x12;\n" +
+	"\x16establishment_duration\x18\x8b\x01 \x01(\x03H)R\x15establishmentDuration\x88\x01\x01\x125\n" +
+	"\x13handshake_completed\x18\x8c\x01 \x01(\bH*R\x12handshakeCompleted\x88\x01\x01\x12@\n" +
+	"\x1ais_first_tunnel_in_session\x18\x8d\x01 \x01(\bH+R\x16isFirstTunnelInSession\x88\x01\x01\x12P\n" +
+	"\"meek_cached_response_miss_position\x18\x8e\x01 \x01(\x03H,R\x1emeekCachedResponseMissPosition\x88\x01\x01\x124\n" +
+	"\x13meek_client_retries\x18\x8f\x01 \x01(\x03H-R\x11meekClientRetries\x88\x01\x01\x12O\n" +
+	"\"meek_peak_cached_response_hit_size\x18\x90\x01 \x01(\x03H.R\x1dmeekPeakCachedResponseHitSize\x88\x01\x01\x12H\n" +
+	"\x1emeek_peak_cached_response_size\x18\x91\x01 \x01(\x03H/R\x1ameekPeakCachedResponseSize\x88\x01\x01\x12;\n" +
+	"\x17meek_peak_response_size\x18\x92\x01 \x01(\x03H0R\x14meekPeakResponseSize\x88\x01\x01\x12M\n" +
+	" meek_underlying_connection_count\x18\x93\x01 \x01(\x03H1R\x1dmeekUnderlyingConnectionCount\x88\x01\x01\x12=\n" +
+	"\x18meek_server_http_version\x18\x94\x01 \x01(\tH2R\x15meekServerHttpVersion\x88\x01\x01\x12,\n" +
+	"\x0fnew_tactics_tag\x18\x95\x01 \x01(\tH3R\rnewTacticsTag\x88\x01\x01\x12f\n" +
+	".peak_concurrent_dialing_port_forward_count_tcp\x18\x96\x01 \x01(\x03H4R(peakConcurrentDialingPortForwardCountTcp\x88\x01\x01\x12W\n" +
+	"&peak_concurrent_port_forward_count_tcp\x18\x97\x01 \x01(\x03H5R!peakConcurrentPortForwardCountTcp\x88\x01\x01\x12W\n" +
+	"&peak_concurrent_port_forward_count_udp\x18\x98\x01 \x01(\x03H6R!peakConcurrentPortForwardCountUdp\x88\x01\x01\x12`\n" +
+	"*peak_concurrent_proximate_accepted_clients\x18\x99\x01 \x01(\x03H7R&peakConcurrentProximateAcceptedClients\x88\x01\x01\x12f\n" +
+	"-peak_concurrent_proximate_established_clients\x18\x9a\x01 \x01(\x03H8R)peakConcurrentProximateEstablishedClients\x88\x01\x01\x127\n" +
+	"\x15peak_dns_failure_rate\x18\x9b\x01 \x01(\x01H9R\x12peakDnsFailureRate\x88\x01\x01\x12M\n" +
+	"!peak_dns_failure_rate_sample_size\x18\x9c\x01 \x01(\x03H:R\x1cpeakDnsFailureRateSampleSize\x88\x01\x01\x12O\n" +
+	"\"peak_tcp_port_forward_failure_rate\x18\x9d\x01 \x01(\x01H;R\x1dpeakTcpPortForwardFailureRate\x88\x01\x01\x12e\n" +
+	".peak_tcp_port_forward_failure_rate_sample_size\x18\x9e\x01 \x01(\x03H<R'peakTcpPortForwardFailureRateSampleSize\x88\x01\x01\x12N\n" +
+	"!pre_handshake_random_stream_count\x18\x9f\x01 \x01(\x03H=R\x1dpreHandshakeRandomStreamCount\x88\x01\x01\x12c\n" +
+	",pre_handshake_random_stream_downstream_bytes\x18\xa0\x01 \x01(\x03H>R'preHandshakeRandomStreamDownstreamBytes\x88\x01\x01\x12p\n" +
+	"3pre_handshake_random_stream_received_upstream_bytes\x18\xa1\x01 \x01(\x03H?R-preHandshakeRandomStreamReceivedUpstreamBytes\x88\x01\x01\x12l\n" +
+	"1pre_handshake_random_stream_sent_downstream_bytes\x18\xa2\x01 \x01(\x03H@R+preHandshakeRandomStreamSentDownstreamBytes\x88\x01\x01\x12_\n" +
+	"*pre_handshake_random_stream_upstream_bytes\x18\xa3\x01 \x01(\x03HAR%preHandshakeRandomStreamUpstreamBytes\x88\x01\x01\x12'\n" +
+	"\fsplit_tunnel\x18\xa4\x01 \x01(\bHBR\vsplitTunnel\x88\x01\x01\x12?\n" +
 	"\n" +
-	"dest_bytes\x18\xb1\x01 \x01(\x03HPR\tdestBytes\x88\x01\x01\x12/\n" +
-	"\x11dest_bytes_up_tcp\x18\xb2\x01 \x01(\x03HQR\x0edestBytesUpTcp\x88\x01\x01\x123\n" +
-	"\x13dest_bytes_down_tcp\x18\xb3\x01 \x01(\x03HRR\x10destBytesDownTcp\x88\x01\x01\x12/\n" +
-	"\x11dest_bytes_up_udp\x18\xb4\x01 \x01(\x03HSR\x0edestBytesUpUdp\x88\x01\x01\x123\n" +
-	"\x13dest_bytes_down_udp\x18\xb5\x01 \x01(\x03HTR\x10destBytesDownUdp\x88\x01\x01\x124\n" +
-	"\x13relayed_steering_ip\x18\xb6\x01 \x01(\tHUR\x11relayedSteeringIp\x88\x01\x01\x12J\n" +
-	"\x1frequest_check_server_entry_tags\x18\xb7\x01 \x01(\x03HVR\x1brequestCheckServerEntryTags\x88\x01\x01\x12?\n" +
-	"\x19checked_server_entry_tags\x18\xb8\x01 \x01(\x03HWR\x16checkedServerEntryTags\x88\x01\x01\x12?\n" +
-	"\x19invalid_server_entry_tags\x18\xb9\x01 \x01(\x03HXR\x16invalidServerEntryTags\x88\x01\x01\x122\n" +
-	"\x12ssh_protocol_bytes\x18\xba\x01 \x01(\x03HYR\x10sshProtocolBytes\x88\x01\x01\x12C\n" +
-	"\x1bssh_protocol_bytes_overhead\x18\xbb\x01 \x01(\x03HZR\x18sshProtocolBytesOverhead\x88\x01\x01B\x0e\n" +
+	"start_time\x18\xa5\x01 \x01(\v2\x1a.google.protobuf.TimestampHCR\tstartTime\x88\x01\x01\x122\n" +
+	"\x12station_ip_address\x18\xa6\x01 \x01(\tHDR\x10stationIpAddress\x88\x01\x01\x12N\n" +
+	"!total_packet_tunnel_channel_count\x18\xa7\x01 \x01(\x03HER\x1dtotalPacketTunnelChannelCount\x88\x01\x01\x12D\n" +
+	"\x1ctotal_port_forward_count_tcp\x18\xa8\x01 \x01(\x03HFR\x18totalPortForwardCountTcp\x88\x01\x01\x12D\n" +
+	"\x1ctotal_port_forward_count_udp\x18\xa9\x01 \x01(\x03HGR\x18totalPortForwardCountUdp\x88\x01\x01\x12?\n" +
+	"\x19total_udpgw_channel_count\x18\xaa\x01 \x01(\x03HHR\x16totalUdpgwChannelCount\x88\x01\x01\x124\n" +
+	"\x13random_stream_count\x18\xab\x01 \x01(\x03HIR\x11randomStreamCount\x88\x01\x01\x12E\n" +
+	"\x1crandom_stream_upstream_bytes\x18\xac\x01 \x01(\x03HJR\x19randomStreamUpstreamBytes\x88\x01\x01\x12V\n" +
+	"%random_stream_received_upstream_bytes\x18\xad\x01 \x01(\x03HKR!randomStreamReceivedUpstreamBytes\x88\x01\x01\x12I\n" +
+	"\x1erandom_stream_downstream_bytes\x18\xae\x01 \x01(\x03HLR\x1brandomStreamDownstreamBytes\x88\x01\x01\x12R\n" +
+	"#random_stream_sent_downstream_bytes\x18\xaf\x01 \x01(\x03HMR\x1frandomStreamSentDownstreamBytes\x88\x01\x01\x124\n" +
+	"\x13relayed_steering_ip\x18\xb6\x01 \x01(\tHNR\x11relayedSteeringIp\x88\x01\x01\x12J\n" +
+	"\x1frequest_check_server_entry_tags\x18\xb7\x01 \x01(\x03HOR\x1brequestCheckServerEntryTags\x88\x01\x01\x12?\n" +
+	"\x19checked_server_entry_tags\x18\xb8\x01 \x01(\x03HPR\x16checkedServerEntryTags\x88\x01\x01\x12?\n" +
+	"\x19invalid_server_entry_tags\x18\xb9\x01 \x01(\x03HQR\x16invalidServerEntryTags\x88\x01\x01\x122\n" +
+	"\x12ssh_protocol_bytes\x18\xba\x01 \x01(\x03HRR\x10sshProtocolBytes\x88\x01\x01\x12C\n" +
+	"\x1bssh_protocol_bytes_overhead\x18\xbb\x01 \x01(\x03HSR\x18sshProtocolBytesOverhead\x88\x01\x01B\x0e\n" +
 	"\f_base_paramsB\x0e\n" +
 	"\f_dial_paramsB\x16\n" +
-	"\x14_inproxy_dial_paramsB\f\n" +
-	"\n" +
-	"_tunnel_idB\x1f\n" +
+	"\x14_inproxy_dial_paramsB\x1f\n" +
 	"\x1d_burst_downstream_first_bytesB\"\n" +
 	" _burst_downstream_first_durationB \n" +
 	"\x1e_burst_downstream_first_offsetB\x1e\n" +
@@ -1061,36 +903,13 @@ const file_ca_psiphon_psiphond_server_tunnel_proto_rawDesc = "" +
 	"\x1d_random_stream_upstream_bytesB(\n" +
 	"&_random_stream_received_upstream_bytesB!\n" +
 	"\x1f_random_stream_downstream_bytesB&\n" +
-	"$_random_stream_sent_downstream_bytesB\x11\n" +
-	"\x0f_dest_bytes_asnB\r\n" +
-	"\v_dest_bytesB\x14\n" +
-	"\x12_dest_bytes_up_tcpB\x16\n" +
-	"\x14_dest_bytes_down_tcpB\x14\n" +
-	"\x12_dest_bytes_up_udpB\x16\n" +
-	"\x14_dest_bytes_down_udpB\x16\n" +
+	"$_random_stream_sent_downstream_bytesB\x16\n" +
 	"\x14_relayed_steering_ipB\"\n" +
 	" _request_check_server_entry_tagsB\x1c\n" +
 	"\x1a_checked_server_entry_tagsB\x1c\n" +
 	"\x1a_invalid_server_entry_tagsB\x15\n" +
 	"\x13_ssh_protocol_bytesB\x1e\n" +
-	"\x1c_ssh_protocol_bytes_overhead\"\xce\x03\n" +
-	"\x18ServerTunnelASNDestBytes\x12 \n" +
-	"\ttunnel_id\x18d \x01(\tH\x00R\btunnelId\x88\x01\x01\x12\x1e\n" +
-	"\bdest_asn\x18e \x01(\tH\x01R\adestAsn\x88\x01\x01\x12\"\n" +
-	"\n" +
-	"dest_bytes\x18f \x01(\x03H\x02R\tdestBytes\x88\x01\x01\x12.\n" +
-	"\x11dest_bytes_up_tcp\x18g \x01(\x03H\x03R\x0edestBytesUpTcp\x88\x01\x01\x122\n" +
-	"\x13dest_bytes_down_tcp\x18h \x01(\x03H\x04R\x10destBytesDownTcp\x88\x01\x01\x12.\n" +
-	"\x11dest_bytes_up_udp\x18i \x01(\x03H\x05R\x0edestBytesUpUdp\x88\x01\x01\x122\n" +
-	"\x13dest_bytes_down_udp\x18j \x01(\x03H\x06R\x10destBytesDownUdp\x88\x01\x01B\f\n" +
-	"\n" +
-	"_tunnel_idB\v\n" +
-	"\t_dest_asnB\r\n" +
-	"\v_dest_bytesB\x14\n" +
-	"\x12_dest_bytes_up_tcpB\x16\n" +
-	"\x14_dest_bytes_down_tcpB\x14\n" +
-	"\x12_dest_bytes_up_udpB\x16\n" +
-	"\x14_dest_bytes_down_udpBHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
+	"\x1c_ssh_protocol_bytes_overheadJ\x04\bd\x10eJ\x06\b\xb0\x01\x10\xb6\x01BHZFgithub.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphondb\x06proto3"
 
 var (
 	file_ca_psiphon_psiphond_server_tunnel_proto_rawDescOnce sync.Once
@@ -1104,20 +923,19 @@ func file_ca_psiphon_psiphond_server_tunnel_proto_rawDescGZIP() []byte {
 	return file_ca_psiphon_psiphond_server_tunnel_proto_rawDescData
 }
 
-var file_ca_psiphon_psiphond_server_tunnel_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_ca_psiphon_psiphond_server_tunnel_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
 var file_ca_psiphon_psiphond_server_tunnel_proto_goTypes = []any{
-	(*ServerTunnel)(nil),             // 0: ca.psiphon.psiphond.ServerTunnel
-	(*ServerTunnelASNDestBytes)(nil), // 1: ca.psiphon.psiphond.ServerTunnelASNDestBytes
-	(*BaseParams)(nil),               // 2: ca.psiphon.psiphond.BaseParams
-	(*DialParams)(nil),               // 3: ca.psiphon.psiphond.DialParams
-	(*InproxyDialParams)(nil),        // 4: ca.psiphon.psiphond.InproxyDialParams
-	(*timestamppb.Timestamp)(nil),    // 5: google.protobuf.Timestamp
+	(*ServerTunnel)(nil),          // 0: ca.psiphon.psiphond.ServerTunnel
+	(*BaseParams)(nil),            // 1: ca.psiphon.psiphond.BaseParams
+	(*DialParams)(nil),            // 2: ca.psiphon.psiphond.DialParams
+	(*InproxyDialParams)(nil),     // 3: ca.psiphon.psiphond.InproxyDialParams
+	(*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp
 }
 var file_ca_psiphon_psiphond_server_tunnel_proto_depIdxs = []int32{
-	2, // 0: ca.psiphon.psiphond.ServerTunnel.base_params:type_name -> ca.psiphon.psiphond.BaseParams
-	3, // 1: ca.psiphon.psiphond.ServerTunnel.dial_params:type_name -> ca.psiphon.psiphond.DialParams
-	4, // 2: ca.psiphon.psiphond.ServerTunnel.inproxy_dial_params:type_name -> ca.psiphon.psiphond.InproxyDialParams
-	5, // 3: ca.psiphon.psiphond.ServerTunnel.start_time:type_name -> google.protobuf.Timestamp
+	1, // 0: ca.psiphon.psiphond.ServerTunnel.base_params:type_name -> ca.psiphon.psiphond.BaseParams
+	2, // 1: ca.psiphon.psiphond.ServerTunnel.dial_params:type_name -> ca.psiphon.psiphond.DialParams
+	3, // 2: ca.psiphon.psiphond.ServerTunnel.inproxy_dial_params:type_name -> ca.psiphon.psiphond.InproxyDialParams
+	4, // 3: ca.psiphon.psiphond.ServerTunnel.start_time:type_name -> google.protobuf.Timestamp
 	4, // [4:4] is the sub-list for method output_type
 	4, // [4:4] is the sub-list for method input_type
 	4, // [4:4] is the sub-list for extension type_name
@@ -1134,14 +952,13 @@ func file_ca_psiphon_psiphond_server_tunnel_proto_init() {
 	file_ca_psiphon_psiphond_dial_params_proto_init()
 	file_ca_psiphon_psiphond_inproxy_dial_params_proto_init()
 	file_ca_psiphon_psiphond_server_tunnel_proto_msgTypes[0].OneofWrappers = []any{}
-	file_ca_psiphon_psiphond_server_tunnel_proto_msgTypes[1].OneofWrappers = []any{}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: unsafe.Slice(unsafe.StringData(file_ca_psiphon_psiphond_server_tunnel_proto_rawDesc), len(file_ca_psiphon_psiphond_server_tunnel_proto_rawDesc)),
 			NumEnums:      0,
-			NumMessages:   2,
+			NumMessages:   1,
 			NumExtensions: 0,
 			NumServices:   0,
 		},

+ 18 - 0
psiphon/server/proto/ca.psiphon.psiphond/asn_dest_bytes.proto

@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package ca.psiphon.psiphond;
+
+import "ca.psiphon.psiphond/dest_params.proto";
+
+option go_package = "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphond";
+
+message AsnDestBytes {
+    optional ca.psiphon.psiphond.DestParams dest_params = 1;
+
+    // Fields 1-99 are reserved for field groupings.
+
+    optional string asn = 100;
+    optional int64 bytes_tcp = 101;
+    optional int64 bytes_udp = 102;
+    optional int64 bytes = 103;
+}

+ 13 - 0
psiphon/server/proto/ca.psiphon.psiphond/dest_params.proto

@@ -0,0 +1,13 @@
+syntax = "proto3";
+
+package ca.psiphon.psiphond;
+
+option go_package = "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphond";
+
+message DestParams {
+    optional string client_asn = 1;
+    optional string client_platform = 2;
+    optional string client_region = 3;
+    optional string device_region = 4;
+    optional string sponsor_id = 5;
+}

+ 0 - 16
psiphon/server/proto/ca.psiphon.psiphond/domain_bytes.proto

@@ -1,16 +0,0 @@
-syntax = "proto3";
-
-package ca.psiphon.psiphond;
-
-import "ca.psiphon.psiphond/base_params.proto";
-
-option go_package = "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphond";
-
-message DomainBytes {
-    optional ca.psiphon.psiphond.BaseParams base_params = 1;
-
-    // Fields 1-99 are reserved for field groupings.
-
-    optional int64 bytes = 100;
-    optional string domain = 101;
-}

+ 18 - 0
psiphon/server/proto/ca.psiphon.psiphond/domain_dest_bytes.proto

@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package ca.psiphon.psiphond;
+
+import "ca.psiphon.psiphond/dest_params.proto";
+
+option go_package = "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/pb/psiphond";
+
+message DomainDestBytes {
+    optional ca.psiphon.psiphond.DestParams dest_params = 1;
+
+    // Fields 1-99 are reserved for field groupings.
+
+    optional string domain = 100;
+    optional int64 bytes_tcp = 101;
+    optional int64 bytes_udp = 102;
+    optional int64 bytes = 103;
+}

+ 6 - 3
psiphon/server/proto/ca.psiphon.psiphond/psiphond.proto

@@ -3,7 +3,8 @@ syntax = "proto3";
 package ca.psiphon.psiphond;
 
 import "google/protobuf/timestamp.proto";
-import "ca.psiphon.psiphond/domain_bytes.proto";
+import "ca.psiphon.psiphond/asn_dest_bytes.proto";
+import "ca.psiphon.psiphond/domain_dest_bytes.proto";
 import "ca.psiphon.psiphond/failed_tunnel.proto";
 import "ca.psiphon.psiphond/inproxy_broker.proto";
 import "ca.psiphon.psiphond/irregular_tunnel.proto";
@@ -27,9 +28,10 @@ message Psiphond {
   string host_type = 3;
   string host_build_rev = 4;
   string provider = 5;
+
+  reserved 101, 114; // retired fields
  
   oneof metric {
-    ca.psiphon.psiphond.DomainBytes domain_bytes = 101;
     ca.psiphon.psiphond.FailedTunnel failed_tunnel = 102;
     ca.psiphon.psiphond.InproxyBroker inproxy_broker = 103;
     ca.psiphon.psiphond.IrregularTunnel irregular_tunnel = 104;
@@ -42,10 +44,11 @@ message Psiphond {
     ca.psiphon.psiphond.ServerPanic server_panic = 111;
     ca.psiphon.psiphond.ServerPacket server_packet = 112;
     ca.psiphon.psiphond.ServerTunnel server_tunnel = 113;
-    ca.psiphon.psiphond.ServerTunnelASNDestBytes server_tunnel_asn_dest_bytes = 114;
     ca.psiphon.psiphond.Tactics tactics = 115;
     ca.psiphon.psiphond.TacticsSpeedTest tactics_speedtest = 116;
     ca.psiphon.psiphond.UniqueUser unique_user = 117;
+    ca.psiphon.psiphond.AsnDestBytes asn_dest_bytes = 118;
+    ca.psiphon.psiphond.DomainDestBytes domain_dest_bytes = 119;
     ca.psiphon.psiphond.DslRelayGetServerEntries dsl_relay_get_server_entries = 120;
   }
 }

+ 1 - 20
psiphon/server/proto/ca.psiphon.psiphond/server_tunnel.proto

@@ -16,7 +16,7 @@ message ServerTunnel {
 
     // Fields 1-99 are reserved for field groupings.
 
-    optional string tunnel_id = 100;
+    reserved 100, 176 to 181; // retired fields
 
     optional int64 burst_downstream_first_bytes = 101;
     optional int64 burst_downstream_first_duration = 102;
@@ -93,12 +93,6 @@ message ServerTunnel {
     optional int64 random_stream_received_upstream_bytes = 173;
     optional int64 random_stream_downstream_bytes = 174;
     optional int64 random_stream_sent_downstream_bytes = 175;
-    optional string dest_bytes_asn = 176;
-    optional int64 dest_bytes = 177;
-    optional int64 dest_bytes_up_tcp = 178;
-    optional int64 dest_bytes_down_tcp = 179;
-    optional int64 dest_bytes_up_udp = 180;
-    optional int64 dest_bytes_down_udp = 181;
     optional string relayed_steering_ip = 182;
     optional int64 request_check_server_entry_tags = 183;
     optional int64 checked_server_entry_tags = 184;
@@ -106,16 +100,3 @@ message ServerTunnel {
     optional int64 ssh_protocol_bytes = 186;
     optional int64 ssh_protocol_bytes_overhead = 187;
 }
-
-message ServerTunnelASNDestBytes {
-    // Fields 1-99 are reserved for field groupings.
-
-    optional string tunnel_id = 100;
-    optional string dest_asn = 101;
-    optional int64 dest_bytes = 102;
-    optional int64 dest_bytes_up_tcp = 103;
-    optional int64 dest_bytes_down_tcp = 104;
-    optional int64 dest_bytes_up_udp = 105;
-    optional int64 dest_bytes_down_udp = 106;
-
-}

+ 25 - 50
psiphon/server/protobufConverter.go

@@ -21,6 +21,7 @@ type protobufFieldGroupConfig struct {
 	baseParams        bool
 	dialParams        bool
 	inproxyDialParams bool
+	destParams        bool
 }
 
 // protobufMessageFieldGroups defines field group requirements for each message type
@@ -33,8 +34,11 @@ var protobufMessageFieldGroups = map[string]protobufFieldGroupConfig{
 	"unique_user": {
 		baseParams: true,
 	},
-	"domain_bytes": {
-		baseParams: true,
+	"asn_dest_bytes": {
+		destParams: true,
+	},
+	"domain_dest_bytes": {
+		destParams: true,
 	},
 	"server_blocklist_hit": {
 		baseParams: true,
@@ -160,60 +164,19 @@ func logFieldsToProtobuf(logFields LogFields) []*pbr.Router {
 	case "server_tunnel":
 		msg := &pb.ServerTunnel{}
 		protobufPopulateMessage(logFields, msg, eventName)
-
-		// Capture the tunnel ID once here to avoid looking it up for every sub-message.
-		tunnelID := msg.TunnelId
-
-		// Populate and append the initial server tunnel protobuf message.
 		psiphondWrapped.Metric = &pb.Psiphond_ServerTunnel{ServerTunnel: msg}
-
-		out = append(out, newProtobufRoutedMessage(psiphondWrapped))
-
-		// If this message includes asn_dest_bytes_* maps, emit
-		// one protobuf ServerTunnelASNDestBytes per ASN.
-		if asnBytes, hasASNBytes := logFields["asn_dest_bytes"]; hasASNBytes {
-			for asn, totalBytes := range asnBytes.(map[string]int64) {
-				msg := &pb.ServerTunnelASNDestBytes{
-					TunnelId:  tunnelID,
-					DestAsn:   &asn,
-					DestBytes: &totalBytes,
-				}
-
-				if value, exists := logFields["asn_dest_bytes_up_tcp"].(map[string]int64)[asn]; exists {
-					msg.DestBytesUpTcp = &value
-				}
-
-				if value, exists := logFields["asn_dest_bytes_down_tcp"].(map[string]int64)[asn]; exists {
-					msg.DestBytesDownTcp = &value
-				}
-
-				if value, exists := logFields["asn_dest_bytes_up_udp"].(map[string]int64)[asn]; exists {
-					msg.DestBytesUpUdp = &value
-				}
-
-				if value, exists := logFields["asn_dest_bytes_down_udp"].(map[string]int64)[asn]; exists {
-					msg.DestBytesDownUdp = &value
-				}
-
-				psiphondWrapped = newPsiphondProtobufMessageWrapper(pbTimestamp, hostType)
-				psiphondWrapped.Metric = &pb.Psiphond_ServerTunnelAsnDestBytes{ServerTunnelAsnDestBytes: msg}
-
-				out = append(out, newProtobufRoutedMessage(psiphondWrapped))
-			}
-		}
-
-		// Return early with the slice of wrapped messages here to skip
-		// extra append attempts at the end of this switch, since we've
-		// manually appended all of the wrapper messages ourselves.
-		return out
 	case "unique_user":
 		msg := &pb.UniqueUser{}
 		protobufPopulateMessage(logFields, msg, eventName)
 		psiphondWrapped.Metric = &pb.Psiphond_UniqueUser{UniqueUser: msg}
-	case "domain_bytes":
-		msg := &pb.DomainBytes{}
+	case "asn_dest_bytes":
+		msg := &pb.AsnDestBytes{}
+		protobufPopulateMessage(logFields, msg, eventName)
+		psiphondWrapped.Metric = &pb.Psiphond_AsnDestBytes{AsnDestBytes: msg}
+	case "domain_dest_bytes":
+		msg := &pb.DomainDestBytes{}
 		protobufPopulateMessage(logFields, msg, eventName)
-		psiphondWrapped.Metric = &pb.Psiphond_DomainBytes{DomainBytes: msg}
+		psiphondWrapped.Metric = &pb.Psiphond_DomainDestBytes{DomainDestBytes: msg}
 	case "server_load":
 		if region, hasRegion := logFields["region"]; hasRegion {
 			for _, proto := range append(protocol.SupportedTunnelProtocols, "ALL") {
@@ -365,6 +328,14 @@ func protobufPopulateInproxyDialParams(logFields LogFields) *pb.InproxyDialParam
 	return msg
 }
 
+// protobufPopulateDestParams populates DestParams from LogFields.
+func protobufPopulateDestParams(logFields LogFields) *pb.DestParams {
+	msg := &pb.DestParams{}
+	protobufPopulateMessageFromFields(logFields, msg)
+
+	return msg
+}
+
 // protobufPopulateMessage is the single function that handles all protobuf message types.
 func protobufPopulateMessage(logFields LogFields, msg proto.Message, eventName string) {
 	config, exists := protobufMessageFieldGroups[eventName]
@@ -412,6 +383,10 @@ func protobufPopulateFieldGroups(logFields LogFields, msg proto.Message, config
 			if config.inproxyDialParams {
 				field.Set(reflect.ValueOf(protobufPopulateInproxyDialParams(logFields)))
 			}
+		case "DestParams":
+			if config.destParams {
+				field.Set(reflect.ValueOf(protobufPopulateDestParams(logFields)))
+			}
 		}
 	}
 }

+ 138 - 299
psiphon/server/server_test.go

@@ -686,7 +686,7 @@ func TestCheckPruneServerEntries(t *testing.T) {
 		})
 }
 
-func TestBurstMonitorAndDestinationBytes(t *testing.T) {
+func TestBurstMonitorAndASNDestBytes(t *testing.T) {
 	runServer(t,
 		&runServerConfig{
 			tunnelProtocol:       "OSSH",
@@ -695,40 +695,24 @@ func TestBurstMonitorAndDestinationBytes(t *testing.T) {
 			doTunneledNTPRequest: true,
 			doDanglingTCPConn:    true,
 			doBurstMonitor:       true,
-			doDestinationBytes:   true,
+			doASNDestBytes:       true,
 			doLogHostProvider:    true,
 			doLogProtobuf:        useProtobufLogging,
 		})
 }
 
-func TestBurstMonitorAndLegacyDestinationBytes(t *testing.T) {
-	runServer(t,
-		&runServerConfig{
-			tunnelProtocol:           "OSSH",
-			requireAuthorization:     true,
-			doTunneledWebRequest:     true,
-			doTunneledNTPRequest:     true,
-			doDanglingTCPConn:        true,
-			doBurstMonitor:           true,
-			doLegacyDestinationBytes: true,
-			doLogHostProvider:        true,
-			doLogProtobuf:            useProtobufLogging,
-		})
-}
-
 func TestChangeBytesConfig(t *testing.T) {
 	runServer(t,
 		&runServerConfig{
-			tunnelProtocol:           "OSSH",
-			requireAuthorization:     true,
-			doTunneledWebRequest:     true,
-			doTunneledNTPRequest:     true,
-			doDanglingTCPConn:        true,
-			doDestinationBytes:       true,
-			doLegacyDestinationBytes: true,
-			doChangeBytesConfig:      true,
-			doLogHostProvider:        true,
-			doLogProtobuf:            useProtobufLogging,
+			tunnelProtocol:       "OSSH",
+			requireAuthorization: true,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+			doDanglingTCPConn:    true,
+			doASNDestBytes:       true,
+			doChangeBytesConfig:  true,
+			doLogHostProvider:    true,
+			doLogProtobuf:        useProtobufLogging,
 		})
 }
 
@@ -801,41 +785,40 @@ func TestDomainRequest(t *testing.T) {
 }
 
 type runServerConfig struct {
-	tunnelProtocol           string
-	clientTunnelProtocol     string
-	passthrough              bool
-	tlsProfile               string
-	doHotReload              bool
-	doDefaultSponsorID       bool
-	denyTrafficRules         bool
-	requireAuthorization     bool
-	omitAuthorization        bool
-	doTunneledWebRequest     bool
-	doTunneledDomainRequest  bool
-	doTunneledNTPRequest     bool
-	applyPrefix              bool
-	forceFragmenting         bool
-	forceLivenessTest        bool
-	doPruneServerEntries     bool
-	checkPruneServerEntries  bool
-	doDanglingTCPConn        bool
-	doPacketManipulation     bool
-	doBurstMonitor           bool
-	doSplitTunnel            bool
-	limitQUICVersions        bool
-	doDestinationBytes       bool
-	doLegacyDestinationBytes bool
-	doChangeBytesConfig      bool
-	doLogHostProvider        bool
-	inspectFlows             bool
-	doSteeringIP             bool
-	doTargetBrokerSpecs      bool
-	useLegacyAPIEncoding     bool
-	doPersonalPairing        bool
-	doRestrictInproxy        bool
-	useInproxyMediaStreams   bool
-	doUncompressedTactics    bool
-	doLogProtobuf            bool
+	tunnelProtocol          string
+	clientTunnelProtocol    string
+	passthrough             bool
+	tlsProfile              string
+	doHotReload             bool
+	doDefaultSponsorID      bool
+	denyTrafficRules        bool
+	requireAuthorization    bool
+	omitAuthorization       bool
+	doTunneledWebRequest    bool
+	doTunneledDomainRequest bool
+	doTunneledNTPRequest    bool
+	applyPrefix             bool
+	forceFragmenting        bool
+	forceLivenessTest       bool
+	doPruneServerEntries    bool
+	checkPruneServerEntries bool
+	doDanglingTCPConn       bool
+	doPacketManipulation    bool
+	doBurstMonitor          bool
+	doSplitTunnel           bool
+	limitQUICVersions       bool
+	doASNDestBytes          bool
+	doChangeBytesConfig     bool
+	doLogHostProvider       bool
+	inspectFlows            bool
+	doSteeringIP            bool
+	doTargetBrokerSpecs     bool
+	useLegacyAPIEncoding    bool
+	doPersonalPairing       bool
+	doRestrictInproxy       bool
+	useInproxyMediaStreams  bool
+	doUncompressedTactics   bool
+	doLogProtobuf           bool
 }
 
 var (
@@ -971,8 +954,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		runConfig.applyPrefix ||
 		runConfig.forceFragmenting ||
 		runConfig.doBurstMonitor ||
-		runConfig.doDestinationBytes ||
-		runConfig.doLegacyDestinationBytes ||
+		runConfig.doASNDestBytes ||
 		runConfig.doTunneledDomainRequest
 
 	// All servers require a tactics config with valid keys.
@@ -1125,8 +1107,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 			propagationChannelID,
 			livenessTestSize,
 			runConfig.doBurstMonitor,
-			runConfig.doDestinationBytes,
-			runConfig.doLegacyDestinationBytes,
+			runConfig.doASNDestBytes,
 			runConfig.applyPrefix,
 			runConfig.forceFragmenting,
 			"classic",
@@ -1276,15 +1257,10 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	}
 	resetReassembleServerLoadLogs(expectedServerLoadProtocolLogs)
 
-	expectedTunnelLogs := 1
-	if runConfig.doDestinationBytes && !runConfig.doChangeBytesConfig {
-		expectedTunnelLogs++ // 1 base + 1 ASN
-	}
-	resetReassembleServerTunnelLogs(expectedTunnelLogs)
-
-	uniqueUserLog := make(chan map[string]interface{}, 1)
-	domainBytesLog := make(chan map[string]interface{}, 1)
 	serverTunnelLog := make(chan map[string]interface{}, 1)
+	uniqueUserLog := make(chan map[string]interface{}, 1)
+	asnDestBytesLog := make(chan map[string]interface{}, 1)
+	domainDestBytesLog := make(chan map[string]interface{}, 1)
 
 	// Max 3 discovery logs:
 	// 1. server startup
@@ -1324,9 +1300,14 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 			case uniqueUserLog <- logFields:
 			default:
 			}
-		case "domain_bytes":
+		case "asn_dest_bytes":
+			select {
+			case asnDestBytesLog <- logFields:
+			default:
+			}
+		case "domain_dest_bytes":
 			select {
-			case domainBytesLog <- logFields:
+			case domainDestBytesLog <- logFields:
 			default:
 			}
 		case "server_tunnel":
@@ -1422,15 +1403,6 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 				if err != nil {
 					return errors.Trace(err)
 				}
-			} else if strings.HasPrefix(eventName, "server_tunnel") {
-
-				// Multiple protobuf server_tunnel* logs are reassembled into
-				// one JSON server_tunnel log.
-				reflectedLogFields, err = reassembleServerTunnelLog(
-					eventName, reflectedLogFields)
-				if err != nil {
-					return errors.Trace(err)
-				}
 			}
 
 			if reflectedLogFields != nil {
@@ -1451,9 +1423,9 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		}
 
 		socketReader.Start()
-		readerShutdownCtx, readerShutdownCtxCancel :=
-			context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
 		defer func() {
+			readerShutdownCtx, readerShutdownCtxCancel :=
+				context.WithDeadline(context.Background(), time.Now())
 			readerShutdownCtxCancel()
 			socketReader.Stop(readerShutdownCtx)
 		}()
@@ -1567,8 +1539,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 				propagationChannelID,
 				livenessTestSize,
 				runConfig.doBurstMonitor,
-				runConfig.doDestinationBytes,
-				runConfig.doLegacyDestinationBytes,
+				runConfig.doASNDestBytes,
 				runConfig.applyPrefix,
 				runConfig.forceFragmenting,
 				"consistent",
@@ -2136,7 +2107,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 
 	if runConfig.doChangeBytesConfig {
 
-		if !runConfig.doDestinationBytes || !runConfig.doLegacyDestinationBytes {
+		if !runConfig.doASNDestBytes {
 			t.Fatalf("invalid test configuration")
 		}
 
@@ -2162,7 +2133,6 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 			livenessTestSize,
 			runConfig.doBurstMonitor,
 			false,
-			false,
 			runConfig.applyPrefix,
 			runConfig.forceFragmenting,
 			"consistent",
@@ -2343,8 +2313,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	if runConfig.limitQUICVersions {
 		expectQUICVersion = limitQUICVersions[0]
 	}
-	expectDestinationBytesFields := runConfig.doDestinationBytes && !runConfig.doChangeBytesConfig
-	expectLegacyDestinationBytesFields := runConfig.doLegacyDestinationBytes && !runConfig.doChangeBytesConfig
+	expectASNDestBytes := runConfig.doASNDestBytes && !runConfig.doChangeBytesConfig
 	expectMeekHTTPVersion := ""
 	if protocol.TunnelProtocolUsesMeek(runConfig.tunnelProtocol) {
 		if protocol.TunnelProtocolUsesFrontedMeek(runConfig.tunnelProtocol) {
@@ -2361,13 +2330,13 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 	}
 	expectDSLPrioritized := doDSL
 
-	// The client still reports zero domain_bytes when no port forwards are
+	// The client still reports domain_bytes up when no port forwards are
 	// allowed (expectTrafficFailure).
 	//
 	// Limitation: this check is disabled in the in-proxy case since, in the
 	// self-proxy scheme, the proxy shuts down before the client can send its
 	// final status request.
-	expectDomainBytes := !runConfig.doChangeBytesConfig && !doInproxy
+	expectDomainDestBytes := !runConfig.doChangeBytesConfig && !doInproxy
 
 	select {
 	case logFields := <-serverTunnelLog:
@@ -2390,8 +2359,6 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 			expectUDPDataTransfer,
 			expectDomainPortForward,
 			expectQUICVersion,
-			expectDestinationBytesFields,
-			expectLegacyDestinationBytesFields,
 			passthroughAddress,
 			expectMeekHTTPVersion,
 			expectCheckServerEntryPruneCount,
@@ -2426,22 +2393,42 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		}
 	}
 
-	if expectDomainBytes {
+	if expectASNDestBytes {
+		select {
+		case logFields := <-asnDestBytesLog:
+			err := checkExpectedASNDestBytesLogFields(
+				runConfig,
+				logFields)
+			if err != nil {
+				t.Fatalf("invalid ASN dest bytes log fields: %s", err)
+			}
+		default:
+			t.Fatalf("missing ASN bytes log")
+		}
+	} else {
+		select {
+		case <-asnDestBytesLog:
+			t.Fatalf("unexpected ASN dest bytes log")
+		default:
+		}
+	}
+
+	if expectDomainDestBytes {
 		select {
-		case logFields := <-domainBytesLog:
-			err := checkExpectedDomainBytesLogFields(
+		case logFields := <-domainDestBytesLog:
+			err := checkExpectedDomainDestBytesLogFields(
 				runConfig,
 				logFields)
 			if err != nil {
-				t.Fatalf("invalid domain bytes log fields: %s", err)
+				t.Fatalf("invalid domain dest bytes log fields: %s", err)
 			}
 		default:
 			t.Fatalf("missing domain bytes log")
 		}
 	} else {
 		select {
-		case <-domainBytesLog:
-			t.Fatalf("unexpected domain bytes log")
+		case <-domainDestBytesLog:
+			t.Fatalf("unexpected domain dest bytes log")
 		default:
 		}
 	}
@@ -2743,10 +2730,8 @@ func protoToLogFields(msg proto.Message, logFields map[string]interface{}, runCo
 }
 
 var (
-	reassembledServerLoadLogFields   map[string]interface{}
-	serverLoadLogComponentSequence   []int
-	reassembledServerTunnelLogFields map[string]interface{}
-	serverTunnelComponentSequence    []int
+	reassembledServerLoadLogFields map[string]interface{}
+	serverLoadLogComponentSequence []int
 )
 
 func resetReassembleServerLoadLogs(expectedProtocolLogs int) {
@@ -2766,11 +2751,6 @@ func resetReassembleServerLoadLogs(expectedProtocolLogs int) {
 		2 + expectedProtocolLogs}
 }
 
-func resetReassembleServerTunnelLogs(expectedTunnelLogs int) {
-	serverTunnelComponentSequence = []int{expectedTunnelLogs}
-	reassembledServerTunnelLogFields = nil
-}
-
 func reassembleServerLoadLog(
 	eventName string,
 	reflectedLogFields map[string]interface{}) (map[string]interface{}, error) {
@@ -2860,90 +2840,6 @@ func reassembleServerLoadLog(
 	return nil, nil
 }
 
-func reassembleServerTunnelLog(
-	eventName string,
-	reflectedLogFields map[string]interface{}) (map[string]interface{}, error) {
-
-	// Reassemble protobuf server_tunnel components into a single set of fields
-	// compatible with the existing JSON log content checker.
-	if !strings.HasPrefix(eventName, "server_tunnel") {
-		return nil, errors.TraceNew("unexpected non-server_tunnel log")
-	}
-
-	i := 0
-	for ; i < len(serverTunnelComponentSequence); i++ {
-		if serverTunnelComponentSequence[i] > 0 {
-			break
-		}
-	}
-	if i >= len(serverTunnelComponentSequence) {
-		return nil, errors.TraceNew("unexpected server_tunnel sequence")
-	}
-
-	serverTunnelComponentSequence[i] -= 1
-	sequenceComplete := serverTunnelComponentSequence[i] == 0
-
-	if reassembledServerTunnelLogFields == nil {
-		reassembledServerTunnelLogFields = make(map[string]interface{})
-	}
-
-	serverTunnelLogFields := reassembledServerTunnelLogFields
-
-	switch eventName {
-	case "server_tunnel":
-		// Base server_tunnel message - copy all fields
-		for k, v := range reflectedLogFields {
-			serverTunnelLogFields[k] = v
-		}
-
-	case "server_tunnel_asn_dest_bytes":
-		// Initialize ASN byte maps if they don't exist
-		if serverTunnelLogFields["asn_dest_bytes"] == nil {
-			serverTunnelLogFields["asn_dest_bytes"] = make(map[string]interface{})
-		}
-		if serverTunnelLogFields["asn_dest_bytes_up_tcp"] == nil {
-			serverTunnelLogFields["asn_dest_bytes_up_tcp"] = make(map[string]interface{})
-		}
-		if serverTunnelLogFields["asn_dest_bytes_down_tcp"] == nil {
-			serverTunnelLogFields["asn_dest_bytes_down_tcp"] = make(map[string]interface{})
-		}
-		if serverTunnelLogFields["asn_dest_bytes_up_udp"] == nil {
-			serverTunnelLogFields["asn_dest_bytes_up_udp"] = make(map[string]interface{})
-		}
-		if serverTunnelLogFields["asn_dest_bytes_down_udp"] == nil {
-			serverTunnelLogFields["asn_dest_bytes_down_udp"] = make(map[string]interface{})
-		}
-
-		// Populate ASN-specific byte counts
-		if destAsn, ok := reflectedLogFields["dest_asn"].(string); ok {
-			if destBytes, ok := reflectedLogFields["dest_bytes"].(int64); ok {
-				serverTunnelLogFields["asn_dest_bytes"].(map[string]interface{})[destAsn] = destBytes
-			}
-			if destBytesUpTcp, ok := reflectedLogFields["dest_bytes_up_tcp"].(int64); ok {
-				serverTunnelLogFields["asn_dest_bytes_up_tcp"].(map[string]interface{})[destAsn] = destBytesUpTcp
-			}
-			if destBytesDownTcp, ok := reflectedLogFields["dest_bytes_down_tcp"].(int64); ok {
-				serverTunnelLogFields["asn_dest_bytes_down_tcp"].(map[string]interface{})[destAsn] = destBytesDownTcp
-			}
-			if destBytesUpUdp, ok := reflectedLogFields["dest_bytes_up_udp"].(int64); ok {
-				serverTunnelLogFields["asn_dest_bytes_up_udp"].(map[string]interface{})[destAsn] = destBytesUpUdp
-			}
-			if destBytesDownUdp, ok := reflectedLogFields["dest_bytes_down_udp"].(int64); ok {
-				serverTunnelLogFields["asn_dest_bytes_down_udp"].(map[string]interface{})[destAsn] = destBytesDownUdp
-			}
-		}
-	default:
-		return nil, fmt.Errorf("unmatched server_tunnel event: %s", eventName)
-	}
-
-	if sequenceComplete {
-		serverTunnelLogFields["event_name"] = "server_tunnel"
-		return serverTunnelLogFields, nil
-	}
-
-	return nil, nil
-}
-
 func checkExpectedServerTunnelLogFields(
 	runConfig *runServerConfig,
 	expectPropagationChannelID string,
@@ -2958,8 +2854,6 @@ func checkExpectedServerTunnelLogFields(
 	expectUDPDataTransfer bool,
 	expectDomainPortForward bool,
 	expectQUICVersion string,
-	expectDestinationBytesFields bool,
-	expectLegacyDestinationBytesFields bool,
 	expectPassthroughAddress *string,
 	expectMeekHTTPVersion string,
 	expectCheckServerEntryPruneCount int,
@@ -2977,7 +2871,6 @@ func checkExpectedServerTunnelLogFields(
 	for _, name := range []string{
 		"host_id",
 		"server_entry_tag",
-		"tunnel_id",
 		"start_time",
 		"duration",
 		"session_id",
@@ -3620,86 +3513,6 @@ func checkExpectedServerTunnelLogFields(
 		}
 	}
 
-	for _, name := range []string{
-		"asn_dest_bytes",
-		"asn_dest_bytes_up_tcp",
-		"asn_dest_bytes_down_tcp",
-		"asn_dest_bytes_up_udp",
-		"asn_dest_bytes_down_udp",
-	} {
-		if expectDestinationBytesFields && fields[name] == nil {
-			return fmt.Errorf("missing expected field '%s'", name)
-
-		} else if !expectDestinationBytesFields && fields[name] != nil {
-			return fmt.Errorf("unexpected field '%s'", name)
-		}
-	}
-
-	if expectDestinationBytesFields {
-		for _, pair := range [][]string{
-			{"asn_dest_bytes", "bytes"},
-			{"asn_dest_bytes_up_tcp", "bytes_up_tcp"},
-			{"asn_dest_bytes_down_tcp", "bytes_down_tcp"},
-			{"asn_dest_bytes_up_udp", "bytes_up_udp"},
-			{"asn_dest_bytes_down_udp", "bytes_down_udp"},
-		} {
-			if _, ok := fields[pair[0]].(map[string]any)[testGeoIPASN].(float64); !ok {
-				return fmt.Errorf("missing field entry %s: '%v'", pair[0], testGeoIPASN)
-			}
-			value0 := int64(fields[pair[0]].(map[string]any)[testGeoIPASN].(float64))
-			value1 := int64(fields[pair[1]].(float64))
-			ok := value0 == value1
-			if pair[0] == "asn_dest_bytes_up_udp" || pair[0] == "asn_dest_bytes_down_udp" || pair[0] == "asn_dest_bytes" {
-				// DNS requests are excluded from destination bytes counting
-				ok = value0 > 0 && value0 < value1
-			}
-			if !ok {
-				return fmt.Errorf("unexpected field value %s: %v != %v", pair[0], fields[pair[0]], fields[pair[1]])
-			}
-		}
-	}
-
-	for _, name := range []string{
-		"dest_bytes_asn",
-		"dest_bytes_up_tcp",
-		"dest_bytes_down_tcp",
-		"dest_bytes_up_udp",
-		"dest_bytes_down_udp",
-		"dest_bytes",
-	} {
-		if expectLegacyDestinationBytesFields && fields[name] == nil {
-			return fmt.Errorf("missing expected field '%s'", name)
-
-		} else if !expectLegacyDestinationBytesFields && fields[name] != nil {
-			return fmt.Errorf("unexpected field '%s'", name)
-		}
-	}
-
-	if expectLegacyDestinationBytesFields {
-		name := "dest_bytes_asn"
-		if fields[name].(string) != testGeoIPASN {
-			return fmt.Errorf("unexpected field value %s: '%v'", name, fields[name])
-		}
-		for _, pair := range [][]string{
-			{"dest_bytes_up_tcp", "bytes_up_tcp"},
-			{"dest_bytes_down_tcp", "bytes_down_tcp"},
-			{"dest_bytes_up_udp", "bytes_up_udp"},
-			{"dest_bytes_down_udp", "bytes_down_udp"},
-			{"dest_bytes", "bytes"},
-		} {
-			value0 := int64(fields[pair[0]].(float64))
-			value1 := int64(fields[pair[1]].(float64))
-			ok := value0 == value1
-			if pair[0] == "dest_bytes_up_udp" || pair[0] == "dest_bytes_down_udp" || pair[0] == "dest_bytes" {
-				// DNS requests are excluded from destination bytes counting
-				ok = value0 > 0 && value0 < value1
-			}
-			if !ok {
-				return fmt.Errorf("unexpected field value %s: %v != %v", pair[0], fields[pair[0]], fields[pair[1]])
-			}
-		}
-	}
-
 	if expectPassthroughAddress != nil {
 		name := "passthrough_address"
 		if fields[name] == nil {
@@ -3795,19 +3608,20 @@ func checkExpectedUniqueUserLogFields(
 	return nil
 }
 
-func checkExpectedDomainBytesLogFields(
+func checkExpectedDomainDestBytesLogFields(
 	runConfig *runServerConfig,
 	fields map[string]interface{}) error {
 
 	for _, name := range []string{
-		"session_id",
-		"propagation_channel_id",
-		"sponsor_id",
+		"client_asn",
 		"client_platform",
+		"client_region",
 		"device_region",
-		"device_location",
+		"sponsor_id",
 		"domain",
 		"bytes",
+		"bytes_tcp",
+		"bytes_udp",
 	} {
 		if fields[name] == nil || fmt.Sprintf("%s", fields[name]) == "" {
 			return fmt.Errorf("missing expected field '%s'", name)
@@ -3823,6 +3637,41 @@ func checkExpectedDomainBytesLogFields(
 	return nil
 }
 
+func checkExpectedASNDestBytesLogFields(
+	runConfig *runServerConfig,
+	fields map[string]interface{}) error {
+
+	for _, name := range []string{
+		"client_asn",
+		"client_platform",
+		"client_region",
+		"device_region",
+		"sponsor_id",
+		"asn",
+		"bytes",
+		"bytes_tcp",
+		"bytes_udp",
+	} {
+		if fields[name] == nil || fmt.Sprintf("%s", fields[name]) == "" {
+			return fmt.Errorf("missing expected field '%s'", name)
+		}
+
+		if name == "asn" {
+			if fields[name].(string) != testGeoIPASN {
+				return fmt.Errorf("unexpected field value %s: '%v'", name, fields[name])
+			}
+		}
+		for _, name := range []string{"bytes_tcp", "bytes_udp", "bytes"} {
+			value := int64(fields[name].(float64))
+			if value <= 0 {
+				return fmt.Errorf("unexpected field value %s: %v", name, fields[name])
+			}
+		}
+	}
+
+	return nil
+}
+
 func checkExpectedDSLPendingPrioritizeDial(
 	clientConfig *psiphon.Config,
 	networkID string) error {
@@ -4458,8 +4307,7 @@ func paveTacticsConfigFile(
 	propagationChannelID string,
 	livenessTestSize int,
 	doBurstMonitor bool,
-	doDestinationBytes bool,
-	doLegacyDestinationBytes bool,
+	doASNDestBytes bool,
 	applyOsshPrefix bool,
 	enableOsshPrefixFragmenting bool,
 	discoveryStategy string,
@@ -4485,7 +4333,6 @@ func paveTacticsConfigFile(
           %s
           %s
           %s
-          %s
           "LimitTunnelProtocols" : ["%s"],
           "FragmentorLimitProtocols" : ["%s"],
           "FragmentorProbability" : 1.0,
@@ -4571,20 +4418,13 @@ func paveTacticsConfigFile(
 	`
 	}
 
-	destinationBytesParameters := ""
-	if doDestinationBytes {
-		destinationBytesParameters = fmt.Sprintf(`
+	asnDestBytesParameters := ""
+	if doASNDestBytes {
+		asnDestBytesParameters = fmt.Sprintf(`
           "DestinationBytesMetricsASNs" : ["%s"],
 	`, testGeoIPASN)
 	}
 
-	legacyDestinationBytesParameters := ""
-	if doLegacyDestinationBytes {
-		legacyDestinationBytesParameters = fmt.Sprintf(`
-          "DestinationBytesMetricsASN" : "%s",
-	`, testGeoIPASN)
-	}
-
 	osshPrefix := ""
 	if applyOsshPrefix {
 		osshPrefix = fmt.Sprintf(`
@@ -4611,8 +4451,7 @@ func paveTacticsConfigFile(
 		tacticsRequestPrivateKey,
 		tacticsRequestObfuscatedKey,
 		burstParameters,
-		destinationBytesParameters,
-		legacyDestinationBytesParameters,
+		asnDestBytesParameters,
 		osshPrefix,
 		inproxyParametersJSON,
 		restrictInproxyParameters,

+ 22 - 0
psiphon/server/services.go

@@ -172,6 +172,10 @@ func RunServices(configJSON []byte) (retErr error) {
 
 	support.discovery = makeDiscovery(support)
 
+	if config.RunDestBytesLogger() {
+		support.destBytesLogger = newDestBytesLogger(support)
+	}
+
 	// After this point, errors should be delivered to the errors channel and
 	// orderly shutdown should flow through to the end of the function to ensure
 	// all workers are synchronously stopped.
@@ -315,6 +319,23 @@ func RunServices(configJSON []byte) (retErr error) {
 		}()
 	}
 
+	if config.RunDestBytesLogger() {
+		err = support.destBytesLogger.Start()
+		if err != nil {
+			select {
+			case errorChannel <- err:
+			default:
+			}
+		} else {
+			waitGroup.Add(1)
+			go func() {
+				defer waitGroup.Done()
+				<-shutdownBroadcast
+				support.destBytesLogger.Stop()
+			}()
+		}
+	}
+
 	// The tunnel server is always run; it launches multiple
 	// listeners, depending on which tunnel protocols are enabled.
 	waitGroup.Add(1)
@@ -625,6 +646,7 @@ type SupportServices struct {
 	ServerTacticsParametersCache *ServerTacticsParametersCache
 	dslRelay                     *dsl.Relay
 	discovery                    *Discovery
+	destBytesLogger              *destBytesLogger
 }
 
 // NewSupportServices initializes a new SupportServices.

+ 11 - 63
psiphon/server/tunnelServer.go

@@ -3673,8 +3673,6 @@ func (sshClient *sshClient) logTunnel(additionalMetrics []LogFields) {
 
 	sshClient.sshServer.support.Config.AddServerEntryTag(logFields)
 
-	logFields["tunnel_id"] = base64.RawURLEncoding.EncodeToString(prng.Bytes(protocol.PSIPHON_API_TUNNEL_ID_LENGTH))
-
 	if sshClient.isInproxyTunnelProtocol {
 		sshClient.peerGeoIPData.SetLogFieldsWithPrefix("", "inproxy_proxy", logFields)
 		logFields.Add(
@@ -3748,14 +3746,13 @@ func (sshClient *sshClient) logTunnel(additionalMetrics []LogFields) {
 
 		// Only log destination bytes for ASNs that remain enabled in tactics.
 		//
-		// Any counts accumulated before DestinationBytesMetricsASN[s] changes
+		// Any counts accumulated before DestinationBytesMetricsASNs changes
 		// are lost. At this time we can't change destination byte counting
 		// dynamically, after a tactics hot reload, as there may be
 		// destination bytes port forwards that were in place before the
 		// change, which will continue to count.
 
 		destinationBytesMetricsASNs := []string{}
-		destinationBytesMetricsASN := ""
 
 		// Target this using the client, not peer, GeoIP. In the case of
 		// in-proxy tunnel protocols, the client GeoIP fields will be None
@@ -3765,66 +3762,27 @@ func (sshClient *sshClient) logTunnel(additionalMetrics []LogFields) {
 		p, err := sshClient.sshServer.support.ServerTacticsParametersCache.Get(sshClient.clientGeoIPData)
 		if err == nil && !p.IsNil() {
 			destinationBytesMetricsASNs = p.Strings(parameters.DestinationBytesMetricsASNs)
-			destinationBytesMetricsASN = p.String(parameters.DestinationBytesMetricsASN)
 		}
 		p.Close()
 
-		if destinationBytesMetricsASN != "" {
-
-			// Log any parameters.DestinationBytesMetricsASN data in the
-			// legacy log field format.
-
-			destinationBytesMetrics, ok :=
-				sshClient.destinationBytesMetrics[destinationBytesMetricsASN]
-
-			if ok {
-				bytesUpTCP := destinationBytesMetrics.tcpMetrics.getBytesUp()
-				bytesDownTCP := destinationBytesMetrics.tcpMetrics.getBytesDown()
-				bytesUpUDP := destinationBytesMetrics.udpMetrics.getBytesUp()
-				bytesDownUDP := destinationBytesMetrics.udpMetrics.getBytesDown()
-
-				logFields["dest_bytes_asn"] = destinationBytesMetricsASN
-				logFields["dest_bytes"] = bytesUpTCP + bytesDownTCP + bytesUpUDP + bytesDownUDP
-				logFields["dest_bytes_up_tcp"] = bytesUpTCP
-				logFields["dest_bytes_down_tcp"] = bytesDownTCP
-				logFields["dest_bytes_up_udp"] = bytesUpUDP
-				logFields["dest_bytes_down_udp"] = bytesDownUDP
-			}
-		}
-
 		if len(destinationBytesMetricsASNs) > 0 {
 
-			destBytes := make(map[string]int64)
-			destBytesUpTCP := make(map[string]int64)
-			destBytesDownTCP := make(map[string]int64)
-			destBytesUpUDP := make(map[string]int64)
-			destBytesDownUDP := make(map[string]int64)
-
 			for _, ASN := range destinationBytesMetricsASNs {
 
-				destinationBytesMetrics, ok :=
-					sshClient.destinationBytesMetrics[ASN]
+				destinationBytesMetrics, ok := sshClient.destinationBytesMetrics[ASN]
 				if !ok {
 					continue
 				}
 
-				bytesUpTCP := destinationBytesMetrics.tcpMetrics.getBytesUp()
-				bytesDownTCP := destinationBytesMetrics.tcpMetrics.getBytesDown()
-				bytesUpUDP := destinationBytesMetrics.udpMetrics.getBytesUp()
-				bytesDownUDP := destinationBytesMetrics.udpMetrics.getBytesDown()
-
-				destBytes[ASN] = bytesUpTCP + bytesDownTCP + bytesUpUDP + bytesDownUDP
-				destBytesUpTCP[ASN] = bytesUpTCP
-				destBytesDownTCP[ASN] = bytesDownTCP
-				destBytesUpUDP[ASN] = bytesUpUDP
-				destBytesDownUDP[ASN] = bytesDownUDP
+				sshClient.sshServer.support.destBytesLogger.AddASNBytes(
+					ASN,
+					sshClient.clientGeoIPData,
+					sshClient.handshakeState.apiParams,
+					destinationBytesMetrics.tcpMetrics.getBytesUp()+
+						destinationBytesMetrics.tcpMetrics.getBytesDown(),
+					destinationBytesMetrics.udpMetrics.getBytesUp()+
+						destinationBytesMetrics.udpMetrics.getBytesDown())
 			}
-
-			logFields["asn_dest_bytes"] = destBytes
-			logFields["asn_dest_bytes_up_tcp"] = destBytesUpTCP
-			logFields["asn_dest_bytes_down_tcp"] = destBytesDownTCP
-			logFields["asn_dest_bytes_up_udp"] = destBytesUpUDP
-			logFields["asn_dest_bytes_down_udp"] = destBytesDownUDP
 		}
 	}
 
@@ -4600,13 +4558,7 @@ func (sshClient *sshClient) setDestinationBytesMetrics() {
 
 	ASNs := p.Strings(parameters.DestinationBytesMetricsASNs)
 
-	// Merge in any legacy parameters.DestinationBytesMetricsASN
-	// configuration. Data for this target will be logged using the legacy
-	// log field format; see logTunnel. If an ASN is in _both_ configuration
-	// parameters, its data will be logged in both log field formats.
-	ASN := p.String(parameters.DestinationBytesMetricsASN)
-
-	if len(ASNs) == 0 && ASN == "" {
+	if len(ASNs) == 0 {
 		return
 	}
 
@@ -4617,10 +4569,6 @@ func (sshClient *sshClient) setDestinationBytesMetrics() {
 			sshClient.destinationBytesMetrics[ASN] = &protocolDestinationBytesMetrics{}
 		}
 	}
-
-	if ASN != "" {
-		sshClient.destinationBytesMetrics[ASN] = &protocolDestinationBytesMetrics{}
-	}
 }
 
 func (sshClient *sshClient) newDestinationBytesMetricsUpdater(

+ 8 - 2
vendor/github.com/Psiphon-Inc/uds-ipc/reader.go

@@ -351,10 +351,16 @@ func (r *Reader) handleConnection(conn net.Conn) {
 
 		// Set read deadline based on shutdown state.
 		// Potential errors are ignored because there is nothing to do with them.
-		deadline := time.Now()
+		var deadline time.Time
+
 		if !draining {
 			// Normal operation - set inactivity timeout to close idle connections.
-			deadline = deadline.Add(r.inactivityTimeout)
+			deadline = time.Now().Add(r.inactivityTimeout)
+		} else {
+			// Draining - add a short inactivity timeout to allow continued reading of
+			// data from the socket while draining (using time.Now() is too fast).
+			// nolint: mnd
+			deadline = time.Now().Add(time.Millisecond)
 		}
 
 		_ = conn.SetReadDeadline(deadline)

+ 1 - 1
vendor/modules.txt

@@ -24,7 +24,7 @@ github.com/Jigsaw-Code/outline-ss-server/service/metrics
 # github.com/Psiphon-Inc/rotate-safe-writer v0.0.0-20210303140923-464a7a37606e
 ## explicit
 github.com/Psiphon-Inc/rotate-safe-writer
-# github.com/Psiphon-Inc/uds-ipc v0.0.0-20251007150957-166e89b034be
+# github.com/Psiphon-Inc/uds-ipc v1.0.1
 ## explicit; go 1.24.0
 github.com/Psiphon-Inc/uds-ipc
 # github.com/Psiphon-Labs/bolt v0.0.0-20200624191537-23cedaef7ad7