mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-11 05:40:59 +00:00
feat(wireguard): multi-client support
WireGuard inbounds now manage per-client peers using xray-core's native WireGuard users (AddUser/RemoveUser). Each client lives in settings.clients (canonical, like every other protocol) and is projected to peers[] only when emitting the xray config, at level 0 so the dispatcher's per-user traffic/online counters work with no extra plumbing. Backend: internal/util/wireguard gains KeyToHex (base64 to hex for the gRPC path), PublicKeyFromPrivate and GenerateWireguardPSK; xray/api.go builds a wireguard account in AddUser with hex keys (RemoveUser already worked); client CRUD generates a keypair and allocates a unique tunnel address per client and never rotates keys on edit; an idempotent migration converts legacy settings.peers into managed clients; WireGuard is included in the raw subscription. Frontend: WireGuard in the add-client modal with keys on the credential tab, client schema, per-client QR/link/.conf, inbound form reduced to server settings; i18n added across 13 locales. Fix: guard the settings[clients] assertion in add/update so a legacy WireGuard inbound stored without a clients key no longer panics.
This commit is contained in:
@@ -3,6 +3,9 @@ package wireguard
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
@@ -22,3 +25,75 @@ func GenerateWireguardKeypair() (privateKey string, publicKey string, err error)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(priv[:]), base64.StdEncoding.EncodeToString(pub[:]), nil
|
||||
}
|
||||
|
||||
// GenerateWireguardPSK generates a base64 encoded 32-byte pre-shared key for Wireguard.
|
||||
func GenerateWireguardPSK() (string, error) {
|
||||
var psk [32]byte
|
||||
if _, err := rand.Read(psk[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(psk[:]), nil
|
||||
}
|
||||
|
||||
// PublicKeyFromPrivate derives the base64 public key for a base64 (or hex) Wireguard private key.
|
||||
func PublicKeyFromPrivate(privateKey string) (string, error) {
|
||||
priv, err := decodeWireguardKey(privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var pub [32]byte
|
||||
curve25519.ScalarBaseMult(&pub, &priv)
|
||||
return base64.StdEncoding.EncodeToString(pub[:]), nil
|
||||
}
|
||||
|
||||
// KeyToHex converts a base64 (or already-hex) 32-byte Wireguard key into the
|
||||
// lowercase hex form xray-core's wireguard proxy expects: its ParseKey uses
|
||||
// hex.DecodeString, and the device IPC layer wants hex for public_key and
|
||||
// preshared_key. An empty input yields an empty result so optional keys pass
|
||||
// through untouched.
|
||||
func KeyToHex(key string) (string, error) {
|
||||
if key == "" {
|
||||
return "", nil
|
||||
}
|
||||
raw, err := decodeWireguardKey(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(raw[:]), nil
|
||||
}
|
||||
|
||||
// decodeWireguardKey accepts a 64-char hex key or a base64 key (standard or
|
||||
// URL-safe alphabet, with or without padding) and returns the raw 32 bytes.
|
||||
func decodeWireguardKey(key string) ([32]byte, error) {
|
||||
var out [32]byte
|
||||
if key == "" {
|
||||
return out, errors.New("wireguard: empty key")
|
||||
}
|
||||
|
||||
if len(key) == 64 {
|
||||
if raw, err := hex.DecodeString(key); err == nil {
|
||||
if len(raw) != 32 {
|
||||
return out, errors.New("wireguard: key must decode to 32 bytes")
|
||||
}
|
||||
copy(out[:], raw)
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
trimmed := strings.TrimRight(key, "=")
|
||||
var raw []byte
|
||||
var err error
|
||||
if strings.ContainsAny(trimmed, "+/") {
|
||||
raw, err = base64.RawStdEncoding.DecodeString(trimmed)
|
||||
} else {
|
||||
raw, err = base64.RawURLEncoding.DecodeString(trimmed)
|
||||
}
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if len(raw) != 32 {
|
||||
return out, errors.New("wireguard: key must decode to 32 bytes")
|
||||
}
|
||||
copy(out[:], raw)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateWireguardKeypairRoundTrip(t *testing.T) {
|
||||
priv, pub, err := GenerateWireguardKeypair()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateWireguardKeypair: %v", err)
|
||||
}
|
||||
for name, key := range map[string]string{"private": priv, "public": pub} {
|
||||
raw, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
t.Fatalf("%s key not base64: %v", name, err)
|
||||
}
|
||||
if len(raw) != 32 {
|
||||
t.Fatalf("%s key decodes to %d bytes, want 32", name, len(raw))
|
||||
}
|
||||
}
|
||||
|
||||
derived, err := PublicKeyFromPrivate(priv)
|
||||
if err != nil {
|
||||
t.Fatalf("PublicKeyFromPrivate: %v", err)
|
||||
}
|
||||
if derived != pub {
|
||||
t.Fatalf("PublicKeyFromPrivate(priv) = %q, want %q", derived, pub)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyFromPrivateKnownVector(t *testing.T) {
|
||||
privHex := "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"
|
||||
wantPubHex := "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"
|
||||
|
||||
privBytes, err := hex.DecodeString(privHex)
|
||||
if err != nil {
|
||||
t.Fatalf("decode priv vector: %v", err)
|
||||
}
|
||||
pubB64, err := PublicKeyFromPrivate(base64.StdEncoding.EncodeToString(privBytes))
|
||||
if err != nil {
|
||||
t.Fatalf("PublicKeyFromPrivate: %v", err)
|
||||
}
|
||||
gotPubHex, err := KeyToHex(pubB64)
|
||||
if err != nil {
|
||||
t.Fatalf("KeyToHex: %v", err)
|
||||
}
|
||||
if gotPubHex != wantPubHex {
|
||||
t.Fatalf("derived public key hex = %q, want %q", gotPubHex, wantPubHex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyToHex(t *testing.T) {
|
||||
low := make([]byte, 32)
|
||||
for i := range low {
|
||||
low[i] = byte(i)
|
||||
}
|
||||
high := make([]byte, 32)
|
||||
for i := range high {
|
||||
high[i] = 0xff
|
||||
}
|
||||
|
||||
for _, raw := range [][]byte{low, high} {
|
||||
wantHex := hex.EncodeToString(raw)
|
||||
std := base64.StdEncoding.EncodeToString(raw)
|
||||
url := base64.URLEncoding.EncodeToString(raw)
|
||||
padless := strings.TrimRight(std, "=")
|
||||
for label, in := range map[string]string{"std": std, "url": url, "padless": padless, "hex": wantHex} {
|
||||
got, err := KeyToHex(in)
|
||||
if err != nil {
|
||||
t.Fatalf("KeyToHex(%s=%q): %v", label, in, err)
|
||||
}
|
||||
if got != wantHex {
|
||||
t.Fatalf("KeyToHex(%s) = %q, want %q", label, got, wantHex)
|
||||
}
|
||||
if back, err := hex.DecodeString(got); err != nil || len(back) != 32 {
|
||||
t.Fatalf("KeyToHex output not a 32-byte hex key: err=%v len=%d", err, len(back))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyToHexEmpty(t *testing.T) {
|
||||
got, err := KeyToHex("")
|
||||
if err != nil {
|
||||
t.Fatalf("KeyToHex(\"\"): %v", err)
|
||||
}
|
||||
if got != "" {
|
||||
t.Fatalf("KeyToHex(\"\") = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyToHexRejectsBadInput(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"not base64": "this is not base64 @@@@",
|
||||
"wrong length": base64.StdEncoding.EncodeToString(make([]byte, 16)),
|
||||
}
|
||||
for name, in := range cases {
|
||||
if _, err := KeyToHex(in); err == nil {
|
||||
t.Fatalf("KeyToHex(%s=%q) expected error, got nil", name, in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWireguardPSK(t *testing.T) {
|
||||
a, err := GenerateWireguardPSK()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateWireguardPSK: %v", err)
|
||||
}
|
||||
b, err := GenerateWireguardPSK()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateWireguardPSK: %v", err)
|
||||
}
|
||||
if a == b {
|
||||
t.Fatalf("two PSKs are identical: %q", a)
|
||||
}
|
||||
raw, err := base64.StdEncoding.DecodeString(a)
|
||||
if err != nil || len(raw) != 32 {
|
||||
t.Fatalf("PSK not a 32-byte base64 key: err=%v len=%d", err, len(raw))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user