Skip to content

Commit d46603d

Browse files
authored
feat(cli): add WireGuard peer management commands (#47)
* feat(cli): add WireGuard peer management commands * update Dockerfile to use new cmd format
1 parent b478245 commit d46603d

File tree

8 files changed

+308
-88
lines changed

8 files changed

+308
-88
lines changed

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ ENV GOARCH=$TARGETARCH
1818
RUN set -ex \
1919
&& go build -v -tags \
2020
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_clash_api" \
21-
-o /usr/local/bin/sing-box-extensions ./cmd/sing-box-extensions
21+
-o /usr/local/bin/sbx ./cmd/sing-box-extensions
2222

2323
FROM debian:bullseye-slim
2424
RUN set -ex \
2525
&& apt-get update \
2626
&& apt-get install -y ca-certificates tzdata nftables wireguard-tools \
2727
&& rm -rf /var/lib/apt/lists/*
2828

29-
COPY --from=builder /usr/local/bin/sing-box-extensions /usr/local/bin/sing-box-extensions
29+
COPY --from=builder /usr/local/bin/sbx /usr/local/bin/sbx
3030

31-
ENTRYPOINT ["/usr/local/bin/sing-box-extensions", "-d", "/data", "-c", "/config.json", "run"]
31+
ENTRYPOINT ["/usr/local/bin/sbx", "run", "--config", "/config.json"]

cmd/sing-box-extensions/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
build:
2+
go build -v -tags \
3+
"with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_acme" \
4+
-o sbx .
5+
run:
6+
go run -tags=with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_acme . run --config $(config)

cmd/sing-box-extensions/cmd_check.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
package main
22

3-
import "fmt"
3+
import (
4+
"context"
5+
"fmt"
46

5-
type CheckCmd struct {
7+
box "github.com/sagernet/sing-box"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var checkCmd = &cobra.Command{
12+
Use: "check",
13+
Short: "Check configuration",
14+
RunE: func(cmd *cobra.Command, args []string) error {
15+
path, err := cmd.Flags().GetString("config")
16+
if err != nil {
17+
return fmt.Errorf("get config flag: %w", err)
18+
}
19+
if err := check(path); err != nil {
20+
return fmt.Errorf("configuration check failed: %w", err)
21+
}
22+
return nil
23+
},
624
}
725

8-
func check(configFile string) error {
9-
instance, cancel, err := prepare(configFile)
26+
func init() {
27+
rootCmd.AddCommand(checkCmd)
28+
checkCmd.Flags().String("config", "config.json", "Configuration file path")
29+
}
1030

31+
func check(configPath string) error {
32+
options, err := readConfig(configPath)
33+
if err != nil {
34+
return err
35+
}
36+
ctx, cancel := context.WithCancel(globalCtx)
37+
instance, err := box.New(box.Options{
38+
Context: ctx,
39+
Options: options,
40+
})
1141
if err == nil {
12-
_ = instance.Close()
42+
instance.Close()
1343
}
1444
cancel()
1545
return err
1646
}
17-
18-
func (c *CheckCmd) Run() error {
19-
err := check(args.ConfigFile)
20-
if err == nil {
21-
fmt.Println("Configuration is valid")
22-
} else {
23-
return err
24-
}
25-
return nil
26-
}

cmd/sing-box-extensions/cmd_run.go

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,63 @@ package main
33
import (
44
"context"
55
"fmt"
6-
C "github.com/sagernet/sing-box/constant"
7-
"github.com/sagernet/sing-box/experimental/libbox"
8-
"github.com/sagernet/sing-box/log"
9-
E "github.com/sagernet/sing/common/exceptions"
106
"os"
117
"os/signal"
12-
"path/filepath"
138
runtimeDebug "runtime/debug"
149
"syscall"
1510
"time"
11+
12+
box "github.com/sagernet/sing-box"
13+
C "github.com/sagernet/sing-box/constant"
14+
"github.com/sagernet/sing-box/log"
15+
"github.com/sagernet/sing-box/option"
16+
E "github.com/sagernet/sing/common/exceptions"
17+
"github.com/sagernet/sing/common/json"
18+
"github.com/spf13/cobra"
1619
)
1720

18-
type RunCmd struct {
21+
func init() {
22+
rootCmd.AddCommand(runCmd)
23+
runCmd.Flags().String("config", "config.json", "Configuration file path")
1924
}
2025

21-
func prepare(configFile string) (*libbox.BoxService, context.CancelFunc, error) {
22-
ctx, cancel := context.WithCancel(newBaseContext())
23-
24-
var config string
26+
var runCmd = &cobra.Command{
27+
Use: "run",
28+
Short: "Run service",
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
path, err := cmd.Flags().GetString("config")
31+
if err != nil {
32+
return fmt.Errorf("get config flag: %w", err)
33+
}
34+
return run(path)
35+
},
36+
}
2537

26-
if data, err := os.ReadFile(configFile); err != nil {
27-
return nil, cancel, fmt.Errorf("reading config file: %w", err)
28-
} else {
29-
config = string(data)
38+
func readConfig(path string) (option.Options, error) {
39+
content, err := os.ReadFile(path)
40+
if err != nil {
41+
return option.Options{}, fmt.Errorf("reading config file: %w", err)
3042
}
31-
if err := libbox.Setup(&libbox.SetupOptions{
32-
BasePath: args.DataDir,
33-
WorkingPath: filepath.Join(args.DataDir, "data"),
34-
TempPath: filepath.Join(args.DataDir, "temp"),
35-
}); err != nil {
36-
return nil, cancel, fmt.Errorf("setup libbox: %w", err)
43+
options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, content)
44+
if err != nil {
45+
return option.Options{}, fmt.Errorf("parsing config file: %w", err)
3746
}
38-
39-
instance, err := libbox.NewServiceWithContext(ctx, config, nil)
40-
return instance, cancel, err
47+
return options, nil
4148
}
4249

43-
func create(config string) (*libbox.BoxService, context.CancelFunc, error) {
44-
instance, cancel, err := prepare(config)
50+
func create(configPath string) (*box.Box, context.CancelFunc, error) {
51+
options, err := readConfig(configPath)
4552
if err != nil {
46-
cancel()
47-
return nil, nil, fmt.Errorf("setup libbox: %w", err)
53+
return nil, nil, fmt.Errorf("read config: %w", err)
54+
}
55+
56+
ctx, cancel := context.WithCancel(globalCtx)
57+
instance, err := box.New(box.Options{
58+
Context: ctx,
59+
Options: options,
60+
})
61+
if err != nil {
62+
return nil, nil, fmt.Errorf("create service: %w", err)
4863
}
4964

5065
osSignals := make(chan os.Signal, 1)
@@ -80,20 +95,20 @@ func closeMonitor(ctx context.Context) {
8095
log.Fatal("sing-box did not close!")
8196
}
8297

83-
func (c *RunCmd) Run() error {
98+
func run(configPath string) error {
8499
osSignals := make(chan os.Signal, 1)
85100
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
86101
defer signal.Stop(osSignals)
87102
for {
88-
instance, cancel, err := create(args.ConfigFile)
103+
instance, cancel, err := create(configPath)
89104
if err != nil {
90105
return err
91106
}
92107
runtimeDebug.FreeOSMemory()
93108
for {
94109
osSignal := <-osSignals
95110
if osSignal == syscall.SIGHUP {
96-
err = check(args.ConfigFile)
111+
err = check(configPath)
97112
if err != nil {
98113
log.Error(E.Cause(err, "reload service"))
99114
continue
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/base64"
7+
"encoding/hex"
8+
"fmt"
9+
"net"
10+
"strings"
11+
12+
"github.com/spf13/cobra"
13+
)
14+
15+
const socket = "/var/run/wireguard/wg0.sock"
16+
17+
func init() {
18+
rootCmd.AddCommand(wgPeersCmd)
19+
20+
addPeerCmd.Flags().String("public-key", "", "Public key of the peer to add")
21+
addPeerCmd.Flags().StringSlice("allowed-ips", []string{"0.0.0.0/0"}, "Allowed IPs for the peer")
22+
addPeerCmd.Flags().String("socket", socket, "WireGuard socket path")
23+
addPeerCmd.MarkFlagRequired("public-key")
24+
25+
removePeerCmd.Flags().String("public-key", "", "Public key of the peer to remove")
26+
removePeerCmd.Flags().String("socket", socket, "WireGuard socket path")
27+
removePeerCmd.MarkFlagRequired("public-key")
28+
29+
listPeersCmd.Flags().String("socket", socket, "WireGuard socket path")
30+
31+
wgPeersCmd.AddCommand(addPeerCmd)
32+
wgPeersCmd.AddCommand(removePeerCmd)
33+
wgPeersCmd.AddCommand(listPeersCmd)
34+
}
35+
36+
var wgPeersCmd = &cobra.Command{
37+
Use: "wg-peers",
38+
Short: "Manage WireGuard peers",
39+
Long: `Add or remove WireGuard peers`,
40+
}
41+
42+
var addPeerCmd = &cobra.Command{
43+
Use: "add",
44+
Short: "Add a WireGuard peer",
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
key, _ := cmd.Flags().GetString("public-key")
47+
allowedIPs, _ := cmd.Flags().GetStringSlice("allowed-ips")
48+
sock, _ := cmd.Flags().GetString("socket")
49+
err := addPeer(key, allowedIPs, sock)
50+
if err != nil {
51+
return fmt.Errorf("adding peer: %w", err)
52+
}
53+
fmt.Println("Peer added")
54+
return nil
55+
},
56+
}
57+
58+
func addPeer(publicKey string, allowedIPs []string, wgIfc string) error {
59+
publicKeyHex, err := keyToHex(publicKey)
60+
if err != nil {
61+
return err
62+
}
63+
64+
req := "public_key=" + publicKeyHex + "\n"
65+
for _, ip := range allowedIPs {
66+
_, _, err := net.ParseCIDR(ip)
67+
if err != nil {
68+
return fmt.Errorf("parsing allowed IP %s: %w", ip, err)
69+
}
70+
req += "allowed_ip=" + ip + "\n"
71+
}
72+
_, err = sendReq("set=1\n"+req, wgIfc)
73+
if err != nil {
74+
return fmt.Errorf("sending request: %w", err)
75+
}
76+
return nil
77+
}
78+
79+
var removePeerCmd = &cobra.Command{
80+
Use: "remove",
81+
Short: "Remove a WireGuard peer",
82+
RunE: func(cmd *cobra.Command, args []string) error {
83+
key, _ := cmd.Flags().GetString("public-key")
84+
sock, _ := cmd.Flags().GetString("socket")
85+
err := removePeer(key, sock)
86+
if err != nil {
87+
return fmt.Errorf("removing peer: %w", err)
88+
}
89+
fmt.Println("Peer removed")
90+
return nil
91+
},
92+
}
93+
94+
func removePeer(publicKey string, wgIfc string) error {
95+
publicKeyHex, err := keyToHex(publicKey)
96+
if err != nil {
97+
return err
98+
}
99+
100+
req := "set=1\npublic_key=" + publicKeyHex + "\nremove=true\n"
101+
_, err = sendReq(req, wgIfc)
102+
if err != nil {
103+
return fmt.Errorf("sending request: %w", err)
104+
}
105+
return nil
106+
}
107+
108+
var listPeersCmd = &cobra.Command{
109+
Use: "list",
110+
Short: "List WireGuard peers",
111+
Long: `List all WireGuard peers`,
112+
RunE: func(cmd *cobra.Command, args []string) error {
113+
sock, _ := cmd.Flags().GetString("socket")
114+
res, err := sendReq("get=1\n\n", sock)
115+
if err != nil {
116+
return fmt.Errorf("retrieving peers: %w", err)
117+
}
118+
idx := strings.Index(res, "public_key")
119+
if idx == -1 {
120+
fmt.Println("No peers found.")
121+
return nil
122+
}
123+
printPeers(res[idx:])
124+
return nil
125+
},
126+
}
127+
128+
func printPeers(s string) {
129+
var peers []string
130+
s = strings.TrimSpace(s)
131+
for {
132+
idx := strings.Index(s[1:], "public_key")
133+
if idx == -1 {
134+
peers = append(peers, s)
135+
break
136+
}
137+
idx++ // adjust for the offset
138+
peers = append(peers, s[:idx])
139+
s = s[idx:]
140+
}
141+
fmt.Println("--------------------------------")
142+
fmt.Println(strings.Join(peers, "\n--------------------------------\n"))
143+
fmt.Println()
144+
}
145+
146+
func keyToHex(publicKey string) (string, error) {
147+
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKey)
148+
if err != nil {
149+
return "", fmt.Errorf("decoding public key: %w", err)
150+
}
151+
return hex.EncodeToString(publicKeyBytes), nil
152+
}
153+
154+
func sendReq(req, wgSock string) (string, error) {
155+
conn, err := net.Dial("unix", wgSock)
156+
if err != nil {
157+
return "", fmt.Errorf("dialing socket: %w", err)
158+
}
159+
defer conn.Close()
160+
161+
switch {
162+
case req[len(req)-1] != '\n': // has no trailing newline
163+
req += "\n\n"
164+
case req[len(req)-2:] != "\n\n": // has one trailing newline
165+
req += "\n"
166+
}
167+
_, err = conn.Write([]byte(req))
168+
if err != nil {
169+
return "", fmt.Errorf("writing to socket: %w", err)
170+
}
171+
172+
r := bufio.NewReader(conn)
173+
out := make([]byte, 0, 1024)
174+
for {
175+
b, err := r.ReadBytes('\n')
176+
if err != nil {
177+
return "", fmt.Errorf("reading from socket: %w", err)
178+
}
179+
if bytes.Equal(b, []byte{'\n'}) {
180+
return string(out), nil
181+
}
182+
out = append(out, b...)
183+
}
184+
}

0 commit comments

Comments
 (0)