UTProto is a generalized fork of Telegram's MTProto FakeTLS transport,
re-engineered to carry arbitrary payloads (VPN traffic, application data,
HTTP — anything that fits into a net.Conn) instead of MTProto frames.
Pure Go, zero third-party dependencies. Drop-in for any sing-box / Xray / v2ray / Mihomo / Clash-style proxy core with a thin adapter.
UTProto is a two-layer obfuscated transport:
[raw TCP socket]
↓
[FakeTLS framing] TLS record header \x17\x03\x03 + length
↓
[obfuscated2 layer] AES-256-CTR keyed from 64-byte init header
↓
[your payload] arbitrary bytes
The ClientHello — emitted before the encrypted stream begins — is a
byte-for-byte clone of a current Chrome-on-Linux ClientHello, with ECH
(extension 0xfe0d) and a randomized extension permutation. To a DPI
observer looking at the TLS handshake bytes, the connection is
indistinguishable from a real browser talking to a real TLS server.
- Not MTProto. UTProto borrows MTProto's wire mimicry but uses it as a framing layer for arbitrary bytes. It does not implement Telegram's MTProto protocol semantics, message types, or auth.
- Not a proxy core. UTProto provides the transport. You bring the proxy logic (VLESS, Shadowsocks, your own — your choice) on top.
- Not a censorship-circumvention silver bullet. Like Reality, naive, Hysteria2, and other modern transports, UTProto is one tool in a toolbox. Its strength is wire-mimicry of a popular browser's TLS handshake.
go get github.com/TwilgateLabs/utprotoGo 1.22 or newer.
import (
"context"
"fmt"
"net"
"github.com/TwilgateLabs/utproto"
)
func dial() (net.Conn, error) {
tcp, err := net.Dial("tcp", "your-server.example.com:443")
if err != nil {
return nil, err
}
secret, _ := utproto.DecodeSecret("ee" + "00112233445566778899aabbccddeeff")
cfg := &utproto.Config{
Secret: secret,
TLSDomain: "your-fake-sni.example.com",
}
return utproto.Dial(context.Background(), tcp, cfg)
}The returned net.Conn carries your payload. Read/write it like any other
net.Conn — UTProto handles framing transparently.
ln, _ := net.Listen("tcp", ":443")
users := []utproto.ServerUser{
{Name: "alice", Secret: parseHexSecret("00112233445566778899aabbccddeeff")},
{Name: "bob", Secret: parseHexSecret("ffeeddccbbaa99887766554433221100")},
}
for {
raw, err := ln.Accept()
if err != nil { continue }
go func() {
defer raw.Close()
conn, user, err := utproto.Accept(context.Background(), raw, users)
if err != nil {
// ServerHello identification failed. err.(*utproto.HandshakeError)
// exposes the original ClientHello bytes — you can fall back to
// proxying them to a real TLS server to avoid leaking that you
// run UTProto.
return
}
log.Printf("user %s connected", user.Name)
// Read/write conn — your application bytes.
}()
}The wire format is documented in SPEC.md.
examples/singbox/— a sing-box adapter that wires UTProto into a sing-boxprotocol.Outbound/protocol.Inbound. Use this as a reference if you want to port UTProto into Xray-core, Mihomo, v2ray, or any other sing-box-derived stack.
// Types
type Config struct {
Secret [16]byte
TLSDomain string
}
type ServerUser struct {
Name string
Secret [16]byte
}
type HandshakeError struct {
Kind string // sentinel: ErrSkew, ErrUser, ErrUnknown
Err error
Buffer []byte // original ClientHello bytes, for DPI fallback
}
// Constructors
func Dial(ctx context.Context, raw net.Conn, cfg *Config) (net.Conn, error)
func Accept(ctx context.Context, raw net.Conn, users []ServerUser) (net.Conn, *ServerUser, error)
// Helpers
func GenCurve25519PublicKey() ([32]byte, error)
func GenFakeMLKem768Key(buf []byte) errorUTProto's ClientHello renderer (tls_init.go) and the obfuscated2 layer
(obfuscated.go) are ported from
tdlib's td/mtproto/TlsInit.cpp and
td/mtproto/TcpTransport.cpp, both under the Boost Software License 1.0.
See NOTICE for attribution.
Maintained by Twilgate Labs as part of the InHive VPN client stack.