feat(wireguard): implement proxy.UserManager for dynamic peer management#6360
feat(wireguard): implement proxy.UserManager for dynamic peer management#6360bitwiresys wants to merge 12 commits into
Conversation
Add AddUser / RemoveUser / GetUser / GetUsers / GetUsersCount to the
WireGuard inbound Server so that peers can be inserted and removed at
runtime through the xray management API without restarting the process.
Problem
-------
The WireGuard inbound previously only supported static peers defined in
the JSON config. Every user change required a full process restart, which
breaks the standard xray API-driven user lifecycle (used by panels and
management tools that call AddUserOperation / RemoveUserOperation).
Solution
--------
* proxy/wireguard/account.go (new)
- MemoryAccount: runtime peer credentials (public key, pre-shared key,
allowed IPs). Implements protocol.Account (Equals / ToProto).
- PeerConfig.AsAccount(): lets API callers use "@type":
"xray.proxy.wireguard.PeerConfig" in AddUserOperation requests.
- buildPeerIPC / buildRemovePeerIPC: produce the WireGuard IPC strings
needed by device.Device.IpcSet to add and remove individual peers.
- parseFirstAddr: extracts a netip.Addr from a CIDR or plain address
(used to key the IP→user reverse lookup map).
* proxy/wireguard/server.go
- Server gains three new fields:
peers sync.Map // email (or pubkey) → *protocol.MemoryUser
peersByIP sync.Map // netip.Addr → *protocol.MemoryUser
peerCount atomic.Int64
- NewServer seeds peers/peersByIP from the static peer list so that
GetUser / GetUsers work immediately without an explicit AddUser call
for peers that were configured at startup.
- HandleConnection tags each session's Inbound.User with the matching
*protocol.MemoryUser when the tunnel source IP is in peersByIP. This
makes the peer identity visible in access logs and enables per-user
traffic stats through the existing stats.Manager infrastructure.
- AddUser: calls device.IpcSet to install the peer into the running
WireGuard device, then records the peer in the in-memory maps.
Falls back to the public key as the email when Email is empty.
Returns an error when the email is already registered.
- RemoveUser: removes the peer from the maps, then calls IpcSet with
"remove=true" to uninstall it from the device.
- GetUser / GetUsers / GetUsersCount: read-only map lookups.
- ipcOverride (unexported): test hook that substitutes a wgIpcSetter
mock for the concrete *device.Device, keeping unit tests free of
kernel-level WireGuard setup.
* proxy/wireguard/account_test.go (new)
Tests for MemoryAccount, PeerConfig.AsAccount, buildPeerIPC,
buildRemovePeerIPC, and parseFirstAddr.
* proxy/wireguard/server_test.go (new)
Full coverage of AddUser / RemoveUser / GetUser / GetUsers /
GetUsersCount including error paths (duplicate add, IPC error,
wrong account type, peer not found).
All 18 tests pass with go test ./proxy/wireguard/...
Compatibility
-------------
The change is additive: no existing API, config field, or behaviour is
removed or altered. The static-peer path in Start() is unchanged. Panels
that do not call the management API continue to work exactly as before.
Fix check-format CI failure: gofumpt requires multi-field anonymous structs to use the expanded form (one field per line), aligned comments must not have extra spaces beyond a single space, and function call arguments with trailing variadic elements must have the closing paren on its own line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These belong to our private deployment pipeline, not to this PR.
|
Hi @LjhAUMEM, thanks for the review! These two files ( Also, what specifically feels ugly about the implementation? Happy to clean it up. |
|
另外把 wgIpcSetter interface 删了,直接使用 dev,还有 type Server struct 里的注释删了,两个 test 文件也删了 其余的晚点再看 |
…evice directly - Remove the wgIpcSetter interface and the test-only ipcOverride field; AddUser/RemoveUser now use the concrete *device.Device via s.device(). - Drop the explanatory comments inside the Server struct. - Remove the two WireGuard UserManager test files.
|
Thanks for the review! Done in d737888:
Left |
|
test里把最重要的测试部分用interface替换成了mock 说要删interface又把整个test删了 看这种东西感觉像和机器说话不是和人交流。。。 |
我就是让直接删的 test 文件没错啊,一个 account 要啥 test 感觉差不多该到我接手的部分了,等今晚有空再说,对话效率有点低 @Fangliding 路由那个 inbound 的 protocol.MemoryUser Account 不是必须的吧,只有 email 也可以路由吧 |
|
路由只看email但是getinbounduser会解引用看 而且这个pr看起来也没法用adu命令 |
|
@LjhAUMEM all three are in d737888 — dropped the @Fangliding fair point on the |
|
On On |
|
@bitwiresys 你先不用修改,先提前问下,我可以直接 push 到你的 branch 吗 |
|
如果真要区分用户那也只会分配一个 v4/32 + v6/128,只匹配一个 v4 + v6 倒也还能写 |
|
Yeah, go for it — "Allow edits from maintainers" is on, so you can push straight to |
|
为什么执意要给wireguard加个这玩意 #6314 (comment) |
不过我是随意, |
|
cfwarp是自己写的服务端 不是标准wireguard 它的ipv4 local都能是一样的 只是套了这个协议传l3包而已 |
|
The request actually came from #6314 (not from us) — after #6287 reworked the WG inbound and Finalmask landed, a WG inbound that a panel can manage users on becomes useful. Concrete use case: multi-protocol panels (PasarGuard / Marzban-style) provision and revoke users for every inbound through one xray gRPC path ( You're right that for pure anti-censorship hysteria/xhttp3 are the better UDP options; WG isn't trying to compete there. Its value is native client support (every OS/router ships a WG client) and giving panels one consistent user-management API across all protocols — not censorship resistance. And agreed the peer/user-api fit isn't perfect (the |
那肯定不是说完全一样, 不过 cf warp 好像也没有说不带 reversed 不给连的情况, |
|
其实真正区分用户应该用 pub key, |
|
算了,当我没说,我看下怎么实现吧 |
想了想除了对齐还有另一个原因,wg 的 peer 注定只能给一个设备用,单 peer 多设备必定冲突,不像其他入站可以单 user 多用户 |
|
@LjhAUMEM ready 时说一下,会合并 |
|
估计这两天 |
|
@bitwiresys 应该差不多了,你方便测试下吗 |
|
@LjhAUMEM |
|
|
Tested your branch — looks good on our side. CI (built Functional test on a real node: ran a WG inbound with a peer that carries That's exactly what panels need — Works for us, thanks for the rework. |


Problem
The WireGuard inbound only supports static peers defined in the JSON config. Every user change requires a full process restart, which breaks the standard xray API-driven user lifecycle used by panels and management tools (AddUserOperation / RemoveUserOperation via the gRPC management API).
Solution
This PR adds full
proxy.UserManagersupport to the WireGuard inboundServer, enabling peers to be added and removed at runtime without restarting xray.New file:
proxy/wireguard/account.goMemoryAccount— runtime peer credentials (public key, pre-shared key, allowed IPs). Implementsprotocol.Account(Equals/ToProto).PeerConfig.AsAccount()— lets API callers use"@type": "xray.proxy.wireguard.PeerConfig"inAddUserOperationrequests, consistent with how every other inbound protocol works.buildPeerIPC/buildRemovePeerIPC— produce WireGuard IPC strings fordevice.Device.IpcSet.parseFirstAddr— extracts anetip.Addrfrom a CIDR or plain address (used for the IP→user reverse lookup map).Changes to
proxy/wireguard/server.goNew fields on
Server:NewServerseedspeers/peersByIPfrom the static peer list so thatGetUser/GetUserswork immediately for peers configured at startup.HandleConnectiontags each session'sInbound.Userwith the matching*protocol.MemoryUserwhen the tunnel source IP is inpeersByIP. This makes the peer identity visible in access logs and enables per-user traffic stats through the existingstats.Manager.UserManager methods:
AddUserdevice.IpcSetto install the peer, then records it in the maps. Falls back to the public key whenEmailis empty.RemoveUserIpcSetwithremove=true.GetUserGetUsersGetUsersCountNew test files
account_test.go— coversMemoryAccount,PeerConfig.AsAccount, IPC helpers, andparseFirstAddr.server_test.go— full coverage of all UserManager methods including error paths: duplicate add, IPC failure, wrong account type, peer not found. Uses amockIpcinjected via the unexportedipcOverridefield so no live WireGuard device is required.Compatibility
The change is purely additive. No existing config field, API, or runtime behaviour is removed or altered. The static-peer path in
Start()is untouched. Existing configs that do not use the management API continue to work exactly as before.Example API usage