AmneziaWG parameters explained: Jc, S1 to S4, H1 to H4, I1 to I5 and 3.x
AmneziaWG keeps WireGuard's cryptography and changes only what the packets look like. Jc, Jmin and Jmax add junk packets before each handshake. S1 to S4 put random bytes in front of each packet type. H1 to H4 replace WireGuard's fixed message type numbers. I1 to I5 send decoy packets that imitate other protocols, and versions 3.0 and 3.1 add header protection, random padding, random timers and trailers. The rule behind most broken setups: everything except the junk and decoy settings must be identical on client and server.
Tahir Mehmood11 min read
AmneziaWG is a fork of WireGuard maintained by the Amnezia team: a Go implementation (amneziawg-go), a Linux kernel module, and awg / awg-quick tools that work like wg and wg-quick. The key exchange and the encryption are unchanged. What changes is everything a DPI system can see from outside. This post goes through each parameter, what it does to the packets, and the constraints between them. I checked everything against the Amnezia docs and the amneziawg-go, amneziawg-tools and kernel module sources as of the 3.1 releases (August and September 2026).
What does AmneziaWG change compared to WireGuard?
It changes three things a censor can match on: packet types, packet sizes and the start of the session. Plain WireGuard is easy to fingerprint. Every packet starts with a 4-byte type field (one type byte and three zero bytes) whose value is 1, 2, 3 or 4: initiation, response, cookie reply, data. The handshake initiation is always 148 bytes, the response always 92, the cookie reply always 64. A rule that says "UDP, 148 bytes, starts with 01 00 00 00" catches every WireGuard handshake initiation on the internet.
AmneziaWG makes each of those values configurable, and, in later versions, random within a range. According to the AmneziaWG documentation, when every mechanism is off (values 0, or H1 to H4 set to 1, 2, 3 and 4), an AmneziaWG config is plain WireGuard and talks to a normal WireGuard peer.
Which AmneziaWG version do I have: 1.0, 1.5, 2.0, 3.0 or 3.1?
The version decides which keys exist. A client that doesn't know a key either rejects the config or silently behaves differently from the server, so this is the first thing to check when mixing apps.
| Version | What it added | Keys |
|---|---|---|
| 1.0 | Junk packets, handshake padding, custom headers (single values) | Jc, Jmin, Jmax, S1, S2, H1 to H4 |
| 1.5 | Decoy packets that imitate other UDP protocols | I1 to I5 |
| 2.0 | Headers as ranges, padding for cookie and data packets | H1 to H4 as a-b, S3, S4 |
| 3.0 | Header protection, content padding, random timers | HeaderProtectionKey, ContentPaddingAddition, RekeyAfterTime, RekeyTimeout, RejectAfterTime, KeepaliveTimeout, MaxHandshakeAttempts |
| 3.1 | Random trailers, no cookie replies | RandomTrailers, DisableCookies |
Two notes on this table. The S3 and S4 code first appeared during the 1.5 beta work, so some third-party clients label them 1.5; the Amnezia docs describe them as part of 2.0. And the 1.5 beta had extra keys (J1 to J3, Itime) that were removed again in 2.0. If you find them in an old config, delete them.
Amnezia's own position is that 2.0, like 1.5, "has become vulnerable to blocking", and their migration guide recommends moving self-hosted servers to 3.1. Their docs say 3.1 was developed after widespread blocking in Russia in June and July 2026. Moving from 2.0 to 3.1 through the AmneziaVPN app means removing the protocol from the server and issuing new configs to every user.
What do Jc, Jmin and Jmax do?
They send junk before every handshake: Jc packets of random content, each with a random size between Jmin and Jmax bytes. They go out right after the I1 to I5 decoys (if any) and right before the real handshake initiation, so the start of the session is blurred in both size and time.
These three are the only classic parameters that don't have to match. The kernel module's README says all parameters must be the same on client and server "except for Jc, Jmin, and Jmax". The receiver doesn't recognise junk as any packet type and drops it. With debug logging on, you see one line per junk packet:
awg0: Unknown message from 176.61.144.77:40419 encountered, packet dropped
That is the kernel module's wording. amneziawg-go logs Received message with unknown type. Seeing a few of these per handshake on a server is normal. Seeing only these, and never a handshake initiation, is the classic mismatch described further down.
Limits from the kernel module README:
Jc: 1 to 128, recommended 4 to 12.Jmin: recommended 8, and not larger thanJmax.Jmax: at most 1280, recommended 80.
The amneziawg-go README adds a warning: if Jmax is at or above the system MTU, the junk packets get fragmented, and fragmented UDP looks suspicious. Junk is only sent at handshakes, which happen every 2 minutes while traffic flows, so its bandwidth cost is close to nothing.
What do S1, S2, S3 and S4 do?
They put random bytes in front of each packet type, which changes its size and pushes WireGuard's own fields deeper into the packet. The Amnezia docs give the resulting sizes:
len(init) = 148 + S1
len(resp) = 92 + S2
len(cookie) = 64 + S3
len(data) = payload + S4
All four must match on both sides. The receiver identifies a packet's type by checking its exact length (for example 148 + S1) and its header value together. If the client's S1 is 40 and the server's is 41, the server never recognises the initiation; it is dropped as an unknown message.
Constraints, from the kernel module README (written assuming a 1280-byte MTU):
S1at most 1132 (1280 minus 148), recommended 15 to 150.S2at most 1188 (1280 minus 92), recommended 15 to 150.S1 + 56must not equalS2. With that equality, 148 + S1 equals 92 + S2, and the initiation and the response have the same length. Length is one of the two things the receiver uses to tell them apart.
S4 is the one with a running cost. It is added to every data packet, so it reduces the room left for your traffic. awg-quick still sets the interface MTU to the route MTU minus 80, exactly like wg-quick, and does not subtract S4. With S4 = 20 on a 1500-byte path, set MTU = 1400 in the [Interface] section yourself.
What do H1, H2, H3 and H4 do?
They replace the message type numbers. H1 is for the handshake initiation, H2 for the response, H3 for the cookie reply and H4 for data packets. In 1.0 each is a single number. From 2.0 each can be a range written a-b: the sender picks a random value from the range for every packet, and the receiver accepts any value in it. Two servers configured with different ranges don't share header values, so one DPI rule can't match them all.
Rules:
- All four must match on both sides.
- The four ranges must not overlap. The kernel module refuses the config with
H1 must not overlap with others; amneziawg-go reportsheaders must not overlap. - The kernel module README recommends values from 5 to 2147483647. Each header's own WireGuard value means "off":
H1 = 1,H2 = 2,H3 = 3,H4 = 4.
A config with ranges needs a 2.0 or newer client. A 1.0 client expects single values.
What are I1 to I5, the custom signature packets?
They are up to five decoy UDP packets sent before the handshake, in order from I1 to I5. The idea is that the session begins with something that looks like a known protocol, for example a QUIC Initial or a DNS query. Each value is written in what Amnezia calls CPS (Custom Protocol Signature), a sequence of tags:
| Tag | Meaning |
|---|---|
<b 0x...> |
Static bytes, written in hex |
<r N> |
N random bytes |
<rc N> |
N random letters, A to Z and a to z |
<rd N> |
N random digits |
<t> |
The current Unix time, 4 bytes |
The docs limit N to 1000 for the random tags. Usually I1 holds a hex capture of a real protocol's first packet, and I2 to I5 add timestamps and random data. Like junk packets, I-packets are only needed on the client side; the amneziawg-go README recommends setting them there only. A malformed value stops the interface from coming up; the kernel module logs I1-packet invalid format.
Two practical points. Don't copy an example from a blog post or the docs into production; the Amnezia docs say outright that their CPS example is syntax only and not a usable signature. And keep the final packet size under your path MTU, for the same fragmentation reason as Jmax.
What do the 3.0 and 3.1 parameters do?
They target statistical analysis of whole sessions rather than single packets. Header protection is the one that changes compatibility.
| Key | Since | Must match | What it does |
|---|---|---|---|
HeaderProtectionKey |
3.0 | Yes | Encrypts WireGuard's unencrypted header fields with ChaCha20. Generate it with awg genkey. |
ContentPaddingAddition |
3.0 | No | Adds a random number of bytes (from a range) to data packets, within the interface MTU |
RekeyAfterTime, RekeyTimeout, RejectAfterTime, KeepaliveTimeout, MaxHandshakeAttempts |
3.0 | No | Turn WireGuard's fixed timers (120 s, 5 s, 180 s, 10 s, 18 attempts) into random ranges |
RandomTrailers |
3.1 | Turn on both sides | Adds random bytes to the end of handshake packets and zeros inside data packets |
DisableCookies |
3.1 | No | Stops this side from sending cookie replies, whose behaviour can be used as a fingerprint during active probing |
Header protection takes its nonce from the first 12 bytes of each packet's S prefix. So with a HeaderProtectionKey set, every one of S1 to S4 must be at least 12. The kernel module refuses smaller values with S1 must be more then 12 to use headerProtection (the typo is in the source). With header protection on, Amnezia recommends leaving H1 to H4 at 1, 2, 3 and 4, because the type is hidden anyway.
Adding a HeaderProtectionKey to a running server breaks every client config that lacks the same key. The server decrypts headers that were never encrypted and recognises nothing. Plan it as a re-issue of all configs, not as a tuning change.
RandomTrailers needs care. The receiver only accepts a handshake packet longer than its expected size when its own RandomTrailers is on, so a client with it on and a server with it off will never complete a handshake. The docs also recommend equal values for S1 to S4 when you use it.
Which AmneziaWG parameters must match on client and server?
| Must be identical | Can differ per side |
|---|---|
S1, S2, S3, S4 |
Jc, Jmin, Jmax |
H1, H2, H3, H4 |
I1 to I5 |
HeaderProtectionKey |
ContentPaddingAddition |
RandomTrailers (in practice) |
The five timer ranges |
DisableCookies, MTU |
Plus everything plain WireGuard needs: matching keys, the right endpoint and port, and the client's public key in the server's peer list.
What does a working AmneziaWG server config look like?
Here is a 2.0-style server config. The numbers are an example of the shape only. Generate your own random values; a value that appears in public examples is a value a censor can match.
# /etc/amnezia/amneziawg/awg0.conf
[Interface]
PrivateKey = qN3xT8vL5mR2wY7kP0sA4dF9gH6jB1cE3uI8oZ5nV2M=
Address = 10.8.0.1/24
ListenPort = 47921
Jc = 5
Jmin = 12
Jmax = 74
S1 = 61
S2 = 103
S3 = 27
S4 = 14
H1 = 834211075-834211911
H2 = 1429063301-1429064450
H3 = 1810947722-1810948127
H4 = 2041175503-2041178819
[Peer]
PublicKey = Rv7mK2pX9wL4nQ8sT1yB6cF3hJ5dG0aE7uZ4oI2kN8Y=
AllowedIPs = 10.8.0.2/32
S1 + 56 is 117, not equal to S2, and the four header ranges don't overlap. The client config repeats S1 to S4 and H1 to H4 exactly, and may use its own Jc, Jmin, Jmax and I1 to I5. Keys and service work like WireGuard:
# awg genkey | tee server.key | awg pubkey > server.pub
# systemctl enable --now awg-quick@awg0
# awg show awg0
# awg showconf awg0
awg showconf prints the running config including every obfuscation value, which is the fastest way to compare what the server actually uses with what you think you configured.
There is no "best" port. Amnezia's own 3.1 setup guide simply asks for any free port below 9999 and gives 585 and 1234 as examples. The parameters matter much more than the port number.
Why does AmneziaWG show no handshake?
Start with the plain WireGuard causes, because all of them apply: endpoint, port, UDP blocked, unknown public key, clock. The WireGuard handshake checklist covers them with the commands. Then the AmneziaWG-specific ones:
-
S or H values differ. Turn on debug logging on the server and watch while the client connects:
# echo "module amneziawg +p" > /sys/kernel/debug/dynamic_debug/control # dmesg -wT | grep awg0For amneziawg-go, start it with
LOG_LEVEL=verbose amneziawg-go -f awg0. Expect a few "unknown message" lines per attempt (the junk and I-packets). If that is all you see, and never a line about receiving a handshake initiation, the server doesn't recognise the initiation: compareS1,H1and the rest withawg showconf awg0. -
The client app speaks an older version. A 3.1 server with header protection, or a 2.0 server with header ranges, can't be reached by a client that only understands 1.0. The Amnezia docs note that a 3.1 guest connection created in AmneziaVPN older than 5.0.1.5 shows as "Not installed".
-
HeaderProtectionKeyorRandomTrailersset on one side only. Both fail silently, in the same way as a wrong S value. -
A plain WireGuard client against an AmneziaWG server. The server drops its packets as unknown, unless every mechanism on the server is off.
-
The config never loaded. Overlapping H ranges, an S value under 12 with header protection, or a bad I-packet string stop
awg-quick upwith an error. With the kernel module that error is a generic "invalid argument"; the line that names the parameter appears indmesgonce debug logging is on.
How do I check an AmneziaWG server from outside?
You need a client that completes the handshake with the same parameters. A port check tells you nothing, for the reasons in the WireGuard port monitoring post, and a plain WireGuard probe fails on purpose, because hiding from exactly that kind of probe is the point of AmneziaWG.
Full disclosure: TunnelHQ runs that handshake from its own checkers. It reads the obfuscation parameters, such as Jc, Jmin and S1, from the client config you paste in, and alerts when the handshake or the traffic through the tunnel fails. You can try a single config in the free tester without an account.


