178 lines
5.1 KiB
Go
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)
|
|
}
|