GHOSTWIRE 0.1.2: WireGuard server manager with web UI and API
Single Go binary that manages a WireGuard server based on pivpn's defaults: - config.json as the single source of truth, reconciled to the kernel via netlink, wgctrl and its own nftables table (NAT, forward, input) - web interface (dashboard, peers, peer detail, add peer, server, settings) and a JSON API for the future iOS app, with session and API-token auth - client private keys are never stored; configs and QR codes shown once - per-peer traffic statistics in stats.json, logs in GHOSTWIRE.jsonl - HTTPS via Let's Encrypt, self-signed, certificate files or off - self-managing: install, update (restores the old binary on failure), uninstall and passwd subcommands; systemd unit generated by the binary Tested end to end on Ubuntu 26.04 (kernel 7.0) at dev.redetzke.aero. Claude-Session: https://claude.ai/code/session_01RAnLbyQZ5ZTA7KqwXP98nw
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// Subnets that commonly appear on home and office networks. A tunnel network
|
||||
// overlapping one of them breaks routing for clients that sit on such a LAN.
|
||||
// Source: pivpn, https://community.openvpn.net/openvpn/wiki/AvoidRoutingConflicts
|
||||
var avoidedSubnets = mustPrefixes(
|
||||
"10.0.0.0/24", "10.0.1.0/24", "10.1.1.0/24", "10.1.10.0/24", "10.2.0.0/24",
|
||||
"10.8.0.0/24", "10.10.1.0/24", "10.90.90.0/24", "10.100.1.0/24",
|
||||
"10.255.255.0/24", "192.168.0.0/24", "192.168.1.0/24",
|
||||
)
|
||||
|
||||
func mustPrefixes(s ...string) []netip.Prefix {
|
||||
out := make([]netip.Prefix, len(s))
|
||||
for i, v := range s {
|
||||
out[i] = netip.MustParsePrefix(v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// hostNetworks returns the networks of all addresses configured on this host.
|
||||
func hostNetworks() []netip.Prefix {
|
||||
var out []netip.Prefix
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
for _, a := range addrs {
|
||||
if n, ok := a.(*net.IPNet); ok {
|
||||
if p, err := netip.ParsePrefix(n.String()); err == nil {
|
||||
out = append(out, p.Masked())
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hasGlobalIPv6() bool {
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
for _, a := range addrs {
|
||||
if n, ok := a.(*net.IPNet); ok && n.IP.To4() == nil && n.IP.IsGlobalUnicast() && !n.IP.IsPrivate() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// randomSubnet picks a random, unused /bits network from 10/8, then
|
||||
// 172.16/12, then 192.168/16, like pivpn does.
|
||||
func randomSubnet(bits int) (netip.Prefix, error) {
|
||||
taken := append(append([]netip.Prefix{}, avoidedSubnets...), hostNetworks()...)
|
||||
for _, pool := range mustPrefixes("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16") {
|
||||
if bits < pool.Bits() {
|
||||
continue
|
||||
}
|
||||
count := int64(1) << (bits - pool.Bits())
|
||||
base := binary.BigEndian.Uint32(pool.Addr().AsSlice())
|
||||
size := uint32(1) << (32 - bits)
|
||||
for range 2000 {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(count))
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], base+uint32(n.Int64())*size)
|
||||
cand := netip.PrefixFrom(netip.AddrFrom4(b), bits)
|
||||
if !overlapsAny(cand, taken) {
|
||||
return cand, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return netip.Prefix{}, errors.New("no free private IPv4 subnet found")
|
||||
}
|
||||
|
||||
func overlapsAny(p netip.Prefix, list []netip.Prefix) bool {
|
||||
for _, q := range list {
|
||||
if p.Overlaps(q) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func addrToU32(a netip.Addr) uint32 { return binary.BigEndian.Uint32(a.AsSlice()) }
|
||||
|
||||
func u32ToAddr(v uint32) netip.Addr {
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], v)
|
||||
return netip.AddrFrom4(b)
|
||||
}
|
||||
|
||||
// serverIPv4 is the first host address of the tunnel network.
|
||||
func serverIPv4(n netip.Prefix) netip.Addr { return n.Addr().Next() }
|
||||
|
||||
func lastAddr(n netip.Prefix) netip.Addr {
|
||||
return u32ToAddr(addrToU32(n.Addr()) | (1<<(32-n.Bits()) - 1))
|
||||
}
|
||||
|
||||
// nextFreeIPv4 returns the lowest unused peer address (server is .1).
|
||||
func nextFreeIPv4(c *Config) (netip.Addr, error) {
|
||||
n := netip.MustParsePrefix(c.Server.IPv4)
|
||||
used := map[netip.Addr]bool{}
|
||||
for _, p := range c.Peers {
|
||||
if a, err := netip.ParseAddr(p.IPv4); err == nil {
|
||||
used[a] = true
|
||||
}
|
||||
}
|
||||
last := lastAddr(n)
|
||||
for a := serverIPv4(n).Next(); a.Less(last); a = a.Next() {
|
||||
if !used[a] {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, badRequest("no free address left in %s", n)
|
||||
}
|
||||
|
||||
// capacity is the number of peer addresses in the tunnel network.
|
||||
func capacity(n netip.Prefix) int { return 1<<(32-n.Bits()) - 3 }
|
||||
|
||||
// mapIPv6 puts the 32 bits of an IPv4 address into the low bits of the IPv6
|
||||
// network: 10.84.12.8 in fd11:5ee:bad:c0de::/64 becomes fd11:5ee:bad:c0de::a54:c08.
|
||||
func mapIPv6(v6net netip.Prefix, v4 netip.Addr) netip.Addr {
|
||||
b := v6net.Addr().As16()
|
||||
copy(b[12:], v4.AsSlice())
|
||||
return netip.AddrFrom16(b)
|
||||
}
|
||||
Reference in New Issue
Block a user