Skip to content

Протокол Trojan

Trojan — это протокол, разработанный так, чтобы быть неотличимым от обычного TLS/HTTPS-трафика. Он полностью полагается на транспортный уровень TLS для шифрования — сам протокол передаёт аутентификацию и команды в открытом виде через TLS-туннель. Когда поступает невалидный запрос, сервер может «откатиться» к реальному веб-серверу, что делает прокси необнаружимым.

Обзор

  • Направление: Входящий + Исходящий
  • Транспорт: TCP, UNIX-сокет (должен использовать транспорт TLS/REALITY)
  • Шифрование: Нет (делегировано TLS транспортного уровня)
  • Аутентификация: Hex-строка SHA224(пароль)
  • Mux: Не поддерживается нативно (но возможна обёртка XUDP через другие уровни)
  • Fallback: Встроенный многоуровневый fallback (SNI, ALPN, путь)

Формат передачи данных

TCP-запрос (от клиента к серверу)

+----------------------------------------------+------+-----+----------+------+
| SHA224(password) hex                         | CRLF | Cmd | Address  | CRLF |
| 56 bytes ASCII                               | 2B   | 1B  | variable | 2B   |
+----------------------------------------------+------+-----+----------+------+
| Payload ...                                                                 |
+-----------------------------------------------------------------------------+

Исходный код: proxy/trojan/protocol.go:64-95

ПолеРазмерОписание
Password Hash56 байтHex-кодированный SHA-224 от открытого пароля
CRLF2 байта\r\n (0x0D 0x0A)
Command1 байт0x01 = TCP Connect, 0x03 = UDP Associate
AddressпеременныйВ стиле SOCKS5: AddrType(1B) + Address + Port(2B, BE)
CRLF2 байта\r\n (0x0D 0x0A)

Типы адресов (аналогично SOCKS5):

БайтТип
0x01IPv4 (4 байта)
0x03Домен (1 байт длины + строка)
0x04IPv6 (16 байт)

Исходный код: proxy/trojan/protocol.go:16-21

После заголовка оставшиеся данные в TCP-потоке представляют собой необработанную полезную нагрузку.

TCP-ответ

Сервер отправляет необработанную полезную нагрузку обратно — без заголовка ответа. Это объясняется тем, что протокол разработан так, чтобы выглядеть как обычный TLS-трафик.

UDP-фрейминг

Когда команда — UDP (0x03), полезная нагрузка фреймируется попакетно:

+----------+---------+------+---------+
| Address  | Length  | CRLF | Payload |
| variable | 2B BE   | 2B   | Length  |
+----------+---------+------+---------+
| next packet ...                     |
+-------------------------------------+

Исходный код: proxy/trojan/protocol.go:125-150

ПолеРазмерОписание
AddressпеременныйВ стиле SOCKS5: AddrType(1B) + Address + Port(2B, BE)
Length2 байтаBig-endian uint16, длина полезной нагрузки (максимум 8192)
CRLF2 байта\r\n
PayloadLength байтUDP-датаграмма

PacketReader на стороне сервера читает каждый фреймированный пакет и прикрепляет назначение как buffer.UDP:

go
func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
    addr, port, err := addrParser.ReadAddressPort(nil, r)
    // ...
    remain := int(binary.BigEndian.Uint16(lengthBuf[:]))
    if remain > maxLength { return nil, errors.New("oversize payload") }
    // чтение CRLF, затем чтение `remain` байт полезной нагрузки
}

Исходный код: proxy/trojan/protocol.go:220-262

Аутентификация по паролю

Пароль хешируется SHA-224 и кодируется в hex для получения 56-байтовой ASCII-строки:

go
func hexSha224(password string) []byte {
    buf := make([]byte, 56)
    hash := sha256.New224()
    hash.Write([]byte(password))
    hex.Encode(buf, hash.Sum(nil))
    return buf
}

Исходный код: proxy/trojan/config.go:43-49

MemoryAccount хранит как открытый пароль, так и предварительно вычисленный 56-байтовый hex-ключ.

Валидатор

Validator использует два экземпляра sync.Map:

  • users: сопоставляет hex-кодированный хеш SHA224 -> *protocol.MemoryUser
  • email: сопоставляет email в нижнем регистре -> *protocol.MemoryUser

Исходный код: proxy/trojan/validator.go:12-82

Входящий обработчик (сервер)

Файл: proxy/trojan/server.go

Обработка соединений

mermaid
sequenceDiagram
    participant C as Клиент
    participant S as Сервер
    participant F as Fallback

    C->>S: TLS ClientHello
    S->>C: TLS ServerHello + Сертификат
    C->>S: SHA224(password) + CRLF + Команда + Адрес + CRLF + Полезная нагрузка

    alt Валидный пароль
        S->>S: Разбор команды и адреса
        alt TCP-команда
            S->>C: Проксированный ответ в сыром виде
        else UDP-команда
            S->>C: Фреймированные UDP-пакеты
        end
    else Невалидные или неполные данные
        S->>F: Перенаправление всего соединения на fallback
    end

Сервер читает первый буфер (до buf.Size байт) и выполняет быструю валидацию:

go
if firstLen < 58 || first.Byte(56) != '\r' {
    // Не протокол trojan - fallback
    shouldFallback = true
} else {
    user = s.validator.Get(hexString(first.BytesTo(56)))
    if user == nil {
        shouldFallback = true
    }
}

Исходный код: proxy/trojan/server.go:176-201

Ключевое наблюдение: первые 56 байт должны быть валидным hex-хешем SHA224, за которым следует \r на позиции 56. Это проверяется до полного разбора заголовка.

Механизм Fallback

Когда fallback настроен, невалидные соединения прозрачно перенаправляются на другой сервер. Система fallback поддерживает три уровня сопоставления:

  1. SNI (name): Сопоставляется с TLS ServerName
  2. ALPN (alpn): Сопоставляется с согласованным протоколом ALPN
  3. Путь (path): Сопоставляется с путём HTTP-запроса (извлекается из первых байт)
go
type Fallback struct {
    Name string  // SNI match
    Alpn string  // ALPN match
    Path string  // HTTP path match
    Dest string  // Destination address (e.g., "127.0.0.1:8080")
    Type string  // Network type ("tcp" or "unix")
    Xver uint64  // PROXY protocol version (0=none, 1=v1, 2=v2)
}

Структура данных fallback — это 3-уровневая вложенная карта: map[name]map[alpn]map[path]*Fallback

Исходный код: proxy/trojan/server.go:66-113

Обработчик fallback также поддерживает PROXY-протокол (v1 текстовый и v2 бинарный) для передачи реального IP-адреса клиента бэкенду:

go
// PROXY protocol v1
"PROXY TCP4 " + remoteAddr + " " + localAddr + " " + remotePort + " " + localPort + "\r\n"

// PROXY protocol v2
"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"  // signature
"\x21\x11\x00\x0C"                                      // v2 + PROXY + AF_INET + STREAM + 12 bytes

Исходный код: proxy/trojan/server.go:493-522

Исходящий обработчик (клиент)

Файл: proxy/trojan/client.go

Клиентский обработчик:

  1. Подключается к серверу через настроенный транспорт (обычно TLS)
  2. Создаёт ConnWriter, который лениво записывает заголовок при первом вызове Write()
  3. Для UDP оборачивает в PacketWriter для попакетного фрейминга
  4. Отправляет первую полезную нагрузку вместе с заголовком для оптимизации 0-RTT
go
connWriter := &ConnWriter{
    Writer:  bufferWriter,
    Target:  destination,
    Account: account,
}
// Первая запись инициирует отправку заголовка
buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100)

Исходный код: proxy/trojan/client.go:99-128

ConnWriter.writeHeader() собирает:

go
buffer.Write(c.Account.Key)   // 56-byte SHA224 hex
buffer.Write(crlf)            // \r\n
buffer.WriteByte(command)     // 0x01 or 0x03
addrParser.WriteAddressPort(&buffer, c.Target.Address, c.Target.Port)
buffer.Write(crlf)            // \r\n

Исходный код: proxy/trojan/protocol.go:64-95

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

  1. Требование TLS: Trojan сам по себе не обеспечивает шифрования. Без TLS хеш пароля и весь трафик передаются в открытом виде. Протокол предназначен для использования исключительно с транспортом TLS или REALITY.

  2. Fallback критически важен для скрытности: Когда валидный заголовок Trojan не найден, сервер должен перенаправить соединение на реальный веб-сервер. Это означает, что сканирование портов и зондирование не могут отличить сервер от обычного HTTPS-сайта.

  3. Нет заголовка ответа: В отличие от VMess, заголовок от сервера к клиенту полностью отсутствует. Это упрощает реализацию, но означает, что клиент не может обнаружить серверные ошибки на уровне протокола.

  4. Максимальный размер UDP-полезной нагрузки: Каждый UDP-фрейм ограничен 8192 байтами (maxLength = 8192). Пакеты, превышающие этот размер, отклоняются.

  5. ConnReader с сохранением состояния: ConnReader.ParseHeader() вызывается только один раз. После этого Read() передаёт данные напрямую нижележащему читателю. Флаг headerParsed предотвращает повторный разбор.

  6. Поддержка сетей: Сервер поддерживает net.Network_TCP и net.Network_UNIX, но не необработанный UDP. UDP-трафик туннелируется в виде фреймированных данных внутри TCP/TLS-соединения.

Исходный код: proxy/trojan/server.go:144-146

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