При генерации клиентского конфига настройка multiMode для gRPC-транспорта выписывается под именем mode. У gRPC в Xray поля mode не существует — Go молча отбрасывает неизвестные ключи JSON, поэтому multiMode у клиента всегда остаётся false, что бы администратор ни выставил в панели. Мультиплексирование gRPC не работает никогда.
backend-main/src/modules/subscription-template/generators/xray-json.generator.service.ts:115-119
grpc: (host) => ({
serviceName: host.transportOptions.serviceName,
authority: host.transportOptions.authority,
mode: !!host.transportOptions.multiMode, // ← должно быть multiMode
}),
В схеме панели поле названо верно (backend-main/libs/contract/models/resolved-proxy-config.schema.ts:73-77):
export const GrpcTransportOptionsSchema = z.object({
authority: z.string().nullable(),
serviceName: z.string().nullable(),
multiMode: z.boolean(),
});
То есть ошибка только на этапе генерации конфига.
Для справки — структура gRPC-транспорта в Xray, infra/conf/transport_method.go:578-587:
type GRPCConfig struct {
Authority string `json:"authority"`
ServiceName string `json:"serviceName"`
MultiMode bool `json:"multiMode"`
IdleTimeout int32 `json:"idle_timeout"`
HealthCheckTimeout int32 `json:"health_check_timeout"`
PermitWithoutStream bool `json:"permit_without_stream"`
InitialWindowsSize int32 `json:"initial_windows_size"`
UserAgent string `json:"user_agent"`
}
Поле mode в Xray есть только у XHTTP (там же, строка 260).
Баг незаметен, потому что подключение устанавливается: сервер регистрирует оба метода — и Tun, и TunMulti (transport/internet/grpc/hub.go:132). Клиент просто уходит в немультиплексированный режим, и со стороны всё выглядит работающим.
Исправление
grpc: (host) => ({
serviceName: host.transportOptions.serviceName,
authority: host.transportOptions.authority,
multiMode: !!host.transportOptions.multiMode,
}),
Сопутствующая проблема: недоступные параметры keepalive
Схема и генератор поддерживают для gRPC только три поля. Параметры idle_timeout, health_check_timeout и permit_without_stream выставить через панель невозможно — их нет ни в схеме, ни в генераторе.
Для мобильных клиентов это ощутимо. Без permit_without_stream: true gRPC не отправляет PING, когда нет активных потоков, а приложение в фоне находится именно в этом состоянии. Соединение умирает незаметно: клиент показывает «подключено», трафик не идёт до переподключения вручную.
Предложение — добавить их по аналогии с остальными полями:
// resolved-proxy-config.schema.ts
export const GrpcTransportOptionsSchema = z.object({
authority: z.string().nullable(),
serviceName: z.string().nullable(),
multiMode: z.boolean(),
idleTimeout: z.int().nullable(),
healthCheckTimeout: z.int().nullable(),
permitWithoutStream: z.boolean().nullable(),
});
// xray-json.generator.service.ts
grpc: (host) => ({
serviceName: host.transportOptions.serviceName,
authority: host.transportOptions.authority,
multiMode: !!host.transportOptions.multiMode,
...(host.transportOptions.idleTimeout != null && {
idle_timeout: host.transportOptions.idleTimeout,
}),
...(host.transportOptions.healthCheckTimeout != null && {
health_check_timeout: host.transportOptions.healthCheckTimeout,
}),
...(host.transportOptions.permitWithoutStream != null && {
permit_without_stream: host.transportOptions.permitWithoutStream,
}),
}),
Имена внутри панели могут быть любыми, но в сгенерированный конфиг они обязаны попадать ровно в том виде, в котором их ждёт Xray — со подчёркиваниями: idle_timeout, health_check_timeout, permit_without_stream. Расхождение здесь даёт ту же тихую поломку, что и с mode.