Протокол Trojan
Trojan — это прокси-протокол, разработанный для маскировки под HTTPS-трафик. Он использует схему аутентификации на основе паролей с хешированием SHA-224 и поддерживает фоллбэк на реальный веб-сервер для нераспознанного трафика.
Исходный код: protocol/trojan/, transport/trojan/
Формат данных
Протокол Trojan использует простой, совместимый с TLS формат данных:
+----------+------+---------+----------+------+----------+
| Password | CRLF | Command | Address | CRLF | Payload |
| (56 hex) | \r\n | (1 byte)| (variable)|\r\n | (variable)|
+----------+------+---------+----------+------+----------+Получение пароля
Пароль преобразуется в 56-байтовый hex-кодированный хеш SHA-224:
const KeyLength = 56
func Key(password string) [KeyLength]byte {
var key [KeyLength]byte
hash := sha256.New224() // SHA-224, НЕ SHA-256
hash.Write([]byte(password))
hex.Encode(key[:], hash.Sum(nil)) // 28 байт -> 56 hex-символов
return key
}SHA-224 производит 28 байт (224 бита), которые hex-кодируются ровно в 56 символов. Они передаются как есть (не в base64) при рукопожатии.
Команды
const (
CommandTCP = 1 // TCP-подключение
CommandUDP = 3 // UDP-ассоциация
CommandMux = 0x7f // Мультиплексирование Trojan-Go
)TCP-рукопожатие
Client -> Server:
[56 bytes: hex SHA224(password)]
[2 bytes: \r\n]
[1 byte: 0x01 (TCP)]
[variable: SOCKS address (type + addr + port)]
[2 bytes: \r\n]
[payload data...]Реализация использует объединение буферов для эффективности:
func ClientHandshake(conn net.Conn, key [KeyLength]byte, destination M.Socksaddr, payload []byte) error {
headerLen := KeyLength + M.SocksaddrSerializer.AddrPortLen(destination) + 5
header := buf.NewSize(headerLen + len(payload))
header.Write(key[:]) // 56 байт хеша пароля
header.Write(CRLF) // \r\n
header.WriteByte(CommandTCP) // 0x01
M.SocksaddrSerializer.WriteAddrPort(header, destination)
header.Write(CRLF) // \r\n
header.Write(payload) // объединённая первая полезная нагрузка
conn.Write(header.Bytes()) // один системный вызов записи
}Формат UDP-пакетов
После начального рукопожатия (которое использует CommandUDP), UDP-пакеты фреймируются следующим образом:
+----------+--------+------+----------+
| Address | Length | CRLF | Payload |
| (variable)| (2 BE) | \r\n | (Length) |
+----------+--------+------+----------+func WritePacket(conn net.Conn, buffer *buf.Buffer, destination M.Socksaddr) error {
header := buf.With(buffer.ExtendHeader(...))
M.SocksaddrSerializer.WriteAddrPort(header, destination)
binary.Write(header, binary.BigEndian, uint16(bufferLen))
header.Write(CRLF)
conn.Write(buffer.Bytes())
}
func ReadPacket(conn net.Conn, buffer *buf.Buffer) (M.Socksaddr, error) {
destination := M.SocksaddrSerializer.ReadAddrPort(conn)
var length uint16
binary.Read(conn, binary.BigEndian, &length)
rw.SkipN(conn, 2) // пропустить CRLF
buffer.ReadFullFrom(conn, int(length))
return destination, nil
}Начальное UDP-рукопожатие
Первый UDP-пакет включает и заголовок Trojan, И адрес/длину первого пакета:
[56 bytes key][CRLF][0x03 UDP][dest addr][CRLF][dest addr][length][CRLF][payload]
^рукопожатие^ ^первый пакет^Обратите внимание, что адрес назначения появляется дважды: один раз в рукопожатии, один раз во фрейме пакета.
Сервисный уровень Trojan
transport/trojan/service.go реализует серверную обработку протокола:
type Service[K comparable] struct {
users map[K][56]byte // пользователь -> ключ
keys map[[56]byte]K // ключ -> пользователь (обратный поиск)
handler Handler // обработчик TCP + UDP
fallbackHandler N.TCPConnectionHandlerEx
logger logger.ContextLogger
}Обработка соединений на стороне сервера
func (s *Service[K]) NewConnection(ctx, conn, source, onClose) error {
// 1. Прочитать 56-байтовый ключ пароля
var key [KeyLength]byte
n, err := conn.Read(key[:])
if n != KeyLength {
return s.fallback(ctx, conn, source, key[:n], ...)
}
// 2. Аутентификация
if user, loaded := s.keys[key]; loaded {
ctx = auth.ContextWithUser(ctx, user)
} else {
return s.fallback(ctx, conn, source, key[:], ...)
}
// 3. Пропустить CRLF, прочитать команду
rw.SkipN(conn, 2)
binary.Read(conn, binary.BigEndian, &command)
// 4. Прочитать адрес назначения, пропустить завершающий CRLF
destination := M.SocksaddrSerializer.ReadAddrPort(conn)
rw.SkipN(conn, 2)
// 5. Диспетчеризация по команде
switch command {
case CommandTCP:
s.handler.NewConnectionEx(ctx, conn, source, destination, onClose)
case CommandUDP:
s.handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, ...)
default: // CommandMux (0x7f)
HandleMuxConnection(ctx, conn, source, s.handler, s.logger, onClose)
}
}Механизм фоллбэка
При неудачной аутентификации сервис поддерживает фоллбэк на реальный веб-сервер:
func (s *Service[K]) fallback(ctx, conn, source, header, err, onClose) error {
if s.fallbackHandler == nil {
return E.Extend(err, "fallback disabled")
}
// Вернуть уже прочитанные байты обратно в соединение
conn = bufio.NewCachedConn(conn, buf.As(header).ToOwned())
s.fallbackHandler.NewConnectionEx(ctx, conn, source, M.Socksaddr{}, onClose)
return nil
}Это критически важно для обхода цензуры: если зонд отправляет не-Trojan данные, они перенаправляются на реальный веб-сервер, делая сервис неотличимым от обычного HTTPS-сайта.
Поддержка мультиплексирования (Trojan-Go)
Реализация мультиплексирования использует smux (Simple Multiplexer) для совместимости с Trojan-Go:
func HandleMuxConnection(ctx, conn, source, handler, logger, onClose) error {
session, _ := smux.Server(conn, smuxConfig())
for {
stream, _ := session.AcceptStream()
go newMuxConnection(ctx, stream, source, handler, logger)
}
}Каждый mux-поток содержит собственный байт команды и адрес назначения:
func newMuxConnection0(ctx, conn, source, handler) error {
reader := bufio.NewReader(conn)
command, _ := reader.ReadByte()
destination, _ := M.SocksaddrSerializer.ReadAddrPort(reader)
switch command {
case CommandTCP:
handler.NewConnectionEx(ctx, conn, source, destination, nil)
case CommandUDP:
handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, ...)
}
}Конфигурация smux отключает keepalive:
func smuxConfig() *smux.Config {
config := smux.DefaultConfig()
config.KeepAliveDisabled = true
return config
}Реализация входящих соединений (Inbound)
type Inbound struct {
inbound.Adapter
router adapter.ConnectionRouterEx
logger log.ContextLogger
listener *listener.Listener
service *trojan.Service[int]
users []option.TrojanUser
tlsConfig tls.ServerConfig
fallbackAddr M.Socksaddr
fallbackAddrTLSNextProto map[string]M.Socksaddr // ALPN-фоллбэк
transport adapter.V2RayServerTransport
}ALPN-фоллбэк
Trojan поддерживает фоллбэк по ALPN, позволяя задавать разные цели фоллбэка в зависимости от согласованного TLS-протокола:
func (h *Inbound) fallbackConnection(ctx, conn, metadata, onClose) {
if len(h.fallbackAddrTLSNextProto) > 0 {
if tlsConn, loaded := common.Cast[tls.Conn](conn); loaded {
negotiatedProtocol := tlsConn.ConnectionState().NegotiatedProtocol
fallbackAddr = h.fallbackAddrTLSNextProto[negotiatedProtocol]
}
}
if !fallbackAddr.IsValid() {
fallbackAddr = h.fallbackAddr // фоллбэк по умолчанию
}
metadata.Destination = fallbackAddr
h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}Совместимость с kTLS
Входящее соединение включает kTLS (kernel TLS) при выполнении условий:
tlsConfig, _ := tls.NewServerWithOptions(tls.ServerOptions{
KTLSCompatible: transport.Type == "" && !multiplex.Enabled,
// kTLS только когда: нет V2Ray-транспорта И нет мультиплексирования
})Реализация исходящих соединений (Outbound)
type Outbound struct {
outbound.Adapter
key [56]byte // предварительно вычисленный ключ SHA224
multiplexDialer *mux.Client
tlsConfig tls.Config
tlsDialer tls.Dialer
transport adapter.V2RayClientTransport
}Ключ вычисляется один раз при создании:
outbound.key = trojan.Key(options.Password)Поток соединения
func (h *trojanDialer) DialContext(ctx, network, destination) (net.Conn, error) {
// 1. Установить соединение: транспорт > TLS > сырой TCP
var conn net.Conn
if h.transport != nil {
conn = h.transport.DialContext(ctx)
} else if h.tlsDialer != nil {
conn = h.tlsDialer.DialTLSContext(ctx, h.serverAddr)
} else {
conn = h.dialer.DialContext(ctx, "tcp", h.serverAddr)
}
// 2. Обернуть протоколом Trojan
switch network {
case "tcp":
return trojan.NewClientConn(conn, h.key, destination)
case "udp":
return bufio.NewBindPacketConn(
trojan.NewClientPacketConn(conn, h.key), destination)
}
}Ранние данные (Lazy Write)
ClientConn реализует N.EarlyWriter, что означает, что заголовок Trojan отправляется только при первом вызове Write(), объединённый с первой полезной нагрузкой:
func (c *ClientConn) Write(p []byte) (n int, err error) {
if c.headerWritten {
return c.ExtendedConn.Write(p)
}
err = ClientHandshake(c.ExtendedConn, c.key, c.destination, p)
c.headerWritten = true
n = len(p)
return
}Пример конфигурации
{
"type": "trojan",
"tag": "trojan-in",
"listen": "::",
"listen_port": 443,
"users": [
{ "name": "user1", "password": "my-secret-password" }
],
"tls": {
"enabled": true,
"server_name": "example.com",
"certificate_path": "/path/to/cert.pem",
"key_path": "/path/to/key.pem"
},
"fallback": {
"server": "127.0.0.1",
"server_port": 8080
},
"fallback_for_alpn": {
"h2": {
"server": "127.0.0.1",
"server_port": 8081
}
}
}