Протокол ShadowTLS
ShadowTLS — это протокол транспортного уровня, который маскирует прокси-трафик под легитимный TLS-трафик, перехватывая TLS-рукопожатие с реальным сервером. Поддерживает три версии протокола с возрастающей сложностью.
Исходный код: protocol/shadowtls/inbound.go, protocol/shadowtls/outbound.go, sing-shadowtls
Концепция протокола
В отличие от традиционных прокси на основе TLS, которые генерируют собственные сертификаты (обнаруживаемые при проверке сертификатов), ShadowTLS выполняет реальное TLS-рукопожатие с легитимным сервером (например, www.microsoft.com), делая рукопожатие неотличимым от обычного HTTPS-трафика для наблюдателей. После рукопожатия канал данных перехватывается для передачи прокси-трафика.
Версии протокола
Версия 1
Простейшая версия. Клиент инициирует TLS-рукопожатие через сервер ShadowTLS, который ретранслирует его на реальный TLS-сервер («сервер рукопожатия»). После завершения рукопожатия TLS-соединение перепрофилируется для передачи прокси-данных.
Ограничение: Принудительно использует TLS 1.2 для обеспечения предсказуемого поведения рукопожатия.
if options.Version == 1 {
options.TLS.MinVersion = "1.2"
options.TLS.MaxVersion = "1.2"
}Версия 2
Добавляет аутентификацию на основе пароля. Сервер может отличить легитимных клиентов ShadowTLS от зондов. Поддерживает серверы рукопожатия по SNI:
if options.Version > 1 {
handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
for _, entry := range options.HandshakeForServerName.Entries() {
handshakeForServerName[entry.Key] = shadowtls.HandshakeConfig{
Server: entry.Value.ServerOptions.Build(),
Dialer: handshakeDialer,
}
}
}Версия 3
Наиболее продвинутая версия. Вводит привязку канала на основе session ID — клиент и сервер встраивают данные аутентификации в TLS session ID, обеспечивая верификацию без дополнительного обмена данными.
case 3:
if idConfig, loaded := tlsConfig.(tls.WithSessionIDGenerator); loaded {
// Использовать хук session ID библиотеки TLS
tlsHandshakeFunc = func(ctx, conn, sessionIDGenerator) error {
idConfig.SetSessionIDGenerator(sessionIDGenerator)
return tls.ClientHandshake(ctx, conn, tlsConfig)
}
} else {
// Фоллбэк на стандартный TLS с ручной инъекцией session ID
stdTLSConfig := tlsConfig.STDConfig()
tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(password, stdTLSConfig)
}Архитектура входящих соединений (Inbound)
type Inbound struct {
inbound.Adapter
router adapter.Router
logger logger.ContextLogger
listener *listener.Listener
service *shadowtls.Service
}Конфигурация сервиса
service, _ := shadowtls.NewService(shadowtls.ServiceConfig{
Version: options.Version,
Password: options.Password,
Users: common.Map(options.Users, func(it option.ShadowTLSUser) shadowtls.User {
return (shadowtls.User)(it)
}),
Handshake: shadowtls.HandshakeConfig{
Server: options.Handshake.ServerOptions.Build(),
Dialer: handshakeDialer,
},
HandshakeForServerName: handshakeForServerName, // маршрутизация по SNI
StrictMode: options.StrictMode,
WildcardSNI: shadowtls.WildcardSNI(options.WildcardSNI),
Handler: (*inboundHandler)(inbound),
Logger: logger,
})Ключевые поля:
- Handshake: Целевой сервер рукопожатия по умолчанию
- HandshakeForServerName: Карта SNI -> сервер рукопожатия для мультидоменной поддержки
- StrictMode: Отклонять соединения, не прошедшие аутентификацию (вместо молчаливой пересылки)
- WildcardSNI: Принимать любое значение SNI (полезно для сценариев с CDN)
Wildcard SNI
Опция WildcardSNI контролирует обработку SNI:
serverIsDomain := options.Handshake.ServerIsDomain()
if options.WildcardSNI != option.ShadowTLSWildcardSNIOff {
serverIsDomain = true // принудительное разрешение домена для wildcard
}Поток соединения (Inbound)
func (h *Inbound) NewConnectionEx(ctx, conn, metadata, onClose) {
// Сервис ShadowTLS обрабатывает всю ретрансляцию рукопожатия и извлечение данных
err := h.service.NewConnection(ctx, conn, metadata.Source, metadata.Destination, onClose)
N.CloseOnHandshakeFailure(conn, onClose, err)
}После того как сервис ShadowTLS извлекает реальный поток данных, он вызывает обработчик входящего соединения:
type inboundHandler Inbound
func (h *inboundHandler) NewConnectionEx(ctx, conn, source, destination, onClose) {
metadata.Inbound = h.Tag()
metadata.InboundType = h.Type()
metadata.Source = source
metadata.Destination = destination
if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
metadata.User = userName
}
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}Архитектура исходящих соединений (Outbound)
type Outbound struct {
outbound.Adapter
client *shadowtls.Client
}Исходящее соединение ShadowTLS работает только по TCP и служит обёрткой транспорта — обычно используется в цепочке с другим протоколом (например, Shadowsocks):
func (h *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
switch network {
case "tcp":
return h.client.DialContext(ctx) // возвращает «чистое» соединение
default:
return nil, os.ErrInvalid // UDP не поддерживается
}
}
func (h *Outbound) ListenPacket(ctx, destination) (net.PacketConn, error) {
return nil, os.ErrInvalid // UDP не поддерживается
}Требование TLS
Исходящее соединение ShadowTLS требует включения TLS:
if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired
}Конфигурация клиента
client, _ := shadowtls.NewClient(shadowtls.ClientConfig{
Version: options.Version,
Password: options.Password,
Server: options.ServerOptions.Build(),
Dialer: outboundDialer,
TLSHandshake: tlsHandshakeFunc, // рукопожатие, зависящее от версии
Logger: logger,
})TLS-рукопожатие в зависимости от версии
var tlsHandshakeFunc shadowtls.TLSHandshakeFunc
switch options.Version {
case 1, 2:
// Простой: просто выполнить TLS-рукопожатие
tlsHandshakeFunc = func(ctx, conn, _ TLSSessionIDGeneratorFunc) error {
return common.Error(tls.ClientHandshake(ctx, conn, tlsConfig))
}
case 3:
// Сложный: инъекция генератора session ID для привязки канала
tlsHandshakeFunc = func(ctx, conn, sessionIDGenerator) error {
idConfig.SetSessionIDGenerator(sessionIDGenerator)
return common.Error(tls.ClientHandshake(ctx, conn, tlsConfig))
}
}Как работает ShadowTLS (подробно)
Клиент Сервер ShadowTLS Реальный TLS-сервер
| | |
|--- TLS ClientHello ---->|--- TLS ClientHello ----->|
| | |
|<-- TLS ServerHello -----|<-- TLS ServerHello ------|
|<-- Certificate ---------|<-- Certificate ----------|
|<-- ServerHelloDone -----|<-- ServerHelloDone ------|
| | |
|--- ClientKeyExchange -->|--- ClientKeyExchange --->|
|--- ChangeCipherSpec --->|--- ChangeCipherSpec ---->|
|--- Finished ----------->|--- Finished ------------>|
| | |
|<-- ChangeCipherSpec ----|<-- ChangeCipherSpec -----|
|<-- Finished ------------|<-- Finished -------------|
| | |
| [TLS-рукопожатие завершено - наблюдатель видит |
| валидный сертификат] |
| | |
|=== Прокси-данные =======>| [данные НЕ отправляются |
|<=== Прокси-данные ========| реальному TLS-серверу] |После рукопожатия сервер ShadowTLS:
- Отключается от реального TLS-сервера
- Извлекает поток прокси-данных от клиента
- Перенаправляет его в настроенный внутренний обработчик
Типичный паттерн использования
ShadowTLS используется как detour для другого протокола:
{
"outbounds": [
{
"type": "shadowsocks",
"tag": "ss-out",
"detour": "shadowtls-out",
"method": "2022-blake3-aes-256-gcm",
"password": "ss-password"
},
{
"type": "shadowtls",
"tag": "shadowtls-out",
"server": "my-server.com",
"server_port": 443,
"version": 3,
"password": "shadowtls-password",
"tls": {
"enabled": true,
"server_name": "www.microsoft.com"
}
}
]
}Соединение Shadowsocks туннелируется через обёртку ShadowTLS, которая выполняет рукопожатие с реальным сертификатом www.microsoft.com.
Пример конфигурации (Inbound)
{
"type": "shadowtls",
"tag": "shadowtls-in",
"listen": "::",
"listen_port": 443,
"version": 3,
"users": [
{ "name": "user1", "password": "user-password" }
],
"handshake": {
"server": "www.microsoft.com",
"server_port": 443
},
"handshake_for_server_name": {
"www.google.com": {
"server": "www.google.com",
"server_port": 443
}
},
"strict_mode": true
}