first working PoC

This commit is contained in:
Leo Antunes 2019-03-25 01:02:10 +01:00
parent 902372ed9b
commit 5e01de00c1
8 changed files with 606 additions and 0 deletions

5
.gitignore vendored
View File

@ -4,9 +4,14 @@
*.dll
*.so
*.dylib
wesher
wg
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Misc
*.tmp

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# wesher
Mesh overlay network manager, using [wireguard](https://www.wireguard.com/).
**⚠ WARNING**: since mesh membership is controlled by a mesh-wide pre-shared key, this effectively downgrades some of the
security benefits from wireguard. See "security considerations" below for more info.
## Quickstart
Before starting, make sure [wireguard](https://www.wireguard.com/) is installed on all nodes.
Install `wesher` on all nodes with:
```
$ go get github.com/costela/wesher
```
On the first node (assuming `$GOPATH/bin` is in the `$PATH`):
```
# wesher
```
Running the command above on a terminal will currently output a generated cluster key, like:
```
new cluster key generated: XXXXX
```
Then, on any further node:
```
# wesher --clusterkey XXXXX --joinaddrs x.x.x.x
```
Where `XXXXX` is the base64 encoded 32 bit key printed by the step above and `x.x.x.x` is the hostname or IP of any of
the nodes already joined to the mesh cluster.
*Note*: `wireguard`, and therefore `wesher`, need root access.
## Overview
## Configuration options
## Security considerations
The decision of whom to allow in the mesh is made by [memberlist](github.com/hashicorp/memberlist) and is secured by a
cluster-wide pre-shared key.
Compromise of this key will allow an attacker to:
- access services exposed on the overlay network
- impersonate and/or disrupt traffic to/from other nodes
It will not, however, allow the attacker access to decrypt the traffic between other nodes.
This pre-shared key is currently static, set up during cluster bootstrapping, but will - in a future version - be
rotated.
## Current known limitations
### Overlay IP collisions
Since the assignment of IPs on the overlay network is currently decided by the individual node and implemented as a
naive hashing of the hostname, there can be no guarantee two hosts will not generate the same overlay IPs.
This limitation may be worked around in a future version.

239
cluster.go Normal file
View File

@ -0,0 +1,239 @@
package main
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path"
"time"
"github.com/mattn/go-isatty"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/memberlist"
"github.com/sirupsen/logrus"
)
// ClusterState keeps track of information needed to rejoin the cluster
type ClusterState struct {
ClusterKey []byte
Nodes []node
}
type cluster struct {
localName string // used to avoid LocalNode(); should not change
ml *memberlist.Memberlist
wg *wgState
state *ClusterState
events chan memberlist.NodeEvent
}
const statePath = "/var/lib/wesher/state.json"
func newCluster(config *config, wg *wgState) (*cluster, error) {
clusterKey := config.ClusterKey
state := loadState()
if len(clusterKey) == 0 {
clusterKey = state.ClusterKey
}
if len(clusterKey) == 0 {
clusterKey = make([]byte, clusterKeyLen)
_, err := rand.Read(clusterKey)
if err != nil {
return nil, err
}
// TODO: refactor this into subcommand ("showkey"?)
if isatty.IsTerminal(os.Stdout.Fd()) {
fmt.Printf("new cluster key generated: %s\n", base64.StdEncoding.EncodeToString(clusterKey))
}
}
state.ClusterKey = clusterKey
mlConfig := memberlist.DefaultWANConfig()
mlConfig.LogOutput = logrus.StandardLogger().WriterLevel(logrus.DebugLevel)
mlConfig.SecretKey = clusterKey
mlConfig.BindAddr = config.BindAddr
mlConfig.BindPort = config.ClusterPort
mlConfig.AdvertisePort = config.ClusterPort
if config.UseIPAsName && config.BindAddr != "0.0.0.0" {
mlConfig.Name = config.BindAddr
}
ml, err := memberlist.Create(mlConfig)
if err != nil {
return nil, err
}
cluster := cluster{
localName: ml.LocalNode().Name,
ml: ml,
wg: wg,
events: make(chan memberlist.NodeEvent, 1),
state: state,
}
mlConfig.Conflict = &cluster
mlConfig.Events = &memberlist.ChannelEventDelegate{Ch: cluster.events}
mlConfig.Delegate = &cluster
wg.assignIP((*net.IPNet)(config.OverlayNet), cluster.localName)
ml.UpdateNode(1 * time.Second) // we currently do not update after creation
return &cluster, nil
}
func (c *cluster) NotifyConflict(node, other *memberlist.Node) {
logrus.Errorf("node name conflict detected: %s", other.Name)
}
// none if these are used
func (c *cluster) NotifyMsg([]byte) {}
func (c *cluster) GetBroadcasts(overhead, limit int) [][]byte { return nil }
func (c *cluster) LocalState(join bool) []byte { return nil }
func (c *cluster) MergeRemoteState(buf []byte, join bool) {}
type nodeMeta struct {
OverlayAddr net.IP
PubKey string
}
func (c *cluster) NodeMeta(limit int) []byte {
buf := &bytes.Buffer{}
if err := gob.NewEncoder(buf).Encode(nodeMeta{
OverlayAddr: c.wg.OverlayAddr,
PubKey: c.wg.PubKey,
}); err != nil {
logrus.Errorf("could not encode local state: %s", err)
return nil
}
if buf.Len() > limit {
logrus.Errorf("could not fit node metadata into %d bytes", limit)
return nil
}
return buf.Bytes()
}
func decodeNodeMeta(b []byte) (nodeMeta, error) {
nm := nodeMeta{}
if err := gob.NewDecoder(bytes.NewReader(b)).Decode(&nm); err != nil {
return nm, errwrap.Wrapf("could not decode: {{err}}", err)
}
return nm, nil
}
func (c *cluster) join(addrs []string) error {
if len(addrs) == 0 {
for _, n := range c.state.Nodes {
addrs = append(addrs, n.Addr.String())
}
}
if _, err := c.ml.Join(addrs); err != nil {
return err
} else if len(addrs) > 0 && c.ml.NumMembers() < 2 {
return errors.New("could not join to any of the provided addresses")
}
return nil
}
func (c *cluster) leave() {
c.saveState()
c.ml.Leave(10 * time.Second)
c.ml.Shutdown() // ignore errors
}
func (c *cluster) members() (<-chan []node, <-chan error) {
changes := make(chan []node)
errc := make(chan error, 1)
go func() {
for {
event := <-c.events
if event.Node.Name == c.localName {
// ignore events about ourselves
continue
}
switch event.Event {
case memberlist.NodeJoin:
logrus.Infof("node %s joined", event.Node)
case memberlist.NodeUpdate:
logrus.Infof("node %s updated", event.Node)
case memberlist.NodeLeave:
logrus.Infof("node %s left", event.Node)
}
nodes := make([]node, 0)
var errs error
for _, n := range c.ml.Members() {
if n.Name == c.localName {
continue
}
meta, err := decodeNodeMeta(n.Meta)
if err != nil {
errs = multierror.Append(errs, err)
continue
}
nodes = append(nodes, node{
Addr: n.Addr,
nodeMeta: meta,
})
}
c.state.Nodes = nodes
changes <- nodes
if errs != nil {
errc <- errs
}
c.saveState()
}
}()
return changes, errc
}
type node struct {
Addr net.IP
nodeMeta
}
func (n *node) String() string {
return n.Addr.String()
}
func (c *cluster) saveState() error {
if err := os.MkdirAll(path.Dir(statePath), 0700); err != nil {
return err
}
stateOut, err := json.MarshalIndent(c.state, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(statePath, stateOut, 0700)
}
func loadState() *ClusterState {
content, err := ioutil.ReadFile(statePath)
if err != nil {
if !os.IsNotExist(err) {
logrus.Warnf("could not open state in %s: %s", statePath, err)
}
return &ClusterState{}
}
s := &ClusterState{}
if err := json.Unmarshal(content, s); err != nil {
logrus.Warnf("could not decode state: %s", err)
return &ClusterState{} // avoid partially unmarshalled content
}
return s
}

55
config.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"fmt"
"net"
"github.com/stevenroose/gonfig"
)
const clusterKeyLen = 32
type config struct {
LogLevel string `desc:"set the verbosity (debug/info/warn/error)" default:"warn"`
ClusterKey []byte `desc:"shared key for cluster membership; must be 32 bytes base64 encoded; will be generated if not provided"`
JoinAddrs []string `desc:"comma separated list of IP addresses to at least one existing cluster member; if not provided, will attempt resuming any known state or otherwise wait for further members."`
BindAddr string `desc:"IP address to bind to for cluster membership" default:"0.0.0.0"`
ClusterPort int `desc:"port used for membership gossip traffic (both TCP and UDP); must be the same across cluster" default:"7946"`
WireguardPort int `desc:"port used for wireguard traffic (UDP); must be the same across cluster" default:"51820"`
OverlayNet *network `desc:"the network in which to allocate addresses for the overlay mesh network (CIDR format); smaller networks increase the chance of IP collision" default:"10.0.0.0/8"`
InterfaceName string `desc:"name of the wireguard interface to create and manage" default:"wgoverlay"`
// for easier local testing
UseIPAsName bool `default:"false" opts:"hidden"`
}
func loadConfig() (*config, error) {
var config config
err := gonfig.Load(&config, gonfig.Conf{EnvPrefix: "WESHER_"})
if err != nil {
return nil, err
}
// perform some validation
if len(config.ClusterKey) != 0 && len(config.ClusterKey) != clusterKeyLen {
return nil, fmt.Errorf("unsupported cluster key length; expected %d, got %d", clusterKeyLen, len(config.ClusterKey))
}
if bits, _ := ((*net.IPNet)(config.OverlayNet)).Mask.Size(); bits%8 != 0 {
return nil, fmt.Errorf("unsupported overlay network size; net mask must be multiple of 8, got %d", bits)
}
return &config, nil
}
type network net.IPNet
// UnmarshalText parses the provided byte array into the network receiver
func (n *network) UnmarshalText(data []byte) error {
_, ipnet, err := net.ParseCIDR(string(data))
if err != nil {
return err
}
*n = network(*ipnet)
return nil
}

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module github.com/costela/wesher
require (
github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/memberlist v0.1.3
github.com/mattn/go-isatty v0.0.7
github.com/sirupsen/logrus v1.3.0
github.com/stevenroose/gonfig v0.1.4
)

57
go.sum Normal file
View File

@ -0,0 +1,57 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/stevenroose/gonfig v0.1.4 h1:oaMK7zCihVqlPIHXHNwDT9Hl/tH09RALGQ8TmUuYdl0=
github.com/stevenroose/gonfig v0.1.4/go.mod h1:JBkjIE8NdLbRNBowFCgK7wirNR0GHhnRhtdJgZMIylM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 h1:x6r4Jo0KNzOOzYd8lbcRsqjuqEASK6ob3auvWYM4/8U=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

63
main.go Normal file
View File

@ -0,0 +1,63 @@
package main // import "github.com/costela/wesher"
import (
"os"
"os/signal"
"syscall"
"github.com/sirupsen/logrus"
)
func main() {
config, err := loadConfig()
if err != nil {
logrus.Fatal(err)
}
logLevel, err := logrus.ParseLevel(config.LogLevel)
if err != nil {
logrus.Fatalf("could not parse loglevel: %s", err)
}
logrus.SetLevel(logLevel)
wg, err := newWGConfig(config.InterfaceName, config.WireguardPort)
if err != nil {
logrus.Fatal(err)
}
cluster, err := newCluster(config, wg)
if err != nil {
logrus.Fatalf("could not create cluster: %s", err)
}
nodec, errc := cluster.members() // avoid deadlocks by starting before join
if err := cluster.join(config.JoinAddrs); err != nil {
logrus.Fatalf("could not join cluster: %s", err)
}
incomingSigs := make(chan os.Signal, 1)
signal.Notify(incomingSigs, syscall.SIGTERM, os.Interrupt)
for {
select {
case nodes := <-nodec:
logrus.Info("cluster members:\n")
for _, node := range nodes {
logrus.Infof("\taddr: %s, overlay: %s, pubkey: %s", node.Addr, node.OverlayAddr, node.PubKey)
}
if err := wg.downInterface(); err != nil {
logrus.Errorf("could not down interface: %s", err)
}
if err := wg.writeConf(nodes); err != nil {
logrus.Errorf("could not write config: %s", err)
}
if err := wg.upInterface(); err != nil {
logrus.Errorf("could not up interface: %s", err)
}
case errs := <-errc:
logrus.Errorf("could not receive node info: %s", errs)
case <-incomingSigs:
logrus.Info("terminating...")
cluster.leave()
os.Exit(0)
}
}
}

117
wireguard.go Normal file
View File

@ -0,0 +1,117 @@
package main
import (
"crypto/md5"
"fmt"
"net"
"os"
"os/exec"
"strings"
"text/template"
)
const wgConfPath = "/etc/wireguard/%s.conf"
const wgConfTpl = `
# this file was generated automatically by wesher - DO NOT MODIFY
[Interface]
PrivateKey = {{ .PrivKey }}
Address = {{ .OverlayAddr }}
ListenPort = {{ .Port }}
{{ range .Nodes }}
[Peer]
PublicKey = {{ .PubKey }}
Endpoint = {{ .Addr }}:{{ $.Port }}
AllowedIPs = {{ .OverlayAddr }}/32
{{ end }}`
type wgState struct {
iface string
OverlayAddr net.IP
Port int
PrivKey string
PubKey string
}
func newWGConfig(iface string, port int) (*wgState, error) {
if err := exec.Command("wg").Run(); err != nil {
return nil, fmt.Errorf("could not exec wireguard: %s", err)
}
privKey, pubKey, err := wgKeyPair()
if err != nil {
return nil, err
}
wgState := wgState{
iface: iface,
Port: port,
PrivKey: privKey,
PubKey: pubKey,
}
return &wgState, nil
}
func (wg *wgState) assignIP(ipnet *net.IPNet, name string) {
// TODO: this is way too brittle and opaque
ip := []byte(ipnet.IP)
bits, size := ipnet.Mask.Size()
h := md5.New()
h.Write([]byte(name))
hb := h.Sum(nil)
for i := 0; i < (size-bits)/8; i++ {
ip[size/8-i-1] = hb[i]
}
wg.OverlayAddr = net.IP(ip)
}
func (wg *wgState) writeConf(nodes []node) error {
tpl := template.Must(template.New("wgconf").Parse(wgConfTpl))
out, err := os.OpenFile(
fmt.Sprintf(wgConfPath, wg.iface),
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
0600,
)
if err != nil {
return err
}
return tpl.Execute(out, struct {
*wgState
Nodes []node
}{wg, nodes})
}
func (wg *wgState) downInterface() error {
if err := exec.Command("wg-quick", "down", wg.iface).Run(); err != nil {
return err
}
return nil
}
func (wg *wgState) upInterface() error {
if err := exec.Command("wg-quick", "up", wg.iface).Run(); err != nil {
return err
}
return nil
}
func wgKeyPair() (string, string, error) {
cmd := exec.Command("wg", "genkey")
outPriv := strings.Builder{}
cmd.Stdout = &outPriv
if err := cmd.Run(); err != nil {
return "", "", err
}
cmd = exec.Command("wg", "pubkey")
outPub := strings.Builder{}
cmd.Stdout = &outPub
cmd.Stdin = strings.NewReader(outPriv.String())
if err := cmd.Run(); err != nil {
return "", "", err
}
return strings.TrimSpace(outPriv.String()), strings.TrimSpace(outPub.String()), nil
}