device.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. package amneziawgnet
  2. import (
  3. "fmt"
  4. "net/netip"
  5. "strings"
  6. "github.com/amnezia-vpn/amneziawg-go/v3/device"
  7. "gvisor.dev/gvisor/pkg/tcpip/stack"
  8. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  9. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  10. )
  11. // DeviceOptions carries AmneziaWG 3.0's device-wide fields (header
  12. // protection, content padding, and the five session-timing knobs) --
  13. // mirrored from amneziawg.Instance's identically named fields by every
  14. // caller (see the 3 Desired{} call sites), not read from Instance
  15. // directly, since amneziawgnet has no dependency on internal/amneziawg
  16. // beyond the plain data types it already imports. Zero-value DeviceOptions
  17. // means amneziawg-go's own real-protocol defaults throughout: classic
  18. // (non-3.0) obfuscation, and its built-in session timings (120s/5s/180s/
  19. // 10s/18 attempts -- device/constants.go).
  20. type DeviceOptions struct {
  21. // HeaderProtectionKey is a base64 32-byte key. Empty disables AWG 3.0
  22. // header protection entirely. Non-empty requires every one of
  23. // Obfuscation31.S1-S4 to be >= 12 (amneziawg-go's own HeaderCipherNonceSize
  24. // requirement) -- IpcSet will reject the config otherwise.
  25. HeaderProtectionKey string
  26. // ContentPaddingAddition, RekeyAfterTime, RekeyTimeout, RejectAfterTime,
  27. // KeepaliveTimeout, and MaxHandshakeAttempts are each a "low-high" range
  28. // (or a bare integer), amneziawg-go's own UintRange.FromString grammar
  29. // (confirmed directly against v3.0.3's device/uapi.go -- all six share
  30. // the identical parser). Empty leaves that one field at amneziawg-go's
  31. // own default.
  32. ContentPaddingAddition string
  33. RekeyAfterTime string
  34. RekeyTimeout string
  35. RejectAfterTime string
  36. KeepaliveTimeout string
  37. MaxHandshakeAttempts string
  38. // RandomTrailers and DisableCookies are AmneziaWG 3.1's two device-wide
  39. // bool toggles (confirmed against amneziawg-go v3.1.20260814's
  40. // device/uapi.go: "random_trailers"/"disable_cookies", both
  41. // strconv.ParseBool). Unlike the string fields above, buildUAPIConfig
  42. // emits these unconditionally on every call -- a bool has no "absent"
  43. // value to gate on, and always emitting both means the reconfigure-
  44. // in-place diff correctly notices a true->false edit, not just
  45. // false->true. RandomTrailers requires the peer to also run AmneziaWG
  46. // 3.1+ with it enabled: amneziawg-go's own receive path only accepts
  47. // an oversized (trailer-padded) packet when the RECEIVING side's own
  48. // RandomTrailers is also true, so a one-sided setting makes that
  49. // side's packets start getting silently dropped by the other.
  50. // DisableCookies is purely local (no peer-side coordination needed)
  51. // but trades away WireGuard's handshake-flood DoS-protection cookie
  52. // replies for a less distinctive packet shape during a flood.
  53. RandomTrailers bool
  54. DisableCookies bool
  55. // Logger is passed to device.NewDevice as-is; nil uses a silent logger
  56. // (device.NewLogger(device.LogLevelSilent, "")).
  57. Logger *device.Logger
  58. }
  59. // Device is one running embedded AmneziaWG interface: an amneziawg-go
  60. // Device over a gVisor netstack, plus the raw *stack.Stack a caller needs to
  61. // attach a TCP/UDP forwarder (see forwarder.go / udp.go). Closing it tears
  62. // down both the WireGuard device and the underlying tun/stack.
  63. type Device struct {
  64. *device.Device
  65. Stack *stack.Stack
  66. // localAddrs snapshots the netstack interface addresses (gVisor exposes
  67. // no read-back); set once at construction, read-only afterwards.
  68. localAddrs []netip.Addr
  69. }
  70. // LocalAddresses returns the configured tunnel-local address(es).
  71. func (d *Device) LocalAddresses() []netip.Addr { return d.localAddrs }
  72. // NewDevice constructs, configures, and brings up an embedded AmneziaWG
  73. // interface for inst in one call: a gVisor-backed tun.Device sized to
  74. // amneziawg.EffectiveMTU, addressed with inst.Address, configured via
  75. // UAPI with inst.Obfuscation, inst.PrivateKey, opts' AWG 3.0 fields, and one
  76. // UAPI peer per inst.Peers entry. It does not attach a forwarder or start
  77. // relaying traffic -- that's the caller's job (see AttachTCPForwarder /
  78. // AttachUDPHandler) -- which is exactly why a caller that will relay real
  79. // traffic must NOT use this function: see newUnconfiguredDevice's doc
  80. // comment for why, and use newUnconfiguredDevice + Configure instead.
  81. func NewDevice(inst amneziawg.Instance, opts DeviceOptions) (*Device, error) {
  82. dev, err := newUnconfiguredDevice(inst, opts)
  83. if err != nil {
  84. return nil, err
  85. }
  86. if err := dev.Configure(inst, opts); err != nil {
  87. return nil, err
  88. }
  89. return dev, nil
  90. }
  91. // newUnconfiguredDevice builds the tun/netstack/device trio but does not
  92. // configure any peers or bring the interface up -- a caller that will relay
  93. // real traffic MUST attach its TCP/UDP handlers (AttachTCPForwarder /
  94. // AttachUDPHandler) against the returned Device.Stack BEFORE calling
  95. // Configure, not after.
  96. //
  97. // This ordering is not a style preference: Configure's IpcSet is what
  98. // starts each configured peer's receive goroutine (amneziawg-go's
  99. // Peer.Start, called from handlePostConfig), and a peer whose handshake
  100. // completes fast enough (e.g. an already-connected client reconnecting
  101. // right as an MTU/address change forces this package's own Manager to
  102. // rebuild the Device) can begin delivering packets into the stack
  103. // immediately -- concurrently with a caller that only calls
  104. // gstack.SetTransportProtocolHandler (AttachTCPForwarder/AttachUDPHandler)
  105. // after Configure returns. A -race CI run caught exactly this as a real
  106. // WARNING: DATA RACE between stack.(*nic).DeliverTransportPacket (the
  107. // peer's receive goroutine, reading the handler table) and
  108. // stack.(*Stack).SetTransportProtocolHandler (the attaching goroutine,
  109. // writing it). See manager.go's ensureLocked rebuild branch for the real
  110. // call order this function exists to support.
  111. func newUnconfiguredDevice(inst amneziawg.Instance, opts DeviceOptions) (*Device, error) {
  112. addrs, err := hostAddresses(inst.Address)
  113. if err != nil {
  114. return nil, fmt.Errorf("amneziawgnet: %w", err)
  115. }
  116. mtu := amneziawg.EffectiveMTU(inst.MTU, inst.Obfuscation.S4)
  117. tun, gstack, err := createNetTUNWithStack(addrs, mtu)
  118. if err != nil {
  119. return nil, fmt.Errorf("amneziawgnet: create netstack: %w", err)
  120. }
  121. logger := opts.Logger
  122. if logger == nil {
  123. logger = device.NewLogger(device.LogLevelSilent, "")
  124. }
  125. dev := device.NewDevice(tun, newResolvingBind(), logger)
  126. return &Device{Device: dev, Stack: gstack, localAddrs: addrs}, nil
  127. }
  128. // Configure applies inst/opts to d via UAPI and brings the interface up.
  129. // Call at most once per Device, and -- for any caller relaying real
  130. // traffic -- only after any AttachTCPForwarder/AttachUDPHandler
  131. // registration against d.Stack (see newUnconfiguredDevice's doc comment
  132. // for why the order matters). Closes d and returns an error if either step
  133. // fails; the caller owns closing anything else it already built against
  134. // d.Stack in that case (e.g. a UDP relay or port-forward set).
  135. func (d *Device) Configure(inst amneziawg.Instance, opts DeviceOptions) error {
  136. conf, err := buildUAPIConfig(inst, opts)
  137. if err != nil {
  138. d.Close()
  139. return fmt.Errorf("amneziawgnet: %w", err)
  140. }
  141. if err := d.IpcSet(conf); err != nil {
  142. d.Close()
  143. return fmt.Errorf("amneziawgnet: IpcSet for inbound %d: %w", inst.Id, err)
  144. }
  145. if err := d.Up(); err != nil {
  146. d.Close()
  147. return fmt.Errorf("amneziawgnet: bring up inbound %d: %w", inst.Id, err)
  148. }
  149. return nil
  150. }
  151. // hostAddresses parses each of inst.Address's CIDR strings (e.g.
  152. // "10.8.1.1/24") down to the bare host address the netstack's NIC gets
  153. // configured with -- the interface's own address, not the subnet it routes.
  154. func hostAddresses(addresses []string) ([]netip.Addr, error) {
  155. out := make([]netip.Addr, 0, len(addresses))
  156. for _, a := range addresses {
  157. prefix, err := netip.ParsePrefix(a)
  158. if err != nil {
  159. return nil, fmt.Errorf("invalid interface address %q: %w", a, err)
  160. }
  161. out = append(out, prefix.Addr())
  162. }
  163. return out, nil
  164. }
  165. // buildUAPIConfig renders inst (plus opts' AWG 3.0 fields) as a WireGuard
  166. // UAPI "set" configuration string -- private_key/listen_port/jc.../s1-s4/
  167. // h1-h4/i1-i5 device lines, the AWG 3.0 device lines when opts asks for them,
  168. // then one public_key/preshared_key/allowed_ip block per peer. Field names
  169. // and format match amneziawg-go v3.0.3's device/uapi.go exactly (confirmed
  170. // against its real source during Phase 0 spiking, not just its docs).
  171. func buildUAPIConfig(inst amneziawg.Instance, opts DeviceOptions) (string, error) {
  172. var b strings.Builder
  173. privHex, err := wireguard.KeyToHex(inst.PrivateKey)
  174. if err != nil {
  175. return "", fmt.Errorf("invalid server private key: %w", err)
  176. }
  177. fmt.Fprintf(&b, "private_key=%s\n", privHex)
  178. fmt.Fprintf(&b, "listen_port=%d\n", inst.ListenPort)
  179. // replace_peers makes every apply a full resync (matches this package's
  180. // own Manager.Ensure semantics): peers no longer in inst.Peers are
  181. // dropped instead of lingering from a previous IpcSet call.
  182. b.WriteString("replace_peers=true\n")
  183. o := inst.Obfuscation
  184. fmt.Fprintf(&b, "jc=%d\njmin=%d\njmax=%d\n", o.Jc, o.Jmin, o.Jmax)
  185. fmt.Fprintf(&b, "s1=%d\ns2=%d\ns3=%d\ns4=%d\n", o.S1, o.S2, o.S3, o.S4)
  186. writeOptionalLine(&b, "h1", o.H1)
  187. writeOptionalLine(&b, "h2", o.H2)
  188. writeOptionalLine(&b, "h3", o.H3)
  189. writeOptionalLine(&b, "h4", o.H4)
  190. writeOptionalLine(&b, "i1", o.I1)
  191. writeOptionalLine(&b, "i2", o.I2)
  192. writeOptionalLine(&b, "i3", o.I3)
  193. writeOptionalLine(&b, "i4", o.I4)
  194. writeOptionalLine(&b, "i5", o.I5)
  195. // An omitted line means "unchanged" to amneziawg-go, so a cleared key can
  196. // only reach a live device as the all-zero one that disables the feature.
  197. hpHex := strings.Repeat("0", 64)
  198. if opts.HeaderProtectionKey != "" {
  199. var err error
  200. hpHex, err = wireguard.KeyToHex(opts.HeaderProtectionKey)
  201. if err != nil {
  202. return "", fmt.Errorf("invalid header protection key: %w", err)
  203. }
  204. }
  205. fmt.Fprintf(&b, "header_protection_key=%s\n", hpHex)
  206. if opts.ContentPaddingAddition != "" {
  207. fmt.Fprintf(&b, "content_padding_addition=%s\n", opts.ContentPaddingAddition)
  208. }
  209. if opts.RekeyAfterTime != "" {
  210. fmt.Fprintf(&b, "rekey_after_time=%s\n", opts.RekeyAfterTime)
  211. }
  212. if opts.RekeyTimeout != "" {
  213. fmt.Fprintf(&b, "rekey_timeout=%s\n", opts.RekeyTimeout)
  214. }
  215. if opts.RejectAfterTime != "" {
  216. fmt.Fprintf(&b, "reject_after_time=%s\n", opts.RejectAfterTime)
  217. }
  218. if opts.KeepaliveTimeout != "" {
  219. fmt.Fprintf(&b, "keepalive_timeout=%s\n", opts.KeepaliveTimeout)
  220. }
  221. if opts.MaxHandshakeAttempts != "" {
  222. fmt.Fprintf(&b, "max_handshake_attempts=%s\n", opts.MaxHandshakeAttempts)
  223. }
  224. fmt.Fprintf(&b, "random_trailers=%t\n", opts.RandomTrailers)
  225. fmt.Fprintf(&b, "disable_cookies=%t\n", opts.DisableCookies)
  226. for _, p := range inst.Peers {
  227. pubHex, err := wireguard.KeyToHex(p.PublicKey)
  228. if err != nil {
  229. return "", fmt.Errorf("peer %q: invalid public key: %w", p.Email, err)
  230. }
  231. fmt.Fprintf(&b, "public_key=%s\n", pubHex)
  232. if p.PresharedKey != "" {
  233. pskHex, err := wireguard.KeyToHex(p.PresharedKey)
  234. if err != nil {
  235. return "", fmt.Errorf("peer %q: invalid preshared key: %w", p.Email, err)
  236. }
  237. fmt.Fprintf(&b, "preshared_key=%s\n", pskHex)
  238. }
  239. for _, allowedIP := range p.AllowedIPs {
  240. fmt.Fprintf(&b, "allowed_ip=%s\n", allowedIP)
  241. }
  242. }
  243. return b.String(), nil
  244. }
  245. // writeOptionalLine writes a "name=v" UAPI line only when v is set -- used for
  246. // h1-h4 and i1-i5, whose empty value means "let amneziawg-go fall back to its
  247. // own default," mirroring how internal/amneziawg's generateServerConfig
  248. // treats the same optional fields.
  249. func writeOptionalLine(b *strings.Builder, name, v string) {
  250. if v == "" {
  251. return
  252. }
  253. fmt.Fprintf(b, "%s=%s\n", name, v)
  254. }