Contents

Trojan

Look exactly like an HTTPS server — proxy traffic hidden behind a plausible TLS site.

Trojan does not try to look like nothing — it tries to look like something completely ordinary: an HTTPS web server. Everything rides inside a normal TLS connection, and the server’s last line of defence is that a mislabelled visitor is quietly handed a real website. There is no custom obfuscation and no magic packet shape; the disguise is that there is nothing to see.

The password is a hash sent as plain hex text

The client authenticates by sending 56 ASCII characters — the hex encoding of SHA-224(password) — as the first thing on the wire. No challenge, no nonce, no handshake of its own; TLS already did that.

proxy/trojan/config.go proxy/trojan/protocol.go
func hexSha224(password string) []byte {
	buf := make([]byte, 56)
	hash := sha256.New224()
	common.Must2(hash.Write([]byte(password)))
	hex.Encode(buf, hash.Sum(nil))
	return buf
}

The request header is SOCKS, wrapped in CRLFs

After the password comes a miniature SOCKS request — a command byte, then an address and port in the same ATYP-style encoding SOCKS5 uses (0x01 IPv4, 0x03 domain, 0x04 IPv6), framed by CRLFs.

proxy/trojan/protocol.go
+-----------------------+-------+---------+----------------------+-------+---------+
| SHA224(password) hex  | CR LF | command | ATYP + addr + port   | CR LF | payload |
|      56 bytes         |       | 1 byte  |      variable        |       |   ...   |
+-----------------------+-------+---------+----------------------+-------+---------+

command is 0x01 for TCP and 0x03 for UDP. On the reader side ParseHeader reads exactly the 56-byte hash, the CRLF, the command, the address/port, and a trailing CRLF — then the socket becomes a raw pipe to the destination. UDP is length-prefixed (addr | 2-byte length | CRLF | payload) and the reader rejects any packet whose declared length exceeds maxLength (8192) as an oversize payload.

A wrong password becomes a real website, not an error

This is the whole point of Trojan. The server reads the first burst of bytes and decides in one shot whether it is talking to a real client or a probe. If the first read is too short, byte 56 is not \r, or the 56-byte hash matches no user — and fallbacks is configured — it does not close the connection or send an error: it falls back.

proxy/trojan/server.go proxy/trojan/validator.go
shouldFallback := false
if firstLen < 58 || first.Byte(56) != '\r' {
	shouldFallback = true // not trojan protocol
} else {
	user = s.validator.Get(hexString(first.BytesTo(56)))
	if user == nil {
		shouldFallback = true // not a valid user
	}
}
if isfb && shouldFallback {
	return s.fallback(ctx, err, /* ... */, first, firstLen, bufferedReader)
}

Fallback dials a configured backend (typically a real nginx) and splices the two connections together — including the bytes it already read. The prober’s exact request is replayed to a genuine web server and the genuine response comes back. To an outside observer the address serves a normal site over HTTPS; poking it with the wrong password just serves that site.

Correct password → tunnel to the requested destination; anything else → transparent relay to a real web server. Both paths are indistinguishable HTTPS from the outside.

Trojan carries no obfuscation of its own — so the TLS must be real

Because the plaintext is a bare hash plus a SOCKS request, a Trojan server with no valid certificate is trivially unmasked. The disguise lives entirely in the TLS layer above it; get that wrong and the “looks like a website” story collapses.

Don't

Self-signed cert, no fallback

A probe connecting with the wrong password gets its connection dropped (server-side error: invalid protocol or invalid user) and the TLS cert doesn’t chain to anything real. The endpoint behaves like nothing else on the internet — a strong signal to block it.

Do

Real cert + fallback to a live site

Serve a valid certificate for a domain you actually control and point fallbacks at a real backend. Wrong password serves that site; right password tunnels. The address is a plausible HTTPS host either way.

{
	"protocol": "trojan",
	"settings": {
		"clients": [{ "password": "correct horse battery staple" }],
		"fallbacks": [{ "dest": "127.0.0.1:8080" }]
	},
	"streamSettings": {
		"security": "tls",
		"tlsSettings": { "certificates": [{ "certificateFile": "/etc/ssl/fullchain.pem", "keyFile": "/etc/ssl/key.pem" }] }
	}
}

The client side is the mirror image: ConnWriter.writeHeader prepends the account key and SOCKS request to the first write, and the client waits up to 100 ms (buf.CopyOnceTimeout) for the first chunk of application data so the header and initial payload leave in a single flush. On the server, a fallback’s xver setting makes Server.fallback emit a PROXY-protocol v1/v2 header to the backend so it still sees the real client IP.

proxy/trojan/client.go