243 lines
5.8 KiB
Go
243 lines
5.8 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strconv"
|
||
|
|
"text/tabwriter"
|
||
|
|
)
|
||
|
|
|
||
|
|
// clientKeys holds a freshly generated key triple.
|
||
|
|
type clientKeys struct {
|
||
|
|
priv string
|
||
|
|
pub string
|
||
|
|
psk string
|
||
|
|
}
|
||
|
|
|
||
|
|
// generateClientKeys produces the client's private/public/preshared keys.
|
||
|
|
func generateClientKeys() clientKeys {
|
||
|
|
priv, err := output("awg", "genkey")
|
||
|
|
if err != nil {
|
||
|
|
die("awg genkey failed: %v", err)
|
||
|
|
}
|
||
|
|
pub, err := outputWithInput(priv, "awg", "pubkey")
|
||
|
|
if err != nil {
|
||
|
|
die("awg pubkey failed: %v", err)
|
||
|
|
}
|
||
|
|
psk, err := output("awg", "genpsk")
|
||
|
|
if err != nil {
|
||
|
|
die("awg genpsk failed: %v", err)
|
||
|
|
}
|
||
|
|
return clientKeys{priv: priv, pub: pub, psk: psk}
|
||
|
|
}
|
||
|
|
|
||
|
|
// createClientConfig writes <name>.conf plus a scannable <name>.png QR code.
|
||
|
|
func createClientConfig(c *Config, id int64, name, ip string, keys clientKeys) {
|
||
|
|
filename := filepath.Join(clientDir, name+".conf")
|
||
|
|
|
||
|
|
content := fmt.Sprintf(`# Client: %s
|
||
|
|
# ID: %d
|
||
|
|
|
||
|
|
[Interface]
|
||
|
|
PrivateKey = %s
|
||
|
|
Address = %s/32
|
||
|
|
DNS = %s
|
||
|
|
MTU = %s
|
||
|
|
|
||
|
|
Jc = %s
|
||
|
|
Jmin = %s
|
||
|
|
Jmax = %s
|
||
|
|
S1 = %s
|
||
|
|
S2 = %s
|
||
|
|
H1 = %s
|
||
|
|
H2 = %s
|
||
|
|
H3 = %s
|
||
|
|
H4 = %s
|
||
|
|
|
||
|
|
[Peer]
|
||
|
|
PublicKey = %s
|
||
|
|
PresharedKey = %s
|
||
|
|
AllowedIPs = 0.0.0.0/0
|
||
|
|
Endpoint = %s:%s
|
||
|
|
PersistentKeepalive = 25
|
||
|
|
`, name, id,
|
||
|
|
keys.priv, ip, c.DNS, c.MTU,
|
||
|
|
c.Jc, c.Jmin, c.Jmax, c.S1, c.S2, c.H1, c.H2, c.H3, c.H4,
|
||
|
|
c.ServerPub, keys.psk,
|
||
|
|
c.PublicIP, c.Port)
|
||
|
|
|
||
|
|
if err := os.WriteFile(filename, []byte(content), 0600); err != nil {
|
||
|
|
die("Failed to write client config: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
pngPath := filepath.Join(clientDir, name+".png")
|
||
|
|
if err := run("qrencode", "-s", "8", "-o", pngPath, "-r", filename); err != nil {
|
||
|
|
die("qrencode failed: %v", err)
|
||
|
|
}
|
||
|
|
// The QR code encodes the full .conf, private key included; qrencode
|
||
|
|
// creates it with the process umask (typically 0644). Lock it down to
|
||
|
|
// match the .conf it was generated from.
|
||
|
|
if err := os.Chmod(pngPath, 0600); err != nil {
|
||
|
|
die("Failed to secure QR code permissions: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
info("Client config created: %s.conf", name)
|
||
|
|
}
|
||
|
|
|
||
|
|
// createClient generates keys, config, QR, registers the client and hot-adds
|
||
|
|
// the peer to the running interface.
|
||
|
|
func createClient(c *Config, osInfo *OSInfo, args []string) {
|
||
|
|
if len(args) == 0 || args[0] == "" {
|
||
|
|
die("Client name required")
|
||
|
|
}
|
||
|
|
if len(args) > 1 {
|
||
|
|
die("Unexpected argument: %s", args[1])
|
||
|
|
}
|
||
|
|
|
||
|
|
name := sanitize(args[0])
|
||
|
|
if name == "" {
|
||
|
|
die("Sanitized name is empty")
|
||
|
|
}
|
||
|
|
|
||
|
|
clients := loadRegistry()
|
||
|
|
id := getNextID(clients)
|
||
|
|
lastIP := getLastIP(clients)
|
||
|
|
nextIP := incrementIP(lastIP, c.Network)
|
||
|
|
|
||
|
|
keys := generateClientKeys()
|
||
|
|
createClientConfig(c, id, name, nextIP, keys)
|
||
|
|
|
||
|
|
clients = append(clients, Client{
|
||
|
|
ID: id,
|
||
|
|
Name: name,
|
||
|
|
IP: nextIP,
|
||
|
|
PublicKey: keys.pub,
|
||
|
|
PrivateKey: keys.priv,
|
||
|
|
PSKKey: keys.psk,
|
||
|
|
IsEnabled: "ACTIVE",
|
||
|
|
CreatedAt: nowUnix(),
|
||
|
|
})
|
||
|
|
saveRegistry(clients)
|
||
|
|
|
||
|
|
info("Client registered: id=%d name=%s ip=%s", id, name, nextIP)
|
||
|
|
|
||
|
|
// Activate peer on the running server immediately — no restart needed.
|
||
|
|
confAppendPeer(c.Interface, name, keys.pub, keys.psk, nextIP)
|
||
|
|
awgPeerAdd(c.Interface, keys.pub, keys.psk, nextIP)
|
||
|
|
}
|
||
|
|
|
||
|
|
// listClients prints an aligned table of all registered clients.
|
||
|
|
func listClients() {
|
||
|
|
clients := loadRegistry()
|
||
|
|
if len(clients) == 0 {
|
||
|
|
fmt.Println("No clients registered")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||
|
|
fmt.Fprintln(w, "ID\tNAME\tIP\tSTATUS")
|
||
|
|
for _, cl := range clients {
|
||
|
|
fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", cl.ID, cl.Name, cl.IP, cl.IsEnabled)
|
||
|
|
}
|
||
|
|
w.Flush()
|
||
|
|
}
|
||
|
|
|
||
|
|
// parseID validates and parses a numeric client ID argument.
|
||
|
|
func parseID(args []string) int64 {
|
||
|
|
if len(args) == 0 {
|
||
|
|
die("Invalid ID")
|
||
|
|
}
|
||
|
|
id, err := strconv.ParseInt(args[0], 10, 64)
|
||
|
|
if err != nil || id < 0 {
|
||
|
|
die("Invalid ID")
|
||
|
|
}
|
||
|
|
return id
|
||
|
|
}
|
||
|
|
|
||
|
|
func disableClient(c *Config, osInfo *OSInfo, args []string) {
|
||
|
|
id := parseID(args)
|
||
|
|
clients := loadRegistry()
|
||
|
|
cl := findClient(clients, id)
|
||
|
|
if cl == nil {
|
||
|
|
die("Client with ID %d not found", id)
|
||
|
|
}
|
||
|
|
if cl.IsEnabled != "ACTIVE" {
|
||
|
|
die("Client %d (%s) is already DISABLED", id, cl.Name)
|
||
|
|
}
|
||
|
|
|
||
|
|
pubkey := cl.PublicKey
|
||
|
|
name := cl.Name
|
||
|
|
cl.IsEnabled = "DISABLED"
|
||
|
|
saveRegistry(clients)
|
||
|
|
|
||
|
|
awgPeerRemove(c.Interface, pubkey)
|
||
|
|
confRebuild(c, osInfo, clients)
|
||
|
|
|
||
|
|
info("Client %d (%s): ACTIVE → DISABLED", id, name)
|
||
|
|
}
|
||
|
|
|
||
|
|
func enableClient(c *Config, osInfo *OSInfo, args []string) {
|
||
|
|
id := parseID(args)
|
||
|
|
clients := loadRegistry()
|
||
|
|
cl := findClient(clients, id)
|
||
|
|
if cl == nil {
|
||
|
|
die("Client with ID %d not found", id)
|
||
|
|
}
|
||
|
|
if cl.IsEnabled != "DISABLED" {
|
||
|
|
die("Client %d (%s) is already ACTIVE", id, cl.Name)
|
||
|
|
}
|
||
|
|
|
||
|
|
pubkey, psk, ip, name := cl.PublicKey, cl.PSKKey, cl.IP, cl.Name
|
||
|
|
cl.IsEnabled = "ACTIVE"
|
||
|
|
saveRegistry(clients)
|
||
|
|
|
||
|
|
awgPeerAdd(c.Interface, pubkey, psk, ip)
|
||
|
|
confRebuild(c, osInfo, clients)
|
||
|
|
|
||
|
|
info("Client %d (%s): DISABLED → ACTIVE", id, name)
|
||
|
|
}
|
||
|
|
|
||
|
|
// setClientComment updates a client's free-form note in the registry. It touches
|
||
|
|
// neither the interface config nor the running peers — the comment is metadata
|
||
|
|
// only — so no rebuild/restart is needed.
|
||
|
|
func setClientComment(id int64, comment string) *Client {
|
||
|
|
clients := loadRegistry()
|
||
|
|
cl := findClient(clients, id)
|
||
|
|
if cl == nil {
|
||
|
|
die("Client with ID %d not found", id)
|
||
|
|
}
|
||
|
|
cl.Comment = comment
|
||
|
|
saveRegistry(clients)
|
||
|
|
info("Client %d (%s): comment updated", id, cl.Name)
|
||
|
|
return cl
|
||
|
|
}
|
||
|
|
|
||
|
|
func deleteClient(c *Config, osInfo *OSInfo, args []string) {
|
||
|
|
id := parseID(args)
|
||
|
|
clients := loadRegistry()
|
||
|
|
cl := findClient(clients, id)
|
||
|
|
if cl == nil {
|
||
|
|
die("Client with ID %d not found", id)
|
||
|
|
}
|
||
|
|
|
||
|
|
name := cl.Name
|
||
|
|
pubkey := cl.PublicKey
|
||
|
|
|
||
|
|
remaining := make([]Client, 0, len(clients))
|
||
|
|
for _, x := range clients {
|
||
|
|
if x.ID != id {
|
||
|
|
remaining = append(remaining, x)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
saveRegistry(remaining)
|
||
|
|
|
||
|
|
os.Remove(filepath.Join(clientDir, name+".conf"))
|
||
|
|
os.Remove(filepath.Join(clientDir, name+".png"))
|
||
|
|
|
||
|
|
awgPeerRemove(c.Interface, pubkey)
|
||
|
|
confRebuild(c, osInfo, remaining)
|
||
|
|
deleteStats(pubkey) // drop the peer's accumulated traffic record
|
||
|
|
|
||
|
|
info("Client %d (%s) removed", id, name)
|
||
|
|
}
|