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
| Field | Size | Description |
|---|---|---|
| Password Hash | 56 bytes | Hex-encoded SHA-224 of the plaintext password |
| CRLF | 2 bytes | \r\n (0x0D 0x0A) |
| Command | 1 byte | 0x01 = TCP Connect, 0x03 = UDP Associate |
| Address | variable | SOCKS5-style: AddrType(1B) + Address + Port(2B, BE) |
| CRLF | 2 bytes | \r\n (0x0D 0x0A) |
Address types (same as SOCKS5):
| Byte | Type |
|---|---|
0x01 | IPv4 (4 bytes) |
0x03 | Domain (1-byte length + string) |
0x04 | IPv6 (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
| Field | Size | Description |
|---|---|---|
| Address | variable | SOCKS5-style: AddrType(1B) + Address + Port(2B, BE) |
| Length | 2 bytes | Big-endian uint16, payload length (max 8192) |
| CRLF | 2 bytes | \r\n |
| Payload | Length bytes | The UDP datagram |
The PacketReader on the server side reads each framed packet and attaches the destination as 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") }
// 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:
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.MemoryUseremail: maps lowercase email ->*protocol.MemoryUser
Source: proxy/trojan/validator.go:12-82
Inbound Handler (Server)
File: proxy/trojan/server.go
Connection Processing
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
endThe server reads the first buffer (up to buf.Size bytes) and performs quick validation:
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:
- SNI (
name): Matched against TLS ServerName - ALPN (
alpn): Matched against negotiated ALPN protocol - Path (
path): Matched against the HTTP request path (extracted from first bytes)
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:
// 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 bytesSource: proxy/trojan/server.go:493-522
Outbound Handler (Client)
File: proxy/trojan/client.go
The client handler:
- Dials the server via the configured transport (typically TLS)
- Creates a
ConnWriterthat lazily writes the header on firstWrite()call - For UDP, wraps with
PacketWriterfor per-packet framing - Sends first payload along with header for 0-RTT optimization
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:
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\nSource: proxy/trojan/protocol.go:64-95
Implementation Notes
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.
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.
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.
Maximum UDP payload: Each UDP frame is limited to 8192 bytes (
maxLength = 8192). Packets exceeding this are rejected.ConnReader is stateful:
ConnReader.ParseHeader()is called only once. After that,Read()passes through directly to the underlying reader. TheheaderParsedflag prevents re-parsing.Network support: The server supports
net.Network_TCPandnet.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