|
@@ -14,8 +14,18 @@ import (
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
|
|
+ wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+// A real X25519 pair, so PublicKeyFromPrivate agrees with the stored value.
|
|
|
|
|
+var awgTestPrivateKey, awgTestPublicKey = func() (string, string) {
|
|
|
|
|
+ priv, pub, err := wgutil.GenerateWireguardKeypair()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ panic(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return priv, pub
|
|
|
|
|
+}()
|
|
|
|
|
+
|
|
|
func TestCheckForwardedPortsConflict_EmptySpecNoConflict(t *testing.T) {
|
|
func TestCheckForwardedPortsConflict_EmptySpecNoConflict(t *testing.T) {
|
|
|
setupConflictDB(t)
|
|
setupConflictDB(t)
|
|
|
svc := &InboundService{}
|
|
svc := &InboundService{}
|
|
@@ -134,7 +144,7 @@ func TestNormalizeAmneziaWGSettings_GeneratesFull31Set(t *testing.T) {
|
|
|
setupConflictDB(t)
|
|
setupConflictDB(t)
|
|
|
svc := &InboundService{}
|
|
svc := &InboundService{}
|
|
|
inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51820, Settings: ""}
|
|
inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51820, Settings: ""}
|
|
|
- if err := svc.normalizeAmneziaWGSettings(inbound); err != nil {
|
|
|
|
|
|
|
+ if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
|
|
|
t.Fatalf("normalize empty settings: %v", err)
|
|
t.Fatalf("normalize empty settings: %v", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -188,7 +198,7 @@ func TestNormalizeAmneziaWGSettings_RejectsBad31Values(t *testing.T) {
|
|
|
Port: 51820,
|
|
Port: 51820,
|
|
|
Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24,` + c.snippet + `},"clients":[]}`,
|
|
Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24,` + c.snippet + `},"clients":[]}`,
|
|
|
}
|
|
}
|
|
|
- if err := svc.normalizeAmneziaWGSettings(inbound); err == nil {
|
|
|
|
|
|
|
+ if err := svc.normalizeAmneziaWGSettings(inbound, ""); err == nil {
|
|
|
t.Errorf("%s must be rejected", c.name)
|
|
t.Errorf("%s must be rejected", c.name)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -203,7 +213,7 @@ func TestNormalizeAmneziaWGSettings_CanonicalizesRangeValues(t *testing.T) {
|
|
|
Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24,` +
|
|
Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24,` +
|
|
|
`"rekeyAfterTime":"110 - 140","rejectAfterTime":"190-250","keepaliveTimeout":" "},"clients":[]}`,
|
|
`"rekeyAfterTime":"110 - 140","rejectAfterTime":"190-250","keepaliveTimeout":" "},"clients":[]}`,
|
|
|
}
|
|
}
|
|
|
- if err := svc.normalizeAmneziaWGSettings(inbound); err != nil {
|
|
|
|
|
|
|
+ if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
|
|
|
t.Fatalf("normalize: %v", err)
|
|
t.Fatalf("normalize: %v", err)
|
|
|
}
|
|
}
|
|
|
var parsed amneziawg.InboundSettings
|
|
var parsed amneziawg.InboundSettings
|
|
@@ -245,7 +255,7 @@ func TestNormalizeAmneziaWGSettings_RejectsInjectedClientAllowedIPs(t *testing.T
|
|
|
`"clients":[{"email":"a@x","enable":true,"publicKey":"pk",` +
|
|
`"clients":[{"email":"a@x","enable":true,"publicKey":"pk",` +
|
|
|
`"allowedIPs":["10.8.1.2/32\n[Interface]\nPostUp = touch /tmp/pwned"]}]}`,
|
|
`"allowedIPs":["10.8.1.2/32\n[Interface]\nPostUp = touch /tmp/pwned"]}]}`,
|
|
|
}
|
|
}
|
|
|
- err := svc.normalizeAmneziaWGSettings(inbound)
|
|
|
|
|
|
|
+ err := svc.normalizeAmneziaWGSettings(inbound, "")
|
|
|
if err == nil {
|
|
if err == nil {
|
|
|
t.Fatalf("an allowedIPs entry carrying a config-injection payload must be rejected; settings became:\n%s", inbound.Settings)
|
|
t.Fatalf("an allowedIPs entry carrying a config-injection payload must be rejected; settings became:\n%s", inbound.Settings)
|
|
|
}
|
|
}
|
|
@@ -263,7 +273,7 @@ func TestNormalizeAmneziaWGSettings_CanonicalizesClientAllowedIPs(t *testing.T)
|
|
|
Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24},` +
|
|
Settings: `{"server":{"privateKey":"x","publicKey":"y","subnetIp":"10.8.1.0","subnetCidr":24},` +
|
|
|
`"clients":[{"email":"a@x","enable":true,"publicKey":"pk","allowedIPs":[" 10.8.1.2 "]}]}`,
|
|
`"clients":[{"email":"a@x","enable":true,"publicKey":"pk","allowedIPs":[" 10.8.1.2 "]}]}`,
|
|
|
}
|
|
}
|
|
|
- if err := svc.normalizeAmneziaWGSettings(inbound); err != nil {
|
|
|
|
|
|
|
+ if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
|
|
|
t.Fatalf("normalize: %v", err)
|
|
t.Fatalf("normalize: %v", err)
|
|
|
}
|
|
}
|
|
|
var parsed amneziawg.InboundSettings
|
|
var parsed amneziawg.InboundSettings
|
|
@@ -375,7 +385,7 @@ func TestNormalizeAmneziaWGSettingsKeepsClearedDNS(t *testing.T) {
|
|
|
t.Fatalf("marshal settings: %v", err)
|
|
t.Fatalf("marshal settings: %v", err)
|
|
|
}
|
|
}
|
|
|
inbound := &model.Inbound{Protocol: model.AmneziaWG, Settings: string(bs)}
|
|
inbound := &model.Inbound{Protocol: model.AmneziaWG, Settings: string(bs)}
|
|
|
- if err := (&InboundService{}).normalizeAmneziaWGSettings(inbound); err != nil {
|
|
|
|
|
|
|
+ if err := (&InboundService{}).normalizeAmneziaWGSettings(inbound, ""); err != nil {
|
|
|
t.Fatalf("normalizeAmneziaWGSettings: %v", err)
|
|
t.Fatalf("normalizeAmneziaWGSettings: %v", err)
|
|
|
}
|
|
}
|
|
|
for _, key := range []string{`"primaryDns"`, `"secondaryDns"`} {
|
|
for _, key := range []string{`"primaryDns"`, `"secondaryDns"`} {
|
|
@@ -384,3 +394,63 @@ func TestNormalizeAmneziaWGSettingsKeepsClearedDNS(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// An enabled peer with no address is skipped by InstanceFromInbound, and when it
|
|
|
|
|
+// is the only one the entire inbound never starts, with nothing logged anywhere.
|
|
|
|
|
+func TestNormalizeAmneziaWGSettings_RejectsEmptyClientAllowedIPs(t *testing.T) {
|
|
|
|
|
+ setupConflictDB(t)
|
|
|
|
|
+ svc := &InboundService{}
|
|
|
|
|
+ inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51823, Settings: `{
|
|
|
|
|
+ "server": {"privateKey":"` + awgTestPrivateKey + `","publicKey":"` + awgTestPublicKey + `","subnetIp":"10.8.1.0","subnetCidr":24},
|
|
|
|
|
+ "clients": [{"email":"ghost","enable":true,"publicKey":"` + awgTestPublicKey + `","allowedIPs":[]}]
|
|
|
|
|
+ }`}
|
|
|
|
|
+ err := svc.normalizeAmneziaWGSettings(inbound, "")
|
|
|
|
|
+ if err == nil || !strings.Contains(err.Error(), "allowedIPs is required") {
|
|
|
|
|
+ t.Fatalf("err = %v, want an allowedIPs refusal naming the client", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !strings.Contains(fmt.Sprint(err), "ghost") {
|
|
|
|
|
+ t.Fatalf("error must name the offending client, got %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Omitting the server keys on update means "unchanged": minting a fresh pair
|
|
|
|
|
+// invalidates every client config already distributed, with no warning.
|
|
|
|
|
+func TestNormalizeAmneziaWGSettings_KeepsStoredServerKeysWhenOmitted(t *testing.T) {
|
|
|
|
|
+ setupConflictDB(t)
|
|
|
|
|
+ svc := &InboundService{}
|
|
|
|
|
+ stored := `{"server":{"privateKey":"` + awgTestPrivateKey + `","publicKey":"` + awgTestPublicKey + `","subnetIp":"10.8.1.0","subnetCidr":24}}`
|
|
|
|
|
+
|
|
|
|
|
+ inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51824, Settings: `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24,"randomTrailers":true}}`}
|
|
|
|
|
+ if err := svc.normalizeAmneziaWGSettings(inbound, stored); err != nil {
|
|
|
|
|
+ t.Fatalf("normalize: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ var parsed amneziawg.InboundSettings
|
|
|
|
|
+ if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
|
|
|
|
|
+ t.Fatalf("normalized settings must carry a server block (err=%v): %s", err, inbound.Settings)
|
|
|
|
|
+ }
|
|
|
|
|
+ if parsed.Server.PrivateKey != awgTestPrivateKey || parsed.Server.PublicKey != awgTestPublicKey {
|
|
|
|
|
+ t.Fatalf("server keypair was rotated by an unrelated edit: private=%q public=%q", parsed.Server.PrivateKey, parsed.Server.PublicKey)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A payload carrying only the private half used to pass straight through, so
|
|
|
|
|
+// every rendered client config got "PublicKey = " with nothing after it.
|
|
|
|
|
+func TestNormalizeAmneziaWGSettings_DerivesServerPublicKeyFromPrivate(t *testing.T) {
|
|
|
|
|
+ setupConflictDB(t)
|
|
|
|
|
+ svc := &InboundService{}
|
|
|
|
|
+ inbound := &model.Inbound{Protocol: model.AmneziaWG, Port: 51825, Settings: `{"server":{"privateKey":"` + awgTestPrivateKey + `","subnetIp":"10.8.1.0","subnetCidr":24}}`}
|
|
|
|
|
+ if err := svc.normalizeAmneziaWGSettings(inbound, ""); err != nil {
|
|
|
|
|
+ t.Fatalf("normalize: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ var parsed amneziawg.InboundSettings
|
|
|
|
|
+ if err := json.Unmarshal([]byte(inbound.Settings), &parsed); err != nil || parsed.Server == nil {
|
|
|
|
|
+ t.Fatalf("normalized settings must carry a server block (err=%v): %s", err, inbound.Settings)
|
|
|
|
|
+ }
|
|
|
|
|
+ want, err := wgutil.PublicKeyFromPrivate(awgTestPrivateKey)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("derive expected key: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if parsed.Server.PublicKey != want {
|
|
|
|
|
+ t.Fatalf("server publicKey = %q, want %q derived from the supplied private key", parsed.Server.PublicKey, want)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|