Files
3x-ui/internal/util/wireguard/wireguard.go
T
MHSanaei 9c8cd08f90 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.
2026-06-28 00:44:38 +02:00

100 lines
2.6 KiB
Go

package wireguard
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"strings"
"golang.org/x/crypto/curve25519"
)
// GenerateWireguardKeypair generates a base64 encoded private and public key pair for Wireguard.
func GenerateWireguardKeypair() (privateKey string, publicKey string, err error) {
var priv [32]byte
if _, err := rand.Read(priv[:]); err != nil {
return "", "", err
}
priv[0] &= 248
priv[31] &= 127
priv[31] |= 64
var pub [32]byte
curve25519.ScalarBaseMult(&pub, &priv)
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
}