Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions internal/config/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ func normalizeConfigPresetName(name string) string {
switch name {
case "tcp", "tcp-survival", "tcp-survive":
return "tcp-survival"
case "high-latency", "highlatency", "latency", "mobile":
return "high-latency"
default:
return name
}
Expand All @@ -33,11 +35,11 @@ func isKnownConfigPreset(name string) bool {

// Server preset validation remains independent from the client preset set.
// Keep these helpers here because server.go uses them during final validation.
const serverConfigPresetNames = "default, speed, survival, tcp-survival"
const serverConfigPresetNames = "default, speed, survival, tcp-survival, high-latency"

func isKnownServerConfigPreset(name string) bool {
switch normalizeConfigPresetName(name) {
case "default", "speed", "survival", "tcp-survival":
case "default", "speed", "survival", "tcp-survival", "high-latency":
return true
default:
return false
Expand Down Expand Up @@ -89,7 +91,9 @@ func applyServerConfigPreset(cfg *ServerConfig, isDefined configKeyDefinedFunc)
return nil
}
preset := normalizeConfigPresetName(cfg.ConfigPreset)
if !isKnownConfigPreset(preset) {
// Validated against the server set, which carries presets the client has no
// use for.
if !isKnownServerConfigPreset(preset) {
return invalidConfigPresetError(preset)
}
cfg.ConfigPreset = preset
Expand All @@ -101,10 +105,49 @@ func applyServerConfigPreset(cfg *ServerConfig, isDefined configKeyDefinedFunc)
applyServerSurvivalPreset(cfg, isDefined)
case "tcp-survival":
applyServerTCPSurvivalPreset(cfg, isDefined)
case "high-latency":
applyServerHighLatencyPreset(cfg, isDefined)
}
return nil
}

// applyServerHighLatencyPreset targets mobile carriers where the base RTT is
// several hundred milliseconds. The defaults assume an ACK can come back
// quickly: a 0.6s retransmit timeout on a 400ms path with 100ms of jitter
// expires while the ACK is still in flight, so the server resends packets that
// were never lost, and the duplicates add the delay that expires the next
// timer. A 1000 packet window also cannot keep such a path full.
//
// These values come from a deployed Hamrahe Avval server that was hand tuned
// against the problem before this preset existed.
func applyServerHighLatencyPreset(cfg *ServerConfig, isDefined configKeyDefinedFunc) {
setServerFloat(isDefined, "ARQ_INITIAL_RTO_SECONDS", &cfg.ARQInitialRTOSeconds, 1.0)
setServerFloat(isDefined, "ARQ_MAX_RTO_SECONDS", &cfg.ARQMaxRTOSeconds, 4.0)
// Reporting a gap before the reordering window has drained asks for data
// that is merely late, which on a long path is most of it.
setServerFloat(isDefined, "ARQ_DATA_NACK_INITIAL_DELAY_SECONDS", &cfg.ARQDataNackInitialDelaySeconds, 0.3)
setServerFloat(isDefined, "ARQ_DATA_NACK_REPEAT_SECONDS", &cfg.ARQDataNackRepeatSeconds, 0.8)
setServerInt(isDefined, "ARQ_DATA_NACK_MAX_GAP", &cfg.ARQDataNackMaxGap, 64)
// Bandwidth times delay: the window and batch have to cover a full round
// trip of data or the link idles waiting for ACKs.
setServerInt(isDefined, "ARQ_WINDOW_SIZE", &cfg.ARQWindowSize, 4096)
setServerInt(isDefined, "MAX_PACKETS_PER_BATCH", &cfg.MaxPacketsPerBatch, 32)
setServerInt(isDefined, "MAX_DNS_RESPONSE_BYTES", &cfg.MaxDNSResponseBytes, 65535)
// A long path keeps far more requests in flight at once, so the ingress
// side needs room to hold them rather than shedding them.
setServerInt(isDefined, "UDP_READERS", &cfg.UDPReaders, 16)
setServerInt(isDefined, "DNS_REQUEST_WORKERS", &cfg.DNSRequestWorkers, 16)
setServerInt(isDefined, "MAX_CONCURRENT_REQUESTS", &cfg.MaxConcurrentRequests, 32768)
setServerInt(isDefined, "MAX_INGRESS_QUEUE_BYTES", &cfg.MaxIngressQueueBytes, 128*1024*1024)
setServerInt(isDefined, "SOCKET_BUFFER_SIZE", &cfg.SocketBufferSize, 32*1024*1024)
setServerInt(isDefined, "DEFERRED_SESSION_WORKERS", &cfg.DeferredSessionWorkers, 16)
setServerInt(isDefined, "DEFERRED_SESSION_QUEUE_LIMIT", &cfg.DeferredSessionQueueLimit, 16384)
// SESSION_TIMEOUT_SECONDS is deliberately left at the default. Raising it
// keeps abandoned sessions holding slots in a fixed size table, and a client
// that reconnects mid stall then meets SESSION_BUSY, which makes it
// reconnect again.
}

func invalidConfigPresetError(name string) error {
return presetError{name: name}
}
Expand Down
45 changes: 45 additions & 0 deletions internal/config/presets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package config

import "testing"

// The high-latency preset exists because the stock ARQ timings assume a short
// round trip. It is server-only, and explicit keys must still win over it.
func TestServerHighLatencyPreset(t *testing.T) {
cfg := &ServerConfig{ConfigPreset: "high-latency"}
if err := applyServerConfigPreset(cfg, func(string) bool { return false }); err != nil {
t.Fatal(err)
}
if cfg.ARQInitialRTOSeconds != 1.0 || cfg.ARQMaxRTOSeconds != 4.0 {
t.Fatalf("retransmit timeout was not widened for a long path: %v/%v",
cfg.ARQInitialRTOSeconds, cfg.ARQMaxRTOSeconds)
}
if cfg.ARQWindowSize != 4096 || cfg.MaxPacketsPerBatch != 32 {
t.Fatalf("window and batch must cover a full round trip: %d/%d",
cfg.ARQWindowSize, cfg.MaxPacketsPerBatch)
}
if cfg.SessionTimeoutSecs != 0 {
t.Fatalf("the preset must not extend session retention: %v", cfg.SessionTimeoutSecs)
}

// An operator's own value wins.
explicit := &ServerConfig{ConfigPreset: "high-latency", ARQInitialRTOSeconds: 2.0}
if err := applyServerConfigPreset(explicit, func(key string) bool {
return key == "ARQ_INITIAL_RTO_SECONDS"
}); err != nil {
t.Fatal(err)
}
if explicit.ARQInitialRTOSeconds != 2.0 {
t.Fatalf("explicit key was overwritten by the preset: %v", explicit.ARQInitialRTOSeconds)
}

// Aliases resolve, and the client must not accept a server-only preset.
if normalizeConfigPresetName("HIGH_LATENCY") != "high-latency" {
t.Fatal("alias did not normalize")
}
if isKnownConfigPreset("high-latency") {
t.Fatal("high-latency must not be offered to clients")
}
if !isKnownServerConfigPreset("high-latency") {
t.Fatal("server must accept high-latency")
}
}
14 changes: 14 additions & 0 deletions server_config.high-latency.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# ==============================================================================
# CottenDns Go Server — high-latency preset
# ==============================================================================
# For mobile carriers where the base round trip is several hundred
# milliseconds. The stock timings assume an ACK returns quickly, so on such a
# path the server retransmits packets that were never lost and the duplicates
# add the delay that expires the next timer.
#
# Explicit keys always win over the preset, so override anything below freely.
# ==============================================================================

CONFIG_PRESET = "high-latency"
DOMAIN = ["REPLACE_WITH_YOUR_DELEGATED_DOMAIN"]
ENCRYPTION_KEY_FILE = "encrypt_key.txt"
Loading