├── .github └── workflows │ └── main.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── buf.gen.yaml ├── go.mod ├── go.sum ├── internal ├── network │ ├── ip.go │ └── wg_device.go └── state │ ├── state.go │ └── store.go ├── main.go └── pkg ├── commands ├── agent.go ├── config.go ├── info.go ├── init.go └── root.go ├── ikto └── ikto.go ├── proto ├── api.pb.go ├── api.proto └── api_grpc.pb.go ├── server └── server.go └── types ├── peer.go └── public_key.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Set up Go 22 | uses: actions/setup-go@v5 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v6 26 | with: 27 | distribution: goreleaser 28 | version: '~> v2' 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nats.creds 2 | ikto.json -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: ikto 2 | builds: 3 | - env: [CGO_ENABLED=0] 4 | goos: 5 | - linux 6 | goarch: 7 | - amd64 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ikto 2 | 3 | Ikto is a wireguard mesh builder based on nats. The first Ikto goal is to be the building block of our micro-vm orchestrator, [Ravel](https://github.com/valyentdev/ravel), networking features. 4 | 5 | ## Concepts 6 | 7 | Ikto connects to a [NATS Jetstream](https://docs.nats.io/nats-concepts/jetstream) KV bucket and watch it. It update the local peer configuration and distant peers when changes are made on the KV bucket. In fact, the peer authentication is made via NATS. **If a node has an authenticated read-write access to the NATS cluster and the KV store, it can add himself to the mesh.** 8 | This means that, for now, there is now central control plane for the mesh. 9 | 10 | ### IPAM 11 | It's important that each node get an unique IP address. Here are the steps that Ikto follow to reach this goal: 12 | 1. Before starting ikto, you generate a random address with `ikto init`. 13 | 2. When ikto start, it gets the value on the key `peers.{base64_encoded_peerIP}`. 14 | 3. If a value already exist, it checks that the corresponding peer is himself (comparing the public keys) and update itself 15 | 4. If not ikto fails because of the already in use address and you need to generate a new random IP (so Ikto will better work if you have a lot more available ip than nodes) 16 | 5. If it doesn't exist, Ikto try to create a value [with optimistic locking ](https://docs.nats.io/nats-concepts/jetstream/key-value-store#atomic-operations-used-for-locking-and-concurrency-control) 17 | 18 | As a consequence of nats concurrency control properties, duplicated addresses should never happend. 19 | 20 | 21 | ## Getting started 22 | 23 | ### Pre-requisites 24 | - An available NATS cluster (or just one nats-server) 25 | - Wireguard installed on each node 26 | 27 | 28 | ### Installation 29 | 30 | You can download the latest release from github releases: 31 | ```bash 32 | wget https://github.com/valyentdev/ikto/releases/download/v0.3.0/ikto_0.3.0_linux_amd64.tar.gz 33 | tar -xvf ikto_0.3.0_linux_amd64.tar.gz 34 | cp ikto SOMEWHERE_IN_YOUR_PATH 35 | ``` 36 | 37 | We'll provide an install script in the future. 38 | 39 | ### Configuration 40 | 41 | On each node, you can configure ikto: 42 | ```bash 43 | $ ikto init > ikto.json 44 | ``` 45 | 46 | File generated: 47 | ```json 48 | { 49 | "name": "", 50 | "advertise_address": "", 51 | "private_address": "fd10:2082:5bc1::", 52 | "subnet_prefix": 48, 53 | "mesh_cidr": "fd10::/16", 54 | "wg_dev_name": "wg-ikto", 55 | "wg_port": 51820, 56 | "private_key_path": "", 57 | "nats_creds": "", 58 | "nats_url": "nats://", 59 | "nats_kv": "ikto-mesh" 60 | } 61 | ``` 62 | 63 | Finally you can run it: 64 | ```bash 65 | $ ikto agent -c ikto.json 66 | ``` 67 | 68 | 69 | Ikto listen on an unix socket by default on /tmp/ikto.sock 70 | ```bash 71 | $ ikto agent -c ikto.json -s /var/run/ikto.sock 72 | ``` 73 | 74 | 75 | ## Contributing 76 | 77 | You can signal bugs or request a feature by opening an issue and/or a pull request on this repository. If you have any question you can join our [Discord](https://discord.valyent.dev/) where we are available almost every days. 78 | 79 | ## License 80 | 81 | Copyright 2024 SAS Valyent 82 | 83 | Licensed under the Apache License, Version 2.0 (the "License"); 84 | you may not use these files except in compliance with the License. 85 | You may obtain a copy of the License at 86 | 87 | http://www.apache.org/licenses/LICENSE-2.0 88 | 89 | Unless required by applicable law or agreed to in writing, software 90 | distributed under the License is distributed on an "AS IS" BASIS, 91 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 92 | See the License for the specific language governing permissions and 93 | limitations under the License. 94 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | plugins: 3 | - plugin: go 4 | out: . 5 | opt: 6 | - paths=import 7 | - module=github.com/valyentdev/ikto 8 | - plugin: go-grpc 9 | out: . 10 | opt: 11 | - paths=import 12 | - module=github.com/valyentdev/ikto 13 | - require_unimplemented_servers=false 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/valyentdev/ikto 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/nats-io/nats.go v1.36.0 7 | github.com/spf13/cobra v1.8.1 8 | github.com/vishvananda/netlink v1.2.1-beta.2 9 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 10 | google.golang.org/grpc v1.67.1 11 | google.golang.org/protobuf v1.34.2 12 | ) 13 | 14 | require ( 15 | github.com/google/go-cmp v0.6.0 // indirect 16 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 17 | github.com/josharian/native v1.1.0 // indirect 18 | github.com/klauspost/compress v1.17.2 // indirect 19 | github.com/mdlayher/genetlink v1.3.2 // indirect 20 | github.com/mdlayher/netlink v1.7.2 // indirect 21 | github.com/mdlayher/socket v0.4.1 // indirect 22 | github.com/nats-io/nkeys v0.4.7 // indirect 23 | github.com/nats-io/nuid v1.0.1 // indirect 24 | github.com/spf13/pflag v1.0.5 // indirect 25 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect 26 | golang.org/x/crypto v0.26.0 // indirect 27 | golang.org/x/net v0.28.0 // indirect 28 | golang.org/x/sync v0.8.0 // indirect 29 | golang.org/x/sys v0.25.0 // indirect 30 | golang.org/x/text v0.18.0 // indirect 31 | golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect 32 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect 33 | 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 3 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 4 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 5 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 6 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 7 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 8 | github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= 9 | github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 10 | github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= 11 | github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= 12 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 13 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 14 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 15 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 16 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= 17 | github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= 18 | github.com/nats-io/nats.go v1.36.0 h1:suEUPuWzTSse/XhESwqLxXGuj8vGRuPRoG7MoRN/qyU= 19 | github.com/nats-io/nats.go v1.36.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 20 | github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= 21 | github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 22 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 23 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 24 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 25 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 26 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 27 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 28 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 29 | github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= 30 | github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= 31 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= 32 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 33 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 34 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 35 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 36 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 37 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 38 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 39 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 42 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 43 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 44 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 45 | golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= 46 | golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= 47 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= 48 | golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= 49 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= 50 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 51 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 52 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 53 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 54 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 55 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 56 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 57 | -------------------------------------------------------------------------------- /internal/network/ip.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "crypto/rand" 5 | "net" 6 | ) 7 | 8 | func randomBuffer(size int) []byte { 9 | buf := make([]byte, size) 10 | _, err := rand.Read(buf) 11 | if err != nil { 12 | panic(err) 13 | } 14 | return buf 15 | } 16 | 17 | func RandomSubnet(network net.IPNet, prefix int) net.IPNet { 18 | ones, _ := network.Mask.Size() 19 | 20 | randomBuffer := randomBuffer(len(network.IP)) 21 | 22 | newIp := make([]byte, len(network.IP)) 23 | copy(newIp, network.IP) 24 | 25 | // Set the random bits from the random buffer 26 | // on bits from size to prefix 27 | for bitIndex := ones; bitIndex < prefix; bitIndex++ { 28 | byteIndex := bitIndex / 8 29 | 30 | newIp[byteIndex] |= randomBuffer[byteIndex] & (1 << uint(7-bitIndex%8)) 31 | } 32 | 33 | return net.IPNet{ 34 | IP: newIp, 35 | Mask: net.CIDRMask(prefix, 32), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/network/wg_device.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net" 7 | 8 | "github.com/valyentdev/ikto/pkg/types" 9 | "github.com/vishvananda/netlink" 10 | "golang.zx2c4.com/wireguard/wgctrl" 11 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 12 | ) 13 | 14 | type WGDevice struct { 15 | name string 16 | port int 17 | wg *wgctrl.Client 18 | privateKey wgtypes.Key 19 | } 20 | 21 | func New(name string, port int, privateKey wgtypes.Key) (*WGDevice, error) { 22 | client, err := wgctrl.New() 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to create wgctrl client: %w", err) 25 | } 26 | 27 | return &WGDevice{ 28 | wg: client, 29 | name: name, 30 | port: port, 31 | privateKey: privateKey, 32 | }, nil 33 | } 34 | 35 | func (d *WGDevice) Ensure() error { 36 | link, err := netlink.LinkByName(d.name) 37 | if err != nil { 38 | switch err.(type) { 39 | case netlink.LinkNotFoundError: 40 | link = &netlink.Wireguard{ 41 | LinkAttrs: netlink.LinkAttrs{ 42 | Name: d.name, 43 | }, 44 | } 45 | 46 | if err := netlink.LinkAdd(link); err != nil { 47 | return err 48 | } 49 | default: 50 | return fmt.Errorf("failed to check link %s: %w", d.name, err) 51 | } 52 | } 53 | 54 | if err := netlink.LinkSetUp(link); err != nil { 55 | return fmt.Errorf("failed to set link up: %w", err) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (d WGDevice) SetAddr(ipnet net.IPNet) error { 62 | link, err := netlink.LinkByName(d.name) 63 | if err != nil { 64 | return fmt.Errorf("failed to get link: %w", err) 65 | } 66 | 67 | newAddr := &netlink.Addr{ 68 | IPNet: &ipnet, 69 | } 70 | 71 | if err := netlink.AddrReplace(link, newAddr); err != nil { 72 | return fmt.Errorf("failed to add addr: %w", err) 73 | } 74 | list, err := netlink.AddrList(link, netlink.FAMILY_V6) 75 | if err != nil { 76 | return fmt.Errorf("failed to list addrs: %w", err) 77 | } 78 | 79 | for _, addr := range list { 80 | if !addr.Equal(*newAddr) { 81 | if err := netlink.AddrDel(link, &addr); err != nil { 82 | slog.Error("failed to delete addr", "error", err) 83 | } 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (m *WGDevice) InitConfig() error { 91 | return m.wg.ConfigureDevice(m.name, wgtypes.Config{ 92 | PrivateKey: &m.privateKey, 93 | ListenPort: &m.port, 94 | }) 95 | } 96 | 97 | func (m *WGDevice) Remove() error { 98 | link, err := netlink.LinkByName(m.name) 99 | if err != nil { 100 | switch err.(type) { 101 | case netlink.LinkNotFoundError: 102 | return nil 103 | default: 104 | return fmt.Errorf("failed to get link: %w", err) 105 | } 106 | } 107 | 108 | if err := netlink.LinkDel(link); err != nil { 109 | return fmt.Errorf("failed to delete link: %w", err) 110 | } 111 | 112 | return nil 113 | 114 | } 115 | 116 | func (m *WGDevice) RemovePeer(publicKey wgtypes.Key) error { 117 | return m.wg.ConfigureDevice(m.name, wgtypes.Config{ 118 | Peers: []wgtypes.PeerConfig{ 119 | { 120 | PublicKey: publicKey, 121 | Remove: true, 122 | }, 123 | }, 124 | }) 125 | } 126 | 127 | func (m *WGDevice) AddPeer(peer types.Peer) error { 128 | return m.configurePeers([]types.Peer{peer}, false) 129 | } 130 | 131 | func (m *WGDevice) ReplacePeers(peers []types.Peer) error { 132 | return m.configurePeers(peers, true) 133 | } 134 | 135 | func (m *WGDevice) configurePeers(peers []types.Peer, replacePeers bool) error { 136 | peerConfigs := make([]wgtypes.PeerConfig, 0, len(peers)) 137 | for _, member := range peers { 138 | peerConfig, err := member.WGPeerConfig() 139 | if err != nil { 140 | slog.Error("failed to get peer config", "error", err, "peer_name", member.Name, "public_key", member.PublicKey.String(), "advertise_address", member.AdvertiseAddress, "allowed_ip", member.AllowedIP, "wg_port", member.WGPort) 141 | continue 142 | } 143 | 144 | peerConfigs = append(peerConfigs, peerConfig) 145 | } 146 | 147 | return m.wg.ConfigureDevice(m.name, wgtypes.Config{ 148 | Peers: peerConfigs, 149 | ReplacePeers: replacePeers, 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /internal/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log/slog" 8 | "sync" 9 | 10 | "github.com/nats-io/nats.go/jetstream" 11 | "github.com/valyentdev/ikto/pkg/types" 12 | ) 13 | 14 | type SyncedState struct { 15 | stop chan struct{} 16 | finish chan struct{} 17 | config Config 18 | peers map[string]types.Peer 19 | mutex sync.RWMutex 20 | } 21 | 22 | type Config struct { 23 | KV jetstream.KeyValue 24 | IgnorePeer types.PublicKey 25 | 26 | OnPeerPut func(peer types.Peer) 27 | OnPeerDelete func(peer types.Peer) 28 | OnInitPeers func(map[string]types.Peer) 29 | } 30 | 31 | func New(config Config) *SyncedState { 32 | if config.OnPeerPut == nil { 33 | config.OnPeerPut = func(peer types.Peer) {} 34 | } 35 | 36 | if config.OnPeerDelete == nil { 37 | config.OnPeerDelete = func(peer types.Peer) {} 38 | } 39 | 40 | if config.OnInitPeers == nil { 41 | config.OnInitPeers = func(peers map[string]types.Peer) {} 42 | } 43 | 44 | return &SyncedState{ 45 | stop: make(chan struct{}), 46 | finish: make(chan struct{}), 47 | peers: make(map[string]types.Peer), 48 | config: config, 49 | } 50 | } 51 | 52 | func (w *SyncedState) Start(ctx context.Context) error { 53 | kv := w.config.KV 54 | sub := "peers.*" 55 | watcher, err := kv.Watch(ctx, sub) 56 | if err != nil { 57 | return fmt.Errorf("failed to watch: %w", err) 58 | } 59 | 60 | slog.Info("Started watching peers") 61 | updates := watcher.Updates() 62 | w.init(updates) 63 | 64 | go func() { 65 | slog.Info("Started continuous peer synchronization") 66 | w.sync(updates) 67 | }() 68 | 69 | return nil 70 | } 71 | 72 | func (w *SyncedState) init(entries <-chan jetstream.KeyValueEntry) { 73 | for entry := range entries { 74 | if entry == nil { 75 | break 76 | } 77 | 78 | if entry.Operation() != jetstream.KeyValuePut { 79 | continue 80 | } 81 | 82 | peer, err := readPeer(entry.Value()) 83 | if err != nil { 84 | slog.Error("failed to read peer", "error", err) 85 | continue 86 | } 87 | 88 | if peer.PublicKey == w.config.IgnorePeer { 89 | continue 90 | } 91 | 92 | w.peers[entry.Key()] = peer 93 | } 94 | slog.Info("Initializing peers", "count", len(w.peers)) 95 | w.config.OnInitPeers(w.peers) 96 | } 97 | 98 | func (w *SyncedState) sync(entries <-chan jetstream.KeyValueEntry) { 99 | for { 100 | 101 | select { 102 | case <-w.stop: 103 | close(w.finish) 104 | return 105 | case entry := <-entries: 106 | if entry == nil { 107 | continue 108 | } 109 | 110 | key := entry.Key() 111 | 112 | switch entry.Operation() { 113 | case jetstream.KeyValuePut: 114 | peer, err := readPeer(entry.Value()) 115 | if err != nil { 116 | slog.Error("failed to read peer", "error", err) 117 | continue 118 | } 119 | w.onPeerPut(key, peer) 120 | case jetstream.KeyValueDelete: 121 | w.onPeerDelete(key) 122 | case jetstream.KeyValuePurge: 123 | w.onPeerDelete(key) 124 | } 125 | } 126 | } 127 | } 128 | 129 | func (w *SyncedState) onPeerPut(key string, peer types.Peer) { 130 | if peer.PublicKey == w.config.IgnorePeer { 131 | return 132 | } 133 | slog.Info("Peer put", "public_key", peer.PublicKey.String(), "ip", peer.AllowedIP) 134 | w.mutex.Lock() 135 | w.peers[key] = peer 136 | w.mutex.Unlock() 137 | w.config.OnPeerPut(peer) 138 | 139 | } 140 | 141 | func (w *SyncedState) onPeerDelete(key string) { 142 | w.mutex.Lock() 143 | defer w.mutex.Unlock() 144 | 145 | peer, ok := w.peers[key] 146 | if !ok { 147 | return 148 | } 149 | 150 | slog.Info("Peer delete", "public_key", peer.PublicKey.String(), "ip", peer.AllowedIP) 151 | delete(w.peers, key) 152 | w.config.OnPeerDelete(peer) 153 | } 154 | 155 | func (w *SyncedState) Stop() { 156 | close(w.stop) 157 | <-w.finish 158 | } 159 | 160 | func (s *SyncedState) ListPeers() []types.Peer { 161 | s.mutex.RLock() 162 | peers := make([]types.Peer, 0, len(s.peers)) 163 | for _, peer := range s.peers { 164 | peers = append(peers, peer) 165 | } 166 | s.mutex.RUnlock() 167 | return peers 168 | } 169 | 170 | func readPeer(data []byte) (types.Peer, error) { 171 | var p types.Peer 172 | err := json.Unmarshal(data, &p) 173 | if err != nil { 174 | return types.Peer{}, err 175 | } 176 | 177 | return p, nil 178 | 179 | } 180 | -------------------------------------------------------------------------------- /internal/state/store.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | 8 | "github.com/nats-io/nats.go/jetstream" 9 | "github.com/valyentdev/ikto/pkg/types" 10 | ) 11 | 12 | type Store struct { 13 | kv jetstream.KeyValue 14 | } 15 | 16 | func NewStore(kv jetstream.KeyValue) *Store { 17 | return &Store{ 18 | kv: kv, 19 | } 20 | } 21 | 22 | func (s *Store) CreatePeer(ctx context.Context, peer types.Peer) (uint64, error) { 23 | bytes, err := json.Marshal(peer) 24 | if err != nil { 25 | return 0, err 26 | } 27 | 28 | revision, err := s.kv.Create(ctx, getKey(peer.AllowedIP), bytes) 29 | if err != nil { 30 | return 0, err 31 | } 32 | 33 | return revision, nil 34 | } 35 | 36 | func getKey(ip string) string { 37 | return "peers." + base64.URLEncoding.EncodeToString([]byte(ip)) 38 | } 39 | 40 | func (s *Store) GetPeer(ctx context.Context, ip string) (types.Peer, uint64, error) { 41 | entry, err := s.kv.Get(ctx, getKey(ip)) 42 | if err != nil { 43 | return types.Peer{}, 0, err 44 | } 45 | 46 | var peer types.Peer 47 | if err := json.Unmarshal(entry.Value(), &peer); err != nil { 48 | return types.Peer{}, 0, err 49 | } 50 | 51 | return peer, entry.Revision(), nil 52 | } 53 | 54 | func (s *Store) UpdatePeer(ctx context.Context, peer types.Peer, revision uint64) (uint64, error) { 55 | bytes, err := json.Marshal(peer) 56 | if err != nil { 57 | return 0, err 58 | } 59 | 60 | r, err := s.kv.Update(ctx, getKey(peer.AllowedIP), bytes, revision) 61 | if err != nil { 62 | return 0, err 63 | } 64 | 65 | return r, err 66 | } 67 | 68 | func (s *Store) DeletePeer(ctx context.Context, ip string, revision uint64) error { 69 | return s.kv.Delete(ctx, getKey(ip), jetstream.LastRevision(revision)) 70 | } 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/valyentdev/ikto/pkg/commands" 8 | ) 9 | 10 | func main() { 11 | root := commands.NewRootCommand() 12 | 13 | if err := root.Execute(); err != nil { 14 | fmt.Println(err) 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/commands/agent.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/valyentdev/ikto/pkg/ikto" 12 | "github.com/valyentdev/ikto/pkg/server" 13 | ) 14 | 15 | func NewAgentCommand() *cobra.Command { 16 | var configPath string 17 | var socket string 18 | cmd := &cobra.Command{ 19 | Use: "agent", 20 | Short: "Start the ikto agent", 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | if len(configPath) == 0 { 23 | return fmt.Errorf("config path is required") 24 | } 25 | 26 | configFile, err := os.ReadFile(configPath) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | var config Config 32 | if err := json.Unmarshal(configFile, &config); err != nil { 33 | return err 34 | } 35 | 36 | iktoConfig, err := config.Validate() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | ikto, err := ikto.NewIkto(&iktoConfig) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = ikto.Start() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) 52 | 53 | err = server.StartAdminServer(ctx, *ikto, socket) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | ikto.Stop() 59 | 60 | return nil 61 | }, 62 | } 63 | 64 | cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to the configuration file") 65 | cmd.MarkFlagRequired("config") 66 | 67 | cmd.Flags().StringVarP(&socket, "socket", "s", "/tmp/ikto.sock", "Path to the socket file") 68 | 69 | return cmd 70 | } 71 | -------------------------------------------------------------------------------- /pkg/commands/config.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/valyentdev/ikto/pkg/ikto" 8 | ) 9 | 10 | type Config struct { 11 | Name string `json:"name"` 12 | 13 | AdvertiseAddress string `json:"advertise_address"` 14 | PrivateAddress string `json:"private_address"` 15 | HostPrefixLength int `json:"subnet_prefix"` 16 | MeshIPNet string `json:"mesh_cidr"` 17 | 18 | WGDevName string `json:"wg_dev_name"` 19 | WGPort int `json:"wg_port"` 20 | PrivateKeyPath string `json:"private_key_path"` 21 | 22 | NatsCreds string `json:"nats_creds"` 23 | NatsURL string `json:"nats_url"` 24 | NatsKV string `json:"nats_kv"` 25 | } 26 | 27 | func (c *Config) Validate() (ikto.Config, error) { 28 | _, ipnet, err := net.ParseCIDR(c.MeshIPNet) 29 | if err != nil { 30 | return ikto.Config{}, err 31 | } 32 | 33 | advertiseAddress := net.ParseIP(c.AdvertiseAddress) 34 | if advertiseAddress == nil { 35 | return ikto.Config{}, fmt.Errorf("advertise address is required") 36 | } 37 | 38 | privateAddress := net.ParseIP(c.PrivateAddress) 39 | if privateAddress == nil { 40 | return ikto.Config{}, fmt.Errorf("private address is invalid") 41 | } 42 | if len(ipnet.IP) == 4 { 43 | privateAddress = privateAddress.To4() 44 | } else { 45 | privateAddress = privateAddress.To16() 46 | } 47 | 48 | fmt.Println(privateAddress) 49 | if !ipnet.Contains(privateAddress) { 50 | return ikto.Config{}, fmt.Errorf("private address is not in mesh network") 51 | } 52 | 53 | return ikto.Config{ 54 | Name: c.Name, 55 | 56 | NatsCreds: c.NatsCreds, 57 | NatsURL: c.NatsURL, 58 | NatsKV: c.NatsKV, 59 | 60 | AdvertiseAddress: advertiseAddress, 61 | PrivateAddress: privateAddress, 62 | MeshIPNet: *ipnet, 63 | HostPrefixLength: c.HostPrefixLength, 64 | 65 | WGDevName: c.WGDevName, 66 | WGPort: c.WGPort, 67 | PrivateKeyPath: c.PrivateKeyPath, 68 | }, nil 69 | } 70 | 71 | func DefaultConfig() *Config { 72 | return &Config{ 73 | Name: "", 74 | NatsCreds: "", 75 | NatsURL: "nats://", 76 | NatsKV: "ikto-mesh", 77 | PrivateKeyPath: "", 78 | AdvertiseAddress: "", 79 | MeshIPNet: "", 80 | WGDevName: "wg-ikto", 81 | WGPort: 51820, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/commands/info.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/valyentdev/ikto/pkg/proto" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials/insecure" 12 | ) 13 | 14 | func NewInfoCommand() *cobra.Command { 15 | var socket string 16 | var cmd = &cobra.Command{ 17 | Use: "info", 18 | Short: "Print info about the local node", 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | 21 | conn, err := grpc.NewClient("0.0.0.0", grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) { 22 | return net.Dial("unix", socket) 23 | }), grpc.WithTransportCredentials(insecure.NewCredentials())) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | client := proto.NewAdminServiceClient(conn) 29 | 30 | infos, err := client.NodeInfo(context.Background(), nil) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | fmt.Println("Local Node:") 36 | printPeer(infos.Self) 37 | fmt.Println("Peers:") 38 | for _, peer := range infos.Peers { 39 | printPeer(peer) 40 | } 41 | 42 | return nil 43 | }, 44 | } 45 | 46 | cmd.Flags().StringVar(&socket, "socket", "/tmp/ikto.sock", "Path to the admin socket") 47 | 48 | return cmd 49 | } 50 | 51 | func printPeer(peer *proto.Peer) { 52 | fmt.Printf("Name: %s\n", peer.Name) 53 | fmt.Printf("Public Key: %s\n", peer.PublicKey) 54 | fmt.Printf("Advertise Address: %s\n", peer.AdvertiseAddr) 55 | fmt.Printf("Allowed IP: %s\n", peer.AllowedIp) 56 | fmt.Printf("WireGuard Port: %d\n", peer.WgPort) 57 | fmt.Println() 58 | } 59 | -------------------------------------------------------------------------------- /pkg/commands/init.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/json" 6 | "net" 7 | "os" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func randomBuffer(size int) []byte { 13 | buf := make([]byte, size) 14 | _, err := rand.Read(buf) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return buf 19 | } 20 | 21 | func RandomSubnet(network net.IPNet, prefix int) net.IPNet { 22 | ones, _ := network.Mask.Size() 23 | 24 | randomBuffer := randomBuffer(len(network.IP)) 25 | 26 | newIp := make([]byte, len(network.IP)) 27 | copy(newIp, network.IP) 28 | 29 | // Set the random bits from the random buffer 30 | // on bits from size to prefix 31 | for bitIndex := ones; bitIndex < prefix; bitIndex++ { 32 | byteIndex := bitIndex / 8 33 | 34 | newIp[byteIndex] |= randomBuffer[byteIndex] & (1 << uint(7-bitIndex%8)) 35 | } 36 | 37 | return net.IPNet{ 38 | IP: newIp, 39 | Mask: net.CIDRMask(prefix, 32), 40 | } 41 | } 42 | 43 | func NewInitCommand() *cobra.Command { 44 | var meshIpNet string 45 | var subnetPrefix int 46 | cmd := &cobra.Command{ 47 | Use: "init", 48 | Short: "Initialize configuration for ikto.", 49 | Long: `Initialize configuration for ikto by generating a random private address 50 | in your mesh subnet.You can specify the mesh subnet and the subnet prefix 51 | length. Feel free to choose your private address in your mesh subnet. 52 | `, 53 | Run: func(cmd *cobra.Command, args []string) { 54 | config := DefaultConfig() 55 | _, ipNet, err := net.ParseCIDR(meshIpNet) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | config.MeshIPNet = ipNet.String() 61 | 62 | config.HostPrefixLength = subnetPrefix 63 | 64 | config.PrivateAddress = RandomSubnet(*ipNet, subnetPrefix).IP.String() 65 | bytes, err := json.MarshalIndent(config, "", " ") 66 | if err != nil { 67 | panic(err) 68 | } 69 | os.Stdout.Write(append(bytes, '\n')) 70 | }, 71 | } 72 | 73 | cmd.Flags().StringVarP(&meshIpNet, "mesh-ip-net", "m", "fd10::/16", "Mesh IP Net") 74 | cmd.Flags().IntVarP(&subnetPrefix, "subnet-prefix-length", "p", 48, "Subnet Prefix") 75 | 76 | return cmd 77 | } 78 | -------------------------------------------------------------------------------- /pkg/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewRootCommand() *cobra.Command { 8 | var root = &cobra.Command{ 9 | Use: "ikto", 10 | Short: "A NATS based wireguard mesh network builder", 11 | Long: "A NATS based wireguard mesh network builder", 12 | } 13 | 14 | root.AddCommand(NewAgentCommand()) 15 | root.AddCommand(NewInitCommand()) 16 | root.AddCommand(NewInfoCommand()) 17 | return root 18 | } 19 | -------------------------------------------------------------------------------- /pkg/ikto/ikto.go: -------------------------------------------------------------------------------- 1 | package ikto 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net" 8 | "os" 9 | "strings" 10 | 11 | "github.com/nats-io/nats.go" 12 | "github.com/nats-io/nats.go/jetstream" 13 | "github.com/valyentdev/ikto/internal/network" 14 | "github.com/valyentdev/ikto/internal/state" 15 | "github.com/valyentdev/ikto/pkg/types" 16 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 17 | ) 18 | 19 | type Config struct { 20 | Name string 21 | 22 | AdvertiseAddress net.IP 23 | PrivateAddress net.IP 24 | MeshIPNet net.IPNet 25 | HostPrefixLength int 26 | 27 | WGDevName string 28 | WGPort int 29 | PrivateKeyPath string 30 | 31 | NatsCreds string 32 | NatsURL string 33 | NatsKV string 34 | } 35 | 36 | func (c *Config) getPrivateCIDR() string { 37 | return fmt.Sprintf("%s/%d", c.PrivateAddress.String(), c.HostPrefixLength) 38 | } 39 | 40 | type Ikto struct { 41 | config Config 42 | nc *nats.Conn 43 | js jetstream.JetStream 44 | kv jetstream.KeyValue 45 | 46 | store *state.Store 47 | self types.Peer 48 | state *state.SyncedState 49 | wg *network.WGDevice 50 | } 51 | 52 | var ErrAddressAlreadyInUse = fmt.Errorf("address already in use") 53 | 54 | func NewIkto(c *Config) (*Ikto, error) { 55 | privateKeyFile, err := os.ReadFile(c.PrivateKeyPath) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to read private key: %w", err) 58 | } 59 | 60 | privateKey, err := wgtypes.ParseKey(strings.TrimSpace(string(privateKeyFile))) 61 | if err != nil { 62 | return nil, fmt.Errorf("failed to parse private key: %w", err) 63 | } 64 | 65 | publicKey := privateKey.PublicKey() 66 | 67 | self := types.Peer{ 68 | Name: c.Name, 69 | PublicKey: types.PublicKey(publicKey), 70 | AdvertiseAddress: c.AdvertiseAddress.String(), 71 | AllowedIP: c.getPrivateCIDR(), 72 | WGPort: c.WGPort, 73 | } 74 | 75 | slog.Info("Starting with self config", "name", self.Name, "public_key", self.PublicKey.String(), "advertise_address", self.AdvertiseAddress, "allowed_ip", self.AllowedIP, "wg_port", self.WGPort, "wg_dev_name", c.WGDevName) 76 | 77 | wg, err := network.New(fmt.Sprintf(c.WGDevName), c.WGPort, privateKey) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to create wg service: %w", err) 80 | } 81 | 82 | err = wg.Ensure() 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to ensure wireguard device: %w", err) 85 | } 86 | 87 | err = wg.InitConfig() 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to init wireguard config: %w", err) 90 | } 91 | 92 | meshOnes, _ := c.MeshIPNet.Mask.Size() 93 | 94 | err = wg.SetAddr(net.IPNet{ 95 | IP: c.PrivateAddress, 96 | Mask: net.CIDRMask(meshOnes, len(c.PrivateAddress)*8), 97 | }) 98 | if err != nil { 99 | return nil, fmt.Errorf("failed to set address: %w", err) 100 | } 101 | 102 | nc, err := nats.Connect(c.NatsURL, nats.UserCredentials(c.NatsCreds, c.NatsCreds)) 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to connect to nats: %w", err) 105 | } 106 | 107 | js, err := jetstream.New(nc) 108 | if err != nil { 109 | return nil, fmt.Errorf("failed to create jetstream client: %w", err) 110 | } 111 | 112 | kv, err := js.KeyValue(context.Background(), c.NatsKV) 113 | if err != nil { 114 | return nil, fmt.Errorf("failed to get key value store: %w", err) 115 | } 116 | 117 | store := state.NewStore(kv) 118 | 119 | state := state.New(state.Config{ 120 | KV: kv, 121 | IgnorePeer: self.PublicKey, 122 | 123 | OnPeerPut: func(peer types.Peer) { 124 | err := wg.AddPeer(peer) 125 | if err != nil { 126 | fmt.Printf("failed to add peer: %v", err) 127 | } 128 | }, 129 | OnPeerDelete: func(peer types.Peer) { 130 | err := wg.RemovePeer(peer.PublicKey.WG()) 131 | if err != nil { 132 | fmt.Printf("failed to remove peer: %v", err) 133 | } 134 | }, 135 | OnInitPeers: func(m map[string]types.Peer) { 136 | peers := make([]types.Peer, 0, len(m)) 137 | for _, peer := range m { 138 | peers = append(peers, peer) 139 | } 140 | 141 | err := wg.ReplacePeers(peers) 142 | if err != nil { 143 | fmt.Printf("failed to replace peers: %v", err) 144 | } 145 | }, 146 | }) 147 | return &Ikto{ 148 | config: *c, 149 | nc: nc, 150 | js: js, 151 | kv: kv, 152 | 153 | store: store, 154 | self: self, 155 | state: state, 156 | wg: wg, 157 | }, nil 158 | } 159 | 160 | func (i *Ikto) init() error { 161 | previous, revision, err := i.store.GetPeer(context.Background(), i.self.AllowedIP) 162 | if err != nil && err != jetstream.ErrKeyNotFound { 163 | return fmt.Errorf("failed to get self: % w", err) 164 | } 165 | if err == jetstream.ErrKeyNotFound { 166 | _, err := i.store.CreatePeer(context.Background(), i.self) 167 | if err != nil { 168 | return fmt.Errorf("failed to create self: %w", err) 169 | } 170 | return nil 171 | } 172 | 173 | if previous.PublicKey != i.self.PublicKey { 174 | return ErrAddressAlreadyInUse 175 | } 176 | 177 | _, err = i.store.UpdatePeer(context.Background(), i.self, revision) 178 | if err != nil { 179 | return fmt.Errorf("failed to update self: %w", err) 180 | } 181 | 182 | return nil 183 | } 184 | 185 | func (i *Ikto) Start() error { 186 | if err := i.init(); err != nil { 187 | return fmt.Errorf("failed to init: %w", err) 188 | } 189 | 190 | err := i.state.Start(context.Background()) 191 | if err != nil { 192 | return fmt.Errorf("failed to start state: %w", err) 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func (i *Ikto) Stop() { 199 | slog.Info("stopping") 200 | i.state.Stop() 201 | i.nc.Close() 202 | } 203 | 204 | func (i *Ikto) Leave() { 205 | slog.Error("leaving network") 206 | } 207 | 208 | func (i *Ikto) Self() types.Peer { 209 | return i.self 210 | } 211 | 212 | func (i *Ikto) Peers() []types.Peer { 213 | return i.state.ListPeers() 214 | } 215 | -------------------------------------------------------------------------------- /pkg/proto/api.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc (unknown) 5 | // source: pkg/proto/api.proto 6 | 7 | package proto 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | emptypb "google.golang.org/protobuf/types/known/emptypb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type NodeInfoResponse struct { 25 | state protoimpl.MessageState 26 | sizeCache protoimpl.SizeCache 27 | unknownFields protoimpl.UnknownFields 28 | 29 | Self *Peer `protobuf:"bytes,1,opt,name=self,proto3" json:"self,omitempty"` 30 | Peers []*Peer `protobuf:"bytes,4,rep,name=peers,proto3" json:"peers,omitempty"` 31 | } 32 | 33 | func (x *NodeInfoResponse) Reset() { 34 | *x = NodeInfoResponse{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_pkg_proto_api_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *NodeInfoResponse) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*NodeInfoResponse) ProtoMessage() {} 47 | 48 | func (x *NodeInfoResponse) ProtoReflect() protoreflect.Message { 49 | mi := &file_pkg_proto_api_proto_msgTypes[0] 50 | if protoimpl.UnsafeEnabled && x != nil { 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | if ms.LoadMessageInfo() == nil { 53 | ms.StoreMessageInfo(mi) 54 | } 55 | return ms 56 | } 57 | return mi.MessageOf(x) 58 | } 59 | 60 | // Deprecated: Use NodeInfoResponse.ProtoReflect.Descriptor instead. 61 | func (*NodeInfoResponse) Descriptor() ([]byte, []int) { 62 | return file_pkg_proto_api_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *NodeInfoResponse) GetSelf() *Peer { 66 | if x != nil { 67 | return x.Self 68 | } 69 | return nil 70 | } 71 | 72 | func (x *NodeInfoResponse) GetPeers() []*Peer { 73 | if x != nil { 74 | return x.Peers 75 | } 76 | return nil 77 | } 78 | 79 | type Peer struct { 80 | state protoimpl.MessageState 81 | sizeCache protoimpl.SizeCache 82 | unknownFields protoimpl.UnknownFields 83 | 84 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 85 | PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` 86 | AdvertiseAddr string `protobuf:"bytes,3,opt,name=advertise_addr,json=advertiseAddr,proto3" json:"advertise_addr,omitempty"` 87 | // string private_addr = 4; 88 | WgPort int32 `protobuf:"varint,5,opt,name=wg_port,json=wgPort,proto3" json:"wg_port,omitempty"` 89 | AllowedIp string `protobuf:"bytes,6,opt,name=allowed_ip,json=allowedIp,proto3" json:"allowed_ip,omitempty"` 90 | } 91 | 92 | func (x *Peer) Reset() { 93 | *x = Peer{} 94 | if protoimpl.UnsafeEnabled { 95 | mi := &file_pkg_proto_api_proto_msgTypes[1] 96 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 97 | ms.StoreMessageInfo(mi) 98 | } 99 | } 100 | 101 | func (x *Peer) String() string { 102 | return protoimpl.X.MessageStringOf(x) 103 | } 104 | 105 | func (*Peer) ProtoMessage() {} 106 | 107 | func (x *Peer) ProtoReflect() protoreflect.Message { 108 | mi := &file_pkg_proto_api_proto_msgTypes[1] 109 | if protoimpl.UnsafeEnabled && x != nil { 110 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 111 | if ms.LoadMessageInfo() == nil { 112 | ms.StoreMessageInfo(mi) 113 | } 114 | return ms 115 | } 116 | return mi.MessageOf(x) 117 | } 118 | 119 | // Deprecated: Use Peer.ProtoReflect.Descriptor instead. 120 | func (*Peer) Descriptor() ([]byte, []int) { 121 | return file_pkg_proto_api_proto_rawDescGZIP(), []int{1} 122 | } 123 | 124 | func (x *Peer) GetName() string { 125 | if x != nil { 126 | return x.Name 127 | } 128 | return "" 129 | } 130 | 131 | func (x *Peer) GetPublicKey() string { 132 | if x != nil { 133 | return x.PublicKey 134 | } 135 | return "" 136 | } 137 | 138 | func (x *Peer) GetAdvertiseAddr() string { 139 | if x != nil { 140 | return x.AdvertiseAddr 141 | } 142 | return "" 143 | } 144 | 145 | func (x *Peer) GetWgPort() int32 { 146 | if x != nil { 147 | return x.WgPort 148 | } 149 | return 0 150 | } 151 | 152 | func (x *Peer) GetAllowedIp() string { 153 | if x != nil { 154 | return x.AllowedIp 155 | } 156 | return "" 157 | } 158 | 159 | var File_pkg_proto_api_proto protoreflect.FileDescriptor 160 | 161 | var file_pkg_proto_api_proto_rawDesc = []byte{ 162 | 0x0a, 0x13, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2e, 163 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x69, 0x6b, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 164 | 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 165 | 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x54, 0x0a, 0x10, 0x4e, 0x6f, 0x64, 0x65, 166 | 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x04, 167 | 0x73, 0x65, 0x6c, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x69, 0x6b, 0x74, 168 | 0x6f, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x04, 0x73, 0x65, 0x6c, 0x66, 0x12, 0x20, 0x0a, 0x05, 169 | 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x69, 0x6b, 170 | 0x74, 0x6f, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x98, 171 | 0x01, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 172 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 173 | 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 174 | 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x64, 175 | 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 176 | 0x28, 0x09, 0x52, 0x0d, 0x61, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x41, 0x64, 0x64, 177 | 0x72, 0x12, 0x17, 0x0a, 0x07, 0x77, 0x67, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 178 | 0x28, 0x05, 0x52, 0x06, 0x77, 0x67, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x6c, 179 | 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 180 | 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x32, 0x4c, 0x0a, 0x0c, 0x41, 0x64, 0x6d, 181 | 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x08, 0x4e, 0x6f, 0x64, 182 | 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 183 | 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x16, 0x2e, 184 | 0x69, 0x6b, 0x74, 0x6f, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 185 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 186 | 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x61, 0x6c, 0x79, 0x65, 0x6e, 0x74, 0x64, 0x65, 0x76, 187 | 0x2f, 0x69, 0x6b, 0x74, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 188 | 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 189 | } 190 | 191 | var ( 192 | file_pkg_proto_api_proto_rawDescOnce sync.Once 193 | file_pkg_proto_api_proto_rawDescData = file_pkg_proto_api_proto_rawDesc 194 | ) 195 | 196 | func file_pkg_proto_api_proto_rawDescGZIP() []byte { 197 | file_pkg_proto_api_proto_rawDescOnce.Do(func() { 198 | file_pkg_proto_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_proto_api_proto_rawDescData) 199 | }) 200 | return file_pkg_proto_api_proto_rawDescData 201 | } 202 | 203 | var file_pkg_proto_api_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 204 | var file_pkg_proto_api_proto_goTypes = []interface{}{ 205 | (*NodeInfoResponse)(nil), // 0: ikto.NodeInfoResponse 206 | (*Peer)(nil), // 1: ikto.Peer 207 | (*emptypb.Empty)(nil), // 2: google.protobuf.Empty 208 | } 209 | var file_pkg_proto_api_proto_depIdxs = []int32{ 210 | 1, // 0: ikto.NodeInfoResponse.self:type_name -> ikto.Peer 211 | 1, // 1: ikto.NodeInfoResponse.peers:type_name -> ikto.Peer 212 | 2, // 2: ikto.AdminService.NodeInfo:input_type -> google.protobuf.Empty 213 | 0, // 3: ikto.AdminService.NodeInfo:output_type -> ikto.NodeInfoResponse 214 | 3, // [3:4] is the sub-list for method output_type 215 | 2, // [2:3] is the sub-list for method input_type 216 | 2, // [2:2] is the sub-list for extension type_name 217 | 2, // [2:2] is the sub-list for extension extendee 218 | 0, // [0:2] is the sub-list for field type_name 219 | } 220 | 221 | func init() { file_pkg_proto_api_proto_init() } 222 | func file_pkg_proto_api_proto_init() { 223 | if File_pkg_proto_api_proto != nil { 224 | return 225 | } 226 | if !protoimpl.UnsafeEnabled { 227 | file_pkg_proto_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 228 | switch v := v.(*NodeInfoResponse); i { 229 | case 0: 230 | return &v.state 231 | case 1: 232 | return &v.sizeCache 233 | case 2: 234 | return &v.unknownFields 235 | default: 236 | return nil 237 | } 238 | } 239 | file_pkg_proto_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 240 | switch v := v.(*Peer); i { 241 | case 0: 242 | return &v.state 243 | case 1: 244 | return &v.sizeCache 245 | case 2: 246 | return &v.unknownFields 247 | default: 248 | return nil 249 | } 250 | } 251 | } 252 | type x struct{} 253 | out := protoimpl.TypeBuilder{ 254 | File: protoimpl.DescBuilder{ 255 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 256 | RawDescriptor: file_pkg_proto_api_proto_rawDesc, 257 | NumEnums: 0, 258 | NumMessages: 2, 259 | NumExtensions: 0, 260 | NumServices: 1, 261 | }, 262 | GoTypes: file_pkg_proto_api_proto_goTypes, 263 | DependencyIndexes: file_pkg_proto_api_proto_depIdxs, 264 | MessageInfos: file_pkg_proto_api_proto_msgTypes, 265 | }.Build() 266 | File_pkg_proto_api_proto = out.File 267 | file_pkg_proto_api_proto_rawDesc = nil 268 | file_pkg_proto_api_proto_goTypes = nil 269 | file_pkg_proto_api_proto_depIdxs = nil 270 | } 271 | -------------------------------------------------------------------------------- /pkg/proto/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ikto; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | option go_package = "github.com/valyentdev/ikto/pkg/proto"; 8 | 9 | service AdminService { 10 | rpc NodeInfo(google.protobuf.Empty) returns (NodeInfoResponse) {} 11 | } 12 | 13 | message NodeInfoResponse { 14 | Peer self = 1; 15 | repeated Peer peers = 4; 16 | } 17 | 18 | message Peer { 19 | string name = 1; 20 | string public_key = 2; 21 | string advertise_addr = 3; 22 | // string private_addr = 4; 23 | int32 wg_port = 5; 24 | string allowed_ip = 6; 25 | } 26 | -------------------------------------------------------------------------------- /pkg/proto/api_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc (unknown) 5 | // source: pkg/proto/api.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | emptypb "google.golang.org/protobuf/types/known/emptypb" 15 | ) 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the grpc package it is being compiled against. 19 | // Requires gRPC-Go v1.32.0 or later. 20 | const _ = grpc.SupportPackageIsVersion7 21 | 22 | // AdminServiceClient is the client API for AdminService service. 23 | // 24 | // 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. 25 | type AdminServiceClient interface { 26 | NodeInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*NodeInfoResponse, error) 27 | } 28 | 29 | type adminServiceClient struct { 30 | cc grpc.ClientConnInterface 31 | } 32 | 33 | func NewAdminServiceClient(cc grpc.ClientConnInterface) AdminServiceClient { 34 | return &adminServiceClient{cc} 35 | } 36 | 37 | func (c *adminServiceClient) NodeInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*NodeInfoResponse, error) { 38 | out := new(NodeInfoResponse) 39 | err := c.cc.Invoke(ctx, "/ikto.AdminService/NodeInfo", in, out, opts...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return out, nil 44 | } 45 | 46 | // AdminServiceServer is the server API for AdminService service. 47 | // All implementations should embed UnimplementedAdminServiceServer 48 | // for forward compatibility 49 | type AdminServiceServer interface { 50 | NodeInfo(context.Context, *emptypb.Empty) (*NodeInfoResponse, error) 51 | } 52 | 53 | // UnimplementedAdminServiceServer should be embedded to have forward compatible implementations. 54 | type UnimplementedAdminServiceServer struct { 55 | } 56 | 57 | func (UnimplementedAdminServiceServer) NodeInfo(context.Context, *emptypb.Empty) (*NodeInfoResponse, error) { 58 | return nil, status.Errorf(codes.Unimplemented, "method NodeInfo not implemented") 59 | } 60 | 61 | // UnsafeAdminServiceServer may be embedded to opt out of forward compatibility for this service. 62 | // Use of this interface is not recommended, as added methods to AdminServiceServer will 63 | // result in compilation errors. 64 | type UnsafeAdminServiceServer interface { 65 | mustEmbedUnimplementedAdminServiceServer() 66 | } 67 | 68 | func RegisterAdminServiceServer(s grpc.ServiceRegistrar, srv AdminServiceServer) { 69 | s.RegisterService(&AdminService_ServiceDesc, srv) 70 | } 71 | 72 | func _AdminService_NodeInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 73 | in := new(emptypb.Empty) 74 | if err := dec(in); err != nil { 75 | return nil, err 76 | } 77 | if interceptor == nil { 78 | return srv.(AdminServiceServer).NodeInfo(ctx, in) 79 | } 80 | info := &grpc.UnaryServerInfo{ 81 | Server: srv, 82 | FullMethod: "/ikto.AdminService/NodeInfo", 83 | } 84 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 85 | return srv.(AdminServiceServer).NodeInfo(ctx, req.(*emptypb.Empty)) 86 | } 87 | return interceptor(ctx, in, info, handler) 88 | } 89 | 90 | // AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service. 91 | // It's only intended for direct use with grpc.RegisterService, 92 | // and not to be introspected or modified (even as a copy) 93 | var AdminService_ServiceDesc = grpc.ServiceDesc{ 94 | ServiceName: "ikto.AdminService", 95 | HandlerType: (*AdminServiceServer)(nil), 96 | Methods: []grpc.MethodDesc{ 97 | { 98 | MethodName: "NodeInfo", 99 | Handler: _AdminService_NodeInfo_Handler, 100 | }, 101 | }, 102 | Streams: []grpc.StreamDesc{}, 103 | Metadata: "pkg/proto/api.proto", 104 | } 105 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/valyentdev/ikto/pkg/ikto" 8 | "github.com/valyentdev/ikto/pkg/proto" 9 | "google.golang.org/grpc" 10 | "google.golang.org/protobuf/types/known/emptypb" 11 | ) 12 | 13 | func StartAdminServer(ctx context.Context, ikto ikto.Ikto, socket string) error { 14 | s := &server{ 15 | ikto: ikto, 16 | } 17 | 18 | ln, err := net.Listen("unix", socket) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | grpcServer := grpc.NewServer() 24 | 25 | proto.RegisterAdminServiceServer(grpcServer, s) 26 | 27 | go func() { 28 | <-ctx.Done() 29 | grpcServer.GracefulStop() 30 | }() 31 | 32 | if err := grpcServer.Serve(ln); err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | type server struct { 40 | ikto ikto.Ikto 41 | } 42 | 43 | // NodeInfo implements proto.AdminServiceServer. 44 | func (s *server) NodeInfo(context.Context, *emptypb.Empty) (*proto.NodeInfoResponse, error) { 45 | self := s.ikto.Self() 46 | peers := s.ikto.Peers() 47 | 48 | peersProto := make([]*proto.Peer, 0, len(peers)) 49 | 50 | for _, peer := range peers { 51 | peersProto = append(peersProto, &proto.Peer{ 52 | Name: peer.Name, 53 | PublicKey: peer.PublicKey.String(), 54 | AdvertiseAddr: peer.AdvertiseAddress, 55 | AllowedIp: peer.AllowedIP, 56 | WgPort: int32(peer.WGPort), 57 | }) 58 | } 59 | 60 | return &proto.NodeInfoResponse{ 61 | Self: &proto.Peer{ 62 | Name: self.Name, 63 | PublicKey: self.PublicKey.String(), 64 | AdvertiseAddr: self.AdvertiseAddress, 65 | AllowedIp: self.AllowedIP, 66 | WgPort: int32(self.WGPort), 67 | }, 68 | Peers: peersProto, 69 | }, nil 70 | 71 | } 72 | 73 | var _ proto.AdminServiceServer = (*server)(nil) 74 | -------------------------------------------------------------------------------- /pkg/types/peer.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 8 | ) 9 | 10 | type Peer struct { 11 | Name string `json:"name"` 12 | PublicKey PublicKey `json:"public_key"` 13 | AdvertiseAddress string `json:"advertise_address"` 14 | AllowedIP string `json:"allowed_ip"` 15 | WGPort int `json:"wg_port"` 16 | } 17 | 18 | func (p *Peer) WGPeerConfig() (wgtypes.PeerConfig, error) { 19 | _, ipnet, err := net.ParseCIDR(p.AllowedIP) 20 | if err != nil { 21 | return wgtypes.PeerConfig{}, err 22 | } 23 | 24 | advertiseAddress := net.ParseIP(p.AdvertiseAddress) 25 | if advertiseAddress == nil { 26 | return wgtypes.PeerConfig{}, fmt.Errorf("invalid public address: %s", p.AdvertiseAddress) 27 | } 28 | 29 | return wgtypes.PeerConfig{ 30 | PublicKey: p.PublicKey.WG(), 31 | Endpoint: &net.UDPAddr{ 32 | IP: advertiseAddress, 33 | Port: p.WGPort, 34 | }, 35 | AllowedIPs: []net.IPNet{*ipnet}, 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/types/public_key.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "golang.zx2c4.com/wireguard/wgctrl/wgtypes" 7 | ) 8 | 9 | type PublicKey [32]byte 10 | 11 | var _ json.Marshaler = (*PublicKey)(nil) 12 | var _ json.Unmarshaler = (*PublicKey)(nil) 13 | 14 | func (k *PublicKey) String() string { 15 | return k.WG().String() 16 | } 17 | 18 | func (k PublicKey) WG() wgtypes.Key { 19 | return wgtypes.Key(k) 20 | } 21 | 22 | func (n PublicKey) MarshalJSON() ([]byte, error) { 23 | return json.Marshal(n.String()) 24 | } 25 | 26 | func (n *PublicKey) UnmarshalJSON(data []byte) error { 27 | var s string 28 | if err := json.Unmarshal(data, &s); err != nil { 29 | return err 30 | } 31 | key, err := ParseKey(s) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | *n = key 37 | return nil 38 | } 39 | 40 | func ParseKey(s string) (PublicKey, error) { 41 | key, err := wgtypes.ParseKey(s) 42 | if err != nil { 43 | return PublicKey{}, err 44 | } 45 | 46 | return PublicKey(key), nil 47 | } 48 | --------------------------------------------------------------------------------