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 .conf plus a scannable .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) }