init commit

This commit is contained in:
2026-07-18 10:02:43 +03:00
commit 3b4a1f5388
31 changed files with 6580 additions and 0 deletions
+242
View File
@@ -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)
}