From 4cd055144d2a32e677f52441299f0bba18228319 Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:54:04 +0800 Subject: [PATCH] feat(config): sync make default country configurable --- .env.example | 2 ++ cmd/telesrv/main.go | 2 ++ docs/configuration.en.md | 1 + docs/configuration.zh-CN.md | 1 + internal/compat/tdesktop/config.go | 10 ------- internal/config/config.go | 27 +++++++++++++++++- internal/config/config_test.go | 41 ++++++++++++++++++++++++++++ internal/rpc/help.go | 6 +++- internal/rpc/router.go | 1 + internal/rpc/router_dispatch_test.go | 25 +++++++++++++++++ 10 files changed, 104 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index e78d074a..770c50b7 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 +# ISO 3166-1 alpha-2 code returned by help.getNearestDc. CN preselects +86. +TELESRV_DEFAULT_COUNTRY_CODE=CN # Single-backend default: accept any client wire DC label during key exchange. TELESRV_STRICT_DC_CHECK=false TELESRV_DEV_AUTH_CODE=12345 diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index 1c0d9beb..257f4fd5 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -312,6 +312,7 @@ func run(logger *zap.Logger) error { logger.Info("telesrv 启动", zap.String("listen", cfg.ListenAddr), zap.Int("dc", cfg.DC), + zap.String("default_country_code", cfg.DefaultCountryCode), zap.String("advertise", net.JoinHostPort(cfg.AdvertiseIP, portStr)), zap.Int("tl_layer", tg.Layer), zap.String("git_commit", buildMeta.Commit), @@ -861,6 +862,7 @@ func run(logger *zap.Logger) error { updatesService := updates.NewService(updateStateStore, updateEventStore, updates.WithLogger(logger.Named("app").Named("updates"))) router := rpc.New(rpc.Config{ DC: cfg.DC, + DefaultCountryCode: cfg.DefaultCountryCode, IP: cfg.AdvertiseIP, Port: port, OutboundPushTimeout: cfg.OutboundPushTimeout, diff --git a/docs/configuration.en.md b/docs/configuration.en.md index b97a8da5..5c7a5196 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -22,6 +22,7 @@ This document describes every setting loaded by `internal/config`. Defaults and | `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` | 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_DEFAULT_COUNTRY_CODE` | ISO alpha-2 / `CN` | Country returned by `help.getNearestDc` for login-page preselection. Clients map `CN` to calling code `+86`, `US` to `+1`, and so on. Input is trimmed, uppercased, and validated as a country or autonomous area; malformed or unknown values fail startup. | | `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. | diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index 13005591..1b4684b2 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -22,6 +22,7 @@ | `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 元数据使用的规范 DC ID;当前单后端不会按它分区密钥交换状态。 | +| `TELESRV_DEFAULT_COUNTRY_CODE` | ISO alpha-2 / `CN` | `help.getNearestDc` 返回的登录页默认国家。客户端把 `CN` 映射为国际区号 `+86`、`US` 映射为 `+1`。输入会 trim、转大写并校验为国家或自治地区;格式错误或未知值会让启动失败。 | | `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 白名单;`*` 只用于临时调试。 | diff --git a/internal/compat/tdesktop/config.go b/internal/compat/tdesktop/config.go index e551cf3b..c323f005 100644 --- a/internal/compat/tdesktop/config.go +++ b/internal/compat/tdesktop/config.go @@ -58,13 +58,3 @@ func BuildConfig(dc int, ip string, port int, now time.Time, publicBaseURL strin config.SetReactionsDefault(&tg.ReactionEmoji{Emoticon: DefaultReactionEmoticon}) return config } - -// NearestDC 构造 help.getNearestDc 返回值。 -func NearestDC(dc int) *tg.NearestDC { - return &tg.NearestDC{ - // 默认国家=中国:DrKLO/TDesktop 登录页据此预选区号(+86)。 - Country: "CN", - ThisDC: dc, - NearestDC: dc, - } -} diff --git a/internal/config/config.go b/internal/config/config.go index 5b62b9d7..e41b6c3a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,10 +11,15 @@ import ( "strings" "time" + "golang.org/x/text/language" + "telesrv/internal/links" ) -const defaultConfigFile = ".env" +const ( + defaultConfigFile = ".env" + defaultCountryCode = "CN" +) // Config 是 telesrv 的运行配置。 type Config struct { @@ -31,6 +36,9 @@ type Config struct { RSAKeyPath string // DC 是本 server 的 DC ID。 DC int + // DefaultCountryCode 是 help.getNearestDc 返回的 ISO 3166-1 alpha-2 国家码。 + // 客户端登录页据此预选国家和国际电话区号;例如 CN 对应 +86。 + DefaultCountryCode string // 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 @@ -461,6 +469,10 @@ func Load() (Config, error) { if err != nil { return Config{}, fmt.Errorf("TELESRV_PUBLIC_APP_NAME: %w", err) } + countryCode, err := normalizeDefaultCountryCode(envOr("TELESRV_DEFAULT_COUNTRY_CODE", defaultCountryCode)) + if err != nil { + return Config{}, fmt.Errorf("TELESRV_DEFAULT_COUNTRY_CODE: %w", err) + } cfg := Config{ ListenAddr: envOr("TELESRV_LISTEN", "0.0.0.0:2398"), @@ -475,6 +487,7 @@ func Load() (Config, error) { AdvertiseIP: envOr("TELESRV_ADVERTISE_IP", "127.0.0.1"), RSAKeyPath: envOr("TELESRV_RSA_KEY", "data/server_rsa.pem"), DC: envIntOr("TELESRV_DC", 2), + DefaultCountryCode: countryCode, StrictDCCheck: envBoolOr("TELESRV_STRICT_DC_CHECK", false), MTProtoMaxConnections: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS", 200000), MTProtoMaxConnectionsPerIP: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS_PER_IP", 4096), @@ -690,6 +703,18 @@ func Load() (Config, error) { return cfg, nil } +func normalizeDefaultCountryCode(raw string) (string, error) { + code := strings.ToUpper(strings.TrimSpace(raw)) + if len(code) != 2 || code[0] < 'A' || code[0] > 'Z' || code[1] < 'A' || code[1] > 'Z' { + return "", fmt.Errorf("must be a two-letter ISO 3166-1 alpha-2 code") + } + region, err := language.ParseRegion(code) + if err != nil || !region.IsCountry() { + return "", fmt.Errorf("must identify a country or autonomous area") + } + return region.String(), nil +} + func validateTelegramLoginConfig(cfg Config) error { if !cfg.TelegramLoginEnabled { return nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bbbd093d..4b760a6f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -68,6 +68,47 @@ func TestLoadUsesExplicitAdvertiseIP(t *testing.T) { } } +func TestLoadDefaultCountryCode(t *testing.T) { + t.Run("default", func(t *testing.T) { + disableDefaultConfigFile(t) + t.Setenv("TELESRV_DEFAULT_COUNTRY_CODE", "") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.DefaultCountryCode != "CN" { + t.Fatalf("DefaultCountryCode = %q, want CN", cfg.DefaultCountryCode) + } + }) + + t.Run("normalized override", func(t *testing.T) { + disableDefaultConfigFile(t) + t.Setenv("TELESRV_DEFAULT_COUNTRY_CODE", " us ") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.DefaultCountryCode != "US" { + t.Fatalf("DefaultCountryCode = %q, want US", cfg.DefaultCountryCode) + } + }) +} + +func TestLoadRejectsInvalidDefaultCountryCode(t *testing.T) { + for _, value := range []string{"+86", "CHN", "C1", "中", "ZZ"} { + t.Run(value, func(t *testing.T) { + disableDefaultConfigFile(t) + t.Setenv("TELESRV_DEFAULT_COUNTRY_CODE", value) + + if _, err := Load(); err == nil { + t.Fatalf("Load accepted TELESRV_DEFAULT_COUNTRY_CODE=%q", value) + } + }) + } +} + func TestLoadStrictDCCheck(t *testing.T) { t.Run("defaults off", func(t *testing.T) { disableDefaultConfigFile(t) diff --git a/internal/rpc/help.go b/internal/rpc/help.go index c4021efc..22baf36a 100644 --- a/internal/rpc/help.go +++ b/internal/rpc/help.go @@ -19,7 +19,11 @@ func (r *Router) registerHelp(d *tlprofile.Dispatcher) { return r.onHelpGetConfig(ctx) }) registerRPC[*tg.HelpGetNearestDCRequest](d, tlprofile.SemanticMethodHelpGetNearestDC, func(ctx context.Context, layerRequest *tg.HelpGetNearestDCRequest) (any, error) { - return tdesktop.NearestDC(r.cfg.DC), nil + return &tg.NearestDC{ + Country: r.cfg.DefaultCountryCode, + ThisDC: r.cfg.DC, + NearestDC: r.cfg.DC, + }, nil }) registerRPC[*tg.HelpGetInviteTextRequest](d, tlprofile.SemanticMethodHelpGetInviteText, func(ctx context.Context, layerRequest *tg.HelpGetInviteTextRequest) (any, error) { return &tg.HelpInviteText{Message: "Join me on " + branding.ProductName + "."}, nil diff --git a/internal/rpc/router.go b/internal/rpc/router.go index 0575646f..f4d6207b 100644 --- a/internal/rpc/router.go +++ b/internal/rpc/router.go @@ -52,6 +52,7 @@ var ( // Config 是 Router 所需的服务端信息。 type Config struct { DC int + DefaultCountryCode string // help.getNearestDc 返回的 ISO 3166-1 alpha-2 国家码。 IP string // 对外公布的 DC IP(写入 DCOptions) Port int // 对外公布的 DC 端口 InstanceID string // 进程内唯一标识,用于跨实例 ephemeral push 去重。 diff --git a/internal/rpc/router_dispatch_test.go b/internal/rpc/router_dispatch_test.go index 097c829c..cc95d577 100644 --- a/internal/rpc/router_dispatch_test.go +++ b/internal/rpc/router_dispatch_test.go @@ -71,6 +71,31 @@ func TestDispatchUnwrapsWrappers(t *testing.T) { } } +func TestDispatchNearestDCUsesConfiguredDefaultCountryCode(t *testing.T) { + r := New(Config{ + DC: 2, + DefaultCountryCode: "US", + IP: "127.0.0.1", + Port: 2398, + }, Deps{}, zaptest.NewLogger(t), clock.System) + + var b bin.Buffer + if err := (&tg.HelpGetNearestDCRequest{}).Encode(&b); err != nil { + t.Fatalf("encode help.getNearestDc: %v", err) + } + enc, err := r.Dispatch(context.Background(), [8]byte{}, 0, &b) + if err != nil { + t.Fatalf("dispatch help.getNearestDc: %v", err) + } + nearest, ok := enc.(*tg.NearestDC) + if !ok { + t.Fatalf("result type = %T, want *tg.NearestDC", enc) + } + if nearest.Country != "US" || nearest.ThisDC != 2 || nearest.NearestDC != 2 { + t.Fatalf("nearestDc = %+v", nearest) + } +} + func TestDispatchRejectsAuthorizedRPCBeforeLogin(t *testing.T) { r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{ Auth: &captureAuthService{},