Files
vppn/peer/wgdev.go
2026-06-06 18:46:46 +02:00

178 lines
5.1 KiB
Go

package peer
import (
"fmt"
"net"
"net/netip"
"os"
"time"
"github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func createWGDevice(name string, privKey wgtypes.Key, listenPort int, vpnIP netip.Addr, network []byte) (*wgctrl.Client, error) {
if len(network) != 4 {
return nil, fmt.Errorf("expected 4-byte network, got %d", len(network))
}
la := netlink.NewLinkAttrs()
la.Name = name
if err := netlink.LinkAdd(&netlink.GenericLink{LinkAttrs: la, LinkType: "wireguard"}); err != nil {
return nil, fmt.Errorf("add wireguard link: %w", err)
}
link, err := netlink.LinkByName(name)
if err != nil {
_ = destroyWGDevice(name)
return nil, fmt.Errorf("get wireguard link: %w", err)
}
a4 := vpnIP.As4()
if err := netlink.AddrAdd(link, &netlink.Addr{
IPNet: &net.IPNet{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(24, 32),
},
}); err != nil {
_ = destroyWGDevice(name)
return nil, fmt.Errorf("add VPN address: %w", err)
}
if err := netlink.LinkSetUp(link); err != nil {
_ = destroyWGDevice(name)
return nil, fmt.Errorf("set link up: %w", err)
}
client, err := wgctrl.New()
if err != nil {
_ = destroyWGDevice(name)
return nil, fmt.Errorf("new wgctrl client: %w", err)
}
cfg := wgtypes.Config{
PrivateKey: &privKey,
ListenPort: &listenPort,
}
if err := client.ConfigureDevice(name, cfg); err != nil {
client.Close()
_ = destroyWGDevice(name)
return nil, fmt.Errorf("configure wireguard: %w", err)
}
return client, nil
}
func destroyWGDevice(name string) error {
link, err := netlink.LinkByName(name)
if err != nil {
return fmt.Errorf("get link %q: %w", name, err)
}
return netlink.LinkDel(link)
}
// applyBaseConfig adds the relay peer with /24 AllowedIPs, making it the
// fallback route for all VPN traffic.
func applyBaseConfig(client *wgctrl.Client, devName string, relayPubKey wgtypes.Key, relayEndpoint netip.AddrPort, network []byte) error {
if len(network) != 4 {
return fmt.Errorf("expected 4-byte network, got %d", len(network))
}
keepalive := 25 * time.Second
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: relayPubKey,
Endpoint: net.UDPAddrFromAddrPort(relayEndpoint),
AllowedIPs: []net.IPNet{{
IP: net.IP{network[0], network[1], network[2], 0},
Mask: net.CIDRMask(24, 32),
}},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &keepalive,
}},
}
return client.ConfigureDevice(devName, cfg)
}
// addProbeEntry adds a peer with no AllowedIPs and a 5s keepalive so WireGuard
// attempts handshakes without routing any traffic through it yet.
func addProbeEntry(client *wgctrl.Client, devName string, pubKey wgtypes.Key, endpoint netip.AddrPort) error {
keepalive := 5 * time.Second
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{},
ReplaceAllowedIPs: true,
PersistentKeepaliveInterval: &keepalive,
}},
}
return client.ConfigureDevice(devName, cfg)
}
// addDirectPeer adds a peer with a known endpoint and /32 AllowedIPs in one
// step, for use when the hub reports a peer with a stable public endpoint.
func addDirectPeer(client *wgctrl.Client, devName string, pubKey wgtypes.Key, endpoint netip.AddrPort, vpnIP netip.Addr) error {
a4 := vpnIP.As4()
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Endpoint: net.UDPAddrFromAddrPort(endpoint),
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(32, 32),
}},
ReplaceAllowedIPs: true,
}},
}
return client.ConfigureDevice(devName, cfg)
}
// promoteToDirect upgrades a probe entry to a /32 AllowedIPs entry, causing
// WireGuard to prefer the direct path over the relay's /24 route.
func promoteToDirect(client *wgctrl.Client, devName string, pubKey wgtypes.Key, vpnIP netip.Addr) error {
a4 := vpnIP.As4()
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
AllowedIPs: []net.IPNet{{
IP: net.IP(a4[:]),
Mask: net.CIDRMask(32, 32),
}},
ReplaceAllowedIPs: true,
}},
}
return client.ConfigureDevice(devName, cfg)
}
func removePeerEntry(client *wgctrl.Client, devName string, pubKey wgtypes.Key) error {
cfg := wgtypes.Config{
Peers: []wgtypes.PeerConfig{{
PublicKey: pubKey,
Remove: true,
}},
}
return client.ConfigureDevice(devName, cfg)
}
func enableForwarding(ifaceName string) error {
path := fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/forwarding", ifaceName)
return os.WriteFile(path, []byte("1\n"), 0644)
}
func getPeerEndpoint(client *wgctrl.Client, devName string, pubKey wgtypes.Key) (netip.AddrPort, time.Time, error) {
dev, err := client.Device(devName)
if err != nil {
return netip.AddrPort{}, time.Time{}, fmt.Errorf("get device: %w", err)
}
for _, p := range dev.Peers {
if p.PublicKey == pubKey {
if p.Endpoint == nil {
return netip.AddrPort{}, p.LastHandshakeTime, nil
}
return p.Endpoint.AddrPort(), p.LastHandshakeTime, nil
}
}
return netip.AddrPort{}, time.Time{}, fmt.Errorf("peer %v not found in device %s", pubKey, devName)
}