outbound.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. package amneziawg
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "net/netip"
  7. "strconv"
  8. "strings"
  9. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  10. )
  11. // OutboundPeer is one remote AmneziaWG server: its public key, the routes
  12. // AllowedIPs steers into the tunnel, and its "host:port" Endpoint.
  13. type OutboundPeer struct {
  14. PublicKey string
  15. PresharedKey string
  16. AllowedIPs []string
  17. Endpoint string
  18. KeepAlive int
  19. }
  20. // OutboundInstance is the desired runtime config of one client-mode
  21. // AmneziaWG outbound -- the mirror of Instance, consumed by amneziawgnet.
  22. type OutboundInstance struct {
  23. Tag string
  24. Address []string
  25. MTU int
  26. PrivateKey string
  27. Obfuscation Obfuscation31
  28. Peers []OutboundPeer
  29. ListenPort int
  30. DNS string
  31. }
  32. // OutboundSettings is the Settings JSON stored on an "amneziawg" outbound
  33. // row; flat obfuscation keys mirror ServerSettings so values paste 1:1.
  34. type OutboundSettings struct {
  35. MTU int `json:"mtu,omitempty"`
  36. SecretKey string `json:"secretKey"`
  37. Address []string `json:"address"`
  38. ListenPort int `json:"listenPort,omitempty"`
  39. DNS string `json:"dns,omitempty"`
  40. // Flat Obfuscation31 mirror -- see OutboundSettings' doc comment.
  41. Jc int `json:"jc"`
  42. Jmin int `json:"jmin"`
  43. Jmax int `json:"jmax"`
  44. S1 int `json:"s1"`
  45. S2 int `json:"s2"`
  46. S3 int `json:"s3"`
  47. S4 int `json:"s4"`
  48. H1 string `json:"h1"`
  49. H2 string `json:"h2"`
  50. H3 string `json:"h3"`
  51. H4 string `json:"h4"`
  52. I1 string `json:"i1,omitempty"`
  53. I2 string `json:"i2,omitempty"`
  54. I3 string `json:"i3,omitempty"`
  55. I4 string `json:"i4,omitempty"`
  56. I5 string `json:"i5,omitempty"`
  57. HeaderProtectionKey string `json:"headerProtectionKey,omitempty"`
  58. ContentPaddingAddition string `json:"contentPaddingAddition,omitempty"`
  59. RekeyAfterTime string `json:"rekeyAfterTime,omitempty"`
  60. RekeyTimeout string `json:"rekeyTimeout,omitempty"`
  61. RejectAfterTime string `json:"rejectAfterTime,omitempty"`
  62. KeepaliveTimeout string `json:"keepaliveTimeout,omitempty"`
  63. MaxHandshakeAttempts string `json:"maxHandshakeAttempts,omitempty"`
  64. RandomTrailers bool `json:"randomTrailers"`
  65. DisableCookies bool `json:"disableCookies"`
  66. Peers []OutboundSettingsPeer `json:"peers"`
  67. }
  68. // OutboundSettingsPeer is one entry of OutboundSettings.Peers.
  69. type OutboundSettingsPeer struct {
  70. PublicKey string `json:"publicKey"`
  71. PresharedKey string `json:"presharedKey,omitempty"`
  72. AllowedIPs []string `json:"allowedIPs"`
  73. Endpoint string `json:"endpoint"`
  74. KeepAlive int `json:"keepAlive,omitempty"`
  75. }
  76. // Obfuscation folds the flat wire fields back into the grouped type, matching
  77. // ServerSettings.Obfuscation.
  78. func (s OutboundSettings) Obfuscation() Obfuscation31 {
  79. return Obfuscation31{
  80. Jc: s.Jc, Jmin: s.Jmin, Jmax: s.Jmax,
  81. S1: s.S1, S2: s.S2, S3: s.S3, S4: s.S4,
  82. H1: s.H1, H2: s.H2, H3: s.H3, H4: s.H4,
  83. I1: s.I1, I2: s.I2, I3: s.I3, I4: s.I4, I5: s.I5,
  84. HeaderProtectionKey: s.HeaderProtectionKey,
  85. ContentPaddingAddition: s.ContentPaddingAddition,
  86. RekeyAfterTime: s.RekeyAfterTime,
  87. RekeyTimeout: s.RekeyTimeout,
  88. RejectAfterTime: s.RejectAfterTime,
  89. KeepaliveTimeout: s.KeepaliveTimeout,
  90. MaxHandshakeAttempts: s.MaxHandshakeAttempts,
  91. RandomTrailers: s.RandomTrailers,
  92. DisableCookies: s.DisableCookies,
  93. }
  94. }
  95. // IsAmneziaWGOutbound reports whether a raw outbound JSON object from the
  96. // Xray template carries the panel's amneziawg pseudo-protocol.
  97. func IsAmneziaWGOutbound(raw []byte) bool {
  98. var probe struct {
  99. Protocol string `json:"protocol"`
  100. }
  101. if err := json.Unmarshal(raw, &probe); err != nil {
  102. return false
  103. }
  104. return probe.Protocol == "amneziawg"
  105. }
  106. // outboundSettingsOf extracts the nested "settings" block from a raw
  107. // amneziawg template outbound.
  108. func outboundSettingsOf(raw []byte) (json.RawMessage, bool) {
  109. var wrapper struct {
  110. Settings json.RawMessage `json:"settings"`
  111. }
  112. if err := json.Unmarshal(raw, &wrapper); err != nil || len(wrapper.Settings) == 0 {
  113. return nil, false
  114. }
  115. return wrapper.Settings, true
  116. }
  117. // InstanceFromOutbound derives a client-mode instance from one raw template
  118. // outbound; false when unusable or a peer lacks key/endpoint/allowedIPs.
  119. func InstanceFromOutbound(tag string, raw []byte) (OutboundInstance, bool) {
  120. settingsRaw, ok := outboundSettingsOf(raw)
  121. if !ok {
  122. return OutboundInstance{}, false
  123. }
  124. var parsed OutboundSettings
  125. if err := json.Unmarshal(settingsRaw, &parsed); err != nil {
  126. return OutboundInstance{}, false
  127. }
  128. inst := OutboundInstance{
  129. Tag: tag,
  130. Address: parsed.Address,
  131. MTU: parsed.MTU,
  132. PrivateKey: parsed.SecretKey,
  133. ListenPort: parsed.ListenPort,
  134. DNS: NormalizeDNSServer(parsed.DNS),
  135. Obfuscation: Obfuscation31{
  136. Jc: parsed.Jc, Jmin: parsed.Jmin, Jmax: parsed.Jmax,
  137. S1: parsed.S1, S2: parsed.S2, S3: parsed.S3, S4: parsed.S4,
  138. H1: parsed.H1, H2: parsed.H2, H3: parsed.H3, H4: parsed.H4,
  139. I1: parsed.I1, I2: parsed.I2, I3: parsed.I3, I4: parsed.I4, I5: parsed.I5,
  140. HeaderProtectionKey: parsed.HeaderProtectionKey,
  141. ContentPaddingAddition: parsed.ContentPaddingAddition,
  142. RekeyAfterTime: parsed.RekeyAfterTime,
  143. RekeyTimeout: parsed.RekeyTimeout,
  144. RejectAfterTime: parsed.RejectAfterTime,
  145. KeepaliveTimeout: parsed.KeepaliveTimeout,
  146. MaxHandshakeAttempts: parsed.MaxHandshakeAttempts,
  147. RandomTrailers: parsed.RandomTrailers,
  148. DisableCookies: parsed.DisableCookies,
  149. },
  150. }
  151. for _, p := range parsed.Peers {
  152. if p.PublicKey == "" || len(p.AllowedIPs) == 0 || p.Endpoint == "" {
  153. continue
  154. }
  155. peer := OutboundPeer(p)
  156. peer.AllowedIPs = peer.AllowedIPs[:0:0]
  157. for _, a := range p.AllowedIPs {
  158. prefix, err := netip.ParsePrefix(strings.TrimSpace(a))
  159. if err != nil {
  160. return OutboundInstance{}, false
  161. }
  162. peer.AllowedIPs = append(peer.AllowedIPs, prefix.String())
  163. }
  164. inst.Peers = append(inst.Peers, peer)
  165. }
  166. if len(inst.Address) == 0 || len(inst.Peers) == 0 {
  167. return OutboundInstance{}, false
  168. }
  169. return inst, true
  170. }
  171. // validateEndpoint accepts "host:port" with a numeric port and no control
  172. // characters; hostnames resolve at IpcSet time via resolvingBind.
  173. func validateEndpoint(ep string) error {
  174. if ep == "" {
  175. return fmt.Errorf("endpoint is required")
  176. }
  177. if err := ValidateConfigValue("endpoint", ep); err != nil {
  178. return err
  179. }
  180. host, portS, err := net.SplitHostPort(ep)
  181. if err != nil {
  182. return fmt.Errorf("invalid endpoint %q: must be host:port", ep)
  183. }
  184. port, err := strconv.Atoi(portS)
  185. if err != nil || port <= 0 || port > 65535 {
  186. return fmt.Errorf("invalid endpoint %q: bad port", ep)
  187. }
  188. if strings.TrimSpace(host) == "" {
  189. return fmt.Errorf("invalid endpoint %q: empty host", ep)
  190. }
  191. return nil
  192. }
  193. // validateTunnelAddresses requires every entry to be a parseable IP prefix
  194. // (the outbound's own tunnel address(es), e.g. "10.8.1.2/32").
  195. func validateTunnelAddresses(addrs []string) error {
  196. if len(addrs) == 0 {
  197. return fmt.Errorf("at least one tunnel address is required")
  198. }
  199. for _, a := range addrs {
  200. prefix, err := netip.ParsePrefix(a)
  201. if err != nil {
  202. return fmt.Errorf("invalid tunnel address %q: %w", a, err)
  203. }
  204. _ = prefix
  205. }
  206. return nil
  207. }
  208. // NormalizeDNSServer converts a bare IP or IP:port into a standard host:port.
  209. func NormalizeDNSServer(s string) string {
  210. s = strings.TrimSpace(s)
  211. if s == "" {
  212. return ""
  213. }
  214. if addr, err := netip.ParseAddr(s); err == nil {
  215. return netip.AddrPortFrom(addr, 53).String()
  216. }
  217. if ap, err := netip.ParseAddrPort(s); err == nil {
  218. return ap.String()
  219. }
  220. return s
  221. }
  222. // ValidateDNSServer checks that dns is empty or a valid IP or IP:port.
  223. func ValidateDNSServer(s string) error {
  224. if s == "" {
  225. return nil
  226. }
  227. if err := ValidateConfigValue("dns", s); err != nil {
  228. return err
  229. }
  230. if _, err := netip.ParseAddr(s); err == nil {
  231. return nil
  232. }
  233. if _, err := netip.ParseAddrPort(s); err == nil {
  234. return nil
  235. }
  236. return fmt.Errorf("must be an IP address or IP:port")
  237. }
  238. // ValidateAmneziaWGOutbound rejects settings that could break the embedded
  239. // device's UAPI apply or smuggle control characters downstream.
  240. func ValidateAmneziaWGOutbound(tag string, raw []byte) error {
  241. if strings.TrimSpace(tag) == "" {
  242. return fmt.Errorf("amneziawg outbound: tag must be a non-empty string")
  243. }
  244. settingsRaw, ok := outboundSettingsOf(raw)
  245. if !ok {
  246. return fmt.Errorf("amneziawg outbound %q: missing settings block", tag)
  247. }
  248. var parsed OutboundSettings
  249. if err := json.Unmarshal(settingsRaw, &parsed); err != nil {
  250. return fmt.Errorf("amneziawg outbound %q: invalid settings: %w", tag, err)
  251. }
  252. if err := validateTunnelAddresses(parsed.Address); err != nil {
  253. return fmt.Errorf("amneziawg outbound %q: %w", tag, err)
  254. }
  255. if err := ValidateDNSServer(parsed.DNS); err != nil {
  256. return fmt.Errorf("amneziawg outbound %q: invalid dns: %w", tag, err)
  257. }
  258. if strings.TrimSpace(parsed.SecretKey) == "" {
  259. return fmt.Errorf("amneziawg outbound %q: privateKey is required", tag)
  260. }
  261. if _, err := wireguard.KeyToHex(parsed.SecretKey); err != nil {
  262. return fmt.Errorf("amneziawg outbound %q: invalid privateKey: %w", tag, err)
  263. }
  264. if err := ValidateObfuscation(parsed.Obfuscation()); err != nil {
  265. return fmt.Errorf("amneziawg outbound %q: %w", tag, err)
  266. }
  267. for n, iv := range map[string]string{
  268. "i1": parsed.I1, "i2": parsed.I2, "i3": parsed.I3, "i4": parsed.I4, "i5": parsed.I5,
  269. } {
  270. if err := ValidateConfigValue(n, iv); err != nil {
  271. return fmt.Errorf("amneziawg outbound %q: %w", tag, err)
  272. }
  273. }
  274. if err := validateHeaderProtectionKey(parsed.HeaderProtectionKey); err != nil {
  275. return fmt.Errorf("amneziawg outbound %q: %w", tag, err)
  276. }
  277. if len(parsed.Peers) == 0 {
  278. return fmt.Errorf("amneziawg outbound %q: at least one peer is required", tag)
  279. }
  280. for i, p := range parsed.Peers {
  281. if strings.TrimSpace(p.PublicKey) == "" {
  282. return fmt.Errorf("amneziawg outbound %q: peer %d: publicKey is required", tag, i)
  283. }
  284. if _, err := wireguard.KeyToHex(p.PublicKey); err != nil {
  285. return fmt.Errorf("amneziawg outbound %q: peer %d: invalid publicKey: %w", tag, i, err)
  286. }
  287. if p.PresharedKey != "" {
  288. if _, err := wireguard.KeyToHex(p.PresharedKey); err != nil {
  289. return fmt.Errorf("amneziawg outbound %q: peer %d: invalid presharedKey: %w", tag, i, err)
  290. }
  291. }
  292. if err := validateEndpoint(p.Endpoint); err != nil {
  293. return fmt.Errorf("amneziawg outbound %q: peer %d: %w", tag, i, err)
  294. }
  295. if len(p.AllowedIPs) == 0 {
  296. return fmt.Errorf("amneziawg outbound %q: peer %d: at least one allowedIPs entry is required", tag, i)
  297. }
  298. for _, a := range p.AllowedIPs {
  299. if _, err := netip.ParsePrefix(a); err != nil {
  300. return fmt.Errorf("amneziawg outbound %q: peer %d: invalid allowedIP %q: %w", tag, i, a, err)
  301. }
  302. }
  303. }
  304. return nil
  305. }