zapret

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: GPL-3.0 Imports: 16 Imported by: 0

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

View Source
const Unreachable int64 = 1<<62 - 1

Unreachable sorts after every real measurement.

Variables

This section is empty.

Functions

func Apply

func Apply(ctx context.Context, client *http.Client, dir string, rel Release) error

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

func Covered(dir string) []string

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

func ExcludeNodes(dir string, servers []string) error

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

func Newer(local, remote string) bool

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

func PinInterface(bat string, ifIndex int) (string, bool)

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

func StripVoiceUDP(bat string) (string, bool)

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

func Version(dir string) string

Version reports the installed bundle's release, or "" when unknown (no bundle, or one installed from a source that carried no version).

func VersionFromName

func VersionFromName(name string) string

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

func WriteVersion(dir, version string) error

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

func LatestRelease(ctx context.Context, client *http.Client) (Release, error)

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

func Best(results []Result, baseline int) (Result, bool)

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

func Rank(results []Result) []Result

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.

func (Result) OKCount

func (r Result) OKCount() int

OKCount is how many targets completed.

func (Result) Score

func (r Result) Score() int64

Score is the median round-trip over the targets that completed, or Unreachable when none did.

Median rather than mean for the same reason node selection uses it: one slow destination should not decide between two otherwise equal strategies.

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 NewRunner

func NewRunner(dir string) *Runner

func (*Runner) Pick

func (r *Runner) Pick(context.Context, []Strategy, []string, func(Result)) ([]Result, int, error)

func (*Runner) Probe

func (r *Runner) Probe(context.Context, []string) []TargetResult

func (*Runner) Start

func (r *Runner) Start(context.Context, Strategy) (bool, error)

func (*Runner) Stop

func (r *Runner) Stop(context.Context) error

func (*Runner) StrategyPath

func (r *Runner) StrategyPath(name string) string

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

func Discover(dir string, names []string) []Strategy

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

func Install(archivePath, dir string) ([]Strategy, error)

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

func InstallDir(src, dir string) ([]Strategy, error)

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.

Jump to

Keyboard shortcuts

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