package main import ( "bytes" "context" "errors" "fmt" "html/template" "net/http" "net/netip" "net/url" "os/exec" "strconv" "strings" "time" ) // PeerStatus is what the kernel knows about a peer. type PeerStatus struct { Endpoint string Handshake time.Time // zero: never RX, TX int64 } // parseWGDump parses `wg show all dump`: per interface a line with its keys // and port, then one line per peer (public key, preshared key, endpoint, // allowed IPs, latest handshake, received, sent, keepalive), tab separated. func parseWGDump(dump string) map[string]map[string]PeerStatus { out := map[string]map[string]PeerStatus{} for _, line := range strings.Split(dump, "\n") { f := strings.Split(line, "\t") if len(f) != 9 { // interface lines have 5 fields continue } hs, _ := strconv.ParseInt(f[5], 10, 64) rx, _ := strconv.ParseInt(f[6], 10, 64) tx, _ := strconv.ParseInt(f[7], 10, 64) p := PeerStatus{RX: rx, TX: tx} if f[3] != "(none)" { p.Endpoint = f[3] } if hs > 0 { p.Handshake = time.Unix(hs, 0) } if out[f[0]] == nil { out[f[0]] = map[string]PeerStatus{} } out[f[0]][f[1]] = p } return out } // wgStatus asks the kernel; without wg or the permission there is no status. func wgStatus() map[string]map[string]PeerStatus { out, err := command("wg", "show", "all", "dump") if err != nil { return nil } return parseWGDump(out) } // Device is the tunnel's interface name. func (t WGTunnel) Device() string { if t.Interface != "" { return t.Interface } return "wg-" + t.Name } // ClientConfig is a phone's or laptop's config, shown once after it was made. type ClientConfig struct { Name string Text string QR template.HTML // an SVG, empty if qrencode is unavailable Tunnel string } // ClientForm is the "add a phone or laptop" form. type ClientForm struct { Tunnel string Name string Address string Endpoint string Allowed string DNS string Errors FormErrors } type wgData struct { Tunnels []WGTunnel New WGTunnel Client ClientForm Config *ClientConfig Live bool // status is available ImportName, ImportText string // the import form, shown again after an error } func renderQR(text string) template.HTML { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "qrencode", "-t", "SVG", "-m", "2", "-o", "-") cmd.Stdin = strings.NewReader(text) var out bytes.Buffer cmd.Stdout = &out if cmd.Run() != nil { return "" } svg := out.String() if i := strings.Index(svg, "= 0 { return template.HTML(svg[i:]) // our own tool's output for our own text } return "" } // lanSubnets are the networks of the LANs, for a client's default routes. func (s Settings) lanSubnets() []string { var out []string for _, l := range s.form().LANs { a, ok := ipv4(l.Address) if !ok { continue } bits := 24 if n, ok, _ := optionalInt(l.Prefix, 8, 30); ok { bits = n } out = append(out, netip.PrefixFrom(a, bits).Masked().String()) } return out } // nextClientAddress suggests an unused address in the tunnel's first IPv4 network. func nextClientAddress(t WGTunnel) string { var prefix netip.Prefix used := map[netip.Addr]bool{} for _, a := range splitList(t.Addresses) { if p, err := netip.ParsePrefix(a); err == nil && p.Addr().Is4() { if !prefix.IsValid() { prefix = p.Masked() } used[p.Addr()] = true } } if !prefix.IsValid() { return "" } for _, p := range t.Peers { for _, a := range splitList(p.Allowed) { if pp, err := netip.ParsePrefix(a); err == nil { used[pp.Addr()] = true } } } for a := prefix.Addr().Next(); prefix.Contains(a); a = a.Next() { if !used[a] && a != prefix.Addr() { if next := a.Next(); !prefix.Contains(next) { break // the broadcast address } return a.String() } } return "" } func (a *app) wgView(s Settings, mutate func(*wgData)) wgData { d := wgData{Tunnels: s.tunnels(a.sys), New: WGTunnel{KillSwitch: true, Peers: []WGPeer{{}}}} status := wgStatus() d.Live = status != nil for i := range d.Tunnels { t := &d.Tunnels[i] t.Peers = append(t.Peers, WGPeer{}) // the "add a peer" row t.Live = status[t.Device()] } if len(d.Tunnels) > 0 { t := d.Tunnels[0] d.Client = ClientForm{Tunnel: t.Name, Address: nextClientAddress(t), Allowed: strings.Join(append(addrNetworks(t.Addresses), s.lanSubnets()...), ", ")} if ip := firstV4(t.Addresses); t.DNS && ip != "" { d.Client.DNS = ip } } if mutate != nil { mutate(&d) } return d } func addrNetworks(addrs string) []string { var out []string for _, a := range splitList(addrs) { if p, err := netip.ParsePrefix(a); err == nil && p.Addr().Is4() { out = append(out, p.Masked().String()) } } return out } func firstV4(addrs string) string { for _, a := range splitList(addrs) { if p, err := netip.ParsePrefix(a); err == nil && p.Addr().Is4() { return p.Addr().String() } } return "" } func (a *app) wireguardPage(w http.ResponseWriter, r *http.Request, sess session) { p := page{Title: "WireGuard", CSRF: sess.csrf, LoggedIn: true, Flash: r.URL.Query().Get("msg")} s, err := loadSettings(a.sys.settingsPath()) if err != nil { p.Error = err.Error() a.render(w, "wireguard", p) return } p.Data = a.wgView(s, nil) a.render(w, "wireguard", p) } func tunnelFromRequest(r *http.Request) WGTunnel { v := r.PostFormValue t := WGTunnel{ Name: v("name"), Interface: v("interface"), ListenPort: v("listenPort"), MTU: v("mtu"), Addresses: v("addresses"), SourceAddresses: v("sourceAddresses"), Zone: v("zone"), Internet: v("internet") == "on", AllowManagement: v("allowManagement") == "on", DNS: v("dns") == "on", Masquerade: v("masquerade") == "on", KillSwitch: v("killSwitch") == "on", NewKey: v("newKey") == "on", PrivateKey: v("privateKey"), } n, _ := strconv.Atoi(v("peers")) for i := range min(n, 256) { f := func(k string) string { return r.PostFormValue(fmt.Sprintf("peer.%d.%s", i, k)) } t.Peers = append(t.Peers, WGPeer{ Name: f("name"), PublicKey: f("publicKey"), Endpoint: f("endpoint"), Allowed: f("allowed"), Keepalive: f("keepalive"), PSK: f("psk"), NewPSK: f("newPsk") == "on", Remove: f("remove") == "on", }) } return t } func (a *app) wireguardSave(w http.ResponseWriter, r *http.Request, sess session) { p := page{Title: "WireGuard", CSRF: sess.csrf, LoggedIn: true} s, err := loadSettings(a.sys.settingsPath()) if err != nil { p.Error = err.Error() a.render(w, "wireguard", p) return } t := tunnelFromRequest(r) files, err := s.applyTunnel(a.sys, t) if err == nil { if err = writeSecrets(a.sys.wgDir(), files); err == nil { err = s.save(a.sys.settingsPath()) } } if err != nil { var fe FormErrors if !errors.As(err, &fe) { p.Error = "Saving: " + err.Error() p.Data = a.wgView(mustLoad(a.sys), nil) a.render(w, "wireguard", p) return } t.Errors, t.PrivateKey = fe, "" for i := range t.Peers { t.Peers[i].PSK = "" } old, _ := loadSettings(a.sys.settingsPath()) p.Error = "Please correct the marked fields." p.Data = a.wgView(old, func(d *wgData) { for i := range d.Tunnels { if d.Tunnels[i].Name == strings.TrimSpace(t.Name) { t.PublicKey, t.KeyPresent, t.Live = d.Tunnels[i].PublicKey, d.Tunnels[i].KeyPresent, d.Tunnels[i].Live d.Tunnels[i] = t return } } d.New = t }) w.WriteHeader(http.StatusUnprocessableEntity) a.render(w, "wireguard", p) return } http.Redirect(w, r, "/wireguard?msg="+url.QueryEscape("Saved "+strings.TrimSpace(t.Name)+". Apply to make the changes take effect."), http.StatusSeeOther) } func mustLoad(sys System) Settings { s, err := loadSettings(sys.settingsPath()) if err != nil { return Settings{} } return s } func (a *app) wireguardDelete(w http.ResponseWriter, r *http.Request, _ session) { s, err := loadSettings(a.sys.settingsPath()) msg := "" switch { case err != nil: msg = err.Error() case r.PostFormValue("confirm") != "on": msg = "Tick the box to delete the tunnel." case !s.deleteTunnel(r.PostFormValue("name")): msg = "No such tunnel." default: if err := s.save(a.sys.settingsPath()); err != nil { msg = "Saving: " + err.Error() } else { msg = "Deleted " + r.PostFormValue("name") + " (its key file stays). Apply to make it take effect." } } http.Redirect(w, r, "/wireguard?msg="+url.QueryEscape(msg), http.StatusSeeOther) } // wireguardClient adds a peer for a phone or laptop: the router makes the // client's keys and shows its configuration once. func (a *app) wireguardClient(w http.ResponseWriter, r *http.Request, sess session) { p := page{Title: "WireGuard", CSRF: sess.csrf, LoggedIn: true} s, err := loadSettings(a.sys.settingsPath()) if err != nil { p.Error = err.Error() a.render(w, "wireguard", p) return } f := ClientForm{Tunnel: r.PostFormValue("tunnel"), Name: strings.TrimSpace(r.PostFormValue("name")), Address: strings.TrimSpace(r.PostFormValue("address")), Endpoint: strings.TrimSpace(r.PostFormValue("endpoint")), Allowed: r.PostFormValue("allowed"), DNS: strings.TrimSpace(r.PostFormValue("dns")), Errors: FormErrors{}} var tunnel *WGTunnel for _, t := range s.tunnels(a.sys) { if t.Name == f.Tunnel { tunnel = &t } } if tunnel == nil { f.Errors["tunnel"] = "choose a tunnel" } else if !tunnel.KeyPresent { f.Errors["tunnel"] = "this tunnel has no private key yet" } if !nameRe.MatchString(f.Name) { f.Errors["name"] = "lowercase letters, digits and dashes, starting with a letter" } addr, aerr := netip.ParseAddr(strings.SplitN(f.Address, "/", 2)[0]) if aerr != nil { f.Errors["address"] = "an IP address inside the tunnel's network" } if f.Endpoint == "" || !hostPort.MatchString(f.Endpoint) || portOf(f.Endpoint) < 1 || portOf(f.Endpoint) > 65535 { f.Errors["endpoint"] = "the router's public name or address and port, like vpn.example.org:51820" } allowed := splitList(f.Allowed) if len(allowed) == 0 { f.Errors["allowed"] = "what the client sends through the tunnel" } for _, c := range allowed { if !cidrOK(c) { f.Errors["allowed"] = fmt.Sprintf("%q is not a network like 192.168.10.0/24", c) } } if f.DNS != "" && !ipOK(f.DNS) { f.Errors["dns"] = "an IP address" } if tunnel != nil && len(f.Errors) == 0 { for _, pr := range tunnel.Peers { if pr.Name == f.Name { f.Errors["name"] = "a peer of this name exists" } for _, c := range splitList(pr.Allowed) { if pp, err := netip.ParsePrefix(c); err == nil && pp.Contains(addr) { f.Errors["address"] = "already used by peer " + pr.Name } } } } fail := func(msg string, status int) { p.Error = msg p.Data = a.wgView(s, func(d *wgData) { d.Client = f }) w.WriteHeader(status) a.render(w, "wireguard", p) } if len(f.Errors) > 0 { fail("Please correct the marked fields.", http.StatusUnprocessableEntity) return } clientPriv, clientPub, err := newPrivateKey() psk, err2 := newPresharedKey() if err != nil || err2 != nil { fail("Making keys failed.", http.StatusInternalServerError) return } prefixBits := 32 if addr.Is6() { prefixBits = 128 } clientNet := netip.PrefixFrom(addr, prefixBits).String() pskPath := a.sys.wgPSKPath(tunnel.Name, f.Name) router := obj(s, "router") wg := obj(obj(router, "wireguard"), tunnel.Name) peers := obj(wg, "peers") peers[f.Name] = map[string]any{ "publicKey": clientPub, "presharedKeyFile": pskPath, "allowedIPs": []any{clientNet}, } if err := writeSecrets(a.sys.wgDir(), []secretFile{{pskPath, psk}}); err != nil { fail("Storing the preshared key: "+err.Error(), http.StatusInternalServerError) return } if err := s.save(a.sys.settingsPath()); err != nil { fail("Saving: "+err.Error(), http.StatusInternalServerError) return } var cfg strings.Builder fmt.Fprintf(&cfg, "[Interface]\nPrivateKey = %s\nAddress = %s\n", clientPriv, clientNet) if f.DNS != "" { fmt.Fprintf(&cfg, "DNS = %s\n", f.DNS) } fmt.Fprintf(&cfg, "\n[Peer]\nPublicKey = %s\nPresharedKey = %s\nEndpoint = %s\nAllowedIPs = %s\nPersistentKeepalive = 25\n", tunnel.PublicKey, psk, f.Endpoint, strings.Join(allowed, ", ")) p.Flash = "Added " + f.Name + ". Apply to make the tunnel accept it. Its configuration is shown here once." p.Data = a.wgView(s, func(d *wgData) { d.Config = &ClientConfig{Name: f.Name, Text: cfg.String(), QR: renderQR(cfg.String()), Tunnel: tunnel.Name} }) a.render(w, "wireguard", p) }