Skip to content

Trojan Protocol

Trojan is a protocol designed to be indistinguishable from normal TLS/HTTPS traffic. It relies entirely on the TLS transport layer for encryption -- the protocol itself transmits authentication and commands in plaintext over the TLS tunnel. When an invalid request arrives, the server can "fall back" to a real web server, making the proxy undetectable.

Overview

  • Direction: Inbound + Outbound
  • Transport: TCP, UNIX socket (must use TLS/REALITY transport)
  • Encryption: None (delegated to transport-layer TLS)
  • Authentication: SHA224(password) hex string
  • Mux: Not natively supported (but XUDP wrapping possible via other layers)
  • Fallback: Built-in multi-level fallback (SNI, ALPN, path)

Wire Format

TCP Request (Client to Server)

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

Source: proxy/trojan/protocol.go:64-95

FieldSizeDescription
Password Hash56 bytesHex-encoded SHA-224 of the plaintext password
CRLF2 bytes\r\n (0x0D 0x0A)
Command1 byte0x01 = TCP Connect, 0x03 = UDP Associate
AddressvariableSOCKS5-style: AddrType(1B) + Address + Port(2B, BE)
CRLF2 bytes\r\n (0x0D 0x0A)

Address types (same as SOCKS5):

ByteType
0x01IPv4 (4 bytes)
0x03Domain (1-byte length + string)
0x04IPv6 (16 bytes)

Source: proxy/trojan/protocol.go:16-21

After the header, the remaining data on the TCP stream is raw payload.

TCP Response

The server sends raw payload back -- no response header. This is because the protocol is designed to look like normal TLS traffic.

UDP Framing

When the command is UDP (0x03), payload is framed per-packet:

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

Source: proxy/trojan/protocol.go:125-150

FieldSizeDescription
AddressvariableSOCKS5-style: AddrType(1B) + Address + Port(2B, BE)
Length2 bytesBig-endian uint16, payload length (max 8192)
CRLF2 bytes\r\n
PayloadLength bytesThe UDP datagram

The PacketReader on the server side reads each framed packet and attaches the destination as 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") }
    // read CRLF, then read `remain` bytes of payload
}

Source: proxy/trojan/protocol.go:220-262

Password Authentication

The password is hashed with SHA-224 and hex-encoded to produce a 56-byte ASCII string:

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
}

Source: proxy/trojan/config.go:43-49

The MemoryAccount stores both the plaintext password and the pre-computed 56-byte hex key.

Validator

The Validator uses two sync.Map instances:

  • users: maps hex-encoded SHA224 hash -> *protocol.MemoryUser
  • email: maps lowercase email -> *protocol.MemoryUser

Source: proxy/trojan/validator.go:12-82

Inbound Handler (Server)

File: proxy/trojan/server.go

Connection Processing

mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    participant F as Fallback

    C->>S: TLS ClientHello
    S->>C: TLS ServerHello + Certificate
    C->>S: SHA224(password) + CRLF + Command + Address + CRLF + Payload

    alt Valid password
        S->>S: Parse command & address
        alt TCP Command
            S->>C: Raw proxied response
        else UDP Command
            S->>C: Framed UDP packets
        end
    else Invalid or short data
        S->>F: Forward entire connection to fallback
    end

The server reads the first buffer (up to buf.Size bytes) and performs quick validation:

go
if firstLen < 58 || first.Byte(56) != '\r' {
    // Not trojan protocol - fallback
    shouldFallback = true
} else {
    user = s.validator.Get(hexString(first.BytesTo(56)))
    if user == nil {
        shouldFallback = true
    }
}

Source: proxy/trojan/server.go:176-201

The key insight: the first 56 bytes must be a valid hex SHA224 hash, followed by \r at position 56. This is checked before full header parsing.

Fallback Mechanism

When fallback is configured, invalid connections are transparently forwarded to another server. The fallback system supports three levels of matching:

  1. SNI (name): Matched against TLS ServerName
  2. ALPN (alpn): Matched against negotiated ALPN protocol
  3. Path (path): Matched against the HTTP request path (extracted from first bytes)
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)
}

The fallback data structure is a 3-level nested map: map[name]map[alpn]map[path]*Fallback

Source: proxy/trojan/server.go:66-113

The fallback handler also supports PROXY protocol (v1 text and v2 binary) to forward the real client IP to the backend:

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

Source: proxy/trojan/server.go:493-522

Outbound Handler (Client)

File: proxy/trojan/client.go

The client handler:

  1. Dials the server via the configured transport (typically TLS)
  2. Creates a ConnWriter that lazily writes the header on first Write() call
  3. For UDP, wraps with PacketWriter for per-packet framing
  4. Sends first payload along with header for 0-RTT optimization
go
connWriter := &ConnWriter{
    Writer:  bufferWriter,
    Target:  destination,
    Account: account,
}
// First write triggers header send
buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100)

Source: proxy/trojan/client.go:99-128

The ConnWriter.writeHeader() assembles:

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

Source: proxy/trojan/protocol.go:64-95

Implementation Notes

  1. TLS requirement: Trojan provides zero encryption itself. Without TLS, the password hash and all traffic are transmitted in cleartext. The protocol is designed to always be used with TLS or REALITY transport.

  2. Fallback is critical for stealth: When no valid Trojan header is found, the server should forward the connection to a real web server. This means port scanning and probing cannot distinguish the server from a normal HTTPS site.

  3. No response header: Unlike VMess, there is no server-to-client header at all. This simplifies implementation but means the client has no way to detect server-side errors at the protocol level.

  4. Maximum UDP payload: Each UDP frame is limited to 8192 bytes (maxLength = 8192). Packets exceeding this are rejected.

  5. ConnReader is stateful: ConnReader.ParseHeader() is called only once. After that, Read() passes through directly to the underlying reader. The headerParsed flag prevents re-parsing.

  6. Network support: The server supports net.Network_TCP and net.Network_UNIX, but not raw UDP. UDP traffic is tunneled as framed data inside the TCP/TLS connection.

Source: proxy/trojan/server.go:144-146

Technical analysis for re-implementation purposes.