├── .envrc ├── .github └── workflows │ ├── ci.yml │ ├── release.yml │ └── tip.yaml ├── .gitignore ├── .golangci.toml ├── LICENSE ├── Makefile ├── README.md ├── certc ├── cert.go ├── cert_test.go └── json.go ├── client.go ├── client ├── destination.go ├── direct.go ├── peer.go ├── peer_control.go ├── peer_direct.go ├── peer_relay.go ├── source.go └── status.go ├── client_config.go ├── client_destinations.go ├── client_endpoint.go ├── client_sources.go ├── cmd └── connet │ ├── client.go │ ├── control.go │ ├── main.go │ ├── relay.go │ └── server.go ├── control ├── clients.go ├── ingress.go ├── relays.go ├── secrets.go ├── server.go └── store.go ├── cryptoc ├── derive.go ├── derive_test.go ├── hkdf.go ├── stream.go ├── stream_test.go └── streamer.go ├── e2e_test.go ├── examples ├── client-base.toml ├── client-destination.toml ├── client-source.toml ├── client-token.secret ├── minimal.toml ├── relay-token.secret └── routes.toml ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── iterc ├── iter.go └── slices.go ├── logc └── log.go ├── model ├── build.go ├── encryption.go ├── forward.go ├── hostport.go ├── key.go ├── protos.go ├── proxy.go ├── role.go └── route.go ├── netc ├── addrs.go ├── addrs_test.go ├── backoff.go ├── join.go ├── name.go └── prefix.go ├── nix ├── client-module.nix ├── control-server-module.nix ├── docker.nix ├── module.nix ├── package.nix ├── relay-server-module.nix └── server-module.nix ├── notify ├── value.go └── value_test.go ├── process-compose.yaml ├── proto ├── client.proto ├── connect.proto ├── error.proto ├── model.proto ├── pbclient │ ├── client.pb.go │ └── proto.go ├── pbconnect │ ├── connect.pb.go │ └── proto.go ├── pberror │ ├── error.go │ └── error.pb.go ├── pbmodel │ ├── addr.go │ └── model.pb.go ├── pbrelay │ └── relay.pb.go ├── proto.go └── relay.proto ├── quicc ├── conf.go ├── conn.go └── rtt.go ├── relay ├── clients.go ├── control.go ├── ingress.go ├── server.go └── store.go ├── restr ├── ip.go ├── ip_test.go ├── name.go └── name_test.go ├── selfhosted ├── clients.go └── relays.go ├── server.go ├── server_config.go ├── statusc ├── server.go └── status.go └── websocketc └── join.go /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | layout go 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches: [main] 5 | 6 | permissions: 7 | id-token: write 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: DeterminateSystems/nix-installer-action@main 17 | with: 18 | determinate: true 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: DeterminateSystems/flakehub-cache-action@main 21 | - name: Build connet 22 | run: nix develop --command make build 23 | 24 | test: 25 | name: Test 26 | runs-on: ubuntu-latest 27 | needs: [build] 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: DeterminateSystems/nix-installer-action@main 31 | with: 32 | determinate: true 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | - uses: DeterminateSystems/flakehub-cache-action@main 35 | - name: Run tests 36 | run: nix develop --command make test 37 | - name: Run lint 38 | run: nix develop --command make lint 39 | - name: Go module tidy 40 | run: nix develop --command go mod tidy 41 | - name: Gen proto 42 | run: nix develop --command make gen 43 | - name: Check if tidy or gen proto changed anything 44 | run: git diff --exit-code 45 | 46 | nix-build: 47 | name: Build nix packages 48 | runs-on: ubuntu-latest 49 | needs: [build] 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: DeterminateSystems/nix-installer-action@main 53 | with: 54 | determinate: true 55 | github-token: ${{ secrets.GITHUB_TOKEN }} 56 | - uses: DeterminateSystems/flakehub-cache-action@main 57 | - uses: DeterminateSystems/flake-checker-action@main 58 | - name: Build default 59 | run: nix build . 60 | - name: Build docker 61 | run: nix build .#docker 62 | - name: Flake check 63 | run: nix flake check -L 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_run: 3 | workflows: [ci] 4 | types: [completed] 5 | branches: [main] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: "Version to release (format: vX.Y.Z)" 10 | required: true 11 | upload: 12 | description: "Upload final artifacts to github" 13 | default: false 14 | push: 15 | tags: 16 | - "v[0-9]+.[0-9]+.[0-9]+" 17 | 18 | concurrency: 19 | group: ${{ github.workflow }} 20 | cancel-in-progress: false 21 | 22 | permissions: 23 | contents: write 24 | packages: write 25 | id-token: write 26 | 27 | jobs: 28 | setup: 29 | name: Setup 30 | runs-on: ubuntu-latest 31 | outputs: 32 | version: ${{ steps.extract_version.outputs.version }} 33 | steps: 34 | - name: Exract the Version 35 | id: extract_version 36 | run: | 37 | if [[ "${{ github.event_name }}" == "push" ]]; then 38 | # Remove the leading 'v' from the tag 39 | VERSION=${GITHUB_REF#refs/tags/v} 40 | echo "version=$VERSION" >> $GITHUB_OUTPUT 41 | elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then 42 | VERSION=${{ github.event.inputs.version }} 43 | VERSION=${VERSION#v} 44 | echo "version=$VERSION" >> $GITHUB_OUTPUT 45 | else 46 | echo "Error: Unsupported event type." 47 | exit 1 48 | fi 49 | 50 | binary: 51 | name: Binaries 52 | runs-on: ubuntu-latest 53 | needs: [setup] 54 | env: 55 | CONNET_VERSION: ${{ needs.setup.outputs.version }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: DeterminateSystems/nix-installer-action@main 59 | with: 60 | determinate: true 61 | github-token: ${{ secrets.GITHUB_TOKEN }} 62 | - uses: DeterminateSystems/flakehub-cache-action@main 63 | - name: Build release 64 | run: nix develop --command make release 65 | - name: Upload release 66 | uses: softprops/action-gh-release@v2 67 | if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload == 'true' || github.event_name == 'push' }} 68 | with: 69 | tag_name: v${{ env.CONNET_VERSION }} 70 | files: | 71 | dist/archive/connet-${{ env.CONNET_VERSION }}-*.tar.gz 72 | dist/archive/connet-${{ env.CONNET_VERSION }}-*.zip 73 | 74 | docker-x86: 75 | name: Docker x86 76 | runs-on: ubuntu-latest 77 | needs: [setup] 78 | env: 79 | CONNET_VERSION: ${{ needs.setup.outputs.version }} 80 | steps: 81 | - uses: actions/checkout@v4 82 | - uses: DeterminateSystems/nix-installer-action@main 83 | with: 84 | determinate: true 85 | github-token: ${{ secrets.GITHUB_TOKEN }} 86 | - uses: DeterminateSystems/flakehub-cache-action@main 87 | - name: Docker build 88 | run: nix build .#docker 89 | - name: Docker login 90 | uses: docker/login-action@v3 91 | with: 92 | registry: ghcr.io 93 | username: ${{ github.actor }} 94 | password: ${{ secrets.GITHUB_TOKEN }} 95 | - name: Docker push 96 | if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload == 'true' || github.event_name == 'push' }} 97 | run: nix develop --command skopeo copy "docker-archive:result" "docker://ghcr.io/connet-dev/connet:${CONNET_VERSION}-amd64" 98 | 99 | docker-arm: 100 | name: Docker arm 101 | runs-on: ubuntu-latest 102 | needs: [setup] 103 | env: 104 | CONNET_VERSION: ${{ needs.setup.outputs.version }} 105 | steps: 106 | - uses: actions/checkout@v4 107 | - uses: docker/setup-qemu-action@v3 108 | - uses: DeterminateSystems/nix-installer-action@main 109 | with: 110 | determinate: true 111 | github-token: ${{ secrets.GITHUB_TOKEN }} 112 | extra-conf: system = aarch64-linux 113 | - uses: DeterminateSystems/flakehub-cache-action@main 114 | - name: Docker build 115 | run: nix build .#docker 116 | - name: Docker login 117 | uses: docker/login-action@v3 118 | with: 119 | registry: ghcr.io 120 | username: ${{ github.actor }} 121 | password: ${{ secrets.GITHUB_TOKEN }} 122 | - name: Docker push 123 | if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload == 'true' || github.event_name == 'push' }} 124 | run: nix develop --command skopeo copy "docker-archive:result" "docker://ghcr.io/connet-dev/connet:${CONNET_VERSION}-arm64" 125 | 126 | docker-multiarch: 127 | name: Tag multi-arch 128 | runs-on: ubuntu-latest 129 | needs: [setup, docker-x86, docker-arm] 130 | env: 131 | CONNET_VERSION: ${{ needs.setup.outputs.version }} 132 | steps: 133 | - uses: actions/checkout@v4 134 | - uses: DeterminateSystems/nix-installer-action@main 135 | with: 136 | determinate: true 137 | github-token: ${{ secrets.GITHUB_TOKEN }} 138 | - uses: DeterminateSystems/flakehub-cache-action@main 139 | - name: Docker login 140 | uses: docker/login-action@v3 141 | with: 142 | registry: ghcr.io 143 | username: ${{ github.actor }} 144 | password: ${{ secrets.GITHUB_TOKEN }} 145 | - name: Docker tag 146 | if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload == 'true' || github.event_name == 'push' }} 147 | run: nix develop --command manifest-tool push from-args --platforms linux/amd64,linux/arm64 --template ghcr.io/connet-dev/connet:${CONNET_VERSION}-ARCHVARIANT --target ghcr.io/connet-dev/connet:${CONNET_VERSION} 148 | -------------------------------------------------------------------------------- /.github/workflows/tip.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | 5 | permissions: 6 | packages: write 7 | id-token: write 8 | contents: read 9 | 10 | jobs: 11 | docker-build-x86: 12 | name: Build x86 image 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: DeterminateSystems/nix-installer-action@main 17 | with: 18 | determinate: true 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: DeterminateSystems/flakehub-cache-action@main 21 | - name: Docker build 22 | run: nix build .#docker 23 | - name: Docker login 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Docker push 30 | run: nix develop --command skopeo copy "docker-archive:result" "docker://ghcr.io/connet-dev/connet:latest-amd64" 31 | 32 | docker-build-arm: 33 | name: Build arm image 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: docker/setup-qemu-action@v3 38 | - uses: DeterminateSystems/nix-installer-action@main 39 | with: 40 | determinate: true 41 | github-token: ${{ secrets.GITHUB_TOKEN }} 42 | extra-conf: system = aarch64-linux 43 | - uses: DeterminateSystems/flakehub-cache-action@main 44 | - name: Docker build 45 | run: nix build .#docker 46 | - name: Docker login 47 | uses: docker/login-action@v3 48 | with: 49 | registry: ghcr.io 50 | username: ${{ github.actor }} 51 | password: ${{ secrets.GITHUB_TOKEN }} 52 | - name: Docker push 53 | run: nix develop --command skopeo copy "docker-archive:result" "docker://ghcr.io/connet-dev/connet:latest-arm64" 54 | 55 | docker-multiarch: 56 | name: Tag multi-arch 57 | runs-on: ubuntu-latest 58 | needs: [docker-build-x86, docker-build-arm] 59 | steps: 60 | - uses: actions/checkout@v4 61 | - uses: DeterminateSystems/nix-installer-action@main 62 | with: 63 | determinate: true 64 | github-token: ${{ secrets.GITHUB_TOKEN }} 65 | - uses: DeterminateSystems/flakehub-cache-action@main 66 | - name: Docker login 67 | uses: docker/login-action@v3 68 | with: 69 | registry: ghcr.io 70 | username: ${{ github.actor }} 71 | password: ${{ secrets.GITHUB_TOKEN }} 72 | - name: Docker tag 73 | run: nix develop --command manifest-tool push from-args --platforms linux/amd64,linux/arm64 --template ghcr.io/connet-dev/connet:latest-ARCHVARIANT --target ghcr.io/connet-dev/connet:latest 74 | 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .direnv 3 | result 4 | dist 5 | -------------------------------------------------------------------------------- /.golangci.toml: -------------------------------------------------------------------------------- 1 | [linters-settings.errcheck] 2 | exclude-functions = ["(github.com/quic-go/quic-go.Connection).CloseWithError"] 3 | 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build test lint test-always 2 | 3 | default: all 4 | 5 | all: build test lint 6 | 7 | build: 8 | go install -v github.com/connet-dev/connet/cmd/... 9 | 10 | test: 11 | go test -v -cover -timeout 10s ./... 12 | 13 | lint: 14 | golangci-lint run 15 | 16 | test-always: 17 | go test -v -cover -timeout 10s -count 1 ./... 18 | 19 | test-nix: 20 | nix build .#checks.x86_64-linux.moduleTest 21 | 22 | test-nix-interactive: 23 | nix run .#checks.x86_64-linux.moduleTest.driverInteractive 24 | 25 | .PHONY: gen 26 | gen: 27 | fd --extension ".pb.go" . --exec-batch rm {} 28 | protoc --proto_path=proto/ --go_opt=module=github.com/connet-dev/connet --go_out=./ proto/*.proto 29 | 30 | .PHONY: run-server run-client run-sws 31 | run-server: build 32 | connet server --config examples/minimal.toml 33 | 34 | run-client: build 35 | connet --config examples/minimal.toml 36 | 37 | .PHONY: update-go update-nix 38 | 39 | update-go: 40 | go get -u ./... 41 | go mod tidy 42 | 43 | update-nix: 44 | nix flake update 45 | 46 | .PHONY: release-clean release-build release-archive release 47 | 48 | release-clean: 49 | rm -rf dist/ 50 | 51 | release-build: 52 | GOOS=darwin GOARCH=amd64 go build -v -o dist/build/darwin-amd64/connet github.com/connet-dev/connet/cmd/connet 53 | GOOS=darwin GOARCH=arm64 go build -v -o dist/build/darwin-arm64/connet github.com/connet-dev/connet/cmd/connet 54 | GOOS=linux GOARCH=amd64 go build -v -o dist/build/linux-amd64/connet github.com/connet-dev/connet/cmd/connet 55 | GOOS=linux GOARCH=arm64 go build -v -o dist/build/linux-arm64/connet github.com/connet-dev/connet/cmd/connet 56 | GOOS=freebsd GOARCH=amd64 go build -v -o dist/build/freebsd-amd64/connet github.com/connet-dev/connet/cmd/connet 57 | GOOS=freebsd GOARCH=arm64 go build -v -o dist/build/freebsd-arm64/connet github.com/connet-dev/connet/cmd/connet 58 | GOOS=windows GOARCH=amd64 go build -v -o dist/build/windows-amd64/connet.exe github.com/connet-dev/connet/cmd/connet 59 | GOOS=windows GOARCH=arm64 go build -v -o dist/build/windows-arm64/connet.exe github.com/connet-dev/connet/cmd/connet 60 | 61 | CONNET_VERSION ?= $(shell git describe --exact-match --tags 2> /dev/null || git rev-parse --short HEAD) 62 | 63 | release-archive: 64 | mkdir dist/archive 65 | for x in $(shell ls dist/build); do \ 66 | if [[ $$x == windows* ]]; then \ 67 | zip --junk-paths dist/archive/connet-$(CONNET_VERSION)-$$x.zip dist/build/$$x/*; \ 68 | else \ 69 | tar -czf dist/archive/connet-$(CONNET_VERSION)-$$x.tar.gz -C dist/build/$$x connet; \ 70 | fi \ 71 | done 72 | 73 | release: release-clean release-build release-archive 74 | -------------------------------------------------------------------------------- /certc/cert.go: -------------------------------------------------------------------------------- 1 | package certc 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/ed25519" 7 | "crypto/elliptic" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/tls" 11 | "crypto/x509" 12 | "crypto/x509/pkix" 13 | "encoding/pem" 14 | "fmt" 15 | "io" 16 | "math/big" 17 | "net" 18 | "time" 19 | ) 20 | 21 | var SharedSubject = pkix.Name{ 22 | Country: []string{"US"}, 23 | Organization: []string{"Connet"}, 24 | } 25 | 26 | type Cert struct { 27 | der []byte 28 | pk crypto.PrivateKey 29 | } 30 | 31 | func FromTLS(tlsCert tls.Certificate) *Cert { 32 | return &Cert{ 33 | der: tlsCert.Leaf.Raw, 34 | pk: tlsCert.PrivateKey, 35 | } 36 | } 37 | 38 | type CertOpts struct { 39 | Domains []string 40 | IPs []net.IP 41 | } 42 | 43 | type certType struct{ string } 44 | 45 | var ( 46 | intermediateCert = certType{"intermediate"} 47 | serverCert = certType{"server"} 48 | clientCert = certType{"client"} 49 | ) 50 | 51 | func NewRoot() (*Cert, error) { 52 | pub, priv, err := ed25519.GenerateKey(rand.Reader) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | template := &x509.Certificate{ 58 | SerialNumber: big.NewInt(time.Now().UnixMicro()), 59 | 60 | NotBefore: time.Now(), 61 | NotAfter: time.Now().Add(90 * 24 * time.Hour), 62 | 63 | Subject: SharedSubject, 64 | 65 | BasicConstraintsValid: true, 66 | IsCA: true, 67 | 68 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 69 | ExtKeyUsage: []x509.ExtKeyUsage{}, 70 | } 71 | 72 | der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return &Cert{der, priv}, nil 77 | } 78 | 79 | func (c *Cert) new(opts CertOpts, typ certType) (*Cert, error) { 80 | parent, err := x509.ParseCertificate(c.der) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | var priv crypto.PrivateKey 86 | switch parent.PublicKeyAlgorithm { 87 | case x509.RSA: 88 | priv, err = rsa.GenerateKey(rand.Reader, 4096) 89 | case x509.ECDSA: 90 | priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 91 | case x509.Ed25519: 92 | _, priv, err = ed25519.GenerateKey(rand.Reader) 93 | } 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | csrTemplate := &x509.CertificateRequest{ 99 | Subject: SharedSubject, 100 | 101 | DNSNames: opts.Domains, 102 | IPAddresses: opts.IPs, 103 | } 104 | 105 | csrData, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | csr, err := x509.ParseCertificateRequest(csrData) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | certTemplate := &x509.Certificate{ 116 | Signature: csr.Signature, 117 | SignatureAlgorithm: csr.SignatureAlgorithm, 118 | 119 | PublicKey: csr.PublicKey, 120 | PublicKeyAlgorithm: csr.PublicKeyAlgorithm, 121 | 122 | SerialNumber: big.NewInt(time.Now().UnixMicro()), 123 | 124 | NotBefore: time.Now(), 125 | NotAfter: time.Now().Add(90 * 24 * time.Hour), 126 | 127 | Issuer: parent.Subject, 128 | Subject: csr.Subject, 129 | 130 | DNSNames: opts.Domains, 131 | IPAddresses: opts.IPs, 132 | 133 | BasicConstraintsValid: false, 134 | IsCA: false, 135 | } 136 | 137 | switch typ { 138 | case intermediateCert: 139 | certTemplate.BasicConstraintsValid = true 140 | certTemplate.IsCA = true 141 | 142 | certTemplate.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign 143 | certTemplate.ExtKeyUsage = []x509.ExtKeyUsage{} 144 | case serverCert: 145 | certTemplate.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageContentCommitment 146 | certTemplate.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} 147 | case clientCert: 148 | certTemplate.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageContentCommitment 149 | certTemplate.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} 150 | } 151 | 152 | der, err := x509.CreateCertificate(rand.Reader, certTemplate, parent, csr.PublicKey, c.pk) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | return &Cert{der, priv}, nil 158 | } 159 | 160 | func (c *Cert) NewIntermediate(opts CertOpts) (*Cert, error) { 161 | return c.new(opts, intermediateCert) 162 | } 163 | 164 | func (c *Cert) NewServer(opts CertOpts) (*Cert, error) { 165 | return c.new(opts, serverCert) 166 | } 167 | 168 | func (c *Cert) NewClient(opts CertOpts) (*Cert, error) { 169 | return c.new(opts, clientCert) 170 | } 171 | 172 | func (c *Cert) Cert() (*x509.Certificate, error) { 173 | return x509.ParseCertificate(c.der) 174 | } 175 | 176 | func (c *Cert) Raw() []byte { 177 | return c.der 178 | } 179 | 180 | func (c *Cert) CertPool() (*x509.CertPool, error) { 181 | cert, err := c.Cert() 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | pool := x509.NewCertPool() 187 | pool.AddCert(cert) 188 | return pool, nil 189 | } 190 | 191 | func (c *Cert) TLSCert() (tls.Certificate, error) { 192 | cert, err := c.Cert() 193 | if err != nil { 194 | return tls.Certificate{}, err 195 | } 196 | return tls.Certificate{ 197 | Certificate: [][]byte{c.der}, 198 | PrivateKey: c.pk, 199 | Leaf: cert, 200 | }, nil 201 | } 202 | 203 | func (c *Cert) Encode(certOut io.Writer, keyOut io.Writer) error { 204 | if err := pem.Encode(certOut, &pem.Block{ 205 | Type: "CERTIFICATE", 206 | Bytes: c.der, 207 | }); err != nil { 208 | return fmt.Errorf("cert encode: %w", err) 209 | } 210 | 211 | keyData, err := x509.MarshalPKCS8PrivateKey(c.pk) 212 | if err != nil { 213 | return fmt.Errorf("key marshal: %w", err) 214 | } 215 | 216 | if err := pem.Encode(keyOut, &pem.Block{ 217 | Type: "PRIVATE KEY", 218 | Bytes: keyData, 219 | }); err != nil { 220 | return fmt.Errorf("key encode: %w", err) 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func (c *Cert) EncodeToMemory() ([]byte, []byte, error) { 227 | certPEM := pem.EncodeToMemory(&pem.Block{ 228 | Type: "CERTIFICATE", 229 | Bytes: c.der, 230 | }) 231 | 232 | keyData, err := x509.MarshalPKCS8PrivateKey(c.pk) 233 | if err != nil { 234 | return nil, nil, fmt.Errorf("mem key marshal: %w", err) 235 | } 236 | keyPEM := pem.EncodeToMemory(&pem.Block{ 237 | Type: "PRIVATE KEY", 238 | Bytes: keyData, 239 | }) 240 | return certPEM, keyPEM, nil 241 | } 242 | 243 | func DecodeFromMemory(cert, key []byte) (*Cert, error) { 244 | certDER, _ := pem.Decode(cert) 245 | if certDER == nil { 246 | return nil, fmt.Errorf("cert: no pem block") 247 | } 248 | if certDER.Type != "CERTIFICATE" { 249 | return nil, fmt.Errorf("cert type: %s", certDER.Type) 250 | } 251 | 252 | keyDER, _ := pem.Decode(key) 253 | if keyDER == nil { 254 | return nil, fmt.Errorf("cert key: no pem block") 255 | } 256 | if keyDER.Type != "PRIVATE KEY" { 257 | return nil, fmt.Errorf("cert key type: %s", keyDER.Type) 258 | } 259 | 260 | keyValue, err := x509.ParsePKCS8PrivateKey(keyDER.Bytes) 261 | if err != nil { 262 | return nil, fmt.Errorf("cert parse key: %w", err) 263 | } 264 | 265 | return &Cert{der: certDER.Bytes, pk: keyValue}, nil 266 | } 267 | 268 | func SelfSigned(domain string) (tls.Certificate, *x509.CertPool, error) { 269 | root, err := NewRoot() 270 | if err != nil { 271 | return tls.Certificate{}, nil, err 272 | } 273 | cert, err := root.NewServer(CertOpts{ 274 | Domains: []string{domain}, 275 | }) 276 | if err != nil { 277 | return tls.Certificate{}, nil, err 278 | } 279 | tlsCert, err := cert.TLSCert() 280 | if err != nil { 281 | return tls.Certificate{}, nil, err 282 | } 283 | pool, err := cert.CertPool() 284 | if err != nil { 285 | return tls.Certificate{}, nil, err 286 | } 287 | return tlsCert, pool, nil 288 | } 289 | -------------------------------------------------------------------------------- /certc/cert_test.go: -------------------------------------------------------------------------------- 1 | package certc 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "io" 10 | "net" 11 | "testing" 12 | 13 | "github.com/quic-go/quic-go" 14 | "github.com/stretchr/testify/require" 15 | "golang.org/x/sync/errgroup" 16 | ) 17 | 18 | func TestChain(t *testing.T) { 19 | root, err := NewRoot() 20 | require.NoError(t, err) 21 | 22 | inter, err := root.NewIntermediate(CertOpts{ 23 | Domains: []string{"zzz"}, 24 | }) 25 | require.NoError(t, err) 26 | caPool, err := inter.CertPool() 27 | require.NoError(t, err) 28 | 29 | server, err := inter.NewServer(CertOpts{ 30 | Domains: []string{"zzz"}, 31 | }) 32 | require.NoError(t, err) 33 | serverCert, err := server.TLSCert() 34 | require.NoError(t, err) 35 | 36 | client, err := inter.NewClient(CertOpts{ 37 | Domains: []string{"zzz"}, 38 | }) 39 | require.NoError(t, err) 40 | clientCert, err := client.TLSCert() 41 | require.NoError(t, err) 42 | 43 | testConnectivity(t, serverCert, caPool, clientCert, caPool) 44 | testConnectivityDyn(t, serverCert, caPool, clientCert, caPool) 45 | } 46 | 47 | func TestChainRoot(t *testing.T) { 48 | root, err := NewRoot() 49 | require.NoError(t, err) 50 | rootCert, err := root.Cert() 51 | require.NoError(t, err) 52 | 53 | inter, err := root.NewIntermediate(CertOpts{ 54 | Domains: []string{"zzz"}, 55 | }) 56 | require.NoError(t, err) 57 | caPool, err := inter.CertPool() 58 | require.NoError(t, err) 59 | caPool.AddCert(rootCert) 60 | 61 | server, err := root.NewServer(CertOpts{ 62 | Domains: []string{"zzz"}, 63 | }) 64 | require.NoError(t, err) 65 | serverCert, err := server.TLSCert() 66 | require.NoError(t, err) 67 | 68 | client, err := inter.NewClient(CertOpts{ 69 | Domains: []string{"zzz"}, 70 | }) 71 | require.NoError(t, err) 72 | clientCert, err := client.TLSCert() 73 | require.NoError(t, err) 74 | 75 | testConnectivity(t, serverCert, caPool, clientCert, caPool) 76 | testConnectivityDyn(t, serverCert, caPool, clientCert, caPool) 77 | } 78 | 79 | func TestExchange(t *testing.T) { 80 | serverRoot, err := NewRoot() 81 | require.NoError(t, err) 82 | serverCA, err := serverRoot.CertPool() 83 | require.NoError(t, err) 84 | 85 | serverCert, err := serverRoot.NewServer(CertOpts{ 86 | Domains: []string{"zzz"}, 87 | }) 88 | require.NoError(t, err) 89 | serverTLS, err := serverCert.TLSCert() 90 | require.NoError(t, err) 91 | 92 | clientRoot, err := NewRoot() 93 | require.NoError(t, err) 94 | clientCA, err := clientRoot.CertPool() 95 | require.NoError(t, err) 96 | 97 | clientCert, err := clientRoot.NewClient(CertOpts{ 98 | Domains: []string{"zzz"}, 99 | }) 100 | require.NoError(t, err) 101 | clientTLS, err := clientCert.TLSCert() 102 | require.NoError(t, err) 103 | 104 | testConnectivity(t, serverTLS, clientCA, clientTLS, serverCA) 105 | testConnectivityDyn(t, serverTLS, clientCA, clientTLS, serverCA) 106 | } 107 | 108 | func TestMulti(t *testing.T) { 109 | root, err := NewRoot() 110 | require.NoError(t, err) 111 | 112 | serverCert1, err := root.NewServer(CertOpts{ 113 | Domains: []string{"zzz1"}, 114 | }) 115 | require.NoError(t, err) 116 | serverTLS1, err := serverCert1.TLSCert() 117 | require.NoError(t, err) 118 | serverCA1, err := serverCert1.CertPool() 119 | require.NoError(t, err) 120 | 121 | clientCert1, err := root.NewClient(CertOpts{ 122 | Domains: []string{"zzz1"}, 123 | }) 124 | require.NoError(t, err) 125 | clientTLS1, err := clientCert1.TLSCert() 126 | require.NoError(t, err) 127 | 128 | serverCert2, err := root.NewServer(CertOpts{ 129 | Domains: []string{"zzz2"}, 130 | }) 131 | require.NoError(t, err) 132 | serverTLS2, err := serverCert2.TLSCert() 133 | require.NoError(t, err) 134 | serverCA2, err := serverCert2.CertPool() 135 | require.NoError(t, err) 136 | 137 | clientCert2, err := root.NewClient(CertOpts{ 138 | Domains: []string{"zzz2"}, 139 | }) 140 | require.NoError(t, err) 141 | clientTLS2, err := clientCert2.TLSCert() 142 | require.NoError(t, err) 143 | 144 | clientCA := x509.NewCertPool() 145 | clientXCert1, err := clientCert1.Cert() 146 | require.NoError(t, err) 147 | clientCA.AddCert(clientXCert1) 148 | clientXCert2, err := clientCert2.Cert() 149 | require.NoError(t, err) 150 | clientCA.AddCert(clientXCert2) 151 | 152 | serverConf := &tls.Config{ 153 | Certificates: []tls.Certificate{serverTLS1, serverTLS2}, 154 | ClientCAs: clientCA, 155 | ClientAuth: tls.RequireAndVerifyClientCert, 156 | NextProtos: []string{"test"}, 157 | } 158 | 159 | clientConf1 := &tls.Config{ 160 | Certificates: []tls.Certificate{clientTLS1}, 161 | RootCAs: serverCA1, 162 | ServerName: "zzz1", 163 | NextProtos: []string{"test"}, 164 | } 165 | 166 | testConnectivityTLS(t, serverConf, clientConf1) 167 | 168 | clientConf2 := &tls.Config{ 169 | Certificates: []tls.Certificate{clientTLS2}, 170 | RootCAs: serverCA2, 171 | ServerName: "zzz2", 172 | NextProtos: []string{"test"}, 173 | } 174 | 175 | testConnectivityTLS(t, serverConf, clientConf2) 176 | } 177 | 178 | func testConnectivity(t *testing.T, serverCert tls.Certificate, clientCA *x509.CertPool, clientCert tls.Certificate, rootCA *x509.CertPool) { 179 | serverConf := &tls.Config{ 180 | Certificates: []tls.Certificate{serverCert}, 181 | ClientCAs: clientCA, 182 | ClientAuth: tls.RequireAndVerifyClientCert, 183 | NextProtos: []string{"test"}, 184 | } 185 | 186 | clientConf := &tls.Config{ 187 | Certificates: []tls.Certificate{clientCert}, 188 | RootCAs: rootCA, 189 | ServerName: clientCert.Leaf.DNSNames[0], 190 | NextProtos: []string{"test"}, 191 | } 192 | 193 | testConnectivityTLS(t, serverConf, clientConf) 194 | } 195 | 196 | func testConnectivityDyn(t *testing.T, serverCert tls.Certificate, clientCA *x509.CertPool, clientCert tls.Certificate, rootCA *x509.CertPool) { 197 | serverConf := &tls.Config{ 198 | ClientAuth: tls.RequireAndVerifyClientCert, 199 | NextProtos: []string{"test"}, 200 | } 201 | serverConf.GetConfigForClient = func(_ *tls.ClientHelloInfo) (*tls.Config, error) { 202 | conf := serverConf.Clone() 203 | conf.Certificates = []tls.Certificate{serverCert} 204 | conf.ClientCAs = clientCA 205 | return conf, nil 206 | } 207 | 208 | clientConf := &tls.Config{ 209 | Certificates: []tls.Certificate{clientCert}, 210 | RootCAs: rootCA, 211 | ServerName: clientCert.Leaf.DNSNames[0], 212 | NextProtos: []string{"test"}, 213 | } 214 | 215 | testConnectivityTLS(t, serverConf, clientConf) 216 | } 217 | 218 | func testConnectivityTLS(t *testing.T, serverConf *tls.Config, clientConf *tls.Config) { 219 | udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: 12345}) 220 | require.NoError(t, err) 221 | defer udpConn.Close() 222 | 223 | l, err := quic.Listen(udpConn, serverConf, &quic.Config{}) 224 | require.NoError(t, err) 225 | defer l.Close() 226 | 227 | g, ctx := errgroup.WithContext(context.Background()) 228 | g.Go(func() error { 229 | c, err := l.Accept(ctx) 230 | if err != nil { 231 | return fmt.Errorf("server accept conn: %w", err) 232 | } 233 | 234 | peerCerts := c.ConnectionState().TLS.PeerCertificates 235 | if len(peerCerts) != 1 { 236 | return fmt.Errorf("expected 1 client certificate, but found: %d", len(peerCerts)) 237 | } 238 | if !bytes.Equal(peerCerts[0].Raw, clientConf.Certificates[0].Leaf.Raw) { 239 | return fmt.Errorf("expected matching certs") 240 | } 241 | 242 | s, err := c.AcceptStream(ctx) 243 | if err != nil { 244 | return fmt.Errorf("server accept stream: %w", err) 245 | } 246 | defer s.Close() 247 | 248 | buf := make([]byte, 1) 249 | if _, err := io.ReadFull(s, buf); err != nil { 250 | return fmt.Errorf("server read: %w", err) 251 | } 252 | if _, err := s.Write(buf); err != nil { 253 | return fmt.Errorf("server write: %w", err) 254 | } 255 | return nil 256 | }) 257 | 258 | c, err := quic.DialAddr(ctx, "127.0.0.1:12345", clientConf, &quic.Config{}) 259 | require.NoError(t, err) 260 | defer c.CloseWithError(0, "done") 261 | 262 | g.Go(func() error { 263 | s, err := c.OpenStreamSync(context.Background()) 264 | if err != nil { 265 | return fmt.Errorf("client stream: %w", err) 266 | } 267 | defer s.Close() 268 | 269 | buf := make([]byte, 1) 270 | buf[0] = 33 271 | if _, err := s.Write(buf); err != nil { 272 | return fmt.Errorf("client write: %w", err) 273 | } 274 | buf[0] = 0 275 | if _, err := io.ReadFull(s, buf); err != nil { 276 | return fmt.Errorf("client read: %w", err) 277 | } 278 | return nil 279 | }) 280 | 281 | err = g.Wait() 282 | require.NoError(t, err) 283 | } 284 | -------------------------------------------------------------------------------- /certc/json.go: -------------------------------------------------------------------------------- 1 | package certc 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/json" 6 | ) 7 | 8 | func MarshalJSONCert(cert *x509.Certificate) ([]byte, error) { 9 | s := struct { 10 | Cert []byte `json:"cert"` 11 | }{ 12 | Cert: cert.Raw, 13 | } 14 | return json.Marshal(s) 15 | } 16 | 17 | func UnmarshalJSONCert(b []byte) (*x509.Certificate, error) { 18 | s := struct { 19 | Cert []byte `json:"cert"` 20 | }{} 21 | 22 | if err := json.Unmarshal(b, &s); err != nil { 23 | return nil, err 24 | } 25 | 26 | return x509.ParseCertificate(s.Cert) 27 | } 28 | -------------------------------------------------------------------------------- /client/direct.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "log/slog" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "github.com/connet-dev/connet/model" 13 | "github.com/connet-dev/connet/proto/pberror" 14 | "github.com/connet-dev/connet/quicc" 15 | "github.com/quic-go/quic-go" 16 | ) 17 | 18 | type DirectServer struct { 19 | transport *quic.Transport 20 | logger *slog.Logger 21 | 22 | servers map[string]*vServer 23 | serversMu sync.RWMutex 24 | } 25 | 26 | func NewDirectServer(transport *quic.Transport, logger *slog.Logger) (*DirectServer, error) { 27 | return &DirectServer{ 28 | transport: transport, 29 | logger: logger.With("component", "direct-server"), 30 | 31 | servers: map[string]*vServer{}, 32 | }, nil 33 | } 34 | 35 | type vServer struct { 36 | serverName string 37 | serverCert tls.Certificate 38 | clients map[model.Key]*vClient 39 | clientCA atomic.Pointer[x509.CertPool] 40 | mu sync.RWMutex 41 | } 42 | 43 | type vClient struct { 44 | cert *x509.Certificate 45 | ch chan quic.Connection 46 | } 47 | 48 | func (s *vServer) dequeue(key model.Key, cert *x509.Certificate) *vClient { 49 | s.mu.Lock() 50 | defer s.mu.Unlock() 51 | 52 | if exp, ok := s.clients[key]; ok && exp.cert.Equal(cert) { 53 | delete(s.clients, key) 54 | return exp 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (s *vServer) updateClientCA() { 61 | s.mu.RLock() 62 | defer s.mu.RUnlock() 63 | 64 | clientCA := x509.NewCertPool() 65 | for _, exp := range s.clients { 66 | clientCA.AddCert(exp.cert) 67 | } 68 | s.clientCA.Store(clientCA) 69 | } 70 | 71 | func (s *DirectServer) addServerCert(cert tls.Certificate) { 72 | serverName := cert.Leaf.DNSNames[0] 73 | 74 | s.serversMu.Lock() 75 | defer s.serversMu.Unlock() 76 | 77 | s.logger.Debug("add server cert", "server", serverName, "cert", model.NewKey(cert.Leaf)) 78 | s.servers[serverName] = &vServer{ 79 | serverName: serverName, 80 | serverCert: cert, 81 | clients: map[model.Key]*vClient{}, 82 | } 83 | } 84 | 85 | func (s *DirectServer) getServer(serverName string) *vServer { 86 | s.serversMu.RLock() 87 | defer s.serversMu.RUnlock() 88 | 89 | return s.servers[serverName] 90 | } 91 | 92 | func (s *DirectServer) expect(serverCert tls.Certificate, cert *x509.Certificate) (chan quic.Connection, func()) { 93 | key := model.NewKey(cert) 94 | srv := s.getServer(serverCert.Leaf.DNSNames[0]) 95 | 96 | defer srv.updateClientCA() 97 | 98 | srv.mu.Lock() 99 | defer srv.mu.Unlock() 100 | 101 | s.logger.Debug("expect client", "server", srv.serverName, "cert", key) 102 | ch := make(chan quic.Connection) 103 | srv.clients[key] = &vClient{cert: cert, ch: ch} 104 | return ch, func() { 105 | srv.mu.Lock() 106 | defer srv.mu.Unlock() 107 | 108 | if exp, ok := srv.clients[key]; ok { 109 | s.logger.Debug("unexpect client", "server", srv.serverName, "cert", key) 110 | close(exp.ch) 111 | delete(srv.clients, key) 112 | } 113 | } 114 | } 115 | 116 | func (s *DirectServer) Run(ctx context.Context) error { 117 | tlsConf := &tls.Config{ 118 | ClientAuth: tls.RequireAndVerifyClientCert, 119 | NextProtos: model.ConnectDirectNextProtos, 120 | } 121 | tlsConf.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) { 122 | srv := s.getServer(chi.ServerName) 123 | if srv == nil { 124 | return nil, fmt.Errorf("server not found: %s", chi.ServerName) 125 | } 126 | conf := tlsConf.Clone() 127 | conf.Certificates = []tls.Certificate{srv.serverCert} 128 | conf.ClientCAs = srv.clientCA.Load() 129 | return conf, nil 130 | } 131 | 132 | l, err := s.transport.Listen(tlsConf, quicc.StdConfig) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | s.logger.Debug("listening for conns") 138 | for { 139 | conn, err := l.Accept(ctx) 140 | if err != nil { 141 | s.logger.Debug("accept error", "err", err) 142 | return fmt.Errorf("accept: %w", err) 143 | } 144 | go s.runConn(conn) 145 | } 146 | } 147 | 148 | func (s *DirectServer) runConn(conn quic.Connection) { 149 | srv := s.getServer(conn.ConnectionState().TLS.ServerName) 150 | if srv == nil { 151 | conn.CloseWithError(quic.ApplicationErrorCode(pberror.Code_AuthenticationFailed), "unknown server") 152 | return 153 | } 154 | 155 | cert := conn.ConnectionState().TLS.PeerCertificates[0] 156 | key := model.NewKey(cert) 157 | s.logger.Debug("accepted conn", "server", srv.serverName, "cert", key, "remote", conn.RemoteAddr()) 158 | 159 | exp := srv.dequeue(key, cert) 160 | if exp == nil { 161 | conn.CloseWithError(quic.ApplicationErrorCode(pberror.Code_AuthenticationFailed), "unknown client") 162 | return 163 | } 164 | 165 | s.logger.Debug("accept client", "server", srv.serverName, "cert", key) 166 | exp.ch <- conn 167 | close(exp.ch) 168 | 169 | srv.updateClientCA() 170 | } 171 | -------------------------------------------------------------------------------- /client/peer_control.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/connet-dev/connet/model" 8 | "github.com/connet-dev/connet/proto" 9 | "github.com/connet-dev/connet/proto/pbclient" 10 | "github.com/quic-go/quic-go" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | type peerControl struct { 15 | local *peer 16 | fwd model.Forward 17 | role model.Role 18 | opt model.RouteOption 19 | conn quic.Connection 20 | notify func(error) 21 | } 22 | 23 | func (d *peerControl) run(ctx context.Context) error { 24 | g, ctx := errgroup.WithContext(ctx) 25 | 26 | g.Go(func() error { return d.runAnnounce(ctx) }) 27 | if d.opt.AllowRelay() { 28 | g.Go(func() error { return d.runRelay(ctx) }) 29 | } 30 | 31 | return g.Wait() 32 | } 33 | 34 | func (d *peerControl) runAnnounce(ctx context.Context) error { 35 | stream, err := d.conn.OpenStreamSync(ctx) 36 | if err != nil { 37 | return fmt.Errorf("announce open stream: %w", err) 38 | } 39 | defer stream.Close() 40 | 41 | g, ctx := errgroup.WithContext(ctx) 42 | 43 | g.Go(func() error { 44 | <-ctx.Done() 45 | stream.CancelRead(0) 46 | return nil 47 | }) 48 | 49 | g.Go(func() error { 50 | defer d.local.logger.Debug("completed announce notify") 51 | return d.local.selfListen(ctx, func(peer *pbclient.Peer) error { 52 | d.local.logger.Debug("updated announce", "direct", len(peer.Directs), "relays", len(peer.RelayIds)) 53 | return proto.Write(stream, &pbclient.Request{ 54 | Announce: &pbclient.Request_Announce{ 55 | Forward: d.fwd.PB(), 56 | Role: d.role.PB(), 57 | Peer: peer, 58 | }, 59 | }) 60 | }) 61 | }) 62 | 63 | g.Go(func() error { 64 | for { 65 | resp, err := pbclient.ReadResponse(stream) 66 | d.notify(err) 67 | if err != nil { 68 | return err 69 | } 70 | if resp.Announce == nil { 71 | return fmt.Errorf("announce unexpected response") 72 | } 73 | 74 | // TODO on server restart peers is reset and client loses active peers 75 | // only for them to come back at the next tick, with different ID 76 | d.local.setPeers(resp.Announce.Peers) 77 | } 78 | }) 79 | 80 | return g.Wait() 81 | } 82 | 83 | func (d *peerControl) runRelay(ctx context.Context) error { 84 | stream, err := d.conn.OpenStreamSync(ctx) 85 | if err != nil { 86 | return fmt.Errorf("relay open stream: %w", err) 87 | } 88 | defer stream.Close() 89 | 90 | if err := proto.Write(stream, &pbclient.Request{ 91 | Relay: &pbclient.Request_Relay{ 92 | Forward: d.fwd.PB(), 93 | Role: d.role.PB(), 94 | ClientCertificate: d.local.clientCert.Leaf.Raw, 95 | }, 96 | }); err != nil { 97 | return err 98 | } 99 | 100 | g, ctx := errgroup.WithContext(ctx) 101 | 102 | g.Go(func() error { 103 | <-ctx.Done() 104 | stream.CancelRead(0) 105 | return nil 106 | }) 107 | 108 | g.Go(func() error { 109 | for { 110 | resp, err := pbclient.ReadResponse(stream) 111 | if err != nil { 112 | d.notify(err) 113 | return err 114 | } 115 | if resp.Relay == nil { 116 | return fmt.Errorf("relay unexpected response") 117 | } 118 | 119 | d.local.setRelays(resp.Relay.Relays) 120 | } 121 | }) 122 | 123 | return g.Wait() 124 | } 125 | -------------------------------------------------------------------------------- /client/peer_relay.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/connet-dev/connet/model" 14 | "github.com/connet-dev/connet/netc" 15 | "github.com/connet-dev/connet/proto" 16 | "github.com/connet-dev/connet/proto/pbconnect" 17 | "github.com/connet-dev/connet/proto/pberror" 18 | "github.com/connet-dev/connet/quicc" 19 | "github.com/quic-go/quic-go" 20 | "golang.org/x/sync/errgroup" 21 | ) 22 | 23 | type relayID string 24 | 25 | type relayPeer struct { 26 | local *peer 27 | 28 | serverID relayID 29 | serverHostports []model.HostPort 30 | serverConf atomic.Pointer[serverTLSConfig] 31 | 32 | closer chan struct{} 33 | 34 | logger *slog.Logger 35 | } 36 | 37 | func newRelayPeer(local *peer, id relayID, hps []model.HostPort, serverConf *serverTLSConfig, logger *slog.Logger) *relayPeer { 38 | r := &relayPeer{ 39 | local: local, 40 | serverID: id, 41 | serverHostports: hps, 42 | closer: make(chan struct{}), 43 | logger: logger.With("relay", id, "addrs", hps), 44 | } 45 | r.serverConf.Store(serverConf) 46 | return r 47 | } 48 | 49 | func (r *relayPeer) run(ctx context.Context) { 50 | g, ctx := errgroup.WithContext(ctx) 51 | 52 | g.Go(func() error { return r.runConn(ctx) }) 53 | g.Go(func() error { 54 | <-r.closer 55 | return errPeeringStop 56 | }) 57 | 58 | if err := g.Wait(); err != nil { 59 | r.logger.Debug("error while running relaying", "err", err) 60 | } 61 | } 62 | 63 | func (r *relayPeer) runConn(ctx context.Context) error { 64 | boff := netc.MinBackoff 65 | for { 66 | conn, err := r.connectAny(ctx) 67 | if err != nil { 68 | r.logger.Debug("could not connect relay", "err", err) 69 | if errors.Is(err, context.Canceled) { 70 | return err 71 | } 72 | 73 | select { 74 | case <-ctx.Done(): 75 | return ctx.Err() 76 | case <-time.After(boff): 77 | boff = netc.NextBackoff(boff) 78 | } 79 | continue 80 | } 81 | boff = netc.MinBackoff 82 | 83 | if err := r.keepalive(ctx, conn); err != nil { 84 | r.logger.Debug("disconnected relay", "err", err) 85 | } 86 | } 87 | } 88 | 89 | func (r *relayPeer) connectAny(ctx context.Context) (quic.Connection, error) { 90 | for _, hp := range r.serverHostports { 91 | if conn, err := r.connect(ctx, hp); err != nil { 92 | r.logger.Debug("cannot connet relay", "hostport", hp, "err", err) 93 | } else { 94 | return conn, nil 95 | } 96 | } 97 | return nil, fmt.Errorf("cannot connect to relay: %s", r.serverID) 98 | } 99 | 100 | func (r *relayPeer) connect(ctx context.Context, hp model.HostPort) (quic.Connection, error) { 101 | addr, err := net.ResolveUDPAddr("udp", hp.String()) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | cfg := r.serverConf.Load() 107 | r.logger.Debug("dialing relay", "addr", addr, "server", cfg.name, "cert", cfg.key) 108 | conn, err := r.local.direct.transport.Dial(quicc.RTTContext(ctx), addr, &tls.Config{ 109 | Certificates: []tls.Certificate{r.local.clientCert}, 110 | RootCAs: cfg.cas, 111 | ServerName: cfg.name, 112 | NextProtos: model.ConnectRelayNextProtos, 113 | }, quicc.StdConfig) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | if err := r.check(ctx, conn); err != nil { 119 | conn.CloseWithError(quic.ApplicationErrorCode(pberror.Code_ConnectionCheckFailed), "connection check failed") 120 | return nil, err 121 | } 122 | return conn, nil 123 | } 124 | 125 | func (r *relayPeer) check(ctx context.Context, conn quic.Connection) error { 126 | stream, err := conn.OpenStreamSync(ctx) 127 | if err != nil { 128 | return err 129 | } 130 | defer stream.Close() 131 | 132 | if err := proto.Write(stream, &pbconnect.Request{}); err != nil { 133 | return err 134 | } 135 | if _, err := pbconnect.ReadResponse(stream); err != nil { 136 | return err 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func (r *relayPeer) keepalive(ctx context.Context, conn quic.Connection) error { 143 | defer conn.CloseWithError(quic.ApplicationErrorCode(pberror.Code_RelayKeepaliveClosed), "keepalive closed") 144 | 145 | r.local.addRelayConn(r.serverID, conn) 146 | defer r.local.removeRelayConn(r.serverID) 147 | 148 | quicc.RTTLogStats(conn, r.logger) 149 | for { 150 | select { 151 | case <-ctx.Done(): 152 | return context.Cause(ctx) 153 | case <-conn.Context().Done(): 154 | return context.Cause(conn.Context()) 155 | case <-time.After(30 * time.Second): 156 | quicc.RTTLogStats(conn, r.logger) 157 | } 158 | } 159 | } 160 | 161 | func (r *relayPeer) stop() { 162 | close(r.closer) 163 | } 164 | -------------------------------------------------------------------------------- /client/status.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | type PeerStatus struct { 4 | Relays []RelayConnection `json:"relays"` 5 | Connections []PeerConnection `json:"connections"` 6 | } 7 | 8 | type RelayConnection struct { 9 | ID string `json:"id"` 10 | Addr string `json:"addr"` 11 | } 12 | 13 | type PeerConnection struct { 14 | ID string `json:"id"` 15 | Style string `json:"style"` 16 | Addr string `json:"addr"` 17 | } 18 | 19 | func (p *peer) status() (PeerStatus, error) { 20 | stat := PeerStatus{} 21 | 22 | relays, err := p.relayConns.Peek() 23 | if err != nil { 24 | return PeerStatus{}, err 25 | } 26 | for id, conn := range relays { 27 | stat.Relays = append(stat.Relays, RelayConnection{ 28 | ID: string(id), 29 | Addr: conn.RemoteAddr().String(), 30 | }) 31 | } 32 | 33 | conns, err := p.peerConns.Peek() 34 | if err != nil { 35 | return PeerStatus{}, err 36 | } 37 | for key, conn := range conns { 38 | stat.Connections = append(stat.Connections, PeerConnection{ 39 | ID: key.id, 40 | Style: key.style.String(), 41 | Addr: conn.RemoteAddr().String(), 42 | }) 43 | } 44 | 45 | return stat, nil 46 | } 47 | -------------------------------------------------------------------------------- /client_config.go: -------------------------------------------------------------------------------- 1 | package connet 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/quic-go/quic-go" 16 | ) 17 | 18 | type clientConfig struct { 19 | token string 20 | 21 | controlAddr *net.UDPAddr 22 | controlHost string 23 | controlCAs *x509.CertPool 24 | 25 | directAddr *net.UDPAddr 26 | directResetKey *quic.StatelessResetKey 27 | 28 | logger *slog.Logger 29 | } 30 | 31 | func newClientConfig(opts []ClientOption) (*clientConfig, error) { 32 | cfg := &clientConfig{ 33 | logger: slog.Default(), 34 | } 35 | for _, opt := range opts { 36 | if err := opt(cfg); err != nil { 37 | return nil, err 38 | } 39 | } 40 | 41 | if cfg.controlAddr == nil { 42 | if err := ClientControlAddress("127.0.0.1:19190")(cfg); err != nil { 43 | return nil, fmt.Errorf("default control address: %w", err) 44 | } 45 | } 46 | 47 | if cfg.directAddr == nil { 48 | if err := ClientDirectAddress(":19192")(cfg); err != nil { 49 | return nil, fmt.Errorf("default direct address: %w", err) 50 | } 51 | } 52 | 53 | if cfg.directResetKey == nil { 54 | if err := clientDirectStatelessResetKey()(cfg); err != nil { 55 | return nil, fmt.Errorf("default stateless reset key: %w", err) 56 | } 57 | if cfg.directResetKey == nil { 58 | cfg.logger.Warn("running without a stateless reset key") 59 | } 60 | } 61 | 62 | return cfg, nil 63 | } 64 | 65 | type ClientOption func(cfg *clientConfig) error 66 | 67 | func ClientToken(token string) ClientOption { 68 | return func(cfg *clientConfig) error { 69 | cfg.token = token 70 | return nil 71 | } 72 | } 73 | 74 | func ClientControlAddress(address string) ClientOption { 75 | return func(cfg *clientConfig) error { 76 | if i := strings.LastIndex(address, ":"); i < 0 { 77 | // missing :port, lets give it the default 78 | address = fmt.Sprintf("%s:%d", address, 19190) 79 | } 80 | addr, err := net.ResolveUDPAddr("udp", address) 81 | if err != nil { 82 | return fmt.Errorf("resolve control address: %w", err) 83 | } 84 | host, _, err := net.SplitHostPort(address) 85 | if err != nil { 86 | return fmt.Errorf("split control address: %w", err) 87 | } 88 | 89 | cfg.controlAddr = addr 90 | cfg.controlHost = host 91 | 92 | return nil 93 | } 94 | } 95 | 96 | func ClientControlCAs(certFile string) ClientOption { 97 | return func(cfg *clientConfig) error { 98 | casData, err := os.ReadFile(certFile) 99 | if err != nil { 100 | return fmt.Errorf("read server CAs: %w", err) 101 | } 102 | 103 | cas := x509.NewCertPool() 104 | if !cas.AppendCertsFromPEM(casData) { 105 | return fmt.Errorf("missing server CA certificate in %s", certFile) 106 | } 107 | 108 | cfg.controlCAs = cas 109 | 110 | return nil 111 | } 112 | } 113 | 114 | func ClientDirectAddress(address string) ClientOption { 115 | return func(cfg *clientConfig) error { 116 | addr, err := net.ResolveUDPAddr("udp", address) 117 | if err != nil { 118 | return fmt.Errorf("resolve direct address: %w", err) 119 | } 120 | 121 | cfg.directAddr = addr 122 | 123 | return nil 124 | } 125 | } 126 | 127 | func ClientDirectStatelessResetKey(key *quic.StatelessResetKey) ClientOption { 128 | return func(cfg *clientConfig) error { 129 | cfg.directResetKey = key 130 | return nil 131 | } 132 | } 133 | 134 | func ClientDirectStatelessResetKeyFile(path string) ClientOption { 135 | return func(cfg *clientConfig) error { 136 | keyBytes, err := os.ReadFile(path) 137 | if err != nil { 138 | return fmt.Errorf("read stateless reset key: %w", err) 139 | } 140 | if len(keyBytes) < 32 { 141 | return fmt.Errorf("stateless reset key len %d", len(keyBytes)) 142 | } 143 | 144 | key := quic.StatelessResetKey(keyBytes) 145 | cfg.directResetKey = &key 146 | 147 | return nil 148 | } 149 | } 150 | 151 | func clientDirectStatelessResetKey() ClientOption { 152 | return func(cfg *clientConfig) error { 153 | var name = fmt.Sprintf("stateless-reset-%s.key", 154 | strings.TrimPrefix(strings.ReplaceAll(cfg.directAddr.String(), ":", "-"), "-")) 155 | 156 | var path string 157 | if cacheDir := os.Getenv("CACHE_DIRECTORY"); cacheDir != "" { 158 | path = filepath.Join(cacheDir, name) 159 | } else if userCacheDir, err := os.UserCacheDir(); err == nil { 160 | dir := filepath.Join(userCacheDir, "connet") 161 | switch _, err := os.Stat(dir); { 162 | case err == nil: 163 | // the directory is already there, nothing to do 164 | case errors.Is(err, os.ErrNotExist): 165 | if err := os.Mkdir(dir, 0700); err != nil { 166 | return fmt.Errorf("mkdir cache dir: %w", err) 167 | } 168 | default: 169 | return fmt.Errorf("stat cache dir: %w", err) 170 | } 171 | 172 | path = filepath.Join(dir, name) 173 | } else { 174 | return nil 175 | } 176 | 177 | switch _, err := os.Stat(path); { 178 | case err == nil: 179 | keyBytes, err := os.ReadFile(path) 180 | if err != nil { 181 | return fmt.Errorf("read stateless reset key: %w", err) 182 | } 183 | if len(keyBytes) < 32 { 184 | return fmt.Errorf("stateless reset key len %d", len(keyBytes)) 185 | } 186 | key := quic.StatelessResetKey(keyBytes) 187 | cfg.directResetKey = &key 188 | case errors.Is(err, os.ErrNotExist): 189 | var key quic.StatelessResetKey 190 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 191 | return fmt.Errorf("generate stateless reset key: %w", err) 192 | } 193 | if err := os.WriteFile(path, key[:], 0600); err != nil { 194 | return fmt.Errorf("write stateless reset key: %w", err) 195 | } 196 | cfg.directResetKey = &key 197 | default: 198 | return fmt.Errorf("stat stateless reset key file: %w", err) 199 | } 200 | 201 | return nil 202 | } 203 | } 204 | 205 | func ClientLogger(logger *slog.Logger) ClientOption { 206 | return func(cfg *clientConfig) error { 207 | cfg.logger = logger 208 | return nil 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /client_destinations.go: -------------------------------------------------------------------------------- 1 | package connet 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "log/slog" 8 | "net" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | 13 | "github.com/connet-dev/connet/netc" 14 | ) 15 | 16 | // DestinationTCP creates a new destination which connects to a downstream TCP server 17 | func (c *Client) DestinationTCP(ctx context.Context, cfg DestinationConfig, addr string) error { 18 | dst, err := c.Destination(ctx, cfg) 19 | if err != nil { 20 | return err 21 | } 22 | go func() { 23 | dstSrv := NewTCPDestination(dst, addr, c.logger) 24 | if err := dstSrv.Run(ctx); err != nil { 25 | c.logger.Info("shutting down destination tcp", "err", err) 26 | } 27 | }() 28 | return nil 29 | } 30 | 31 | // DestinationTLS creates a new destination which connects to a downstream TLS server 32 | func (c *Client) DestinationTLS(ctx context.Context, cfg DestinationConfig, addr string, cas *x509.CertPool) error { 33 | dst, err := c.Destination(ctx, cfg) 34 | if err != nil { 35 | return err 36 | } 37 | go func() { 38 | dstSrv := NewTLSDestination(dst, addr, &tls.Config{RootCAs: cas}, c.logger) 39 | if err := dstSrv.Run(ctx); err != nil { 40 | c.logger.Info("shutting down destination tls", "err", err) 41 | } 42 | }() 43 | return nil 44 | } 45 | 46 | // DestinationHTTP creates a new destination which exposes an HTTP server for a given [http.Handler] 47 | func (c *Client) DestinationHTTP(ctx context.Context, cfg DestinationConfig, handler http.Handler) error { 48 | dst, err := c.Destination(ctx, cfg) 49 | if err != nil { 50 | return err 51 | } 52 | go func() { 53 | dstSrv := NewHTTPDestination(dst, handler) 54 | if err := dstSrv.Run(ctx); err != nil { 55 | c.logger.Info("shutting down destination http", "err", err) 56 | } 57 | }() 58 | return nil 59 | } 60 | 61 | // DestinationHTTPProxy creates a new destination which exposes an HTTP proxy server to another HTTP server 62 | func (c *Client) DestinationHTTPProxy(ctx context.Context, cfg DestinationConfig, dstUrl *url.URL) error { 63 | dst, err := c.Destination(ctx, cfg) 64 | if err != nil { 65 | return err 66 | } 67 | go func() { 68 | dstSrv := NewHTTPProxyDestination(dst, dstUrl, nil) 69 | if err := dstSrv.Run(ctx); err != nil { 70 | c.logger.Info("shutting down destination http", "err", err) 71 | } 72 | }() 73 | return nil 74 | } 75 | 76 | // DestinationHTTPSProxy creates a new destination which exposes an HTTP proxy server to another HTTPS server 77 | func (c *Client) DestinationHTTPSProxy(ctx context.Context, cfg DestinationConfig, dstUrl *url.URL, cas *x509.CertPool) error { 78 | dst, err := c.Destination(ctx, cfg) 79 | if err != nil { 80 | return err 81 | } 82 | go func() { 83 | dstSrv := NewHTTPProxyDestination(dst, dstUrl, &tls.Config{RootCAs: cas}) 84 | if err := dstSrv.Run(ctx); err != nil { 85 | c.logger.Info("shutting down destination http", "err", err) 86 | } 87 | }() 88 | return nil 89 | } 90 | 91 | type dialer interface { 92 | DialContext(ctx context.Context, network, address string) (net.Conn, error) 93 | } 94 | 95 | type TCPDestination struct { 96 | dst Destination 97 | dialer dialer 98 | addr string 99 | logger *slog.Logger 100 | } 101 | 102 | func newTCPDestination(dst Destination, d dialer, addr string, logger *slog.Logger) *TCPDestination { 103 | return &TCPDestination{ 104 | dst: dst, 105 | addr: addr, 106 | dialer: d, 107 | logger: logger.With("destination", dst.Config().Forward, "addr", addr), 108 | } 109 | } 110 | 111 | func NewTCPDestination(dst Destination, addr string, logger *slog.Logger) *TCPDestination { 112 | return newTCPDestination(dst, &net.Dialer{}, addr, logger) 113 | } 114 | 115 | func NewTLSDestination(dst Destination, addr string, cfg *tls.Config, logger *slog.Logger) *TCPDestination { 116 | return newTCPDestination(dst, &tls.Dialer{NetDialer: &net.Dialer{}, Config: cfg}, addr, logger) 117 | } 118 | 119 | func (d *TCPDestination) Run(ctx context.Context) error { 120 | return (&netc.Joiner{ 121 | Accept: func(ctx context.Context) (net.Conn, error) { 122 | conn, err := d.dst.AcceptContext(ctx) 123 | d.logger.Debug("destination accept", "err", err) 124 | return conn, err 125 | }, 126 | Dial: func(ctx context.Context) (net.Conn, error) { 127 | conn, err := d.dialer.DialContext(ctx, "tcp", d.addr) 128 | d.logger.Debug("destination dial", "err", err) 129 | return conn, err 130 | }, 131 | Join: func(ctx context.Context, acceptConn, dialConn net.Conn) { 132 | err := netc.Join(acceptConn, dialConn) 133 | d.logger.Debug("destination disconnected", "err", err) 134 | }, 135 | }).Run(ctx) 136 | } 137 | 138 | type HTTPDestination struct { 139 | dst Destination 140 | handler http.Handler 141 | } 142 | 143 | func NewHTTPDestination(dst Destination, handler http.Handler) *HTTPDestination { 144 | return &HTTPDestination{dst, handler} 145 | } 146 | 147 | func NewHTTPFileDestination(dst Destination, root string) *HTTPDestination { 148 | mux := http.NewServeMux() 149 | mux.Handle("/", http.FileServer(http.Dir(root))) 150 | return NewHTTPDestination(dst, mux) 151 | } 152 | 153 | func NewHTTPProxyDestination(dst Destination, dstURL *url.URL, cfg *tls.Config) *HTTPDestination { 154 | return NewHTTPDestination(dst, &httputil.ReverseProxy{ 155 | Rewrite: func(pr *httputil.ProxyRequest) { 156 | pr.SetURL(dstURL) 157 | pr.SetXForwarded() 158 | }, 159 | Transport: &http.Transport{ 160 | TLSClientConfig: cfg, 161 | }, 162 | }) 163 | } 164 | 165 | func (d *HTTPDestination) Run(ctx context.Context) error { 166 | srv := &http.Server{ 167 | Handler: d.handler, 168 | } 169 | 170 | go func() { 171 | <-ctx.Done() 172 | srv.Close() 173 | }() 174 | 175 | return srv.Serve(d.dst) 176 | } 177 | -------------------------------------------------------------------------------- /client_endpoint.go: -------------------------------------------------------------------------------- 1 | package connet 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | "net" 8 | "net/netip" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "github.com/connet-dev/connet/client" 13 | "github.com/connet-dev/connet/statusc" 14 | "github.com/quic-go/quic-go" 15 | ) 16 | 17 | // DestinationConfig structure represents destination configuration. See [Client.DestinationConfig] 18 | type DestinationConfig = client.DestinationConfig 19 | 20 | // NewDestinationConfig creates a destination config for a given name. See [client.NewDestinationConfig] 21 | var NewDestinationConfig = client.NewDestinationConfig 22 | 23 | // Destination is type of endpoint that can receive remote connections and traffic. 24 | // It implements net.Listener interface, so it 25 | type Destination interface { 26 | Config() DestinationConfig 27 | Context() context.Context 28 | 29 | Accept() (net.Conn, error) 30 | AcceptContext(ctx context.Context) (net.Conn, error) 31 | 32 | Client() *Client 33 | Status(ctx context.Context) (EndpointStatus, error) 34 | 35 | Addr() net.Addr 36 | Close() error 37 | } 38 | 39 | // SourceConfig structure represents source configuration. See [Client.SourceConfig] 40 | type SourceConfig = client.SourceConfig 41 | 42 | // NewSourceConfig creates a destination config for a given name. See [client.NewSourceConfig] 43 | var NewSourceConfig = client.NewSourceConfig 44 | 45 | type Source interface { 46 | Config() SourceConfig 47 | Context() context.Context 48 | 49 | Dial(network, address string) (net.Conn, error) 50 | DialContext(ctx context.Context, network, address string) (net.Conn, error) 51 | 52 | Client() *Client 53 | Status(ctx context.Context) (EndpointStatus, error) 54 | 55 | Close() error 56 | } 57 | 58 | type EndpointStatus struct { 59 | Status statusc.Status 60 | Peer client.PeerStatus 61 | } 62 | 63 | var ( 64 | ErrNoActiveDestinations = client.ErrNoActiveDestinations 65 | ErrNoDialedDestinations = client.ErrNoDialedDestinations 66 | ) 67 | 68 | type clientDestination struct { 69 | *client.Destination 70 | *clientEndpoint 71 | } 72 | 73 | func newClientDestination(ctx context.Context, cl *Client, cfg DestinationConfig) (*clientDestination, error) { 74 | dst, err := client.NewDestination(cfg, cl.directServer, cl.rootCert, cl.logger) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | ep, err := newClientEndpoint(ctx, cl, dst, cl.logger.With("destination", cfg.Forward), func() { 80 | cl.removeDestination(cfg.Forward) 81 | }) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return &clientDestination{dst, ep}, nil 87 | } 88 | 89 | type clientSource struct { 90 | *client.Source 91 | *clientEndpoint 92 | } 93 | 94 | func newClientSource(ctx context.Context, cl *Client, cfg SourceConfig) (*clientSource, error) { 95 | src, err := client.NewSource(cfg, cl.directServer, cl.rootCert, cl.logger) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | ep, err := newClientEndpoint(ctx, cl, src, cl.logger.With("source", cfg.Forward), func() { 101 | cl.removeSource(cfg.Forward) 102 | }) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return &clientSource{src, ep}, nil 108 | } 109 | 110 | type endpoint interface { 111 | RunPeer(ctx context.Context) error 112 | RunAnnounce(ctx context.Context, conn quic.Connection, directAddrs []netip.AddrPort, firstReport func(error)) error 113 | PeerStatus() (client.PeerStatus, error) 114 | } 115 | 116 | type clientEndpoint struct { 117 | client *Client 118 | ep endpoint 119 | clientCleanup func() 120 | 121 | ctx context.Context 122 | ctxCancel context.CancelCauseFunc 123 | closer chan struct{} 124 | 125 | onlineReport func(err error) 126 | connStatus atomic.Value 127 | 128 | logger *slog.Logger 129 | } 130 | 131 | // a client endpoint could close when: 132 | // - the user cancels the incomming context. This could happen while setting up the endpoint too. 133 | // - the user calls Close explicitly. 134 | // - the parent client is closing, so it calls close on the endpoint too. Session might be closing at the same time. 135 | // - an error happens in runPeer 136 | // - a terminal error happens in runAnnounce 137 | func newClientEndpoint(ctx context.Context, cl *Client, ep endpoint, logger *slog.Logger, clientCleanup func()) (*clientEndpoint, error) { 138 | ctx, ctxCancel := context.WithCancelCause(ctx) 139 | cep := &clientEndpoint{ 140 | client: cl, 141 | ep: ep, 142 | clientCleanup: clientCleanup, 143 | 144 | ctx: ctx, 145 | ctxCancel: ctxCancel, 146 | closer: make(chan struct{}), 147 | 148 | logger: logger, 149 | } 150 | cep.connStatus.Store(statusc.NotConnected) 151 | context.AfterFunc(ctx, cep.cleanup) 152 | 153 | errCh := make(chan error) 154 | var reportOnce sync.Once 155 | cep.onlineReport = func(err error) { 156 | reportOnce.Do(func() { 157 | if err != nil { 158 | errCh <- err 159 | } 160 | close(errCh) 161 | }) 162 | } 163 | 164 | go cep.runPeer(ctx) 165 | go cep.runAnnounce(ctx) 166 | 167 | select { 168 | case <-ctx.Done(): 169 | cep.ctxCancel(ctx.Err()) 170 | return nil, ctx.Err() 171 | case err := <-errCh: 172 | if err != nil { 173 | cep.ctxCancel(err) 174 | return nil, err 175 | } 176 | } 177 | 178 | return cep, nil 179 | } 180 | 181 | func (e *clientEndpoint) Context() context.Context { 182 | return e.ctx 183 | } 184 | 185 | func (e *clientEndpoint) Client() *Client { 186 | return e.client 187 | } 188 | 189 | func (e *clientEndpoint) Status(ctx context.Context) (EndpointStatus, error) { 190 | peerStatus, err := e.ep.PeerStatus() 191 | if err != nil { 192 | return EndpointStatus{}, err 193 | } 194 | return EndpointStatus{ 195 | Status: e.connStatus.Load().(statusc.Status), 196 | Peer: peerStatus, 197 | }, nil 198 | } 199 | 200 | func (e *clientEndpoint) Addr() net.Addr { 201 | return e.client.directAddr 202 | } 203 | 204 | func (e *clientEndpoint) Close() error { 205 | e.ctxCancel(net.ErrClosed) 206 | <-e.closer 207 | return nil 208 | } 209 | 210 | func (e *clientEndpoint) runPeer(ctx context.Context) { 211 | if err := e.ep.RunPeer(ctx); err != nil { 212 | e.ctxCancel(err) 213 | } 214 | } 215 | 216 | func (e *clientEndpoint) runAnnounce(ctx context.Context) { 217 | err := e.client.currentSession.Listen(ctx, func(sess *session) error { 218 | if sess != nil { 219 | go e.runAnnounceSession(ctx, sess) 220 | } 221 | return nil 222 | }) 223 | if err != nil { 224 | e.ctxCancel(err) 225 | } 226 | } 227 | 228 | func (e *clientEndpoint) runAnnounceSession(ctx context.Context, sess *session) { 229 | for { 230 | err := e.ep.RunAnnounce(ctx, sess.conn, sess.addrs, func(err error) { 231 | if err == nil { 232 | e.connStatus.Store(statusc.Connected) 233 | } 234 | e.onlineReport(err) 235 | }) 236 | e.connStatus.CompareAndSwap(statusc.Connected, statusc.Reconnecting) 237 | 238 | switch { 239 | case err == nil: 240 | case errors.Is(err, context.Canceled): 241 | return 242 | case sess.conn.Context().Err() != nil: 243 | return 244 | default: 245 | e.logger.Debug("announce stopped", "err", err) 246 | } 247 | } 248 | } 249 | 250 | func (e *clientEndpoint) cleanup() { 251 | defer close(e.closer) 252 | defer e.connStatus.Store(statusc.Disconnected) 253 | e.clientCleanup() 254 | } 255 | -------------------------------------------------------------------------------- /client_sources.go: -------------------------------------------------------------------------------- 1 | package connet 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | 14 | "github.com/connet-dev/connet/model" 15 | "github.com/connet-dev/connet/netc" 16 | "github.com/connet-dev/connet/websocketc" 17 | "github.com/gorilla/websocket" 18 | ) 19 | 20 | // SourceTCP creates a new source, and exposes it to a local TCP address to accept incoming traffic 21 | func (c *Client) SourceTCP(ctx context.Context, cfg SourceConfig, addr string) error { 22 | src, err := c.Source(ctx, cfg) 23 | if err != nil { 24 | return err 25 | } 26 | go func() { 27 | srcSrv := NewTCPSource(src, addr, c.logger) 28 | if err := srcSrv.Run(ctx); err != nil { 29 | c.logger.Info("shutting down source tcp", "err", err) 30 | } 31 | }() 32 | return nil 33 | } 34 | 35 | // SourceTLS creates a new source, and exposes it to local TCP address as a TLS server 36 | func (c *Client) SourceTLS(ctx context.Context, cfg SourceConfig, addr string, cert tls.Certificate) error { 37 | src, err := c.Source(ctx, cfg) 38 | if err != nil { 39 | return err 40 | } 41 | go func() { 42 | srcSrv := NewTLSSource(src, addr, &tls.Config{Certificates: []tls.Certificate{cert}}, c.logger) 43 | if err := srcSrv.Run(ctx); err != nil { 44 | c.logger.Info("shutting down source tls", "err", err) 45 | } 46 | }() 47 | return nil 48 | } 49 | 50 | // SourceHTTP creates a new source, and exposes it to local TCP address as an HTTP server 51 | func (c *Client) SourceHTTP(ctx context.Context, cfg SourceConfig, srcURL *url.URL) error { 52 | src, err := c.Source(ctx, cfg) 53 | if err != nil { 54 | return err 55 | } 56 | go func() { 57 | srcSrv := NewHTTPSource(src, srcURL, nil) 58 | if err := srcSrv.Run(ctx); err != nil { 59 | c.logger.Info("shutting down source http", "err", err) 60 | } 61 | }() 62 | return nil 63 | } 64 | 65 | // SourceHTTPS creates a new source, and exposes it to local TCP address as an HTTPS server 66 | func (c *Client) SourceHTTPS(ctx context.Context, cfg SourceConfig, srcURL *url.URL, cert tls.Certificate) error { 67 | src, err := c.Source(ctx, cfg) 68 | if err != nil { 69 | return err 70 | } 71 | go func() { 72 | srcSrv := NewHTTPSource(src, srcURL, &tls.Config{Certificates: []tls.Certificate{cert}}) 73 | if err := srcSrv.Run(ctx); err != nil { 74 | c.logger.Info("shutting down source https", "err", err) 75 | } 76 | }() 77 | return nil 78 | } 79 | 80 | type Binder func(ctx context.Context) (net.Listener, error) 81 | 82 | type TCPSource struct { 83 | src Source 84 | bind Binder 85 | logger *slog.Logger 86 | } 87 | 88 | func NewTCPSource(src Source, addr string, logger *slog.Logger) *TCPSource { 89 | return &TCPSource{ 90 | src: src, 91 | bind: func(ctx context.Context) (net.Listener, error) { 92 | return net.Listen("tcp", addr) 93 | }, 94 | logger: logger.With("source", src.Config().Forward, "addr", addr), 95 | } 96 | } 97 | 98 | func NewTLSSource(src Source, addr string, cfg *tls.Config, logger *slog.Logger) *TCPSource { 99 | return &TCPSource{ 100 | src: src, 101 | bind: func(ctx context.Context) (net.Listener, error) { 102 | return tls.Listen("tcp", addr, cfg) 103 | }, 104 | logger: logger.With("source", src.Config().Forward, "addr", addr), 105 | } 106 | } 107 | 108 | func (s *TCPSource) Run(ctx context.Context) error { 109 | s.logger.Debug("starting source server") 110 | l, err := s.bind(ctx) 111 | if err != nil { 112 | return fmt.Errorf("source server listen: %w", err) 113 | } 114 | defer l.Close() 115 | 116 | go func() { 117 | <-ctx.Done() 118 | l.Close() 119 | }() 120 | 121 | s.logger.Info("listening for conns", "local", l.Addr()) 122 | return (&netc.Joiner{ 123 | Accept: func(ctx context.Context) (net.Conn, error) { 124 | conn, err := l.Accept() 125 | s.logger.Debug("source accept", "err", err) 126 | return conn, err 127 | }, 128 | Dial: func(ctx context.Context) (net.Conn, error) { 129 | conn, err := s.src.DialContext(ctx, "", "") 130 | s.logger.Debug("source dial", "err", err) 131 | return conn, err 132 | }, 133 | Join: func(ctx context.Context, acceptConn, dialConn net.Conn) { 134 | if proxyConn, ok := dialConn.(model.ProxyProtoConn); ok { 135 | if err := proxyConn.WriteProxyHeader(acceptConn.RemoteAddr(), acceptConn.LocalAddr()); err != nil { 136 | s.logger.Debug("source write proxy header", "err", err) 137 | return 138 | } 139 | } 140 | 141 | err := netc.Join(acceptConn, dialConn) 142 | s.logger.Debug("source disconnected", "err", err) 143 | }, 144 | }).Run(ctx) 145 | } 146 | 147 | type HTTPSource struct { 148 | src Source 149 | srcURL *url.URL 150 | cfg *tls.Config 151 | } 152 | 153 | func NewHTTPSource(src Source, srcURL *url.URL, cfg *tls.Config) *HTTPSource { 154 | return &HTTPSource{src, srcURL, cfg} 155 | } 156 | 157 | func (s *HTTPSource) Run(ctx context.Context) error { 158 | fwd := s.src.Config().Forward.String() 159 | var targetURL url.URL = *s.srcURL 160 | targetURL.Scheme = "http" 161 | targetURL.Host = fwd 162 | 163 | srv := &http.Server{ 164 | Addr: s.srcURL.Host, 165 | TLSConfig: s.cfg, 166 | Handler: &httputil.ReverseProxy{ 167 | Rewrite: func(pr *httputil.ProxyRequest) { 168 | pr.SetURL(&targetURL) 169 | pr.SetXForwarded() 170 | }, 171 | Transport: &http.Transport{ 172 | DialContext: s.src.DialContext, 173 | }, 174 | ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { 175 | w.WriteHeader(http.StatusBadGateway) 176 | switch { 177 | case errors.Is(err, ErrNoActiveDestinations): 178 | fmt.Fprintf(w, "[source %s] no active destinations found", fwd) 179 | case errors.Is(err, ErrNoDialedDestinations): 180 | fmt.Fprintf(w, "[source %s] cannot dial active destinations", fwd) 181 | default: 182 | fmt.Fprintf(w, "[source %s] %v", fwd, err) 183 | } 184 | }, 185 | }, 186 | } 187 | 188 | go func() { 189 | <-ctx.Done() 190 | srv.Close() 191 | }() 192 | 193 | if s.cfg != nil { 194 | return srv.ListenAndServeTLS("", "") 195 | } 196 | return srv.ListenAndServe() 197 | } 198 | 199 | type WSSource struct { 200 | src Source 201 | srcURL *url.URL 202 | cfg *tls.Config 203 | logger *slog.Logger 204 | upgrader websocket.Upgrader 205 | } 206 | 207 | func NewWSSource(src Source, srcURL *url.URL, cfg *tls.Config, logger *slog.Logger) *WSSource { 208 | return &WSSource{ 209 | src, srcURL, cfg, logger, websocket.Upgrader{}, 210 | } 211 | } 212 | 213 | func (s *WSSource) handle(w http.ResponseWriter, r *http.Request) { 214 | hconn, err := s.upgrader.Upgrade(w, r, nil) 215 | if err != nil { 216 | s.logger.Debug("could upgrade connection", "err", err) 217 | return 218 | } 219 | defer hconn.Close() 220 | 221 | sconn, err := s.src.DialContext(r.Context(), "", "") 222 | if err != nil { 223 | s.logger.Debug("could not dial destination", "err", err) 224 | return 225 | } 226 | defer sconn.Close() 227 | 228 | err = websocketc.Join(sconn, hconn) 229 | s.logger.Debug("completed websocket connection", "err", err) 230 | } 231 | 232 | func (s *WSSource) Run(ctx context.Context) error { 233 | mux := http.NewServeMux() 234 | path := s.srcURL.Path 235 | if path == "" { 236 | path = "/" 237 | } 238 | mux.HandleFunc(path, s.handle) 239 | 240 | srv := &http.Server{ 241 | Addr: s.srcURL.Host, 242 | TLSConfig: s.cfg, 243 | Handler: mux, 244 | } 245 | 246 | go func() { 247 | <-ctx.Done() 248 | srv.Close() 249 | }() 250 | 251 | if s.cfg != nil { 252 | return srv.ListenAndServeTLS("", "") 253 | } 254 | return srv.ListenAndServe() 255 | } 256 | -------------------------------------------------------------------------------- /cmd/connet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | 15 | "github.com/connet-dev/connet/model" 16 | "github.com/connet-dev/connet/statusc" 17 | "github.com/pelletier/go-toml/v2" 18 | "github.com/spf13/cobra" 19 | "golang.org/x/sync/errgroup" 20 | ) 21 | 22 | type Config struct { 23 | LogLevel string `toml:"log-level"` 24 | LogFormat string `toml:"log-format"` 25 | 26 | Client ClientConfig `toml:"client"` 27 | Server ServerConfig `toml:"server"` 28 | 29 | Control ControlConfig `toml:"control"` 30 | Relay RelayConfig `toml:"relay"` 31 | } 32 | 33 | func main() { 34 | ctx, cancel := signal.NotifyContext(context.Background(), 35 | syscall.SIGINT, syscall.SIGTERM) 36 | defer cancel() 37 | 38 | rootCmd := clientCmd() 39 | rootCmd.AddCommand(serverCmd()) 40 | rootCmd.AddCommand(controlCmd()) 41 | rootCmd.AddCommand(relayCmd()) 42 | rootCmd.AddCommand(checkCmd()) 43 | rootCmd.AddCommand(versionCmd()) 44 | 45 | if err := rootCmd.ExecuteContext(ctx); err != nil { 46 | if cerr := context.Cause(ctx); errors.Is(cerr, context.Canceled) { 47 | return 48 | } 49 | printError(err, 0) 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | func printError(err error, level int) { 55 | errStr := err.Error() 56 | 57 | nextErr := errors.Unwrap(err) 58 | if nextErr != nil { 59 | errStr = strings.TrimSuffix(errStr, nextErr.Error()) 60 | errStr = strings.TrimSuffix(errStr, ": ") 61 | } 62 | 63 | fmt.Fprintf(os.Stderr, "error: %s%s\n", strings.Repeat(" ", level*2), errStr) 64 | if nextErr != nil { 65 | printError(nextErr, level+1) 66 | } 67 | } 68 | 69 | type cobraRunE = func(cmd *cobra.Command, args []string) error 70 | 71 | func wrapErr(ws string, runErr cobraRunE) cobraRunE { 72 | return func(cmd *cobra.Command, args []string) error { 73 | if err := runErr(cmd, args); err != nil { 74 | return fmt.Errorf("%s: %w", ws, err) 75 | } 76 | return nil 77 | } 78 | } 79 | 80 | func checkCmd() *cobra.Command { 81 | cmd := &cobra.Command{ 82 | Use: "check ", 83 | Short: "check configuration file", 84 | Args: cobra.ExactArgs(1), 85 | } 86 | 87 | cmd.RunE = wrapErr("run configuration check", func(_ *cobra.Command, args []string) error { 88 | cfg, err := loadConfigFrom(args[0]) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | if _, err := logger(cfg); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | }) 99 | 100 | return cmd 101 | } 102 | 103 | func versionCmd() *cobra.Command { 104 | cmd := &cobra.Command{ 105 | Use: "version", 106 | Short: "print version information", 107 | } 108 | 109 | cmd.RunE = wrapErr("run configuration check", func(_ *cobra.Command, args []string) error { 110 | fmt.Println(model.BuildVersion()) 111 | return nil 112 | }) 113 | 114 | return cmd 115 | } 116 | 117 | func loadConfigs(files []string) (Config, error) { 118 | var merged Config 119 | 120 | for _, f := range files { 121 | cfg, err := loadConfigFrom(f) 122 | if err != nil { 123 | return Config{}, fmt.Errorf("load config %s: %w", f, err) 124 | } 125 | merged.merge(cfg) 126 | } 127 | 128 | return merged, nil 129 | } 130 | 131 | func loadConfigFrom(file string) (Config, error) { 132 | var cfg Config 133 | 134 | f, err := os.Open(file) 135 | if err != nil { 136 | return cfg, err 137 | } 138 | 139 | dec := toml.NewDecoder(f) 140 | dec = dec.DisallowUnknownFields() 141 | err = dec.Decode(&cfg) 142 | if err != nil { 143 | var serr *toml.StrictMissingError 144 | var derr *toml.DecodeError 145 | if errors.As(err, &serr) { 146 | fmt.Println(serr.String()) 147 | } else if errors.As(err, &derr) { 148 | fmt.Println(derr.String()) 149 | } 150 | } 151 | return cfg, err 152 | } 153 | 154 | func logger(cfg Config) (*slog.Logger, error) { 155 | var logLevel slog.Level 156 | switch cfg.LogLevel { 157 | case "debug": 158 | logLevel = slog.LevelDebug 159 | case "warn": 160 | logLevel = slog.LevelWarn 161 | case "error": 162 | logLevel = slog.LevelError 163 | case "info", "": 164 | logLevel = slog.LevelInfo 165 | default: 166 | return nil, fmt.Errorf("invalid level '%s' (debug|info|warn|error)", cfg.LogLevel) 167 | } 168 | 169 | switch cfg.LogFormat { 170 | case "json": 171 | return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ 172 | Level: logLevel, 173 | })), nil 174 | case "text", "": 175 | return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ 176 | Level: logLevel, 177 | })), nil 178 | default: 179 | return nil, fmt.Errorf("invalid format '%s' (json|text)", cfg.LogFormat) 180 | } 181 | } 182 | 183 | func loadTokens(tokensFile string) ([]string, error) { 184 | f, err := os.Open(tokensFile) 185 | if err != nil { 186 | return nil, fmt.Errorf("open tokens file: %w", err) 187 | } 188 | 189 | var tokens []string 190 | scanner := bufio.NewScanner(f) 191 | for scanner.Scan() { 192 | tokens = append(tokens, scanner.Text()) 193 | } 194 | if err := scanner.Err(); err != nil { 195 | return nil, fmt.Errorf("read tokens file: %w", err) 196 | } 197 | return tokens, nil 198 | } 199 | 200 | func (c *Config) merge(o Config) { 201 | c.LogLevel = override(c.LogLevel, o.LogLevel) 202 | c.LogFormat = override(c.LogFormat, o.LogFormat) 203 | 204 | c.Server.merge(o.Server) 205 | c.Client.merge(o.Client) 206 | 207 | c.Control.merge(o.Control) 208 | c.Relay.merge(o.Relay) 209 | } 210 | 211 | func override(s, o string) string { 212 | if o != "" { 213 | return o 214 | } 215 | return s 216 | } 217 | 218 | func overrides(s, o []string) []string { 219 | if len(o) > 0 { 220 | return o 221 | } 222 | return s 223 | } 224 | 225 | func mergeSlices[S ~[]T, T interface{ merge(T) T }](c S, o S) S { 226 | if len(c) == len(o) { 227 | for i := range c { 228 | c[i] = c[i].merge(o[i]) 229 | } 230 | } else if len(o) > 0 { 231 | return o 232 | } 233 | return c 234 | } 235 | 236 | type withStatus[T any] interface { 237 | Run(context.Context) error 238 | Status(context.Context) (T, error) 239 | } 240 | 241 | func runWithStatus[T any](ctx context.Context, srv withStatus[T], statusAddr *net.TCPAddr, logger *slog.Logger) error { 242 | if statusAddr == nil { 243 | return srv.Run(ctx) 244 | } 245 | 246 | g, ctx := errgroup.WithContext(ctx) 247 | g.Go(func() error { return srv.Run(ctx) }) 248 | g.Go(func() error { 249 | logger.Debug("running status server", "addr", statusAddr) 250 | return statusc.Run(ctx, statusAddr, srv.Status) 251 | }) 252 | return g.Wait() 253 | } 254 | -------------------------------------------------------------------------------- /cmd/connet/relay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "fmt" 7 | "log/slog" 8 | "net" 9 | "os" 10 | 11 | "github.com/connet-dev/connet/relay" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type RelayConfig struct { 16 | Token string `toml:"token"` 17 | TokenFile string `toml:"token-file"` 18 | 19 | Ingresses []RelayIngress `toml:"ingress"` 20 | 21 | ControlAddr string `toml:"control-addr"` 22 | ControlCAs string `toml:"control-cas"` 23 | 24 | StatusAddr string `toml:"status-addr"` 25 | StoreDir string `toml:"store-dir"` 26 | } 27 | 28 | type RelayIngress struct { 29 | Addr string `toml:"addr"` 30 | Hostports []string `toml:"hostports"` 31 | IPRestriction 32 | } 33 | 34 | func relayCmd() *cobra.Command { 35 | cmd := &cobra.Command{ 36 | Use: "relay", 37 | Short: "run connet relay server", 38 | } 39 | cmd.Flags().SortFlags = false 40 | 41 | filenames := cmd.Flags().StringArray("config", nil, "config file to load") 42 | 43 | var flagsConfig Config 44 | cmd.Flags().StringVar(&flagsConfig.LogLevel, "log-level", "", "log level to use") 45 | cmd.Flags().StringVar(&flagsConfig.LogFormat, "log-format", "", "log formatter to use") 46 | 47 | cmd.Flags().StringVar(&flagsConfig.Relay.Token, "token", "", "token to use") 48 | cmd.Flags().StringVar(&flagsConfig.Relay.TokenFile, "token-file", "", "token file to use") 49 | 50 | var ingress RelayIngress 51 | cmd.Flags().StringVar(&ingress.Addr, "addr", "", "server addr to use") 52 | cmd.Flags().StringArrayVar(&ingress.Hostports, "hostport", nil, "server public host[:port] to use (if port is missing will use addr's port)") 53 | cmd.Flags().StringArrayVar(&ingress.IPRestriction.AllowCIDRs, "allow-cidr", nil, "cidr to allow client connections from") 54 | cmd.Flags().StringArrayVar(&ingress.IPRestriction.DenyCIDRs, "deny-cidr", nil, "cidr to deny client connections from") 55 | 56 | cmd.Flags().StringVar(&flagsConfig.Relay.ControlAddr, "control-addr", "", "control server address to connect") 57 | cmd.Flags().StringVar(&flagsConfig.Relay.ControlCAs, "control-cas", "", "control server CAs to use") 58 | 59 | cmd.Flags().StringVar(&flagsConfig.Relay.StatusAddr, "status-addr", "", "status server address to listen") 60 | cmd.Flags().StringVar(&flagsConfig.Relay.StoreDir, "store-dir", "", "storage dir, /tmp subdirectory if empty") 61 | 62 | cmd.RunE = wrapErr("run connet relay server", func(cmd *cobra.Command, _ []string) error { 63 | cfg, err := loadConfigs(*filenames) 64 | if err != nil { 65 | return fmt.Errorf("load config: %w", err) 66 | } 67 | 68 | if !ingress.isZero() { 69 | flagsConfig.Relay.Ingresses = append(flagsConfig.Relay.Ingresses, ingress) 70 | } 71 | cfg.merge(flagsConfig) 72 | 73 | logger, err := logger(cfg) 74 | if err != nil { 75 | return fmt.Errorf("configure logger: %w", err) 76 | } 77 | 78 | return relayRun(cmd.Context(), cfg.Relay, logger) 79 | }) 80 | 81 | return cmd 82 | } 83 | 84 | func relayRun(ctx context.Context, cfg RelayConfig, logger *slog.Logger) error { 85 | relayCfg := relay.Config{ 86 | Logger: logger, 87 | } 88 | 89 | if cfg.TokenFile != "" { 90 | tokens, err := loadTokens(cfg.TokenFile) 91 | if err != nil { 92 | return err 93 | } 94 | relayCfg.ControlToken = tokens[0] 95 | } else { 96 | relayCfg.ControlToken = cfg.Token 97 | } 98 | 99 | if len(cfg.Ingresses) == 0 { 100 | cfg.Ingresses = append(cfg.Ingresses, RelayIngress{}) 101 | } 102 | 103 | var usedDefault bool 104 | for ix, ingressCfg := range cfg.Ingresses { 105 | if ingressCfg.Addr == "" && !usedDefault { 106 | ingressCfg.Addr = ":19191" 107 | usedDefault = true 108 | } 109 | if ingress, err := ingressCfg.parse(); err != nil { 110 | return fmt.Errorf("parse ingress at %d: %w", ix, err) 111 | } else { 112 | relayCfg.Ingress = append(relayCfg.Ingress, ingress) 113 | } 114 | } 115 | 116 | if cfg.ControlAddr == "" { 117 | cfg.ControlAddr = "localhost:19189" 118 | } 119 | controlAddr, err := net.ResolveUDPAddr("udp", cfg.ControlAddr) 120 | if err != nil { 121 | return fmt.Errorf("resolve control address: %w", err) 122 | } 123 | relayCfg.ControlAddr = controlAddr 124 | 125 | if cfg.ControlCAs != "" { 126 | casData, err := os.ReadFile(cfg.ControlCAs) 127 | if err != nil { 128 | return fmt.Errorf("read server CAs: %w", err) 129 | } 130 | 131 | cas := x509.NewCertPool() 132 | if !cas.AppendCertsFromPEM(casData) { 133 | return fmt.Errorf("missing server CA certificate in %s", cfg.ControlCAs) 134 | } 135 | relayCfg.ControlCAs = cas 136 | } 137 | 138 | controlHost, _, err := net.SplitHostPort(cfg.ControlAddr) 139 | if err != nil { 140 | return fmt.Errorf("split control address: %w", err) 141 | } 142 | relayCfg.ControlHost = controlHost 143 | 144 | var statusAddr *net.TCPAddr 145 | if cfg.StatusAddr != "" { 146 | addr, err := net.ResolveTCPAddr("tcp", cfg.StatusAddr) 147 | if err != nil { 148 | return fmt.Errorf("resolve status address: %w", err) 149 | } 150 | statusAddr = addr 151 | } 152 | 153 | if cfg.StoreDir == "" { 154 | dir, err := os.MkdirTemp("", "connet-relay-") 155 | if err != nil { 156 | return fmt.Errorf("create /tmp dir: %w", err) 157 | } 158 | logger.Info("using temporary store directory", "dir", dir) 159 | cfg.StoreDir = dir 160 | } 161 | relayCfg.Stores = relay.NewFileStores(cfg.StoreDir) 162 | 163 | srv, err := relay.NewServer(relayCfg) 164 | if err != nil { 165 | return fmt.Errorf("create relay server: %w", err) 166 | } 167 | return runWithStatus(ctx, srv, statusAddr, logger) 168 | } 169 | 170 | func (cfg RelayIngress) parse() (relay.Ingress, error) { 171 | bldr := relay.NewIngressBuilder(). 172 | WithAddrFrom(cfg.Addr).WithRestrFrom(cfg.AllowCIDRs, cfg.DenyCIDRs) 173 | 174 | for ix, hp := range cfg.Hostports { 175 | bldr = bldr.WithHostportFrom(hp) 176 | if bldr.Error() != nil { 177 | return relay.Ingress{}, fmt.Errorf("parse hostport at %d: %w", ix, bldr.Error()) 178 | } 179 | } 180 | 181 | return bldr.Ingress() 182 | } 183 | 184 | func (c *RelayConfig) merge(o RelayConfig) { 185 | if o.Token != "" || o.TokenFile != "" { // new config completely overrides token 186 | c.Token = o.Token 187 | c.TokenFile = o.TokenFile 188 | } 189 | 190 | c.Ingresses = mergeSlices(c.Ingresses, o.Ingresses) 191 | 192 | c.ControlAddr = override(c.ControlAddr, o.ControlAddr) 193 | c.ControlCAs = override(c.ControlCAs, o.ControlCAs) 194 | 195 | c.StatusAddr = override(c.StatusAddr, o.StatusAddr) 196 | c.StoreDir = override(c.StoreDir, o.StoreDir) 197 | } 198 | 199 | func (c RelayIngress) merge(o RelayIngress) RelayIngress { 200 | return RelayIngress{ 201 | Addr: override(c.Addr, o.Addr), 202 | Hostports: overrides(c.Hostports, o.Hostports), 203 | IPRestriction: c.IPRestriction.merge(o.IPRestriction), 204 | } 205 | } 206 | 207 | func (s RelayIngress) isZero() bool { 208 | return s.Addr == "" && len(s.Hostports) == 0 && len(s.IPRestriction.AllowCIDRs) == 0 && len(s.IPRestriction.DenyCIDRs) == 0 209 | } 210 | 211 | var _ = RelayIngress.merge 212 | -------------------------------------------------------------------------------- /cmd/connet/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net" 8 | 9 | "github.com/connet-dev/connet" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type ServerConfig struct { 14 | Ingresses []ControlIngress `toml:"ingress"` 15 | 16 | Tokens []string `toml:"tokens"` 17 | TokensFile string `toml:"tokens-file"` 18 | TokenRestrictions []TokenRestriction `toml:"token-restriction"` 19 | 20 | RelayIngresses []RelayIngress `toml:"relay-ingress"` 21 | 22 | StatusAddr string `toml:"status-addr"` 23 | StoreDir string `toml:"store-dir"` 24 | } 25 | 26 | func serverCmd() *cobra.Command { 27 | cmd := &cobra.Command{ 28 | Use: "server", 29 | Short: "run connet server", 30 | } 31 | cmd.Flags().SortFlags = false 32 | 33 | filenames := cmd.Flags().StringArray("config", nil, "config file to load, can be passed mulitple times") 34 | 35 | var flagsConfig Config 36 | cmd.Flags().StringVar(&flagsConfig.LogLevel, "log-level", "", "log level to use") 37 | cmd.Flags().StringVar(&flagsConfig.LogFormat, "log-format", "", "log formatter to use") 38 | 39 | var clientIngress ControlIngress 40 | cmd.Flags().StringVar(&clientIngress.Addr, "addr", "", "control server addr to use") 41 | cmd.Flags().StringVar(&clientIngress.Cert, "cert-file", "", "control server cert to use") 42 | cmd.Flags().StringVar(&clientIngress.Key, "key-file", "", "control server key to use") 43 | cmd.Flags().StringArrayVar(&clientIngress.IPRestriction.AllowCIDRs, "allow-cidr", nil, "cidr to allow client connections from") 44 | cmd.Flags().StringArrayVar(&clientIngress.IPRestriction.DenyCIDRs, "deny-cidr", nil, "cidr to deny client connections from") 45 | 46 | cmd.Flags().StringArrayVar(&flagsConfig.Server.Tokens, "tokens", nil, "tokens for clients to connect") 47 | cmd.Flags().StringVar(&flagsConfig.Server.TokensFile, "tokens-file", "", "tokens file to load") 48 | 49 | var relayIngress RelayIngress 50 | cmd.Flags().StringVar(&relayIngress.Addr, "relay-addr", "", "relay server addr to use") 51 | cmd.Flags().StringArrayVar(&relayIngress.Hostports, "relay-hostport", nil, "relay server public host[:port] to use (if port is missing will use addr's port)") 52 | cmd.Flags().StringArrayVar(&relayIngress.IPRestriction.AllowCIDRs, "relay-allow-cidr", nil, "cidr to allow client relay connections from") 53 | cmd.Flags().StringArrayVar(&relayIngress.IPRestriction.DenyCIDRs, "relay-deny-cidr", nil, "cidr to deny client relay connections from") 54 | 55 | cmd.Flags().StringVar(&flagsConfig.Server.StatusAddr, "status-addr", "", "status server address to listen") 56 | cmd.Flags().StringVar(&flagsConfig.Server.StoreDir, "store-dir", "", "storage dir, /tmp subdirectory if empty") 57 | 58 | cmd.RunE = wrapErr("run connet server", func(cmd *cobra.Command, _ []string) error { 59 | cfg, err := loadConfigs(*filenames) 60 | if err != nil { 61 | return fmt.Errorf("load config: %w", err) 62 | } 63 | 64 | if !clientIngress.isZero() { 65 | flagsConfig.Server.Ingresses = append(flagsConfig.Server.Ingresses, clientIngress) 66 | } 67 | if !relayIngress.isZero() { 68 | flagsConfig.Server.RelayIngresses = append(flagsConfig.Server.RelayIngresses, relayIngress) 69 | } 70 | cfg.merge(flagsConfig) 71 | 72 | logger, err := logger(cfg) 73 | if err != nil { 74 | return fmt.Errorf("configure logger: %w", err) 75 | } 76 | 77 | return serverRun(cmd.Context(), cfg.Server, logger) 78 | }) 79 | 80 | return cmd 81 | } 82 | 83 | func serverRun(ctx context.Context, cfg ServerConfig, logger *slog.Logger) error { 84 | var opts []connet.ServerOption 85 | 86 | var usedClientDefault bool 87 | for ix, ingressCfg := range cfg.Ingresses { 88 | if ingressCfg.Addr == "" && !usedClientDefault { 89 | ingressCfg.Addr = ":19190" 90 | usedClientDefault = true 91 | } 92 | if ingress, err := ingressCfg.parse(); err != nil { 93 | return fmt.Errorf("parse ingress at %d: %w", ix, err) 94 | } else { 95 | opts = append(opts, connet.ServerClientsIngress(ingress)) 96 | } 97 | } 98 | 99 | var err error 100 | tokens := cfg.Tokens 101 | if cfg.TokensFile != "" { 102 | tokens, err = loadTokens(cfg.TokensFile) 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | clientAuth, err := parseClientAuth(tokens, cfg.TokenRestrictions) 108 | if err != nil { 109 | return err 110 | } 111 | opts = append(opts, connet.ServerClientsAuthenticator(clientAuth)) 112 | 113 | var usedRelayDefault bool 114 | for ix, ingressCfg := range cfg.RelayIngresses { 115 | if ingressCfg.Addr == "" && !usedRelayDefault { 116 | ingressCfg.Addr = ":19191" 117 | usedRelayDefault = true 118 | } 119 | if ingress, err := ingressCfg.parse(); err != nil { 120 | return fmt.Errorf("parse ingress at %d: %w", ix, err) 121 | } else { 122 | opts = append(opts, connet.ServerRelayIngress(ingress)) 123 | } 124 | } 125 | 126 | var statusAddr *net.TCPAddr 127 | if cfg.StatusAddr != "" { 128 | addr, err := net.ResolveTCPAddr("tcp", cfg.StatusAddr) 129 | if err != nil { 130 | return fmt.Errorf("resolve status address: %w", err) 131 | } 132 | statusAddr = addr 133 | } 134 | 135 | if cfg.StoreDir != "" { 136 | opts = append(opts, connet.ServerStoreDir(cfg.StoreDir)) 137 | } 138 | 139 | opts = append(opts, connet.ServerLogger(logger)) 140 | 141 | srv, err := connet.NewServer(opts...) 142 | if err != nil { 143 | return fmt.Errorf("create server: %w", err) 144 | } 145 | return runWithStatus(ctx, srv, statusAddr, logger) 146 | } 147 | 148 | func (c *ServerConfig) merge(o ServerConfig) { 149 | c.Ingresses = mergeSlices(c.Ingresses, o.Ingresses) 150 | if len(o.Tokens) > 0 || o.TokensFile != "" { // new config completely overrides tokens 151 | c.Tokens = o.Tokens 152 | c.TokensFile = o.TokensFile 153 | } 154 | c.TokenRestrictions = mergeSlices(c.TokenRestrictions, o.TokenRestrictions) 155 | 156 | c.RelayIngresses = mergeSlices(c.RelayIngresses, o.RelayIngresses) 157 | 158 | c.StatusAddr = override(c.StatusAddr, o.StatusAddr) 159 | c.StoreDir = override(c.StoreDir, o.StoreDir) 160 | } 161 | -------------------------------------------------------------------------------- /control/ingress.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/connet-dev/connet/restr" 9 | ) 10 | 11 | type Ingress struct { 12 | Addr *net.UDPAddr 13 | TLS *tls.Config 14 | Restr restr.IP 15 | } 16 | 17 | type IngressBuilder struct { 18 | ingress Ingress 19 | err error 20 | } 21 | 22 | func NewIngressBuilder() *IngressBuilder { return &IngressBuilder{} } 23 | 24 | func (b *IngressBuilder) WithAddr(addr *net.UDPAddr) *IngressBuilder { 25 | if b.err != nil { 26 | return b 27 | } 28 | b.ingress.Addr = addr 29 | return b 30 | } 31 | 32 | func (b *IngressBuilder) WithAddrFrom(addrStr string) *IngressBuilder { 33 | if b.err != nil { 34 | return b 35 | } 36 | 37 | addr, err := net.ResolveUDPAddr("udp", addrStr) 38 | if err != nil { 39 | b.err = fmt.Errorf("resolve udp address: %w", err) 40 | return b 41 | } 42 | return b.WithAddr(addr) 43 | } 44 | 45 | func (b *IngressBuilder) WithTLS(cfg *tls.Config) *IngressBuilder { 46 | if b.err != nil { 47 | return b 48 | } 49 | 50 | b.ingress.TLS = cfg 51 | return b 52 | } 53 | 54 | func (b *IngressBuilder) WithTLSCert(cert tls.Certificate) *IngressBuilder { 55 | if b.err != nil { 56 | return b 57 | } 58 | 59 | return b.WithTLS(&tls.Config{Certificates: []tls.Certificate{cert}}) 60 | } 61 | 62 | func (b *IngressBuilder) WithTLSCertFrom(certFile, keyFile string) *IngressBuilder { 63 | if b.err != nil { 64 | return b 65 | } 66 | 67 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 68 | if err != nil { 69 | b.err = fmt.Errorf("load certificate: %w", err) 70 | return b 71 | } 72 | 73 | return b.WithTLSCert(cert) 74 | } 75 | 76 | func (b *IngressBuilder) WithRestr(iprestr restr.IP) *IngressBuilder { 77 | if b.err != nil { 78 | return b 79 | } 80 | 81 | b.ingress.Restr = iprestr 82 | return b 83 | } 84 | 85 | func (b *IngressBuilder) WithRestrFrom(allows []string, denies []string) *IngressBuilder { 86 | if b.err != nil { 87 | return b 88 | } 89 | 90 | iprestr, err := restr.ParseIP(allows, denies) 91 | if err != nil { 92 | b.err = fmt.Errorf("parse restrictions: %w", err) 93 | return b 94 | } 95 | return b.WithRestr(iprestr) 96 | } 97 | 98 | func (b *IngressBuilder) Error() error { 99 | return b.err 100 | } 101 | 102 | func (b *IngressBuilder) Ingress() (Ingress, error) { 103 | return b.ingress, b.err 104 | } 105 | -------------------------------------------------------------------------------- /control/secrets.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/segmentio/ksuid" 10 | "golang.org/x/crypto/nacl/secretbox" 11 | ) 12 | 13 | var errEncryptedDataMissing = errors.New("encrypted data missing") 14 | var errSecretboxOpen = errors.New("secretbox open failed") 15 | 16 | type reconnectToken struct { 17 | secretKey [32]byte 18 | } 19 | 20 | func (s *reconnectToken) seal(data []byte) ([]byte, error) { 21 | var nonce [24]byte 22 | if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { 23 | return nil, fmt.Errorf("generate rand: %w", err) 24 | } 25 | 26 | return secretbox.Seal(nonce[:], data, &nonce, &s.secretKey), nil 27 | } 28 | 29 | func (s *reconnectToken) open(encrypted []byte) ([]byte, error) { 30 | if len(encrypted) < 24 { 31 | return nil, errEncryptedDataMissing 32 | } 33 | 34 | var decryptNonce [24]byte 35 | copy(decryptNonce[:], encrypted[:24]) 36 | data, ok := secretbox.Open(nil, encrypted[24:], &decryptNonce, &s.secretKey) 37 | if !ok { 38 | return nil, errSecretboxOpen 39 | } 40 | return data, nil 41 | } 42 | 43 | func (s *reconnectToken) sealID(id ksuid.KSUID) ([]byte, error) { 44 | return s.seal(id.Bytes()) 45 | } 46 | 47 | func (s *reconnectToken) openID(encrypted []byte) (ksuid.KSUID, error) { 48 | data, err := s.open(encrypted) 49 | if err != nil { 50 | return ksuid.Nil, err 51 | } 52 | id, err := ksuid.FromBytes(data) 53 | if err != nil { 54 | return ksuid.Nil, fmt.Errorf("ksuid decode: %w", err) 55 | } 56 | return id, nil 57 | } 58 | -------------------------------------------------------------------------------- /control/server.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/connet-dev/connet/iterc" 9 | "github.com/connet-dev/connet/model" 10 | "github.com/segmentio/ksuid" 11 | "golang.org/x/sync/errgroup" 12 | ) 13 | 14 | type Config struct { 15 | ClientsIngress []Ingress 16 | ClientsAuth ClientAuthenticator 17 | 18 | RelaysIngress []Ingress 19 | RelaysAuth RelayAuthenticator 20 | 21 | Stores Stores 22 | 23 | Logger *slog.Logger 24 | } 25 | 26 | func NewServer(cfg Config) (*Server, error) { 27 | configStore, err := cfg.Stores.Config() 28 | if err != nil { 29 | return nil, fmt.Errorf("config store open: %w", err) 30 | } 31 | 32 | relays, err := newRelayServer(cfg.RelaysIngress, cfg.RelaysAuth, configStore, cfg.Stores, cfg.Logger) 33 | if err != nil { 34 | return nil, fmt.Errorf("create relay server: %w", err) 35 | } 36 | 37 | clients, err := newClientServer(cfg.ClientsIngress, cfg.ClientsAuth, relays, configStore, cfg.Stores, cfg.Logger) 38 | if err != nil { 39 | return nil, fmt.Errorf("create client server: %w", err) 40 | } 41 | 42 | return &Server{ 43 | clients: clients, 44 | relays: relays, 45 | }, nil 46 | } 47 | 48 | type Server struct { 49 | clients *clientServer 50 | relays *relayServer 51 | } 52 | 53 | func (s *Server) Run(ctx context.Context) error { 54 | g, ctx := errgroup.WithContext(ctx) 55 | 56 | g.Go(func() error { return s.relays.run(ctx) }) 57 | g.Go(func() error { return s.clients.run(ctx) }) 58 | 59 | return g.Wait() 60 | } 61 | 62 | func (s *Server) Status(ctx context.Context) (Status, error) { 63 | clients, err := s.getClients() 64 | if err != nil { 65 | return Status{}, err 66 | } 67 | 68 | peers, err := s.getPeers() 69 | if err != nil { 70 | return Status{}, err 71 | } 72 | 73 | relays, err := s.getRelays() 74 | if err != nil { 75 | return Status{}, err 76 | } 77 | 78 | return Status{ 79 | ServerID: s.relays.id, 80 | Clients: clients, 81 | Peers: peers, 82 | Relays: relays, 83 | }, nil 84 | } 85 | 86 | func (s *Server) getClients() ([]StatusClient, error) { 87 | clientMsgs, _, err := s.clients.conns.Snapshot() 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | var clients []StatusClient 93 | for _, msg := range clientMsgs { 94 | clients = append(clients, StatusClient{ 95 | ID: msg.Key.ID, 96 | Addr: msg.Value.Addr, 97 | }) 98 | } 99 | 100 | return clients, nil 101 | } 102 | 103 | func (s *Server) getPeers() ([]StatusPeer, error) { 104 | peerMsgs, _, err := s.clients.peers.Snapshot() 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | var peers []StatusPeer 110 | for _, msg := range peerMsgs { 111 | peers = append(peers, StatusPeer{ 112 | ID: msg.Key.ID, 113 | Role: msg.Key.Role, 114 | Forward: msg.Key.Forward, 115 | }) 116 | } 117 | 118 | return peers, nil 119 | } 120 | 121 | func (s *Server) getRelays() ([]StatusRelay, error) { 122 | msgs, _, err := s.relays.conns.Snapshot() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | var relays []StatusRelay 128 | for _, msg := range msgs { 129 | relays = append(relays, StatusRelay{ 130 | ID: msg.Key.ID, 131 | Hostports: iterc.MapSlice(msg.Value.Hostports, model.HostPort.String), 132 | }) 133 | } 134 | 135 | return relays, nil 136 | } 137 | 138 | type Status struct { 139 | ServerID string `json:"server_id"` 140 | Clients []StatusClient `json:"clients"` 141 | Peers []StatusPeer `json:"peers"` 142 | Relays []StatusRelay `json:"relays"` 143 | } 144 | 145 | type StatusClient struct { 146 | ID ksuid.KSUID `json:"id"` 147 | Addr string `json:"addr"` 148 | } 149 | 150 | type StatusPeer struct { 151 | ID ksuid.KSUID `json:"id"` 152 | Role model.Role `json:"role"` 153 | Forward model.Forward `json:"forward"` 154 | } 155 | 156 | type StatusRelay struct { 157 | ID ksuid.KSUID `json:"id"` 158 | Hostports []string `json:"hostport"` 159 | } 160 | -------------------------------------------------------------------------------- /control/store.go: -------------------------------------------------------------------------------- 1 | package control 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/json" 6 | "path/filepath" 7 | 8 | "github.com/connet-dev/connet/certc" 9 | "github.com/connet-dev/connet/logc" 10 | "github.com/connet-dev/connet/model" 11 | "github.com/connet-dev/connet/proto/pbclient" 12 | "github.com/segmentio/ksuid" 13 | ) 14 | 15 | type Stores interface { 16 | Config() (logc.KV[ConfigKey, ConfigValue], error) 17 | 18 | ClientConns() (logc.KV[ClientConnKey, ClientConnValue], error) 19 | ClientPeers() (logc.KV[ClientPeerKey, ClientPeerValue], error) 20 | 21 | RelayConns() (logc.KV[RelayConnKey, RelayConnValue], error) 22 | RelayClients() (logc.KV[RelayClientKey, RelayClientValue], error) 23 | RelayForwards(id ksuid.KSUID) (logc.KV[RelayForwardKey, RelayForwardValue], error) 24 | RelayServers() (logc.KV[RelayServerKey, RelayServerValue], error) 25 | RelayServerOffsets() (logc.KV[RelayConnKey, int64], error) 26 | } 27 | 28 | func NewFileStores(dir string) Stores { 29 | return &fileStores{dir} 30 | } 31 | 32 | type fileStores struct { 33 | dir string 34 | } 35 | 36 | func (f *fileStores) Config() (logc.KV[ConfigKey, ConfigValue], error) { 37 | return logc.NewKV[ConfigKey, ConfigValue](filepath.Join(f.dir, "config")) 38 | } 39 | 40 | func (f *fileStores) ClientConns() (logc.KV[ClientConnKey, ClientConnValue], error) { 41 | return logc.NewKV[ClientConnKey, ClientConnValue](filepath.Join(f.dir, "client-conns")) 42 | } 43 | 44 | func (f *fileStores) ClientPeers() (logc.KV[ClientPeerKey, ClientPeerValue], error) { 45 | return logc.NewKV[ClientPeerKey, ClientPeerValue](filepath.Join(f.dir, "client-peers")) 46 | } 47 | 48 | func (f *fileStores) RelayConns() (logc.KV[RelayConnKey, RelayConnValue], error) { 49 | return logc.NewKV[RelayConnKey, RelayConnValue](filepath.Join(f.dir, "relay-conns")) 50 | } 51 | 52 | func (f *fileStores) RelayClients() (logc.KV[RelayClientKey, RelayClientValue], error) { 53 | return logc.NewKV[RelayClientKey, RelayClientValue](filepath.Join(f.dir, "relay-clients")) 54 | } 55 | 56 | func (f *fileStores) RelayForwards(id ksuid.KSUID) (logc.KV[RelayForwardKey, RelayForwardValue], error) { 57 | return logc.NewKV[RelayForwardKey, RelayForwardValue](filepath.Join(f.dir, "relay-forwards", id.String())) 58 | } 59 | 60 | func (f *fileStores) RelayServers() (logc.KV[RelayServerKey, RelayServerValue], error) { 61 | return logc.NewKV[RelayServerKey, RelayServerValue](filepath.Join(f.dir, "relay-servers")) 62 | } 63 | 64 | func (f *fileStores) RelayServerOffsets() (logc.KV[RelayConnKey, int64], error) { 65 | return logc.NewKV[RelayConnKey, int64](filepath.Join(f.dir, "relay-server-offsets")) 66 | } 67 | 68 | type ConfigKey string 69 | 70 | var ( 71 | configClientStatelessReset ConfigKey = "client-stateless-reset" 72 | configRelayStatelessReset ConfigKey = "relay-stateless-reset" 73 | configServerID ConfigKey = "server-id" 74 | configServerClientSecret ConfigKey = "server-client-secret" 75 | configServerRelaySecret ConfigKey = "server-relay-secret" 76 | ) 77 | 78 | type ConfigValue struct { 79 | Int64 int64 `json:"int64,omitempty"` 80 | String string `json:"string,omitempty"` 81 | Bytes []byte `json:"bytes,omitempty"` 82 | } 83 | 84 | type ClientConnKey struct { 85 | ID ksuid.KSUID `json:"id"` 86 | } 87 | 88 | type ClientConnValue struct { 89 | Authentication ClientAuthentication `json:"authentication"` 90 | Addr string `json:"addr"` 91 | } 92 | 93 | type ClientPeerKey struct { 94 | Forward model.Forward `json:"forward"` 95 | Role model.Role `json:"role"` 96 | ID ksuid.KSUID `json:"id"` // TODO consider using the server cert key 97 | } 98 | 99 | type ClientPeerValue struct { 100 | Peer *pbclient.Peer `json:"peer"` 101 | } 102 | 103 | type cacheKey struct { 104 | forward model.Forward 105 | role model.Role 106 | } 107 | 108 | type RelayConnKey struct { 109 | ID ksuid.KSUID `json:"id"` 110 | } 111 | 112 | type RelayConnValue struct { 113 | Authentication RelayAuthentication `json:"authentication"` 114 | Hostport model.HostPort `json:"hostport"` 115 | Hostports []model.HostPort `json:"hostports"` 116 | } 117 | 118 | type RelayClientKey struct { 119 | Forward model.Forward `json:"forward"` 120 | Role model.Role `json:"role"` 121 | Key model.Key `json:"key"` 122 | } 123 | 124 | type RelayClientValue struct { 125 | Cert *x509.Certificate `json:"cert"` 126 | Authentication ClientAuthentication `json:"authentication"` 127 | } 128 | 129 | func (v RelayClientValue) MarshalJSON() ([]byte, error) { 130 | s := struct { 131 | Cert []byte `json:"cert"` 132 | Authentication []byte `json:"authentication"` 133 | }{ 134 | Cert: v.Cert.Raw, 135 | Authentication: v.Authentication, 136 | } 137 | return json.Marshal(s) 138 | } 139 | 140 | func (v *RelayClientValue) UnmarshalJSON(b []byte) error { 141 | s := struct { 142 | Cert []byte `json:"cert"` 143 | Authentication []byte `json:"authentication"` 144 | }{} 145 | 146 | if err := json.Unmarshal(b, &s); err != nil { 147 | return err 148 | } 149 | 150 | cert, err := x509.ParseCertificate(s.Cert) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | *v = RelayClientValue{cert, s.Authentication} 156 | return nil 157 | } 158 | 159 | type RelayForwardKey struct { 160 | Forward model.Forward `json:"forward"` 161 | } 162 | 163 | type RelayForwardValue struct { 164 | Cert *x509.Certificate `json:"cert"` 165 | } 166 | 167 | func (v RelayForwardValue) MarshalJSON() ([]byte, error) { 168 | return certc.MarshalJSONCert(v.Cert) 169 | } 170 | 171 | func (v *RelayForwardValue) UnmarshalJSON(b []byte) error { 172 | cert, err := certc.UnmarshalJSONCert(b) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | *v = RelayForwardValue{cert} 178 | return nil 179 | } 180 | 181 | type RelayServerKey struct { 182 | Forward model.Forward `json:"forward"` 183 | RelayID ksuid.KSUID `json:"relay_id"` 184 | } 185 | 186 | type RelayServerValue struct { 187 | Hostport model.HostPort `json:"hostport"` 188 | Hostports []model.HostPort `json:"hostports"` 189 | Cert *x509.Certificate `json:"cert"` 190 | } 191 | 192 | func (v RelayServerValue) MarshalJSON() ([]byte, error) { 193 | s := struct { 194 | Hostport model.HostPort `json:"hostport"` 195 | Hostports []model.HostPort `json:"hostports"` 196 | Cert []byte `json:"cert"` 197 | }{ 198 | Hostport: v.Hostport, 199 | Hostports: v.Hostports, 200 | Cert: v.Cert.Raw, 201 | } 202 | return json.Marshal(s) 203 | } 204 | 205 | func (v *RelayServerValue) UnmarshalJSON(b []byte) error { 206 | s := struct { 207 | Hostport model.HostPort `json:"hostport"` 208 | Hostports []model.HostPort `json:"hostports"` 209 | Cert []byte `json:"cert"` 210 | }{} 211 | 212 | if err := json.Unmarshal(b, &s); err != nil { 213 | return err 214 | } 215 | 216 | cert, err := x509.ParseCertificate(s.Cert) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | *v = RelayServerValue{Hostport: s.Hostport, Hostports: s.Hostports, Cert: cert} 222 | return nil 223 | } 224 | 225 | type relayCacheValue struct { 226 | Hostports []model.HostPort 227 | Cert *x509.Certificate 228 | } 229 | -------------------------------------------------------------------------------- /cryptoc/derive.go: -------------------------------------------------------------------------------- 1 | package cryptoc 2 | 3 | import ( 4 | "crypto/ecdh" 5 | "hash" 6 | 7 | "golang.org/x/crypto/blake2s" 8 | ) 9 | 10 | func DeriveKeys(selfSecret *ecdh.PrivateKey, peerPublic *ecdh.PublicKey, initiator bool) ([]byte, []byte, error) { 11 | ck, hk := initck() 12 | 13 | if initiator { 14 | hk = mixHash(hk, selfSecret.PublicKey().Bytes()) 15 | hk = mixHash(hk, peerPublic.Bytes()) 16 | } else { 17 | hk = mixHash(hk, peerPublic.Bytes()) 18 | hk = mixHash(hk, selfSecret.PublicKey().Bytes()) 19 | } 20 | 21 | dh, err := selfSecret.ECDH(peerPublic) 22 | if err != nil { 23 | return nil, nil, err 24 | } 25 | ck = hkdf1(newhash, ck, dh) 26 | 27 | hk1, hk2 := hkdf2(newhash, ck, hk) 28 | return hk1, hk2, nil 29 | } 30 | 31 | func initck() ([]byte, []byte) { 32 | ck := make([]byte, blake2s.Size) 33 | copy(ck, "connet-chaining") 34 | 35 | hk := make([]byte, blake2s.Size) 36 | copy(hk, "connet-hashing") 37 | 38 | return ck, hk 39 | } 40 | 41 | func newhash() hash.Hash { 42 | h, err := blake2s.New256([]byte("connet-hash")) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return h 47 | } 48 | 49 | func mixHash(oldHash, data []byte) []byte { 50 | h := newhash() 51 | h.Write(oldHash) 52 | h.Write(data) 53 | return h.Sum(nil) 54 | } 55 | -------------------------------------------------------------------------------- /cryptoc/derive_test.go: -------------------------------------------------------------------------------- 1 | package cryptoc 2 | 3 | import ( 4 | "crypto/ecdh" 5 | "crypto/rand" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDeriveKeys(t *testing.T) { 12 | srcKey, err := ecdh.X25519().GenerateKey(rand.Reader) 13 | require.NoError(t, err) 14 | dstKey, err := ecdh.X25519().GenerateKey(rand.Reader) 15 | require.NoError(t, err) 16 | 17 | sl, sr, err := DeriveKeys(srcKey, dstKey.PublicKey(), true) 18 | require.NoError(t, err) 19 | dl, dr, err := DeriveKeys(dstKey, srcKey.PublicKey(), false) 20 | require.NoError(t, err) 21 | 22 | require.Equal(t, sl, dl) 23 | require.Equal(t, sr, dr) 24 | } 25 | -------------------------------------------------------------------------------- /cryptoc/hkdf.go: -------------------------------------------------------------------------------- 1 | package cryptoc 2 | 3 | import ( 4 | "crypto/hmac" 5 | "hash" 6 | ) 7 | 8 | type hasher func() hash.Hash 9 | 10 | func hkdf1(h hasher, chainingKey, inputKey []byte) []byte { 11 | tempMac := hmac.New(h, chainingKey) 12 | tempMac.Write(inputKey) 13 | tempKey := tempMac.Sum(nil) 14 | 15 | out1Mac := hmac.New(h, tempKey) 16 | out1Mac.Write([]byte{0x01}) 17 | return out1Mac.Sum(nil) 18 | } 19 | 20 | func hkdf2(h hasher, chainingKey, inputKey []byte) ([]byte, []byte) { 21 | tempMac := hmac.New(h, chainingKey) 22 | tempMac.Write(inputKey) 23 | tempKey := tempMac.Sum(nil) 24 | 25 | out1Mac := hmac.New(h, tempKey) 26 | out1Mac.Write([]byte{0x01}) 27 | out1 := out1Mac.Sum(nil) 28 | 29 | out2Mac := hmac.New(h, tempKey) 30 | out2Mac.Write(out1) 31 | out2Mac.Write([]byte{0x02}) 32 | out2 := out2Mac.Sum(nil) 33 | 34 | return out1, out2 35 | } 36 | 37 | func hkdf3(h hasher, chainingKey, inputKey []byte) ([]byte, []byte, []byte) { 38 | tempMac := hmac.New(h, chainingKey) 39 | tempMac.Write(inputKey) 40 | tempKey := tempMac.Sum(nil) 41 | 42 | out1Mac := hmac.New(h, tempKey) 43 | out1Mac.Write([]byte{0x01}) 44 | out1 := out1Mac.Sum(nil) 45 | 46 | out2Mac := hmac.New(h, tempKey) 47 | out2Mac.Write(out1) 48 | out2Mac.Write([]byte{0x02}) 49 | out2 := out2Mac.Sum(nil) 50 | 51 | out3Mac := hmac.New(h, tempKey) 52 | out3Mac.Write(out2) 53 | out3Mac.Write([]byte{0x03}) 54 | out3 := out3Mac.Sum(nil) 55 | 56 | return out1, out2, out3 57 | } 58 | 59 | var _ = hkdf3 60 | -------------------------------------------------------------------------------- /cryptoc/stream.go: -------------------------------------------------------------------------------- 1 | package cryptoc 2 | 3 | import ( 4 | "crypto/cipher" 5 | "encoding/binary" 6 | "io" 7 | "net" 8 | "slices" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | type asymStream struct { 15 | stream io.ReadWriter 16 | reader cipher.AEAD 17 | writer cipher.AEAD 18 | 19 | readMu sync.Mutex 20 | readBuffLen []byte 21 | readBuff []byte 22 | readNonce []byte 23 | 24 | readPlainBuff []byte 25 | readPlainBegin int 26 | readPlainEnd int 27 | 28 | writeMu sync.Mutex 29 | writeBuff []byte 30 | writeNonce []byte 31 | writePlainMax int 32 | 33 | closed atomic.Bool 34 | } 35 | 36 | const maxBuff = 65535 37 | 38 | func NewStream(stream io.ReadWriter, reader cipher.AEAD, writer cipher.AEAD) net.Conn { 39 | return &asymStream{ 40 | stream: stream, 41 | reader: reader, 42 | writer: writer, 43 | 44 | readBuffLen: make([]byte, 2), 45 | readBuff: make([]byte, maxBuff), 46 | readNonce: make([]byte, reader.NonceSize()), 47 | 48 | readPlainBuff: make([]byte, maxBuff-reader.Overhead()), 49 | readPlainBegin: 0, 50 | readPlainEnd: 0, 51 | 52 | writeBuff: make([]byte, maxBuff), 53 | writeNonce: make([]byte, writer.NonceSize()), 54 | writePlainMax: maxBuff - writer.Overhead() - 2, 55 | } 56 | } 57 | 58 | func (s *asymStream) Read(p []byte) (int, error) { 59 | if s.closed.Load() { 60 | return 0, io.ErrClosedPipe 61 | } 62 | 63 | s.readMu.Lock() 64 | defer s.readMu.Unlock() 65 | 66 | var err error 67 | if s.readPlainBegin >= s.readPlainEnd { 68 | if _, err := io.ReadFull(s.stream, s.readBuffLen); err != nil { 69 | return 0, err 70 | } 71 | 72 | readLen := int(binary.BigEndian.Uint16(s.readBuffLen)) 73 | if n, err := io.ReadFull(s.stream, s.readBuff[:readLen]); err != nil { 74 | return 0, err 75 | } else { 76 | s.readBuff = s.readBuff[:n] 77 | } 78 | 79 | s.readPlainBuff = s.readPlainBuff[:cap(s.readPlainBuff)] 80 | s.readPlainBuff, err = s.reader.Open(s.readPlainBuff[:0], s.readNonce, s.readBuff, nil) 81 | if err != nil { 82 | return 0, err 83 | } 84 | 85 | incrementNonce(s.readNonce) 86 | 87 | s.readPlainBegin = 0 88 | s.readPlainEnd = len(s.readPlainBuff) 89 | } 90 | 91 | n := copy(p, s.readPlainBuff[s.readPlainBegin:s.readPlainEnd]) 92 | s.readPlainBegin += n 93 | 94 | return n, nil 95 | } 96 | 97 | func (s *asymStream) Write(p []byte) (int, error) { 98 | if s.closed.Load() { 99 | return 0, io.ErrClosedPipe 100 | } 101 | 102 | s.writeMu.Lock() 103 | defer s.writeMu.Unlock() 104 | 105 | var written int 106 | for chunk := range slices.Chunk(p, s.writePlainMax) { 107 | s.writeBuff = s.writeBuff[:cap(s.writeBuff)] 108 | 109 | out := s.writer.Seal(s.writeBuff[2:2], s.writeNonce, chunk, nil) 110 | s.writeBuff = s.writeBuff[:2+len(out)] 111 | 112 | incrementNonce(s.writeNonce) 113 | 114 | binary.BigEndian.PutUint16(s.writeBuff, uint16(len(out))) 115 | if _, err := s.stream.Write(s.writeBuff); err != nil { 116 | return written, err 117 | } 118 | 119 | written += len(chunk) 120 | } 121 | 122 | return written, nil 123 | } 124 | 125 | func (s *asymStream) Close() error { 126 | if s.closed.CompareAndSwap(false, true) { 127 | if stream, ok := s.stream.(io.Closer); ok { 128 | return stream.Close() 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func (s *asymStream) LocalAddr() net.Addr { 136 | if stream, ok := s.stream.(interface{ LocalAddr() net.Addr }); ok { 137 | return stream.LocalAddr() 138 | } 139 | return nil 140 | } 141 | 142 | func (s *asymStream) RemoteAddr() net.Addr { 143 | if stream, ok := s.stream.(interface{ RemoteAddr() net.Addr }); ok { 144 | return stream.RemoteAddr() 145 | } 146 | return nil 147 | } 148 | 149 | func (s *asymStream) SetDeadline(t time.Time) error { 150 | if stream, ok := s.stream.(interface{ SetDeadline(t time.Time) error }); ok { 151 | return stream.SetDeadline(t) 152 | } 153 | return nil 154 | } 155 | 156 | func (s *asymStream) SetReadDeadline(t time.Time) error { 157 | if stream, ok := s.stream.(interface{ SetReadDeadline(t time.Time) error }); ok { 158 | return stream.SetReadDeadline(t) 159 | } 160 | return nil 161 | } 162 | 163 | func (s *asymStream) SetWriteDeadline(t time.Time) error { 164 | if stream, ok := s.stream.(interface{ SetWriteDeadline(t time.Time) error }); ok { 165 | return stream.SetWriteDeadline(t) 166 | } 167 | return nil 168 | } 169 | 170 | func incrementNonce(nonce []byte) { 171 | for i := len(nonce) - 1; i >= 0; i++ { 172 | nonce[i]++ 173 | if nonce[i] > 0 { 174 | break 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /cryptoc/stream_test.go: -------------------------------------------------------------------------------- 1 | package cryptoc 2 | 3 | import ( 4 | "crypto/cipher" 5 | "crypto/rand" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | "golang.org/x/crypto/chacha20poly1305" 13 | ) 14 | 15 | func TestStream(t *testing.T) { 16 | serverReader, clientWriter := io.Pipe() 17 | clientReader, serverWriter := io.Pipe() 18 | 19 | var client = &rwc{clientReader, clientWriter} 20 | var server = &rwc{serverReader, serverWriter} 21 | 22 | var clientAEAD = newAEAD(t) 23 | var serverAEAD = newAEAD(t) 24 | 25 | var clientStream = NewStream(client, serverAEAD, clientAEAD) 26 | var serverStream = NewStream(server, clientAEAD, serverAEAD) 27 | 28 | go func() { 29 | _, err := io.Copy(serverStream, serverStream) 30 | require.NoError(t, err) 31 | }() 32 | 33 | t.Run("small", func(t *testing.T) { 34 | go func() { 35 | for i := 0; i < 10; i++ { 36 | var out = []byte(fmt.Sprintf("hello world %d", i)) 37 | _, err := clientStream.Write(out) 38 | require.NoError(t, err) 39 | } 40 | }() 41 | 42 | for i := 0; i < 10; i++ { 43 | var out = []byte(fmt.Sprintf("hello world %d", i)) 44 | 45 | var in = make([]byte, len(out)) 46 | n, err := clientStream.Read(in) 47 | require.NoError(t, err) 48 | require.Equal(t, len(out), n) 49 | require.Equal(t, out, in) 50 | } 51 | }) 52 | 53 | t.Run("big", func(t *testing.T) { 54 | var out = make([]byte, 1024*1024) 55 | _, err := io.ReadFull(rand.Reader, out) 56 | require.NoError(t, err) 57 | 58 | go func() { 59 | _, err := clientStream.Write(out) 60 | require.NoError(t, err) 61 | }() 62 | 63 | var in = make([]byte, len(out)) 64 | n, err := io.ReadFull(clientStream, in) 65 | require.NoError(t, err) 66 | require.Equal(t, len(out), n) 67 | require.Equal(t, out, in) 68 | }) 69 | } 70 | 71 | func newAEAD(t *testing.T) cipher.AEAD { 72 | key := make([]byte, chacha20poly1305.KeySize) 73 | _, err := io.ReadFull(rand.Reader, key) 74 | require.NoError(t, err) 75 | ccp, err := chacha20poly1305.New(key) 76 | require.NoError(t, err) 77 | return ccp 78 | } 79 | 80 | type rwc struct { 81 | reader io.ReadCloser 82 | writer io.WriteCloser 83 | } 84 | 85 | func (r *rwc) Close() error { 86 | return errors.Join(r.writer.Close(), r.reader.Close()) 87 | } 88 | 89 | func (r *rwc) Read(p []byte) (n int, err error) { 90 | return r.reader.Read(p) 91 | } 92 | 93 | func (r *rwc) Write(p []byte) (n int, err error) { 94 | return r.writer.Write(p) 95 | } 96 | -------------------------------------------------------------------------------- /cryptoc/streamer.go: -------------------------------------------------------------------------------- 1 | package cryptoc 2 | 3 | import ( 4 | "crypto/ecdh" 5 | "io" 6 | "net" 7 | 8 | "golang.org/x/crypto/chacha20poly1305" 9 | ) 10 | 11 | type Streamer func(io.ReadWriter) net.Conn 12 | 13 | func NewStreamer(selfSecret *ecdh.PrivateKey, peerPublic *ecdh.PublicKey, initiator bool) (Streamer, error) { 14 | lKey, rKey, err := DeriveKeys(selfSecret, peerPublic, initiator) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | lCipher, err := chacha20poly1305.New(lKey) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | rCipher, err := chacha20poly1305.New(rKey) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return func(stream io.ReadWriter) net.Conn { 30 | if initiator { 31 | return NewStream(stream, rCipher, lCipher) 32 | } 33 | return NewStream(stream, lCipher, rCipher) 34 | }, nil 35 | } 36 | -------------------------------------------------------------------------------- /examples/client-base.toml: -------------------------------------------------------------------------------- 1 | log-level = "debug" 2 | 3 | [client] 4 | token-file = "examples/client-token.secret" 5 | server-cas = ".direnv/minica.pem" 6 | direct-addr = ":" 7 | -------------------------------------------------------------------------------- /examples/client-destination.toml: -------------------------------------------------------------------------------- 1 | log-level = "debug" 2 | 3 | [client] 4 | token-file = "examples/client-token.secret" 5 | server-cas = ".direnv/minica.pem" 6 | 7 | [client.destinations.sws] 8 | relay-encryptions = ["tls", "dhxcp"] 9 | url = "file:." 10 | -------------------------------------------------------------------------------- /examples/client-source.toml: -------------------------------------------------------------------------------- 1 | log-level = "debug" 2 | 3 | [client] 4 | token-file = "examples/client-token.secret" 5 | server-cas = ".direnv/minica.pem" 6 | direct-addr = ":19193" 7 | 8 | [client.sources.sws] 9 | relay-encryptions = ["tls"] 10 | url = "tcp://:9999" 11 | -------------------------------------------------------------------------------- /examples/client-token.secret: -------------------------------------------------------------------------------- 1 | xxyxx 2 | -------------------------------------------------------------------------------- /examples/minimal.toml: -------------------------------------------------------------------------------- 1 | log-level = "debug" 2 | 3 | [client] 4 | token-file = "examples/client-token.secret" 5 | server-cas = ".direnv/minica.pem" 6 | 7 | [client.destinations.sws] 8 | url = "file:." 9 | 10 | [client.sources.sws] 11 | url = "tcp://:9999" 12 | 13 | [server] 14 | tokens-file = "examples/client-token.secret" 15 | 16 | [[server.ingress]] 17 | cert-file = ".direnv/localhost/cert.pem" 18 | key-file = ".direnv/localhost/key.pem" 19 | 20 | [control] 21 | clients-tokens-file = "examples/client-token.secret" 22 | relays-tokens-file = "examples/relay-token.secret" 23 | 24 | [[control.clients-ingress]] 25 | cert-file = ".direnv/localhost/cert.pem" 26 | key-file = ".direnv/localhost/key.pem" 27 | 28 | [[control.relays-ingress]] 29 | cert-file = ".direnv/localhost/cert.pem" 30 | key-file = ".direnv/localhost/key.pem" 31 | 32 | [relay] 33 | token-file = "examples/relay-token.secret" 34 | control-cas = ".direnv/localhost/cert.pem" 35 | -------------------------------------------------------------------------------- /examples/relay-token.secret: -------------------------------------------------------------------------------- 1 | yyxyy 2 | -------------------------------------------------------------------------------- /examples/routes.toml: -------------------------------------------------------------------------------- 1 | log-level = "debug" 2 | 3 | [server] 4 | tokens-file = "examples/client-token.secret" 5 | 6 | [[server.ingress]] 7 | cert-file = ".direnv/localhost/cert.pem" 8 | key-file = ".direnv/localhost/key.pem" 9 | 10 | [client] 11 | token-file = "examples/client-token.secret" 12 | server-cas = ".direnv/minica.pem" 13 | 14 | [client.destinations.sws-direct] 15 | route = "direct" 16 | url = "file:." 17 | 18 | [client.sources.sws-direct] 19 | route = "direct" 20 | url = "tcp://:9999" 21 | 22 | [client.destinations.sws-relay] 23 | route = "relay" 24 | url = "file:." 25 | 26 | [client.sources.sws-relay] 27 | route = "relay" 28 | url = "tcp://:9998" 29 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1737717945, 24 | "narHash": "sha256-ET91TMkab3PmOZnqiJQYOtSGvSTvGeHoegAv4zcTefM=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "ecd26a469ac56357fd333946a99086e992452b6a", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A flake for connet project"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils, ... }: 10 | { 11 | nixosModules.default = ./nix/client-module.nix; 12 | nixosModules.server = ./nix/server-module.nix; 13 | nixosModules.control-server = ./nix/control-server-module.nix; 14 | nixosModules.relay-server = ./nix/relay-server-module.nix; 15 | } // flake-utils.lib.eachDefaultSystem (system: 16 | let 17 | pkgs = import nixpkgs { 18 | inherit system; 19 | }; 20 | testCerts = pkgs.runCommand "test-certs" { } '' 21 | mkdir $out && cd $out 22 | ${pkgs.minica}/bin/minica -ip-addresses 192.168.1.2 23 | ''; 24 | in 25 | { 26 | formatter = pkgs.nixpkgs-fmt; 27 | packages = { 28 | default = pkgs.callPackage ./nix/package.nix { }; 29 | docker = pkgs.callPackage ./nix/docker.nix { }; 30 | }; 31 | devShells.default = pkgs.mkShellNoCC { 32 | buildInputs = with pkgs; [ 33 | go 34 | gopls 35 | golangci-lint 36 | fd 37 | manifest-tool 38 | protobuf 39 | protoc-gen-go 40 | process-compose 41 | skopeo 42 | zip 43 | (pkgs.writeShellScriptBin "gen-local-certs" '' 44 | set -euo pipefail 45 | cd .direnv 46 | ${minica}/bin/minica -domains localhost -ip-addresses 127.0.0.1 47 | cd .. 48 | '') 49 | (writeShellScriptBin "clean-local-certs" '' 50 | set -euo pipefail 51 | rm -rf .direnv/localhost 52 | rm .direnv/minica.pem 53 | rm .direnv/minica-key.pem 54 | '') 55 | ]; 56 | }; 57 | checks = { 58 | moduleTest = pkgs.testers.runNixOSTest { 59 | name = "moduleTest"; 60 | nodes.destination = { 61 | imports = [ self.nixosModules.default ]; 62 | environment.etc."server.cert" = { 63 | source = "${testCerts}/192.168.1.2/cert.pem"; 64 | }; 65 | environment.etc."tokens" = { 66 | text = "token-dst"; 67 | }; 68 | services.connet-client = { 69 | enable = true; 70 | openFirewall = true; 71 | settings = { 72 | log-level = "debug"; 73 | client = { 74 | token-file = "/etc/tokens"; 75 | server-addr = "192.168.1.2:19190"; 76 | server-cas = "/etc/server.cert"; 77 | destinations = { 78 | files.url = "file:."; 79 | filesd = { 80 | url = "file:."; 81 | route = "direct"; 82 | }; 83 | filesr = { 84 | url = "file:."; 85 | route = "relay"; 86 | }; 87 | }; 88 | }; 89 | }; 90 | }; 91 | }; 92 | 93 | nodes.source = { 94 | imports = [ self.nixosModules.default ]; 95 | environment.etc."server.cert" = { 96 | source = "${testCerts}/192.168.1.2/cert.pem"; 97 | }; 98 | environment.etc."tokens" = { 99 | text = "token-src"; 100 | }; 101 | services.connet-client = { 102 | enable = true; 103 | openFirewall = true; 104 | settings = { 105 | log-level = "debug"; 106 | client = { 107 | token-file = "/etc/tokens"; 108 | server-addr = "192.168.1.2:19190"; 109 | server-cas = "/etc/server.cert"; 110 | sources = { 111 | files.url = "tcp://:3000"; 112 | filesd = { 113 | url = "tcp://:3001"; 114 | route = "direct"; 115 | }; 116 | filesr = { 117 | url = "tcp://:3002"; 118 | route = "relay"; 119 | }; 120 | }; 121 | }; 122 | }; 123 | }; 124 | }; 125 | 126 | nodes.server = { 127 | imports = [ self.nixosModules.server ]; 128 | environment.etc."server.cert" = { 129 | source = "${testCerts}/192.168.1.2/cert.pem"; 130 | }; 131 | environment.etc."server.key" = { 132 | source = "${testCerts}/192.168.1.2/key.pem"; 133 | }; 134 | environment.etc."tokens" = { 135 | text = "token-dst\ntoken-src"; 136 | }; 137 | services.connet-server = { 138 | enable = true; 139 | openFirewall = true; 140 | settings = { 141 | log-level = "debug"; 142 | server = { 143 | tokens-file = "/etc/tokens"; 144 | ingress = [{ 145 | cert-file = "/etc/server.cert"; 146 | key-file = "/etc/server.key"; 147 | }]; 148 | relay-ingress = [{ 149 | hostports = [ "server" ]; 150 | }]; 151 | }; 152 | }; 153 | }; 154 | }; 155 | 156 | testScript = '' 157 | start_all() 158 | server.wait_for_unit("connet-server.service") 159 | destination.wait_for_unit("connet-client.service") 160 | source.wait_for_unit("connet-client.service") 161 | source.wait_for_open_port(3000) 162 | source.succeed("${pkgs.curl}/bin/curl http://localhost:3000") 163 | source.wait_for_open_port(3001) 164 | source.succeed("${pkgs.curl}/bin/curl http://localhost:3001") 165 | source.wait_for_open_port(3002) 166 | source.succeed("${pkgs.curl}/bin/curl http://localhost:3002") 167 | ''; 168 | }; 169 | }; 170 | }); 171 | } 172 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/connet-dev/connet 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.5.3 7 | github.com/klev-dev/klevdb v0.8.0 8 | github.com/mr-tron/base58 v1.2.0 9 | github.com/pelletier/go-toml/v2 v2.2.4 10 | github.com/pires/go-proxyproto v0.8.0 11 | github.com/quic-go/quic-go v0.50.1 12 | github.com/segmentio/ksuid v1.0.4 13 | github.com/spf13/cobra v1.9.1 14 | github.com/stretchr/testify v1.9.0 15 | golang.org/x/crypto v0.37.0 16 | golang.org/x/sync v0.13.0 17 | google.golang.org/protobuf v1.36.6 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 23 | github.com/gofrs/flock v0.12.1 // indirect 24 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect 25 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 26 | github.com/onsi/ginkgo/v2 v2.23.4 // indirect 27 | github.com/plar/go-adaptive-radix-tree/v2 v2.0.3 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | github.com/spf13/pflag v1.0.6 // indirect 30 | go.uber.org/automaxprocs v1.6.0 // indirect 31 | go.uber.org/mock v0.5.1 // indirect 32 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 33 | golang.org/x/mod v0.24.0 // indirect 34 | golang.org/x/net v0.39.0 // indirect 35 | golang.org/x/sys v0.32.0 // indirect 36 | golang.org/x/tools v0.32.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 5 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 6 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 7 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 8 | github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 9 | github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 10 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 11 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 12 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 13 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 14 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 15 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 16 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 17 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 18 | github.com/klev-dev/klevdb v0.8.0 h1:eYFjofts4Mjc6otYUJEG/QBeTp5hqohquYK2gd5ivi0= 19 | github.com/klev-dev/klevdb v0.8.0/go.mod h1:Uv5at8UhqBosgp3LjVsN4ny6yClr/QGuK8YdtsgxAgM= 20 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 21 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 22 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 23 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 24 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 25 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 26 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 27 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 28 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 29 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 30 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 31 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 32 | github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= 33 | github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= 34 | github.com/plar/go-adaptive-radix-tree/v2 v2.0.3 h1:cJx/EUTduV4q10O5HSzHgPrViApJkJQk9OSeaT7UYUU= 35 | github.com/plar/go-adaptive-radix-tree/v2 v2.0.3/go.mod h1:8yf9K81YK94H4gKh/K3hCBeC2s4JA/PYgqMkkOadwvk= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 39 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 40 | github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= 41 | github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= 42 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 43 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 44 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 45 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 46 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 47 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 48 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 49 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 50 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 51 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 52 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 53 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 54 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 55 | go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= 56 | go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 57 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 58 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 59 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 60 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 61 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 62 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 63 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 64 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 65 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 66 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 67 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 68 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 69 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 70 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 71 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 72 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 73 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 74 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 75 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 76 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 79 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | -------------------------------------------------------------------------------- /iterc/iter.go: -------------------------------------------------------------------------------- 1 | package iterc 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | func Map[P any, R any](it iter.Seq[P], f func(P) R) iter.Seq[R] { 8 | return func(yield func(R) bool) { 9 | for p := range it { 10 | if !yield(f(p)) { 11 | return 12 | } 13 | } 14 | } 15 | } 16 | 17 | func Filter[P any](it iter.Seq[P], f func(P) bool) iter.Seq[P] { 18 | return func(yield func(P) bool) { 19 | for p := range it { 20 | if f(p) { 21 | if !yield(p) { 22 | return 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | func Flatten[S ~[]P, P any](it iter.Seq[S]) iter.Seq[P] { 30 | return func(yield func(P) bool) { 31 | for s := range it { 32 | for _, p := range s { 33 | if !yield(p) { 34 | return 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /iterc/slices.go: -------------------------------------------------------------------------------- 1 | package iterc 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | ) 7 | 8 | func MapSlice[S ~[]P, P any, R any](s S, f func(P) R) []R { 9 | return slices.Collect(Map(slices.Values(s), f)) 10 | } 11 | 12 | func MapSliceStrings[S ~[]P, P fmt.Stringer](s S) []string { 13 | return slices.Collect(Map(slices.Values(s), P.String)) 14 | } 15 | 16 | func FilterSlice[S ~[]P, P any](s S, f func(P) bool) S { 17 | return slices.Collect(Filter(slices.Values(s), f)) 18 | } 19 | 20 | func FlattenSlice[SP ~[]S, S ~[]P, P any](sp SP) S { 21 | return slices.Collect(Flatten(slices.Values(sp))) 22 | } 23 | -------------------------------------------------------------------------------- /logc/log.go: -------------------------------------------------------------------------------- 1 | package logc 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "maps" 9 | "slices" 10 | 11 | "github.com/klev-dev/klevdb" 12 | ) 13 | 14 | const ( 15 | OffsetInvalid = klevdb.OffsetInvalid 16 | OffsetOldest = klevdb.OffsetOldest 17 | OffsetNewest = klevdb.OffsetNewest 18 | ) 19 | 20 | var ErrNotFound = klevdb.ErrNotFound 21 | 22 | type Message[K comparable, V any] struct { 23 | Offset int64 24 | Key K 25 | Value V 26 | Delete bool 27 | } 28 | 29 | type KV[K comparable, V any] interface { 30 | Put(k K, v V) error 31 | Del(k K) error 32 | 33 | Get(k K) (V, error) 34 | GetOrDefault(k K, v V) (V, error) 35 | GetOrInit(k K, fn func(K) (V, error)) (V, error) 36 | 37 | Consume(ctx context.Context, offset int64) ([]Message[K, V], int64, error) 38 | Snapshot() ([]Message[K, V], int64, error) // TODO this could possible return too much data 39 | 40 | Close() error 41 | } 42 | 43 | func NewKV[K comparable, V any](dir string) (KV[K, V], error) { 44 | log, err := klevdb.OpenTBlocking(dir, klevdb.Options{ 45 | CreateDirs: true, 46 | KeyIndex: true, 47 | AutoSync: true, 48 | Check: true, 49 | }, klevdb.JsonCodec[K]{}, klevdb.JsonCodec[V]{}) 50 | if err != nil { 51 | return nil, fmt.Errorf("log open: %w", err) 52 | } 53 | return &kv[K, V]{log}, nil 54 | } 55 | 56 | type kv[K comparable, V any] struct { 57 | log klevdb.TBlockingLog[K, V] 58 | } 59 | 60 | func (l *kv[K, V]) Put(k K, v V) error { 61 | _, err := l.log.Publish([]klevdb.TMessage[K, V]{{ 62 | Key: k, 63 | Value: v, 64 | }}) 65 | return err 66 | } 67 | 68 | func (l *kv[K, V]) Del(k K) error { 69 | _, err := l.log.Publish([]klevdb.TMessage[K, V]{{ 70 | Key: k, 71 | ValueEmpty: true, 72 | }}) 73 | return err 74 | } 75 | 76 | func (l *kv[K, V]) Get(k K) (V, error) { 77 | msg, err := l.log.GetByKey(k, false) 78 | if err != nil { 79 | var v V 80 | return v, err 81 | } 82 | if msg.ValueEmpty { 83 | var v V 84 | return v, fmt.Errorf("key not found: %w", ErrNotFound) 85 | } 86 | return msg.Value, nil 87 | } 88 | 89 | func (l *kv[K, V]) GetOrDefault(k K, dv V) (V, error) { 90 | switch v, err := l.Get(k); { 91 | case err == nil: 92 | return v, nil 93 | case errors.Is(err, ErrNotFound): 94 | return dv, nil 95 | default: 96 | return v, err 97 | } 98 | } 99 | 100 | func (l *kv[K, V]) GetOrInit(k K, fn func(K) (V, error)) (V, error) { 101 | switch v, err := l.Get(k); { 102 | case err == nil: 103 | return v, nil 104 | case errors.Is(err, ErrNotFound): 105 | nv, err := fn(k) 106 | if err != nil { 107 | return v, err 108 | } 109 | if err := l.Put(k, nv); err != nil { 110 | return v, err 111 | } 112 | return nv, nil 113 | default: 114 | return v, err 115 | } 116 | } 117 | 118 | func (l *kv[K, V]) Consume(ctx context.Context, offset int64) ([]Message[K, V], int64, error) { 119 | nextOffset, msgs, err := l.log.ConsumeBlocking(ctx, offset, 32) 120 | if err != nil { 121 | return nil, OffsetInvalid, err 122 | } 123 | nmsgs := make([]Message[K, V], len(msgs)) 124 | for i, msg := range msgs { 125 | nmsgs[i] = Message[K, V]{ 126 | Offset: msg.Offset, 127 | Key: msg.Key, 128 | Value: msg.Value, 129 | Delete: msg.ValueEmpty, 130 | } 131 | } 132 | return nmsgs, nextOffset, nil 133 | } 134 | 135 | func (l *kv[K, V]) Snapshot() ([]Message[K, V], int64, error) { 136 | maxOffset, err := l.log.NextOffset() 137 | if err != nil { 138 | return nil, OffsetInvalid, err 139 | } 140 | 141 | sum := map[K]Message[K, V]{} 142 | for offset := OffsetOldest; offset < maxOffset; { 143 | nextOffset, msgs, err := l.log.Consume(offset, 32) 144 | if err != nil { 145 | return nil, OffsetInvalid, err 146 | } 147 | offset = nextOffset 148 | 149 | for _, msg := range msgs { 150 | if msg.ValueEmpty { 151 | delete(sum, msg.Key) 152 | } else { 153 | sum[msg.Key] = Message[K, V]{ 154 | Offset: msg.Offset, 155 | Key: msg.Key, 156 | Value: msg.Value, 157 | } 158 | } 159 | } 160 | } 161 | 162 | return slices.SortedFunc(maps.Values(sum), func(l, r Message[K, V]) int { 163 | return cmp.Compare(l.Offset, r.Offset) 164 | }), maxOffset, nil 165 | } 166 | 167 | func (l *kv[K, V]) Close() error { 168 | return l.log.Close() 169 | } 170 | -------------------------------------------------------------------------------- /model/build.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "runtime/debug" 4 | 5 | func BuildVersion() string { 6 | if bi, ok := debug.ReadBuildInfo(); ok { 7 | for _, setting := range bi.Settings { 8 | if setting.Key == "vcs.revision" { 9 | return setting.Value 10 | } 11 | } 12 | } 13 | 14 | return "/+unknown+/" 15 | } 16 | -------------------------------------------------------------------------------- /model/encryption.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | 7 | "github.com/connet-dev/connet/proto/pbconnect" 8 | ) 9 | 10 | type EncryptionScheme struct{ string } 11 | 12 | var ( 13 | NoEncryption = EncryptionScheme{"none"} 14 | TLSEncryption = EncryptionScheme{"tls"} 15 | DHXCPEncryption = EncryptionScheme{"dhxcp"} 16 | ) 17 | 18 | func EncryptionFromPB(pb pbconnect.RelayEncryptionScheme) EncryptionScheme { 19 | switch pb { 20 | case pbconnect.RelayEncryptionScheme_EncryptionNone: 21 | return NoEncryption 22 | case pbconnect.RelayEncryptionScheme_TLS: 23 | return TLSEncryption 24 | case pbconnect.RelayEncryptionScheme_DHX25519_CHACHAPOLY: 25 | return DHXCPEncryption 26 | default: 27 | panic(fmt.Sprintf("invalid encryption scheme: %d", pb)) 28 | } 29 | } 30 | 31 | func ParseEncryptionScheme(s string) (EncryptionScheme, error) { 32 | switch s { 33 | case NoEncryption.string: 34 | return NoEncryption, nil 35 | case TLSEncryption.string: 36 | return TLSEncryption, nil 37 | case DHXCPEncryption.string: 38 | return DHXCPEncryption, nil 39 | default: 40 | return EncryptionScheme{}, fmt.Errorf("invalid encryption scheme '%s'", s) 41 | } 42 | } 43 | 44 | func (e EncryptionScheme) PB() pbconnect.RelayEncryptionScheme { 45 | switch e { 46 | case NoEncryption: 47 | return pbconnect.RelayEncryptionScheme_EncryptionNone 48 | case TLSEncryption: 49 | return pbconnect.RelayEncryptionScheme_TLS 50 | case DHXCPEncryption: 51 | return pbconnect.RelayEncryptionScheme_DHX25519_CHACHAPOLY 52 | default: 53 | panic(fmt.Sprintf("invalid encryption scheme: %s", e.string)) 54 | } 55 | } 56 | 57 | func PBFromEncryptions(schemes []EncryptionScheme) []pbconnect.RelayEncryptionScheme { 58 | pbs := make([]pbconnect.RelayEncryptionScheme, len(schemes)) 59 | for i, sc := range schemes { 60 | pbs[i] = sc.PB() 61 | } 62 | return pbs 63 | } 64 | 65 | func EncryptionsFromPB(pbs []pbconnect.RelayEncryptionScheme) []EncryptionScheme { 66 | schemes := make([]EncryptionScheme, len(pbs)) 67 | for i, s := range pbs { 68 | schemes[i] = EncryptionFromPB(s) 69 | } 70 | return schemes 71 | } 72 | 73 | func SelectEncryptionScheme(dst []EncryptionScheme, src []EncryptionScheme) (EncryptionScheme, error) { 74 | switch { 75 | case slices.Contains(dst, TLSEncryption) && slices.Contains(src, TLSEncryption): 76 | return TLSEncryption, nil 77 | case slices.Contains(dst, DHXCPEncryption) && slices.Contains(src, DHXCPEncryption): 78 | return DHXCPEncryption, nil 79 | case slices.Contains(dst, NoEncryption) && slices.Contains(src, NoEncryption): 80 | return NoEncryption, nil 81 | default: 82 | return EncryptionScheme{}, fmt.Errorf("no shared encryption schemes") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /model/forward.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/connet-dev/connet/proto/pbmodel" 5 | ) 6 | 7 | type Forward struct{ string } 8 | 9 | func NewForward(s string) Forward { 10 | return Forward{s} 11 | } 12 | 13 | func ForwardFromPB(f *pbmodel.Forward) Forward { 14 | return Forward{f.Name} 15 | } 16 | 17 | func (f Forward) PB() *pbmodel.Forward { 18 | return &pbmodel.Forward{Name: f.string} 19 | } 20 | 21 | func (f Forward) String() string { 22 | return f.string 23 | } 24 | 25 | func PBFromForwards(fwds []Forward) []*pbmodel.Forward { 26 | pbs := make([]*pbmodel.Forward, len(fwds)) 27 | for i, fwd := range fwds { 28 | pbs[i] = fwd.PB() 29 | } 30 | return pbs 31 | } 32 | 33 | func (f Forward) MarshalText() ([]byte, error) { 34 | return []byte(f.string), nil 35 | } 36 | 37 | func (f *Forward) UnmarshalText(b []byte) error { 38 | *f = Forward{string(b)} 39 | return nil 40 | } 41 | 42 | func ForwardNames(fwds []Forward) []string { 43 | var strs = make([]string, len(fwds)) 44 | for i, fwd := range fwds { 45 | strs[i] = fwd.string 46 | } 47 | return strs 48 | } 49 | -------------------------------------------------------------------------------- /model/hostport.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/connet-dev/connet/iterc" 7 | "github.com/connet-dev/connet/proto/pbmodel" 8 | ) 9 | 10 | type HostPort struct { 11 | Host string `json:"host"` 12 | Port uint16 `json:"port"` 13 | } 14 | 15 | func HostPortFromPB(h *pbmodel.HostPort) HostPort { 16 | return HostPort{ 17 | Host: h.Host, 18 | Port: uint16(h.Port), 19 | } 20 | } 21 | 22 | func HostPortFromPBs(hs []*pbmodel.HostPort) []HostPort { 23 | return iterc.MapSlice(hs, HostPortFromPB) 24 | } 25 | 26 | func (h HostPort) PB() *pbmodel.HostPort { 27 | return &pbmodel.HostPort{ 28 | Host: h.Host, 29 | Port: uint32(h.Port), 30 | } 31 | } 32 | 33 | func (h HostPort) String() string { 34 | return fmt.Sprintf("%s:%d", h.Host, h.Port) 35 | } 36 | -------------------------------------------------------------------------------- /model/key.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | 7 | "github.com/mr-tron/base58" 8 | "golang.org/x/crypto/blake2s" 9 | ) 10 | 11 | type Key struct{ string } 12 | 13 | func NewKey(cert *x509.Certificate) Key { 14 | return NewKeyRaw(cert.Raw) 15 | } 16 | 17 | func NewKeyTLS(cert tls.Certificate) Key { 18 | return NewKeyRaw(cert.Leaf.Raw) 19 | } 20 | 21 | func NewKeyRaw(raw []byte) Key { 22 | hash := blake2s.Sum256(raw) 23 | return Key{base58.Encode(hash[:])} 24 | } 25 | 26 | func NewKeyString(s string) Key { 27 | return Key{s} 28 | } 29 | 30 | func (k Key) String() string { 31 | return k.string 32 | } 33 | 34 | func (k Key) IsValid() bool { 35 | return k.string != "" 36 | } 37 | 38 | func (k Key) MarshalText() ([]byte, error) { 39 | return []byte(k.string), nil 40 | } 41 | 42 | func (k *Key) UnmarshalText(b []byte) error { 43 | k.string = string(b) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /model/protos.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/connet-dev/connet/iterc" 5 | "github.com/quic-go/quic-go" 6 | ) 7 | 8 | type ClientNextProto struct{ string } 9 | 10 | func (v ClientNextProto) String() string { 11 | return v.string 12 | } 13 | 14 | func GetClientNextProto(conn quic.Connection) ClientNextProto { 15 | proto := conn.ConnectionState().TLS.NegotiatedProtocol 16 | for _, v := range AllClientNextProtos { 17 | if v.string == proto { 18 | return v 19 | } 20 | } 21 | return CNUnknown 22 | } 23 | 24 | var ( 25 | CNUnknown = ClientNextProto{} 26 | CNv02 = ClientNextProto{"connet-client/0.2"} // 0.8.0 27 | ) 28 | 29 | var AllClientNextProtos = []ClientNextProto{CNv02} 30 | 31 | var ClientNextProtos = iterc.MapSliceStrings(AllClientNextProtos) 32 | 33 | type ConnectDirectNextProto struct{ string } 34 | 35 | func (v ConnectDirectNextProto) String() string { 36 | return v.string 37 | } 38 | 39 | var ( 40 | CCv01 = ConnectDirectNextProto{"connet-peer/0.1"} // 0.7.0 41 | ) 42 | 43 | var AllConnectDirectNextProtos = []ConnectDirectNextProto{CCv01} 44 | 45 | var ConnectDirectNextProtos = iterc.MapSlice(AllConnectDirectNextProtos, ConnectDirectNextProto.String) 46 | 47 | type ConnectRelayNextProto struct{ string } 48 | 49 | func (v ConnectRelayNextProto) String() string { 50 | return v.string 51 | } 52 | 53 | var ( 54 | CRv01 = ConnectRelayNextProto{"connet-peer-relay/0.1"} // 0.7.0 55 | ) 56 | 57 | var AllConnectRelayNextProtos = []ConnectRelayNextProto{CRv01} 58 | 59 | var ConnectRelayNextProtos = iterc.MapSlice(AllConnectRelayNextProtos, ConnectRelayNextProto.String) 60 | 61 | type RelayNextProto struct{ string } 62 | 63 | func (v RelayNextProto) String() string { 64 | return v.string 65 | } 66 | 67 | func GetRelayNextProto(conn quic.Connection) RelayNextProto { 68 | proto := conn.ConnectionState().TLS.NegotiatedProtocol 69 | for _, v := range AllRelayNextProtos { 70 | if v.string == proto { 71 | return v 72 | } 73 | } 74 | return RNUnknown 75 | } 76 | 77 | var ( 78 | RNUnknown = RelayNextProto{} 79 | RNv02 = RelayNextProto{"connet-relay/0.2"} // 0.8.0 80 | ) 81 | 82 | var AllRelayNextProtos = []RelayNextProto{RNv02} 83 | 84 | var RelayNextProtos = iterc.MapSliceStrings(AllRelayNextProtos) 85 | -------------------------------------------------------------------------------- /model/proxy.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/connet-dev/connet/proto/pbconnect" 8 | "github.com/pires/go-proxyproto" 9 | ) 10 | 11 | type ProxyVersion struct{ string } 12 | 13 | var ( 14 | ProxyNone = ProxyVersion{"none"} 15 | ProxyV1 = ProxyVersion{"v1"} 16 | ProxyV2 = ProxyVersion{"v2"} 17 | ) 18 | 19 | func ProxyVersionFromPB(r pbconnect.ProxyProtoVersion) ProxyVersion { 20 | switch r { 21 | case pbconnect.ProxyProtoVersion_V1: 22 | return ProxyV1 23 | case pbconnect.ProxyProtoVersion_V2: 24 | return ProxyV2 25 | default: 26 | return ProxyNone 27 | } 28 | } 29 | 30 | func ParseProxyVersion(s string) (ProxyVersion, error) { 31 | switch s { 32 | case ProxyV1.string: 33 | return ProxyV1, nil 34 | case ProxyV2.string: 35 | return ProxyV2, nil 36 | } 37 | return ProxyNone, fmt.Errorf("invalid proxy proto version: %s", s) 38 | } 39 | 40 | func (v ProxyVersion) PB() pbconnect.ProxyProtoVersion { 41 | switch v { 42 | case ProxyV1: 43 | return pbconnect.ProxyProtoVersion_V1 44 | case ProxyV2: 45 | return pbconnect.ProxyProtoVersion_V2 46 | default: 47 | return pbconnect.ProxyProtoVersion_ProxyProtoNone 48 | } 49 | } 50 | 51 | func (v ProxyVersion) Wrap(conn net.Conn) net.Conn { 52 | if v == ProxyNone { 53 | return conn 54 | } 55 | version := byte(2) 56 | if v == ProxyV1 { 57 | version = byte(1) 58 | } 59 | return &proxyProtoConn{conn, version} 60 | } 61 | 62 | type ProxyProtoConn interface { 63 | WriteProxyHeader(sourceAddr, destAddr net.Addr) error 64 | } 65 | 66 | type proxyProtoConn struct { 67 | net.Conn 68 | proxyProtoVersion byte 69 | } 70 | 71 | var _ ProxyProtoConn = (*proxyProtoConn)(nil) 72 | 73 | func (c *proxyProtoConn) WriteProxyHeader(sourceAddr net.Addr, destAddr net.Addr) error { 74 | pp := proxyproto.HeaderProxyFromAddrs(c.proxyProtoVersion, sourceAddr, destAddr) 75 | _, err := pp.WriteTo(c.Conn) 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /model/role.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/connet-dev/connet/proto/pbmodel" 7 | ) 8 | 9 | type Role struct{ string } 10 | 11 | var ( 12 | UnknownRole = Role{} 13 | Destination = Role{"destination"} 14 | Source = Role{"source"} 15 | ) 16 | 17 | func RoleFromPB(r pbmodel.Role) Role { 18 | switch r { 19 | case pbmodel.Role_RoleDestination: 20 | return Destination 21 | case pbmodel.Role_RoleSource: 22 | return Source 23 | default: 24 | return UnknownRole 25 | } 26 | } 27 | 28 | func ParseRole(s string) (Role, error) { 29 | switch s { 30 | case Destination.string: 31 | return Destination, nil 32 | case Source.string: 33 | return Source, nil 34 | } 35 | return UnknownRole, fmt.Errorf("invalid role '%s'", s) 36 | } 37 | 38 | func (r Role) PB() pbmodel.Role { 39 | switch r { 40 | case Destination: 41 | return pbmodel.Role_RoleDestination 42 | case Source: 43 | return pbmodel.Role_RoleSource 44 | default: 45 | return pbmodel.Role_RoleUnknown 46 | } 47 | } 48 | 49 | func (r Role) Invert() Role { 50 | switch r { 51 | case Destination: 52 | return Source 53 | case Source: 54 | return Destination 55 | default: 56 | return UnknownRole 57 | } 58 | } 59 | 60 | func (r Role) String() string { 61 | return r.string 62 | } 63 | 64 | func (r Role) MarshalText() ([]byte, error) { 65 | return []byte(r.string), nil 66 | } 67 | 68 | func (r *Role) UnmarshalText(b []byte) error { 69 | switch s := string(b); s { 70 | case Destination.string: 71 | *r = Destination 72 | case Source.string: 73 | *r = Source 74 | default: 75 | return fmt.Errorf("invalid role '%s'", s) 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /model/route.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "fmt" 4 | 5 | type RouteOption struct{ string } 6 | 7 | var ( 8 | RouteAny = RouteOption{"any"} 9 | RouteDirect = RouteOption{"direct"} 10 | RouteRelay = RouteOption{"relay"} 11 | ) 12 | 13 | func ParseRouteOption(s string) (RouteOption, error) { 14 | switch s { 15 | case RouteAny.string: 16 | return RouteAny, nil 17 | case RouteDirect.string: 18 | return RouteDirect, nil 19 | case RouteRelay.string: 20 | return RouteRelay, nil 21 | } 22 | return RouteOption{}, fmt.Errorf("invalid route option '%s'", s) 23 | } 24 | 25 | func (r RouteOption) AllowFrom(from RouteOption) bool { 26 | switch from { 27 | case RouteDirect: 28 | return r.AllowDirect() 29 | case RouteRelay: 30 | return r.AllowRelay() 31 | } 32 | return false 33 | } 34 | 35 | func (r RouteOption) AllowDirect() bool { 36 | return r == RouteAny || r == RouteDirect 37 | } 38 | 39 | func (r RouteOption) AllowRelay() bool { 40 | return r == RouteAny || r == RouteRelay 41 | } 42 | -------------------------------------------------------------------------------- /netc/addrs.go: -------------------------------------------------------------------------------- 1 | package netc 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/netip" 7 | ) 8 | 9 | func LocalAddrs() ([]netip.Addr, error) { 10 | ifaces, err := net.Interfaces() 11 | if err != nil { 12 | return nil, fmt.Errorf("net interfaces: %w", err) 13 | } 14 | 15 | var localAddrs []netip.Addr 16 | for _, iface := range ifaces { 17 | addrs, err := iface.Addrs() 18 | if err != nil { 19 | continue 20 | } 21 | 22 | NEXT: 23 | for _, addr := range addrs { 24 | var ip net.IP 25 | switch ipAddr := addr.(type) { 26 | case *net.IPAddr: 27 | ip = ipAddr.IP 28 | case *net.IPNet: 29 | ip = ipAddr.IP 30 | default: 31 | continue NEXT 32 | } 33 | if ip.IsLoopback() { 34 | continue 35 | } 36 | if ip4 := ip.To4(); ip4 != nil { 37 | localAddrs = append(localAddrs, netip.AddrFrom4([4]byte(ip4))) 38 | } 39 | if ip6 := ip.To16(); ip6 != nil { 40 | localAddrs = append(localAddrs, netip.AddrFrom16([16]byte(ip6))) 41 | } 42 | } 43 | } 44 | 45 | return localAddrs, nil 46 | } 47 | -------------------------------------------------------------------------------- /netc/addrs_test.go: -------------------------------------------------------------------------------- 1 | package netc 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLocalAddrs(t *testing.T) { 11 | ls, err := LocalAddrs() 12 | require.NoError(t, err) 13 | 14 | for _, l := range ls { 15 | fmt.Println("addr:", l) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /netc/backoff.go: -------------------------------------------------------------------------------- 1 | package netc 2 | 3 | import ( 4 | "context" 5 | "math/rand/v2" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | const ( 11 | MinBackoff time.Duration = 10 * time.Millisecond 12 | MaxBackoff time.Duration = 15 * time.Second 13 | ) 14 | 15 | func NextBackoff(d time.Duration) time.Duration { 16 | return NextBackoffCustom(d, MinBackoff, MaxBackoff) 17 | } 18 | 19 | func NextBackoffCustom(d, jmin, jmax time.Duration) time.Duration { 20 | dt := int64(d*3 - jmin) 21 | nd := jmin + time.Duration(rand.Int64N(dt)) 22 | return min(jmax, nd) 23 | } 24 | 25 | type SpinBackoff struct { 26 | MinBackoff time.Duration 27 | MaxBackoff time.Duration 28 | 29 | init sync.Once 30 | lastWait time.Time 31 | lastBoff time.Duration 32 | } 33 | 34 | // Wait will block on backoff if called too often 35 | func (s *SpinBackoff) Wait(ctx context.Context) error { 36 | s.init.Do(func() { 37 | if s.MinBackoff == 0 { 38 | s.MinBackoff = MinBackoff 39 | } 40 | if s.MaxBackoff == 0 { 41 | s.MaxBackoff = MaxBackoff 42 | } 43 | s.MaxBackoff = max(s.MinBackoff, s.MaxBackoff) 44 | }) 45 | 46 | delta := time.Since(s.lastWait) 47 | s.lastWait = time.Now() 48 | 49 | if delta > s.MaxBackoff { 50 | s.lastBoff = s.MinBackoff 51 | return nil 52 | } 53 | 54 | s.lastBoff = NextBackoffCustom(s.lastBoff, s.MinBackoff, s.MaxBackoff) 55 | select { 56 | case <-ctx.Done(): 57 | return ctx.Err() 58 | case <-time.After(s.lastBoff): 59 | return nil 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /netc/join.go: -------------------------------------------------------------------------------- 1 | package netc 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | func Join(l io.ReadWriteCloser, r io.ReadWriteCloser) error { 12 | var g errgroup.Group 13 | g.Go(func() error { 14 | defer l.Close() 15 | _, err := io.Copy(l, r) 16 | return err 17 | }) 18 | g.Go(func() error { 19 | defer r.Close() 20 | _, err := io.Copy(r, l) 21 | return err 22 | }) 23 | return g.Wait() 24 | } 25 | 26 | type Joiner struct { 27 | Accept func(context.Context) (net.Conn, error) 28 | Dial func(context.Context) (net.Conn, error) 29 | Join func(ctx context.Context, acceptConn net.Conn, dialConn net.Conn) 30 | } 31 | 32 | func (j *Joiner) Run(ctx context.Context) error { 33 | for { 34 | acceptConn, err := j.Accept(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | go func() { 40 | defer acceptConn.Close() 41 | 42 | dialConn, err := j.Dial(ctx) 43 | if err != nil { 44 | return 45 | } 46 | defer dialConn.Close() 47 | 48 | j.Join(ctx, acceptConn, dialConn) 49 | }() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /netc/name.go: -------------------------------------------------------------------------------- 1 | package netc 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/mr-tron/base58" 9 | ) 10 | 11 | func GenServerName(prefix string) string { 12 | data := make([]byte, 32) 13 | if _, err := io.ReadFull(rand.Reader, data); err != nil { 14 | panic(err) 15 | } 16 | return fmt.Sprintf("%s-%s", prefix, base58.Encode(data)) 17 | } 18 | -------------------------------------------------------------------------------- /netc/prefix.go: -------------------------------------------------------------------------------- 1 | package netc 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | ) 7 | 8 | func ParseCIDRs(strs []string) ([]netip.Prefix, error) { 9 | var err error 10 | cidrs := make([]netip.Prefix, len(strs)) 11 | for i, str := range strs { 12 | cidrs[i], err = ParseCIDR(str) 13 | if err != nil { 14 | return nil, fmt.Errorf("parse CIDR at %d: %w", i, err) 15 | } 16 | } 17 | return cidrs, nil 18 | } 19 | 20 | func ParseCIDR(str string) (netip.Prefix, error) { 21 | if cidr, err := netip.ParsePrefix(str); err == nil { 22 | return cidr, nil 23 | } else if addr, aerr := netip.ParseAddr(str); aerr == nil { 24 | return netip.PrefixFrom(addr, addr.BitLen()), nil 25 | } else { 26 | return netip.Prefix{}, fmt.Errorf("parse CIDR %s: %w", str, err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nix/client-module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | import ./module.nix { 3 | inherit config lib pkgs; 4 | role = "client"; 5 | ports = [ 6 | { path = [ "client" "direct-addr" ]; default = ":19192"; } 7 | ]; 8 | } 9 | -------------------------------------------------------------------------------- /nix/control-server-module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | import ./module.nix { 3 | inherit config lib pkgs; 4 | role = "control"; 5 | hasCerts = true; 6 | hasStorage = true; 7 | ports = [ 8 | { path = [ "control" "clients-addr" ]; default = ":19190"; } 9 | { path = [ "control" "relays-addr" ]; default = ":19189"; } 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /nix/docker.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | let 3 | connet = pkgs.callPackage ./package.nix { }; 4 | in 5 | pkgs.dockerTools.buildLayeredImage { 6 | name = "ghcr.io/connet-dev/connet"; 7 | tag = "latest-${if pkgs.stdenv.hostPlatform.isAarch then "arm64" else "amd64"}"; 8 | contents = with pkgs; [ cacert ]; 9 | config = { 10 | Entrypoint = [ "${connet}/bin/connet" ]; 11 | Cmd = [ "--help" ]; 12 | ExposedPorts = { 13 | # working ports 14 | "19189/udp" = { }; # control listens for relays 15 | "19190/udp" = { }; # control listens for clients 16 | "19191/udp" = { }; # relay listens for clients 17 | "19192/udp" = { }; # client listens for clients 18 | }; 19 | Volumes = { 20 | "/tmp" = { }; 21 | }; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /nix/module.nix: -------------------------------------------------------------------------------- 1 | { role, ports, hasCerts ? false, hasStorage ? false, config, lib, pkgs, ... }: 2 | let 3 | cfg = config.services."connet-${role}"; 4 | settingsFormat = pkgs.formats.toml { }; 5 | portFromPath = { path, default }: lib.trivial.pipe cfg.settings [ 6 | (lib.attrByPath path default) 7 | (lib.splitString ":") 8 | lib.last 9 | lib.toInt 10 | ]; 11 | usesACME = hasCerts && builtins.isString cfg.useACMEHost; 12 | noStorageSpec = hasStorage && builtins.isNull (lib.attrByPath [ role "store-dir" ] null cfg.settings); 13 | in 14 | { 15 | options.services."connet-${role}" = { 16 | enable = lib.mkEnableOption "connet ${role}"; 17 | 18 | package = lib.mkOption { 19 | default = pkgs.callPackage ./package.nix { }; 20 | type = lib.types.package; 21 | }; 22 | 23 | user = lib.mkOption { 24 | default = "connet"; 25 | type = lib.types.str; 26 | description = '' 27 | User account under which connet runs. 28 | 29 | ::: {.note} 30 | If left as the default value this user will automatically be created 31 | on system activation, otherwise you are responsible for 32 | ensuring the user exists before the connet service starts. 33 | ::: 34 | ''; 35 | }; 36 | 37 | group = lib.mkOption { 38 | default = "connet"; 39 | type = lib.types.str; 40 | description = '' 41 | Group under which connet runs. 42 | 43 | ::: {.note} 44 | If left as the default value this group will automatically be created 45 | on system activation, otherwise you are responsible for 46 | ensuring the group exists before the connet service starts. 47 | ::: 48 | ''; 49 | }; 50 | 51 | settings = lib.mkOption { 52 | description = "See docs at https://github.com/connet-dev/connet?tab=readme-ov-file#configuration"; 53 | default = { }; 54 | type = lib.types.submodule { 55 | freeformType = settingsFormat.type; 56 | }; 57 | }; 58 | 59 | openFirewall = lib.mkOption { 60 | default = false; 61 | type = lib.types.bool; 62 | description = "Whether to open the firewall for the specified port."; 63 | }; 64 | } // lib.optionalAttrs hasCerts { 65 | useACMEHost = lib.mkOption { 66 | default = null; 67 | type = lib.types.nullOr lib.types.str; 68 | description = '' 69 | A host of an existing ACME certificate to use. 70 | *Note that this option does not create any certificates, nor 71 | does it add subdomains to existing ones – you will need to create them 72 | manually using [](#opt-security.acme.certs).* 73 | ''; 74 | example = "example.com"; 75 | }; 76 | }; 77 | 78 | config = lib.mkIf cfg.enable { 79 | warnings = lib.flatten [ 80 | (lib.optionals 81 | (usesACME && builtins.isString (lib.attrByPath [ role "cert-file" ] null cfg.settings)) 82 | [ "ACME config for ${cfg.useACMEHost} overrides `${role}.cert-file`" ]) 83 | (lib.optionals 84 | (usesACME && builtins.isString (lib.attrByPath [ role "key-file" ] null cfg.settings)) 85 | [ "ACME config for ${cfg.useACMEHost} overrides `${role}.key-file`" ]) 86 | ]; 87 | 88 | boot.kernel.sysctl."net.core.rmem_max" = lib.mkDefault 7500000; 89 | boot.kernel.sysctl."net.core.wmem_max" = lib.mkDefault 7500000; 90 | 91 | users.users = lib.optionalAttrs (cfg.user == "connet") { 92 | connet = { 93 | isSystemUser = true; 94 | group = cfg.group; 95 | }; 96 | }; 97 | 98 | users.groups = lib.optionalAttrs (cfg.group == "connet") { 99 | connet = { }; 100 | }; 101 | 102 | networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall (builtins.map portFromPath ports); 103 | 104 | environment.etc."connet-${role}.toml" = { 105 | user = cfg.user; 106 | group = cfg.group; 107 | source = settingsFormat.generate "connet-config-${role}.toml" (lib.recursiveUpdate 108 | cfg.settings 109 | (lib.recursiveUpdate 110 | (lib.optionalAttrs usesACME ( 111 | let 112 | sslCertDir = config.security.acme.certs.${cfg.useACMEHost}.directory; 113 | in 114 | { 115 | ${role} = { 116 | cert-file = "${sslCertDir}/cert.pem"; 117 | key-file = "${sslCertDir}/key.pem"; 118 | }; 119 | } 120 | )) 121 | (lib.optionalAttrs noStorageSpec { 122 | ${role} = { "store-dir" = "/var/lib/connet-${role}"; }; 123 | }))); 124 | }; 125 | 126 | systemd.packages = [ cfg.package ]; 127 | systemd.services."connet-${role}" = { 128 | description = "connet ${role}"; 129 | after = [ "network.target" "network-online.target" ]; 130 | requires = [ "network-online.target" ] ++ lib.optionals usesACME [ "acme-finished-${cfg.useACMEHost}.target" ]; 131 | wantedBy = [ "multi-user.target" ]; 132 | restartTriggers = [ config.environment.etc."connet-${role}.toml".source ]; 133 | serviceConfig = { 134 | User = cfg.user; 135 | Group = cfg.group; 136 | ExecStart = "${cfg.package}/bin/connet ${if role == "client" then "" else "${role} "} --config /etc/connet-${role}.toml"; 137 | Restart = "on-failure"; 138 | CacheDirectory = "connet-${role}"; 139 | CacheDirectoryMode = "0700"; 140 | } // lib.optionalAttrs noStorageSpec { 141 | StateDirectory = "connet-${role}"; 142 | StateDirectoryMode = "0700"; 143 | }; 144 | }; 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | let 3 | sourceFiles = lib.fileset.difference ../. (lib.fileset.unions [ 4 | (lib.fileset.maybeMissing ../result) 5 | ../.envrc 6 | ../.gitignore 7 | ../examples 8 | ../flake.lock 9 | ../flake.nix 10 | ../LICENSE 11 | ../Makefile 12 | ../nix 13 | ../process-compose.yaml 14 | ../README.md 15 | ]); 16 | in 17 | # lib.fileset.trace sourceFiles 18 | pkgs.buildGoModule 19 | { 20 | name = "connet"; 21 | 22 | src = lib.fileset.toSource { 23 | root = ../.; 24 | fileset = sourceFiles; 25 | }; 26 | 27 | vendorHash = "sha256-Qg5GmyY+VJ4k/rb/PfmGRpI4iWE+U0Oor9Umrs37Vwk="; 28 | subPackages = [ "cmd/connet" ]; 29 | 30 | nativeBuildInputs = [ pkgs.installShellFiles ]; 31 | postInstall = lib.optionalString (pkgs.stdenv.buildPlatform.canExecute pkgs.stdenv.hostPlatform) '' 32 | installShellCompletion --cmd connet \ 33 | --bash <($out/bin/connet completion bash) \ 34 | --fish <($out/bin/connet completion fish) \ 35 | --zsh <($out/bin/connet completion zsh) 36 | ''; 37 | 38 | meta = with lib; { 39 | description = "A p2p reverse proxy, written in Golang"; 40 | homepage = "https://github.com/connet-dev/connet"; 41 | license = licenses.asl20; 42 | mainProgram = "connet"; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /nix/relay-server-module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | import ./module.nix { 3 | inherit config lib pkgs; 4 | role = "relay"; 5 | hasStorage = true; 6 | ports = [ 7 | { path = [ "relay" "addr" ]; default = ":19191"; } 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /nix/server-module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | import ./module.nix { 3 | inherit config lib pkgs; 4 | role = "server"; 5 | hasCerts = true; 6 | hasStorage = true; 7 | ports = [ 8 | { path = [ "server" "addr" ]; default = ":19190"; } 9 | { path = [ "server" "relay-addr" ]; default = ":19191"; } 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /notify/value.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "maps" 7 | "slices" 8 | "sync/atomic" 9 | 10 | "github.com/connet-dev/connet/iterc" 11 | ) 12 | 13 | var errNotifyClosed = errors.New("notify already closed") 14 | var errNotifyEmpty = errors.New("no value") 15 | 16 | type V[T any] struct { 17 | value atomic.Pointer[value[T]] 18 | barrier chan *version[T] 19 | } 20 | 21 | type value[T any] struct { 22 | value T 23 | version uint64 24 | } 25 | 26 | type version[T any] struct { 27 | value T 28 | version uint64 29 | waiter chan struct{} 30 | } 31 | 32 | func NewEmpty[T any]() *V[T] { 33 | v := &V[T]{ 34 | barrier: make(chan *version[T], 1), 35 | } 36 | v.barrier <- &version[T]{waiter: make(chan struct{})} 37 | return v 38 | } 39 | 40 | func New[T any](t T) *V[T] { 41 | v := &V[T]{ 42 | barrier: make(chan *version[T], 1), 43 | } 44 | v.barrier <- &version[T]{waiter: make(chan struct{})} 45 | v.value.Store(&value[T]{t, 0}) 46 | return v 47 | } 48 | 49 | func (v *V[T]) Get(ctx context.Context, version uint64) (T, uint64, error) { 50 | if current := v.value.Load(); current != nil && current.version > version { 51 | return current.value, current.version, nil 52 | } 53 | 54 | next, ok := <-v.barrier 55 | if !ok { 56 | var t T 57 | return t, 0, errNotifyClosed 58 | } 59 | 60 | current := v.value.Load() 61 | 62 | v.barrier <- next 63 | 64 | if current != nil && current.version > version { 65 | return current.value, current.version, nil 66 | } 67 | 68 | select { 69 | case <-next.waiter: 70 | return next.value, next.version, nil 71 | case <-ctx.Done(): 72 | var t T 73 | return t, 0, ctx.Err() 74 | } 75 | } 76 | 77 | func (v *V[T]) GetAny(ctx context.Context) (T, uint64, error) { 78 | if current := v.value.Load(); current != nil { 79 | return current.value, current.version, nil 80 | } 81 | 82 | next, ok := <-v.barrier 83 | if !ok { 84 | var t T 85 | return t, 0, errNotifyClosed 86 | } 87 | 88 | current := v.value.Load() 89 | 90 | v.barrier <- next 91 | 92 | if current != nil { 93 | return current.value, current.version, nil 94 | } 95 | 96 | select { 97 | case <-next.waiter: 98 | return next.value, next.version, nil 99 | case <-ctx.Done(): 100 | var t T 101 | return t, 0, ctx.Err() 102 | } 103 | } 104 | 105 | func (v *V[T]) Peek() (T, error) { 106 | if current := v.value.Load(); current != nil { 107 | return current.value, nil 108 | } 109 | var t T 110 | return t, errNotifyEmpty 111 | } 112 | 113 | func (v *V[T]) Sync(f func()) { 114 | next, ok := <-v.barrier 115 | if !ok { 116 | return 117 | } 118 | defer func() { 119 | v.barrier <- next 120 | }() 121 | 122 | f() 123 | } 124 | 125 | func (v *V[T]) Set(t T) { 126 | v.UpdateOpt(func(_ T) (T, bool) { 127 | return t, true 128 | }) 129 | } 130 | 131 | func (v *V[T]) Update(f func(t T) T) { 132 | v.UpdateOpt(func(t T) (T, bool) { 133 | return f(t), true 134 | }) 135 | } 136 | 137 | func (v *V[T]) UpdateOpt(f func(t T) (T, bool)) bool { 138 | next, ok := <-v.barrier 139 | if !ok { 140 | return false 141 | } 142 | 143 | if current := v.value.Load(); current != nil { 144 | if value, updated := f(current.value); updated { 145 | next.value = value 146 | next.version = current.version + 1 147 | } else { 148 | return false 149 | } 150 | } else { 151 | var t T 152 | if value, updated := f(t); updated { 153 | next.value = value 154 | next.version = 0 155 | } else { 156 | return false 157 | } 158 | } 159 | v.value.Store(&value[T]{next.value, next.version}) 160 | 161 | close(next.waiter) 162 | 163 | v.barrier <- &version[T]{waiter: make(chan struct{})} 164 | 165 | return true 166 | } 167 | 168 | func (v *V[T]) Listen(ctx context.Context, f func(t T) error) error { 169 | t, ver, err := v.GetAny(ctx) 170 | if err != nil { 171 | return err 172 | } 173 | if err := f(t); err != nil { 174 | return err 175 | } 176 | for { 177 | t, ver, err = v.Get(ctx, ver) 178 | if err != nil { 179 | return err 180 | } 181 | if err := f(t); err != nil { 182 | return err 183 | } 184 | } 185 | } 186 | 187 | func (v *V[T]) Notify(ctx context.Context) <-chan T { 188 | ch := make(chan T, 1) 189 | go func() { 190 | defer close(ch) 191 | _ = v.Listen(ctx, func(t T) error { 192 | ch <- t 193 | return nil 194 | }) 195 | }() 196 | return ch 197 | } 198 | 199 | func SliceAppend[S []T, T any](v *V[S], val T) { 200 | v.Update(func(t S) S { 201 | return append(slices.Clone(t), val) 202 | }) 203 | } 204 | 205 | func SliceRemove[S []T, T comparable](v *V[S], val T) { 206 | v.Update(func(t S) S { 207 | return iterc.FilterSlice(t, func(el T) bool { return el != val }) 208 | }) 209 | } 210 | 211 | func MapPut[M ~map[K]T, K comparable, T any](m *V[M], k K, v T) { 212 | m.Update(func(t M) M { 213 | t = maps.Clone(t) 214 | t[k] = v 215 | return t 216 | }) 217 | } 218 | 219 | func MapDelete[M ~map[K]T, K comparable, T any](m *V[M], k K) { 220 | m.Update(func(t M) M { 221 | t = maps.Clone(t) 222 | delete(t, k) 223 | return t 224 | }) 225 | } 226 | 227 | func MapDeleteFunc[M ~map[K]T, K comparable, T any](m *V[M], del func(K, T) bool) { 228 | m.Update(func(t M) M { 229 | t = maps.Clone(t) 230 | maps.DeleteFunc(t, del) 231 | return t 232 | }) 233 | } 234 | -------------------------------------------------------------------------------- /notify/value_test.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNV(t *testing.T) { 12 | n := NewEmpty[int]() 13 | 14 | go func() { 15 | for i := 0; i <= 1000; i++ { 16 | n.Set(i) 17 | } 18 | }() 19 | 20 | version := uint64(0) 21 | observed := 0 22 | for { 23 | v, next, err := n.Get(context.Background(), version) 24 | require.NoError(t, err) 25 | version = next 26 | observed++ 27 | if v == 1000 { 28 | break 29 | } 30 | } 31 | fmt.Println("observed", observed) 32 | } 33 | -------------------------------------------------------------------------------- /process-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "0.5" 2 | 3 | processes: 4 | build: 5 | command: make build 6 | availability: 7 | restart: "exit_on_failure" 8 | certs: 9 | command: gen-local-certs 10 | server: 11 | command: connet server --config examples/minimal.toml 12 | depends_on: 13 | build: 14 | condition: process_completed_successfully 15 | certs: 16 | condition: process_completed 17 | client-dst: 18 | command: connet --config examples/client-destination.toml 19 | depends_on: 20 | build: 21 | condition: process_completed_successfully 22 | certs: 23 | condition: process_completed 24 | server: 25 | condition: process_started 26 | client-src: 27 | command: connet --config examples/client-source.toml 28 | depends_on: 29 | build: 30 | condition: process_completed_successfully 31 | certs: 32 | condition: process_completed 33 | server: 34 | condition: process_started 35 | client-dst: 36 | condition: process_started 37 | -------------------------------------------------------------------------------- /proto/client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package client; 3 | 4 | import "error.proto"; 5 | import "model.proto"; 6 | 7 | option go_package = "github.com/connet-dev/connet/proto/pbclient"; 8 | 9 | message Authenticate { 10 | string token = 1; 11 | bytes reconnect_token = 2; 12 | string build_version = 3; 13 | } 14 | 15 | message AuthenticateResp { 16 | error.Error error = 1; 17 | 18 | model.AddrPort public = 2; 19 | bytes reconnect_token = 3; 20 | } 21 | 22 | message Request { 23 | // Soft one-of 24 | Announce announce = 1; 25 | Relay relay = 2; 26 | 27 | message Announce { 28 | model.Forward forward = 1; 29 | model.Role role = 2; 30 | Peer peer = 3; 31 | } 32 | message Relay { 33 | model.Forward forward = 1; 34 | model.Role role = 2; 35 | bytes client_certificate = 3; // certificate to use when connecting to a relay 36 | } 37 | } 38 | 39 | message Response { 40 | error.Error error = 1; 41 | 42 | // Soft one-of if error is nil 43 | Announce announce = 2; 44 | Relays relay = 3; 45 | 46 | message Announce { 47 | repeated RemotePeer peers = 1; 48 | } 49 | message Relays { 50 | repeated Relay relays = 1; 51 | } 52 | } 53 | 54 | message Peer { 55 | repeated model.AddrPort directs = 3; 56 | repeated string relayIds = 6; 57 | bytes server_certificate = 4; // certificate to use when connecting to this client 58 | bytes client_certificate = 5; // certificate that this client uses when connecting 59 | } 60 | 61 | message RemotePeer { 62 | string id = 1; 63 | Peer peer = 8; 64 | } 65 | 66 | message Relay { 67 | string id = 3; 68 | repeated model.HostPort addresses = 4; 69 | bytes server_certificate = 2; 70 | } 71 | -------------------------------------------------------------------------------- /proto/connect.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package connect; 3 | 4 | import "error.proto"; 5 | 6 | option go_package = "github.com/connet-dev/connet/proto/pbconnect"; 7 | 8 | message Request { 9 | // Soft one-of 10 | Connect connect = 1; 11 | 12 | message Connect { 13 | repeated RelayEncryptionScheme source_encryption = 1; 14 | TLSConfiguration source_tls = 2; 15 | ECDHConfiguration source_dh_x25519 = 3; 16 | } 17 | } 18 | 19 | message Response { 20 | error.Error error = 1; 21 | 22 | Connect connect = 2; 23 | 24 | message Connect { 25 | ProxyProtoVersion proxy_proto = 1; 26 | RelayEncryptionScheme destination_encryption = 2; 27 | TLSConfiguration destination_tls = 3; 28 | ECDHConfiguration destination_dh_x25519 = 4; 29 | } 30 | } 31 | 32 | enum ProxyProtoVersion { 33 | ProxyProtoNone = 0; 34 | V1 = 1; 35 | V2 = 2; 36 | } 37 | 38 | enum RelayEncryptionScheme { 39 | EncryptionNone = 0; 40 | TLS = 1; 41 | DHX25519_CHACHAPOLY = 2; 42 | } 43 | 44 | message TLSConfiguration { 45 | string client_name = 1; 46 | } 47 | 48 | message ECDHConfiguration { 49 | string client_name = 1; 50 | bytes key_time = 2; 51 | bytes signature = 3; 52 | } 53 | -------------------------------------------------------------------------------- /proto/error.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package error; 3 | 4 | option go_package = "github.com/connet-dev/connet/proto/pberror"; 5 | 6 | enum Code { 7 | // Generic 8 | Unknown = 0; 9 | RequestUnknown = 1; 10 | ConnectionCheckFailed = 2; 11 | 12 | // Authentication 13 | AuthenticationFailed = 100; 14 | ForwardNotAllowed = 101; 15 | RoleNotAllowed = 102; 16 | 17 | // Announce 18 | AnnounceValidationFailed = 200; 19 | AnnounceInvalidClientCertificate = 201; 20 | AnnounceInvalidServerCertificate = 202; 21 | 22 | // Relay 23 | RelayValidationFailed = 300; 24 | RelayInvalidCertificate = 301; 25 | RelayKeepaliveClosed = 302; 26 | 27 | // Direct 28 | DirectConnectionClosed = 400; 29 | DirectKeepaliveClosed = 401; 30 | 31 | // Client connect codes 32 | DestinationNotFound = 500; 33 | DestinationDialFailed = 501; 34 | DestinationRelayEncryptionError = 502; 35 | } 36 | 37 | message Error { 38 | Code code = 1; 39 | string message = 2; 40 | } 41 | -------------------------------------------------------------------------------- /proto/model.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package model; 3 | 4 | option go_package = "github.com/connet-dev/connet/proto/pbmodel"; 5 | 6 | message Addr { 7 | bytes v4 = 1; 8 | bytes v6 = 2; 9 | } 10 | 11 | message AddrPort { 12 | Addr addr = 1; 13 | uint32 port = 2; // really uint16, but not a thing in protobuf 14 | } 15 | 16 | message HostPort { 17 | string host = 1; 18 | uint32 port = 2; 19 | } 20 | 21 | message Forward { 22 | string name = 1; 23 | } 24 | 25 | enum Role { 26 | RoleUnknown = 0; 27 | RoleDestination = 1; 28 | RoleSource = 2; 29 | } 30 | -------------------------------------------------------------------------------- /proto/pbclient/proto.go: -------------------------------------------------------------------------------- 1 | package pbclient 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/connet-dev/connet/proto" 8 | ) 9 | 10 | func ReadRequest(r io.Reader) (*Request, error) { 11 | req := &Request{} 12 | if err := proto.Read(r, req); err != nil { 13 | return nil, fmt.Errorf("server request read: %w", err) 14 | } 15 | return req, nil 16 | } 17 | 18 | func ReadResponse(r io.Reader) (*Response, error) { 19 | resp := &Response{} 20 | if err := proto.Read(r, resp); err != nil { 21 | return nil, fmt.Errorf("server response read: %w", err) 22 | } 23 | if resp.Error != nil { 24 | return nil, resp.Error 25 | } 26 | return resp, nil 27 | } 28 | -------------------------------------------------------------------------------- /proto/pbconnect/proto.go: -------------------------------------------------------------------------------- 1 | package pbconnect 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/connet-dev/connet/proto" 8 | pberror "github.com/connet-dev/connet/proto/pberror" 9 | ) 10 | 11 | func ReadRequest(r io.Reader) (*Request, error) { 12 | req := &Request{} 13 | if err := proto.Read(r, req); err != nil { 14 | return nil, err 15 | } 16 | return req, nil 17 | } 18 | 19 | func ReadResponse(r io.Reader) (*Response, error) { 20 | resp := &Response{} 21 | if err := proto.Read(r, resp); err != nil { 22 | return nil, err 23 | } 24 | if resp.Error != nil { 25 | return nil, resp.Error 26 | } 27 | return resp, nil 28 | } 29 | 30 | func WriteError(w io.Writer, code pberror.Code, msg string, args ...any) error { 31 | err := pberror.NewError(code, msg, args...) 32 | if err := proto.Write(w, &Response{Error: err}); err != nil { 33 | return fmt.Errorf("write err response: %w", err) 34 | } 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /proto/pberror/error.go: -------------------------------------------------------------------------------- 1 | package pberror 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/quic-go/quic-go" 8 | ) 9 | 10 | func NewError(code Code, msg string, args ...any) *Error { 11 | return &Error{ 12 | Code: code, 13 | Message: fmt.Sprintf(msg, args...), 14 | } 15 | } 16 | 17 | func (e *Error) Error() string { 18 | return fmt.Sprintf("%s (%d)", e.Message, e.Code) 19 | } 20 | 21 | func GetError(err error) *Error { 22 | var e *Error 23 | if errors.As(err, &e) { 24 | return e 25 | } 26 | return nil 27 | } 28 | 29 | func GetAppError(err error) *quic.ApplicationError { 30 | var e *quic.ApplicationError 31 | if errors.As(err, &e) { 32 | return e 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /proto/pbmodel/addr.go: -------------------------------------------------------------------------------- 1 | package pbmodel 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | ) 7 | 8 | func AddrFromNetip(addr netip.Addr) *Addr { 9 | if addr.Is6() { 10 | v6 := addr.As16() 11 | return &Addr{V6: v6[:]} 12 | } 13 | v4 := addr.As4() 14 | return &Addr{V4: v4[:]} 15 | } 16 | 17 | func (a *Addr) AsNetip() netip.Addr { 18 | if len(a.V6) > 0 { 19 | return netip.AddrFrom16([16]byte(a.V6)) 20 | } 21 | return netip.AddrFrom4([4]byte(a.V4)) 22 | } 23 | 24 | func AddrPortFromNet(addr net.Addr) (*AddrPort, error) { 25 | switch t := addr.(type) { 26 | case *net.UDPAddr: 27 | return AddrPortFromNetip(t.AddrPort()), nil 28 | case *net.TCPAddr: 29 | return AddrPortFromNetip(t.AddrPort()), nil 30 | default: 31 | naddr, err := netip.ParseAddrPort(addr.String()) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return AddrPortFromNetip(naddr), nil 36 | } 37 | } 38 | 39 | func AddrPortFromNetip(addr netip.AddrPort) *AddrPort { 40 | return &AddrPort{ 41 | Addr: AddrFromNetip(addr.Addr()), 42 | Port: uint32(addr.Port()), 43 | } 44 | } 45 | 46 | func (a *AddrPort) AsNetip() netip.AddrPort { 47 | return netip.AddrPortFrom(a.Addr.AsNetip(), uint16(a.Port)) 48 | } 49 | 50 | func AsNetips(pb []*AddrPort) []netip.AddrPort { 51 | s := make([]netip.AddrPort, len(pb)) 52 | for i, pbi := range pb { 53 | s[i] = pbi.AsNetip() 54 | } 55 | return s 56 | } 57 | 58 | func AsAddrPorts(addrs []netip.AddrPort) []*AddrPort { 59 | s := make([]*AddrPort, len(addrs)) 60 | for i, addr := range addrs { 61 | s[i] = AddrPortFromNetip(addr) 62 | } 63 | return s 64 | } 65 | -------------------------------------------------------------------------------- /proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/connet-dev/connet/proto/pberror" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | func Write(w io.Writer, msg proto.Message) error { 12 | msgBytes, err := proto.Marshal(msg) 13 | if err != nil { 14 | return err 15 | } 16 | szBytes := make([]byte, 0, 8) 17 | szBytes = binary.BigEndian.AppendUint64(szBytes, uint64(len(msgBytes))) 18 | if _, err := w.Write(szBytes); err != nil { 19 | if aperr := pberror.GetAppError(err); aperr != nil { 20 | return &pberror.Error{ 21 | Code: pberror.Code(aperr.ErrorCode), 22 | Message: aperr.ErrorMessage, 23 | } 24 | } 25 | return err 26 | } 27 | _, err = w.Write(msgBytes) 28 | if err != nil { 29 | if aperr := pberror.GetAppError(err); aperr != nil { 30 | return &pberror.Error{ 31 | Code: pberror.Code(aperr.ErrorCode), 32 | Message: aperr.ErrorMessage, 33 | } 34 | } 35 | } 36 | return err 37 | } 38 | 39 | func Read(r io.Reader, msg proto.Message) error { 40 | szBytes := make([]byte, 8) 41 | 42 | _, err := io.ReadFull(r, szBytes) 43 | if err != nil { 44 | if aperr := pberror.GetAppError(err); aperr != nil { 45 | return &pberror.Error{ 46 | Code: pberror.Code(aperr.ErrorCode), 47 | Message: aperr.ErrorMessage, 48 | } 49 | } 50 | return err 51 | } 52 | sz := binary.BigEndian.Uint64(szBytes) 53 | 54 | msgBytes := make([]byte, sz) 55 | _, err = io.ReadFull(r, msgBytes) 56 | if err != nil { 57 | if aperr := pberror.GetAppError(err); aperr != nil { 58 | return &pberror.Error{ 59 | Code: pberror.Code(aperr.ErrorCode), 60 | Message: aperr.ErrorMessage, 61 | } 62 | } 63 | return err 64 | } 65 | 66 | return proto.Unmarshal(msgBytes, msg) 67 | } 68 | -------------------------------------------------------------------------------- /proto/relay.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package relay; 3 | 4 | import "error.proto"; 5 | import "model.proto"; 6 | 7 | option go_package = "github.com/connet-dev/connet/proto/pbrelay"; 8 | 9 | message AuthenticateReq { 10 | string token = 1; 11 | repeated model.HostPort addresses = 5; 12 | bytes reconnect_token = 3; 13 | string build_version = 4; 14 | } 15 | 16 | message AuthenticateResp { 17 | error.Error error = 1; 18 | string control_id = 2; 19 | bytes reconnect_token = 3; 20 | } 21 | 22 | enum ChangeType { 23 | ChangeUnknown = 0; 24 | ChangePut = 1; 25 | ChangeDel = 2; 26 | } 27 | 28 | message ClientsReq { 29 | int64 offset = 1; 30 | } 31 | 32 | message ClientsResp { 33 | repeated Change changes = 1; 34 | int64 offset = 2; 35 | bool restart = 3; 36 | 37 | message Change { 38 | ChangeType change = 1; 39 | model.Forward forward = 2; 40 | model.Role role = 3; 41 | string certificate_key = 4; 42 | bytes certificate = 5; 43 | } 44 | } 45 | 46 | message ServersReq { 47 | int64 offset = 1; 48 | } 49 | 50 | message ServersResp { 51 | repeated Change changes = 1; 52 | int64 offset = 2; 53 | bool restart = 3; 54 | 55 | message Change { 56 | ChangeType change = 1; 57 | model.Forward forward = 2; 58 | bytes server_certificate = 3; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /quicc/conf.go: -------------------------------------------------------------------------------- 1 | package quicc 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/quic-go/quic-go" 8 | ) 9 | 10 | func ClientTransport(conn net.PacketConn, statelessResetKey *quic.StatelessResetKey) *quic.Transport { 11 | return &quic.Transport{ 12 | Conn: conn, 13 | StatelessResetKey: statelessResetKey, 14 | ConnContext: RTTContext, 15 | // TODO review other options 16 | } 17 | } 18 | 19 | func ServerTransport(conn net.PacketConn, statelessResetKey *quic.StatelessResetKey) *quic.Transport { 20 | return &quic.Transport{ 21 | Conn: conn, 22 | ConnectionIDLength: 8, 23 | StatelessResetKey: statelessResetKey, 24 | ConnContext: RTTContext, 25 | // TODO review other options 26 | } 27 | } 28 | 29 | var StdConfig = &quic.Config{ 30 | MaxIdleTimeout: 20 * time.Second, 31 | KeepAlivePeriod: 10 * time.Second, 32 | Tracer: RTTTracer, 33 | // TODO review other options 34 | } 35 | -------------------------------------------------------------------------------- /quicc/conn.go: -------------------------------------------------------------------------------- 1 | package quicc 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/quic-go/quic-go" 7 | ) 8 | 9 | func StreamConn(s quic.Stream, c quic.Connection) net.Conn { 10 | return &streamConn{ 11 | Stream: s, 12 | Local: c.LocalAddr(), 13 | Remote: c.RemoteAddr(), 14 | } 15 | } 16 | 17 | type streamConn struct { 18 | quic.Stream 19 | Local net.Addr 20 | Remote net.Addr 21 | } 22 | 23 | func (s *streamConn) LocalAddr() net.Addr { 24 | return s.Local 25 | } 26 | 27 | func (s *streamConn) RemoteAddr() net.Addr { 28 | return s.Remote 29 | } 30 | 31 | func (s *streamConn) Close() error { 32 | s.Stream.CancelRead(0) 33 | return s.Stream.Close() 34 | } 35 | -------------------------------------------------------------------------------- /quicc/rtt.go: -------------------------------------------------------------------------------- 1 | package quicc 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "sync/atomic" 7 | 8 | "github.com/quic-go/quic-go" 9 | "github.com/quic-go/quic-go/logging" 10 | ) 11 | 12 | type rttContextKeyType struct{} 13 | 14 | var rttContextKey rttContextKeyType 15 | 16 | type rttStats struct { 17 | stats atomic.Pointer[logging.RTTStats] 18 | } 19 | 20 | func RTTContext(ctx context.Context) context.Context { 21 | return context.WithValue(ctx, rttContextKey, &rttStats{}) 22 | } 23 | 24 | func RTTTracer(ctx context.Context, pers logging.Perspective, ci quic.ConnectionID) *logging.ConnectionTracer { 25 | v, ok := ctx.Value(rttContextKey).(*rttStats) 26 | if !ok { 27 | return nil 28 | } 29 | return &logging.ConnectionTracer{ 30 | UpdatedMetrics: func(rttStats *logging.RTTStats, cwnd, bytesInFlight logging.ByteCount, packetsInFlight int) { 31 | // make a copy of the stats at this point of time 32 | stats := *rttStats 33 | v.stats.Store(&stats) 34 | }, 35 | } 36 | } 37 | 38 | func RTTStats(conn quic.Connection) *logging.RTTStats { 39 | v, ok := conn.Context().Value(rttContextKey).(*rttStats) 40 | if !ok { 41 | return nil 42 | } 43 | return v.stats.Load() 44 | } 45 | 46 | func RTTLogStats(conn quic.Connection, logger *slog.Logger) { 47 | if rttStats := RTTStats(conn); rttStats != nil { 48 | logger.Debug("rtt stats", "last", rttStats.LatestRTT(), "smoothed", rttStats.SmoothedRTT()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /relay/ingress.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/connet-dev/connet/model" 10 | "github.com/connet-dev/connet/restr" 11 | ) 12 | 13 | type Ingress struct { 14 | Addr *net.UDPAddr 15 | Hostports []model.HostPort 16 | Restr restr.IP 17 | } 18 | 19 | type IngressBuilder struct { 20 | ingress Ingress 21 | err error 22 | } 23 | 24 | func NewIngressBuilder() *IngressBuilder { return &IngressBuilder{} } 25 | 26 | func (b *IngressBuilder) WithAddr(addr *net.UDPAddr) *IngressBuilder { 27 | if b.err != nil { 28 | return b 29 | } 30 | b.ingress.Addr = addr 31 | return b 32 | } 33 | 34 | func (b *IngressBuilder) WithAddrFrom(addrStr string) *IngressBuilder { 35 | if b.err != nil { 36 | return b 37 | } 38 | 39 | addr, err := net.ResolveUDPAddr("udp", addrStr) 40 | if err != nil { 41 | b.err = fmt.Errorf("resolve udp address: %w", err) 42 | return b 43 | } 44 | return b.WithAddr(addr) 45 | } 46 | 47 | func (b *IngressBuilder) WithHostports(hps []model.HostPort) *IngressBuilder { 48 | if b.err != nil { 49 | return b 50 | } 51 | b.ingress.Hostports = hps 52 | return b 53 | } 54 | 55 | func (b *IngressBuilder) WithHostport(hp model.HostPort) *IngressBuilder { 56 | if b.err != nil { 57 | return b 58 | } 59 | b.ingress.Hostports = append(b.ingress.Hostports, hp) 60 | return b 61 | } 62 | 63 | func (b *IngressBuilder) WithHostportFrom(hostport string) *IngressBuilder { 64 | if b.err != nil { 65 | return b 66 | } 67 | 68 | if strings.HasPrefix(hostport, "[") { 69 | closeBracket := strings.LastIndex(hostport, "]") 70 | if closeBracket < 0 { 71 | b.err = fmt.Errorf("cannot parse hostport, missing ]") 72 | return b 73 | } 74 | colonPort := hostport[closeBracket+1:] 75 | if len(colonPort) > 0 { 76 | if colonPort[0] != ':' { 77 | b.err = fmt.Errorf("cannot parse hostport, missing :") 78 | return b 79 | } 80 | portStr := hostport[1:] 81 | if len(portStr) == 0 { 82 | b.err = fmt.Errorf("cannot parse hostport, missing port") 83 | return b 84 | } 85 | port, err := strconv.ParseInt(portStr, 10, 16) 86 | if err != nil { 87 | b.err = fmt.Errorf("cannot parse port: %w", err) 88 | return b 89 | } 90 | return b.WithHostport(model.HostPort{Host: hostport[:closeBracket+1], Port: uint16(port)}) 91 | } 92 | } else if colonIndex := strings.LastIndex(hostport, ":"); colonIndex != -1 { 93 | portStr := hostport[colonIndex+1:] 94 | if len(portStr) == 0 { 95 | b.err = fmt.Errorf("cannot parse hostport, missing port") 96 | return b 97 | } 98 | port, err := strconv.ParseInt(portStr, 10, 16) 99 | if err != nil { 100 | b.err = fmt.Errorf("cannot parse port: %w", err) 101 | return b 102 | } 103 | return b.WithHostport(model.HostPort{Host: hostport[:colonIndex], Port: uint16(port)}) 104 | } 105 | 106 | return b.WithHostport(model.HostPort{Host: hostport}) 107 | } 108 | 109 | func (b *IngressBuilder) WithRestr(iprestr restr.IP) *IngressBuilder { 110 | if b.err != nil { 111 | return b 112 | } 113 | 114 | b.ingress.Restr = iprestr 115 | return b 116 | } 117 | 118 | func (b *IngressBuilder) WithRestrFrom(allows []string, denies []string) *IngressBuilder { 119 | if b.err != nil { 120 | return b 121 | } 122 | 123 | iprestr, err := restr.ParseIP(allows, denies) 124 | if err != nil { 125 | b.err = fmt.Errorf("parse restrictions: %w", err) 126 | return b 127 | } 128 | return b.WithRestr(iprestr) 129 | } 130 | 131 | func (b *IngressBuilder) Error() error { 132 | return b.err 133 | } 134 | 135 | func (b *IngressBuilder) Ingress() (Ingress, error) { 136 | if b.err != nil { 137 | return b.ingress, b.err 138 | } 139 | 140 | for i, hp := range b.ingress.Hostports { 141 | if hp.Host == "" { 142 | switch { 143 | case b.ingress.Addr == nil: 144 | hp.Host = "localhost" 145 | case len(b.ingress.Addr.IP) == 0: 146 | hp.Host = "localhost" 147 | default: 148 | hp.Host = b.ingress.Addr.IP.String() 149 | } 150 | } 151 | if hp.Port == 0 { 152 | switch { 153 | case b.ingress.Addr == nil: 154 | hp.Port = 19191 155 | case b.ingress.Addr.Port == 0: 156 | hp.Port = 19191 // TODO maybe an error, it might be a random port 157 | default: 158 | hp.Port = uint16(b.ingress.Addr.Port) 159 | } 160 | } 161 | 162 | b.ingress.Hostports[i] = hp 163 | } 164 | 165 | return b.ingress, b.err 166 | } 167 | -------------------------------------------------------------------------------- /relay/server.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/x509" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "maps" 11 | "net" 12 | "slices" 13 | 14 | "github.com/connet-dev/connet/iterc" 15 | "github.com/connet-dev/connet/model" 16 | "github.com/connet-dev/connet/notify" 17 | "github.com/connet-dev/connet/statusc" 18 | "github.com/quic-go/quic-go" 19 | "golang.org/x/sync/errgroup" 20 | ) 21 | 22 | type Config struct { 23 | ControlAddr *net.UDPAddr 24 | ControlHost string 25 | ControlToken string 26 | ControlCAs *x509.CertPool 27 | 28 | Ingress []Ingress 29 | 30 | Stores Stores 31 | 32 | Logger *slog.Logger 33 | } 34 | 35 | func NewServer(cfg Config) (*Server, error) { 36 | if len(cfg.Ingress) == 0 { 37 | return nil, fmt.Errorf("relay server is missing ingresses") 38 | } 39 | 40 | configStore, err := cfg.Stores.Config() 41 | if err != nil { 42 | return nil, fmt.Errorf("relay stores: %w", err) 43 | } 44 | 45 | statelessResetVal, err := configStore.GetOrInit(configStatelessReset, func(ck ConfigKey) (ConfigValue, error) { 46 | var key quic.StatelessResetKey 47 | if _, err := io.ReadFull(rand.Reader, key[:]); err != nil { 48 | return ConfigValue{}, fmt.Errorf("generate rand: %w", err) 49 | } 50 | return ConfigValue{Bytes: key[:]}, nil 51 | }) 52 | if err != nil { 53 | return nil, fmt.Errorf("relay stateless reset key: %w", err) 54 | } 55 | var statelessResetKey quic.StatelessResetKey 56 | copy(statelessResetKey[:], statelessResetVal.Bytes) 57 | 58 | control, err := newControlClient(cfg, configStore) 59 | if err != nil { 60 | return nil, fmt.Errorf("relay control client: %w", err) 61 | } 62 | 63 | clients := newClientsServer(cfg, control.tlsAuthenticate, control.authenticate) 64 | 65 | return &Server{ 66 | ingress: cfg.Ingress, 67 | statelessResetKey: &statelessResetKey, 68 | 69 | control: control, 70 | clients: clients, 71 | }, nil 72 | } 73 | 74 | type Server struct { 75 | ingress []Ingress 76 | statelessResetKey *quic.StatelessResetKey 77 | 78 | control *controlClient 79 | clients *clientsServer 80 | } 81 | 82 | func (s *Server) Run(ctx context.Context) error { 83 | g, ctx := errgroup.WithContext(ctx) 84 | 85 | transports := notify.NewEmpty[[]*quic.Transport]() 86 | 87 | for _, ingress := range s.ingress { 88 | g.Go(func() error { 89 | return s.clients.run(ctx, clientsServerCfg{ 90 | ingress: ingress, 91 | statelessResetKey: s.statelessResetKey, 92 | addedTransport: func(t *quic.Transport) { 93 | notify.SliceAppend(transports, t) 94 | }, 95 | removeTransport: func(t *quic.Transport) { 96 | notify.SliceRemove(transports, t) 97 | }, 98 | }) 99 | }) 100 | } 101 | 102 | g.Go(func() error { 103 | return s.control.run(ctx, func(ctx context.Context) ([]*quic.Transport, error) { 104 | t, _, err := transports.GetAny(ctx) 105 | return t, err 106 | }) 107 | }) 108 | 109 | return g.Wait() 110 | } 111 | 112 | func (s *Server) Status(ctx context.Context) (Status, error) { 113 | stat := s.control.connStatus.Load().(statusc.Status) 114 | 115 | controlID, err := s.getControlID() 116 | if err != nil { 117 | return Status{}, err 118 | } 119 | 120 | fwds := s.getForwards() 121 | 122 | return Status{ 123 | Status: stat, 124 | Hostports: iterc.MapSliceStrings(s.control.hostports), 125 | ControlServerAddr: s.control.controlAddr.String(), 126 | ControlServerID: controlID, 127 | Forwards: fwds, 128 | }, nil 129 | } 130 | 131 | func (s *Server) getControlID() (string, error) { 132 | controlIDConfig, err := s.control.config.GetOrDefault(configControlID, ConfigValue{}) 133 | if err != nil { 134 | return "", err 135 | } 136 | return controlIDConfig.String, nil 137 | } 138 | 139 | func (s *Server) getForwards() []model.Forward { 140 | s.clients.forwardMu.RLock() 141 | defer s.clients.forwardMu.RUnlock() 142 | 143 | return slices.Collect(maps.Keys(s.clients.forwards)) 144 | } 145 | 146 | type Status struct { 147 | Status statusc.Status `json:"status"` 148 | Hostports []string `json:"hostports"` 149 | ControlServerAddr string `json:"control_server_addr"` 150 | ControlServerID string `json:"control_server_id"` 151 | Forwards []model.Forward `json:"forwards"` 152 | } 153 | -------------------------------------------------------------------------------- /relay/store.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/json" 6 | "path/filepath" 7 | 8 | "github.com/connet-dev/connet/certc" 9 | "github.com/connet-dev/connet/logc" 10 | "github.com/connet-dev/connet/model" 11 | ) 12 | 13 | type Stores interface { 14 | Config() (logc.KV[ConfigKey, ConfigValue], error) 15 | Clients() (logc.KV[ClientKey, ClientValue], error) 16 | Servers() (logc.KV[ServerKey, ServerValue], error) 17 | } 18 | 19 | func NewFileStores(dir string) Stores { 20 | return &fileStores{dir} 21 | } 22 | 23 | type fileStores struct { 24 | dir string 25 | } 26 | 27 | func (f *fileStores) Config() (logc.KV[ConfigKey, ConfigValue], error) { 28 | return logc.NewKV[ConfigKey, ConfigValue](filepath.Join(f.dir, "config")) 29 | } 30 | 31 | func (f *fileStores) Clients() (logc.KV[ClientKey, ClientValue], error) { 32 | return logc.NewKV[ClientKey, ClientValue](filepath.Join(f.dir, "clients")) 33 | } 34 | 35 | func (f *fileStores) Servers() (logc.KV[ServerKey, ServerValue], error) { 36 | return logc.NewKV[ServerKey, ServerValue](filepath.Join(f.dir, "servers")) 37 | } 38 | 39 | type ConfigKey string 40 | 41 | var ( 42 | configStatelessReset ConfigKey = "stateless-reset" 43 | configControlID ConfigKey = "control-id" 44 | configControlReconnect ConfigKey = "control-reconnect" 45 | configClientsStreamOffset ConfigKey = "clients-stream-offset" 46 | configClientsLogOffset ConfigKey = "clients-log-offset" 47 | ) 48 | 49 | type ConfigValue struct { 50 | Int64 int64 `json:"int64,omitempty"` 51 | String string `json:"string,omitempty"` 52 | Bytes []byte `json:"bytes,omitempty"` 53 | } 54 | 55 | type ClientKey struct { 56 | Forward model.Forward `json:"forward"` 57 | Role model.Role `json:"role"` 58 | Key model.Key `json:"key"` 59 | } 60 | 61 | type ClientValue struct { 62 | Cert *x509.Certificate `json:"cert"` 63 | } 64 | 65 | func (v ClientValue) MarshalJSON() ([]byte, error) { 66 | return certc.MarshalJSONCert(v.Cert) 67 | } 68 | 69 | func (v *ClientValue) UnmarshalJSON(b []byte) error { 70 | cert, err := certc.UnmarshalJSONCert(b) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | *v = ClientValue{cert} 76 | return nil 77 | } 78 | 79 | type ServerKey struct { 80 | Forward model.Forward `json:"forward"` 81 | } 82 | 83 | type ServerValue struct { 84 | Name string `json:"name"` 85 | Cert *certc.Cert `json:"cert"` 86 | Clients map[serverClientKey]ClientValue `json:"clients"` 87 | } 88 | 89 | func (v ServerValue) MarshalJSON() ([]byte, error) { 90 | cert, key, err := v.Cert.EncodeToMemory() 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | s := struct { 96 | Name string `json:"name"` 97 | Cert []byte `json:"cert"` 98 | CertKey []byte `json:"cert_key"` 99 | Clients []serverClientValue `json:"clients"` 100 | }{ 101 | Name: v.Name, 102 | Cert: cert, 103 | CertKey: key, 104 | } 105 | 106 | for k, v := range v.Clients { 107 | s.Clients = append(s.Clients, serverClientValue{ 108 | Role: k.Role, 109 | Value: v, 110 | }) 111 | } 112 | 113 | return json.Marshal(s) 114 | } 115 | 116 | func (v *ServerValue) UnmarshalJSON(b []byte) error { 117 | s := struct { 118 | Name string `json:"name"` 119 | Cert []byte `json:"cert"` 120 | CertKey []byte `json:"cert_key"` 121 | Clients []serverClientValue `json:"clients"` 122 | }{} 123 | if err := json.Unmarshal(b, &s); err != nil { 124 | return err 125 | } 126 | 127 | cert, err := certc.DecodeFromMemory(s.Cert, s.CertKey) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | sv := ServerValue{ 133 | Name: s.Name, 134 | Cert: cert, 135 | Clients: map[serverClientKey]ClientValue{}, 136 | } 137 | 138 | for _, cl := range s.Clients { 139 | sv.Clients[serverClientKey{cl.Role, model.NewKey(cl.Value.Cert)}] = cl.Value 140 | } 141 | 142 | *v = sv 143 | return nil 144 | } 145 | 146 | type serverClientKey struct { 147 | Role model.Role `json:"role"` 148 | Key model.Key `json:"key"` 149 | } 150 | 151 | type serverClientValue struct { 152 | Role model.Role `json:"role"` 153 | Value ClientValue `json:"value"` 154 | } 155 | -------------------------------------------------------------------------------- /restr/ip.go: -------------------------------------------------------------------------------- 1 | package restr 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/netip" 7 | 8 | "github.com/connet-dev/connet/netc" 9 | ) 10 | 11 | type IP struct { 12 | Allows []netip.Prefix `json:"allows,omitempty"` 13 | Denies []netip.Prefix `json:"denies,omitempty"` 14 | } 15 | 16 | // ParseIP parses a slice of allows/denys restrictions in CIDR format. 17 | func ParseIP(allowsStr []string, deniesStr []string) (IP, error) { 18 | allows, err := netc.ParseCIDRs(allowsStr) 19 | if err != nil { 20 | return IP{}, fmt.Errorf("parse allow cidrs %v: %w", allowsStr, err) 21 | } 22 | 23 | denies, err := netc.ParseCIDRs(deniesStr) 24 | if err != nil { 25 | return IP{}, fmt.Errorf("parse deny cidrs: %w", err) 26 | } 27 | 28 | return IP{allows, denies}, nil 29 | } 30 | 31 | func (r IP) IsEmpty() bool { 32 | return len(r.Allows) == 0 && len(r.Denies) == 0 33 | } 34 | 35 | func (r IP) IsNotEmpty() bool { 36 | return !r.IsEmpty() 37 | } 38 | 39 | // IsAllowed checks if an IP address is allowed according to Allows and Denies rules. 40 | // 41 | // If the ip matches any of the Denies rules, IsAllowed returns false. 42 | // If the ip matches any of the Allows rules (after checking all Denies rules), IsAllowed returns true. 43 | // 44 | // Finally, if the ip matches no Allows or Denies rules, IsAllowed returns true only if no explicit Allows rules were defined. 45 | func (r IP) IsAllowed(ip netip.Addr) bool { 46 | ip = ip.Unmap() // remove any ipv6 prefix for ipv4 47 | 48 | for _, d := range r.Denies { 49 | if d.Contains(ip) { 50 | return false 51 | } 52 | } 53 | 54 | for _, a := range r.Allows { 55 | if a.Contains(ip) { 56 | return true 57 | } 58 | } 59 | 60 | return len(r.Allows) == 0 61 | } 62 | 63 | // IsAllowedAddr extracts the IP address from net.Addr and checks if it is allowed 64 | func (r IP) IsAllowedAddr(addr net.Addr) bool { 65 | switch taddr := addr.(type) { 66 | case *net.UDPAddr: 67 | return r.IsAllowed(taddr.AddrPort().Addr()) 68 | case *net.TCPAddr: 69 | return r.IsAllowed(taddr.AddrPort().Addr()) 70 | default: 71 | naddr, err := netip.ParseAddrPort(addr.String()) 72 | if err != nil { 73 | return false 74 | } 75 | return r.IsAllowed(naddr.Addr()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /restr/ip_test.go: -------------------------------------------------------------------------------- 1 | package restr 2 | 3 | import ( 4 | "net/netip" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIP(t *testing.T) { 11 | tcs := []struct { 12 | name string 13 | allow []string 14 | deny []string 15 | check string 16 | accept bool 17 | }{ 18 | { 19 | name: "empty", 20 | check: "10.100.2.100", 21 | accept: true, 22 | }, 23 | { 24 | name: "allow match", 25 | allow: []string{"10.100.2.0/24"}, 26 | check: "10.100.2.100", 27 | accept: true, 28 | }, 29 | { 30 | name: "allow nomatch", 31 | allow: []string{"10.100.2.0/24"}, 32 | check: "10.101.2.100", 33 | accept: false, 34 | }, 35 | { 36 | name: "deny match", 37 | deny: []string{"10.100.2.0/24"}, 38 | check: "10.100.2.100", 39 | accept: false, 40 | }, 41 | { 42 | name: "deny empty allow", 43 | deny: []string{"10.100.2.0/24"}, 44 | check: "10.101.2.100", 45 | accept: true, 46 | }, 47 | { 48 | name: "deny with allow", 49 | allow: []string{"10.100.2.0/24"}, 50 | deny: []string{"10.100.2.0/24"}, 51 | check: "10.100.2.100", 52 | accept: false, 53 | }, 54 | { 55 | name: "allow explicit", 56 | allow: []string{"10.101.2.0/24"}, 57 | deny: []string{"10.100.2.0/24"}, 58 | check: "10.102.2.100", 59 | accept: false, 60 | }, 61 | { 62 | name: "allow exact", 63 | allow: []string{"10.101.2.0/24"}, 64 | deny: []string{"10.100.2.0/24"}, 65 | check: "10.101.2.100", 66 | accept: true, 67 | }, 68 | } 69 | 70 | for _, tc := range tcs { 71 | t.Run(tc.name, func(t *testing.T) { 72 | restr, err := ParseIP(tc.allow, tc.deny) 73 | require.NoError(t, err) 74 | require.Equal(t, tc.accept, restr.IsAllowed(netip.MustParseAddr(tc.check))) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /restr/name.go: -------------------------------------------------------------------------------- 1 | package restr 2 | 3 | import "regexp" 4 | 5 | type Name struct { 6 | Expression *regexp.Regexp `json:"expression,omitempty"` 7 | } 8 | 9 | func ParseName(s string) (Name, error) { 10 | exp, err := regexp.Compile(s) 11 | if err != nil { 12 | return Name{}, err 13 | } 14 | return Name{exp}, nil 15 | } 16 | 17 | func (r Name) IsAllowed(s string) bool { 18 | if r.Expression == nil { 19 | return true 20 | } 21 | return r.Expression.MatchString(s) 22 | } 23 | -------------------------------------------------------------------------------- /restr/name_test.go: -------------------------------------------------------------------------------- 1 | package restr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestName(t *testing.T) { 10 | tcs := []struct { 11 | name string 12 | exp string 13 | accept bool 14 | }{ 15 | { 16 | name: "exact", 17 | exp: `^exact$`, 18 | accept: true, 19 | }, 20 | { 21 | name: "exact-no", 22 | exp: `^exact$`, 23 | accept: false, 24 | }, 25 | { 26 | name: "oneof", 27 | exp: `oneof|twoof`, 28 | accept: true, 29 | }, 30 | { 31 | name: "three", 32 | exp: `oneof|twoof`, 33 | accept: false, 34 | }, 35 | } 36 | 37 | for _, tc := range tcs { 38 | t.Run(tc.name, func(t *testing.T) { 39 | restr, err := ParseName(tc.exp) 40 | require.NoError(t, err) 41 | require.Equal(t, tc.accept, restr.IsAllowed(tc.name)) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /selfhosted/clients.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "github.com/connet-dev/connet/control" 5 | "github.com/connet-dev/connet/model" 6 | "github.com/connet-dev/connet/proto/pberror" 7 | "github.com/connet-dev/connet/restr" 8 | ) 9 | 10 | type ClientAuthentication struct { 11 | Token string 12 | IPs restr.IP 13 | Names restr.Name 14 | Role model.Role 15 | } 16 | 17 | func NewClientAuthenticator(auths ...ClientAuthentication) control.ClientAuthenticator { 18 | s := &clientsAuthenticator{map[string]*ClientAuthentication{}} 19 | for _, auth := range auths { 20 | s.tokens[auth.Token] = &auth 21 | } 22 | return s 23 | } 24 | 25 | type clientsAuthenticator struct { 26 | tokens map[string]*ClientAuthentication 27 | } 28 | 29 | func (s *clientsAuthenticator) Authenticate(req control.ClientAuthenticateRequest) (control.ClientAuthentication, error) { 30 | r, ok := s.tokens[req.Token] 31 | if !ok { 32 | return nil, pberror.NewError(pberror.Code_AuthenticationFailed, "token not found") 33 | } 34 | if !r.IPs.IsAllowedAddr(req.Addr) { 35 | return nil, pberror.NewError(pberror.Code_AuthenticationFailed, "address not allowed: %s", req.Addr) 36 | } 37 | return []byte(req.Token), nil 38 | } 39 | 40 | func (s *clientsAuthenticator) Validate(auth control.ClientAuthentication, fwd model.Forward, role model.Role) (model.Forward, error) { 41 | r, ok := s.tokens[string(auth)] 42 | if !ok { 43 | return model.Forward{}, pberror.NewError(pberror.Code_AuthenticationFailed, "token not found") 44 | } 45 | if !r.Names.IsAllowed(fwd.String()) { 46 | return model.Forward{}, pberror.NewError(pberror.Code_ForwardNotAllowed, "forward not allowed: %s", fwd) 47 | } 48 | if r.Role != model.UnknownRole && r.Role != role { 49 | return model.Forward{}, pberror.NewError(pberror.Code_RoleNotAllowed, "role not allowed: %s", role) 50 | } 51 | return fwd, nil 52 | } 53 | -------------------------------------------------------------------------------- /selfhosted/relays.go: -------------------------------------------------------------------------------- 1 | package selfhosted 2 | 3 | import ( 4 | "github.com/connet-dev/connet/control" 5 | "github.com/connet-dev/connet/model" 6 | "github.com/connet-dev/connet/proto/pberror" 7 | "github.com/connet-dev/connet/restr" 8 | ) 9 | 10 | type RelayAuthentication struct { 11 | Token string 12 | IPs restr.IP 13 | } 14 | 15 | func NewRelayAuthenticator(auths ...RelayAuthentication) control.RelayAuthenticator { 16 | s := &relayAuthenticator{map[string]*RelayAuthentication{}} 17 | for _, auth := range auths { 18 | s.tokens[auth.Token] = &auth 19 | } 20 | return s 21 | } 22 | 23 | type relayAuthenticator struct { 24 | tokens map[string]*RelayAuthentication 25 | } 26 | 27 | func (s *relayAuthenticator) Authenticate(req control.RelayAuthenticateRequest) (control.RelayAuthentication, error) { 28 | r, ok := s.tokens[req.Token] 29 | if !ok { 30 | return nil, pberror.NewError(pberror.Code_AuthenticationFailed, "token not found") 31 | } 32 | if !r.IPs.IsAllowedAddr(req.Addr) { 33 | return nil, pberror.NewError(pberror.Code_AuthenticationFailed, "address not allowed: %s", req.Addr) 34 | } 35 | return []byte(r.Token), nil 36 | } 37 | 38 | func (s *relayAuthenticator) Allow(_ control.RelayAuthentication, _ control.ClientAuthentication, _ model.Forward) (bool, error) { 39 | return true, nil 40 | } 41 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package connet 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "path/filepath" 9 | 10 | "github.com/connet-dev/connet/certc" 11 | "github.com/connet-dev/connet/control" 12 | "github.com/connet-dev/connet/netc" 13 | "github.com/connet-dev/connet/relay" 14 | "github.com/connet-dev/connet/selfhosted" 15 | "golang.org/x/sync/errgroup" 16 | ) 17 | 18 | type Server struct { 19 | serverConfig 20 | 21 | control *control.Server 22 | relay *relay.Server 23 | } 24 | 25 | func NewServer(opts ...ServerOption) (*Server, error) { 26 | cfg, err := newServerConfig(opts) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | rootCert, err := certc.NewRoot() 32 | if err != nil { 33 | return nil, fmt.Errorf("generate relays root cert: %w", err) 34 | } 35 | 36 | relaysAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:19189") 37 | if err != nil { 38 | return nil, fmt.Errorf("resolve relays address: %w", err) 39 | } 40 | relaysCert, err := rootCert.NewServer(certc.CertOpts{ 41 | IPs: []net.IP{relaysAddr.IP}, 42 | }) 43 | if err != nil { 44 | return nil, fmt.Errorf("generate relays cert: %w", err) 45 | } 46 | relaysCAs, err := relaysCert.CertPool() 47 | if err != nil { 48 | return nil, fmt.Errorf("get relays CAs: %w", err) 49 | } 50 | relaysTLSCert, err := relaysCert.TLSCert() 51 | if err != nil { 52 | return nil, fmt.Errorf("get relays TLS cert: %w", err) 53 | } 54 | 55 | relayAuth := selfhosted.RelayAuthentication{ 56 | Token: netc.GenServerName("relay"), 57 | } 58 | 59 | control, err := control.NewServer(control.Config{ 60 | ClientsIngress: cfg.clientsIngresses, 61 | ClientsAuth: cfg.clientsAuth, 62 | 63 | RelaysIngress: []control.Ingress{{ 64 | Addr: relaysAddr, 65 | TLS: &tls.Config{ 66 | Certificates: []tls.Certificate{relaysTLSCert}, 67 | }, 68 | }}, 69 | RelaysAuth: selfhosted.NewRelayAuthenticator(relayAuth), 70 | 71 | Stores: control.NewFileStores(filepath.Join(cfg.dir, "control")), 72 | Logger: cfg.logger, 73 | }) 74 | if err != nil { 75 | return nil, fmt.Errorf("create control server: %w", err) 76 | } 77 | 78 | relay, err := relay.NewServer(relay.Config{ 79 | ControlAddr: relaysAddr, 80 | ControlHost: relaysTLSCert.Leaf.IPAddresses[0].String(), 81 | ControlToken: relayAuth.Token, 82 | ControlCAs: relaysCAs, 83 | 84 | Ingress: cfg.relayIngresses, 85 | 86 | Stores: relay.NewFileStores(filepath.Join(cfg.dir, "relay")), 87 | Logger: cfg.logger, 88 | }) 89 | if err != nil { 90 | return nil, fmt.Errorf("create relay server: %w", err) 91 | } 92 | 93 | return &Server{ 94 | serverConfig: *cfg, 95 | 96 | control: control, 97 | relay: relay, 98 | }, nil 99 | } 100 | 101 | func (s *Server) Run(ctx context.Context) error { 102 | g, ctx := errgroup.WithContext(ctx) 103 | g.Go(func() error { return s.control.Run(ctx) }) 104 | g.Go(func() error { return s.relay.Run(ctx) }) 105 | return g.Wait() 106 | } 107 | 108 | func (s *Server) Status(ctx context.Context) (ServerStatus, error) { 109 | control, err := s.control.Status(ctx) 110 | if err != nil { 111 | return ServerStatus{}, err 112 | } 113 | 114 | relay, err := s.relay.Status(ctx) 115 | if err != nil { 116 | return ServerStatus{}, err 117 | } 118 | 119 | return ServerStatus{control, relay}, nil 120 | } 121 | 122 | type ServerStatus struct { 123 | Control control.Status `json:"control"` 124 | Relay relay.Status `json:"relay"` 125 | } 126 | -------------------------------------------------------------------------------- /server_config.go: -------------------------------------------------------------------------------- 1 | package connet 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net" 7 | "os" 8 | 9 | "github.com/connet-dev/connet/control" 10 | "github.com/connet-dev/connet/model" 11 | "github.com/connet-dev/connet/relay" 12 | "github.com/connet-dev/connet/selfhosted" 13 | ) 14 | 15 | type serverConfig struct { 16 | clientsIngresses []control.Ingress 17 | clientsAuth control.ClientAuthenticator 18 | 19 | relayIngresses []relay.Ingress 20 | 21 | dir string 22 | logger *slog.Logger 23 | } 24 | 25 | func newServerConfig(opts []ServerOption) (*serverConfig, error) { 26 | cfg := &serverConfig{ 27 | logger: slog.Default(), 28 | } 29 | for _, opt := range opts { 30 | if err := opt(cfg); err != nil { 31 | return nil, err 32 | } 33 | } 34 | 35 | if len(cfg.clientsIngresses) == 0 { 36 | addr, err := net.ResolveUDPAddr("udp", ":19190") 37 | if err != nil { 38 | return nil, fmt.Errorf("resolve clients address: %w", err) 39 | } 40 | if err := ServerClientsIngress(control.Ingress{Addr: addr})(cfg); err != nil { 41 | return nil, fmt.Errorf("default clients address: %w", err) 42 | } 43 | } 44 | 45 | for i, ingress := range cfg.clientsIngresses { 46 | if ingress.TLS == nil { 47 | return nil, fmt.Errorf("ingress at %d is missing tls config", i) 48 | } 49 | } 50 | 51 | if len(cfg.relayIngresses) == 0 { 52 | addr, err := net.ResolveUDPAddr("udp", ":19191") 53 | if err != nil { 54 | return nil, fmt.Errorf("resolve clients relay address: %w", err) 55 | } 56 | hps := []model.HostPort{{Host: "localhost", Port: 19191}} 57 | if err := ServerRelayIngress(relay.Ingress{Addr: addr, Hostports: hps})(cfg); err != nil { 58 | return nil, fmt.Errorf("default clients relay address: %w", err) 59 | } 60 | } 61 | 62 | if cfg.dir == "" { 63 | if err := serverStoreDirTemp()(cfg); err != nil { 64 | return nil, fmt.Errorf("default store dir: %w", err) 65 | } 66 | cfg.logger.Info("using temporary store directory", "dir", cfg.dir) 67 | } 68 | 69 | return cfg, nil 70 | } 71 | 72 | type ServerOption func(*serverConfig) error 73 | 74 | func ServerClientsIngress(icfg control.Ingress) ServerOption { 75 | return func(cfg *serverConfig) error { 76 | cfg.clientsIngresses = append(cfg.clientsIngresses, icfg) 77 | 78 | return nil 79 | } 80 | } 81 | 82 | func ServerClientsTokens(tokens ...string) ServerOption { 83 | return func(cfg *serverConfig) error { 84 | auths := make([]selfhosted.ClientAuthentication, len(tokens)) 85 | for i, t := range tokens { 86 | auths[i] = selfhosted.ClientAuthentication{Token: t} 87 | } 88 | 89 | cfg.clientsAuth = selfhosted.NewClientAuthenticator(auths...) 90 | 91 | return nil 92 | } 93 | } 94 | 95 | func ServerClientsAuthenticator(clientsAuth control.ClientAuthenticator) ServerOption { 96 | return func(cfg *serverConfig) error { 97 | cfg.clientsAuth = clientsAuth 98 | 99 | return nil 100 | } 101 | } 102 | 103 | func ServerRelayIngress(icfg relay.Ingress) ServerOption { 104 | return func(cfg *serverConfig) error { 105 | cfg.relayIngresses = append(cfg.relayIngresses, icfg) 106 | 107 | return nil 108 | } 109 | } 110 | 111 | func ServerStoreDir(dir string) ServerOption { 112 | return func(cfg *serverConfig) error { 113 | cfg.dir = dir 114 | return nil 115 | } 116 | } 117 | 118 | func serverStoreDirTemp() ServerOption { 119 | return func(cfg *serverConfig) error { 120 | tmpDir, err := os.MkdirTemp("", "connet-server-") 121 | if err != nil { 122 | return fmt.Errorf("create /tmp dir: %w", err) 123 | } 124 | cfg.dir = tmpDir 125 | return nil 126 | } 127 | } 128 | 129 | func ServerLogger(logger *slog.Logger) ServerOption { 130 | return func(cfg *serverConfig) error { 131 | cfg.logger = logger 132 | return nil 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /statusc/server.go: -------------------------------------------------------------------------------- 1 | package statusc 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | func Run[T any](ctx context.Context, addr *net.TCPAddr, f func(ctx context.Context) (T, error)) error { 12 | srv := &http.Server{ 13 | Addr: addr.String(), 14 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | stat, err := f(r.Context()) 16 | if err == nil { 17 | w.Header().Add("Content-Type", "application/json") 18 | enc := json.NewEncoder(w) 19 | err = enc.Encode(stat) 20 | } 21 | if err != nil { 22 | w.WriteHeader(http.StatusInternalServerError) 23 | fmt.Fprintf(w, "server error: %v", err.Error()) 24 | } 25 | }), 26 | } 27 | 28 | go func() { 29 | <-ctx.Done() 30 | srv.Close() 31 | }() 32 | 33 | return srv.ListenAndServe() 34 | } 35 | -------------------------------------------------------------------------------- /statusc/status.go: -------------------------------------------------------------------------------- 1 | package statusc 2 | 3 | import "fmt" 4 | 5 | type Status struct{ string } 6 | 7 | var ( 8 | NotConnected = Status{"not_connected"} 9 | Connected = Status{"connected"} 10 | Reconnecting = Status{"reconnecting"} 11 | Disconnected = Status{"disconnected"} 12 | ) 13 | 14 | func (s Status) String() string { 15 | return s.string 16 | } 17 | 18 | func (s Status) MarshalText() ([]byte, error) { 19 | return []byte(s.string), nil 20 | } 21 | 22 | func (s *Status) UnmarshalText(b []byte) error { 23 | switch str := string(b); str { 24 | case NotConnected.string: 25 | *s = NotConnected 26 | case Connected.string: 27 | *s = Connected 28 | case Reconnecting.string: 29 | *s = Reconnecting 30 | case Disconnected.string: 31 | *s = Disconnected 32 | default: 33 | return fmt.Errorf("invalid status '%s'", s) 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /websocketc/join.go: -------------------------------------------------------------------------------- 1 | package websocketc 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/gorilla/websocket" 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | func Join(nc net.Conn, wc *websocket.Conn) error { 12 | var g errgroup.Group 13 | 14 | g.Go(func() error { 15 | defer nc.Close() 16 | for { 17 | _, data, err := wc.ReadMessage() 18 | if err != nil { 19 | return fmt.Errorf("websocked connection read: %w", err) 20 | } 21 | if _, err := nc.Write(data); err != nil { 22 | return fmt.Errorf("source connection write: %w", err) 23 | } 24 | } 25 | }) 26 | 27 | g.Go(func() error { 28 | defer wc.Close() 29 | var buf = make([]byte, 4096) 30 | for { 31 | n, err := nc.Read(buf) 32 | if err != nil { 33 | return fmt.Errorf("source connection read: %w", err) 34 | } 35 | if err := wc.WriteMessage(websocket.BinaryMessage, buf[0:n]); err != nil { 36 | return fmt.Errorf("websocked connection write: %w", err) 37 | } 38 | } 39 | }) 40 | 41 | return g.Wait() 42 | } 43 | --------------------------------------------------------------------------------