How to block torrents on your VPN or proxy

If you share access to your VPS through a VPN or a proxy, your users' torrent traffic leaves for the internet from your IP address — and the copyright complaints land on you. In this article you'll learn why torrents can't be filtered out completely and why that doesn't stop you from solving the problem, set up blocking either at the Xray level (Remnawave, 3x-ui, Marzban) or in netfilter for WireGuard and OpenVPN, and put it together into a working three-layer setup.

What you'll need

  • A ForestsNet Cloud VPS with SSH access as root or a user with sudo.
  • A clear idea of what you're running: an Xray panel (Remnawave, 3x-ui, Marzban) or a classic VPN (WireGuard, OpenVPN). Everything below depends on this — the two approaches are not interchangeable.
  • Basic familiarity with the firewall: iptables basics or the ufw firewall.

First, the honest limits

Torrent clients can hide, and they hide well. Here's how the share of recognised connections under deep packet inspection (DPI) changes depending on what the user has enabled:

  • Plain protocol, no encryption — 98.3% of connections are recognised, in 3.2 seconds on average.
  • MSE, the protocol's built-in encryption — 41.7%.
  • MSE + uTP, with DHT disabled — 7.1%.

Proxies look similar: according to the Remnawave documentation, the sniffing built into Xray identifies BitTorrent in roughly 10–30% of cases.

Seeing "7%", it's easy to conclude the whole idea is pointless. It isn't, and here's why.

You don't need to filter every packet — catching one is enough. What does the work after that isn't the filter, it's the penalty: the source gets banned for a while. A user who starts seeding will make hundreds of requests to peers and trackers within the first few seconds. Even at 10% recognition they'll be caught almost immediately — and what cuts them off is the ban, not the filter.

So everything below describes a "catch it, then ban the source" scheme rather than an attempt to build an impenetrable wall. Nobody gets the wall.

ℹ️ Tip: tell your users before you turn any of this on. A torrent that dies silently looks like your service is broken, and you're the one who'll be sorting that out.

Option A. An Xray panel: Remnawave, 3x-ui, Marzban

If you run a proxy panel, this is where you start — and the only place you start.

Traffic between the user and your VPS is already encrypted by the client, so the firewall on the server sees noise, not torrents. Any iptables rule from the second half of this article simply has nothing to look at. The blocking has to happen inside Xray itself, where the traffic is already decrypted.

Step 1. Enable sniffing — nothing works without it

Add this to your inbound settings:

"sniffing": {
  "enabled": true,
  "destOverride": ["http", "tls", "quic"]
}

This is not an optional tweak. Without sniffing enabled, Xray-Core cannot determine the protocol of the traffic, BitTorrent included, and the blocking rule never fires at all. A domain-based rule is dead without it too: the proxy only sees the destination IP address, not the domain name the client is reaching for.

If you configure everything else but skip this block, the blocking will look configured and will do nothing. This is the single most common reason behind "I set it all up and torrents still go through".

Step 2. Add the routing rules

In your main Xray profile, under routing rules:

{
  "protocol": ["bittorrent"],
  "outboundTag": "TORRENT"
},
{
  "domain": ["geosite:category-public-tracker"],
  "ruleTag": "TORRENT_BY_DOMAIN",
  "outboundTag": "TORRENT"
},
{
  "port": "6881-6889,6969,9696,51413,21413,17417,37305",
  "ruleTag": "TORRENT_BY_PORT",
  "outboundTag": "TORRENT"
}

The three rules catch different things: the recognised protocol, requests to public trackers by domain name, and the standard torrent ports.

Why bother with a port rule when the port takes two seconds to change. A fair objection — but here the port isn't a filter, it's a tripwire. The TORRENT_BY_PORT tag goes into the blocker's list further down, so a single packet to 6881 is enough to get the source banned. A user can change their own port, but sooner or later they'll reach out to someone else's peer or tracker sitting on a standard one, and that's all it takes.

Step 3. Add the outbound all of this points to

This is the block people forget most often, and then the rules above point nowhere:

{
  "tag": "TORRENT",
  "protocol": "blackhole"
}

blackhole means "throw it away". Without it, outboundTag: "TORRENT" has nothing to refer to.

Step 4. Turn on the blocker in the Remnawave node

On their own, the rules above only drop packets. To have the source picked up a temporary ban, enable the plugin on the node:

{
  "torrentBlocker": {
    "enabled": true,
    "ignoreLists": { "ip": [] },
    "blockDuration": 3600,
    "includeRuleTags": ["TORRENT_BY_DOMAIN", "TORRENT_BY_PORT"]
  },
  "sharedLists": []
}

How it works: Xray catches a packet matching a rule from includeRuleTags, calls a webhook, and the node adds the IP to nftables and tears down established connections with conntrack -D. blockDuration: 3600 is a one-hour ban.

Worth knowing up front:

  • You need Xray-Core 26.3.27 or newer — older builds simply don't have the webhook.
  • The plugin is enabled on the node separately; installing the panel is not enough.
  • ignoreLists.ip takes individual addresses; CIDR ranges are not supported there.

If you run 3x-ui or Marzban

There's no built-in plugin with this logic. Its role is filled by a separate service, kutovoys/xray-torrent-blocker — a Go program that reads Xray's access.log, finds entries with the relevant tag and bans the source. The routing rules and sniffing are configured exactly as above.

Option B. WireGuard and OpenVPN: filtering in netfilter

Here the situation is reversed: once decrypted on the server, the traffic travels in the clear, so DPI applies. The rules go into the FORWARD chain with the tunnel interface — wg0 for WireGuard, tun0 for OpenVPN. The examples below use wg0; substitute your own.

Step 1. Shut down DHT and UDP trackers

This is the cheapest step and the most painful one for a torrent client: DHT is never encrypted, and that is the protocol's weak spot. A client that loses DHT and trackers loses its ability to find peers.

Match the bencoded form — with the string-length prefix, not the bare words:

sudo iptables -A FORWARD -i wg0 -p udp -m string --algo bm --string "9:find_node" -j DROP
sudo iptables -A FORWARD -i wg0 -p udp -m string --algo bm --string "9:get_peers" -j DROP
sudo iptables -A FORWARD -i wg0 -p udp -m string --algo bm --hex-string "|3a 61 6e 6e 6f 75 6e 63 65 5f 70 65 65 72|" -j DROP
sudo iptables -A FORWARD -i wg0 -p udp -m string --algo bm --hex-string "|0000041727101980|" -j DROP

The first three are DHT protocol queries (find_node, get_peers, announce_peer) in their bencoded form. The last rule is the magic constant of a UDP tracker connect request; it's fixed by the specification and produces no false positives at all.

Don't forget to save the rules, or they won't survive a reboot:

sudo netfilter-persistent save
ℹ️ Be careful with ready-made scripts from the internet. The most widely copied ruleset (shopeevpn/Block-Bittorrent and its many clones) blocks the bare strings announce, torrent and info_hash — with no length prefix. Rules like that break ordinary web browsing: the word announce shows up in completely unrelated requests. The same scripts still carry rules for .exe?/c+dir and /default.ida? — signatures of the Code Red worm from 2001, copy-pasted in and unrelated to torrents. Read what a third-party script actually adds before you run it.

Step 2. If you need serious inspection — xt_ndpi

The xt_ndpi kernel module brings full DPI straight into iptables. Two things trip people up most often:

Use the maintained fork. Nearly every guide points at betolj/ndpi-netfilter, which is abandoned — its last commit is from 2017. The maintained version is vel21ripn/nDPI, branch flow_info-4.

DHT parsing is off by default. That's exactly the part you need in order to catch encrypted sessions, and exactly the part people forget to enable. The fork's README says it plainly: the ability to parse BitTorrent DHT messages was added to detect encrypted connections, but DHT message analysis is off by default. This is where the recurring "bittorrent matching is not working properly" reports come from — the module gets installed, DHT analysis never gets enabled, only plain BitTorrent is caught, and the conclusion is that the module is broken.

Enable it through the module parameters:

sudo modprobe xt_ndpi bt_hash_size=64 bt_hash_timeout=900
sudo iptables -A FORWARD -i wg0 -m ndpi --bittorrent -j DROP

The module builds for kernels from 4.4 onwards. Kernel 5.18 and newer needs either a kernel with CONFIG_LIVEPATCH=y (Ubuntu and AlmaLinux ship it) or a kernel patch.

Step 3. An alternative without building kernel modules — Suricata inline

If you'd rather not deal with kernel modules, Suricata does the same job running inline through NFQUEUE. Take the ready-made ET Open ruleset — emerging-p2p.rules, which already contains DHT signatures — and change the action in them from alert to drop.

One detail you must not skip: the rule that hands traffic to the queue needs --queue-bypass. Without it, Suricata crashing or restarting will cut off all of your users' transit traffic, not just the torrents.

Step 4. A behavioural limit to finish the job

A torrent client holds hundreds of simultaneous connections — ordinary traffic doesn't behave that way. This signal doesn't care which port the client uses or whether encryption is on:

sudo iptables -A FORWARD -i wg0 -p tcp --syn -m connlimit --connlimit-above 150 --connlimit-mask 32 -j DROP

On its own this doesn't block torrents and is useless as your only measure. But it mops up whatever slipped past the DPI and noticeably slows seeding down.

What's next

Build this up in layers, starting with what gives you the most for the least effort:

  1. DHT and UDP trackers — twenty minutes of work, no false positives, an immediately noticeable effect.
  2. Detection with a temporary ban — the key layer. It doesn't filter traffic, it penalises the source, which is why it works even when the recognition rate is low.
  3. connlimit — on top, to mop up the rest.

Once it's set up, check that ordinary traffic didn't suffer: open a few sites through your VPN, test video calls and file downloads. Rules from third-party scripts tend to break exactly this.

From here it's worth going through the server security checklist, and if your firewall has grown a lot of rules, tidying them up with iptables basics and the ufw firewall. Setting up the tunnels themselves is covered in WireGuard VPN and OpenVPN.

Sources