Files
GHOSTWIRE/clientconf.go
T
Daniel Redetzke 3482a03707 Install takes over a pivpn WireGuard server
On a server running pivpn's WireGuard, a new install offers to take it
over: the server key, port, MTU, tunnel networks, endpoint, DNS,
AllowedIPs and keepalive, and every client with its public key,
preshared key and addresses. Devices keep their configs. Clients pivpn
switched off are imported switched off, with the note "Imported from
pivpn". Client private keys in /etc/wireguard/configs are not read.

- Install notes which peers are connected, stops wg-quick@wg0, starts
  GHOSTWIRE on the same wg0 and waits up to 30 s for those peers. The
  wait only reports; idle devices reconnect when they next send.
- If the service does not stay running, install removes what it set up,
  including config.json, and starts pivpn's WireGuard again.
- Without a terminal the takeover needs -import-pivpn; install refuses
  to run next to pivpn otherwise, and the flag is refused on an
  existing install.
- Names GHOSTWIRE does not accept are renamed and listed in the
  summary. An IPv6 address that differs from the mapped one is kept on
  the peer until its config is issued again.
- uninstall without a config of its own (e.g. after a takeover was
  undone) leaves the WireGuard interface alone and removes only the
  firewall table.
- README: "Coming from pivpn?" under the intro, a Features entry and a
  "Moving from pivpn" section.

Tested end to end on Ubuntu 24.04 with pivpn aa96de7.
2026-10-06 00:54:08 +03:00

110 lines
3.0 KiB
Go

package main
import (
"encoding/base64"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
qrcode "github.com/skip2/go-qrcode"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func newPrivateKey() (wgtypes.Key, error) { return wgtypes.GeneratePrivateKey() }
func newPresharedKey() (wgtypes.Key, error) { return wgtypes.GenerateKey() }
func serverPublicKey(c *Config) string {
k, err := wgtypes.ParseKey(c.Server.PrivateKey)
if err != nil {
return ""
}
return k.PublicKey().String()
}
// effective returns the peer's value or the server default.
func peerDNS(c *Config, p *Peer) []string {
if p.DNS != nil {
return p.DNS
}
return c.Server.ClientDefaults.DNS
}
func peerAllowedIPs(c *Config, p *Peer) []string {
if p.AllowedIPs != nil {
return p.AllowedIPs
}
return c.Server.ClientDefaults.AllowedIPs
}
func peerKeepalive(c *Config, p *Peer) int {
if p.Keepalive != nil {
return *p.Keepalive
}
return c.Server.ClientDefaults.Keepalive
}
// peerAddresses returns the peer's tunnel addresses as host routes for the
// server side (/32, /128).
func peerAddresses(c *Config, p *Peer) []netip.Prefix {
v4 := netip.MustParseAddr(p.IPv4)
out := []netip.Prefix{netip.PrefixFrom(v4, 32)}
if c.Server.IPv6Enabled {
out = append(out, netip.PrefixFrom(peerIPv6(c, p), 128))
}
return out
}
func endpointString(c *Config) string {
port := c.Server.EndpointPort
if port == 0 {
port = c.Server.ListenPort
}
host := c.Server.Endpoint
if host == "" {
host = "SET-ENDPOINT-IN-SERVER-SETTINGS"
}
return net.JoinHostPort(host, strconv.Itoa(port))
}
// clientConfig renders the wg-quick file for a peer. privateKey may be empty
// when the client generated its own keys.
func clientConfig(c *Config, p *Peer, privateKey string) string {
v4net := netip.MustParsePrefix(c.Server.IPv4)
v4 := netip.MustParseAddr(p.IPv4)
addr := fmt.Sprintf("%s/%d", v4, v4net.Bits())
if c.Server.IPv6Enabled {
v6net := netip.MustParsePrefix(c.Server.IPv6)
addr += fmt.Sprintf(",%s/%d", peerIPv6(c, p), v6net.Bits())
}
if privateKey == "" {
privateKey = "<the private key of this device>"
}
var b strings.Builder
fmt.Fprintf(&b, "[Interface]\nPrivateKey = %s\nAddress = %s\n", privateKey, addr)
if dns := peerDNS(c, p); len(dns) > 0 {
fmt.Fprintf(&b, "DNS = %s\n", strings.Join(dns, ", "))
}
// No MTU line: the client picks one for the network it is on, as pivpn does.
fmt.Fprintf(&b, "\n[Peer]\nPublicKey = %s\n", serverPublicKey(c))
if p.PresharedKey != "" {
fmt.Fprintf(&b, "PresharedKey = %s\n", p.PresharedKey)
}
fmt.Fprintf(&b, "Endpoint = %s\nAllowedIPs = %s\n", endpointString(c), strings.Join(peerAllowedIPs(c, p), ", "))
if ka := peerKeepalive(c, p); ka > 0 {
fmt.Fprintf(&b, "PersistentKeepalive = %d\n", ka)
}
return b.String()
}
// qrDataURL returns a PNG QR code as a data: URL for direct use in <img src>.
func qrDataURL(text string) (string, error) {
png, err := qrcode.Encode(text, qrcode.Medium, 512)
if err != nil {
return "", err
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(png), nil
}