init commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user