├── .github └── workflows │ └── go.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── bootstrap.go ├── cmd.go ├── defaults.go ├── help.go ├── info.go ├── join.go ├── rtt.go ├── tags.go ├── ui.go └── util.go ├── config └── config.go ├── docs ├── cli-params.md ├── config.md ├── features-and-non-features.md ├── multipass-demo-setup.md ├── tags.md ├── tls.md └── wgmesh-ui-sample.png ├── go.mod ├── go.sum ├── meshservice ├── agent.go ├── agent.pb.go ├── agent.proto ├── agent_grpc.pb.go ├── export.go ├── grpc.go ├── interface.go ├── meshservice.go ├── meshservice.pb.go ├── meshservice.proto ├── meshservice_grpc.pb.go ├── serf.go ├── serf_events.go ├── stun.go ├── tls.go └── ui.go ├── scripts ├── .gitignore ├── cert-sample-2 │ ├── README.md │ ├── ca2.csr │ ├── join2-csr.json │ └── join2.csr ├── cert-sample │ ├── README.md │ ├── bootstrap-csr.json │ ├── bootstrap.csr │ ├── ca-config.json │ ├── ca-csr.json │ ├── ca-csr2.json │ ├── ca.csr │ ├── join-csr.json │ └── join.csr ├── multipass-cloudinit.yaml ├── nodejs-dns-zonefile │ ├── README.md │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── sample-template.dns └── wgmesh.service ├── web ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── assets │ └── logo.png │ ├── components │ ├── Navbar.vue │ └── NodesTable.vue │ ├── main.js │ └── pages │ ├── Home.vue │ └── NotFound.vue └── wgmesh.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.15 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | - name: Build 33 | run: go build wgmesh.go 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | wgmesh 17 | .vscode/ 18 | 19 | dist/ 20 | 21 | node_modules/ 22 | BACKLOG.md 23 | wgmesh.conf.* -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod download 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - linux 11 | hooks: 12 | pre: make gen web 13 | post: rice append -v -i github.com/aschmidt75/wgmesh/meshservice --exec {{ .Path }} 14 | archives: 15 | - replacements: 16 | linux: Linux 17 | 386: i386 18 | amd64: x86_64 19 | checksum: 20 | name_template: 'checksums.txt' 21 | snapshot: 22 | name_template: "{{ .Tag }}-next" 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := `git describe --tags` 2 | SOURCES ?= $(shell find . -name "*.go" -type f) 3 | BINARY_NAME = wgmesh 4 | NOW = `date +"%Y-%m-%d_%H-%M-%S"` 5 | MAIN_GO_PATH=wgmesh.go 6 | 7 | all: clean lint build web webappend 8 | 9 | .PHONY: build 10 | build: gen 11 | CGO_ENABLED=0 GOOS=linux go build -i -v -o dist/${BINARY_NAME} ${MAIN_GO_PATH} 12 | 13 | .PHONY: web 14 | web: 15 | ( cd web && npm install && npm run build ) 16 | 17 | .PHONY: staticcheck 18 | staticcheck: 19 | staticcheck ./... 20 | 21 | .PHONY: lint 22 | lint: 23 | @for file in ${SOURCES} ; do \ 24 | golint $$file ; \ 25 | done 26 | 27 | .PHONY: gen 28 | gen: 29 | (cd meshservice ; protoc --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --go_out=. --go-grpc_out=. meshservice.proto agent.proto) 30 | 31 | .PHONY: webappend 32 | webappend: 33 | ( cd meshservice && rice append --exec ../dist/${BINARY_NAME} ) 34 | 35 | .PHONY: release 36 | release: 37 | goreleaser --snapshot --rm-dist 38 | 39 | .PHONY: clean 40 | clean: 41 | rm -rf dist/* 42 | rm -fr web/dist 43 | rm -f cover.out 44 | go clean -testcache 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgmesh 2 | 3 | Automatically build private wireguard mesh networks. 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/aschmidt75/wgmesh)](https://goreportcard.com/report/github.com/aschmidt75/wgmesh) 6 | [![Go](https://github.com/aschmidt75/wgmesh/actions/workflows/go.yml/badge.svg)](https://github.com/aschmidt75/wgmesh/actions/workflows/go.yml) 7 | 8 | ![wgmesh Dashboard](docs/wgmesh-ui-sample.png) 9 | 10 | ## How it works 11 | 12 | * A mesh consist of interconnected nodes. Each node has a [wireguard](https://www.wireguard.com/) interface with all other nodes registered as peers. The mesh is fully-connected through the wireguard-based overlay network. Each node can IP-reach all other nodes via direct host routes. 13 | * At least one of the mesh nodes is a bootstrap node. Besides the wireguard peerings it runs a gRPC-based mesh endpoint, where other nodes can issue join requests. 14 | * New nodes can enter the mesh by joining via a bootstrap node. The bootstrap node learns about the joining node and its wireguard endpoint and public key. It distributes this information to the other mesh nodes. It also fowards a list of mesh peers to the new joining node so it is able to configure its own wireguard interface. 15 | * Existing mesh nodes learn about the new joining node from the bootstrap node and update their wireguard peer configuration accordingly. 16 | * Mesh nodes integrate [serf.io](https://serf.io) to maintain state about the network topology, learn about new nodes joining and failing nodes or nodes leaving the mesh. This is done using serf's encrypted gossip-based cluster membership protocol. 17 | * The bootstrap node's gRPC endpoint runs TLS with requesting and validating client certificates. This way new nodes are required to authenticate themselves by x.509 certificates. 18 | 19 | ## Build 20 | 21 | The default targets of `Makefile` generate protobuf and grpc parts and build the binary in `dist/`. It also builds the web ui (and needs node/npm for this), and 22 | appends the web artifacts using [go.rice](https://github.com/GeertJohan/go.rice). 23 | 24 | ```bash 25 | $ make all 26 | ``` 27 | 28 | The binary can be built without web ui support: 29 | 30 | ```bash 31 | $ make clean gen build 32 | ``` 33 | 34 | Additionally, goreleaser can be used to create a snapshot release for different platforms (also in `dist/`) 35 | 36 | ```bash 37 | $ make release 38 | ``` 39 | 40 | So for a full, releaseable build one needs 41 | * go (tested w/ version go1.15.5 darwin/amd64) 42 | * [rice](https://github.com/GeertJohan/go.rice) 43 | * node (tested w/ v14.16.0) 44 | * npm (tested w/ 6.14.11) 45 | * [goreleaser](https://github.com/goreleaser/goreleaser) (tested w/ version 0.155.0) 46 | 47 | ## Prerequisites 48 | 49 | * Linux 50 | * Wireguard module installed/enabled in kernel 51 | * Works best with a non-NATed setup. It can partially work with NAT, but there's no guarantee. 52 | 53 | ## Usage 54 | 55 | The fastest way to start a mesh is using the development mode, either with a bunch of local virtual machine or with available cloud instances. 56 | [This walkthrough](docs/multipass-demo-setup.md) shows how to use it with local multipass-based ubuntu lts instances. 57 | 58 | The mesh is initiated with a first bootstrap node. It creates a wireguard interface and starts listening for join requests on a gRPC endpoint. Make sure that the bootstrap node is not behind a NAT. In development mode, no security/TLS/mesh encryption is enforced, so all other nodes can join with authentication. This simplifies testing but is not suitable for non-development purposes. 59 | 60 | ```bash 61 | # wgmesh bootstrap -dev 62 | 63 | ** Mesh name: xoJbYw07PM 64 | ** Mesh CIDR range: 10.232.0.0/16 65 | ** gRPC Service listener endpoint: 0.0.0.0:5000 66 | ** This node's name: xoJbYw07PMAE80101 67 | ** This node's mesh IP: 10.232.1.1 68 | ** 69 | ** This mesh is running in DEVELOPMENT MODE without encryption. 70 | ** Do not use this in a production setup. 71 | ** 72 | ** To have another node join this mesh, use this command: 73 | ** wgmesh join -v -dev -n xoJbYw07PM -bootstrap-addr :5000 74 | ** 75 | ** To inspect the wireguard interface and its peer data use: 76 | ** wg show wgxoJbYw07PM 77 | ** 78 | ** To inspect the current mesh status use: wgmesh info 79 | ** 80 | ``` 81 | 82 | For other nodes to join, the (public or private) IP address of the bootstrap node is needed, so joining nodes are able to connect. Switch to a second instance and run the join command as stated above: 83 | 84 | ```bash 85 | # wgmesh join -v -dev -n xoJbYw07PM -bootstrap-addr 10.0.0.0:5000 86 | 87 | INFO[2021/02/27 10:46:31] Fetching external IP from STUN server 88 | INFO[2021/02/27 10:46:31] Using external IP when connecting with mesh ip= 89 | INFO[2021/02/27 10:46:31] Created and configured wireguard interface wgxoJbYw07PM as no-up 90 | WARN[2021/02/27 10:46:31] Using insecure connection to gRPC mesh service 91 | INFO[2021/02/27 10:46:32] Starting gRPC Agent Service at /var/run/wgmesh.sock 92 | ** 93 | ** Mesh 'xoJbYw07PM' has been joined. 94 | ** 95 | ** Mesh name: xoJbYw07PM 96 | ** Mesh CIDR range: 10.232.0.0/16 97 | ** This node's name: xoJbYw07PMAE8B8DB 98 | ** This node's mesh IP: 10.232.184.219 99 | ** 100 | ** This mesh is running in DEVELOPMENT MODE without encryption. 101 | ** Do not use this in a production setup. 102 | ** 103 | ** To inspect the wireguard interface and its peer data use: 104 | ** wg show wgxoJbYw07PM 105 | ** 106 | ** To inspect the current mesh status use: wgmesh info 107 | ** 108 | INFO[2021/02/27 10:46:33] Mesh has 2 nodes 109 | ``` 110 | 111 | Additional nodes can join using the same `join` command. 112 | 113 | On any node, the `info` command prints out connected nodes: 114 | 115 | ```bash 116 | # wgmesh info 117 | Mesh 'xoJbYw07PM' has 2 nodes, started 2021-02-27 10:45:31 +0100 CET 118 | This node 'xoJbYw07PMAE8B8DB' joined 2021-02-27 10:46:31 +0100 CET 119 | 120 | Name |Address |Status |RTT |Tags | 121 | xoJbYw07PMAE8B8DB |10.232.184.219 |alive |7 | _addr=, _port=54540, | 122 | xoJbYw07PMAE80101 |10.232.1.1 |alive |38 | _addr=, _port=54540, | 123 | ``` 124 | 125 | The `ui` command start an HTTP server, serving a simple, vue-based dashboard. By default it binds 126 | to port 9095 on the localhost interface only. It does not authenticate clients and does not (yet) support TLS. 127 | 128 | ```bash 129 | # wgmesh ui 130 | Serving files on 127.0.0.1:9095, press ctrl-C to exit 131 | ``` 132 | 133 | ## License 134 | 135 | (C) 2020,2021 @aschmidt75 136 | Apache License, Version 2.0 137 | 138 | Wireguard ist a registered trademark of Jason A. Donenfeld / [wireguard.com](https://wireguard.com) 139 | -------------------------------------------------------------------------------- /cmd/bootstrap.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/base64" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "net" 10 | "os" 11 | "os/signal" 12 | "regexp" 13 | "syscall" 14 | "time" 15 | 16 | config "github.com/aschmidt75/wgmesh/config" 17 | meshservice "github.com/aschmidt75/wgmesh/meshservice" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | // BootstrapCommand struct 22 | type BootstrapCommand struct { 23 | CommandDefaults 24 | 25 | fs *flag.FlagSet 26 | 27 | // configuration file 28 | config string 29 | // configuration struct 30 | meshConfig config.Config 31 | 32 | // options not in config, only from parameters 33 | devMode bool 34 | } 35 | 36 | // NewBootstrapCommand creates the Bootstrap Command 37 | func NewBootstrapCommand() *BootstrapCommand { 38 | c := &BootstrapCommand{ 39 | CommandDefaults: NewCommandDefaults(), 40 | 41 | config: envStrWithDefault("WGMESH_CONFIG", ""), 42 | meshConfig: config.NewDefaultConfig(), 43 | 44 | fs: flag.NewFlagSet("bootstrap", flag.ContinueOnError), 45 | devMode: false, 46 | } 47 | 48 | c.fs.StringVar(&c.config, "config", c.config, "file name of config file (optional).\nenv:WGMESH_cONFIG") 49 | c.fs.StringVar(&c.meshConfig.MeshName, "name", c.meshConfig.MeshName, "name of the mesh network.\nenv:WGMESH_MESH_NAME") 50 | c.fs.StringVar(&c.meshConfig.MeshName, "n", c.meshConfig.MeshName, "name of the mesh network (short).\nenv:WGMESH_MESH_NAME") 51 | c.fs.StringVar(&c.meshConfig.NodeName, "node-name", c.meshConfig.NodeName, "(optional) name of this node.\nenv:WGMESH_NODE_NAME") 52 | c.fs.StringVar(&c.meshConfig.Bootstrap.MeshCIDRRange, "cidr", c.meshConfig.Bootstrap.MeshCIDRRange, "CIDR range of this mesh (internal ips).\nenv:WGMESH_CIDR_RANGE") 53 | c.fs.StringVar(&c.meshConfig.Bootstrap.MeshIPAMCIDRRange, "cidr-ipam", c.meshConfig.Bootstrap.MeshIPAMCIDRRange, "CIDR (sub)range where this bootstrap mode may allocate ips from. Must be within -cidr range.\nenv:WGMESH_CIDR_RANGE_IPAM") 54 | c.fs.StringVar(&c.meshConfig.Bootstrap.NodeIP, "ip", c.meshConfig.Bootstrap.NodeIP, "internal ip of the bootstrap node. Must be set fixed for bootstrap nodes.\nenv:WGMESH_MESH_IP") 55 | c.fs.StringVar(&c.meshConfig.Wireguard.ListenAddr, "listen-addr", c.meshConfig.Wireguard.ListenAddr, "external wireguard ip.\nenv:WGMESH_WIREGUARD_LISTEN_ADDR") 56 | c.fs.IntVar(&c.meshConfig.Wireguard.ListenPort, "listen-port", c.meshConfig.Wireguard.ListenPort, "set the (external) wireguard listen port.\nenv:WGMESH_WIREGUARD_LISTEN_PORT") 57 | c.fs.StringVar(&c.meshConfig.Bootstrap.GRPCBindAddr, "grpc-bind-addr", c.meshConfig.Bootstrap.GRPCBindAddr, "(public) address to bind grpc mesh service to.\nenv:WGMESH_GRPC_BIND_ADDR") 58 | c.fs.IntVar(&c.meshConfig.Bootstrap.GRPCBindPort, "grpc-bind-port", c.meshConfig.Bootstrap.GRPCBindPort, "port to bind grpc mesh service to.\nenv:WGMESH_GRPC_BIND_PORT") 59 | c.fs.StringVar(&c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerKey, "grpc-server-key", c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerKey, "points to PEM-encoded private key to be used by grpc server.\nenv:WGMESH_SERVER_KEY") 60 | c.fs.StringVar(&c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerCert, "grpc-server-cert", c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerCert, "points to PEM-encoded certificate be used by grpc server.\nenv:WGMESH_SERVER_CERT") 61 | c.fs.StringVar(&c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert, "grpc-ca-cert", c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert, "points to PEM-encoded CA certificate.\nenv:WGMESH_CA_CERT") 62 | c.fs.StringVar(&c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath, "grpc-ca-path", c.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath, "points to a directory containing PEM-encoded CA certificates.\nenv:WGMESH_CA_PATH") 63 | c.fs.StringVar(&c.meshConfig.MemberlistFile, "memberlist-file", c.meshConfig.MemberlistFile, "optional name of file for a log of all current mesh members.\nenv:WGMESH_MEMBERLIST_FILE") 64 | c.fs.StringVar(&c.meshConfig.Bootstrap.MeshEncryptionKey, "mesh-encryption-key", c.meshConfig.Bootstrap.MeshEncryptionKey, "optional key for symmetric encryption of internal mesh traffic. Must be 32 Bytes base64-ed.\nenv:WGMESH_ENCRYPTION_KEY") 65 | c.fs.BoolVar(&c.devMode, "dev", c.devMode, "Enables development mode which runs without encryption, authentication and without TLS") 66 | c.fs.BoolVar(&c.meshConfig.Bootstrap.SerfModeLAN, "serf-mode-lan", c.meshConfig.Bootstrap.SerfModeLAN, "Activates LAN mode or cluster communication. Default is false (=WAN mode).\nenv:WGMESH_SERF_MODE_LAN") 67 | c.fs.StringVar(&c.meshConfig.Agent.GRPCBindSocket, "agent-grpc-bind-socket", c.meshConfig.Agent.GRPCBindSocket, "local socket file to bind grpc agent to.\nenv:WGMESH_AGENT_BIND_SOCKET") 68 | c.fs.StringVar(&c.meshConfig.Agent.GRPCBindSocketIDs, "agent-grpc-bind-socket-id", c.meshConfig.Agent.GRPCBindSocketIDs, " to change bind socket to.\nenv:WGMESH_AGENT_BIND_SOCKET_ID") 69 | c.DefaultFields(c.fs) 70 | 71 | return c 72 | } 73 | 74 | // Name returns the name of the command 75 | func (g *BootstrapCommand) Name() string { 76 | return g.fs.Name() 77 | } 78 | 79 | // Init sets up the command struct from arguments 80 | func (g *BootstrapCommand) Init(args []string) error { 81 | err := g.fs.Parse(args) 82 | if err != nil { 83 | return err 84 | } 85 | g.ProcessDefaults() 86 | 87 | // load config file if we have one 88 | if g.config != "" { 89 | err = g.meshConfig.LoadConfigFromFile(g.config) 90 | if err != nil { 91 | log.WithError(err).Error("Config read error") 92 | return fmt.Errorf("Unable to read configuration from %s", g.config) 93 | } 94 | } 95 | 96 | err = g.fs.Parse(args) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | log.WithField("cfg", g.meshConfig).Trace("Read") 102 | log.WithField("cfg.bootstrap", g.meshConfig.Bootstrap).Trace("Read") 103 | log.WithField("cfg.wireguard", g.meshConfig.Wireguard).Trace("Read") 104 | log.WithField("cfg.agent", g.meshConfig.Agent).Trace("Read") 105 | 106 | // validate given parameters/config 107 | 108 | if g.meshConfig.MeshName != "" && len(g.meshConfig.MeshName) > 10 { 109 | return errors.New("mesh name (--name, -n, mesh-name) must have maximum length of 10") 110 | } 111 | 112 | _, _, err = net.ParseCIDR(g.meshConfig.Bootstrap.MeshCIDRRange) 113 | if err != nil { 114 | return fmt.Errorf("%s is not a valid cidr range for -cidr / bootstrap.mesh-cidr-range", g.meshConfig.Bootstrap.MeshCIDRRange) 115 | } 116 | 117 | if net.ParseIP(g.meshConfig.Bootstrap.NodeIP) == nil { 118 | return fmt.Errorf("%s is not a valid ip for -ip", g.meshConfig.Bootstrap.NodeIP) 119 | } 120 | 121 | // ip must be a local one 122 | if pr, _ := isPrivateIP(g.meshConfig.Bootstrap.NodeIP); pr == false { 123 | return fmt.Errorf("-ip %s is not RFC1918, must be a private address", g.meshConfig.Bootstrap.NodeIP) 124 | } 125 | 126 | if g.meshConfig.Wireguard.ListenPort < 0 || g.meshConfig.Wireguard.ListenPort > 65535 { 127 | return fmt.Errorf("%d is not valid for -listen-port", g.meshConfig.Wireguard.ListenPort) 128 | } 129 | 130 | if net.ParseIP(g.meshConfig.Bootstrap.GRPCBindAddr) == nil { 131 | return fmt.Errorf("%s is not a valid ip for -grpc-bind-addr", g.meshConfig.Bootstrap.GRPCBindAddr) 132 | } 133 | 134 | if g.meshConfig.Bootstrap.GRPCBindPort < 0 || g.meshConfig.Bootstrap.GRPCBindPort > 65535 { 135 | return fmt.Errorf("%d is not valid for -grpc-bind-port", g.meshConfig.Bootstrap.GRPCBindPort) 136 | } 137 | 138 | if g.meshConfig.Agent.GRPCBindSocketIDs != "" { 139 | re := regexp.MustCompile(`^[0-9]+:[0-9]+$`) 140 | 141 | if !re.Match([]byte(g.meshConfig.Agent.GRPCBindSocketIDs)) { 142 | return fmt.Errorf("%s is not valid for -grpc-bind-socket-id", g.meshConfig.Agent.GRPCBindSocketIDs) 143 | } 144 | } 145 | 146 | if g.meshConfig.Bootstrap.MeshEncryptionKey != "" { 147 | b, err := base64.StdEncoding.DecodeString(g.meshConfig.Bootstrap.MeshEncryptionKey) 148 | if err != nil || len(b) != 32 { 149 | return fmt.Errorf("%s is not valid for -mesh-encryption-key, must be 32 bytes, base64-encoded", g.meshConfig.Bootstrap.MeshEncryptionKey) 150 | } 151 | } 152 | 153 | withGrpcSecure := false 154 | if g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerKey != "" { 155 | withGrpcSecure = true 156 | 157 | if !fileExists(g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerKey) { 158 | return fmt.Errorf("%s not found for -grpc-server-key", g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerKey) 159 | } 160 | } 161 | if g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerCert != "" { 162 | withGrpcSecure = true 163 | 164 | if !fileExists(g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerCert) { 165 | return fmt.Errorf("%s not found for -grpc-server-cert", g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerCert) 166 | } 167 | } 168 | if g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert != "" { 169 | withGrpcSecure = true 170 | 171 | if !fileExists(g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert) { 172 | return fmt.Errorf("%s not found for -grpc-ca-cert", g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert) 173 | } 174 | } 175 | if g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath != "" { 176 | withGrpcSecure = true 177 | 178 | if !dirExists(g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath) { 179 | return fmt.Errorf("%s not found for -grpc-ca-path", g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath) 180 | } 181 | } 182 | 183 | // when secure setup is desired .. 184 | if withGrpcSecure { 185 | // then we need these settings.. 186 | if g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerKey == "" || g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCServerCert == "" || (g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert == "" && g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath == "") { 187 | // 188 | return fmt.Errorf("-grpc-server-key, -grpc-server-cert, -grpc-ca-cert / -grp-ca-path must be specified together") 189 | } 190 | // and we cannot have dev mode also 191 | if g.devMode { 192 | return fmt.Errorf("Must either set -dev mode for insecure setup or -grpc-server-key, -grpc-server-cert, -grpc-ca-cert / -grp-ca-path must be specified together") 193 | } 194 | } else { 195 | // in a non-secure setup we definitely need -dev mode 196 | if !g.devMode { 197 | return fmt.Errorf("Must either set -dev mode for insecure setup or -grpc-server-key, -grpc-server-cert, -grpc-ca-cert / -grp-ca-path must be specified together") 198 | } 199 | } 200 | 201 | if g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaCert != "" && g.meshConfig.Bootstrap.GRPCTLSConfig.GRPCCaPath != "" { 202 | return fmt.Errorf("-grpc-ca-cert / -grp-ca-path are mutually exclusive") 203 | } 204 | 205 | if g.devMode { 206 | if withGrpcSecure || g.meshConfig.Bootstrap.MeshEncryptionKey != "" { 207 | return fmt.Errorf("cannot combine security parameters -mesh-encryption-key in -dev mode") 208 | } 209 | } 210 | 211 | return nil 212 | } 213 | 214 | // Run runs the command by creating the wireguard interface, 215 | // starting the serf cluster and grpc server 216 | func (g *BootstrapCommand) Run() error { 217 | log.WithField("g", g).Trace( 218 | "Running cli command", 219 | ) 220 | 221 | cfg := g.meshConfig 222 | 223 | // if mesh name is empty 224 | if cfg.MeshName == "" { 225 | cfg.MeshName = randomMeshName() 226 | log.WithField("meshName", cfg.MeshName).Warn("auto-generated mesh name. Use this as -n parameter when joining the mesh.") 227 | } 228 | 229 | ms := meshservice.NewMeshService(cfg.MeshName) 230 | log.WithField("ms", ms).Trace( 231 | "created", 232 | ) 233 | ms.SetMemberlistExportFile(cfg.MemberlistFile) 234 | 235 | // Set serf encryption key when given and we're not in dev mode 236 | if !g.devMode && cfg.Bootstrap.MeshEncryptionKey != "" { 237 | ms.SetEncryptionKey(cfg.Bootstrap.MeshEncryptionKey) 238 | } 239 | 240 | var wgListenAddr net.IP 241 | if cfg.Wireguard.ListenAddr == "" { 242 | 243 | st := meshservice.NewSTUNService() 244 | ips, err := st.GetExternalIP() 245 | 246 | if err != nil { 247 | return err 248 | } 249 | if len(ips) > 0 { 250 | wgListenAddr = ips[0] 251 | log.WithField("ip", wgListenAddr).Info("Using external IP when connecting with mesh") 252 | } 253 | } 254 | if wgListenAddr == nil { 255 | wgListenAddr = getIPFromIPOrIntfParam(cfg.Wireguard.ListenAddr) 256 | log.WithField("ip", wgListenAddr).Trace("parsed -listen-addr") 257 | if wgListenAddr == nil { 258 | return errors.New("need -listen-addr") 259 | } 260 | } 261 | // TODO make sure wgListenAddr matches one of the local interfaces addresses 262 | 263 | _, cidrRangeIpnet, err := net.ParseCIDR(cfg.Bootstrap.MeshCIDRRange) 264 | if err != nil { 265 | return err 266 | } 267 | ms.CIDRRange = *cidrRangeIpnet 268 | 269 | if cfg.Bootstrap.MeshIPAMCIDRRange != "" { 270 | _, cidrRangeIPAMIpnet, err := net.ParseCIDR(cfg.Bootstrap.MeshIPAMCIDRRange) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | // TODO check if this is within cidr range above.. 276 | 277 | ms.CIDRRangeIPAM = cidrRangeIPAMIpnet 278 | 279 | } 280 | 281 | // MeshIP ist composed of what user specifies using -ip, but 282 | // with the net mask of -cidr. e.g. 10.232.0.0/16 with an 283 | // IP of 10.232.5.99 becomes 10.232.5.99/16 284 | ms.MeshIP = net.IPNet{ 285 | IP: net.ParseIP(cfg.Bootstrap.NodeIP), 286 | Mask: ms.CIDRRange.Mask, 287 | } 288 | log.WithField("meship", ms.MeshIP).Trace("using mesh ip") 289 | 290 | // - prepare wireguard interface 291 | pk, err := g.wireguardSetup(&ms, wgListenAddr) 292 | if err != nil { 293 | err2 := ms.RemoveWireguardInterfaceForMesh() 294 | if err2 != nil { 295 | return err2 296 | } 297 | return err 298 | } 299 | // remove wg interface in all cases - at errors 300 | // or at the end of this func. 301 | defer func() { 302 | ms.RemoveWireguardInterfaceForMesh() 303 | }() 304 | 305 | // set up serf 306 | err = g.serfSetup(&ms, pk, wgListenAddr) 307 | if err != nil { 308 | return err 309 | } 310 | 311 | // set up external gRPC interface, be able to listen 312 | // for join requests 313 | if err = g.grpcSetup(&ms); err != nil { 314 | return err 315 | } 316 | 317 | // we created this mesh now 318 | ms.SetTimestamps(time.Now().Unix(), time.Now().Unix()) 319 | 320 | // print out user information on how to connect to this mesh 321 | 322 | fmt.Printf("** \n") 323 | fmt.Printf("** Mesh '%s' has been bootstrapped. Other nodes can join now.\n", cfg.MeshName) 324 | fmt.Printf("** \n") 325 | fmt.Printf("** Mesh name: %s\n", cfg.MeshName) 326 | fmt.Printf("** Mesh CIDR range: %s\n", ms.CIDRRange.String()) 327 | fmt.Printf("** gRPC Service listener endpoint: %s:%d\n", ms.GrpcBindAddr, ms.GrpcBindPort) 328 | fmt.Printf("** This node's name: %s\n", ms.NodeName) 329 | fmt.Printf("** This node's mesh IP: %s\n", ms.MeshIP.IP.String()) 330 | if cfg.MemberlistFile != "" { 331 | fmt.Printf("** Mesh node details export to: %s\n", cfg.MemberlistFile) 332 | } 333 | fmt.Printf("** \n") 334 | if g.devMode { 335 | fmt.Printf("** This mesh is running in DEVELOPMENT MODE without encryption.\n") 336 | fmt.Printf("** Do not use this in a production setup.\n") 337 | fmt.Printf("** \n") 338 | fmt.Printf("** To have another node join this mesh, use this command:\n") 339 | ba := ms.GrpcBindAddr 340 | if ba == "0.0.0.0" { 341 | ba = "" 342 | } 343 | fmt.Printf("** wgmesh join -v -dev -n %s -bootstrap-addr %s:%d\n", cfg.MeshName, ba, ms.GrpcBindPort) 344 | fmt.Printf("** \n") 345 | } else { 346 | if ms.TLSConfig != nil && len(ms.TLSConfig.Cert.Certificate) > 0 { 347 | fmt.Printf("** TLS is enabled for gRPC mesh service\n") 348 | 349 | x, err := x509.ParseCertificate(ms.TLSConfig.Cert.Certificate[0]) 350 | if err == nil { 351 | fmt.Printf("** subject: %s\n", x.Subject) 352 | fmt.Printf("** issuer: %s\n", x.Issuer) 353 | } 354 | } 355 | } 356 | fmt.Printf("** \n") 357 | fmt.Printf("** To inspect the wireguard interface and its peer data use:\n") 358 | fmt.Printf("** wg show %s\n", ms.WireguardInterface.InterfaceName) 359 | fmt.Printf("** \n") 360 | fmt.Printf("** To inspect the current mesh status use: wgmesh info\n") 361 | fmt.Printf("** \n") 362 | 363 | // wait until stopped 364 | g.wait() 365 | 366 | // clean up everything 367 | if err = g.cleanUp(&ms); err != nil { 368 | return err 369 | } 370 | 371 | return nil 372 | } 373 | 374 | // wireguardSetup creates the wireguard interface from parameters. Returns 375 | // the private key to be shared with the mesh 376 | func (g *BootstrapCommand) wireguardSetup(ms *meshservice.MeshService, wgListenAddr net.IP) (pk string, err error) { 377 | cfg := g.meshConfig 378 | 379 | // From the given IP and listen port, create the wireguard interface 380 | // and set up a basic configuration for it. Up the interface 381 | pk, err = ms.CreateWireguardInterfaceForMesh(cfg.Bootstrap.NodeIP, cfg.Wireguard.ListenPort) 382 | if err != nil { 383 | return "", err 384 | } 385 | ms.WireguardPubKey = pk 386 | ms.WireguardListenPort = cfg.Wireguard.ListenPort 387 | ms.WireguardListenIP = wgListenAddr 388 | 389 | // add a route so that all traffic regarding fiven cidr range 390 | // goes to the wireguard interface. 391 | err = ms.SetRoute() 392 | if err != nil { 393 | return "", err 394 | } 395 | 396 | ms.SetNodeName(g.meshConfig.NodeName) 397 | 398 | return pk, nil 399 | } 400 | 401 | // serfSetup initializes the serf cluster from parameters 402 | func (g *BootstrapCommand) serfSetup(ms *meshservice.MeshService, pk string, wgListenAddr net.IP) (err error) { 403 | cfg := g.meshConfig 404 | 405 | // create and start the serf cluster 406 | ms.NewSerfCluster(cfg.Bootstrap.SerfModeLAN) 407 | 408 | err = ms.StartSerfCluster( 409 | true, 410 | pk, 411 | wgListenAddr.String(), 412 | cfg.Wireguard.ListenPort, 413 | ms.MeshIP.IP.String()) 414 | if err != nil { 415 | return err 416 | } 417 | 418 | ms.StartStatsUpdater() 419 | 420 | return nil 421 | } 422 | 423 | // GrpcSetup ... 424 | func (g *BootstrapCommand) grpcSetup(ms *meshservice.MeshService) (err error) { 425 | cfg := g.meshConfig 426 | 427 | // set up TLS config from parameter unless we're in dev mode 428 | if !g.devMode { 429 | ms.TLSConfig, err = meshservice.NewTLSConfigFromFiles( 430 | cfg.Bootstrap.GRPCTLSConfig.GRPCCaCert, 431 | cfg.Bootstrap.GRPCTLSConfig.GRPCCaPath, 432 | cfg.Bootstrap.GRPCTLSConfig.GRPCServerCert, 433 | cfg.Bootstrap.GRPCTLSConfig.GRPCServerKey) 434 | if err != nil { 435 | return err 436 | } 437 | } 438 | 439 | // set up grpc mesh service 440 | ms.GrpcBindAddr = cfg.Bootstrap.GRPCBindAddr 441 | ms.GrpcBindPort = cfg.Bootstrap.GRPCBindPort 442 | 443 | go func() { 444 | log.Infof("Starting gRPC mesh Service at %s:%d", ms.GrpcBindAddr, ms.GrpcBindPort) 445 | err = ms.StartGrpcService() 446 | if err != nil { 447 | log.Error(err) 448 | } 449 | }() 450 | 451 | // start the local agent if argument is given 452 | if cfg.Agent.GRPCBindSocket != "" { 453 | ms.MeshAgentServer = meshservice.NewMeshAgentServerSocket(ms, cfg.Agent.GRPCBindSocket, cfg.Agent.GRPCBindSocketIDs) 454 | log.WithField("mas", ms.MeshAgentServer).Trace("agent") 455 | go func() { 456 | log.Infof("Starting gRPC Agent Service at %s", cfg.Agent.GRPCBindSocket) 457 | err = ms.MeshAgentServer.StartAgentGrpcService() 458 | if err != nil { 459 | log.Error(err) 460 | } 461 | }() 462 | } 463 | 464 | return nil 465 | } 466 | 467 | // waits until being stopped 468 | func (g *BootstrapCommand) wait() { 469 | stopCh := make(chan struct{}) 470 | sigc := make(chan os.Signal, 1) 471 | signal.Notify(sigc, 472 | syscall.SIGINT, 473 | syscall.SIGTERM, 474 | syscall.SIGQUIT) 475 | go func() { 476 | <-sigc 477 | stopCh <- struct{}{} 478 | }() 479 | 480 | <-stopCh 481 | } 482 | 483 | // CleanUp takes down all internal services and cleans up 484 | // interfaces, sockets etc. 485 | func (g *BootstrapCommand) cleanUp(ms *meshservice.MeshService) error { 486 | cfg := g.meshConfig 487 | 488 | ms.MeshAgentServer.StopAgentGrpcService() 489 | 490 | ms.LeaveSerfCluster() 491 | 492 | ms.StopGrpcService() 493 | 494 | // delete memberlist-file 495 | os.Remove(cfg.MemberlistFile) 496 | 497 | // Wireguard will be removed by deferred func 498 | 499 | return nil 500 | } 501 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | // Runner is able to execute a single command 12 | type Runner interface { 13 | Init([]string) error 14 | Run() error 15 | Name() string 16 | } 17 | 18 | // VersionInfo captures version information injected 19 | // by the build process 20 | type VersionInfo struct { 21 | Version string 22 | Commit string 23 | Date string 24 | } 25 | 26 | var cmds = []Runner{ 27 | NewBootstrapCommand(), 28 | NewJoinCommand(), 29 | NewTagsCommand(), 30 | NewRTTCommand(), 31 | NewInfoCommand(), 32 | NewUICommand(), 33 | } 34 | 35 | // ProcessCommands takes the command line arguments and 36 | // starts the processing according to the above defined commands 37 | func ProcessCommands(args []string, vi VersionInfo) error { 38 | if len(args) < 1 { 39 | DisplayHelp(vi) 40 | return errors.New("please use one of the above commands") 41 | } 42 | 43 | subcommand := os.Args[1] 44 | if subcommand == "version" { 45 | fmt.Printf("wgmesh %s (%s) - %s\n", vi.Version, vi.Commit, vi.Date) 46 | fmt.Printf("(C) 2021 @aschmidt75 Apache License, Version 2.0\n") 47 | os.Exit(0) 48 | } 49 | 50 | for _, cmd := range cmds { 51 | if cmd.Name() == subcommand { 52 | err := cmd.Init(os.Args[2:]) 53 | if err != nil { 54 | return err 55 | } 56 | return cmd.Run() 57 | } 58 | } 59 | 60 | return fmt.Errorf("Unknown subcommand: %s", subcommand) 61 | } 62 | 63 | func fileExists(filename string) bool { 64 | info, err := os.Stat(filename) 65 | if os.IsNotExist(err) { 66 | return false 67 | } 68 | return !info.IsDir() 69 | } 70 | 71 | func dirExists(filename string) bool { 72 | info, err := os.Stat(filename) 73 | if os.IsNotExist(err) { 74 | return false 75 | } 76 | return info.IsDir() 77 | } 78 | 79 | func envStrWithDefault(key string, defaultValue string) string { 80 | res := os.Getenv(key) 81 | if res == "" { 82 | return defaultValue 83 | } 84 | return res 85 | } 86 | 87 | func envBoolWithDefault(key string, defaultValue bool) bool { 88 | res := os.Getenv(key) 89 | if res == "" { 90 | return defaultValue 91 | } 92 | if res == "1" || res == "true" || res == "on" { 93 | return true 94 | } 95 | return false 96 | } 97 | 98 | func envIntWithDefault(key string, defaultValue int) int { 99 | res := os.Getenv(key) 100 | if res == "" { 101 | return defaultValue 102 | } 103 | v, err := strconv.Atoi(res) 104 | if err != nil { 105 | return -1 106 | } 107 | return v 108 | } 109 | 110 | func randomMeshName() string { 111 | chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 112 | b := make([]byte, 10) 113 | for i := range b { 114 | b[i] = chars[rand.Intn(len(chars))] 115 | } 116 | return string(b) 117 | } 118 | -------------------------------------------------------------------------------- /cmd/defaults.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // CommandDefaults struct defines a FlagSet and 11 | // shared flags for all commands 12 | type CommandDefaults struct { 13 | verbose, debug bool 14 | } 15 | 16 | // NewCommandDefaults returns the defaults 17 | func NewCommandDefaults() CommandDefaults { 18 | return CommandDefaults{ 19 | debug: false, 20 | verbose: false, 21 | } 22 | } 23 | 24 | // DefaultFields handles default fields in given flag set 25 | func (c *CommandDefaults) DefaultFields(fs *flag.FlagSet) { 26 | fs.BoolVar(&c.verbose, "v", c.verbose, "show more output") 27 | fs.BoolVar(&c.debug, "d", c.debug, "show debug output") 28 | } 29 | 30 | // ProcessDefaults sets logging and other defaults 31 | func (c *CommandDefaults) ProcessDefaults() { 32 | 33 | log.SetLevel(log.ErrorLevel) 34 | if c.debug { 35 | log.SetLevel(log.DebugLevel) 36 | } 37 | if c.verbose { 38 | log.SetLevel(log.InfoLevel) 39 | } 40 | 41 | if os.Getenv("WGMESH_TRACE") != "" { 42 | log.SetLevel(log.TraceLevel) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /cmd/help.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // DisplayHelp shows the main help text 8 | func DisplayHelp(vi VersionInfo) { 9 | fmt.Printf("wgmesh %s (%s) - %s", vi.Version, vi.Commit, vi.Date) 10 | fmt.Println() 11 | fmt.Println(" bootstrap Starts a bootstrap node") 12 | fmt.Println(" join Joins a mesh network by connecting to a bootstrap node") 13 | fmt.Println(" info Print out information about the mesh and its nodes") 14 | fmt.Println(" tags Set or remove tags on nodes") 15 | fmt.Println(" rtt Query RTTs for all nodes") 16 | fmt.Println(" ui Starts the web user interface") 17 | fmt.Println() 18 | } 19 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "text/tabwriter" 10 | "time" 11 | 12 | config "github.com/aschmidt75/wgmesh/config" 13 | meshservice "github.com/aschmidt75/wgmesh/meshservice" 14 | log "github.com/sirupsen/logrus" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | // InfoCommand struct 19 | type InfoCommand struct { 20 | CommandDefaults 21 | 22 | fs *flag.FlagSet 23 | 24 | // configuration file 25 | config string 26 | // configuration struct 27 | meshConfig config.Config 28 | 29 | // options not in config, only from parameters 30 | watchFlag bool 31 | } 32 | 33 | // NewInfoCommand creates the Info Command structure and sets the parameters 34 | func NewInfoCommand() *InfoCommand { 35 | c := &InfoCommand{ 36 | CommandDefaults: NewCommandDefaults(), 37 | config: envStrWithDefault("WGMESH_CONFIG", ""), 38 | meshConfig: config.NewDefaultConfig(), 39 | fs: flag.NewFlagSet("info", flag.ContinueOnError), 40 | watchFlag: false, 41 | } 42 | 43 | c.fs.StringVar(&c.config, "config", c.config, "file name of config file (optional).\nenv:WGMESH_cONFIG") 44 | c.fs.StringVar(&c.meshConfig.Agent.GRPCSocket, "agent-grpc-socket", c.meshConfig.Agent.GRPCSocket, "agent socket to dial") 45 | c.fs.BoolVar(&c.watchFlag, "watch", c.watchFlag, "watch for changes until interrupted") 46 | 47 | c.DefaultFields(c.fs) 48 | 49 | return c 50 | } 51 | 52 | // Name returns the name of the command 53 | func (g *InfoCommand) Name() string { 54 | return g.fs.Name() 55 | } 56 | 57 | // Init sets up the command struct from arguments 58 | func (g *InfoCommand) Init(args []string) error { 59 | err := g.fs.Parse(args) 60 | if err != nil { 61 | return err 62 | } 63 | g.ProcessDefaults() 64 | 65 | // load config file if we have one 66 | if g.config != "" { 67 | err = g.meshConfig.LoadConfigFromFile(g.config) 68 | if err != nil { 69 | log.WithError(err).Error("Config read error") 70 | return fmt.Errorf("Unable to read configuration from %s", g.config) 71 | } 72 | } 73 | 74 | err = g.fs.Parse(args) 75 | if err != nil { 76 | return err 77 | } 78 | log.WithField("cfg", g.meshConfig).Trace("Read") 79 | log.WithField("cfg.agent", g.meshConfig.Agent).Trace("Read") 80 | 81 | return nil 82 | } 83 | 84 | // Run queries the agent for Info info 85 | func (g *InfoCommand) Run() error { 86 | log.WithField("g", g).Trace( 87 | "Running cli command", 88 | ) 89 | 90 | // 91 | endpoint := fmt.Sprintf("unix://%s", g.meshConfig.Agent.GRPCSocket) 92 | 93 | conn, err := grpc.Dial(endpoint, grpc.WithInsecure(), grpc.WithBlock()) 94 | if err != nil { 95 | log.Error(err) 96 | return fmt.Errorf("cannot connect to %s", endpoint) 97 | } 98 | defer conn.Close() 99 | 100 | agent := meshservice.NewAgentClient(conn) 101 | log.WithField("agent", agent).Trace("got grpc service client") 102 | 103 | ctx0, cancel := context.WithTimeout(context.Background(), 2*time.Second) 104 | defer cancel() 105 | 106 | if g.watchFlag { 107 | 108 | g.singleCycle(ctx0, agent) 109 | 110 | for { 111 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 112 | defer cancel() 113 | 114 | r, err := agent.WaitForChangeInMesh(ctx, &meshservice.WaitInfo{ 115 | TimeoutSecs: 10, 116 | }) 117 | if err != nil { 118 | log.WithError(err).Error("error while waiting for mesh changes") 119 | break 120 | } 121 | for { 122 | wr, err := r.Recv() 123 | if err == io.EOF { 124 | break 125 | } 126 | if err != nil { 127 | log.WithError(err).Debug("error while waiting for mesh changes") 128 | break 129 | } 130 | log.WithField("wr", wr).Trace(".") 131 | 132 | if wr.WasTimeout { 133 | continue 134 | } 135 | if wr.ChangesOccured { 136 | time.Sleep(1 * time.Second) 137 | g.singleCycle(ctx, agent) 138 | } 139 | 140 | } 141 | } 142 | 143 | return nil 144 | } 145 | 146 | return g.singleCycle(ctx0, agent) 147 | } 148 | 149 | func (g *InfoCommand) singleCycle(ctx context.Context, agent meshservice.AgentClient) error { 150 | 151 | meshInfo, err := agent.Info(ctx, &meshservice.AgentEmpty{}) 152 | if err != nil { 153 | log.WithError(err).Error("Unable to query infos from agent") 154 | return err 155 | } 156 | 157 | fmt.Printf("Mesh '%s' has %d nodes, started %s\n", meshInfo.Name, meshInfo.NodeCount, time.Unix(int64(meshInfo.MeshCeationTS), 0)) 158 | fmt.Printf("This node '%s' joined %s\n", meshInfo.NodeName, time.Unix(int64(meshInfo.NodeJoinTS), 0)) 159 | 160 | r, err := agent.Nodes(ctx, &meshservice.AgentEmpty{}) 161 | if err != nil { 162 | log.WithError(err).Error("Unable to query nodes from agent") 163 | } 164 | 165 | fmt.Println() 166 | 167 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.Debug) 168 | 169 | fmt.Fprintln(w, "Name\tAddress\tStatus\tRTT\tTags\t") 170 | 171 | for { 172 | memberInfo, err := r.Recv() 173 | if err != nil { 174 | break 175 | } 176 | 177 | tagStr := "" 178 | for _, tag := range memberInfo.Tags { 179 | if tag.Key[0] != '_' { 180 | tagStr = fmt.Sprintf("%s %s=%s,", tagStr, tag.Key, tag.Value) 181 | } 182 | } 183 | 184 | fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t\n", memberInfo.NodeName, memberInfo.Addr, memberInfo.Status, memberInfo.RttMsec, tagStr) 185 | } 186 | w.Flush() 187 | 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /cmd/join.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "flag" 9 | "fmt" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "syscall" 17 | "time" 18 | 19 | wgwrapper "github.com/aschmidt75/go-wg-wrapper/pkg/wgwrapper" 20 | config "github.com/aschmidt75/wgmesh/config" 21 | meshservice "github.com/aschmidt75/wgmesh/meshservice" 22 | "github.com/cristalhq/jwt/v3" 23 | log "github.com/sirupsen/logrus" 24 | "google.golang.org/grpc" 25 | "google.golang.org/grpc/credentials" 26 | "google.golang.org/grpc/metadata" 27 | ) 28 | 29 | // JoinCommand struct 30 | type JoinCommand struct { 31 | CommandDefaults 32 | 33 | fs *flag.FlagSet 34 | 35 | // configuration file 36 | config string 37 | // configuration struct 38 | meshConfig config.Config 39 | 40 | // options not in config, only from parameters 41 | devMode bool 42 | } 43 | 44 | // NewJoinCommand creates the Join Command 45 | func NewJoinCommand() *JoinCommand { 46 | c := &JoinCommand{ 47 | CommandDefaults: NewCommandDefaults(), 48 | fs: flag.NewFlagSet("join", flag.ContinueOnError), 49 | 50 | config: envStrWithDefault("WGMESH_CONFIG", ""), 51 | meshConfig: config.NewDefaultConfig(), 52 | 53 | devMode: false, 54 | } 55 | 56 | c.fs.StringVar(&c.config, "config", c.config, "file name of config file (optional).\nenv:WGMESH_cONFIG") 57 | c.fs.StringVar(&c.meshConfig.MeshName, "name", c.meshConfig.MeshName, "name of the mesh network.\nenv:WGMESH_MESH_NAME") 58 | c.fs.StringVar(&c.meshConfig.MeshName, "n", c.meshConfig.MeshName, "name of the mesh network (short).\nenv:WGMESH_MESH_NAME") 59 | c.fs.StringVar(&c.meshConfig.NodeName, "node-name", c.meshConfig.NodeName, "(optional) name of this node.\nenv:WGMESH_NODE_NAME") 60 | c.fs.StringVar(&c.meshConfig.Join.BootstrapEndpoint, "bootstrap-addr", c.meshConfig.Join.BootstrapEndpoint, "IP:Port of remote mesh bootstrap node.\nenv:WGMESH_BOOTSTRAP_ADDR") 61 | c.fs.StringVar(&c.meshConfig.Wireguard.ListenAddr, "listen-addr", c.meshConfig.Wireguard.ListenAddr, "external wireguard ip.\nenv:WGMESH_WIREGUARD_LISTEN_ADDR") 62 | c.fs.IntVar(&c.meshConfig.Wireguard.ListenPort, "listen-port", c.meshConfig.Wireguard.ListenPort, "set the (external) wireguard listen port.\nenv:WGMESH_WIREGUARD_LISTEN_PORT") 63 | c.fs.StringVar(&c.meshConfig.Join.ClientKey, "client-key", c.meshConfig.Join.ClientKey, "points to PEM-encoded private key to be used.\nenv:WGMESH_CLIENT_KEY") 64 | c.fs.StringVar(&c.meshConfig.Join.ClientCert, "client-cert", c.meshConfig.Join.ClientCert, "points to PEM-encoded certificate be used.\nenv:WGMESH_CLIENT_CERT") 65 | c.fs.StringVar(&c.meshConfig.Join.ClientCaCert, "ca-cert", c.meshConfig.Join.ClientCaCert, "points to PEM-encoded CA certificate.\nenv:WGMESH_CA_CERT") 66 | c.fs.StringVar(&c.meshConfig.MemberlistFile, "memberlist-file", c.meshConfig.MemberlistFile, "optional name of file for a log of all current mesh members.\nenv:WGMESH_MEMBERLIST_FILE") 67 | c.fs.StringVar(&c.meshConfig.Agent.GRPCBindSocket, "agent-grpc-bind-socket", c.meshConfig.Agent.GRPCBindSocket, "local socket file to bind grpc agent to.\nenv:WGMESH_AGENT_BIND_SOCKET") 68 | c.fs.StringVar(&c.meshConfig.Agent.GRPCBindSocketIDs, "agent-grpc-bind-socket-id", c.meshConfig.Agent.GRPCBindSocketIDs, " to change bind socket to.\nenv:WGMESH_AGENT_BIND_SOCKET_ID") 69 | c.fs.BoolVar(&c.devMode, "dev", c.devMode, "Enables development mode which runs without encryption, authentication and without TLS") 70 | c.DefaultFields(c.fs) 71 | 72 | return c 73 | } 74 | 75 | // Name returns the name of the command 76 | func (g *JoinCommand) Name() string { 77 | return g.fs.Name() 78 | } 79 | 80 | // Init sets up the command struct from arguments 81 | func (g *JoinCommand) Init(args []string) error { 82 | err := g.fs.Parse(args) 83 | if err != nil { 84 | return err 85 | } 86 | g.ProcessDefaults() 87 | 88 | // load config file if we have one 89 | if g.config != "" { 90 | err = g.meshConfig.LoadConfigFromFile(g.config) 91 | if err != nil { 92 | log.WithError(err).Error("Config read error") 93 | return fmt.Errorf("Unable to read configuration from %s", g.config) 94 | } 95 | } 96 | 97 | err = g.fs.Parse(args) 98 | if err != nil { 99 | return err 100 | } 101 | log.WithField("cfg", g.meshConfig).Trace("Read") 102 | log.WithField("cfg.join", g.meshConfig.Join).Trace("Read") 103 | log.WithField("cfg.wireguard", g.meshConfig.Wireguard).Trace("Read") 104 | log.WithField("cfg.agent", g.meshConfig.Agent).Trace("Read") 105 | 106 | if g.meshConfig.MeshName == "" { 107 | return errors.New("mesh name (--name, -n) may not be empty") 108 | } 109 | if len(g.meshConfig.MeshName) > 10 { 110 | return errors.New("mesh name (--name, -n) must have maximum length of 10") 111 | } 112 | 113 | arr := strings.Split(g.meshConfig.Join.BootstrapEndpoint, ":") 114 | if len(arr) != 2 { 115 | return errors.New("-bootstrap-addr must be :") 116 | } 117 | if net.ParseIP(arr[0]) == nil { 118 | return fmt.Errorf("%s is not a valid ip for -bootstrap-addr", arr[0]) 119 | } 120 | _, err = strconv.Atoi(arr[1]) 121 | if err != nil { 122 | return fmt.Errorf("%s is not a valid port for -bootstrap-addr", arr[1]) 123 | } 124 | 125 | if g.meshConfig.Wireguard.ListenPort < 0 || g.meshConfig.Wireguard.ListenPort > 65535 { 126 | return fmt.Errorf("%d is not valid for -listen-port", g.meshConfig.Wireguard.ListenPort) 127 | } 128 | 129 | if g.meshConfig.Agent.GRPCBindSocketIDs != "" { 130 | re := regexp.MustCompile(`^[0-9]+:[0-9]+$`) 131 | 132 | if !re.Match([]byte(g.meshConfig.Agent.GRPCBindSocketIDs)) { 133 | return fmt.Errorf("%s is not valid for -grpc-bind-socket-id", g.meshConfig.Agent.GRPCBindSocketIDs) 134 | } 135 | } 136 | 137 | withGrpcSecure := false 138 | if g.meshConfig.Join.ClientKey != "" { 139 | withGrpcSecure = true 140 | 141 | if !fileExists(g.meshConfig.Join.ClientKey) { 142 | return fmt.Errorf("%s not found for -client-key", g.meshConfig.Join.ClientKey) 143 | } 144 | } 145 | if g.meshConfig.Join.ClientCert != "" { 146 | withGrpcSecure = true 147 | 148 | if !fileExists(g.meshConfig.Join.ClientCert) { 149 | return fmt.Errorf("%s not found for -client-cert", g.meshConfig.Join.ClientCert) 150 | } 151 | } 152 | if g.meshConfig.Join.ClientCaCert != "" { 153 | withGrpcSecure = true 154 | 155 | if !fileExists(g.meshConfig.Join.ClientCaCert) { 156 | return fmt.Errorf("%s not found for -ca-cert", g.meshConfig.Join.ClientCaCert) 157 | } 158 | } 159 | 160 | if withGrpcSecure { 161 | if g.meshConfig.Join.ClientKey == "" || g.meshConfig.Join.ClientCert == "" || g.meshConfig.Join.ClientCaCert == "" { 162 | // 163 | return fmt.Errorf("-client-key, -client-cert, -ca-cert must be specified together") 164 | } 165 | if g.devMode { 166 | return fmt.Errorf("Must either set -dev mode for insecure setup or -client-key, -client-cert, -ca-cert must be specified together") 167 | } 168 | } else { 169 | if !g.devMode { 170 | return fmt.Errorf("Must either set -dev mode for insecure setup or -client-key, -client-cert, -ca-cert must be specified together") 171 | } 172 | } 173 | 174 | if g.devMode { 175 | if withGrpcSecure { 176 | return fmt.Errorf("cannot combine security parameters in -dev mode") 177 | } 178 | } 179 | 180 | return nil 181 | } 182 | 183 | // Run runs the command by creating the wireguard interface, 184 | // starting the serf cluster and grpc server 185 | func (g *JoinCommand) Run() error { 186 | log.WithField("g", g).Trace( 187 | "Running cli command", 188 | ) 189 | 190 | cfg := g.meshConfig 191 | 192 | var listenIP net.IP 193 | if cfg.Wireguard.ListenAddr == "" { 194 | 195 | st := meshservice.NewSTUNService() 196 | ips, err := st.GetExternalIP() 197 | 198 | if err != nil { 199 | return err 200 | } 201 | if len(ips) > 0 { 202 | listenIP = ips[0] 203 | log.WithField("ip", listenIP).Info("Using external IP when connecting with mesh") 204 | 205 | } 206 | } 207 | if listenIP == nil { 208 | listenIP = getIPFromIPOrIntfParam(cfg.Wireguard.ListenAddr) 209 | log.WithField("ip", listenIP).Trace("parsed -listen-addr") 210 | if listenIP == nil { 211 | return errors.New("need -listen-addr") 212 | } 213 | 214 | } 215 | 216 | ms := meshservice.NewMeshService(cfg.MeshName) 217 | log.WithField("ms", ms).Trace("created") 218 | ms.WireguardListenIP = listenIP 219 | 220 | ms.SetMemberlistExportFile(cfg.MemberlistFile) 221 | 222 | pk, err := ms.CreateWireguardInterface(cfg.Wireguard.ListenPort) 223 | if err != nil { 224 | return err 225 | } 226 | // remove wg interface in all cases - at errors 227 | // or at the end of this func. 228 | defer func() { 229 | ms.RemoveWireguardInterfaceForMesh() 230 | }() 231 | ms.WireguardPubKey = pk 232 | 233 | // set up TLS configuration from given parameters unless we're in dev mode 234 | if !g.devMode { 235 | ms.TLSConfig, err = meshservice.NewTLSConfigFromFiles(cfg.Join.ClientCaCert, "", cfg.Join.ClientCert, cfg.Join.ClientKey) 236 | if err != nil { 237 | return err 238 | } 239 | } 240 | 241 | var opts []grpc.DialOption = []grpc.DialOption{ 242 | grpc.WithBlock(), 243 | grpc.WithTimeout(5 * time.Second), 244 | } 245 | if ms.TLSConfig != nil { 246 | transportCreds := credentials.NewTLS(&tls.Config{ 247 | Certificates: []tls.Certificate{ms.TLSConfig.Cert}, 248 | RootCAs: ms.TLSConfig.CertPool, 249 | }) 250 | 251 | opts = append(opts, grpc.WithTransportCredentials(transportCreds)) 252 | log.Debug("TLS-connecting to gRPC mesh service") 253 | 254 | } else { 255 | log.Warn("Using insecure connection to gRPC mesh service") 256 | opts = append(opts, grpc.WithInsecure()) 257 | } 258 | 259 | conn, err := grpc.Dial(g.meshConfig.Join.BootstrapEndpoint, opts...) 260 | if err != nil { 261 | log.Error(err) 262 | return fmt.Errorf("cannot connect to %s", g.meshConfig.Join.BootstrapEndpoint) 263 | } 264 | defer conn.Close() 265 | 266 | service := meshservice.NewMeshClient(conn) 267 | log.WithField("service", service).Trace("got grpc service") 268 | 269 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // TODO make configurable 270 | defer cancel() 271 | 272 | token, authResponses, err := g.handleHandshake(ctx, service, &ms) 273 | if err != nil { 274 | return err 275 | } 276 | // build a jwt containing the auth responses, signing it with received token 277 | signer, err := jwt.NewSignerHS(jwt.HS256, []byte(token)) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | now := time.Now() 283 | 284 | claims := &jwt.RegisteredClaims{ 285 | IssuedAt: jwt.NewNumericDate(now), 286 | NotBefore: jwt.NewNumericDate(now), 287 | ExpiresAt: jwt.NewNumericDate(now.Add(10 * time.Second)), 288 | Subject: strings.Join(authResponses, "::"), 289 | } 290 | 291 | builder := jwt.NewBuilder(signer) 292 | jwt, err := builder.Build(claims) 293 | if err != nil { 294 | return err 295 | } 296 | 297 | mdCtx := metadata.NewOutgoingContext(ctx, metadata.Pairs("authorization", fmt.Sprintf("Bearer: %s", jwt))) 298 | 299 | joinResponse, err := service.Join(mdCtx, &meshservice.JoinRequest{ 300 | Pubkey: ms.WireguardPubKey, 301 | EndpointIP: listenIP.String(), 302 | EndpointPort: int32(cfg.Wireguard.ListenPort), 303 | MeshName: cfg.MeshName, 304 | NodeName: cfg.NodeName, 305 | }) 306 | if err != nil { 307 | log.Error(err) 308 | 309 | // remove wireguard interface 310 | err = ms.RemoveWireguardInterfaceForMesh() 311 | if err != nil { 312 | log.Error(err) 313 | } 314 | return fmt.Errorf("cannot communicate with endpoint at %s", g.meshConfig.Join.BootstrapEndpoint) 315 | } 316 | log.WithField("jr", joinResponse).Trace("got joinResponse") 317 | 318 | // 319 | if joinResponse.Result == meshservice.JoinResponse_ERROR { 320 | log.Errorf("Unable to join mesh, message: '%s'. Exiting", joinResponse.ErrorMessage) 321 | return nil 322 | } 323 | 324 | ms.SetTimestamps(joinResponse.CreationTS, time.Now().Unix()) 325 | 326 | if !g.devMode { 327 | ms.SetEncryptionKey(string(joinResponse.SerfEncryptionKey)) 328 | } 329 | 330 | // MeshIP ist composed of what user specifies using -ip, but 331 | // with the net mask of -cidr. e.g. 10.232.0.0/16 with an 332 | // IP of 10.232.5.99 becomes 10.232.5.99/16 333 | ms.MeshIP = net.IPNet{ 334 | IP: net.ParseIP(joinResponse.JoiningNodeMeshIP), 335 | Mask: ms.CIDRRange.Mask, 336 | } 337 | log.WithField("meship", ms.MeshIP).Trace("using mesh ip") 338 | 339 | // we have been assigned a local IP for the wireguard interface. Apply it. 340 | err = ms.AssignJoiningNodeIP(joinResponse.JoiningNodeMeshIP) 341 | if err != nil { 342 | log.Error(err) 343 | log.Error("Unable assign mesh ip. Exiting") 344 | 345 | // TODO: inform bootstrap explicitly about this, because we're not able 346 | // to inform the cluster via gossip. Need to leave explicitly 347 | 348 | // take down interface 349 | err2 := ms.RemoveWireguardInterfaceForMesh() 350 | if err2 != nil { 351 | return err2 352 | } 353 | return err 354 | } 355 | 356 | // set my own node name. Can be empty, it is then derived from the 357 | // local mesh ip to have unique names within the serf cluster 358 | ms.SetNodeName(cfg.NodeName) 359 | 360 | // query the list of all peers. 361 | stream, err := service.Peers(ctx, &meshservice.Empty{}) 362 | if err != nil { 363 | return err 364 | } 365 | 366 | wg := wgwrapper.New() 367 | 368 | // apply peer updates. So we will have wireguard peerings 369 | // to all nodes before we join the serf cluster. 370 | meshPeerIPs := ms.ApplyPeerUpdatesFromStream(wg, stream) 371 | 372 | // the interface is fully configured, up it 373 | wg.SetInterfaceUp(ms.WireguardInterface) 374 | 375 | // Add a route to the CIDR range of the mesh. All detail data 376 | // comes from the join response 377 | _, meshCidr, _ := net.ParseCIDR(joinResponse.MeshCidr) 378 | ms.CIDRRange = *meshCidr 379 | err = ms.SetRoute() 380 | if err != nil { 381 | return err 382 | } 383 | 384 | // start the serf part. make it join all received peers 385 | err = g.serfSetup(&ms, listenIP, meshPeerIPs, joinResponse.SerfModeLAN) 386 | if err != nil { 387 | return err 388 | } 389 | 390 | err = g.grpcSetup(&ms) 391 | if err != nil { 392 | return err 393 | } 394 | 395 | fmt.Printf("** \n") 396 | fmt.Printf("** Mesh '%s' has been joined.\n", cfg.MeshName) 397 | fmt.Printf("** \n") 398 | fmt.Printf("** Mesh name: %s\n", cfg.MeshName) 399 | fmt.Printf("** Mesh CIDR range: %s\n", ms.CIDRRange.String()) 400 | fmt.Printf("** This node's name: %s\n", ms.NodeName) 401 | fmt.Printf("** This node's mesh IP: %s\n", ms.MeshIP.IP.String()) 402 | if cfg.MemberlistFile != "" { 403 | fmt.Printf("** Mesh node details export to: %s\n", cfg.MemberlistFile) 404 | } 405 | fmt.Printf("** \n") 406 | if g.devMode { 407 | fmt.Printf("** This mesh is running in DEVELOPMENT MODE without encryption.\n") 408 | fmt.Printf("** Do not use this in a production setup.\n") 409 | fmt.Printf("** \n") 410 | } else { 411 | if ms.TLSConfig != nil && len(ms.TLSConfig.Cert.Certificate) > 0 { 412 | fmt.Printf("** TLS is enabled for gRPC mesh service\n") 413 | 414 | x, err := x509.ParseCertificate(ms.TLSConfig.Cert.Certificate[0]) 415 | if err == nil { 416 | fmt.Printf("** subject: %s\n", x.Subject) 417 | fmt.Printf("** issuer: %s\n", x.Issuer) 418 | } 419 | } 420 | } 421 | fmt.Printf("** \n") 422 | fmt.Printf("** To inspect the wireguard interface and its peer data use:\n") 423 | fmt.Printf("** wg show %s\n", ms.WireguardInterface.InterfaceName) 424 | fmt.Printf("** \n") 425 | fmt.Printf("** To inspect the current mesh status use: wgmesh info\n") 426 | fmt.Printf("** \n") 427 | 428 | g.wait() 429 | 430 | if err = g.cleanUp(&ms); err != nil { 431 | return err 432 | } 433 | 434 | return nil 435 | } 436 | 437 | func (g *JoinCommand) handleHandshake(ctx context.Context, service meshservice.MeshClient, ms *meshservice.MeshService) (tokenStr string, authResponses []string, err error) { 438 | 439 | // call Begin method of boostrap's grpc service 440 | handshakeResponse, err := service.Begin(ctx, &meshservice.HandshakeRequest{ 441 | MeshName: g.meshConfig.MeshName, 442 | }) 443 | if err != nil { 444 | log.WithError(err).Error("unable to begin handshake") 445 | } 446 | log.WithField("hr", handshakeResponse).Trace("got handshakeResponse") 447 | if handshakeResponse.Result != meshservice.HandshakeResponse_OK { 448 | msg := "bootstrap node returned handshake error" 449 | log.WithField("msg", handshakeResponse.ErrorMessage).Error(msg) 450 | return "", []string{}, errors.New(msg) 451 | } 452 | 453 | // TODO process authn/authz requests ... 454 | authResps := []string{} 455 | 456 | return handshakeResponse.JoinToken, authResps, nil 457 | } 458 | 459 | // grpcSetup starts the local agent 460 | func (g *JoinCommand) grpcSetup(ms *meshservice.MeshService) (err error) { 461 | cfg := g.meshConfig 462 | 463 | // start the local agent if argument is given 464 | if cfg.Agent.GRPCBindSocket != "" { 465 | ms.MeshAgentServer = meshservice.NewMeshAgentServerSocket(ms, cfg.Agent.GRPCBindSocket, cfg.Agent.GRPCBindSocketIDs) 466 | log.WithField("mas", ms.MeshAgentServer).Trace("agent") 467 | go func() { 468 | log.Infof("Starting gRPC Agent Service at %s", cfg.Agent.GRPCBindSocket) 469 | err = ms.MeshAgentServer.StartAgentGrpcService() 470 | if err != nil { 471 | log.Error(err) 472 | } 473 | }() 474 | } 475 | return nil 476 | } 477 | 478 | // serfSetup ... 479 | func (g *JoinCommand) serfSetup(ms *meshservice.MeshService, listenIP net.IP, meshIPs []string, lanMode bool) (err error) { 480 | ms.NewSerfCluster(lanMode) 481 | 482 | err = ms.StartSerfCluster(false, ms.WireguardPubKey, listenIP.String(), g.meshConfig.Wireguard.ListenPort, ms.MeshIP.IP.String()) 483 | if err != nil { 484 | return err 485 | } 486 | 487 | ms.StartStatsUpdater() 488 | 489 | // join the cluster 490 | ms.JoinSerfCluster(meshIPs) 491 | 492 | return nil 493 | } 494 | 495 | // waits until being stopped 496 | func (g *JoinCommand) wait() { 497 | 498 | stopCh := make(chan struct{}) 499 | sigc := make(chan os.Signal, 1) 500 | signal.Notify(sigc, 501 | syscall.SIGINT, 502 | syscall.SIGTERM, 503 | syscall.SIGQUIT) 504 | go func() { 505 | <-sigc 506 | stopCh <- struct{}{} 507 | }() 508 | 509 | <-stopCh 510 | } 511 | 512 | // CleanUp .. 513 | func (g *JoinCommand) cleanUp(ms *meshservice.MeshService) error { 514 | // take everything down 515 | ms.MeshAgentServer.StopAgentGrpcService() 516 | 517 | ms.LeaveSerfCluster() 518 | 519 | // delete memberlist-file 520 | os.Remove(g.meshConfig.MemberlistFile) 521 | 522 | return nil 523 | } 524 | -------------------------------------------------------------------------------- /cmd/rtt.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "text/tabwriter" 9 | "time" 10 | 11 | config "github.com/aschmidt75/wgmesh/config" 12 | meshservice "github.com/aschmidt75/wgmesh/meshservice" 13 | log "github.com/sirupsen/logrus" 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | // RTTCommand struct 18 | type RTTCommand struct { 19 | CommandDefaults 20 | 21 | fs *flag.FlagSet 22 | 23 | // configuration file 24 | config string 25 | // configuration struct 26 | meshConfig config.Config 27 | } 28 | 29 | // NewRTTCommand creates the Tag Command 30 | func NewRTTCommand() *RTTCommand { 31 | c := &RTTCommand{ 32 | CommandDefaults: NewCommandDefaults(), 33 | config: envStrWithDefault("WGMESH_CONFIG", ""), 34 | meshConfig: config.NewDefaultConfig(), 35 | fs: flag.NewFlagSet("rtt", flag.ContinueOnError), 36 | } 37 | 38 | c.fs.StringVar(&c.config, "config", c.config, "file name of config file (optional).\nenv:WGMESH_cONFIG") 39 | c.fs.StringVar(&c.meshConfig.Agent.GRPCSocket, "agent-grpc-socket", c.meshConfig.Agent.GRPCSocket, "agent socket to dial") 40 | c.DefaultFields(c.fs) 41 | 42 | return c 43 | } 44 | 45 | // Name returns the name of the command 46 | func (g *RTTCommand) Name() string { 47 | return g.fs.Name() 48 | } 49 | 50 | // Init sets up the command struct from arguments 51 | func (g *RTTCommand) Init(args []string) error { 52 | err := g.fs.Parse(args) 53 | if err != nil { 54 | return err 55 | } 56 | g.ProcessDefaults() 57 | 58 | // load config file if we have one 59 | if g.config != "" { 60 | err = g.meshConfig.LoadConfigFromFile(g.config) 61 | if err != nil { 62 | log.WithError(err).Error("Config read error") 63 | return fmt.Errorf("Unable to read configuration from %s", g.config) 64 | } 65 | } 66 | 67 | err = g.fs.Parse(args) 68 | if err != nil { 69 | return err 70 | } 71 | log.WithField("cfg", g.meshConfig).Trace("Read") 72 | log.WithField("cfg.agent", g.meshConfig.Agent).Trace("Read") 73 | 74 | return nil 75 | } 76 | 77 | // Run queries the agent for RTT info 78 | func (g *RTTCommand) Run() error { 79 | log.WithField("g", g).Trace( 80 | "Running cli command", 81 | ) 82 | 83 | // 84 | endpoint := fmt.Sprintf("unix://%s", g.meshConfig.Agent.GRPCSocket) 85 | 86 | conn, err := grpc.Dial(endpoint, grpc.WithInsecure(), grpc.WithBlock()) 87 | if err != nil { 88 | log.Error(err) 89 | return fmt.Errorf("cannot connect to %s", endpoint) 90 | } 91 | defer conn.Close() 92 | 93 | agent := meshservice.NewAgentClient(conn) 94 | log.WithField("agent", agent).Trace("got grpc service client") 95 | 96 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 97 | defer cancel() 98 | 99 | r, err := agent.RTT(ctx, &meshservice.AgentEmpty{}) 100 | if err != nil { 101 | log.WithError(err).Error("Unable to query RTTs from agent") 102 | } 103 | 104 | allNames := make([]string, 0) 105 | res := make(map[string]map[string]int) 106 | for { 107 | rttInfo, err := r.Recv() 108 | if err != nil { 109 | break 110 | } 111 | 112 | log.WithField("r", rttInfo).Trace("Got response") 113 | allNames = append(allNames, rttInfo.NodeName) 114 | res[rttInfo.NodeName] = make(map[string]int) 115 | 116 | for _, nodeInfo := range rttInfo.Rtts { 117 | elem := res[rttInfo.NodeName] 118 | elem[nodeInfo.NodeName] = int(nodeInfo.RttMsec) 119 | } 120 | 121 | } 122 | log.WithField("res", res).Trace("results") 123 | 124 | // sort allNames 125 | 126 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.AlignRight|tabwriter.Debug) 127 | line := "/" 128 | 129 | for _, colsName := range allNames { 130 | line = fmt.Sprintf("%s\t%s", line, colsName) 131 | } 132 | line = fmt.Sprintf("%s\t", line) 133 | 134 | fmt.Fprintln(w, line) 135 | 136 | for _, rowsName := range allNames { 137 | line := rowsName 138 | 139 | for _, colsName := range allNames { 140 | line = fmt.Sprintf("%s\t%d", line, res[rowsName][colsName]) 141 | } 142 | 143 | line = fmt.Sprintf("%s\t", line) 144 | fmt.Fprintln(w, line) 145 | 146 | } 147 | w.Flush() 148 | 149 | return err 150 | } 151 | -------------------------------------------------------------------------------- /cmd/tags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "time" 11 | 12 | config "github.com/aschmidt75/wgmesh/config" 13 | meshservice "github.com/aschmidt75/wgmesh/meshservice" 14 | log "github.com/sirupsen/logrus" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | // TagsCommand struct 19 | type TagsCommand struct { 20 | CommandDefaults 21 | 22 | fs *flag.FlagSet 23 | 24 | // configuration file 25 | config string 26 | // configuration struct 27 | meshConfig config.Config 28 | 29 | // options not in config, only from parameters 30 | tagStr string 31 | deleteFlag string 32 | } 33 | 34 | // NewTagsCommand creates the Tag Command 35 | func NewTagsCommand() *TagsCommand { 36 | c := &TagsCommand{ 37 | CommandDefaults: NewCommandDefaults(), 38 | config: envStrWithDefault("WGMESH_CONFIG", ""), 39 | meshConfig: config.NewDefaultConfig(), 40 | fs: flag.NewFlagSet("tags", flag.ContinueOnError), 41 | tagStr: "", 42 | deleteFlag: "", 43 | } 44 | 45 | c.fs.StringVar(&c.config, "config", c.config, "file name of config file (optional).\nenv:WGMESH_cONFIG") 46 | c.fs.StringVar(&c.tagStr, "set", c.tagStr, "set tag key=value") 47 | c.fs.StringVar(&c.deleteFlag, "delete", c.deleteFlag, "to delete a key") 48 | c.fs.StringVar(&c.meshConfig.Agent.GRPCSocket, "agent-grpc-socket", c.meshConfig.Agent.GRPCSocket, "agent socket to dial") 49 | 50 | c.DefaultFields(c.fs) 51 | 52 | return c 53 | } 54 | 55 | // Name returns the name of the command 56 | func (g *TagsCommand) Name() string { 57 | return g.fs.Name() 58 | } 59 | 60 | // Init sets up the command struct from arguments 61 | func (g *TagsCommand) Init(args []string) error { 62 | err := g.fs.Parse(args) 63 | if err != nil { 64 | return err 65 | } 66 | g.ProcessDefaults() 67 | 68 | // load config file if we have one 69 | if g.config != "" { 70 | err = g.meshConfig.LoadConfigFromFile(g.config) 71 | if err != nil { 72 | log.WithError(err).Error("Config read error") 73 | return fmt.Errorf("Unable to read configuration from %s", g.config) 74 | } 75 | } 76 | 77 | err = g.fs.Parse(args) 78 | if err != nil { 79 | return err 80 | } 81 | log.WithField("cfg", g.meshConfig).Trace("Read") 82 | log.WithField("cfg.agent", g.meshConfig.Agent).Trace("Read") 83 | 84 | if g.tagStr != "" { 85 | arr := strings.Split(g.tagStr, "=") 86 | if len(arr) < 2 { 87 | return errors.New("Set a tag using -set=key=value") 88 | } 89 | if strings.HasPrefix(arr[0], "_") { 90 | return errors.New("Tag keys may not start with underscore _") 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // Run runs the command by creating the wireguard interface, 98 | // starting the serf cluster and grpc server 99 | func (g *TagsCommand) Run() error { 100 | log.WithField("g", g).Trace( 101 | "Running cli command", 102 | ) 103 | 104 | // 105 | endpoint := fmt.Sprintf("unix://%s", g.meshConfig.Agent.GRPCSocket) 106 | 107 | conn, err := grpc.Dial(endpoint, grpc.WithInsecure(), grpc.WithBlock()) 108 | if err != nil { 109 | log.Error(err) 110 | return fmt.Errorf("cannot connect to %s", endpoint) 111 | } 112 | defer conn.Close() 113 | 114 | agent := meshservice.NewAgentClient(conn) 115 | log.WithField("agent", agent).Trace("got grpc service client") 116 | 117 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 118 | defer cancel() 119 | 120 | if g.tagStr == "" && g.deleteFlag == "" { 121 | // show all tags 122 | client, err := agent.Tags(ctx, &meshservice.AgentEmpty{}) 123 | if err != nil { 124 | log.Error(err) 125 | return fmt.Errorf("cannot communicate with endpoint at %s", endpoint) 126 | } 127 | 128 | c := 0 129 | for { 130 | tag, err := client.Recv() 131 | if err == io.EOF { 132 | break 133 | } 134 | if err != nil { 135 | log.WithError(err).Debug("error while retrieving tag list") 136 | break 137 | } 138 | if !strings.HasPrefix(tag.Key, "_") { 139 | fmt.Printf("%s=%s\n", tag.Key, tag.Value) 140 | c++ 141 | } 142 | } 143 | if c == 0 { 144 | fmt.Println("no tags") 145 | } 146 | return nil 147 | } 148 | 149 | if g.deleteFlag != "" { 150 | r, err := agent.Untag(ctx, &meshservice.NodeTag{ 151 | Key: g.deleteFlag, 152 | }) 153 | if err != nil { 154 | log.Error(err) 155 | return fmt.Errorf("cannot communicate with endpoint at %s", endpoint) 156 | } 157 | log.WithField("r", r).Trace("got tagResponse") 158 | 159 | if r.Ok { 160 | log.Info("Tag deleted") 161 | } else { 162 | log.Error("Tag not deleted") 163 | } 164 | } 165 | 166 | if g.tagStr != "" { 167 | arr := strings.SplitN(g.tagStr, "=", 2) 168 | 169 | r, err := agent.Tag(ctx, &meshservice.NodeTag{ 170 | Key: arr[0], 171 | Value: arr[1], 172 | }) 173 | if err != nil { 174 | log.Error(err) 175 | return fmt.Errorf("cannot communicate with endpoint at %s", endpoint) 176 | } 177 | log.WithField("r", r).Trace("got tagResponse") 178 | 179 | if r.Ok { 180 | log.Info("Tag set") 181 | } else { 182 | log.Error("Tag not set") 183 | } 184 | 185 | } 186 | 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /cmd/ui.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | config "github.com/aschmidt75/wgmesh/config" 8 | meshservice "github.com/aschmidt75/wgmesh/meshservice" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // UICommand struct 13 | type UICommand struct { 14 | CommandDefaults 15 | 16 | fs *flag.FlagSet 17 | 18 | // configuration file 19 | config string 20 | // configuration struct 21 | meshConfig config.Config 22 | 23 | // options not in config, only from parameters 24 | } 25 | 26 | // NewUICommand creates the UI Command structure and sets the parameters 27 | func NewUICommand() *UICommand { 28 | c := &UICommand{ 29 | CommandDefaults: NewCommandDefaults(), 30 | fs: flag.NewFlagSet("ui", flag.ContinueOnError), 31 | config: envStrWithDefault("WGMESH_CONFIG", ""), 32 | meshConfig: config.NewDefaultConfig(), 33 | } 34 | 35 | c.fs.StringVar(&c.config, "config", c.config, "file name of config file (optional).\nenv:WGMESH_cONFIG") 36 | c.fs.StringVar(&c.meshConfig.Agent.GRPCSocket, "agent-grpc-socket", c.meshConfig.Agent.GRPCSocket, "agent socket to dial") 37 | c.fs.StringVar(&c.meshConfig.UI.HTTPBindAddr, "http-bind-addr", c.meshConfig.UI.HTTPBindAddr, "HTTP bind address") 38 | c.fs.IntVar(&c.meshConfig.UI.HTTPBindPort, "http-bind-port", c.meshConfig.UI.HTTPBindPort, "HTTP bind port") 39 | 40 | c.DefaultFields(c.fs) 41 | 42 | return c 43 | } 44 | 45 | // Name returns the name of the command 46 | func (g *UICommand) Name() string { 47 | return g.fs.Name() 48 | } 49 | 50 | // Init sets up the command struct from arguments 51 | func (g *UICommand) Init(args []string) error { 52 | err := g.fs.Parse(args) 53 | if err != nil { 54 | return err 55 | } 56 | g.ProcessDefaults() 57 | 58 | // load config file if we have one 59 | if g.config != "" { 60 | err = g.meshConfig.LoadConfigFromFile(g.config) 61 | if err != nil { 62 | log.WithError(err).Error("Config read error") 63 | return fmt.Errorf("Unable to read configuration from %s", g.config) 64 | } 65 | } 66 | 67 | err = g.fs.Parse(args) 68 | if err != nil { 69 | return err 70 | } 71 | log.WithField("cfg", g.meshConfig).Trace("Read") 72 | log.WithField("cfg.agent", g.meshConfig.Agent).Trace("Read") 73 | 74 | return nil 75 | } 76 | 77 | // Run starts an http server to serve the user interface 78 | func (g *UICommand) Run() error { 79 | log.WithField("g", g).Trace( 80 | "Running cli command", 81 | ) 82 | 83 | uiServer := meshservice.NewUIServer( 84 | g.meshConfig.Agent.GRPCSocket, 85 | g.meshConfig.UI.HTTPBindAddr, 86 | g.meshConfig.UI.HTTPBindPort) 87 | uiServer.Serve() 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | wgwrapper "github.com/aschmidt75/go-wg-wrapper/pkg/wgwrapper" 11 | ) 12 | 13 | // given an IP address or interface name or empty, this returns the IP 14 | // as net.IP. If empty string is given, this takes the IP address of 15 | // the interface where the default route is attached to. 16 | func getIPFromIPOrIntfParam(i string) net.IP { 17 | 18 | if i == "" { 19 | wg := wgwrapper.New() 20 | i, _ = wg.DefaultRouteInterface() 21 | } 22 | 23 | // is this an IP address? 24 | ipv6_regex := `^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$` 25 | ipv4_regex := `^(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4})` 26 | 27 | ok, _ := regexp.MatchString(ipv4_regex+`|`+ipv6_regex, i) 28 | if ok { 29 | return net.ParseIP(i) 30 | } 31 | 32 | arr := strings.Split(i, "%") 33 | idx := 0 34 | if len(arr) >= 2 { 35 | i = arr[0] 36 | var err error 37 | idx, err = strconv.Atoi(arr[1]) 38 | if err != nil { 39 | idx = 0 40 | } 41 | } 42 | 43 | // is it a valid interface name? 44 | intf, err := net.InterfaceByName(i) 45 | if err != nil { 46 | return nil 47 | } 48 | 49 | addrs, err := intf.Addrs() 50 | if err != nil { 51 | return nil 52 | } 53 | 54 | if idx >= len(addrs) { 55 | return nil 56 | } 57 | 58 | s := addrs[idx].String() 59 | if strings.IndexAny(s, "/") >= 0 { 60 | arr = strings.Split(s, "/") 61 | s = arr[0] 62 | } 63 | 64 | return net.ParseIP(s) 65 | } 66 | 67 | // https://stackoverflow.com/questions/41240761/check-if-ip-address-is-in-private-network-space/41273687#41273687 68 | func isPrivateIP(ip string) (bool, error) { 69 | var err error 70 | private := false 71 | IP := net.ParseIP(ip) 72 | if IP == nil { 73 | err = errors.New("Invalid IP") 74 | } else { 75 | _, private24BitBlock, _ := net.ParseCIDR("10.0.0.0/8") 76 | _, private20BitBlock, _ := net.ParseCIDR("172.16.0.0/12") 77 | _, private16BitBlock, _ := net.ParseCIDR("192.168.0.0/16") 78 | private = private24BitBlock.Contains(IP) || private20BitBlock.Contains(IP) || private16BitBlock.Contains(IP) 79 | } 80 | return private, err 81 | } 82 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strconv" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | // Config is the main Configuration struct 12 | type Config struct { 13 | // MeshName is the name of the mesh to form or to join 14 | MeshName string `yaml:"mesh-name"` 15 | 16 | // NodeName is the name of the current node. If not set it 17 | // will be formed from the mesh ip assigned 18 | NodeName string `yaml:"node-name"` 19 | 20 | // Bootstrap is the config part for bootstrap mode 21 | Bootstrap *BootstrapConfig `yaml:"bootstrap,omitempty"` 22 | 23 | // Join is the config part for join mode 24 | Join *JoinConfig `yaml:"join,omitempty"` 25 | 26 | // Wireguard is the configuration part for wireguard-related settings 27 | Wireguard *WireguardConfig `yaml:"wireguard,omitempty"` 28 | 29 | // Agent contains optional agent configuration 30 | Agent *AgentConfig `yaml:"agent,omitempty"` 31 | 32 | // UI contains web user interface configuration 33 | UI *UIConfig `yaml:"ui,omitempty"` 34 | 35 | // MemberlistFile is an optional setting. If set, node information is written 36 | // here periodically 37 | MemberlistFile string `yaml:"memberlist-file"` 38 | } 39 | 40 | // BootstrapConfig contains condfiguration parts for bootstrap mode 41 | type BootstrapConfig struct { 42 | // MeshCIDRRange is the CIDR (e.g. 10.232.0.0/16) to be used for the mesh 43 | // when assigning new mesh-internal ip addresses 44 | MeshCIDRRange string `yaml:"mesh-cidr-range"` 45 | 46 | // MeshIPAMCIDRRange is an optional setting where this is a subnet of 47 | // MeshCIDRRange and IP addresses are assigned only from this range 48 | MeshIPAMCIDRRange string `yaml:"mesh-ipam-cidr-range"` 49 | 50 | // NodeIP sets the internal mesh ip of this node (e.g. .1 for a given subnet) 51 | NodeIP string `yaml:"node-ip"` 52 | 53 | // GRPCBindAddr is the ip address where bootstrap node expose their 54 | // gRPC intnerface and listen for join requests 55 | GRPCBindAddr string `yaml:"grpc-bind-addr"` 56 | 57 | // GRPCBindPort is the port number where bootstrap node expose their 58 | // gRPC intnerface and listen for join requests 59 | GRPCBindPort int `yaml:"grpc-bind-port"` 60 | 61 | // GRPCTLSConfig is the optional TLS settings struct for the gRPC interface 62 | GRPCTLSConfig *BootstrapGRPCTLSConfig `yaml:"grpc-tls,omitempty"` 63 | 64 | // MeshEncryptionKey is an optional key for symmetric encryption of internal mesh traffic. 65 | // Must be 32 Bytes base64-ed. 66 | MeshEncryptionKey string `yaml:"mesh-encryption-key"` 67 | 68 | // SerfModeLAN activates LAN mode or cluster communication. Default is false (=WAN mode). 69 | SerfModeLAN bool `yaml:"serf-mode-lan"` 70 | } 71 | 72 | // JoinConfig contains condfiguration parts for join mode 73 | type JoinConfig struct { 74 | // BootstrapEndpoint is the IP:Port of remote mesh bootstrap node. 75 | BootstrapEndpoint string `yaml:"bootstrap-endpoint"` 76 | 77 | // ClientKey points to PEM-encoded private key to be used by the joining client when dialing the bootstrap node. 78 | ClientKey string `yaml:"client-key"` 79 | 80 | // ClientCert points to PEM-encoded certificate be used by the joining client when dialing the bootstrap node. 81 | ClientCert string `yaml:"client-cert"` 82 | 83 | // ClientCaCert points to PEM-encoded CA certificate. 84 | ClientCaCert string `yaml:"ca-cert"` 85 | } 86 | 87 | // BootstrapGRPCTLSConfig contains settings necessary for configuration TLS for the bootstrap node 88 | type BootstrapGRPCTLSConfig struct { 89 | // GRPCServerKey points to PEM-encoded private key to be used by grpc server. 90 | GRPCServerKey string `yaml:"grpc-server-key"` 91 | 92 | // GRPCServerCert points to PEM-encoded certificate be used by grpc server. 93 | GRPCServerCert string `yaml:"grpc-server-cert"` 94 | 95 | // GRPCCaCert points to PEM-encoded CA certificate. 96 | GRPCCaCert string `yaml:"grpc-ca-cert"` 97 | 98 | // GRPCCaPath points to a directory containing PEM-encoded CA certificates. 99 | GRPCCaPath string `yaml:"grpc-ca-path"` 100 | } 101 | 102 | // WireguardConfig contains wireguard-related settings 103 | type WireguardConfig struct { 104 | // ListenAddr is the ip address where wireguard should listen for packets 105 | ListenAddr string `yaml:"listen-addr"` 106 | 107 | // ListenPort is the (external) wireguard listen port 108 | ListenPort int `yaml:"listen-port"` 109 | } 110 | 111 | // AgentConfig contains settings for the gRPC-based local agent 112 | type AgentConfig struct { 113 | // GRPCBindSocket is the local socket file to bind grpc agent to. 114 | GRPCBindSocket string `yaml:"agent-grpc-bind-socket"` 115 | 116 | // GRPCBindSocketIDs of the form to change bind socket to. 117 | GRPCBindSocketIDs string `yaml:"agent-grpc-bind-socket-id"` 118 | 119 | // GRPCSocket is the local socket file, used by agent clients. 120 | GRPCSocket string `yaml:"agent-grpc-socket"` 121 | } 122 | 123 | // UIConfig contains config entries for the web user interface 124 | type UIConfig struct { 125 | HTTPBindAddr string `yaml:"http-bind-addr"` 126 | HTTPBindPort int `yaml:"http-bind-port"` 127 | } 128 | 129 | // LoadConfigFromFile reads yaml config file from given path 130 | func (cfg *Config) LoadConfigFromFile(path string) error { 131 | b, err := ioutil.ReadFile(path) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | if err = yaml.Unmarshal(b, cfg); err != nil { 137 | return err 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // NewDefaultConfig creates a default configuration with valid presets. 144 | // These presets can be used with `-dev` mode. 145 | func NewDefaultConfig() Config { 146 | return Config{ 147 | MeshName: envStrWithDefault("WGMESH_MESH_NAME", ""), 148 | NodeName: envStrWithDefault("WGMESH_NODE_NAME", ""), 149 | Bootstrap: &BootstrapConfig{ 150 | MeshCIDRRange: envStrWithDefault("WGMESH_CIDR_RANGE", "10.232.0.0/16"), 151 | MeshIPAMCIDRRange: envStrWithDefault("WGMESH_CIDR_RANGE_IPAM", ""), 152 | NodeIP: envStrWithDefault("WGMESH_MESH_IP", "10.232.1.1"), 153 | GRPCBindAddr: envStrWithDefault("WGMESH_GRPC_BIND_ADDR", "0.0.0.0"), 154 | GRPCBindPort: envIntWithDefault("WGMESH_GRPC_BIND_PORT", 5000), 155 | GRPCTLSConfig: &BootstrapGRPCTLSConfig{ 156 | GRPCServerKey: envStrWithDefault("WGMESH_SERVER_KEY", ""), 157 | GRPCServerCert: envStrWithDefault("WGMESH_SERVER_CERT", ""), 158 | GRPCCaCert: envStrWithDefault("WGMESH_CA_CERT", ""), 159 | GRPCCaPath: envStrWithDefault("WGMESH_CA_PATH", ""), 160 | }, 161 | MeshEncryptionKey: envStrWithDefault("WGMESH_ENCRYPTION_KEY", ""), 162 | SerfModeLAN: envBoolWithDefault("WGMESH_SERF_MODE_LAN", false), 163 | }, 164 | Join: &JoinConfig{ 165 | BootstrapEndpoint: envStrWithDefault("WGMESH_BOOTSTRAP_ADDR", ""), 166 | ClientKey: envStrWithDefault("WGMESH_CLIENT_KEY", ""), 167 | ClientCert: envStrWithDefault("WGMESH_CLIENT_CERT", ""), 168 | ClientCaCert: envStrWithDefault("WGMESH_CA_CERT", ""), 169 | }, 170 | Wireguard: &WireguardConfig{ 171 | ListenAddr: envStrWithDefault("WGMESH_WIREGUARD_LISTEN_ADDR", ""), 172 | ListenPort: envIntWithDefault("WGMESH_WIREGUARD_LISTEN_PORT", 54540), 173 | }, 174 | Agent: &AgentConfig{ 175 | GRPCBindSocket: envStrWithDefault("WGMESH_AGENT_BIND_SOCKET", "/var/run/wgmesh.sock"), 176 | GRPCBindSocketIDs: envStrWithDefault("WGMESH_AGENT_BIND_SOCKET_ID", ""), 177 | GRPCSocket: envStrWithDefault("WGMESH_AGENT_SOCKET", "/var/run/wgmesh.sock"), 178 | }, 179 | UI: &UIConfig{ 180 | HTTPBindAddr: envStrWithDefault("WGMESH_HTTP_BIND_ADDR", "127.0.0.1"), 181 | HTTPBindPort: envIntWithDefault("WGMESH_HTTP_BIND_PORT", 9095), 182 | }, 183 | MemberlistFile: envStrWithDefault("WGMESH_MEMBERLIST_FILE", ""), 184 | } 185 | } 186 | 187 | func envStrWithDefault(key string, defaultValue string) string { 188 | res := os.Getenv(key) 189 | if res == "" { 190 | return defaultValue 191 | } 192 | return res 193 | } 194 | 195 | func envBoolWithDefault(key string, defaultValue bool) bool { 196 | res := os.Getenv(key) 197 | if res == "" { 198 | return defaultValue 199 | } 200 | if res == "1" || res == "true" || res == "on" { 201 | return true 202 | } 203 | return false 204 | } 205 | 206 | func envIntWithDefault(key string, defaultValue int) int { 207 | res := os.Getenv(key) 208 | if res == "" { 209 | return defaultValue 210 | } 211 | v, err := strconv.Atoi(res) 212 | if err != nil { 213 | return -1 214 | } 215 | return v 216 | } 217 | -------------------------------------------------------------------------------- /docs/cli-params.md: -------------------------------------------------------------------------------- 1 | 2 | ## CLI Usage and Parameters 3 | 4 | `wgmesh` understands the following commands: 5 | 6 | * `bootstrap` is used to start a node in bootstrap mode. At least one node in a mesh has to be a bootstrap node, so that other nodes are able to join. This command will run in foreground. It can be stopped with CTRL-C or otherwise terminating/killing it. Mesh connectivity is maintained as long as the command is running. 7 | * `join` is used to join an existing mesh by connecting to a bootstrap node. This command will run in foreground and maintain mesh connectivity as long as it is running. 8 | * `info` prints out information about the mesh and its nodes. It can be used on bootstrapped or joined nodes where one of the above commands is running. 9 | * `tags` is used to set or remove tags on the current node. 10 | * `rtt` prints out a table of round-trip-times for all nodes. 11 | 12 | ### Common parameter for all commands 13 | 14 | * `-v` enables verbose mode which shows more ouput, e.g. nodes joining or leaving 15 | * `-d` enables debug mode, showing much more output of internal state changes etc. 16 | 17 | ### Common parameters for `bootstrap`/`join` 18 | 19 | * `dev` enables **DEVELOPMENT** mode. In this mode, no encryption is in place, no TLS setup needs to be specified. **This is suitable for trying things out but leaves your mesh more or less open for everyone to access it. Do not use this for non-development setups.** 20 | * `name`, `n` (optional) name of mesh to bootstrap or join. This can have a maximum length of 10 characters, as it will be used to form the interface and node names for wireguard. If left empty, wgmesh will assign a random 10-char string. 21 | * `node-name` (optional) name of the current node. It is advised to keep it short as it will be used in gossip traffic between nodes. If left empty, wgmesh will automatically assign a name combined of the mesh name and the internal node ip. 22 | * `listen-addr` Endpoint IP address where the wireguard interface is listening for UDP packets. May be specified as an IP address or as a interface name. If left empty, wgmesh uses STUN to determine the hosts' public ip. Leaving this parameter empty makes sense in non-NATed and NATed environment with public IP addresses. It does not make sense in private network setups, IP addresses should be explicitly given here. 23 | * `listen-port` (default 54540) UDP port of the wireguard endpoint 24 | * `agent-bind-socket` is a path to the socket file where the local wgmesh agent serves gRPC requests, such as the `info` or `tags` commands 25 | * `agent-bind-socket-id` is of the form UID:GID and is used to chown the above agent-bind-socket file to this user id and group id. 26 | * `memberlist-file` points to a JSON file where wgmesh stores up-to-date information about the current mesh topology. Every time nodes enter or leave the mesh, or tags are updated, this file gets rewritten. 27 | 28 | ### `bootstrap` 29 | 30 | This command is for bootstrap nodes only. 31 | 32 | * `ip` (default 10.232.1.1 for bootstrap nodes, not used for joining nodes) private IP of this node. This needs to be specified for the bootstrap nodes. wgmesh will take this ip address on the wireguard interface for itself. 33 | * `cidr` (default 10.232.0.0/16) This is the private network in CIDR notation. All nodes joining the mesh will be assigned an IP address within this address space. 34 | * `cidr-ipam` (no default) This is the subnet within -cidr, in CIDR notation. It makes this bootstrap node assign new IP addresses from this subnet range only. This way, a mesh may be split into several, per-bootstrap-node-managed sub-entities. 35 | * `grpc-bind-addr` (default 0.0.0.0 only applies for bootstrap nodes) Bind address for the public gRPC service of bootstrap nodes 36 | * `grpc-bind-port`(default 5000 only for bootstrap nodes) TCP port number for the public gRPC service 37 | * `grpc-server-key` points to the PEM-encoded private key. This is used for the external gRPC service. 38 | * `grpc-server-cert` points to the PEM-encoded certificate. 39 | * `grpc-ca-cert` points to a PEM-encoded CA certificate used to authenticate joining nodes. Mutually exlusive with `grpc-ca-path` 40 | * `grpc-ca-path` points to a directory where PEM-encoded certificates reside. They are used to authenticate joining nodes. Mutually exlusive with `grpc-ca-cert` 41 | * `mesh-encryption-key` (optional) base64-encoded, 32 bytes symmetric encryption key used to encrypt internal mesh traffic. If this is left out, wgmesh will assign a randomized key. 42 | * `serf-mode-lan` if set to true, use the LAN mode defaults for Serf, otherwise use the WAN mode defaults (e.g. timeouts, fan-outs etc.). This is set on the bootstrap node only and will be propagated to joining nodes. 43 | 44 | ### `join` 45 | 46 | This command is for joining nodes only 47 | 48 | * `bootstrap-addr` (mandatory, only for joining nodes) Address (IP:port) of the gRPC service of a bootstrap node. 49 | * `client-key` points to the PEM-encoded private key. This is used when connecting to the bootstrap node. 50 | * `client-cert` points to the PEM-encoded certificate. This must be recognized by the bootstrap mode (see there `grpc-ca-cert` or `grpc-ca-path`) 51 | * `ca-cert` points to a PEM-encoded CA certificate 52 | 53 | ### `info` 54 | 55 | * `agent-grpc-socket` is the socket file, see above `agent-bind-socket`. 56 | * `watch` keeps running in foreground and prints out mesh information everytime a change occures within the mesh topology. 57 | 58 | ### `tags` 59 | 60 | * `agent-grpc-socket` is the socket file, see above `agent-bind-socket`. 61 | * `set` is used to set a tag as a key=value pair on the current node. This tag will be propagated to all nodes in the mesh. 62 | * `delete` removes a tag from the current node. Tag removal will be propagated to all mesh nodes. 63 | 64 | ### `rtt` 65 | 66 | * `agent-grpc-socket` is the socket file, see above `agent-bind-socket`. 67 | 68 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | wgmesh allows for a `config` parameter which points to a yaml configuration file. If given, config file is read and command line parameters are applied on top of it. 4 | 5 | Example: 6 | 7 | ```yaml 8 | mesh-name: somemesh 9 | node-name: mynodename 10 | bootstrap: 11 | mesh-cidr-range: 10.233.0.0/16 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/features-and-non-features.md: -------------------------------------------------------------------------------- 1 | ### Features 2 | 3 | * connects multiple nodes using wireguard 4 | * fully automated exchange of wireguard endpoint, public key 5 | * IP address management for mesh nodes 6 | * TLS for gRPC endpoints 7 | * keeps track of nodes in case of node failures, automatically removing wireguard peers 8 | * keeps track of new nodes joining the mash, automatically adding peer data 9 | 10 | ### Non-Features 11 | 12 | * no subnet support. All nodes set individual /32 host routes to all other nodes. 13 | * it does not do any routing by a daemon 14 | * it does not guarantee any NAT support. E.g. if two nodes are behind a NAT, they typically cannot connect to each other 15 | -------------------------------------------------------------------------------- /docs/multipass-demo-setup.md: -------------------------------------------------------------------------------- 1 | # Demo Setup using `multipass` 2 | 3 | ## Instances 4 | 5 | Launch a number of [multipass](https://multipass.run/) instances: 6 | 7 | ```bash 8 | $ NUM_INSTANCES=5 9 | $ for i in $(seq 1 ${NUM_INSTANCES}); do multipass launch -c 1 -m 256M -n wg${i} --cloud-init scripts/multipass-cloudinit.yaml lts; done 10 | ``` 11 | 12 | The cloud-init script installs the latest release of `wgmesh` to `/usr/local/bin`. It stops a number of non-necessary daemons as to save memory and be ready to work with a minimal 256M. 13 | 14 | ## Start the bootstrap node 15 | 16 | The first multipass node shall be the bootstrap node. We need to give the other nodes its ip address, so query this first. Also we want nodes to use the default interface's ip as the listen address. 17 | 18 | ```bash 19 | $ WGMESH_BOOTSTRAP_NODE=$(multipass info wg1 --format json | jq -r ".info.wg1.ipv4[0]") 20 | $ echo ${WGMESH_BOOTSTRAP_NODE} 21 | 192.168.64.12 22 | 23 | $ WGMESH_MULTIPASS_DEVICE=$(multipass exec wg1 -- /bin/bash -c "ip route show default | sed -e 's/.*dev \(.*\) proto .*/\1/g'") 24 | $ echo ${WGMESH_MULTIPASS_DEVICE} 25 | enp0s2 26 | ``` 27 | 28 | Your IP address will probably differ. Start the bootstrap node in `-dev` mode. We're injecting a mesh name here. 29 | 30 | ```bash 31 | $ multipass exec wg1 -- sudo wgmesh bootstrap -v -dev -name demo -listen-addr ${WGMESH_BOOTSTRAP_NODE} 32 | ``` 33 | 34 | ## Start remaining nodes and join the mesh 35 | 36 | Iterate through all remaining nodes and execute the `node` command. It makes sense to run this in separate terminals each so changes can be easily observed: 37 | 38 | ```bash 39 | $ multipass exec wg2 -- sudo wgmesh join -v -dev -n demo -bootstrap-addr ${WGMESH_BOOTSTRAP_NODE}:5000 -listen-addr ${WGMESH_MULTIPASS_DEVICE} 40 | ``` 41 | 42 | This lets the node join via the given bootstrap address' grpc service on port 5000. It also sets its own wireguard listening address to that of `enp0s2` which is the instances default network interface. 43 | 44 | ## Inspect details on individual nodes 45 | 46 | To show details about the mesh and its nodes, choose one of the multipass instances, connect and issue commands such as `info`: 47 | 48 | ```bash 49 | $ multipass shell wg2 50 | (...) 51 | $ sudo wgmesh info 52 | ``` 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/tags.md: -------------------------------------------------------------------------------- 1 | ## Tags 2 | 3 | Thanks to serf, all nodes in the mesh carry key/value parameters named "tags". wgmesh uses these tags to store additional information about nodes for both internal workings and added functionality as well. 4 | 5 | ### Internal wgmesh tags 6 | 7 | Tags that start with an underscore are internal tags. They are not shown by `wgmesh info`, but can be accessed. The following tags are used internally: 8 | 9 | * `_port` is the wireguard listen port 10 | * `_addr` is the wireguard listen address 11 | * `_pk` is the wireguard public key 12 | * `_i` is the mesh-internal IP address of the node 13 | * `_t` stores the node type: `b` for bootstrap nodes, `n` otherwise 14 | 15 | ### Setting tags using the CLI 16 | 17 | The `tags` command can be used on any node to set or delete a tag for that node. It connects to the local gRPC service and modifies the tag list as desired, which is then automatically broadcasted by serf to all mesh nodes. 18 | 19 | ```bash 20 | # wgmesh tags --help 21 | Usage of tags: 22 | -agent-grpc-socket string 23 | agent socket to dial (default "/var/run/wgmesh.sock") 24 | -d show debug output 25 | -delete string 26 | to delete a key 27 | -set string 28 | set tag key=value 29 | -v show more output 30 | ``` 31 | 32 | e.g. to set a tag use the `-set` option with a key/value pair 33 | 34 | ```bash 35 | # wgset tags -set=node_size=small 36 | ``` 37 | 38 | will set the tag `node_size` to the value `small` on the node where it is executed. 39 | 40 | To delete a tag, use the `-delete` option with a key: 41 | ```bash 42 | # wgset tags -delete=node_size 43 | ``` 44 | 45 | To show all tags for this node, use the `tags` command without options. 46 | 47 | ### Reserved tag names 48 | 49 | Besides the internal tag names, the following tag names are reserved: 50 | 51 | #### Services 52 | 53 | If a tag key begins with `svc:` it is treated as a mesh service, e.g. when exporting mesh node information. 54 | The tag string is interpreted as a comma-separated list of key=value pairs, with the following keys: 55 | 56 | * `port` is the port number where a service may be announced on a node. 57 | 58 | *Example* 59 | 60 | * key `svc:nginx` and value `port=80` 61 | -------------------------------------------------------------------------------- /docs/tls.md: -------------------------------------------------------------------------------- 1 | # TLS mode 2 | 3 | ## CA setup and certification generation 4 | 5 | e.g. using cfssl. Must have a CA certificate (i.e. of an intermediate), a key and cert file for bootstrap node 6 | with correct hostname (and SANs), and key and cert file for joining node(s). All key material must be provided 7 | in PEM format. 8 | 9 | ## bootstrap 10 | 11 | ```bash 12 | # wgmesh bootstrap -v \ 13 | -grpc-ca-cert /path/to/ca.pem \ 14 | -grpc-server-cert /path/to/bootstrap.pem \ 15 | -grpc-server-key /path/to/bootstrap-key.pem 16 | ``` 17 | 18 | ```bash 19 | # wgmesh join -v \ 20 | -ca-cert /root/wgmesh-tls/ca.pem \ 21 | -client-cert /root/wgmesh-tls/join.pem \ 22 | -client-key /root/wgmesh-tls/join-key.pem \ 23 | -n okOJGomAHM \ 24 | -bootstrap-addr :5000 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /docs/wgmesh-ui-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschmidt75/wgmesh/29dbaa307b0b047baec91e321cd094b56e84a3e2/docs/wgmesh-ui-sample.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aschmidt75/wgmesh 2 | 3 | go 1.15 4 | 5 | replace github.com/aschmidt75/wgmesh/cmd => ./cmd 6 | 7 | require ( 8 | github.com/GeertJohan/go.rice v1.0.2 9 | github.com/armon/go-metrics v0.3.6 // indirect 10 | github.com/aschmidt75/go-wg-wrapper v0.1.1 11 | github.com/cristalhq/jwt/v3 v3.0.12 12 | github.com/golang/protobuf v1.4.3 13 | github.com/google/btree v1.0.0 // indirect 14 | github.com/google/uuid v1.2.0 15 | github.com/hashicorp/errwrap v1.1.0 // indirect 16 | github.com/hashicorp/go-immutable-radix v1.3.0 // indirect 17 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 18 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 19 | github.com/hashicorp/golang-lru v0.5.4 // indirect 20 | github.com/hashicorp/memberlist v0.2.2 21 | github.com/hashicorp/serf v0.9.5 22 | github.com/mdlayher/netlink v1.2.1 // indirect 23 | github.com/miekg/dns v1.1.38 // indirect 24 | github.com/sirupsen/logrus v1.7.0 25 | go.opencensus.io v0.22.6 26 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect 27 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect 28 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 29 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 30 | golang.org/x/text v0.3.5 // indirect 31 | golang.org/x/tools v0.1.0 // indirect 32 | golang.zx2c4.com/wireguard v0.0.20201118 // indirect 33 | google.golang.org/genproto v0.0.0-20210201184850-646a494a81ea // indirect 34 | google.golang.org/grpc v1.35.0 35 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect 36 | google.golang.org/protobuf v1.25.0 37 | gopkg.in/yaml.v2 v2.2.5 38 | gortc.io/stun v1.23.0 39 | ) 40 | -------------------------------------------------------------------------------- /meshservice/agent.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | context "context" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "math/rand" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/hashicorp/serf/serf" 16 | log "github.com/sirupsen/logrus" 17 | grpc "google.golang.org/grpc" 18 | "google.golang.org/protobuf/proto" 19 | ) 20 | 21 | // MeshAgentServer implements the gRPC part of agent.proto 22 | type MeshAgentServer struct { 23 | UnimplementedAgentServer 24 | grpcServer *grpc.Server 25 | 26 | grpcBindSocket string 27 | grpcBindSocketID string 28 | 29 | ms *MeshService 30 | } 31 | 32 | func (as *MeshAgentServer) meshService() *MeshService { 33 | return as.ms 34 | } 35 | 36 | // NewMeshAgentServerSocket creates a new agent service for a local bind socket 37 | func NewMeshAgentServerSocket(ms *MeshService, grpcBindSocket string, grpcBindSocketID string) *MeshAgentServer { 38 | return &MeshAgentServer{ 39 | grpcServer: grpc.NewServer(), 40 | ms: ms, 41 | grpcBindSocket: grpcBindSocket, 42 | grpcBindSocketID: grpcBindSocketID, 43 | } 44 | } 45 | 46 | // Info returns details about the mesh 47 | func (as *MeshAgentServer) Info(ctx context.Context, ae *AgentEmpty) (*MeshInfo, error) { 48 | log.Trace("agent: Info requested") 49 | 50 | creationTS, nodeJoinTS := as.meshService().GetTimestamps() 51 | 52 | return &MeshInfo{ 53 | Name: as.meshService().MeshName, 54 | NodeName: as.meshService().NodeName, 55 | NodeCount: int32(as.meshService().Serf().NumNodes()), 56 | MeshCeationTS: int64(creationTS.Unix()), 57 | NodeJoinTS: int64(nodeJoinTS.Unix()), 58 | }, nil 59 | } 60 | 61 | // Tag ... 62 | func (as *MeshAgentServer) Tag(ctx context.Context, tr *NodeTag) (*TagResult, error) { 63 | log.WithFields(log.Fields{ 64 | "k": tr.Key, 65 | "v": tr.Value, 66 | }).Trace("agent: Tag requested") 67 | 68 | t := as.meshService().Serf().LocalMember().Tags 69 | t[tr.Key] = tr.Value 70 | err := as.meshService().Serf().SetTags(t) 71 | if err != nil { 72 | log.WithError(err).Error("unable to set tags at serf node") 73 | return &TagResult{ 74 | Ok: false, 75 | }, nil 76 | } 77 | return &TagResult{ 78 | Ok: true, 79 | }, nil 80 | } 81 | 82 | // Untag ... 83 | func (as *MeshAgentServer) Untag(ctx context.Context, tr *NodeTag) (*TagResult, error) { 84 | log.WithFields(log.Fields{ 85 | "k": tr.Key, 86 | "v": tr.Value, 87 | }).Trace("agent: Untag requested") 88 | 89 | t := as.meshService().Serf().LocalMember().Tags 90 | 91 | if _, ex := t[tr.Key]; ex == false { 92 | return &TagResult{ 93 | Ok: false, 94 | }, nil 95 | } 96 | 97 | delete(t, tr.Key) 98 | 99 | err := as.meshService().Serf().SetTags(t) 100 | if err != nil { 101 | log.WithError(err).Error("unable to set tags at serf node") 102 | return &TagResult{ 103 | Ok: false, 104 | }, nil 105 | } 106 | return &TagResult{ 107 | Ok: true, 108 | }, nil 109 | } 110 | 111 | // Tags streams all current tags of the local node 112 | func (as *MeshAgentServer) Tags(cte *AgentEmpty, server Agent_TagsServer) error { 113 | for key, value := range as.meshService().Serf().LocalMember().Tags { 114 | if err := server.Send(&NodeTag{ 115 | Key: key, 116 | Value: value, 117 | }); err != nil { 118 | log.WithError(err).Error("unable to stream send tag") 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | // Nodes ... 125 | func (as *MeshAgentServer) Nodes(cte *AgentEmpty, agentNodesServer Agent_NodesServer) error { 126 | log.Trace("agent: Nodes requested") 127 | 128 | myCoord, err := as.meshService().Serf().GetCoordinate() 129 | if err != nil { 130 | log.WithError(err).Warn("Unable to get my own coordinate, check config") 131 | return err 132 | } 133 | 134 | for _, member := range as.meshService().Serf().Members() { 135 | var rtt int32 136 | memberCoord, ok := as.meshService().Serf().GetCachedCoordinate(member.Name) 137 | if ok && memberCoord != nil { 138 | d := memberCoord.DistanceTo(myCoord) 139 | rtt = int32(d / time.Millisecond) 140 | } 141 | 142 | tags := make([]*MemberInfoTag, 0) 143 | for tagKey, tagValue := range member.Tags { 144 | tags = append(tags, &MemberInfoTag{ 145 | Key: tagKey, 146 | Value: tagValue, 147 | }) 148 | } 149 | 150 | memberInfo := &MemberInfo{ 151 | NodeName: member.Name, 152 | Addr: member.Addr.String(), 153 | Status: member.Status.String(), 154 | RttMsec: rtt, 155 | Tags: tags, 156 | } 157 | 158 | if err := agentNodesServer.Send(memberInfo); err != nil { 159 | log.WithError(err).Error("unable to stream send nodes info") 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // WaitForChangeInMesh ... 167 | func (as *MeshAgentServer) WaitForChangeInMesh(wi *WaitInfo, server Agent_WaitForChangeInMeshServer) error { 168 | 169 | ch := make(chan serf.Event) 170 | key := fmt.Sprintf("agent-waitforchange-%d", rand.Int63n(math.MaxInt64)) 171 | as.meshService().RegisterEventNotifier(key, &ch) 172 | 173 | for { 174 | select { 175 | case <-ch: 176 | as.meshService().DeregisterEventNotifier(key) 177 | server.Send(&WaitResponse{ 178 | WasTimeout: false, 179 | ChangesOccured: true, 180 | }) 181 | return nil 182 | case <-time.After(time.Duration(wi.TimeoutSecs) * time.Second): 183 | as.meshService().DeregisterEventNotifier(key) 184 | server.Send(&WaitResponse{ 185 | WasTimeout: true, 186 | ChangesOccured: false, 187 | }) 188 | return nil 189 | } 190 | } 191 | } 192 | 193 | // RTT ... 194 | func (as *MeshAgentServer) RTT(cte *AgentEmpty, rttServer Agent_RTTServer) error { 195 | log.Trace("agent: RTT requested") 196 | 197 | ch := make(chan RTTResponse) 198 | doneCh := make(chan struct{}) 199 | 200 | go func() { 201 | for { 202 | select { 203 | case rtt := <-ch: 204 | //log.WithField("rtt", rtt).Trace("RTT") 205 | 206 | rtts := make([]*RTTNodeInfo, len(rtt.Rtts)) 207 | for idx, rttResponseInfo := range rtt.Rtts { 208 | rtts[idx] = &RTTNodeInfo{ 209 | NodeName: rttResponseInfo.Node, 210 | RttMsec: rttResponseInfo.RttMsec, 211 | } 212 | } 213 | rttInfo := &RTTInfo{ 214 | NodeName: rtt.Node, 215 | Rtts: rtts, 216 | } 217 | if err := rttServer.Send(rttInfo); err != nil { 218 | log.WithError(err).Error("unable to stream send rtt info") 219 | } 220 | 221 | case <-doneCh: 222 | return 223 | } 224 | } 225 | }() 226 | as.meshService().setRttResponseCh(&ch) 227 | 228 | // send a user event which makes all nodes report their rtts 229 | rttRequestBuf, _ := proto.Marshal(&RTTRequest{ 230 | RequestedBy: as.meshService().NodeName, 231 | }) 232 | as.meshService().Serf().UserEvent(serfEventMarkerRTTReq, []byte(rttRequestBuf), true) 233 | 234 | // wait until all are collected and streamed out 235 | time.Sleep(time.Duration(as.meshService().Serf().NumNodes()+2) * time.Second) 236 | 237 | // done 238 | doneCh <- struct{}{} 239 | 240 | return nil 241 | 242 | } 243 | 244 | // StartAgentGrpcService .. 245 | func (as *MeshAgentServer) StartAgentGrpcService() error { 246 | lis, err := net.Listen("unix", as.grpcBindSocket) 247 | if err != nil { 248 | log.Errorf("failed to listen: %v", err) 249 | return errors.New("unable to start grpc mesh service") 250 | } 251 | 252 | if as.grpcBindSocketID != "" { 253 | arr := strings.Split(as.grpcBindSocketID, ":") 254 | if len(arr) == 2 { 255 | 256 | uid, _ := strconv.Atoi(arr[0]) 257 | gid, _ := strconv.Atoi(arr[1]) 258 | 259 | if err := os.Chown(as.grpcBindSocket, uid, gid); err != nil { 260 | log.WithError(err).Error("unable to assign uid:gid as per -grpc-bing-socket-id") 261 | os.Exit(10) 262 | } 263 | } 264 | } 265 | 266 | RegisterAgentServer(as.grpcServer, as) 267 | 268 | if err := as.grpcServer.Serve(lis); err != nil { 269 | log.Errorf("failed to serve: %v", err) 270 | return errors.New("unable to start grpc mesh service") 271 | } 272 | 273 | return nil 274 | } 275 | 276 | // StopAgentGrpcService ... 277 | func (as *MeshAgentServer) StopAgentGrpcService() { 278 | 279 | log.Debug("Stopping gRPC Agent service") 280 | as.grpcServer.GracefulStop() 281 | log.Info("Stopped gRPC Agent service") 282 | } 283 | -------------------------------------------------------------------------------- /meshservice/agent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package meshservice; 4 | 5 | option go_package = "github.com/aschmidt75/wgmesh/meshservice"; 6 | 7 | service Agent { 8 | 9 | // Info returns a summary about the running mesh 10 | rpc Info(AgentEmpty) returns (MeshInfo) {} 11 | 12 | // Nodes streams the current list of nodes known to the mesh 13 | rpc Nodes(AgentEmpty) returns (stream MemberInfo) {} 14 | 15 | // This methods blocks until a change in the mesh setup 16 | // has occured 17 | rpc WaitForChangeInMesh(WaitInfo) returns (stream WaitResponse) {} 18 | 19 | // Tag sets a tag on a wgmesh node 20 | rpc Tag(NodeTag) returns (TagResult) {} 21 | 22 | // Untag remove a tag on a wgmesh node 23 | rpc Untag(NodeTag) returns (TagResult) {} 24 | 25 | // Tags streams all tags of the local node 26 | rpc Tags(AgentEmpty) returns (stream NodeTag) {} 27 | 28 | // RTT yields the complete rtt timings for all nodes 29 | rpc RTT(AgentEmpty) returns (stream RTTInfo) {} 30 | } 31 | 32 | message AgentEmpty { 33 | } 34 | 35 | message MeshInfo { 36 | string name = 1; 37 | int32 nodeCount = 2; 38 | string nodeName = 3; 39 | int64 meshCeationTS = 4; 40 | int64 nodeJoinTS = 5; 41 | } 42 | 43 | message MemberInfoTag { 44 | string key = 1; 45 | string value = 2; 46 | } 47 | 48 | message MemberInfo { 49 | string nodeName = 1; 50 | string addr = 2; 51 | string status = 3; 52 | int32 rttMsec = 4; 53 | repeated MemberInfoTag tags = 5; 54 | bool isLocalNode = 6; 55 | } 56 | 57 | message RTTNodeInfo { 58 | string nodeName = 1; 59 | int32 rttMsec = 2; 60 | } 61 | 62 | message RTTInfo { 63 | string nodeName = 1; 64 | repeated RTTNodeInfo rtts = 2; 65 | } 66 | 67 | message NodeTag { 68 | string key = 1; 69 | string value = 2; 70 | } 71 | 72 | message TagResult { 73 | bool ok = 1; 74 | } 75 | 76 | message WaitInfo { 77 | int32 timeoutSecs = 1; 78 | } 79 | 80 | message WaitResponse { 81 | // true if we ran into a timeout 82 | bool wasTimeout = 1; 83 | 84 | // true if changes occured within the 85 | // mesh setup (nodes joined, left, tags changed, other..) 86 | bool changesOccured = 2; 87 | } -------------------------------------------------------------------------------- /meshservice/agent_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package meshservice 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // AgentClient is the client API for Agent service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type AgentClient interface { 21 | // Info returns a summary about the running mesh 22 | Info(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (*MeshInfo, error) 23 | // Nodes streams the current list of nodes known to the mesh 24 | Nodes(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (Agent_NodesClient, error) 25 | // This methods blocks until a change in the mesh setup 26 | // has occured 27 | WaitForChangeInMesh(ctx context.Context, in *WaitInfo, opts ...grpc.CallOption) (Agent_WaitForChangeInMeshClient, error) 28 | // Tag sets a tag on a wgmesh node 29 | Tag(ctx context.Context, in *NodeTag, opts ...grpc.CallOption) (*TagResult, error) 30 | // Untag remove a tag on a wgmesh node 31 | Untag(ctx context.Context, in *NodeTag, opts ...grpc.CallOption) (*TagResult, error) 32 | // Tags streams all tags of the local node 33 | Tags(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (Agent_TagsClient, error) 34 | // RTT yields the complete rtt timings for all nodes 35 | RTT(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (Agent_RTTClient, error) 36 | } 37 | 38 | type agentClient struct { 39 | cc grpc.ClientConnInterface 40 | } 41 | 42 | func NewAgentClient(cc grpc.ClientConnInterface) AgentClient { 43 | return &agentClient{cc} 44 | } 45 | 46 | func (c *agentClient) Info(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (*MeshInfo, error) { 47 | out := new(MeshInfo) 48 | err := c.cc.Invoke(ctx, "/meshservice.Agent/Info", in, out, opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | 55 | func (c *agentClient) Nodes(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (Agent_NodesClient, error) { 56 | stream, err := c.cc.NewStream(ctx, &Agent_ServiceDesc.Streams[0], "/meshservice.Agent/Nodes", opts...) 57 | if err != nil { 58 | return nil, err 59 | } 60 | x := &agentNodesClient{stream} 61 | if err := x.ClientStream.SendMsg(in); err != nil { 62 | return nil, err 63 | } 64 | if err := x.ClientStream.CloseSend(); err != nil { 65 | return nil, err 66 | } 67 | return x, nil 68 | } 69 | 70 | type Agent_NodesClient interface { 71 | Recv() (*MemberInfo, error) 72 | grpc.ClientStream 73 | } 74 | 75 | type agentNodesClient struct { 76 | grpc.ClientStream 77 | } 78 | 79 | func (x *agentNodesClient) Recv() (*MemberInfo, error) { 80 | m := new(MemberInfo) 81 | if err := x.ClientStream.RecvMsg(m); err != nil { 82 | return nil, err 83 | } 84 | return m, nil 85 | } 86 | 87 | func (c *agentClient) WaitForChangeInMesh(ctx context.Context, in *WaitInfo, opts ...grpc.CallOption) (Agent_WaitForChangeInMeshClient, error) { 88 | stream, err := c.cc.NewStream(ctx, &Agent_ServiceDesc.Streams[1], "/meshservice.Agent/WaitForChangeInMesh", opts...) 89 | if err != nil { 90 | return nil, err 91 | } 92 | x := &agentWaitForChangeInMeshClient{stream} 93 | if err := x.ClientStream.SendMsg(in); err != nil { 94 | return nil, err 95 | } 96 | if err := x.ClientStream.CloseSend(); err != nil { 97 | return nil, err 98 | } 99 | return x, nil 100 | } 101 | 102 | type Agent_WaitForChangeInMeshClient interface { 103 | Recv() (*WaitResponse, error) 104 | grpc.ClientStream 105 | } 106 | 107 | type agentWaitForChangeInMeshClient struct { 108 | grpc.ClientStream 109 | } 110 | 111 | func (x *agentWaitForChangeInMeshClient) Recv() (*WaitResponse, error) { 112 | m := new(WaitResponse) 113 | if err := x.ClientStream.RecvMsg(m); err != nil { 114 | return nil, err 115 | } 116 | return m, nil 117 | } 118 | 119 | func (c *agentClient) Tag(ctx context.Context, in *NodeTag, opts ...grpc.CallOption) (*TagResult, error) { 120 | out := new(TagResult) 121 | err := c.cc.Invoke(ctx, "/meshservice.Agent/Tag", in, out, opts...) 122 | if err != nil { 123 | return nil, err 124 | } 125 | return out, nil 126 | } 127 | 128 | func (c *agentClient) Untag(ctx context.Context, in *NodeTag, opts ...grpc.CallOption) (*TagResult, error) { 129 | out := new(TagResult) 130 | err := c.cc.Invoke(ctx, "/meshservice.Agent/Untag", in, out, opts...) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return out, nil 135 | } 136 | 137 | func (c *agentClient) Tags(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (Agent_TagsClient, error) { 138 | stream, err := c.cc.NewStream(ctx, &Agent_ServiceDesc.Streams[2], "/meshservice.Agent/Tags", opts...) 139 | if err != nil { 140 | return nil, err 141 | } 142 | x := &agentTagsClient{stream} 143 | if err := x.ClientStream.SendMsg(in); err != nil { 144 | return nil, err 145 | } 146 | if err := x.ClientStream.CloseSend(); err != nil { 147 | return nil, err 148 | } 149 | return x, nil 150 | } 151 | 152 | type Agent_TagsClient interface { 153 | Recv() (*NodeTag, error) 154 | grpc.ClientStream 155 | } 156 | 157 | type agentTagsClient struct { 158 | grpc.ClientStream 159 | } 160 | 161 | func (x *agentTagsClient) Recv() (*NodeTag, error) { 162 | m := new(NodeTag) 163 | if err := x.ClientStream.RecvMsg(m); err != nil { 164 | return nil, err 165 | } 166 | return m, nil 167 | } 168 | 169 | func (c *agentClient) RTT(ctx context.Context, in *AgentEmpty, opts ...grpc.CallOption) (Agent_RTTClient, error) { 170 | stream, err := c.cc.NewStream(ctx, &Agent_ServiceDesc.Streams[3], "/meshservice.Agent/RTT", opts...) 171 | if err != nil { 172 | return nil, err 173 | } 174 | x := &agentRTTClient{stream} 175 | if err := x.ClientStream.SendMsg(in); err != nil { 176 | return nil, err 177 | } 178 | if err := x.ClientStream.CloseSend(); err != nil { 179 | return nil, err 180 | } 181 | return x, nil 182 | } 183 | 184 | type Agent_RTTClient interface { 185 | Recv() (*RTTInfo, error) 186 | grpc.ClientStream 187 | } 188 | 189 | type agentRTTClient struct { 190 | grpc.ClientStream 191 | } 192 | 193 | func (x *agentRTTClient) Recv() (*RTTInfo, error) { 194 | m := new(RTTInfo) 195 | if err := x.ClientStream.RecvMsg(m); err != nil { 196 | return nil, err 197 | } 198 | return m, nil 199 | } 200 | 201 | // AgentServer is the server API for Agent service. 202 | // All implementations must embed UnimplementedAgentServer 203 | // for forward compatibility 204 | type AgentServer interface { 205 | // Info returns a summary about the running mesh 206 | Info(context.Context, *AgentEmpty) (*MeshInfo, error) 207 | // Nodes streams the current list of nodes known to the mesh 208 | Nodes(*AgentEmpty, Agent_NodesServer) error 209 | // This methods blocks until a change in the mesh setup 210 | // has occured 211 | WaitForChangeInMesh(*WaitInfo, Agent_WaitForChangeInMeshServer) error 212 | // Tag sets a tag on a wgmesh node 213 | Tag(context.Context, *NodeTag) (*TagResult, error) 214 | // Untag remove a tag on a wgmesh node 215 | Untag(context.Context, *NodeTag) (*TagResult, error) 216 | // Tags streams all tags of the local node 217 | Tags(*AgentEmpty, Agent_TagsServer) error 218 | // RTT yields the complete rtt timings for all nodes 219 | RTT(*AgentEmpty, Agent_RTTServer) error 220 | mustEmbedUnimplementedAgentServer() 221 | } 222 | 223 | // UnimplementedAgentServer must be embedded to have forward compatible implementations. 224 | type UnimplementedAgentServer struct { 225 | } 226 | 227 | func (UnimplementedAgentServer) Info(context.Context, *AgentEmpty) (*MeshInfo, error) { 228 | return nil, status.Errorf(codes.Unimplemented, "method Info not implemented") 229 | } 230 | func (UnimplementedAgentServer) Nodes(*AgentEmpty, Agent_NodesServer) error { 231 | return status.Errorf(codes.Unimplemented, "method Nodes not implemented") 232 | } 233 | func (UnimplementedAgentServer) WaitForChangeInMesh(*WaitInfo, Agent_WaitForChangeInMeshServer) error { 234 | return status.Errorf(codes.Unimplemented, "method WaitForChangeInMesh not implemented") 235 | } 236 | func (UnimplementedAgentServer) Tag(context.Context, *NodeTag) (*TagResult, error) { 237 | return nil, status.Errorf(codes.Unimplemented, "method Tag not implemented") 238 | } 239 | func (UnimplementedAgentServer) Untag(context.Context, *NodeTag) (*TagResult, error) { 240 | return nil, status.Errorf(codes.Unimplemented, "method Untag not implemented") 241 | } 242 | func (UnimplementedAgentServer) Tags(*AgentEmpty, Agent_TagsServer) error { 243 | return status.Errorf(codes.Unimplemented, "method Tags not implemented") 244 | } 245 | func (UnimplementedAgentServer) RTT(*AgentEmpty, Agent_RTTServer) error { 246 | return status.Errorf(codes.Unimplemented, "method RTT not implemented") 247 | } 248 | func (UnimplementedAgentServer) mustEmbedUnimplementedAgentServer() {} 249 | 250 | // UnsafeAgentServer may be embedded to opt out of forward compatibility for this service. 251 | // Use of this interface is not recommended, as added methods to AgentServer will 252 | // result in compilation errors. 253 | type UnsafeAgentServer interface { 254 | mustEmbedUnimplementedAgentServer() 255 | } 256 | 257 | func RegisterAgentServer(s grpc.ServiceRegistrar, srv AgentServer) { 258 | s.RegisterService(&Agent_ServiceDesc, srv) 259 | } 260 | 261 | func _Agent_Info_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 262 | in := new(AgentEmpty) 263 | if err := dec(in); err != nil { 264 | return nil, err 265 | } 266 | if interceptor == nil { 267 | return srv.(AgentServer).Info(ctx, in) 268 | } 269 | info := &grpc.UnaryServerInfo{ 270 | Server: srv, 271 | FullMethod: "/meshservice.Agent/Info", 272 | } 273 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 274 | return srv.(AgentServer).Info(ctx, req.(*AgentEmpty)) 275 | } 276 | return interceptor(ctx, in, info, handler) 277 | } 278 | 279 | func _Agent_Nodes_Handler(srv interface{}, stream grpc.ServerStream) error { 280 | m := new(AgentEmpty) 281 | if err := stream.RecvMsg(m); err != nil { 282 | return err 283 | } 284 | return srv.(AgentServer).Nodes(m, &agentNodesServer{stream}) 285 | } 286 | 287 | type Agent_NodesServer interface { 288 | Send(*MemberInfo) error 289 | grpc.ServerStream 290 | } 291 | 292 | type agentNodesServer struct { 293 | grpc.ServerStream 294 | } 295 | 296 | func (x *agentNodesServer) Send(m *MemberInfo) error { 297 | return x.ServerStream.SendMsg(m) 298 | } 299 | 300 | func _Agent_WaitForChangeInMesh_Handler(srv interface{}, stream grpc.ServerStream) error { 301 | m := new(WaitInfo) 302 | if err := stream.RecvMsg(m); err != nil { 303 | return err 304 | } 305 | return srv.(AgentServer).WaitForChangeInMesh(m, &agentWaitForChangeInMeshServer{stream}) 306 | } 307 | 308 | type Agent_WaitForChangeInMeshServer interface { 309 | Send(*WaitResponse) error 310 | grpc.ServerStream 311 | } 312 | 313 | type agentWaitForChangeInMeshServer struct { 314 | grpc.ServerStream 315 | } 316 | 317 | func (x *agentWaitForChangeInMeshServer) Send(m *WaitResponse) error { 318 | return x.ServerStream.SendMsg(m) 319 | } 320 | 321 | func _Agent_Tag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 322 | in := new(NodeTag) 323 | if err := dec(in); err != nil { 324 | return nil, err 325 | } 326 | if interceptor == nil { 327 | return srv.(AgentServer).Tag(ctx, in) 328 | } 329 | info := &grpc.UnaryServerInfo{ 330 | Server: srv, 331 | FullMethod: "/meshservice.Agent/Tag", 332 | } 333 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 334 | return srv.(AgentServer).Tag(ctx, req.(*NodeTag)) 335 | } 336 | return interceptor(ctx, in, info, handler) 337 | } 338 | 339 | func _Agent_Untag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 340 | in := new(NodeTag) 341 | if err := dec(in); err != nil { 342 | return nil, err 343 | } 344 | if interceptor == nil { 345 | return srv.(AgentServer).Untag(ctx, in) 346 | } 347 | info := &grpc.UnaryServerInfo{ 348 | Server: srv, 349 | FullMethod: "/meshservice.Agent/Untag", 350 | } 351 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 352 | return srv.(AgentServer).Untag(ctx, req.(*NodeTag)) 353 | } 354 | return interceptor(ctx, in, info, handler) 355 | } 356 | 357 | func _Agent_Tags_Handler(srv interface{}, stream grpc.ServerStream) error { 358 | m := new(AgentEmpty) 359 | if err := stream.RecvMsg(m); err != nil { 360 | return err 361 | } 362 | return srv.(AgentServer).Tags(m, &agentTagsServer{stream}) 363 | } 364 | 365 | type Agent_TagsServer interface { 366 | Send(*NodeTag) error 367 | grpc.ServerStream 368 | } 369 | 370 | type agentTagsServer struct { 371 | grpc.ServerStream 372 | } 373 | 374 | func (x *agentTagsServer) Send(m *NodeTag) error { 375 | return x.ServerStream.SendMsg(m) 376 | } 377 | 378 | func _Agent_RTT_Handler(srv interface{}, stream grpc.ServerStream) error { 379 | m := new(AgentEmpty) 380 | if err := stream.RecvMsg(m); err != nil { 381 | return err 382 | } 383 | return srv.(AgentServer).RTT(m, &agentRTTServer{stream}) 384 | } 385 | 386 | type Agent_RTTServer interface { 387 | Send(*RTTInfo) error 388 | grpc.ServerStream 389 | } 390 | 391 | type agentRTTServer struct { 392 | grpc.ServerStream 393 | } 394 | 395 | func (x *agentRTTServer) Send(m *RTTInfo) error { 396 | return x.ServerStream.SendMsg(m) 397 | } 398 | 399 | // Agent_ServiceDesc is the grpc.ServiceDesc for Agent service. 400 | // It's only intended for direct use with grpc.RegisterService, 401 | // and not to be introspected or modified (even as a copy) 402 | var Agent_ServiceDesc = grpc.ServiceDesc{ 403 | ServiceName: "meshservice.Agent", 404 | HandlerType: (*AgentServer)(nil), 405 | Methods: []grpc.MethodDesc{ 406 | { 407 | MethodName: "Info", 408 | Handler: _Agent_Info_Handler, 409 | }, 410 | { 411 | MethodName: "Tag", 412 | Handler: _Agent_Tag_Handler, 413 | }, 414 | { 415 | MethodName: "Untag", 416 | Handler: _Agent_Untag_Handler, 417 | }, 418 | }, 419 | Streams: []grpc.StreamDesc{ 420 | { 421 | StreamName: "Nodes", 422 | Handler: _Agent_Nodes_Handler, 423 | ServerStreams: true, 424 | }, 425 | { 426 | StreamName: "WaitForChangeInMesh", 427 | Handler: _Agent_WaitForChangeInMesh_Handler, 428 | ServerStreams: true, 429 | }, 430 | { 431 | StreamName: "Tags", 432 | Handler: _Agent_Tags_Handler, 433 | ServerStreams: true, 434 | }, 435 | { 436 | StreamName: "RTT", 437 | Handler: _Agent_RTT_Handler, 438 | ServerStreams: true, 439 | }, 440 | }, 441 | Metadata: "agent.proto", 442 | } 443 | -------------------------------------------------------------------------------- /meshservice/export.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "encoding/json" 5 | ioutil "io/ioutil" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "time" 11 | 12 | "github.com/hashicorp/serf/serf" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // SetMemberlistExportFile sets the file name for an export 17 | // of the current memberlist. If empty no file is written 18 | func (ms *MeshService) SetMemberlistExportFile(f string) { 19 | ms.memberExportFile = f 20 | } 21 | 22 | type exportedMember struct { 23 | Addr string `json:"addr"` 24 | Status string `json:"st"` 25 | RTT int64 `json:"rtt"` 26 | Tags map[string]string `json:"tags"` 27 | } 28 | 29 | type exportedService struct { 30 | Nodes []string `json:"nodes"` 31 | Port int `json:"port"` 32 | Tags map[string]string `json:"tags"` 33 | } 34 | 35 | type exportedMemberList struct { 36 | Members map[string]exportedMember `json:"members"` 37 | Services map[string]exportedService `json:"services"` 38 | LastUpdate int64 `json:"lastUpdate"` 39 | } 40 | 41 | func (ms *MeshService) updateMemberExport() { 42 | 43 | if ms.lastUpdatedTS.Unix() <= ms.lastExportedTS.Unix() { 44 | return 45 | } 46 | log.Debug("updateMemberExport") 47 | 48 | e := &exportedMemberList{ 49 | Members: make(map[string]exportedMember), 50 | Services: make(map[string]exportedService), 51 | LastUpdate: ms.lastUpdatedTS.Unix(), 52 | } 53 | myCoord, err := ms.Serf().GetCoordinate() 54 | if err != nil { 55 | log.WithError(err).Warn("Unable to get my own coordinate, check config") 56 | myCoord = nil 57 | } 58 | 59 | for _, member := range ms.Serf().Members() { 60 | em := exportedMember{ 61 | Addr: member.Addr.String(), 62 | Status: member.Status.String(), 63 | Tags: member.Tags, 64 | } 65 | // compute RTT if we have all distances 66 | memberCoord, ok := ms.Serf().GetCachedCoordinate(member.Name) 67 | if ok && memberCoord != nil { 68 | d := memberCoord.DistanceTo(myCoord) 69 | em.RTT = int64(d / time.Millisecond) 70 | 71 | // TODO: for LAN mode add Microseconds as well 72 | } 73 | 74 | // 75 | e.Members[member.Name] = em 76 | 77 | // grab tags for service entries, put into service map 78 | ms.processTagsForMember(&member, e) 79 | } 80 | 81 | content, err := json.MarshalIndent(e, "", " ") 82 | if err != nil { 83 | log.WithError(err).Error("unable to write to file") 84 | } 85 | 86 | ioutil.WriteFile(ms.memberExportFile, content, 0640) 87 | 88 | ms.lastExportedTS = ms.lastUpdatedTS 89 | } 90 | 91 | func (ms *MeshService) processTagsForMember(member *serf.Member, e *exportedMemberList) { 92 | svcKeyRe := regexp.MustCompile(`^svc:`) 93 | for k, v := range member.Tags { 94 | arr := svcKeyRe.Split(k, 2) 95 | if arr != nil && len(arr) == 2 && arr[1] != "" { 96 | 97 | expSvc, ex := e.Services[arr[1]] 98 | if !ex { 99 | expSvc = exportedService{ 100 | Tags: make(map[string]string), 101 | Nodes: make([]string, 0), 102 | } 103 | } 104 | 105 | // put member on the node list 106 | expSvc.Nodes = append(expSvc.Nodes, member.Name) 107 | 108 | // Split value 109 | arrV := strings.Split(v, ",") 110 | for _, elemV := range arrV { 111 | arrE := strings.Split(elemV, "=") 112 | 113 | if len(arrE) > 0 { 114 | ek := arrE[0] 115 | 116 | if ek == "port" && len(arrE) == 2 { 117 | expSvc.Port, _ = strconv.Atoi(arrE[1]) 118 | continue 119 | } 120 | // put into general tags otherwise 121 | if len(arrE) >= 1 { 122 | ek = arrE[0] 123 | } 124 | ev := "" 125 | if len(arrE) == 2 { 126 | ev = arrE[1] 127 | } 128 | expSvc.Tags[ek] = ev 129 | 130 | } 131 | } 132 | 133 | e.Services[arr[1]] = expSvc 134 | 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /meshservice/grpc.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | context "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "math/rand" 9 | "net" 10 | "strconv" 11 | "strings" 12 | 13 | wgwrapper "github.com/aschmidt75/go-wg-wrapper/pkg/wgwrapper" 14 | "github.com/cristalhq/jwt/v3" 15 | log "github.com/sirupsen/logrus" 16 | grpc "google.golang.org/grpc" 17 | "google.golang.org/grpc/credentials" 18 | "google.golang.org/grpc/metadata" 19 | "google.golang.org/protobuf/proto" 20 | ) 21 | 22 | // Begin starts the join process with a handshake 23 | func (ms *MeshService) Begin(ctx context.Context, req *HandshakeRequest) (*HandshakeResponse, error) { 24 | log.WithField("req", req).Trace("Got begin request") 25 | 26 | if req.MeshName != ms.MeshName { 27 | return &HandshakeResponse{ 28 | Result: HandshakeResponse_ERROR, 29 | ErrorMessage: "Unknown mesh", 30 | }, nil 31 | } 32 | 33 | return &HandshakeResponse{ 34 | Result: HandshakeResponse_OK, 35 | JoinToken: randomTokenAsString(), 36 | }, nil 37 | } 38 | 39 | func (ms *MeshService) parseJWT(md metadata.MD) error { 40 | log.WithField("md", md).Trace("parseJWT") 41 | if t, ok := md["authorization"]; ok { 42 | for _, value := range t { 43 | if strings.HasPrefix(value, "Bearer: ") { 44 | arr := strings.Split(value, " ") 45 | if len(arr) > 1 { 46 | tokenStr := arr[1] 47 | 48 | key := []byte(`secret`) 49 | verifier, err := jwt.NewVerifierHS(jwt.HS256, key) 50 | if err != nil { 51 | return err 52 | } 53 | token, err := jwt.ParseAndVerifyString(tokenStr, verifier) 54 | if err != nil { 55 | log.WithError(err).Error("Unable to parse/verify join token") 56 | return err 57 | } 58 | var claims jwt.StandardClaims 59 | err = json.Unmarshal(token.RawClaims(), &claims) 60 | if err != nil { 61 | log.WithError(err).Error("Unable to parse/verify join token claims") 62 | return err 63 | } 64 | 65 | // TODO check claims, check authn/authz requirements 66 | // log.WithField("claims", claims).Debug("Verified handshake token claims") 67 | 68 | return nil 69 | } 70 | } 71 | } 72 | } 73 | 74 | return errors.New("authorization header not found or not valid") 75 | } 76 | 77 | // Join allows other nodes to join by sending a JoinRequest 78 | func (ms *MeshService) Join(ctx context.Context, req *JoinRequest) (*JoinResponse, error) { 79 | 80 | log.WithField("req", req).Trace("Got join request") 81 | 82 | md, ok := metadata.FromIncomingContext(ctx) 83 | if !ok { 84 | return &JoinResponse{ 85 | Result: JoinResponse_ERROR, 86 | ErrorMessage: "internal error while processing authorization", 87 | JoiningNodeMeshIP: "", 88 | }, nil 89 | } 90 | if err := ms.parseJWT(md); err != nil { 91 | log.Error(err) 92 | return &JoinResponse{ 93 | Result: JoinResponse_ERROR, 94 | ErrorMessage: "error in authorization", 95 | JoiningNodeMeshIP: "", 96 | }, nil 97 | } 98 | 99 | if req.MeshName != ms.MeshName { 100 | return &JoinResponse{ 101 | Result: JoinResponse_ERROR, 102 | ErrorMessage: "Unknown mesh", 103 | JoiningNodeMeshIP: "", 104 | }, nil 105 | } 106 | 107 | // choose a random ip address from the address pool of this node 108 | // which has not been used before. Choose from cidr range or 109 | // if specified from the IPAM cidr range 110 | var mip net.IP 111 | for { 112 | var _net *net.IPNet = &ms.CIDRRange 113 | if ms.CIDRRangeIPAM != nil { 114 | _net = ms.CIDRRangeIPAM 115 | } 116 | 117 | mip, _ = newIPInNet(*_net) 118 | 119 | if ms.isIPAvailable(mip) { 120 | break 121 | } 122 | } 123 | 124 | // TODO: check if joining node wishes to have a explicit node name 125 | // if so, check if this name is already in use. 126 | if ms.isNodeNameInUse(req.NodeName) { 127 | return &JoinResponse{ 128 | Result: JoinResponse_ERROR, 129 | ErrorMessage: "Request node name is already in use", 130 | JoiningNodeMeshIP: "", 131 | }, nil 132 | } 133 | 134 | // 135 | targetWGIP := net.IPNet{ 136 | mip, 137 | net.CIDRMask(32, 32), 138 | } 139 | 140 | /* 141 | keepAliveSeconds := 0 142 | if req.Nat { 143 | keepAliveSeconds = 20 144 | } 145 | */ 146 | // take public key and endpoint, add as peer to own wireguard interface 147 | p := wgwrapper.WireguardPeer{ 148 | RemoteEndpointIP: req.EndpointIP, 149 | ListenPort: int(req.EndpointPort), 150 | Pubkey: req.Pubkey, 151 | AllowedIPs: []net.IPNet{ 152 | targetWGIP, 153 | }, 154 | Psk: nil, 155 | //PersistentKeepaliveInterval: time.Duration(keepAliveSeconds) * time.Second, 156 | } 157 | log.WithField("peer", p).Trace("Adding peer") 158 | 159 | wg := wgwrapper.New() 160 | 161 | ok, err := wg.AddPeer(ms.WireguardInterface, p) 162 | if err != nil { 163 | log.Error(err) 164 | return &JoinResponse{ 165 | Result: JoinResponse_ERROR, 166 | ErrorMessage: "Unable to add peer", 167 | JoiningNodeMeshIP: "", 168 | }, nil 169 | } 170 | if !ok && err == nil { 171 | return &JoinResponse{ 172 | Result: JoinResponse_ERROR, 173 | ErrorMessage: "Peer already present", 174 | JoiningNodeMeshIP: "", 175 | }, nil 176 | } 177 | 178 | log.WithFields(log.Fields{ 179 | "ip": mip.String(), 180 | }).Info("node joined mesh") 181 | log.WithFields(log.Fields{ 182 | "ip": mip.String(), 183 | "pk": req.Pubkey, 184 | }).Debug("node joined mesh") 185 | 186 | // send out a Peer Update as message to all serf nodes 187 | peerAnnouncementBuf, _ := proto.Marshal(&Peer{ 188 | Type: Peer_JOIN, 189 | Pubkey: req.Pubkey, 190 | EndpointIP: req.EndpointIP, 191 | EndpointPort: int32(req.EndpointPort), 192 | MeshIP: targetWGIP.IP.String(), 193 | }) 194 | // send out a join request event 195 | ms.Serf().UserEvent(serfEventMarkerJoin, []byte(peerAnnouncementBuf), true) 196 | 197 | // return successful join response to client 198 | return &JoinResponse{ 199 | Result: JoinResponse_OK, 200 | ErrorMessage: "", 201 | JoiningNodeMeshIP: mip.String(), 202 | MeshCidr: ms.CIDRRange.String(), 203 | CreationTS: int64(ms.creationTS.Unix()), 204 | SerfEncryptionKey: ms.GetEncryptionKey(), 205 | }, nil 206 | } 207 | 208 | // Peers serves a list of all current peers, starting with this node. 209 | // All data is derived from serf's memberlist 210 | func (ms *MeshService) Peers(e *Empty, stream Mesh_PeersServer) error { 211 | for _, member := range ms.Serf().Members() { 212 | t := member.Tags 213 | 214 | //log.WithField("t", t).Trace("Peers: sending member tags") 215 | 216 | port, _ := strconv.Atoi(t[nodeTagPort]) 217 | err := stream.Send(&Peer{ 218 | Pubkey: t[nodeTagPubKey], 219 | EndpointIP: t[nodeTagAddr], 220 | EndpointPort: int32(port), 221 | MeshIP: t[nodeTagMeshIP], 222 | Type: Peer_JOIN, 223 | }) 224 | if err != nil { 225 | return err 226 | } 227 | } 228 | 229 | return nil 230 | } 231 | 232 | func newIPInNet(ipnet net.IPNet) (net.IP, error) { 233 | 234 | ipmask := ipnet.Mask 235 | 236 | var newIP [4]byte 237 | if len(ipnet.IP) == 4 { 238 | newIP = [4]byte{ 239 | (byte(rand.Intn(250)+2) & ^ipmask[0]) + ipnet.IP[0], 240 | (byte(rand.Intn(250)) & ^ipmask[1]) + ipnet.IP[1], 241 | (byte(rand.Intn(250)) & ^ipmask[2]) + ipnet.IP[2], 242 | (byte(rand.Intn(250)+1) & ^ipmask[3]) + ipnet.IP[3], 243 | } 244 | } 245 | if len(ipnet.IP) == 16 { 246 | newIP = [4]byte{ 247 | (byte(rand.Intn(250)+2) & ^ipmask[0]) + ipnet.IP[12], 248 | (byte(rand.Intn(250)) & ^ipmask[1]) + ipnet.IP[13], 249 | (byte(rand.Intn(250)) & ^ipmask[2]) + ipnet.IP[14], 250 | (byte(rand.Intn(250)+1) & ^ipmask[3]) + ipnet.IP[15], 251 | } 252 | } 253 | log.WithField("newIP", newIP).Trace("newIPInNet.dump") 254 | 255 | return net.IPv4(newIP[0], newIP[1], newIP[2], newIP[3]), nil 256 | } 257 | 258 | func (ms *MeshService) newTLSCredentials() credentials.TransportCredentials { 259 | return credentials.NewTLS(&tls.Config{ 260 | //ServerName: serverNameOverride, 261 | InsecureSkipVerify: false, 262 | ClientAuth: tls.RequireAndVerifyClientCert, 263 | Certificates: []tls.Certificate{ms.TLSConfig.Cert}, 264 | ClientCAs: ms.TLSConfig.CertPool, 265 | }) 266 | } 267 | 268 | // StartGrpcService .. 269 | func (ms *MeshService) StartGrpcService() error { 270 | lis, err := net.Listen("tcp", net.JoinHostPort(ms.GrpcBindAddr, strconv.Itoa(ms.GrpcBindPort))) 271 | if err != nil { 272 | log.Errorf("failed to listen: %v", err) 273 | return errors.New("unable to start grpc mesh service") 274 | } 275 | 276 | if ms.TLSConfig != nil { 277 | log.Debug("Starting TLS gRPC mesh service") 278 | ms.grpcServer = grpc.NewServer(grpc.Creds(ms.newTLSCredentials())) 279 | } else { 280 | log.Warn("Starting an insecure gRPC mesh service") 281 | ms.grpcServer = grpc.NewServer() 282 | } 283 | RegisterMeshServer(ms.grpcServer, ms) 284 | if err := ms.grpcServer.Serve(lis); err != nil { 285 | log.Errorf("failed to serve: %v", err) 286 | return errors.New("unable to start grpc mesh service") 287 | } 288 | 289 | return nil 290 | } 291 | 292 | // StopGrpcService stops the grpc server 293 | func (ms *MeshService) StopGrpcService() { 294 | 295 | log.Debug("Stopping gRPC mesh service") 296 | ms.grpcServer.GracefulStop() 297 | log.Info("Stopped gRPC mesh service") 298 | } 299 | 300 | func (ms *MeshService) isIPAvailable(ip net.IP) bool { 301 | s := ip.String() 302 | 303 | for _, member := range ms.Serf().Members() { 304 | wgIP := member.Tags[nodeTagAddr] 305 | if wgIP == s { 306 | return false 307 | } 308 | } 309 | 310 | return true 311 | } 312 | 313 | func randomTokenAsString() string { 314 | chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 315 | b := make([]byte, 32) 316 | for i := range b { 317 | b[i] = chars[rand.Intn(len(chars))] 318 | } 319 | // return string(b) 320 | return "secret" 321 | } 322 | -------------------------------------------------------------------------------- /meshservice/interface.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "os/exec" 10 | 11 | wgwrapper "github.com/aschmidt75/go-wg-wrapper/pkg/wgwrapper" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func intfNameForMesh(meshName string) string { 16 | return fmt.Sprintf("wg%s", meshName) 17 | } 18 | 19 | // CreateWireguardInterfaceForMesh creates a new wireguard interface based on the 20 | // name of the mesh, a bootstrap IP and a listen port. The interfacae is also up'ed. 21 | func (ms *MeshService) CreateWireguardInterfaceForMesh(bootstrapIP string, wgListenPort int) (string, error) { 22 | log.WithFields(log.Fields{ 23 | "n": ms.MeshName, 24 | "ip": bootstrapIP, 25 | }).Trace("CreateWireguardInterfaceForMesh") 26 | 27 | intfName := intfNameForMesh(ms.MeshName) 28 | 29 | ms.WireguardListenPort = wgListenPort 30 | 31 | i, err := net.InterfaceByName(intfName) 32 | if err == nil { 33 | log.WithField("i", i).Error("Interface already exists") 34 | return "", errors.New("a create wireguard interface for this mesh already exists") 35 | } 36 | 37 | wg := wgwrapper.New() 38 | 39 | wgi := wgwrapper.NewWireguardInterface(intfName, ms.MeshIP) 40 | ms.WireguardInterface = wgi 41 | 42 | err = wg.AddInterface(wgi) 43 | if err != nil { 44 | log.WithField("err", err).Error("unable to create interface") 45 | return "", errors.New("unable to create wireguard interface") 46 | } 47 | 48 | log.WithField("wgi", wgi).Debug("created") 49 | 50 | // listen on a port 51 | wgi.ListenPort = ms.WireguardListenPort 52 | err = wg.Configure(&wgi) 53 | if err != nil { 54 | log.WithField("err", err).Error("unable to configure interface") 55 | return "", errors.New("unable to configure wireguard interface") 56 | } 57 | 58 | err = wg.SetInterfaceUp(wgi) 59 | if err != nil { 60 | log.WithField("err", err).Error("unable to ifup interface") 61 | return "", errors.New("unable to setup wireguard interface") 62 | } 63 | 64 | log.Infof("Created and configured wireguard interface %s", intfName) 65 | 66 | return wgi.PublicKey, nil 67 | } 68 | 69 | // CreateWireguardInterface creates a new wireguard interface based on the 70 | // name of the mesh, and a listen port. The interfacae does not yet carry an internal ip and is not up'ed. 71 | // Returns the pub key 72 | func (ms *MeshService) CreateWireguardInterface(wgListenPort int) (string, error) { 73 | log.WithFields(log.Fields{ 74 | "n": ms.MeshName, 75 | }).Trace("CreateWireguardInterface") 76 | 77 | intfName := intfNameForMesh(ms.MeshName) 78 | 79 | ms.WireguardListenPort = wgListenPort 80 | 81 | i, err := net.InterfaceByName(intfName) 82 | if err == nil { 83 | log.WithField("i", i).Error("Interface already exists") 84 | return "", errors.New("a wireguard interface for this mesh already exists") 85 | } 86 | 87 | wg := wgwrapper.New() 88 | 89 | wgi := wgwrapper.NewWireguardInterfaceNoAddr(intfName) 90 | 91 | err = wg.AddInterfaceNoAddr(wgi) 92 | if err != nil { 93 | log.WithField("err", err).Error("unable to create interface") 94 | return "", errors.New("unable to create wireguard interface") 95 | } 96 | 97 | log.WithField("wgi", wgi).Debug("created") 98 | ms.WireguardInterface = wgi 99 | 100 | // listen on a port 101 | wgi.ListenPort = ms.WireguardListenPort 102 | err = wg.Configure(&wgi) 103 | if err != nil { 104 | log.WithField("err", err).Error("unable to configure interface") 105 | return "", errors.New("unable to configure wireguard interface") 106 | } 107 | 108 | log.Infof("Created and configured wireguard interface %s as no-up", intfName) 109 | 110 | return wgi.PublicKey, nil 111 | } 112 | 113 | // AssignJoiningNodeIP sets the ip address of the wireguard interface 114 | func (ms *MeshService) AssignJoiningNodeIP(ip string) error { 115 | intfName := intfNameForMesh(ms.MeshName) 116 | 117 | intf, err := net.InterfaceByName(intfName) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | // Assign IP if desired and not yet present 123 | a, err := intf.Addrs() 124 | if err != nil { 125 | return err 126 | } 127 | if len(a) == 0 { 128 | cmd := exec.Command("/sbin/ip", "address", "add", "dev", intfName, ip) 129 | var stdout, stderr bytes.Buffer 130 | cmd.Stdout = &stdout 131 | cmd.Stderr = &stderr 132 | err := cmd.Run() 133 | if err != nil { 134 | return err 135 | } 136 | _, errStr := string(stdout.Bytes()), string(stderr.Bytes()) 137 | if len(errStr) > 0 { 138 | e := fmt.Sprintf("/sbin/ip reported: %s", errStr) 139 | return errors.New(e) 140 | } 141 | } 142 | 143 | a, err = intf.Addrs() 144 | if len(a) == 0 { 145 | e := fmt.Sprintf("unable to add ip address %s to interface %s: %s", ip, intfName, err) 146 | return errors.New(e) 147 | } 148 | 149 | return nil 150 | } 151 | 152 | // SetRoute adds a route for the cidr range to the wireguard interface 153 | func (ms *MeshService) SetRoute() error { 154 | wg := wgwrapper.New() 155 | 156 | // Add route 157 | err := wg.SetRoute(ms.WireguardInterface, ms.CIDRRange.String()) 158 | if err != nil { 159 | log.WithError(err).Error("unable to add route to target") 160 | return err 161 | } 162 | log.WithField("target", ms.CIDRRange.String()).Debug("Added route") 163 | 164 | return nil 165 | } 166 | 167 | // RemoveWireguardInterfaceForMesh removes the wireguard interface for this mesh 168 | // Removing the interface will also remove the route(s) associated with it. 169 | func (ms *MeshService) RemoveWireguardInterfaceForMesh() error { 170 | log.WithFields(log.Fields{ 171 | "n": ms.MeshName, 172 | }).Trace("RemoveWireguardInterfaceForMesh") 173 | 174 | intfName := intfNameForMesh(ms.MeshName) 175 | 176 | i, err := net.InterfaceByName(intfName) 177 | if err != nil { 178 | log.WithField("i", i).Error("Interface does not exist") 179 | return nil 180 | } 181 | 182 | wg := wgwrapper.New() 183 | 184 | return wg.DeleteInterface(wgwrapper.WireguardInterface{InterfaceName: intfName}) 185 | } 186 | 187 | // ApplyPeerUpdatesFromStream reads peer data from an incoming stream and apply 188 | // these to the interface. 189 | // Returns a list of MeshIPs from all peers, with the first entry being the bootstrap node 190 | // where we joined. 191 | func (ms *MeshService) ApplyPeerUpdatesFromStream(wg wgwrapper.WireguardWrapper, stream Mesh_PeersClient) []string { 192 | peerCh := make(chan *Peer, 10) 193 | go peerAdder(ms, wg, peerCh) 194 | 195 | res := make([]string, 0) 196 | 197 | for { 198 | peer, err := stream.Recv() 199 | if err == io.EOF { 200 | break 201 | } 202 | if err != nil { 203 | log.WithError(err).Error("Error while receiving peers") 204 | break 205 | } 206 | 207 | peerCh <- peer 208 | 209 | res = append(res, peer.MeshIP) 210 | } 211 | 212 | // terminate peerAdder by sending nil 213 | peerCh <- nil 214 | 215 | return res 216 | } 217 | 218 | func peerAdder(ms *MeshService, wg wgwrapper.WireguardWrapper, peerCh <-chan *Peer) error { 219 | for { 220 | select { 221 | case peer := <-peerCh: 222 | if peer == nil { 223 | log.Trace("processed all peers") 224 | return nil 225 | } 226 | log.WithField("peer", peer).Debug("received peer") 227 | 228 | // Add the peer to the interface, making the peer the 229 | // only allowed ip 230 | ok, err := wg.AddPeer(ms.WireguardInterface, wgwrapper.WireguardPeer{ 231 | RemoteEndpointIP: peer.EndpointIP, 232 | ListenPort: int(peer.EndpointPort), 233 | Pubkey: peer.Pubkey, 234 | AllowedIPs: []net.IPNet{ 235 | { 236 | IP: net.ParseIP(peer.MeshIP), 237 | Mask: net.CIDRMask(32, 32), 238 | }, 239 | }, 240 | }) 241 | 242 | if err != nil { 243 | log.WithError(err).Errorf("unable to add peer %s", peer.Pubkey) 244 | } else { 245 | if !ok { 246 | log.Errorf("unable to add peer %s", peer.Pubkey) 247 | } else { 248 | log.Trace("added peer") 249 | } 250 | } 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /meshservice/meshservice.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "net" 7 | "time" 8 | 9 | wgwrapper "github.com/aschmidt75/go-wg-wrapper/pkg/wgwrapper" 10 | serf "github.com/hashicorp/serf/serf" 11 | grpc "google.golang.org/grpc" 12 | ) 13 | 14 | // MeshService collects all information about running a mesh node 15 | // for both bootstrap and join modes. 16 | type MeshService struct { 17 | // Name of the mesh network. 18 | MeshName string 19 | 20 | // Name of this node 21 | NodeName string 22 | 23 | // eg. 10.232.0.0/16. All nodes in the mesh will have an 24 | // IP address within this range 25 | CIDRRange net.IPNet 26 | 27 | // If set, this bootstrap will assign IP addresses from 28 | // this range only. 29 | CIDRRangeIPAM *net.IPNet 30 | 31 | // Local mesh IP of this node 32 | MeshIP net.IPNet 33 | 34 | // Listen port for Wireguard 35 | WireguardListenPort int 36 | 37 | // Listen IP for Wireguard 38 | WireguardListenIP net.IP 39 | 40 | // Own public key 41 | WireguardPubKey string 42 | 43 | // The interface we're controlling 44 | WireguardInterface wgwrapper.WireguardInterface 45 | 46 | // Bind Address for gRPC Mesh service 47 | GrpcBindAddr string 48 | 49 | // Bind port for gRPC Mesh service 50 | GrpcBindPort int 51 | 52 | // (optional) TLS config struct for gRPC Mesh service 53 | TLSConfig *TLSConfig 54 | 55 | // Serf 56 | cfg *serf.Config 57 | s *serf.Serf 58 | serfEncryptionKey []byte 59 | 60 | // if set, exports the serf member list to this file 61 | memberExportFile string 62 | 63 | // timestamp of latest update to the member state 64 | lastUpdatedTS time.Time 65 | lastExportedTS time.Time 66 | 67 | // gRPC 68 | UnimplementedMeshServer 69 | grpcServer *grpc.Server 70 | 71 | // Local agent gRPC server 72 | MeshAgentServer *MeshAgentServer 73 | 74 | // when the first bootstrap node started this mesh 75 | creationTS time.Time 76 | 77 | // when this node joined the mesh 78 | joinTS time.Time 79 | 80 | // 81 | rttResponseChan *chan RTTResponse 82 | 83 | // 84 | serfEventNotifierMap map[string]SerfEventChan 85 | } 86 | 87 | const ( 88 | nodeTagPort = "_port" 89 | nodeTagAddr = "_addr" 90 | nodeTagPubKey = "_pk" 91 | nodeTagMeshIP = "_i" 92 | nodeTagNodeType = "_t" 93 | 94 | serfEventMarkerJoin = "_j" 95 | serfEventMarkerRTTReq = "_rtt0" 96 | serfEventMarkerRTTRes = "_rtt1" 97 | ) 98 | 99 | // SerfEventChan is a pointer to a channel of serf events, 100 | // so that events can be forwarded to other listeners 101 | type SerfEventChan *chan serf.Event 102 | 103 | // RegisterEventNotifier registers an channel 104 | func (ms *MeshService) RegisterEventNotifier(key string, sec SerfEventChan) { 105 | ms.serfEventNotifierMap[key] = sec 106 | } 107 | 108 | // DeregisterEventNotifier registers an channel 109 | func (ms *MeshService) DeregisterEventNotifier(key string) { 110 | delete(ms.serfEventNotifierMap, key) 111 | } 112 | 113 | // NewMeshService creates a new MeshService for a node 114 | func NewMeshService(meshName string) MeshService { 115 | return MeshService{ 116 | MeshName: meshName, 117 | creationTS: time.Now(), 118 | serfEventNotifierMap: make(map[string]SerfEventChan), 119 | serfEncryptionKey: make([]byte, 0), 120 | } 121 | } 122 | 123 | // SetNodeName applies a name to this node 124 | func (ms *MeshService) SetNodeName(name string) { 125 | if name != "" { 126 | ms.NodeName = name 127 | return 128 | } 129 | 130 | // if no name is given, derive one from the 131 | // IPv4 address 132 | if len(ms.MeshIP.IP) == 16 { 133 | i := int(ms.MeshIP.IP[12]) * 16777216 134 | i += int(ms.MeshIP.IP[13]) * 65536 135 | i += int(ms.MeshIP.IP[14]) * 256 136 | i += int(ms.MeshIP.IP[15]) 137 | ms.NodeName = fmt.Sprintf("%s%X", ms.MeshName, i) 138 | } 139 | if len(ms.MeshIP.IP) == 4 { 140 | i := int(ms.MeshIP.IP[0]) * 16777216 141 | i += int(ms.MeshIP.IP[1]) * 65536 142 | i += int(ms.MeshIP.IP[2]) * 256 143 | i += int(ms.MeshIP.IP[3]) 144 | ms.NodeName = fmt.Sprintf("%s%X", ms.MeshName, i) 145 | } 146 | } 147 | 148 | func (ms *MeshService) setRttResponseCh(ch *chan RTTResponse) { 149 | ms.rttResponseChan = ch 150 | } 151 | 152 | func (ms *MeshService) releaseRttResponseCh() { 153 | ms.rttResponseChan = nil 154 | } 155 | 156 | // SetTimestamps sets the creation and join timestamp after grpc join call 157 | func (ms *MeshService) SetTimestamps(creationTS, joinTS int64) { 158 | ms.creationTS = time.Unix(creationTS, 0) 159 | ms.joinTS = time.Unix(joinTS, 0) 160 | } 161 | 162 | // GetTimestamps returns the creation and join timestamp 163 | func (ms *MeshService) GetTimestamps() (time.Time, time.Time) { 164 | return ms.creationTS, ms.joinTS 165 | } 166 | 167 | // SetEncryptionKey sets serf encryption key from a base64 string 168 | func (ms *MeshService) SetEncryptionKey(encKeyB64 string) error { 169 | var err error 170 | ms.serfEncryptionKey, err = base64.StdEncoding.DecodeString(encKeyB64) 171 | if err != nil { 172 | return err 173 | } 174 | return nil 175 | } 176 | 177 | // GetEncryptionKey returns serf encryption key from a base64 string 178 | func (ms *MeshService) GetEncryptionKey() string { 179 | return base64.StdEncoding.EncodeToString(ms.serfEncryptionKey) 180 | } 181 | 182 | // Serf returns the serf instance 183 | func (ms *MeshService) Serf() *serf.Serf { 184 | return ms.s 185 | } 186 | -------------------------------------------------------------------------------- /meshservice/meshservice.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package meshservice; 4 | 5 | option go_package = "github.com/aschmidt75/wgmesh/meshservice"; 6 | 7 | service Mesh { 8 | // Joining node starts to shake hands and receives a token and 9 | // additional authorization requirements 10 | rpc Begin(HandshakeRequest) returns (HandshakeResponse) {} 11 | 12 | // Join start the join process by sending a JoinRequest 13 | // and receiving a JoinResponse with setup details. 14 | rpc Join(JoinRequest) returns (JoinResponse) {} 15 | 16 | // Peers returns a stream of all peers currently connected to the mesh 17 | rpc Peers(Empty) returns (stream Peer) {} 18 | } 19 | 20 | message Empty { 21 | 22 | } 23 | 24 | // HandshakeRequest includes details about which mesh to join 25 | message HandshakeRequest { 26 | // name of mesh to join 27 | string meshName = 1; 28 | } 29 | 30 | // HandshakeResponse indicates if joining the desired mesh is 31 | // acceptable and may include authenication/authorization 32 | // requirements which joining nodes have to fulfil. 33 | message HandshakeResponse { 34 | enum Result { 35 | OK = 0; 36 | ERROR = 1; 37 | } 38 | Result result = 1; 39 | string errorMessage = 2; 40 | 41 | // token which joining node has to reuse when using Join/Peers methods 42 | string joinToken = 3; 43 | 44 | // additional authentication/authorization requirements which joining nodes have to fulfil 45 | // Reserved for future use 46 | map authReqs = 4; 47 | } 48 | 49 | // JoinRequest is sent by a joining node when their wireguard interface 50 | // is set up and is ready to join. It includes wireguard details such as 51 | // the public key etc, and an optional node name. 52 | message JoinRequest { 53 | // wireguard: public key of joining node 54 | string pubkey = 1; 55 | 56 | // wireguard: endpoint IP of joining node 57 | string endpointIP = 2; 58 | 59 | // wireguard: endpoint UDP port of joining node 60 | int32 endpointPort = 3; 61 | 62 | // name of mesh to join 63 | string meshName = 4; 64 | 65 | // optional name of node 66 | string nodeName = 5; 67 | } 68 | 69 | // JoinResponse indicates if joinrequest has been accepted. If so, 70 | // it includes an IP address for the joining node to assign to its 71 | // wireguard interface, and additional data to fully join the mesh. 72 | message JoinResponse { 73 | enum Result { 74 | OK = 0; 75 | ERROR = 1; 76 | } 77 | Result result = 1; 78 | string errorMessage = 2; 79 | 80 | // this will be the joining's mesh ip 81 | string joiningNodeMeshIP = 3; 82 | 83 | // cidr of the mesh 84 | string meshCidr = 4; 85 | 86 | // creation time stamp 87 | int64 creationTS = 5; 88 | 89 | // encryption key for serf gossip protocol 90 | string serfEncryptionKey = 6; 91 | 92 | // use serf LAN configuration (true) or WAN configuration (false) 93 | bool serfModeLAN = 7; 94 | } 95 | 96 | // mesh-internal message formats via serf user events 97 | 98 | // Peer contains connection data for an individual 99 | // Wireguard Peer 100 | message Peer { 101 | enum AnnouncementType { 102 | JOIN = 0; 103 | LEAVE = 1; 104 | } 105 | AnnouncementType type = 1; 106 | string pubkey = 2; // public key 107 | string endpointIP = 3; // endpoint 108 | int32 endpointPort = 4; // endpoint 109 | string meshIP = 5; // internal mesh ip 110 | } 111 | 112 | message RTTRequest { 113 | string requestedBy = 1; // node name 114 | } 115 | 116 | message RTTResponseInfo { 117 | string node = 1; // node name 118 | int32 rttMsec = 2; 119 | } 120 | message RTTResponse { 121 | string node = 1; // node name 122 | repeated RTTResponseInfo rtts = 2; 123 | } -------------------------------------------------------------------------------- /meshservice/meshservice_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package meshservice 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // MeshClient is the client API for Mesh service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type MeshClient interface { 21 | // Joining node starts to shake hands and receives a token and 22 | // additional authorization requirements 23 | Begin(ctx context.Context, in *HandshakeRequest, opts ...grpc.CallOption) (*HandshakeResponse, error) 24 | // Join start the join process by sending a JoinRequest 25 | // and receiving a JoinResponse with setup details. 26 | Join(ctx context.Context, in *JoinRequest, opts ...grpc.CallOption) (*JoinResponse, error) 27 | // Peers returns a stream of all peers currently connected to the mesh 28 | Peers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Mesh_PeersClient, error) 29 | } 30 | 31 | type meshClient struct { 32 | cc grpc.ClientConnInterface 33 | } 34 | 35 | func NewMeshClient(cc grpc.ClientConnInterface) MeshClient { 36 | return &meshClient{cc} 37 | } 38 | 39 | func (c *meshClient) Begin(ctx context.Context, in *HandshakeRequest, opts ...grpc.CallOption) (*HandshakeResponse, error) { 40 | out := new(HandshakeResponse) 41 | err := c.cc.Invoke(ctx, "/meshservice.Mesh/Begin", in, out, opts...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return out, nil 46 | } 47 | 48 | func (c *meshClient) Join(ctx context.Context, in *JoinRequest, opts ...grpc.CallOption) (*JoinResponse, error) { 49 | out := new(JoinResponse) 50 | err := c.cc.Invoke(ctx, "/meshservice.Mesh/Join", in, out, opts...) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return out, nil 55 | } 56 | 57 | func (c *meshClient) Peers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Mesh_PeersClient, error) { 58 | stream, err := c.cc.NewStream(ctx, &Mesh_ServiceDesc.Streams[0], "/meshservice.Mesh/Peers", opts...) 59 | if err != nil { 60 | return nil, err 61 | } 62 | x := &meshPeersClient{stream} 63 | if err := x.ClientStream.SendMsg(in); err != nil { 64 | return nil, err 65 | } 66 | if err := x.ClientStream.CloseSend(); err != nil { 67 | return nil, err 68 | } 69 | return x, nil 70 | } 71 | 72 | type Mesh_PeersClient interface { 73 | Recv() (*Peer, error) 74 | grpc.ClientStream 75 | } 76 | 77 | type meshPeersClient struct { 78 | grpc.ClientStream 79 | } 80 | 81 | func (x *meshPeersClient) Recv() (*Peer, error) { 82 | m := new(Peer) 83 | if err := x.ClientStream.RecvMsg(m); err != nil { 84 | return nil, err 85 | } 86 | return m, nil 87 | } 88 | 89 | // MeshServer is the server API for Mesh service. 90 | // All implementations must embed UnimplementedMeshServer 91 | // for forward compatibility 92 | type MeshServer interface { 93 | // Joining node starts to shake hands and receives a token and 94 | // additional authorization requirements 95 | Begin(context.Context, *HandshakeRequest) (*HandshakeResponse, error) 96 | // Join start the join process by sending a JoinRequest 97 | // and receiving a JoinResponse with setup details. 98 | Join(context.Context, *JoinRequest) (*JoinResponse, error) 99 | // Peers returns a stream of all peers currently connected to the mesh 100 | Peers(*Empty, Mesh_PeersServer) error 101 | mustEmbedUnimplementedMeshServer() 102 | } 103 | 104 | // UnimplementedMeshServer must be embedded to have forward compatible implementations. 105 | type UnimplementedMeshServer struct { 106 | } 107 | 108 | func (UnimplementedMeshServer) Begin(context.Context, *HandshakeRequest) (*HandshakeResponse, error) { 109 | return nil, status.Errorf(codes.Unimplemented, "method Begin not implemented") 110 | } 111 | func (UnimplementedMeshServer) Join(context.Context, *JoinRequest) (*JoinResponse, error) { 112 | return nil, status.Errorf(codes.Unimplemented, "method Join not implemented") 113 | } 114 | func (UnimplementedMeshServer) Peers(*Empty, Mesh_PeersServer) error { 115 | return status.Errorf(codes.Unimplemented, "method Peers not implemented") 116 | } 117 | func (UnimplementedMeshServer) mustEmbedUnimplementedMeshServer() {} 118 | 119 | // UnsafeMeshServer may be embedded to opt out of forward compatibility for this service. 120 | // Use of this interface is not recommended, as added methods to MeshServer will 121 | // result in compilation errors. 122 | type UnsafeMeshServer interface { 123 | mustEmbedUnimplementedMeshServer() 124 | } 125 | 126 | func RegisterMeshServer(s grpc.ServiceRegistrar, srv MeshServer) { 127 | s.RegisterService(&Mesh_ServiceDesc, srv) 128 | } 129 | 130 | func _Mesh_Begin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 131 | in := new(HandshakeRequest) 132 | if err := dec(in); err != nil { 133 | return nil, err 134 | } 135 | if interceptor == nil { 136 | return srv.(MeshServer).Begin(ctx, in) 137 | } 138 | info := &grpc.UnaryServerInfo{ 139 | Server: srv, 140 | FullMethod: "/meshservice.Mesh/Begin", 141 | } 142 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 143 | return srv.(MeshServer).Begin(ctx, req.(*HandshakeRequest)) 144 | } 145 | return interceptor(ctx, in, info, handler) 146 | } 147 | 148 | func _Mesh_Join_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 149 | in := new(JoinRequest) 150 | if err := dec(in); err != nil { 151 | return nil, err 152 | } 153 | if interceptor == nil { 154 | return srv.(MeshServer).Join(ctx, in) 155 | } 156 | info := &grpc.UnaryServerInfo{ 157 | Server: srv, 158 | FullMethod: "/meshservice.Mesh/Join", 159 | } 160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 161 | return srv.(MeshServer).Join(ctx, req.(*JoinRequest)) 162 | } 163 | return interceptor(ctx, in, info, handler) 164 | } 165 | 166 | func _Mesh_Peers_Handler(srv interface{}, stream grpc.ServerStream) error { 167 | m := new(Empty) 168 | if err := stream.RecvMsg(m); err != nil { 169 | return err 170 | } 171 | return srv.(MeshServer).Peers(m, &meshPeersServer{stream}) 172 | } 173 | 174 | type Mesh_PeersServer interface { 175 | Send(*Peer) error 176 | grpc.ServerStream 177 | } 178 | 179 | type meshPeersServer struct { 180 | grpc.ServerStream 181 | } 182 | 183 | func (x *meshPeersServer) Send(m *Peer) error { 184 | return x.ServerStream.SendMsg(m) 185 | } 186 | 187 | // Mesh_ServiceDesc is the grpc.ServiceDesc for Mesh service. 188 | // It's only intended for direct use with grpc.RegisterService, 189 | // and not to be introspected or modified (even as a copy) 190 | var Mesh_ServiceDesc = grpc.ServiceDesc{ 191 | ServiceName: "meshservice.Mesh", 192 | HandlerType: (*MeshServer)(nil), 193 | Methods: []grpc.MethodDesc{ 194 | { 195 | MethodName: "Begin", 196 | Handler: _Mesh_Begin_Handler, 197 | }, 198 | { 199 | MethodName: "Join", 200 | Handler: _Mesh_Join_Handler, 201 | }, 202 | }, 203 | Streams: []grpc.StreamDesc{ 204 | { 205 | StreamName: "Peers", 206 | Handler: _Mesh_Peers_Handler, 207 | ServerStreams: true, 208 | }, 209 | }, 210 | Metadata: "meshservice.proto", 211 | } 212 | -------------------------------------------------------------------------------- /meshservice/serf.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ioutil "io/ioutil" 7 | 8 | "os" 9 | reflect "reflect" 10 | "time" 11 | 12 | memberlist "github.com/hashicorp/memberlist" 13 | serf "github.com/hashicorp/serf/serf" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // NewSerfCluster sets up a cluster with a given nodeName, 18 | // a bind address. it also registers a user event listener 19 | // which acts upon Join and Leave user messages 20 | func (ms *MeshService) NewSerfCluster(lanMode bool) { 21 | 22 | cfg := serfCustomConfig(ms.NodeName, ms.MeshIP.IP.String(), lanMode) 23 | 24 | // set up the event handler for all user events 25 | ch := make(chan serf.Event, 1) 26 | go ms.serfEventHandler(ch) 27 | cfg.EventCh = ch 28 | 29 | if log.GetLevel() == log.TraceLevel { 30 | log.Trace("enabling serf log output") 31 | cfg.LogOutput = log.StandardLogger().Out 32 | } else { 33 | cfg.LogOutput = ioutil.Discard 34 | } 35 | cfg.MemberlistConfig.LogOutput = cfg.LogOutput 36 | 37 | if len(ms.serfEncryptionKey) == 32 { 38 | cfg.MemberlistConfig.SecretKey = ms.serfEncryptionKey 39 | } 40 | 41 | ms.cfg = cfg 42 | 43 | } 44 | 45 | func (ms *MeshService) isNodeNameInUse(nodeName string) bool { 46 | if nodeName == "" { 47 | return false 48 | } 49 | return false 50 | } 51 | 52 | // StartSerfCluster is used by bootstrap to set up the initial serf cluster node 53 | // A set of node tags is derived from all parameters so that other nodes have 54 | // all data to connect. 55 | func (ms *MeshService) StartSerfCluster(isBootstrap bool, pubkey string, endpointIP string, endpointPort int, meshIP string) error { 56 | 57 | s, err := serf.Create(ms.cfg) 58 | if err != nil { 59 | return errors.New("Unable to set up serf cluster") 60 | } 61 | nodeType := "n" 62 | if isBootstrap { 63 | nodeType = "b" 64 | } 65 | tags := map[string]string{ 66 | nodeTagNodeType: nodeType, 67 | nodeTagPubKey: pubkey, 68 | nodeTagAddr: fmt.Sprintf("%s", endpointIP), 69 | nodeTagPort: fmt.Sprintf("%d", endpointPort), 70 | nodeTagMeshIP: meshIP, 71 | } 72 | log.WithField("tags", tags).Trace("setting tags for this node") 73 | s.SetTags(tags) 74 | 75 | ms.s = s 76 | 77 | log.Debug("started serf cluster") 78 | 79 | return nil 80 | } 81 | 82 | // JoinSerfCluster calls serf.Join, given a number of cluster nodes received from the bootstrap node 83 | func (ms *MeshService) JoinSerfCluster(clusterNodes []string) { 84 | log.WithField("l", clusterNodes).Trace("cluster node list") 85 | 86 | log.Debugf("Joining serf cluster via %d nodes", len(clusterNodes)) 87 | ms.Serf().Join(clusterNodes, true) 88 | } 89 | 90 | // LeaveSerfCluster leaves the cluster 91 | func (ms *MeshService) LeaveSerfCluster() { 92 | ms.Serf().Leave() 93 | 94 | time.Sleep(3 * time.Second) 95 | log.Info("Left the serf cluster") 96 | 97 | ms.Serf().Shutdown() 98 | log.Debug("Shut down the serf instance") 99 | } 100 | 101 | // StatsUpdate produces a mesh statistic update on log 102 | func (ms *MeshService) StatsUpdate() { 103 | log.WithField("stats", ms.Serf().Stats()).Debug("serf cluster statistics") 104 | } 105 | 106 | type statsContent struct { 107 | numNodes int 108 | } 109 | 110 | func (ms *MeshService) getStats() *statsContent { 111 | return &statsContent{ 112 | numNodes: ms.Serf().NumNodes(), 113 | } 114 | } 115 | 116 | // StartStatsUpdater starts the statistics update ticker 117 | func (ms *MeshService) StartStatsUpdater() { 118 | 119 | // TODO make configurable 120 | ticker1 := time.NewTicker(1000 * time.Millisecond) 121 | ticker2 := time.NewTicker(60 * time.Second) 122 | done := make(chan bool) 123 | 124 | var last *statsContent 125 | 126 | // The first update dumps the node count only when it changes 127 | go func() { 128 | for { 129 | select { 130 | case <-done: 131 | return 132 | case _ = <-ticker1.C: 133 | if ms.memberExportFile != "" { 134 | ms.updateMemberExport() 135 | } 136 | 137 | if last == nil { 138 | last = ms.getStats() 139 | log.Infof("Mesh has %d nodes", ms.Serf().NumNodes()) 140 | } else { 141 | 142 | s := ms.getStats() 143 | if reflect.DeepEqual(*last, *s) == false { 144 | last = s 145 | log.Infof("Mesh has %d nodes", ms.Serf().NumNodes()) 146 | } 147 | } 148 | } 149 | } 150 | }() 151 | 152 | // the seconds update dumps serf stats on trace 153 | go func() { 154 | for { 155 | select { 156 | case <-done: 157 | return 158 | case _ = <-ticker2.C: 159 | ms.StatsUpdate() 160 | } 161 | } 162 | }() 163 | } 164 | 165 | func serfCustomConfig(nodeName string, bindAddr string, lanMode bool) *serf.Config { 166 | 167 | var ml *memberlist.Config 168 | 169 | if lanMode { 170 | ml = memberlist.DefaultLANConfig() 171 | } else { 172 | ml = memberlist.DefaultWANConfig() 173 | } 174 | ml.BindPort = 5353 175 | ml.BindAddr = bindAddr 176 | 177 | return &serf.Config{ 178 | NodeName: nodeName, 179 | BroadcastTimeout: 5 * time.Second, 180 | LeavePropagateDelay: 1 * time.Second, 181 | EventBuffer: 512, 182 | QueryBuffer: 512, 183 | LogOutput: os.Stderr, 184 | ProtocolVersion: 4, 185 | ReapInterval: 15 * time.Second, 186 | RecentIntentTimeout: 5 * time.Minute, 187 | ReconnectInterval: 30 * time.Second, 188 | ReconnectTimeout: 24 * time.Hour, 189 | QueueCheckInterval: 30 * time.Second, 190 | QueueDepthWarning: 128, 191 | MaxQueueDepth: 4096, 192 | TombstoneTimeout: 24 * time.Hour, 193 | FlapTimeout: 60 * time.Second, 194 | MemberlistConfig: ml, 195 | QueryTimeoutMult: 16, 196 | QueryResponseSizeLimit: 1024, 197 | QuerySizeLimit: 1024, 198 | EnableNameConflictResolution: true, 199 | DisableCoordinates: false, 200 | ValidateNodeNames: false, 201 | UserEventSizeLimit: 512, 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /meshservice/serf_events.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "math/rand" 5 | "net" 6 | "time" 7 | 8 | wgwrapper "github.com/aschmidt75/go-wg-wrapper/pkg/wgwrapper" 9 | serf "github.com/hashicorp/serf/serf" 10 | log "github.com/sirupsen/logrus" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | // parses the user event as a Peer announcement and adds the peer 15 | // to the wireguard interface 16 | func (ms *MeshService) serfHandleJoinRequestEvent(userEv serf.UserEvent) { 17 | peerAnnouncement := &Peer{} 18 | err := proto.Unmarshal(userEv.Payload, peerAnnouncement) 19 | if err != nil { 20 | log.WithError(err).Error("unable to unmarshal a user event") 21 | } 22 | log.WithField("pa", peerAnnouncement).Trace("user event: peerAnnouncement") 23 | 24 | if peerAnnouncement.Type == Peer_JOIN { 25 | 26 | wg := wgwrapper.New() 27 | ok, err := wg.AddPeer(ms.WireguardInterface, wgwrapper.WireguardPeer{ 28 | RemoteEndpointIP: peerAnnouncement.EndpointIP, 29 | ListenPort: int(peerAnnouncement.EndpointPort), 30 | Pubkey: peerAnnouncement.Pubkey, 31 | AllowedIPs: []net.IPNet{ 32 | { 33 | IP: net.ParseIP(peerAnnouncement.MeshIP), 34 | Mask: net.CIDRMask(32, 32), 35 | }, 36 | }, 37 | }) 38 | 39 | if err != nil { 40 | log.WithError(err).Error("unable to add peer after user event") 41 | } else { 42 | if ok { 43 | log.WithFields(log.Fields{ 44 | "pk": peerAnnouncement.Pubkey, 45 | "ip": peerAnnouncement.MeshIP, 46 | }).Info("added peer") 47 | } else { 48 | // if we're a bootstrap node then this peer has already been added 49 | // by the grpc join request function. 50 | } 51 | } 52 | } 53 | } 54 | 55 | func (ms *MeshService) serfHandleRTTRequestEvent(userEv serf.UserEvent) { 56 | go func(ms *MeshService) { 57 | // 58 | sl := rand.Intn(ms.Serf().NumNodes() * 1000) 59 | log.WithField("msec", sl).Trace("Delaying rtt response") 60 | time.Sleep(time.Duration(sl) * time.Millisecond) 61 | 62 | // compose my own rtt list 63 | myCoord, err := ms.Serf().GetCoordinate() 64 | if err != nil { 65 | log.WithError(err).Warn("Unable to get my own coordinate, check config") 66 | return 67 | } 68 | rtts := make([]*RTTResponseInfo, ms.Serf().NumNodes()) 69 | for idx, member := range ms.Serf().Members() { 70 | memberCoord, ok := ms.Serf().GetCachedCoordinate(member.Name) 71 | if ok && memberCoord != nil { 72 | d := memberCoord.DistanceTo(myCoord) 73 | rtts[idx] = &RTTResponseInfo{ 74 | Node: member.Name, 75 | RttMsec: int32(d / time.Millisecond), 76 | } 77 | } 78 | } 79 | 80 | // post as user event 81 | rttResponseBuf, err := proto.Marshal(&RTTResponse{ 82 | Node: ms.NodeName, 83 | Rtts: rtts, 84 | }) 85 | if err != nil { 86 | log.WithError(err).Error("Unable to marshal rtt response message") 87 | return 88 | } 89 | ms.Serf().UserEvent(serfEventMarkerRTTRes, []byte(rttResponseBuf), true) 90 | 91 | }(ms) 92 | } 93 | 94 | func (ms *MeshService) serfHandleRTTResponseEvent(userEv serf.UserEvent) { 95 | 96 | rttResponse := &RTTResponse{} 97 | err := proto.Unmarshal(userEv.Payload, rttResponse) 98 | if err != nil { 99 | log.WithError(err).Error("unable to unmarshal rtt response user event") 100 | } 101 | 102 | log.WithField("rttinfo", rttResponse).Trace("user event: rttResponse") 103 | 104 | // forward to current rtt response chan 105 | if ms.rttResponseChan != nil { 106 | *ms.rttResponseChan <- *rttResponse 107 | } 108 | } 109 | 110 | func (ms *MeshService) serfHandleMemberEvent(ev serf.MemberEvent) { 111 | for _, member := range ev.Members { 112 | // remove this peer from wireguard interface 113 | wg := wgwrapper.New() 114 | 115 | err := wg.RemovePeerByPubkey(ms.WireguardInterface, member.Tags[nodeTagPubKey]) 116 | if err != nil { 117 | log.WithError(err).Error("unable to remove failed/left wireguard peer") 118 | } 119 | 120 | err = ms.Serf().RemoveFailedNodePrune(member.Name) 121 | if err != nil { 122 | log.WithError(err).Error("unable to remove failed/left serf node") 123 | } else { 124 | log.WithFields(log.Fields{ 125 | "node": member.Name, 126 | "ip": member.Addr.String(), 127 | }).Info("node left mesh") 128 | } 129 | 130 | } 131 | 132 | } 133 | 134 | func (ms *MeshService) serfEventHandler(ch <-chan serf.Event) { 135 | for { 136 | select { 137 | case ev := <-ch: 138 | 139 | go func(ev serf.Event) { 140 | for key, ch := range ms.serfEventNotifierMap { 141 | log.WithFields(log.Fields{ 142 | "key": key, 143 | "ev": ev}).Trace("Forwarding event") 144 | *ch <- ev 145 | } 146 | }(ev) 147 | 148 | if ev.EventType() == serf.EventUser { 149 | userEv := ev.(serf.UserEvent) 150 | 151 | if userEv.Name == serfEventMarkerJoin { 152 | log.WithField("ev", userEv).Debug("received join request event") 153 | go ms.serfHandleJoinRequestEvent(userEv) 154 | } 155 | if userEv.Name == serfEventMarkerRTTReq { 156 | log.WithField("ev", userEv).Debug("received rtt request event") 157 | go ms.serfHandleRTTRequestEvent(userEv) 158 | } 159 | if userEv.Name == serfEventMarkerRTTRes { 160 | log.WithField("ev", userEv).Debug("received rtt response event") 161 | go ms.serfHandleRTTResponseEvent(userEv) 162 | } 163 | 164 | } 165 | 166 | if ev.EventType() == serf.EventMemberJoin { 167 | evJoin := ev.(serf.MemberEvent) 168 | 169 | log.WithField("members", evJoin.Members).Debug("received join event") 170 | ms.lastUpdatedTS = time.Now() 171 | } 172 | if ev.EventType() == serf.EventMemberUpdate { 173 | evUpdate := ev.(serf.MemberEvent) 174 | 175 | log.WithField("members", evUpdate.Members).Debug("received member update") 176 | ms.lastUpdatedTS = time.Now() 177 | } 178 | if ev.EventType() == serf.EventMemberLeave || ev.EventType() == serf.EventMemberFailed || ev.EventType() == serf.EventMemberReap { 179 | evMember := ev.(serf.MemberEvent) 180 | 181 | log.WithField("members", evMember.Members).Debug("received leave/failed event") 182 | ms.lastUpdatedTS = time.Now() 183 | 184 | go ms.serfHandleMemberEvent(evMember) 185 | 186 | } 187 | 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /meshservice/stun.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "net" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "gortc.io/stun" 8 | ) 9 | 10 | // STUNService ... 11 | type STUNService struct { 12 | ip4, ip6 bool 13 | 14 | stunServerURI string 15 | } 16 | 17 | // NewSTUNService creates a new STUNService struct with 18 | // default settings working for IPv4 19 | func NewSTUNService() STUNService { 20 | return STUNService{ 21 | ip4: true, 22 | ip6: false, 23 | stunServerURI: "stun.l.google.com:19302", 24 | } 25 | } 26 | 27 | // GetExternalIP retrieves my own external ip by querying it 28 | // from the STUN server 29 | func (st *STUNService) GetExternalIP() ([]net.IP, error) { 30 | 31 | ips := make([]net.IP, 0) 32 | 33 | log.Info("Fetching external IP from STUN server") 34 | if st.ip4 { 35 | c, err := stun.Dial("udp4", st.stunServerURI) 36 | if err != nil { 37 | return ips, err 38 | } 39 | 40 | message, err := stun.Build(stun.TransactionID, stun.BindingRequest) 41 | if err != nil { 42 | return ips, err 43 | } 44 | 45 | if err := c.Do(message, func(res stun.Event) { 46 | if res.Error != nil { 47 | return 48 | } 49 | 50 | var xorAddr stun.XORMappedAddress 51 | if err := xorAddr.GetFrom(res.Message); err != nil { 52 | return 53 | } 54 | ips = append(ips, xorAddr.IP) 55 | }); err != nil { 56 | return ips, err 57 | } 58 | 59 | } 60 | 61 | return ips, nil 62 | } 63 | -------------------------------------------------------------------------------- /meshservice/tls.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | ioutil "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // TLSConfig ... 15 | type TLSConfig struct { 16 | Cert tls.Certificate 17 | CertPool *x509.CertPool 18 | } 19 | 20 | // NewTLSConfigFromFiles creates a new TLS config from given files/paths 21 | func NewTLSConfigFromFiles(caCertFile, caPath, certFile, keyFile string) (*TLSConfig, error) { 22 | 23 | tlsConfig := &TLSConfig{} 24 | 25 | var err error 26 | 27 | tlsConfig.CertPool = x509.NewCertPool() 28 | if caCertFile != "" { 29 | caCertPEM, err := ioutil.ReadFile(caCertFile) 30 | if err != nil { 31 | return nil, err 32 | } 33 | if !tlsConfig.CertPool.AppendCertsFromPEM(caCertPEM) { 34 | return nil, fmt.Errorf("credentials: failed to append certificate") 35 | 36 | } 37 | } 38 | if caPath != "" { 39 | err := filepath.Walk(caPath, func(path string, info os.FileInfo, err error) error { 40 | certPEM, err := ioutil.ReadFile(path) 41 | if err == nil { 42 | if ok := tlsConfig.CertPool.AppendCertsFromPEM(certPEM); ok { 43 | log.WithField("cafile", path).Debug("Added certificate from -ca-path") 44 | } 45 | } 46 | 47 | return nil 48 | }) 49 | if err != nil { 50 | return nil, err 51 | } 52 | } 53 | 54 | tlsConfig.Cert, err = tls.LoadX509KeyPair(certFile, keyFile) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return tlsConfig, nil 60 | } 61 | -------------------------------------------------------------------------------- /meshservice/ui.go: -------------------------------------------------------------------------------- 1 | package meshservice 2 | 3 | import ( 4 | context "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | sync "sync" 10 | "time" 11 | 12 | rice "github.com/GeertJohan/go.rice" 13 | log "github.com/sirupsen/logrus" 14 | "golang.org/x/net/websocket" 15 | grpc "google.golang.org/grpc" 16 | ) 17 | 18 | // UIServer ... 19 | type UIServer struct { 20 | agentGrpcSocket string 21 | httpBindAddr string 22 | httpBindPort int 23 | conf rice.Config 24 | box *rice.Box 25 | 26 | meshInfo *MeshInfo 27 | members []*MemberInfo 28 | m sync.Mutex 29 | lastUpdated time.Time 30 | } 31 | 32 | // NewUIServer ... 33 | func NewUIServer(agentGrpcSocket string, httpBindAddr string, httpBindPort int) *UIServer { 34 | conf := rice.Config{ 35 | LocateOrder: []rice.LocateMethod{rice.LocateAppended, rice.LocateFS}, 36 | } 37 | box, err := conf.FindBox("../web/dist") 38 | if err != nil { 39 | log.WithError(err).Fatalf("unable to serve web interface\n", err) 40 | } 41 | 42 | log.WithField("box", box).Trace("Loaded assets") 43 | 44 | return &UIServer{ 45 | agentGrpcSocket: agentGrpcSocket, 46 | httpBindAddr: httpBindAddr, 47 | httpBindPort: httpBindPort, 48 | box: box, 49 | conf: conf, 50 | meshInfo: &MeshInfo{}, 51 | members: make([]*MemberInfo, 0), 52 | lastUpdated: time.Now(), 53 | } 54 | } 55 | 56 | // Serve starts the HTTP server and the agent query 57 | func (u *UIServer) Serve() { 58 | 59 | http.Handle("/", http.FileServer(u.box.HTTPBox())) 60 | http.HandleFunc("/api/nodes", u.apiNodesHandler) 61 | http.HandleFunc("/api/mesh", u.apiMeshHandler) 62 | http.Handle("/api/updates", websocket.Handler(u.updater)) 63 | 64 | listenSpec := fmt.Sprintf("%s:%d", u.httpBindAddr, u.httpBindPort) 65 | 66 | fmt.Printf("Serving files on %s, press ctrl-C to exit\n", listenSpec) 67 | go func() { 68 | err := http.ListenAndServe(listenSpec, nil) 69 | if err != nil { 70 | log.WithError(err).Fatalf("error serving files") 71 | } 72 | }() 73 | 74 | go func() { 75 | err := u.agentUpdater() 76 | if err != nil { 77 | log.WithError(err).Fatalf("Unable to query meshervice agent for updates") 78 | } 79 | }() 80 | 81 | select {} 82 | 83 | } 84 | 85 | // simple websocket updater 86 | func (u *UIServer) updater(conn *websocket.Conn) { 87 | lastUpdated := time.Now() 88 | for { 89 | 90 | l := func(u *UIServer) time.Time { 91 | u.m.Lock() 92 | defer u.m.Unlock() 93 | 94 | return u.lastUpdated 95 | }(u) 96 | 97 | if l.After(lastUpdated) { 98 | 99 | type wsUpdateStruct struct { 100 | // Aspect decribes what has changes 101 | Aspect string `json:"a"` 102 | // Ts is the timestamp 103 | Ts string `json:"ts"` 104 | } 105 | upd := wsUpdateStruct{ 106 | Aspect: "nodes", 107 | Ts: l.Format(time.UnixDate), 108 | } 109 | updJSON, err := json.Marshal(upd) 110 | if err == nil { 111 | _, err := conn.Write(updJSON) 112 | if err != nil { 113 | log.WithError(err).Error("Error sending ws update") 114 | return 115 | } 116 | } 117 | 118 | lastUpdated = l 119 | } 120 | time.Sleep(100 * time.Millisecond) 121 | } 122 | } 123 | 124 | // returns all nodes 125 | func (u *UIServer) apiNodesHandler(w http.ResponseWriter, req *http.Request) { 126 | u.m.Lock() 127 | defer u.m.Unlock() 128 | 129 | type uiNodeInfo struct { 130 | Name string `json:"name"` 131 | MeshIP string `json:"meshIP"` 132 | Tags map[string]string `json:"tags"` 133 | RttMsec int32 `json:"rttMsec"` 134 | IsSelf bool `json:"isSelf"` 135 | } 136 | 137 | type uiNodes struct { 138 | Nodes []uiNodeInfo `json:"nodes"` 139 | } 140 | 141 | nodes := uiNodes{ 142 | Nodes: make([]uiNodeInfo, len(u.members)), 143 | } 144 | for idx, member := range u.members { 145 | log.WithField("m", member).Trace(".") 146 | nodes.Nodes[idx] = uiNodeInfo{ 147 | Name: member.NodeName, 148 | MeshIP: member.Addr, 149 | Tags: make(map[string]string), 150 | RttMsec: member.RttMsec, 151 | IsSelf: false, 152 | } 153 | for _, tag := range member.Tags { 154 | m := nodes.Nodes[idx].Tags 155 | m[tag.Key] = tag.Value 156 | } 157 | } 158 | 159 | w.Header().Add("Content-Type", "application/json") 160 | 161 | bytes, err := json.Marshal(nodes) 162 | if err != nil { 163 | log.WithError(err).Error("Unable to marshal nodelist as json") 164 | fmt.Fprintf(w, "[]") 165 | } 166 | 167 | w.Header().Add("Content-Length", fmt.Sprintf("%d", len(bytes))) 168 | w.Write(bytes) 169 | } 170 | 171 | // returns mesh info 172 | func (u *UIServer) apiMeshHandler(w http.ResponseWriter, req *http.Request) { 173 | u.m.Lock() 174 | defer u.m.Unlock() 175 | 176 | if u.meshInfo == nil { 177 | w.Header().Add("Content-Type", "application/json") 178 | w.Header().Add("Content-Length", fmt.Sprintf("%d", 0)) 179 | return 180 | } 181 | 182 | type uiMeshInfo struct { 183 | Name string `json:"name"` 184 | NodeName string `json:"thisNodeName"` 185 | NodeCount int `json:"nodeCount"` 186 | } 187 | 188 | meshInfo := uiMeshInfo{ 189 | Name: u.meshInfo.Name, 190 | NodeName: u.meshInfo.NodeName, 191 | NodeCount: int(u.meshInfo.NodeCount), 192 | } 193 | 194 | w.Header().Add("Content-Type", "application/json") 195 | 196 | bytes, err := json.Marshal(meshInfo) 197 | if err != nil { 198 | log.WithError(err).Error("Unable to marshal mesh info as json") 199 | fmt.Fprintf(w, "[]") 200 | } 201 | 202 | w.Header().Add("Content-Length", fmt.Sprintf("%d", len(bytes))) 203 | w.Write(bytes) 204 | } 205 | 206 | // watches for changes in mesh via agent grpc socket 207 | func (u *UIServer) agentUpdater() error { 208 | endpoint := fmt.Sprintf("unix://%s", u.agentGrpcSocket) 209 | 210 | conn, err := grpc.Dial(endpoint, grpc.WithInsecure(), grpc.WithBlock()) 211 | if err != nil { 212 | log.Error(err) 213 | return fmt.Errorf("cannot connect to %s", endpoint) 214 | } 215 | defer conn.Close() 216 | 217 | agent := NewAgentClient(conn) 218 | log.WithField("agent", agent).Trace("got grpc service client") 219 | 220 | ctx0, cancel := context.WithTimeout(context.Background(), 2*time.Second) 221 | defer cancel() 222 | 223 | u.singleCycle(ctx0, agent) 224 | 225 | for { 226 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 227 | defer cancel() 228 | 229 | r, err := agent.WaitForChangeInMesh(ctx, &WaitInfo{ 230 | TimeoutSecs: 10, 231 | }) 232 | if err != nil { 233 | log.WithError(err).Error("error while waiting for mesh changes") 234 | break 235 | } 236 | for { 237 | wr, err := r.Recv() 238 | if err == io.EOF { 239 | break 240 | } 241 | if err != nil { 242 | log.WithError(err).Debug("error while waiting for mesh changes") 243 | break 244 | } 245 | 246 | if wr.WasTimeout { 247 | continue 248 | } 249 | if wr.ChangesOccured { 250 | time.Sleep(1 * time.Second) 251 | u.singleCycle(ctx, agent) 252 | } 253 | 254 | } 255 | } 256 | 257 | return nil 258 | } 259 | 260 | func (u *UIServer) singleCycle(ctx context.Context, agent AgentClient) error { 261 | u.m.Lock() 262 | defer u.m.Unlock() 263 | 264 | meshInfo, err := agent.Info(ctx, &AgentEmpty{}) 265 | if err != nil { 266 | log.WithError(err).Error("Unable to query infos from agent") 267 | return err 268 | } 269 | u.meshInfo = meshInfo 270 | 271 | r, err := agent.Nodes(ctx, &AgentEmpty{}) 272 | if err != nil { 273 | log.WithError(err).Error("Unable to query nodes from agent") 274 | } 275 | 276 | u.members = make([]*MemberInfo, meshInfo.NodeCount) 277 | idx := 0 278 | for { 279 | memberInfo, err := r.Recv() 280 | if err != nil { 281 | break 282 | } 283 | u.members[idx] = memberInfo 284 | idx = idx + 1 285 | } 286 | log.WithField("u.members", u.members).Trace("query") 287 | 288 | u.lastUpdated = time.Now() 289 | 290 | return nil 291 | } 292 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.key 3 | -------------------------------------------------------------------------------- /scripts/cert-sample-2/README.md: -------------------------------------------------------------------------------- 1 | 2 | For testing purposes, generate a 2nd ca and cert 3 | 4 | ```bash 5 | $ cfssl gencert -initca ca-csr2.json | cfssljson -bare ca2 6 | $ cfssl gencert -ca ca2.pem -ca-key ca2-key.pem join2-csr.json | cfssljson -bare join2 7 | ``` 8 | -------------------------------------------------------------------------------- /scripts/cert-sample-2/ca2.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICgzCCAWsCAQAwPjELMAkGA1UEBhMCQVUxFTATBgNVBAoTDE5vV2hlcmUgSW5j 3 | LjEYMBYGA1UEAxMPd3d3Lm5vd2hlcnJlLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOC 4 | AQ8AMIIBCgKCAQEAxFQNbzb4q/O5F8pP86FYjqSqiwQODhWICTaB0UgiZZQdqQ55 5 | 1X9Wa0csAFeXk6Ed9g4nrgtUh4zYpLjs7kx19Ql70i0WY5s/uYvsEPV+7GLkrVTs 6 | SH+qjVCOs026SZt9eW7IqOwm6FETy4D+HeVneqG6zFw2FetJHJVKAhvAzEdiig8a 7 | ZniSj9xlqMmqYaJeEGQQ4ESGYy7xun6PVE4DfjHPOZiZkBNKf+baxy3y2j0aGhZM 8 | pWGcgCcRIGfXrpHam0gNhlffFWiYb+ygde52bLz09bYekLttRjoUXYRAdFsOp70r 9 | mtAAfzIFqfgYizwRjSQwXJqQ0HHq3AAvTWN5AQIDAQABoAAwDQYJKoZIhvcNAQEL 10 | BQADggEBAKARXLO17FR5PM61w9mP+Id2BDAuyYhQ0F+XXjWHlmMMrGx16v7WhfDv 11 | acyVmnf0qhV5MW6DeVywbflhAM1aOUwE5nZLiMLWObUJzxsgXzH33fXol8z9clp1 12 | wLaM1iX7/Z0ZG+VuU5vbF6v0qlowCziBJ3kmCKAk2Z2lKlZhMsPMf44xblwgA5dm 13 | lhcHiExhDR4FMqecs38C74VdxdJ8xCPZpGGAhwzdvgfPTqqhiX7auCv7xthNkB/s 14 | 39eLCPCqBVtYz+78fR6vmrgkeRFQ5rfhXu1Ig8g6C8Ow9rQz+vnAI/EE+Y4UnV78 15 | sUOfDVv0ZSQlrC/4aAzIOmAv1Dp8VKI= 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /scripts/cert-sample-2/join2-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosts": [ 3 | "localhost" 4 | ], 5 | "CN": "wgmesh-join", 6 | "key": { 7 | "algo": "rsa", 8 | "size": 2048 9 | }, 10 | "names": [ 11 | { 12 | "C": "AU", 13 | "O": "NoWhere Inc." 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scripts/cert-sample-2/join2.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICpjCCAY4CAQAwOjELMAkGA1UEBhMCQVUxFTATBgNVBAoTDE5vV2hlcmUgSW5j 3 | LjEUMBIGA1UEAxMLd2dtZXNoLWpvaW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 4 | ggEKAoIBAQDW+SBDvt3AitHPotEwS3UjLG2RTBYfjXvJ78MQELBJtSWXApDoIUnx 5 | 9WT8NrjRC+EXu61z1MSDU/r+qpehmSx3gElU6yl5uaOLbWp/rFVeqsT4pGKvgc0q 6 | QmCOHj31jWSBRPaCYojdeYPKtvI26f41lSn3AlkiWG5dPKhNE1ALQoh/Duh9j5nb 7 | gbUu5kl6ZGxXEiVh5qyGwZK2r0Z0K98j/i9u6HX7+wk36VrMXlinemXOjSXeEoYI 8 | vrCs/QQ64JTqCT3pj0BX/dmU416gIWKZexKnv2Q+ZUDaxqz2G6/FFk+6Xke8rRfD 9 | 89totDk5uQqHNEcqDhFiuN4IevMy9Ov7AgMBAAGgJzAlBgkqhkiG9w0BCQ4xGDAW 10 | MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAlxEyc3Ur 11 | 1tctbF8Z7O9xnhyEoEe/3EhVgO6sNUdQXVqUM+nOFCIdYPiW3Ed8OX6Dupu6XcVE 12 | 8w8vHSbi1F/LmXuhD9asqSAeB7Xu9FL8fZTU5RAtFVKSWSz3MfDxXWwrRk7CXEU+ 13 | iJQ96HHkU0xhzhAvkHXWSUGywwoQZ2MoQ+bJc2sNJg8XOX2dEYvSfq6PbB9w0Rka 14 | 4xom9z1aiCYf710vFTwjsiJuhEPkDYFY+9klId6Iy0o0wv9UwyJ/jqDIaLR1ecyJ 15 | L52kTIwZTj8RGr9fhfOgeyGT/KBPg/KPEMxUO4W/dPi90wWmDW5uDJ+aTXUCSHeP 16 | YrkdlC4MGdj/6g== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /scripts/cert-sample/README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ```bash 4 | $ cfssl gencert -initca ca-csr.json | cfssljson -bare ca 5 | $ cfssl gencert -ca ca.pem -ca-key ca-key.pem bootstrap-csr.json | cfssljson -bare bootstrap 6 | $ cfssl gencert -ca ca.pem -ca-key ca-key.pem join-csr.json | cfssljson -bare join 7 | ``` 8 | -------------------------------------------------------------------------------- /scripts/cert-sample/bootstrap-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosts": [ 3 | "192.168.64.12" 4 | ], 5 | "CN": "wgmesh-bootstrap", 6 | "key": { 7 | "algo": "rsa", 8 | "size": 2048 9 | }, 10 | "names": [ 11 | { 12 | "C": "NZ", 13 | "O": "SomeWhere Inc." 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scripts/cert-sample/bootstrap.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICqDCCAZACAQAwQTELMAkGA1UEBhMCTloxFzAVBgNVBAoTDlNvbWVXaGVyZSBJ 3 | bmMuMRkwFwYDVQQDExB3Z21lc2gtYm9vdHN0cmFwMIIBIjANBgkqhkiG9w0BAQEF 4 | AAOCAQ8AMIIBCgKCAQEA7H6Hnn4uurk/Js+Y5ZnGOZkmvjGVyhtONb0oj/Px6ECe 5 | E3AmkFgJmdv0la6AY2KZMmUYMbEk6yqVgc5rbaQ33VwWIkG4hfhygM4LKUTFLFXQ 6 | s+F15r4Lf9g4epVjWfL29nxgV2vNGoo1qY5vY/6M5q4drDSsMPKQ7mg555xP4d26 7 | g1QPJIGy20QF11ScnpA3tWN/iguR/SuIYtRX+JyvVh47H3gcn0xcsFuMOAQZpvEA 8 | UAd5lxknexWA41cvOsJbNIPZ3jwjuC+vAKTr/wCvIKC6rJSwL0atcNUThGdvV/tY 9 | JZD7bIor4GLqcF+ggF6nDUwVafoRRXvnbnpAcZ8SVwIDAQABoCIwIAYJKoZIhvcN 10 | AQkOMRMwETAPBgNVHREECDAGhwTAqEAMMA0GCSqGSIb3DQEBCwUAA4IBAQAxXP4U 11 | ybzy4V56rYj/ZWMDe+M0GQlNLVliJ/jwkoHZgBLwHHMxgmRMwSKX8OJH2OIdVLdk 12 | 9oSIZ8UAsJaby2dCHqGjRLWphCiCIQMwB0/3jPsVsSNwiGvtDrzbJL1cRjIhQrQk 13 | MXBePXwQAbxo261+NuUl9JXW0OM33GgXq0Uf0criw1PNiWON7WC/pVNZgpFaLJpU 14 | vIiiUo3uNRcMSBmQR80Qwq5bZ7/4I1I4jQ9KuKvaPq5mtGu7m733z70NiQTvw2ho 15 | G0i3ZtCjy7CWQgDKYFEO6o01iM0WxEfEzS4bCylXnH3TJogb4W5cAt+V8Y5QgOTa 16 | jNpjK6llTajJrywv 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /scripts/cert-sample/ca-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "signing": { 3 | "default": { 4 | "expiry": "8760h" 5 | }, 6 | "profiles": { 7 | "default": { 8 | "usages": ["signing", "key encipherment", "server auth", "client auth"], 9 | "expiry": "8760h" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /scripts/cert-sample/ca-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "www.somehwere.io", 3 | "key": { 4 | "algo": "rsa", 5 | "size": 2048 6 | }, 7 | "names": [ 8 | { 9 | "C": "NZ", 10 | "O": "SomeWhere Inc." 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /scripts/cert-sample/ca-csr2.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "www.nowherre.io", 3 | "key": { 4 | "algo": "rsa", 5 | "size": 2048 6 | }, 7 | "names": [ 8 | { 9 | "C": "AU", 10 | "O": "NoWhere Inc." 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /scripts/cert-sample/ca.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICgzCCAWsCAQAwPjELMAkGA1UEBhMCTloxFzAVBgNVBAoTDlNvbWVXaGVyZSBJ 3 | bmMuMRYwFAYDVQQDEw13d3cuc2FtcGxlLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOC 4 | AQ8AMIIBCgKCAQEAvM6eouABU4rsRCKf2mjCWNrl9+tW79PkyH/TWIdJr7jq2Xux 5 | TdqfhJFu5OVU+SoSyuD9VOCFfPNz2/kTkFuJP+9kGwG21miU6jjcTU28Xxdxp5gy 6 | /JSoEQHmY8awuS2pbyM+hIzoSgqVFH3BnvWGRq4rGj+n2jK9NoB5pT5MX/flzdHJ 7 | jyYoBSBoeGYKAVjkiPT8Kez3HVOhsI41g00TA+LBwPEU3tPN8Wl/JqD67L7N4NEQ 8 | a03jNO32ZoMyqsnEIN7Cd2ykTjWkleLWBS2+vNnzRNowdCb3KqMpCjeeWE1gHa3S 9 | dMogfAp35I6Hd4LZL2LNUqALrpjo2IOlfrFW0QIDAQABoAAwDQYJKoZIhvcNAQEL 10 | BQADggEBAK0X8kVYbv1gUBAz4sszhVmJ4rmr3Jb0i2F7jwMeRyxMvdjQ0ow3it08 11 | kyqmZVozrlG925/8Jdtpq+1Qk0UQZWYNrpf99+jDjMdpwJoKqdU2tQj/nOkIm2B7 12 | uRgCfn4Rh5pL24Gnscw32oeHf8fcA9oNtxpMhUi/o03S6/9abhcCy1ZZiN9L3tq5 13 | aET/QND2TyEQpyH9nXysPaxYnv6OFrg91qDLmEC1yl5+XKgCKOIpfx7kFEiqiRv6 14 | BO9E238Y+ZylvSRgPTDS+IMRgIlqAmqpr3+xKMzuicopNBdWi7NWPVbjm5Pwz14z 15 | fkXiWUojakCK8UelAJ4XUf7VQiEYwdM= 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /scripts/cert-sample/join-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosts": [ 3 | "localhost" 4 | ], 5 | "CN": "wgmesh-join", 6 | "key": { 7 | "algo": "rsa", 8 | "size": 2048 9 | }, 10 | "names": [ 11 | { 12 | "C": "NZ", 13 | "O": "SomeWhere Inc." 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /scripts/cert-sample/join.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICqDCCAZACAQAwPDELMAkGA1UEBhMCTloxFzAVBgNVBAoTDlNvbWVXaGVyZSBJ 3 | bmMuMRQwEgYDVQQDEwt3Z21lc2gtam9pbjCCASIwDQYJKoZIhvcNAQEBBQADggEP 4 | ADCCAQoCggEBALDs8x5X1mT6rDpl4VTfe8SzYoHg2thnbNzOhHdvR9vkgxHK31V1 5 | 1JrYaqZUjLNGIi5p4W9HxJzf+Ta5751s13f8cvNMnorCfRoCPVqROceG1zXApotv 6 | pmzooYkUgJlanrr0ABu+uZUYEa6EFkoHePXIXi7eUSIGdHUZD7v482o6DQzLmnlR 7 | seckz9L+LhB+jxeboi2FqQRHGJjeGdB8EIJjLswrFR+Spq/uDB2GTSpTbcDRQG24 8 | hp5+3KHy8Kkb55OXx+xOr8eURTtqKqk24miHGvYK0GAOB4m4MfW5yW7iIuiCZQY5 9 | yDYIcEo7oOBAf3VMYfS5FLEBhCAg7YMP0psCAwEAAaAnMCUGCSqGSIb3DQEJDjEY 10 | MBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCnjhtl 11 | EYeeYLcMhf7okklywZ5NawtSlBOdanVVnR80RkiEX/GUNyroJHlJldUolRGOhIXu 12 | 8j2VnYbIIjft3Nc0Ar+cEueswKgn/wIgUjWK+h5rkLwRwrlA9wmt+oQsQPIXJICC 13 | JZPcibxvyAjteDFlOx1/Om1F/4BBzKQS3Aw3a1UpdyF940+iEChcBBQC9GhoGWH3 14 | GGfl2brXVsd3nYslF53LYC0PMtJzP+llTtjw2naVl04cmfB3/NRCnMC/RX160K+y 15 | BrAPs+HQHJ5eawcrKIW2I7Bx3+Lt94V4crGAvPuHNGBRm18AMzuKnPuHTK/MTEGc 16 | Sg5Sj+Xa6Cr6vqCm 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /scripts/multipass-cloudinit.yaml: -------------------------------------------------------------------------------- 1 | bootcmd: 2 | - apt-get update -y -q && apt-get install -y -q wireguard-tools 3 | - systemctl stop snapd multipathd unattended-upgrades 4 | - wget https://github.com/aschmidt75/wgmesh/releases/download/v0.1.2/wgmesh_0.1.2_Linux_x86_64.tar.gz 5 | - tar -xzf wgmesh_0.1.2_Linux_x86_64.tar.gz && chmod +x wgmesh && mv wgmesh /usr/local/bin && wgmesh version 6 | - curl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -o /usr/bin/cfssl && chmod +x /usr/bin/cfssl && /usr/bin/cfssl version 7 | - curl https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -o /usr/bin/cfssljson && chmod +x /usr/bin/cfssljson -------------------------------------------------------------------------------- /scripts/nodejs-dns-zonefile/README.md: -------------------------------------------------------------------------------- 1 | # nodejs-dns-zonefile 2 | 3 | This directory includes a small NodeJS script to transform the output of the mesh member list (using `-memberlist-file`) 4 | into a DNS zone file compatible DNS record set. It reads the memberlist file from STDIN and writes zone records to STDOUT, 5 | based upon a template file (see `sample-template.dns`). 6 | 7 | Wgmesh updates the memberlist every time a change event occurs within the mesh. It includes a timestamp which can be used as a serial within the zone file (see template). 8 | 9 | ```bash 10 | $ node --version 11 | v10.19.0 12 | $ npm install 13 | (...) 14 | 15 | $ cat /var/log/memberlist.json | node index.js >records.dns 16 | ``` 17 | 18 | As an example, [coredns](https://coredns.io) is able to serve this using a sample configuration 19 | 20 | ```bash 21 | $ cat >coredns.cfg </path/to/output/of/this/script.inc 36 | ``` 37 | 38 | Then 39 | ```bash 40 | $ coredns -conf coredns.conf 41 | $ dig -p 8053 @127.0.0.1 node1.samplemesh.local +short 42 | (...) 43 | ``` -------------------------------------------------------------------------------- /scripts/nodejs-dns-zonefile/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const util = require('util'); 4 | const render = require('template-file'); 5 | 6 | const args = process.argv.slice(2); 7 | 8 | if (args.length != 1) { 9 | console.error("please set template file as 1st argument. Will read from stdin and write to stdout."); 10 | process.exit(1); 11 | } 12 | 13 | var templateBuffer = fs.readFileSync(args[0]); 14 | 15 | var stdinBuffer = fs.readFileSync(0); 16 | if (stdinBuffer.length <= 0) { 17 | console.error("no data") 18 | process.exit(2); 19 | } 20 | const obj = JSON.parse(stdinBuffer); 21 | 22 | // restructure 23 | 24 | let data = { 25 | serial: obj.lastUpdate, 26 | a_records: [], 27 | cname_records: [], 28 | txt_records: [] 29 | } 30 | 31 | var all_names = []; 32 | var all_ips = []; 33 | for ( const key in obj.members) { 34 | nodeName = key 35 | nodeElem = obj.members[key] 36 | 37 | data.a_records.push({ 38 | name: nodeName, 39 | ip: nodeElem.addr 40 | }) 41 | all_names.push(nodeName) 42 | all_ips.push(nodeElem.addr) 43 | } 44 | 45 | if (all_ips.length > 0) { 46 | data.a_records.push({ 47 | name: "all", 48 | ip: all_ips.reduce((acc, ip) => { 49 | return ""+acc+" "+ip; 50 | }, "") 51 | }) 52 | 53 | } 54 | 55 | if (obj.services !== undefined) { 56 | for ( const key in obj.services) { 57 | nodeName = key 58 | nodeElem = obj.services[key] 59 | 60 | data.cname_records.push({ 61 | cname: nodeName, 62 | names: nodeElem.nodes.reduce((acc, n) => { 63 | return ""+acc+" "+n; 64 | }, "") 65 | }) 66 | data.txt_records.push({ 67 | name: nodeName, 68 | text: util.format("port=%s",nodeElem.port), 69 | }) 70 | } 71 | 72 | } 73 | 74 | console.log(render.render(templateBuffer.toString(), data)) -------------------------------------------------------------------------------- /scripts/nodejs-dns-zonefile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-dns-zonefile", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "github.com/aschmidt75/wgmesh" 12 | }, 13 | "author": "@aschmidt75", 14 | "license": "Apache-2.0", 15 | "dependencies": { 16 | "template-file": "^5.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/nodejs-dns-zonefile/sample-template.dns: -------------------------------------------------------------------------------- 1 | $ORIGIN samplemesh.local. 2 | $TTL 60 3 | @ 1800 IN SOA ns1 none ( 4 | {{ serial }} ; serial from wgmesh 5 | 7200 ; refresh (2 hours) 6 | 3600 ; retry (1 hour) 7 | 1209600 ; expire (2 weeks) 8 | 3600 ; minimum (1 hour) 9 | ) 10 | @ 1800 IN NS ns1 11 | 12 | ; A Records 13 | ; All nodes appear here with their wireguard mesh ip as an a record 14 | ; Additionally there are records summarizing mesh resources, e.g. all nodes 15 | {{#a_records}} 16 | {{ this.name }} IN A {{ this.ip }} 17 | {{/a_records}} 18 | 19 | ; CNAME Records 20 | ; All services appear here with a CNAME record pointing to the mesh 21 | ; nodes which currently include the service 22 | {{#cname_records}} 23 | {{ this.cname }} IN CNAME {{ this.names }} 24 | {{/cname_records}} 25 | 26 | ; TXT Record 27 | ; All services appear here with their tag information 28 | {{#txt_records}} 29 | {{ this.name }} IN TXT {{ this.text }} 30 | {{/txt_records}} -------------------------------------------------------------------------------- /scripts/wgmesh.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=wgmesh bootstrap 3 | After=network.target 4 | StartLimitIntervalSec=0 5 | 6 | [Service] 7 | Type=simple 8 | Restart=always 9 | RestartSec=1 10 | User=root 11 | #ExecStartPre=/sbin/ip link show wgm1 >/dev/null 2>&1 && /sbin/ip link del dev wgm1 12 | ExecStart=/usr/local/bin/wgmesh bootstrap -n m1 -v \ 13 | -grpc-ca-cert /root/wgmesh-tls/ca.pem \ 14 | -grpc-server-cert /root/wgmesh-tls/bootstrap.pem \ 15 | -grpc-server-key /root/wgmesh-tls/bootstrap-key.pem 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | 20 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | serve-dev.js -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschmidt75/wgmesh/29dbaa307b0b047baec91e321cd094b56e84a3e2/web/README.md -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wgmesh-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "bootstrap": "^4.6.0", 13 | "bootstrap-vue": "^2.21.2", 14 | "core-js": "^3.10.0", 15 | "rxjs": "^6.6.7", 16 | "util": "^0.12.3", 17 | "vue": "^2.6.12", 18 | "vue-router": "^3.5.1" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.5.12", 22 | "@vue/cli-plugin-eslint": "~4.5.12", 23 | "@vue/cli-service": "~4.5.12", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^7.23.0", 26 | "eslint-plugin-vue": "^7.8.0", 27 | "vue-template-compiler": "^2.6.12" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/essential", 36 | "eslint:recommended" 37 | ], 38 | "parserOptions": { 39 | "parser": "babel-eslint" 40 | }, 41 | "rules": {} 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschmidt75/wgmesh/29dbaa307b0b047baec91e321cd094b56e84a3e2/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschmidt75/wgmesh/29dbaa307b0b047baec91e321cd094b56e84a3e2/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/src/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /web/src/components/NodesTable.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 216 | 217 | 218 | 223 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | //src/main.js 2 | import Vue from 'vue' 3 | import Home from './pages/Home.vue' 4 | import NotFound from './pages/NotFound.vue' 5 | import BootstrapVue from 'bootstrap-vue' 6 | import VueRouter from 'vue-router' 7 | import BootstrapVueIcons from 'bootstrap-vue' 8 | import 'bootstrap/dist/css/bootstrap.css' 9 | import 'bootstrap-vue/dist/bootstrap-vue.css' 10 | 11 | Vue.use(BootstrapVue) 12 | Vue.use(VueRouter) 13 | Vue.use(BootstrapVueIcons) 14 | Vue.config.productionTip = false 15 | 16 | 17 | const routes = { 18 | '/': Home, 19 | '/notFound': NotFound, 20 | } 21 | 22 | new Vue({ 23 | el: '#app', 24 | data: { 25 | currentRoute: window.location.pathname 26 | }, 27 | computed: { 28 | ViewComponent () { 29 | let r = this.currentRoute; 30 | if (!r.startsWith("/")) { 31 | r = "/"+r; 32 | } 33 | const res = routes[r] || NotFound; 34 | return res 35 | } 36 | }, 37 | render (h) { return h(this.ViewComponent) } 38 | }).$mount('#app') 39 | 40 | 41 | -------------------------------------------------------------------------------- /web/src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 35 | -------------------------------------------------------------------------------- /web/src/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /wgmesh.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "time" 8 | 9 | "github.com/aschmidt75/wgmesh/cmd" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | version = "dev" 15 | commit = "none" 16 | date = "unknown" 17 | ) 18 | 19 | func main() { 20 | tf := &log.TextFormatter{} 21 | tf.FullTimestamp = true 22 | tf.DisableTimestamp = false 23 | tf.TimestampFormat = "2006/01/02 15:04:05" 24 | tf.DisableColors = false 25 | tf.DisableSorting = true 26 | log.SetFormatter(tf) 27 | 28 | rand.Seed(time.Now().UnixNano()) 29 | if err := cmd.ProcessCommands(os.Args[1:], cmd.VersionInfo{ 30 | Version: version, 31 | Commit: commit, 32 | Date: date, 33 | }); err != nil { 34 | fmt.Println(err) 35 | os.Exit(1) 36 | } 37 | } 38 | --------------------------------------------------------------------------------