├── .github
├── .kodiak.toml
├── CODEOWNERS
└── workflows
│ ├── publish-latest.yml
│ ├── publish.yml
│ └── test.yml
├── LICENSE
├── README.md
├── client
├── auth.go
├── client.go
└── doc.go
├── cmd
├── reverst
│ ├── config.go
│ └── main.go
└── reverstd
│ └── main.go
├── dagger.json
├── dagger
├── .gitattributes
├── .gitignore
├── go.mod
├── go.sum
└── main.go
├── docs
├── PROTOCOL.md
├── diagram.d2
├── diagram.png
└── gopher-glasses.svg
├── examples
└── simple
│ ├── README.md
│ ├── config.yml
│ ├── group.yml
│ ├── server.crt
│ └── server.key
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── internal
├── auth
│ ├── auth.go
│ └── auth_test.go
├── config
│ ├── config.go
│ ├── fsnotify.go
│ ├── k8s.go
│ └── k8s_test.go
├── roundrobbin
│ ├── set.go
│ └── set_test.go
├── server
│ ├── metrics.go
│ └── server.go
├── synctyped
│ └── synctyped.go
└── test
│ └── integration_test.go
└── pkg
└── protocol
├── protocol.go
└── responsecode_string.go
/.github/.kodiak.toml:
--------------------------------------------------------------------------------
1 | # .kodiak.toml
2 | version = 1
3 |
4 | [approve]
5 | auto_approve_usernames = ["dependabot"]
6 |
7 | [update]
8 | always = true
9 |
10 | [merge]
11 | method = "squash"
12 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in
2 | # the repo. Unless a later match takes precedence,
3 | # @global-owner1 and @global-owner2 will be requested for
4 | # review when someone opens a pull request.
5 | * @flipt-io/maintainers
--------------------------------------------------------------------------------
/.github/workflows/publish-latest.yml:
--------------------------------------------------------------------------------
1 | name: 'Publish Latest'
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 |
8 | # limit concurrency of workflow to one run at a time
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 |
12 | jobs:
13 | publish-image:
14 | runs-on: ubuntu-latest
15 | permissions:
16 | contents: read
17 | packages: write
18 | steps:
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v3
21 |
22 | - name: Login to GitHub Container Registry
23 | uses: docker/login-action@v3
24 | with:
25 | registry: ghcr.io
26 | username: ${{ github.repository_owner }}
27 | password: ${{ secrets.GITHUB_TOKEN }}
28 |
29 | - name: Tag current SHA being pushed to main as latest
30 | run: |
31 | docker buildx imagetools create \
32 | --tag ghcr.io/${{ github.repository }}:latest \
33 | ghcr.io/${{ github.repository }}:${{ github.sha }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: 'Publish'
2 |
3 | on:
4 | workflow_dispatch:
5 | merge_group:
6 |
7 | jobs:
8 | publish-image:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | packages: write
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 | - name: Call Dagger Function
17 | uses: dagger/dagger-for-github@v5
18 | with:
19 | version: "0.11.6"
20 | verb: call
21 | args: publish --source . --password env:GITHUB_TOKEN --tag ${{ github.sha }}
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: 'Test'
2 |
3 | on:
4 | pull_request:
5 | merge_group:
6 |
7 | jobs:
8 | unit:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 | - name: Call Dagger Function
14 | uses: dagger/dagger-for-github@v5
15 | with:
16 | version: "0.11.6"
17 | verb: call
18 | args: test-unit --source .
19 |
20 | integration:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v4
25 | - name: Call Dagger Function
26 | uses: dagger/dagger-for-github@v5
27 | with:
28 | version: "0.11.6"
29 | verb: call
30 | args: test-integration --source .
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | reverst: HTTP reverse tunnels over QUIC
2 | ---------------------------------------
3 |
4 |
5 |
6 |
7 |
8 | > Ti esrever dna ti pilf nwod gnaht ym tup i
9 |
10 | Reverst is a (load-balanced) reverse-tunnel server and Go server-client library built on QUIC and HTTP/3.
11 |
12 | - Go Powered: Written in Go using [quic-go](https://github.com/quic-go/quic-go)
13 | - Compatible: The Go `client` package is built on `net/http` standard-library abstractions
14 | - Load-balanced: Run multiple instances of your services behind the same tunnel
15 | - Performant: Built on top of QUIC and HTTP/3
16 |
17 | ## Use-case
18 |
19 | Reverst is for exposing services on the public internet from within restrictive networks (e.g. behind NAT gateways).
20 | The tunnel binary is intended to be deployed on the public internet.
21 | Client servers then dial out to the tunnels and register themselves on target tunnel groups.
22 | A tunnel group is a load-balanced set of client-servers, which is exposed through the reverst tunnel HTTP interface.
23 |
24 | ## Client
25 |
26 | [](https://pkg.go.dev/go.flipt.io/reverst/client)
27 |
28 | The following section refers to the Go tunnel client code.
29 | This can be added as a dependency to any Go code that requires exposing through a `reverstd` tunnel server.
30 |
31 | ### Install
32 |
33 | ```console
34 | go get go.flipt.io/reverst/client
35 | ```
36 |
37 | ### Building
38 |
39 | ```console
40 | go install ./client/...
41 | ```
42 |
43 | ## Server and CLI
44 |
45 | ### Building
46 |
47 | The following builds both `reverstd` (tunnel server) and `reverst` (tunnel cli client).
48 |
49 | ```console
50 | go install ./cmd/...
51 | ```
52 |
53 | ### Testing
54 |
55 | Reverst uses Dagger to setup and run an integration test suite.
56 |
57 | #### Unit
58 |
59 | ```console
60 | dagger call testUnit --source=.
61 | ```
62 |
63 | #### Integration
64 |
65 | ```console
66 | dagger call testIntegration --source=.
67 | ```
68 |
69 | The test suite sets up a tunnel, registers a server-client to the tunnel and then requests the service through the tunnels HTTP interface.
70 |
71 | ### Examples
72 |
73 | Head over to the [examples](./examples) directory for some walkthroughs running `reverstd` and `reverst`.
74 |
75 | ### Usage and Configuration
76 |
77 | #### Command-Line Flags and Environment Variables
78 |
79 | The following flags can be used to configure a running instance of the `reverst` server.
80 |
81 | ```console
82 | ➜ reverstd -h
83 | COMMAND
84 | reverstd
85 |
86 | USAGE
87 | reverstd [FLAGS]
88 |
89 | FLAGS
90 | -l, --log LEVEL debug, info, warn or error (default: INFO)
91 | -a, --tunnel-address STRING address for accepting tunnelling quic connections (default: 127.0.0.1:7171)
92 | -s, --http-address STRING address for serving HTTP requests (default: 0.0.0.0:8181)
93 | -n, --server-name STRING server name used to identify tunnel via TLS (required)
94 | -k, --private-key-path STRING path to TLS private key PEM file (required)
95 | -c, --certificate-path STRING path to TLS certificate PEM file (required)
96 | -g, --tunnel-groups STRING path to file or k8s configmap identifier (default: groups.yml)
97 | -w, --watch-groups watch tunnel groups sources for updates
98 | --management-address STRING HTTP address for management API
99 | --max-idle-timeout DURATION maximum time a connection can be idle (default: 1m0s)
100 | --keep-alive-period DURATION period between keep-alive events (default: 30s)
101 | ```
102 |
103 | The long form names of each flag can also be referenced as environment variable names.
104 | To do so, prefix them with `REVERST_`, replace each `-` with `_` and uppercase the letters.
105 |
106 | For example, `--tunnel-address` becomes `REVERST_TUNNEL_ADDRESS`.
107 |
108 | #### Tunnel Groups Configuration YAML
109 |
110 | **configuring**
111 |
112 | Currently, the tunnel groups configuration can be sourced from two different locations types (`file` and `k8s`).
113 | Both tunnel group sources support watching sources for changes over time (see `-w` flag).
114 |
115 | - Local filesystem (`file://[path]`)
116 |
117 | The standard and simplest method is to point `reverstd` at your configuration YAML file on your machine via its path.
118 |
119 | ```console
120 | reverstd -g path/to/configuration.yml
121 | // alternatively:
122 | reverstd -g file:///path/to/configuration.yml
123 | ```
124 |
125 | - Kubernetes ConfigMap `k8s://configmap/[namespace]/[name]/[key]`
126 |
127 | Alternatively, you can configure reverst to connect to a Kubernetes API server and fetch / watch configuration from.
128 |
129 | ```console
130 | reverstd -g k8s://configmap/default/tunnelconfig/groups.yml
131 | ```
132 |
133 | **defining**
134 |
135 | The `reverstd` server take a path to a YAML encoded file, which identifies the tunnel groups to be hosted.
136 | A tunnel group is a load-balancer on which tunneled servers can register themselves.
137 | The file contains a top-level key groups, under which each tunnel group is uniquely named.
138 |
139 | ```yaml
140 | groups:
141 | "group-name":
142 | hosts:
143 | - "some.host.address.dev" # Host for routing inbound HTTP requests to tunnel group
144 | authentication:
145 | basic:
146 | username: "user"
147 | password: "pass"
148 | ```
149 |
150 | Each group body contains import details for configuring the tunnel groups.
151 |
152 | **hosts**
153 |
154 | This is an array of strings which is used in routing HTTP requests to the tunnel group when one of the hostnames matches.
155 |
156 | **authentication**
157 |
158 | This identifies how to authenticate new tunnels attempting to register with the group.
159 | Multiple authentication strategies can be enabled at once.
160 | The following types are supported:
161 |
162 | - `basic` supports username and password authentication (default scheme `Basic`)
163 | - `bearer` supports static token based matching (default scheme `Bearer`)
164 | - `external` supports offloading authentication and authorization to an external service (default scheme `Bearer`)
165 |
166 | > [!Note]
167 | > If enabling both `bearer` and `external` you will need to override one of their schemes to distinguish them.
168 |
169 |
170 |
171 | Example configuration with multiple authentication strategies
172 |
173 | The following contains all three strategies (basic, bearer and external) enabled at once with different schemes:
174 |
175 | ```yaml
176 | groups:
177 | "group-name":
178 | hosts:
179 | - "some.host.address.dev" # Host for routing inbound HTTP requests to tunnel group
180 | authentication:
181 | basic:
182 | username: "user"
183 | password: "pass"
184 | bearer:
185 | token: "some-token"
186 | external:
187 | scheme: "JWT"
188 | endpoint: "http://some-external-endpoint/auth/ext"
189 | ```
190 |
191 |
192 |
193 | If no strategies are supplied then authentication is disabled (strongly discouraged).
194 |
--------------------------------------------------------------------------------
/client/auth.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "fmt"
7 | "log/slog"
8 |
9 | "go.flipt.io/reverst/pkg/protocol"
10 | )
11 |
12 | const authorizationMetadataKey = "Authorization"
13 |
14 | // Authenticator is a type which adds authentication credentials to an outbound
15 | // register listener request.
16 | // It is called before the request is serialized and written to the stream.
17 | type Authenticator interface {
18 | Authenticate(context.Context, *protocol.RegisterListenerRequest) error
19 | }
20 |
21 | // AuthenticatorFunc is a function which implements the Authenticator interface
22 | type AuthenticatorFunc func(context.Context, *protocol.RegisterListenerRequest) error
23 |
24 | // Authenticate delegates to the underlying AuthenticatorFunc
25 | func (a AuthenticatorFunc) Authenticate(ctx context.Context, r *protocol.RegisterListenerRequest) error {
26 | return a(ctx, r)
27 | }
28 |
29 | var defaultAuthenticator Authenticator = AuthenticatorFunc(func(ctx context.Context, rlr *protocol.RegisterListenerRequest) error {
30 | slog.Warn("No authenticator provided, attempting to register connection without credentials")
31 | return nil
32 | })
33 |
34 | type AuthenticatorOptions struct {
35 | scheme string
36 | }
37 |
38 | type AuthorizationOption func(*AuthenticatorOptions)
39 |
40 | func WithScheme(scheme string) AuthorizationOption {
41 | return func(ao *AuthenticatorOptions) {
42 | ao.scheme = scheme
43 | }
44 | }
45 |
46 | // BasicAuthenticator returns an instance of Authenticator which configures Basic authentication
47 | // on requests passed to Authenticate using the provided username and password
48 | func BasicAuthenticator(username, password string, opts ...AuthorizationOption) Authenticator {
49 | options := AuthenticatorOptions{scheme: "Basic"}
50 | for _, opt := range opts {
51 | opt(&options)
52 | }
53 |
54 | return AuthenticatorFunc(func(ctx context.Context, rlr *protocol.RegisterListenerRequest) error {
55 | if rlr.Metadata == nil {
56 | rlr.Metadata = map[string]string{}
57 | }
58 |
59 | rlr.Metadata[authorizationMetadataKey] = fmt.Sprintf("%s %s",
60 | options.scheme,
61 | base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))),
62 | )
63 |
64 | return nil
65 | })
66 | }
67 |
68 | // BearerAuthenticator returns an instance of Authenticator which configures Bearer authentication
69 | // on requests passed to Authenticate using the provided token string
70 | func BearerAuthenticator(token string, opts ...AuthorizationOption) Authenticator {
71 | options := AuthenticatorOptions{scheme: "Bearer"}
72 | for _, opt := range opts {
73 | opt(&options)
74 | }
75 |
76 | return AuthenticatorFunc(func(ctx context.Context, rlr *protocol.RegisterListenerRequest) error {
77 | if rlr.Metadata == nil {
78 | rlr.Metadata = map[string]string{}
79 | }
80 |
81 | rlr.Metadata[authorizationMetadataKey] = fmt.Sprintf("%s %s", options.scheme, token)
82 |
83 | return nil
84 | })
85 | }
86 |
--------------------------------------------------------------------------------
/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "log/slog"
10 | "net"
11 | "net/http"
12 | "net/url"
13 | "time"
14 |
15 | "github.com/quic-go/quic-go"
16 | "github.com/quic-go/quic-go/http3"
17 | "go.flipt.io/reverst/pkg/protocol"
18 | "k8s.io/apimachinery/pkg/util/wait"
19 | )
20 |
21 | var (
22 | // DefaultTLSConfig is the default configuration used for establishing
23 | // TLS over QUIC.
24 | DefaultTLSConfig = &tls.Config{
25 | NextProtos: []string{protocol.Name},
26 | }
27 | // DefaultQuicConfig is the default configuration used for establishing
28 | // QUIC connections.
29 | DefaultQuicConfig = &quic.Config{
30 | MaxIdleTimeout: 20 * time.Second,
31 | KeepAlivePeriod: 10 * time.Second,
32 | }
33 |
34 | // DefaultBackoff is the default backoff used when dialing and serving
35 | // a connection.
36 | DefaultBackoff = wait.Backoff{
37 | Steps: 5,
38 | Duration: 100 * time.Millisecond,
39 | Factor: 2.0,
40 | Jitter: 0.1,
41 | }
42 |
43 | // ErrNotFound is returned when a tunnel group is referenced that the
44 | // target reverst tunnel server does not know (CodeNotFound)
45 | ErrNotFound = errors.New("not found")
46 | // ErrBadRequest is returned when a tunnel registration request is rejected
47 | // due to an unexpected request payload (CodeBadRequest)
48 | ErrBadRequest = errors.New("bad request")
49 | // ErrUnauthorized is returned when the caller is not properly authenticated to
50 | // establish a tunnel on the request tunnel group (CodeUnauthorized)
51 | ErrUnauthorized = errors.New("unauthorized")
52 | // ErrServerError is returned when something unexplained went wrong on the
53 | // remote reverst tunnel server (CodeServerError)
54 | ErrServerError = errors.New("server error")
55 | )
56 |
57 | // Server is an alternative HTTP server that dials to a reverst Tunnel server
58 | // and attempts to remotely register itself as a listener.
59 | // Given the connection is established and authorized as a valid listener, the
60 | // server switches into serving mode and handles HTTP/3 requests over the connection.
61 | // The Tunnel should forward requests to this connection and any others in the
62 | // same tunnel group. The group is identified via the TLSConfig.ServerName.
63 | type Server struct {
64 | // TunnelGroup is an identifier for the group in which this server should
65 | // be registered against on the target tunnel server.
66 | TunnelGroup string
67 |
68 | // Handler is the root http.Handler of the server instance.
69 | Handler http.Handler
70 |
71 | // Logger allows the caller to configure a custome *slog.Logger instance.
72 | // If not defined then Server uses the default instance returned by slog.Default.
73 | Logger *slog.Logger
74 |
75 | // TLSConfig is used to configure TLS encryption over the Quic connection.
76 | // See DefaultTLSConfig for the parameters used which this is set to nil.
77 | TLSConfig *tls.Config
78 |
79 | // QuicConfig is used to configure Quic connections.
80 | // See DefaultQuicConfig for the parameters used which this is set to nil.
81 | QuicConfig *quic.Config
82 |
83 | // Authenticator is the Authenticator used to authenticate outbound
84 | // listener registration requests.
85 | Authenticator Authenticator
86 |
87 | // OnConnectionReady is called when the server has successfully
88 | // registered itself with the upstream tunnel server
89 | OnConnectionReady func(protocol.RegisterListenerResponse)
90 | }
91 |
92 | func coallesce[T any](v, d *T) *T {
93 | if v == nil {
94 | return d
95 | }
96 |
97 | return v
98 | }
99 |
100 | func (s *Server) getTLSConfig(addr string) (*tls.Config, error) {
101 | tlsConf := coallesce(s.TLSConfig, DefaultTLSConfig)
102 | if tlsConf.ServerName == "" {
103 | // if the TLS ServerName is not explicitly supplied
104 | // then we will parse the dial address and use the hostname
105 | // defined on that instead
106 | url, err := url.Parse(addr)
107 | if err != nil {
108 | return nil, err
109 | }
110 |
111 | tlsConf.ServerName = url.Hostname()
112 | }
113 |
114 | return tlsConf, nil
115 | }
116 |
117 | // DialAndServe dials out to the provided address and attempts to register the server
118 | // as a listener on the remote tunnel group.
119 | func (s *Server) DialAndServe(ctx context.Context, addr string) (err error) {
120 | attrs := []slog.Attr{slog.String("addr", addr)}
121 | if host, port, err := net.SplitHostPort(addr); err == nil {
122 | attrs = []slog.Attr{slog.String("host", host), slog.String("port", port)}
123 | }
124 |
125 | log := slog.New(coallesce(s.Logger, slog.Default()).Handler().WithAttrs(attrs))
126 | log.Debug("Dialing tunnel")
127 |
128 | var lastErr error
129 | err = wait.ExponentialBackoffWithContext(ctx, DefaultBackoff, func(context.Context) (done bool, err error) {
130 | err = s.dialAndServe(ctx, log, addr)
131 | if err != nil {
132 | lastErr = err
133 | if errors.Is(err, context.Canceled) {
134 | return false, nil
135 | }
136 |
137 | // these errors are considered non-recoverable
138 | // not-found is considered recoverable under the situation that the
139 | // tunnel has recently been requested for provisioning and is coming online
140 | if errors.Is(err, ErrUnauthorized) ||
141 | errors.Is(err, ErrBadRequest) {
142 | return false, err
143 | }
144 |
145 | // we log out the error under debug as this function will be repeated
146 | // and hopefully will eventually succeed
147 | // if not then the last observed error should be returned and logged
148 | // at a higher log level
149 | log.Debug("Error while attempting to dial and register", "error", err)
150 |
151 | return false, nil
152 | }
153 |
154 | return true, nil
155 | })
156 |
157 | // this signifies that the exponential backoff was exhausted or exceeded a deadline
158 | // in this situation we simply return the last observed error in the dial and serve attempts
159 | if !errors.Is(err, context.Canceled) && wait.Interrupted(err) {
160 | err = lastErr
161 | }
162 |
163 | return err
164 | }
165 |
166 | func (s *Server) dialAndServe(
167 | ctx context.Context,
168 | log *slog.Logger,
169 | addr string,
170 | ) (err error) {
171 | defer func() {
172 | if err != nil {
173 | err = fmt.Errorf("dialing and registering connection: %w", err)
174 | }
175 | }()
176 |
177 | tlsConf, err := s.getTLSConfig(addr)
178 | if err != nil {
179 | return err
180 | }
181 |
182 | conn, err := quic.DialAddr(ctx,
183 | addr,
184 | tlsConf,
185 | coallesce(s.QuicConfig, DefaultQuicConfig),
186 | )
187 | if err != nil {
188 | return err
189 | }
190 |
191 | go func() {
192 | <-ctx.Done()
193 |
194 | _ = conn.CloseWithError(protocol.ApplicationOK, "")
195 | }()
196 |
197 | log.Debug("Attempting to register")
198 |
199 | for {
200 | // register server as a listener on remote tunnel
201 | err := s.register(conn)
202 | if err == nil {
203 | break
204 | }
205 |
206 | // in the event a stream was closed unexpectedly while handling
207 | // registration then the likelihood is that the server went away
208 | // and will ultimately close the connection with the reason
209 | // expressed in the application error code and message
210 | // so we instead re-attempt registration in this scenario and
211 | // let it fail attempting to open another stream with the
212 | // connections application error instead
213 | if errors.Is(err, io.ErrUnexpectedEOF) {
214 | continue
215 | }
216 |
217 | message := "unexpected error"
218 | var aerr *quic.ApplicationError
219 | if errors.As(err, &aerr) {
220 | message = aerr.ErrorMessage
221 | switch aerr.ErrorCode {
222 | case protocol.ApplicationError:
223 | err = ErrServerError
224 | case protocol.ApplicationClientError:
225 | message = "client error"
226 | switch aerr.ErrorMessage {
227 | case "unauthorized":
228 | err = ErrUnauthorized
229 | case "not found":
230 | err = ErrNotFound
231 | case "bad request":
232 | err = ErrBadRequest
233 | }
234 | }
235 | }
236 |
237 | return fmt.Errorf("%s: %w", message, err)
238 | }
239 |
240 | log.Info("Starting reverse server")
241 |
242 | return (&http3.Server{Handler: s.Handler}).ServeQUICConn(conn)
243 | }
244 |
245 | func (s *Server) register(conn quic.Connection) error {
246 | stream, err := conn.OpenStream()
247 | if err != nil {
248 | return fmt.Errorf("opening stream: %w", err)
249 | }
250 |
251 | defer stream.Close()
252 |
253 | enc := protocol.NewEncoder[protocol.RegisterListenerRequest](stream)
254 | defer enc.Close()
255 |
256 | req := &protocol.RegisterListenerRequest{
257 | Version: protocol.Version,
258 | TunnelGroup: s.TunnelGroup,
259 | }
260 |
261 | auth := defaultAuthenticator
262 | if s.Authenticator != nil {
263 | auth = s.Authenticator
264 | }
265 |
266 | if err := auth.Authenticate(stream.Context(), req); err != nil {
267 | return fmt.Errorf("registering new connection: %w", err)
268 | }
269 |
270 | if err := enc.Encode(req); err != nil {
271 | return fmt.Errorf("encoding register listener request: %w", err)
272 | }
273 |
274 | dec := protocol.NewDecoder[protocol.RegisterListenerResponse](stream)
275 | defer dec.Close()
276 |
277 | resp, err := dec.Decode()
278 | if err != nil {
279 | // EOF is not expected at this point, so we adapt it
280 | // The calling code is expected to introspect into
281 | // the state of the connection at this point
282 | if errors.Is(err, io.EOF) {
283 | err = io.ErrUnexpectedEOF
284 | }
285 |
286 | return fmt.Errorf("decoding register listener response: %w", err)
287 | }
288 |
289 | if s.OnConnectionReady != nil {
290 | s.OnConnectionReady(resp)
291 | }
292 |
293 | return nil
294 | }
295 |
--------------------------------------------------------------------------------
/client/doc.go:
--------------------------------------------------------------------------------
1 | // package client
2 | //
3 | // The client package contains the client-side types for interfacing with reverst tunnels.
4 | // The client itself is a http Server implementation that dials out to a tunnel server, performs
5 | // a handshake to identify and authenticate the relevant tunnel group to register with, and then
6 | // it switches roles into that of the server.
7 | //
8 | // # Example
9 | //
10 | // package main
11 | //
12 | // import (
13 | // "context"
14 | // "crypto/tls"
15 | // "net/http"
16 | //
17 | // "go.flipt.io/reverst/client"
18 | // )
19 | //
20 | // func main() {
21 | // server := &client.Server {
22 | // TunnelGroup: "some-group",
23 | // Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request {
24 | // w.Write([]byte("Hello, World!"))
25 | // })),
26 | // TLSConfig: &tls.Config{InsecureSkipVerify: true}
27 | // }
28 | //
29 | // server.DialAndServe(ctx, "some.reverst.tunnel:8443")
30 | // }
31 | package client
32 |
--------------------------------------------------------------------------------
/cmd/reverst/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "time"
6 |
7 | "go.flipt.io/reverst/internal/config"
8 | )
9 |
10 | type Config struct {
11 | Level config.Level `ff:" short=l | long=log | default=info | usage: 'debug, info, warn or error' "`
12 | TunnelAddress string `ff:" short=a | long=tunnel-address | default='127.0.0.1:7171' | usage: address for accepting tunnelling quic connections "`
13 | ServerName string `ff:" short=n | long=server-name | usage: server name used to identify tunnel via TLS (required) "`
14 | TunnelGroup string `ff:" short=g | long=tunnel-group | usage: tunnel group to join (defaults to server name) "`
15 | CACertificatePath string `ff:" short=c | long=cacert-path | usage: path to TLS CA certificate PEM file "`
16 | InsecureSkipVerify bool `ff:" | long=insecure | default=false | usage: skip TLS certficate verification "`
17 |
18 | Username string `ff:" long=username | usage: username for basic authentication "`
19 | Password string `ff:" long=password | usage: password for basic authentication "`
20 | Token string `ff:" long=token | usage: token for bearer authentication "`
21 | Scheme string `ff:" long=scheme | usage: optionally override auth scheme "`
22 |
23 | MaxIdleTimeout time.Duration `ff:" long=max-idle-timeout | default=1m | usage: maximum time a connection can be idle "`
24 | KeepAlivePeriod time.Duration `ff:" long=keep-alive-period | default=30s | usage: period between keep-alive events "`
25 | }
26 |
27 | func (c Config) Validate() error {
28 | if c.ServerName == "" {
29 | return errors.New("server-name must be non-empty string")
30 | }
31 |
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/cmd/reverst/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "crypto/x509"
7 | "errors"
8 | "fmt"
9 | "log/slog"
10 | "net/http"
11 | "net/http/httputil"
12 | "net/url"
13 | "os"
14 | "os/signal"
15 | "strings"
16 | "syscall"
17 |
18 | "github.com/peterbourgon/ff/v4"
19 | "github.com/peterbourgon/ff/v4/ffhelp"
20 | "github.com/peterbourgon/ff/v4/ffyaml"
21 | "github.com/quic-go/quic-go"
22 | "go.flipt.io/reverst/client"
23 | "go.flipt.io/reverst/pkg/protocol"
24 | )
25 |
26 | func main() {
27 | flags := ff.NewFlagSet("reverst")
28 | _ = flags.StringLong("config", "config.yml", "path to config file")
29 |
30 | var conf Config
31 | if err := flags.AddStruct(&conf); err != nil {
32 | panic(err)
33 | }
34 |
35 | httpcmd := &ff.Command{
36 | Name: "http",
37 | Usage: "http [FLAGS] [ADDR]",
38 | Flags: flags,
39 | Exec: func(ctx context.Context, args []string) error {
40 | logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
41 | Level: slog.Level(conf.Level),
42 | }))
43 |
44 | slog.SetDefault(logger)
45 |
46 | if err := conf.Validate(); err != nil {
47 | return err
48 | }
49 |
50 | addr := args[0]
51 | if !strings.Contains(addr, ":") {
52 | addr = fmt.Sprintf("http://0.0.0.0:%s", addr)
53 | }
54 |
55 | targetURL, err := url.Parse(addr)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | tlsConf := &tls.Config{
61 | MinVersion: tls.VersionTLS13,
62 | NextProtos: []string{protocol.Name},
63 | ServerName: conf.ServerName,
64 | InsecureSkipVerify: conf.InsecureSkipVerify,
65 | }
66 |
67 | tlsConf.RootCAs, _ = x509.SystemCertPool()
68 | if tlsConf.RootCAs == nil {
69 | tlsConf.RootCAs = x509.NewCertPool()
70 | }
71 |
72 | if conf.CACertificatePath != "" {
73 | caCertRaw, err := os.ReadFile(conf.CACertificatePath)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | if !tlsConf.RootCAs.AppendCertsFromPEM(caCertRaw) {
79 | return fmt.Errorf("failed to append cert at path: %q", conf.CACertificatePath)
80 | }
81 | }
82 |
83 | tunnelGroup := conf.TunnelGroup
84 | if tunnelGroup == "" {
85 | tunnelGroup = conf.ServerName
86 | }
87 |
88 | logger = logger.With(
89 | "tunnel_group", tunnelGroup,
90 | "server_name", conf.ServerName,
91 | "target", targetURL,
92 | )
93 |
94 | var (
95 | auth client.Authenticator
96 | scheme = conf.Scheme
97 | )
98 | if conf.Username != "" {
99 | if scheme == "" {
100 | scheme = "Basic"
101 | }
102 |
103 | auth = client.BasicAuthenticator(
104 | conf.Username,
105 | conf.Password,
106 | client.WithScheme(scheme),
107 | )
108 | } else if conf.Token != "" {
109 | if scheme == "" {
110 | scheme = "Bearer"
111 | }
112 |
113 | auth = client.BearerAuthenticator(conf.Token, client.WithScheme(scheme))
114 | }
115 |
116 | return (&client.Server{
117 | TunnelGroup: tunnelGroup,
118 | Handler: httputil.NewSingleHostReverseProxy(targetURL),
119 | Logger: logger,
120 | Authenticator: auth,
121 | TLSConfig: tlsConf,
122 | QuicConfig: &quic.Config{
123 | MaxIdleTimeout: conf.MaxIdleTimeout,
124 | KeepAlivePeriod: conf.KeepAlivePeriod,
125 | },
126 | }).DialAndServe(ctx, conf.TunnelAddress)
127 | },
128 | }
129 |
130 | cmd := &ff.Command{
131 | Name: "reverst",
132 | Usage: "reverst [FLAGS] [COMMAND]",
133 | Subcommands: []*ff.Command{
134 | httpcmd,
135 | },
136 | }
137 |
138 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
139 | go func() {
140 | <-ctx.Done()
141 | stop()
142 | }()
143 |
144 | if err := cmd.ParseAndRun(ctx, os.Args[1:],
145 | ff.WithEnvVarPrefix("REVERST"),
146 | ff.WithConfigFileFlag("config"),
147 | ff.WithConfigFileParser(ffyaml.Parse),
148 | ff.WithConfigAllowMissingFile(),
149 | ); err != nil {
150 | if errors.Is(err, http.ErrServerClosed) {
151 | return
152 | }
153 |
154 | fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Command(cmd))
155 | if !errors.Is(err, ff.ErrHelp) {
156 | fmt.Fprintf(os.Stderr, "error: %v\n", err)
157 | }
158 |
159 | os.Exit(1)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/cmd/reverstd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log/slog"
8 | "net/http"
9 | "os"
10 | "os/signal"
11 | "syscall"
12 |
13 | "github.com/peterbourgon/ff/v4"
14 | "github.com/peterbourgon/ff/v4/ffhelp"
15 | "go.flipt.io/reverst/internal/config"
16 | "go.flipt.io/reverst/internal/server"
17 | )
18 |
19 | func main() {
20 | flags := ff.NewFlagSet("reverstd")
21 |
22 | var conf config.Config
23 | if err := flags.AddStruct(&conf); err != nil {
24 | panic(err)
25 | }
26 |
27 | cmd := &ff.Command{
28 | Name: "reverstd",
29 | Usage: "reverstd [FLAGS]",
30 | Flags: flags,
31 | Exec: func(ctx context.Context, args []string) error {
32 | slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
33 | Level: slog.Level(conf.Level),
34 | })))
35 |
36 | if err := conf.Validate(); err != nil {
37 | return err
38 | }
39 |
40 | // start a subscription for tunnel group configuration
41 | // this function should push at-least one tunnel groups
42 | // instance on the channel before returning a non-nil error
43 | groupsChan := make(chan *config.TunnelGroups, 1)
44 | if err := conf.SubscribeTunnelGroups(ctx, groupsChan); err != nil {
45 | return err
46 | }
47 |
48 | server, err := server.New(conf, groupsChan)
49 | if err != nil {
50 | return err
51 | }
52 |
53 | return server.ListenAndServe(ctx)
54 | },
55 | }
56 |
57 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
58 | go func() {
59 | <-ctx.Done()
60 | stop()
61 | }()
62 |
63 | if err := cmd.ParseAndRun(ctx, os.Args[1:],
64 | ff.WithEnvVarPrefix("REVERST"),
65 | ); err != nil {
66 | if errors.Is(err, http.ErrServerClosed) {
67 | return
68 | }
69 |
70 | fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Command(cmd))
71 | if !errors.Is(err, ff.ErrHelp) {
72 | fmt.Fprintf(os.Stderr, "error: %v\n", err)
73 | }
74 |
75 | os.Exit(1)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/dagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reverst",
3 | "sdk": "go",
4 | "dependencies": [
5 | {
6 | "name": "go",
7 | "source": "github.com/vito/daggerverse/go@f7223d2d82fb91622cbb7954177388d995a98d59"
8 | }
9 | ],
10 | "source": "dagger",
11 | "engineVersion": "v0.11.6"
12 | }
13 |
--------------------------------------------------------------------------------
/dagger/.gitattributes:
--------------------------------------------------------------------------------
1 | /dagger.gen.go linguist-generated
2 | /internal/dagger/** linguist-generated
3 | /internal/querybuilder/** linguist-generated
4 | /internal/telemetry/** linguist-generated
5 |
--------------------------------------------------------------------------------
/dagger/.gitignore:
--------------------------------------------------------------------------------
1 | /dagger.gen.go
2 | /internal/dagger
3 | /internal/querybuilder
4 | /internal/telemetry
5 |
--------------------------------------------------------------------------------
/dagger/go.mod:
--------------------------------------------------------------------------------
1 | module dagger/reverst
2 |
3 | go 1.21.7
4 |
5 | require (
6 | github.com/99designs/gqlgen v0.17.44
7 | github.com/Khan/genqlient v0.7.0
8 | github.com/vektah/gqlparser/v2 v2.5.11
9 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
10 | golang.org/x/sync v0.7.0
11 | )
12 |
13 | require (
14 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect
15 | github.com/go-logr/logr v1.4.1 // indirect
16 | github.com/go-logr/stdr v1.2.2 // indirect
17 | github.com/google/uuid v1.6.0 // indirect
18 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
19 | github.com/sosodev/duration v1.2.0 // indirect
20 | go.opentelemetry.io/otel v1.26.0
21 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
22 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0
23 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0
24 | go.opentelemetry.io/otel/metric v1.26.0 // indirect
25 | go.opentelemetry.io/otel/sdk v1.26.0
26 | go.opentelemetry.io/otel/trace v1.26.0
27 | go.opentelemetry.io/proto/otlp v1.2.0 // indirect
28 | golang.org/x/net v0.23.0 // indirect
29 | golang.org/x/sys v0.19.0 // indirect
30 | golang.org/x/text v0.14.0 // indirect
31 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
32 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
33 | google.golang.org/grpc v1.63.2
34 | google.golang.org/protobuf v1.33.0 // indirect
35 | )
36 |
37 | replace go.flipt.io/reverst => ../
38 |
--------------------------------------------------------------------------------
/dagger/go.sum:
--------------------------------------------------------------------------------
1 | github.com/99designs/gqlgen v0.17.44 h1:OS2wLk/67Y+vXM75XHbwRnNYJcbuJd4OBL76RX3NQQA=
2 | github.com/99designs/gqlgen v0.17.44/go.mod h1:UTCu3xpK2mLI5qcMNw+HKDiEL77it/1XtAjisC4sLwM=
3 | github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w=
4 | github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM=
5 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
6 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
7 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
8 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
12 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
13 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
14 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
15 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
16 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
17 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
18 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
19 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
20 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
21 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
25 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
26 | github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
27 | github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
28 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
29 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
30 | github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
31 | github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
32 | go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
33 | go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
34 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
35 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
36 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8=
37 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo=
38 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI=
39 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38=
40 | go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
41 | go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
42 | go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
43 | go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
44 | go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
45 | go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
46 | go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
47 | go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
48 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
49 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
50 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
51 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
52 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
53 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
54 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
55 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
56 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
57 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
58 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
59 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
60 | google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
61 | google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
62 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0=
63 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
64 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
65 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
66 | google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
67 | google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
68 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
69 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
72 |
--------------------------------------------------------------------------------
/dagger/main.go:
--------------------------------------------------------------------------------
1 | // A generated module for Reverst functions
2 | //
3 | // This module has been generated via dagger init and serves as a reference to
4 | // basic module structure as you get started with Dagger.
5 | //
6 | // Two functions have been pre-created. You can modify, delete, or add to them,
7 | // as needed. They demonstrate usage of arguments and return types using simple
8 | // echo and grep commands. The functions can be called from the dagger CLI or
9 | // from one of the SDKs.
10 | //
11 | // The first line in this comment block is a short description line and the
12 | // rest is a long description with more detail on the module's purpose or usage,
13 | // if appropriate. All modules should have a short description.
14 |
15 | package main
16 |
17 | import (
18 | "context"
19 | "crypto/rand"
20 | "crypto/rsa"
21 | "crypto/x509"
22 | "crypto/x509/pkix"
23 | "dagger/reverst/internal/dagger"
24 | "encoding/pem"
25 | "fmt"
26 | "math/big"
27 | "time"
28 | )
29 |
30 | const (
31 | goBuildCachePath = "/root/.cache/go-build"
32 | goModCachePath = "/go/pkg/mod"
33 | )
34 |
35 | type Reverst struct{}
36 |
37 | // Returns a built container with reverst on the path
38 | func (m *Reverst) BuildContainer(
39 | ctx context.Context,
40 | source *dagger.Directory,
41 | ) (*Container, error) {
42 | build := dag.
43 | Go().
44 | FromVersion("1.22-alpine3.18").
45 | Build(source, dagger.GoBuildOpts{
46 | Packages: []string{"./cmd/reverstd/..."},
47 | })
48 |
49 | return dag.
50 | Container().
51 | From("alpine:3.18").
52 | WithFile("/usr/local/bin/reverstd", build.File("reverstd")).
53 | WithEntrypoint([]string{"reverstd"}), nil
54 | }
55 |
56 | func (m *Reverst) TestUnit(
57 | ctx context.Context,
58 | source *dagger.Directory,
59 | ) (string, error) {
60 | out, err := dag.Container().
61 | From("golang:1.22-alpine3.18").
62 | WithExec([]string{"apk", "add", "gcc", "build-base"}).
63 | With(dag.Go().GlobalCache).
64 | WithEnvVariable("CGO_ENABLED", "1").
65 | WithMountedDirectory("/src", source).
66 | WithWorkdir("/src").
67 | WithExec([]string{"go", "test", "-race", "-count=5", "./..."}).
68 | Stdout(ctx)
69 | if err != nil {
70 | return out, err
71 | }
72 |
73 | return out, nil
74 | }
75 |
76 | func (m *Reverst) TestIntegration(
77 | ctx context.Context,
78 | source *dagger.Directory,
79 | //+optional
80 | verbose bool,
81 | ) (string, error) {
82 | ctr, err := m.BuildContainer(ctx, source)
83 | if err != nil {
84 | return "", err
85 | }
86 |
87 | key, cert, err := generateKeyPair()
88 | if err != nil {
89 | return "", err
90 | }
91 |
92 | reverst := ctr.
93 | WithEnvVariable("REVERST_LOG", "debug").
94 | WithEnvVariable("REVERST_TUNNEL_ADDRESS", "0.0.0.0:7171").
95 | WithEnvVariable("REVERST_SERVER_NAME", "local.example").
96 | WithNewFile("/etc/reverst/key.pem", dagger.ContainerWithNewFileOpts{
97 | Contents: string(key),
98 | }).
99 | WithEnvVariable("REVERST_PRIVATE_KEY_PATH", "/etc/reverst/key.pem").
100 | WithNewFile("/etc/reverst/cert.pem", dagger.ContainerWithNewFileOpts{
101 | Contents: string(cert),
102 | }).
103 | WithEnvVariable("REVERST_CERTIFICATE_PATH", "/etc/reverst/cert.pem").
104 | WithNewFile("/etc/reverst/groups.yml", dagger.ContainerWithNewFileOpts{
105 | Contents: `groups:
106 | "local.example":
107 | hosts: ["local.example"]
108 | authentication:
109 | basic:
110 | username: "user"
111 | password: "pass"
112 | `,
113 | }).
114 | WithEnvVariable("REVERST_TUNNEL_GROUPS", "/etc/reverst/groups.yml").
115 | WithEnvVariable("REVERST_WATCH_GROUPS", "true").
116 | WithExposedPort(7171, dagger.ContainerWithExposedPortOpts{
117 | Protocol: dagger.Udp,
118 | }).
119 | WithExposedPort(8181, dagger.ContainerWithExposedPortOpts{
120 | Protocol: dagger.Tcp,
121 | }).
122 | WithExec(nil).
123 | AsService()
124 |
125 | cmd := []string{"go", "test", "./internal/test/...", "-integration"}
126 | if verbose {
127 | cmd = append(cmd, "-v")
128 | }
129 |
130 | out, err := dag.Container().
131 | From("golang:1.22-alpine3.18").
132 | WithServiceBinding("local.example", reverst).
133 | With(dag.Go().GlobalCache).
134 | WithMountedDirectory("/src", source).
135 | WithWorkdir("/src").
136 | WithExec(cmd).
137 | Stdout(ctx)
138 | if err != nil {
139 | return out, err
140 | }
141 |
142 | return out, nil
143 | }
144 |
145 | func (m *Reverst) Publish(
146 | ctx context.Context,
147 | source *dagger.Directory,
148 | password *Secret,
149 | //+optional
150 | //+default="ghcr.io"
151 | registry string,
152 | //+optional
153 | //+default="flipt-io"
154 | username string,
155 | //+optional
156 | //+default="reverst"
157 | image string,
158 | //+optional
159 | //+default="latest"
160 | tag string,
161 | ) (string, error) {
162 | ctr, err := m.BuildContainer(ctx, source)
163 | if err != nil {
164 | return "", err
165 | }
166 |
167 | return ctr.
168 | WithRegistryAuth(registry, username, password).
169 | Publish(ctx, fmt.Sprintf("%s/%s/%s:%s", registry, username, image, tag))
170 | }
171 |
172 | func generateKeyPair() ([]byte, []byte, error) {
173 | key, err := rsa.GenerateKey(rand.Reader, 2048)
174 | if err != nil {
175 | return nil, nil, err
176 | }
177 |
178 | keyPem := pem.EncodeToMemory(&pem.Block{
179 | Type: "RSA PRIVATE KEY",
180 | Bytes: x509.MarshalPKCS1PrivateKey(key),
181 | })
182 |
183 | crt := x509.Certificate{
184 | NotBefore: time.Now(),
185 | NotAfter: time.Now().AddDate(10, 0, 0),
186 | SerialNumber: big.NewInt(1),
187 | Subject: pkix.Name{
188 | CommonName: "Flipt",
189 | Organization: []string{"Flipt Corp."},
190 | },
191 | BasicConstraintsValid: true,
192 | }
193 | cert, err := x509.CreateCertificate(rand.Reader, &crt, &crt, &key.PublicKey, key)
194 | if err != nil {
195 | return nil, nil, err
196 | }
197 |
198 | // Generate a pem block with the certificate
199 | return keyPem, pem.EncodeToMemory(&pem.Block{
200 | Type: "CERTIFICATE",
201 | Bytes: cert,
202 | }), nil
203 | }
204 |
--------------------------------------------------------------------------------
/docs/PROTOCOL.md:
--------------------------------------------------------------------------------
1 | Tunnel Protocol
2 | ---------------
3 |
4 | This page aims to document the tunnel registration protocol.
5 |
6 |