@@ -481,6 +481,252 @@ func TestBatchImportNodesAutoAssignSkipsUsedInboundPorts(t *testing.T) {
481481 }
482482}
483483
484+ func TestCreateNodeWireGuardPersistsConfig (t * testing.T ) {
485+ gin .SetMode (gin .TestMode )
486+ handler := newTestHandler (t , func (proxyAddr , username , password string ) (* services.IPInfo , error ) {
487+ return nil , fmt .Errorf ("not used" )
488+ })
489+
490+ payload := map [string ]interface {}{
491+ "name" : "warp-node" ,
492+ "remark" : "cf" ,
493+ "type" : "wireguard" ,
494+ "enabled" : true ,
495+ "config" : `{
496+ "server":"engage.cloudflareclient.com",
497+ "server_port":2408,
498+ "local_address":["172.16.0.2/32","2606:4700:110:8765::2/128"],
499+ "private_key":"private-key",
500+ "peer_public_key":"peer-public-key",
501+ "allowed_ips":["0.0.0.0/0","::/0"],
502+ "reserved":[162,104,222],
503+ "detour":"warp-selector",
504+ "domain_resolver":"local",
505+ "domain_resolver_strategy":"prefer_ipv4",
506+ "udp_fragment":true,
507+ "connect_timeout":"5s"
508+ }` ,
509+ }
510+ body , _ := json .Marshal (payload )
511+
512+ rec := httptest .NewRecorder ()
513+ ctx , _ := gin .CreateTestContext (rec )
514+ req , _ := http .NewRequest (http .MethodPost , "/api/nodes" , bytes .NewReader (body ))
515+ req .Header .Set ("Content-Type" , "application/json" )
516+ ctx .Request = req
517+
518+ handler .CreateNode (ctx )
519+ if rec .Code != http .StatusCreated {
520+ t .Fatalf ("unexpected status %d body=%s" , rec .Code , rec .Body .String ())
521+ }
522+
523+ var (
524+ nodeType string
525+ configJSON string
526+ )
527+ if err := handler .db .QueryRow (
528+ "SELECT type, config FROM proxy_nodes WHERE name = ?" ,
529+ "warp-node" ,
530+ ).Scan (& nodeType , & configJSON ); err != nil {
531+ t .Fatalf ("query node: %v" , err )
532+ }
533+ if nodeType != "wireguard" {
534+ t .Fatalf ("expected wireguard type, got %q" , nodeType )
535+ }
536+
537+ var cfg models.WireGuardConfig
538+ if err := json .Unmarshal ([]byte (configJSON ), & cfg ); err != nil {
539+ t .Fatalf ("unmarshal config: %v" , err )
540+ }
541+ if cfg .Server != "engage.cloudflareclient.com" || cfg .ServerPort != 2408 {
542+ t .Fatalf ("unexpected endpoint: %+v" , cfg )
543+ }
544+ if len (cfg .LocalAddress ) != 2 || cfg .LocalAddress [1 ] != "2606:4700:110:8765::2/128" {
545+ t .Fatalf ("unexpected local_address: %+v" , cfg .LocalAddress )
546+ }
547+ if cfg .PeerPublicKey != "peer-public-key" || cfg .Detour != "warp-selector" {
548+ t .Fatalf ("unexpected wireguard config: %+v" , cfg )
549+ }
550+ if cfg .UDPFragment == nil || ! * cfg .UDPFragment {
551+ t .Fatalf ("expected udp_fragment=true, got %+v" , cfg .UDPFragment )
552+ }
553+ }
554+
555+ func TestParseShareLinkWireGuard (t * testing.T ) {
556+ gin .SetMode (gin .TestMode )
557+ handler := newTestHandler (t , func (proxyAddr , username , password string ) (* services.IPInfo , error ) {
558+ return nil , fmt .Errorf ("not used" )
559+ })
560+
561+ link := "wireguard://private-key@engage.cloudflareclient.com:2408?publickey=peer-public-key&ip=172.16.0.2/32&ipv6=2606:4700:110:8765::2/128&allowedips=0.0.0.0/0,::/0&reserved=162,104,222&mtu=1280&workers=2&detour=warp-selector&domain_resolver=local&domain_resolver_strategy=prefer_ipv4&udp_fragment=1#WARP"
562+ body := bytes .NewBufferString (fmt .Sprintf (`{"link":%q}` , link ))
563+
564+ rec := httptest .NewRecorder ()
565+ ctx , _ := gin .CreateTestContext (rec )
566+ req , _ := http .NewRequest (http .MethodPost , "/api/parse-link" , body )
567+ req .Header .Set ("Content-Type" , "application/json" )
568+ ctx .Request = req
569+
570+ handler .ParseShareLink (ctx )
571+ if rec .Code != http .StatusOK {
572+ t .Fatalf ("unexpected status %d body=%s" , rec .Code , rec .Body .String ())
573+ }
574+
575+ var resp struct {
576+ Type string `json:"type"`
577+ Name string `json:"name"`
578+ Config string `json:"config"`
579+ }
580+ if err := json .Unmarshal (rec .Body .Bytes (), & resp ); err != nil {
581+ t .Fatalf ("unmarshal response: %v" , err )
582+ }
583+ if resp .Type != "wireguard" || resp .Name != "WARP" {
584+ t .Fatalf ("unexpected response: %+v" , resp )
585+ }
586+
587+ var cfg models.WireGuardConfig
588+ if err := json .Unmarshal ([]byte (resp .Config ), & cfg ); err != nil {
589+ t .Fatalf ("unmarshal config: %v" , err )
590+ }
591+ if cfg .PrivateKey != "private-key" || cfg .Workers != 2 || cfg .MTU != 1280 {
592+ t .Fatalf ("unexpected parsed config: %+v" , cfg )
593+ }
594+ if len (cfg .Reserved ) != 3 || cfg .Reserved [0 ] != 162 {
595+ t .Fatalf ("unexpected reserved bytes: %+v" , cfg .Reserved )
596+ }
597+ }
598+
599+ func TestReplaceNodeWithWireGuardLinkClearsIPAndPreservesNameWhenRequested (t * testing.T ) {
600+ gin .SetMode (gin .TestMode )
601+ handler := newTestHandler (t , func (proxyAddr , username , password string ) (* services.IPInfo , error ) {
602+ return nil , fmt .Errorf ("not used" )
603+ })
604+
605+ nodeID := insertTestNode (t , handler .db )
606+ if _ , err := handler .db .Exec (`
607+ UPDATE proxy_nodes
608+ SET name = 'old-name', node_ip = '8.8.8.8', location = 'Old', country_code = 'US', latency = 99
609+ WHERE id = ?
610+ ` , nodeID ); err != nil {
611+ t .Fatalf ("seed node: %v" , err )
612+ }
613+
614+ link := "wireguard://private-key@engage.cloudflareclient.com:2408?publickey=peer-public-key&ip=172.16.0.2/32&ipv6=2606:4700:110:8765::2/128&allowedips=0.0.0.0/0,::/0&reserved=162,104,222#WARP"
615+ body := bytes .NewBufferString (fmt .Sprintf (`{"link":%q,"update_name":false}` , link ))
616+
617+ rec := httptest .NewRecorder ()
618+ ctx , _ := gin .CreateTestContext (rec )
619+ ctx .Params = gin.Params {gin.Param {Key : "id" , Value : strconv .Itoa (nodeID )}}
620+ req , _ := http .NewRequest (http .MethodPut , "/api/nodes/" + strconv .Itoa (nodeID )+ "/replace" , body )
621+ req .Header .Set ("Content-Type" , "application/json" )
622+ ctx .Request = req
623+
624+ handler .ReplaceNode (ctx )
625+ if rec .Code != http .StatusOK {
626+ t .Fatalf ("unexpected status %d body=%s" , rec .Code , rec .Body .String ())
627+ }
628+
629+ var (
630+ name string
631+ nodeType string
632+ configJSON string
633+ nodeIP string
634+ location string
635+ countryCode string
636+ latency int
637+ )
638+ if err := handler .db .QueryRow (`
639+ SELECT name, type, config, node_ip, location, country_code, latency
640+ FROM proxy_nodes WHERE id = ?
641+ ` , nodeID ).Scan (& name , & nodeType , & configJSON , & nodeIP , & location , & countryCode , & latency ); err != nil {
642+ t .Fatalf ("query node: %v" , err )
643+ }
644+ if name != "old-name" || nodeType != "wireguard" {
645+ t .Fatalf ("unexpected node identity: name=%q type=%q" , name , nodeType )
646+ }
647+ if nodeIP != "" || location != "" || countryCode != "" || latency != 0 {
648+ t .Fatalf ("expected IP status cleared, got ip=%q location=%q country=%q latency=%d" , nodeIP , location , countryCode , latency )
649+ }
650+
651+ var cfg models.WireGuardConfig
652+ if err := json .Unmarshal ([]byte (configJSON ), & cfg ); err != nil {
653+ t .Fatalf ("unmarshal config: %v" , err )
654+ }
655+ if cfg .Server != "engage.cloudflareclient.com" || cfg .PeerPublicKey != "peer-public-key" {
656+ t .Fatalf ("unexpected replaced config: %+v" , cfg )
657+ }
658+ }
659+
660+ func TestBatchImportNodesAcceptsWireGuardYAMLContent (t * testing.T ) {
661+ gin .SetMode (gin .TestMode )
662+ handler := newTestHandler (t , func (proxyAddr , username , password string ) (* services.IPInfo , error ) {
663+ return nil , fmt .Errorf ("not used" )
664+ })
665+
666+ yaml := `
667+ proxies:
668+ - name: "WARP"
669+ type: wireguard
670+ server: engage.cloudflareclient.com
671+ port: 2408
672+ ip: 172.16.0.2
673+ ipv6: "2606:4700:110:8765::2"
674+ private-key: private-key
675+ public-key: peer-public-key
676+ allowed-ips: ["0.0.0.0/0", "::/0"]
677+ reserved: [162, 104, 222]
678+ mtu: 1280
679+ udp: true
680+ dialer-proxy: warp-selector
681+ `
682+ payload := map [string ]interface {}{
683+ "content" : yaml ,
684+ "enabled" : true ,
685+ }
686+ body , _ := json .Marshal (payload )
687+
688+ rec := httptest .NewRecorder ()
689+ ctx , _ := gin .CreateTestContext (rec )
690+ req , _ := http .NewRequest (http .MethodPost , "/api/nodes/batch-import" , bytes .NewReader (body ))
691+ req .Header .Set ("Content-Type" , "application/json" )
692+ ctx .Request = req
693+
694+ handler .BatchImportNodes (ctx )
695+ if rec .Code != http .StatusOK {
696+ t .Fatalf ("unexpected status %d body=%s" , rec .Code , rec .Body .String ())
697+ }
698+
699+ var resp struct {
700+ Success int `json:"success"`
701+ Failed int `json:"failed"`
702+ }
703+ if err := json .Unmarshal (rec .Body .Bytes (), & resp ); err != nil {
704+ t .Fatalf ("unmarshal response: %v" , err )
705+ }
706+ if resp .Success != 1 || resp .Failed != 0 {
707+ t .Fatalf ("unexpected import response: %+v" , resp )
708+ }
709+
710+ var configJSON string
711+ if err := handler .db .QueryRow (
712+ "SELECT config FROM proxy_nodes WHERE name = ?" ,
713+ "WARP" ,
714+ ).Scan (& configJSON ); err != nil {
715+ t .Fatalf ("query node: %v" , err )
716+ }
717+
718+ var cfg models.WireGuardConfig
719+ if err := json .Unmarshal ([]byte (configJSON ), & cfg ); err != nil {
720+ t .Fatalf ("unmarshal config: %v" , err )
721+ }
722+ if cfg .Network != "udp" || cfg .Detour != "warp-selector" {
723+ t .Fatalf ("unexpected batch-import config: %+v" , cfg )
724+ }
725+ if len (cfg .LocalAddress ) != 2 || cfg .LocalAddress [0 ] != "172.16.0.2/32" {
726+ t .Fatalf ("unexpected local_address: %+v" , cfg .LocalAddress )
727+ }
728+ }
729+
484730func TestReorderNodesPreserveInboundPortsKeepsPorts (t * testing.T ) {
485731 gin .SetMode (gin .TestMode )
486732 handler := newTestHandler (t , func (proxyAddr , username , password string ) (* services.IPInfo , error ) {
0 commit comments