├── .gitignore ├── .semver ├── LICENSE ├── Makefile ├── README.md ├── app ├── nx-cli │ ├── installer │ │ ├── installer.go │ │ ├── installer_darwin.go │ │ ├── installer_helpers.go │ │ ├── installer_helpers_test.go │ │ ├── installer_linux.go │ │ └── installer_windows.go │ └── main.go ├── nx-daemon │ ├── config │ │ ├── config.go │ │ ├── config_darwin.go │ │ ├── config_linux.go │ │ └── config_windows.go │ ├── daemon │ │ └── daemon.go │ ├── main.go │ └── webserver │ │ ├── api │ │ └── api.go │ │ └── webserver.go └── nx-server │ ├── main.go │ └── runtime │ └── k8s │ ├── probe.go │ └── runtime.go ├── business ├── caroot │ └── caroot.go ├── netmux │ ├── agent.go │ ├── model.go │ ├── service.go │ └── service_test.go ├── networkallocator │ ├── dnsallocator │ │ ├── dnsallocator.go │ │ ├── dnsallocator_linux.go │ │ ├── dnsallocator_windows.go │ │ └── dsnallocator_darwin.go │ ├── ipallocator │ │ ├── cidrcalc.go │ │ ├── cidrcalc_test.go │ │ └── ipallocator.go │ └── networkallocator.go ├── portforwarder │ └── portforwarder.go └── shell │ ├── cmd_all.go │ ├── cmd_darwin.go │ ├── cmd_linux.go │ ├── cmd_windows.go │ └── cmd_windows_test.go ├── foundation ├── buildinfo │ ├── build-date │ ├── build-hash │ ├── build-semver │ └── buildinfo.go ├── memstore │ └── memstore.go ├── metrics │ └── metrics.go ├── pipe │ └── pipe.go └── wire │ ├── wire.go │ └── wire_test.go ├── go.mod ├── go.sum └── zarf ├── docker ├── helpers │ └── sample-service │ │ └── Dockerfile └── netmux │ └── Dockerfile ├── docs ├── diagrams │ ├── arch-direct-conn.puml │ ├── arch.puml │ ├── bridge.puml │ ├── comm-state.puml │ ├── config.puml │ ├── control-conn.puml │ ├── proxy.puml │ ├── rev-proxy.puml │ └── wireprotocol.puml ├── dux-netmux.png ├── gophercon-br-2023-presentation │ └── Apresentação GopherCon BR _ 2023 - Netmux.pdf └── guides │ ├── add-trusted.png │ ├── install.md │ ├── nssm.png │ ├── nw-conns.png │ └── using-kubernetes.md ├── grafana-dash └── Netmux - Overview-1687464467415.json ├── manifests ├── grafana │ ├── all.yaml │ └── kustomization.yaml ├── kafka │ ├── kafka-ui.yaml │ ├── kafka.yaml │ └── kustomization.yaml ├── kustomization.yaml ├── netmux │ ├── kustomization.yaml │ ├── namespace.yaml │ ├── netmux-deployment-namespace.yaml │ ├── netmux-service.yaml │ ├── rbac-role-binding.yaml │ ├── rbac-role.yaml │ └── rbac-service-account.yaml ├── postgres │ ├── all.yaml │ └── kustomization.yaml ├── prometheus │ ├── config.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ ├── pvc.yaml │ ├── rbac.yaml │ └── service.yaml └── sample-svc │ ├── kustomization.yaml │ ├── rev-test-pod.yaml │ ├── sample-deployment.yaml │ ├── sample-revservice.yaml │ └── sample-service.yaml └── sample-apps ├── kcons └── main.go ├── kprod └── main.go ├── sample-service-reverse └── main.go ├── sample-service └── main.go └── tun-sample └── tun-sample.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | .idea 11 | .vscode 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | /app/test 24 | /stage 25 | /zarf/docker/nx-server/service 26 | /zarf/docker/netmux/bin/ 27 | /zarf/dist 28 | /zarf/docker/helpers/sample-service/bin/linux/amd64/service 29 | /zarf/docker/helpers/sample-service/bin/linux/arm64/service 30 | -------------------------------------------------------------------------------- /.semver: -------------------------------------------------------------------------------- 1 | 0.1.4 -------------------------------------------------------------------------------- /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 | DT := $(shell date +%Y.%m.%d.%H.%M.%S) 2 | HASH := $(shell git rev-parse HEAD) 3 | USER := $(shell whoami) 4 | SEMVER := $(shell cat .semver) 5 | KUBECONFIG := ~/.kube/config 6 | name := netmux 7 | 8 | version: 9 | echo $(DT) > ./foundation/buildinfo/build-date 10 | cp .semver ./foundation/buildinfo/build-semver 11 | git rev-parse HEAD > ./foundation/buildinfo/build-hash 12 | 13 | lint: 14 | find . -name '*.go' | xargs -I{} gofumpt -w {} 15 | golangci-lint run ./... 16 | 17 | test: 18 | go test ./... 19 | 20 | test-race: 21 | go test -race ./... 22 | 23 | docker-img-local-amd64: version 24 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ./zarf/docker/netmux/bin/linux/amd64/$(name) ./app/nx-server 25 | 26 | - docker rmi -f duxthemux/$(name):latest 27 | docker build -f ./zarf/docker/netmux/Dockerfile -t duxthemux/$(name):latest --platform=linux/arm64 . --load 28 | 29 | 30 | docker-img-local-arm64: version 31 | GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ./zarf/docker/netmux/bin/linux/arm64/$(name) ./app/nx-server 32 | 33 | - docker rmi -f duxthemux/$(name):latest 34 | docker build -f ./zarf/docker/netmux/Dockerfile -t duxthemux/$(name):latest --platform=linux/arm64 . --load 35 | 36 | 37 | docker-img: version 38 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ./zarf/docker/netmux/bin/linux/amd64/$(name) ./app/nx-server 39 | GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ./zarf/docker/netmux/bin/linux/arm64/$(name) ./app/nx-server 40 | 41 | upx --best --lzma ./zarf/docker/netmux/bin/linux/amd64/$(name) 42 | upx --best --lzma ./zarf/docker/netmux/bin/linux/arm64/$(name) 43 | 44 | - docker rmi -f duxthemux/$(name):latest 45 | docker buildx build -f ./zarf/docker/netmux/Dockerfile -t duxthemux/$(name):latest --platform=linux/arm64,linux/amd64 . --push 46 | 47 | my-bins: 48 | go build -ldflags="-s -w" -o zarf/dist/nx ./app/nx-cli 49 | go build -ldflags="-s -w" -o zarf/dist/nx-daemon ./app/nx-daemon 50 | 51 | dist-bins: 52 | GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o zarf/dist/darwin_arm64/nx ./app/nx-cli 53 | GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o zarf/dist/darwin_arm64/nx-daemon ./app/nx-daemon 54 | 55 | GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o zarf/dist/darwin_amd64/nx ./app/nx-cli 56 | GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o zarf/dist/darwin_amd64/nx-daemon ./app/nx-daemon 57 | 58 | GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o zarf/dist/linux_arm64/nx ./app/nx-cli 59 | GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o zarf/dist/linux_arm64/nx-daemon ./app/nx-daemon 60 | 61 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o zarf/dist/linux_amd64/nx ./app/nx-cli 62 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o zarf/dist/linux_amd64/nx-daemon ./app/nx-daemon 63 | 64 | GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o zarf/dist/windows_amd64/nx.exe ./app/nx-cli 65 | GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o zarf/dist/windows_amd64/nx-daemon.exe ./app/nx-daemon 66 | 67 | tar czvfp zarf/dist/netmuxcli-$(SEMVER)-darwin_arm64.tgz -C zarf/dist/darwin_arm64 . 68 | tar czvfp zarf/dist/netmuxcli-$(SEMVER)-darwin_amd64.tgz -C zarf/dist/darwin_amd64 . 69 | tar czvfp zarf/dist/netmuxcli-$(SEMVER)-linux_arm64.tgz -C zarf/dist/linux_arm64 . 70 | tar czvfp zarf/dist/netmuxcli-$(SEMVER)-linux_amd64.tgz -C zarf/dist/linux_amd64 . 71 | cd zarf/dist/windows_amd64 && zip -r ../netmuxcli-$(SEMVER)-windows_amd64.zip . && cd - 72 | # ------------- 73 | docker-init-buildx: 74 | docker buildx create --use 75 | 76 | sample-server: 77 | GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ./zarf/docker/helpers/sample-service/bin/linux/amd64/service ./zarf/sample-apps/sample-service 78 | GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ./zarf/docker/helpers/sample-service/bin/linux/arm64/service ./zarf/sample-apps/sample-service 79 | 80 | upx --best --lzma ./zarf/docker/helpers/sample-service/bin/linux/amd64/service 81 | upx --best --lzma ./zarf/docker/helpers/sample-service/bin/linux/arm64/service 82 | 83 | 84 | - docker rmi -f duxthemux/sample-service:latest 85 | docker buildx build -f ./zarf/docker/helpers/sample-service/Dockerfile -t duxthemux/sample-service:latest --platform=linux/arm64,linux/amd64 . --push -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netmux 2 | 3 | ![Dux the King of Mux](zarf/docs/dux-netmux.png "Dux - The King of Mux") 4 | 5 | Dux - the King of Mux 6 | 7 | Logo art by Ceci - Cecilia Simão / Grande - Marcelo Grandioso (Gaudioso) 8 | 9 | This is netmux, a network multiplexer and meshing platform. 10 | It was designed to make your life easier and more enjoyable when 11 | developing your container based services and/or doing work on remote infra. 12 | 13 | We plan to support bare-metal shortly - and other use cases will come. 14 | 15 | For installation instructions, please refer to the [installation guide.](./zarf/docs/guides/install.md) 16 | 17 | For Kubernetes usage, please refer to the [using kubernetes guide.](./zarf/docs/guides/using-kubernetes.md) 18 | 19 | [Introductory documentation: Presented at Gophercon Brazil 2023 (PT-BR only at the moment)](./zarf/docs/gophercon-br-2023-presentation/Apresentação%20GopherCon%20BR%20_%202023%20-%20Netmux.pdf) 20 | 21 | ## Sponsors 22 | 23 | Digital Circle - https://www.digitalcircle.com.br -------------------------------------------------------------------------------- /app/nx-cli/installer/installer.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type Installer struct{} 12 | 13 | func (i *Installer) Install() error { 14 | return install() 15 | } 16 | 17 | func (i *Installer) Uninstall() error { 18 | return uninstall() 19 | } 20 | 21 | func New() *Installer { 22 | return &Installer{} 23 | } 24 | 25 | func execCmd(step string, cmd []string) func() error { 26 | return func() error { 27 | log.Printf("Executing %s: %s", step, strings.Join(cmd, " ")) 28 | bs, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() 29 | if err != nil { 30 | return fmt.Errorf("could execute %s: %s, %w", step, string(bs), err) 31 | } 32 | return nil 33 | } 34 | } 35 | 36 | func execCmdSleep(step string, cmd []string, n int) func() error { 37 | return func() error { 38 | bs, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() 39 | if err != nil { 40 | return fmt.Errorf("could execute %s: %s, %w", step, string(bs), err) 41 | } 42 | 43 | time.Sleep(time.Second * time.Duration(n)) 44 | return nil 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/nx-cli/installer/installer_darwin.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | const samplePlistContents = ` 15 | 16 | 17 | 18 | EnvironmentVariables 19 | 20 | PATH 21 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin: 22 | 23 | UserName 24 | root 25 | GroupName 26 | wheel 27 | Label 28 | nx-daemon 29 | Program 30 | $USERHOME/.nx/nx-daemon 31 | RunAtLoad 32 | 33 | KeepAlive 34 | 35 | LaunchOnlyOnce 36 | 37 | StandardOutPath 38 | /tmp/nx-daemon.stdout 39 | StandardErrorPath 40 | /tmp/nx-daemon.stderr 41 | WorkingDirectory 42 | $USERHOME/.nx 43 | 44 | ` 45 | 46 | const sampleConfig = `network: 10.0.2.0/24 47 | endpoints: 48 | - name: netmux 49 | endpoint: netmux:50000 50 | kubernetes: 51 | config: ${USERHOME}/.kube/config 52 | namespace: netmux 53 | endpoint: netmux # netmux 54 | port: 50000 55 | context: default 56 | # user: ${USER} 57 | # kubectl: /path/to/kubectl 58 | ` 59 | 60 | func install() error { 61 | userName := os.Getenv("SUDO_USER") 62 | if userName == "" { 63 | return fmt.Errorf("could not find SUDO_USER") 64 | } 65 | 66 | myUser, err := user.Lookup(userName) 67 | if err != nil { 68 | return fmt.Errorf("could not find os user for %s: %w", userName, err) 69 | } 70 | 71 | dirName := filepath.Join(myUser.HomeDir, ".nx") 72 | 73 | steps := []func() error{ 74 | execCmd("fix nx folder perms", []string{"chmod", "a+wrx", dirName}), 75 | execCmd("copy daemon", []string{"cp", "./nx-daemon", dirName}), 76 | execCmd("fix daemon perms", []string{"chmod", "u+x", filepath.Join(dirName, "nx-daemon")}), 77 | execCmd("copy nx", []string{"cp", "./nx", dirName}), 78 | execCmd("fix nx perms", []string{"chmod", "a+x", filepath.Join(dirName, "nx")}), 79 | execCmd("fix perms plist", []string{"chmod", "a+r", "/Library/LaunchDaemons/nx-daemon.plist"}), 80 | execCmdSleep("launch daemon", []string{"launchctl", "load", "-w", "/Library/LaunchDaemons/nx-daemon.plist"}, 5), 81 | execCmd("fix perms ca.cer", []string{"chmod", "a+r", filepath.Join(dirName, "ca.cer")}), 82 | execCmd("fix perms ca.key", []string{"chmod", "a+r", filepath.Join(dirName, "ca.key")}), 83 | execCmd("allow cert import", []string{"security", "authorizationdb", "write", "com.apple.trust-settings.admin", "allow"}), 84 | execCmd("install cert", []string{"security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", filepath.Join(dirName, "ca.cer")}), 85 | execCmd("deny cert import", []string{"security", "authorizationdb", "remove", "com.apple.trust-settings.admin"}), 86 | } 87 | 88 | _, err = os.Stat(dirName) 89 | if err == nil || !errors.Is(err, os.ErrNotExist) { 90 | return fmt.Errorf("dir %s already exists - cant continue", dirName) 91 | } 92 | 93 | err = os.Mkdir(dirName, 0o600) 94 | if err != nil { 95 | return fmt.Errorf("could not create folder %s: %w", dirName, err) 96 | } 97 | 98 | configContent := strings.ReplaceAll(sampleConfig, "$USER", userName) 99 | configContent = strings.ReplaceAll(configContent, "$USERHOME", myUser.HomeDir) 100 | 101 | err = os.WriteFile(filepath.Join(dirName, "netmux.yaml"), []byte(configContent), 0o600) 102 | if err != nil { 103 | return fmt.Errorf("error creating netmux config file") 104 | } 105 | 106 | plistContent := strings.ReplaceAll(samplePlistContents, "$USERHOME", myUser.HomeDir) 107 | 108 | err = os.WriteFile("/Library/LaunchDaemons/nx-daemon.plist", []byte(plistContent), 0o600) 109 | if err != nil { 110 | return fmt.Errorf("could not write plist file: %w", err) 111 | } 112 | 113 | for _, step := range steps { 114 | err := step() 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | 120 | log.Printf("Installation finished, please add the %s to your PATH", dirName) 121 | 122 | return nil 123 | } 124 | 125 | func uninstall() error { 126 | userName := os.Getenv("SUDO_USER") 127 | if userName == "" { 128 | return fmt.Errorf("could not find SUDO_USER") 129 | } 130 | 131 | myUser, err := user.Lookup(userName) 132 | if err != nil { 133 | return fmt.Errorf("could not find os user for %s: %w", userName, err) 134 | } 135 | 136 | dirName := filepath.Join(myUser.HomeDir, ".nx") 137 | 138 | bs, err := exec.Command("launchctl", "unload", "-w", "/Library/LaunchDaemons/nx-daemon.plist").CombinedOutput() 139 | if err != nil { 140 | return fmt.Errorf("error loading plist file: %s, %w", string(bs), err) 141 | } 142 | 143 | if err = os.RemoveAll(dirName); err != nil { 144 | return fmt.Errorf("could not delete .nx folder: %w", err) 145 | } 146 | 147 | if err = os.Remove("/Library/LaunchDaemons/nx-daemon.plist"); err != nil { 148 | return fmt.Errorf("could not remove plist file: %w", err) 149 | } 150 | 151 | log.Printf("NX uninstalled correctly.") 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /app/nx-cli/installer/installer_helpers.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import "strings" 4 | 5 | func GetLinuxId(s string) string { 6 | lines := strings.Split(s, "\n") 7 | 8 | for _, line := range lines { 9 | parts := strings.Split(line, "=") 10 | if parts[0] == "ID" { 11 | return parts[1] 12 | } 13 | } 14 | 15 | return "" 16 | } 17 | 18 | func GetLinuxIdLike(s string) string { 19 | lines := strings.Split(s, "\n") 20 | 21 | for _, line := range lines { 22 | parts := strings.Split(line, "=") 23 | if parts[0] == "ID_LIKE" { 24 | return parts[1] 25 | } 26 | } 27 | 28 | return "" 29 | } 30 | -------------------------------------------------------------------------------- /app/nx-cli/installer/installer_helpers_test.go: -------------------------------------------------------------------------------- 1 | package installer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/duxthemux/netmux/app/nx-cli/installer" 9 | ) 10 | 11 | const alpineOsRelease = `NAME="Alpine Linux" 12 | ID=alpine 13 | VERSION_ID=3.18.3 14 | PRETTY_NAME="Alpine Linux v3.18" 15 | HOME_URL="https://alpinelinux.org/" 16 | BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"` 17 | 18 | const ubuntuOsRelease = `PRETTY_NAME="Ubuntu 23.04" 19 | NAME="Ubuntu" 20 | VERSION_ID="23.04" 21 | VERSION="23.04 (Lunar Lobster)" 22 | VERSION_CODENAME=lunar 23 | ID=ubuntu 24 | ID_LIKE=debian 25 | HOME_URL="https://www.ubuntu.com/" 26 | SUPPORT_URL="https://help.ubuntu.com/" 27 | BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" 28 | PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" 29 | UBUNTU_CODENAME=lunar 30 | LOGO=ubuntu-logo` 31 | 32 | const brokenUbuntuOsRelease = `PRETTY_NAME="Ubuntu 23.04" 33 | NAME="Ubuntu" 34 | VERSION_ID="23.04" 35 | VERSION="23.04 (Lunar Lobster)" 36 | VERSION_CODENAME=lunar 37 | _ID=ubuntu 38 | ID_LIKE=debian 39 | HOME_URL="https://www.ubuntu.com/" 40 | SUPPORT_URL="https://help.ubuntu.com/" 41 | BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" 42 | PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" 43 | UBUNTU_CODENAME=lunar 44 | LOGO=ubuntu-logo` 45 | 46 | func TestGetLinuxId(t *testing.T) { 47 | id := installer.GetLinuxId(ubuntuOsRelease) 48 | require.Equal(t, "ubuntu", id) 49 | id = installer.GetLinuxIdLike(ubuntuOsRelease) 50 | require.Equal(t, "debian", id) 51 | id = installer.GetLinuxId(alpineOsRelease) 52 | require.Equal(t, "alpine", id) 53 | id = installer.GetLinuxId(brokenUbuntuOsRelease) 54 | require.Equal(t, "", id) 55 | } 56 | -------------------------------------------------------------------------------- /app/nx-cli/installer/installer_linux.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | const sampleSystemDUnit = `[Unit] 14 | Description=NX Daemon 15 | 16 | [Service] 17 | Type=simple 18 | User=root 19 | Restart=always 20 | WorkingDirectory=/srv/nx 21 | ExecStart=/srv/nx/nx-daemon 22 | 23 | [Install] 24 | WantedBy=multi-user.target` 25 | 26 | const sampleConfig = `network: 10.0.2.0/24 27 | endpoints: 28 | - name: netmux 29 | endpoint: netmux:50000 30 | kubernetes: 31 | config: ${USERHOME}/.kube/config 32 | namespace: netmux 33 | endpoint: netmux # netmux 34 | port: 50000 35 | context: default 36 | # user: ${USER} 37 | # kubectl: /path/to/kubectl 38 | ` 39 | 40 | func install() error { 41 | userName := os.Getenv("SUDO_USER") 42 | if userName == "" { 43 | return fmt.Errorf("could not find SUDO_USER") 44 | } 45 | 46 | myUser, err := user.Lookup(userName) 47 | if err != nil { 48 | return fmt.Errorf("could not find os user for %s: %w", userName, err) 49 | } 50 | 51 | osReleaseBytes, err := os.ReadFile("/etc/os-release") 52 | if err != nil { 53 | return fmt.Errorf("error retrieving release: %w", err) 54 | } 55 | 56 | linuxId := GetLinuxId(string(osReleaseBytes)) 57 | linuxLikeId := GetLinuxIdLike(string(osReleaseBytes)) 58 | 59 | dirName := "/srv/nx" 60 | 61 | steps := []func() error{ 62 | execCmd("fix nx folder perms", []string{"chmod", "a+wrx", dirName}), 63 | execCmd("copy daemon", []string{"cp", "./nx-daemon", dirName}), 64 | execCmd("fix daemon perms", []string{"chmod", "u+x", filepath.Join(dirName, "nx-daemon")}), 65 | execCmd("copy nx", []string{"cp", "./nx", dirName}), 66 | execCmd("fix nx perms", []string{"chmod", "a+x", filepath.Join(dirName, "nx")}), 67 | execCmd("reload daemon", []string{"systemctl", "daemon-reload"}), 68 | execCmd("enable service", []string{"systemctl", "enable", "nx-daemon.service"}), 69 | execCmdSleep("enable service", []string{"systemctl", "start", "nx-daemon.service"}, 5), 70 | 71 | execCmd("fix perms ca.cer", []string{"chmod", "a+r", filepath.Join(dirName, "ca.cer")}), 72 | execCmd("fix perms ca.key", []string{"chmod", "a+r", filepath.Join(dirName, "ca.key")}), 73 | } 74 | 75 | switch { 76 | case linuxLikeId == "debian": 77 | steps = append(steps, execCmd("copy cert to trusted store", []string{"cp", filepath.Join(dirName, "ca.cer"), "/usr/local/share/ca-certificates/netmux.crt"})) 78 | steps = append(steps, execCmd("update certificate cache", []string{"update-ca-certificates"})) 79 | case linuxId == "fedora": 80 | steps = append(steps, execCmd("copy cert to trusted store", []string{"cp", filepath.Join(dirName, "ca.cer"), "/etc/pki/ca-trust/source/anchors/netmux.crt"})) 81 | steps = append(steps, execCmd("update certificate cache", []string{"update-ca-trust"})) 82 | default: 83 | return fmt.Errorf("linux distro not known: (%s/%s)", linuxId, linuxLikeId) 84 | } 85 | 86 | _, err = os.Stat(dirName) 87 | if err == nil || !errors.Is(err, os.ErrNotExist) { 88 | return fmt.Errorf("dir %s already exists - cant continue", dirName) 89 | } 90 | 91 | err = os.Mkdir(dirName, 0o600) 92 | if err != nil { 93 | return fmt.Errorf("could not create folder %s: %w", dirName, err) 94 | } 95 | 96 | err = os.WriteFile("/etc/systemd/system/nx-daemon.service", []byte(sampleSystemDUnit), 0o600) 97 | if err != nil { 98 | return fmt.Errorf("error generating service unit: %w", err) 99 | } 100 | 101 | configContent := strings.ReplaceAll(sampleConfig, "$USER", userName) 102 | configContent = strings.ReplaceAll(configContent, "$USERHOME", myUser.HomeDir) 103 | 104 | err = os.WriteFile(filepath.Join(dirName, "netmux.yaml"), []byte(configContent), 0o600) 105 | if err != nil { 106 | return fmt.Errorf("error creating netmux config file") 107 | } 108 | 109 | for _, step := range steps { 110 | err := step() 111 | if err != nil { 112 | return err 113 | } 114 | } 115 | 116 | log.Printf("Installation finished, please add the %s to your PATH", dirName) 117 | 118 | return nil 119 | } 120 | 121 | func uninstall() error { 122 | userName := os.Getenv("SUDO_USER") 123 | if userName == "" { 124 | return fmt.Errorf("could not find SUDO_USER") 125 | } 126 | 127 | steps := []func() error{ 128 | execCmd("disable daemon", []string{"systemctl", "stop", "nx-daemon.service"}), 129 | execCmd("disable daemon", []string{"systemctl", "disable", "nx-daemon.service"}), 130 | execCmd("clean up config file", []string{"rm", "/etc/systemd/system/nx-daemon.service"}), 131 | execCmd("clean up files", []string{"rm", "-rf", "/srx/nx"}), 132 | } 133 | 134 | for _, step := range steps { 135 | err := step() 136 | if err != nil { 137 | return err 138 | } 139 | } 140 | 141 | log.Printf("NX uninstalled correctly.") 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /app/nx-cli/installer/installer_windows.go: -------------------------------------------------------------------------------- 1 | package installer 2 | 3 | func install() error { 4 | panic("implement me") 5 | } 6 | 7 | func uninstall() error { 8 | panic("implement me") 9 | } 10 | -------------------------------------------------------------------------------- /app/nx-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "regexp" 13 | "slices" 14 | "strings" 15 | 16 | "github.com/jedib0t/go-pretty/v6/table" 17 | "github.com/urfave/cli/v2" 18 | 19 | "github.com/duxthemux/netmux/app/nx-cli/installer" 20 | ) 21 | 22 | type Endpoint struct { 23 | Name string `json:"name"` 24 | Endpoint string `json:"endpoint"` 25 | Kubernetes struct { 26 | Config string `json:"config"` 27 | Namespace string `json:"namespace"` 28 | Endpoint string `json:"endpoint"` 29 | Context string `json:"context"` 30 | Port string `json:"port"` 31 | } `json:"kubernetes"` 32 | Status string `json:"status"` 33 | Bridges []struct { 34 | Namespace string `json:"namespace"` 35 | Name string `json:"name"` 36 | LocalAddr string `json:"localAddr"` 37 | LocalPort string `json:"localPort"` 38 | ContainerAddr string `json:"containerAddr"` 39 | ContainerPort string `json:"containerPort"` 40 | Direction string `json:"direction"` 41 | Family string `json:"family"` 42 | Status string `json:"status"` 43 | } `json:"bridges"` 44 | } 45 | 46 | type ListRow struct { 47 | Type string 48 | Name string 49 | Parent string 50 | Desc string 51 | Status string 52 | } 53 | 54 | func (l *ListRow) String() string { 55 | if l.Type == "SVC" { 56 | return fmt.Sprintf("%s.%s", l.Parent, l.Name) 57 | } 58 | 59 | return l.Name 60 | } 61 | 62 | type ListOutput struct { 63 | Endpoints []Endpoint `json:"endpoints"` 64 | } 65 | 66 | func httpGet(ctx context.Context, cli http.Client, url string) ([]byte, error) { 67 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 68 | if err != nil { 69 | return nil, fmt.Errorf("error creating request: %w", err) 70 | } 71 | 72 | res, err := cli.Do(req) 73 | if err != nil { 74 | return nil, fmt.Errorf("error calling http endpoint: %w", err) 75 | } 76 | 77 | defer func() { 78 | _ = res.Body.Close() 79 | }() 80 | 81 | buf := &bytes.Buffer{} 82 | 83 | _, err = io.Copy(buf, res.Body) 84 | if err != nil { 85 | return nil, fmt.Errorf("error reading response: %w", err) 86 | } 87 | 88 | if res.StatusCode != http.StatusOK { 89 | return nil, fmt.Errorf("error receiving response: %s - %s", res.Status, buf.String()) 90 | } 91 | 92 | return buf.Bytes(), nil 93 | } 94 | 95 | func list(ctx context.Context, cli http.Client, filter string) error { 96 | 97 | responseBytes, err := httpGet(ctx, cli, "https://nx/api/v1/services/") 98 | if err != nil { 99 | return err 100 | } 101 | 102 | out := ListOutput{} 103 | if err = json.Unmarshal(responseBytes, &out); err != nil { 104 | return fmt.Errorf("error unmarshalling status: %w", err) 105 | } 106 | 107 | var rx *regexp.Regexp 108 | 109 | if filter != "" { 110 | filter = strings.ReplaceAll(filter, "+", ".*") 111 | rx, err = regexp.Compile(filter) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | 117 | rows := make([]ListRow, 0) 118 | for _, endpoint := range out.Endpoints { 119 | row := ListRow{ 120 | Type: "EP", 121 | Name: endpoint.Name, 122 | Parent: "--", 123 | Desc: fmt.Sprintf( 124 | "%s %s.%s:%s", 125 | endpoint.Kubernetes.Context, 126 | endpoint.Kubernetes.Namespace, 127 | endpoint.Kubernetes.Endpoint, 128 | endpoint.Kubernetes.Port), 129 | Status: endpoint.Status, 130 | } 131 | 132 | if rx != nil { 133 | if rx.MatchString(row.String()) { 134 | rows = append(rows, row) 135 | } 136 | } else { 137 | rows = append(rows, row) 138 | } 139 | 140 | for _, svc := range endpoint.Bridges { 141 | 142 | row := ListRow{ 143 | Type: "SVC", 144 | Name: svc.Name, 145 | Parent: endpoint.Name, 146 | Desc: fmt.Sprintf( 147 | "%s %s: %s:%s => %s:%s", 148 | svc.Name, 149 | svc.Direction, 150 | svc.LocalAddr, 151 | svc.LocalPort, 152 | svc.ContainerAddr, 153 | svc.ContainerPort), 154 | Status: svc.Status, 155 | } 156 | 157 | if rx != nil { 158 | if rx.MatchString(row.String()) { 159 | rows = append(rows, row) 160 | } 161 | } else { 162 | rows = append(rows, row) 163 | } 164 | 165 | } 166 | } 167 | 168 | slices.SortFunc(rows, func(a, b ListRow) int { 169 | if a.Parent == b.Parent { 170 | return strings.Compare(a.Name, b.Name) 171 | } 172 | 173 | return strings.Compare(a.Parent, b.Parent) 174 | }) 175 | 176 | tbWriter := table.NewWriter() 177 | tbWriter.SetOutputMirror(os.Stdout) 178 | 179 | defer tbWriter.Render() 180 | 181 | tbWriter.AppendHeader(table.Row{"#", "K", "Name", "Parent", "Description", "Status"}) 182 | 183 | for i, row := range rows { 184 | tbWriter.AppendRow(table.Row{ 185 | fmt.Sprintf("%03d", i), 186 | row.Type, 187 | row.Name, 188 | row.Parent, 189 | row.Desc, 190 | row.Status, 191 | }) 192 | 193 | } 194 | 195 | return nil 196 | } 197 | 198 | func connect(ctx context.Context, cli http.Client, ctxName string) error { 199 | _, err := httpGet(ctx, cli, fmt.Sprintf("https://nx/api/v1/context/%s/connect", ctxName)) 200 | 201 | return err 202 | } 203 | 204 | func disconnect(ctx context.Context, cli http.Client, ctxName string) error { 205 | _, err := httpGet(ctx, cli, fmt.Sprintf("https://nx/api/v1/context/%s/disconnect", ctxName)) 206 | 207 | return err 208 | } 209 | 210 | func start(ctx context.Context, cli http.Client, ctxName string, svc string) error { 211 | _, err := httpGet(ctx, cli, fmt.Sprintf("https://nx/api/v1/services/%s/%s/start", ctxName, svc)) 212 | 213 | return err 214 | } 215 | 216 | func stop(ctx context.Context, cli http.Client, ctxName string, svc string) error { 217 | _, err := httpGet(ctx, cli, fmt.Sprintf("https://nx/api/v1/services/%s/%s/stop", ctxName, svc)) 218 | 219 | return err 220 | } 221 | 222 | func exit(ctx context.Context, cli http.Client) error { 223 | _, err := httpGet(ctx, cli, "https://nx/api/v1/misc/exit") 224 | 225 | return err 226 | } 227 | 228 | func reload(ctx context.Context, cli http.Client) error { 229 | _, err := httpGet(ctx, cli, "https://nx/api/v1/misc/reload") 230 | 231 | return err 232 | } 233 | 234 | func cleanup(ctx context.Context, cli http.Client) error { 235 | _, err := httpGet(ctx, cli, "https://nx/api/v1/misc/cleanup") 236 | 237 | return err 238 | } 239 | 240 | //nolint:funlen 241 | func main() { 242 | httpCli := http.Client{} 243 | ctx := context.Background() 244 | 245 | listFilter := "" 246 | 247 | app := &cli.App{ 248 | Name: "nx", 249 | Usage: "netmux command line client", 250 | Commands: []*cli.Command{ 251 | { 252 | Name: "install", 253 | Usage: "Install nx-daemon in your machine (call w root/admin) - Only for MAC ATM", 254 | Action: func(_ *cli.Context) error { 255 | myInstaller := installer.New() 256 | return myInstaller.Install() 257 | }, 258 | }, 259 | { 260 | Name: "uninstall", 261 | Usage: "Uninstall nx-daemon in your machine (call w root/admin) - Only for MAC ATM", 262 | Action: func(_ *cli.Context) error { 263 | myInstaller := installer.New() 264 | return myInstaller.Uninstall() 265 | }, 266 | }, 267 | { 268 | Name: "list", 269 | Aliases: []string{"ls", "l"}, 270 | Flags: []cli.Flag{ 271 | &cli.StringFlag{ 272 | Name: "filter", 273 | Category: "filter", 274 | DefaultText: "", 275 | FilePath: "", 276 | Usage: "", 277 | Required: false, 278 | Hidden: false, 279 | HasBeenSet: false, 280 | Value: "", 281 | Destination: &listFilter, 282 | Aliases: nil, 283 | EnvVars: nil, 284 | TakesFile: false, 285 | Action: nil, 286 | }, 287 | }, 288 | Usage: "Lists known info", 289 | Action: func(cCtx *cli.Context) error { 290 | return list(ctx, httpCli, listFilter) 291 | }, 292 | }, 293 | { 294 | Name: "connect", 295 | Aliases: []string{"con", "c"}, 296 | Usage: "Connects to Endpoint [endpoint]", 297 | Action: func(cCtx *cli.Context) error { 298 | return connect(ctx, httpCli, cCtx.Args().Get(0)) 299 | }, 300 | }, 301 | { 302 | Name: "disconnect", 303 | Aliases: []string{"dis", "d"}, 304 | Usage: "Disconnects from Endpoint [endpoint]", 305 | Action: func(cCtx *cli.Context) error { 306 | return disconnect(ctx, httpCli, cCtx.Args().Get(0)) 307 | }, 308 | }, 309 | { 310 | Name: "start", 311 | Aliases: []string{"on", "+"}, 312 | Usage: "Starts service [endpoint] [svc]", 313 | Action: func(cCtx *cli.Context) error { 314 | return start(ctx, httpCli, cCtx.Args().Get(0), cCtx.Args().Get(1)) 315 | }, 316 | }, 317 | { 318 | Name: "stop", 319 | Aliases: []string{"off", "-"}, 320 | Usage: "Stops service [endpoint] [svc]", 321 | Action: func(cCtx *cli.Context) error { 322 | return stop(ctx, httpCli, cCtx.Args().Get(0), cCtx.Args().Get(1)) 323 | }, 324 | }, 325 | { 326 | Name: "exit", 327 | Aliases: []string{}, 328 | Usage: "Stops the daemon", 329 | Action: func(cCtx *cli.Context) error { 330 | return exit(ctx, httpCli) 331 | }, 332 | }, 333 | { 334 | Name: "reload", 335 | Aliases: []string{}, 336 | Usage: "Reload the daemon config", 337 | Action: func(cCtx *cli.Context) error { 338 | return reload(ctx, httpCli) 339 | }, 340 | }, 341 | { 342 | Name: "cleanup", 343 | Aliases: []string{}, 344 | Usage: "Cleans dns entries", 345 | Action: func(cCtx *cli.Context) error { 346 | return cleanup(ctx, httpCli) 347 | }, 348 | }, 349 | }, 350 | 351 | Action: func(*cli.Context) error { 352 | return fmt.Errorf("unknown command") 353 | }, 354 | } 355 | 356 | if err := app.Run(os.Args); err != nil { 357 | log.Fatal(err) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /app/nx-daemon/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | 9 | "github.com/duxthemux/netmux/business/portforwarder" 10 | ) 11 | 12 | // Config represents the agent central userconfig file. 13 | type Config struct { 14 | Address string `json:"address" yaml:"address,omitempty"` 15 | Network string `json:"network" yaml:"network"` 16 | User string `json:"-" yaml:"user,omitempty"` 17 | Cert string `json:"cert" yaml:"cert,omitempty"` 18 | Key string `json:"key" yaml:"key,omitempty"` 19 | IFace string `json:"iface" yaml:"iface,omitempty"` 20 | LogLevel string `json:"logLevel" yaml:"logLevel,omitempty"` 21 | LogFormat string `json:"logFormat" yaml:"logFormat,omitempty"` 22 | Endpoints Endpoints `json:"endpoints" yaml:"endpoints,omitempty"` 23 | } 24 | 25 | func (c *Config) Load(fname string) error { 26 | if fname == "" { 27 | fname = DefaultConfigPath 28 | } 29 | 30 | if os.Getenv("CONFIG") != "" { 31 | fname = os.Getenv("CONFIG") 32 | } 33 | 34 | fileBytes, err := os.ReadFile(fname) 35 | if err != nil { 36 | return fmt.Errorf("error loading userconfig: %w", err) 37 | } 38 | 39 | err = yaml.Unmarshal(fileBytes, c) 40 | 41 | if err != nil { 42 | return fmt.Errorf("error unmashaling userconfig: %w", err) 43 | } 44 | 45 | if c.IFace == "" { 46 | c.IFace = DefaultIface 47 | } 48 | 49 | if c.Address == "" { 50 | c.Address = "localhost:50000" 51 | } 52 | 53 | if c.Network == "" { 54 | c.Network = "10.10.10.0/24" 55 | } 56 | 57 | return nil 58 | } 59 | 60 | type Endpoints []Endpoint 61 | 62 | func (e Endpoints) FindByName(name string) (Endpoint, bool) { 63 | for _, v := range e { 64 | if v.Name == name { 65 | return v, true 66 | } 67 | } 68 | 69 | return Endpoint{}, false 70 | } 71 | 72 | type Endpoint struct { 73 | Name string `yaml:"name"` 74 | Endpoint string `yaml:"endpoint"` 75 | Kubernetes portforwarder.KubernetesInfo `yaml:"kubernetes"` 76 | } 77 | 78 | func New() *Config { 79 | return &Config{ 80 | IFace: DefaultIface, 81 | Address: "localhost:50000", 82 | Endpoints: []Endpoint{{ 83 | Name: "", 84 | Endpoint: "", 85 | Kubernetes: portforwarder.KubernetesInfo{ 86 | Config: "/home/USER/.kube/config", 87 | Namespace: "default", 88 | Endpoint: "netmux", 89 | Context: "netmux", 90 | Port: "50000", 91 | }, 92 | }}, 93 | } 94 | } 95 | 96 | func (t *Config) ContextByName(n string) Endpoint { 97 | for _, v := range t.Endpoints { 98 | if v.Name == n { 99 | return v 100 | } 101 | } 102 | 103 | return Endpoint{} 104 | } 105 | -------------------------------------------------------------------------------- /app/nx-daemon/config/config_darwin.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | DefaultLogFile = "/tmp/nx-daemon.log" 5 | DefaultConfigPath = "netmux.yaml" 6 | DefaultIface = "lo0" 7 | ) 8 | -------------------------------------------------------------------------------- /app/nx-daemon/config/config_linux.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | DefaultLogFile = "/tmp/nx-daemon.log" 5 | DefaultConfigPath = "netmux.yaml" 6 | DefaultIface = "lo" 7 | DefaultTokensVaultPath = "/etc/netmux-tokens.yaml" 8 | ) 9 | -------------------------------------------------------------------------------- /app/nx-daemon/config/config_windows.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | DefaultConfigPath = "netmux.yaml" 5 | DefaultTokensVaultPath = "netmux-tokens.yaml" 6 | DefaultIface = "LB" 7 | DefaultLogFile = `nx-daemon.log` 8 | ) 9 | -------------------------------------------------------------------------------- /app/nx-daemon/daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net" 8 | "os" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/duxthemux/netmux/app/nx-daemon/config" 14 | "github.com/duxthemux/netmux/business/netmux" 15 | "github.com/duxthemux/netmux/business/networkallocator" 16 | "github.com/duxthemux/netmux/business/networkallocator/dnsallocator" 17 | "github.com/duxthemux/netmux/business/portforwarder" 18 | "github.com/duxthemux/netmux/foundation/memstore" 19 | "github.com/duxthemux/netmux/foundation/metrics" 20 | ) 21 | 22 | var ( 23 | ErrEndpointAlreadyConnected = fmt.Errorf("endpoint already connect") 24 | ErrEndpointNotConnected = fmt.Errorf("endpoint not connect") 25 | ErrBridgeNotFound = fmt.Errorf("bridge not found") 26 | ErrBridgeAlreadyConnected = fmt.Errorf("bridge already connected") 27 | ErrBridgeDirectionInvalid = fmt.Errorf("bridge direction invalid") 28 | ) 29 | 30 | type OperationalBridge struct { 31 | netmux.Bridge 32 | cancel func(err error) 33 | } 34 | 35 | type OperationalEndPoint struct { 36 | agent *netmux.Agent 37 | config config.Endpoint 38 | cancel func(err error) 39 | availableBridges *memstore.Map[netmux.Bridge] 40 | operationalBridges *memstore.Map[*OperationalBridge] 41 | } 42 | 43 | func NewOperationalEndPoint() *OperationalEndPoint { 44 | return &OperationalEndPoint{ 45 | agent: &netmux.Agent{}, 46 | config: config.Endpoint{}, 47 | cancel: func(err error) {}, 48 | availableBridges: memstore.New[netmux.Bridge](), 49 | operationalBridges: memstore.New[*OperationalBridge](), 50 | } 51 | } 52 | 53 | type StatusBridges struct { 54 | netmux.Bridge 55 | Status string `json:"status"` 56 | } 57 | 58 | type StatusEndPoints struct { 59 | config.Endpoint 60 | Status string `json:"status"` 61 | Bridges []StatusBridges `json:"bridges"` 62 | } 63 | 64 | type Status struct { 65 | Endpoints []StatusEndPoints `json:"endpoints"` 66 | } 67 | 68 | type Daemon struct { 69 | cfg *config.Config 70 | networkAllocator *networkallocator.NetworkAllocator 71 | operationalEndpoints *memstore.Map[*OperationalEndPoint] 72 | metricsFactroy metrics.Factory 73 | } 74 | 75 | type Opts func(d *Daemon) 76 | 77 | func WithMetrics(m metrics.Factory) Opts { 78 | return func(d *Daemon) { 79 | d.metricsFactroy = m 80 | } 81 | } 82 | 83 | func New(cfg *config.Config, nw *networkallocator.NetworkAllocator, opts ...Opts) *Daemon { 84 | ret := &Daemon{ 85 | cfg: cfg, 86 | networkAllocator: nw, 87 | operationalEndpoints: memstore.New[*OperationalEndPoint](), 88 | } 89 | for _, opt := range opts { 90 | opt(ret) 91 | } 92 | 93 | return ret 94 | } 95 | 96 | func (d *Daemon) GetConfig() *config.Config { 97 | return d.cfg 98 | } 99 | 100 | //nolint:funlen,cyclop 101 | func (d *Daemon) Connect(ctx context.Context, endpointName string) error { 102 | if ctx.Err() != nil { 103 | return fmt.Errorf("context cancelled when connecting: %w", ctx.Err()) 104 | } 105 | 106 | endpoint := d.operationalEndpoints.Get(endpointName) 107 | if endpoint != nil { 108 | return ErrEndpointAlreadyConnected 109 | } 110 | 111 | ctx, cancel := context.WithCancelCause(ctx) 112 | 113 | epCfg, found := d.cfg.Endpoints.FindByName(endpointName) 114 | if !found { 115 | return fmt.Errorf("config not found for endpoint %s", endpointName) 116 | } 117 | 118 | var localAgent *netmux.Agent 119 | 120 | var err error 121 | 122 | if epCfg.Kubernetes != (portforwarder.KubernetesInfo{}) { 123 | portForwarder := portforwarder.New() 124 | if err := portForwarder.Start(ctx, epCfg.Kubernetes); err != nil { 125 | cancel(fmt.Errorf("error starting portforwad: %w", err)) 126 | 127 | return fmt.Errorf("error connecting port forward: %w", err) 128 | } 129 | 130 | agentEndPointPortForward := net.JoinHostPort("localhost", strconv.Itoa(portForwarder.Port)) 131 | 132 | localAgent, err = netmux.NewAgent(ctx, 133 | agentEndPointPortForward, d.networkAllocator, netmux.AgentWithMetrics(d.metricsFactroy)) 134 | if err != nil { 135 | cancel(fmt.Errorf("error creating agent: %w", err)) 136 | 137 | return fmt.Errorf("could not connect to endpoint") 138 | } 139 | } else { 140 | localAgent, err = netmux.NewAgent(ctx, epCfg.Endpoint, d.networkAllocator, netmux.AgentWithMetrics(d.metricsFactroy)) 141 | if err != nil { 142 | cancel(fmt.Errorf("error creating agent: %w", err)) 143 | 144 | return fmt.Errorf("could not connect to endpoint") 145 | } 146 | } 147 | 148 | operationalEndPoint := NewOperationalEndPoint() 149 | 150 | operationalEndPoint.agent = localAgent 151 | operationalEndPoint.cancel = cancel 152 | operationalEndPoint.config = epCfg 153 | operationalEndPoint.availableBridges = memstore.New[netmux.Bridge]() 154 | 155 | go func(operationalEndPoint *OperationalEndPoint) { 156 | for { 157 | select { 158 | case <-ctx.Done(): 159 | _ = operationalEndPoint.operationalBridges.ForEach(func(k string, v *OperationalBridge) error { 160 | if v == nil || v.cancel == nil { 161 | return nil 162 | } 163 | 164 | v.cancel(fmt.Errorf("connection closing: %w", ctx.Err())) 165 | 166 | return nil 167 | }) 168 | 169 | return 170 | case evt := <-localAgent.Events(): 171 | slog.Info(fmt.Sprintf("Event: %v: %s", evt.EvtName, evt.Bridge.String())) 172 | 173 | switch evt.EvtName { 174 | case netmux.EventBridgeUp: 175 | operationalEndPoint.availableBridges.Set(evt.Bridge.Name, evt.Bridge) 176 | case netmux.EventBridgeDel: 177 | operationalEndPoint.availableBridges.Del(evt.Bridge.Name) 178 | case netmux.EventBridgeAdd: 179 | operationalEndPoint.availableBridges.Set(evt.Bridge.Name, evt.Bridge) 180 | } 181 | } 182 | } 183 | }(operationalEndPoint) 184 | 185 | d.operationalEndpoints.Set(endpointName, operationalEndPoint) 186 | 187 | return nil 188 | } 189 | 190 | func (d *Daemon) Disconnect(endpoint string) error { 191 | managedEndpoint := d.operationalEndpoints.Get(endpoint) 192 | if managedEndpoint == nil { 193 | return ErrEndpointNotConnected 194 | } 195 | 196 | managedEndpoint.cancel(fmt.Errorf("Disconnect called")) 197 | d.operationalEndpoints.Del(endpoint) 198 | 199 | return nil 200 | } 201 | 202 | func (d *Daemon) Exit() { 203 | os.Exit(1) 204 | } 205 | 206 | func (d *Daemon) CleanUp() error { 207 | if err := d.networkAllocator.CleanUp("nx"); err != nil { 208 | return fmt.Errorf("error during cleanup: %w", err) 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func (d *Daemon) GetStatus() Status { 215 | ret := Status{} 216 | 217 | for _, endpoint := range d.cfg.Endpoints { 218 | epStatus := StatusEndPoints{ 219 | Endpoint: endpoint, 220 | Status: "off", 221 | } 222 | 223 | opEndpoint := d.operationalEndpoints.Get(endpoint.Name) 224 | if opEndpoint != nil { 225 | epStatus.Status = "on" 226 | _ = opEndpoint.availableBridges.ForEach(func(k string, v netmux.Bridge) error { 227 | bridge := StatusBridges{Bridge: v, Status: "off"} 228 | opBridge := opEndpoint.operationalBridges.Get(v.Name) 229 | if opBridge != nil { 230 | bridge.Status = "on" 231 | } 232 | epStatus.Bridges = append(epStatus.Bridges, bridge) 233 | 234 | return nil 235 | }) 236 | } 237 | 238 | ret.Endpoints = append(ret.Endpoints, epStatus) 239 | } 240 | 241 | return ret 242 | } 243 | 244 | func (d *Daemon) startIndividualService(ctx context.Context, endpoint string, svc string) error { 245 | managedEndpoint := d.operationalEndpoints.Get(endpoint) 246 | if managedEndpoint == nil { 247 | return ErrEndpointNotConnected 248 | } 249 | 250 | if op := managedEndpoint.operationalBridges.Get(svc); op != nil { 251 | return ErrBridgeAlreadyConnected 252 | } 253 | 254 | bridge := managedEndpoint.availableBridges.Get(svc) 255 | if bridge == (netmux.Bridge{}) { 256 | return ErrBridgeNotFound 257 | } 258 | 259 | ctx, cancel := context.WithCancelCause(ctx) 260 | 261 | operationalBridge := &OperationalBridge{ 262 | Bridge: bridge, 263 | cancel: cancel, 264 | } 265 | 266 | switch bridge.Direction { 267 | case netmux.DirectionL2C: 268 | go func() { 269 | if err := managedEndpoint.agent.ServeProxy(ctx, bridge); err != nil { 270 | slog.Warn("error serving proxy", "err", err) 271 | } 272 | }() 273 | managedEndpoint.operationalBridges.Set(svc, operationalBridge) 274 | 275 | return nil 276 | case netmux.DirectionC2L: 277 | go func() { 278 | closer, err := managedEndpoint.agent.ServeReverse(ctx, bridge) 279 | if err != nil { 280 | slog.Warn("error serving proxy", "err", err) 281 | } 282 | 283 | operationalBridge.cancel = closer 284 | }() 285 | managedEndpoint.operationalBridges.Set(svc, operationalBridge) 286 | 287 | return nil 288 | default: 289 | return ErrBridgeDirectionInvalid 290 | } 291 | } 292 | 293 | //nolint:nestif 294 | func (d *Daemon) StartService(ctx context.Context, endpoint string, svc string) error { 295 | if strings.Contains(svc, "+") { 296 | svc := strings.ReplaceAll(svc, "+", ".*") 297 | 298 | svcRegex, err := regexp.Compile(svc) 299 | if err != nil { 300 | return fmt.Errorf("error compiling regex: %w", err) 301 | } 302 | 303 | managedEndpoint := d.operationalEndpoints.Get(endpoint) 304 | if managedEndpoint == nil { 305 | return ErrEndpointNotConnected 306 | } 307 | 308 | errs := make([]string, 0) 309 | 310 | _ = managedEndpoint.availableBridges.ForEach(func(k string, v netmux.Bridge) error { 311 | if svcRegex.MatchString(k) { 312 | if err = d.startIndividualService(ctx, endpoint, k); err != nil { 313 | errs = append(errs, fmt.Sprintf("error closing %s: %s", k, err.Error())) 314 | } 315 | } 316 | 317 | return nil 318 | }) 319 | 320 | if len(errs) > 0 { 321 | return fmt.Errorf(strings.Join(errs, "\n")) 322 | } 323 | 324 | return nil 325 | } 326 | 327 | return d.startIndividualService(ctx, endpoint, svc) 328 | } 329 | 330 | //nolint:nestif 331 | func (d *Daemon) StopService(endpoint string, svc string) error { 332 | if strings.Contains(svc, "+") { 333 | svc := strings.ReplaceAll(svc, "+", ".*") 334 | 335 | svcRegex, err := regexp.Compile(svc) 336 | if err != nil { 337 | return fmt.Errorf("error compiling regex: %w", err) 338 | } 339 | 340 | managedEndpoint := d.operationalEndpoints.Get(endpoint) 341 | if managedEndpoint == nil { 342 | return ErrEndpointNotConnected 343 | } 344 | 345 | errs := make([]string, 0) 346 | bridges := make([]string, 0) 347 | 348 | _ = managedEndpoint.operationalBridges.ForEach(func(k string, v *OperationalBridge) error { 349 | if svcRegex.MatchString(k) { 350 | bridges = append(bridges, k) 351 | } 352 | 353 | return nil 354 | }) 355 | 356 | for _, k := range bridges { 357 | if err = d.stopIndividualService(endpoint, k); err != nil { 358 | errs = append(errs, fmt.Sprintf("error closing %s: %s", k, err.Error())) 359 | } 360 | } 361 | 362 | if len(errs) > 0 { 363 | return fmt.Errorf(strings.Join(errs, "\n")) 364 | } 365 | 366 | return nil 367 | } 368 | 369 | return d.stopIndividualService(endpoint, svc) 370 | } 371 | 372 | func (d *Daemon) stopIndividualService(endpoint string, svc string) error { 373 | managedEndpoint := d.operationalEndpoints.Get(endpoint) 374 | if managedEndpoint == nil { 375 | return ErrEndpointNotConnected 376 | } 377 | 378 | operationalBridge := managedEndpoint.operationalBridges.Get(svc) 379 | if operationalBridge == nil { 380 | return ErrBridgeNotFound 381 | } 382 | 383 | operationalBridge.cancel(fmt.Errorf("StopService called for %s", endpoint)) 384 | managedEndpoint.operationalBridges.Del(svc) 385 | 386 | return nil 387 | } 388 | 389 | func (d *Daemon) DNSEntries() []dnsallocator.DNSEntry { 390 | return d.networkAllocator.DNSEntries() 391 | } 392 | 393 | func (d *Daemon) Reload() error { 394 | return d.cfg.Load("") 395 | } 396 | -------------------------------------------------------------------------------- /app/nx-daemon/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | 14 | "golang.org/x/sync/errgroup" 15 | "gopkg.in/natefinch/lumberjack.v2" 16 | 17 | configlib "github.com/duxthemux/netmux/app/nx-daemon/config" 18 | "github.com/duxthemux/netmux/app/nx-daemon/daemon" 19 | "github.com/duxthemux/netmux/business/caroot" 20 | "github.com/duxthemux/netmux/business/networkallocator" 21 | "github.com/duxthemux/netmux/foundation/buildinfo" 22 | "github.com/duxthemux/netmux/foundation/metrics" 23 | 24 | "github.com/duxthemux/netmux/app/nx-daemon/webserver" 25 | ) 26 | 27 | const ( 28 | MaxSize = 1 29 | MaxBackups = 3 30 | MaxAge = 28 31 | ) 32 | 33 | func setupLog() { 34 | var logWriter io.Writer = os.Stdout 35 | 36 | if os.Getenv("LOGFILE") != "-" { 37 | logWriter = &lumberjack.Logger{ 38 | Filename: configlib.DefaultLogFile, 39 | MaxSize: MaxSize, // megabytes 40 | MaxAge: MaxAge, // days 41 | MaxBackups: MaxBackups, 42 | } 43 | } 44 | 45 | logger := slog.New(slog.NewTextHandler(logWriter, &slog.HandlerOptions{ 46 | AddSource: true, 47 | Level: slog.LevelDebug, 48 | ReplaceAttr: nil, 49 | })) 50 | 51 | slog.SetDefault(logger) 52 | } 53 | 54 | //nolint:funlen 55 | func run() error { 56 | ctx, cancel := context.WithCancelCause(context.Background()) 57 | defer cancel(fmt.Errorf("nx-server main run ended")) 58 | 59 | ctx, _ = signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGKILL) 60 | 61 | slog.Info(buildinfo.String("nx-daemon")) 62 | 63 | agentConfig := configlib.New() 64 | 65 | if err := agentConfig.Load(""); err != nil { 66 | slog.Warn(fmt.Sprintf("error loading userconfig: %s", err.Error())) 67 | } 68 | 69 | networkAllocator, err := networkallocator.New(agentConfig.IFace, agentConfig.Network) 70 | if err != nil { 71 | return fmt.Errorf("error creating network allocator: %w", err) 72 | } 73 | 74 | _ = networkAllocator.CleanUp("") 75 | 76 | metricsFactory := metrics.NewPromFactory() 77 | 78 | svc := daemon.New(agentConfig, networkAllocator, daemon.WithMetrics(metricsFactory)) 79 | 80 | address, err := networkAllocator.GetIP("nx") 81 | if err != nil { 82 | return fmt.Errorf("failed to allocate address: %w", err) 83 | } 84 | 85 | defer func() { 86 | if err := networkAllocator.ReleaseIP(address); err != nil { 87 | slog.Warn("error releasing address", "err", err) 88 | } 89 | }() 90 | 91 | aCa := caroot.New() 92 | 93 | if err = aCa.Init(".", nil); err != nil { 94 | return fmt.Errorf("failed to init CA: %w", err) 95 | } 96 | 97 | aWebserver := webserver.New(svc) 98 | 99 | group, ctx := errgroup.WithContext(ctx) 100 | 101 | group.Go(func() error { 102 | if err = aWebserver.Run(ctx, "nx", address, "443", aCa); err != nil { 103 | if errors.Is(http.ErrServerClosed, err) { 104 | return nil 105 | } 106 | 107 | return fmt.Errorf("failed to serve: %w", err) 108 | } 109 | 110 | return nil 111 | }) 112 | 113 | group.Go(func() error { 114 | if err = metricsFactory.Start(ctx, ":50001"); err != nil { 115 | return fmt.Errorf("error starting metrics factory: %w", err) 116 | } 117 | 118 | return nil 119 | }) 120 | 121 | if err = group.Wait(); err != nil { 122 | return fmt.Errorf("error processing group: %w", err) 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func main() { 129 | setupLog() 130 | 131 | if err := run(); err != nil { 132 | panic(err) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/nx-daemon/webserver/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/gorilla/mux" 9 | 10 | "github.com/duxthemux/netmux/app/nx-daemon/daemon" 11 | "github.com/duxthemux/netmux/business/caroot" 12 | ) 13 | 14 | type API struct { 15 | Service *daemon.Daemon 16 | } 17 | 18 | func (a *API) pluginContext(ctx context.Context, router *mux.Router) { 19 | router.Name("contextConnect"). 20 | Methods(http.MethodGet). 21 | Path("/{context}/connect"). 22 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 23 | ctxName := mux.Vars(request)["context"] 24 | if ctxName == "" { 25 | http.Error(responseWriter, "context name is required", http.StatusBadRequest) 26 | 27 | return 28 | } 29 | 30 | err := a.Service.Connect(ctx, ctxName) 31 | if err != nil { 32 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 33 | } 34 | }) 35 | 36 | router.Name("contextDisconnect"). 37 | Methods(http.MethodGet). 38 | Path("/{context}/disconnect"). 39 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 40 | ctxName := mux.Vars(request)["context"] 41 | if ctxName == "" { 42 | http.Error(responseWriter, "context name is required", http.StatusBadRequest) 43 | 44 | return 45 | } 46 | 47 | err := a.Service.Disconnect(ctxName) 48 | if err != nil { 49 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 50 | } 51 | }) 52 | } 53 | 54 | func (a *API) pluginServices(ctx context.Context, router *mux.Router) { 55 | router.Name("servicesList"). 56 | Methods(http.MethodGet). 57 | Path("/"). 58 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 59 | svcs := a.Service.GetStatus() 60 | responseWriter.Header().Set("Content-Type", "application/json") 61 | err := json.NewEncoder(responseWriter).Encode(svcs) 62 | if err != nil { 63 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 64 | } 65 | }) 66 | 67 | router.Name("servicesStart"). 68 | Methods(http.MethodGet). 69 | Path("/{context}/{name}/start"). 70 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 71 | vars := mux.Vars(request) 72 | ctxName := vars["context"] 73 | svcName := vars["name"] 74 | if ctxName == "" { 75 | http.Error(responseWriter, "context name is required", http.StatusBadRequest) 76 | 77 | return 78 | } 79 | 80 | err := a.Service.StartService(ctx, ctxName, svcName) 81 | if err != nil { 82 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 83 | } 84 | }) 85 | 86 | router.Name("servicesStop"). 87 | Methods(http.MethodGet). 88 | Path("/{context}/{name}/stop"). 89 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 90 | vars := mux.Vars(request) 91 | ctxName := vars["context"] 92 | svcName := vars["name"] 93 | if ctxName == "" { 94 | http.Error(responseWriter, "context name is required", http.StatusBadRequest) 95 | 96 | return 97 | } 98 | 99 | err := a.Service.StopService(ctxName, svcName) 100 | if err != nil { 101 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 102 | } 103 | }) 104 | } 105 | 106 | func (a *API) pluginConfig(_ context.Context, router *mux.Router, caRoot *caroot.CA) { 107 | router.Name("configMain"). 108 | Methods(http.MethodGet). 109 | Path("/main"). 110 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 111 | cfg := a.Service.GetConfig() 112 | responseWriter.Header().Set("Content-Type", "application/json") 113 | err := json.NewEncoder(responseWriter).Encode(cfg) 114 | if err != nil { 115 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 116 | } 117 | }) 118 | 119 | router.Name("configHosts"). 120 | Methods(http.MethodGet). 121 | Path("/hosts"). 122 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 123 | cfg := a.Service.DNSEntries() 124 | responseWriter.Header().Set("Content-Type", "application/json") 125 | 126 | err := json.NewEncoder(responseWriter).Encode(cfg) 127 | if err != nil { 128 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 129 | } 130 | }) 131 | 132 | router.Name("configCa"). 133 | Methods(http.MethodGet). 134 | Path("/caRoot"). 135 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 136 | bytes, err := caRoot.CaCerBytes() 137 | if err != nil { 138 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 139 | 140 | return 141 | } 142 | responseWriter.Header().Set("Content-Type", "application/pkix-cert") 143 | 144 | _, _ = responseWriter.Write(bytes) 145 | }) 146 | } 147 | 148 | func (a *API) pluginMisc(_ context.Context, router *mux.Router) { 149 | router.Name("miscReload"). 150 | Methods(http.MethodGet). 151 | Path("/reload"). 152 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 153 | if err := a.Service.Reload(); err != nil { 154 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 155 | } 156 | }) 157 | 158 | router.Name("miscExit"). 159 | Methods(http.MethodGet). 160 | Path("/exit"). 161 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 162 | a.Service.Exit() 163 | }) 164 | 165 | router.Name("miscCleanup"). 166 | Methods(http.MethodGet). 167 | Path("/cleanup"). 168 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 169 | if err := a.Service.CleanUp(); err != nil { 170 | http.Error(responseWriter, err.Error(), http.StatusInternalServerError) 171 | } 172 | }) 173 | router.Name("miscTest"). 174 | Methods(http.MethodGet). 175 | Path("/test"). 176 | HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { 177 | _, _ = responseWriter.Write([]byte("Netmux is here - OK!")) 178 | }) 179 | } 180 | 181 | func (a *API) Plugin(ctx context.Context, rootRouter *mux.Router, ca *caroot.CA) error { 182 | apiV1Router := rootRouter.Name("apiV1-router").PathPrefix("/api/v1/").Subrouter() 183 | 184 | configRouter := apiV1Router.Name("userconfig-router").PathPrefix("/userconfig/").Subrouter() 185 | a.pluginConfig(ctx, configRouter, ca) 186 | 187 | ctxRouter := apiV1Router.Name("context-router").PathPrefix("/context/").Subrouter() 188 | a.pluginContext(ctx, ctxRouter) 189 | 190 | servicesRouter := apiV1Router.Name("services-router").PathPrefix("/services/").Subrouter() 191 | a.pluginServices(ctx, servicesRouter) 192 | 193 | miscRouter := apiV1Router.Name("misc-router").PathPrefix("/misc/").Subrouter() 194 | 195 | a.pluginMisc(ctx, miscRouter) 196 | 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /app/nx-daemon/webserver/webserver.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | 15 | "github.com/duxthemux/netmux/app/nx-daemon/webserver/api" 16 | 17 | "github.com/duxthemux/netmux/app/nx-daemon/daemon" 18 | "github.com/duxthemux/netmux/business/caroot" 19 | ) 20 | 21 | type WebServer struct { 22 | Service *daemon.Daemon 23 | server http.Server 24 | api api.API 25 | } 26 | 27 | const ReaderTimeout = time.Second * 5 28 | 29 | //nolint:funlen 30 | func (w *WebServer) Run(ctx context.Context, name string, addr string, port string, certAuth *caroot.CA) error { 31 | root := mux.NewRouter() 32 | 33 | root.Handle("/metrics", promhttp.Handler()).Name("prometheus-metrics") 34 | 35 | err := w.api.Plugin(ctx, root, certAuth) 36 | if err != nil { 37 | return fmt.Errorf("error adding api routes: %w", err) 38 | } 39 | 40 | cert, err := certAuth.GetOrGenFromRoot(name) 41 | if err != nil { 42 | return fmt.Errorf("could not retrieve certificate for %s: %w", name, err) 43 | } 44 | 45 | server := http.Server{ 46 | Addr: addr + ":" + port, 47 | Handler: root, 48 | TLSConfig: &tls.Config{ 49 | MinVersion: tls.VersionTLS13, 50 | NextProtos: []string{"http/1.1"}, 51 | Certificates: []tls.Certificate{cert}, 52 | }, 53 | ReadHeaderTimeout: ReaderTimeout, 54 | } 55 | 56 | go func() { 57 | <-ctx.Done() 58 | _ = server.Shutdown(ctx) 59 | }() 60 | 61 | err = root.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 62 | tmpl, err := route.GetPathTemplate() 63 | if err != nil { 64 | return fmt.Errorf("error getting path template: %w", err) 65 | } 66 | methods, _ := route.GetMethods() 67 | 68 | methodsStr := strings.Join(methods, ", ") 69 | if len(methods) == 0 { 70 | methodsStr = "ALL" 71 | } 72 | 73 | name := route.GetName() 74 | 75 | slog.Debug(fmt.Sprintf("* %s: [%s] %s", name, methodsStr, tmpl)) 76 | 77 | return nil 78 | }) 79 | 80 | if err != nil { 81 | return fmt.Errorf("error walking routes: %w", err) 82 | } 83 | 84 | slog.Info(fmt.Sprintf("Webserver running at: %s:%s", addr, port)) 85 | 86 | err = server.ListenAndServeTLS("", "") 87 | if err != nil { 88 | return fmt.Errorf("error running server: %w", err) 89 | } 90 | 91 | return nil 92 | } 93 | 94 | const ReadHeaderTimeout = time.Second * 5 95 | 96 | func New(svc *daemon.Daemon) *WebServer { 97 | ret := &WebServer{ 98 | Service: svc, 99 | server: http.Server{ 100 | ReadHeaderTimeout: ReadHeaderTimeout, 101 | }, 102 | api: api.API{Service: svc}, 103 | } 104 | 105 | return ret 106 | } 107 | -------------------------------------------------------------------------------- /app/nx-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net" 8 | "os" 9 | "strconv" 10 | 11 | "golang.org/x/sync/errgroup" 12 | 13 | "github.com/duxthemux/netmux/app/nx-server/runtime/k8s" 14 | "github.com/duxthemux/netmux/business/netmux" 15 | "github.com/duxthemux/netmux/foundation/buildinfo" 16 | "github.com/duxthemux/netmux/foundation/metrics" 17 | ) 18 | 19 | const ( 20 | EnvLogLevel = "LOGLEVEL" 21 | EnvLogSrc = "LOGSRC" 22 | ) 23 | 24 | func logInit() { 25 | logLevel := os.Getenv(EnvLogLevel) 26 | 27 | slogLevel := slog.LevelInfo 28 | 29 | err := slogLevel.UnmarshalText([]byte(logLevel)) 30 | if err != nil { 31 | slogLevel = slog.LevelDebug 32 | } 33 | 34 | slogAddSource, _ := strconv.ParseBool(os.Getenv(EnvLogSrc)) 35 | 36 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 37 | AddSource: slogAddSource, 38 | Level: slogLevel, 39 | ReplaceAttr: nil, 40 | })) 41 | 42 | slog.SetDefault(logger) 43 | } 44 | 45 | //nolint:funlen 46 | func run() error { 47 | ctx, cancel := context.WithCancelCause(context.Background()) 48 | defer cancel(fmt.Errorf("nx-server run ended")) 49 | 50 | metricsProvider := metrics.NewPromFactory() 51 | 52 | netmuxService := netmux.NewService( 53 | netmux.WithEventsLogger(func(evt netmux.Event) { 54 | slog.Info(fmt.Sprintf("Event: %v: %s", evt.EvtName, evt.Bridge.String())) 55 | }), 56 | netmux.WithMetrics(metricsProvider), 57 | ) 58 | 59 | logInit() 60 | 61 | slog.Info(buildinfo.StringOneLine("nx-server")) 62 | 63 | addr := os.Getenv("ADDR") 64 | if addr == "" { 65 | addr = ":50000" 66 | } 67 | 68 | listener, err := net.Listen("tcp", addr) 69 | if err != nil { 70 | return fmt.Errorf("error setting up service listener: %w", err) 71 | } 72 | 73 | k8sRuntime := k8s.NewRuntime(k8s.Opts{}) 74 | 75 | defer func() { 76 | if err := k8sRuntime.Close(); err != nil { 77 | slog.Warn("error closing k8s runtime", "err", err) 78 | } 79 | }() 80 | 81 | probe := k8s.NewProbe(":8083") 82 | 83 | netmuxService.AddEventSource(ctx, k8sRuntime) 84 | 85 | group, ctx := errgroup.WithContext(ctx) 86 | 87 | group.Go(func() error { 88 | defer cancel(fmt.Errorf("k8sRuntime ended")) 89 | 90 | return k8sRuntime.Run(ctx) //nolint:wrapcheck 91 | }) 92 | 93 | group.Go(func() error { 94 | defer cancel(fmt.Errorf("netmuxService ended")) 95 | 96 | return netmuxService.Serve(ctx, listener) //nolint:wrapcheck 97 | }) 98 | 99 | group.Go(func() error { 100 | defer cancel(fmt.Errorf("probe ended")) 101 | 102 | return probe.Run(ctx) //nolint:wrapcheck 103 | }) 104 | 105 | group.Go(func() error { 106 | defer cancel(fmt.Errorf("metricsServer ended")) 107 | 108 | return metricsProvider.Start(ctx, ":8081") //nolint:wrapcheck 109 | }) 110 | 111 | probe.Ready() 112 | 113 | return group.Wait() //nolint:wrapcheck 114 | } 115 | 116 | func main() { 117 | err := run() 118 | if err != nil { 119 | panic(err) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/nx-server/runtime/k8s/probe.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type Probe struct { 13 | addr string 14 | ready bool 15 | } 16 | 17 | const ( 18 | TimeoutGraceful = time.Second * 5 19 | ReadHeaderTimeout = time.Second * 5 20 | ) 21 | 22 | func (k *Probe) Ready() { 23 | k.ready = true 24 | } 25 | 26 | func (k *Probe) Run(ctx context.Context) error { 27 | if ctx.Err() != nil { 28 | return fmt.Errorf("context closed calling Run: %w", ctx.Err()) 29 | } 30 | 31 | ctx, cancel := context.WithCancelCause(ctx) 32 | defer cancel(fmt.Errorf("deferred")) 33 | 34 | mux := http.NewServeMux() 35 | mux.HandleFunc("/live", func(writer http.ResponseWriter, request *http.Request) { 36 | _, _ = writer.Write([]byte("ok")) 37 | }) 38 | 39 | mux.HandleFunc("/ready", func(writer http.ResponseWriter, request *http.Request) { 40 | if !k.ready { 41 | slog.Warn("k8sprobe: Not ready") 42 | http.Error(writer, "not ready yet", http.StatusServiceUnavailable) 43 | 44 | return 45 | } 46 | _, _ = writer.Write([]byte("ok")) 47 | }) 48 | 49 | slog.Info(fmt.Sprintf("Starting k8sprobes on %s", k.addr)) 50 | server := http.Server{ 51 | Addr: k.addr, 52 | Handler: mux, 53 | ReadHeaderTimeout: ReadHeaderTimeout, 54 | } 55 | 56 | go func() { 57 | <-ctx.Done() 58 | 59 | ctx, cancel := context.WithTimeout(context.Background(), TimeoutGraceful) 60 | defer cancel() 61 | 62 | err := server.Shutdown(ctx) //nolint:contextcheck 63 | if err != nil { 64 | slog.Warn(fmt.Sprintf("Errorf closing probe server: %s", err.Error())) 65 | } 66 | }() 67 | 68 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 69 | return fmt.Errorf("error serving http: %w", err) 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func NewProbe(addr string) *Probe { 76 | return &Probe{ 77 | addr: addr, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/nx-server/runtime/k8s/runtime.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | 9 | "gopkg.in/yaml.v3" 10 | corev1 "k8s.io/api/core/v1" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/watch" 13 | "k8s.io/client-go/kubernetes" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/clientcmd" 16 | 17 | "github.com/duxthemux/netmux/business/netmux" 18 | ) 19 | 20 | type Opts struct { 21 | Kubefile string 22 | Namespaces []string 23 | All bool 24 | } 25 | 26 | func MyNamespace() (string, error) { 27 | bs, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 28 | if err != nil { 29 | return "", fmt.Errorf("error reading namespace: %w", err) 30 | } 31 | 32 | return string(bs), nil 33 | } 34 | 35 | type Runtime struct { 36 | opts Opts 37 | cancel func(err error) 38 | chEvents chan netmux.Event 39 | } 40 | 41 | func (k *Runtime) Events() <-chan netmux.Event { 42 | return k.chEvents 43 | } 44 | 45 | func NewRuntime(opts Opts) *Runtime { 46 | ret := &Runtime{ 47 | opts: opts, 48 | chEvents: make(chan netmux.Event), 49 | } 50 | 51 | return ret 52 | } 53 | 54 | func (k *Runtime) loadFromAnnotation(s string) ([]netmux.Bridge, error) { 55 | ret := make([]netmux.Bridge, 0) 56 | 57 | err := yaml.Unmarshal([]byte(s), &ret) 58 | if err != nil { 59 | return nil, fmt.Errorf("error parsing annotation: %w", err) 60 | } 61 | 62 | return ret, nil 63 | } 64 | 65 | func (k *Runtime) resolveConfig(fname string) (*rest.Config, error) { 66 | if fname != "" { 67 | ret, err := clientcmd.BuildConfigFromFlags("", fname) 68 | if err != nil { 69 | return nil, fmt.Errorf("errmr building config from file %s: %w", fname, err) 70 | } 71 | 72 | return ret, nil 73 | } 74 | 75 | slog.Info("Using InClusterConfig") 76 | 77 | ret, err := rest.InClusterConfig() 78 | if err != nil { 79 | return nil, fmt.Errorf("error building incluster config: %w", err) 80 | } 81 | 82 | return ret, nil 83 | } 84 | 85 | //nolint:funlen,cyclop 86 | func (k *Runtime) handleServiceWithAnnotations(evt watch.EventType, dep *corev1.Service) { 87 | bridges, err := k.loadFromAnnotation(dep.Annotations["nx"]) 88 | if err != nil { 89 | slog.Warn(fmt.Sprintf("error reading annotation for %s.%s: %s", dep.Name, dep.Namespace, err.Error())) 90 | 91 | return 92 | } 93 | 94 | for i := range bridges { 95 | nxa := bridges[i] 96 | if nxa.Name == "" { 97 | nxa.Name = dep.Name 98 | slog.Debug(fmt.Sprintf("Using name from service: %s.%s", dep.Namespace, dep.Name)) 99 | } 100 | 101 | if nxa.ContainerAddr == "" { 102 | nxa.ContainerAddr = dep.Spec.ClusterIP 103 | slog.Debug(fmt.Sprintf("Fixing bridge w/o remote addr: %s.%s => %s", dep.Namespace, dep.Name, nxa.ContainerAddr)) 104 | } 105 | 106 | if nxa.LocalAddr == "" { 107 | nxa.LocalAddr = dep.Name 108 | slog.Debug(fmt.Sprintf("Fixing bridge w/o local addr: %s.%s => %s", dep.Namespace, dep.Name, nxa.LocalAddr)) 109 | } 110 | 111 | if nxa.ContainerPort == "" { 112 | nxa.ContainerPort = fmt.Sprintf("%v", dep.Spec.Ports[0].Port) 113 | slog.Debug(fmt.Sprintf("Fixing bridge w/o remote port: %s.%s => %s", dep.Namespace, dep.Name, nxa.ContainerPort)) 114 | } 115 | 116 | if nxa.LocalPort == "" { 117 | nxa.LocalPort = fmt.Sprintf("%v", dep.Spec.Ports[0].Port) 118 | slog.Debug(fmt.Sprintf("Fixing bridge w/o local port: %s.%s => %s", dep.Namespace, dep.Name, nxa.LocalPort)) 119 | } 120 | 121 | if nxa.Direction == "" { 122 | nxa.Direction = "L2C" 123 | 124 | slog.Debug(fmt.Sprintf("Fixing bridge w/o direction: %s.%s => %s", dep.Namespace, dep.Name, "L2C")) 125 | } 126 | 127 | if nxa.Family == "" { 128 | nxa.Family = "tcp" 129 | 130 | slog.Debug(fmt.Sprintf("Fixing bridge w/o proto: %s.%s => %s", dep.Namespace, dep.Name, "tcp")) 131 | } 132 | 133 | nxa.Namespace = dep.Namespace 134 | 135 | slog.Info(fmt.Sprintf("K8S Event %v for %s.%s", evt, dep.Name, dep.Namespace)) 136 | 137 | switch evt { 138 | case watch.Added: 139 | slog.Info(fmt.Sprintf("Added service: %s", nxa.Name)) 140 | k.chEvents <- netmux.Event{ 141 | EvtName: netmux.EventBridgeAdd, 142 | Bridge: nxa, 143 | } 144 | 145 | case watch.Deleted: 146 | slog.Info(fmt.Sprintf("Deleted service: %s", nxa.Name)) 147 | k.chEvents <- netmux.Event{ 148 | EvtName: netmux.EventBridgeDel, 149 | Bridge: nxa, 150 | } 151 | 152 | case watch.Modified: 153 | slog.Info(fmt.Sprintf("Modified service: %s", nxa.Name)) 154 | k.chEvents <- netmux.Event{ 155 | EvtName: netmux.EventBridgeUp, 156 | Bridge: nxa, 157 | } 158 | case watch.Bookmark: 159 | case watch.Error: 160 | slog.Warn(fmt.Sprintf("error during event collection: %v", evt)) 161 | 162 | default: 163 | slog.Warn(fmt.Sprintf("unknown state while processing k8s events: %v", evt)) 164 | } 165 | } 166 | } 167 | 168 | func (k *Runtime) handleServiceWithoutAnnotations(evt watch.EventType, dep *corev1.Service) { 169 | nxa := netmux.Bridge{} 170 | nxa.Name = dep.Name 171 | nxa.ContainerAddr = dep.Spec.ClusterIP 172 | nxa.ContainerPort = fmt.Sprintf("%v", dep.Spec.Ports[0].Port) 173 | nxa.LocalAddr = dep.Name 174 | nxa.LocalPort = fmt.Sprintf("%v", dep.Spec.Ports[0].Port) 175 | nxa.Direction = "L2C" 176 | nxa.Namespace = dep.Namespace 177 | nxa.Family = "tcp" 178 | nxa.Name = dep.Name 179 | 180 | slog.Info(fmt.Sprintf("K8S Event %v for %s.%s", evt, dep.Name, dep.Namespace)) 181 | 182 | switch evt { 183 | case watch.Added: 184 | slog.Info(fmt.Sprintf("Added service: %s", nxa.Name)) 185 | k.chEvents <- netmux.Event{ 186 | EvtName: netmux.EventBridgeAdd, 187 | Bridge: nxa, 188 | } 189 | 190 | case watch.Deleted: 191 | slog.Info(fmt.Sprintf("Deleted service: %s", nxa.Name)) 192 | k.chEvents <- netmux.Event{ 193 | EvtName: netmux.EventBridgeDel, 194 | Bridge: nxa, 195 | } 196 | case watch.Modified: 197 | slog.Info(fmt.Sprintf("Modified service: %s", nxa.Name)) 198 | k.chEvents <- netmux.Event{ 199 | EvtName: netmux.EventBridgeUp, 200 | Bridge: nxa, 201 | } 202 | case watch.Bookmark: 203 | case watch.Error: 204 | slog.Warn(fmt.Sprintf("error during event collection: %v", evt)) 205 | 206 | default: 207 | slog.Warn(fmt.Sprintf("unknown state while processing k8s events: %v", evt)) 208 | } 209 | } 210 | 211 | func (k *Runtime) handleService(evt watch.EventType, dep *corev1.Service) { 212 | if dep.Annotations["nx"] != "" { 213 | k.handleServiceWithAnnotations(evt, dep) 214 | 215 | return 216 | } 217 | 218 | k.handleServiceWithoutAnnotations(evt, dep) 219 | } 220 | 221 | func (k *Runtime) runOnNS(ctx context.Context, cli *kubernetes.Clientset, ns string) error { 222 | slog.Info("K8s monitoring", "ns", ns) 223 | 224 | wservices, err := cli.CoreV1().Services(ns).Watch(ctx, v1.ListOptions{}) 225 | if err != nil { 226 | return fmt.Errorf("error watching services: %w", err) 227 | } 228 | 229 | go func() { 230 | for { 231 | select { 232 | case x := <-wservices.ResultChan(): 233 | p, ok := x.Object.(*corev1.Service) 234 | if ok && p != nil { 235 | k.handleService(x.Type, p) 236 | } 237 | 238 | case <-ctx.Done(): 239 | return 240 | } 241 | } 242 | }() 243 | slog.Debug("Namespace monitoring on") 244 | 245 | return nil 246 | } 247 | 248 | func (k *Runtime) Close() error { 249 | k.cancel(fmt.Errorf("k8s Runtime ended")) 250 | 251 | return nil 252 | } 253 | 254 | func (k *Runtime) Run(ctx context.Context) error { 255 | opts := k.opts 256 | ctx, cancel := context.WithCancelCause(ctx) 257 | k.cancel = cancel 258 | 259 | kubeConfig, err := k.resolveConfig(opts.Kubefile) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | clientset, err := kubernetes.NewForConfig(kubeConfig) 265 | if err != nil { 266 | return fmt.Errorf("error creating k8s client: %w", err) 267 | } 268 | 269 | ns, err := MyNamespace() 270 | if err != nil { 271 | return fmt.Errorf("error getting my namespace: %w", err) 272 | } 273 | 274 | err = k.runOnNS(ctx, clientset, ns) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | <-ctx.Done() 280 | 281 | return nil 282 | } 283 | -------------------------------------------------------------------------------- /business/caroot/caroot.go: -------------------------------------------------------------------------------- 1 | package caroot 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "fmt" 12 | "log/slog" 13 | "math/big" 14 | "os" 15 | "path" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | const ( 21 | DefaultPerm = 0o600 22 | DefaultRsaKeySize = 4096 23 | DefaultYearCertDur = 1000 24 | ) 25 | 26 | //nolint:gochecknoglobals 27 | var rootDir = "certs" 28 | 29 | type CA struct { 30 | ca *x509.Certificate 31 | catls tls.Certificate 32 | start int64 33 | mtx sync.Mutex 34 | } 35 | 36 | func New() *CA { 37 | return &CA{} 38 | } 39 | 40 | func exists(s string) bool { 41 | _, err := os.Stat(s) 42 | 43 | return !os.IsNotExist(err) 44 | } 45 | 46 | func createDefaultCert() *x509.Certificate { 47 | return &x509.Certificate{ 48 | SerialNumber: big.NewInt(time.Now().Unix()), 49 | Subject: pkix.Name{ 50 | Organization: []string{"Netmux"}, 51 | CommonName: "Netmux Root CA", 52 | Country: []string{"na"}, 53 | Province: []string{"na"}, 54 | Locality: []string{"na"}, 55 | StreetAddress: []string{"na"}, 56 | PostalCode: []string{"na"}, 57 | }, 58 | NotBefore: time.Now().Add(time.Hour * -24), 59 | NotAfter: time.Now().AddDate(DefaultYearCertDur, 0, 0), 60 | IsCA: true, 61 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 62 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 63 | BasicConstraintsValid: true, 64 | } 65 | } 66 | 67 | //nolint:funlen,cyclop 68 | func (c *CA) Init(rdir string, installca func(ca string)) error { 69 | if rdir != "" { 70 | rootDir = rdir 71 | } 72 | 73 | _ = os.MkdirAll(rootDir, os.ModePerm) 74 | 75 | slog.Info(fmt.Sprintf("USING ROOT CA as: %s", rootDir)) 76 | 77 | c.start = time.Now().Unix() 78 | 79 | //nolint:nestif 80 | if !exists(path.Join(rootDir, "ca.cer")) { 81 | slog.Info("Initiating new CA CERT") 82 | 83 | c.ca = createDefaultCert() 84 | 85 | priv, _ := rsa.GenerateKey(rand.Reader, DefaultRsaKeySize) 86 | 87 | pub := &priv.PublicKey 88 | 89 | caBytes, err := x509.CreateCertificate(rand.Reader, c.ca, c.ca, pub, priv) 90 | if err != nil { 91 | return fmt.Errorf("error creeating certificate: %w", err) 92 | } 93 | 94 | out := &bytes.Buffer{} 95 | 96 | if err = pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil { 97 | return fmt.Errorf("error encoding ca: %w", err) 98 | } 99 | 100 | cert := out.Bytes() 101 | 102 | err = os.WriteFile(path.Join(rootDir, "ca.cer"), cert, DefaultPerm) 103 | if err != nil { 104 | return fmt.Errorf("error writing ca.cer: %w", err) 105 | } 106 | 107 | keyOut, err := os.OpenFile(path.Join(rootDir, "ca.key"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, DefaultPerm) 108 | if err != nil { 109 | return fmt.Errorf("could not open ca.key: %w", err) 110 | } 111 | 112 | if err = pem.Encode( 113 | keyOut, 114 | &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { 115 | return fmt.Errorf("error encoding rsa key: %w", err) 116 | } 117 | 118 | _ = keyOut.Close() 119 | 120 | slog.Info("written key.pem") 121 | 122 | if installca != nil { 123 | slog.Info("Installing CA") 124 | installca(path.Join(rootDir, "ca.cer")) 125 | } 126 | } 127 | 128 | var err error 129 | 130 | // Load CA 131 | c.catls, err = tls.LoadX509KeyPair(path.Join(rootDir, "ca.cer"), path.Join(rootDir, "ca.key")) 132 | if err != nil { 133 | return fmt.Errorf("error lading x509 keypair: %w", err) 134 | } 135 | 136 | c.ca, err = x509.ParseCertificate(c.catls.Certificate[0]) 137 | if err != nil { 138 | return fmt.Errorf("error parsing x509 certificate: %w", err) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (c *CA) GenCertForDomain(domain string) ([]byte, []byte, error) { 145 | c.mtx.Lock() 146 | 147 | time.Sleep(time.Nanosecond) 148 | 149 | ser := time.Now().UnixNano() 150 | 151 | c.mtx.Unlock() 152 | 153 | cert := &x509.Certificate{ 154 | SerialNumber: big.NewInt(ser), 155 | Subject: pkix.Name{ 156 | Organization: []string{"Digital Circle"}, 157 | Country: []string{"BR"}, 158 | CommonName: domain, 159 | }, 160 | NotBefore: time.Now(), 161 | NotAfter: time.Now().AddDate(1, 0, 0), 162 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 163 | KeyUsage: x509.KeyUsageDigitalSignature, 164 | } 165 | 166 | const KeySize = 2048 167 | 168 | cert.DNSNames = append(cert.DNSNames, domain) 169 | 170 | priv, err := rsa.GenerateKey(rand.Reader, KeySize) 171 | if err != nil { 172 | return nil, nil, fmt.Errorf("error generating rsa key: %w", err) 173 | } 174 | 175 | pub := &priv.PublicKey 176 | 177 | // Sign the certificate 178 | certBytes, err := x509.CreateCertificate(rand.Reader, cert, c.ca, pub, c.catls.PrivateKey) 179 | if err != nil { 180 | return nil, nil, fmt.Errorf("error creating x509 cert: %w", err) 181 | } 182 | // Public key 183 | certBuffer := &bytes.Buffer{} 184 | if err = pem.Encode(certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}); err != nil { 185 | return nil, nil, fmt.Errorf("error encoding certificate %w", err) 186 | } 187 | 188 | keyBuffer := &bytes.Buffer{} 189 | 190 | if err = pem.Encode( 191 | keyBuffer, 192 | &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { 193 | return nil, nil, fmt.Errorf("error encoding rsa key: %w", err) 194 | } 195 | 196 | return keyBuffer.Bytes(), certBuffer.Bytes(), nil 197 | } 198 | 199 | func (c *CA) GenCertFilesForDomain(domain string, dir string) error { 200 | key, cert, err := c.GenCertForDomain(domain) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | _ = os.MkdirAll(dir, os.ModePerm) 206 | 207 | keyfile := path.Join(dir, domain+".key") 208 | 209 | certfile := path.Join(dir, domain+".cer") 210 | 211 | err = os.WriteFile(keyfile, key, DefaultPerm) 212 | if err != nil { 213 | return fmt.Errorf("error writing key: %w", err) 214 | } 215 | 216 | err = os.WriteFile(certfile, cert, DefaultPerm) 217 | if err != nil { 218 | return fmt.Errorf("error writing cert: %w", err) 219 | } 220 | 221 | return nil 222 | } 223 | 224 | func (c *CA) GenCertFilesForDomainInRootDir(d string) error { 225 | return c.GenCertFilesForDomain(d, rootDir) 226 | } 227 | 228 | func (c *CA) GetCertFromRoot(domain string) (tls.Certificate, error) { 229 | cer, err := tls.LoadX509KeyPair(path.Join(rootDir, domain+".cer"), path.Join(rootDir, domain+".key")) 230 | if err != nil { 231 | return tls.Certificate{}, fmt.Errorf("error loading cert: %w", err) 232 | } 233 | 234 | return cer, nil 235 | } 236 | 237 | func (c *CA) GetOrGenFromRoot(domain string) (tls.Certificate, error) { 238 | if exists(path.Join(rootDir, domain+".cer")) { 239 | return c.GetCertFromRoot(domain) 240 | } 241 | 242 | if err := c.GenCertFilesForDomainInRootDir(domain); err != nil { 243 | return tls.Certificate{}, err 244 | } 245 | 246 | return c.GetCertFromRoot(domain) 247 | } 248 | 249 | func (c *CA) GetCATLS() tls.Certificate { 250 | return c.catls 251 | } 252 | 253 | func (c *CA) CaCerBytes() ([]byte, error) { 254 | bs, err := os.ReadFile("ca.cer") 255 | if err != nil { 256 | return nil, fmt.Errorf("error reading ca.cer: %w", err) 257 | } 258 | 259 | return bs, nil 260 | } 261 | -------------------------------------------------------------------------------- /business/netmux/agent.go: -------------------------------------------------------------------------------- 1 | package netmux 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net" 10 | "runtime" 11 | 12 | "github.com/duxthemux/netmux/foundation/memstore" 13 | "github.com/duxthemux/netmux/foundation/metrics" 14 | "github.com/duxthemux/netmux/foundation/pipe" 15 | "github.com/duxthemux/netmux/foundation/wire" 16 | ) 17 | 18 | const ( 19 | MaxEventsBacklog = 24 20 | ) 21 | 22 | func helperError(err error) { 23 | if err != nil { 24 | _, file, line, ok := runtime.Caller(1) 25 | if ok { 26 | slog.Warn("error", "err", err, "file", file, "line", line) 27 | 28 | return 29 | } 30 | 31 | slog.Warn("error - could not find caller", "err", err) 32 | } 33 | } 34 | 35 | //---------------------------------------------------------------------------------------------------------------------- 36 | 37 | // IPAllocator will provide the capability of allocating an IP address in the local machine, while associating it with 38 | // the provided name. 39 | // Release will do the opposite and make that address available for a later call 40 | // Implementations are expected to do something like, adding an entry to the hosts file and associating a new 41 | // ip address to a local interface. 42 | type IPAllocator interface { 43 | GetIP(name ...string) (string, error) 44 | ReleaseIP(ip string) error 45 | } 46 | 47 | // PerfReporter allows reporting the amount of data copied from A to B and vice versa. 48 | type PerfReporter interface { 49 | AtoB(total int64) 50 | BotA(total int64) 51 | } 52 | 53 | //---------------------------------------------------------------------------------------------------------------------- 54 | 55 | type Agent struct { 56 | endpoint string 57 | cmdConn net.Conn 58 | events chan Event 59 | wire wire.Wire 60 | 61 | ipAllocator IPAllocator 62 | 63 | bridges *memstore.Map[Bridge] 64 | closers *memstore.Map[io.Closer] 65 | 66 | response chan CmdRawResponse 67 | reportMetricFactory metrics.Factory 68 | } 69 | 70 | //nolint:funlen,cyclop 71 | func (c *Agent) ServeProxy(ctx context.Context, bridge Bridge) error { 72 | if ctx.Err() != nil { 73 | return fmt.Errorf("context cancelled when serving prody: %w", ctx.Err()) 74 | } 75 | 76 | ctx, cancel := context.WithCancelCause(ctx) 77 | defer cancel(fmt.Errorf("deferred serveproxy ended")) 78 | 79 | lname := bridge.LocalAddr 80 | if lname == "" { 81 | lname = bridge.Name 82 | } 83 | 84 | ipAddr, err := c.ipAllocator.GetIP(lname) 85 | if err != nil { 86 | return fmt.Errorf("error allocating ip for bridge %s: %w", bridge.Name, err) 87 | } 88 | 89 | defer func() { 90 | if err := c.ipAllocator.ReleaseIP(ipAddr); err != nil { 91 | slog.Warn("error releasing ip addr", "bridge", bridge, "err", err) 92 | } 93 | }() 94 | 95 | bridge.LocalAddr = ipAddr 96 | 97 | listener, err := net.Listen(bridge.Family, bridge.FullLocalAddr()) 98 | if err != nil { 99 | return fmt.Errorf("error listening at %s while serving proxy: %w", bridge.FullLocalAddr(), err) 100 | } 101 | 102 | go func() { 103 | <-ctx.Done() 104 | helperIoClose(listener) 105 | }() 106 | 107 | for { 108 | cli, err := listener.Accept() 109 | if err != nil { 110 | return fmt.Errorf("error acceptin conn: %w", err) 111 | } 112 | 113 | lcon, err := c.Proxy(ProxyRequest{ 114 | Name: bridge.Name, 115 | Family: bridge.Family, 116 | Endpoint: bridge.FullContainerAddr(), 117 | }) 118 | if err != nil { 119 | return fmt.Errorf("error establishing proxy connection for %s: %w", bridge.Name, err) 120 | } 121 | 122 | piper := pipe.New(lcon, cli) 123 | 124 | if c.reportMetricFactory != nil { 125 | obsB := c.reportMetricFactory.New("proxy", "name", "from", "to"). 126 | Counter(map[string]string{ 127 | "name": bridge.Name, 128 | "from": bridge.FullLocalAddr(), 129 | "to": bridge.FullContainerAddr(), 130 | }) 131 | 132 | obsA := c.reportMetricFactory.New("proxy", "name", "from", "to"). 133 | Counter(map[string]string{ 134 | "name": bridge.Name, 135 | "from": bridge.FullContainerAddr(), 136 | "to": bridge.FullLocalAddr(), 137 | }) 138 | 139 | piper.BMetric = obsB.Add 140 | 141 | piper.AMetric = obsA.Add 142 | } 143 | 144 | go func() { 145 | if err := piper.Run(ctx); err != nil { 146 | slog.Warn("error while piping", "bridge", bridge.Name, "err", err) 147 | } 148 | }() 149 | } 150 | } 151 | 152 | func (c *Agent) ServeReverse(ctx context.Context, b Bridge) (func(err error), error) { 153 | return c.RevProxyListen(ctx, RevProxyListenRequest{ 154 | Name: b.Name, 155 | Family: b.Family, 156 | RemoteAddr: b.FullContainerAddr(), 157 | LocalAddr: b.FullLocalAddr(), 158 | }) 159 | } 160 | 161 | func (c *Agent) Proxy(req ProxyRequest) (io.ReadWriteCloser, error) { 162 | if req.Family == "" { 163 | return nil, fmt.Errorf("no family provided") 164 | } 165 | 166 | con, err := net.Dial(req.Family, c.endpoint) 167 | if err != nil { 168 | return nil, fmt.Errorf("error dialing: %w", err) 169 | } 170 | 171 | if err = c.wire.WriteJSON(con, CmdProxy, req); err != nil { 172 | return nil, fmt.Errorf("client.Proxy: error sending command: %w", err) 173 | } 174 | 175 | return con, nil 176 | } 177 | 178 | //nolint:funlen 179 | func (c *Agent) handleRevProxyWork(ctx context.Context, rpe RevProxyWorkRequest, rplreq RevProxyListenRequest) { //nolint:lll 180 | rconn, err := net.Dial("tcp", c.endpoint) 181 | if err != nil { 182 | slog.Warn("Agent.handleRevProxyWork:error dialing remote proxy", "err", err) 183 | 184 | return 185 | } 186 | 187 | defer helperIoClose(rconn) 188 | 189 | if err = c.wire.WriteJSON(rconn, CmdRevProxyWork, rpe); err != nil { 190 | slog.Warn("Agent.handleRevProxyWork: error writing to wire", "err", err) 191 | 192 | return 193 | } 194 | 195 | rperes := RevProxyWorkResponse{} 196 | if err = c.wire.ReadJSON(rconn, CmdRevProxyWork, &rperes); err != nil { 197 | slog.Warn("Agent.handleRevProxyWork: error receiving work confirmation", "err", err) 198 | 199 | return 200 | } 201 | 202 | lconn, err := net.Dial("tcp", rplreq.LocalAddr) 203 | if err != nil { 204 | slog.Warn("error opening local port", "err", err) 205 | 206 | return 207 | } 208 | 209 | defer helperIoClose(lconn) 210 | 211 | slog.Debug( 212 | "client.handleRevProxyWork: proxying:", 213 | "rconn-addr", 214 | rconn.RemoteAddr().String(), 215 | "lconn-addr", 216 | lconn.RemoteAddr().String()) 217 | 218 | ctx, cancel := context.WithCancelCause(ctx) 219 | defer cancel(fmt.Errorf("handleRevProxyWork ended")) 220 | 221 | piper := pipe.New(lconn, rconn) 222 | 223 | if c.reportMetricFactory != nil { 224 | obsB := c.reportMetricFactory.New("rev-proxy", "name", "from", "to"). 225 | Counter(map[string]string{ 226 | "name": rplreq.Name, 227 | "from": rplreq.RemoteAddr, 228 | "to": rplreq.LocalAddr, 229 | }) 230 | 231 | obsA := c.reportMetricFactory.New("rev-proxy", "name", "from", "to"). 232 | Counter(map[string]string{ 233 | "name": rplreq.Name, 234 | "from": rplreq.LocalAddr, 235 | "to": rplreq.RemoteAddr, 236 | }) 237 | 238 | piper.BMetric = obsB.Add 239 | 240 | piper.AMetric = obsA.Add 241 | } 242 | 243 | if err := piper.Run(ctx); err != nil { 244 | slog.Warn("error piping rev proxy work", "err", err) 245 | } 246 | } 247 | 248 | func (c *Agent) handleRevProxyListen(ctx context.Context, conn net.Conn, req RevProxyListenRequest) { 249 | for { 250 | rpe := RevProxyWorkRequest{} 251 | 252 | if err := c.wire.ReadJSON(conn, CmdRevProxyWork, &rpe); err != nil { 253 | slog.Warn("error receiving payload", "err", err) 254 | 255 | return 256 | } 257 | 258 | slog.Debug("client.handleRevProxyListen: got new conn", "addr", conn.RemoteAddr().String()) 259 | 260 | go c.handleRevProxyWork(ctx, rpe, req) 261 | } 262 | } 263 | 264 | func (c *Agent) RevProxyListen(ctx context.Context, req RevProxyListenRequest) (func(err error), error) { 265 | con, err := net.Dial("tcp", c.endpoint) 266 | if err != nil { 267 | return nil, fmt.Errorf("error dialing: %w", err) 268 | } 269 | 270 | if err = c.wire.WriteJSON(con, CmdRevProxyListen, req); err != nil { 271 | return nil, fmt.Errorf("error marshalling request: %w", err) 272 | } 273 | 274 | rplres := RevProxyListenResponse{} 275 | if err = c.wire.ReadJSON(con, CmdRevProxyListen, &rplres); err != nil { 276 | return nil, fmt.Errorf("error reading rev proxy listen confirmation") 277 | } 278 | 279 | go func() { 280 | c.handleRevProxyListen(ctx, con, req) 281 | }() 282 | 283 | return func(err error) { 284 | if err != nil { 285 | slog.Warn("error closing rev. proxy", "err", err) 286 | } 287 | 288 | helperIoClose(con) 289 | }, nil 290 | } 291 | 292 | func (c *Agent) Events() <-chan Event { 293 | return c.events 294 | } 295 | 296 | //nolint:cyclop,funlen 297 | func (c *Agent) handleControlMessages(ctx context.Context, cmdConn net.Conn) error { 298 | if ctx.Err() != nil { 299 | return fmt.Errorf("context closed when handling control messages: %w", ctx.Err()) 300 | } 301 | 302 | go func() { 303 | <-ctx.Done() 304 | helperIoClose(cmdConn) 305 | }() 306 | 307 | for { 308 | cmd, payload, err := c.wire.Read(cmdConn) 309 | if err != nil { 310 | slog.Warn("client: error reading from control conn", "err", err) 311 | helperIoClose(cmdConn) 312 | close(c.events) 313 | 314 | return fmt.Errorf("client: error reading from control conn: %w", err) 315 | } 316 | 317 | slog.Debug("Event received", "evt", string(payload)) 318 | 319 | switch cmd { 320 | case CmdEvents: 321 | if len(c.events) == MaxEventsBacklog { 322 | var oldest Event 323 | 324 | slog.Warn("client: events chan is full, will discard oldest one", "evt", oldest) 325 | 326 | <-c.events 327 | } 328 | 329 | anEvent := Event{} 330 | if err := json.Unmarshal(payload, &anEvent); err != nil { 331 | slog.Warn("client: error unmarshalling event", "err", err) 332 | } 333 | 334 | switch anEvent.EvtName { 335 | case EventBridgeAdd: 336 | c.bridges.Add(anEvent.Bridge) 337 | case EventBridgeDel: 338 | c.bridges.Del(anEvent.Bridge.Name) 339 | 340 | if closer := c.closers.Get(anEvent.Bridge.Name); closer != nil { 341 | helperIoClose(closer) 342 | } 343 | case EventBridgeUp: 344 | if closer := c.closers.Get(anEvent.Bridge.Name); closer != nil { 345 | helperIoClose(closer) 346 | } 347 | 348 | c.bridges.Del(anEvent.Bridge.Name) 349 | c.bridges.Add(anEvent.Bridge) 350 | } 351 | 352 | c.events <- anEvent 353 | default: 354 | c.response <- CmdRawResponse{ 355 | Cmd: cmd, 356 | Pl: payload, 357 | } 358 | } 359 | } 360 | } 361 | 362 | type AgentOpts func(a *Agent) 363 | 364 | func AgentWithMetrics(m metrics.Factory) AgentOpts { 365 | return func(a *Agent) { 366 | if m != nil { 367 | a.reportMetricFactory = m 368 | } 369 | } 370 | } 371 | 372 | func NewAgent(ctx context.Context, endponit string, ipAllocator IPAllocator, opts ...AgentOpts) (*Agent, error) { 373 | if ctx.Err() != nil { 374 | return nil, fmt.Errorf("context cancelled when creating agent: %w", ctx.Err()) 375 | } 376 | 377 | localWire := wire.Wire{} 378 | 379 | cmdConn, err := net.Dial("tcp", endponit) 380 | if err != nil { 381 | return nil, fmt.Errorf("error dialing endpoint: %w", err) 382 | } 383 | 384 | go func() { 385 | <-ctx.Done() 386 | helperIoClose(cmdConn) 387 | }() 388 | 389 | cmdConnControlRequest := CmdConnControlRequest{} 390 | if err = localWire.WriteJSON(cmdConn, CmdControl, cmdConnControlRequest); err != nil { 391 | return nil, fmt.Errorf("error opening control conn: %w", err) 392 | } 393 | 394 | cmdConnControlResponse := CmdConnControlResponse{} 395 | if err = localWire.ReadJSON(cmdConn, CmdControl, &cmdConnControlResponse); err != nil { 396 | return nil, fmt.Errorf("error opening control conn: %w", err) 397 | } 398 | 399 | ret := &Agent{ 400 | wire: localWire, 401 | endpoint: endponit, 402 | cmdConn: cmdConn, 403 | events: make(chan Event, MaxEventsBacklog), 404 | response: make(chan CmdRawResponse), 405 | bridges: memstore.New[Bridge](), 406 | closers: memstore.New[io.Closer](), 407 | ipAllocator: ipAllocator, 408 | } 409 | 410 | for _, opt := range opts { 411 | opt(ret) 412 | } 413 | 414 | go func(ctx context.Context) { 415 | helperError(ret.handleControlMessages(ctx, cmdConn)) 416 | }(ctx) 417 | 418 | return ret, nil 419 | } 420 | -------------------------------------------------------------------------------- /business/netmux/model.go: -------------------------------------------------------------------------------- 1 | package netmux 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const ( 10 | DirectionL2C = "L2C" 11 | DirectionC2L = "C2L" 12 | 13 | FamilyTCP = "tcp" 14 | FamilyUpd = "upd" 15 | 16 | EventBridgeAdd = "bridge-add" 17 | EventBridgeDel = "bridge-del" 18 | EventBridgeUp = "bridge-up" 19 | ) 20 | 21 | func CmdToString(cmdUint16 uint16) string { 22 | switch cmdUint16 { 23 | case CmdUnknown: 24 | return "Unknown" 25 | case CmdControl: 26 | return "Control" 27 | case CmdProxy: 28 | return "proxy" 29 | case CmdEvents: 30 | return "events" 31 | case CmdRevProxyListen: 32 | return "revproxy-listen" 33 | case CmdRevProxyWork: 34 | return "revproxy-work" 35 | default: 36 | return fmt.Sprintf("code %d now known", cmdUint16) 37 | } 38 | } 39 | 40 | type Bridge struct { 41 | Namespace string `json:"namespace" yaml:"namespace"` 42 | Name string `json:"name,omitempty" yaml:"name"` 43 | LocalAddr string `json:"localAddr,omitempty" yaml:"localAddr"` 44 | LocalPort string `json:"localPort,omitempty" yaml:"localPort"` 45 | ContainerAddr string `json:"containerAddr,omitempty" yaml:"containerAddr"` 46 | ContainerPort string `json:"containerPort,omitempty" yaml:"containerPort"` 47 | Direction string `json:"direction,omitempty" yaml:"direction"` 48 | Family string `json:"family,omitempty" yaml:"family"` 49 | } 50 | 51 | func (b *Bridge) FullLocalAddr() string { 52 | return fmt.Sprintf("%s:%s", b.LocalAddr, b.LocalPort) 53 | } 54 | 55 | func (b *Bridge) FullContainerAddr() string { 56 | return fmt.Sprintf("%s:%s", b.ContainerAddr, b.ContainerPort) 57 | } 58 | 59 | func (b *Bridge) LocalName() string { 60 | if b.Namespace != "" { 61 | return fmt.Sprintf("%s.%s", b.Name, b.Namespace) 62 | } 63 | 64 | return b.Name 65 | } 66 | 67 | func (b *Bridge) Validate() error { 68 | if b.Name == "" { 69 | return fmt.Errorf("invalid name") 70 | } 71 | 72 | if b.LocalAddr == "" { 73 | return fmt.Errorf("invalid local address") 74 | } 75 | 76 | if b.LocalPort == "" { 77 | return fmt.Errorf("invalid local port") 78 | } 79 | 80 | if b.ContainerAddr == "" { 81 | return fmt.Errorf("invalid container address") 82 | } 83 | 84 | if b.ContainerPort == "" { 85 | return fmt.Errorf("invalid container port") 86 | } 87 | 88 | if b.Direction != DirectionL2C && b.Direction != DirectionC2L { 89 | return fmt.Errorf("invalid direction") 90 | } 91 | 92 | if b.Family != FamilyTCP && b.Family != FamilyUpd { 93 | return fmt.Errorf("invalid family") 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (b *Bridge) String() string { 100 | return fmt.Sprintf("%v: %v %v=>%v %v", b.Name, b.Family, b.FullLocalAddr(), b.FullContainerAddr(), b.Direction) 101 | } 102 | 103 | const ( 104 | CmdUnknown uint16 = iota 105 | CmdControl 106 | CmdEvents 107 | CmdProxy 108 | CmdRevProxyListen 109 | CmdRevProxyWork 110 | ) 111 | 112 | type Message struct { 113 | ID int64 `json:"id"` 114 | ReplyTo int64 `json:"replyTo"` 115 | Err string `json:"err"` 116 | } 117 | 118 | type CmdRawResponse struct { 119 | Cmd uint16 120 | Pl []byte 121 | } 122 | 123 | func (c *CmdRawRequest) Read(res any) error { 124 | if err := json.Unmarshal(c.Pl, res); err != nil { 125 | return fmt.Errorf("error unmarshalling cmd: %w", err) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | type CmdRawRequest struct { 132 | Cmd uint16 133 | Pl []byte 134 | } 135 | 136 | func (c *CmdRawRequest) Write(req any) error { 137 | bs, err := json.Marshal(req) 138 | if err != nil { 139 | return fmt.Errorf("error unmarshalling cmd: %w", err) 140 | } 141 | 142 | c.Pl = bs 143 | 144 | return nil 145 | } 146 | 147 | type CmdConnControlRequest struct { 148 | Message 149 | } 150 | type CmdConnControlResponse struct { 151 | Message 152 | } 153 | 154 | type NoopMessage struct { 155 | Message 156 | } 157 | 158 | type PingRequest struct { 159 | Message 160 | CreatedAt time.Time `json:"createdAt"` 161 | } 162 | 163 | type PingResponse struct { 164 | Message 165 | CreatedAt time.Time `json:"createdAt"` 166 | RepliedAt time.Time `json:"repliedAtt"` 167 | } 168 | 169 | type ProxyRequest struct { 170 | Message `json:"message"` 171 | Name string `json:"name" yaml:"name"` 172 | Family string `json:"family,omitempty"` 173 | Endpoint string `json:"endpoint,omitempty"` 174 | } 175 | 176 | type ProxyResponse struct { 177 | Message 178 | } 179 | 180 | type RevProxyListenRequest struct { 181 | Message 182 | Name string `json:"name" yaml:"name"` 183 | Family string `json:"family,omitempty"` 184 | RemoteAddr string `json:"endpoint,omitempty"` 185 | LocalAddr string `json:"localAddr,omitempty"` 186 | } 187 | 188 | type RevProxyListenResponse struct { 189 | Message 190 | } 191 | 192 | type RevProxyWRequest struct { 193 | Message 194 | Family string `json:"family,omitempty"` 195 | Endpoint string `json:"endpoint,omitempty"` 196 | LocalEndpoint string `json:"localEndpoint,omitempty"` 197 | } 198 | 199 | type RevProxyEvent struct { 200 | Message 201 | ID string `json:"id,omitempty"` 202 | } 203 | 204 | type RevProxyWorkRequest struct { 205 | Message 206 | ID string `json:"id,omitempty"` 207 | } 208 | 209 | type RevProxyWorkResponse struct { 210 | Message 211 | ID int `json:"id,omitempty"` 212 | } 213 | 214 | type EventRequest struct { 215 | Message 216 | } 217 | 218 | type EventResponse struct { 219 | Message 220 | } 221 | 222 | type Event struct { 223 | EvtName string `json:"evtName,omitempty"` 224 | Bridge Bridge `json:"bridge"` 225 | } 226 | 227 | func (e Event) String() string { 228 | return fmt.Sprintf("%#v", e) 229 | } 230 | -------------------------------------------------------------------------------- /business/networkallocator/dnsallocator/dnsallocator.go: -------------------------------------------------------------------------------- 1 | package dnsallocator 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "log/slog" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | type DNSEntry struct { 14 | Addr string 15 | Names []string 16 | Comment string 17 | } 18 | 19 | type DNSEntries []DNSEntry 20 | 21 | const DefaultFilePerm = 0o600 22 | 23 | func (d DNSEntries) FindByIP(ip string) DNSEntry { 24 | for _, v := range d { 25 | if v.Addr == ip { 26 | return v 27 | } 28 | } 29 | 30 | return DNSEntry{} 31 | } 32 | 33 | func (d DNSEntries) FindByName(name string) DNSEntry { 34 | for _, v := range d { 35 | for _, n := range v.Names { 36 | if n == name { 37 | return v 38 | } 39 | } 40 | } 41 | 42 | return DNSEntry{} 43 | } 44 | 45 | func (e *DNSEntry) Equals(dnsEntry DNSEntry) bool { 46 | if e.Addr != dnsEntry.Addr { 47 | return false 48 | } 49 | 50 | if len(e.Names) != len(dnsEntry.Names) { 51 | return false 52 | } 53 | 54 | for i := range e.Names { 55 | if e.Names[i] != dnsEntry.Names[i] { 56 | return false 57 | } 58 | } 59 | 60 | return e.Comment == dnsEntry.Comment 61 | } 62 | 63 | func (e *DNSEntry) String() string { 64 | hosts := strings.Join(e.Names, " ") 65 | 66 | return fmt.Sprintf(`%s %s #%s`, e.Addr, hosts, e.Comment) 67 | } 68 | 69 | func (e *DNSEntry) Load(s string) { 70 | parts := strings.Fields(s) 71 | if len(parts) < 1 { 72 | return 73 | } 74 | 75 | e.Addr = parts[0] 76 | 77 | for index := 1; index < len(parts); index++ { 78 | if strings.HasPrefix(parts[index], "#") { 79 | parts[index] = parts[index][1:] 80 | comment := strings.Join(parts[index:], " ") 81 | e.Comment = comment 82 | 83 | return 84 | } 85 | 86 | e.Names = append(e.Names, parts[index]) 87 | } 88 | } 89 | 90 | func (e *DNSEntry) CommentMatches(s string) bool { 91 | return strings.Contains(e.Comment, s) 92 | } 93 | 94 | type DNSAllocator struct { 95 | fname string 96 | entries DNSEntries 97 | } 98 | 99 | func (m *DNSAllocator) LoadBytes(bs []byte) { 100 | fileScanner := bufio.NewScanner(bytes.NewReader(bs)) 101 | for fileScanner.Scan() { 102 | if len(fileScanner.Text()) < 1 { 103 | continue 104 | } 105 | 106 | hosyEntry := DNSEntry{} 107 | 108 | hosyEntry.Load(fileScanner.Text()) 109 | m.entries = append(m.entries, hosyEntry) 110 | } 111 | } 112 | 113 | func (m *DNSAllocator) Bytes() []byte { 114 | buf := &bytes.Buffer{} 115 | 116 | for _, e := range m.entries { 117 | l := e.String() 118 | buf.WriteString(l) 119 | buf.WriteString("\n") 120 | } 121 | 122 | return buf.Bytes() 123 | } 124 | 125 | func (m *DNSAllocator) RemoveByComment(comment string, exception string) error { 126 | var hostEntries []DNSEntry 127 | 128 | err := m.Load() 129 | if err != nil { 130 | return fmt.Errorf("failed to load hosts file: %w", err) 131 | } 132 | 133 | for _, entry := range m.entries { 134 | for _, n := range entry.Names { 135 | if n == exception { 136 | hostEntries = append(hostEntries, entry) 137 | 138 | goto leaveLoop 139 | } 140 | } 141 | 142 | if !entry.CommentMatches(comment) { 143 | hostEntries = append(hostEntries, entry) 144 | } 145 | 146 | leaveLoop: 147 | } 148 | 149 | m.entries = hostEntries 150 | 151 | return m.unSyncSave() 152 | } 153 | 154 | func (m *DNSAllocator) RemoveByName(name string) error { 155 | var hostEntries []DNSEntry //nolint:prealloc 156 | 157 | err := m.Load() 158 | if err != nil { 159 | return fmt.Errorf("failed to load hosts file: %w", err) 160 | } 161 | 162 | found := false 163 | 164 | for _, entry := range m.entries { 165 | for _, n := range entry.Names { 166 | if n == name { 167 | slog.Debug(fmt.Sprintf("Removing hosts entry: %s", entry.String())) 168 | 169 | found = true 170 | 171 | break 172 | } 173 | } 174 | 175 | hostEntries = append(hostEntries, entry) 176 | } 177 | 178 | if found { 179 | m.entries = hostEntries 180 | 181 | return m.unSyncSave() 182 | } 183 | 184 | return nil 185 | } 186 | 187 | func (m *DNSAllocator) Equals(dnsAllocator *DNSAllocator) bool { 188 | if len(m.entries) != len(dnsAllocator.entries) { 189 | return false 190 | } 191 | 192 | for i := range m.entries { 193 | if !m.entries[i].Equals(dnsAllocator.entries[i]) { 194 | return false 195 | } 196 | } 197 | 198 | return true 199 | } 200 | 201 | func (m *DNSAllocator) Load() error { 202 | slog.Debug(fmt.Sprintf("Loading hosts from %s", m.fname)) 203 | fileBytes, err := os.ReadFile(m.fname) 204 | 205 | switch { 206 | case err != nil && !errors.Is(err, os.ErrNotExist): 207 | return fmt.Errorf("failed to read hosts file: %w", err) 208 | case err != nil && errors.Is(err, os.ErrNotExist): 209 | return nil 210 | default: 211 | m.entries = make([]DNSEntry, 0) 212 | m.LoadBytes(fileBytes) 213 | 214 | return nil 215 | } 216 | } 217 | 218 | func (m *DNSAllocator) unSyncSave() error { 219 | // slog.Debug(fmt.Sprintff("Saving hosts to %s", m.fname) 220 | err := os.WriteFile(m.fname, m.Bytes(), DefaultFilePerm) 221 | if err != nil { 222 | panic(err) 223 | } 224 | 225 | return nil 226 | } 227 | 228 | func (m *DNSAllocator) Add(adr string, names []string, comment string) error { 229 | err := m.Load() 230 | if err != nil { 231 | return fmt.Errorf("failed to load hosts file: %w", err) 232 | } 233 | 234 | entry := DNSEntry{ 235 | Addr: adr, 236 | Names: names, 237 | Comment: "src: netmux " + comment, 238 | } 239 | 240 | slog.Debug(fmt.Sprintf("Adding hosts entry: %s", entry.String())) 241 | 242 | m.entries = append(m.entries, entry) 243 | 244 | return m.unSyncSave() 245 | } 246 | 247 | func (m *DNSAllocator) CleanUp(exception string) error { 248 | return m.RemoveByComment("src: netmux", exception) 249 | } 250 | 251 | func (m *DNSAllocator) Entries() DNSEntries { 252 | return m.entries 253 | } 254 | 255 | type Opts func(h *DNSAllocator) 256 | 257 | //nolint:gochecknoglobals 258 | var WithFile = func(f string) func(h *DNSAllocator) { 259 | return func(h *DNSAllocator) { 260 | h.fname = f 261 | } 262 | } 263 | 264 | func New(opts ...Opts) *DNSAllocator { 265 | ret := new(DNSAllocator) 266 | ret.fname = Fname 267 | 268 | for _, o := range opts { 269 | o(ret) 270 | } 271 | 272 | return ret 273 | } 274 | -------------------------------------------------------------------------------- /business/networkallocator/dnsallocator/dnsallocator_linux.go: -------------------------------------------------------------------------------- 1 | // build +linux 2 | package dnsallocator 3 | 4 | const Fname = "/etc/hosts" 5 | -------------------------------------------------------------------------------- /business/networkallocator/dnsallocator/dnsallocator_windows.go: -------------------------------------------------------------------------------- 1 | // build +windows 2 | package dnsallocator 3 | 4 | const Fname = `c:\Windows\System32\drivers\etc\hosts` 5 | -------------------------------------------------------------------------------- /business/networkallocator/dnsallocator/dsnallocator_darwin.go: -------------------------------------------------------------------------------- 1 | // build +darwin 2 | package dnsallocator 3 | 4 | const Fname = "/etc/hosts" 5 | -------------------------------------------------------------------------------- /business/networkallocator/ipallocator/cidrcalc.go: -------------------------------------------------------------------------------- 1 | package ipallocator 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // GetIPV4Addrs will return a slice of strings, each one is an IP Address, part of the CIDR described. 10 | func GetIPV4Addrs(net string, skipGw bool, skipNetwork bool) ([]string, error) { 11 | net = strings.TrimSpace(net) 12 | parts := strings.Split(net, "/") 13 | if len(parts) != 2 { 14 | return nil, fmt.Errorf("cidr %s is not in the format A.B.C.D/MASK", net) 15 | } 16 | 17 | base := parts[0] 18 | maskStr := parts[1] 19 | 20 | mask, err := strconv.Atoi(maskStr) 21 | if err != nil { 22 | return nil, fmt.Errorf("mask is not int: %s", err) 23 | } 24 | 25 | if mask > 32 { 26 | return nil, fmt.Errorf("mask cannot be bigger than 32") 27 | } 28 | 29 | parts = strings.Split(base, ".") 30 | if len(parts) != 4 { 31 | return nil, fmt.Errorf("could not find 4 octets in %s", base) 32 | } 33 | 34 | a, b, c, d := parts[0], parts[1], parts[2], parts[3] 35 | 36 | aint, err := strconv.Atoi(a) 37 | if err != nil { 38 | return nil, fmt.Errorf("error parsing octet A(%s): %w", a, err) 39 | } 40 | 41 | bint, err := strconv.Atoi(b) 42 | if err != nil { 43 | return nil, fmt.Errorf("error parsing octet B(%s): %w", b, err) 44 | } 45 | 46 | cint, err := strconv.Atoi(c) 47 | if err != nil { 48 | return nil, fmt.Errorf("error parsing octet C(%s): %w", c, err) 49 | } 50 | 51 | dint, err := strconv.Atoi(d) 52 | if err != nil { 53 | return nil, fmt.Errorf("error parsing octet D(%s): %w", d, err) 54 | } 55 | 56 | ret := make([]string, 0) 57 | 58 | for count := 1 << (32 - mask); count > 0; count-- { 59 | 60 | if aint > 255 { 61 | return nil, fmt.Errorf("invalid entry") 62 | } 63 | 64 | if (dint != 0 || !skipGw) && (dint != 255 || !skipNetwork) { 65 | ret = append(ret, fmt.Sprintf("%v.%v.%v.%v", aint, bint, cint, dint)) 66 | } 67 | 68 | dint++ 69 | if dint > 255 { 70 | dint = 0 71 | cint++ 72 | if cint > 255 { 73 | cint = 0 74 | bint++ 75 | if bint > 255 { 76 | aint++ 77 | bint = 0 78 | } 79 | } 80 | } 81 | 82 | } 83 | 84 | return ret, nil 85 | } 86 | -------------------------------------------------------------------------------- /business/networkallocator/ipallocator/cidrcalc_test.go: -------------------------------------------------------------------------------- 1 | package ipallocator_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/duxthemux/netmux/business/networkallocator/ipallocator" 9 | ) 10 | 11 | func TestGetAddrs(t *testing.T) { 12 | strs, err := ipallocator.GetIPV4Addrs("10.0.0.0/31", false, false) 13 | assert.NoError(t, err) 14 | assert.Equal(t, len(strs), 2) 15 | assert.Equal(t, strs[1], "10.0.0.1") 16 | } 17 | 18 | func TestGetAddrsNM24(t *testing.T) { 19 | strs, err := ipallocator.GetIPV4Addrs("10.0.0.0/24", false, false) 20 | assert.NoError(t, err) 21 | assert.Equal(t, len(strs), 256) 22 | assert.Equal(t, strs[1], "10.0.0.1") 23 | } 24 | 25 | func TestGetAddrsNM23(t *testing.T) { 26 | strs, err := ipallocator.GetIPV4Addrs("10.0.0.0/23", false, false) 27 | assert.NoError(t, err) 28 | assert.Equal(t, len(strs), 512) 29 | assert.Equal(t, strs[1], "10.0.0.1") 30 | } 31 | 32 | func TestGetAddrsNM23WoGateways(t *testing.T) { 33 | strs, err := ipallocator.GetIPV4Addrs("10.0.0.0/23", true, false) 34 | assert.NoError(t, err) 35 | assert.Equal(t, len(strs), 510) 36 | assert.Equal(t, strs[0], "10.0.0.1") 37 | } 38 | 39 | func TestGetAddrsNM23WoNetwork(t *testing.T) { 40 | strs, err := ipallocator.GetIPV4Addrs("10.0.0.0/23", false, true) 41 | assert.NoError(t, err) 42 | assert.Equal(t, len(strs), 510) 43 | assert.Equal(t, strs[0], "10.0.0.0") 44 | assert.Equal(t, strs[255], "10.0.1.0") 45 | assert.Equal(t, strs[509], "10.0.1.254") 46 | } 47 | 48 | func TestGetAddrsNM23WoNetworkNorGW(t *testing.T) { 49 | strs, err := ipallocator.GetIPV4Addrs("10.0.0.0/23", true, true) 50 | assert.NoError(t, err) 51 | assert.Equal(t, len(strs), 508) 52 | assert.Equal(t, strs[0], "10.0.0.1") 53 | assert.Equal(t, strs[254], "10.0.1.1") 54 | assert.Equal(t, strs[507], "10.0.1.254") 55 | } 56 | -------------------------------------------------------------------------------- /business/networkallocator/ipallocator/ipallocator.go: -------------------------------------------------------------------------------- 1 | package ipallocator 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "sync" 7 | 8 | shell2 "github.com/duxthemux/netmux/business/shell" 9 | ) 10 | 11 | type IPAllocator struct { 12 | shell shell2.Shell 13 | sync.Mutex 14 | iface string 15 | freeAddrs []string 16 | allocAddrs []string 17 | } 18 | 19 | func (i *IPAllocator) Allocate() (string, error) { 20 | i.Lock() 21 | defer i.Unlock() 22 | 23 | if len(i.freeAddrs) == 0 { 24 | return "", fmt.Errorf("no more free addresses") 25 | } 26 | 27 | addr := i.freeAddrs[0] 28 | 29 | i.freeAddrs = i.freeAddrs[1:] 30 | 31 | i.allocAddrs = append(i.allocAddrs, addr) 32 | 33 | err := i.shell.IfconfigAddAlias(i.iface, addr, "255.255.255.0", "10.0.0.1") 34 | if err != nil { 35 | i.freeAddrs = append(i.freeAddrs, addr) 36 | 37 | return "", fmt.Errorf("error adding alias: %w", err) 38 | } 39 | 40 | return addr, nil 41 | } 42 | 43 | func (i *IPAllocator) Release(ipAddress string) error { 44 | i.Lock() 45 | defer i.Unlock() 46 | 47 | err := i.shell.IfconfigRemAlias(i.iface, ipAddress) 48 | if err != nil { 49 | return fmt.Errorf("error removing alias: %w", err) 50 | } 51 | 52 | i.freeAddrs = append(i.freeAddrs, ipAddress) 53 | 54 | for idx, addr := range i.allocAddrs { 55 | if addr == ipAddress { 56 | i.allocAddrs[idx] = i.allocAddrs[len(i.allocAddrs)-1] 57 | i.allocAddrs = i.allocAddrs[:len(i.allocAddrs)-1] 58 | 59 | return nil 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (i *IPAllocator) ReleaseAll(fnCleanupEach func(s string) error) error { 67 | for _, addr := range i.allocAddrs { 68 | if err := i.Release(addr); err != nil { 69 | return fmt.Errorf("error releasing ip address: %w", err) 70 | } 71 | 72 | if err := fnCleanupEach(addr); err != nil { 73 | return fmt.Errorf("error cleaning up leftovers: %w", err) 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (i *IPAllocator) CleanUp() { 81 | for _, addr := range i.freeAddrs { 82 | err := i.Release(addr) 83 | if err != nil { 84 | slog.Debug(fmt.Sprintf("Cleanning ip - error for ip %s: %s", addr, err.Error())) 85 | } 86 | } 87 | } 88 | 89 | func New(iface string, cidr string) (*IPAllocator, error) { 90 | ret := &IPAllocator{ 91 | shell: shell2.New(), 92 | iface: iface, 93 | freeAddrs: []string{}, 94 | } 95 | 96 | freeAddrs, err := GetIPV4Addrs(cidr, true, true) 97 | if err != nil { 98 | return nil, fmt.Errorf("error allocating network addresses: %w", err) 99 | } 100 | 101 | ret.freeAddrs = freeAddrs 102 | 103 | return ret, nil 104 | } 105 | -------------------------------------------------------------------------------- /business/networkallocator/networkallocator.go: -------------------------------------------------------------------------------- 1 | package networkallocator 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/duxthemux/netmux/business/networkallocator/dnsallocator" 10 | "github.com/duxthemux/netmux/business/networkallocator/ipallocator" 11 | ) 12 | 13 | type NetworkAllocator struct { 14 | sync.Mutex 15 | ipAllocator *ipallocator.IPAllocator 16 | dnsAllocator *dnsallocator.DNSAllocator 17 | } 18 | 19 | func (n *NetworkAllocator) GetIP(names ...string) (string, error) { 20 | n.Lock() 21 | defer n.Unlock() 22 | 23 | for _, name := range names { 24 | existingEntry := n.dnsAllocator.Entries().FindByName(name) 25 | if len(existingEntry.Names) > 0 { 26 | if err := n.dnsAllocator.RemoveByName(name); err != nil { 27 | return "", fmt.Errorf("error removing dns entry %w", err) 28 | } 29 | } 30 | } 31 | 32 | ipaddr, err := n.ipAllocator.Allocate() 33 | if err != nil { 34 | return "", fmt.Errorf("error allocating ip address: %w", err) 35 | } 36 | 37 | if err := n.dnsAllocator.Add(ipaddr, names, "name: "+strings.Join(names, ",")+" ip: "+ipaddr); err != nil { 38 | return "", fmt.Errorf("error allocating name: %w", err) 39 | } 40 | 41 | return ipaddr, nil 42 | } 43 | 44 | func (n *NetworkAllocator) ReleaseIP(ipAddress string) error { 45 | n.Lock() 46 | defer n.Unlock() 47 | 48 | slog.Debug("releasing ipAddress address", "ipAddress", ipAddress) 49 | 50 | err := n.dnsAllocator.RemoveByComment("ip: "+ipAddress, "") 51 | if err != nil { 52 | return fmt.Errorf("error removing dns entry: %w", err) 53 | } 54 | 55 | err = n.ipAllocator.Release(ipAddress) 56 | if err != nil { 57 | return fmt.Errorf("error releasing ipAddress address: %w", err) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (n *NetworkAllocator) CleanUp(exception string) error { 64 | if err := n.dnsAllocator.CleanUp(exception); err != nil { 65 | return fmt.Errorf("error cleanning up dns: %w", err) 66 | } 67 | 68 | n.ipAllocator.CleanUp() 69 | 70 | return nil 71 | } 72 | 73 | func (n *NetworkAllocator) DNSEntries() []dnsallocator.DNSEntry { 74 | return n.dnsAllocator.Entries() 75 | } 76 | 77 | func New(iface string, cidr string) (*NetworkAllocator, error) { 78 | slog.Debug("Creating NWAllocator", "iface", iface, "cidr", cidr) 79 | myIpallocator, err := ipallocator.New(iface, cidr) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | ret := &NetworkAllocator{ 85 | ipAllocator: myIpallocator, dnsAllocator: dnsallocator.New(), 86 | } 87 | 88 | err = ret.dnsAllocator.Load() 89 | if err != nil { 90 | return nil, fmt.Errorf("error loading dns entries: %w", err) 91 | } 92 | 93 | return ret, nil 94 | } 95 | -------------------------------------------------------------------------------- /business/portforwarder/portforwarder.go: -------------------------------------------------------------------------------- 1 | package portforwarder 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "os/exec" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/cenkalti/backoff" 18 | corev1 "k8s.io/api/core/v1" 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | "k8s.io/cli-runtime/pkg/genericclioptions" 21 | "k8s.io/client-go/kubernetes" 22 | "k8s.io/client-go/rest" 23 | "k8s.io/client-go/tools/clientcmd" 24 | "k8s.io/client-go/tools/portforward" 25 | "k8s.io/client-go/transport/spdy" 26 | 27 | "github.com/duxthemux/netmux/business/shell" 28 | ) 29 | 30 | type KubernetesInfo struct { 31 | Config string `json:"config,omitempty" yaml:"config,omitempty"` 32 | Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` 33 | Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"` 34 | Context string `json:"context,omitempty" yaml:"context,omitempty"` 35 | Port string `json:"port,omitempty" yaml:"port,omitempty"` 36 | Kubectl string `json:"kubectl" yaml:"kubectl"` 37 | User string `json:"user" yaml:"user"` 38 | } 39 | 40 | func (k KubernetesInfo) IsZeroValue() bool { 41 | return k.Config == "" || k.Namespace == "" || k.Context == "" || k.Endpoint == "" 42 | } 43 | 44 | type portForwardAPodRequest struct { 45 | // RestConfig is the kubernetes userconfig 46 | RestConfig *rest.Config 47 | // Pod is the selected pod for this port forwarding 48 | Pod corev1.Pod 49 | // LocalPort is the local port that will be selected to expose the PodPort 50 | LocalPort int 51 | // PodPort is the target port for the pod 52 | PodPort int 53 | // Steams configures where to write or read input from 54 | Streams genericclioptions.IOStreams 55 | // StopCh is the channel used to manage the port forward lifecycle 56 | StopCh <-chan struct{} 57 | // ReadyCh communicates when the tunnel is ready to receive traffic 58 | ReadyCh chan struct{} 59 | } 60 | 61 | type PortForwarder struct { 62 | portAllocationMx sync.Mutex 63 | stopCh chan struct{} 64 | Port int 65 | } 66 | 67 | func (p *PortForwarder) findAvailableLocalPort() (int, error) { 68 | p.portAllocationMx.Lock() 69 | defer p.portAllocationMx.Unlock() 70 | 71 | listener, err := net.Listen("tcp", ":0") //nolint:gosec 72 | if err != nil { 73 | return 0, fmt.Errorf("error dialing: %w", err) 74 | } 75 | 76 | tcpAddr, ok := listener.Addr().(*net.TCPAddr) 77 | if !ok { 78 | return 0, fmt.Errorf("address it not type net.TCPAddr: %#v", listener.Addr()) 79 | } 80 | 81 | _ = listener.Close() 82 | 83 | return tcpAddr.Port, nil 84 | } 85 | 86 | // resolveClientConfig will retrieve a restconfig, but considering files with 87 | // multiple contexts also. 88 | func resolveClientConfig(configFile string, context string) (*rest.Config, error) { 89 | //nolint:exhaustivestruct 90 | configLoadingRules := &clientcmd.ClientConfigLoadingRules{ //nolint:exhaustruct 91 | ExplicitPath: configFile, 92 | } 93 | //nolint:exhaustivestruct 94 | configOverrides := &clientcmd.ConfigOverrides{CurrentContext: context} //nolint:exhaustruct 95 | 96 | config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 97 | configLoadingRules, configOverrides).ClientConfig() 98 | if err != nil { 99 | return nil, fmt.Errorf("error loading kubeconfig: %w", err) 100 | } 101 | 102 | slog.Info(fmt.Sprintf("Impersonating: %s", config.Impersonate.UserName)) 103 | 104 | return config, nil 105 | } 106 | 107 | const PFWaitTimeout = time.Second * 5 108 | 109 | //nolint:funlen 110 | func (p *PortForwarder) Start(ctx context.Context, kinfo KubernetesInfo) error { 111 | switch { 112 | case kinfo.User != "": 113 | slog.Debug("calling port forward via sudo", "user", kinfo.User) 114 | return p.startSudo(ctx, kinfo) 115 | default: 116 | slog.Debug("calling port forward via api") 117 | return p.startApi(ctx, kinfo) 118 | } 119 | } 120 | 121 | func checkPodOrService(ctx context.Context, req *portForwardAPodRequest) error { 122 | if ctx.Err() != nil { 123 | return fmt.Errorf("error checking pod: %w", ctx.Err()) 124 | } 125 | 126 | clientset, err := kubernetes.NewForConfig(req.RestConfig) 127 | if err != nil { 128 | return fmt.Errorf("error creating kubernetes client: %w", err) 129 | } 130 | 131 | endpointsList, err := clientset.CoreV1().Endpoints(req.Pod.Namespace).List( 132 | ctx, metav1.ListOptions{ //nolint:exhaustruct,exhaustivestruct 133 | FieldSelector: "metadata.name=" + req.Pod.Name, 134 | }) 135 | if err != nil { 136 | return fmt.Errorf("unable to find service %s: %w", req.Pod.Name, err) 137 | } 138 | 139 | if len(endpointsList.Items) > 0 && len(endpointsList.Items[0].Subsets) > 0 && len(endpointsList.Items[0].Subsets[0].Addresses) > 0 { 140 | ipadddr := endpointsList.Items[0].Subsets[0].Addresses[0].IP 141 | 142 | pods, err := clientset.CoreV1().Pods(req.Pod.Namespace).List( 143 | ctx, metav1.ListOptions{ //nolint:exhaustruct,exhaustivestruct 144 | FieldSelector: "status.podIP=" + ipadddr, 145 | }) 146 | if err != nil { 147 | return fmt.Errorf( //nolint:goerr113 148 | "unable to find pod for service %s: %s", 149 | req.Pod.Name, err.Error()) 150 | } 151 | 152 | req.Pod.Name = pods.Items[0].Name 153 | return nil 154 | } 155 | 156 | return fmt.Errorf("could not resolve ip for endpoint %s", req.Pod.Name) 157 | } 158 | 159 | // portForwardAPod wil effectively do the port forward but to a pod. 160 | // If the PF is expected to be closed w a service, please consider 161 | // PFStart, it will resolve a pod from service and "pretend" PF 162 | // is being oriented to a service. 163 | func portForwardAPod(ctx context.Context, req *portForwardAPodRequest) error { 164 | if ctx.Err() != nil { 165 | return fmt.Errorf("context closed when port forwarding: %w", ctx.Err()) 166 | } 167 | 168 | path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", 169 | req.Pod.Namespace, req.Pod.Name) 170 | hostIP := strings.TrimLeft(req.RestConfig.Host, "htps:/") 171 | 172 | transport, upgrader, err := spdy.RoundTripperFor(req.RestConfig) 173 | if err != nil { 174 | return fmt.Errorf("error creating roundtripper: %w", err) 175 | } 176 | 177 | dialer := spdy.NewDialer( 178 | upgrader, 179 | &http.Client{Transport: transport}, //nolint:exhaustruct,exhaustivestruct 180 | http.MethodPost, 181 | &url.URL{Scheme: "https", Path: path, Host: hostIP}) //nolint:exhaustruct,exhaustivestruct 182 | 183 | portForwader, err := portforward.New(dialer, []string{ 184 | fmt.Sprintf("%d:%d", req.LocalPort, req.PodPort), 185 | }, 186 | req.StopCh, 187 | req.ReadyCh, 188 | req.Streams.Out, 189 | req.Streams.ErrOut) 190 | if err != nil { 191 | return fmt.Errorf("error creating portforward: %w", err) 192 | } 193 | 194 | err = portForwader.ForwardPorts() 195 | if err != nil { 196 | return fmt.Errorf("error forwarding ports: %w", err) 197 | } 198 | 199 | go func() { 200 | <-ctx.Done() 201 | portForwader.Close() 202 | }() 203 | 204 | return nil 205 | } 206 | 207 | func New() *PortForwarder { 208 | return &PortForwarder{} 209 | } 210 | 211 | func (p *PortForwarder) startSudo(ctx context.Context, kinfo KubernetesInfo) error { 212 | kubectlCmdTentative := "kubectl" 213 | if kinfo.Kubectl != "" { 214 | kubectlCmdTentative = kinfo.Kubectl 215 | } 216 | 217 | kubectlCmd, err := exec.LookPath(kubectlCmdTentative) 218 | if err != nil { 219 | return fmt.Errorf("kubectl not found in path: %w", err) 220 | } 221 | 222 | port, err := p.findAvailableLocalPort() 223 | if err != nil { 224 | return fmt.Errorf("could not allocate port: %w", err) 225 | } 226 | p.Port = port 227 | 228 | cmdLine := fmt.Sprintf("KUBECONFIG=%s %s port-forward --context=%s --namespace=%s %s %v:%s\n", 229 | kinfo.Config, 230 | kubectlCmd, 231 | kinfo.Context, 232 | kinfo.Namespace, 233 | kinfo.Endpoint, 234 | port, 235 | kinfo.Port) 236 | 237 | slog.Debug("port forward as sudo cmdline", "cmdline", cmdLine) 238 | 239 | sh := shell.New() 240 | shellWriter, err := sh.CmdAs(ctx, kinfo.User) 241 | if err != nil { 242 | return fmt.Errorf("could not create user shell: %w", err) 243 | } 244 | 245 | _, err = shellWriter.Write([]byte(cmdLine)) 246 | if err != nil { 247 | return fmt.Errorf("error piping stdin: %w", err) 248 | } 249 | 250 | bo := backoff.NewExponentialBackOff() 251 | bo.MaxElapsedTime = time.Second * 15 252 | 253 | if err := backoff.Retry(func() error { 254 | con, err := net.Dial("tcp", fmt.Sprintf("localhost:%v", port)) 255 | if err != nil { 256 | return err 257 | } 258 | 259 | _ = con.Close() 260 | return nil 261 | }, bo); err != nil { 262 | return fmt.Errorf("error confirming port is available: %w", err) 263 | } 264 | 265 | return nil 266 | } 267 | 268 | func (p *PortForwarder) startApi(ctx context.Context, kinfo KubernetesInfo) error { 269 | stopCh := make(chan struct{}, 1) 270 | p.stopCh = stopCh 271 | 272 | // use the current context in kubeconfig 273 | config, err := resolveClientConfig(kinfo.Config, kinfo.Context) 274 | if err != nil { 275 | return fmt.Errorf("error resolving client userconfig: %w", err) 276 | } 277 | 278 | lport, err := p.findAvailableLocalPort() 279 | if err != nil { 280 | return fmt.Errorf("error finding available local port: %w", err) 281 | } 282 | 283 | rport, err := strconv.Atoi(kinfo.Port) 284 | if err != nil { 285 | return fmt.Errorf("error converting port to int: %w", err) 286 | } 287 | 288 | pfreq := &portForwardAPodRequest{ 289 | RestConfig: config, 290 | Pod: corev1.Pod{ 291 | TypeMeta: metav1.TypeMeta{}, 292 | ObjectMeta: metav1.ObjectMeta{ 293 | Name: kinfo.Endpoint, 294 | Namespace: kinfo.Namespace, 295 | }, 296 | }, 297 | LocalPort: lport, 298 | PodPort: rport, 299 | Streams: genericclioptions.IOStreams{}, 300 | StopCh: nil, 301 | ReadyCh: nil, 302 | } 303 | 304 | pfreq.RestConfig = config 305 | 306 | // stopCh control the port forwarding lifecycle. When it gets closed the 307 | // port forward will terminate 308 | 309 | // readyCh communicate when the port forward is ready to get traffic 310 | readyCh := make(chan struct{}) 311 | errCh := make(chan error) 312 | // stream is used to tell the port forwarder where to place its output or 313 | // where to expect input if needed. For the port forwarding we just need 314 | // the output eventually 315 | stream := genericclioptions.IOStreams{ 316 | In: os.Stdin, 317 | Out: os.Stdout, 318 | ErrOut: os.Stderr, 319 | } 320 | 321 | pfreq.Streams = stream 322 | pfreq.ReadyCh = readyCh 323 | pfreq.StopCh = stopCh 324 | err = checkPodOrService(ctx, pfreq) 325 | 326 | if err != nil { 327 | return fmt.Errorf("error checking pod or service: %w", err) 328 | } 329 | 330 | go func() { 331 | err := portForwardAPod(ctx, pfreq) 332 | if err != nil { 333 | slog.Warn("error while port forwarding", "err", err) 334 | errCh <- err 335 | } 336 | }() 337 | 338 | select { 339 | case err = <-errCh: 340 | if err != nil { 341 | return fmt.Errorf("error port forwarding: %w", err) 342 | } 343 | case <-time.After(PFWaitTimeout): 344 | case <-readyCh: 345 | } 346 | slog.Info("Port forwarding is ready to get traffic. have fun!") 347 | 348 | p.Port = lport 349 | 350 | return nil 351 | } 352 | -------------------------------------------------------------------------------- /business/shell/cmd_all.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func shStdio(cmdline string) error { 11 | cmd := sh(cmdline) 12 | cmd.Stdout = os.Stdout 13 | cmd.Stderr = os.Stderr 14 | 15 | err := cmd.Run() 16 | if err != nil { 17 | return fmt.Errorf("error running %s: %w", cmdline, err) 18 | } 19 | 20 | return nil 21 | } 22 | 23 | type Shell interface { 24 | IfconfigAddAlias(iface string, ipaddr string, netmask string, gw string) error 25 | IfconfigRemAlias(iface string, ipaddr string) error 26 | CmdAs(ctx context.Context, user string) (io.Writer, error) 27 | } 28 | -------------------------------------------------------------------------------- /business/shell/cmd_darwin.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | type darwinShell struct{} 12 | 13 | func (w *darwinShell) IfconfigAddAlias(iface string, ipaddr string, _ string, _ string) error { 14 | return shStdio(fmt.Sprintf("ifconfig %s alias %s", iface, ipaddr)) 15 | } 16 | 17 | func (w *darwinShell) IfconfigRemAlias(iface string, ipaddr string) error { 18 | return shStdio(fmt.Sprintf("ifconfig %s -alias %s", iface, ipaddr)) 19 | } 20 | 21 | func (w *darwinShell) CmdAs(ctx context.Context, user string) (io.Writer, error) { 22 | 23 | cmd := exec.CommandContext(ctx, "su", "-", user) 24 | cmd.Stdout = os.Stdout 25 | cmd.Stderr = os.Stderr 26 | writer, err := cmd.StdinPipe() 27 | if err != nil { 28 | return nil, fmt.Errorf("error piping stdin: %w", err) 29 | } 30 | 31 | if err = cmd.Start(); err != nil { 32 | return nil, fmt.Errorf("error starting port forward command: %w", err) 33 | } 34 | 35 | return writer, nil 36 | 37 | } 38 | 39 | //nolint:ireturn,nolintlint 40 | func New() Shell { 41 | return &darwinShell{} 42 | } 43 | 44 | func sh(cmdline string) *exec.Cmd { 45 | cmd := exec.Command("sh", "-c", cmdline) 46 | 47 | return cmd 48 | } 49 | -------------------------------------------------------------------------------- /business/shell/cmd_linux.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | type linuxShell struct{} 12 | 13 | func (w *linuxShell) IfconfigAddAlias(iface string, ipaddr string, netmask string, gw string) error { 14 | return shStdio(fmt.Sprintf("ip addr add %s dev %s", ipaddr, iface)) 15 | } 16 | 17 | func (w *linuxShell) IfconfigRemAlias(iface string, ipaddr string) error { 18 | return shStdio(fmt.Sprintf("ip addr del %s dev %s", ipaddr, iface)) 19 | } 20 | 21 | func (w *linuxShell) CmdAs(ctx context.Context, user string) (io.Writer, error) { 22 | 23 | cmd := exec.CommandContext(ctx, "su", "-", user) 24 | cmd.Stdout = os.Stdout 25 | cmd.Stderr = os.Stderr 26 | writer, err := cmd.StdinPipe() 27 | if err != nil { 28 | return nil, fmt.Errorf("error piping stdin: %w", err) 29 | } 30 | 31 | if err = cmd.Start(); err != nil { 32 | return nil, fmt.Errorf("error starting port forward command: %w", err) 33 | } 34 | 35 | return writer, nil 36 | 37 | } 38 | 39 | func New() Shell { 40 | return &linuxShell{} 41 | } 42 | 43 | func sh(cmdline string) *exec.Cmd { 44 | cmd := exec.Command("sh", "-c", cmdline) 45 | return cmd 46 | } 47 | 48 | func shStr(cmdline string) (string, error) { 49 | cmd := sh(cmdline) 50 | 51 | bs, err := cmd.CombinedOutput() 52 | 53 | return string(bs), err 54 | } 55 | 56 | func Ping(h string) (string, error) { 57 | return shStr(fmt.Sprintf("ping -c 4 %s", h)) 58 | } 59 | 60 | func Nmap(h string) (string, error) { 61 | return shStr(fmt.Sprintf("nmap %s", h)) 62 | } 63 | 64 | func Nc(h string) (string, error) { 65 | return shStr(fmt.Sprintf("nc %s", h)) 66 | } 67 | -------------------------------------------------------------------------------- /business/shell/cmd_windows.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "strings" 11 | "time" 12 | 13 | su "github.com/nyaosorg/go-windows-su" 14 | ) 15 | 16 | type winShell struct{} 17 | 18 | func (w *winShell) IfconfigAddAlias(iface string, ipaddr string, netmask string, gw string) error { 19 | err := shStdio(fmt.Sprintf("netsh interface ip add address %s %s %s", iface, ipaddr, netmask)) 20 | if err != nil { 21 | return err 22 | } 23 | time.Sleep(time.Second * 5) 24 | return nil 25 | } 26 | 27 | func (w *winShell) IfconfigRemAlias(iface string, ipaddr string) error { 28 | return shStdio(fmt.Sprintf("netsh interface ip delete address %s %s", iface, ipaddr)) 29 | } 30 | 31 | func (w *winShell) CmdAs(ctx context.Context, user string) (io.Writer, error) { 32 | 33 | cmd := exec.CommandContext(ctx, "runas", "/user:"+user, "cmd") 34 | cmd.Stdout = os.Stdout 35 | cmd.Stderr = os.Stderr 36 | writer, err := cmd.StdinPipe() 37 | if err != nil { 38 | return nil, fmt.Errorf("error piping stdin: %w", err) 39 | } 40 | 41 | if err = cmd.Start(); err != nil { 42 | return nil, fmt.Errorf("error starting port forward command: %w", err) 43 | } 44 | 45 | return writer, nil 46 | 47 | } 48 | 49 | func New() Shell { 50 | return &winShell{} 51 | } 52 | 53 | func sh(cmdline string) *exec.Cmd { 54 | cmd := exec.Command("cmd", "/c", cmdline) 55 | return cmd 56 | } 57 | 58 | func shStr(cmdline string) (string, error) { 59 | cmd := sh(cmdline) 60 | 61 | bs, err := cmd.CombinedOutput() 62 | 63 | return string(bs), err 64 | } 65 | 66 | func shSu(cmdline string) error { 67 | _, err := su.ShellExecute(su.RUNAS, 68 | "cmd", 69 | "/c", 70 | cmdline) 71 | return err 72 | } 73 | 74 | func getUnderlingUser() (*user.User, error) { 75 | usr, err := user.Current() 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to get current user: %w", err) 78 | } 79 | if usr.Username == "root" { 80 | under := os.Getenv("SUDO_USER") 81 | if under != "" { 82 | usr, err := user.Lookup(under) 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to lookup user %s: %w", under, err) 85 | } 86 | return usr, nil 87 | } 88 | } 89 | return usr, nil 90 | } 91 | 92 | func IfconfigAddAlias(iface string, ipaddr string) error { 93 | return shStdio(fmt.Sprintf("ifconfig %s alias %s", iface, ipaddr)) 94 | } 95 | 96 | func IfconfigRemAlias(iface string, ipaddr string) error { 97 | return shStdio(fmt.Sprintf("ifconfig %s -alias %s", iface, ipaddr)) 98 | } 99 | 100 | func LoggedUser() (string, error) { 101 | ret, err := shStr("who | grep console") 102 | if err != nil { 103 | return "", err 104 | } 105 | parts := strings.Split(ret, " ") 106 | return parts[0], nil 107 | } 108 | 109 | func KubeCtlKillAll() error { 110 | return shStdio("killall -9 kubectl") 111 | } 112 | 113 | func KillbyPid(p int) error { 114 | return shStdio(fmt.Sprintf("kill -9 %v", p)) 115 | } 116 | 117 | func LsofTcpConnsbyPid(p int) (string, error) { 118 | return shStr(fmt.Sprintf("lsof -p %v", p)) 119 | } 120 | 121 | func LaunchCtlInstallDaemon() (string, error) { 122 | return shStr("launchctl load /Library/LaunchDaemons/nx.plist") 123 | } 124 | 125 | func LaunchCtlInstallTrayAgent() (string, error) { 126 | usr, err := getUnderlingUser() 127 | if err != nil { 128 | return "", err 129 | } 130 | return shStr(fmt.Sprintf("launchctl load user/%s %s/Library/LaunchAgents/nx.tray.plist", usr.Uid, usr.HomeDir)) 131 | } 132 | 133 | func LaunchCtlEnableTrayAgent() (string, error) { 134 | usr, err := getUnderlingUser() 135 | if err != nil { 136 | return "", err 137 | } 138 | return shStr(fmt.Sprintf("launchctl enable user/%s/nx.tray", usr.Uid)) 139 | } 140 | 141 | func LaunchCtlUninstallTrayAgent() (string, error) { 142 | usr, err := getUnderlingUser() 143 | if err != nil { 144 | return "", err 145 | } 146 | return shStr(fmt.Sprintf("launchctl unload user/%s %s/Library/LaunchAgents/nx.tray.plist", usr.Uid, usr.HomeDir)) 147 | } 148 | 149 | func LaunchCtlDisableTrayAgent() (string, error) { 150 | usr, err := getUnderlingUser() 151 | if err != nil { 152 | return "", err 153 | } 154 | return shStr(fmt.Sprintf("launchctl disable user/%s/nx.tray", usr.Uid)) 155 | } 156 | 157 | func LaunchCtlStartDaemon() (string, error) { 158 | return shStr("launchctl start nx") 159 | } 160 | 161 | func LaunchCtlStartTrayAgent() (string, error) { 162 | return shStr("launchctl start nx") 163 | } 164 | 165 | func LaunchCtlStopDaemon() (string, error) { 166 | return shStr("launchctl stop nx") 167 | } 168 | 169 | func LaunchCtlUnistallDaemon() (string, error) { 170 | return shStr("launchctl unload /Library/LaunchDaemons/nx.plist") 171 | } 172 | 173 | func KillallKubectl() (string, error) { 174 | return shStr("killall -9 kubectl") 175 | } 176 | 177 | func Ping(h string) (string, error) { 178 | return shStr(fmt.Sprintf("ping %s", h)) 179 | } 180 | 181 | func Nmap(h string) (string, error) { 182 | return shStr(fmt.Sprintf("nmap %s", h)) 183 | } 184 | 185 | func Nc(h string) (string, error) { 186 | return shStr(fmt.Sprintf("nc %s", h)) 187 | } 188 | -------------------------------------------------------------------------------- /business/shell/cmd_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package shell 4 | 5 | import "testing" 6 | 7 | func TestSH(t *testing.T) { 8 | ret, err := shStr("echo 123") 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | if ret != "123\r\n" { 13 | t.Fatal("should be 123") 14 | } 15 | } 16 | 17 | func TestWinShell_IfconfigAddAlias(t *testing.T) { 18 | s := New() 19 | 20 | err := s.IfconfigAddAlias("LB", "10.0.0.1", "255.255.255.0", "10.0.0.1") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | err = s.IfconfigAddAlias("LB", "10.0.0.2", "255.255.255.0", "10.0.0.1") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | err = s.IfconfigAddAlias("LB", "10.0.0.3", "255.255.255.0", "10.0.0.1") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | err = s.IfconfigRemAlias("LB", "10.0.0.1") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | err = s.IfconfigRemAlias("LB", "10.0.0.2") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | err = s.IfconfigRemAlias("LB", "10.0.0.3") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /foundation/buildinfo/build-date: -------------------------------------------------------------------------------- 1 | 2023.09.29.19.38.55 2 | -------------------------------------------------------------------------------- /foundation/buildinfo/build-hash: -------------------------------------------------------------------------------- 1 | 665b2353a6dd42f6cc2ff9c9fb4961744fec3562 2 | -------------------------------------------------------------------------------- /foundation/buildinfo/build-semver: -------------------------------------------------------------------------------- 1 | 0.1.4 -------------------------------------------------------------------------------- /foundation/buildinfo/buildinfo.go: -------------------------------------------------------------------------------- 1 | // Package buildinfo allow playing with software version. 2 | package buildinfo 3 | 4 | import ( 5 | _ "embed" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | //go:embed build-date 11 | var Date string 12 | 13 | //go:embed build-semver 14 | var SemVer string 15 | 16 | //go:embed build-hash 17 | var Hash string 18 | 19 | // String will return a formated string with all info above. 20 | func String(msg string) string { 21 | return fmt.Sprintf(`%s 22 | Build Date:%s 23 | SemVer: %s 24 | Hash: %s`, msg, 25 | strings.TrimSpace(Date), 26 | strings.TrimSpace(SemVer), 27 | strings.TrimSpace(Hash)) 28 | } 29 | 30 | // StringOneLine will do similar to String, but all in one line. 31 | func StringOneLine(msg string) string { 32 | return fmt.Sprintf(`%s 33 | Build Date:%s | SemVer: %s | Hash: %s`, msg, 34 | strings.TrimSpace(Date), 35 | strings.TrimSpace(SemVer), 36 | strings.TrimSpace(Hash)) 37 | } 38 | -------------------------------------------------------------------------------- /foundation/memstore/memstore.go: -------------------------------------------------------------------------------- 1 | // Package memstore implements a simple in memory thread safe container for different needs we will have in this repo. 2 | package memstore 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // Map is our core implementation for the in memory storage. 11 | type Map[T any] struct { 12 | sync.RWMutex 13 | items map[string]T 14 | } 15 | 16 | // Add will add a new item to storage, but considers that you dont have a key, and it will generate one for your. 17 | func (c *Map[T]) Add(item T) string { 18 | c.Lock() 19 | defer c.Unlock() 20 | 21 | ret := uuid.NewString() 22 | 23 | c.items[ret] = item 24 | 25 | return ret 26 | } 27 | 28 | // Set is similar to add, but in this case you want to use your own key. This method will overwrite previous item in 29 | // case of conflicting keys. 30 | func (c *Map[T]) Set(key string, item T) { 31 | c.Lock() 32 | defer c.Unlock() 33 | 34 | c.items[key] = item 35 | } 36 | 37 | // Del will remove entry referred by the key from the map. 38 | func (c *Map[T]) Del(keys ...string) { 39 | c.Lock() 40 | defer c.Unlock() 41 | 42 | for _, aKey := range keys { 43 | delete(c.items, aKey) 44 | } 45 | } 46 | 47 | // Get retrieves the item represented by the provided key. 48 | // 49 | //nolint:ireturn,nolintlint 50 | func (c *Map[T]) Get(key string) T { 51 | c.RLock() 52 | defer c.RUnlock() 53 | 54 | return c.items[key] 55 | } 56 | 57 | // ForEach iterates over all items until they are over or when fn returns an error. 58 | // Dont delete items from inside this function. 59 | func (c *Map[T]) ForEach(fn func(k string, v T) error) error { 60 | c.RLock() 61 | defer c.RUnlock() 62 | 63 | for k, v := range c.items { 64 | if err := fn(k, v); err != nil { 65 | return err 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // New will create a new instance of a Map. 73 | func New[T any]() *Map[T] { 74 | return &Map[T]{ 75 | RWMutex: sync.RWMutex{}, 76 | items: map[string]T{}, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /foundation/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Package metrics will allow us to publish/report metrics to measure and manage netmux operations. 2 | package metrics 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log/slog" 9 | "net/http" 10 | "strings" 11 | "time" 12 | 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/promauto" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | ) 17 | 18 | // Observer implements the last mile collector to a metric. Ideally all preparation to report metrics should be chached 19 | // by this entity. 20 | type Counter interface { 21 | Add(value float64) 22 | } 23 | 24 | // Metric represents one data point to be collected. Since it may have flags/labels, we use it as an intermediary 25 | // entity. An observer should be created from the metric for further reporting. 26 | type Metric interface { 27 | Counter(labels map[string]string) Counter 28 | } 29 | 30 | // Factory allows creation of metrics. 31 | type Factory interface { 32 | New(m string, params ...string) Metric 33 | } 34 | 35 | //---------------------------------------------------------------------------------------------------------------------- 36 | 37 | // Stdout metrics family is a simple implementation of our metrics stack, sending them to stdout. 38 | 39 | type StdoutCounter struct { 40 | attrs []any 41 | } 42 | 43 | func (s *StdoutCounter) Add(value float64) { 44 | s.attrs[3] = value 45 | slog.Info("Metric", s.attrs...) 46 | } 47 | 48 | type StdoutMetric struct { 49 | name string 50 | } 51 | 52 | func (s *StdoutMetric) Counter(labels map[string]string) Counter { //nolint:ireturn,nolintlint 53 | attrs := make([]any, len(labels)+4) //nolint:gomnd 54 | attrs[0] = "name" 55 | attrs[1] = s.name 56 | attrs[2] = "value" 57 | attrs[3] = 0 58 | counter := 4 59 | 60 | for k, v := range labels { 61 | attrs[counter] = k 62 | attrs[counter+1] = v 63 | counter += 2 64 | } 65 | 66 | return &StdoutCounter{} 67 | } 68 | 69 | type StdoutFactory struct{} 70 | 71 | func (s *StdoutFactory) New(m string, _ ...string) Metric { //nolint:ireturn,nolintlint 72 | return &StdoutMetric{ 73 | name: m, 74 | } 75 | } 76 | 77 | func NewStdoutFactory() *StdoutFactory { 78 | return &StdoutFactory{} 79 | } 80 | 81 | //---------------------------------------------------------------------------------------------------------------------- 82 | 83 | // Prom family is the implementation of our stack to work on top of prometheus. 84 | 85 | type PromCounter struct { 86 | counter prometheus.Counter 87 | } 88 | 89 | func (s *PromCounter) Add(value float64) { 90 | s.counter.Add(value) 91 | } 92 | 93 | type PromMetric struct { 94 | name string 95 | promMetric *prometheus.CounterVec 96 | } 97 | 98 | func (p *PromMetric) Counter(labels map[string]string) Counter { //nolint:ireturn,nolintlint 99 | ret := &PromCounter{counter: p.promMetric.With(labels)} 100 | 101 | return ret 102 | } 103 | 104 | type PromFactory struct { 105 | metrics map[string]*PromMetric 106 | } 107 | 108 | func (p *PromFactory) Start(ctx context.Context, addr string) error { 109 | server := http.Server{ 110 | Addr: addr, 111 | Handler: promhttp.Handler(), 112 | ReadHeaderTimeout: time.Second * 5, //nolint:gomnd 113 | } 114 | 115 | go func() { 116 | <-ctx.Done() 117 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) //nolint:gomnd 118 | 119 | defer cancel() 120 | 121 | _ = server.Shutdown(ctx) //nolint:contextcheck 122 | }() 123 | 124 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 125 | return fmt.Errorf("error starting prom server: %w", err) 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func (p *PromFactory) New(metric string, labels ...string) Metric { //nolint:ireturn,nolintlint 132 | metric = strings.ToLower(metric) 133 | metric = strings.ReplaceAll(metric, "-", "_") 134 | metric = strings.ReplaceAll(metric, ".", "_") 135 | metric = strings.ReplaceAll(metric, " ", "_") 136 | 137 | ret, ok := p.metrics[metric] 138 | if ok { 139 | return ret 140 | } 141 | 142 | ret = &PromMetric{ 143 | name: metric, 144 | promMetric: promauto.NewCounterVec(prometheus.CounterOpts{ 145 | Namespace: "netmux", 146 | Subsystem: "netmux", 147 | Name: metric, 148 | }, 149 | labels, 150 | ), 151 | } 152 | 153 | p.metrics[metric] = ret 154 | 155 | return ret 156 | } 157 | 158 | func NewPromFactory() *PromFactory { 159 | return &PromFactory{ 160 | metrics: make(map[string]*PromMetric), 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /foundation/pipe/pipe.go: -------------------------------------------------------------------------------- 1 | // Package iopipe simplifies the process of pumping data from one end to another 2 | // while allowing us to measure throughput. 3 | package pipe 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "runtime" 11 | "time" 12 | 13 | "golang.org/x/sync/errgroup" 14 | ) 15 | 16 | // helperIoClose reduces repetitive error handling when closing io.Closer instances. 17 | func helperIoClose(closer io.Closer) { 18 | if err := closer.Close(); err != nil { 19 | _, file, line, ok := runtime.Caller(1) 20 | if ok { 21 | slog.Warn("error closing", "err", err, "file", file, "line", line) 22 | 23 | return 24 | } 25 | 26 | slog.Warn("error closing - could not find caller", "err", err) 27 | } 28 | } 29 | 30 | // --------------------------------------------------------------------------------------------------------------------- 31 | 32 | // countWriter is a wrapper to a io.Writer that allows tracking the amount of data written to it. 33 | type countWriter struct { 34 | w io.Writer 35 | counter int64 36 | } 37 | 38 | // Write - same as io.Writer.Write. Every call will add the len of p to the counter. 39 | func (c *countWriter) Write(p []byte) (int, error) { 40 | c.counter += int64(len(p)) 41 | 42 | ret, err := c.w.Write(p) 43 | if err != nil { 44 | return 0, fmt.Errorf("error writing to inner writer: %w", err) 45 | } 46 | 47 | return ret, nil 48 | } 49 | 50 | // TotalWReset returns acc data and resets counter. 51 | func (c *countWriter) TotalWReset() int64 { 52 | ret := c.counter 53 | c.counter = 0 54 | return ret 55 | } 56 | 57 | // NewCountWriter creates a new countWriter on top of a pre-existing writer. 58 | func newCountWriter(w io.Writer) *countWriter { 59 | return &countWriter{ 60 | w: w, 61 | } 62 | } 63 | 64 | // Int64Metric defines what we will use to produce/inform int64 metrics. 65 | type Float64Metric func(i float64) 66 | 67 | // --------------------------------------------------------------------------------------------------------------------- 68 | 69 | // Pipe will ensure that data flows from A to B and vice-versa - being A and B instances of io.ReadWriteCloser. 70 | type Pipe struct { 71 | AMetric Float64Metric 72 | BMetric Float64Metric 73 | a io.ReadWriteCloser 74 | b io.ReadWriteCloser 75 | } 76 | 77 | // Run will block and to piping until one of the three conditions is met: 78 | // - 1: context is canceled 79 | // - 2: A is closed 80 | // - 3: B is closed 81 | // 82 | //nolint:funlen 83 | func (p *Pipe) Run(ctx context.Context) error { 84 | ctx, cancel := context.WithCancelCause(ctx) 85 | defer cancel(fmt.Errorf("pipe ended")) 86 | 87 | group, ctx := errgroup.WithContext(ctx) 88 | 89 | aCountWriter := newCountWriter(p.a) 90 | bCountWriter := newCountWriter(p.b) 91 | 92 | if p.AMetric != nil || p.BMetric != nil { 93 | group.Go(func() error { 94 | defer cancel(fmt.Errorf("closed metrics collecting function")) 95 | for { 96 | if ctx.Err() != nil { 97 | return fmt.Errorf("context cancelled when capturing metrics: %w", ctx.Err()) 98 | } 99 | 100 | if p.AMetric != nil { 101 | p.AMetric(float64(aCountWriter.TotalWReset())) 102 | } 103 | 104 | if p.BMetric != nil { 105 | p.BMetric(float64(bCountWriter.TotalWReset())) 106 | } 107 | 108 | time.Sleep(time.Second) 109 | } 110 | }) 111 | } 112 | 113 | group.Go(func() error { 114 | <-ctx.Done() 115 | helperIoClose(p.a) 116 | helperIoClose(p.b) 117 | 118 | return nil 119 | }) 120 | 121 | group.Go(func() error { 122 | defer cancel(fmt.Errorf("pipe b->a closed")) 123 | 124 | _, err := io.Copy(aCountWriter, p.b) 125 | if err != nil { 126 | return fmt.Errorf("error copying b-a: %w", err) 127 | } 128 | 129 | return nil 130 | }) 131 | 132 | group.Go(func() error { 133 | defer cancel(fmt.Errorf("pipe a->b closed")) 134 | 135 | _, err := io.Copy(bCountWriter, p.a) 136 | if err != nil { 137 | return fmt.Errorf("error copying a-b: %w", err) 138 | } 139 | 140 | return nil 141 | }) 142 | 143 | if err := group.Wait(); err != nil { 144 | return fmt.Errorf("error processing pipe: %w", err) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | // New will create a new Pipe, copying data from A to B and vice-versa. 151 | func New(a io.ReadWriteCloser, b io.ReadWriteCloser) *Pipe { 152 | return &Pipe{ 153 | a: a, 154 | b: b, 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /foundation/wire/wire.go: -------------------------------------------------------------------------------- 1 | // Package wire implements low level protocol handling over the wire. 2 | // Logic is simple: every payload is composed by: 3 | // - int16 with the type of package 4 | // - int64 with the payload length 5 | // - []byte with the payload itself. Len([]byte) should be equal to the value expressed by int64. 6 | package wire 7 | 8 | import ( 9 | "bytes" 10 | "encoding/base64" 11 | "encoding/binary" 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "time" 16 | ) 17 | 18 | const HeaderLen = 14 19 | 20 | var ProtoIdentifier = []byte("dxmx") 21 | 22 | // Wire is the responsible for reading and writing low level proto to/from the wire. 23 | type Wire struct{} 24 | 25 | // Write will write a payload to the wire. 26 | func (wi *Wire) Write(writer io.Writer, cmd uint16, payload []byte) error { 27 | header := make([]byte, HeaderLen) 28 | copy(header, ProtoIdentifier) 29 | binary.LittleEndian.PutUint16(header[4:], cmd) 30 | binary.LittleEndian.PutUint64(header[6:], uint64(len(payload))) 31 | 32 | if _, err := writer.Write(header); err != nil { 33 | return fmt.Errorf("error sending header: %writer", err) 34 | } 35 | 36 | if _, err := writer.Write(payload); err != nil { 37 | return fmt.Errorf("error sending payload: %writer", err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | // Read extracts next payload from the wire. 44 | func (wi *Wire) Read(reader io.Reader) (cmd uint16, payload []byte, err error) { 45 | header := make([]byte, HeaderLen) 46 | 47 | readBytes, err := reader.Read(header) 48 | if err != nil { 49 | return 0, nil, fmt.Errorf("error reading header: %w", err) 50 | } 51 | if readBytes < HeaderLen { 52 | return 53 | } 54 | 55 | id := header[:4] 56 | 57 | if !bytes.Equal(id, ProtoIdentifier) { 58 | return 0, []byte{}, nil 59 | } 60 | 61 | cmd = binary.LittleEndian.Uint16(header[4:]) 62 | 63 | plLen := binary.LittleEndian.Uint64(header[6:]) 64 | 65 | defer func() { 66 | r := recover() 67 | if r != nil { 68 | // TODO: this is a short term solution to handle undesired connections. 69 | // the timeout reduces overload until we find a better solution. 70 | time.Sleep(time.Second * 5) 71 | err = fmt.Errorf("wrong wire protocol format (recover): %s. %v", base64.StdEncoding.EncodeToString(header), r) 72 | } 73 | }() 74 | 75 | payload = make([]byte, plLen) 76 | 77 | if _, err := reader.Read(payload); err != nil { 78 | return 0, nil, fmt.Errorf("error reading payload: %w", err) 79 | } 80 | 81 | return 82 | } 83 | 84 | // WriteJSON adds a little to Write, allowing prompt marshalling of datastructures to the wire in Json format. 85 | func (wi *Wire) WriteJSON(writer io.Writer, cmd uint16, pl any) error { 86 | bytesPayload, err := json.Marshal(pl) 87 | if err != nil { 88 | return fmt.Errorf("WriteJsonToWire: error marshalling pl: %writer", err) 89 | } 90 | 91 | return wi.Write(writer, cmd, bytesPayload) 92 | } 93 | 94 | // ReadJSON works in a similar way to WriteJSON, but reads from the wire and poupulates a data structure. 95 | // The provided cmd prevents us from extracting data that is not the command we are waiting for. 96 | // If multiple commands may come, please use Read and unmarshall your data manually. 97 | func (wi *Wire) ReadJSON(reader io.Reader, cmd uint16, payload any) error { 98 | recvcmd, payloadBytes, err := wi.Read(reader) 99 | if err != nil { 100 | return fmt.Errorf("ReadJsonFromWire: error reading payload: %w", err) 101 | } 102 | if recvcmd == 0 { 103 | return nil 104 | } 105 | if cmd != recvcmd { 106 | return fmt.Errorf("ReadJsonFromWire: wrong command received: %v", recvcmd) 107 | } 108 | 109 | if err = json.Unmarshal(payloadBytes, payload); err != nil { 110 | return fmt.Errorf("ReadJsonFromWire: error unmarshalling payload: %w", err) 111 | } 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /foundation/wire/wire_test.go: -------------------------------------------------------------------------------- 1 | package wire_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/duxthemux/netmux/foundation/wire" 10 | ) 11 | 12 | //nolint:paralleltest 13 | func TestWireProto(t *testing.T) { 14 | aWire := wire.Wire{} 15 | 16 | type args struct { 17 | cmd uint16 18 | pl []byte 19 | } 20 | 21 | tests := []struct { 22 | name string 23 | args args 24 | wantW []byte 25 | wantErr bool 26 | }{ 27 | { 28 | name: "Simple", 29 | args: args{cmd: 1, pl: []byte("ASD")}, 30 | wantW: []byte("ASD"), 31 | }, 32 | { 33 | name: "With Nils", 34 | args: args{cmd: 1, pl: []byte("ASD\x00123")}, 35 | wantW: []byte("ASD\x00123"), 36 | }, 37 | } 38 | for _, aTest := range tests { 39 | t.Run(aTest.name, func(t *testing.T) { 40 | w := &bytes.Buffer{} 41 | 42 | err := aWire.Write(w, aTest.args.cmd, aTest.args.pl) 43 | assert.NoError(t, err) 44 | 45 | cmd, pl, err := aWire.Read(w) 46 | assert.NoError(t, err) 47 | 48 | assert.Equal(t, cmd, uint16(1)) 49 | assert.Equal(t, aTest.wantW, pl) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/duxthemux/netmux 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/cenkalti/backoff v2.2.1+incompatible 7 | github.com/google/uuid v1.3.0 8 | github.com/gorilla/mux v1.8.0 9 | github.com/jedib0t/go-pretty/v6 v6.4.7 10 | github.com/miekg/dns v1.1.56 11 | github.com/nyaosorg/go-windows-su v0.2.1 12 | github.com/prometheus/client_golang v1.16.0 13 | github.com/stretchr/testify v1.8.2 14 | github.com/twmb/franz-go v1.14.4 15 | github.com/urfave/cli/v2 v2.25.7 16 | golang.org/x/sync v0.3.0 17 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 18 | gopkg.in/yaml.v3 v3.0.1 19 | k8s.io/api v0.26.2 20 | k8s.io/apimachinery v0.26.2 21 | k8s.io/cli-runtime v0.26.2 22 | k8s.io/client-go v0.26.2 23 | ) 24 | 25 | require ( 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 28 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/emicklei/go-restful/v3 v3.10.1 // indirect 31 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 32 | github.com/go-errors/errors v1.0.1 // indirect 33 | github.com/go-logr/logr v1.2.3 // indirect 34 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 35 | github.com/go-openapi/jsonreference v0.20.2 // indirect 36 | github.com/go-openapi/swag v0.22.3 // indirect 37 | github.com/gogo/protobuf v1.3.2 // indirect 38 | github.com/golang/protobuf v1.5.3 // indirect 39 | github.com/google/btree v1.0.1 // indirect 40 | github.com/google/gnostic v0.6.9 // indirect 41 | github.com/google/go-cmp v0.5.9 // indirect 42 | github.com/google/gofuzz v1.2.0 // indirect 43 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 44 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 45 | github.com/imdario/mergo v0.3.13 // indirect 46 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 47 | github.com/josharian/intern v1.0.0 // indirect 48 | github.com/json-iterator/go v1.1.12 // indirect 49 | github.com/klauspost/compress v1.16.7 // indirect 50 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 51 | github.com/mailru/easyjson v0.7.7 // indirect 52 | github.com/mattn/go-runewidth v0.0.13 // indirect 53 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 54 | github.com/moby/spdystream v0.2.0 // indirect 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 56 | github.com/modern-go/reflect2 v1.0.2 // indirect 57 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 59 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 60 | github.com/pierrec/lz4/v4 v4.1.18 // indirect 61 | github.com/pkg/errors v0.9.1 // indirect 62 | github.com/pmezard/go-difflib v1.0.0 // indirect 63 | github.com/prometheus/client_model v0.3.0 // indirect 64 | github.com/prometheus/common v0.42.0 // indirect 65 | github.com/prometheus/procfs v0.10.1 // indirect 66 | github.com/rivo/uniseg v0.2.0 // indirect 67 | github.com/rogpeppe/go-internal v1.10.0 // indirect 68 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 69 | github.com/spf13/cobra v1.6.0 // indirect 70 | github.com/spf13/pflag v1.0.5 // indirect 71 | github.com/twmb/franz-go/pkg/kmsg v1.6.1 // indirect 72 | github.com/xlab/treeprint v1.1.0 // indirect 73 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 74 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 75 | golang.org/x/mod v0.12.0 // indirect 76 | golang.org/x/net v0.15.0 // indirect 77 | golang.org/x/oauth2 v0.6.0 // indirect 78 | golang.org/x/sys v0.12.0 // indirect 79 | golang.org/x/term v0.12.0 // indirect 80 | golang.org/x/text v0.13.0 // indirect 81 | golang.org/x/time v0.3.0 // indirect 82 | golang.org/x/tools v0.13.0 // indirect 83 | google.golang.org/appengine v1.6.7 // indirect 84 | google.golang.org/protobuf v1.30.0 // indirect 85 | gopkg.in/inf.v0 v0.9.1 // indirect 86 | gopkg.in/yaml.v2 v2.4.0 // indirect 87 | k8s.io/klog/v2 v2.90.1 // indirect 88 | k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d // indirect 89 | k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect 90 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 91 | sigs.k8s.io/kustomize/api v0.12.1 // indirect 92 | sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect 93 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 94 | sigs.k8s.io/yaml v1.3.0 // indirect 95 | ) 96 | -------------------------------------------------------------------------------- /zarf/docker/helpers/sample-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | 5 | #RUN apk add curl 6 | 7 | ADD ./zarf/docker/helpers/sample-service/bin/${TARGETOS}/${TARGETARCH}/service /app/service 8 | 9 | CMD "/app/service" -------------------------------------------------------------------------------- /zarf/docker/netmux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | RUN apk add nmap tcpdump libpcap-dev curl openssh git 5 | 6 | ADD ./zarf/docker/netmux/bin/${TARGETOS}/${TARGETARCH}/netmux /app/service 7 | 8 | CMD "/app/service" -------------------------------------------------------------------------------- /zarf/docs/diagrams/arch-direct-conn.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !theme aws-orange 3 | 4 | skinparam component{ 5 | fontColor #444444 6 | } 7 | 8 | component "User" as user 9 | component "Local Machine" { 10 | 11 | component "NX Cli" as nxCli 12 | 13 | component "NX Daemon" as nxDaemon { 14 | port "Control Connection" as portControlConnectionDaemon 15 | component "NX Daemon" as nxDaemonCore 16 | component "Endpoint Agent" as endpointAgent 17 | component "Network Allocator" as networkAllocator 18 | component "DNS Allocator" as dnsAllocator 19 | component "IP Allocator" as ipAllocator 20 | component "Svc Proxy" as svcProxy 21 | component "Svc Rev Proxy Listen" as svcRevProxyListen 22 | component "Svc Rev Proxy Work" as svcRevProxyWork 23 | } 24 | 25 | component "Workload Proxy" as workloadProxy 26 | } 27 | 28 | component "Cluster"{ 29 | port "Control Connection" as portControlConnectionService 30 | component "NX Service" as nxService 31 | component "Proxied Workload Service" as proxiedWorkloadService 32 | component "Proxied Workload" as proxiedWorkload 33 | } 34 | 35 | user -> nxCli : "Command Line" 36 | nxCli -> nxDaemonCore 37 | nxDaemonCore ..> nxService 38 | nxService <..> proxiedWorkloadService 39 | nxDaemonCore -> networkAllocator 40 | networkAllocator -> dnsAllocator 41 | networkAllocator -> ipAllocator 42 | nxDaemonCore <..> endpointAgent 43 | endpointAgent ..> svcProxy 44 | endpointAgent ..> svcRevProxyListen 45 | svcRevProxyListen ..> svcRevProxyWork 46 | svcProxy .> workloadProxy 47 | 48 | nxDaemon .> workloadProxy: "Creates proxy" 49 | portControlConnectionDaemon<-->portControlConnectionService 50 | user <..> workloadProxy 51 | workloadProxy <..> proxiedWorkloadService : "Exchanges Data" 52 | proxiedWorkloadService <..> proxiedWorkload : "Exchanges Data" 53 | 54 | 55 | @enduml -------------------------------------------------------------------------------- /zarf/docs/diagrams/arch.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !theme aws-orange 3 | 4 | skinparam component{ 5 | fontColor #444444 6 | } 7 | 8 | component "Local Machine" { 9 | 10 | component "NX Cli" as nxCli 11 | component "NX Daemon" as nxDaemon { 12 | component "NX Daemon Core" as nxDaemonCore 13 | component "NX Endpoint Agent" as nxAgent 14 | portin "Client Interface" as cliInterface 15 | portout "Control Connection" as portControlConnectionDaemon 16 | component "Network Allocator" as networkAllocator 17 | component "DNS Allocator" as dnsAllocator 18 | component "IP Allocator" as ipAllocator 19 | component "Svc Proxy" as svcProxy 20 | component "Svc Rev Proxy Listen" as svcRevProxyListen 21 | component "Svc Rev Proxy Work" as svcRevProxyWork 22 | } 23 | } 24 | 25 | component "Cluster"{ 26 | portin "Control Connection" as portControlConnectionService 27 | component "NX Service" as nxService 28 | component "Kubernetes" as kubernetes 29 | component "Network Services in Cluster" as nwSvcInCluster 30 | } 31 | 32 | nxCli --> cliInterface: : 1 - HTTP 33 | nxDaemonCore --> nxAgent : 2 34 | cliInterface --> nxDaemonCore : 2 35 | nxAgent --> portControlConnectionDaemon : 3 36 | nxAgent --> networkAllocator : 4 37 | networkAllocator --> ipAllocator :5 38 | networkAllocator --> dnsAllocator : 6 39 | 40 | nxAgent --> svcProxy : 7a 41 | nxAgent --> svcRevProxyListen :7b 42 | svcRevProxyListen --> svcRevProxyWork :8b 43 | 44 | portControlConnectionDaemon<-->portControlConnectionService :1 45 | portControlConnectionService <--> nxService : 2 46 | nxService <-> kubernetes : 3 47 | nxService <--> nwSvcInCluster : 4 48 | 49 | @enduml -------------------------------------------------------------------------------- /zarf/docs/diagrams/bridge.puml: -------------------------------------------------------------------------------- 1 | @startyaml 2 | !theme aws-orange 3 | bridge: 4 | name: any string 5 | namespace: k8s filled 6 | localAddr: string 7 | localPort: string 8 | containerAddr: string 9 | containerPort: string 10 | direction: L2C or C2L 11 | family: tcp | udp 12 | 13 | @endyaml -------------------------------------------------------------------------------- /zarf/docs/diagrams/comm-state.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !theme aws-orange 3 | 4 | 5 | 6 | [*] -> handleConn 7 | 8 | state handleConn { 9 | [*] -> CmdControl 10 | CmdControl -> handleCmdConn 11 | -- 12 | [*] -> CmdProxy 13 | CmdProxy -> handleProxyConn 14 | -- 15 | [*] -> CmdRevProxyListen 16 | CmdRevProxyListen -> handleRevProxyListen 17 | -- 18 | [*] -> CmdRevProxyWork 19 | CmdRevProxyWork -> handleRevProxyWork 20 | } 21 | 22 | state handleCmdConn { 23 | [*] -> ReadFromWire 24 | ReadFromWire -> PingCommand 25 | } 26 | 27 | state handleProxyConn{ 28 | [*] -> Serve 29 | } 30 | state handleRevProxyListen{ 31 | [*] -> Adds 32 | } 33 | @enduml -------------------------------------------------------------------------------- /zarf/docs/diagrams/config.puml: -------------------------------------------------------------------------------- 1 | @startyaml 2 | !theme aws-orange 3 | 4 | endpoints: 5 | - name: local 6 | endpoint: netmux:50000 7 | kubernetes: 8 | config: /Users/psimao/.kube/config 9 | namespace: netmux 10 | endpoint: netmux 11 | port: 50000 12 | context: orbstack 13 | 14 | - name: customer01-ns01 15 | endpoint: netmux:50000 16 | kubernetes: 17 | config: /Users/psimao/.kube/config 18 | namespace: ns01 19 | endpoint: netmux 20 | port: 50000 21 | context: customer01 22 | 23 | - name: customer01-ns02 24 | endpoint: netmux:50000 25 | kubernetes: 26 | config: /Users/psimao/.kube/config 27 | namespace: ns02 28 | endpoint: netmux 29 | port: 50000 30 | context: customer01 31 | 32 | - name: customer02 33 | endpoint: netmux:50000 34 | kubernetes: 35 | config: /Users/psimao/.kube/dedicatedFile 36 | namespace: nsSome 37 | endpoint: netmux 38 | port: 50000 39 | context: customer02 40 | 41 | @endyaml -------------------------------------------------------------------------------- /zarf/docs/diagrams/control-conn.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !theme aws-orange 3 | autonumber 4 | title Control Connection Flow 5 | 6 | participant localAgent as "Local Agent" 7 | participant remoteServer as "Remote Server" 8 | participant eventsSource as "Events Source" 9 | 10 | 11 | localAgent -> remoteServer: Dials in, identify itself as control 12 | localAgent -> localAgent: Creates G, loops over controlConn response 13 | group "Events Source" 14 | ...When event arives... 15 | eventsSource -> remoteServer: Sends events 16 | remoteServer -> remoteServer: Loops over all event listening conns,\npropagate event 17 | remoteServer -> localAgent: Sends event as control message 18 | localAgent -> localAgent: Checks msg is event 19 | localAgent -> localAgent: Reacts to event 20 | end 21 | 22 | group "Sending Command" 23 | localAgent -> remoteServer: Send command 24 | localAgent -> localAgent: Await response (async G controls all incoming messages)\nNon command msgs will not be sent back. 25 | alt Got response 26 | localAgent -> localAgent: Forward response to caller 27 | else Got error 28 | localAgent -> localAgent: Raise Error 29 | else Timeout 30 | localAgent -> localAgent: Raise Timeout Error 31 | end 32 | end 33 | 34 | @enduml -------------------------------------------------------------------------------- /zarf/docs/diagrams/proxy.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !theme aws-orange 3 | autonumber 4 | 5 | title Direct Proxy Flow 6 | 7 | participant client as "Client" 8 | participant localAgent as "Local Agent" 9 | participant remoteServer as "Remote Server" 10 | participant proxiedServer as "Proxied Server" 11 | 12 | client -> localAgent: Establish local connection 13 | localAgent -> remoteServer: Dials new dedicated connection 14 | remoteServer -> proxiedServer: Dials new dedicated connection 15 | remoteServer <- proxiedServer: Connection established 16 | remoteServer <- remoteServer: Loops copying data back and forth 17 | localAgent <- remoteServer: Connection established 18 | localAgent <- localAgent: Loops copying data back and forth 19 | client <- localAgent: Connection established 20 | client <--> proxiedServer: Exchange Data 21 | 22 | @enduml -------------------------------------------------------------------------------- /zarf/docs/diagrams/rev-proxy.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | !theme aws-orange 3 | autonumber 4 | 5 | title Reverse Proxy Flow 6 | 7 | participant localProxiedService as "Local Proxied Service" 8 | participant localAgent as "Local Agent" 9 | participant remoteServer as "Remote Server" 10 | participant remoteClient as "Remote Client" 11 | 12 | 13 | localAgent -> remoteServer: Dials new dedicated connection for Reversing Conns 14 | remoteServer -> remoteServer: Creates listener, awaits remote conns 15 | remoteClient -> remoteServer: Dials in 16 | remoteServer -> localAgent: Sends RevProxyWorkRequest 17 | localAgent -> remoteServer: Creates dedicate connection, to proxy remote connections 18 | localAgent <- remoteServer: Accepts conn, identify pending remote connection 19 | remoteServer <- remoteServer: Loops copying data back and forth 20 | localAgent -> localProxiedService: Creates dedicated connection 21 | localAgent -> localAgent: Loops copying data back and forth 22 | remoteClient <--> localProxiedService: Data Exchange 23 | 24 | @enduml -------------------------------------------------------------------------------- /zarf/docs/diagrams/wireprotocol.puml: -------------------------------------------------------------------------------- 1 | @startyaml 2 | Cmd: x 3 | Len: x 4 | Payload: x 5 | 6 | @endyaml -------------------------------------------------------------------------------- /zarf/docs/dux-netmux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duxthemux/netmux/d744bdd1caa41db53f4089def1b4de88bb8c54fe/zarf/docs/dux-netmux.png -------------------------------------------------------------------------------- /zarf/docs/gophercon-br-2023-presentation/Apresentação GopherCon BR _ 2023 - Netmux.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duxthemux/netmux/d744bdd1caa41db53f4089def1b4de88bb8c54fe/zarf/docs/gophercon-br-2023-presentation/Apresentação GopherCon BR _ 2023 - Netmux.pdf -------------------------------------------------------------------------------- /zarf/docs/guides/add-trusted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duxthemux/netmux/d744bdd1caa41db53f4089def1b4de88bb8c54fe/zarf/docs/guides/add-trusted.png -------------------------------------------------------------------------------- /zarf/docs/guides/install.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | There are multiple possibilities to apply netmux, this guide will explore each as they become available. 4 | 5 | 1. Kubernetes - Please check [guide.](using-kubernetes.md) 6 | 2. Docker (In progress) 7 | 3. Bare Metal (In progress) 8 | 9 | The following components will be required anyhow, and have specific installation guides depending on your OS: 10 | 11 | 1. Netmux Daemon 12 | 2. Netmux Cli 13 | 14 | In order to build them you can call (once you clone the repo) the following command: 15 | 16 | ```shell 17 | make my-bins 18 | ``` 19 | 20 | Your binaries will be created under `zarf/dist` 21 | 22 | Please add `nx` to a folder in your path and `nx-daemon` to a dedicated folder - this place will vary depending on your 23 | os, so please see specific guides below. 24 | 25 | Alternatively, you can grab the latest bins from the releases in the project. 26 | 27 | ## Macos 28 | 29 | ### Installing the Daemon: 30 | 31 | 1. Place the binary in a proper folder - we suggest /usr/local/nx-daemon 32 | 2. Copy the plist file to /Library/LaunchDaemons 33 | 3. Run `sudo launchctl load -w /Library/LaunchDaemons/nx-daemon.plist` 34 | 35 | ### Uninstall 36 | 37 | 1. Run `sudo launchctl unload -w /Library/LaunchDaemons/nx-daemon.plist` 38 | 2. Remove the binary and the plist file 39 | 40 | ### Manual start 41 | `sudo launchctl start -w /Library/LaunchDaemons/nx-daemon.plist` 42 | 43 | ### Manual stop 44 | `sudo launchctl stop -w /Library/LaunchDaemons/nx-daemon.plist` 45 | 46 | #### nx-daemon.plist 47 | 48 | ```xml 49 | 50 | 51 | 52 | 53 | EnvironmentVariables 54 | 55 | PATH 56 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin: 57 | 58 | UserName 59 | root 60 | GroupName 61 | wheel 62 | Label 63 | nx-daemon 64 | Program 65 | /usr/local/nx/nx-daemon 66 | RunAtLoad 67 | 68 | KeepAlive 69 | 70 | LaunchOnlyOnce 71 | 72 | StandardOutPath 73 | /tmp/nx-daemon.stdout 74 | StandardErrorPath 75 | /tmp/nx-daemon.stderr 76 | WorkingDirectory 77 | /usr/local/nx 78 | 79 | 80 | ``` 81 | 82 | ## Linux 83 | 84 | Once you got the binaries, we propose you create a folder called `/srv/nx-daemon`, and put both nx-daemon and nx 85 | there. 86 | 87 | Add this folder to your path, add the following line to your `.bashrc`: 88 | 89 | ``` 90 | export PATH=/srv/nx-daemon:$PATH 91 | ``` 92 | 93 | At the end of the file. Then call `source ~/.bashrc` and your path should be updated. 94 | 95 | Create a nx-daemon config file as explained above. 96 | 97 | The following steps should be executed as ROOT: 98 | 99 | Create a systemd file called /etc/systemd/system/nx-daemon.service - please replace with your username 100 | 101 | ``` 102 | [Unit] 103 | Description=NX Daemon 104 | 105 | [Service] 106 | Type=simple 107 | User=root 108 | Restart=always 109 | WorkingDirectory=/srv/nx-daemon 110 | ExecStart=/srv/nx-daemon/nx-daemon 111 | 112 | [Install] 113 | WantedBy=multi-user.target 114 | ``` 115 | 116 | After this, reload the services with `systemctl daemon-reload`. 117 | 118 | Finally, make it start w your system with `systemctl enable nx-daemon.service`. 119 | 120 | Check if your daemon has started correctly with `systemctl status nx-daemon`. 121 | 122 | Start the daemon - it will generate the certificates to allow communication between cli and the daemon. 123 | 124 | Files will be saved in the `/srv/nx-daemon` 125 | 126 | ### For Specific Linux distros 127 | 128 | #### Fedora 129 | Copy the file `ca.cer` from `/srv/nx-daemon` to `/etc/pki/ca-trust/source/anchors/netmux.crt` 130 | 131 | #### Ubuntu 132 | Copy the file `ca.cer` from `/srv/nx-daemon` to `/usr/local/share/ca-certificates/netmux.crt` 133 | 134 | #### ALL 135 | 136 | Update the trusted store with the command `update-ca-trust` 137 | 138 | now you can start playing w nx command. 139 | 140 | ## Windows 141 | 142 | Download the binaries 143 | 144 | Save them in a new folder `c:\Program Files\nx-daemon` 145 | 146 | Downloand and install nssm from https://nssm.cc 147 | 148 | Save nssm.exe in the same folder 149 | 150 | From there open command prompt and run `nssm install` 151 | 152 | ![NSSM](./nssm.png "nssm") 153 | 154 | Fill in data as shown, press install. 155 | 156 | Create the `netmux.yaml` file in there 157 | 158 | Create a virtual network adapter: 159 | 160 | 1. right click start button -> Device manager 161 | 2. select your PC at the top (otherwise menu item will be missing) 162 | 3. menu "Action" -> "Add legacy hardware" 163 | 4. next -> "install the hardware that I manually selectron from a list (Advanced)" -> next 164 | 5. select "network adapters" -> next -> wait until list is loaded 165 | 6. In the Manufacturer list, select Microsoft. In the Model list, select Microsoft KM-TEST Loopback Adapter 166 | next -> finish 167 | 7. can be found where the other network adapters are. It is usually called "Ethernet 2" or something like that. 168 | 169 | Once created go to network connections, find it there and rename it to `LB` 170 | 171 | ![nwconns](./nw-conns.png "NW Conns") 172 | 173 | Once done, start it with cmd line `sc start nx-daemon` 174 | 175 | With certificates generated, please ensure to add ca.cer to the list of trusted certificates, by double clicking 176 | on it. 177 | ![add-trusted](./add-trusted.png "Add trusted certificate") 178 | 179 | The key detail here is to ensure the cert is added to the `Trusted Root Certification Authorities` 180 | 181 | 182 | ## Running the Daemon 183 | 184 | The Daemon will look for a file called `netmux.yaml` in its working directory it is running. 185 | 186 | This file describes the endpoints you may access. 187 | 188 | At the moment, every change in this file will require netmux to be restarted. 189 | 190 | ```yaml 191 | #the default used ip addresses will be in the range 10.10.10.0/24, but can be customized here. 192 | network: 10.1.0.0/24 193 | 194 | endpoints: 195 | - name: local 196 | endpoint: netmux:50000 197 | kubernetes: 198 | config: /Users/psimao/.kube/config 199 | namespace: netmux 200 | endpoint: netmux 201 | port: 50000 202 | context: orbstack 203 | - name: oci 204 | endpoint: netmux:50000 205 | kubernetes: 206 | config: /Users/psimao/.kube/oci 207 | namespace: netmux 208 | endpoint: service/netmux # netmux 209 | port: 50000 210 | context: context-cijdv4im6wa 211 | user: psimao 212 | kubectl: /opt/homebrew/bin/kubectl 213 | ``` 214 | 215 | > Important: please note, that if you need to use special authenticators to connect to k8s cluster, you may 216 | > add user and kubectl path to your endpoint config. In this case netmux will spawn portforwad by calling 217 | > kubectl impersonating the user as named in the config - so we expect to find the cloud management cli tool 218 | > in the path, allowing proper authentication and port forward to be setup. If these values are not provided, 219 | > netmux will use the traditional API approach. 220 | 221 | ## Installing on Kubernetes 222 | 223 | ### RBAC 224 | Netmux will require special accesses to monitor your namespace, so we 1st will need to address rbac: 225 | 226 | ```yaml 227 | apiVersion: rbac.authorization.k8s.io/v1 228 | kind: Role 229 | metadata: 230 | name: netmux 231 | rules: 232 | - apiGroups: [ "" ] 233 | resources: [ "nodes", "services", "pods", "endpoints" ] 234 | verbs: [ "get", "list", "watch" ] 235 | - apiGroups: [ "extensions" ] 236 | resources: [ "deployments" ] 237 | verbs: [ "get", "list", "watch" ] 238 | 239 | --- 240 | 241 | apiVersion: v1 242 | kind: ServiceAccount 243 | metadata: 244 | name: netmux 245 | 246 | --- 247 | 248 | apiVersion: rbac.authorization.k8s.io/v1 249 | kind: RoleBinding 250 | metadata: 251 | name: netmux 252 | subjects: 253 | - kind: ServiceAccount 254 | name: netmux # name of your service account 255 | namespace: netmux # this is the namespace your service account is in 256 | roleRef: # referring to your ClusterRole 257 | kind: Role 258 | name: netmux 259 | apiGroup: rbac.authorization.k8s.io 260 | 261 | ``` 262 | 263 | ### Deployment 264 | 265 | Once RBAC is set we can deploy it like this: 266 | 267 | ```yaml 268 | kind: Deployment 269 | apiVersion: apps/v1 270 | metadata: 271 | name: netmux 272 | spec: 273 | replicas: 1 274 | selector: 275 | matchLabels: 276 | app: netmux 277 | template: 278 | metadata: 279 | labels: 280 | app: netmux 281 | spec: 282 | serviceAccountName: netmux 283 | 284 | containers: 285 | - name: netmux 286 | image: duxthemux/netmux:latest 287 | imagePullPolicy: IfNotPresent 288 | livenessProbe: 289 | httpGet: 290 | port: 8083 291 | path: /live 292 | readinessProbe: 293 | httpGet: 294 | port: 8083 295 | path: /ready 296 | initialDelaySeconds: 5 297 | periodSeconds: 5 298 | env: 299 | - name: LOGLEVEL 300 | value: debug 301 | - name: LOGSRC 302 | value: "false" 303 | ports: 304 | - containerPort: 50000 305 | protocol: TCP 306 | name: netmux-data 307 | - containerPort: 8082 308 | protocol: TCP 309 | name: prometheus 310 | - containerPort: 8083 311 | protocol: TCP 312 | name: probes 313 | 314 | imagePullSecrets: 315 | - name: reg 316 | ``` 317 | 318 | ### Service 319 | 320 | ```yaml 321 | apiVersion: v1 322 | kind: Service 323 | metadata: 324 | name: netmux 325 | namespace: netmux 326 | annotations: 327 | prometheus.io/scrape: 'true' 328 | prometheus.io.scheme: "http" 329 | prometheus.io/port: "8082" 330 | nx: |- 331 | - name: netmux-prom 332 | direction: L2C 333 | remotePort: "8082" 334 | localPort: "8082" 335 | localAddr: netmux-prom 336 | spec: 337 | ports: 338 | - port: 50000 339 | name: netmux 340 | protocol: TCP 341 | targetPort: 50000 342 | - port: 8082 343 | name: prometheus 344 | protocol: TCP 345 | targetPort: 8082 346 | - port: 8083 347 | name: probes 348 | protocol: TCP 349 | targetPort: 8083 350 | selector: 351 | app: netmux 352 | ``` -------------------------------------------------------------------------------- /zarf/docs/guides/nssm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duxthemux/netmux/d744bdd1caa41db53f4089def1b4de88bb8c54fe/zarf/docs/guides/nssm.png -------------------------------------------------------------------------------- /zarf/docs/guides/nw-conns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duxthemux/netmux/d744bdd1caa41db53f4089def1b4de88bb8c54fe/zarf/docs/guides/nw-conns.png -------------------------------------------------------------------------------- /zarf/docs/guides/using-kubernetes.md: -------------------------------------------------------------------------------- 1 | # Using Netmux with Kubernetes 2 | 3 | ## Simple direct service 4 | The example below tells netmux that a service called sample01 will be exposed 5 | and connections on port 8080 will be redirected to the service pods also at 8080. 6 | 7 | ```yaml 8 | apiVersion: v1 9 | kind: Service 10 | metadata: 11 | name: sample 12 | annotations: 13 | nx: |- 14 | - name: sample01 15 | spec: 16 | ports: 17 | - port: 8080 18 | name: sample 19 | protocol: TCP 20 | targetPort: 8080 21 | 22 | selector: 23 | app: sample 24 | ``` 25 | 26 | ## Reverse service 27 | 28 | When you want the cluster to connect to your machine a reverse connection 29 | needs to be described as below. 30 | 31 | The service in this case will always point to netmux itself. The annotations will 32 | tell the host that connections from this endpoint will be redirected to itself on 8081 33 | 34 | ```yaml 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: sample-rev 39 | namespace: netmux 40 | labels: 41 | app: netmux 42 | annotations: 43 | nx: |- 44 | - localport: "8081" 45 | name: sample-rev 46 | direction: C2L 47 | auto: false 48 | proto: tcp 49 | localaddr: 127.0.0.1 50 | spec: 51 | ports: 52 | - port: 8081 53 | selector: 54 | app: netmux 55 | ``` 56 | 57 | ## Example: kafka cluster 58 | 59 | ```yaml 60 | apiVersion: v1 61 | kind: Service 62 | metadata: 63 | name: kafka 64 | namespace: netmux 65 | annotations: 66 | nx: |- 67 | - name: kafka 68 | - name: kafka-0 69 | localAddr: kafka-0.kafka.netmux.svc.cluster.local 70 | containerAddr: kafka-0.kafka.netmux.svc.cluster.local 71 | - name: kafka-1 72 | localAddr: kafka-1.kafka.netmux.svc.cluster.local 73 | containerAddr: kafka-1.kafka.netmux.svc.cluster.local 74 | - name: kafka-2 75 | localAddr: kafka-2.kafka.netmux.svc.cluster.local 76 | containerAddr: kafka-2.kafka.netmux.svc.cluster.local 77 | labels: 78 | app: kafka-app 79 | spec: 80 | ports: 81 | - name: '9092' 82 | port: 9092 83 | protocol: TCP 84 | targetPort: 9092 85 | selector: 86 | app: kafka-app 87 | --- 88 | apiVersion: apps/v1 89 | kind: StatefulSet 90 | metadata: 91 | name: kafka 92 | namespace: netmux 93 | labels: 94 | app: kafka-app 95 | spec: 96 | serviceName: kafka 97 | replicas: 3 98 | selector: 99 | matchLabels: 100 | app: kafka-app 101 | template: 102 | metadata: 103 | labels: 104 | app: kafka-app 105 | spec: 106 | containers: 107 | - name: kafka-container 108 | image: doughgle/kafka-kraft 109 | ports: 110 | - containerPort: 9092 111 | - containerPort: 9093 112 | env: 113 | - name: REPLICAS 114 | value: '3' 115 | - name: SERVICE 116 | value: kafka 117 | - name: NAMESPACE 118 | value: netmux 119 | - name: SHARE_DIR 120 | value: /mnt/kafka 121 | - name: CLUSTER_ID 122 | value: oh-sxaDRTcyAr6pFRbXyzA 123 | - name: DEFAULT_REPLICATION_FACTOR 124 | value: '3' 125 | - name: DEFAULT_MIN_INSYNC_REPLICAS 126 | value: '2' 127 | volumeMounts: 128 | - name: data 129 | mountPath: /mnt/kafka 130 | volumeClaimTemplates: 131 | - metadata: 132 | name: data 133 | spec: 134 | accessModes: 135 | - "ReadWriteOnce" 136 | resources: 137 | requests: 138 | storage: "1Gi" 139 | ``` 140 | 141 | ## Example Postgres 142 | 143 | ```yaml 144 | apiVersion: v1 145 | kind: ConfigMap 146 | metadata: 147 | name: postgres-userconfig 148 | labels: 149 | app: postgres 150 | data: 151 | POSTGRES_DB: "postgres" 152 | POSTGRES_USER: "postgres" 153 | POSTGRES_PASSWORD: "postgres" 154 | --- 155 | apiVersion: v1 156 | kind: PersistentVolumeClaim 157 | metadata: 158 | name: postgres-pvc 159 | spec: 160 | accessModes: 161 | - ReadWriteOnce 162 | resources: 163 | requests: 164 | storage: 1Gi 165 | --- 166 | apiVersion: apps/v1 167 | kind: Deployment 168 | metadata: 169 | name: postgres 170 | spec: 171 | selector: 172 | matchLabels: 173 | app: postgres 174 | replicas: 1 175 | template: 176 | metadata: 177 | labels: 178 | app: postgres 179 | spec: 180 | containers: 181 | - name: postgres 182 | envFrom: 183 | - configMapRef: 184 | name: postgres-userconfig 185 | image: postgres:latest 186 | imagePullPolicy: "IfNotPresent" 187 | ports: 188 | - containerPort: 5432 189 | volumeMounts: 190 | - mountPath: /var/lib/postgresql/data 191 | name: postgresdb 192 | volumes: 193 | - name: postgresdb 194 | persistentVolumeClaim: 195 | claimName: postgres-pvc 196 | --- 197 | kind: Service 198 | apiVersion: v1 199 | metadata: 200 | name: postgres 201 | annotations: 202 | nx: |- 203 | - name: postgres 204 | spec: 205 | selector: 206 | app: postgres 207 | ports: 208 | - port: 5432 209 | type: ClusterIP 210 | ``` -------------------------------------------------------------------------------- /zarf/manifests/grafana/all.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: grafana-pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 50Gi 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | labels: 16 | app: grafana 17 | name: grafana 18 | spec: 19 | selector: 20 | matchLabels: 21 | app: grafana 22 | template: 23 | metadata: 24 | labels: 25 | app: grafana 26 | spec: 27 | securityContext: 28 | fsGroup: 472 29 | supplementalGroups: 30 | - 0 31 | containers: 32 | - name: grafana 33 | image: grafana/grafana:9.1.0 34 | imagePullPolicy: IfNotPresent 35 | ports: 36 | - containerPort: 3000 37 | name: http-grafana 38 | protocol: TCP 39 | readinessProbe: 40 | failureThreshold: 3 41 | httpGet: 42 | path: /robots.txt 43 | port: 3000 44 | scheme: HTTP 45 | initialDelaySeconds: 10 46 | periodSeconds: 30 47 | successThreshold: 1 48 | timeoutSeconds: 2 49 | livenessProbe: 50 | failureThreshold: 3 51 | initialDelaySeconds: 30 52 | periodSeconds: 10 53 | successThreshold: 1 54 | tcpSocket: 55 | port: 3000 56 | timeoutSeconds: 1 57 | resources: 58 | requests: 59 | cpu: 250m 60 | memory: 750Mi 61 | volumeMounts: 62 | - mountPath: /var/lib/grafana 63 | name: grafana-pv 64 | volumes: 65 | - name: grafana-pv 66 | persistentVolumeClaim: 67 | claimName: grafana-pvc 68 | --- 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | name: grafana 73 | annotations: 74 | nx: |- 75 | - name: grafana 76 | localport: "3001" 77 | spec: 78 | ports: 79 | - port: 3000 80 | protocol: TCP 81 | targetPort: http-grafana 82 | selector: 83 | app: grafana 84 | -------------------------------------------------------------------------------- /zarf/manifests/grafana/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - all.yaml -------------------------------------------------------------------------------- /zarf/manifests/kafka/kafka-ui.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kafka-ui-deployment 5 | labels: 6 | app: kafka-ui 7 | # namespace: mstore 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: kafka-ui 13 | template: 14 | metadata: 15 | labels: 16 | app: kafka-ui 17 | spec: 18 | containers: 19 | - name: kafka-ui 20 | image: provectuslabs/kafka-ui:latest 21 | env: 22 | - name: KAFKA_CLUSTERS_0_NAME 23 | value: "K8 Kafka Cluster" 24 | - name: KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS 25 | value: kafka:9092 26 | imagePullPolicy: Always 27 | resources: 28 | requests: 29 | memory: "256Mi" 30 | cpu: "100m" 31 | limits: 32 | memory: "1024Mi" 33 | cpu: "1000m" 34 | ports: 35 | - containerPort: 8080 36 | --- 37 | apiVersion: v1 38 | kind: Service 39 | metadata: 40 | name: kafka-ui 41 | annotations: 42 | nx: |- 43 | - name: kafka-ui 44 | localPort: "80" 45 | spec: 46 | selector: 47 | app: kafka-ui 48 | ports: 49 | - protocol: TCP 50 | port: 8080 -------------------------------------------------------------------------------- /zarf/manifests/kafka/kafka.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: kafka 5 | annotations: 6 | nx: |- 7 | - name: kafka 8 | - name: kafka-0 9 | localAddr: kafka-0.kafka.netmux.svc.cluster.local 10 | containerAddr: kafka-0.kafka.netmux.svc.cluster.local 11 | - name: kafka-1 12 | localAddr: kafka-1.kafka.netmux.svc.cluster.local 13 | containerAddr: kafka-1.kafka.netmux.svc.cluster.local 14 | - name: kafka-2 15 | localAddr: kafka-2.kafka.netmux.svc.cluster.local 16 | containerAddr: kafka-2.kafka.netmux.svc.cluster.local 17 | labels: 18 | app: kafka-app 19 | spec: 20 | ports: 21 | - name: '9092' 22 | port: 9092 23 | protocol: TCP 24 | targetPort: 9092 25 | selector: 26 | app: kafka-app 27 | --- 28 | apiVersion: apps/v1 29 | kind: StatefulSet 30 | metadata: 31 | name: kafka 32 | labels: 33 | app: kafka-app 34 | spec: 35 | serviceName: kafka 36 | replicas: 3 37 | selector: 38 | matchLabels: 39 | app: kafka-app 40 | template: 41 | metadata: 42 | labels: 43 | app: kafka-app 44 | spec: 45 | containers: 46 | - name: kafka-container 47 | image: doughgle/kafka-kraft 48 | ports: 49 | - containerPort: 9092 50 | - containerPort: 9093 51 | env: 52 | - name: REPLICAS 53 | value: '3' 54 | - name: SERVICE 55 | value: kafka 56 | - name: NAMESPACE 57 | value: netmux 58 | - name: SHARE_DIR 59 | value: /mnt/kafka 60 | - name: CLUSTER_ID 61 | value: oh-sxaDRTcyAr6pFRbXyzA 62 | - name: DEFAULT_REPLICATION_FACTOR 63 | value: '3' 64 | - name: DEFAULT_MIN_INSYNC_REPLICAS 65 | value: '2' 66 | volumeMounts: 67 | - name: data 68 | mountPath: /mnt/kafka 69 | volumeClaimTemplates: 70 | - metadata: 71 | name: data 72 | spec: 73 | accessModes: 74 | - "ReadWriteOnce" 75 | resources: 76 | requests: 77 | storage: "1Gi" -------------------------------------------------------------------------------- /zarf/manifests/kafka/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: netmux 4 | resources: 5 | - kafka.yaml 6 | - kafka-ui.yaml -------------------------------------------------------------------------------- /zarf/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: netmux 2 | 3 | resources: 4 | - netmux 5 | - sample-svc 6 | - postgres 7 | - kafka 8 | - prometheus 9 | - grafana -------------------------------------------------------------------------------- /zarf/manifests/netmux/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: netmux 2 | resources: 3 | - namespace.yaml 4 | - rbac-service-account.yaml 5 | - rbac-role.yaml 6 | - rbac-role-binding.yaml 7 | - netmux-deployment-namespace.yaml 8 | - netmux-service.yaml 9 | -------------------------------------------------------------------------------- /zarf/manifests/netmux/namespace.yaml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: netmux -------------------------------------------------------------------------------- /zarf/manifests/netmux/netmux-deployment-namespace.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: netmux 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: netmux 10 | template: 11 | metadata: 12 | labels: 13 | app: netmux 14 | spec: 15 | serviceAccountName: netmux 16 | 17 | containers: 18 | - name: netmux 19 | image: duxthemux/netmux:latest 20 | imagePullPolicy: IfNotPresent 21 | livenessProbe: 22 | httpGet: 23 | port: 8083 24 | path: /live 25 | readinessProbe: 26 | httpGet: 27 | port: 8083 28 | path: /ready 29 | initialDelaySeconds: 5 30 | periodSeconds: 5 31 | env: 32 | - name: LOGLEVEL 33 | value: debug 34 | - name: LOGSRC 35 | value: "false" 36 | ports: 37 | - containerPort: 50000 38 | protocol: TCP 39 | name: grpc 40 | - containerPort: 8082 41 | protocol: TCP 42 | name: prometheus 43 | - containerPort: 8083 44 | protocol: TCP 45 | name: k8sprobes 46 | 47 | -------------------------------------------------------------------------------- /zarf/manifests/netmux/netmux-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: netmux 5 | namespace: netmux 6 | annotations: 7 | prometheus.io/scrape: 'true' 8 | prometheus.io.scheme: "http" 9 | prometheus.io/port: "8081" 10 | nx: |- 11 | - name: netmux-prom 12 | direction: L2C 13 | remotePort: "8081" 14 | localPort: "8081" 15 | localAddr: netmux-prom 16 | spec: 17 | ports: 18 | - port: 50000 19 | name: netmux 20 | protocol: TCP 21 | targetPort: 50000 22 | - port: 8081 23 | name: prometheus 24 | protocol: TCP 25 | targetPort: 8081 26 | - port: 8083 27 | name: probes 28 | protocol: TCP 29 | targetPort: 8083 30 | selector: 31 | app: netmux -------------------------------------------------------------------------------- /zarf/manifests/netmux/rbac-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: netmux 5 | subjects: 6 | - kind: ServiceAccount 7 | name: netmux # name of your service account 8 | namespace: netmux # this is the namespace your service account is in 9 | roleRef: # referring to your ClusterRole 10 | kind: Role 11 | name: netmux 12 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /zarf/manifests/netmux/rbac-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: netmux 5 | rules: 6 | - apiGroups: [ "" ] 7 | resources: [ "nodes", "services", "pods", "endpoints" ] 8 | verbs: [ "get", "list", "watch" ] 9 | - apiGroups: [ "extensions" ] 10 | resources: [ "deployments" ] 11 | verbs: [ "get", "list", "watch" ] -------------------------------------------------------------------------------- /zarf/manifests/netmux/rbac-service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: netmux -------------------------------------------------------------------------------- /zarf/manifests/postgres/all.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: postgres-userconfig 5 | labels: 6 | app: postgres 7 | data: 8 | POSTGRES_DB: "postgres" 9 | POSTGRES_USER: "postgres" 10 | POSTGRES_PASSWORD: "postgres" 11 | PGDATA: "/data/postgres" 12 | --- 13 | apiVersion: v1 14 | kind: PersistentVolumeClaim 15 | metadata: 16 | name: postgres 17 | spec: 18 | accessModes: 19 | - ReadWriteOnce 20 | resources: 21 | requests: 22 | storage: 8Gi 23 | --- 24 | apiVersion: apps/v1 25 | kind: Deployment 26 | metadata: 27 | name: postgres 28 | spec: 29 | selector: 30 | matchLabels: 31 | app: postgres 32 | replicas: 1 33 | template: 34 | metadata: 35 | labels: 36 | app: postgres 37 | spec: 38 | containers: 39 | - name: postgres 40 | envFrom: 41 | - configMapRef: 42 | name: postgres-userconfig 43 | image: postgres:latest 44 | imagePullPolicy: Always 45 | ports: 46 | - containerPort: 5432 47 | volumeMounts: 48 | - mountPath: /data 49 | name: postgres 50 | volumes: 51 | - name: postgres 52 | persistentVolumeClaim: 53 | claimName: postgres 54 | --- 55 | kind: Service 56 | apiVersion: v1 57 | metadata: 58 | name: postgres 59 | annotations: 60 | nx: |- 61 | - name: postgres 62 | spec: 63 | selector: 64 | app: postgres 65 | ports: 66 | - port: 5432 67 | type: ClusterIP -------------------------------------------------------------------------------- /zarf/manifests/postgres/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - all.yaml -------------------------------------------------------------------------------- /zarf/manifests/prometheus/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: prometheus-server-conf 5 | labels: 6 | name: prometheus-server-conf 7 | data: 8 | prometheus.yml: |- 9 | global: 10 | scrape_interval: 15s 11 | external_labels: 12 | monitor: 'netmux' 13 | # Scraping Prometheus itself 14 | scrape_configs: 15 | - job_name: 'prometheus' 16 | scrape_interval: 5s 17 | static_configs: 18 | - targets: ['localhost:9090'] 19 | - job_name: 'kubernetes-service-endpoints' 20 | kubernetes_sd_configs: 21 | - role: endpoints 22 | relabel_configs: 23 | - action: labelmap 24 | regex: __meta_kubernetes_service_label_(.+) 25 | - source_labels: [__meta_kubernetes_namespace] 26 | action: replace 27 | target_label: kubernetes_namespace 28 | - source_labels: [__meta_kubernetes_service_name] 29 | action: replace 30 | target_label: kubernetes_name 31 | 32 | 33 | # scrape_configs: 34 | # - job_name: 'kubernetes-service-endpoints' 35 | # 36 | # scrape_interval: 15s 37 | # scrape_timeout: 10s 38 | # 39 | # kubernetes_sd_configs: 40 | # - role: endpoints 41 | 42 | # relabel_configs: 43 | # - source_labels: [__meta_kubernetes_service_annotation_se7entyse7en_prometheus_scrape] 44 | # action: keep 45 | # regex: true 46 | # - source_labels: [__meta_kubernetes_service_annotation_se7entyse7en_prometheus_scheme] 47 | # action: replace 48 | # target_label: __scheme__ 49 | # regex: (https?) 50 | # - source_labels: [__meta_kubernetes_service_annotation_se7entyse7en_prometheus_path] 51 | # action: replace 52 | # target_label: __metrics_path__ 53 | # regex: (.+) 54 | # - source_labels: [__address__, __meta_kubernetes_service_annotation_se7entyse7en_prometheus_port] 55 | # action: replace 56 | # target_label: __address__ 57 | # regex: ([^:]+)(?::\d+)?;(\d+) 58 | # replacement: $1:$2 59 | # - source_labels: [__meta_kubernetes_namespace] 60 | # action: replace 61 | # target_label: kubernetes_namespace 62 | # - source_labels: [__meta_kubernetes_service_name] 63 | # action: replace 64 | # target_label: kubernetes_service 65 | # - source_labels: [__meta_kubernetes_pod_name] 66 | # action: replace 67 | # target_label: kubernetes_pod -------------------------------------------------------------------------------- /zarf/manifests/prometheus/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: prometheus 5 | labels: 6 | app: prometheus 7 | spec: 8 | replicas: 1 9 | strategy: 10 | rollingUpdate: 11 | maxSurge: 1 12 | maxUnavailable: 1 13 | type: RollingUpdate 14 | selector: 15 | matchLabels: 16 | app: prometheus 17 | template: 18 | metadata: 19 | labels: 20 | app: prometheus 21 | annotations: 22 | prometheus.io/scrape: "true" 23 | prometheus.io/port: "9090" 24 | spec: 25 | serviceAccountName: prometheus 26 | containers: 27 | - name: prometheus 28 | image: prom/prometheus 29 | args: 30 | - '--storage.tsdb.retention=6h' 31 | - '--storage.tsdb.path=/data' 32 | - '--config.file=/etc/prometheus/prometheus.yml' 33 | ports: 34 | - name: web 35 | containerPort: 9090 36 | volumeMounts: 37 | - name: prometheus-userconfig-volume 38 | mountPath: /etc/prometheus 39 | - name: prometheus 40 | mountPath: /data 41 | restartPolicy: Always 42 | 43 | volumes: 44 | - name: prometheus-userconfig-volume 45 | configMap: 46 | defaultMode: 420 47 | name: prometheus-server-conf 48 | 49 | - name: prometheus 50 | persistentVolumeClaim: 51 | claimName: prometheus -------------------------------------------------------------------------------- /zarf/manifests/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - rbac.yaml 3 | - pvc.yaml 4 | - config.yaml 5 | - deployment.yaml 6 | - service.yaml -------------------------------------------------------------------------------- /zarf/manifests/prometheus/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: prometheus 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 50G 11 | 12 | -------------------------------------------------------------------------------- /zarf/manifests/prometheus/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: prometheus 5 | --- 6 | 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: prometheus 11 | rules: 12 | - apiGroups: [""] 13 | resources: 14 | - nodes 15 | - services 16 | - endpoints 17 | - pods 18 | verbs: ["get", "list", "watch"] 19 | - apiGroups: 20 | - extensions 21 | resources: 22 | - ingresses 23 | verbs: ["get", "list", "watch"] 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | name: prometheus 29 | roleRef: 30 | apiGroup: rbac.authorization.k8s.io 31 | kind: ClusterRole 32 | name: prometheus 33 | subjects: 34 | - kind: ServiceAccount 35 | name: prometheus 36 | namespace: netmux -------------------------------------------------------------------------------- /zarf/manifests/prometheus/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: prometheus 5 | annotations: 6 | prometheus.io/scrape: 'true' 7 | prometheus.io/port: '9090' 8 | nx: |- 9 | - name: prometheus 10 | direction: L2C 11 | remoteport: "9090" 12 | localport: "9090" 13 | localaddr: prometheus 14 | 15 | spec: 16 | selector: 17 | app: prometheus 18 | ports: 19 | - port: 9090 20 | targetPort: 9090 -------------------------------------------------------------------------------- /zarf/manifests/sample-svc/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - rev-test-pod.yaml 3 | - sample-deployment.yaml 4 | - sample-revservice.yaml 5 | - sample-service.yaml -------------------------------------------------------------------------------- /zarf/manifests/sample-svc/rev-test-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: rev-test-pod 5 | labels: 6 | app: pod 7 | spec: 8 | containers: 9 | - name: pod 10 | image: alpine 11 | command: [ "/bin/sh", "-c", "--" ] 12 | args: [ "while true; do sleep 30; done;" ] 13 | imagePullPolicy: IfNotPresent 14 | restartPolicy: Always 15 | -------------------------------------------------------------------------------- /zarf/manifests/sample-svc/sample-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: sample 5 | labels: 6 | app: sample 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: sample 12 | template: 13 | metadata: 14 | labels: 15 | app: sample 16 | spec: 17 | containers: 18 | - name: sample 19 | image: duxthemux/sample-service:latest 20 | ports: 21 | - containerPort: 8080 22 | protocol: TCP 23 | resources: {} 24 | terminationMessagePath: /dev/termination-log 25 | terminationMessagePolicy: File 26 | imagePullPolicy: IfNotPresent 27 | 28 | restartPolicy: Always 29 | # imagePullSecrets: 30 | # - name: reg -------------------------------------------------------------------------------- /zarf/manifests/sample-svc/sample-revservice.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: sample-rev 5 | namespace: netmux 6 | annotations: 7 | nx: |- 8 | - localPort: "8082" 9 | name: sample-rev 10 | direction: C2L 11 | auto: false 12 | proto: tcp 13 | localAddr: 127.0.0.1 14 | spec: 15 | ports: 16 | - port: 8082 17 | name: sample-rev 18 | protocol: TCP 19 | targetPort: 8082 20 | selector: 21 | app: netmux -------------------------------------------------------------------------------- /zarf/manifests/sample-svc/sample-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: sample 5 | annotations: 6 | nx: |- 7 | - name: sample01 8 | spec: 9 | ports: 10 | - port: 8080 11 | name: sample 12 | protocol: TCP 13 | targetPort: 8080 14 | 15 | selector: 16 | app: sample -------------------------------------------------------------------------------- /zarf/sample-apps/kcons/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/twmb/franz-go/pkg/kgo" 10 | ) 11 | 12 | func main() { 13 | if err := run(); err != nil { 14 | panic(err) 15 | } 16 | } 17 | 18 | func run() error { 19 | kcli, err := kgo.NewClient(kgo.SeedBrokers( 20 | "kafka-0.kafka.netmux.svc.cluster.local:9092", 21 | "kafka-1.kafka.netmux.svc.cluster.local:9092", 22 | "kafka-2.kafka.netmux.svc.cluster.local:9092"), 23 | kgo.ConsumeTopics("topic01"), 24 | kgo.ConsumerGroup("cons-01"), 25 | kgo.BlockRebalanceOnPoll(), 26 | kgo.FetchMaxWait(time.Second*3), //nolint:gomnd 27 | kgo.DisableAutoCommit(), 28 | ) 29 | if err != nil { 30 | return fmt.Errorf("error connecting to kafka: %w", err) 31 | } 32 | 33 | err = kcli.Ping(context.Background()) 34 | if err != nil { 35 | return fmt.Errorf("error pinging kafka: %w", err) 36 | } 37 | 38 | for { 39 | fetches := kcli.PollRecords(context.Background(), 3) //nolint:gomnd 40 | if fetches.IsClientClosed() { 41 | return nil 42 | } 43 | 44 | if errs := fetches.Errors(); len(errs) > 0 { 45 | return fmt.Errorf("got fetch errors: %v", errs) 46 | } 47 | 48 | if recs := fetches.Records(); len(recs) > 0 { 49 | for _, rec := range fetches.Records() { 50 | log.Print(string(rec.Value)) 51 | } 52 | 53 | kcli.AllowRebalance() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /zarf/sample-apps/kprod/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/twmb/franz-go/pkg/kgo" 9 | ) 10 | 11 | func main() { 12 | if err := run(); err != nil { 13 | panic(err) 14 | } 15 | } 16 | 17 | func run() error { 18 | kcli, err := kgo.NewClient(kgo.SeedBrokers( 19 | "kafka-0.kafka.netmux.svc.cluster.local:9092", 20 | "kafka-1.kafka.netmux.svc.cluster.local:9092", 21 | "kafka-2.kafka.netmux.svc.cluster.local:9092"), 22 | kgo.ConsumeTopics("topic01"), 23 | kgo.ProducerBatchCompression(kgo.GzipCompression()), 24 | kgo.BlockRebalanceOnPoll(), 25 | ) 26 | if err != nil { 27 | return fmt.Errorf("error connecting to kafka: %w", err) 28 | } 29 | 30 | for { 31 | rec := kgo.StringRecord("Hello: " + time.Now().String()) 32 | rec.Topic = "topic01" 33 | kcli.ProduceSync(context.Background(), rec) 34 | time.Sleep(time.Second * 2) //nolint:gomnd 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /zarf/sample-apps/sample-service-reverse/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/duxthemux/netmux/foundation/buildinfo" 13 | ) 14 | 15 | func main() { 16 | http.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) { 17 | slog.Info(fmt.Sprintf("Got request: %s %s\n", request.Method, request.URL.String())) 18 | responseWriter.Header().Set("content-type", "text/plain") 19 | buf := &bytes.Buffer{} 20 | buf.WriteString("SampleService:\n") 21 | buf.WriteString(buildinfo.StringOneLine("")) 22 | 23 | buf.WriteString("Env:\n") 24 | 25 | for _, envEntry := range os.Environ() { 26 | buf.WriteString(fmt.Sprintf("*** %s\n", envEntry)) 27 | } 28 | 29 | buf.WriteString(fmt.Sprintf("Got request: %s %s\n", request.Method, request.URL.String())) 30 | 31 | defer func() { 32 | _ = request.Body.Close() 33 | }() 34 | 35 | _, _ = io.Copy(buf, request.Body) 36 | buf.WriteString("\n==EOR==\n") 37 | _, _ = responseWriter.Write(buf.Bytes()) 38 | // time.Sleep(time.Second * 3) 39 | }) 40 | 41 | slog.Info("Sample Reverse Service") 42 | slog.Info(buildinfo.StringOneLine("")) 43 | 44 | err := http.ListenAndServe(":8082", nil) //nolint:gosec 45 | if err != nil { 46 | panic(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /zarf/sample-apps/sample-service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/duxthemux/netmux/foundation/buildinfo" 13 | ) 14 | 15 | func main() { 16 | http.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) { 17 | slog.Info(fmt.Sprintf("Got request: %s %s\n", request.Method, request.URL.String())) 18 | responseWriter.Header().Set("content-type", "text/plain") 19 | buf := &bytes.Buffer{} 20 | buf.WriteString("SampleService:\n") 21 | buf.WriteString(buildinfo.StringOneLine("")) 22 | 23 | buf.WriteString("Env:\n") 24 | for _, envEntry := range os.Environ() { 25 | buf.WriteString(fmt.Sprintf("*** %s\n", envEntry)) 26 | } 27 | buf.WriteString(fmt.Sprintf("Got request: %s %s\n", request.Method, request.URL.String())) 28 | 29 | defer func() { 30 | _ = request.Body.Close() 31 | }() 32 | _, _ = io.Copy(buf, request.Body) 33 | buf.WriteString("\n==EOR==\n") 34 | _, _ = responseWriter.Write(buf.Bytes()) 35 | }) 36 | 37 | slog.Info("Sample Service") 38 | slog.Info(buildinfo.StringOneLine("")) 39 | 40 | err := http.ListenAndServe(":8080", nil) //nolint:gosec 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /zarf/sample-apps/tun-sample/tun-sample.go: -------------------------------------------------------------------------------- 1 | // src: https://gist.githubusercontent.com/glacjay/585620/raw/3e685b9e9b035c360afc08621a7802e16bc7add4/ping-linux.go 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | func main() { 13 | file, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0) 14 | if err != nil { 15 | panic(fmt.Errorf("error os.Open(): %v\n", err)) 16 | } 17 | 18 | ifr := make([]byte, 18) 19 | 20 | copy(ifr, []byte("tun0")) 21 | 22 | ifr[16], ifr[17] = 0x01, 0x10 23 | 24 | _, _, errno := syscall.Syscall( 25 | syscall.SYS_IOCTL, uintptr(file.Fd()), 26 | uintptr(0x400454ca), uintptr(unsafe.Pointer(&ifr[0]))) 27 | if errno != 0 { 28 | panic(fmt.Errorf("error syscall.Ioctl(): %v\n", errno)) 29 | } 30 | 31 | cmd := exec.Command("/sbin/ifconfig", "tun0", "192.168.7.1", "pointopoint", "192.168.7.2", "up") 32 | 33 | if err := cmd.Start(); err != nil { 34 | panic(fmt.Errorf("error running command: %v\n", err)) 35 | } 36 | 37 | //------NEXT SLIDE---/// 38 | 39 | for { 40 | buf := make([]byte, 2048) 41 | read, err := file.Read(buf) 42 | if err != nil { 43 | panic(fmt.Errorf("error os.Read(): %v\n", err)) 44 | } 45 | 46 | for i := 0; i < 4; i++ { 47 | buf[i+12], buf[i+16] = buf[i+16], buf[i+12] 48 | } 49 | buf[20], buf[22], buf[23] = 0, 0, 0 50 | 51 | var checksum uint16 52 | for i := 20; i < read; i += 2 { 53 | checksum += uint16(buf[i])<<8 + uint16(buf[i+1]) 54 | } 55 | 56 | checksum = ^(checksum + 4) 57 | 58 | buf[22], buf[23] = byte(checksum>>8), byte(checksum&((1<<8)-1)) 59 | 60 | _, err = file.Write(buf) 61 | if err != nil { 62 | panic(fmt.Errorf("error os.Write(): %v\n", err)) 63 | } 64 | } 65 | } 66 | --------------------------------------------------------------------------------