621e8895c9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
package infra
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"fmt"
|
||
"net"
|
||
"regexp"
|
||
"strings"
|
||
|
||
qrcode "github.com/skip2/go-qrcode"
|
||
)
|
||
|
||
// wgEndpointRe matches "host:port" where host is a hostname or IP and port is 1–65535.
|
||
var wgEndpointRe = regexp.MustCompile(`^[a-zA-Z0-9._\-]+:\d{1,5}$`)
|
||
|
||
// WGClientConfigGen generates the wg0.conf content for a WireGuard peer (client)
|
||
// and a unicode-block QR string suitable for mobile enrollment via Element or a terminal.
|
||
//
|
||
// Pure: no I/O, fully deterministic given the inputs. Returns error on invalid inputs.
|
||
func WGClientConfigGen(in WGClientConfigInput) (WGClientConfig, error) {
|
||
if err := validateWGClientInput(in); err != nil {
|
||
return WGClientConfig{}, err
|
||
}
|
||
|
||
ka := in.PersistentKA
|
||
if ka == 0 {
|
||
ka = 25
|
||
}
|
||
|
||
var b strings.Builder
|
||
|
||
// [Interface] section
|
||
b.WriteString("[Interface]\n")
|
||
fmt.Fprintf(&b, "PrivateKey = %s\n", in.DevicePrivateKey)
|
||
fmt.Fprintf(&b, "Address = %s\n", in.DeviceAddress)
|
||
if in.DNS != "" {
|
||
fmt.Fprintf(&b, "DNS = %s\n", in.DNS)
|
||
}
|
||
b.WriteString("\n")
|
||
|
||
// [Peer] section (hub)
|
||
b.WriteString("[Peer]\n")
|
||
fmt.Fprintf(&b, "PublicKey = %s\n", in.HubPublicKey)
|
||
if in.PresharedKey != "" {
|
||
fmt.Fprintf(&b, "PresharedKey = %s\n", in.PresharedKey)
|
||
}
|
||
fmt.Fprintf(&b, "Endpoint = %s\n", in.HubEndpoint)
|
||
fmt.Fprintf(&b, "AllowedIPs = %s\n", in.HubAllowedIPs)
|
||
fmt.Fprintf(&b, "PersistentKeepalive = %d\n", ka)
|
||
|
||
ini := b.String()
|
||
|
||
qr, err := qrcode.New(ini, qrcode.Medium)
|
||
if err != nil {
|
||
return WGClientConfig{}, fmt.Errorf("wg_client_config: qr encode: %w", err)
|
||
}
|
||
|
||
return WGClientConfig{
|
||
INI: ini,
|
||
QR: qr.ToString(false),
|
||
Filename: "wg0.conf",
|
||
}, nil
|
||
}
|
||
|
||
// validateWGClientInput checks all required fields for correctness.
|
||
func validateWGClientInput(in WGClientConfigInput) error {
|
||
if err := validateWGBase64Key("DevicePrivateKey", in.DevicePrivateKey); err != nil {
|
||
return err
|
||
}
|
||
if err := validateWGBase64Key("HubPublicKey", in.HubPublicKey); err != nil {
|
||
return err
|
||
}
|
||
if in.PresharedKey != "" {
|
||
if err := validateWGBase64Key("PresharedKey", in.PresharedKey); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// Validate DeviceAddress (CIDR)
|
||
if _, _, err := net.ParseCIDR(in.DeviceAddress); err != nil {
|
||
return fmt.Errorf("wg_client_config: DeviceAddress %q is not a valid CIDR: %w", in.DeviceAddress, err)
|
||
}
|
||
|
||
// Validate HubAllowedIPs (comma-separated CIDRs)
|
||
for _, cidr := range strings.Split(in.HubAllowedIPs, ",") {
|
||
cidr = strings.TrimSpace(cidr)
|
||
if cidr == "" {
|
||
continue
|
||
}
|
||
if _, _, err := net.ParseCIDR(cidr); err != nil {
|
||
return fmt.Errorf("wg_client_config: HubAllowedIPs entry %q is not a valid CIDR: %w", cidr, err)
|
||
}
|
||
}
|
||
|
||
// Validate HubEndpoint
|
||
if !wgEndpointRe.MatchString(in.HubEndpoint) {
|
||
return fmt.Errorf("wg_client_config: HubEndpoint %q must be host:port", in.HubEndpoint)
|
||
}
|
||
parts := strings.SplitN(in.HubEndpoint, ":", 2)
|
||
port := 0
|
||
if _, err := fmt.Sscanf(parts[1], "%d", &port); err != nil || port < 1 || port > 65535 {
|
||
return fmt.Errorf("wg_client_config: HubEndpoint port %q out of range 1-65535", parts[1])
|
||
}
|
||
|
||
if in.DevicePrivateKey == "" {
|
||
return fmt.Errorf("wg_client_config: DevicePrivateKey is required")
|
||
}
|
||
if in.HubPublicKey == "" {
|
||
return fmt.Errorf("wg_client_config: HubPublicKey is required")
|
||
}
|
||
if in.HubEndpoint == "" {
|
||
return fmt.Errorf("wg_client_config: HubEndpoint is required")
|
||
}
|
||
if in.HubAllowedIPs == "" {
|
||
return fmt.Errorf("wg_client_config: HubAllowedIPs is required")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// validateWGBase64Key checks that a WireGuard key is a valid 32-byte base64-encoded string (44 chars).
|
||
func validateWGBase64Key(field, key string) error {
|
||
if len(key) != 44 {
|
||
return fmt.Errorf("wg_client_config: %s must be 44 base64 chars, got %d", field, len(key))
|
||
}
|
||
decoded, err := base64.StdEncoding.DecodeString(key)
|
||
if err != nil {
|
||
return fmt.Errorf("wg_client_config: %s is not valid base64: %w", field, err)
|
||
}
|
||
if len(decoded) != 32 {
|
||
return fmt.Errorf("wg_client_config: %s must decode to 32 bytes, got %d", field, len(decoded))
|
||
}
|
||
return nil
|
||
}
|