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

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