Skip to content

Протокол Shadowsocks

Xray-core реализует два поколения Shadowsocks: классические AEAD-шифры (оригинальный Shadowsocks) в proxy/shadowsocks/ и современный протокол Shadowsocks 2022 (через библиотеку sing-shadowsocks) в proxy/shadowsocks_2022/.

Обзор

ВозможностьКлассический SSShadowsocks 2022
ВходящийДаДа (одиночный + многопользовательский + ретрансляция)
ИсходящийДаДа
TCPДаДа
UDPДаДа
ШифрыAES-128/256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305, None2022-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 бита)Тип
0x01IPv4 (4 байта)
0x03Домен (1 байт длины + строка)
0x04IPv6 (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-шифры следуют одному и тому же паттерну:

  1. IV: Случайные байты (размер зависит от шифра) добавляются в начало потока
  2. Получение подключа: HKDF-SHA1 с IV в качестве соли: HKDF(key, iv, "ss-subkey") -> subkey
  3. Аутентифицированные блоки: Каждый блок — [encrypted_length(2B + 16B tag)] [encrypted_payload(N + 16B tag)]
  4. Nonce: Автоинкрементируемый счётчик
go
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

ШифрРазмер ключаРазмер IVAEAD
AES_128_GCM1616AES-128-GCM
AES_256_GCM3232AES-256-GCM
CHACHA20_POLY13053232ChaCha20-Poly1305
XCHACHA20_POLY13053232XChaCha20-Poly1305
NONE00Нет (открытый текст)

Исходный код: proxy/shadowsocks/config.go:62-93

Получение ключа из пароля

Классический Shadowsocks получает ключи шифрования из паролей с помощью итеративного MD5:

go
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

go
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 с ключом каждого пользователя:

go
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 использует детерминированный сброс данных — считывание случайного количества данных перед закрытием невалидных соединений для предотвращения зондирования:

go
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).

Ключевые отличия от классического

  1. Предварительно распределённые ключи вместо паролей — ключи в кодировке base64 и должны точно соответствовать размеру ключа шифра
  2. Защита от повторов встроена в протокол (на основе временных меток)
  3. Раздельное шифрование заголовка и полезной нагрузки с разными nonce
  4. Многопользовательский режим использует серверный PSK + PSK для каждого пользователя (EIH — Encrypted Identity Header)

Поддерживаемые методы

Доступные методы из shadowaead_2022.List:

  • 2022-blake3-aes-128-gcm
  • 2022-blake3-aes-256-gcm
  • 2022-blake3-chacha20-poly1305

Однопользовательский входящий

Файл: proxy/shadowsocks_2022/inbound.go

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) и паролями для каждого пользователя:

go
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

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:

go
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:

go
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:

  1. Генерация случайного IV, запись в соединение
  2. Создание записывателя шифрования через account.Cipher.NewEncryptionWriter()
  3. Запись заголовка адреса в стиле SOCKS5
  4. Потоковая передача полезной нагрузки через записыватель шифрования

Для UDP:

  1. Каждый исходящий пакет независимо шифруется с помощью EncodeUDPPacket()
  2. Пакеты ответов декодируются с помощью DecodeUDPPacket()

Исходный код: proxy/shadowsocks/client.go:48-195

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

  1. Устаревание поточных шифров: Тип NoneCipher существует для обратной совместимости, но не обеспечивает шифрования. Поточные шифры (RC4, ChaCha20 без Poly1305) полностью удалены.

  2. Уникальность IV: Хотя в коде есть тип ошибки ErrIVNotUnique, проверка IV закомментирована в валидаторе (validator.go:148). Классический протокол Shadowsocks в Xray полагается на тег AEAD для аутентификации, а не на отслеживание IV.

  3. Маскирование типа адреса: Парсер адресов Shadowsocks маскирует верхние 4 бита байта типа адреса (b & 0x0F), что позволяет встраивать дополнительные флаги в старшие биты.

  4. Cone NAT: Серверы как классического, так и 2022 поддерживают режим «cone» для UDP. При включении последующие пакеты от того же клиента повторно используют первое назначение для диспетчеризации, эмулируя поведение NAT.

  5. Режим ретрансляции: Shadowsocks 2022 имеет входящий обработчик ретрансляции (inbound_relay.go), который позволяет создавать многоходовые цепочки ретрансляции, хотя это более специализированный сценарий использования.

  6. sing-bridge: Код Shadowsocks 2022 использует вспомогательные функции singbridge для преобразования между типами библиотеки sing (такими как M.Socksaddr) и внутренними типами Xray-core (такими как net.Destination).

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