feat(telegram): split connector into home validator + remote bot
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Failing after 2m6s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 53s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Failing after 2m6s
Move all Telegram egress off the main host. The single connector held the bot token, long-polled Telegram and answered the gateway/backend over the trusted internal network, so the whole component (including login validation) shared fate with its VPN sidecar. Split it into two binaries that share the token: - cmd/validator (home, no VPN): Mini App initData + Login Widget HMAC only, never calls the Bot API. The gateway dials it for Telegram auth, so game login is now independent of Telegram reachability. - cmd/bot (remote): Bot API long-poll + sendMessage, the only component reaching Telegram. It holds no inbound port — it dials the gateway over a new reverse mTLS bot-link (pkg/proto/botlink/v1) and executes the send commands the gateway pushes. The gateway funnels sends to the bot-link: out-of-app push is fire-and-forget (at-most-once, dropped if no bot is connected); the backend admin broadcasts reach a gateway-served relay that forwards them and awaits the bot's ack (SendToUser/SendToGameChannel contract preserved). mTLS (pkg/mtls) is the one inter-service link that leaves the trusted segment; validator<->gateway and the relay stay plaintext internal. The bot is Telegram-rate-limited. One bot now; the gateway bot registry, an owns_updates flag and per-command ids leave seams for N later. Webhook rejected (one URL per token, adds inbound + a static address). The unified test contour runs the split (the bot keeps its VPN sidecar and dials the gateway by its internal name; bot-link certs from deploy/gen-certs.sh, generated in CI). The prod wiring — the bot on a separate host (no VPN), the gateway bot-link port published, PROD_ certs with scheduled rotation, an SSH deploy of both hosts together — is the deferred final stage (PRERELEASE.md TX, Stage 18). Docs: ARCHITECTURE, PRERELEASE (phase TX), platform/telegram + gateway + backend + deploy READMEs, FUNCTIONAL(+ru), CLAUDE.md, .env.example.
This commit is contained in:
@@ -28,10 +28,13 @@ type Config struct {
|
||||
// checks before proxying admin traffic to the backend. Empty disables admin.
|
||||
AdminUser string
|
||||
AdminPassword string
|
||||
// ConnectorAddr is the gRPC address of the Telegram connector side-service. The
|
||||
// gateway calls it to validate Mini App initData and to deliver out-of-app push.
|
||||
// Empty disables the telegram auth path and the out-of-app push channel.
|
||||
ConnectorAddr string
|
||||
// ValidatorAddr is the gRPC address of the Telegram validator side-service (home,
|
||||
// plaintext, internal). The gateway calls it to validate Mini App initData and
|
||||
// Login Widget data. Empty disables the telegram auth path.
|
||||
ValidatorAddr string
|
||||
// BotLink configures the reverse mTLS channel to the remote Telegram bot. An
|
||||
// empty BotLink.Addr disables the bot channel (out-of-app push and admin relay).
|
||||
BotLink BotLinkConfig
|
||||
// SessionTTL bounds how long a resolved session stays cached; SessionCacheMax
|
||||
// caps the number of cached sessions.
|
||||
SessionTTL time.Duration
|
||||
@@ -47,6 +50,29 @@ type Config struct {
|
||||
Telemetry pkgtel.Config
|
||||
}
|
||||
|
||||
// BotLinkConfig configures the gateway's reverse bot-link: the mTLS listener the
|
||||
// remote Telegram bot dials, the plaintext listener the backend admin relay calls,
|
||||
// and the mTLS material. The main host is already public, so exposing the bot-link
|
||||
// listener on a dedicated port adds no static IP; the channel is guarded solely by
|
||||
// mTLS (the bot has no fixed address to allow-list).
|
||||
type BotLinkConfig struct {
|
||||
// Addr is the mTLS gRPC listener the bot dials (e.g. ":9443"). Empty disables
|
||||
// the whole bot channel.
|
||||
Addr string
|
||||
// RelayAddr is the plaintext internal gRPC listener that serves the backend
|
||||
// admin SendToUser/SendToGameChannel relay (e.g. ":9092"). Empty disables it.
|
||||
RelayAddr string
|
||||
// CertFile, KeyFile and CAFile are the gateway server certificate, its key and
|
||||
// the CA bundle that signs the accepted bot client certificates. Required when
|
||||
// Addr is set.
|
||||
CertFile string
|
||||
KeyFile string
|
||||
CAFile string
|
||||
// SendTimeout bounds the admin relay's wait for the bot Ack before reporting
|
||||
// the send as not delivered.
|
||||
SendTimeout time.Duration
|
||||
}
|
||||
|
||||
// RateLimitConfig holds the token-bucket limits per class. Public and admin are
|
||||
// keyed per client IP; the authenticated class is keyed per user id; the email
|
||||
// sub-limit guards the costly email-code path per IP.
|
||||
@@ -72,6 +98,7 @@ const (
|
||||
defaultSessionCacheMax = 50000
|
||||
defaultPushHeartbeatInterval = 10 * time.Second // under the ~15 s edge idle timeout
|
||||
defaultServiceName = "scrabble-gateway"
|
||||
defaultBotLinkSendTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// DefaultMaxBodyBytes is the default request-body cap (GATEWAY_MAX_BODY_BYTES):
|
||||
@@ -104,9 +131,16 @@ func Load() (Config, error) {
|
||||
BackendGRPCAddr: envOr("GATEWAY_BACKEND_GRPC_ADDR", defaultBackendGRPCAddr),
|
||||
AdminUser: os.Getenv("GATEWAY_ADMIN_USER"),
|
||||
AdminPassword: os.Getenv("GATEWAY_ADMIN_PASSWORD"),
|
||||
ConnectorAddr: os.Getenv("GATEWAY_CONNECTOR_ADDR"),
|
||||
ValidatorAddr: os.Getenv("GATEWAY_VALIDATOR_ADDR"),
|
||||
SessionCacheMax: defaultSessionCacheMax,
|
||||
RateLimit: DefaultRateLimit(),
|
||||
BotLink: BotLinkConfig{
|
||||
Addr: os.Getenv("GATEWAY_BOTLINK_ADDR"),
|
||||
RelayAddr: os.Getenv("GATEWAY_BOTLINK_RELAY_ADDR"),
|
||||
CertFile: os.Getenv("GATEWAY_BOTLINK_TLS_CERT"),
|
||||
KeyFile: os.Getenv("GATEWAY_BOTLINK_TLS_KEY"),
|
||||
CAFile: os.Getenv("GATEWAY_BOTLINK_TLS_CA"),
|
||||
},
|
||||
}
|
||||
tel := pkgtel.DefaultConfig(defaultServiceName)
|
||||
tel.ServiceName = envOr("GATEWAY_SERVICE_NAME", tel.ServiceName)
|
||||
@@ -128,12 +162,18 @@ func Load() (Config, error) {
|
||||
if c.MaxBodyBytes, err = envInt("GATEWAY_MAX_BODY_BYTES", DefaultMaxBodyBytes); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if c.BotLink.SendTimeout, err = envDuration("GATEWAY_BOTLINK_SEND_TIMEOUT", defaultBotLinkSendTimeout); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// BotLinkEnabled reports whether the reverse bot-link channel is configured.
|
||||
func (c Config) BotLinkEnabled() bool { return c.BotLink.Addr != "" }
|
||||
|
||||
// AdminEnabled reports whether the admin console proxy should be mounted (both
|
||||
// Basic-Auth credentials are configured).
|
||||
func (c Config) AdminEnabled() bool {
|
||||
@@ -159,6 +199,11 @@ func (c Config) validate() error {
|
||||
if c.MaxBodyBytes <= 0 {
|
||||
return fmt.Errorf("config: GATEWAY_MAX_BODY_BYTES must be positive")
|
||||
}
|
||||
if c.BotLink.Addr != "" {
|
||||
if c.BotLink.CertFile == "" || c.BotLink.KeyFile == "" || c.BotLink.CAFile == "" {
|
||||
return fmt.Errorf("config: GATEWAY_BOTLINK_ADDR requires GATEWAY_BOTLINK_TLS_CERT, _KEY and _CA")
|
||||
}
|
||||
}
|
||||
if err := c.Telemetry.Validate(); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user