warp.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. package integration
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/ecdsa"
  6. "crypto/elliptic"
  7. "crypto/rand"
  8. "crypto/x509"
  9. "encoding/base64"
  10. "encoding/json"
  11. "encoding/pem"
  12. "fmt"
  13. "io"
  14. "net"
  15. "net/http"
  16. "os"
  17. "time"
  18. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  19. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  20. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  21. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  22. )
  23. // WarpService provides business logic for Cloudflare WARP integration.
  24. // It manages WARP configuration and connectivity settings.
  25. type WarpService struct {
  26. service.SettingService
  27. }
  28. const (
  29. warpClientVer = "a-6.30-3596"
  30. )
  31. // warpAPIBase is the Cloudflare WARP registration API base URL. It is a var
  32. // (not a const) so integration tests can point it at a mock server.
  33. var warpAPIBase = "https://api.cloudflareclient.com/v0a4005"
  34. // warpMasqueAPIBase and warpMasqueClientVer are the API revision and client the WARP
  35. // apps enroll secp256r1 MASQUE keys with; a var so tests can point it at a mock.
  36. var warpMasqueAPIBase = "https://api.cloudflareclient.com/v0a4471"
  37. const warpMasqueClientVer = "a-6.35-4471"
  38. func (s *WarpService) GetWarpData() (string, error) {
  39. return s.GetWarp()
  40. }
  41. func (s *WarpService) DelWarpData() error {
  42. return s.SetWarp("")
  43. }
  44. func (s *WarpService) GetWarpConfig() (string, error) {
  45. warpData, err := s.loadWarpCreds()
  46. if err != nil {
  47. return "", err
  48. }
  49. url := fmt.Sprintf("%s/reg/%s", warpAPIBase, warpData["device_id"])
  50. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
  51. if err != nil {
  52. return "", err
  53. }
  54. req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
  55. body, err := s.doWarpRequest(req)
  56. if err != nil {
  57. return "", err
  58. }
  59. return string(body), nil
  60. }
  61. func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error) {
  62. hostName, _ := os.Hostname()
  63. reqBody, err := json.Marshal(map[string]any{
  64. "key": publicKey,
  65. "tos": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
  66. "type": "PC",
  67. "model": "x-ui",
  68. "name": hostName,
  69. })
  70. if err != nil {
  71. return "", err
  72. }
  73. req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, warpAPIBase+"/reg", bytes.NewReader(reqBody))
  74. if err != nil {
  75. return "", err
  76. }
  77. req.Header.Set("CF-Client-Version", warpClientVer)
  78. req.Header.Set("Content-Type", "application/json")
  79. body, err := s.doWarpRequest(req)
  80. if err != nil {
  81. return "", err
  82. }
  83. var rsp map[string]any
  84. if err := json.Unmarshal(body, &rsp); err != nil {
  85. return "", err
  86. }
  87. deviceID, ok := rsp["id"].(string)
  88. if !ok {
  89. return "", common.NewError("warp register: missing 'id' in response")
  90. }
  91. token, ok := rsp["token"].(string)
  92. if !ok {
  93. return "", common.NewError("warp register: missing 'token' in response")
  94. }
  95. account, ok := rsp["account"].(map[string]any)
  96. if !ok {
  97. return "", common.NewError("warp register: missing 'account' in response")
  98. }
  99. license, ok := account["license"].(string)
  100. if !ok {
  101. return "", common.NewError("warp register: missing 'account.license' in response")
  102. }
  103. warpData := map[string]string{
  104. "access_token": token,
  105. "device_id": deviceID,
  106. "license_key": license,
  107. "private_key": secretKey,
  108. }
  109. if config, ok := rsp["config"].(map[string]any); ok {
  110. if clientID, ok := config["client_id"].(string); ok {
  111. warpData["client_id"] = clientID
  112. }
  113. }
  114. warpJSON, err := json.MarshalIndent(warpData, "", " ")
  115. if err != nil {
  116. return "", err
  117. }
  118. if err := s.SetWarp(string(warpJSON)); err != nil {
  119. return "", err
  120. }
  121. result, err := json.MarshalIndent(map[string]any{
  122. "data": warpData,
  123. "config": json.RawMessage(body),
  124. }, "", " ")
  125. if err != nil {
  126. return "", err
  127. }
  128. return string(result), nil
  129. }
  130. // RegWarpMasque registers a WARP device of its own and enrolls an ECDSA P-256 key for
  131. // MASQUE: enrolling replaces a device's WireGuard key, so the stored one stays untouched.
  132. func (s *WarpService) RegWarpMasque() (string, error) {
  133. _, wgPublicKey, err := wireguard.GenerateWireguardKeypair()
  134. if err != nil {
  135. return "", err
  136. }
  137. hostName, _ := os.Hostname()
  138. regBody, err := json.Marshal(map[string]any{
  139. "key": wgPublicKey,
  140. "tos": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
  141. "type": "PC",
  142. "model": "x-ui",
  143. "name": hostName,
  144. })
  145. if err != nil {
  146. return "", err
  147. }
  148. var registration struct {
  149. ID string `json:"id"`
  150. Token string `json:"token"`
  151. }
  152. if err := s.doWarpMasqueRequest(http.MethodPost, warpMasqueAPIBase+"/reg", "", regBody, &registration); err != nil {
  153. return "", err
  154. }
  155. if registration.ID == "" || registration.Token == "" {
  156. return "", common.NewError("warp masque register: missing 'id' or 'token' in response")
  157. }
  158. key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  159. if err != nil {
  160. return "", err
  161. }
  162. publicDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
  163. if err != nil {
  164. return "", err
  165. }
  166. enrollBody, err := json.Marshal(map[string]string{
  167. "key": base64.StdEncoding.EncodeToString(publicDER),
  168. "key_type": "secp256r1",
  169. "tunnel_type": "masque",
  170. "name": hostName,
  171. })
  172. if err != nil {
  173. return "", err
  174. }
  175. var enrolled struct {
  176. Config struct {
  177. Interface struct {
  178. Addresses struct {
  179. V4 string `json:"v4"`
  180. V6 string `json:"v6"`
  181. } `json:"addresses"`
  182. } `json:"interface"`
  183. Peers []struct {
  184. PublicKey string `json:"public_key"`
  185. Endpoint struct {
  186. V4 string `json:"v4"`
  187. } `json:"endpoint"`
  188. } `json:"peers"`
  189. } `json:"config"`
  190. }
  191. if err := s.doWarpMasqueRequest(http.MethodPatch, warpMasqueAPIBase+"/reg/"+registration.ID, registration.Token, enrollBody, &enrolled); err != nil {
  192. return "", err
  193. }
  194. if len(enrolled.Config.Peers) == 0 || enrolled.Config.Peers[0].PublicKey == "" {
  195. return "", common.NewError("warp masque enroll: missing endpoint public key in response")
  196. }
  197. peer := enrolled.Config.Peers[0]
  198. endpoint, _, err := net.SplitHostPort(peer.Endpoint.V4)
  199. if err != nil {
  200. endpoint = peer.Endpoint.V4
  201. }
  202. privateDER, err := x509.MarshalPKCS8PrivateKey(key)
  203. if err != nil {
  204. return "", err
  205. }
  206. var address []string
  207. for _, a := range []string{enrolled.Config.Interface.Addresses.V4, enrolled.Config.Interface.Addresses.V6} {
  208. if a != "" {
  209. address = append(address, a)
  210. }
  211. }
  212. result, err := json.MarshalIndent(map[string]any{
  213. "privateKey": string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDER})),
  214. "publicKey": peer.PublicKey,
  215. "address": address,
  216. "endpoint": endpoint,
  217. }, "", " ")
  218. if err != nil {
  219. return "", err
  220. }
  221. return string(result), nil
  222. }
  223. func (s *WarpService) doWarpMasqueRequest(method, url, token string, body []byte, out any) error {
  224. req, err := http.NewRequestWithContext(context.Background(), method, url, bytes.NewReader(body))
  225. if err != nil {
  226. return err
  227. }
  228. req.Header.Set("CF-Client-Version", warpMasqueClientVer)
  229. req.Header.Set("User-Agent", "WARP for Android")
  230. req.Header.Set("Content-Type", "application/json")
  231. if token != "" {
  232. req.Header.Set("Authorization", "Bearer "+token)
  233. }
  234. respBody, err := s.doWarpRequest(req)
  235. if err != nil {
  236. return err
  237. }
  238. return json.Unmarshal(respBody, out)
  239. }
  240. func (s *WarpService) SetWarpLicense(license string) (string, error) {
  241. warpData, err := s.loadWarpCreds()
  242. if err != nil {
  243. return "", err
  244. }
  245. url := fmt.Sprintf("%s/reg/%s/account", warpAPIBase, warpData["device_id"])
  246. reqBody, err := json.Marshal(map[string]string{"license": license})
  247. if err != nil {
  248. return "", err
  249. }
  250. req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, url, bytes.NewReader(reqBody))
  251. if err != nil {
  252. return "", err
  253. }
  254. req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
  255. req.Header.Set("Content-Type", "application/json")
  256. body, err := s.doWarpRequest(req)
  257. if err != nil {
  258. return "", err
  259. }
  260. var response map[string]any
  261. if err := json.Unmarshal(body, &response); err != nil {
  262. return "", err
  263. }
  264. if _, ok := response["id"].(string); !ok {
  265. return "", common.NewErrorf("warp set license failed: unexpected response: %s", string(body))
  266. }
  267. warpData["license_key"] = license
  268. newWarpData, err := json.MarshalIndent(warpData, "", " ")
  269. if err != nil {
  270. return "", err
  271. }
  272. if err := s.SetWarp(string(newWarpData)); err != nil {
  273. return "", err
  274. }
  275. return string(newWarpData), nil
  276. }
  277. func (s *WarpService) ChangeWarpIP() (string, error) {
  278. warpDataMap, err := s.loadWarpCreds()
  279. if err != nil {
  280. return "", err
  281. }
  282. privKey, pubKey, err := wireguard.GenerateWireguardKeypair()
  283. if err != nil {
  284. return "", err
  285. }
  286. result, err := s.RegWarp(privKey, pubKey)
  287. if err != nil {
  288. return "", err
  289. }
  290. var parsed struct {
  291. Data map[string]string `json:"data"`
  292. Config map[string]any `json:"config"`
  293. }
  294. if err := json.Unmarshal([]byte(result), &parsed); err != nil {
  295. return "", err
  296. }
  297. // RegWarp stores the new device's data, which for a fresh registration
  298. // carries an empty license_key. Re-apply the old license key to the stored
  299. // data BEFORE the remote upgrade attempt, so a failed re-apply can never
  300. // delete the saved key.
  301. var reapplyWarn error
  302. if license, ok := warpDataMap["license_key"]; ok && len(license) >= 26 {
  303. if parsed.Data == nil {
  304. parsed.Data = make(map[string]string)
  305. }
  306. parsed.Data["license_key"] = license
  307. if stored, err := json.MarshalIndent(parsed.Data, "", " "); err != nil {
  308. return "", err
  309. } else if err := s.SetWarp(string(stored)); err != nil {
  310. return "", err
  311. }
  312. }
  313. xraySvc := service.XraySettingService{}
  314. if err := xraySvc.UpdateWarpXraySetting(parsed.Data, parsed.Config); err != nil {
  315. return "", err
  316. }
  317. if license, ok := warpDataMap["license_key"]; ok && len(license) >= 26 {
  318. if _, licErr := s.SetWarpLicense(license); licErr != nil {
  319. // The key is already preserved in storage above; surface the
  320. // remote failure instead of silently downgrading to a free account.
  321. reapplyWarn = licErr
  322. logger.Warning("ChangeWarpIP: failed to re-apply WARP license (key preserved in storage): ", licErr)
  323. }
  324. }
  325. // Return the final stored data (with the preserved license key) instead of
  326. // RegWarp's snapshot, which always carries an empty license.
  327. response := map[string]any{
  328. "data": parsed.Data,
  329. "config": parsed.Config,
  330. }
  331. if reapplyWarn != nil {
  332. response["warning"] = fmt.Sprintf("failed to re-apply WARP license: %v", reapplyWarn)
  333. }
  334. resultJSON, err := json.MarshalIndent(response, "", " ")
  335. if err != nil {
  336. return "", err
  337. }
  338. return string(resultJSON), nil
  339. }
  340. // loadWarpCreds reads the stored warp JSON and ensures access_token + device_id are set.
  341. func (s *WarpService) loadWarpCreds() (map[string]string, error) {
  342. warp, err := s.GetWarp()
  343. if err != nil {
  344. return nil, err
  345. }
  346. var data map[string]string
  347. if err := json.Unmarshal([]byte(warp), &data); err != nil {
  348. return nil, err
  349. }
  350. if data["access_token"] == "" || data["device_id"] == "" {
  351. return nil, common.NewError("warp not registered: missing access_token or device_id")
  352. }
  353. return data, nil
  354. }
  355. // doWarpRequest sends the request and returns the response body on 2xx.
  356. // Non-2xx responses are returned as errors including the status code and body.
  357. func (s *WarpService) doWarpRequest(req *http.Request) ([]byte, error) {
  358. client := s.NewProxiedHTTPClient(15 * time.Second)
  359. resp, err := client.Do(req)
  360. if err != nil {
  361. return nil, err
  362. }
  363. defer resp.Body.Close()
  364. body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
  365. if err != nil {
  366. return nil, err
  367. }
  368. if resp.StatusCode < 200 || resp.StatusCode >= 300 {
  369. if msg := parseWarpError(body); msg != "" {
  370. return nil, common.NewError(msg)
  371. }
  372. return nil, common.NewErrorf("warp api %s %s returned status %d: %s",
  373. req.Method, req.URL.Path, resp.StatusCode, string(body))
  374. }
  375. return body, nil
  376. }
  377. func parseWarpError(body []byte) string {
  378. var env struct {
  379. Errors []struct {
  380. Message string `json:"message"`
  381. } `json:"errors"`
  382. }
  383. if err := json.Unmarshal(body, &env); err != nil {
  384. return ""
  385. }
  386. if len(env.Errors) == 0 || env.Errors[0].Message == "" {
  387. return ""
  388. }
  389. return env.Errors[0].Message
  390. }