diff --git a/internal/tuic/server.go b/internal/tuic/server.go index 1245b335c..4c0ff5e05 100644 --- a/internal/tuic/server.go +++ b/internal/tuic/server.go @@ -91,6 +91,11 @@ func NewServer(inst Instance, relay *SocksRelay) (*Server, error) { if authTimeout <= 0 { authTimeout = 3 } + // A second count past a day only wraps the Duration below; rows stored before the save + // path bounded it fail here instead of panicking the listener on its first handshake. + if maxIdle > MaxTimeoutSeconds || authTimeout > MaxTimeoutSeconds { + return nil, fmt.Errorf("tuic: max_idle_time and authentication_timeout must not exceed %d seconds", MaxTimeoutSeconds) + } maxUdpSize := inst.MaxUdpRelayPacketSize if maxUdpSize <= 0 { maxUdpSize = 1500 @@ -636,6 +641,9 @@ type packetReassembler struct { packets map[packetFragmentKey]*packetReassembly } +// MaxTimeoutSeconds bounds the second-count settings, far above any useful timeout. +const MaxTimeoutSeconds = 86400 + const ( maxSafeUdpRelayPacketSize = maxSocksUdpDatagramSize - 262 maxLegacyUdpRelayPacketSize = maxSocksUdpDatagramSize diff --git a/internal/tuic/server_settings_test.go b/internal/tuic/server_settings_test.go index f5f7eb3b8..c1d6f1458 100644 --- a/internal/tuic/server_settings_test.go +++ b/internal/tuic/server_settings_test.go @@ -127,3 +127,19 @@ func TestEnsureStartupFailureHonorsInboundLogThreshold(t *testing.T) { }) } } + +// Seconds turn into a Duration by multiplication; a pushed value past the int64 range +// wrapped negative and the QUIC listener panicked on the first ClientHello, every start. +func TestNewServerRefusesTimeoutsPastADay(t *testing.T) { + cert, key := generateTestCert(t) + for name, inst := range map[string]Instance{ + "max_idle_time": {MaxIdleTime: 1 << 40}, + "authentication_timeout": {AuthenticationTimeout: MaxTimeoutSeconds + 1}, + } { + inst.Listen, inst.Certificate, inst.PrivateKey = "127.0.0.1", string(cert), string(key) + if s, err := NewServer(inst, &SocksRelay{Addr: "127.0.0.1:1"}); err == nil { + _ = s.Close() + t.Errorf("%s past a day was accepted", name) + } + } +} diff --git a/internal/web/service/inbound_tuic_test.go b/internal/web/service/inbound_tuic_test.go index 4b076be23..84d1b06c5 100644 --- a/internal/web/service/inbound_tuic_test.go +++ b/internal/web/service/inbound_tuic_test.go @@ -359,6 +359,12 @@ func TestNormalizeTuicSettingsCanonicalizesCongestionAndPacketLimit(t *testing.T if err := normalizeTuicSettings(ib); err == nil { t.Fatal("unsupported congestion controller was accepted") } + for _, settings := range []string{`{"max_idle_time":2199023255552}`, `{"server":{"authentication_timeout":86401}}`} { + ib.Settings = settings + if err := normalizeTuicSettings(ib); err == nil { + t.Fatalf("a timeout past a day was accepted: %s", settings) + } + } } func TestTuicRelayIdentitySurvivesNativeEligibilityChanges(t *testing.T) { diff --git a/internal/web/service/tuic_settings.go b/internal/web/service/tuic_settings.go index 4d567f558..16ced47c0 100644 --- a/internal/web/service/tuic_settings.go +++ b/internal/web/service/tuic_settings.go @@ -90,6 +90,19 @@ func normalizeTuicSettingsBlock(settings map[string]json.RawMessage) error { encoded, _ := json.Marshal(value) settings["udp_relay_mode"] = encoded } + for _, key := range []string{"max_idle_time", "authentication_timeout"} { + raw, ok := settings[key] + if !ok { + continue + } + var value int64 + if err := json.Unmarshal(raw, &value); err != nil { + return fmt.Errorf("TUIC %s must be an integer: %w", key, err) + } + if value > tuic.MaxTimeoutSeconds { + return fmt.Errorf("TUIC %s must not exceed %d seconds", key, tuic.MaxTimeoutSeconds) + } + } if raw, ok := settings["max_udp_relay_packet_size"]; ok { var value int if err := json.Unmarshal(raw, &value); err != nil {