Протокол Shadowsocks
Xray-core реализует два поколения Shadowsocks: классические AEAD-шифры (оригинальный Shadowsocks) в proxy/shadowsocks/ и современный протокол Shadowsocks 2022 (через библиотеку sing-shadowsocks) в proxy/shadowsocks_2022/.
Обзор
| Возможность | Классический SS | Shadowsocks 2022 |
|---|---|---|
| Входящий | Да | Да (одиночный + многопользовательский + ретрансляция) |
| Исходящий | Да | Да |
| TCP | Да | Да |
| UDP | Да | Да |
| Шифры | AES-128/256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305, None | 2022-blake3-aes-128/256-gcm, 2022-blake3-chacha20-poly1305 |
| Многопользовательский режим | Да (только AEAD) | Да (с отдельными PSK) |
| Защита от повторов | Зерно поведения для сброса данных | Встроенная (библиотека sing) |
| Формат ключа | Пароль (получение через MD5) | Предварительно распределённый ключ в base64 |
Классический Shadowsocks
Формат передачи данных — TCP-поток
+---------+-------------------+-------------------+
| [IV] | Encrypted Header | Encrypted Payload |
| 0-32B | (chunked AEAD) | (chunked AEAD) |
+---------+-------------------+-------------------+Заголовок (внутри первого зашифрованного блока):
+----------+----------+------+
| AddrType | Address | Port |
| 1 byte | variable | 2B |
+----------+----------+------+Исходный код: proxy/shadowsocks/protocol.go:57-131
Типы адресов:
| Байт (нижние 4 бита) | Тип |
|---|---|
0x01 | IPv4 (4 байта) |
0x03 | Домен (1 байт длины + строка) |
0x04 | IPv6 (16 байт) |
Примечание: парсер адресов использует пользовательский парсер типов, маскирующий верхние 4 бита: b & 0x0F
Исходный код: proxy/shadowsocks/protocol.go:24-31
Формат передачи данных — UDP-пакет
Каждый UDP-пакет шифруется независимо:
+---------+----------+----------+------+---------+----------+
| IV | AddrType | Address | Port | Payload | Auth Tag |
| 0-32B | 1B | variable | 2B | ... | 16B |
+---------+----------+----------+------+---------+----------+Исходный код: proxy/shadowsocks/protocol.go:207-228
Реализации шифров
AEAD-шифры
Все AEAD-шифры следуют одному и тому же паттерну:
- IV: Случайные байты (размер зависит от шифра) добавляются в начало потока
- Получение подключа: HKDF-SHA1 с IV в качестве соли:
HKDF(key, iv, "ss-subkey") -> subkey - Аутентифицированные блоки: Каждый блок —
[encrypted_length(2B + 16B tag)] [encrypted_payload(N + 16B tag)] - Nonce: Автоинкрементируемый счётчик
func (c *AEADCipher) createAuthenticator(key, iv []byte) *crypto.AEADAuthenticator {
subkey := make([]byte, c.KeyBytes)
hkdfSHA1(key, iv, subkey)
aead := c.AEADAuthCreator(subkey)
nonce := crypto.GenerateAEADNonceWithSize(aead.NonceSize())
return &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: nonce,
}
}Исходный код: proxy/shadowsocks/config.go:138-147
| Шифр | Размер ключа | Размер IV | AEAD |
|---|---|---|---|
AES_128_GCM | 16 | 16 | AES-128-GCM |
AES_256_GCM | 32 | 32 | AES-256-GCM |
CHACHA20_POLY1305 | 32 | 32 | ChaCha20-Poly1305 |
XCHACHA20_POLY1305 | 32 | 32 | XChaCha20-Poly1305 |
NONE | 0 | 0 | Нет (открытый текст) |
Исходный код: proxy/shadowsocks/config.go:62-93
Получение ключа из пароля
Классический Shadowsocks получает ключи шифрования из паролей с помощью итеративного MD5:
func passwordToCipherKey(password []byte, keySize int32) []byte {
key := make([]byte, 0, keySize)
md5Sum := md5.Sum(password)
key = append(key, md5Sum[:]...)
for int32(len(key)) < keySize {
md5Hash := md5.New()
md5Hash.Write(md5Sum[:])
md5Hash.Write(password)
md5Hash.Sum(md5Sum[:0])
key = append(key, md5Sum[:]...)
}
return key
}Исходный код: proxy/shadowsocks/config.go:213-228
Получение подключа через HKDF
func hkdfSHA1(secret, salt, outKey []byte) {
r := hkdf.New(sha1.New, secret, salt, []byte("ss-subkey"))
io.ReadFull(r, outKey)
}Исходный код: proxy/shadowsocks/config.go:230-233
Многопользовательская поддержка
Validator перебирает всех зарегистрированных пользователей, пытаясь расшифровать AEAD с ключом каждого пользователя:
func (v *Validator) Get(bs []byte, command RequestCommand) (...) {
for _, user := range v.users {
account := user.Account.(*MemoryAccount)
if account.Cipher.IsAEAD() {
aeadCipher := account.Cipher.(*AEADCipher)
iv := bs[:ivLen]
subkey := hkdfSHA1(account.Key, iv, ...)
aead := aeadCipher.AEADAuthCreator(subkey)
// Попытка расшифровать первый блок
ret, matchErr = aead.Open(data[:0], nonce, bs[ivLen:ivLen+18], nil)
if matchErr == nil { return user }
}
}
}Исходный код: proxy/shadowsocks/validator.go:112-154
Ограничение: Не-AEAD шифры (None) поддерживают только одного пользователя, поскольку отсутствует тег аутентификации для сопоставления.
Исходный код: proxy/shadowsocks/validator.go:33-35
Зерно поведения для сброса данных
Аналогично VMess, Shadowsocks использует детерминированный сброс данных — считывание случайного количества данных перед закрытием невалидных соединений для предотвращения зондирования:
hashkdf := hmac.New(sha256.New, []byte("SSBSKDF"))
hashkdf.Write(account.Key)
behaviorSeed = crc64.Update(behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil))Исходный код: proxy/shadowsocks/validator.go:39-41
Shadowsocks 2022
Shadowsocks 2022 — это полная переработка с улучшенными свойствами безопасности. Xray-core делегирует реализацию протокола библиотеке sing-shadowsocks (github.com/sagernet/sing-shadowsocks).
Ключевые отличия от классического
- Предварительно распределённые ключи вместо паролей — ключи в кодировке base64 и должны точно соответствовать размеру ключа шифра
- Защита от повторов встроена в протокол (на основе временных меток)
- Раздельное шифрование заголовка и полезной нагрузки с разными nonce
- Многопользовательский режим использует серверный PSK + PSK для каждого пользователя (EIH — Encrypted Identity Header)
Поддерживаемые методы
Доступные методы из shadowaead_2022.List:
2022-blake3-aes-128-gcm2022-blake3-aes-256-gcm2022-blake3-chacha20-poly1305
Однопользовательский входящий
Файл: proxy/shadowsocks_2022/inbound.go
service, err := shadowaead_2022.NewServiceWithPassword(config.Method, config.Key, 500, inbound, nil)Исходный код: proxy/shadowsocks_2022/inbound.go:55-58
Сервис обрабатывает:
- TCP:
service.NewConnection(ctx, connection, metadata) - UDP:
service.NewPacket(ctx, pc, packet, metadata)
Многопользовательский входящий
Файл: proxy/shadowsocks_2022/inbound_multi.go
Использует shadowaead_2022.NewMultiService[int] с серверным PSK (в кодировке base64) и паролями для каждого пользователя:
service, err := shadowaead_2022.NewMultiService[int](config.Method, psk, 500, inbound, nil)
service.UpdateUsersWithPasswords(indices, passwords)Исходный код: proxy/shadowsocks_2022/inbound_multi.go:76-86
Идентификация пользователей происходит через внутренний механизм EIH (Encrypted Identity Header) библиотеки sing.
Исходящий
Файл: proxy/shadowsocks_2022/outbound.go
method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil)
// TCP
serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
// UDP
serverConn := o.method.DialPacketConn(connection)Исходный код: proxy/shadowsocks_2022/outbound.go:47-57, proxy/shadowsocks_2022/outbound.go:98-155
UDP поверх TCP (UoT)
Исходящий поддерживает туннелирование UDP через TCP при включённом UdpOverTcp:
if config.UdpOverTcp {
o.uotClient = &uot.Client{Version: uint8(config.UdpOverTcpVersion)}
}Исходный код: proxy/shadowsocks_2022/outbound.go:58-60
Входящий обработчик (сервер классического SS)
Файл: proxy/shadowsocks/server.go
Сервер обрабатывает как TCP, так и UDP:
func (s *Server) Network() []net.Network {
list := s.config.Network
if len(list) == 0 {
list = append(list, net.Network_TCP)
}
return list
}Исходный код: proxy/shadowsocks/server.go:81-87
Поток TCP: Чтение зашифрованного заголовка, сопоставление пользователя через Validator.Get(), расшифровка адреса, диспетчеризация.
Поток UDP: Каждый UDP-пакет шифруется независимо. Сервер декодирует каждый пакет, извлекает назначение и выполняет диспетчеризацию через udp.Dispatcher.
Исходящий обработчик (клиент классического SS)
Файл: proxy/shadowsocks/client.go
Для TCP:
- Генерация случайного IV, запись в соединение
- Создание записывателя шифрования через
account.Cipher.NewEncryptionWriter() - Запись заголовка адреса в стиле SOCKS5
- Потоковая передача полезной нагрузки через записыватель шифрования
Для UDP:
- Каждый исходящий пакет независимо шифруется с помощью
EncodeUDPPacket() - Пакеты ответов декодируются с помощью
DecodeUDPPacket()
Исходный код: proxy/shadowsocks/client.go:48-195
Примечания по реализации
Устаревание поточных шифров: Тип
NoneCipherсуществует для обратной совместимости, но не обеспечивает шифрования. Поточные шифры (RC4, ChaCha20 без Poly1305) полностью удалены.Уникальность IV: Хотя в коде есть тип ошибки
ErrIVNotUnique, проверка IV закомментирована в валидаторе (validator.go:148). Классический протокол Shadowsocks в Xray полагается на тег AEAD для аутентификации, а не на отслеживание IV.Маскирование типа адреса: Парсер адресов Shadowsocks маскирует верхние 4 бита байта типа адреса (
b & 0x0F), что позволяет встраивать дополнительные флаги в старшие биты.Cone NAT: Серверы как классического, так и 2022 поддерживают режим «cone» для UDP. При включении последующие пакеты от того же клиента повторно используют первое назначение для диспетчеризации, эмулируя поведение NAT.
Режим ретрансляции: Shadowsocks 2022 имеет входящий обработчик ретрансляции (
inbound_relay.go), который позволяет создавать многоходовые цепочки ретрансляции, хотя это более специализированный сценарий использования.sing-bridge: Код Shadowsocks 2022 использует вспомогательные функции
singbridgeдля преобразования между типами библиотекиsing(такими какM.Socksaddr) и внутренними типами Xray-core (такими какnet.Destination).