Split the application into modules
Splitting into modules will help keep concerns separate, at the cost of a slightly more verbose code.
This commit is contained in:
18
wg/netlink.go
Normal file
18
wg/netlink.go
Normal file
@ -0,0 +1,18 @@
|
||||
package wg
|
||||
|
||||
import "github.com/vishvananda/netlink"
|
||||
|
||||
// this is only necessary while this PR is open:
|
||||
// https://github.com/vishvananda/netlink/pull/464
|
||||
|
||||
type wireguard struct {
|
||||
netlink.LinkAttrs
|
||||
}
|
||||
|
||||
func (wg *wireguard) Attrs() *netlink.LinkAttrs {
|
||||
return &wg.LinkAttrs
|
||||
}
|
||||
|
||||
func (wg *wireguard) Type() string {
|
||||
return "wireguard"
|
||||
}
|
145
wg/wireguard.go
Normal file
145
wg/wireguard.go
Normal file
@ -0,0 +1,145 @@
|
||||
package wg
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/costela/wesher/common"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type WgState struct {
|
||||
iface string
|
||||
client *wgctrl.Client
|
||||
OverlayAddr net.IPNet
|
||||
Port int
|
||||
PrivKey wgtypes.Key
|
||||
PubKey wgtypes.Key
|
||||
}
|
||||
|
||||
func NewWGConfig(iface string, port int) (*WgState, error) {
|
||||
client, err := wgctrl.New()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not instantiate wireguard client")
|
||||
}
|
||||
|
||||
privKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKey := privKey.PublicKey()
|
||||
|
||||
wgState := WgState{
|
||||
iface: iface,
|
||||
client: client,
|
||||
Port: port,
|
||||
PrivKey: privKey,
|
||||
PubKey: pubKey,
|
||||
}
|
||||
return &wgState, nil
|
||||
}
|
||||
|
||||
func (wg *WgState) AssignOverlayAddr(ipnet *net.IPNet, name string) {
|
||||
// TODO: this is way too brittle and opaque
|
||||
bits, size := ipnet.Mask.Size()
|
||||
ip := make([]byte, len(ipnet.IP))
|
||||
copy(ip, []byte(ipnet.IP))
|
||||
|
||||
h := fnv.New128a()
|
||||
h.Write([]byte(name))
|
||||
hb := h.Sum(nil)
|
||||
|
||||
for i := 1; i <= (size-bits)/8; i++ {
|
||||
ip[len(ip)-i] = hb[len(hb)-i]
|
||||
}
|
||||
|
||||
wg.OverlayAddr = net.IPNet{
|
||||
IP: net.IP(ip),
|
||||
Mask: net.CIDRMask(size, size), // either /32 or /128, depending if ipv4 or ipv6
|
||||
}
|
||||
}
|
||||
|
||||
func (wg *WgState) DownInterface() error {
|
||||
if _, err := wg.client.Device(wg.iface); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // device already gone; noop
|
||||
}
|
||||
return err
|
||||
}
|
||||
link, err := netlink.LinkByName(wg.iface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return netlink.LinkDel(link)
|
||||
}
|
||||
|
||||
func (wg *WgState) SetUpInterface(nodes []common.Node) error {
|
||||
if err := netlink.LinkAdd(&wireguard{LinkAttrs: netlink.LinkAttrs{Name: wg.iface}}); err != nil && !os.IsExist(err) {
|
||||
return errors.Wrapf(err, "could not create interface %s", wg.iface)
|
||||
}
|
||||
|
||||
peerCfgs, err := wg.NodesToPeerConfigs(nodes)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error converting received node information to wireguard format")
|
||||
}
|
||||
if err := wg.client.ConfigureDevice(wg.iface, wgtypes.Config{
|
||||
PrivateKey: &wg.PrivKey,
|
||||
ListenPort: &wg.Port,
|
||||
ReplacePeers: true,
|
||||
Peers: peerCfgs,
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "could not set wireguard configuration for %s", wg.iface)
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByName(wg.iface)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get link information for %s", wg.iface)
|
||||
}
|
||||
if err := netlink.AddrReplace(link, &netlink.Addr{
|
||||
IPNet: &wg.OverlayAddr,
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "could not set address for %s", wg.iface)
|
||||
}
|
||||
// TODO: make MTU configurable?
|
||||
if err := netlink.LinkSetMTU(link, 1420); err != nil {
|
||||
return errors.Wrapf(err, "could not set MTU for %s", wg.iface)
|
||||
}
|
||||
if err := netlink.LinkSetUp(link); err != nil {
|
||||
return errors.Wrapf(err, "could not enable interface %s", wg.iface)
|
||||
}
|
||||
for _, node := range nodes {
|
||||
netlink.RouteAdd(&netlink.Route{
|
||||
LinkIndex: link.Attrs().Index,
|
||||
Dst: &node.OverlayAddr,
|
||||
Scope: netlink.SCOPE_LINK,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wg *WgState) NodesToPeerConfigs(nodes []common.Node) ([]wgtypes.PeerConfig, error) {
|
||||
peerCfgs := make([]wgtypes.PeerConfig, len(nodes))
|
||||
for i, node := range nodes {
|
||||
pubKey, err := wgtypes.ParseKey(node.PubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peerCfgs[i] = wgtypes.PeerConfig{
|
||||
PublicKey: pubKey,
|
||||
ReplaceAllowedIPs: true,
|
||||
Endpoint: &net.UDPAddr{
|
||||
IP: node.Addr,
|
||||
Port: wg.Port,
|
||||
},
|
||||
AllowedIPs: []net.IPNet{
|
||||
node.OverlayAddr,
|
||||
},
|
||||
}
|
||||
}
|
||||
return peerCfgs, nil
|
||||
}
|
81
wg/wireguard_test.go
Normal file
81
wg/wireguard_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package wg
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_wgState_assignOverlayAddr(t *testing.T) {
|
||||
type args struct {
|
||||
ipnet *net.IPNet
|
||||
name string
|
||||
}
|
||||
_, ipv4net, _ := net.ParseCIDR("10.0.0.0/8")
|
||||
_, ipv6net, _ := net.ParseCIDR("2001:db8::/32")
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"assign in big ipv4 net",
|
||||
args{ipv4net, "test"},
|
||||
"10.221.153.165", // if we ever have to change this, we should probably also mark it as a breaking change
|
||||
},
|
||||
{
|
||||
"assign in ipv6 net",
|
||||
args{ipv6net, "test"},
|
||||
"2001:db8:c575:7277:b806:e994:13dd:99a5", // if we ever have to change this, we should probably also mark it as a breaking change
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
wg := &WgState{}
|
||||
wg.AssignOverlayAddr(tt.args.ipnet, tt.args.name)
|
||||
|
||||
if !reflect.DeepEqual(wg.OverlayAddr.IP.String(), tt.want) {
|
||||
t.Errorf("assignOverlayAddr() set = %s, want %s", wg.OverlayAddr, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// This is just to ensure - if we ever change the hashing function - that it spreads the results in a way that at least
|
||||
// avoids the most obvious collisions.
|
||||
func Test_wgState_assignOverlayAddr_no_obvious_collisions(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("10.0.0.0/24")
|
||||
assignments := make(map[string]string)
|
||||
for _, n := range []string{"test", "test1", "test2", "1test", "2test"} {
|
||||
wg := &WgState{}
|
||||
wg.AssignOverlayAddr(ipnet, n)
|
||||
if assigned, ok := assignments[wg.OverlayAddr.String()]; ok {
|
||||
t.Errorf("IP assignment collision: hash(%s) = hash(%s)", n, assigned)
|
||||
}
|
||||
assignments[wg.OverlayAddr.String()] = n
|
||||
}
|
||||
}
|
||||
|
||||
// This should ensure the obvious fact that the same name should map to the same IP if called twice.
|
||||
func Test_wgState_assignOverlayAddr_consistent(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("10.0.0.0/8")
|
||||
wg1 := &WgState{}
|
||||
wg1.AssignOverlayAddr(ipnet, "test")
|
||||
wg2 := &WgState{}
|
||||
wg2.AssignOverlayAddr(ipnet, "test")
|
||||
if wg1.OverlayAddr.String() != wg2.OverlayAddr.String() {
|
||||
t.Errorf("assignOverlayAddr() %s != %s", wg1.OverlayAddr, wg2.OverlayAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_wgState_assignOverlayAddr_repeatable(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("10.0.0.0/8")
|
||||
wg := &WgState{}
|
||||
wg.AssignOverlayAddr(ipnet, "test")
|
||||
gen1 := wg.OverlayAddr.String()
|
||||
wg.AssignOverlayAddr(ipnet, "test")
|
||||
gen2 := wg.OverlayAddr.String()
|
||||
if gen1 != gen2 {
|
||||
t.Errorf("assignOverlayAddr() %s != %s", gen1, gen2)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user