package main import ( "crypto/ecdh" "crypto/rand" "encoding/base64" "fmt" "net/netip" "os" "path/filepath" "regexp" "slices" "strconv" "strings" ) // WireGuard keys are 32 bytes, base64 encoded. // newPrivateKey returns a fresh private key and its public key. func newPrivateKey() (priv, pub string, err error) { raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return "", "", err } // As `wg genkey` does. raw[0] &= 248 raw[31] = raw[31]&127 | 64 return keyPair(raw) } func keyPair(raw []byte) (priv, pub string, err error) { k, err := ecdh.X25519().NewPrivateKey(raw) if err != nil { return "", "", err } return base64.StdEncoding.EncodeToString(raw), base64.StdEncoding.EncodeToString(k.PublicKey().Bytes()), nil } // publicKey derives the public key of a base64 private key. func publicKey(priv string) (string, error) { raw, err := decodeKey(priv) if err != nil { return "", err } _, pub, err := keyPair(raw) return pub, err } func decodeKey(s string) ([]byte, error) { raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(s)) if err != nil || len(raw) != 32 { return nil, fmt.Errorf("not a WireGuard key (44 characters of base64)") } return raw, nil } func newPresharedKey() (string, error) { raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return "", err } return base64.StdEncoding.EncodeToString(raw), nil } func (s System) wgDir() string { return filepath.Join(s.StateDir, "wireguard") } func (s System) wgKeyPath(tunnel string) string { return filepath.Join(s.wgDir(), tunnel+".key") } func (s System) wgPSKPath(tunnel, peer string) string { return filepath.Join(s.wgDir(), tunnel+"-"+peer+".psk") } // A secretFile is written (0600) before the settings that refer to it. type secretFile struct { Path, Content string } func writeSecrets(dir string, files []secretFile) error { if len(files) == 0 { return nil } if err := os.MkdirAll(dir, 0o700); err != nil { return err } for _, f := range files { if err := writeFileAtomic(f.Path, []byte(f.Content+"\n"), 0o600); err != nil { return err } } return nil } func readKeyFile(path string) (string, bool) { data, err := os.ReadFile(path) if err != nil { return "", false } return strings.TrimSpace(string(data)), true } // WGPeer is a peer of a tunnel as the page edits it. type WGPeer struct { Name string PublicKey string Endpoint string Allowed string // comma or newline separated CIDRs Keepalive string PSKSet bool // a preshared key file is configured PSK string // write-only: pasted preshared key NewPSK bool // generate one Remove bool } // WGTunnel is a tunnel as the page edits it. type WGTunnel struct { Name string Interface string ListenPort string MTU string Addresses string // CIDRs SourceAddresses string Zone string Internet bool AllowManagement bool DNS bool Masquerade bool KillSwitch bool Peers []WGPeer PublicKey string // derived from the key file KeyPresent bool NewKey bool // generate a new private key PrivateKey string // write-only: pasted private key Live map[string]PeerStatus // by public key; nil if unknown Errors FormErrors } var ( b64KeyRe = regexp.MustCompile(`^[A-Za-z0-9+/]{43}=$`) hostPort = regexp.MustCompile(`^(\[[0-9a-fA-F:]+\]|[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?):[0-9]{1,5}$`) ) // splitList splits "a, b\nc" into its items. func splitList(v string) []string { return strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == '\n' || r == ' ' || r == '\t' || r == '\r' }) } func stringList(m map[string]any, key string) []string { var out []string items, _ := m[key].([]any) for _, i := range items { if v, ok := i.(string); ok { out = append(out, v) } } return out } // tunnels reads the WireGuard tunnels, sorted by name. func (s Settings) tunnels(sys System) []WGTunnel { router, _ := s["router"].(map[string]any) wgs, _ := router["wireguard"].(map[string]any) var names []string for n := range wgs { names = append(names, n) } slices.Sort(names) var out []WGTunnel for _, name := range names { w, _ := wgs[name].(map[string]any) if w == nil { continue } t := WGTunnel{ Name: name, Interface: str(w, "interface"), ListenPort: str(w, "listenPort"), MTU: str(w, "mtu"), Addresses: strings.Join(stringList(w, "addresses"), ", "), SourceAddresses: strings.Join(stringList(w, "sourceAddresses"), ", "), Zone: str(w, "zone"), Internet: boolean(w, false, "internet"), AllowManagement: boolean(w, false, "allowManagement"), DNS: boolean(w, false, "dns"), Masquerade: boolean(w, false, "masquerade"), KillSwitch: boolean(w, true, "killSwitch"), } if key, ok := readKeyFile(str(w, "privateKeyFile")); ok { if pub, err := publicKey(key); err == nil { t.PublicKey, t.KeyPresent = pub, true } } peers, _ := w["peers"].(map[string]any) var pnames []string for n := range peers { pnames = append(pnames, n) } slices.Sort(pnames) for _, pn := range pnames { p, _ := peers[pn].(map[string]any) t.Peers = append(t.Peers, WGPeer{ Name: pn, PublicKey: str(p, "publicKey"), Endpoint: str(p, "endpoint"), Allowed: strings.Join(stringList(p, "allowedIPs"), ", "), Keepalive: str(p, "persistentKeepalive"), PSKSet: str(p, "presharedKeyFile") != "", }) } out = append(out, t) } return out } func cidrOK(v string) bool { if p, err := netip.ParsePrefix(v); err == nil { return p.IsValid() } return false } func ipOK(v string) bool { _, err := netip.ParseAddr(v) return err == nil } // applyTunnel writes t into the settings (creating the tunnel if it is new) // and returns the key files to store. Only basic checks happen here; the // build validates the rest. A new tunnel gets a generated key unless one was // pasted, and an existing one needs a key file. func (s Settings) applyTunnel(sys System, t WGTunnel) ([]secretFile, error) { errs := FormErrors{} router := obj(s, "router") wgs := obj(router, "wireguard") t.Name = strings.TrimSpace(t.Name) if !nameRe.MatchString(t.Name) { errs["name"] = "lowercase letters, digits and dashes, starting with a letter" return nil, errs } _, existed := wgs[t.Name].(map[string]any) w := obj(wgs, t.Name) var files []secretFile if t.Interface = strings.TrimSpace(t.Interface); t.Interface != "" { if !ifaceRe.MatchString(t.Interface) { errs["interface"] = "an interface name of up to 15 characters" } w["interface"] = t.Interface } else { delete(w, "interface") } if port, ok, err := optionalInt(t.ListenPort, 1, 65535); err != nil { errs["listenPort"] = err.Error() } else if ok { w["listenPort"] = port } else { delete(w, "listenPort") } if mtu, ok, err := optionalInt(t.MTU, 576, 9000); err != nil { errs["mtu"] = err.Error() } else if ok { w["mtu"] = mtu } else { delete(w, "mtu") } setList := func(key, field string, raw string, valid func(string) bool, what string) { items := splitList(raw) for _, i := range items { if !valid(i) { errs[field] = fmt.Sprintf("%q is not %s", i, what) } } if len(items) == 0 { delete(w, key) return } list := make([]any, len(items)) for i, v := range items { list[i] = v } w[key] = list } setList("addresses", "addresses", t.Addresses, cidrOK, "an address like 10.100.0.1/24") setList("sourceAddresses", "sourceAddresses", t.SourceAddresses, ipOK, "an IP address") if t.Zone = strings.TrimSpace(t.Zone); t.Zone != "" && t.Zone != t.Name { if !nameRe.MatchString(t.Zone) { errs["zone"] = "lowercase letters, digits and dashes, starting with a letter" } w["zone"] = t.Zone } else { delete(w, "zone") } w["internet"] = t.Internet w["allowManagement"] = t.AllowManagement w["dns"] = t.DNS w["masquerade"] = t.Masquerade w["killSwitch"] = t.KillSwitch // The private key. keyPath := sys.wgKeyPath(t.Name) switch { case strings.TrimSpace(t.PrivateKey) != "": if _, err := decodeKey(t.PrivateKey); err != nil { errs["privateKey"] = err.Error() } else { files = append(files, secretFile{keyPath, strings.TrimSpace(t.PrivateKey)}) } case t.NewKey || !existed: priv, _, err := newPrivateKey() if err != nil { errs["privateKey"] = err.Error() } files = append(files, secretFile{keyPath, priv}) default: if _, ok := readKeyFile(str(w, "privateKeyFile")); !ok { errs["privateKey"] = "this tunnel has no private key: paste one or generate a new one" } } if len(files) > 0 && errs["privateKey"] == "" { w["privateKeyFile"] = keyPath } // Peers. peers := obj(w, "peers") keep := map[string]bool{} keys := map[string]bool{} for i, p := range t.Peers { field := func(n string) string { return fmt.Sprintf("peers[%d].%s", i, n) } p.Name = strings.TrimSpace(p.Name) if p.Name == "" && p.PublicKey == "" && p.Endpoint == "" && p.Allowed == "" { continue // the "add a peer" row } if p.Remove { continue } if !nameRe.MatchString(p.Name) { errs[field("name")] = "lowercase letters, digits and dashes, starting with a letter" continue } if keep[p.Name] { errs[field("name")] = "used twice" continue } keep[p.Name] = true pm := obj(peers, p.Name) if p.PublicKey = strings.TrimSpace(p.PublicKey); !b64KeyRe.MatchString(p.PublicKey) { errs[field("publicKey")] = "a public key (44 characters of base64)" } else if keys[p.PublicKey] { errs[field("publicKey")] = "used twice" } keys[p.PublicKey] = true pm["publicKey"] = p.PublicKey if p.Endpoint = strings.TrimSpace(p.Endpoint); p.Endpoint == "" { delete(pm, "endpoint") } else if m := hostPort.MatchString(p.Endpoint); !m || portOf(p.Endpoint) > 65535 || portOf(p.Endpoint) < 1 { errs[field("endpoint")] = "host:port, or [IPv6]:port" } else { pm["endpoint"] = p.Endpoint } allowed := splitList(p.Allowed) for _, a := range allowed { if !cidrOK(a) { errs[field("allowed")] = fmt.Sprintf("%q is not an address with a prefix length like 10.100.0.2/32", a) } } list := make([]any, len(allowed)) for i, v := range allowed { list[i] = v } pm["allowedIPs"] = list if ka, ok, err := optionalInt(p.Keepalive, 1, 65535); err != nil { errs[field("keepalive")] = err.Error() } else if ok { pm["persistentKeepalive"] = ka } else { delete(pm, "persistentKeepalive") } pskPath := sys.wgPSKPath(t.Name, p.Name) switch { case strings.TrimSpace(p.PSK) != "": if _, err := decodeKey(p.PSK); err != nil { errs[field("psk")] = err.Error() } else { files = append(files, secretFile{pskPath, strings.TrimSpace(p.PSK)}) pm["presharedKeyFile"] = pskPath } case p.NewPSK: psk, err := newPresharedKey() if err != nil { errs[field("psk")] = err.Error() } files = append(files, secretFile{pskPath, psk}) pm["presharedKeyFile"] = pskPath } } for n := range peers { if !keep[n] { delete(peers, n) } } if len(errs) > 0 { return nil, errs } return files, nil } func portOf(endpoint string) int { i := strings.LastIndex(endpoint, ":") n, err := strconv.Atoi(endpoint[i+1:]) if err != nil { return 0 } return n } // deleteTunnel removes a tunnel from the settings. Its key files stay: they // may be a key a remote side still expects. func (s Settings) deleteTunnel(name string) bool { router := obj(s, "router") wgs := obj(router, "wireguard") if _, ok := wgs[name]; !ok { return false } delete(wgs, name) if len(wgs) == 0 { delete(router, "wireguard") } return true }