first working PoC
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,9 +4,14 @@ | |||||||
| *.dll | *.dll | ||||||
| *.so | *.so | ||||||
| *.dylib | *.dylib | ||||||
|  | wesher | ||||||
|  | wg | ||||||
|  |  | ||||||
| # Test binary, build with `go test -c` | # Test binary, build with `go test -c` | ||||||
| *.test | *.test | ||||||
|  |  | ||||||
| # Output of the go coverage tool, specifically when used with LiteIDE | # Output of the go coverage tool, specifically when used with LiteIDE | ||||||
| *.out | *.out | ||||||
|  |  | ||||||
|  | # Misc | ||||||
|  | *.tmp | ||||||
|   | |||||||
							
								
								
									
										60
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								README.md
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										239
									
								
								cluster.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										55
									
								
								config.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										10
									
								
								go.mod
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										57
									
								
								go.sum
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										63
									
								
								main.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										117
									
								
								wireguard.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Leo Antunes
					Leo Antunes