Skip to content

Commit

Permalink
Split out dsnet lib functions
Browse files Browse the repository at this point in the history
This change moves the core features of dsnet into it's own lib.
This means it's no longer tied to the CLI and instead offers a common
way for implementations to manage WG with dsnet
  • Loading branch information
botto committed Dec 31, 2021
1 parent 2f56c2f commit e8c0e5e
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 0 deletions.
69 changes: 69 additions & 0 deletions lib/lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package lib

import (
"fmt"

"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

func (s *Server) Up() error {
if err := s.CreateLink(); err != nil {
return err
}
return s.ConfigureDevice()
}

// ConfigureDevice sets up the WG interface
func (s *Server) ConfigureDevice() error {
wg, err := wgctrl.New()
if err != nil {
return err
}
defer wg.Close()

dev, err := wg.Device(s.InterfaceName)

if err != nil {
return fmt.Errorf("could not retrieve device '%s' (%v)", s.InterfaceName, err)
}

peers := s.GetPeers()

// compare peers to see if any exist on the device and not the config. If
// so, they should be removed by appending a dummy peer with Remove:true + pubkey.
knownKeys := make(map[wgtypes.Key]bool)

for _, peer := range peers {
knownKeys[peer.PublicKey] = true
}

// find deleted peers, and append dummy "remove" peers
for _, peer := range dev.Peers {
if !knownKeys[peer.PublicKey] {
peers = append(peers, wgtypes.PeerConfig{
PublicKey: peer.PublicKey,
Remove: true,
})
}
}

wgConfig := wgtypes.Config{
PrivateKey: &s.PrivateKey.Key,
ListenPort: &s.ListenPort,
// ReplacePeers with the same peers results in those peers losing
// connection, so it's not possible to do declarative configuration
// idempotently with ReplacePeers like I had assumed. Instead, peers
// must be removed imperatively with Remove:true. Peers can still be
// added/updated with ConfigureDevice declaratively.
ReplacePeers: false,
Peers: peers,
}

err = wg.ConfigureDevice(s.InterfaceName, wgConfig)

if err != nil {
return fmt.Errorf("could not configure device '%s' (%v)", s.InterfaceName, err)
}
return nil
}
87 changes: 87 additions & 0 deletions lib/link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package lib

import (
"errors"
"fmt"
"net"

"github.com/naggie/dsnet/utils"
"github.com/vishvananda/netlink"
)

// CreateLink sets up the WG interface and link with the correct
// address
func (s *Server) CreateLink() error {
if len(s.IP) == 0 && len(s.IP6) == 0 {
return errors.New("no IPv4 or IPv6 ip defined in config")
}

linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = s.InterfaceName

link := &netlink.GenericLink{
LinkAttrs: linkAttrs,
LinkType: "wireguard",
}

err := netlink.LinkAdd(link)
if err != nil && s.FallbackWGBin != "" {
// return fmt.Errorf("could not add interface '%s' (%v), falling back to the userspace implementation", s.InterfaceName, err)
cmdStr := fmt.Sprintf("%s %s", s.FallbackWGBin, s.InterfaceName)
if err = utils.ShellOut(cmdStr, "Userspace implementation"); err != nil {
return fmt.Errorf("failed to start userspace wireguard: %s", err)
}
}

if len(s.IP) != 0 {
addr := &netlink.Addr{
IPNet: &net.IPNet{
IP: s.IP,
Mask: s.Network.IPNet.Mask,
},
}

err = netlink.AddrAdd(link, addr)
if err != nil {
return fmt.Errorf("could not add ipv4 addr %s to interface %s", addr.IP, err)
}
}

if len(s.IP6) != 0 {
addr6 := &netlink.Addr{
IPNet: &net.IPNet{
IP: s.IP6,
Mask: s.Network6.IPNet.Mask,
},
}

err = netlink.AddrAdd(link, addr6)
if err != nil {
return fmt.Errorf("could not add ipv6 addr %s to interface %s", addr6.IP, err)
}
}

// bring up interface (UNKNOWN state instead of UP, a wireguard quirk)
err = netlink.LinkSetUp(link)

if err != nil {
return fmt.Errorf("could not bring up device '%s' (%v)", s.InterfaceName, err)
}
return nil
}

// DeleteLink removes the Netlink interface
func (s *Server) DeleteLink() error {
linkAttrs := netlink.NewLinkAttrs()
linkAttrs.Name = s.InterfaceName

link := &netlink.GenericLink{
LinkAttrs: linkAttrs,
}

err := netlink.LinkDel(link)
if err != nil {
return fmt.Errorf("failed to delete interface(%s): %s", s.InterfaceName, err)
}
return nil
}
19 changes: 19 additions & 0 deletions lib/peer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lib

import (
"net"
"time"
)

type Peer struct {
Hostname string
Owner string
Description string
IP net.IP
IP6 net.IP
Added time.Time
PublicKey JSONKey
PrivateKey JSONKey
PresharedKey JSONKey
Networks []JSONIPNet
}
75 changes: 75 additions & 0 deletions lib/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package lib

import (
"net"

"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

type Server struct {
ExternalHostname string
ExternalIP net.IP
ExternalIP6 net.IP
ListenPort int
Domain string
InterfaceName string
Network JSONIPNet
Network6 JSONIPNet
IP net.IP
IP6 net.IP
DNS net.IP
PrivateKey JSONKey
PostUp string
PostDown string
FallbackWGBin string
Peers []Peer
}

func (s *Server) GetPeers() []wgtypes.PeerConfig {
wgPeers := make([]wgtypes.PeerConfig, 0, len(s.Peers))

for _, peer := range s.Peers {
// create a new PSK in memory to avoid passing the same value by
// pointer to each peer (d'oh)
presharedKey := peer.PresharedKey.Key

// AllowedIPs = private IP + defined networks
allowedIPs := make([]net.IPNet, 0, len(peer.Networks)+2)

if len(peer.IP) > 0 {
allowedIPs = append(
allowedIPs,
net.IPNet{
IP: peer.IP,
Mask: net.IPMask{255, 255, 255, 255},
},
)
}

if len(peer.IP6) > 0 {
allowedIPs = append(
allowedIPs,
net.IPNet{
IP: peer.IP6,
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
)
}

for _, net := range peer.Networks {
allowedIPs = append(allowedIPs, net.IPNet)
}

wgPeers = append(wgPeers, wgtypes.PeerConfig{
PublicKey: peer.PublicKey.Key,
Remove: false,
UpdateOnly: false,
PresharedKey: &presharedKey,
Endpoint: nil,
ReplaceAllowedIPs: true,
AllowedIPs: allowedIPs,
})
}

return wgPeers
}
90 changes: 90 additions & 0 deletions lib/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package lib

import (
"fmt"
"net"
"strings"

"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

type JSONIPNet struct {
IPNet net.IPNet
}

func (n JSONIPNet) MarshalJSON() ([]byte, error) {
if len(n.IPNet.IP) == 0 {
return []byte("\"\""), nil
} else {
return []byte("\"" + n.IPNet.String() + "\""), nil
}
}

func (n *JSONIPNet) UnmarshalJSON(b []byte) error {
cidr := strings.Trim(string(b), "\"")

if cidr == "" {
// Leave as empty/uninitialised IPNet. A bit like omitempty behaviour,
// but we can leave the field there and blank which is useful if the
// user wishes to add the cidr manually.
return nil
}

IP, IPNet, err := net.ParseCIDR(cidr)

if err == nil {
IPNet.IP = IP
n.IPNet = *IPNet
}

return err
}

func (n *JSONIPNet) String() string {
return n.IPNet.String()
}

type JSONKey struct {
Key wgtypes.Key
}

func (k JSONKey) MarshalJSON() ([]byte, error) {
return []byte("\"" + k.Key.String() + "\""), nil
}

func (k JSONKey) PublicKey() JSONKey {
return JSONKey{
Key: k.Key.PublicKey(),
}
}

func (k *JSONKey) UnmarshalJSON(b []byte) error {
b64Key := strings.Trim(string(b), "\"")
key, err := wgtypes.ParseKey(b64Key)
k.Key = key
return err
}

func GenerateJSONPrivateKey() (JSONKey, error) {
privateKey, err := wgtypes.GeneratePrivateKey()

if err != nil {
return JSONKey{}, fmt.Errorf("failed to generate private key: %s", err)
}

return JSONKey{
Key: privateKey,
}, nil
}

func GenerateJSONKey() (JSONKey, error) {
privateKey, err := wgtypes.GenerateKey()

if err != nil {
return JSONKey{}, fmt.Errorf("failed to generate key: %s", err)
}

return JSONKey{
Key: privateKey,
}, nil
}

0 comments on commit e8c0e5e

Please sign in to comment.