mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
a036ddd66f
* fix(amneziawg): wrap the relay port window instead of refusing ids past it An AmneziaWG inbound's loopback relay port is SOCKSBasePort + row id, and AddInbound refused any id that pushed it past 65535. The inbounds table is AUTOINCREMENT, so an id is never reused and the counter is only reset when the table empties: the 435-port window was a lifetime budget, and a database that had ever created more inbounds could never create another AmneziaWG one -- the reporter's counter sits at 70350, so the protocol never worked there at all (#6537). Ids now wrap into the same 435 ports, which leaves every id up to 435 with the exact port it had, so no existing row, relay or generated config moves. Wrapping makes the id -> port map non-injective, and nothing compared two derived relay ports before -- two relays on one port would leave Xray with a duplicate listen and refuse to start, taking the whole panel's proxy down. checkAmneziawgnetSocksRelayCollision now refuses a create or an edit whose derived port another local AmneziaWG row already owns, disabled rows included: a row owns its slot for good, and enabling it later re-runs no port check. * test(amneziawg): give each relay-window fixture its own client email Every fixture built the same client email, and an email is unique across the whole panel, so AddInbound refused the second create with "Duplicate email" before either new guard ran -- CI exercised neither the wrap nor the collision refusal. Each fixture now derives its email from its own tag, which is what the tag already exists for. * fix(amneziawg): say relay port in the relay conflict message A refusal that named the port of the automatic loopback relay read as if the named inbound listened on an unrelated port -- its own port is the WireGuard one. portConflictDetail now carries Relay, and both messages that report a derived relay port say "relay port N"; messages that report a configured port render byte-for-byte as before. * test(amneziawg): pin that a node-assigned inbound owns no relay slot A row adopted from a node carries a NodeID and the protocol it arrived with (inbound_node.go:737), yet injectAmneziawgnetSocks skips it, so it binds no loopback relay. The gate this PR added to checkPortConflictTx never looked at NodeID, so editing such a row can be refused for a slot it does not own. Expected red on this head; the fix follows. * fix(amneziawg): skip the relay guards for node-assigned inbounds Round-2 review finding: the gate this PR added to checkPortConflictTx keyed on inbound.Protocol alone, so it also ran for a row adopted from a node. Such a row carries a NodeID and gets no loopback relay -- injectAmneziawgnetSocks skips it and the desired-instance query is node_id IS NULL -- so it owns no slot and can collide with nothing, yet editing it was refused with "relay port N ... already used by inbound '<local>'", naming a port the edited row never binds. Wrapping made this visible: before it, an adopted id above 435 derived a port above 65535 that no row could hold, so the pre-existing reverse check under the same gate could not fire. Both call sites now require NodeID == nil, matching the local-only predicate the forward check already used. TestCheckPortConflict_NodeAssignedAmneziawgOwnsNoRelaySlot fails without this, with the exact false refusal, and passes with it.
53 lines
1.9 KiB
Go
53 lines
1.9 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// SOCKSBasePort is the first loopback port used for an AmneziaWG inbound's
|
|
// own Xray SOCKS5 relay inbound (see relay.go/SocksInboundSettings).
|
|
const SOCKSBasePort = 65100
|
|
|
|
// relayPortSlots is how many ids fit above SOCKSBasePort before wrapping.
|
|
const relayPortSlots = 65535 - SOCKSBasePort
|
|
|
|
// SOCKSPortForInbound derives one inbound's loopback SOCKS5 relay port from
|
|
// its id, wrapping ids past relayPortSlots so no id ever lacks a port.
|
|
func SOCKSPortForInbound(inboundID int) int {
|
|
return SOCKSBasePort + 1 + (inboundID-1)%relayPortSlots
|
|
}
|
|
|
|
var (
|
|
socksPasswordOnce sync.Once
|
|
socksPassword string
|
|
)
|
|
|
|
// SocksPassword returns the process-wide password used to authenticate into
|
|
// every AmneziaWG SOCKS5 relay inbound, generating and caching it once
|
|
// (lazily, on first use) rather than persisting it anywhere: this traffic
|
|
// never leaves loopback, both the config generator (SocksInboundSettings'
|
|
// caller) and the relay dialer (SocksRelay/UDPRelay) live in this same
|
|
// process, and Xray's own generated config is already rebuilt from scratch
|
|
// on every reconcile -- there is nothing for a stored value to survive
|
|
// across that a fresh one wouldn't equally satisfy. Not a real secret (see
|
|
// SocksRelay's own doc comment); this only needs to be unpredictable enough
|
|
// that nothing outside this process could plausibly guess it and dial in
|
|
// over loopback.
|
|
func SocksPassword() string {
|
|
socksPasswordOnce.Do(func() {
|
|
var b [24]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
// crypto/rand failing is effectively unrecoverable for a
|
|
// process that generates real WireGuard keys elsewhere too;
|
|
// a fixed fallback keeps this from panicking outright.
|
|
socksPassword = fmt.Sprintf("amneziawgnet-fallback-%x", b)
|
|
return
|
|
}
|
|
socksPassword = base64.RawURLEncoding.EncodeToString(b[:])
|
|
})
|
|
return socksPassword
|
|
}
|