Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
stages:
- verify
- docker

variables:
GOTOOLCHAIN: auto
GOFLAGS: "-mod=readonly"

go-verify:
image: golang:1.26.3
stage: verify
script:
- test -z "$(gofmt -l backend/xray/api/account.go backend/xray/api/wireguard_account.go backend/xray/api/wireguard_key.go backend/xray/api/wireguard_account_test.go backend/xray/config.go backend/xray/user.go common/service.pb.go)"
- go mod download
- apt-get update
- apt-get install -y --no-install-recommends curl openssl
- curl -L https://github.com/PasarGuard/scripts/raw/main/install_core.sh | bash -s -- --tag v26.7.11 --os linux --arch 64
- make generate_server_cert
- make generate_client_cert
- go vet ./...
- go test ./... -p 1

go-race-wireguard:
image: golang:1.26.3
stage: verify
script:
- apt-get update
- apt-get install -y --no-install-recommends gcc
- CGO_ENABLED=1 go test -race ./backend/wireguard

docker-build:
image: docker:27.5.1
stage: docker
services:
- name: docker:27.5.1-dind
command: ["--tls=false"]
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
script:
- docker build --build-arg XRAY_TAG=v26.7.11 --tag "$CI_REGISTRY_IMAGE/node:$CI_COMMIT_SHA" .
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS builder

ARG TARGETOS
ARG TARGETARCH
ARG XRAY_TAG=v26.7.11

RUN apk update && apk add --no-cache make

Expand All @@ -12,7 +13,7 @@ RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} make NAME=main build
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make install_xray
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} XRAY_TAG=${XRAY_TAG} make install_xray

FROM alpine:latest

Expand Down
3 changes: 2 additions & 1 deletion Dockerfile.xray
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS builder

ARG TARGETOS
ARG TARGETARCH
ARG XRAY_TAG=v26.7.11

RUN apk update && apk add --no-cache make

Expand All @@ -12,7 +13,7 @@ RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} make NAME=main build
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make install_xray
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} XRAY_TAG=${XRAY_TAG} make install_xray

FROM alpine:latest

Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ MAIN = ./cmd/node
PREFIX ?= $(shell go env GOPATH)
XRAY_OS ?=
XRAY_ARCH ?=
XRAY_TAG ?= v26.7.11
# Map GOARCH to installer arch flag (pure make vars to avoid shell leakage)
XRAY_ARCH_MAP_amd64 = 64
XRAY_ARCH_MAP_386 = 32
Expand Down Expand Up @@ -125,9 +126,9 @@ ifeq ($(UNAME_S),Linux)
if [ "$(DISTRO)" = "debian" ] || [ "$(DISTRO)" = "ubuntu" ] || \
[ "$(DISTRO)" = "centos" ] || [ "$(DISTRO)" = "rhel" ] || [ "$(DISTRO)" = "fedora" ] || \
[ "$(DISTRO)" = "arch" ]; then \
curl -L https://github.com/PasarGuard/scripts/raw/main/install_core.sh | sudo bash -s -- $(XRAY_INSTALL_ARGS); \
curl -L https://github.com/PasarGuard/scripts/raw/main/install_core.sh | sudo bash -s -- --tag $(XRAY_TAG) $(XRAY_INSTALL_ARGS); \
else \
curl -L https://github.com/PasarGuard/scripts/raw/main/install_core.sh | bash -s -- $(XRAY_INSTALL_ARGS); \
curl -L https://github.com/PasarGuard/scripts/raw/main/install_core.sh | bash -s -- --tag $(XRAY_TAG) $(XRAY_INSTALL_ARGS); \
fi

else
Expand Down
1 change: 1 addition & 0 deletions backend/xray/api/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,5 @@ type ProxySettings struct {
Shadowsocks *ShadowsocksTcpAccount
Shadowsocks2022 *ShadowsocksAccount
Hysteria *HysteriaAccount
Wireguard *WireguardAccount
}
61 changes: 61 additions & 0 deletions backend/xray/api/wireguard_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package api

import (
"fmt"

"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/wireguard"

"github.com/pasarguard/node/common"
)

// WireguardAccount is an Xray UserManager WireGuard peer.
type WireguardAccount struct {
BaseAccount
PublicKey string `json:"publicKey"`
PreSharedKey string `json:"preSharedKey,omitempty"`
AllowedIPs []string `json:"allowedIPs"`
}

func (wa *WireguardAccount) Message() (*serial.TypedMessage, error) {
return ToTypedMessage(&wireguard.PeerConfig{
PublicKey: wa.PublicKey,
PreSharedKey: wa.PreSharedKey,
AllowedIps: wa.AllowedIPs,
})
}

func NewWireguardAccount(user *common.User) (*WireguardAccount, error) {
wg := user.GetProxies().GetWireguard()
if wg == nil || wg.GetPublicKey() == "" {
return nil, fmt.Errorf("wireguard public_key is required")
}

pubHex, err := WireguardKeyToHex(wg.GetPublicKey())
if err != nil {
return nil, fmt.Errorf("wireguard public_key: %w", err)
}

pskHex := ""
if psk := wg.GetPreSharedKey(); psk != "" {
pskHex, err = WireguardKeyToHex(psk)
if err != nil {
return nil, fmt.Errorf("wireguard pre_shared_key: %w", err)
}
}

allowed := wg.GetPeerIps()
if len(allowed) == 0 {
return nil, fmt.Errorf("wireguard peer_ips is required")
}

return &WireguardAccount{
BaseAccount: BaseAccount{
Email: user.GetEmail(),
Level: 0,
},
PublicKey: pubHex,
PreSharedKey: pskHex,
AllowedIPs: allowed,
}, nil
}
69 changes: 69 additions & 0 deletions backend/xray/api/wireguard_account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package api

import (
"encoding/base64"
"strings"
"testing"

"github.com/pasarguard/node/common"
"github.com/xtls/xray-core/proxy/wireguard"
"google.golang.org/protobuf/proto"
)

func TestWireguardKeyToHex(t *testing.T) {
raw := strings.Repeat("\x11", 32)
want := strings.Repeat("11", 32)
encoded := base64.StdEncoding.EncodeToString([]byte(raw))

for _, tc := range []struct {
name string
key string
want string
}{
{"base64", encoded, want},
{"hex", want, want},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := WireguardKeyToHex(tc.key)
if err != nil || got != tc.want {
t.Fatalf("WireguardKeyToHex() = %q, %v; want %q, nil", got, err, tc.want)
}
})
}
}

func TestWireguardKeyToHexRejectsInvalidKeys(t *testing.T) {
for _, key := range []string{"", "not-a-key", base64.StdEncoding.EncodeToString([]byte("short")), strings.Repeat("z", 64)} {
if got, err := WireguardKeyToHex(key); err == nil || got != "" {
t.Fatalf("WireguardKeyToHex(%q) = %q, %v; want empty result and error", key, got, err)
}
}
}

func TestNewWireguardAccountNormalizesPSKAndMessage(t *testing.T) {
publicKey := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("P", 32)))
psk := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("S", 32)))
account, err := NewWireguardAccount(&common.User{
Email: "user@example.test",
Proxies: &common.Proxy{Wireguard: &common.Wireguard{
PublicKey: publicKey, PreSharedKey: psk, PeerIps: []string{"10.0.0.2/32"},
}},
})
if err != nil {
t.Fatal(err)
}
if account.GetEmail() != "user@example.test" || account.PreSharedKey != strings.Repeat("53", 32) {
t.Fatalf("account = %#v", account)
}
message, err := account.Message()
if err != nil {
t.Fatal(err)
}
peer := new(wireguard.PeerConfig)
if err := proto.Unmarshal(message.Value, peer); err != nil {
t.Fatal(err)
}
if peer.PreSharedKey != account.PreSharedKey {
t.Fatalf("peer message = %#v, want PSK %q", peer, account.PreSharedKey)
}
}
31 changes: 31 additions & 0 deletions backend/xray/api/wireguard_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package api

import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)

// WireguardKeyToHex normalizes a WireGuard key from base64 (panel) or hex (Xray API) to hex.
func WireguardKeyToHex(key string) (string, error) {
key = strings.TrimSpace(key)
if key == "" {
return "", fmt.Errorf("empty wireguard key")
}

if len(key) == 64 {
if _, err := hex.DecodeString(key); err == nil {
return key, nil
}
}

raw, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return "", fmt.Errorf("invalid wireguard key encoding: %w", err)
}
if len(raw) != 32 {
return "", fmt.Errorf("invalid wireguard key length: %d", len(raw))
}
return hex.EncodeToString(raw), nil
}
47 changes: 45 additions & 2 deletions backend/xray/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
Trojan = "trojan"
Shadowsocks = "shadowsocks"
Hysteria = "hysteria"
Wireguard = "wireguard"
)

type Config struct {
Expand Down Expand Up @@ -98,7 +99,11 @@ func (c *Config) buildInboundUpdates(users []*common.User) (map[string]*Inbound,
if isActive {
update.accounts = append(update.accounts, account)
} else {
update.removeEmailSet[userEmail] = struct{}{}
removeEmail := userEmail
if inbound.Protocol == Wireguard && settings.Wireguard != nil {
removeEmail = settings.Wireguard.GetEmail()
}
update.removeEmailSet[removeEmail] = struct{}{}
}
}
}
Expand Down Expand Up @@ -199,6 +204,21 @@ func (i *Inbound) syncUsers(users []*common.User) {
i.clients[user.GetEmail()] = api.NewHysteriaAccount(user)
}
}

case Wireguard:
for _, user := range users {
if user.GetProxies().GetWireguard() == nil {
continue
}
if slices.Contains(user.Inbounds, i.Tag) {
account, err := api.NewWireguardAccount(user)
if err != nil {
log.Println("error for user", user.GetEmail(), ":", err)
continue
}
i.clients[account.GetEmail()] = account
}
}
}
}

Expand Down Expand Up @@ -235,6 +255,9 @@ func (i *Inbound) updateUser(account api.Account) {

case *api.HysteriaAccount:
i.clients[email] = a

case *api.WireguardAccount:
i.clients[email] = a
}
}

Expand Down Expand Up @@ -291,6 +314,13 @@ func (i *Inbound) updateUsers(accounts []api.Account, removeEmails []string) {
i.clients[account.GetEmail()] = a
}
}

case Wireguard:
for _, account := range accounts {
if a, ok := account.(*api.WireguardAccount); ok {
i.clients[account.GetEmail()] = a
}
}
}

for _, email := range removeEmails {
Expand Down Expand Up @@ -326,7 +356,11 @@ func (c *Config) ToBytes() ([]byte, error) {
}

if len(i.clients) == 0 {
i.Settings["clients"] = []any{}
if i.Protocol == Wireguard {
i.Settings["peers"] = []any{}
} else {
i.Settings["clients"] = []any{}
}
continue
}

Expand Down Expand Up @@ -386,6 +420,15 @@ func (c *Config) ToBytes() ([]byte, error) {
}
}
i.Settings["clients"] = clients

case Wireguard:
peers := make([]*api.WireguardAccount, 0, len(i.clients))
for _, account := range i.clients {
if wgAccount, ok := account.(*api.WireguardAccount); ok {
peers = append(peers, wgAccount)
}
}
i.Settings["peers"] = peers
}
}

Expand Down
Loading