Как стать автором
Обновить

Как XTLS Reality обходит whitelist? Анализ исходного кода Reality

Уровень сложностиСредний
Время на прочтение20 мин
Количество просмотров3.9K
Автор оригинала: ObjShadow

Это статья-перевод оригинальной статьи. Статья несет чисто ознакомительный характер

XTLS/Xray-core - инструмент для обхода цензуры с открытым исходным кодом. Он хорошо известен в Китае своими новыми и практичными концептуальными технологиями, а также создателем RPRX, который однажды исчез и, как считалось, сбежал. К таким технологиям относятся VLESS, XTLS-Vision, XUDP... О какой-то из них вы точно слышали или использовали.

С момента как в Китае началось внедрение новой системы цензурирование: белый список SNI (Server name indication), все инструменты обхода на основе TLS до появления REALITY и ShadowTLS, подключаемые напрямую или через транзит или CDN, стали недоступны.

Ранее широкое внимание привлек инструмент обхода ShadowTLS. Однако в то время ShadowTLS все еще находился в версии v1 с неполной кодовой базой и слабой устойчивостью к цензуре. Позже в Reality появилась возможность обходить цензуру на основе белого списка SNI, и он был интегрирован со зрелым инструментом обхода Xray-core.

Так как же REALITY обходит эту цензурную стратегию? Как понять ее детали с технической точки зрения? Эти два вопроса будут в центре внимания этой статьи. Интерпретируя исходный код REALITY, мы разберемся с конкретной реализацией REALITY для читателей.

Что такое белый список SNI? В чем связь между SNI и TLS?

Вы, возможно, знаете, что широко используемый протокол безопасности прикладного уровня, основа HTTPS, протокол TLS, имеет свой собственный «процесс рукопожатия» при инициировании соединения.

TLS был «гибридной системой шифрования» с момента разработки его первой версии. Это означает, что TLS использует как асимметричные, так и симметричные алгоритмы шифрования. Симметричные алгоритмы шифрования требуют, чтобы обе стороны имели абсолютно одинаковый ключ, а накладные расходы на шифрование и дешифрование низкие. В то время как асимметричное шифрование требует только обмена открытым ключом в своих соответствующих парах ключей, но требует проверки того, что открытый ключ не был заменен или подделан при обмене ключами, что привело к появлению механизма цифрового сертификата. Кроме того, накладные расходы на асимметричное шифрование и дешифрование высоки. Поэтому TLS использует асимметричное шифрование для передачи ключа, используемого для симметричного шифрования, и для того, чтобы обменять открытый ключ, используемый для асимметричного шифрования, родился механизм рукопожатия TLS.

Прежде чем разобраться в белом списке SNI, давайте рассмотрим процесс установления связи для обычного соединения TLS1.3:

Сначала клиент инициирует TLS соединение. После того, как TCP рукопожатие завершено, клиент генерирует пару ключей и отправляет сообщение TLS Client Hello на сервер через открытое TCP соединение: все параметры рукопожатия клиента, включая различные поля расширения и key_share (открытый ключ в паре ключей, только что сгенерированной клиентом). Эти параметры должны быть отправлены вместе в TLS Client Hello, и не важно конфиденциальны они или нет.

Сервер использует алгоритм обмена ключами, разрешенный в TLS Client Hello, для генерации пары ключей и отправляет TLS Server Hello: Сообщение Server Hello TLS1.3 отличается от TLS1.2. Оно содержит только все нечувствительные параметры рукопожатия сервера, такие как key_share.

Получив TLS Server Hello, клиент извлекает key_share и вводит его в функцию обмена ключами Диффла-Хеллмана вместе с закрытым ключом в паре ключей, изначально сгенерированной клиентом.

(Алгоритм обмена ключами Диффла-Хеллмана называется алгоритмом DH. Алгоритм DHE представляет собой вариант DH, который обеспечивает прямую безопасность путем ротации ключей. Алгоритм ECDHE представляет собой вариант DHE, основанный на эллиптических кривых. На момент написания этой статьи последняя версия crypto/tls поддерживает только алгоритм обмена ключами ECDHE при обработке обмена открытыми ключами TLS1.3.)

DH и его производные алгоритмы имеют общее свойство: обменивая открытые или закрытые ключи двух пар ключей, которые соответствуют требованиям алгоритма, а затем вводя две обмененные пары ключей в алгоритм, вы получите точно такое же значение. То есть: генерируя пару ключей A (включая открытый ключ Apub и закрытый ключ Asec) и пару ключей B (включая открытый ключ Bpub и закрытый ключ Bsec), которые соответствуют требованиям алгоритма, и вводя (Apub, Bsec) в алгоритм, полученное значение должно быть точно таким же, как значение, полученное при вводе (Bpub, Asec) в алгоритм.

Здесь клиент использует открытый ключ от сервера и свой собственный закрытый ключ для ввода значения, полученного алгоритмом DH, для генерации ключа. Сгенерированный ключ должен быть таким же, как ключ, вычисленный сервером с использованием открытого ключа от клиента и его собственного закрытого ключа. Сгенерированный здесь ключ называется preMasterKey и используется только для шифрования и дешифрования последующих сообщений рукопожатия.

Затем сервер использует preMasterKey для шифрования конфиденциальных параметров рукопожатия, которые еще не были отправлены, включая цифровой сертификат, используемый для проверки подлинности сервера (поскольку цифровой сертификат содержит соответствующую информацию о доменном имени/IP-адресе), инкапсулирует его в сообщение TLS Application Data (также называемое Encrypted Extensions, но это имя видно только после расшифровки) и прикрепляет его к Change Cipher Spec перед отправкой клиенту. Получив Change Cipher Spec, клиент использует preMasterKey для расшифровки прикрепленных к нему TLS Application Data, проверяет подлинность сервера с помощью цифрового сертификата в нем (то есть доказывает, что сервер содержит определенное доменное имя/IP-адрес), извлекает параметры рукопожатия, создает сообщение TLS Finished и preMasterKey отправляет его на сервер после шифрования, чтобы указать, что рукопожатие TLS было успешно завершено. В то же время клиент вводит полные параметры рукопожатия, ранее извлеченные из дополнительных сообщений после TLS Server Hello и Change Cipher Spec, в алгоритм DH для расчета, который используется для шифрования и дешифрования всех последующих сообщений MasterKey (то есть последующих пакетов данных приложения TLS). Сервер также использует ту же логику для расчета после получения сообщения TLS Finished.

(В широко используемых реализациях библиотеки TLS клиент отправляет сначала TLS Application Data, содержащие данные приложения, вместе с TLS Finished в каждом соединении. Такое поведение называется ложным стартом TLS. «GET /index.html HTTP/1.1» в конце приведенной выше диаграммы относится к такому поведению.)

Среди них, в начале рукопожатия TLS, клиент использует расширение SNI (Server Name Indicator) в сообщении TLS Client Hello, чтобы указать серверу веб-сайт, который он хочет посетить. Это имеет решающее значение для современного Интернета, поскольку сейчас стало обычным делом, когда многие серверы-источники находятся за одним сервером TLS, например, в CDN. Сервер использует SNI, чтобы определить, кто будет аутентифицировать соединение: без него он не сможет узнать, какой TLS сертификат веб-сайта следует предоставить клиенту.

Ключ к реализации цензуры на основе SNI заключается в том, что SNI виден для любого маршрута в сети (конечной точки, через которую передается трафик), поэтому он покажет исходный сервер, к которому клиент хочет подключиться. В то же время цензор контролирует все исходящие маршруты трафика и ограничивает значение расширения SNI TLS Client Hello, которому разрешено проходить, тем самым предотвращая прохождение трафика TLS, которому цензор не доверяет. Мы называем эту стратегию цензуры «белым списком SNI».

Вопрос: Почему в качестве примера выбран TLS 1.3 вместо TLS 1.2? Почему здесь не включена функция ECH TLS 1.3?

Ответ: TLS1.3 в значительной степени является упрощенной версией TLS1.2. Он устраняет множество уязвимостей безопасности в предыдущем поколении проектирования протокола и разработан для простоты в использовании, и его масштаб развертывания расширяется с момента выпуска первой официальной версии в 2018 году. Во-вторых, сервер REALITY в рамках реализации XTLS не поддерживает клиентов REALITY, использующих версии TLS, отличные от TLS1.3, для подключения для передачи трафика уклонения. На момент публикации этой статьи REALITY в рамках реализации XTLS не включает функцию ECH TLS1.3 по умолчанию. На самом деле в файле конфигурации нет соответствующей опции. И суть дизайна REALITY заключается в том, чтобы показать действительный процесс рукопожатия TLS с использованием разрешенных значений SNI цензору, поэтому SNI не должен быть зашифрован

Давайте подумаем как можно поменять SNI

  1. Когда клиент инициирует TLS-рукопожатие с сервером, напрямую изменить значение SNI в TLS Client Hello.

    Этот метод прост и груб, но очевидно, что он не обладает хорошей устойчивостью к цензуре и подорвет механизм TLS аутентификации. Цифровой сертификат, используемый для проверки подлинности сервера во время рукопожатия, должен быть подписан CA, которому доверяет клиент, с использованием закрытого ключа, включая информацию о прикреплении сертификата и информацию о доменном имени в нем. Таким образом, невозможно изменить действительный сертификат с других веб-сайтов, сохранив его действительность без подписи высшим CA измененного сертификата. Фактически, в этом случае даже владелец сертификата не может подделать действительный цифровой сертификат, которым он владеет, сохранив его действительность.

    Значение SNI в TLS Client Hello должно соответствовать доменному имени в информации о SNI действительного цифрового сертификата в TLS Server Hello с сервера. (Оно не обязательно должно быть точно таким же. В цифровом сертификате, используемом для аутентификации сервера TLS, есть тип «сертификата доменного имени с подстановочными знаками». Этот тип сертификата может проверить подлинность всех поддоменов под доменным именем без выдачи сертификатов для каждого поддомена по одному)

    Аналогичным образом, рецензент может записать значение SNI в TLS Client Hello в проходящем TLS трафике, использовать это значение для построения TLS Client Hello и отправить его на соответствующий сервер, проверить действительность цифрового сертификата в возвращенном TLS Server Hello и соответствие доменного имени в информации о расширении сертификата исходному значению SNI. Если что-либо из них не совпадает, исходный трафик можно считать обходным трафиком.

  2. Из пункта 1 мы знаем, что если вы хотите изменить SNI в TLS Client Hello, вам может потребоваться изменить и цифровой сертификат.

    Конечно, это всего лишь шутка. Благодаря математическим свойствам цифровых подписей, их невозможно подделать, сохраняя при этом их действительность в текущих условиях вычислительной мощности.

    Подождите, мы что-то упоминали в последнем разделе? Давайте снова посмотрим на эту диаграмму... цифровой сертификат сервера в рукопожатии TLS1.3 отправляется в зашифрованном виде, верно? Поэтому цензор не может пассивно наблюдать и собирать цифровой сертификат в рукопожатии TLS1.3, инициированном пользователем в 1. Цензор должен создать TLS Client Hello, используя исходное значение SNI, чтобы получить цифровой сертификат сервера и сравнить исходное значение SNI с доменным именем в цифровом сертификате для целей цензуры. Хотя цифровые сертификаты невозможно подделать, если мы можем отличить клиента от цензора, можем ли мы предоставить им разные цифровые сертификаты?

Окунемся в океан кода REALITY?

В предыдущем разделе мы предложили стратегию уклонения «предоставление различных цифровых сертификатов путем различения типов клиентов TLS», что является одним из ключевых дизайнов REALITY. Но как отличить? Как согласовать ключ, используемый для шифрования рукопожатия preMasterKey? В этом разделе мы попытаемся углубиться в исходный код REALITY и объединить знания из первого раздела, чтобы проанализировать технические детали реализации REALITY этой стратегии уклонения.

Пожалуйста, не уходите! Независимо от того, являетесь ли вы новичком в программировании или разработчиком, в этом разделе мы попытаемся объяснить функции всех исходных кодов в этом разделе на естественном языке, который легко понять. Пожалуйста, всегда помните: понимание программы — это понимание идей дизайна, а не понимание знания языка программирования.

Все исходные коды сервера, представленные здесь, основаны на версии коммита ветки в кодовой базе Github XTLS/REALITY (main 079d0bd). Все исходные коды клиентов, представленные здесь, основаны на версии коммита ветки в кодовой базе Github XTLS/Xray-core (main 4c9e4b9)

Клиент

Поскольку конструкция Xray-core такова, что клиент инициирует прокси соединение, начнем с клиента reality package:

Каждый пакет в Xray-core, отвечающий за инкапсуляцию и передачу прокси трафика, размещен в директории transport/internet:

xray-core/
|-- transport/
| |-- internet/
| | |-- reality/
| | | |-- config.go
| | | |-- config.pb.go
| | | |-- config.proto
| | | |-- reality.go

Три файла config.proto, config.pb.go и config.go в этом каталоге соответственно определяют формат поля protobuf3, передаваемого в пакет, определяют публичные методы и структуры для хранения конфигурации. В основном это конфигурации для REALITY

Ключом к инициированию соединения REALITY является reality.go функция UClient, которая вызывается, когда другие пакеты инициируют соединение REALITY, и является основной функциональной точкой входа для клиентской части пакета:

Xray-core/transport/internet/reality/reality.go#L106
func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error)

Как показано выше, UClient принимает доступный для чтения и записи сетевой потоковый интерфейс из пакета net, config для хранения конфигурации REALITY, контекст для управления таймаутами и структуру dest, указывающую назначение.

Одной из ключевых функций UClientявляется инициализация структуры, определенной в пакете UConn

 // Xray-core/transport/internet/reality/reality.go#L64-L69
 type UConn struct {
   *utls.UConn
   ServerName string
   AuthKey    []byte
   Verified   bool
 }
 
// Xray-core/transport/internet/reality/reality.go#L108-L115
uConn := &UConn{}
utlsConfig := &utls.Config{
  VerifyPeerCertificate:  uConn.VerifyPeerCertificate,
  ServerName:             config.ServerName,
  InsecureSkipVerify:     true,
  SessionTicketsDisabled: true,
  KeyLogWriter:           KeyLogWriterFromConfig(config),
}

// Xray-core/transport/internet/reality/reality.go#L124
uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint)

Первое поле UConn— это анонимное поле (то есть поля, имена которых по умолчанию соответствуют именам соответствующих типов), которое является указателем на экземпляр UConn из пакета utls. Этот код VerifyPeerCertificateнастраивает и инициализирует uConn, используемый для инициирования и обработки TLS рукопожатия на основе таких параметров, как SNI, идентификация finterprint в TLS Client Hello и функция, используемая для проверки действительности сертификата сервера и идентификатора сервера.

Теперь ключевой момент: клиенты REALITY используют пространство поля Session ID в TLS Client Hello для скрытой маркировки клиентов, чтобы серверы могли различать цензоров и законных клиентов REALITY. Поле Session ID изначально использовалось для механизма возобновления сеанса 0-RTT в TLS1.2. Хотя TLS1.3 переключился на механизм возобновления сеанса на основе PSK (Pre-shared Key), чтобы максимально сохранить совместимость с TLS1.2, поле Session ID было сохранено в TLS1.3 как поле без какой-либо функции, кроме совместимости. Поэтому Session ID, используемый для каждого соединения TLS1.3, должен генерироваться случайным образом.

Xray-core/transport/internet/reality/reality.go#L126-L135
 uConn.BuildHandshakeState()
 hello := uConn.HandshakeState.Hello
hello.SessionId = make([]byte, 32)
copy(hello.Raw[39:], hello.SessionId)
hello.SessionId[0] = core.Version_x
hello.SessionId[1] = core.Version_y
hello.SessionId[2] = core.Version_z
hello.SessionId[3] = 0
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))

copy(hello.SessionId[8:], config.ShortId)

// Xray-core/transport/internet/reality/reality.go#L139
publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey)

// Xray-core/transport/internet/reality/reality.go#L143
// Используйте пару ключей TLS клиента, сгенерированную BuildHandshakeState()
// Закрытый ключ x25519 и открытый ключ REALITY вводятся в алгоритм ECDH для вычисления общего ключа.
// Введите ключ в HKDF (функция вывода ключа на основе HMAC) для вычисления preMasterKey.
uConn.AuthKey, _ = uConn.HandshakeState.State13.EcdheKey.ECDH(publicKey)
// Xray-core/transport/internet/reality/reality.go#L147-L149
if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil {
  return nil, err
}

// Xray-core/transport/internet/reality/reality.go#L150-L156
// Выберите алгоритм AEAD: AES-GCM или Chacha20-Poly1305.
// Алгоритм AEAD обеспечивает безопасность, целостность и защиту от повторного использования данных зашифрованного текста.
// Также предоставьте гарантии целостности прикрепленных данных
var aead cipher.AEAD
if aesgcmPreferred(hello.CipherSuites) {
  block, _ := aes.NewCipher(uConn.AuthKey)
  aead, _ = cipher.NewGCM(block)
} else {
  aead, _ = chacha20poly1305.New(uConn.AuthKey)
}

// Xray-core/transport/internet/reality/reality.go#L160-L161
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)
copy(hello.Raw[39:], hello.SessionId)

На этом этапе клиент REALITY завершил собственную скрытую маркировку. Далее клиент инициирует TLS-соединение с сервером REALITY:

if err := uConn.HandshakeContext(ctx); err != nil {
  return nil, err
}

Сервер

Что касается исходного кода сервера REALITY, то это на самом деле вариант серверной реализации пакета crypto/tls в стандартной библиотеке Go 1.20. Поскольку сервер REALITY изменяет пакет crypto/tls по принципу минимальной модификации, в каталоге есть много файлов, которые напрямую не связаны с протоколом REALITY, поэтому дерево каталогов здесь не указано.

Ключ к обработке TLS рукопожатия сервером REALITY — это функция Server в файлеtls.go. Фактически, после внимательного прочтения исходного кода клиента в Xray-core, читатель должен иметь общее представление о ключевой логике сервера, различающего законных клиентов REALITY. В связи с этим, следующие комментарии к исходному коду будут относительно лаконичными, и я надеюсь, что читатели меня простят.

Так как же сервер REALITY сообщает легитимному клиенту REALITY, что он может передавать? Как сервер обрабатывает рукопожатие, инициированное нелегальным клиентом (цензором)? Для удобства описания вот вывод:

  1. Сервер REALITY пересылает ClientHello на сервер TLS dest (поддельный сервер), который содержит действительный сертификат, вносит минимальные изменения в ServerHello и Change Cipher Spec из dest и прикрепленной информации о шифровании, а затем пересылает его клиенту REALITY. Этот подход может завершить рукопожатие TLS так же, как и обычный сервер TLS, избегая генерации fingerprint TLS на стороне сервера.

  2. Когда сервер REALITY изменяет информацию о шифровании, прикрепленную после Change Cipher Spec, он заменяет все цифровые сертификаты на «временные сертификаты» и изменяет значение подписи «временного сертификата», чтобы клиент REALITY preMasterKey мог сравнить его с вычисленной подписью, чтобы сообщить клиенту, что передача может быть выполнена.

  3. Сервер REALITY пересылает весь трафик, за исключением трафика от законных клиентов REALITY на dest. Преимущества этого подхода такие же, как и 1.

// REALITY/blob/main/tls.go#L113
// Определение функции, где conn — это TCP-соединение, принятое сервером
func Server(ctx context.Context, conn net.Conn, config *Config) (*Conn, error)

// REALITY/blob/main/tls.go#L119
// TCP-соединение с dest (сервером) в конфигурации REALITY
target, err := config.DialContext(ctx, config.Type, config.Dest)

// REALITY/blob/main/tls.go#L139-L156
// Инициализация мьютекс (исключительная блокировка для задачи)
mutex := new(sync.Mutex)

// экземпляр serverHandshakeStateTLS13 для хранения информации о рукопожатии
hs := serverHandshakeStateTLS13{
  c: &Conn{
    conn: &MirrorConn{
      Mutex:  mutex,
      Conn:   conn,
      Target: target,
	},
	config: config,
  },
  ctx: context.Background(),
}

// Указывает, отправляет ли клиент нерелевантные данные, не завершив рукопожатие
copying := false

// Ждем завершения работы функции. Coroutine: легкий поток на языке Go
waitGroup := new(sync.WaitGroup)
waitGroup.Add(2)

// REALITY/blob/main/tls.go#L158-L223
// Запускаем функции параллельно
go func() {
  for {
    mutex.Lock()
    hs.clientHello, err = hs.c.readClientHello(context.Background())
    // Определяем
    // 1. Отправляет ли клиент данные, не завершив рукопожатие; 
    // 2. Ошибка чтения;
    // 3. Версия ниже TLS1.3; 
    // 4. SNI в ClientHello отсутствует в конфигурации
    // Если выполняется какое-либо условие, цикл завершается (break не перейдет к следующему циклу)
    // (Go использует безусловный for для представления бесконечного цикла)
    if copying || err != nil || hs.c.vers != VersionTLS13 || !config.ServerNames[hs.clientHello.serverName] {
      break
    }
    // Цикл для получения открытого ключа клиента x25519
    // TLS1.3 ClientHello содержит как можно больше открытых ключей 
    // и цифровых сертификатов для разных алгоритмов, чтобы 
    // избежать дополнительной задержки при передаче данных, 
    // связанной с запросом поддерживаемых алгоритмов в TLS1.2.
    for i, keyShare := range hs.clientHello.keyShares {
      // Определяем, является ли тип ключа x25519
      // и равна ли его длина 32 байтам. 
      // Это длина и тип открытого ключа, используемого клиентом REALITY. 
      // Если какое-либо из условий не выполняется, 
      // то продолжается следующий цикл.
      if keyShare.group != X25519 || len(keyShare.data) != 32 {
        continue
      }
      // Закрытый ключ REALITY и открытый ключ клиента в алгоритм 
      // ECDH для расчета общего ключа.
      if hs.c.AuthKey, err = curve25519.X25519(config.PrivateKey, keyShare.data); err != nil {
        break
      }
      // Ключ вводится в HKDF (функция вывода ключа на основе HMAC) 
      // для расчета preMasterKey
      if _, err = hkdf.New(sha256.New, hs.c.AuthKey, hs.clientHello.random[:20], []byte("REALITY")).Read(hs.c.AuthKey); err != nil {
        break
      }
      // Выбор алгоритма AEAD.
      var aead cipher.AEAD
      if aesgcmPreferred(hs.clientHello.cipherSuites) {
        block, _ := aes.NewCipher(hs.c.AuthKey)
        aead, _ = cipher.NewGCM(block)
      } else {
        aead, _ = chacha20poly1305.New(hs.c.AuthKey)
      }
      if config.Show {
        fmt.Printf("REALITY remoteAddr: %v\ths.c.AuthKey[:16]: %v\tAEAD: %T\n", remoteAddr, hs.c.AuthKey[:16], aead) //调试信息
      }
      // Инициализируем два 32-байтовых фрагмента 
      // для хранения зашифрованного и 
      // открытого текста соответственно.
      ciphertext := make([]byte, 32)
      plainText := make([]byte, 32)
      copy(ciphertext, hs.clientHello.sessionId)
      copy(hs.clientHello.sessionId, plainText)
      // Расшифровываем SessionId с помощью алгоритма AEAD
      if _, err = aead.Open(plainText[:0], hs.clientHello.random[20:], ciphertext, hs.clientHello.raw); err != nil {
        break
      }
      // Анализ версии Xray-core, содержащейся в SessionId, 
      // временной метки Unix и ShortId, содержащихся в SessionId
      copy(hs.clientHello.sessionId, ciphertext)
      copy(hs.c.ClientVer[:], plainText)
      hs.c.ClientTime = time.Unix(int64(binary.BigEndian.Uint32(plainText[4:])), 0)
      copy(hs.c.ClientShortId[:], plainText[8:])
      if config.Show {
        fmt.Printf("REALITY remoteAddr: %v\ths.c.ClientVer: %v\n", remoteAddr, hs.c.ClientVer)
        fmt.Printf("REALITY remoteAddr: %v\ths.c.ClientTime: %v\n", remoteAddr, hs.c.ClientTime)
        fmt.Printf("REALITY remoteAddr: %v\ths.c.ClientShortId: %v\n", remoteAddr, hs.c.ClientShortId) //调试信息
      }
      // Определяем, разрешена ли версия Xray-core, задержка по времени, ShortId
      if (config.MinClientVer == nil || Value(hs.c.ClientVer[:]...) >= Value(config.MinClientVer...)) &&
        (config.MaxClientVer == nil || Value(hs.c.ClientVer[:]...) <= Value(config.MaxClientVer...)) &&
        (config.MaxTimeDiff == 0 || time.Since(hs.c.ClientTime).Abs() <= config.MaxTimeDiff) &&
        (config.ShortIds[hs.c.ClientShortId]) {
        hs.c.conn = conn
      }
      // Определяет тип открытого ключа, 
      // используемого клиентом для последующего использования.
      hs.clientHello.keyShares[0].group = CurveID(i)
      break
    }
    if config.Show {
      fmt.Printf("REALITY remoteAddr: %v\ths.c.conn == conn: %v\n", remoteAddr, hs.c.conn == conn) //调试信息
    }
    break
  }
  mutex.Unlock()
  // Определяем, отправляет ли клиент 
  // нерелевантные данные, не завершив рукопожатие.
  if hs.c.conn != conn {
    if config.Show && hs.clientHello != nil {
      fmt.Printf("REALITY remoteAddr: %v\tforwarded SNI: %v\n", remoteAddr, hs.clientHello.serverName) //调试信息
    }
    // Перенаправляем соединение на dest 
    // в конфигурации (т.е. на поддельный сервер)
    io.Copy(target, underlying)
  }
  waitGroup.Done()
}(

До этого момента сервер выполнил задачу различения клиента. Но TLS рукопожатие еще не завершено?

В следующей части сервер REALITY пересылает ClientHello в dest, изменяет ServerHello, возвращенный dest, заменяет все цифровые сертификаты на «временные сертификаты», изменяет значение подписи «временного сертификата», а затем отправляет измененный ServerHello обратно законному клиенту REALITY, тем самым завершая рукопожатие TLS и информируя клиента о том, что он может передавать трафик обхода. Поскольку «временный сертификат» не подписан доверенным CA клиента, клиент REALITY подтверждает личность сервера, проверяя значение подписи цифрового сертификата.

// REALITY/blob/main/tls.go#L225-L349
// Запускаем функцию для установления связи с сервером REALITY и связи с dest
go func() {
// Инициализируем два фрагмента размером = 8192 в длину 
// s2cSaved используется для хранения всех полученных 
// данных из dest 
// buf — это буфер от сервера REALITY до dest
 s2cSaved := make([]byte, 0, size)
 buf := make([]byte, size)
 handshakeLen := 0
f:
  for {
    // Временно отказываемся от использования процессорного 
    // времени для оптимизации производительности программы
    runtime.Gosched()
    // Читаем данные, полученные из dest, в buf (buf будет очищен)
    n, err := target.Read(buf)
    // Определяем, были ли получены данные от dest
    if n == 0 {
      if err != nil {
        conn.Close()
        waitGroup.Done()
        return
      }
      continue
    }

    mutex.Lock()

    s2cSaved = append(s2cSaved, buf[:n]...)
    // Определяем, отправляет ли клиент нерелевантные данные, 
    // не завершив рукопожатие.
    if hs.c.conn != conn {
      // УКазываем, что клиент отправил нерелевантные данные, 
      // не завершив рукопожатие.
      copying = true
      break
    }
    // Определяем, не слишком ли большая длина s2cSaved
    if len(s2cSaved) > size {
      break
    }
    // Traverse type для определения типа пакета, возвращаемого dest
    // REALITY/blob/main/tls.go#L91-L99
    // types = [7]string{
    // "Server Hello",
    // "Change Cipher Spec",
    // "Encrypted Extensions",
    // "Certificate",
    // "Certificate Verify",
    // "Finished",
    // "New Session Ticket",
    // }
    for i, t := range types {
      // Проверяем, был ли отправлен ServerHello
      if hs.c.out.handshakeLen[i] != 0 {
        continue
      }
      // Определяем, является ли тип этого цикла 
      // "Новый сеанс"
      if i == 6 && len(s2cSaved) == 0 {
        break
      }
      // handshakeLen записывает длину пакета рукопожатия от dest. 
      // Здесь мы определяем, что его длина равна 0, 
      // а длина пакета рукопожатия от dest больше 5.
      // REALITY/blob/main/common.go#L63
      if handshakeLen == 0 && len(s2cSaved) > recordHeaderLen {
      // [1:3] указывает на информацию о версии 
      // пакета рукопожатия TLS. В целях совместимости значения 
      // 2-го и 3-го байтов ClientHello TLS 1.3 такие же, 
      // как и у TLS 1.2. В расширении присутствует метка версии 1.3. 
      // Здесь определяется, является ли пакет данных от dest ServerHello, 
      // ChangeCipherSpec или ApplicationData. 
      // Эти три типа пакетов данных должны появиться в рукопожатии.
        if Value(s2cSaved[1:3]...) != VersionTLS12 ||
          (i == 0 && (recordType(s2cSaved[0]) != recordTypeHandshake || s2cSaved[5] != typeServerHello)) ||
          (i == 1 && (recordType(s2cSaved[0]) != recordTypeChangeCipherSpec || s2cSaved[5] != 1)) ||
          (i > 1 && recordType(s2cSaved[0]) != recordTypeApplicationData) {
          break f
        }
        // [3:5] Указывают на информацию о длине пакета 
        // рукопожатия 4-5-го байта пакета рукопожатия TLS.
        handshakeLen = recordHeaderLen + Value(s2cSaved[3:5]...)
      }
      if config.Show {
        fmt.Printf("REALITY remoteAddr: %v\tlen(s2cSaved): %v\t%v: %v\n", remoteAddr, len(s2cSaved), t, handshakeLen) //调试信息
      }

      if handshakeLen > size {
        break f
      }
      // Определяем, является ли тип пакета рукопожатия Change Cipher Spec
      if i == 1 && handshakeLen > 0 && handshakeLen != 6 {
        break f
      }
      // Определите, является ли тип пакета рукопожатия 
      // Encrypted Extensions, который представляет собой 
      // зашифрованную часть, присоединенную после Change Cipher Spec
      if i == 2 && handshakeLen > 512 {
        hs.c.out.handshakeLen[i] = handshakeLen
        hs.c.out.handshakeBuf = buf[:0]
        break
      }
      // Определяем, является ли тип пакета рукопожатия новым сеансом
      if i == 6 && handshakeLen > 0 {
        hs.c.out.handshakeLen[i] = handshakeLen
        break
      }

      if handshakeLen == 0 || len(s2cSaved) < handshakeLen {
        mutex.Unlock()
        continue f
      }
      // Является ли тип пакета рукопожатия Server Hello
      if i == 0 {
        hs.hello = new(serverHelloMsg)
        // unmarshal используется для разбора ServerHello из dest и 
        // заполнения им нового пакета рукопожатия. 
        // Здесь мы проверяем, является ли пакет рукопожатия 
        // после анализа и заполнения допустимым.
        if !hs.hello.unmarshal(s2cSaved[recordHeaderLen:handshakeLen]) ||
          hs.hello.vers != VersionTLS12 || hs.hello.supportedVersion != VersionTLS13 ||
          cipherSuiteTLS13ByID(hs.hello.cipherSuite) == nil ||
          hs.hello.serverShare.group != X25519 || len(hs.hello.serverShare.data) != 32 {
          break f
        }
      }
      hs.c.out.handshakeLen[i] = handshakeLen
      s2cSaved = s2cSaved[handshakeLen:]
      handshakeLen = 0
    }
    start := time.Now()
    // Рукопожатие с клиентом REALITY, 
    // т.е. отправка измененного приветствия сервера, 
    // изменение спецификации шифра и зашифрованных расширений
    err = hs.handshake()
    if config.Show {
      fmt.Printf("REALITY remoteAddr: %v\ths.handshake() err: %v\n", remoteAddr, err) //调试信息
    }
    if err != nil {
      break
    }
    // Запускаем функцию для обработки соединения с dest. 
    // На этом этапе рукопожатие REALITY завершено, 
    // и соединение с dest больше не требуется
    go func() {
      if handshakeLen-len(s2cSaved) > 0 {
        io.ReadFull(target, buf[:handshakeLen-len(s2cSaved)])
      }
      if n, err := target.Read(buf); !hs.c.isHandshakeComplete.Load() {
        if err != nil {
          conn.Close()
        }
        if config.Show {
          fmt.Printf("REALITY remoteAddr: %v\ttime.Since(start): %v\tn: %v\terr: %v\n", remoteAddr, time.Since(start), n, err)
        }
      }
    }()
    err = hs.readClientFinished()
    if config.Show {
      fmt.Printf("REALITY remoteAddr: %v\ths.readClientFinished() err: %v\n", remoteAddr, err)
    }
    if err != nil {
      break
    }
    hs.c.isHandshakeComplete.Store(true)
    break
  }
  mutex.Unlock()
  // Проверяем, не отправил ли сервер REALITY ServerHello. 
  // Обычно это происходит, когда dest возвращает недопустимый 
  // ServerHello или до того, как dest возвращает ServerHello.
  if hs.c.out.handshakeLen[0] == 0 {
    if hs.c.conn == conn {
      waitGroup.Add(1)
      go func() {
        io.Copy(target, underlying)
        waitGroup.Done()
      }()
    }
    conn.Write(s2cSaved)
    io.Copy(underlying, target)
    underlying.CloseWrite()
  }
  waitGroup.Done()
}(

Далее сервер случайным образом генерирует «временный сертификат» в формате ed25519 и заменяет часть подписи цифрового сертификата значением, полученным путем использования в качестве ключа и открытого ключа preMasterKey «временного доверенного сертификата» в качестве входных данных алгоритма HMAC.

// REALITY/blob/main/handshake_server_tls13.go#L55-L59
func init() {
    // Шаблон сертификата x509
  certificate := x509.Certificate{SerialNumber: &big.Int{}}
    // Генерируем 64-байтовый закрытый ключ ed25519
  _, ed25519Priv, _ = ed25519.GenerateKey(rand.Reader)
    // Генерируем временный сертификат x509, 
    // используя предыдущий шаблон
  // Байты 33-64 закрытого ключа ed25519Priv 
  // используются как открытый ключ временного сертификата
  signedCert, _ = x509.CreateCertificate(rand.Reader, &certificate, &certificate, ed25519.PublicKey(ed25519Priv[32:]), ed25519Priv)
}

// REALITY/blob/main/handshake_server_tls13.go#L74-L85
// Вычисляем preMasterKey и присваиваем 
// вычисленное значение ключа hs.sharedKey
{
  hs.suite = cipherSuiteTLS13ByID(hs.hello.cipherSuite)
  c.cipherSuite = hs.suite.id
  hs.transcript = hs.suite.hash.New()
  
  key, _ := generateECDHEKey(c.config.rand(), X25519)
  copy(hs.hello.serverShare.data, key.PublicKey().Bytes())
  peerKey, _ := key.Curve().NewPublicKey(hs.clientHello.keyShares[hs.clientHello.keyShares[0].group].data)
  hs.sharedKey, _ = key.ECDH(peerKey)

  c.serverName = hs.clientHello.serverName
}

// REALITY/blob/main/handshake_server_tls13.go#L94-L106
// Изменяем значение подписи временного сертификата
{
  // Сохраняем временный сертификат в массиве сертификатов
  signedCert := append([]byte{}, signedCert...)

  // Инициализируем объект вычисления hmac и используем preMasterKey в качестве ключа
  h := hmac.New(sha512.New, c.AuthKey)
  h.Write(ed25519Priv[32:])
  h.Sum(signedCert[:len(signedCert)-64])

  // Создаем полный объект сертификата
  hs.cert = &Certificate{
    Certificate: [][]byte{signedCert},
    PrivateKey:  ed25519Priv,
  }
  // Алгоритм подписи — ed25519
  hs.sigAlg = Ed25519
}

На этом этапе сервер REALITY завершил рукопожатие с клиентом.

После того, как сервер REALITY возвращает соединение, вызывающая сторона (обычно это прокси-протокол верхнего уровня, такой как VLESS) может передавать трафик обхода через публичный API, предоставляемый пакетом REALITY, который является тем же, что и crypto/tls. Последующая передача трафика точно такая же, как и crypto/tls.

Проверяем подлинность сервера

Обычно после проверки подлинности сервера клиент REALITY отправляет TLS Finished, тем самым завершая рукопожатие TLS с сервером REALITY и начиная обход передачи трафика. Ниже приведено краткое объяснение ключевой логики проверки клиентом значения подписи сертификата сервера.

Заключение

В этой статье мы узнали о нормальном процессе рукопожатия протокола TLS 1.3 без включенного ECH. На основе этого мы проанализировали исходный код клиента и сервера REALITY и получили представление о конкретной реализации протокола REALITY для обхода стратегии цензуры на основе SNI.

Теги:
Хабы:
+6
Комментарии2

Публикации

Ближайшие события