├── ,gitignore ├── artifacts ├── ns.yml ├── connect-svc.yaml ├── server-svc.yaml ├── connect-dep.yaml ├── server-dep.yaml └── client.yaml ├── go.mod ├── .DEREK.yml ├── go.sum ├── .github └── workflows │ ├── ci-only.yaml │ └── publish.yaml ├── LICENSE ├── Dockerfile ├── README.md └── main.go /,gitignore: -------------------------------------------------------------------------------- 1 | /bin/** 2 | /connect 3 | -------------------------------------------------------------------------------- /artifacts/ns.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: inlets 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/inlets/connect 2 | 3 | go 1.21 4 | 5 | require golang.org/x/sync v0.7.0 6 | -------------------------------------------------------------------------------- /.DEREK.yml: -------------------------------------------------------------------------------- 1 | curators: 2 | - alexellis 3 | 4 | features: 5 | - dco_check 6 | - comments 7 | - pr_description_required 8 | - release_notes 9 | 10 | 11 | contributing_url: https://github.com/alexellis/arkade/blob/master/CONTRIBUTING.md 12 | -------------------------------------------------------------------------------- /artifacts/connect-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: inlets-connect 5 | namespace: inlets 6 | labels: 7 | app: inlets-connect 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - name: inlets-connect 12 | port: 3128 13 | protocol: TCP 14 | targetPort: 3128 15 | selector: 16 | app: inlets-connect 17 | -------------------------------------------------------------------------------- /artifacts/server-svc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: inlets-server 6 | namespace: inlets 7 | labels: 8 | app: inlets-server 9 | spec: 10 | type: LoadBalancer 11 | ports: 12 | - name: inlets-control 13 | port: 8123 14 | protocol: TCP 15 | targetPort: 8123 16 | nodePort: 30000 17 | - name: inlets-connect 18 | port: 3128 19 | protocol: TCP 20 | targetPort: 3128 21 | nodePort: 30001 22 | selector: 23 | app: inlets-server 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 2 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 3 | golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= 4 | golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 5 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 6 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 7 | -------------------------------------------------------------------------------- /artifacts/connect-dep.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: inlets-connect 5 | namespace: inlets 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: inlets-connect 11 | template: 12 | metadata: 13 | labels: 14 | app: inlets-connect 15 | spec: 16 | containers: 17 | - name: inlets-connect 18 | image: ghcr.io/alexellis/inlets-connect 19 | imagePullPolicy: Always 20 | command: ["/usr/bin/connect"] 21 | args: 22 | - "--port=3128" 23 | - "--upstream=kubernetes.default.svc:443" 24 | -------------------------------------------------------------------------------- /artifacts/server-dep.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: inlets-server 6 | namespace: inlets 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: inlets-server 12 | template: 13 | metadata: 14 | labels: 15 | app: inlets-server 16 | spec: 17 | containers: 18 | - name: inlets-server 19 | image: ghcr.io/inlets/inlets-pro:0.9.8 20 | imagePullPolicy: IfNotPresent 21 | command: ["inlets-pro"] 22 | args: 23 | - "server" 24 | - "--auto-tls=true" 25 | - "--auto-tls-san=192.168.0.26" 26 | - "--token=TOKEN" 27 | -------------------------------------------------------------------------------- /artifacts/client.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: inlets-client 6 | namespace: inlets 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: inlets-client 12 | template: 13 | metadata: 14 | labels: 15 | app: inlets-client 16 | spec: 17 | containers: 18 | - name: inlets-client 19 | image: ghcr.io/inlets/inlets-pro:0.9.8 20 | imagePullPolicy: IfNotPresent 21 | command: ["inlets-pro"] 22 | args: 23 | - "tcp" 24 | - "client" 25 | - "--url=wss://192.168.0.26:8123" 26 | - "--ports=3128" 27 | - "--token=TOKEN" 28 | - "--license=" 29 | - "--upstream=inlets-connect" 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-only.yaml: -------------------------------------------------------------------------------- 1 | name: ci-only 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | with: 17 | fetch-depth: 1 18 | - name: Get Repo Owner 19 | id: get_repo_owner 20 | run: echo ::set-output name=repo_owner::$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Local build 26 | id: local_build 27 | uses: docker/build-push-action@v5 28 | with: 29 | outputs: "type=docker,push=false" 30 | platforms: linux/amd64 31 | build-args: | 32 | Version=dev 33 | GitCommit=${{ github.sha }} 34 | tags: | 35 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/inlets-connect:${{ github.sha }} 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Ellis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.22-alpine as builder 2 | 3 | ARG TARGETPLATFORM 4 | ARG BUILDPLATFORM 5 | ARG TARGETOS 6 | ARG TARGETARCH 7 | 8 | ARG GIT_COMMIT 9 | ARG VERSION 10 | 11 | ENV GO111MODULE=on 12 | ENV CGO_ENABLED=0 13 | ENV GOPATH=/go/src/ 14 | WORKDIR /go/src/github.com/inlets/connect 15 | 16 | COPY main.go . 17 | COPY go.mod . 18 | COPY go.sum . 19 | 20 | RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go test -cover ./... 21 | 22 | # add user in this stage because it cannot be done in next stage which is built from scratch 23 | # in next stage we'll copy user and group information from this stage 24 | RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -ldflags "-s -w -X main.GitCommit=${GIT_COMMIT} -X main.Version=${VERSION}" -a -installsuffix cgo -o /usr/bin/inlets-connect 25 | 26 | ARG REPO_URL 27 | 28 | LABEL org.opencontainers.image.source $REPO_URL 29 | 30 | FROM --platform=${BUILDPLATFORM:-linux/amd64} gcr.io/distroless/static:nonroot 31 | WORKDIR / 32 | COPY --from=builder /usr/bin/inlets-connect / 33 | USER nonroot:nonroot 34 | 35 | EXPOSE 3128 36 | 37 | VOLUME /tmp/ 38 | 39 | ENTRYPOINT ["/inlets-connect"] 40 | CMD ["--help"] 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | with: 18 | fetch-depth: 1 19 | - name: Get Repo Owner 20 | id: get_repo_owner 21 | run: echo ::set-output name=repo_owner::$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') 22 | - name: Get TAG 23 | id: get_tag 24 | run: echo ::set-output name=TAG::${GITHUB_REF#refs/tags/} 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v3 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | - name: Login to Docker Registry 30 | uses: docker/login-action@v1 31 | with: 32 | username: ${{ steps.get_repo_owner.outputs.repo_owner }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | registry: ghcr.io 35 | - name: Release build 36 | id: release_build 37 | uses: docker/build-push-action@v5 38 | with: 39 | outputs: "type=registry,push=true" 40 | platforms: linux/amd64,linux/arm/v6,linux/arm64 41 | build-args: | 42 | VERSION=${{ steps.get_tag.outputs.TAG }} 43 | GIT_COMMIT=${{ github.sha }} 44 | tags: | 45 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/inlets-connect:${{ github.sha }} 46 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/inlets-connect:${{ steps.get_tag.outputs.TAG }} 47 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/inlets-connect:latest 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inlets-connect - a tiny HTTP CONNECT proxy 🚦 2 | 3 | inlets-connect is a proxy that supports the HTTP CONNECT method to initiate a TCP connection over HTTP. It can be deployed as a stand-alone binary, a container, or as a side-car. 4 | 5 | It's designed to allow access to a single, predefined address using TCP pass-through, without any decryption or MITM. 6 | 7 | The reason inlets-connect was made was to make it easier to proxy the Kubernetes API server through inlets Pro TCP tunnels, where the TLS SAN name of the API server is fixed to `kubernetes.default.svc` for instance. This tool means that the API server can be accessed remotely from kubectl, ArgoCD and other tooling without resorting to *TLS Insecure Verify*, which is an anti-pattern. 8 | 9 | ## Usage 10 | 11 | ### Kubernetes 12 | 13 | For usage on Kubernetes, see: [artifacts](/artifacts) 14 | 15 | ### Local 16 | 17 | ```bash 18 | go build && ./inlets-connect --upstream 192.168.0.15:443 --port 3128 19 | 20 | curl https://192.168.0.15 -x http://127.0.0.1:3128 21 | ``` 22 | 23 | Assuming that you want to proxy to `https://192.168.0.15`, you can start a HTTPS proxy and then use curl to access it. 24 | 25 | This example allows the proxy running on `127.0.0.1:3128` to accept a CONNECT request and forward traffic to the `--upstream` i.e. `192.168.0.15:443`. 26 | 27 | From within Kubernetes, the `--upstream` is likely to be `kubernetes.default.svc` and the proxy is likely to be run in a Pod. 28 | 29 | ### Usage from within a KUBECONFIG 30 | 31 | Set the `server` as the upstream and the `proxy-url` as the endpoint for kubectl to talk to inlets-connect itself. 32 | 33 | ```yaml 34 | - cluster: 35 | certificate-authority-data: ... 36 | server: https://kubernetes.svc.default:443 37 | proxy-url: http://127.0.0.1:3128 38 | name: openshift-regulated-customer 39 | ``` 40 | 41 | Then use kubectl / helm / arkade as per usual. 42 | 43 | ### Within Docker 44 | 45 | Run the proxy with an allowed upstream of `kubernetes:443` 46 | 47 | ```bash 48 | $ docker run -p 3128:3128 \ 49 | -ti ghcr.io/alexellis/inlets-connect:latest -port 3128 -upstream ghost:443 50 | 51 | 2021/04/15 10:48:49 Version: 0.0.2 Commit: 3ec88704b162263511b46f33ee23f1c72f773d56 52 | 2021/04/15 10:48:49 Listening on 3128, allowed upstream: ghost:443 53 | ``` 54 | 55 | Then access an endpoint local to the proxy i.e. `https://ghost` via the proxy using `curl -x http://proxy:port` 56 | 57 | ```bash 58 | curl https://ghost -x http://127.0.0.1:3128 59 | ``` 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var ( 17 | GitCommit string 18 | Version string 19 | ) 20 | 21 | func main() { 22 | var port int 23 | var validUpstream string 24 | 25 | flag.StringVar(&validUpstream, "upstream", "", "The upstream this proxy is allowed to access i.e. 192.168.0.26:6443") 26 | flag.IntVar(&port, "port", 3128, "The port to listen on") 27 | flag.Parse() 28 | 29 | if len(validUpstream) == 0 { 30 | fmt.Fprintf(os.Stderr, "--upstream is required\n") 31 | os.Exit(1) 32 | return 33 | } 34 | 35 | if strings.Contains(validUpstream, "http://") || strings.Contains(validUpstream, "https://") { 36 | fmt.Fprintf(os.Stderr, "--upstream should be HOST:PORT only\n") 37 | os.Exit(1) 38 | return 39 | } 40 | 41 | log.Printf("inlets-connect by Alex Ellis\n\nVersion: %s\tCommit: %s", Version, GitCommit) 42 | 43 | log.Printf("Listening on %d, allowed upstream: %s", port, validUpstream) 44 | 45 | http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | if r.Method != http.MethodConnect { 47 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 48 | return 49 | } 50 | 51 | defer r.Body.Close() 52 | 53 | if r.Host != validUpstream { 54 | log.Printf("Unauthorized request to: %s, should be: %s", r.Host, validUpstream) 55 | http.Error(w, fmt.Sprintf("Unauthorized request to: %s", r.Host), http.StatusUnauthorized) 56 | return 57 | } 58 | 59 | conn, err := net.DialTimeout("tcp", r.Host, time.Second*5) 60 | if err != nil { 61 | http.Error(w, fmt.Sprintf("Unable to dial %s, error: %s", r.Host, err.Error()), http.StatusServiceUnavailable) 62 | return 63 | } 64 | w.WriteHeader(http.StatusOK) 65 | 66 | log.Printf("Dialed upstream: %s %s", conn.RemoteAddr(), conn.LocalAddr()) 67 | 68 | hj, ok := w.(http.Hijacker) 69 | if !ok { 70 | http.Error(w, "Unable to hijack connection", http.StatusInternalServerError) 71 | return 72 | } 73 | 74 | reqConn, wbuf, err := hj.Hijack() 75 | if err != nil { 76 | http.Error(w, fmt.Sprintf("Unable to hijack connection %s", err), http.StatusInternalServerError) 77 | return 78 | } 79 | defer reqConn.Close() 80 | defer wbuf.Flush() 81 | 82 | ctx, cancel := context.WithCancel(context.Background()) 83 | go func() { 84 | defer cancel() 85 | pipe(reqConn, conn) 86 | }() 87 | go func() { 88 | defer cancel() 89 | pipe(conn, reqConn) 90 | }() 91 | 92 | <-ctx.Done() 93 | 94 | log.Printf("Connection %s done.", conn.RemoteAddr()) 95 | })) 96 | } 97 | 98 | func pipe(from net.Conn, to net.Conn) error { 99 | defer from.Close() 100 | n, err := io.Copy(from, to) 101 | log.Printf("Wrote: %d bytes", n) 102 | if err != nil && strings.Contains(err.Error(), "closed network") { 103 | return nil 104 | } 105 | return err 106 | } 107 | --------------------------------------------------------------------------------