├── .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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------