All systems operational Amsterdam · Paris · Reykjavík +5 Pay with Cryptocurrency
Networking & self-hostingIntermediate21 min readUpdated 2026-08-31

Get a VPN past deep packet inspection

A censor does not block “VPNs”. It blocks a protocol it can recognise — and WireGuard is recognisable in its very first packet. Here is what deep packet inspection really matches on, which obfuscated protocols survive it, and how to run one yourself.

Get a VPN past deep packet inspection
On this page
  1. What deep packet inspection actually matches on
  2. Why moving WireGuard to port 443 does not work
  3. The families that get through, and what each costs you
  4. What you need before you start
  5. Step-by-step
  6. Active probing, and servers that know how to stay quiet
  7. The address is the scarce resource
  8. What obfuscation does not do
  9. Jurisdiction, payment, and why the account matters
  10. Frequently asked questions

The morning a tunnel stops connecting, the instinct is to blame the server. It is almost never the server. What changed is that something between you and it started looking at the shape of your packets instead of only their destination, decided that shape was a VPN, and dropped it. Moving to port 443 will not help, because the port was never what gave you away.

So this guide starts with the part most tutorials skip: what deep packet inspection actually matches on, why WireGuard is the easiest tunnel in the world to fingerprint, and what each family of obfuscation costs you in exchange for getting through — randomised traffic, borrowed TLS, and QUIC that passes for HTTP/3. Then the work: one binary, three working server configs, and a way to test the result the way a censor would rather than the way a hopeful user does. The machine underneath can sit in a jurisdiction the filter has no reach into, rented without ID and paid in Monero — which matters more than it sounds, and the last section explains why.

What deep packet inspection actually matches on

“DPI” sounds like an intelligence problem. Most of the time it is a string match. A filter sitting on a national link cannot afford to think hard about every flow, so it leans on the cheapest signals that work, in roughly this order:

  • A protocol fingerprint at a fixed offset. The first packet of a WireGuard session is a handshake initiation: exactly 148 bytes, first byte 0x01, next three bytes zero. OpenVPN announces itself with an opcode in the first byte and a session id that never varies in position. Matching either is a few instructions per packet with no state to keep, which is why it is always the first thing deployed.
  • Entropy, when there is no fingerprint to find. If a flow’s first bytes are uniformly random — no printable ASCII, no recognisable header — that itself is a signal, because almost nothing legitimate looks like that from byte zero. Filters have blocked whole categories of “fully encrypted” traffic on this basis alone, which is exactly the trap the randomising protocols fall into.
  • Active probing. The filter notes your server’s address, then connects to it itself, from somewhere else, and watches how it replies. A proxy that answers, errors in a characteristic way, or simply holds the connection open confirms the suspicion. This is how an address moves from “maybe” to a blocklist.
  • Behaviour over time. A single long-lived flow to one foreign address carrying a few hundred megabytes with a residential-looking day/night curve is a distinctive thing to be. No amount of payload obfuscation changes it.
  • The address itself. Known VPS ranges, addresses seen in public subscription lists, anything a commercial provider advertises. This one costs the censor nothing and is the reason a shared proxy address dies faster than a protocol does.

The useful takeaway is that the first three are the ones you can do something about, and they are the ones this guide addresses. The last two are why the section on protecting your address is not an afterthought.

Why moving WireGuard to port 443 does not work

It is the most repeated piece of advice on the subject and it has almost no effect, for a reason worth stating plainly: the signature is in the payload, not in the port number. A filter that matches 0x01 at offset zero of a 148-byte UDP datagram finds it on port 443 exactly as easily as on 51820.

Worse, the move can make you stand out. Port 443 carries HTTPS, which is TCP, and HTTP/3 over QUIC, which is UDP but begins with a recognisable QUIC long header and a TLS ClientHello inside it. A UDP flow on 443 that is neither is a small anomaly on a link where everything else on that port is one or the other.

The same goes for the rest of the folklore. Changing MTU alters fragmentation, not the handshake bytes. Adding a pre-shared key to WireGuard strengthens the cryptography and leaves the wire format untouched. Putting the tunnel on a non-standard high port buys you nothing against payload matching, though it does cut the background noise from opportunistic scanners.

What changes the answer is changing what is on the wire: either make the bytes look like nothing recognisable, or make them look like something the censor has decided not to block. Everything below is one of those two moves.

The families that get through, and what each costs you

There are four practical approaches. They are not ranked by strength alone, because the right one depends on what is being done to your traffic.

  • Randomised, header-free traffic — Shadowsocks-2022. The 2022 AEAD ciphers (2022-blake3-aes-128-gcm and friends) put no plaintext header on the wire at all: from the first byte a session is indistinguishable from random data, and a wrong key produces no reply to probe. Cheap, fast, ten minutes to deploy, excellent almost everywhere. Its one weakness is the entropy signal above — against a filter that blocks unclassifiable random flows on principle, looking like nothing is not the same as looking innocent.
  • Borrowed TLS — VLESS with Reality. The strongest option in general use today. Your server performs a genuine TLS 1.3 handshake, and any client that cannot authenticate is handed transparently to a real third-party site, whose real certificate it duly receives. A prober sees an ordinary visitor connecting to an ordinary website, because that is precisely what happened. You need no domain, no certificate and no certificate-transparency footprint — there is nothing registered anywhere that points at you.
  • TLS you own — Trojan, or VLESS over WebSocket behind a web server. The tunnel hides inside your own HTTPS site, on your own domain and certificate. Conceptually simpler and it coexists with a real website on the same address. The cost is that the domain is now a burnable identifier, the certificate is a public record in CT logs, and blocking one hostname is a cheap move for a censor.
  • QUIC-shaped — Hysteria2 and TUIC. These pass for HTTP/3 and can serve a real website to anything that fails authentication. The reason to choose them is usually not censorship at all but loss: on a path that drops five or ten percent of packets, they hold throughput where anything TCP-based collapses. The trade-offs are real — some networks throttle or drop UDP wholesale, and Hysteria’s aggressive congestion control is a poor neighbour if you set the bandwidth figures dishonestly.

If you want a default: deploy Reality as the primary, keep Shadowsocks-2022 on a second port as the fallback, and add Hysteria2 only if your problem turns out to be a lossy path rather than a filtered one. All three run from one daemon, and the steps below configure each in turn.

What you need before you start

Less than you would expect. This is a light workload; the constraint is bandwidth and latency, not the server.

  • A small plan. Encryption and TLS termination are cheap on any modern vCPU with AES-NI. Pup (1 vCPU / 1 GB / 25 GB NVMe, $3.50/mo) carries a personal tunnel comfortably; Cub (1 vCPU / 2 GB / 40 GB, $5.00/mo) is the sensible size if several people share it; Scout (2 vCPU / 4 GB / 70 GB, $9.00/mo) if it is a household or a small team and you want headroom. Traffic is unlimited at 1 Gbps on every tier, which is the specification that actually matters for a tunnel.
  • The right location, chosen for the path. This is the decision people get wrong. Pick the nearest jurisdiction whose route from you is not filtered rather than the most exotic one: for most readers that is Amsterdam, Paris, Bucharest or Sofia, and for anyone in Asia, Kuala Lumpur. Reykjavik and Zurich buy legal distance, not speed — useful for what you host, less useful for what you tunnel through.
  • Debian 13 or Ubuntu LTS from the template library. Full KVM virtualisation means a real kernel and your own network stack, which is what lets you bind low ports and shape UDP without asking anyone.
  • A dedicated IPv4 with no history. Every plan ships one, plus an IPv6 /64. An address that has never sat in a shared proxy pool is the single most valuable thing here, and it is the reason recycled budget-cloud addresses fail within days.
  • Ten minutes of hardening first. SSH keys only, no password logins, default-deny firewall. The Debian hardening checklist covers it; an obfuscated tunnel on a box with a guessable root password is an elaborate lock on an open door.
  • A domain — only if you choose the TLS-you-own route. Reality and Shadowsocks need none, which is a large part of their appeal.

Step-by-step

  1. Deploy in a location the filter does not reach, and harden it first

    Deploy Debian 13 from the template library in the nearest location whose path from you is clean, and give the box ten minutes of basic hardening before anything else: key-only SSH, root password login disabled, nftables default-deny, unattended security upgrades. Then bring it up to date and check the clock, which Reality depends on:

    apt update && apt full-upgrade -y
    apt install -y curl ca-certificates systemd-timesyncd
    timedatectl set-ntp true
    timedatectl status

    Keep this server single-purpose. A tunnel that shares an address with a public web service inherits every reputational problem that service ever has.

  2. Install sing-box from the official repository

    One daemon covers all three protocols, so there is no reason to run three. Use the project’s own repository rather than a distribution package, because this is software where being a release behind is a real cost:

    mkdir -p /etc/apt/keyrings
    curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
    chmod a+r /etc/apt/keyrings/sagernet.asc
    printf 'Types: deb\nURIs: https://deb.sagernet.org/\nSuites: *\nComponents: *\nEnabled: yes\nSigned-By: /etc/apt/keyrings/sagernet.asc\n' > /etc/apt/sources.list.d/sagernet.sources
    apt update && apt install -y sing-box
    sing-box version

    The package ships a systemd unit that reads /etc/sing-box/config.json. Everything below writes that one file; add the inbounds you want and leave out the ones you do not.

  3. Option A — Shadowsocks-2022, the ten-minute fallback

    Start here even if Reality will be your primary, because it takes two minutes and gives you a second door that fails differently. Generate a key of the right length for the cipher — 16 bytes for aes-128, 32 for aes-256:

    sing-box generate rand --base64 16

    Then the inbound, on a high port rather than a memorable one:

    {
      "log": { "level": "warn" },
      "inbounds": [
        {
          "type": "shadowsocks",
          "tag": "ss-in",
          "listen": "::",
          "listen_port": 23456,
          "method": "2022-blake3-aes-128-gcm",
          "password": "PASTE_THE_GENERATED_KEY"
        }
      ],
      "outbounds": [ { "type": "direct" } ]
    }

    Write it to /etc/sing-box/config.json, then systemctl enable --now sing-box. Note there is no TLS here and no certificate to obtain: the protection is that the traffic has no recognisable header at all, and a wrong key gets no answer.

  4. Option B — VLESS with Reality, borrowing someone else’s TLS

    This is the one to run as the primary. Generate the keypair and the identifiers first, and keep the PrivateKey on the server and the PublicKey for the client:

    sing-box generate reality-keypair
    sing-box generate uuid
    openssl rand -hex 8

    Then the inbound. handshake.server is the real site your server impersonates — choose one that is popular, fast from your server’s location, supports TLS 1.3, and is not itself likely to be blocked where you are:

    {
      "type": "vless",
      "tag": "vless-in",
      "listen": "::",
      "listen_port": 443,
      "users": [
        { "uuid": "GENERATED_UUID", "flow": "xtls-rprx-vision" }
      ],
      "tls": {
        "enabled": true,
        "server_name": "www.cloudflare.com",
        "reality": {
          "enabled": true,
          "handshake": { "server": "www.cloudflare.com", "server_port": 443 },
          "private_key": "GENERATED_PRIVATE_KEY",
          "short_id": [ "GENERATED_HEX" ]
        }
      }
    }

    Add that object to the inbounds array beside the Shadowsocks one and reload with systemctl restart sing-box. The client needs five values: your address, port 443, the UUID, the public key, and the short id — plus the same server_name, which is what makes the handshake coherent.

  5. Option C — Hysteria2, when the path is lossy rather than filtered

    Add this one only if you have measured loss on the path and TCP-based tunnels are stalling. It speaks QUIC, so it needs a certificate — either a real one for a domain you own, or a self-signed pair the client is told to expect:

    {
      "type": "hysteria2",
      "tag": "hy2-in",
      "listen": "::",
      "listen_port": 8443,
      "up_mbps": 100,
      "down_mbps": 100,
      "users": [ { "password": "A_LONG_RANDOM_PASSWORD" } ],
      "masquerade": "https://news.ycombinator.com/",
      "tls": {
        "enabled": true,
        "alpn": [ "h3" ],
        "certificate_path": "/etc/sing-box/cert.pem",
        "key_path": "/etc/sing-box/key.pem"
      }
    }

    masquerade is what an unauthenticated visitor gets: a plain HTTP/3 reverse proxy to a real site instead of an error that identifies the daemon. Set up_mbps and down_mbps to figures you can actually sustain — the congestion control believes them, and inflated numbers make you an unpleasant neighbour on the link rather than a faster one.

  6. Open exactly the ports you use, and nothing else

    Default-deny, then the few things that must be reachable. Assuming the nftables setup from the hardening guide, add the tunnel ports to the input chain:

    nft add rule inet filter input tcp dport 443 accept
    nft add rule inet filter input udp dport 443 accept
    nft add rule inet filter input udp dport 23456 accept
    nft list ruleset > /etc/nftables.conf

    Two details that matter more than they look. Keep SSH on its own port and firewalled to the addresses you actually use, because an open SSH port is a much better fingerprint of “server someone administers remotely” than the tunnel is. And do not leave test ports listening after you finish testing — every extra open port is another thing a prober can characterise.

  7. Connect a client, and keep a second door open

    The clients that speak all of these are the sing-box applications on Android, iOS, Windows and macOS, and the same sing-box binary on Linux with a client-side config. Build the client profile from the values you generated: address, port, UUID, public key, short id and server_name for Reality; address, port, cipher and key for Shadowsocks.

    Configure both before you need them, and put them in the same profile so switching is one tap. The failure mode this avoids is specific and common: a block lands, your one protocol stops working, and the only way to fix the server is a connection that is no longer possible. A second protocol on a different port, with a different failure signature, is the cheapest insurance in this entire guide.

    If you administer the box over SSH from inside a filtered network, make sure that path does not depend on the tunnel either.

  8. Test it the way a censor would, not the way a user does

    “It connects” is the weakest possible test. Check the three things a filter checks, from a machine that is not the server:

    curl -sv --max-time 8 https://YOUR_IP 2>&1 | grep -E 'subject:|issuer:'
    nc -vz -w 5 YOUR_IP 23456
    nmap -Pn -sV -p 443,8443,23456 YOUR_IP

    What good looks like: the curl shows a certificate for your handshake target and no hint of a proxy; the nc to the Shadowsocks port connects and then sits silent until it times out; nmap identifies a web server and nothing more interesting. If any probe produces a distinctive banner, an immediate reset, or a version string naming the daemon, fix that before you rely on the setup.

    Then check the boring failure modes too — that the service survives a reboot with systemctl is-enabled sing-box, and that journalctl -u sing-box -f is quiet under normal use rather than logging every connection to disk.

Active probing, and servers that know how to stay quiet

Fingerprinting finds candidates; probing confirms them. Once a filter suspects an address it will open its own connection from an unrelated network and see what comes back. Everything hinges on the answer.

A server that returns a distinctive error, closes at a characteristic moment, or accepts a connection it cannot possibly authenticate has confirmed the guess. This is not theoretical: it is the documented mechanism by which large-scale filters have retired earlier obfuscation protocols en masse, and it is why obfs-style plugins that simply scrambled a header did not survive contact with it.

The modern designs answer the probe in one of two correct ways:

  • Say nothing. Shadowsocks-2022 cannot produce a valid response without the key, so it produces none. To a prober the port is a black hole, which is the same thing a firewalled port looks like. It also carries replay protection with a bounded time window, so a captured session cannot be resent later to elicit behaviour.
  • Say something true, about somebody else. Reality forwards the unauthenticated handshake to a genuine third-party host and relays that host’s real certificate back. The prober gets a valid chain for a site that plainly is not you. Pick a handshake target that is popular, plausibly reachable from your server’s country, and unlikely to be blocked itself — and keep the server’s clock in sync, because Reality rejects handshakes outside a narrow time window.

Test this yourself before you trust it. From an unrelated machine, run curl -v https://YOUR_IP against a Reality port: you should see a certificate belonging to the handshake target and nothing that hints at a proxy. Against a Shadowsocks port, nc should hang and then time out with no bytes returned. Anything else is a finding.

The address is the scarce resource

Protocols are replaceable in ten minutes. Addresses are not. Once an IPv4 is blocked inside a country it usually stays blocked long after whatever triggered it is gone, because nobody on the other side is paid to review the list. Treat the address as the asset and the protocol as a consumable.

Three habits follow from that. Do not publish it. An address posted in a public subscription list, a channel, or a shared configuration is enumerable, and enumeration is the cheapest attack there is — this is why private tunnels outlive free public ones by a wide margin. Do not put a burnable name on it. If you run the TLS-you-own route, a domain that also serves anything attention-getting drags the address down with it. Do not mix roles. A single-purpose box, with your tunnel and nothing else, does not inherit a listing earned by something else you were running.

The other half is where the address came from. High-churn budget clouds recycle IPv4 through thousands of short-lived customers, so a “new” address may already carry someone else’s reputation before your first packet. Every plan here ships a dedicated, screened IPv4 rather than a slice of a shared pool — see what a clean IP actually means for the definition and how to verify it yourself. If yours is ever flagged, or blocked somewhere that matters, request a swap through the panel; you should not have to place a new order to get a working address. And when you want to know whether a problem is a censor or a reputation list, our guide to checking whether an IP is blacklisted tells the two apart.

What obfuscation does not do

The honest accounting, because the rest of this guide is only worth reading if this part is in it.

  • It does not hide that you are sending encrypted traffic somewhere. A network operator still sees a flow to a foreign address, its volume, its timing and how long it lasts. Reality makes that flow look like an ordinary visit to an ordinary website; it does not make it disappear.
  • It does not defeat traffic analysis. Sustained high-volume flows with a distinctive day/night pattern remain distinctive. Against an adversary willing to spend real resources on correlation rather than pattern-matching, payload obfuscation is the wrong tool, and Tor with a pluggable transport is the better-studied answer.
  • It does not protect the endpoints. A compromised device, a logged-in account, browser fingerprinting and everything you type on the other side are untouched by any of this.
  • It does not make the server anonymous. The host knows which address is yours, and a no-KYC signup limits what there is to know rather than eliminating it. We set out exactly what is and is not visible in is a crypto-paid VPS really anonymous, and it is worth ten minutes before you rely on assumptions.
  • It is not permanent. This is an arms race with a release cycle. A configuration that works today may need replacing in a year, which is the real argument for a setup you control and can change in ten minutes rather than a subscription you can only cancel.

What it does do, reliably, is raise the cost of blocking you from “a byte match at line rate” to “a decision to break a lot of ordinary traffic”. In practice that is usually the whole game.

Jurisdiction, payment, and why the account matters

The protocol decides whether packets get through. Everything else about whether the tunnel keeps existing is decided off the wire.

Jurisdiction determines who can compel what. Our footprint is eight locations across the Netherlands, France, Romania, Bulgaria, Sweden, Iceland, Switzerland and Malaysia; a request that carries weight in one carries none in another, and no US-style takedown notice has force in any of them. That is operational policy stated honestly, not legal immunity — local court orders still apply, and there is a hard abuse floor we do not move. Offshore hosting explains the distinction properly.

The account is the part people underestimate. A tunnel is only as private as the record of who rented it. Signup here takes an email address for credential delivery and nothing else — no ID, no card, no address, no phone. There is no verification file to hand over because we never collected one.

The payment is the other half of that. A card links a server to a bank record and a legal name in a database neither of us controls. Checkout here settles on chain, and Monero is first-class rather than an afterthought: our guide to paying for a VPS with XMR walks the whole flow, and buying without a credit card covers getting there from zero crypto. Deploy is about sixty seconds after confirmation, so a replacement address is minutes away if you ever need one.

Put together, that is the shape of a tunnel that survives: a protocol a filter cannot cheaply match, an address nobody else has burned, a jurisdiction outside the filter’s reach, and an account that never held anything worth demanding. If you want the plain version of the same tunnel first, the WireGuard setup guide is ten minutes and a good place to start — come back here when something stops connecting.

Frequently asked questions

Why did my WireGuard tunnel stop working overnight?

Almost always because a filter on the path started matching the protocol rather than because anything changed on your server. WireGuard’s handshake is a fixed 148-byte packet beginning with 0x01, which is trivial to match at line rate, and rollouts tend to happen all at once. Two quick confirmations: the server still answers SSH from elsewhere, and the same tunnel works from a different network. If both hold, it is the path, not the box — and a port change will not fix it.

Is it legal to use an obfuscated VPN?

In most of the world, yes — encrypted tunnels are ordinary infrastructure and this software is mainstream open source. A minority of countries regulate or restrict VPN use, and the rules vary from licensing requirements to outright prohibition, so the answer depends on where you are rather than on where the server is. We are not in a position to give legal advice for your jurisdiction. What we can say is what applies on our side: running a private tunnel for your own traffic on a VPS you rent is ordinary use, and our acceptable use policy’s hard floor — no CSAM, no terrorism — is unaffected either way.

Which should I pick: Reality, Shadowsocks-2022 or Hysteria2?

Reality as the primary: it is the strongest against both fingerprinting and active probing, and it needs no domain or certificate of your own. Shadowsocks-2022 as a second door on another port, because it is two minutes of work and fails differently. Hysteria2 only if you have measured genuine packet loss on the path — it wins on lossy links and buys you nothing extra on a clean one. All three run from a single sing-box daemon, so this is a choice of inbounds, not of software.

Do I need a domain name for this?

Not for Reality or Shadowsocks-2022, which is much of their appeal — no registration, no certificate, and no certificate-transparency record with your address in it. You need a domain only for the TLS-you-own approach (Trojan, or VLESS over WebSocket behind a web server) or if you want a properly signed certificate for Hysteria2. Skipping the domain also removes an identifier that can be blocked independently of your address.

My server’s IP got blocked. Can I just change it?

Yes, and you should not have to place a new order to do it — request the swap through the panel. Before you do, work out what got listed: an address blocked inside one country is a censorship event, while an address on Spamhaus or a DNSBL is a reputation event with a different cause and a different fix, which our blacklist guide walks through. If the address was ever published in a shared configuration or a public subscription list, treat enumeration as the cause and do not publish the replacement.

Does obfuscation slow things down?

Barely, and rarely in the way people expect. Any x86-64 server CPU of the last decade has AES-NI, so the encryption itself is not the bottleneck — on a 1 Gbps port the network is. Reality adds a real TLS handshake at connection setup and effectively nothing afterwards. Shadowsocks-2022 is the lightest of the three. Hysteria2 can be considerably faster than the alternatives on a lossy path and slightly slower on a clean one. The thing that actually costs you latency is geography, which is why choosing the nearest usable location beats choosing the most exotic one.

Is a self-hosted obfuscated tunnel better than a commercial VPN?

For getting through a filter, usually yes, for one structural reason: a commercial provider’s address ranges are published, shared by thousands of users and enumerable, so they are blocked wholesale and stay blocked. Your own address is used by you. The trade-off is honest — a single-user tunnel gives you no crowd to blend into, so it protects against blocking far better than it protects against attribution. Which of those two problems you actually have is the question worth answering first, and what a crypto-paid VPS really hides is the honest breakdown.

Deploy an offshore VPS in about a minute

No-KYC, crypto-paid, all-NVMe. Pick a tier, pay in Monero or any major coin, and get root in roughly 60 seconds.

Fenrir on guard