Skip to content

Уровень безопасности TLS

Введение

Уровень TLS в Xray-core обеспечивает стандартное TLS-шифрование с обширными возможностями настройки. Он поддерживает как стандартный crypto/tls Go, так и uTLS (refraction-networking/utls) для имитации TLS-отпечатков, динамическую генерацию сертификатов из CA, OCSP-скрепление, закрепление сертификатов, возобновление сессий, ECH (Encrypted Client Hello), произвольные наборы шифров, предпочтения кривых и журналирование ключей. TLS не является транспортом сам по себе, а представляет собой уровень безопасности, оборачивающий любой транспорт.

Ключевые файлы

  • transport/internet/tls/tls.go -- Основные типы: Conn, UConn, реестр отпечатков
  • transport/internet/tls/config.go -- Config.GetTLSConfig(), управление сертификатами, параметры
  • transport/internet/tls/pin.go -- Функции хеширования сертификатов для закрепления
  • transport/internet/tls/ech.go -- Поддержка ECH (Encrypted Client Hello)
  • transport/internet/tls/grpc.go -- Вспомогательные функции TLS для gRPC
  • transport/internet/tls/unsafe.go -- Небезопасный доступ к пулу сертификатов

Типы соединений

Conn (стандартный TLS)

tls.Conn (tls/tls.go:26-28) оборачивает crypto/tls.Conn из Go:

go
type Conn struct {
    *tls.Conn
}

Функции:

  • Таймаут корректного закрытия: 250 мс таймаут для TLS close notify, после которого базовое соединение принудительно закрывается (tls.go:30-38)
  • Запись MultiBuffer: Компактирует и эффективно записывает мультибуферы (tls.go:40-45)
  • NegotiatedProtocol: Сообщает результат ALPN (tls.go:54-57)

UConn (имитация TLS-отпечатков uTLS)

tls.UConn (tls/tls.go:71-73) оборачивает utls.UConn для имитации TLS-отпечатков:

go
type UConn struct {
    *utls.UConn
}

Метод WebsocketHandshakeContext (tls.go:94-117) принудительно устанавливает ALPN http/1.1 для WebSocket-соединений с использованием uTLS:

go
func (c *UConn) WebsocketHandshakeContext(ctx context.Context) error {
    c.BuildHandshakeState()
    // Найти ALPNExtension и переопределить на ["http/1.1"]
    // Пересобрать состояние рукопожатия
    return c.HandshakeContext(ctx)
}

Интерфейс

Оба типа реализуют Interface (tls.go:15-21):

go
type Interface interface {
    net.Conn
    HandshakeContext(ctx context.Context) error
    VerifyHostname(host string) error
    HandshakeContextServerName(ctx context.Context) string
    NegotiatedProtocol() string
}

Имитация TLS-отпечатков

Реестр отпечатков

Определены три уровня отпечатков (tls/tls.go:185-257):

PresetFingerprints (рекомендуемые варианты для GUI):

ИмяID uTLS
chromeHelloChrome_Auto
firefoxHelloFirefox_Auto
safariHelloSafari_Auto
iosHelloIOS_Auto
androidHelloAndroid_11_OkHttp
edgeHelloEdge_Auto
360Hello360_Auto
qqHelloQQ_Auto
randomСлучайно выбирается при запуске
randomizedHelloRandomizedALPN с произвольными весами
randomizednoalpnHelloRandomizedNoALPN с произвольными весами

ModernFingerprints: Конкретные версии браузеров (Chrome 83-131, Firefox 99-120 и т.д.)

OtherFingerprints: Устаревшие версии и специальные отпечатки.

Случайный выбор отпечатка

При запуске (tls.go:145-167) "random" назначается случайно выбранному современному отпечатку:

go
func init() {
    bigInt, _ := rand.Int(rand.Reader, big.NewInt(int64(len(ModernFingerprints))))
    stopAt := int(bigInt.Int64())
    // Выбрать stopAt-й элемент из ModernFingerprints
    PresetFingerprints["random"] = v
}

Вариант "randomized" использует встроенную рандомизацию uTLS с настроенными весами:

  • Принудительно TLS 1.3 (TLSVersMax_Set_VersionTLS13 = 1)
  • Отключен P256 для первого обмена ключами (FirstKeyShare_Set_CurveP256 = 0)

GetFingerprint

GetFingerprint (tls.go:169-183) ищет отпечатки во всех трех картах:

go
func GetFingerprint(name string) (fingerprint *utls.ClientHelloID) {
    if name == "" { return &utls.HelloChrome_Auto }
    if fingerprint = PresetFingerprints[name]; fingerprint != nil { return }
    if fingerprint = ModernFingerprints[name]; fingerprint != nil { return }
    if fingerprint = OtherFingerprints[name]; fingerprint != nil { return }
    return  // nil = стандартный Go TLS
}

Пустое имя по умолчанию соответствует Chrome Auto. Если совпадение не найдено, возвращается nil, что сигнализирует вызывающему использовать стандартный Go TLS.

Создание клиента/сервера

Клиент

go
// tls.go:60-63 -- Стандартный Go TLS
func Client(c net.Conn, config *tls.Config) net.Conn {
    tlsConn := tls.Client(c, config)
    return &Conn{Conn: tlsConn}
}

// tls.go:124-127 -- uTLS с отпечатком
func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) net.Conn {
    utlsConn := utls.UClient(c, copyConfig(config), *fingerprint)
    return &UConn{UConn: utlsConn}
}

Сервер

go
// tls.go:66-69
func Server(c net.Conn, config *tls.Config) net.Conn {
    tlsConn := tls.Server(c, config)
    return &Conn{Conn: tlsConn}
}

Копирование конфигурации для uTLS

copyConfig (tls.go:133-143) преобразует crypto/tls.Config в utls.Config:

go
func copyConfig(c *tls.Config) *utls.Config {
    return &utls.Config{
        Rand:                           c.Rand,
        RootCAs:                        c.RootCAs,
        ServerName:                     c.ServerName,
        InsecureSkipVerify:             c.InsecureSkipVerify,
        VerifyPeerCertificate:          c.VerifyPeerCertificate,
        KeyLogWriter:                   c.KeyLogWriter,
        EncryptedClientHelloConfigList: c.EncryptedClientHelloConfigList,
    }
}

Управление сертификатами

GetTLSConfig

Config.GetTLSConfig (tls/config.go:364-481) строит tls.Config:

go
func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
    root, _ := c.getCertPool()
    // Создание RandCarrier с информацией для закрепления/проверки
    config := &tls.Config{
        InsecureSkipVerify:     c.AllowInsecure,
        Rand:                   randCarrier,
        ClientSessionCache:     globalSessionCache,
        RootCAs:                root,
        NextProtos:             c.NextProtocol,
        SessionTicketsDisabled: !c.EnableSessionResumption,
        VerifyPeerCertificate:  randCarrier.verifyPeerCert,
    }
    // Применение опций (WithDestination, WithNextProto и т.д.)
    // Настройка GetCertificate (динамический или статический)
    // Установка ServerName, CurvePreferences, MinVersion, MaxVersion
    // Настройка CipherSuites, KeyLogWriter, ECH
    return config
}

Типы сертификатов

Сертификаты имеют два режима использования:

  • ENCIPHERMENT (Certificate_ENCIPHERMENT): Обычный серверный/клиентский сертификат
  • AUTHORITY_ISSUE (Certificate_AUTHORITY_ISSUE): CA-сертификат для динамической выдачи

Динамическая генерация сертификатов

При наличии сертификатов AUTHORITY_ISSUE getGetCertificateFunc (config.go:172-244) генерирует сертификаты по запросу:

go
func getGetCertificateFunc(c *tls.Config, ca []*Certificate) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
    return func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
        domain := hello.ServerName
        // Проверка существующих сертификатов, удаление просроченных
        // Генерация нового сертификата из CA для домена
        newCert, _ := issueCertificate(rawCert, domain)
        c.Certificates = append(c.Certificates, *newCert)
        return issuedCertificate, nil
    }
}

Это позволяет реализовать сценарии MitM и устраняет необходимость в предварительно сгенерированных сертификатах.

Выбор статического сертификата

getNewGetCertificateFunc (config.go:246-274) сопоставляет сертификаты по SNI:

go
func getNewGetCertificateFunc(certs []*tls.Certificate, rejectUnknownSNI bool) ... {
    // Сопоставление по CommonName или DNSNames
    // Поддержка подстановочного сопоставления (*.example.com)
    // Если rejectUnknownSNI, возврат ошибки для неизвестных доменов
    // В противном случае возврат первого сертификата как запасного
}

Горячая перезагрузка и OCSP

setupOcspTicker (config.go:96-131) периодически:

  1. Перезагружает файлы сертификатов с диска (если заданы CertificatePath/KeyPath)
  2. Получает данные OCSP-скрепления

Интервал по умолчанию: 3600 секунд (1 час) или значение конфигурации OcspStapling.

Закрепление сертификатов

PinnedPeerCertSha256

SHA-256 хеши доверенных сертификатов. Проверка в RandCarrier.verifyPeerCert (config.go:283-350):

go
func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
    // Проверка, совпадает ли листовой сертификат с любым закрепленным хешем -> успех
    // Проверка, совпадает ли CA-сертификат с любым закрепленным хешем -> проверка листового по закрепленному CA
    // Если задан VerifyPeerCertByName, проверка по нескольким доменным именам
    // В противном случае проверка по ServerName
}

Структура RandCarrier (config.go:352-357) использует tls.Config.Rand для передачи состояния проверки (поскольку VerifyPeerCertificate не имеет механизма для передачи произвольных данных):

go
type RandCarrier struct {
    Config               *tls.Config
    RootCAs              *x509.CertPool
    VerifyPeerCertByName []string
    PinnedPeerCertSha256 [][]byte
}

Генерация хеша

GenerateCertHash (tls/pin.go:10-19) вычисляет SHA-256 сертификата:

go
func GenerateCertHash[T *x509.Certificate | []byte](cert T) []byte {
    var out [32]byte
    switch v := any(cert).(type) {
    case *x509.Certificate:
        out = sha256.Sum256(v.Raw)
    case []byte:
        out = sha256.Sum256(v)
    }
    return out[:]
}

Система опций

Функции Option (config.go:484) модифицируют tls.Config после начального построения:

go
type Option func(*tls.Config)

Доступные опции:

  • WithDestination: Устанавливает ServerName из адреса назначения (только если еще не задан) (config.go:490-496)
  • WithOverrideName: Принудительно устанавливает конкретный ServerName (config.go:498-502)
  • WithNextProto: Устанавливает ALPN-протоколы (только если еще не заданы) (config.go:505-511)

Приоритет разрешения SNI

В силу структуры GetTLSConfig (config.go:415-417) порядок разрешения ServerName следующий:

  1. ServerName из конфигурации (если не пустой и не "frommitm")
  2. Опция WithDestination (адрес назначения)
  3. Пустое значение (для серверных конфигураций)

Управление сессиями

Глобальный LRU-кэш сессий (config.go:24):

go
var globalSessionCache = tls.NewLRUClientSessionCache(128)

Билеты сессий отключены по умолчанию (SessionTicketsDisabled: !c.EnableSessionResumption).

Конфигурация версии и шифров

Версия TLS

Настраиваемые минимальная/максимальная версии (config.go:427-447):

  • "1.0" до "1.3"

Наборы шифров

Произвольный список наборов шифров из разделенных двоеточием имен (config.go:449-459):

go
if len(c.CipherSuites) > 0 {
    for _, n := range strings.Split(c.CipherSuites, ":") {
        config.CipherSuites = append(config.CipherSuites, id[n])
    }
}

Предпочтения кривых

ParseCurveName (config.go:525-545) сопоставляет строковые имена с tls.CurveID:

ИмяКривая
curvep256P-256
curvep384P-384
curvep521P-521
x25519X25519
x25519mlkem768X25519MLKEM768 (постквантовый)
secp256r1mlkem768SecP256r1MLKEM768
secp384r1mlkem1024SecP384r1MLKEM1024

ConfigFromStreamSettings

Функция извлечения (config.go:514-523) вызывается каждым транспортом для проверки наличия конфигурации TLS:

go
func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config {
    if settings == nil { return nil }
    config, ok := settings.SecuritySettings.(*Config)
    if !ok { return nil }
    return config
}

Возвращает nil, когда TLS не настроен, сигнализируя транспорту пропустить TLS-обертку.

Примечания по реализации

  • ALPN по умолчанию: Если NextProtocol не настроен, по умолчанию используется ["h2", "http/1.1"] (config.go:423-425).
  • Таймаут закрытия: Закрытие TLS имеет жесткий таймаут 250 мс для предотвращения зависания при корректном завершении (tls.go:30-38).
  • Поддержка MitM: Функция IsFromMitm (config.go:547-549) проверяет наличие специального маркера "frommitm" в ServerName, используемого функциями MitM-прокси.
  • VerifyPeerCertByName: При установке InsecureSkipVerify принудительно устанавливается в true (config.go:393-397), и проверка выполняется вручную в verifyPeerCert по нескольким доменным именам.
  • Журналирование ключей: Включается через конфигурацию MasterKeyLog, запись в файл для отладки в Wireshark (config.go:461-468).
  • ECH: Encrypted Client Hello применяется через ApplyECH, когда настроены EchConfigList или EchServerKeys (config.go:469-478).
  • Срок действия сертификата: Проверяется с запасом в 2 минуты (config.go:133-142).
  • Построение цепочки: Когда BuildChain равен true для CA-сертификата, CA-сертификат добавляется к вновь выданным сертификатам (config.go:155).

Технический анализ для целей повторной реализации.