first working PoC
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										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