From 219e584c163398fffe691ab40d0bdbfbb9c89b80 Mon Sep 17 00:00:00 2001 From: tajirax Date: Tue, 4 Aug 2026 12:10:50 +0330 Subject: [PATCH] Add a high-latency server preset Neither existing preset addresses round trip time. "speed" shortens the retransmit timeout to 0.5s and "survival" leaves it at the 0.6s default while concentrating on FEC, so on a mobile carrier with a 400ms base RTT and 100ms of jitter the server resends packets whose ACKs are still in flight. The duplicates add the delay that expires the next timer, and a 1000 packet window cannot keep such a path full in the first place. The values come from a deployed Hamrahe Avval server that was hand tuned against exactly this before a preset existed: a 1.0s initial and 4.0s maximum RTO, NACK delays that wait for the reordering window, a 4096 packet window, 32 packet batches, and ingress sized for the requests a long path keeps in flight. SESSION_TIMEOUT_SECONDS is deliberately not raised, because holding abandoned sessions in a fixed size table is what makes a reconnecting client meet SESSION_BUSY and reconnect again. The preset is server-only; applyServerConfigPreset now validates against the server set, which it already had but did not use. Co-Authored-By: Claude Opus 5 --- internal/config/presets.go | 49 +++++++++++++++++++++++++++++++-- internal/config/presets_test.go | 45 ++++++++++++++++++++++++++++++ server_config.high-latency.toml | 14 ++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 internal/config/presets_test.go create mode 100644 server_config.high-latency.toml diff --git a/internal/config/presets.go b/internal/config/presets.go index 8d446db..e016afd 100644 --- a/internal/config/presets.go +++ b/internal/config/presets.go @@ -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 } @@ -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 @@ -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 @@ -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} } diff --git a/internal/config/presets_test.go b/internal/config/presets_test.go new file mode 100644 index 0000000..b64c455 --- /dev/null +++ b/internal/config/presets_test.go @@ -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") + } +} diff --git a/server_config.high-latency.toml b/server_config.high-latency.toml new file mode 100644 index 0000000..0490a91 --- /dev/null +++ b/server_config.high-latency.toml @@ -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"