1
0

inbound_amneziawg_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. package service
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. "testing"
  8. "github.com/op/go-logging"
  9. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  10. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  14. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  15. )
  16. // A real X25519 pair, so PublicKeyFromPrivate agrees with the stored value.
  17. var awgTestPrivateKey, awgTestPublicKey = func() (string, string) {
  18. priv, pub, err := wgutil.GenerateWireguardKeypair()
  19. if err != nil {
  20. panic(err)
  21. }
  22. return priv, pub
  23. }()
  24. func TestCheckForwardedPortsConflict_EmptySpecNoConflict(t *testing.T) {
  25. setupConflictDB(t)
  26. svc := &InboundService{}
  27. ctx, err := svc.loadPortConflictContext(database.GetDB())
  28. if err != nil {
  29. t.Fatalf("loadPortConflictContext: %v", err)
  30. }
  31. if hit := svc.checkForwardedPortsConflict(ctx, ""); hit != "" {
  32. t.Fatalf("an empty spec must never conflict; got hit=%q", hit)
  33. }
  34. }
  35. func TestCheckForwardedPortsConflict_CollidesWithPanelPort(t *testing.T) {
  36. setupConflictDB(t)
  37. svc := &InboundService{}
  38. ctx, err := svc.loadPortConflictContext(database.GetDB())
  39. if err != nil {
  40. t.Fatalf("loadPortConflictContext: %v", err)
  41. }
  42. // getString falls back to defaultValueMap's "webPort": "2053" on a fresh
  43. // DB with no explicit setting row.
  44. hit := svc.checkForwardedPortsConflict(ctx, "2053")
  45. if !strings.Contains(hit, "panel") {
  46. t.Fatalf("expected a collision naming the panel's own port, got %q", hit)
  47. }
  48. }
  49. func TestCheckForwardedPortsConflict_CollidesWithEnabledInboundPort(t *testing.T) {
  50. setupConflictDB(t)
  51. seedInboundConflict(t, "vless-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`)
  52. svc := &InboundService{}
  53. ctx, err := svc.loadPortConflictContext(database.GetDB())
  54. if err != nil {
  55. t.Fatalf("loadPortConflictContext: %v", err)
  56. }
  57. hit := svc.checkForwardedPortsConflict(ctx, "8075-8085")
  58. if !strings.Contains(hit, "vless-8080") {
  59. t.Fatalf("expected a collision naming the colliding inbound, got %q", hit)
  60. }
  61. }
  62. func TestCheckForwardedPortsConflict_IgnoresDisabledInboundPort(t *testing.T) {
  63. setupConflictDB(t)
  64. disabled := &model.Inbound{Tag: "vless-8080-off", Enable: false, Listen: "0.0.0.0", Port: 8080, Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`}
  65. if err := database.GetDB().Create(disabled).Error; err != nil {
  66. t.Fatalf("seed disabled inbound: %v", err)
  67. }
  68. svc := &InboundService{}
  69. ctx, err := svc.loadPortConflictContext(database.GetDB())
  70. if err != nil {
  71. t.Fatalf("loadPortConflictContext: %v", err)
  72. }
  73. if hit := svc.checkForwardedPortsConflict(ctx, "8080"); hit != "" {
  74. t.Fatalf("a disabled inbound's port must not be reserved; got hit=%q", hit)
  75. }
  76. }
  77. func TestCheckForwardedPortsConflict_NoCollisionWhenPortsDontOverlap(t *testing.T) {
  78. setupConflictDB(t)
  79. seedInboundConflict(t, "vless-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`)
  80. svc := &InboundService{}
  81. ctx, err := svc.loadPortConflictContext(database.GetDB())
  82. if err != nil {
  83. t.Fatalf("loadPortConflictContext: %v", err)
  84. }
  85. if hit := svc.checkForwardedPortsConflict(ctx, "9075-9085"); hit != "" {
  86. t.Fatalf("unrelated ports must not conflict; got hit=%q", hit)
  87. }
  88. }
  89. // A port-forward spec matching a port used only by an inbound hosted on a
  90. // DIFFERENT node must not conflict: that inbound's DNAT/listen socket lives
  91. // on the node's own host, never on this panel's, so there is nothing here
  92. // for the forwarded port to actually collide with. Mirrors
  93. // TestCheckPortConflict_NodeScope's own reasoning for the general port-
  94. // conflict check.
  95. func TestCheckForwardedPortsConflict_IgnoresPortOnDifferentNode(t *testing.T) {
  96. setupConflictDB(t)
  97. seedInboundConflictNode(t, "node1-8080", "0.0.0.0", 8080, model.VLESS, `{"network":"tcp"}`, `{}`, new(1))
  98. svc := &InboundService{}
  99. ctx, err := svc.loadPortConflictContext(database.GetDB())
  100. if err != nil {
  101. t.Fatalf("loadPortConflictContext: %v", err)
  102. }
  103. if hit := svc.checkForwardedPortsConflict(ctx, "8080"); hit != "" {
  104. t.Fatalf("a port used only on a different node must not conflict; got hit=%q", hit)
  105. }
  106. }
  107. // inboundAmneziaWGServer is pure (no DB), so it needs neither setupConflictDB
  108. // nor CGO/sqlite -- it can run in any Go environment.
  109. func TestInboundAmneziaWGServer_RedactsPrivateKey(t *testing.T) {
  110. settings := `{"server":{"privateKey":"super-secret","publicKey":"pub","mtu":1420,"headerProtectionKey":"MCPfRGcDGotJ6TcnIdDqsemj2cMIiGHnPUHM5ivXN18="},"clients":[]}`
  111. got := inboundAmneziaWGServer(string(model.AmneziaWG), settings)
  112. if got == nil {
  113. t.Fatal("expected a non-nil server block")
  114. }
  115. if got.PrivateKey != "" {
  116. t.Fatalf("PrivateKey must be redacted, got %q", got.PrivateKey)
  117. }
  118. if got.PublicKey != "pub" || got.MTU != 1420 {
  119. t.Fatalf("non-secret fields must still come through unchanged, got %+v", got)
  120. }
  121. // Unlike the private key, the header-protection key is shared with every
  122. // client config, so the clients page must receive it.
  123. if got.HeaderProtectionKey != "MCPfRGcDGotJ6TcnIdDqsemj2cMIiGHnPUHM5ivXN18=" {
  124. t.Fatalf("HeaderProtectionKey must NOT be redacted, got %q", got.HeaderProtectionKey)
  125. }
  126. }
  127. func TestNormalizeAmneziaWGSettings_GeneratesFull31Set(t *testing.T) {
  128. setupConflictDB(t)
  129. svc := &InboundService{}
  130. inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51820, Settings: ""}
  131. if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
  132. t.Fatalf("normalize empty settings: %v", err)
  133. }
  134. var parsed amneziawg.InboundSettings
  135. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
  136. t.Fatalf("normalized settings must carry a server block (err=%v): %s", err, inbound.Settings)
  137. }
  138. srv := parsed.Server
  139. key, err := base64.StdEncoding.DecodeString(srv.HeaderProtectionKey)
  140. if err != nil || len(key) != 32 {
  141. t.Fatalf("headerProtectionKey = %q, must be base64 of 32 bytes (err=%v)", srv.HeaderProtectionKey, err)
  142. }
  143. for field, v := range map[string]string{
  144. "contentPaddingAddition": srv.ContentPaddingAddition,
  145. "rekeyAfterTime": srv.RekeyAfterTime,
  146. "rekeyTimeout": srv.RekeyTimeout,
  147. "rejectAfterTime": srv.RejectAfterTime,
  148. "keepaliveTimeout": srv.KeepaliveTimeout,
  149. "maxHandshakeAttempts": srv.MaxHandshakeAttempts,
  150. "i1": srv.I1,
  151. } {
  152. if v == "" {
  153. t.Errorf("fresh server block must fill %s", field)
  154. }
  155. }
  156. if !srv.RandomTrailers || !srv.DisableCookies {
  157. t.Errorf("fresh server block defaults RandomTrailers/DisableCookies on, got %v/%v", srv.RandomTrailers, srv.DisableCookies)
  158. }
  159. if srv.I2 != "" || srv.I3 != "" || srv.I4 != "" || srv.I5 != "" {
  160. t.Errorf("generated sets must leave I2-I5 empty, got %q/%q/%q/%q", srv.I2, srv.I3, srv.I4, srv.I5)
  161. }
  162. }
  163. func TestNormalizeAmneziaWGSettings_RejectsBad31Values(t *testing.T) {
  164. setupConflictDB(t)
  165. svc := &InboundService{}
  166. cases := []struct {
  167. name string
  168. snippet string
  169. }{
  170. {"bad headerProtectionKey", `"headerProtectionKey":"short"`},
  171. {"zero rekeyTimeout", `"rekeyTimeout":"0"`},
  172. {"S1 past what an iOS client can receive", `"s1":1553`},
  173. {"rekey overlapping reject", `"rekeyAfterTime":"100-200","rejectAfterTime":"150-300"`},
  174. {"control chars in i2", `"i2":"<r 64>\nPostUp = evil"`},
  175. {"line-wrapped headerProtectionKey", `"headerProtectionKey":"MCPfRGcDGotJ6Tcn\r\nIdDqsemj2cMIiGHnPUHM5ivXN18="`},
  176. }
  177. for _, c := range cases {
  178. inbound := &model.Inbound{
  179. Protocol: model.AmneziaWG,
  180. Port: 51820,
  181. Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24,` + c.snippet + `},"clients":[]}`,
  182. }
  183. if err := svc.normalizeAmneziaWGSettings(inbound, ""); err == nil {
  184. t.Errorf("%s must be rejected", c.name)
  185. }
  186. }
  187. }
  188. func TestNormalizeAmneziaWGSettings_CanonicalizesRangeValues(t *testing.T) {
  189. setupConflictDB(t)
  190. svc := &InboundService{}
  191. inbound := &model.Inbound{
  192. Protocol: model.AmneziaWG,
  193. Port: 51820,
  194. Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24,` +
  195. `"rekeyAfterTime":"110 - 140","rejectAfterTime":"190-250","keepaliveTimeout":" "},"clients":[]}`,
  196. }
  197. if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
  198. t.Fatalf("normalize: %v", err)
  199. }
  200. var parsed amneziawg.InboundSettings
  201. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
  202. t.Fatalf("re-parse normalized settings (err=%v): %s", err, inbound.Settings)
  203. }
  204. if parsed.Server.RekeyAfterTime != "110-140" {
  205. t.Errorf("rekeyAfterTime = %q, want canonical \"110-140\"", parsed.Server.RekeyAfterTime)
  206. }
  207. // A whitespace-only value must collapse to "feature off", not be stored
  208. // as a value the server emitter renders into an invalid blank line.
  209. if parsed.Server.KeepaliveTimeout != "" {
  210. t.Errorf("keepaliveTimeout = %q, want collapsed to empty", parsed.Server.KeepaliveTimeout)
  211. }
  212. }
  213. func TestInboundAmneziaWGServer_NonAmneziaWGReturnsNil(t *testing.T) {
  214. if got := inboundAmneziaWGServer(string(model.VLESS), `{"server":{"privateKey":"x"}}`); got != nil {
  215. t.Fatalf("a non-AmneziaWG protocol must return nil, got %+v", got)
  216. }
  217. }
  218. func TestInboundAmneziaWGServer_MissingServerBlockReturnsNil(t *testing.T) {
  219. if got := inboundAmneziaWGServer(string(model.AmneziaWG), `{"clients":[]}`); got != nil {
  220. t.Fatalf("settings with no server block must return nil, got %+v", got)
  221. }
  222. }
  223. // A newline inside a client's allowedIPs used to reach the rendered .conf,
  224. // where a following "[Interface]\nPostUp = ..." runs as root the moment
  225. // whoever applies that config (client app, or awg-quick directly) does so.
  226. func TestNormalizeAmneziaWGSettings_RejectsInjectedClientAllowedIPs(t *testing.T) {
  227. setupConflictDB(t)
  228. svc := &InboundService{}
  229. inbound := &model.Inbound{
  230. Protocol: model.AmneziaWG,
  231. Port: 51820,
  232. Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24},` +
  233. `"clients":[{"email":"a@x","enable":true,"publicKey":"pk",` +
  234. `"allowedIPs":["10.8.1.2/32\n[Interface]\nPostUp = touch /tmp/pwned"]}]}`,
  235. }
  236. err := svc.normalizeAmneziaWGSettings(inbound, "")
  237. if err == nil {
  238. t.Fatalf("an allowedIPs entry carrying a config-injection payload must be rejected; settings became:\n%s", inbound.Settings)
  239. }
  240. if !strings.Contains(err.Error(), "allowedIPs") {
  241. t.Errorf("error should name the offending field, got %q", err)
  242. }
  243. }
  244. func TestNormalizeAmneziaWGSettings_CanonicalizesClientAllowedIPs(t *testing.T) {
  245. setupConflictDB(t)
  246. svc := &InboundService{}
  247. inbound := &model.Inbound{
  248. Protocol: model.AmneziaWG,
  249. Port: 51820,
  250. Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24},` +
  251. `"clients":[{"email":"a@x","enable":true,"publicKey":"pk","allowedIPs":[" 10.8.1.2 "]}]}`,
  252. }
  253. if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
  254. t.Fatalf("normalize: %v", err)
  255. }
  256. var parsed amneziawg.InboundSettings
  257. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil {
  258. t.Fatalf("re-parse normalized settings: %v", err)
  259. }
  260. if len(parsed.Clients) != 1 || len(parsed.Clients[0].AllowedIPs) != 1 || parsed.Clients[0].AllowedIPs[0] != "10.8.1.2/32" {
  261. t.Fatalf("allowedIPs = %v, want [\"10.8.1.2/32\"]", parsed.Clients)
  262. }
  263. }
  264. func TestGetAmneziaWGLogs_ClampsCountAndFiltersEvents(t *testing.T) {
  265. logger.InitLogger(logging.DEBUG)
  266. logger.Info("amneziawg: started interface awg1 for inbound 1")
  267. logger.Info("xray: unrelated line that must never show up here")
  268. logger.Warning("amneziawgnet: reconcile failed for inbound 2: handshake timeout")
  269. svc := &ServerService{}
  270. logs := svc.GetAmneziaWGLogs("not-a-number", "")
  271. if logs == nil {
  272. t.Fatal("GetAmneziaWGLogs must never return nil")
  273. }
  274. for _, line := range logs.Events {
  275. if !strings.Contains(strings.ToLower(line), "amneziawg") {
  276. t.Fatalf("non-AmneziaWG line leaked into the event list: %q", line)
  277. }
  278. }
  279. if len(logs.Events) < 2 {
  280. t.Fatalf("both AmneziaWG lines should be present, got %v", logs.Events)
  281. }
  282. // count caps the event list, so an operator asking for 1 gets 1.
  283. if one := svc.GetAmneziaWGLogs("1", ""); len(one.Events) != 1 {
  284. t.Fatalf("count=1 must cap the event list, got %d", len(one.Events))
  285. }
  286. // filter narrows further, case-insensitively.
  287. filtered := svc.GetAmneziaWGLogs("100", "RECONCILE")
  288. if len(filtered.Events) != 1 || !strings.Contains(filtered.Events[0], "reconcile") {
  289. t.Fatalf("filter must narrow to the matching line, got %v", filtered.Events)
  290. }
  291. }
  292. func TestCheckForwardedPortsConflict_RejectsSpecOverCap(t *testing.T) {
  293. setupConflictDB(t)
  294. svc := &InboundService{}
  295. ctx, err := svc.loadPortConflictContext(database.GetDB())
  296. if err != nil {
  297. t.Fatalf("loadPortConflictContext: %v", err)
  298. }
  299. spec := fmt.Sprintf("20000-%d", 20000+amneziawg.MaxForwardedPorts)
  300. hit := svc.checkForwardedPortsConflict(ctx, spec)
  301. if !strings.Contains(hit, fmt.Sprintf("%d", amneziawg.MaxForwardedPorts)) {
  302. t.Fatalf("expected a collision naming the %d-port cap, got %q", amneziawg.MaxForwardedPorts, hit)
  303. }
  304. }
  305. // A spec covering exactly MaxForwardedPorts ports is AT the cap, not over
  306. // it, and must be accepted -- ExpandForwardedPorts truncates there by
  307. // design, so a naive len(...) >= cap comparison can't tell the two apart.
  308. func TestCheckForwardedPortsConflict_AcceptsSpecExactlyAtCap(t *testing.T) {
  309. setupConflictDB(t)
  310. svc := &InboundService{}
  311. ctx, err := svc.loadPortConflictContext(database.GetDB())
  312. if err != nil {
  313. t.Fatalf("loadPortConflictContext: %v", err)
  314. }
  315. spec := fmt.Sprintf("20000-%d", 20000+amneziawg.MaxForwardedPorts-1)
  316. if hit := svc.checkForwardedPortsConflict(ctx, spec); hit != "" {
  317. t.Fatalf("a spec covering exactly %d ports must be accepted, got collision %q", amneziawg.MaxForwardedPorts, hit)
  318. }
  319. }
  320. // The SOCKS5 relay port an enabled AmneziaWG inbound gets (SOCKSPortForInbound)
  321. // is a phantom, non-DB-row port -- ctx.inbounds alone can't see it, so
  322. // checkForwardedPortsConflict must check it explicitly.
  323. func TestCheckForwardedPortsConflict_CollidesWithAmneziawgnetSocksPort(t *testing.T) {
  324. setupConflictDB(t)
  325. seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, `{}`)
  326. var awgInbound model.Inbound
  327. if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
  328. t.Fatalf("read seeded row: %v", err)
  329. }
  330. relayPort := amneziawgnet.SOCKSPortForInbound(awgInbound.Id)
  331. svc := &InboundService{}
  332. ctx, err := svc.loadPortConflictContext(database.GetDB())
  333. if err != nil {
  334. t.Fatalf("loadPortConflictContext: %v", err)
  335. }
  336. hit := svc.checkForwardedPortsConflict(ctx, fmt.Sprintf("%d", relayPort))
  337. if !strings.Contains(hit, "SOCKS5") {
  338. t.Fatalf("expected a collision naming the AmneziaWG inbound's SOCKS5 relay port, got %q", hit)
  339. }
  340. }
  341. // A cleared DNS field is meaningful (no DNS line in client configs) and must
  342. // survive the save round-trip instead of resurrecting the frontend defaults.
  343. func TestNormalizeAmneziaWGSettingsKeepsClearedDNS(t *testing.T) {
  344. setupConflictDB(t)
  345. server, err := defaultAmneziaWGServer()
  346. if err != nil {
  347. t.Fatalf("defaultAmneziaWGServer: %v", err)
  348. }
  349. server.PrimaryDNS = ""
  350. server.SecondaryDNS = ""
  351. bs, err := json.Marshal(amneziawg.InboundSettings{Server: server, Clients: []model.Client{}})
  352. if err != nil {
  353. t.Fatalf("marshal settings: %v", err)
  354. }
  355. inbound := &model.Inbound{Protocol: model.AmneziaWG, Settings: string(bs)}
  356. if err := (&InboundService{}).normalizeAmneziaWGSettings(inbound, ""); err != nil {
  357. t.Fatalf("normalizeAmneziaWGSettings: %v", err)
  358. }
  359. for _, key := range []string{`"primaryDns"`, `"secondaryDns"`} {
  360. if !strings.Contains(inbound.Settings, key) {
  361. t.Fatalf("cleared %s dropped from persisted settings:\n%s", key, inbound.Settings)
  362. }
  363. }
  364. }
  365. // An enabled peer with no address is skipped by InstanceFromInbound, and when it
  366. // is the only one the entire inbound never starts, with nothing logged anywhere.
  367. func TestNormalizeAmneziaWGSettings_RejectsEmptyClientAllowedIPs(t *testing.T) {
  368. setupConflictDB(t)
  369. svc := &InboundService{}
  370. inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51823, Settings: `{
  371. "server": {"privateKey":"` + awgTestPrivateKey + `","publicKey":"` + awgTestPublicKey + `","subnetIp":"10.8.1.0","subnetCidr":24},
  372. "clients": [{"email":"ghost","enable":true,"publicKey":"` + awgTestPublicKey + `","allowedIPs":[]}]
  373. }`}
  374. err := svc.normalizeAmneziaWGSettings(inbound, "")
  375. if err == nil || !strings.Contains(err.Error(), "allowedIPs is required") {
  376. t.Fatalf("err = %v, want an allowedIPs refusal naming the client", err)
  377. }
  378. if !strings.Contains(fmt.Sprint(err), "ghost") {
  379. t.Fatalf("error must name the offending client, got %v", err)
  380. }
  381. }
  382. // Omitting the server keys on update means "unchanged": minting a fresh pair
  383. // invalidates every client config already distributed, with no warning.
  384. func TestNormalizeAmneziaWGSettings_KeepsStoredServerKeysWhenOmitted(t *testing.T) {
  385. setupConflictDB(t)
  386. svc := &InboundService{}
  387. stored := `{"server":{"privateKey":"` + awgTestPrivateKey + `","publicKey":"` + awgTestPublicKey + `","subnetIp":"10.8.1.0","subnetCidr":24}}`
  388. inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51824, Settings: `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24,"randomTrailers":true}}`}
  389. if err := svc.normalizeAmneziaWGSettings(inbound, stored); err != nil {
  390. t.Fatalf("normalize: %v", err)
  391. }
  392. var parsed amneziawg.InboundSettings
  393. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
  394. t.Fatalf("normalized settings must carry a server block (err=%v): %s", err, inbound.Settings)
  395. }
  396. if parsed.Server.PrivateKey != awgTestPrivateKey || parsed.Server.PublicKey != awgTestPublicKey {
  397. t.Fatalf("server keypair was rotated by an unrelated edit: private=%q public=%q", parsed.Server.PrivateKey, parsed.Server.PublicKey)
  398. }
  399. }
  400. // A payload carrying only the private half used to pass straight through, so
  401. // every rendered client config got "PublicKey = " with nothing after it.
  402. func TestNormalizeAmneziaWGSettings_DerivesServerPublicKeyFromPrivate(t *testing.T) {
  403. setupConflictDB(t)
  404. svc := &InboundService{}
  405. inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51825, Settings: `{"server":{"privateKey":"` + awgTestPrivateKey + `","subnetIp":"10.8.1.0","subnetCidr":24}}`}
  406. if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
  407. t.Fatalf("normalize: %v", err)
  408. }
  409. var parsed amneziawg.InboundSettings
  410. if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
  411. t.Fatalf("normalized settings must carry a server block (err=%v): %s", err, inbound.Settings)
  412. }
  413. want, err := wgutil.PublicKeyFromPrivate(awgTestPrivateKey)
  414. if err != nil {
  415. t.Fatalf("derive expected key: %v", err)
  416. }
  417. if parsed.Server.PublicKey != want {
  418. t.Fatalf("server publicKey = %q, want %q derived from the supplied private key", parsed.Server.PublicKey, want)
  419. }
  420. }