[GH-ISSUE #804] Only 255 AmneziaWG configs (users) can be created #204

Open
opened 2026-10-13 20:15:15 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @theoilside on GitHub (May 12, 2024).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/804

Describe the bug
When I'm trying to create 256th user on AmneziaWG-server using AmneziaVPN Client it generates not working .vpn (for AmneziaVPN) and .conf (for AmneziaWG native client) files with not working corresponding QR-codes.

I made a research and found out that the «latest» working .conf-file have these first lines:

[Interface]
Address = 10.8.1.254/32

Files that were created later have these first lines:

[Interface]
Address = /32

To Reproduce
Steps to reproduce the behavior:

  1. Create ≈255 users on AmneziaWG server via AmneziaVPN client.
  2. Create a new user.
  3. Configs for AmneziaVPN client and for AmneziaWG native clients are not working:
    • AmneziaVPN client is not connecting.
    • AmneziaWG native clients says that peer settings are empty.

Expected behavior
256th user (peer) can be created

Log files
AmneziaVPN.log

Screenshots
Screenshot 2024-05-13 at 03 32 50@2x

Desktop

  • OS: macOS
  • Version 14.4.1

Server (please complete the following information):

  • OS: Ubuntu 22.04
Originally created by @theoilside on GitHub (May 12, 2024). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/804 **Describe the bug** When I'm trying to create 256th user on AmneziaWG-server using AmneziaVPN Client it generates not working .vpn (for AmneziaVPN) and .conf (for AmneziaWG native client) files with not working corresponding QR-codes. I made a research and found out that the «latest» working .conf-file have these first lines: ``` [Interface] Address = 10.8.1.254/32 ``` Files that were created later have these first lines: ``` [Interface] Address = /32 ``` **To Reproduce** Steps to reproduce the behavior: 1. Create ≈255 users on AmneziaWG server via AmneziaVPN client. 2. Create a new user. 3. Configs for AmneziaVPN client and for AmneziaWG native clients are not working: - AmneziaVPN client is not connecting. - AmneziaWG native clients says that peer settings are empty. **Expected behavior** 256th user (peer) can be created **Log files** [AmneziaVPN.log](https://github.com/amnezia-vpn/amnezia-client/files/15287854/AmneziaVPN.log) **Screenshots** ![Screenshot 2024-05-13 at 03 32 50@2x](https://github.com/amnezia-vpn/amnezia-client/assets/72651791/cc18fb15-0713-4116-8cd7-8821d4a24816) **Desktop** - OS: macOS - Version 14.4.1 **Server (please complete the following information):** - OS: Ubuntu 22.04
Author
Owner

@HeisLuka commented on GitHub (Sep 9, 2026):

I checked the current dev implementation. The exact historical symptom (Address = /32) appears to have changed, but the underlying address-pool bug is still present.

WireguardConfigurator::prepareWireguardConfig() currently reads all existing peer AllowedIPs, takes the last one and calculates the next address like this:

quint8 lastOctet =
    static_cast<quint8>(lastIp.toIPv4Address());

switch (lastOctet) {
case 254:
    result.setAddress(lastIp.toIPv4Address() + 3);
    break;
case 255:
    result.setAddress(lastIp.toIPv4Address() + 2);
    break;
default:
    result.setAddress(lastIp.toIPv4Address() + 1);
    break;
}

The default WG/AWG subnet is:

10.8.1.0/24

So the normal sequence is effectively:

10.8.1.1
...
10.8.1.253
10.8.1.254

and after .254 the special case produces:

10.8.2.1

This avoids .255 / the next .0, but it does not actually extend the configured VPN subnet.

The WireGuard/AWG server interface is still created from:

$SUBNET_IP/$SUBNET_CIDR

and the server startup scripts also restrict forwarding and NAT to exactly that configured subnet.

For AWG, for example:

iptables -A FORWARD -i awg0 -o eth0 \
    -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT

iptables -t nat -A POSTROUTING \
    -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR \
    -o eth0 -j MASQUERADE

Therefore with the default 10.8.1.0/24, a generated client such as:

10.8.2.1/32

is outside the configured VPN source range.

So I think the current +3/+2 logic is a workaround for the octet boundary rather than a complete fix for #804.

Suggested implementation

I would move this into a prefix-aware address allocator instead of manipulating the last octet:

allocatePeerAddress(
    serverAddress,
    prefixLength,
    usedPeerAddresses
)

The allocator should:

  • calculate the actual network bounds from serverAddress/prefixLength;
  • collect all existing peer addresses into a set;
  • reserve the server interface address;
  • select a free address that is actually inside the configured prefix;
  • handle network/broadcast addresses according to the real CIDR;
  • never generate an address outside the pool;
  • return an explicit address pool exhausted error when no address is available.

The current .254/.255 special cases are also incorrect for larger prefixes.

For example, with a /16, an address ending in .255 is not necessarily the subnet broadcast address, and an address ending in .0 is not necessarily the network address. Those properties depend on the actual prefix, not the final octet.

Another issue with the current allocator

Using:

lastIp = ips.last();

means peer ordering in the config file becomes allocator state.

A more robust implementation should use the complete set of already allocated IPs rather than assume that the final [Peer] contains the highest allocated address.

That also makes it possible to safely reuse holes after peer deletion and prevents duplicate allocation if peers are not ordered by IP.

What should happen after the default /24 is full?

There are two separate product choices.

If /24 remains the intended default capacity, the client should stop cleanly when the pool is exhausted:

No free addresses remain in the VPN subnet.

It should fail before appending a new [Peer] and before generating a broken client config.

If Amnezia intends to support more than ~254 clients on a single WG/AWG server, then the server itself needs a larger configured address pool.

In that case all of these must use the same larger prefix:

server interface address/prefix
peer allocator
FORWARD rules
NAT rules

Simply continuing into the next /24 in the client allocator is not enough.

Regression tests

I would cover:

  • default /24 allocates every valid peer address once;
  • the next allocation fails explicitly instead of producing an off-subnet address;
  • a larger CIDR correctly crosses octet boundaries;
  • .0 / .255 handling is based on the prefix rather than the last octet;
  • peers stored out of IP order cannot cause duplicate allocation;
  • a deleted address can be reused if that is the intended allocation policy;
  • the server interface address is never assigned to a peer;
  • every generated peer IP satisfies peerAddress ∈ configuredSubnet;
  • pool exhaustion leaves the server config unchanged.

So the core invariant I would use for the fix is:

allocated peer IP =
    unique free host
    AND member of the same configured prefix
    used by interface + forwarding + NAT

The current implementation does not enforce that invariant.

<!-- gh-comment-id:5607608276 --> @HeisLuka commented on GitHub (Sep 9, 2026): I checked the current `dev` implementation. The exact historical symptom (`Address = /32`) appears to have changed, but the underlying address-pool bug is still present. `WireguardConfigurator::prepareWireguardConfig()` currently reads all existing peer `AllowedIPs`, takes the **last** one and calculates the next address like this: ```cpp quint8 lastOctet = static_cast<quint8>(lastIp.toIPv4Address()); switch (lastOctet) { case 254: result.setAddress(lastIp.toIPv4Address() + 3); break; case 255: result.setAddress(lastIp.toIPv4Address() + 2); break; default: result.setAddress(lastIp.toIPv4Address() + 1); break; } ``` The default WG/AWG subnet is: ```text 10.8.1.0/24 ``` So the normal sequence is effectively: ```text 10.8.1.1 ... 10.8.1.253 10.8.1.254 ``` and after `.254` the special case produces: ```text 10.8.2.1 ``` This avoids `.255` / the next `.0`, but it does **not** actually extend the configured VPN subnet. The WireGuard/AWG server interface is still created from: ```text $SUBNET_IP/$SUBNET_CIDR ``` and the server startup scripts also restrict forwarding and NAT to exactly that configured subnet. For AWG, for example: ```bash iptables -A FORWARD -i awg0 -o eth0 \ -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR -j ACCEPT iptables -t nat -A POSTROUTING \ -s $AWG_SUBNET_IP/$WIREGUARD_SUBNET_CIDR \ -o eth0 -j MASQUERADE ``` Therefore with the default `10.8.1.0/24`, a generated client such as: ```text 10.8.2.1/32 ``` is outside the configured VPN source range. So I think the current `+3/+2` logic is a workaround for the octet boundary rather than a complete fix for #804. ### Suggested implementation I would move this into a prefix-aware address allocator instead of manipulating the last octet: ```text allocatePeerAddress( serverAddress, prefixLength, usedPeerAddresses ) ``` The allocator should: * calculate the actual network bounds from `serverAddress/prefixLength`; * collect **all** existing peer addresses into a set; * reserve the server interface address; * select a free address that is actually inside the configured prefix; * handle network/broadcast addresses according to the real CIDR; * never generate an address outside the pool; * return an explicit `address pool exhausted` error when no address is available. The current `.254/.255` special cases are also incorrect for larger prefixes. For example, with a `/16`, an address ending in `.255` is not necessarily the subnet broadcast address, and an address ending in `.0` is not necessarily the network address. Those properties depend on the actual prefix, not the final octet. ### Another issue with the current allocator Using: ```cpp lastIp = ips.last(); ``` means peer ordering in the config file becomes allocator state. A more robust implementation should use the complete set of already allocated IPs rather than assume that the final `[Peer]` contains the highest allocated address. That also makes it possible to safely reuse holes after peer deletion and prevents duplicate allocation if peers are not ordered by IP. ### What should happen after the default /24 is full? There are two separate product choices. If `/24` remains the intended default capacity, the client should stop cleanly when the pool is exhausted: ```text No free addresses remain in the VPN subnet. ``` It should fail **before** appending a new `[Peer]` and before generating a broken client config. If Amnezia intends to support more than ~254 clients on a single WG/AWG server, then the server itself needs a larger configured address pool. In that case all of these must use the same larger prefix: ```text server interface address/prefix peer allocator FORWARD rules NAT rules ``` Simply continuing into the next `/24` in the client allocator is not enough. ### Regression tests I would cover: * default `/24` allocates every valid peer address once; * the next allocation fails explicitly instead of producing an off-subnet address; * a larger CIDR correctly crosses octet boundaries; * `.0` / `.255` handling is based on the prefix rather than the last octet; * peers stored out of IP order cannot cause duplicate allocation; * a deleted address can be reused if that is the intended allocation policy; * the server interface address is never assigned to a peer; * every generated peer IP satisfies `peerAddress ∈ configuredSubnet`; * pool exhaustion leaves the server config unchanged. So the core invariant I would use for the fix is: ```text allocated peer IP = unique free host AND member of the same configured prefix used by interface + forwarding + NAT ``` The current implementation does not enforce that invariant.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amnezia-client#204
No description provided.