Documentation
¶
Overview ¶
Package zapret drives a bundled zapret installation: the set of DPI-bypass strategies shipped as .bat files around winws.exe.
Why this belongs in a VPN client at all: a tunnel and a DPI bypass solve the same problem in opposite ways. The tunnel moves traffic to another country -- correct, but it adds the whole round trip to every packet, which is what makes voice chat and video unpleasant. zapret instead confuses the censor's inspection so the traffic reaches its real destination directly, at the ISP's own latency. For the services it can handle, that is strictly better, and the tunnel is left for what it cannot.
The hard part is that there is no single strategy: the bundle ships around twenty because each provider's DPI implementation falls for something different, and which one works is discoverable only by trying. Doing that by hand means running a batch file, opening a site, guessing, repeating. This package makes it a measurement.
Index ¶
- Constants
- func Apply(ctx context.Context, client *http.Client, dir string, rel Release) error
- func Covered(dir string) []string
- func DefaultTargets() []string
- func ExcludeNodes(dir string, servers []string) error
- func Newer(local, remote string) bool
- func PinInterface(bat string, ifIndex int) (string, bool)
- func StripVoiceUDP(bat string) (string, bool)
- func Version(dir string) string
- func VersionFromName(name string) string
- func WriteVersion(dir, version string) error
- type Release
- type Result
- type Runner
- func (r *Runner) Pick(context.Context, []Strategy, []string, func(Result)) ([]Result, int, error)
- func (r *Runner) Probe(context.Context, []string) []TargetResult
- func (r *Runner) Start(context.Context, Strategy) (bool, error)
- func (r *Runner) Stop(context.Context) error
- func (r *Runner) StrategyPath(name string) string
- type Strategy
- type TargetResult
Constants ¶
const Unreachable int64 = 1<<62 - 1
Unreachable sorts after every real measurement.
Variables ¶
This section is empty.
Functions ¶
func Apply ¶
Apply downloads a release and replaces the bundle at dir with it.
The replacement is staged: the archive is unpacked beside the live bundle and verified (Install refuses anything without strategies and bin/winws.exe), and only then swapped in. A download that arrives truncated, or a "release" that is not a bundle, therefore leaves the working installation untouched — the alternative, unpacking over the live directory, turns one bad night at the mirror into a machine with no bypass at all.
The caller must stop winws first. On Windows a running executable pins its directory, so the swap fails while a strategy is up — and installing a new bypass under a running old one is not something to do quietly anyway.
func Covered ¶
Covered reads the installed bundle's host lists and returns the domains the bypass acts on, lowercased, de-duplicated and sorted.
A missing or unreadable list is not an error: the lists are optional per strategy, and an empty result simply means "coverage unknown", which the caller must treat as "cover nothing extra" rather than "cover everything". The failure mode this guards against is concrete — assuming the bypass covers a service it does not (say api.anthropic.com, which no shipped list mentions) routes that service direct into a censor that answers with a forged 403.
func DefaultTargets ¶
func DefaultTargets() []string
DefaultTargets are the destinations a strategy is judged on.
They are deliberately the ones that break: Discord's API and CDN, YouTube's page, its video hosts and its image CDN. Probing something universally reachable would score every strategy as perfect, including the ones that change nothing.
Probes must be made WITHOUT any proxy. Through a tunnel they would all succeed regardless of the strategy — measuring the tunnel, not the bypass.
func ExcludeNodes ¶
ExcludeNodes writes the exit-node addresses into the bundle's exclusion list, preserving any lines the user put there themselves.
Why the bypass must not touch the tunnel's own traffic: winws attaches to every interface and filters by port, and the ports it watches (443, 8443, 2053, 2083, 2087, 2096) are exactly the ports exit nodes are commonly served on. A VLESS-Reality handshake carries a TLS ClientHello like any other, so a desync aimed at the censor can land on the connection to the node instead — the observed shape is a TCP port that opens and then goes silent, which reads as "the node is down" and sends the user hunting through their subscription for a fault that is on their own machine.
Only IP literals are written. Resolving a hostname here would mean a DNS lookup on a path that may not be up yet, and a wrong or stale answer would exclude someone else's address while leaving the node exposed.
func Newer ¶
Newer reports whether remote is a later release than local.
Versions are compared component by component as numbers, so 1.10.1 correctly beats 1.9.9 — a string comparison would get that backwards, and getting it backwards means silently re-installing an older bundle over a newer one. An unknown local version (empty) counts as older: the bundle came from somewhere that carried no version, and taking the published release is the safe resolution. An unparseable remote version is never taken.
func PinInterface ¶
PinInterface confines the packet filter to one network interface and reports whether anything changed.
Without it, winws filters every interface on the machine — including the tunnel's own adapter. Inside a tun the traffic is still plain TLS, so the desync fires a second time on the very ClientHello sing-box is about to read, and sing-box's stack (unlike the router the fake packet was crafted to fool) accepts it. The stream is then garbage: sniffing times out, the domain rule that would have sent YouTube down the direct path never matches, and the connection falls through to the tunnel and hangs there.
Measured on a live machine, both directions: with the filter unpinned, a browser reached YouTube and Discord through the tunnel not at all (curl never finished the handshake, the tunnel log showed a flat 300ms sniff timeout on every attempt) while everything absent from zapret's host lists was fine. Bound to the physical adapter, both came back. The bypass still does its job — it is applied where the packets actually meet the DPI, and nowhere else.
The index is WinDivert's interface index (Windows ifIndex). A non-positive index means "not known", and the batch is returned untouched: filtering everything is wrong, but filtering a guessed interface is worse — it would silently bypass nothing at all.
func StripVoiceUDP ¶
StripVoiceUDP removes the strategy's real-time-UDP block and reports whether anything changed.
The block it removes looks like this, one line of a shipped strategy:
--filter-udp=19294-19344,50000-50100 --filter-l7=discord,stun --dpi-desync=fake --dpi-desync-fake-discord=... --dpi-desync-fake-stun=... --new
What it does is punch Discord's voice through the DPI on the *direct* path, which is exactly right for a machine with no tunnel — and exactly wrong for one with a tunnel carrying voice. WinDivert grabs those packets before they ever reach the tunnel's router, so the traffic leaves from the ISP address no matter what the routing table says; on a network where Discord's voice servers are blocked by address, the client then sits on "no route" forever while the routing rules insist the tunnel should have carried it.
Measured on the author's network, both directions: bypass running, voice UDP exits the ISP address and voice never connects; bypass stopped, the same UDP exits the node and voice works — but YouTube dies, because the rest of the strategy went with it. Removing this one block is what lets both work at once.
The port list is also taken out of the WinDivert capture filter (--wf-udp), so the packets are not intercepted at all rather than intercepted and passed through: an untouched packet cannot be reinjected on the wrong path.
Everything else is left byte for byte. The other blocks are what carry YouTube.
func Version ¶
Version reports the installed bundle's release, or "" when unknown (no bundle, or one installed from a source that carried no version).
func VersionFromName ¶
VersionFromName extracts a release version from an archive or folder name.
It is how an imported bundle gets a version at all: the release archive is named after its version, and reading it means the first update check compares against something real instead of re-downloading a bundle the user just installed by hand.
func WriteVersion ¶
WriteVersion records the installed release inside the bundle directory.
Types ¶
type Release ¶
type Release struct {
// Version is the release tag, e.g. "1.10.1".
Version string `json:"version"`
// ArchiveURL is the .zip asset's download URL.
ArchiveURL string `json:"-"`
// Size is the asset's size in bytes, as reported by the API.
Size int64 `json:"size,omitempty"`
}
Release is a published bundle version and the archive to fetch it from.
func LatestRelease ¶
LatestRelease reports the newest published bundle.
Only .zip assets are considered. Upstream publishes .rar and .tar.gz beside the zip, and the installer reads zip alone — picking an archive this program cannot open would turn every update into a failure that looks like a network problem.
type Result ¶
type Result struct {
Strategy Strategy `json:"-"`
Name string `json:"strategy"`
// Started reports whether winws actually came up. A strategy whose process
// never started scores zero for a reason worth telling apart from "started
// and did not help".
Started bool `json:"started"`
Targets []TargetResult `json:"targets"`
}
Result is a strategy's measured outcome.
func Best ¶
Best returns the strategy to keep, and whether it is worth keeping at all.
baseline is how many targets already worked with zapret off. A strategy is only reported when it beats that: if everything already works, enabling a packet-mangling driver buys nothing and costs a kernel filter on every connection. And if nothing beats the baseline, saying so is far more useful than handing back the least-bad option and letting the user believe the block is now handled.
func Rank ¶
Rank orders strategies best-first: coverage, then latency, then bundle order.
Coverage dominates because the strategies differ in *what* they unblock, not in speed: one may carry YouTube but not Discord voice. A strategy that carries four of five destinations slowly is worth more than one that carries two quickly, and ranking by latency first would systematically pick the narrower bypass.
type Runner ¶
type Runner struct {
Dir string
Settle time.Duration
ProbeTimeout time.Duration
KeepVoiceInTunnel bool
PinIfaceIndex int
}
Runner is the non-Windows stub.
zapret's Windows build is winws.exe on the WinDivert driver; there is no equivalent to drive here. The type exists so the control layer compiles and vets on every platform (the same split core/control uses for its own OS-specific pieces) and reports plainly that the feature is Windows-only rather than failing somewhere deeper with a confusing error. Every knob the control layer sets on a Runner has to exist here too, even where nothing reads it: core/control is platform-neutral and assigns them unconditionally, so a field present only in the Windows build breaks the vet and the build on every other platform. PinIfaceIndex was added to the Windows runner without a counterpart here and did exactly that.
func (*Runner) StrategyPath ¶
type Strategy ¶
type Strategy struct {
// Name is the display name, taken from the file name without extension.
Name string
// Path is the absolute path of the .bat that launches it.
Path string
}
Strategy is one bypass configuration from the bundle.
func Discover ¶
Discover turns a directory listing into the strategies worth probing.
It takes the file names rather than reading the disk so the selection rules stay testable without a bundle on hand — the platform layer supplies the listing.
Ordering is alphabetical with the plain "general" first: it is the bundle's default and the one most likely to work, so a probe run that is interrupted early has still tried the best candidate.
func Install ¶
Install unpacks a zapret bundle archive into dir and reports the strategies it found.
The archive is taken exactly as downloaded. A release page hands out a zip whose contents sit under a single versioned folder (`zapret-discord-youtube-1.10.1/…`); that wrapper is stripped so upgrading to a new release does not nest the bundle one level deeper every time and quietly break every stored strategy path.
Only .zip is handled. RAR is a proprietary format with no decoder in the standard library, and adding one to a privacy tool for the sake of an archive format is a dependency to audit forever — the upstream project publishes zip releases, so the honest answer to a .rar is to say so.
func InstallDir ¶
InstallDir copies an already-unpacked bundle into dir.
It follows the same rule as the archive path: if the given folder is just a wrapper holding the real bundle (which is what unpacking a release usually produces — `zapret-discord-youtube-1.10.1/` inside the folder you unpacked into), descend into it, so the installed layout is identical either way and stored strategy paths keep working.
type TargetResult ¶
type TargetResult struct {
// Target is the probed URL.
Target string `json:"target"`
// OK is true when the request completed — any HTTP status counts, because a
// censored destination fails by timing out or resetting, not by answering.
OK bool `json:"ok"`
// RTTMs is the round-trip in milliseconds; meaningful only when OK.
RTTMs int64 `json:"rtt_ms"`
}
TargetResult is one control request made while a strategy was active.