Help us shape the future of Go! Take the 2026 Go Developer Survey

desync

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 23 Imported by: 0

README

desync

CI Go Reference

A Go library that adapts direct TCP and UDP connections to networks with deep packet inspection.

desync wraps a socket your program already owns and reshapes how the first TLS ClientHello goes out on the wire. The server still receives the exact same handshake. It is not a VPN or a proxy service, needs no driver or administrator rights, and depends only on the Go standard library.

conn := desync.WrapConn(raw, desync.Options{
    TLSRec: []desync.Pos{{Offset: 1, FromSNI: true}}, // split the ClientHello right after the start of the server name
})

Why

A filter on the path and the destination server can read the same bytes differently. Where the TLS records end, how the bytes are split into writes, and what the first transmission of a segment contains can change what the filter sees, while the server reassembles an identical handshake.

Methods

Method What it does Windows Linux macOS
TLS record fragmentation (TLSRec) Splits the ClientHello into several valid TLS records ✓ ✓ ✓
TCP write splitting (Split, SplitDelay) Sends the ClientHello in separate writes, optionally with a pause ✓ ✓ ✓
Split wait (SplitWait) Before each further piece, waits until the kernel reports the previous bytes as sent or acknowledged sent, acked¹ sent, acked acked
Disorder (Disorder) The first TCP piece goes out with hop limit 1 and expires; TCP retransmits it after the later pieces ✓¹ ✓ ✓²
Out-of-band byte (OOB) One urgent byte follows the first TCP piece; the server's TCP removes it from the stream ✓ ✓ ✓
Low-TTL fake piece (Fake) First transmission of a prefix, or of one split piece, carries other bytes with a short hop limit; a retransmission carries the original ✓ ✓ —³
Decoy ClientHello (Fake.SNI, FromOriginal, Randomize) The fake bytes are a valid ClientHello for another server name, built from the template or the real hello ✓ ✓ —³
Fake with split pieces (Fake + Split, Disorder, OOB) The piece ending at the fake prefix is the fake; the pieces around it are ordinary, disorder or out-of-band ✓ ✓ —³
Low-TTL UDP burst (WrapPacketConn) Bounded low-TTL datagrams before the first datagrams to each peer; originals are unchanged ✓ ✓ ✓
QUIC Initial decoy (UDPOptions.Template, QUICOnly) The burst datagrams are QUIC v1 Initials for another server name; optionally only QUIC Initials get a burst ✓ ✓ ✓

Linux includes Android. ¹ Inferred from counters, not reliable; see platform capabilities. ² The transmission before the hop limit is restored is inferred, not observed; see disorder. ³ Not implementable: macOS cannot make a retransmission carry other bytes than the first transmission; see connection I/O.

Only a connection that starts with a TLS ClientHello is transformed. Any other traffic, and everything after the ClientHello, passes through untouched. Unsupported methods return an explicit error, never a silent fallback to another method. PlatformCapabilities() reports what this build implements on the current OS, and ProbeConn checks an actual socket.

Install

go get github.com/Nemu-x/desync

Usage

Wrap the connection before the first TLS byte is written:

func connect(ctx context.Context, address, serverName string) (*tls.Conn, error) {
    raw, err := (&net.Dialer{}).DialContext(ctx, "tcp", address)
    if err != nil {
        return nil, err
    }

    opts := desync.Options{
        TLSRec: []desync.Pos{{Offset: 3, FromSNI: true}},
    }
    if err := opts.Validate(); err != nil {
        raw.Close()
        return nil, err
    }

    conn := tls.Client(desync.WrapConn(raw, opts), &tls.Config{ServerName: serverName})
    if err := conn.HandshakeContext(ctx); err != nil {
        conn.Close()
        return nil, err
    }
    return conn, nil
}

WrapConn returns the connection itself when no method is enabled. Options are copied on wrap, so changing them later does not affect an existing connection. Buffered and partial writes, deadlines and closing are specified in connection I/O contracts.

Positions
Value ParsePos Meaning
Pos{Offset: 3} "3" Byte 3 of the TLS input, counting from the first record header
Pos{Offset: 1, FromSNI: true} "1+s" One handshake byte after the start of the server name
Pos{Offset: -2, FromSNI: true} "-2+s" Two bytes before the server name

SNI-relative positions skip existing record headers. Positions without an SNI, outside the ClientHello, or on an existing record boundary are ignored. TLSRec positions choose record boundaries; Split positions choose write boundaries.

Random cuts

Options.Random adds bounded random record and write cuts on top of the fixed positions; nil keeps a strategy fully deterministic. Each RandomCut draws Count cuts uniformly from the inclusive range From..To (both absolute or both SNI-relative), at least MinPiece bytes apart:

opts := desync.Options{
    TLSRec: []desync.Pos{{Offset: 1, FromSNI: true}},
    Random: &desync.RandomOptions{
        TLSRec: []desync.RandomCut{{From: desync.Pos{Offset: -8, FromSNI: true}, To: desync.Pos{Offset: 8, FromSNI: true}, Count: 2, MinPiece: 2}},
        Split:  []desync.RandomCut{{From: desync.Pos{Offset: 1}, To: desync.Pos{Offset: 120}}},
        Seed:   0, // 0: a new seed per connection; nonzero: the same cuts on every connection
    },
}

Random cuts never change handshake bytes, create empty records or writes, or add delays beyond SplitDelay. A nonzero Seed reproduces a test or a bug report. opts.Key() is a stable description of every option, the random policy and seed included: derive cached strategy IDs from it so an ID never silently changes meaning.

A ClientHello may arrive across several records and Write calls. Assembly is bounded by default to a 64 KiB handshake, 256 records and a 5-second timeout for incomplete input; Options.ClientHello changes these limits. A complete ClientHello written at once starts no timer.

Platform capabilities and split wait
caps := desync.PlatformCapabilities()
// windows: {FakeTCP:true FakeUDP:true TTLControl:true OOB:true
//           Disorder:{Supported:true Mechanism:SIO_TCP_INFO Observed:true Reliable:false}
//           Ack:{Mechanism:SIO_TCP_INFO Sent:true Acked:true Reliable:false}}
// linux:   {FakeTCP:true FakeUDP:true TTLControl:true OOB:true
//           Disorder:{Supported:true Mechanism:SIOCOUTQNSD Observed:true Reliable:true}
//           Ack:{Mechanism:SIOCOUTQ/SIOCOUTQNSD Sent:true Acked:true Reliable:true}}
// darwin:  {FakeUDP:true TTLControl:true OOB:true Disorder:{Supported:true Mechanism:SO_NWRITE}
//           Ack:{Mechanism:SO_NWRITE Acked:true Reliable:true} Unsupported:map[fake-tcp:no splice: ...]}

opts := desync.Options{
    Split:            []desync.Pos{{Offset: 1, FromSNI: true}},
    SplitWait:        desync.SplitWaitAcked, // or SplitWaitSent
    SplitWaitTimeout: time.Second,           // per wait; default 2 s
}
if err := opts.Validate(); errors.Is(err, desync.ErrSplitWaitUnsupported) {
    // this OS cannot observe it; choose another strategy explicitly
}

SplitWait reads the socket's send queue between pieces: SplitWaitSent waits until every byte written so far was transmitted at least once, SplitWaitAcked until the peer acknowledged it. The wait ends as soon as the kernel reports the condition. It is not a delay, and it is never replaced by one: an OS or socket that cannot observe the condition fails Validate or the first Write with ErrSplitWaitUnsupported / ErrSplitWaitRequiresTCP before anything is sent. SplitDelay remains the separate, explicit pause and runs after the wait when both are set.

OS Interface Sent Acked Reliable
Linux, Android ioctl SIOCOUTQ (unacknowledged bytes), SIOCOUTQNSD (unsent bytes) ✓ ✓ ✓
macOS SO_NWRITE (send-buffer bytes, kept until acknowledged; same value as TCP_CONNECTION_INFO.tcpi_snd_sbbytes) — ✓ ✓
Windows 10 1703+ SIO_TCP_INFO counters (BytesOut, BytesRetrans, BytesInFlight) ✓ ✓ —

Reliable means the kernel's own queue state is read. Windows has no unsent-bytes value, so the wait compares counter growth with the bytes written; on loopback BytesOut exceeded the written bytes for large writes, so the wait can end early. Close, write deadlines (including changes to them) and SplitWaitTimeout bound every wait; a timeout returns ErrSplitWaitTimeout and closes the connection. Where the observation is reliable, SplitWaitAcked writes a piece only after every earlier byte was transmitted and acknowledged, so no segment carries bytes of two pieces; with SplitWaitSent a retransmission can still combine them. The OS still divides a piece into segments. Details and measurements: connection I/O, validation.

Disorder and out-of-band byte
opts := desync.Options{
    Split:    []desync.Pos{{Offset: 1, FromSNI: true}},
    Disorder: true,                 // ByeDPI --disorder
    OOB:      &desync.OOBOptions{}, // ByeDPI --oob; both together: --disoob
}
  • Both act on the first TCP piece, so they need Split or Random.Split positions; a ClientHello where no split position resolves is written in one piece, as with Split alone. TLSRec, SplitDelay, SplitWaitSent and Fake (on a later piece, see below) combine with them; Disorder rejects SplitWaitAcked (the dropped piece is acknowledged only after its retransmission).
  • Disorder sets hop limit 1 for the first piece, so it expires at the first router. The hop limit is restored before the next piece and on every error, and TCP retransmits the piece with the normal limit after the server received the later pieces. On Linux and Windows the limit is restored only after the kernel reports the piece as transmitted (SIOCOUTQNSD, SIO_TCP_INFO), bounded by SplitWaitTimeout (ErrSplitWaitTimeout), the write deadline and Close. macOS cannot observe unsent bytes: the piece is written once SO_NWRITE reports an empty send buffer, so the kernel builds the segment inside the send call, and Capabilities.Disorder.Observed is false.
  • OOB sends the first piece followed by one byte (Byte, default 'a') with MSG_OOB. The server's TCP takes that byte out of the stream as urgent data, so the application reads the original bytes; an inspecting device that ignores the urgent pointer sees one extra byte. A server using SO_OOBINLINE, or a middlebox that clears the URG flag, would put the byte into the stream and break the handshake.
Low-TTL fake piece (Windows, Linux)
opts := desync.Options{
    TLSRec: []desync.Pos{{Offset: 1, FromSNI: true}},
    Fake:   &desync.FakeOptions{TTL: 4, Prefix: -1},
}
  • TTL must expire after the filter and before the server. It depends on the route; 4 is an example, not a default. AutoTTL derives it per connection where that can be observed, FakeTTLCandidates covers the rest (see automatic TTL).
  • Prefix is measured after record fragmentation. Negative values count from the end; -1 leaves the last byte for a normal write.
  • Payload replaces the bytes of the first transmission; by default an embedded TLS template is used, padded or truncated to the fake piece. Offset starts the decoy at a later byte (ByeDPI --fake-offset).
  • Timeout (default 3 s) bounds the native waits. Write deadlines on the wrapper bound normal writes; Close interrupts both.

Windows sends the prefix with TransmitFile from a delete-on-close temporary file, then puts the original bytes in the file cache for the retransmission and restores the socket's hop limits; native resources are kept until the operation completes or is cancelled. Linux and Android vmsplice a shared page holding the fake bytes into a pipe and splice it to the socket, so the send queue references the page instead of a copy; once SIOCOUTQNSD reports the prefix transmitted, the page is overwritten with the original bytes, then the hop limit is restored. This needs a route whose device supports scatter-gather (loopback, Ethernet, Wi-Fi, tun and WireGuard do); otherwise the kernel copies the spliced bytes and the server receives the fake, which fails the handshake. An error after a transformed send is terminal for the connection. macOS reports it unsupported (Capabilities.Unsupported["fake-tcp"]).

Decoy ClientHello (ByeDPI --fake-sni, --fake-tls-mod):

opts := desync.Options{
    Fake: &desync.FakeOptions{
        TTL:       4,
        SNI:       []string{"www.example.com", "cdn-##.example.net"}, // one per connection; * ? # are random
        Randomize: true, // fresh random, session ID and key shares per connection
        // FromOriginal: true, // build the decoy from the real ClientHello instead of the template
    },
}
  • With SNI, Randomize or FromOriginal the decoy is decoded and re-encoded as a valid ClientHello with every length recomputed, then padded or truncated to the fake piece. The template's padding extension absorbs a longer or shorter name, so it keeps its 517 bytes; a decoy built from the real hello changes length with the name.
  • Names must be DNS host names (no IP literals, at most 64 names); * becomes a random letter or digit, ? a letter, # a digit. A custom Payload must then be a ClientHello. FromOriginal needs SNI or Randomize and excludes Payload.

Fake with split pieces (ByeDPI --split/--disorder/--oob followed by --fake):

opts := desync.Options{
    Split:    []desync.Pos{{Offset: 1, FromSNI: true}},
    Disorder: true,                                   // optional: the first piece goes out with hop limit 1
    Fake:     &desync.FakeOptions{TTL: 4, Prefix: -1}, // the piece from 1+s to the last byte is the fake
}
  • The fake prefix end is one more cut. The fake piece is the piece that ends there and starts at the previous split cut (or at 0); every other piece is an ordinary write, and Disorder/OOB still act on the first piece. If for a ClientHello the fake piece would be the first piece while Disorder or OOB is set, the write fails with ErrFakeFirstPiece before any byte is sent.
  • SplitDelay and SplitWaitSent apply between all pieces; SplitWaitAcked is rejected (the fake piece is acknowledged only after its retransmission).
  • Linux lowers the hop limit for the fake piece only once SIOCOUTQNSD reports the earlier pieces transmitted, so none of them goes out with it (ByeDPI waits the same way). Windows, like ByeDPI, does not wait: the TTL applies to the TransmitFile request. The pieces after the fake are written as soon as the native request is submitted.
  • After an error at or after the fake piece, Write counts only the pieces before it.
Automatic TTL
dialer := net.Dialer{Control: desync.ControlAutoTTL} // before connect; no-op where unobservable
raw, err := dialer.Dial("tcp", "[2001:db8::1]:443")
if err != nil {
    return err
}
conn := desync.WrapConn(raw, desync.Options{
    Fake: &desync.FakeOptions{
        AutoTTL: &desync.AutoTTL{}, // Delta 1, Min 3, Max 20
        TTL:     4,                 // fallback where the path cannot be observed; 0: fail instead
    },
})
// after the first Write: ttl, auto := conn.(*desync.Conn).FakeTTL()
  • The client reads the TTL of the server's SYN-ACK, guesses its initial value (64, 128 or 255; EstimateHops) and so the number of routers, and sends the fake with hops - Delta, clamped to Min..Max (zapret's --dpi-desync-autottl). A result that does not stay below the hop count is rejected: the fallback TTL is used, or the write fails with ErrAutoTTLUnobserved before anything is sent.
  • Only Linux/Android over IPv6 can observe it without privileges (IPV6_RECVHOPLIMIT, enabled by ControlAutoTTL before connect). Linux IPv4, Windows and macOS keep no received TTL for a TCP socket; PlatformCapabilities().AutoTTL and Unsupported["auto-ttl-ipv4"]/["auto-ttl-ipv6"] say why. There AutoTTL without a fallback TTL fails Validate (Windows) or the write (ErrAutoTTLUnsupported, Linux IPv4).
  • ObserveHops(conn) returns the observed TTL and hops without changing anything, for diagnostics or to set UDPOptions.TTL with AutoTTL{}.TTL(hops). The UDP burst itself has no automatic TTL: it precedes the peer's first reply.
  • Where nothing can be observed, probe a range instead. FakeTTLCandidates(base, 2, 8) (and UDPTTLCandidates) return one option set per TTL, each with a stable, distinct Key(); register them as controller candidates and let discovery keep the one that verifies:
candidates, err := desync.FakeTTLCandidates(desync.Options{Fake: &desync.FakeOptions{Prefix: -1}}, 2, 8)
// one strategy ID per candidate, e.g. "fake-ttl-" + strconv.Itoa(c.Fake.TTL);
// derive the controller's PolicyVersion from the candidates' Key() values

The estimate assumes that the return path is as long as the forward path and that the server uses a standard initial TTL; a CDN or middlebox answering with its own TTL, or an asymmetric route, makes it wrong. See connection I/O.

Low-TTL UDP burst (Windows, Linux, macOS)
pc, err := net.ListenPacket("udp", ":0")
if err != nil {
    return err
}
packets, err := desync.WrapPacketConn(pc, desync.UDPOptions{TTL: 4, Repeats: 6, Packets: 3})
if err != nil {
    pc.Close()
    return err
}
defer packets.Close()

_, err = packets.WriteTo(datagram, peer)
  • Needs an unconnected UDP socket that exposes SyscallConn. The wrapper owns all writes to it.
  • Repeats (1–16, default 6) low-TTL datagrams go before each of the first Packets (1–8, default 3) datagrams to a peer. The default payload is 64 zero bytes.
  • Template: desync.UDPTemplateQUICInitial generates a QUIC v1 client Initial per burst instead: long header, version 1, random 8-byte DCID and SCID, a CRYPTO frame with a ClientHello for SNI (default www.wikipedia.org, ALPN h3, QUIC transport parameters), PADDING to 1200 bytes, protected with the Initial keys of RFC 9001, so a device that decrypts Initials reads the decoy name. desync.QUICInitialPayload(sni) returns one such packet for a fixed Payload.
  • QUICOnly: true gives a burst only to datagrams that look like a QUIC client Initial (long header, version 1, a draft or 2, Initial type, at least 1200 bytes); other datagrams pass untouched and do not count towards Packets.
  • Originals keep their content, order and source port. The hop limit is restored before every original; if it cannot be restored, the socket is closed.
  • Up to 64 peers per socket are tracked, with a 2-minute idle expiry; further peers pass through unchanged.
  • TTL-expired ICMP replies do not surface as receive errors. Windows suppresses them on the socket (SIO_UDP_NETRESET); Linux reports them on an unconnected socket only with IP_RECVERR/IPV6_RECVERR, which the wrapper turns off if they were enabled; macOS never delivers them to an unconnected socket.
  • Counters() reports original and fake datagrams, errors and peers. A shared *UDPCounters aggregates several sockets. Counters show what was sent, not whether it helped.

Strategy catalog

The presets package is the shared list of strategies: the TCP presets desync-probe tests and a set of UDP bursts, each with a name, a description, the options, the platform features it needs and a rank. Applications and desync-probe use the same catalog, so a strategy ID means the same transformation in a probe report, a controller cache and a saved setting.

usable, skipped := presets.Available(desync.PlatformCapabilities())
for _, p := range presets.Ranked(usable) {
    fmt.Println(p.ID(), p.Name, p.Description) // p.Options goes to desync.WrapConn
}
for _, s := range skipped {
    fmt.Println("not here:", s.Name, s.Reason)
}

m, ok := presets.ByID("s30879a4293") // a saved choice; m.TCP or m.UDP is set
  • IDs. ID() is s plus the first 10 hex digits of the SHA-256 of Options.Key() for TCP (OptionsID, the ID desync-probe reports) and u plus 10 for UDPOptions.Key() (UDPOptionsID). An ID changes exactly when the transformation changes, so an ID saved by an older version may not be found; fall back instead of guessing. Persist IDs, never options: the presets.ID type marshals as a JSON string, rejects malformed values, accepts unknown well-formed ones and resolves with Lookup.
  • Platforms. Available keeps the presets whose features Capabilities provides and that Validate accepts on the running system; the others come back with the reason from Capabilities.Unsupported. Filter / FilterUDP look only at the capabilities, for example to show what another OS supports. TCP() and UDP() return the whole catalog in a stable order, the baseline none first.
  • Order. Ranked sorts by Rank, catalog order within a rank. It is a suggestion for which strategy to try first, not a measured success rate: record splits (one ordinary write, no extra packets), then write splits (one extra segment), split waits (up to a round trip), disorder and the out-of-band byte (a retransmission, and a server that must discard urgent data), single fakes (a TTL that has to expire between the filter and the server, and that breaks the handshake when the fake reaches the server) and last fakes combined with other methods. UDP bursts with fewer extra datagrams rank first.
  • Controller. Use presets.IDs(...) as controller.Config.Candidates and CatalogVersion in its PolicyVersion; see controller.
  • UDP. udp-zero-* send 64 zero bytes before each of the first datagrams to a peer; udp-quic-* send QUIC Initial decoys only before QUIC client Initials and leave other UDP traffic alone.

TCP presets (-list-presets shows the ones your system supports):

Name ID Rank Needs
none s21c85503c1 0
tlsrec-1s s30879a4293 10
tlsrec-3s s15f5cfcd23 10
tlsrec-before-sni s1855283037 10
tlsrec-multi sf0976f29b4 10
split-1 se00f908f95 20
split-1s saac5e9ce30 20
tlsrec-1s-split-1 se37a819919 20
random-tlsrec s554e5bf206 10
random-split s5642b6c74f 20
disorder-1 s49914f9987 40 disorder
disorder-1s s03a1c27bb3 40 disorder
oob-1 s13ab09bb4d 40 oob
oob-1s scacee1fcd5 40 oob
disoob-1s s4797c0aa21 40 disorder, oob
split-1s-wait-sent s09fcfddf9c 30 split-wait-sent (not macOS)
split-1s-wait-acked s30e9c34c4a 30 split-wait-acked
fake-ttl3, fake-ttl5, fake-ttl8 s9e8b15049f, s62607cf66f, sbdf889895a 50 fake-tcp (Windows, Linux)
fake-ttl5-sni s6aafd402a0 50 fake-tcp
split-1s-fake-ttl5 s6fae1e8a40 50 fake-tcp
disorder-1-fake-ttl5 s63fc503aca 60 disorder, fake-tcp

UDP presets (all need fake-udp: Windows, Linux, macOS):

Name ID Rank Burst
udp-zero-ttl3, -ttl5, -ttl8 u6ebe839e20, ud13ae97453, u8c9983f843 20 6 zero-payload datagrams, TTL 3 / 5 / 8
udp-zero-ttl5-r2 uebdba14699 10 2, TTL 5
udp-zero-ttl5-r12 ufe49f977b8 30 12, TTL 5
udp-quic-ttl3, -ttl5, -ttl8 ud88b8db473, u9309af956b, u127438077f 20 6 QUIC Initial decoys, QUIC only, TTL 3 / 5 / 8
udp-quic-ttl5-r2 u044556c7c3 10 2, TTL 5

Strategy controller

The optional controller package picks a verified strategy for a destination and network and caches it. It runs caller-supplied application probes with bounded concurrency, attempts and deadlines, shares discovery between callers, keeps negative results and a last-known-good ranking, and supports manual override and background revalidation. The connection wrapper does not depend on it, and it ships no probe targets of its own. A runnable example with a local HTTPS server is in controller/example_test.go.

desync-socks

To find a strategy first, see testing on your network.

A minimal SOCKS5 CONNECT proxy for trying methods on a real network:

go run ./cmd/desync-socks -listen 127.0.0.1:10802 -tlsrec 3+s
go run ./cmd/desync-socks -listen 127.0.0.1:10802 -split 1+s -split-wait acked
go run ./cmd/desync-socks -listen 127.0.0.1:10802 -split 1+s -disorder
go run ./cmd/desync-socks -listen 127.0.0.1:10802 -split 3+s -oob -oob-byte a
go run ./cmd/desync-socks -listen 127.0.0.1:10802 -tlsrec 1+s -fake -1 -ttl 4   # Windows, Linux
go run ./cmd/desync-socks -listen 127.0.0.1:10802 -fake -1 -ttl 4 -fake-sni www.example.com -fake-tls-mod rand
go run ./cmd/desync-socks -listen 127.0.0.1:10802 -split 1+s -disorder -fake -1 -ttl 4

Point the application under test at the SOCKS endpoint. It does not change system proxy settings, has no authentication and no UDP.

Testing on your network

desync-probe measures which strategies open which sites on the network it runs on. It needs no installation, no administrator rights and no arguments: it tests a built-in list of commonly filtered sites (plus ya.ru and example.com as controls) with every strategy your system supports, prints a summary and writes report.json next to the program. A run takes a few minutes; blocked sites that never answer make it slower, and it stops after 20 minutes at most.

Before you start: run it on the network you want to test (home Wi-Fi, mobile data), and turn off any VPN or proxy app, otherwise you measure the VPN. Download the file for your system from the latest release (or the desync-binaries artifact of a CI run).

Windows (desync-probe-windows-amd64.exe; -windows-arm64.exe for ARM laptops): double-click it. If SmartScreen warns, choose More info → Run anyway. The window stays open until you press Enter. Or in PowerShell:

cd $HOME\Downloads
.\desync-probe-windows-amd64.exe

macOS (desync-probe-darwin-arm64 for Apple silicon, -darwin-amd64 for Intel), in Terminal:

cd ~/Downloads
chmod +x desync-probe-darwin-arm64
xattr -d com.apple.quarantine desync-probe-darwin-arm64   # the binary is not notarized
./desync-probe-darwin-arm64

Linux (desync-probe-linux-amd64 or -linux-arm64):

chmod +x desync-probe-linux-amd64
./desync-probe-linux-amd64

Android in Termux (desync-probe-android-arm64):

cd ~
curl -LO https://github.com/Nemu-x/desync/releases/latest/download/desync-probe-android-arm64
chmod +x desync-probe-android-arm64
./desync-probe-android-arm64
termux-setup-storage                     # once; allow access when asked
cp report.json ~/storage/downloads/      # now it is in the phone's Downloads folder

Send back report.json (the program prints where it wrote it) as a file in a messenger or by email, and say which network it was (provider, home or mobile). The report contains the site names, the server addresses they resolved to, which strategies worked, error classes (reset, timeout, eof, cert, ...), timings, the program version, OS and CPU type, and what the library supports on that OS. It does not contain your IP addresses (local or public), MAC address, computer name or user name; DNS answers that are not public addresses are written as non-public, and error texts are stripped of addresses.

Useful options (-h lists all): -targets youtube.com,discord.com or -list sites.txt to test other sites, -presets tlsrec-1s,split-1s to test fewer strategies (-list-presets shows them with their IDs), -repeat 3 for more runs per pair, -dns 1.1.1.1 to bypass the system resolver. A custom strategy uses the desync-socks flags, e.g. -tlsrec 3+s -split 1+s -name mine. The strategies and their IDs come from the shared strategy catalog; every ID is derived from Options.Key(), so the same ID in two reports, or in a report and an application setting, means the same transformation. The exit code is 0 whenever the measurement ran, however many sites were blocked.

How a run works: every name is resolved once; then each strategy dials the first IPv4 answer, wraps the socket with desync, completes a TLS handshake that verifies the real certificate for that name, and sends HEAD / over HTTP/1.1 or HTTP/2. A strategy works for a site when every repetition got an HTTP response, is flaky when some did, and fails when none did. The probe also implements controller.Prober (a working strategy yields Application proof), so the same measurement can later drive automatic discovery.

Effectiveness

Whether a method works depends on the route and the filtering equipment. Test on the network you target, compare strategies on fresh connections (an established connection keeps its handshake), and keep the ordinary route available as a way back.

Development

go test -count=1 -timeout=3m ./...
go vet ./...
go test -race -count=1 ./...

Fuzz targets, benchmarks and what has been verified are listed in validation. Design notes: architecture, roadmap.

License

MIT, see LICENSE. The default fake TLS template, the fake, decoy, disorder and out-of-band mechanisms come from ByeDPI (MIT), the automatic TTL estimate from zapret (MIT); what was taken is listed in third_party/NOTICE.

Documentation

Index

Constants

View Source
const (
	DefaultMaxClientHelloSize    = 64 << 10
	DefaultMaxClientHelloRecords = 256
	DefaultClientHelloTimeout    = 5 * time.Second
)
View Source
const (
	// MaxRandomRanges bounds RandomOptions.TLSRec and RandomOptions.Split each.
	MaxRandomRanges = 16
	// MaxRandomCount bounds RandomCut.Count, the cuts drawn from one range.
	MaxRandomCount = 64
)
View Source
const DefaultOOBByte = 'a'

DefaultOOBByte is the out-of-band byte sent when OOBOptions.Byte is zero.

View Source
const DefaultQUICSNI = "www.wikipedia.org"

DefaultQUICSNI is the decoy server name of the QUIC Initial template when UDPOptions.SNI is empty (the server name of the TCP fake template).

Variables

View Source
var (
	// ErrAutoTTLUnsupported reports that this platform, or the connection's
	// address family, gives an unprivileged TCP socket no way to read the TTL
	// or hop limit of the packets it received. PlatformCapabilities().AutoTTL
	// and Unsupported["auto-ttl-ipv4"/"auto-ttl-ipv6"] say why.
	ErrAutoTTLUnsupported = errors.New("desync: auto TTL cannot observe the server's TTL here")
	// ErrAutoTTLUnobserved reports that the server's TTL was not observed or
	// does not yield a usable hop count, so no TTL could be derived.
	ErrAutoTTLUnobserved = errors.New("desync: auto TTL could not derive a TTL from the observed path")
)
View Source
var (
	// ErrSplitWaitUnsupported reports that the requested SplitWait mode cannot
	// be observed on this platform or socket. The wrapper never replaces it
	// with a timed pause; use SplitDelay for an explicit delay.
	ErrSplitWaitUnsupported = errors.New("desync: split wait is not supported on this platform")
	// ErrSplitWaitRequiresTCP reports a connection that does not expose a
	// TCP socket through SyscallConn, so its send queue cannot be observed.
	ErrSplitWaitRequiresTCP = errors.New("desync: split wait requires a TCP connection exposing SyscallConn")
	// ErrSplitWaitTimeout reports that the previous piece was not observed as
	// sent or acknowledged within SplitWaitTimeout. The connection is closed.
	ErrSplitWaitTimeout = errors.New("desync: split wait timed out")
)
View Source
var (
	// ErrDisorderUnsupported reports that Options.Disorder is not implemented
	// on this platform.
	ErrDisorderUnsupported = errors.New("desync: disorder is not implemented on this platform")
	// ErrOOBUnsupported reports that Options.OOB is not implemented on this
	// platform.
	ErrOOBUnsupported = errors.New("desync: out-of-band data is not implemented on this platform")
	// ErrSocketMethodRequiresTCP reports that Disorder or OOB was requested on
	// a connection that does not expose a TCP socket through SyscallConn.
	ErrSocketMethodRequiresTCP = errors.New("desync: disorder and OOB require a TCP connection exposing SyscallConn")
)
View Source
var (
	ErrFakeUnsupported = errors.New("desync: TCP fake is not implemented on this platform")
	ErrFakeRequiresTCP = errors.New("desync: TCP fake requires a connection exposing SyscallConn")
	ErrFakeBusy        = errors.New("desync: too many pending TCP fake operations")
	// ErrFakeFirstPiece reports that, for this ClientHello, the fake piece
	// would be the first TCP piece, which Disorder or OOB already sends. It
	// is returned before any byte is written.
	ErrFakeFirstPiece = errors.New("desync: the fake piece is the first TCP piece, which Disorder/OOB send; add a split position before the fake prefix")
)
View Source
var (
	ErrClientHelloTooLarge             = errors.New("desync: ClientHello exceeds configured size limit")
	ErrTooManyClientHelloRecords       = errors.New("desync: ClientHello exceeds configured record limit")
	ErrClientHelloTimeout        error = &clientHelloTimeoutError{}
)
View Source
var ErrIncompleteClientHello = errors.New("desync: incomplete TLS ClientHello")

ErrIncompleteClientHello means that record framing or the complete first ClientHello handshake needs more bytes. It is not a successful missing SNI.

View Source
var ErrNotClientHello = errors.New("desync: not a TLS ClientHello")

ErrNotClientHello means the bytes are not the start of a TLS ClientHello.

View Source
var ErrUDPFakeRequiresSocket = errors.New("desync: UDP fake requires an unconnected UDP socket exposing SyscallConn")
View Source
var ErrUDPFakeUnsupported = errors.New("desync: UDP fake is not implemented on this platform")

Functions

func ControlAutoTTL

func ControlAutoTTL(network, address string, c syscall.RawConn) error

ControlAutoTTL prepares a socket before it connects, so that ObserveHops and FakeOptions.AutoTTL can later read the TTL of the server's SYN-ACK. Use it as (or call it from) net.Dialer.Control. Where the platform or family cannot observe the TTL it does nothing and returns nil, so a dialer can use it everywhere; the failure is reported when the TTL is needed.

On Linux it enables IPV6_RECVHOPLIMIT on IPv6 sockets. The kernel then clones every in-order segment to record its hop limit; the *Conn wrapper disables the option again once it read the value for the fake.

func EstimateHops

func EstimateHops(ttl int) (int, bool)

EstimateHops guesses how many routers decremented a packet that arrived with the given TTL (or IPv6 hop limit). Senders start at one of the common initial values 64 (Linux, macOS, most network gear), 128 (Windows) or 255; paths are assumed shorter than 32 hops, so 33..64 is read as starting at 64, 97..128 at 128 and 224..255 at 255. Any other value is ambiguous and returns false. The guess is zapret's hop_count_guess (MIT, see third_party/NOTICE), except that 64 and 128 count as zero hops.

func FragmentRecord

func FragmentRecord(rec []byte, cuts []int) []byte

FragmentRecord splits one TLS record into several records at the given absolute offsets (header included in the numbering, like ParseHello's offsets). Every piece gets its own 5-byte header with the original content type and version, so the peer reassembles the same handshake message; a DPI box that only reads the first record no longer sees the whole ClientHello. Offsets outside the payload are ignored; rec is not modified.

func QUICInitialPayload

func QUICInitialPayload(sni string) ([]byte, error)

QUICInitialPayload returns one QUIC v1 Initial decoy of 1200 bytes whose ClientHello carries sni (DefaultQUICSNI when empty; ByeDPI wildcards '*', '?' and '#' are expanded). Use it as a fixed UDPOptions.Payload; UDPOptions.Template = UDPTemplateQUICInitial generates a fresh one per burst instead.

func SupportsFake

func SupportsFake() bool

SupportsFake reports platform availability, not effectiveness on a network.

func SupportsUDPFake

func SupportsUDPFake() bool

func WrapConn

func WrapConn(c net.Conn, opts Options) net.Conn

WrapConn returns c with the options applied, or c itself when they are empty.

Types

type AckCapability

type AckCapability struct {
	// Mechanism names the kernel interface, empty when there is none.
	Mechanism string
	// Sent and Acked report which SplitWait modes are implemented.
	Sent, Acked bool
	// Reliable is true when the decision reads the kernel's own per-socket
	// send-queue state. It is false when it is inferred from cumulative
	// counters that can run ahead of the queue, so the wait may end early
	// (see docs/connection-io.md).
	Reliable bool
}

AckCapability describes how SplitWait observes a socket on this OS.

func (AckCapability) Supports

func (a AckCapability) Supports(mode SplitWait) bool

Supports reports whether mode can be honored. SplitWaitNone always can.

type AckState

type AckState struct {
	// Unacked counts written bytes not yet acknowledged, sent or not.
	Unacked int64
	// Unsent counts written bytes not yet transmitted once.
	Unsent int64
	// InFlight counts transmitted bytes not yet acknowledged.
	InFlight int64
	// Sent is the cumulative number of distinct bytes transmitted on the
	// connection (counter-based platforms only).
	Sent int64
}

AckState is a snapshot of a socket's send queue. Values that the platform cannot observe are -1.

type AutoTTL

type AutoTTL struct {
	// Delta is subtracted from the hop count, 1..254 (0 means 1). It is the
	// margin between the fake's last hop and the server.
	Delta int
	// Min and Max bound the TTL, 1..255 (0 means 3 and 20). A path so short
	// that hops-Delta is below Min still uses Min, unless Min reaches the
	// server; then no TTL is derived.
	Min, Max int
}

AutoTTL derives a low TTL from the observed length of the path to the server: TTL = hops - Delta, clamped to Min..Max. A result that is not below the hop count would reach the server, so it is rejected instead. The hop count comes from the TTL of the server's packets (see EstimateHops), which assumes the return path is as long as the forward path.

Zero fields select the defaults (Delta 1, Min 3, Max 20, zapret's --dpi-desync-autottl default), so &AutoTTL{} is a complete policy.

func (AutoTTL) TTL

func (a AutoTTL) TTL(hops int) (int, bool)

TTL returns the TTL for a path of hops routers, or false when no TTL in Min..Max stays below the hop count. The policy must be valid.

func (AutoTTL) Validate

func (a AutoTTL) Validate() error

Validate checks the policy after applying defaults.

type AutoTTLCapability

type AutoTTLCapability struct {
	// Mechanism names the kernel interface, empty when there is none.
	Mechanism string
	// IPv4 and IPv6 report the peer families that can be observed.
	IPv4, IPv6 bool
}

AutoTTLCapability describes how the package observes the server's TTL.

type Capabilities

type Capabilities struct {
	// OS is runtime.GOOS.
	OS string
	// FakeTCP reports Options.Fake support (same as SupportsFake).
	FakeTCP bool
	// FakeUDP reports WrapPacketConn support (same as SupportsUDPFake).
	FakeUDP bool
	// TTLControl reports that the package changes and restores a socket's
	// TTL or hop limit on this OS (used by Fake, Disorder and the UDP burst).
	TTLControl bool
	// Disorder describes Options.Disorder support.
	Disorder DisorderCapability
	// OOB reports Options.OOB support.
	OOB bool
	// Ack describes send-queue / acknowledgement observation for SplitWait.
	Ack AckCapability
	// AutoTTL describes observing the server's TTL for FakeOptions.AutoTTL.
	AutoTTL AutoTTLCapability
	// Unsupported explains, keyed by method ("fake-tcp", "fake-udp",
	// "disorder", "oob", "auto-ttl-ipv4", "auto-ttl-ipv6"), why a method is
	// not implemented on this OS.
	Unsupported map[string]string
}

Capabilities describes what this build of the package implements on the current operating system. It reports availability, not effectiveness on a particular network. ProbeConn checks an actual socket.

func PlatformCapabilities

func PlatformCapabilities() Capabilities

PlatformCapabilities reports what the package implements on this OS.

type ClientHelloOptions

type ClientHelloOptions struct {
	// MaxSize includes the four-byte handshake header; valid range 4..1 MiB.
	MaxSize int
	// MaxRecords bounds input records through the end of the hello (1..4096).
	MaxRecords int
	// Timeout bounds incomplete assembly, not socket transmission. It starts
	// with the first accepted incomplete write and is never extended by input.
	// The valid range is positive through one minute; zero defaults to 5 s.
	Timeout time.Duration
}

ClientHelloOptions bounds first-handshake assembly. Zero fields use the defaults above. The options apply only when a transformation is enabled.

type Conn

type Conn struct {
	net.Conn
	// contains filtered or unexported fields
}

Conn applies Options to the first ClientHello written to an underlying conn.

func (*Conn) Close

func (c *Conn) Close() error

func (*Conn) FakeTTL

func (c *Conn) FakeTTL() (ttl int, auto bool)

FakeTTL reports the TTL the fake prefix was sent with, and whether AutoTTL derived it (false: the fixed or fallback Fake.TTL). It is 0 until the fake was attempted.

func (*Conn) NetConn

func (c *Conn) NetConn() net.Conn

NetConn returns the wrapped connection.

func (*Conn) Read

func (c *Conn) Read(p []byte) (int, error)

Read remains independent of the writer lock. Closing incomplete assembly wakes a pending Read and preserves its actual timeout reason.

func (*Conn) SetDeadline

func (c *Conn) SetDeadline(t time.Time) error

func (*Conn) SetWriteDeadline

func (c *Conn) SetWriteDeadline(t time.Time) error

func (*Conn) Write

func (c *Conn) Write(p []byte) (int, error)

Write holds a ClientHello until its whole handshake is complete, then sends it desynchronised; every other write goes straight through.

type ConnCapabilities

type ConnCapabilities struct {
	// TTL is the socket's current TTL (IPv4 peer) or unicast hop limit (IPv6
	// peer), or -1 when it cannot be read; TTLErr explains why.
	TTL    int
	TTLErr error
	// Ack lists the SplitWait modes this socket can honor; AckErr explains a
	// failed probe, in which case no mode is available.
	Ack    AckCapability
	AckErr error
	// State is the send-queue snapshot taken by the probe.
	State AckState
}

ConnCapabilities is the result of probing an actual connection.

func ProbeConn

func ProbeConn(c net.Conn) (ConnCapabilities, error)

ProbeConn queries the socket under c (unwrapping *Conn and other wrappers with a NetConn method) without changing it. It returns an error only when no socket is reachable; per-feature failures are reported in the result.

type DisorderCapability

type DisorderCapability struct {
	// Supported reports that Options.Disorder is implemented.
	Supported bool
	// Mechanism names the socket query used around the low-hop piece.
	Mechanism string
	// Observed is true when the hop limit is restored only after the kernel
	// reported the low-hop piece as transmitted. When false (macOS), the
	// piece is written once the send buffer is empty, so the kernel builds
	// its segment inside the send call; this is inferred, not observed.
	Observed bool
	// Reliable is true when the observation reads the kernel's queue state
	// rather than counters (see AckCapability.Reliable).
	Reliable bool
}

DisorderCapability describes Options.Disorder on this OS.

type FakeOptions

type FakeOptions struct {
	// TTL expires the fake before the server (1..255). With AutoTTL it is
	// the fallback used when the path cannot be observed; 0 then fails the
	// write instead.
	TTL int
	// AutoTTL derives the TTL from the TTL of the server's packets on this
	// connection (see AutoTTL, ControlAutoTTL and
	// PlatformCapabilities().AutoTTL).
	AutoTTL *AutoTTL
	// Prefix is the end of the fake piece, measured after TLS record
	// fragmentation. Negative offsets count from the end; zero defaults to
	// -1 (all but the last byte).
	Prefix int
	// Payload defaults to ByeDPI v0.17.3's MIT-licensed TLS template.
	// It is zero-padded or truncated to the fake piece's length.
	Payload []byte
	// Timeout defaults to 3 seconds for native queue/completion waits.
	// Normal tail writes obey the connection's write deadline. A deadline on
	// the wrapper can shorten native waits; Close interrupts them.
	Timeout time.Duration

	// SNI replaces the decoy's server name. With several names one is drawn
	// per connection. In a name '*' becomes a random letter or digit, '?' a
	// random letter and '#' a random digit. Names must be DNS host names
	// (at most 64 names). A template's padding extension absorbs the length
	// change, so the template keeps its 517 bytes.
	SNI []string
	// FromOriginal builds the decoy from the connection's own ClientHello
	// (ByeDPI --fake-tls-mod orig) instead of Payload or the template. It
	// needs SNI or Randomize and excludes Payload.
	FromOriginal bool
	// Randomize replaces the decoy's random, session ID and key share values
	// with fresh random bytes per connection (ByeDPI --fake-tls-mod rand).
	// It needs a ClientHello decoy (the template, a ClientHello Payload or
	// FromOriginal).
	Randomize bool
	// Offset starts the decoy transmission at this byte of the decoy
	// (ByeDPI --fake-offset), for example to align a decoy with a fake
	// piece that does not start the stream. 0 starts at its first byte.
	Offset int
}

FakeOptions changes one TCP piece's initial transmission to low-TTL decoy bytes while retaining the original piece for TCP retransmission. It does not insert bytes into the application's stream. Implemented on Windows and Linux (including Android); see PlatformCapabilities for the macOS reason.

Without Split the fake piece is the prefix [0, Prefix). With Split or Random.Split positions the prefix end is one more cut, and the fake piece is the piece that ends there (ByeDPI --fake as one of several parts): the pieces before it and after it are ordinary writes, Disorder/OOB still act on the first piece, and SplitDelay/SplitWaitSent apply between pieces.

type Hello

type Hello struct {
	// RecordLen is the full length of the first record, header included.
	RecordLen int
	// SNI is the server name, "" when the hello has none.
	SNI string
	// SNIOffset is where the server name starts; -1 without SNI. The name can
	// cross record headers, so use SNI rather than slicing contiguous wire bytes.
	SNIOffset int
}

Hello describes the first complete ClientHello. Offsets refer to original wire bytes, including any existing TLS record headers.

func ParseHello

func ParseHello(b []byte) (Hello, error)

ParseHello parses a complete ClientHello across consecutive TLS records. It uses default size/record limits, ignores bytes after its final record, and returns ErrIncompleteClientHello if the handshake is not complete. RecordLen remains the length of the first input record for compatibility.

type HopObservation

type HopObservation struct {
	// TTL is the TTL or hop limit of the last in-order segment received
	// from the server, normally its SYN-ACK.
	TTL int
	// Hops is EstimateHops(TTL).
	Hops int
}

HopObservation is what ObserveHops read from a connection.

func ObserveHops

func ObserveHops(c net.Conn) (HopObservation, error)

ObserveHops reads the TTL of the packets the server sent on c (unwrapping *Conn and other NetConn wrappers) and estimates the path length. It works only where PlatformCapabilities().AutoTTL reports the connection's family, and only for a socket created with ControlAutoTTL; otherwise the error wraps ErrAutoTTLUnsupported or ErrAutoTTLUnobserved. It changes nothing; call it before the first Write of a *Conn with AutoTTL, which turns the observation off once it used it.

type OOBOptions

type OOBOptions struct {
	// Byte is the urgent byte; zero selects DefaultOOBByte ('a').
	Byte byte
}

OOBOptions sends one byte of TCP urgent data (MSG_OOB) right after the first TCP piece (ByeDPI --oob). A receiver without SO_OOBINLINE removes the urgent byte from the stream, so the server reads the original bytes; an inspecting device that ignores the urgent pointer sees one extra byte.

type Options

type Options struct {
	// ClientHello bounds incomplete assembly. Nil/zero fields use defaults.
	ClientHello *ClientHelloOptions
	// Fake selects a platform-specific low-TTL first transmission.
	Fake *FakeOptions
	// TLSRec splits the ClientHello into separate TLS records at these
	// positions (ByeDPI --tlsrec).
	TLSRec []Pos
	// Split sends the (possibly record-split) ClientHello in separate TCP
	// writes, cut at these positions of the original record (ByeDPI --split).
	Split []Pos
	// SplitDelay is a pause between the TCP pieces; 0 sends them back to back.
	SplitDelay time.Duration
	// SplitWait waits, before each further TCP piece, until the kernel
	// reports the previous bytes as sent or acknowledged. Unsupported modes
	// fail Validate and the first Write; see PlatformCapabilities.
	SplitWait SplitWait
	// SplitWaitTimeout bounds each SplitWait wait (default 2 s, max 1 min).
	SplitWaitTimeout time.Duration
	// Random adds bounded random record/write cuts; nil keeps the strategy
	// deterministic. Include Key() in a strategy ID so it is never reused for
	// a different randomization policy or seed.
	Random *RandomOptions
	// Disorder sends the first TCP piece with hop limit 1, so it expires at
	// the first router, then restores the hop limit and sends the rest; TCP
	// retransmits the first piece with the normal hop limit (ByeDPI
	// --disorder). It needs Split or Random.Split positions. The hop limit is
	// restored once the kernel reports the piece as transmitted (bounded by
	// SplitWaitTimeout); see PlatformCapabilities().Disorder.
	Disorder bool
	// OOB sends one urgent (MSG_OOB) byte right after the first TCP piece
	// (ByeDPI --oob; with Disorder, --disoob). It needs Split or Random.Split
	// positions.
	OOB *OOBOptions
}

Options selects what happens to the first complete TLS ClientHello of a connection. Everything after it is passed through untouched, and a connection that does not start with a ClientHello is never modified.

func FakeTTLCandidates

func FakeTTLCandidates(base Options, from, to int) ([]Options, error)

FakeTTLCandidates expands a TTL range into one Options per TTL, in ascending order: copies of base with Fake.TTL set to from..to and Fake.AutoTTL cleared. It is the deterministic fallback where the path cannot be observed (see PlatformCapabilities().AutoTTL): register each candidate under an ID derived from its Key, which is stable for a given base and TTL, and let automatic discovery probe them. base.Fake must be set; the candidates share base's slices, which WrapConn copies.

func (Options) Enabled

func (o Options) Enabled() bool

Enabled reports whether the options change anything.

func (Options) Key

func (o Options) Key() string

Key returns a stable, canonical description of every field that affects the transformation, including the randomization policy and seed. Consumers that cache strategies by ID (for example with the controller package) should derive the ID or PolicyVersion from Key, so a cached ID never silently changes meaning. Equal keys mean identical behavior for this library version prefix; different keys may still behave alike (for example a nil ClientHello and its explicit defaults). The fake payload is represented by its SHA-256 digest rather than its bytes.

func (Options) Validate

func (o Options) Validate() error

type PacketConn

type PacketConn struct {
	net.PacketConn
	// contains filtered or unexported fields
}

PacketConn owns writes to its UDP socket. Do not write to the underlying socket separately: its TTL is changed temporarily during injection. ReadFrom, deadlines and Close retain net.PacketConn semantics. No raw socket or packet driver is required. Peers are bounded to 64 per socket and expire after idle.

func WrapPacketConn

func WrapPacketConn(pc net.PacketConn, opts UDPOptions) (*PacketConn, error)

func (*PacketConn) Close

func (c *PacketConn) Close() error

func (*PacketConn) Counters

func (c *PacketConn) Counters() UDPSnapshot

func (*PacketConn) WriteTo

func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (int, error)

type Pos

type Pos struct {
	Offset  int
	FromSNI bool
}

Pos is an absolute byte offset in the original TLS wire input, or an offset from the server name ("+s") in logical handshake bytes, skipping record headers.

func ParsePos

func ParsePos(s string) (Pos, error)

ParsePos parses "3" (byte 3 of the record) or "1+s" / "-2+s" (relative to the first byte of the server name). It is the notation ByeDPI uses.

func (Pos) String

func (p Pos) String() string

type RandomCut

type RandomCut struct {
	From, To Pos
	// Count is 1..MaxRandomCount; zero means one cut.
	Count int
	// MinPiece is 1..ClientHello max size bytes; zero means one byte.
	MinPiece int
}

RandomCut draws Count cut positions uniformly from the inclusive range [From, To]. Both ends use one anchor: absolute (wire offsets, like Pos) or SNI-relative (logical handshake bytes from the server name, like "+s").

Cuts are chosen among logical handshake byte boundaries, so an absolute range never places a cut inside an existing record header; a wire offset in a header means the start of that record's payload. The range is clipped to the ClientHello; an SNI-relative range is ignored without an SNI, like Pos.

MinPiece is the minimum distance between two cuts of this range and from either end of the handshake. When the clipped range cannot hold Count cuts at that spacing, as many as fit are drawn. A random cut that coincides with another cut or an existing record boundary adds nothing.

type RandomOptions

type RandomOptions struct {
	// TLSRec ranges add TLS record boundaries (randomized --tlsrec).
	TLSRec []RandomCut
	// Split ranges add TCP write boundaries (randomized --split). Like Split,
	// they cannot be combined with Fake yet.
	Split []RandomCut
	// Seed makes the draws reproducible: every connection wrapped with the
	// same nonzero Seed and options cuts the same ClientHello identically,
	// which is meant for tests and bug reports. Zero (the default) draws an
	// independent seed from crypto/rand for every connection.
	Seed uint64
}

RandomOptions adds bounded random cuts to the deterministic TLSRec/Split positions. It is opt-in: a nil Options.Random keeps a strategy fully deterministic, and the deterministic positions keep their exact meaning when randomization is enabled; random cuts are only added to them.

Random cuts are drawn once per connection, for its first ClientHello. They never change logical handshake bytes, never create an empty TLS record or an empty write, and add no delay beyond an explicitly configured SplitDelay.

type SplitWait

type SplitWait uint8

SplitWait selects what the wrapper observes on the socket before it writes the next TCP piece of a split ClientHello. It is not a delay: the wait ends as soon as the kernel reports the condition, and fails with an error when it cannot be observed. The OS still decides how bytes become segments.

const (
	// SplitWaitNone writes the pieces back to back (plus SplitDelay).
	SplitWaitNone SplitWait = iota
	// SplitWaitSent waits until every byte written so far has been
	// transmitted at least once (it left the socket's unsent queue).
	SplitWaitSent
	// SplitWaitAcked waits until every byte written so far has been
	// acknowledged by the peer.
	SplitWaitAcked
)

func ParseSplitWait

func ParseSplitWait(s string) (SplitWait, error)

ParseSplitWait parses "none", "sent" or "acked".

func (SplitWait) String

func (w SplitWait) String() string

type UDPCounters

type UDPCounters struct {
	// contains filtered or unexported fields
}

UDPCounters is a concurrency-safe, optional aggregate; its zero value is ready.

func (*UDPCounters) Snapshot

func (c *UDPCounters) Snapshot() UDPSnapshot

type UDPOptions

type UDPOptions struct {
	TTL int
	// Repeats defaults to 6, and must be between 1 and 16.
	Repeats int
	// Packets defaults to 3 initial application datagrams per peer (1-8).
	Packets int
	// Payload defaults to 64 zero bytes. Custom payloads may contain 1-1200 bytes.
	Payload []byte

	// QUIC decoys. Template generates the decoy instead of Payload (which
	// must then be empty); SNI is the server name in the QUIC Initial
	// template's ClientHello (default DefaultQUICSNI, wildcards as in
	// FakeOptions.SNI); QUICOnly limits the burst to datagrams that look like
	// a QUIC client Initial (long header, version 1, a draft or 2, Initial
	// type, at least 1200 bytes), so other UDP traffic passes untouched and
	// does not count towards Packets.
	Template UDPTemplate
	SNI      string
	QUICOnly bool

	// Counters can be shared by sockets belonging to one outbound. Do not copy
	// or reset a counters value after it has been used.
	Counters *UDPCounters
}

UDPOptions inserts a bounded burst of low-TTL datagrams before the first few application datagrams to each peer. The original payload is unchanged. TTL must expire beyond the inspecting device and before the actual peer; effectiveness and a suitable TTL must be established on the user's network.

func UDPTTLCandidates

func UDPTTLCandidates(base UDPOptions, from, to int) ([]UDPOptions, error)

UDPTTLCandidates is FakeTTLCandidates for the UDP burst. The burst precedes the peer's first reply, so there is no packet whose TTL could be observed in time; a TTL range probed by discovery, or AutoTTL.TTL applied to the hops ObserveHops measured on a TCP connection to the same server, selects it instead.

func (UDPOptions) Key

func (o UDPOptions) Key() string

Key returns a stable, canonical description of the fields that affect the UDP burst, like Options.Key (Counters is not part of it), including the QUIC decoy Template, SNI and QUICOnly. The payload is represented by its SHA-256 digest.

func (UDPOptions) Validate

func (o UDPOptions) Validate() error

type UDPSnapshot

type UDPSnapshot struct {
	Packets     uint64 `json:"packets"`
	FakePackets uint64 `json:"fakePackets"`
	FakeErrors  uint64 `json:"fakeErrors"`
	Peers       uint64 `json:"peers"`
}

type UDPTemplate

type UDPTemplate int

UDPTemplate selects a generated decoy for the UDP burst.

const (
	// UDPTemplateZero is 64 zero bytes (ByeDPI's UDP fake), the default.
	UDPTemplateZero UDPTemplate = iota
	// UDPTemplateQUICInitial is a QUIC v1 client Initial packet (RFC 9000,
	// RFC 9001): long header, version 1, random 8-byte DCID and SCID, a
	// CRYPTO frame with a TLS ClientHello for UDPOptions.SNI, PADDING to
	// 1200 bytes, payload and header protection with the Initial keys derived
	// from the DCID. An inspecting device that decrypts Initials, as anyone
	// can, reads the decoy's server name. A new packet is generated for every
	// burst.
	UDPTemplateQUICInitial
)

func (UDPTemplate) String

func (t UDPTemplate) String() string

Directories

Path Synopsis
cmd
desync-probe command
Command desync-probe measures which desync strategies reach which sites on the network it runs on, and writes a report that is safe to share.
Command desync-probe measures which desync strategies reach which sites on the network it runs on, and writes a report that is safe to share.
desync-socks command
Command desync-socks is a minimal SOCKS5 proxy (CONNECT only, no auth) that applies desync.Options to every outgoing connection.
Command desync-socks is a minimal SOCKS5 proxy (CONNECT only, no auth) that applies desync.Options to every outgoing connection.
Package controller selects opaque strategy IDs using explicitly supplied, application-aware probes.
Package controller selects opaque strategy IDs using explicitly supplied, application-aware probes.
Package presets is the shared strategy catalog of desync: named TCP transformations (desync.Options) and UDP bursts (desync.UDPOptions) with stable IDs, the platform features each one needs, and an ordering hint.
Package presets is the shared strategy catalog of desync: named TCP transformations (desync.Options) and UDP bursts (desync.UDPOptions) with stable IDs, the platform features each one needs, and an ordering hint.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL