mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 15:47:14 +00:00
909feefd1d
RouteThroughXray/RouteOutboundTag were client-only, but the more common case is "route this whole AmneziaWG server's traffic through Xray", not configuring every peer individually. Add the same pair to ServerSettings (inbound-level) while keeping the per-client fields as an override — matching how ExternalInterface/IPv6Enabled already work at the server level next to per-client settings like ForwardedPorts. Effective per-peer decision (computed once, in InstanceFromInbound, not duplicated at each consumer): - routed = client.RouteThroughXray || server.RouteThroughXray - outbound tag = client's own if set, else the server's default This means a peer can be routed by the inbound-wide default with no config of its own, opt in on its own even when the default is off, or keep the default's on/off but pick a different outbound than everyone else. internal/web/service/xray.go's injectAmneziawgEgress now calls amneziawg.InstanceFromInbound instead of re-parsing InboundSettings and reading model.Client fields directly — the same effective-routing computation the kernel-side TPROXY rules use, so the two independent reconcile loops (Xray-config generation and the AWG manager) can never quietly disagree about which peers are actually routed. Frontend: Switch + conditional outbound Select added to the AWG inbound form (mirroring the client-form version and mtproto's own UI), plus the inbound-defaults.ts default-object fix that's bitten this project's CI before (Phase 2a) whenever ServerSettings gains a new required-shaped field. Test fixtures in xray_config_inject_test.go needed a real Server block and PublicKey once injectAmneziawgEgress started requiring a usable InstanceFromInbound result — both were implicit fixture gaps, not behavior the old tests were actually asserting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
174 lines
7.3 KiB
Go
174 lines
7.3 KiB
Go
// Package amneziawg manages native AmneziaWG interfaces (via awg-quick/awg,
|
|
// the AmneziaWG DKMS kernel module's userspace tools) as sidecars to the
|
|
// panel, the same way internal/mtproto manages mtg processes: one inbound
|
|
// row maps to one desired Instance, and a Manager reconciles the running
|
|
// interfaces toward whatever the database currently wants.
|
|
package amneziawg
|
|
|
|
import "github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
|
// Obfuscation20 is an AmneziaWG 2.0 obfuscation parameter set (junk packets,
|
|
// padding, magic headers, the I1 signature packet). The same values must be
|
|
// applied on both ends of a tunnel, so the server stores them and every
|
|
// client config inherits them verbatim.
|
|
type Obfuscation20 struct {
|
|
Jc int `json:"jc"`
|
|
Jmin int `json:"jmin"`
|
|
Jmax int `json:"jmax"`
|
|
S1 int `json:"s1"`
|
|
S2 int `json:"s2"`
|
|
S3 int `json:"s3"`
|
|
S4 int `json:"s4"`
|
|
H1 string `json:"h1"`
|
|
H2 string `json:"h2"`
|
|
H3 string `json:"h3"`
|
|
H4 string `json:"h4"`
|
|
I1 string `json:"i1,omitempty"`
|
|
}
|
|
|
|
// Peer is one desired AmneziaWG peer: a client device the interface accepts.
|
|
// Email attributes traffic and online status back to the owning client, the
|
|
// same role SecretEntry.Name plays for mtproto.
|
|
type Peer struct {
|
|
Email string
|
|
PublicKey string
|
|
PresharedKey string
|
|
AllowedIPs []string
|
|
|
|
// ForwardedPorts is a raw, user-supplied port list ("80, 443, 8000-8100")
|
|
// DNAT'd to this peer's tunnel address. Empty means no port-forwarding.
|
|
ForwardedPorts string
|
|
|
|
// RouteThroughXray and RouteOutboundTag are this peer's EFFECTIVE routing
|
|
// decision — already resolved by InstanceFromInbound from the per-client
|
|
// setting OR'd with the inbound-wide ServerSettings.RouteThroughXray
|
|
// default (and the client's own RouteOutboundTag falling back to the
|
|
// server's when the client didn't set one). Callers never need to look at
|
|
// the raw client/server fields separately.
|
|
//
|
|
// When true, TPROXYs this peer's traffic (matched by its tunnel source
|
|
// IP) into the single shared loopback Xray dokodemo-door bridge (see
|
|
// EgressPort in route_egress.go) instead of letting it NAT straight out
|
|
// through ExternalInterface. All routed peers, across every AmneziaWG
|
|
// instance, share that one bridge and one fwmark/policy-route pair; the
|
|
// per-peer distinction happens downstream in Xray's own router, which the
|
|
// web service feeds a source-IP-matched rule per peer. RouteOutboundTag
|
|
// is the Xray outbound/balancer tag that rule targets; empty means
|
|
// Xray's default routing decides.
|
|
RouteThroughXray bool
|
|
RouteOutboundTag string
|
|
}
|
|
|
|
// Instance is the desired runtime configuration of one AmneziaWG inbound: a
|
|
// single interface (e.g. awg1) with a set of peers, mirroring how one mtproto
|
|
// inbound maps to one mtg process (internal/mtproto.Instance).
|
|
type Instance struct {
|
|
Id int
|
|
Tag string
|
|
InterfaceName string
|
|
ListenPort int
|
|
PrivateKey string
|
|
PublicKey string
|
|
// Address holds the interface's own tunnel address(es), e.g. "10.8.1.1/24".
|
|
// Carries both the IPv4 and (when enabled) IPv6 server address.
|
|
Address []string
|
|
MTU int
|
|
|
|
Obfuscation Obfuscation20
|
|
Peers []Peer
|
|
|
|
// ExternalInterface is the host NIC PostUp/PostDown NAT rules attach to.
|
|
// Empty means auto-detect at config-generation time.
|
|
ExternalInterface string
|
|
|
|
// IPv6Enabled turns on the per-peer NDP proxy PostUp/PostDown entries
|
|
// (ip -6 neigh add/del proxy) for peers that have an IPv6 AllowedIPs
|
|
// entry. IPv6ExternalInterface overrides ExternalInterface for those
|
|
// entries specifically; empty means reuse ExternalInterface.
|
|
IPv6Enabled bool
|
|
IPv6ExternalInterface string
|
|
}
|
|
|
|
// ServerSettings is the "server" block of an AmneziaWG inbound's Settings
|
|
// JSON: the interface-level configuration shared by every client/peer. The
|
|
// listen port is deliberately not duplicated here — it lives on the inbound
|
|
// row itself (Inbound.Port), like every other protocol.
|
|
type ServerSettings struct {
|
|
PrivateKey string `json:"privateKey"`
|
|
PublicKey string `json:"publicKey"`
|
|
|
|
SubnetIP string `json:"subnetIp"`
|
|
SubnetCIDR int `json:"subnetCidr"`
|
|
MTU int `json:"mtu,omitempty"`
|
|
|
|
// PrimaryDNS/SecondaryDNS seed the DNS line of downloadable client
|
|
// configs; the server's own interface never sets one (see BuildClientConfig).
|
|
PrimaryDNS string `json:"primaryDns,omitempty"`
|
|
SecondaryDNS string `json:"secondaryDns,omitempty"`
|
|
|
|
// ExternalInterface is the host NIC PostUp/PostDown NAT rules attach to.
|
|
// Empty means auto-detect.
|
|
ExternalInterface string `json:"externalInterface,omitempty"`
|
|
|
|
// IPv6Enabled turns on native IPv6 for clients: an IPv6 host address is
|
|
// allocated from IPv6Subnet alongside each client's IPv4 one, and the
|
|
// server proxies NDP for each enabled client's address so upstream
|
|
// routers see it as directly reachable (no NAT66). IPv6ExternalInterface
|
|
// overrides ExternalInterface for the NDP-proxy PostUp/PostDown entries
|
|
// specifically; empty reuses ExternalInterface.
|
|
IPv6Enabled bool `json:"ipv6Enabled,omitempty"`
|
|
IPv6Subnet string `json:"ipv6Subnet,omitempty"`
|
|
IPv6ExternalInterface string `json:"ipv6ExternalInterface,omitempty"`
|
|
|
|
// RouteThroughXray, when true, is the inbound-wide default: every peer
|
|
// TPROXYs into Xray unless it explicitly turns its own RouteThroughXray
|
|
// off... except a plain bool can't distinguish "peer left it unset" from
|
|
// "peer explicitly opted out", so in practice this ORs with each peer's
|
|
// own flag (see Peer.RouteThroughXray) — turning this on routes every
|
|
// peer, turning it off still lets individual peers opt in on their own.
|
|
// RouteOutboundTag is the default outbound/balancer tag used when a
|
|
// routed peer didn't set its own; empty means Xray's default routing.
|
|
RouteThroughXray bool `json:"routeThroughXray,omitempty"`
|
|
RouteOutboundTag string `json:"routeOutboundTag,omitempty"`
|
|
|
|
// Obfuscation20's fields, repeated flat (not embedded) rather than
|
|
// nested under their own key: encoding/json would happily inline an
|
|
// embedded Obfuscation20 the same way, but the frontend's Go->Zod/TS
|
|
// generator (tools/openapigen) does not — it emits a genuinely nested
|
|
// `obfuscation20` object, which would silently diverge from the real
|
|
// wire JSON. See Obfuscation() below for the manager-facing conversion.
|
|
Jc int `json:"jc"`
|
|
Jmin int `json:"jmin"`
|
|
Jmax int `json:"jmax"`
|
|
S1 int `json:"s1"`
|
|
S2 int `json:"s2"`
|
|
S3 int `json:"s3"`
|
|
S4 int `json:"s4"`
|
|
H1 string `json:"h1"`
|
|
H2 string `json:"h2"`
|
|
H3 string `json:"h3"`
|
|
H4 string `json:"h4"`
|
|
I1 string `json:"i1,omitempty"`
|
|
}
|
|
|
|
// Obfuscation extracts the Obfuscation20 parameter set from a ServerSettings
|
|
// block, for callers (the Manager, ValidateObfuscation) that want the
|
|
// grouped type rather than the flat wire fields.
|
|
func (s ServerSettings) Obfuscation() Obfuscation20 {
|
|
return Obfuscation20{
|
|
Jc: s.Jc, Jmin: s.Jmin, Jmax: s.Jmax,
|
|
S1: s.S1, S2: s.S2, S3: s.S3, S4: s.S4,
|
|
H1: s.H1, H2: s.H2, H3: s.H3, H4: s.H4,
|
|
I1: s.I1,
|
|
}
|
|
}
|
|
|
|
// InboundSettings is the full Settings JSON shape stored on an AmneziaWG
|
|
// inbound row: one server block plus the usual generic client list, so bulk
|
|
// operations, the QR modal and subscriptions all come from the same shared
|
|
// infrastructure every other protocol uses.
|
|
type InboundSettings struct {
|
|
Server *ServerSettings `json:"server"`
|
|
Clients []model.Client `json:"clients"`
|
|
}
|