Files
3x-ui/internal/amneziawg/types.go
T
mrchatam 7a41c59494 fix(amneziawg): honor inbound listen when binding UDP socket (#6461)
* fix(amneziawg): honor inbound listen when binding UDP socket

AmneziaWG inbounds ignored the listen field and always opened
awgconn.NewDefaultBind(), so multi-IP hosts replied from the primary
address and handshakes to a secondary IP never completed (#6367).

Carry Inbound.Listen on amneziawg.Instance, open a Bind pinned to that
address (wildcard when empty/0.0.0.0/::), and include listen in
addressFingerprint so edits rebuild the Device.

Fixes #6367

* fix(amneziawg): fall back to wildcard when listen is unusable

Invalid or non-local listen values no longer hard-fail inbound startup;
treat ::0/[::0] as wildcards and normalize listen in the bind fingerprint.

* fix(amneziawg): use ListenConfig.ListenPacket for noctx

---------

Co-authored-by: mrchatam <mrchatam@users.noreply.github.com>
Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
2026-09-12 11:11:02 +02:00

249 lines
11 KiB
Go

// Package amneziawg holds the AmneziaWG protocol's shared, DB-backed shapes
// (Instance, Peer, Obfuscation31, ServerSettings/InboundSettings) and the
// pure functions that derive an Instance from a stored inbound row. It has
// no OS dependency of its own: internal/amneziawgnet embeds amneziawg-go
// over a gVisor netstack and owns the actual running interfaces, one
// Manager-managed Device per desired Instance -- see that package's Manager
// for the reconcile-on-tick lifecycle (modeled on internal/mtproto's own
// Manager), and instance.go's own doc comment for how this package's role
// narrowed to protocol-shape-only after the kernel-module (DKMS) + awg-quick
// architecture this fork originally shipped was retired.
package amneziawg
import "github.com/mhsanaei/3x-ui/v3/internal/database/model"
// Obfuscation31 is an AmneziaWG 3.1 obfuscation parameter set (junk packets,
// padding, magic headers, the five CPS signature-packet slots, and the 3.x
// header-protection/content-padding/timing/boolean fields). 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 Obfuscation31 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-I5 are the real protocol's five CPS signature-packet slots
// (confirmed against amneziawg-go v3.0.3's device/uapi.go: "i1"
// through "i5" are five independent UAPI setters, device.ipackets[0..4],
// all parsed via the identical newObfChain grammar).
I1 string `json:"i1,omitempty"`
I2 string `json:"i2,omitempty"`
I3 string `json:"i3,omitempty"`
I4 string `json:"i4,omitempty"`
I5 string `json:"i5,omitempty"`
// HeaderProtectionKey is a base64 32-byte key shared by both ends; the
// ranges/booleans below are 3.x-only and optional.
HeaderProtectionKey string `json:"headerProtectionKey,omitempty"`
ContentPaddingAddition string `json:"contentPaddingAddition,omitempty"`
RekeyAfterTime string `json:"rekeyAfterTime,omitempty"`
RekeyTimeout string `json:"rekeyTimeout,omitempty"`
RejectAfterTime string `json:"rejectAfterTime,omitempty"`
KeepaliveTimeout string `json:"keepaliveTimeout,omitempty"`
MaxHandshakeAttempts string `json:"maxHandshakeAttempts,omitempty"`
RandomTrailers bool `json:"randomTrailers,omitempty"`
DisableCookies bool `json:"disableCookies,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")
// forwarded to this peer's tunnel address by internal/amneziawgnet's
// PortForwardSet listener supervisor. Empty means no port-forwarding.
ForwardedPorts 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
// Listen is an optional host bind address (e.g. "203.0.113.10").
// Empty/wildcard keeps dual-stack StdNetBind; a real IP pins the UDP socket.
Listen string
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 carries the full AmneziaWG 3.1 parameter set, including
// the 3.x header-protection/content-padding/timing/boolean fields (see
// Obfuscation31's own doc comment) -- amneziawgnet.DeviceOptions is
// what actually consumes it when building the embedded Device's UAPI
// config.
Obfuscation Obfuscation31
Peers []Peer
// ExternalInterface named the host NIC PostUp/PostDown NAT rules
// attached to under the retired kernel-module architecture. Also the
// fallback host NIC internal/amneziawgnet's IPv6-address-alias
// mechanism (desiredV6Aliases) uses when IPv6ExternalInterface is left
// blank.
ExternalInterface string
// IPv6Enabled/IPv6ExternalInterface gate internal/amneziawgnet's
// IPv6-address-alias mechanism (desiredV6Aliases,
// internal/web/service/xray.go's injectAmneziawgV6Egress): each peer
// with an IPv6 AllowedIPs entry gets that address aliased onto this
// host NIC (ip -6 addr add) and a dedicated Xray freedom outbound bound
// to it, giving that peer's own outbound connections a distinct public
// source identity. Narrower in scope than these identically-named
// fields' role under the retired kernel-module architecture, which used
// per-peer NDP-proxy entries (ip -6 neigh add proxy) to also support
// unsolicited inbound connections toward the peer -- that capability is
// the separate, not-yet-built Phase 3.6 (port-forwarding).
IPv6Enabled bool
IPv6ExternalInterface string
// RouteThroughXray gated the kernel-module architecture's opt-in
// TPROXY-into-Xray bridge. The embedded path (internal/amneziawgnet)
// has no equivalent opt-in at all -- every peer's traffic already goes
// through Xray's own SOCKS5 inbound unconditionally, since there's no
// other way for decapsulated gVisor traffic to reach the real internet
// -- so this field is now vestigial: read from existing stored settings
// for backward compatibility, but not acted on by anything. Slated for
// removal alongside the frontend toggle in a follow-up.
RouteThroughXray bool
}
// 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 client configs' DNS line. Blank is
// meaningful, so no omitempty: a dropped key resurrects frontend defaults.
PrimaryDNS string `json:"primaryDns"`
SecondaryDNS string `json:"secondaryDns"`
// ExternalInterface, IPv6Enabled, and IPv6ExternalInterface are live
// again as of Phase 3.5 -- see the matching fields on Instance for what
// they gate (internal/amneziawgnet's IPv6-address-alias mechanism).
// IPv6Subnet was never actually vestigial either: InstanceFromInbound
// already consumes it (via serverAddressV6) to build the server's own
// tunnel address, same as always. Only RouteThroughXray, below, remains
// genuinely vestigial as of the hard cutover to the embedded path
// (internal/amneziawgnet) -- read from existing stored settings for
// backward compatibility, but not acted on by anything.
ExternalInterface string `json:"externalInterface,omitempty"`
IPv6Enabled bool `json:"ipv6Enabled,omitempty"`
IPv6Subnet string `json:"ipv6Subnet,omitempty"`
IPv6ExternalInterface string `json:"ipv6ExternalInterface,omitempty"`
RouteThroughXray bool `json:"routeThroughXray,omitempty"`
// Obfuscation31's fields, repeated flat (not embedded) rather than
// nested under their own key: encoding/json would happily inline an
// embedded Obfuscation31 the same way, but the frontend's Go->Zod/TS
// generator (tools/openapigen) does not — it emits a genuinely nested
// `obfuscation31` 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"`
I2 string `json:"i2,omitempty"`
I3 string `json:"i3,omitempty"`
I4 string `json:"i4,omitempty"`
I5 string `json:"i5,omitempty"`
// HeaderProtectionKey and ContentPaddingAddition are AmneziaWG 3.0
// fields, flat and top-level for the same tools/openapigen reason as
// the block above; Obfuscation() below folds them back into
// Obfuscation31's own identically named fields.
// HeaderProtectionKey is a base64 32-byte key; empty (the default)
// disables AWG 3.0 header protection. A non-empty value requires
// every one of S1-S4 above to be >= 12 -- ValidateObfuscation
// enforces this at save time, not just at IpcSet time.
// ContentPaddingAddition is a "low-high" range or bare integer, the
// same grammar and uint32 cap as H1-H4.
HeaderProtectionKey string `json:"headerProtectionKey,omitempty"`
ContentPaddingAddition string `json:"contentPaddingAddition,omitempty"`
// RekeyAfterTime/RekeyTimeout/RejectAfterTime/KeepaliveTimeout/
// MaxHandshakeAttempts mirror Instance's identically named fields --
// see that type's own doc comment for the grammar/width/real-default
// details. Flat and top-level for the same tools/openapigen reason as
// the rest of this struct.
RekeyAfterTime string `json:"rekeyAfterTime,omitempty"`
RekeyTimeout string `json:"rekeyTimeout,omitempty"`
RejectAfterTime string `json:"rejectAfterTime,omitempty"`
KeepaliveTimeout string `json:"keepaliveTimeout,omitempty"`
MaxHandshakeAttempts string `json:"maxHandshakeAttempts,omitempty"`
// RandomTrailers/DisableCookies mirror Instance's identically named
// AmneziaWG 3.1 fields -- see that type's own doc comment for the real
// protocol/interop details. Both real bool fields (not omitempty):
// buildUAPIConfig always emits both lines explicitly so the
// reconfigure-in-place diff correctly notices a true->false edit, not
// just false->true.
RandomTrailers bool `json:"randomTrailers"`
DisableCookies bool `json:"disableCookies"`
}
// Obfuscation extracts the Obfuscation31 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() Obfuscation31 {
return Obfuscation31{
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, I2: s.I2, I3: s.I3, I4: s.I4, I5: s.I5,
HeaderProtectionKey: s.HeaderProtectionKey,
ContentPaddingAddition: s.ContentPaddingAddition,
RekeyAfterTime: s.RekeyAfterTime,
RekeyTimeout: s.RekeyTimeout,
RejectAfterTime: s.RejectAfterTime,
KeepaliveTimeout: s.KeepaliveTimeout,
MaxHandshakeAttempts: s.MaxHandshakeAttempts,
RandomTrailers: s.RandomTrailers,
DisableCookies: s.DisableCookies,
}
}
// 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"`
}