WireGuard Obfuscator is a tool designed to disguise WireGuard traffic as random data or a different protocol, making it much harder for DPI (Deep Packet Inspection) systems to detect and block. This can be extremely useful if your ISP or government attempts to block or throttle WireGuard traffic.
Project Goals:
What it's NOT:
Table of Contents:
Originally built as a quick personal solution, this project has grown into a fully-featured tool with the following capabilities:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
| WireGuard peer | | WireGuard peer | | WireGuard peer | | WireGuard peer |
└───────▲────────┘ └───────▲────────┘ └────────▲───────┘ └───────▲────────┘
| | └──────┐ ┌─────┘
┌───────▼────────┐ ┌───────▼────────┐ ┌─────▼────▼─────┐
| Obfuscator | | Obfuscator | | Obfuscator |
└───────▲────────┘ └───────▲────────┘ └────────▲───────┘
| | |
┌───────▼──────────────────▼─────────────────────────────▼────────────────┐
| | | Internet | |
└───────▲──────────────────▲─────────────────────────────▲────────────────┘
| | |
| ┌───────▼────────┐ |
└─────────>| Obfuscator |<───────────────────┘
└───────▲────────┘
|
┌───────────▼────────────┐
| WireGuard server peer |
└────────────────────────┘
In most cases, the obfuscator is used in a scenario where there is a clear separation between a server (with a static or public IP address) and clients (which may be behind NAT). We’ll focus on this setup here. If both ends have public IPs and can initiate connections to each other, refer to the "Two-way mode" section below.
Usually, the obfuscator is installed on the same device as your WireGuard client or server. In this setup, you configure WireGuard to connect to the obfuscator’s address and port (typically 127.0.0.1 and a custom port), while the real remote address and port are specified in the obfuscator’s configuration.
For example, a standard WireGuard client configuration:
[Peer]
Endpoint = example.com:19999
would become:
[Peer]
Endpoint = 127.0.0.1:3333
And the obfuscator would be launched/configured like this:
source-lport = 3333
target = example.com:19999
On the server side, the WireGuard config:
[Interface]
ListenPort = 19999
would be changed to:
[Interface]
ListenPort = 5555
With the obfuscator running with this config:
source-lport = 19999
target = 127.0.0.1:5555
The application maintains its own internal address mapping table, so a single server-side obfuscator can handle connections from multiple clients - each with their own obfuscator instance - using just one server port. Likewise, on the client side, a single obfuscator can support connections to multiple peers (though this is rarely needed in typical use).
The obfuscator automatically determines the direction (obfuscation or deobfuscation) for each packet, so the configuration files on both the client and server sides look nearly identical. The only thing that matters is that both sides use the same key.
The key is simply a plain text string. Cryptographic strength is not required here: feel free to use any common word or phrase (longer is better, but even four or five characters is usually enough in practice). The main thing is that your key is not the same as everyone else’s!
The obfuscator can be run with a command line configuration or using a configuration file. Available command line arguments are:
-? or --help-V or --version-c <filename> or --config=<filename>-i <interface> or --source-if=<interface>0.0.0.0, e.g. all interfaces. Can be used to listen only on a specific interface.-p <port> or --source-lport=<port>-t <address:port> or --target=<address:port>address:port format. All obfuscated/deobfuscated data will be forwarded to this address. Required.-k <key> or --key=<key>-a <type> or --masking=<type>AUTO. Supported values: STUN, AUTO, NONE. See "Masking" for details.-b <bindings> or --static-bindings=<bindings><client_ip>:<client_port>:<forward_port>. You can also repeat this option (on the command line or on multiple lines in the configuration file) instead of writing one very long line. See "Two-way mode" for details.-f <mark> or --fwmark=<mark>0, (i.e. disabled). Can be 0-65535 or 0x0000-0xFFFF.-v <level> or --verbose=<level>INFO. Accepted values are:ERRORS (critical errors only)WARNINGS (important messages)INFO (informational messages: status messages, connection established, etc.)DEBUG (detailed debug messages)TRACE (very detailed debug messages, including packet dumps)-L <path> or --log-file=<path>SIGHUP, so it can be rotated by logrotate. Optional, by default the log goes to stderr. See "Logging" for details.-T <value> or --log-timestamps=<value>AUTO, which adds timestamps only when writing to a log file. Supported values: AUTO, TRUE, FALSE.Additional arguments for advanced users:
-m <max_clients> or --max-clients=<max_clients>1024.-l <timeout> or --idle-timeout=<timeout>300 seconds (5 minutes).-n <timeout> or --in-timeout=<timeout>idle-timeout, but it only counts data received from the target. If nothing arrives from the target for this period, the session is disconnected. This is meant for client-side setups, to detect a dead or silently blocked server. If the local client is still sending traffic and static-bindings are not used, a new session is created immediately — with a fresh outbound UDP port. That can restore connectivity when a particular source IP:port pair has been banned by DPI. Optional, default is 0 (disabled).-d <length> or --max-dummy=<length>0 and 1024. If set to 0, no dummy data will be added. Default is 4. Note: total packet size with dummy bytes will be limited to 1024 bytes.-e or --allow-cleanallow-clean = true. Not compatible with static-bindings. See "Allowing Non-Obfuscated Clients" for details. Disabled by default.-R <sec> or --resolve-interval=<sec>target hostname and any hostnames in static-bindings every N seconds. Optional, default is 0 (disabled). Lookups run in a background thread so a slow DNS server cannot stall packet forwarding. IPv4 literals are never re-queried. SIGHUP (systemctl reload) always triggers a refresh, even when the interval is 0. If the interval is non-zero and a hostname cannot be resolved at startup (the network is not up yet), the obfuscator waits and retries instead of exiting. A change of address is logged at INFO. Use this for DDNS or split-horizon DNS, when the address of the peer can change without restarting the obfuscator.You can use the --config argument to specify a configuration file, which allows you to set all these parameters in the key=value format. For example:
# Instance name
[main]
source-lport = 13255
target = 10.13.1.100:13255
key = love
static-bindings = 1.2.3.4:12883:6670, 5.6.7.8:12083:6679
verbose = 2
# You can specify multiple instances
[second_server]
source-if = 0.0.0.0
source-lport = 13255
target = 10.13.1.100:13255
key = hate
verbose = 4
As you can see, the configuration file allows you to define settings for multiple obfuscator instances. This makes it easy to run several instances of the obfuscator with different settings, all from a single configuration file.
Be sure to check the Caveats and Recommendations section below for important notes on configuration and usage.
When using WireGuard, especially in combination with tools like WireGuard Obfuscator, it's important to ensure that traffic to the VPN server itself is not accidentally routed through the VPN tunnel. Otherwise, you may encounter a routing loop or complete loss of connection right after the handshake.
WireGuard routes packets based on the AllowedIPs list. Normally, it automatically excludes the server’s IP address (as specified in the Endpoint field) to avoid routing handshake and keepalive packets through the tunnel itself.
However, when you use WireGuard Obfuscator running locally (e.g., on 127.0.0.1), WireGuard only sees the obfuscator's local address, not the actual public IP of the VPN server. It has no awareness of the real server endpoint, which is hidden inside the obfuscator's config.
This becomes a problem especially when the peer is configured with:
AllowedIPs = 0.0.0.0/0
i.e., when all traffic is routed through the VPN tunnel. In this case, if the real server IP is not explicitly excluded, WireGuard Obfuscator may try to send its own traffic to the VPN server through the tunnel, leading to a routing loop or connection loss.
0.0.0.0/0WireGuard does not support negation syntax (e.g., !203.0.113.45). To avoid routing traffic to the server through the tunnel, you can manually split 0.0.0.0/0 into a set of smaller CIDR blocks that exclude the server's IP.
For example, if your server’s public IP is 203.0.113.45, then instead of:
AllowedIPs = 0.0.0.0/0
You would use:
AllowedIPs = 0.0.0.0/1, 128.0.0.0/2, 224.0.0.0/3, 208.0.0.0/4, 192.0.0.0/5,
204.0.0.0/6, 200.0.0.0/7, 202.0.0.0/8, 203.128.0.0/9, 203.64.0.0/10,
203.32.0.0/11, 203.16.0.0/12, 203.8.0.0/13, 203.4.0.0/14, 203.2.0.0/15,
203.1.0.0/16, 203.0.128.0/17, 203.0.0.0/18, 203.0.64.0/19, 203.0.96.0/20,
203.0.120.0/21, 203.0.116.0/22, 203.0.114.0/23, 203.0.112.0/24,
203.0.113.128/25, 203.0.113.64/26, 203.0.113.0/27, 203.0.113.48/28,
203.0.113.32/29, 203.0.113.40/30, 203.0.113.46/31, 203.0.113.44/32
This long list routes all traffic through the tunnel except for the server's IP (203.0.113.45), which stays outside the tunnel and avoids the loop.
You can use the following script to calculate the subnet list automatically: https://colab.research.google.com/drive/1spIsqkB4YOsctmZV83aG1HKISFFxxMCZ
fwmarkOn Linux, there's a cleaner approach (since version 1.4): use the FwMark option in the WireGuard config. This is useful only when AllowedIPs = 0.0.0.0/0, as it allows the system to distinguish between traffic going through the tunnel and traffic required to establish or maintain the tunnel (e.g., handshake packets).
Example WireGuard config:
[Interface]
FwMark = 0xdead
Then, in WireGuard Obfuscator, specify the same mark:
In the config file:
fwmark = 0xdead
Or via command-line:
--fwmark 0xdead
Note: Using
fwmarkrequires root privileges. Make sure to run WireGuard Obfuscator as root when using this option.
As of version 1.4, masking support is available - the ability to disguise traffic as another protocol. This is especially useful when DPI only allows whitelisted protocols. You can set masking mode using the masking option in the config file or the --masking parameter on the command line.
At the moment, the only available option is STUN emulation. Since STUN is commonly used for video calls, it is rarely blocked. So, currently supported values are:
NONE
No masking at all. The obfuscator will not mask outgoing traffic and will not recognize or process any incoming masked traffic.AUTOSTUNSometimes not all of your devices can run the obfuscator. A typical example: your main connection goes through a censored network and needs obfuscation, but you'd also like to occasionally connect to the same WireGuard server directly from a phone (without a local obfuscator instance) over a network where WireGuard is not blocked.
For this case, there is the allow-clean option (-e / --allow-clean on the command line, allow-clean = true in the configuration file). When it is enabled on the server-side obfuscator, clients that send plain (non-obfuscated) WireGuard traffic are accepted alongside obfuscated ones:
Keep in mind:
allow-clean enabled can no longer be used as a client-side obfuscator (a plain packet from the source is now treated as "clean client traffic" instead of "traffic to obfuscate").static-bindings (two-way mode): for a static binding, there is no way to know in advance whether the client's traffic must be obfuscated. The obfuscator will refuse to start if both options are set.(for advanced users)
In some setups, both WireGuard peers have public IP addresses and can each initiate connections. In this scenario, you need both ends to accept and send connections through the obfuscator. This is where two-way mode comes in.
A static binding tells the obfuscator, right from startup, which peer IPs and ports should be mapped to which local ports. This allows the obfuscator to know exactly how to route packets from the server to the correct local WireGuard instance - even if that peer hasn’t sent any packets yet. Without static bindings, the obfuscator only learns how to forward packets after seeing traffic from a client.
The static-bindings option accepts a comma-separated list. You can also repeat the option (on the command line or on multiple lines in the configuration file) instead of writing one very long line.
Suppose you have two peers:
1.2.3.4, runs WireGuard locally on port 11115.6.7.8, runs WireGuard locally on port 6666We can set up the obfuscator on both peers:
2222 and listens for incoming obfuscated handshakes from Peer A on port 3333.4444 and listens for local WireGuard traffic on port 5555.Peer A WireGuard config (1.2.3.4):
[Interface]
PrivateKey = <A's private key>
ListenPort = 1111
[Peer]
PublicKey = <B's public key>
Endpoint = 127.0.0.1:2222
Peer A Obfuscator config (1.2.3.4):
source-lport = 2222
target = 5.6.7.8:4444
static-bindings = 127.0.0.1:1111:3333
key = your_secret_key
Peer B Obfuscator config (5.6.7.8):
source-lport = 4444
target = 127.0.0.1:6666
static-bindings = 1.2.3.4:3333:5555
key = your_secret_key
Peer B WireGuard config (5.6.7.8):
[Interface]
PrivateKey = <B's private key>
ListenPort = 6666
[Peer]
PublicKey = <A's public key>
Endpoint = 127.0.0.1:5555
In this example the line:
static-bindings = 1.2.3.4:1111:3333
Visually, it looks like this:
┌───────────────────────────┐ ┌───────────────────────────┐
│ Peer A (1.2.3.4) │ │ Peer B (5.6.7.8) │
│ ┌─────────────────┐ | │ ┌─────────────────┐ |
│ │ WireGuard │ | │ │ WireGuard │ |
│ │ ListenPort=1111 | | │ │ ListenPort=6666 | |
│ └─────▲───────────┘ | │ └─────▲───────────┘ |
│ │ │ │ │ │
│ ┌─────▼───────────────┐ │ │ ┌─────▼───────────────┐ │
│ │ source-lport=2222 | │ │ │ local port=5555 │ │
│ │ | │ │ │ │ │
│ │ Obfuscator | │ │ │ Obfuscator | │
│ │ static-bind | │ │ │ static-bind | │
│ │ 127.0.0.1:1111:3333 | │ │ │ 1.2.3.4:3333:5555 | │
│ │ | │ │ │ │ │
│ │ local port=3333 │ | │ │ source-lport=4444 | │
│ └─────▼───────────────┘ │ │ └─────▼───────────────┘ │
│ │ │ │ │ │
└────────┼──────────────────┘ └────────┼──────────────────┘
│ │
│ UDP/obfuscated traffic │
│<--------------------------------------->│
When Peer A initiates a handshake with Peer B:
1111 to the local obfuscator on port 2222.4444, using 3333 as the source port.
Without static bindings, the obfuscator dynamically selects the source port and creates a mapping in its NAT table.
With the static binding127.0.0.1:1111:3333, it knows to always use port3333as the source port for packets from127.0.0.1:1111.
6666, using 5555 as the source port.
Without static bindings, the obfuscator dynamically selects the source port and creates a mapping.
With the static binding1.2.3.4:3333:5555, it knows to use port5555as the source port for packets from1.2.3.4:3333.
6666 to the obfuscator on port 5555.3333, using 4444 as the source port.
With static bindings, the necessary mappings already exist.
2222 to Peer A’s WireGuard on port 1111.
With static bindings, the necessary mappings already exist.
When Peer B initiates a handshake with Peer A, the process is the same but in reverse:
6666 to the local obfuscator on port 5555.3333, using 4444 as the source port.
Without static bindings, reverse connections would not work because the obfuscator would not know how to forward packets.
With the static binding1.2.3.4:3333:5555, the mapping already exists, so it knows to forward packets to1.2.3.4:3333.
1111, using 2222 as the source port.
Without static bindings, reverse connections would not work because the obfuscator would not know how to forward packets.
With the static binding127.0.0.1:1111:3333, the mapping already exists, so it knows to forward packets to127.0.0.1:1111.
1111 to the obfuscator on port 2222.4444, using 3333 as the source port.6666, using 5555 as the source port.With static bindings, each obfuscator knows in advance how to forward packets between the server and local WireGuard, regardless of which peer initiates the connection. This enables fully bidirectional, peer-to-peer WireGuard tunnels - even if both sides can initiate connections at any time.
By default the obfuscator writes its log to stderr. In most setups this is exactly what you want, because the service manager already collects it: journalctl -u wg-obfuscator on systemd, logread on OpenWrt, docker logs in a container. All of them add their own timestamps.
If you would rather keep the log yourself, use the log-file option:
log-file = /var/log/wg-obfuscator.log
log-timestamps = AUTO
When a log file is set, the log goes only to the file, and every line gets a timestamp:
2025-08-16 04:05:12.345 [main][I] Starting WireGuard Obfuscator v1.5
2025-08-16 04:05:12.346 [main][I] Listening on port 0.0.0.0:13255 for source
2025-08-16 04:05:19.881 [main][I] New client connected: 192.168.1.10:51820
Each line contains the timestamp, the instance name (the configuration file section), and a single letter for the logging level: Error, Warning, Info, Debug or Trace.
Timestamps are controlled by the log-timestamps option. The default, AUTO, adds them only when writing to a log file, since the service manager adds its own timestamps to stderr output. Set it to TRUE if you redirect stderr to a file yourself, or to FALSE if you do not want timestamps in the log file at all.
If several configuration sections are used, all of them can share the same log file: every line is written in one piece, so lines never interleave, and they can be told apart by the instance name in the prefix.
The obfuscator does not rotate the log itself, but it reopens the file on SIGHUP, which is all an external rotator needs:
/var/log/wg-obfuscator.log {
weekly
rotate 8
compress
missingok
notifempty
postrotate
systemctl reload wg-obfuscator 2>/dev/null || killall -HUP wg-obfuscator 2>/dev/null || true
endscript
}
systemctl reload sends SIGHUP to the main process, which forwards it to the instances of the other configuration sections, so a single signal is enough regardless of how many instances are running.
If the log file cannot be opened at startup, the obfuscator reports the error and exits instead of silently logging into nowhere.
See Download section below for download links.
On Linux, the obfuscator can be installed as a systemd service for automatic startup and management.
To build and install on Linux from the source code, simply run:
make
sudo make install
This will install the obfuscator as a systemd service.
You can start it with:
sudo systemctl start wg-obfuscator
The configuration file is located at:
/etc/wg-obfuscator.conf
ALT Linux apt-rpm package in SisyphusYou can download ready-to-run binaries with all required DLL libraries.
If you want to build this tool for Windows from the source code, you need MSYS2 and the following packages:
base-develgccgitInstall the required packages, then run:
make
Note: On Windows, the obfuscator is only available as a command-line application. You need to run it from the terminal and manage startup yourself, so it's required to use some additional tools if you want to install it as a system service.
You can download ready-to-run binaries for both x64 and ARM versions of macOS.
To build Obfuscator from the source code just type:
make
Note: On macOS, the obfuscator is only available as a command-line application. You need to run it from the terminal and manage startup yourself.
A very simple Android port of the obfuscator is available: https://github.com/ClusterM/wg-obfuscator-android/ - it allows you to obfuscate WireGuard traffic on Android devices, including phones, tablets, and Android TVs.
WireGuard Obfuscator is available as a multi-architecture Docker image: clustermeerkat/wg-obfuscator on Docker Hub
Supported tags:
latest - always points to the most recent stable release.nightly - built automatically from the current main branch; may be unstable. Use only for testing new features.1.0, 1.1) - for specific releases.Architectures available:
linux/amd64linux/arm64linux/arm/v7linux/arm/v6linux/arm/v5linux/386linux/ppc64lelinux/s390xNote: Make sure to match the exposed port (
13255in the example below) with thesource-lportvalue in your configuration file.
version: '3.8'
services:
wg-obfuscator:
image: clustermeerkat/wg-obfuscator:latest
volumes:
- ./.wg-obfuscator.conf:/etc/wg-obfuscator/wg-obfuscator.conf
ports:
- "13255:13255/udp"
container_name: wg-obfuscator-container
restart: unless-stopped
image can be changed to use a specific tag (e.g., clustermeerkat/wg-obfuscator:1.1)..wg-obfuscator.conf in the same directory as docker-compose.yml, or adjust the volume path.13255:13255/udp) must correspond to your obfuscator’s listen port.You can also run the container directly:
docker run -d \
--name wg-obfuscator \
-v $PWD/.wg-obfuscator.conf:/etc/wg-obfuscator/wg-obfuscator.conf \
-p 13255:13255/udp \
clustermeerkat/wg-obfuscator:latest
If you need a full WireGuard server with obfuscation and a web management interface rather than the standalone obfuscator, use WireGuard Obfuscator Easy. It is a Docker-only panel that creates clients, generates configs and QR codes, and runs this obfuscator for you.
WireGuard Obfuscator is available as native OpenWrt packages with full UCI integration and optional LuCI web interface.
Note: Packages are architecture-specific (
.ipkon OpenWrt ≤24,.apkon OpenWrt ≥25) and must be built for your router's platform. See the documentation for build instructions.
Note: On OpenWrt ≥25, a self-built
.apkcarries no signature, so it can only be installed withapk add --allow-untrusted. LuCI's Upload Package button always rejects it withUNTRUSTED signature. See the OpenWrt documentation.
📖 Complete OpenWrt Documentation
The documentation covers:
WireGuard Obfuscator can run as a Docker container on MikroTik devices with RouterOS 7.4+ (ARM64/x86_64).
📖 Complete MikroTik Documentation
The documentation covers:
Key points:
PersistentKeepalive option. A value of 25 seconds is usually sufficient.recv or recvfrom errors in DEBUG level logs), ensure that your WireGuard configuration has appropriate MTU settings. Especially when using masking (it adds extra bytes to each packet), you may need to reduce the MTU. A common setting is MTU = 1420, but you may need to reduce it based on your network conditions.allow-clean option, remember that the traffic of non-obfuscated clients is fully visible to DPI as WireGuard, and the obfuscator port accepts plain WireGuard traffic from anyone. See "Allowing Non-Obfuscated Clients" for details.--verbose=DEBUG (DEBUG level) to see detailed logs. This can help diagnose many common problems.C
78.2%
Shell
9.4%
Nix
6.3%
JavaScript
3.7%
Makefile
2.3%
WireGuard Obfuscator is a tool designed to disguise WireGuard traffic as random data or a different protocol, making it much harder for DPI (Deep Packet Inspection) systems to detect and block. This can be extremely useful if your ISP or government attempts to block or throttle WireGuard traffic.
Project Goals:
What it's NOT:
Table of Contents:
Originally built as a quick personal solution, this project has grown into a fully-featured tool with the following capabilities:
┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
| WireGuard peer | | WireGuard peer | | WireGuard peer | | WireGuard peer |
└───────▲────────┘ └───────▲────────┘ └────────▲───────┘ └───────▲────────┘
| | └──────┐ ┌─────┘
┌───────▼────────┐ ┌───────▼────────┐ ┌─────▼────▼─────┐
| Obfuscator | | Obfuscator | | Obfuscator |
└───────▲────────┘ └───────▲────────┘ └────────▲───────┘
| | |
┌───────▼──────────────────▼─────────────────────────────▼────────────────┐
| | | Internet | |
└───────▲──────────────────▲─────────────────────────────▲────────────────┘
| | |
| ┌───────▼────────┐ |
└─────────>| Obfuscator |<───────────────────┘
└───────▲────────┘
|
┌───────────▼────────────┐
| WireGuard server peer |
└────────────────────────┘
In most cases, the obfuscator is used in a scenario where there is a clear separation between a server (with a static or public IP address) and clients (which may be behind NAT). We’ll focus on this setup here. If both ends have public IPs and can initiate connections to each other, refer to the "Two-way mode" section below.
Usually, the obfuscator is installed on the same device as your WireGuard client or server. In this setup, you configure WireGuard to connect to the obfuscator’s address and port (typically 127.0.0.1 and a custom port), while the real remote address and port are specified in the obfuscator’s configuration.
For example, a standard WireGuard client configuration:
[Peer]
Endpoint = example.com:19999
would become:
[Peer]
Endpoint = 127.0.0.1:3333
And the obfuscator would be launched/configured like this:
source-lport = 3333
target = example.com:19999
On the server side, the WireGuard config:
[Interface]
ListenPort = 19999
would be changed to:
[Interface]
ListenPort = 5555
With the obfuscator running with this config:
source-lport = 19999
target = 127.0.0.1:5555
The application maintains its own internal address mapping table, so a single server-side obfuscator can handle connections from multiple clients - each with their own obfuscator instance - using just one server port. Likewise, on the client side, a single obfuscator can support connections to multiple peers (though this is rarely needed in typical use).
The obfuscator automatically determines the direction (obfuscation or deobfuscation) for each packet, so the configuration files on both the client and server sides look nearly identical. The only thing that matters is that both sides use the same key.
The key is simply a plain text string. Cryptographic strength is not required here: feel free to use any common word or phrase (longer is better, but even four or five characters is usually enough in practice). The main thing is that your key is not the same as everyone else’s!
The obfuscator can be run with a command line configuration or using a configuration file. Available command line arguments are:
-? or --help-V or --version-c <filename> or --config=<filename>-i <interface> or --source-if=<interface>0.0.0.0, e.g. all interfaces. Can be used to listen only on a specific interface.-p <port> or --source-lport=<port>-t <address:port> or --target=<address:port>address:port format. All obfuscated/deobfuscated data will be forwarded to this address. Required.-k <key> or --key=<key>-a <type> or --masking=<type>AUTO. Supported values: STUN, AUTO, NONE. See "Masking" for details.-b <bindings> or --static-bindings=<bindings><client_ip>:<client_port>:<forward_port>. You can also repeat this option (on the command line or on multiple lines in the configuration file) instead of writing one very long line. See "Two-way mode" for details.-f <mark> or --fwmark=<mark>0, (i.e. disabled). Can be 0-65535 or 0x0000-0xFFFF.-v <level> or --verbose=<level>INFO. Accepted values are:ERRORS (critical errors only)WARNINGS (important messages)INFO (informational messages: status messages, connection established, etc.)DEBUG (detailed debug messages)TRACE (very detailed debug messages, including packet dumps)-L <path> or --log-file=<path>SIGHUP, so it can be rotated by logrotate. Optional, by default the log goes to stderr. See "Logging" for details.-T <value> or --log-timestamps=<value>AUTO, which adds timestamps only when writing to a log file. Supported values: AUTO, TRUE, FALSE.Additional arguments for advanced users:
-m <max_clients> or --max-clients=<max_clients>1024.-l <timeout> or --idle-timeout=<timeout>300 seconds (5 minutes).-n <timeout> or --in-timeout=<timeout>idle-timeout, but it only counts data received from the target. If nothing arrives from the target for this period, the session is disconnected. This is meant for client-side setups, to detect a dead or silently blocked server. If the local client is still sending traffic and static-bindings are not used, a new session is created immediately — with a fresh outbound UDP port. That can restore connectivity when a particular source IP:port pair has been banned by DPI. Optional, default is 0 (disabled).-d <length> or --max-dummy=<length>0 and 1024. If set to 0, no dummy data will be added. Default is 4. Note: total packet size with dummy bytes will be limited to 1024 bytes.-e or --allow-cleanallow-clean = true. Not compatible with static-bindings. See "Allowing Non-Obfuscated Clients" for details. Disabled by default.-R <sec> or --resolve-interval=<sec>target hostname and any hostnames in static-bindings every N seconds. Optional, default is 0 (disabled). Lookups run in a background thread so a slow DNS server cannot stall packet forwarding. IPv4 literals are never re-queried. SIGHUP (systemctl reload) always triggers a refresh, even when the interval is 0. If the interval is non-zero and a hostname cannot be resolved at startup (the network is not up yet), the obfuscator waits and retries instead of exiting. A change of address is logged at INFO. Use this for DDNS or split-horizon DNS, when the address of the peer can change without restarting the obfuscator.You can use the --config argument to specify a configuration file, which allows you to set all these parameters in the key=value format. For example:
# Instance name
[main]
source-lport = 13255
target = 10.13.1.100:13255
key = love
static-bindings = 1.2.3.4:12883:6670, 5.6.7.8:12083:6679
verbose = 2
# You can specify multiple instances
[second_server]
source-if = 0.0.0.0
source-lport = 13255
target = 10.13.1.100:13255
key = hate
verbose = 4
As you can see, the configuration file allows you to define settings for multiple obfuscator instances. This makes it easy to run several instances of the obfuscator with different settings, all from a single configuration file.
Be sure to check the Caveats and Recommendations section below for important notes on configuration and usage.
When using WireGuard, especially in combination with tools like WireGuard Obfuscator, it's important to ensure that traffic to the VPN server itself is not accidentally routed through the VPN tunnel. Otherwise, you may encounter a routing loop or complete loss of connection right after the handshake.
WireGuard routes packets based on the AllowedIPs list. Normally, it automatically excludes the server’s IP address (as specified in the Endpoint field) to avoid routing handshake and keepalive packets through the tunnel itself.
However, when you use WireGuard Obfuscator running locally (e.g., on 127.0.0.1), WireGuard only sees the obfuscator's local address, not the actual public IP of the VPN server. It has no awareness of the real server endpoint, which is hidden inside the obfuscator's config.
This becomes a problem especially when the peer is configured with:
AllowedIPs = 0.0.0.0/0
i.e., when all traffic is routed through the VPN tunnel. In this case, if the real server IP is not explicitly excluded, WireGuard Obfuscator may try to send its own traffic to the VPN server through the tunnel, leading to a routing loop or connection loss.
0.0.0.0/0WireGuard does not support negation syntax (e.g., !203.0.113.45). To avoid routing traffic to the server through the tunnel, you can manually split 0.0.0.0/0 into a set of smaller CIDR blocks that exclude the server's IP.
For example, if your server’s public IP is 203.0.113.45, then instead of:
AllowedIPs = 0.0.0.0/0
You would use:
AllowedIPs = 0.0.0.0/1, 128.0.0.0/2, 224.0.0.0/3, 208.0.0.0/4, 192.0.0.0/5,
204.0.0.0/6, 200.0.0.0/7, 202.0.0.0/8, 203.128.0.0/9, 203.64.0.0/10,
203.32.0.0/11, 203.16.0.0/12, 203.8.0.0/13, 203.4.0.0/14, 203.2.0.0/15,
203.1.0.0/16, 203.0.128.0/17, 203.0.0.0/18, 203.0.64.0/19, 203.0.96.0/20,
203.0.120.0/21, 203.0.116.0/22, 203.0.114.0/23, 203.0.112.0/24,
203.0.113.128/25, 203.0.113.64/26, 203.0.113.0/27, 203.0.113.48/28,
203.0.113.32/29, 203.0.113.40/30, 203.0.113.46/31, 203.0.113.44/32
This long list routes all traffic through the tunnel except for the server's IP (203.0.113.45), which stays outside the tunnel and avoids the loop.
You can use the following script to calculate the subnet list automatically: https://colab.research.google.com/drive/1spIsqkB4YOsctmZV83aG1HKISFFxxMCZ
fwmarkOn Linux, there's a cleaner approach (since version 1.4): use the FwMark option in the WireGuard config. This is useful only when AllowedIPs = 0.0.0.0/0, as it allows the system to distinguish between traffic going through the tunnel and traffic required to establish or maintain the tunnel (e.g., handshake packets).
Example WireGuard config:
[Interface]
FwMark = 0xdead
Then, in WireGuard Obfuscator, specify the same mark:
In the config file:
fwmark = 0xdead
Or via command-line:
--fwmark 0xdead
Note: Using
fwmarkrequires root privileges. Make sure to run WireGuard Obfuscator as root when using this option.
As of version 1.4, masking support is available - the ability to disguise traffic as another protocol. This is especially useful when DPI only allows whitelisted protocols. You can set masking mode using the masking option in the config file or the --masking parameter on the command line.
At the moment, the only available option is STUN emulation. Since STUN is commonly used for video calls, it is rarely blocked. So, currently supported values are:
NONE
No masking at all. The obfuscator will not mask outgoing traffic and will not recognize or process any incoming masked traffic.AUTOSTUNSometimes not all of your devices can run the obfuscator. A typical example: your main connection goes through a censored network and needs obfuscation, but you'd also like to occasionally connect to the same WireGuard server directly from a phone (without a local obfuscator instance) over a network where WireGuard is not blocked.
For this case, there is the allow-clean option (-e / --allow-clean on the command line, allow-clean = true in the configuration file). When it is enabled on the server-side obfuscator, clients that send plain (non-obfuscated) WireGuard traffic are accepted alongside obfuscated ones:
Keep in mind:
allow-clean enabled can no longer be used as a client-side obfuscator (a plain packet from the source is now treated as "clean client traffic" instead of "traffic to obfuscate").static-bindings (two-way mode): for a static binding, there is no way to know in advance whether the client's traffic must be obfuscated. The obfuscator will refuse to start if both options are set.(for advanced users)
In some setups, both WireGuard peers have public IP addresses and can each initiate connections. In this scenario, you need both ends to accept and send connections through the obfuscator. This is where two-way mode comes in.
A static binding tells the obfuscator, right from startup, which peer IPs and ports should be mapped to which local ports. This allows the obfuscator to know exactly how to route packets from the server to the correct local WireGuard instance - even if that peer hasn’t sent any packets yet. Without static bindings, the obfuscator only learns how to forward packets after seeing traffic from a client.
The static-bindings option accepts a comma-separated list. You can also repeat the option (on the command line or on multiple lines in the configuration file) instead of writing one very long line.
Suppose you have two peers:
1.2.3.4, runs WireGuard locally on port 11115.6.7.8, runs WireGuard locally on port 6666We can set up the obfuscator on both peers:
2222 and listens for incoming obfuscated handshakes from Peer A on port 3333.4444 and listens for local WireGuard traffic on port 5555.Peer A WireGuard config (1.2.3.4):
[Interface]
PrivateKey = <A's private key>
ListenPort = 1111
[Peer]
PublicKey = <B's public key>
Endpoint = 127.0.0.1:2222
Peer A Obfuscator config (1.2.3.4):
source-lport = 2222
target = 5.6.7.8:4444
static-bindings = 127.0.0.1:1111:3333
key = your_secret_key
Peer B Obfuscator config (5.6.7.8):
source-lport = 4444
target = 127.0.0.1:6666
static-bindings = 1.2.3.4:3333:5555
key = your_secret_key
Peer B WireGuard config (5.6.7.8):
[Interface]
PrivateKey = <B's private key>
ListenPort = 6666
[Peer]
PublicKey = <A's public key>
Endpoint = 127.0.0.1:5555
In this example the line:
static-bindings = 1.2.3.4:1111:3333
Visually, it looks like this:
┌───────────────────────────┐ ┌───────────────────────────┐
│ Peer A (1.2.3.4) │ │ Peer B (5.6.7.8) │
│ ┌─────────────────┐ | │ ┌─────────────────┐ |
│ │ WireGuard │ | │ │ WireGuard │ |
│ │ ListenPort=1111 | | │ │ ListenPort=6666 | |
│ └─────▲───────────┘ | │ └─────▲───────────┘ |
│ │ │ │ │ │
│ ┌─────▼───────────────┐ │ │ ┌─────▼───────────────┐ │
│ │ source-lport=2222 | │ │ │ local port=5555 │ │
│ │ | │ │ │ │ │
│ │ Obfuscator | │ │ │ Obfuscator | │
│ │ static-bind | │ │ │ static-bind | │
│ │ 127.0.0.1:1111:3333 | │ │ │ 1.2.3.4:3333:5555 | │
│ │ | │ │ │ │ │
│ │ local port=3333 │ | │ │ source-lport=4444 | │
│ └─────▼───────────────┘ │ │ └─────▼───────────────┘ │
│ │ │ │ │ │
└────────┼──────────────────┘ └────────┼──────────────────┘
│ │
│ UDP/obfuscated traffic │
│<--------------------------------------->│
When Peer A initiates a handshake with Peer B:
1111 to the local obfuscator on port 2222.4444, using 3333 as the source port.
Without static bindings, the obfuscator dynamically selects the source port and creates a mapping in its NAT table.
With the static binding127.0.0.1:1111:3333, it knows to always use port3333as the source port for packets from127.0.0.1:1111.
6666, using 5555 as the source port.
Without static bindings, the obfuscator dynamically selects the source port and creates a mapping.
With the static binding1.2.3.4:3333:5555, it knows to use port5555as the source port for packets from1.2.3.4:3333.
6666 to the obfuscator on port 5555.3333, using 4444 as the source port.
With static bindings, the necessary mappings already exist.
2222 to Peer A’s WireGuard on port 1111.
With static bindings, the necessary mappings already exist.
When Peer B initiates a handshake with Peer A, the process is the same but in reverse:
6666 to the local obfuscator on port 5555.3333, using 4444 as the source port.
Without static bindings, reverse connections would not work because the obfuscator would not know how to forward packets.
With the static binding1.2.3.4:3333:5555, the mapping already exists, so it knows to forward packets to1.2.3.4:3333.
1111, using 2222 as the source port.
Without static bindings, reverse connections would not work because the obfuscator would not know how to forward packets.
With the static binding127.0.0.1:1111:3333, the mapping already exists, so it knows to forward packets to127.0.0.1:1111.
1111 to the obfuscator on port 2222.4444, using 3333 as the source port.6666, using 5555 as the source port.With static bindings, each obfuscator knows in advance how to forward packets between the server and local WireGuard, regardless of which peer initiates the connection. This enables fully bidirectional, peer-to-peer WireGuard tunnels - even if both sides can initiate connections at any time.
By default the obfuscator writes its log to stderr. In most setups this is exactly what you want, because the service manager already collects it: journalctl -u wg-obfuscator on systemd, logread on OpenWrt, docker logs in a container. All of them add their own timestamps.
If you would rather keep the log yourself, use the log-file option:
log-file = /var/log/wg-obfuscator.log
log-timestamps = AUTO
When a log file is set, the log goes only to the file, and every line gets a timestamp:
2025-08-16 04:05:12.345 [main][I] Starting WireGuard Obfuscator v1.5
2025-08-16 04:05:12.346 [main][I] Listening on port 0.0.0.0:13255 for source
2025-08-16 04:05:19.881 [main][I] New client connected: 192.168.1.10:51820
Each line contains the timestamp, the instance name (the configuration file section), and a single letter for the logging level: Error, Warning, Info, Debug or Trace.
Timestamps are controlled by the log-timestamps option. The default, AUTO, adds them only when writing to a log file, since the service manager adds its own timestamps to stderr output. Set it to TRUE if you redirect stderr to a file yourself, or to FALSE if you do not want timestamps in the log file at all.
If several configuration sections are used, all of them can share the same log file: every line is written in one piece, so lines never interleave, and they can be told apart by the instance name in the prefix.
The obfuscator does not rotate the log itself, but it reopens the file on SIGHUP, which is all an external rotator needs:
/var/log/wg-obfuscator.log {
weekly
rotate 8
compress
missingok
notifempty
postrotate
systemctl reload wg-obfuscator 2>/dev/null || killall -HUP wg-obfuscator 2>/dev/null || true
endscript
}
systemctl reload sends SIGHUP to the main process, which forwards it to the instances of the other configuration sections, so a single signal is enough regardless of how many instances are running.
If the log file cannot be opened at startup, the obfuscator reports the error and exits instead of silently logging into nowhere.
See Download section below for download links.
On Linux, the obfuscator can be installed as a systemd service for automatic startup and management.
To build and install on Linux from the source code, simply run:
make
sudo make install
This will install the obfuscator as a systemd service.
You can start it with:
sudo systemctl start wg-obfuscator
The configuration file is located at:
/etc/wg-obfuscator.conf
ALT Linux apt-rpm package in SisyphusYou can download ready-to-run binaries with all required DLL libraries.
If you want to build this tool for Windows from the source code, you need MSYS2 and the following packages:
base-develgccgitInstall the required packages, then run:
make
Note: On Windows, the obfuscator is only available as a command-line application. You need to run it from the terminal and manage startup yourself, so it's required to use some additional tools if you want to install it as a system service.
You can download ready-to-run binaries for both x64 and ARM versions of macOS.
To build Obfuscator from the source code just type:
make
Note: On macOS, the obfuscator is only available as a command-line application. You need to run it from the terminal and manage startup yourself.
A very simple Android port of the obfuscator is available: https://github.com/ClusterM/wg-obfuscator-android/ - it allows you to obfuscate WireGuard traffic on Android devices, including phones, tablets, and Android TVs.
WireGuard Obfuscator is available as a multi-architecture Docker image: clustermeerkat/wg-obfuscator on Docker Hub
Supported tags:
latest - always points to the most recent stable release.nightly - built automatically from the current main branch; may be unstable. Use only for testing new features.1.0, 1.1) - for specific releases.Architectures available:
linux/amd64linux/arm64linux/arm/v7linux/arm/v6linux/arm/v5linux/386linux/ppc64lelinux/s390xNote: Make sure to match the exposed port (
13255in the example below) with thesource-lportvalue in your configuration file.
version: '3.8'
services:
wg-obfuscator:
image: clustermeerkat/wg-obfuscator:latest
volumes:
- ./.wg-obfuscator.conf:/etc/wg-obfuscator/wg-obfuscator.conf
ports:
- "13255:13255/udp"
container_name: wg-obfuscator-container
restart: unless-stopped
image can be changed to use a specific tag (e.g., clustermeerkat/wg-obfuscator:1.1)..wg-obfuscator.conf in the same directory as docker-compose.yml, or adjust the volume path.13255:13255/udp) must correspond to your obfuscator’s listen port.You can also run the container directly:
docker run -d \
--name wg-obfuscator \
-v $PWD/.wg-obfuscator.conf:/etc/wg-obfuscator/wg-obfuscator.conf \
-p 13255:13255/udp \
clustermeerkat/wg-obfuscator:latest
If you need a full WireGuard server with obfuscation and a web management interface rather than the standalone obfuscator, use WireGuard Obfuscator Easy. It is a Docker-only panel that creates clients, generates configs and QR codes, and runs this obfuscator for you.
WireGuard Obfuscator is available as native OpenWrt packages with full UCI integration and optional LuCI web interface.
Note: Packages are architecture-specific (
.ipkon OpenWrt ≤24,.apkon OpenWrt ≥25) and must be built for your router's platform. See the documentation for build instructions.
Note: On OpenWrt ≥25, a self-built
.apkcarries no signature, so it can only be installed withapk add --allow-untrusted. LuCI's Upload Package button always rejects it withUNTRUSTED signature. See the OpenWrt documentation.
📖 Complete OpenWrt Documentation
The documentation covers:
WireGuard Obfuscator can run as a Docker container on MikroTik devices with RouterOS 7.4+ (ARM64/x86_64).
📖 Complete MikroTik Documentation
The documentation covers:
Key points:
PersistentKeepalive option. A value of 25 seconds is usually sufficient.recv or recvfrom errors in DEBUG level logs), ensure that your WireGuard configuration has appropriate MTU settings. Especially when using masking (it adds extra bytes to each packet), you may need to reduce the MTU. A common setting is MTU = 1420, but you may need to reduce it based on your network conditions.allow-clean option, remember that the traffic of non-obfuscated clients is fully visible to DPI as WireGuard, and the obfuscator port accepts plain WireGuard traffic from anyone. See "Allowing Non-Obfuscated Clients" for details.--verbose=DEBUG (DEBUG level) to see detailed logs. This can help diagnose many common problems.C
78.2%
Shell
9.4%
Nix
6.3%
JavaScript
3.7%
Makefile
2.3%