constellation/cli/vpn/vpn.go
2022-04-01 09:58:26 +02:00

167 lines
4.3 KiB
Go

package vpn
import (
"fmt"
"net"
"time"
wgquick "github.com/nmiculinic/wg-quick-go"
"github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
const (
interfaceName = "wg0"
wireguardPort = 51820
)
type vpn interface {
ConfigureDevice(name string, cfg wgtypes.Config) error
}
type networkLink interface {
LinkAdd(link netlink.Link) error
LinkByName(name string) (netlink.Link, error)
ParseAddr(s string) (*netlink.Addr, error)
AddrAdd(link netlink.Link, addr *netlink.Addr) error
LinkSetUp(link netlink.Link) error
}
type netLink struct{}
func newNetLink() *netLink {
return &netLink{}
}
func (n *netLink) LinkAdd(link netlink.Link) error {
return netlink.LinkAdd(link)
}
func (n *netLink) LinkByName(name string) (netlink.Link, error) {
return netlink.LinkByName(name)
}
func (n *netLink) ParseAddr(s string) (*netlink.Addr, error) {
return netlink.ParseAddr(s)
}
func (n *netLink) AddrAdd(link netlink.Link, addr *netlink.Addr) error {
return netlink.AddrAdd(link, addr)
}
func (n *netLink) LinkSetUp(link netlink.Link) error {
return netlink.LinkSetUp(link)
}
type Configurer struct {
netLink networkLink
vpn vpn
}
// NewConfigurerWithDefaults creates a new vpn client.
func NewConfigurerWithDefaults() (*Configurer, error) {
vpn, err := wgctrl.New()
if err != nil {
return nil, err
}
return &Configurer{netLink: newNetLink(), vpn: vpn}, nil
}
// NewConfigurer creates a new vpn client with the provided
// network link and vpn interface.
func NewConfigurer(netLink networkLink, vpn vpn) (*Configurer, error) {
return &Configurer{netLink: netLink, vpn: vpn}, nil
}
// Configure configures a WireGuard interface
// WireGuard will listen on its default port.
// The peer must have the IP 10.118.0.1 in the vpn.
func (c *Configurer) Configure(clientVpnIp, coordinatorPubKey, coordinatorPubIP, clientPrivKey string) error {
wgLink := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: interfaceName}}
if err := c.netLink.LinkAdd(wgLink); err != nil {
return err
}
link, err := c.netLink.LinkByName(interfaceName)
if err != nil {
return err
}
addr, err := c.netLink.ParseAddr(clientVpnIp + "/16")
if err != nil {
return err
}
if err := c.netLink.AddrAdd(link, addr); err != nil {
return err
}
if err := c.netLink.LinkSetUp(link); err != nil {
return err
}
config, err := NewConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey)
if err != nil {
return err
}
return c.vpn.ConfigureDevice(interfaceName, config)
}
// NewConfig creates a new WireGuard configuration.
func NewConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey string) (wgtypes.Config, error) {
_, allowedIPs, err := net.ParseCIDR("10.118.0.1/32")
if err != nil {
return wgtypes.Config{}, fmt.Errorf("parsing CIDR: %w", err)
}
coordinatorPubKeyParsed, err := wgtypes.ParseKey(coordinatorPubKey)
if err != nil {
return wgtypes.Config{}, fmt.Errorf("parsing coordinator public key: %w", err)
}
var endpoint *net.UDPAddr
if ip := net.ParseIP(coordinatorPubIP); ip != nil {
endpoint = &net.UDPAddr{IP: ip, Port: wireguardPort}
} else {
endpoint = nil
}
clientPrivKeyParsed, err := wgtypes.ParseKey(clientPrivKey)
if err != nil {
return wgtypes.Config{}, fmt.Errorf("parsing client private key: %w", err)
}
listenPort := wireguardPort
keepAlive := 10 * time.Second
return wgtypes.Config{
PrivateKey: &clientPrivKeyParsed,
ListenPort: &listenPort,
ReplacePeers: false,
Peers: []wgtypes.PeerConfig{
{
PublicKey: coordinatorPubKeyParsed,
UpdateOnly: false,
Endpoint: endpoint,
AllowedIPs: []net.IPNet{*allowedIPs},
PersistentKeepaliveInterval: &keepAlive,
},
},
}, nil
}
// NewWGQuickConfig create a new WireGuard wg-quick configuration file and mashals it to bytes.
func NewWGQuickConfig(config wgtypes.Config, clientVPNIP string, mtu int) ([]byte, error) {
clientIP := net.ParseIP(clientVPNIP)
if clientIP == nil {
return nil, fmt.Errorf("invalid client vpn ip '%s'", clientVPNIP)
}
quickfile := wgquick.Config{
Config: config,
Address: []net.IPNet{{IP: clientIP, Mask: []byte{255, 255, 0, 0}}},
MTU: mtu,
}
data, err := quickfile.MarshalText()
if err != nil {
return nil, fmt.Errorf("marshal wg-quick config: %w", err)
}
return data, nil
}