This commit is contained in:
onysd 2026-07-18 10:33:17 +03:00
parent 6b29556ef8
commit 8875cd33fd
14 changed files with 136 additions and 4 deletions

View file

@ -897,6 +897,7 @@ func run(logger *zap.Logger) error {
srv := mtprotoedge.New(mtprotoedge.Options{
Logger: logger.Named("mtprotoedge"),
DC: cfg.DC,
StrictDC: cfg.StrictDCCheck,
RSAKey: rsaKey,
LayerRPC: router,
AuthKeys: authKeyStore,

25
deploy/deploy_test.go Normal file
View file

@ -0,0 +1,25 @@
package deploy
import (
"testing"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
// TestMigrationsLoad asserts the embedded migrations directory parses
// cleanly with no database connection involved. Two independently-added
// migrations landing on the same version (e.g. an upstream sync and a local
// feature branch both picking "next free sequential number" at merge-base
// time) don't show up as a git conflict — they're different files — so iofs
// only catches it at load time, and previously that meant "at server
// startup," crashing telesrv with "postgres migrate: iofs source: failed to
// init driver ... duplicate migration file." This is why our own migrations
// use a YYYYMMDDHHMMSS timestamp version instead of continuing upstream's
// plain sequential numbering (see deploy/migrations/README.md): a 14-digit
// timestamp never collides with upstream's numbers and always sorts after
// them, no renumbering needed on the next sync.
func TestMigrationsLoad(t *testing.T) {
if _, err := iofs.New(Migrations, "migrations"); err != nil {
t.Fatalf("iofs.New(migrations): %v", err)
}
}

View file

@ -0,0 +1,33 @@
# Migration numbering
Upstream (`iamxvbaba/gramsrv`) numbers its migrations sequentially
(`0001`, `0002`, ..., currently up to `0092`). This fork periodically merges
upstream, and upstream keeps adding its own sequentially-numbered migrations
between syncs.
**Any new OwpenGram-specific migration must use a `YYYYMMDDHHMMSS` timestamp
as its version instead of continuing the sequential numbering** — e.g.
`20260713103024_owpengram_system_account_brand.up.sql`.
Why: golang-migrate treats the version as a plain number and only cares
about ordering, not format. If we keep picking "the next free sequential
number" for our own migrations, the next upstream sync is very likely to
have independently picked the *same* number for one of its own new
migrations — two different new files, so git sees no conflict, and the
collision only surfaces at server startup (`iofs source: failed to init
driver ... duplicate migration file`, see `deploy/deploy_test.go`'s
`TestMigrationsLoad` for the regression test that now catches this at
`go test` time instead). This happened once already (our `0086`-`0088`
collided with upstream's own new `0086`-`0088`), and renumbering our
migrations to fit after upstream's (`0093`-`0095`) would only have deferred
the exact same problem to the *next* sync.
A 14-digit timestamp is always numerically larger than upstream's 4-5 digit
sequential numbers, so it always sorts after everything upstream has today
or will plausibly reach, and it never collides with another OwpenGram
migration as long as two aren't created in the same second. No renumbering
is ever needed again on future syncs — just keep using the timestamp at the
time the migration is written.
Upstream's own migrations keep their original sequential numbers untouched;
only migrations authored in this fork use the timestamp scheme.

View file

@ -30,6 +30,13 @@ 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 the OwpenGram client forks intentionally 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 bool
// MTProtoMaxConnections / PerIP 覆盖 raw Accept、codec sniff、握手到认证 session
// 的完整物理连接生命周期;负数关闭对应 admission 上限。
MTProtoMaxConnections int
@ -444,6 +451,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),
StrictDCCheck: envBoolOr("TELESRV_STRICT_DC_CHECK", false),
MTProtoMaxConnections: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS", 200000),
MTProtoMaxConnectionsPerIP: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS_PER_IP", 4096),
MTProtoMaxConcurrentHandshakes: envIntOr("TELESRV_MTPROTO_MAX_CONCURRENT_HANDSHAKES", 256),

View file

@ -15,7 +15,7 @@ import (
const EmailPhonePrefix = "888"
// MaxEmailSignupPhoneLen mirrors the users.phone column width (see migration
// 0087) and ValidPhone's upper bound.
// widen_phone_for_email_signup) and ValidPhone's upper bound.
const MaxEmailSignupPhoneLen = 200
// emailPhoneEscape marks a 2-character escape sequence standing in for one

View file

@ -38,6 +38,7 @@ func (s *Server) runServerExchange(ctx context.Context, conn transport.Conn) (ex
timeout: exchange.DefaultTimeout,
key: s.key,
dc: s.dc,
strictDC: s.strictDC,
log: s.log.Named("exchange"),
rng: compatServerRNG{rand: s.rand},
commitKey: s.commitExchangeAuthKey,
@ -65,6 +66,7 @@ type serverExchangeCompat struct {
timeout time.Duration
key exchange.PrivateKey
dc int
strictDC bool
log *zap.Logger
rng compatServerRNG
commitKey func(context.Context, exchange.ServerExchangeResult, int) error
@ -345,10 +347,23 @@ func (s serverExchangeCompat) validatePQInnerDataDC(d mt.PQInnerDataClass) error
switch innerDataDC := d.(type) {
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 the
// OwpenGram client forks deliberately 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)",
zap.Int("server_dc", s.dc),
zap.Int("client_dc", innerDataDC.DC))
return nil
}
return wrongDCError(s.dc, innerDataDC.DC)
}
case *mt.PQInnerDataTempDC:
if !sameDCByAbs(innerDataDC.DC, s.dc) {
if !sameDCByAbs(innerDataDC.DC, s.dc) && s.strictDC {
return wrongDCError(s.dc, innerDataDC.DC)
}
if innerDataDC.DC < 0 {

View file

@ -366,8 +366,8 @@ func TestKeyExchangeAcceptsAndroidMediaTempNegativeDC(t *testing.T) {
}
}
func TestKeyExchangeRejectsWrongNegativeTempDC(t *testing.T) {
ex := serverExchangeCompat{dc: 2, log: zaptest.NewLogger(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) {
@ -378,6 +378,38 @@ func TestKeyExchangeRejectsWrongNegativeTempDC(t *testing.T) {
}
}
// 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; the OwpenGram client forks deliberately 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) {
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)
}
if err := ex.validatePQInnerDataDC(&mt.PQInnerDataTempDC{DC: -3}); err != nil {
t.Fatalf("temp DC mismatch: err = %v, want nil (lenient by default)", 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) {
ex := serverExchangeCompat{dc: 2, strictDC: true, log: zaptest.NewLogger(t)}
err := ex.validatePQInnerDataDC(&mt.PQInnerDataDC{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)
}
}
func TestDecodeCompatPQInnerDataTemp(t *testing.T) {
want := mt.PQInnerData{
Pq: []byte{0x0f},

View file

@ -301,6 +301,22 @@ 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 the OwpenGram
// client forks intentionally run in "single-server backend" mode, where
// dc_id 1..5 all alias to this one server (see owpengram_servers.cpp /
// ApplyServerToDcOptions in the desktop client) so that any old data
// referencing a specific dc_id still resolves correctly. When tdesktop
// 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 bool
// RSAKey 是 server RSA 私钥用于密钥交换。nil 时无法完成握手。
RSAKey *rsa.PrivateKey
// AuthKeys 持久化 auth key。默认内存实现。
@ -480,6 +496,7 @@ type Server struct {
outboundScratchPool *outboundScratchPool
dc int
strictDC bool
key exchange.PrivateKey
authKeys store.AuthKeyStore
conns *SessionManager
@ -531,6 +548,7 @@ func New(opts Options) *Server {
outboundControlBudget: newOutboundTrackedBudget(defaultOutboundControlMaxBytes),
outboundScratchPool: newOutboundScratchPool(opts.OutboundWriteGlobalMaxBytes),
dc: opts.DC,
strictDC: opts.StrictDC,
key: exchange.PrivateKey{RSA: opts.RSAKey},
authKeys: opts.AuthKeys,
conns: conns,