From 9ef746d45ec422db239a276cf1a81deacb903830 Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:17:32 +0800 Subject: [PATCH] test(mtproto): cover single-backend DC aliases Follow up gramsrv PR #17 with boundary coverage and public configuration docs. Canonical-Telesrv: b4df3e72c24470e4ce09e5fc1d46e0c2944f6d3e Reviewed-Head: e7eb07b9eb520d23a02721d45c30996a6809ace6 Co-authored-by: onysd --- .env.example | 2 + docs/configuration.en.md | 3 +- docs/configuration.zh-CN.md | 3 +- internal/config/config.go | 10 +-- internal/config/config_test.go | 28 +++++++ internal/mtprotoedge/exchange_compat.go | 27 +++---- internal/mtprotoedge/exchange_test.go | 103 +++++++++++++++++------- internal/mtprotoedge/server.go | 20 ++--- 8 files changed, 127 insertions(+), 69 deletions(-) diff --git a/.env.example b/.env.example index 6aeed89e..999bc813 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,8 @@ TELESRV_LISTEN=0.0.0.0:2398 TELESRV_ADVERTISE_IP=127.0.0.1 TELESRV_DC=2 +# Single-backend default: accept any client wire DC label during key exchange. +TELESRV_STRICT_DC_CHECK=false TELESRV_DEV_AUTH_CODE=12345 TELESRV_AUTH_CODE_TTL=5m TELESRV_AUTH_CODE_MAX_ATTEMPTS=5 diff --git a/docs/configuration.en.md b/docs/configuration.en.md index d24b4776..a614d795 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -21,7 +21,8 @@ This document describes every setting loaded by `internal/config`. Defaults and | `TELESRV_LISTEN` | string / `0.0.0.0:2398` | MTProto TCP listen address. Must match the address/port reachable by patched clients. | | `TELESRV_ADVERTISE_IP` | string / `127.0.0.1` | Client-reachable server IP used by media/call fallbacks. The current static Desktop DC patch does not derive its MTProto endpoint from this value. | | `TELESRV_RSA_KEY` | path / `data/server_rsa.pem` | MTProto RSA private key. Generated when missing. Treat the file as a secret and keep it stable across restarts. | -| `TELESRV_DC` | int / `2` | Server DC ID. Must match patched client expectations and stored media/DC metadata. | +| `TELESRV_DC` | int / `2` | Canonical server DC ID used in server-originated configuration and media/DC metadata. It does not partition key-exchange state on the current single backend. | +| `TELESRV_STRICT_DC_CHECK` | bool / `false` | Default `false` accepts every wire int32 DC label for permanent and temporary key exchange. `true` requires permanent `dc_id == TELESRV_DC` and temporary `abs(dc_id) == TELESRV_DC`; it is only a diagnostic and does not provide multi-DC isolation. | | `TELESRV_WEBSOCKET_ENABLE` | bool / `true` | Enables MTProto-over-WebSocket demultiplexing on the MTProto listener. | | `TELESRV_WEBSOCKET_ALLOWED_ORIGINS` | list / `http://localhost:1234,http://127.0.0.1:1234` | Browser WebSocket origin allow-list. `*` is for temporary debugging only. | | `TELESRV_MTPROTO_MAX_CONNECTIONS` | int / `200000` | Global physical connection admission limit. Negative disables this gate. | diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index 51085e06..94c5c150 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -21,7 +21,8 @@ | `TELESRV_LISTEN` | string / `0.0.0.0:2398` | MTProto TCP 监听地址,必须与 patched 客户端可达地址/端口一致。 | | `TELESRV_ADVERTISE_IP` | string / `127.0.0.1` | 媒体、通话等回退路径使用的客户端可达 IP;当前 TDesktop 静态 DC patch 不从这里获取 MTProto 地址。 | | `TELESRV_RSA_KEY` | path / `data/server_rsa.pem` | MTProto RSA 私钥;缺失时自动生成。属于敏感文件,重启和升级间必须稳定保存。 | -| `TELESRV_DC` | int / `2` | 服务端 DC ID,必须与客户端 patch 及媒体/DC 元数据一致。 | +| `TELESRV_DC` | int / `2` | 服务端输出配置及媒体/DC 元数据使用的规范 DC ID;当前单后端不会按它分区密钥交换状态。 | +| `TELESRV_STRICT_DC_CHECK` | bool / `false` | 默认 `false`,永久与临时密钥交换接受任意 wire int32 DC 标签。设为 `true` 时永久标签必须等于 `TELESRV_DC`、临时标签绝对值必须等于 `TELESRV_DC`;它仅是诊断开关,不提供多 DC 隔离。 | | `TELESRV_WEBSOCKET_ENABLE` | bool / `true` | 在 MTProto 监听端口启用 MTProto-over-WebSocket 分流。 | | `TELESRV_WEBSOCKET_ALLOWED_ORIGINS` | list / `http://localhost:1234,http://127.0.0.1:1234` | 浏览器 WebSocket origin 白名单;`*` 只用于临时调试。 | | `TELESRV_MTPROTO_MAX_CONNECTIONS` | int / `200000` | 全局物理连接 admission 上限;负数关闭该门禁。 | diff --git a/internal/config/config.go b/internal/config/config.go index df46392b..ff79edf9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,12 +31,10 @@ type Config struct { RSAKeyPath string // DC 是本 server 的 DC ID。 DC int - // StrictDCCheck turns on exact DC-ID validation for the permanent-key - // exchange (default off = lenient). See mtprotoedge.Options.StrictDC doc - // for the full rationale: telesrv is always a single physical backend, but - // many client forks alias dc_id 1..5 to it, so a mismatched client-chosen - // dc_id is expected, not an attack — strict mode exists only for a - // hypothetical future real multi-DC deployment. + // StrictDCCheck enables the default-off key-exchange DC-label diagnostic. + // The normal single-backend mode accepts every wire int32 label without + // partitioning auth keys, sessions, or business state. See + // mtprotoedge.Options.StrictDC for the optional strict behavior. StrictDCCheck bool // MTProtoMaxConnections / PerIP 覆盖 raw Accept、codec sniff、握手到认证 session // 的完整物理连接生命周期;负数关闭对应 admission 上限。 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index dd16f92f..78a2e075 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -49,6 +49,34 @@ func TestLoadUsesExplicitAdvertiseIP(t *testing.T) { } } +func TestLoadStrictDCCheck(t *testing.T) { + t.Run("defaults off", func(t *testing.T) { + disableDefaultConfigFile(t) + t.Setenv("TELESRV_STRICT_DC_CHECK", "") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.StrictDCCheck { + t.Fatal("StrictDCCheck = true, want default false") + } + }) + + t.Run("explicitly enabled", func(t *testing.T) { + disableDefaultConfigFile(t) + t.Setenv("TELESRV_STRICT_DC_CHECK", "true") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if !cfg.StrictDCCheck { + t.Fatal("StrictDCCheck = false, want true") + } + }) +} + func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) { disableDefaultConfigFile(t) t.Setenv("TELESRV_MTPROTO_MAX_CONNECTIONS", "12345") diff --git a/internal/mtprotoedge/exchange_compat.go b/internal/mtprotoedge/exchange_compat.go index cf217e16..56db3218 100644 --- a/internal/mtprotoedge/exchange_compat.go +++ b/internal/mtprotoedge/exchange_compat.go @@ -25,14 +25,12 @@ import ( // runServerExchange is a gotd server exchange compatibility shim. // -// DrKLO Android marks media temporary auth-key exchange with a negative DC in -// p_q_inner_data_temp_dc (for example DC 2 -> -2). gotd v0.158.0 validates this -// field by exact equality and rejects that legitimate media-temp path. The -// temp-key DC check accepts any value whose absolute value matches this -// server DC. The permanent-key check is lenient by default too (see -// Options.StrictDC doc) — self-hosted single-server deployments commonly -// have clients that alias dc_id 1..5 to the one backend, so a client-chosen -// dc_id that isn't our configured DC is expected, not an error. +// In the default single-backend mode, p_q_inner_data_dc and +// p_q_inner_data_temp_dc carry client routing labels only: every int32 value is +// admitted and the label is not persisted or used for key/session identity. +// This also covers DrKLO Android's negative media-temp labels. StrictDC retains +// exact permanent / absolute-value temporary validation as an explicit, +// default-off diagnostic for a future real multi-DC deployment. func (s *Server) runServerExchange(ctx context.Context, conn transport.Conn) (exchange.ServerExchangeResult, error) { ex := serverExchangeCompat{ conn: conn, @@ -351,14 +349,7 @@ func (s serverExchangeCompat) validatePQInnerDataDC(d mt.PQInnerDataClass) error case *mt.PQInnerDataDC: if innerDataDC.DC != s.dc { if !s.strictDC { - // Lenient by default (Options.StrictDC doc has the full - // rationale): telesrv is a single physical backend, and - // self-hosted client forks commonly alias dc_id 1..5 to this - // one server, so a client-chosen dc_id that isn't our - // configured DC is expected, not an error. dc_id plays no - // role in key derivation, so accepting it doesn't weaken the - // exchange. - s.log.Debug("Accepted permanent auth key DC mismatch (lenient mode)", + s.log.Debug("Accepted permanent auth key DC alias", zap.Int("server_dc", s.dc), zap.Int("client_dc", innerDataDC.DC)) return nil @@ -369,8 +360,8 @@ func (s serverExchangeCompat) validatePQInnerDataDC(d mt.PQInnerDataClass) error if !sameDCByAbs(innerDataDC.DC, s.dc) && s.strictDC { return wrongDCError(s.dc, innerDataDC.DC) } - if innerDataDC.DC < 0 { - s.log.Warn("Accepted Android media temp auth key negative DC", + if !sameDCByAbs(innerDataDC.DC, s.dc) { + s.log.Debug("Accepted temporary auth key DC alias", zap.Int("server_dc", s.dc), zap.Int("client_dc", innerDataDC.DC), zap.Int("expires_in", innerDataDC.ExpiresIn)) diff --git a/internal/mtprotoedge/exchange_test.go b/internal/mtprotoedge/exchange_test.go index e15650f3..d98a96db 100644 --- a/internal/mtprotoedge/exchange_test.go +++ b/internal/mtprotoedge/exchange_test.go @@ -7,6 +7,7 @@ import ( "crypto/rsa" "encoding/binary" "errors" + "fmt" "math/big" "net" "testing" @@ -366,41 +367,85 @@ func TestKeyExchangeAcceptsAndroidMediaTempNegativeDC(t *testing.T) { } } -func TestKeyExchangeRejectsWrongNegativeTempDCWhenStrict(t *testing.T) { - ex := serverExchangeCompat{dc: 2, strictDC: true, log: zaptest.NewLogger(t)} - err := ex.validatePQInnerDataDC(&mt.PQInnerDataTempDC{DC: -3}) - var exErr *exchange.ServerExchangeError - if !errors.As(err, &exErr) { - t.Fatalf("err = %T %v, want ServerExchangeError", err, err) - } - if exErr.Code != codec.CodeWrongDC { - t.Fatalf("error code = %d, want %d", exErr.Code, codec.CodeWrongDC) - } -} - -// TestKeyExchangeAcceptsMismatchedDCByDefault asserts that, in the default -// lenient mode, neither permanent nor temp key exchange requires dc_id to -// equal the server's configured DC. telesrv is always a single physical -// backend; self-hosted client forks commonly alias dc_id 1..5 to it (see -// Options.StrictDC doc), so a mismatched client-chosen dc_id must not be -// rejected — doing so previously broke every account whose client picked a -// starting dc_id other than the server's. -func TestKeyExchangeAcceptsMismatchedDCByDefault(t *testing.T) { +func TestKeyExchangeAcceptsAnyDCLabelByDefault(t *testing.T) { ex := serverExchangeCompat{dc: 2, log: zaptest.NewLogger(t)} - if err := ex.validatePQInnerDataDC(&mt.PQInnerDataDC{DC: 3}); err != nil { - t.Fatalf("permanent DC mismatch: err = %v, want nil (lenient by default)", err) + labels := []int{ + 2, // canonical + 3, // another production DC + 0, // no conventional DC mapping + -2, // Android media-temp convention + 10002, // test-environment style label + -10002, // negative test-environment style label + -1 << 31, + 1<<31 - 1, } - if err := ex.validatePQInnerDataDC(&mt.PQInnerDataTempDC{DC: -3}); err != nil { - t.Fatalf("temp DC mismatch: err = %v, want nil (lenient by default)", err) + + for _, label := range labels { + t.Run(fmt.Sprintf("permanent_%d", label), func(t *testing.T) { + if err := ex.validatePQInnerDataDC(&mt.PQInnerDataDC{DC: label}); err != nil { + t.Fatalf("validate permanent DC label %d: %v", label, err) + } + }) + t.Run(fmt.Sprintf("temporary_%d", label), func(t *testing.T) { + if err := ex.validatePQInnerDataDC(&mt.PQInnerDataTempDC{DC: label, ExpiresIn: 60}); err != nil { + t.Fatalf("validate temporary DC label %d: %v", label, err) + } + }) } } -// TestKeyExchangeRejectsMismatchedPermanentDCWhenStrict asserts that -// strictDC=true still enforces exact DC-ID equality for permanent-key -// exchange (kept for a hypothetical future real multi-DC deployment). -func TestKeyExchangeRejectsMismatchedPermanentDCWhenStrict(t *testing.T) { +func TestKeyExchangePersistsArbitraryPermanentDCLabelByDefault(t *testing.T) { + const clientDC = 10002 + keys := memory.NewAuthKeyStore() + addr, pub, _ := startTestServer(t, Options{DC: 2, AuthKeys: keys}) + + _, auth, _ := dialHandshake(t, addr, clientDC, pub) + saved, found, err := keys.Get(context.Background(), auth.AuthKey.ID) + if err != nil { + t.Fatalf("get persisted auth key: %v", err) + } + if !found { + t.Fatalf("auth key %x was not persisted", auth.AuthKey.ID) + } + if saved.Value != [256]byte(auth.AuthKey.Value) { + t.Fatal("persisted auth key value mismatch") + } +} + +func TestKeyExchangeStrictDCValidation(t *testing.T) { ex := serverExchangeCompat{dc: 2, strictDC: true, log: zaptest.NewLogger(t)} - err := ex.validatePQInnerDataDC(&mt.PQInnerDataDC{DC: 3}) + tests := []struct { + name string + data mt.PQInnerDataClass + wantErr bool + }{ + {name: "permanent exact", data: &mt.PQInnerDataDC{DC: 2}}, + {name: "permanent other", data: &mt.PQInnerDataDC{DC: 3}, wantErr: true}, + {name: "permanent zero", data: &mt.PQInnerDataDC{DC: 0}, wantErr: true}, + {name: "temporary positive exact", data: &mt.PQInnerDataTempDC{DC: 2}}, + {name: "temporary negative exact", data: &mt.PQInnerDataTempDC{DC: -2}}, + {name: "temporary other", data: &mt.PQInnerDataTempDC{DC: 3}, wantErr: true}, + {name: "temporary negative other", data: &mt.PQInnerDataTempDC{DC: -3}, wantErr: true}, + {name: "temporary test label", data: &mt.PQInnerDataTempDC{DC: 10002}, wantErr: true}, + {name: "temporary min int32", data: &mt.PQInnerDataTempDC{DC: -1 << 31}, wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ex.validatePQInnerDataDC(test.data) + if !test.wantErr { + if err != nil { + t.Fatalf("validate: %v", err) + } + return + } + assertWrongDCError(t, err) + }) + } +} + +func assertWrongDCError(t *testing.T, err error) { + t.Helper() var exErr *exchange.ServerExchangeError if !errors.As(err, &exErr) { t.Fatalf("err = %T %v, want ServerExchangeError", err, err) diff --git a/internal/mtprotoedge/server.go b/internal/mtprotoedge/server.go index a067dcac..4524b144 100644 --- a/internal/mtprotoedge/server.go +++ b/internal/mtprotoedge/server.go @@ -303,20 +303,12 @@ type Options struct { // DC 是本 server 的 DC ID。默认 2。 DC int - // StrictDC turns on exact DC-ID validation for the permanent-key exchange - // (default off = lenient). telesrv is always a single physical backend — - // there is no real multi-DC federation behind it — but self-hosted client - // forks commonly run in "single-server backend" mode, where dc_id 1..5 all - // alias to this one server so that any old data referencing a specific - // dc_id still resolves correctly. When a client adds a new local account it - // picks its own starting dc_id (its usual multi-DC load-spreading - // behavior, unrelated to which physical server it's actually talking to) - // — that choice is not guaranteed to equal our configured DC. Strict - // validation would reject those accounts with "-444 wrong dc_id" even - // though they are connecting to the right (and only) server; dc_id is a - // client-side routing label here, not part of key derivation, so - // accepting the mismatch does not weaken the exchange. The switch exists - // for a hypothetical future real multi-DC deployment. + // StrictDC enables DC-label validation during key exchange. It is false by + // default: this single physical backend accepts every wire int32 label for + // permanent and temporary keys, and the label never changes auth-key + // persistence, session identity, or business state. When enabled, + // permanent labels must equal DC and temporary labels may equal +/-DC. + // This diagnostic switch does not itself provide multi-DC isolation. StrictDC bool // RSAKey 是 server RSA 私钥,用于密钥交换。nil 时无法完成握手。 RSAKey *rsa.PrivateKey