Протокол 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 Hash | 56 байт | Hex-кодированный SHA-224 от открытого пароля |
| CRLF | 2 байта | \r\n (0x0D 0x0A) |
| Command | 1 байт | 0x01 = TCP Connect, 0x03 = UDP Associate |
| Address | переменный | В стиле SOCKS5: AddrType(1B) + Address + Port(2B, BE) |
| CRLF | 2 байта | \r\n (0x0D 0x0A) |
Типы адресов (аналогично SOCKS5):
| Байт | Тип |
|---|---|
0x01 | IPv4 (4 байта) |
0x03 | Домен (1 байт длины + строка) |
0x04 | IPv6 (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) |
| Length | 2 байта | Big-endian uint16, длина полезной нагрузки (максимум 8192) |
| CRLF | 2 байта | \r\n |
| Payload | Length байт | UDP-датаграмма |
PacketReader на стороне сервера читает каждый фреймированный пакет и прикрепляет назначение как buffer.UDP:
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-строки:
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.MemoryUseremail: сопоставляет email в нижнем регистре ->*protocol.MemoryUser
Исходный код: proxy/trojan/validator.go:12-82
Входящий обработчик (сервер)
Файл: proxy/trojan/server.go
Обработка соединений
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 байт) и выполняет быструю валидацию:
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 поддерживает три уровня сопоставления:
- SNI (
name): Сопоставляется с TLS ServerName - ALPN (
alpn): Сопоставляется с согласованным протоколом ALPN - Путь (
path): Сопоставляется с путём HTTP-запроса (извлекается из первых байт)
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-адреса клиента бэкенду:
// 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
Клиентский обработчик:
- Подключается к серверу через настроенный транспорт (обычно TLS)
- Создаёт
ConnWriter, который лениво записывает заголовок при первом вызовеWrite() - Для UDP оборачивает в
PacketWriterдля попакетного фрейминга - Отправляет первую полезную нагрузку вместе с заголовком для оптимизации 0-RTT
connWriter := &ConnWriter{
Writer: bufferWriter,
Target: destination,
Account: account,
}
// Первая запись инициирует отправку заголовка
buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100)Исходный код: proxy/trojan/client.go:99-128
ConnWriter.writeHeader() собирает:
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
Примечания по реализации
Требование TLS: Trojan сам по себе не обеспечивает шифрования. Без TLS хеш пароля и весь трафик передаются в открытом виде. Протокол предназначен для использования исключительно с транспортом TLS или REALITY.
Fallback критически важен для скрытности: Когда валидный заголовок Trojan не найден, сервер должен перенаправить соединение на реальный веб-сервер. Это означает, что сканирование портов и зондирование не могут отличить сервер от обычного HTTPS-сайта.
Нет заголовка ответа: В отличие от VMess, заголовок от сервера к клиенту полностью отсутствует. Это упрощает реализацию, но означает, что клиент не может обнаружить серверные ошибки на уровне протокола.
Максимальный размер UDP-полезной нагрузки: Каждый UDP-фрейм ограничен 8192 байтами (
maxLength = 8192). Пакеты, превышающие этот размер, отклоняются.ConnReader с сохранением состояния:
ConnReader.ParseHeader()вызывается только один раз. После этогоRead()передаёт данные напрямую нижележащему читателю. ФлагheaderParsedпредотвращает повторный разбор.Поддержка сетей: Сервер поддерживает
net.Network_TCPиnet.Network_UNIX, но не необработанный UDP. UDP-трафик туннелируется в виде фреймированных данных внутри TCP/TLS-соединения.
Исходный код: proxy/trojan/server.go:144-146