first working PoC
This commit is contained in:
parent
902372ed9b
commit
5e01de00c1
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue