Standard library · Network

net.reality

The REALITY security layer of Xray: a client that proves itself inside a TLS 1.3 handshake, a server that shows everyone else the real website, key and short id helpers, and link parameters.

import net.reality

REALITY is a way to hide a proxy server behind a website that already exists. The client sends an ordinary TLS 1.3 handshake whose server name is that website. Inside the handshake, in a field that is random bytes to everybody else, it puts a small sealed message that only the owner of the server's key can open. If the message opens and the short id is known, the server answers as itself and the connection carries your traffic. If not (a censor probing the address, a browser, a scanner) the server does nothing clever: it forwards the bytes to the real website and forwards the answers back, so the probe sees the real site's real certificate. There is no certificate of your own to find.

This package is both halves. DialOver runs the client half on a connected socket and returns a tls.Conn. Accept runs the server half on an accepted socket: it reads the first record, decides, and either returns an authenticated tls.Conn or relays the connection to the destination. GenerateKey and NewShortId make the secrets, and ParseParams and FormatParams read and write the security=reality part of a vless:// link. The ClientHello is built by net.utls so it looks like a browser, and the handshake itself is the TLS 1.3 code of tls.

What happens on the wire

StepWhat
Client helloA browser-like ClientHello with an x25519 key share and the real website as server name. The 32 byte session id is the sealed message.
Sealed message16 bytes: three version bytes, one zero byte, the time as 4 bytes and the 8 byte short id. It is sealed with AES-256-GCM. The key is HKDF-SHA256 over the x25519 secret between the client's ephemeral key and the server's static key, with the first 20 bytes of the hello random as salt and REALITY as info. The nonce is the last 12 bytes of the random, and the whole ClientHello (with the session id set to zeros) is the associated data.
Server, not authenticatedConnects to the destination, sends it the bytes it has read and copies in both directions until both sides close.
Server, authenticatedCompletes a TLS 1.3 handshake with a one-off ed25519 certificate. The certificate's signature field is not a signature: it is HMAC-SHA512 of the certificate's public key under the key derived above.
Client checkInstead of checking a certificate chain, the client recomputes that HMAC, and verifies the ed25519 CertificateVerify signature with the certificate's key. Only the real server can produce the HMAC.

What trips people up

  • The server name must be one of server_names. Otherwise the server forwards you to the website like any stranger, and the client reports server did not sign with ed25519, because the website's certificate is not an ed25519 HMAC certificate. That message means "authentication failed" in every case: wrong public key, unknown short id, wrong name.
  • A current Xray server only accepts a hello with a hybrid key share. Since the ML-KEM change Xray rejects a ClientHello that has no X25519MLKEM768 share before the plain x25519 one. This package always sends one (see the limits), so use the chrome or edge fingerprint with such a server. The firefox and safari profiles send no hybrid share and are not authenticated by it.
  • The destination has to look like your server. Xray makes its handshake records exactly as long as the destination's. The destination should be a TLS 1.3 site that negotiates the same ALPN you offer (usually h2) and does not select X25519MLKEM768, because this package cannot finish a hybrid key exchange (see the limits).
  • Close sockets of failed clients. A rejected client is relayed until it closes; a program that leaves its socket open keeps the relay open too.
  • A short id has to be an even number of hex digits, at most 16. It is zero padded to 8 bytes. An empty short id is the all-zero id and must be in the server's list to work.
  • Clocks. max_time_diff is in milliseconds. It defaults to 60000, so a client whose clock is more than a minute off is sent to the real site; 0 turns the check off (Xray's own default).

Keys and short ids

GenerateKey returns an x25519 pair as the 43 character base64url text that Xray prints for xray x25519. The private key stays on the server; the public key goes into every client. PublicKeyOf recomputes the public key from the private one. A short id tells clients apart without any extra key.

net-reality-guide-keys.salam
import net.reality

func main:
    keys := reality.GenerateKey()
    println "private key length:", keys.private_key.len()
    println "public key length:", keys.public_key.len()
    println "public matches private:", reality.PublicKeyOf(keys.private_key) == keys.public_key

    sid := reality.NewShortId(4)
    println "short id digits:", sid.len()
    println "valid short ids:", reality.ValidShortId(sid), reality.ValidShortId("abcd1234"), reality.ValidShortId("zz")
    println "valid public keys:", reality.ValidPublicKey(keys.public_key), reality.ValidPublicKey("short")
end
Outputprivate key length: 43 public key length: 43 public matches private: true short id digits: 8 valid short ids: true true false valid public keys: true false

A REALITY share link is a vless:// link with security=reality and the keys pbk (public key), sid (short id), spx (spider path), sni, fp (fingerprint) and flow. The package that reads vless:// links is net.vless; the part that is about REALITY is here. Give ParseParams the query text (the part after ?) and it fills a Settings; FormatParams goes back. Values are kept as written and are not percent decoded.

net-reality-guide-link.salam
import net.reality

func main:
    query := "security=reality&pbk=IH8tyHx_SyHSCyaQgNTOvHYefAehmhpbMgkwFeAPnXY&sid=abcd1234&spx=%2F&sni=www.example.com&fp=chrome&flow=xtls-rprx-vision"
    s := reality.ParseParams(query)
    println s.server_name, s.public_key
    println s.short_id, s.fingerprint, s.flow
    println reality.FormatParams(s)
end
Outputwww.example.com IH8tyHx_SyHSCyaQgNTOvHYefAehmhpbMgkwFeAPnXY abcd1234 chrome xtls-rprx-vision security=reality&pbk=IH8tyHx_SyHSCyaQgNTOvHYefAehmhpbMgkwFeAPnXY&sid=abcd1234&spx=%2F&sni=www.example.com&fp=chrome&flow=xtls-rprx-vision

A client, a server and a probe

The example runs everything in one program. The destination is a plain decoy that answers one request. The first client has the right key and short id: the server accepts it and the two sides exchange a message through an ordinary tls.Conn. The second connection never speaks TLS and so cannot be authenticated; it is relayed to the destination and gets the decoy's answer. A real deployment is the same code with a real destination and the two halves in different programs.

net-reality-guide-loopback.salam
import net.reality
import net.tcp
import tls

mut ss := reality.ServerSettings {}

func decoy(arg: void*):
    srv := tcp.Server { fd = arg as i64 }
    c := tcp.Accept(srv)
    if tcp.ConnOk(c):
        tcp.Read(c, 4096)
        tcp.Write(c, "decoy site says hello")
        tcp.Close(c)
    end
end

func serve(arg: void*):
    srv := tcp.Server { fd = arg as i64 }
    c := tcp.Accept(srv)
    if not tcp.ConnOk(c):
        ret
    end
    r := reality.Accept(c.fd, ss)
    if r.ok:
        mut t := r.conn
        d := tls.Read(t, 4096)
        tls.Write(t, "hello " + d + " from short id " + r.short_id)
        tls.Close(t)
    end
end

func main:
    keys := reality.GenerateKey()
    dest := tcp.Bind(0)
    dth := spawn(decoy, dest.fd as void*)
    ss.private_key = keys.private_key
    ss.dest = "127.0.0.1:" + tcp.Port(dest)
    ss.server_names = "www.example.com"
    ss.short_ids = "abcd1234"

    srv := tcp.Bind(0)
    sth := spawn(serve, srv.fd as void*)
    c := tcp.Dial("127.0.0.1", tcp.Port(srv))
    mut s := reality.Settings {}
    s.server_name = "www.example.com"
    s.public_key = keys.public_key
    s.short_id = "abcd1234"
    mut t := reality.DialOver(c.fd, s)
    println "client connected:", tls.ConnOk(t)
    tls.Write(t, "reality")
    println tls.Read(t, 4096)
    tls.Close(t)
    join(sth)
    tcp.Close(tcp.Dial("127.0.0.1", tcp.Port(dest)))
    join(dth)

    dest2 := tcp.Bind(0)
    dth2 := spawn(decoy, dest2.fd as void*)
    ss.dest = "127.0.0.1:" + tcp.Port(dest2)
    srv2 := tcp.Bind(0)
    sth2 := spawn(serve, srv2.fd as void*)
    p := tcp.Dial("127.0.0.1", tcp.Port(srv2))
    tcp.Write(p, "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n")
    println "a probe gets:", tcp.Read(p, 4096)
    tcp.Close(p)
    join(sth2)
    join(dth2)
    tcp.CloseServer(srv)
    tcp.CloseServer(srv2)
    tcp.CloseServer(dest)
    tcp.CloseServer(dest2)
end
Outputclient connected: true hello reality from short id abcd123400000000 a probe gets: decoy site says hello

Using it in a tunnel

After DialOver or Accept succeeds you hold a tls.Conn, which is what net.transport keeps for TLS connections. To use REALITY as the security layer of a VLESS outbound, dial the TCP socket as usual and call reality.DialOver(fd, settings) where the transport would call tls.WrapClient; on the server call reality.Accept(fd, server_settings) where it would call tls.WrapServer. tunnel.WrapTls turns the result into a stream. The connection then carries the VLESS header and payload.

What was tested against a real Xray

Xray 26.11.9 (Linux amd64, from the official release, checksum checked) was run on the loopback interface with a local TLS 1.3 server as its destination:

  • This client against a real Xray REALITY server (VLESS inbound): the handshake completes and a VLESS request through it reaches a TCP echo service and comes back.
  • A real Xray client (VLESS outbound with REALITY, fingerprint chrome) against this server: the request is accepted, the VLESS header arrives, and the data round trips.
  • A plain openssl s_client against this server is shown the destination's certificate.

The tests in the repository use this package against itself, because Xray is not available there.

Limits

  • Written from memory of the REALITY source and checked only against Xray 26.11.9. The sealed message layout, the key derivation and the certificate rule match that build on loopback. Older Xray and sing-box builds were not tried. Version and time checks are done the way that build does them, not from a specification.
  • The hybrid key share is a placeholder. The X25519MLKEM768 share carries a well formed but random ML-KEM encapsulation key, with no key behind it. A destination that chooses that group completes the exchange with it and this package cannot finish, because ML-KEM is not implemented. Choose a destination that does not negotiate X25519MLKEM768, or accept that it does not work with that server.
  • The flow xtls-rprx-vision is not implemented. flow is carried in Settings and in link parameters only. Use an empty flow.
  • The spider path spx is stored and written back and is not used. The client does not browse the destination after a failed check.
  • The optional ML-DSA-65 certificate signature of Xray is not checked.
  • No replay protection. A recorded ClientHello authenticates again inside the max_time_diff window (60 seconds by default).
  • No HelloRetryRequest. The hello is fixed at the time it is built, so a server that wants another group fails the handshake.
  • Not constant time. The ed25519 code is correct against RFC 8032 vectors but is not hardened against timing attacks. The server signs with a fresh key for each connection.
  • The server relays an unauthenticated client with the first record only up to its first read of 16384 bytes; a ClientHello split over several records is relayed but not authenticated.

Types

Settings

struct Settings
What a client needs to reach a REALITY server.
NameTypeDescription
server_namestr = ""The server name in the ClientHello. It must be one of the server's server_names and a name the destination serves.
public_keystr = ""The server's public key, 43 base64url characters.
short_idstr = ""Up to 16 hex digits, an even count. Empty means all zeros.
spider_xstr = ""The spx value of a link. Stored and written back, not used.
fingerprintstr = "chrome"The net.utls profile name. Empty means chrome.
alpnstr = ""ALPN override, comma separated. Empty keeps the profile's list.
flowstr = ""The flow of a link. Carried only; xtls-rprx-vision is not implemented.
clock_offsetint = 0Seconds added to the timestamp in the ClientHello. For tests of the server time check.

ServerSettings

struct ServerSettings
What a server needs.
NameTypeDescription
private_keystr = ""The server's private key, 43 base64url characters.
deststr = ""The website to show to everyone else, as host:port.
server_namesstr = ""Comma separated names a client may use as its server name.
short_idsstr = ""Comma separated short ids that are accepted.
max_time_diffint = 60000The largest allowed difference between the client's clock and the server's, in milliseconds. The default is 60000; 0 turns the check off.
dest_timeout_msint = 5000The time allowed to connect to the destination.

KeyPair

struct KeyPair
An x25519 key pair in the text form that Xray uses.
NameTypeDescription
private_keystr = ""The private key.
public_keystr = ""The public key.

Accepted

struct Accepted
The outcome of Accept.
NameTypeDescription
okbool = falseThe client was authenticated and conn is open.
proxiedbool = falseThe connection was not authenticated and has been relayed to the destination until it ended.
conntls.ConnThe TLS connection when ok is true.
snistr = ""The server name the client sent.
short_idstr = ""The client's short id as 16 hex digits.
client_versionstr = ""The version the client wrote into its proof as x.y.z; Salam clients send 26.3.27, the default minimum of Xray-core 26.3.27 to 26.10.
errstr = ""Why it was not accepted.

Functions

GenerateKey

func GenerateKey(): KeyPair
Makes a new x25519 key pair.

PublicKeyOf

func PublicKeyOf(private_key: str): str
The public key for a private key, or an empty text when the private key is not valid.

NewShortId

func NewShortId(bytes: int): str
Makes a random short id of the given number of bytes, 1 to 8, as hex.

ValidPublicKey

func ValidPublicKey(public_key: str): bool
True when the text is a 43 character base64url key.

ValidShortId

func ValidShortId(short_id: str): bool
True when the text is an even number of hex digits, at most 16.

DialOver

func DialOver(fd: i64, s: Settings): tls.Conn
Runs the client half on a connected socket. Check tls.ConnOk on the result.

Accept

func Accept(fd: i64, ss: ServerSettings): Accepted
Runs the server half on an accepted socket. It blocks while it relays a rejected client.

ParseParams

func ParseParams(query: str): Settings
Reads pbk, sid, spx, sni, fp, flow and alpn from a query text.

FormatParams

func FormatParams(s: Settings): str
Writes the REALITY part of a link query, starting with security=reality.