├── .github ├── images │ ├── localrelay_conns-fs8.png │ ├── localrelay_conns_ips-fs8.png │ ├── localrelay_monitor-fs8.png │ └── localrelay_status2-fs8.png ├── osv.toml └── workflows │ ├── .drone.yml │ ├── codeql-analysis.yml │ └── go.yml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── cmd └── localrelay │ ├── args.go │ ├── config.go │ ├── conns.go │ ├── daemon.go │ ├── fork_posix.go │ ├── fork_windows.go │ ├── format_test.go │ ├── ipc.go │ ├── ipcClient.go │ ├── ipcServer.go │ ├── ipc_posix.go │ ├── ipc_posix_test.go │ ├── ipc_windows.go │ ├── ipc_windows_test.go │ ├── logo.ico │ ├── main.go │ ├── metrics.go │ ├── new.go │ ├── print.go │ ├── rsrc_windows_386.syso │ ├── rsrc_windows_amd64.syso │ ├── run.go │ ├── service.go │ ├── status.go │ └── updates.go ├── dialer.go ├── docker-compose.yml ├── examples ├── basic │ └── main.go ├── certificate-pinning │ └── main.go ├── close │ └── main.go ├── failover │ └── main.go ├── http-privacy │ ├── access-tor.png │ ├── ifconfig.me.png │ └── main.go ├── http │ └── main.go ├── https │ └── main.go ├── metrics │ └── main.go ├── proxy │ ├── main.go │ └── tor hidden service.png └── timeout │ └── main.go ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── icon.png ├── internal ├── httperror │ ├── 503.html │ └── pages.go └── ipc │ ├── connect_posix.go │ ├── connect_windows.go │ ├── ipc.go │ ├── listen_posix.go │ └── listen_windows.go ├── logger.go ├── metrics.go ├── pkg └── api │ ├── client.go │ ├── doc.go │ └── models.go ├── relay.go ├── relay_test.go ├── relayfailovertcp.go ├── relayhttp.go ├── relayhttps.go ├── relaytcp.go ├── relayudp.go ├── scripts └── wix │ ├── README.md │ └── localrelay.template.wxs └── v2 ├── go.mod ├── go.sum ├── logger.go ├── metrics.go ├── relay.go ├── relay_test.go ├── relayfailovertcp.go ├── relayhttp.go ├── relayhttps.go ├── relaytcp.go ├── relayudp.go ├── target.go └── target_test.go /.github/images/localrelay_conns-fs8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/.github/images/localrelay_conns-fs8.png -------------------------------------------------------------------------------- /.github/images/localrelay_conns_ips-fs8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/.github/images/localrelay_conns_ips-fs8.png -------------------------------------------------------------------------------- /.github/images/localrelay_monitor-fs8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/.github/images/localrelay_monitor-fs8.png -------------------------------------------------------------------------------- /.github/images/localrelay_status2-fs8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/.github/images/localrelay_status2-fs8.png -------------------------------------------------------------------------------- /.github/osv.toml: -------------------------------------------------------------------------------- 1 | [[IgnoredVulns]] 2 | id = "GO-2023-2185 " 3 | reason = "Not relevent" -------------------------------------------------------------------------------- /.github/workflows/.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | type: docker 4 | name: go-1-22 5 | 6 | steps: 7 | - name: test 8 | image: golang:1.22 9 | commands: 10 | - go test ./... 11 | - go build -v ./cmd/localrelay 12 | 13 | - name: goreleaser-validate 14 | image: goreleaser/goreleaser 15 | commands: 16 | - goreleaser check 17 | 18 | - name: run gitleaks 19 | image: plugins/gitleaks 20 | settings: 21 | path: . 22 | 23 | - name: build-failed-notification 24 | image: curlimages/curl:8.00.1 25 | environment: 26 | notify: 27 | from_secret: notification_webhook 28 | when: 29 | status: 30 | - failure 31 | commands: 32 | - "curl -v $notify -F 'title=Build Failed: ${DRONE_REPO}' -F $'message=[Build Failed] ${DRONE_REPO}\n[COMMIT] ${DRONE_COMMIT}\n[BRANCH] ${DRONE_BRANCH}\n'" 33 | --- 34 | kind: pipeline 35 | type: docker 36 | name: go-1-19 37 | 38 | steps: 39 | - name: test 40 | image: golang:1.19 41 | commands: 42 | - go test ./... 43 | - go build -v ./cmd/localrelay 44 | 45 | - name: build-failed-notification 46 | image: curlimages/curl:8.00.1 47 | environment: 48 | notify: 49 | from_secret: notification_webhook 50 | when: 51 | status: 52 | - failure 53 | commands: 54 | - "curl -v $notify -F 'title=Build Failed: ${DRONE_REPO}' -F $'message=[Build Failed] ${DRONE_REPO}\n[COMMIT] ${DRONE_COMMIT}\n[BRANCH] ${DRONE_BRANCH}\n'" 55 | 56 | --- 57 | kind: pipeline 58 | type: docker 59 | name: go-1-20 60 | 61 | steps: 62 | - name: test 63 | image: golang:1.20 64 | commands: 65 | - go test ./... 66 | - go build -v ./cmd/localrelay 67 | 68 | - name: build-failed-notification 69 | image: curlimages/curl:8.00.1 70 | environment: 71 | notify: 72 | from_secret: notification_webhook 73 | when: 74 | status: 75 | - failure 76 | commands: 77 | - "curl -v $notify -F 'title=Build Failed: ${DRONE_REPO}' -F $'message=[Build Failed] ${DRONE_REPO}\n[COMMIT] ${DRONE_COMMIT}\n[BRANCH] ${DRONE_BRANCH}\n'" 78 | 79 | --- 80 | kind: pipeline 81 | type: docker 82 | name: go-1-21 83 | 84 | steps: 85 | - name: test 86 | image: golang:1.21 87 | commands: 88 | - go test ./... 89 | - go build -v ./cmd/localrelay 90 | 91 | - name: build-failed-notification 92 | image: curlimages/curl:8.00.1 93 | environment: 94 | notify: 95 | from_secret: notification_webhook 96 | when: 97 | status: 98 | - failure 99 | commands: 100 | - "curl -v $notify -F 'title=Build Failed: ${DRONE_REPO}' -F $'message=[Build Failed] ${DRONE_REPO}\n[COMMIT] ${DRONE_COMMIT}\n[BRANCH] ${DRONE_BRANCH}\n'" 101 | 102 | 103 | --- 104 | kind: pipeline 105 | type: docker 106 | name: osv-dependency-scan 107 | 108 | steps: 109 | - name: osv-vuln-dep-scanner 110 | image: ghcr.io/google/osv-scanner:latest 111 | commands: 112 | - /osv-scanner --config=/drone/src/.github/osv.toml -r ./ 113 | 114 | - name: build-failed-notification 115 | image: curlimages/curl:8.00.1 116 | environment: 117 | notify: 118 | from_secret: notification_webhook 119 | when: 120 | status: 121 | - failure 122 | commands: 123 | - "curl -v $notify -F 'title=Vulnerability In ${DRONE_REPO}' -F $'message=[Build Failed] ${DRONE_REPO}\n[COMMIT] ${DRONE_COMMIT}\n[BRANCH] ${DRONE_BRANCH}'" -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '15 12 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test-linux: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.21 19 | 20 | - name: Build 21 | run: go build -v ./cmd/localrelay 22 | 23 | - name: Test 24 | run: go test -v ./... 25 | test-macos: 26 | runs-on: macos-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v2 32 | with: 33 | go-version: 1.21 34 | 35 | - name: Build 36 | run: go build -v ./cmd/localrelay 37 | 38 | - name: Test 39 | run: go test -v ./... 40 | test-windows: 41 | runs-on: windows-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - name: Set up Go 46 | uses: actions/setup-go@v2 47 | with: 48 | go-version: 1.21 49 | 50 | - name: Build 51 | run: go build -v ./cmd/localrelay 52 | 53 | - name: Test 54 | run: go test -v ./... 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/localrelay/*.toml 2 | bin/ 3 | cmd/localrelay/localrelay 4 | *.exe 5 | *.exe~ 6 | .vscode 7 | *.deb 8 | /localrelay 9 | packages/ 10 | dist/ 11 | *.msi 12 | *.wixpdb 13 | scripts/wix/localrelay.wxs -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | # - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - 11 | main: ./cmd/localrelay 12 | id: "cli" 13 | binary: "localrelay" 14 | ldflags: 15 | - "-s -w" 16 | - "-X main.VERSION={{.Version}}" 17 | - "-X main.COMMIT={{.ShortCommit}}" 18 | - "-X main.BRANCH={{.Branch}}" 19 | env: 20 | - CGO_ENABLED=0 21 | goos: 22 | - linux 23 | - windows 24 | - darwin 25 | - openbsd 26 | - freebsd 27 | - netbsd 28 | - solaris 29 | ignore: 30 | - goos: windows 31 | goarch: arm64 32 | - goos: windows 33 | goarch: arm 34 | hooks: 35 | post: 36 | - cmd: bash -c 'if [ "{{.Os}}" == "windows" ] && [ "{{.Arch}}" == "amd64" ]; then make wix; mv ./scripts/wix/localrelay.msi ./dist; fi' 37 | 38 | archives: 39 | - format: tar.gz 40 | # this name template makes the OS and Arch compatible with the results of uname. 41 | name_template: >- 42 | {{ .ProjectName }}_ 43 | {{- title .Os }}_ 44 | {{- if eq .Arch "amd64" }}x86_64 45 | {{- else if eq .Arch "386" }}i386 46 | {{- else }}{{ .Arch }}{{ end }} 47 | {{- if .Arm }}v{{ .Arm }}{{ end }} 48 | # use zip for windows archives 49 | format_overrides: 50 | - goos: windows 51 | format: zip 52 | files: 53 | - LICENSE 54 | 55 | checksum: 56 | name_template: "checksums.txt" 57 | extra_files: 58 | - glob: ./dist/*.msi 59 | - name_template: localrelay.msi 60 | 61 | snapshot: 62 | name_template: "{{ incpatch .Version }}-next" 63 | changelog: 64 | sort: asc 65 | filters: 66 | exclude: 67 | - "^docs:" 68 | - "^test:" 69 | - "^ci:" 70 | gitea_urls: 71 | api: "{{ .Env.GITEA_API }}" 72 | download: "{{ .Env.GITEA_DOWNLOAD }}" 73 | # set to true if you use a self-signed certificate 74 | skip_tls_verify: false 75 | release: 76 | gitea: 77 | owner: go-compile 78 | name: localrelay 79 | disable: false 80 | # The lines beneath this are called `modelines`. See `:help modeline` 81 | # Feel free to remove those if you don't want/use them. 82 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 83 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 84 | 85 | nfpms: 86 | # note that this is an array of nfpm configs 87 | - # ID of the nfpm config, must be unique. 88 | # Defaults to "default". 89 | id: nfpms-build 90 | 91 | # You can change the file name of the package. 92 | # 93 | # Default:`{{ .PackageName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}` 94 | file_name_template: "{{ .ConventionalFileName }}" 95 | 96 | # Your app's vendor. 97 | # Default is empty. 98 | vendor: Go-Compile 99 | 100 | # Template to your app's homepage. 101 | # Default is empty. 102 | homepage: https://github.com/go-compile/localrelay 103 | 104 | # Template to your app's description. 105 | # Default is empty. 106 | description: Reverse proxy routing system allowing you to use fail overs, connect to onion hidden services. 107 | # Your app's license. 108 | # Default is empty. 109 | license: GPL-3.0 110 | 111 | maintainer: Go-Compile 112 | 113 | # Formats to be generated. 114 | formats: 115 | - deb 116 | - rpm 117 | - termux.deb # Since GoReleaser v1.11. 118 | - archlinux # Since GoReleaser v1.13. 119 | 120 | # Template to the path that the binaries should be installed. 121 | # Defaults to `/usr/bin`. 122 | bindir: /usr/bin 123 | 124 | # Section. 125 | section: default 126 | 127 | # Custom configuration applied only to the RPM packager. 128 | rpm: 129 | # The package group. This option is deprecated by most distros 130 | # but required by old distros like CentOS 5 / EL 5 and earlier. 131 | group: Unspecified 132 | 133 | # Compression algorithm (gzip (default), lzma or xz). 134 | compression: lzma 135 | 136 | signs: 137 | - artifacts: checksum 138 | output: true 139 | id: "628B769BFD007F8233FDAD1853F4922E9D5497B8" 140 | 141 | chocolateys: 142 | - authors: "Go Compile" 143 | title: "Localrelay" 144 | project_source_url: "https://github.com/go-compile/localrelay" 145 | project_url: "https://github.com/go-compile/localrelay" 146 | url_template: "https://github.com/go-compile/localrelay/releases/download/{{.Tag}}/{{.ArtifactName }}" 147 | license_url: "https://github.com/go-compile/localrelay/blob/master/LICENSE" 148 | tags: "reverse proxy tor socks5 failover http https tcp" 149 | summary: "Localrelay is a light-weight reverse proxy" 150 | package_source_url: "https://github.com/go-compile/localrelay" 151 | require_license_acceptance: false 152 | icon_url: "https://raw.githubusercontent.com/go-compile/localrelay/master/icon.png" 153 | description: "{{ .ProjectName }} is a reverse proxy routing system, allowing you to use fail overs and connect to onion hidden services." 154 | release_notes: "https://github.com/go-compile/localrelay/releases/tag/v{{.Version}}" 155 | api_key: "{{.Env.CHOCOLATEY_API_KEY }}" 156 | source_repo: "https://push.chocolatey.org/" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine AS builder 2 | 3 | ARG VERSION 4 | ARG COMMIT 5 | ARG BRANCH 6 | 7 | WORKDIR /app/src 8 | 9 | COPY ./cmd/localrelay /app/src/ 10 | 11 | RUN go mod init localrelay-cli 12 | RUN go mod tidy 13 | # Download latest version from master branch 14 | RUN go get github.com/go-compile/localrelay@master 15 | 16 | # Build as static binary 17 | RUN CGO_ENABLED=0 go build -v -ldflags="-s -w -extldflags -static -X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT} -X main.BRANCH=${BRANCH}" -tags osusergo,netgo -o /app/localrelay 18 | 19 | # RUN arch=$TARGETPLATFORM; amd64="linux/amd64" 20 | # RUN if [ $arch = $amd64 ]; then apk add upx; fi 21 | # RUN if [ $arch = $amd64 ]; then upx --ultra-brute /app/localrelay; fi 22 | 23 | RUN rm -rf /app/src 24 | 25 | # Create empty dir to be used as mkdir COPY src. 26 | # SCRATCH images do not have mkdir thus we must use "COPY /app/empty /dst" 27 | RUN mkdir /app/empty 28 | 29 | FROM scratch 30 | 31 | COPY --from=builder /app /usr/bin 32 | COPY --from=builder /app/empty /var/run 33 | 34 | # fix TLS unable to verify CA errors 35 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 36 | 37 | WORKDIR /app 38 | 39 | HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ 40 | CMD [ "localrelay", "status" ] 41 | 42 | CMD ["/usr/bin/localrelay", "start-service-daemon"] 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # VERSION=$(shell git describe --tags --abbrev=0) 2 | VERSION=v2.0.0-rc3 3 | COMMIT=$(shell git rev-parse --short HEAD) 4 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 5 | 6 | build: 7 | goreleaser release --snapshot --clean --skip=publish,sign 8 | 9 | release: 10 | goreleaser release --clean --skip=publish 11 | 12 | wix: 13 | cp ./scripts/wix/localrelay.template.wxs ./scripts/wix/localrelay.wxs 14 | sed -i -E 's/LR_VERSION/${VERSION}/g' ./scripts/wix/localrelay.wxs 15 | wix build ./scripts/wix/localrelay.wxs 16 | 17 | publish: 18 | goreleaser release --clean 19 | 20 | install: 21 | cd ./cmd/localrelay/ && go install -v -ldflags="-s -w -X main.VERSION=${VERSION} -X main.BRANCH=${BRANCH} -X main.COMMIT=${COMMIT}" 22 | 23 | dev-install: 24 | cd ./cmd/localrelay/ && go build -v -ldflags="-s -w -X main.VERSION=${VERSION} -X main.BRANCH=${BRANCH} -X main.COMMIT=${COMMIT}" 25 | sudo chown root:root ./cmd/localrelay/localrelay 26 | sudo chmod 755 ./cmd/localrelay/localrelay 27 | sudo mv ./cmd/localrelay/localrelay /usr/bin/ 28 | sudo localrelay restart 29 | 30 | install-deps: 31 | # Install developer dependencies 32 | 33 | # install build system 34 | go install github.com/goreleaser/goreleaser@latest 35 | 36 | docker: 37 | docker build . --tag localrelay --build-arg VERSION=${VERSION} --build-arg COMMIT=${COMMIT} --build-arg BRANCH=${BRANCH} 38 | 39 | docker-push: 40 | docker buildx build --platform linux/amd64,linux/arm/v7,linux/arm64 . --tag gocompile/localrelay:latest --build-arg VERSION=${VERSION} --push 41 | docker buildx build --platform linux/amd64,linux/arm/v7,linux/arm64 . --tag gocompile/localrelay:${VERSION} --build-arg VERSION=${VERSION} --push 42 | 43 | clean: 44 | rm -rf ./dist/ 45 | go clean 46 | rm ./scripts/wix/localrelay.wxs 47 | rm ./scripts/wix/localrelay.msi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Localrelay 2 | 3 | [![GitHub release](https://img.shields.io/github/release/go-compile/localrelay.svg)](https://github.com/go-compile/localrelay/releases) 4 | [![Go Report Card](https://goreportcard.com/badge/go-compile/localrelay)](https://goreportcard.com/report/go-compile/localrelay) 5 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/go-compile/localrelay) 6 | [![Docker Size](https://img.shields.io/docker/image-size/gocompile/localrelay?sort=date)](https://hub.docker.com/r/gocompile/localrelay/) 7 | [![Docker Version](https://img.shields.io/docker/v/gocompile/localrelay?label=docker%20version&sort=semver)](https://hub.docker.com/r/gocompile/localrelay/) 8 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/go-compile/localrelay/.github/workflows/go.yml) 9 | 10 | 11 | 12 | Localrelay is a cross platform (Windows, Linux, Mac, Android, and more) reverse proxy which allows the destination address to be customised and can even use a SOCKS5 proxy. Supporting both raw TCP connections and HTTP/HTTPS connections. **Localrelay allows you to host services e.g. Nextcloud on Tor and access it on your mobile or laptop anywhere without needing to open your firewall**. 13 | 14 |
15 | 16 | **[\[ Wiki & Guide \]](https://github.com/go-compile/localrelay/wiki)** 17 | [\[ Download Release \]](https://github.com/go-compile/localrelay/releases/latest) 18 | [\[ Docker Image \]](https://hub.docker.com/r/gocompile/localrelay) 19 | 20 |
21 | 22 | 23 | 24 | ## Common Localrelay Use Cases 25 | 26 | 1. **YOUR HOME LAB AWAY FROM HOME**
27 | Localrelay is commonly used with failover proxies to allow for custom routing rules based on network connectivity. Connect directly to your home lab via the IP when on your home network, and when away, connect via a hop server or proxy! 28 | 29 | > When **home connect directly to the IP**, when away from home **connect with Tor**! Or when away connect via a hop server, or several. 30 | 31 | 2. **FORCE APPLICATIONS TO USE TOR**
32 | Another use of Localrelay is force an application which doesn't allow for proxies to use one. For example, you can force Bitwarden to connect over Tor, or force Nextcloud to do the same. 33 | 34 | > Protect your IP and force applications to use SOCKS5 proxies even when they don't natively support it, all possible with Localrelay. 35 | 36 | 3. **FAILOVER ROUTING**
37 | Prevent service downtime by setting up failover relays for TCP, UDP, HTTP or HTTPS destinations. Localrelay will automatically pick the next available destination and route your traffic over it. 38 | 39 | > Stop downtime, setup failover routing to ensure you always have a route to your destination. You can even setup proxy failover, if one SOCKS proxy fails, use another automatically! All can be configured exactly how you wish with a simple config file. 40 | 41 | 4. **LOADBALANCING WITH FAILOVER**
42 | Distribute your load evenly, or bias using weights, with Localrelay load balancer. 43 | 44 | > Load balance your server connections for HTTP(s), TCP, or UDP. Or setup your browser to load balance between SOCKS5 proxies, giving you a new IP address per website you visit! 45 | 46 | --- 47 | 48 | If you self host a service for example; [Bitwarden](https://github.com/dani-garcia/vaultwarden), [Nextcloud](https://github.com/nextcloud), [Syncthing](https://github.com/syncthing/syncthing), [Grafana](https://github.com/grafana/grafana), [Gitea](https://github.com/go-gitea/gitea)... You may not want to expose your public IP address to the internet, you may prefer to protect it behind Tor. 49 | 50 | Access your local or remote services securely over [Tor](https://www.torproject.org/) without needing to port forward. 51 | 52 | Many apps such as Nextcloud, Termis and Bitwarden do not allow you to specify a proxy when connecting to your self-hosted server. Localrelay allows you to host a local reverse proxy on your devices loopback. This relay then encrypts the outgoing traffic through your set SOCKS5 proxy (Tor: 127.0.0.1:9050). 53 | 54 | When at **home connect locally**, when away **connect over Tor**. Securely connect remotely over Tor without port forwarding AND when at home connect directly with high speeds. 55 | 56 | ### Features 57 | - Proxy TCP, UDP, HTTP, and HTTPS connections 58 | - Use SOCKS5 proxies for all remote hosts, some or none, completely customisable! 59 | - Load balance. 60 | - Failover. 61 | - Failover with SOCKS5 proxies. 62 | - Run relays in the background and start at boot. 63 | - View active connections, source, destination, relay and active duration. 64 | - View each relay's bandwidth live (auto updating stats page). 65 | - Drop connections via the CLI. 66 | - Drop all connections from a specified IP. 67 | - Stop, start, restart relays ran by the service. 68 | - CLI to create relay configs. 69 | - Built in HTTP API over a unix socket. 70 | - View all connected IP addresses. 71 | 72 | ### Manage the Localrelay Service 73 | 74 | You can optionally install Localrelay as a service/daemon on Windows, Mac, Linux, and Unix other like systems to **run your relays in the background** and start at boot. 75 | 76 | 77 | | Reverse Proxy Screenshots | 78 | |:--:| 79 | | ![Localrelay CLI status command](./.github/images/localrelay_status2-fs8.png) | 80 | | ![Localrelay CLI monitor proxies](./.github/images/localrelay_monitor-fs8.png) | 81 | | ![Localrelay CLI view connected IP addresses to relays](./.github/images/localrelay_conns_ips-fs8.png) | 82 | | ![Relay spoofing useragent & using Tor](./examples/http-privacy/access-tor.png)| 83 | 84 | ### Install & Build 85 | 86 | To install Localrelay you can either build from source, or use one of the installers. 87 | 88 | - [Install Localrelay for Windows](https://github.com/go-compile/localrelay/wiki/Install) 89 | - [Install Localrelay for Ubuntu and Debian](https://github.com/go-compile/localrelay/wiki/Install) 90 | - [Install Localrelay for Android](https://github.com/go-compile/localrelay/wiki/Install) 91 | - [Install Localrelay Universal Linux](https://github.com/go-compile/localrelay/wiki/Install) 92 | - [Install Localrelay for Mac](https://github.com/go-compile/localrelay/wiki/Install) 93 | 94 | ## CLI Usage 95 | 96 | This is a basic overview, [view the wiki for more detailed information](https://github.com/go-compile/localrelay/wiki/CLI). 97 | 98 | ### Create Relay 99 | 100 | To run a relay you must first create a relay config, this allows for permanent storage of relay configuration and easy management. You can create as many of these as you like. 101 | 102 | #### Syntax 103 | 104 | ```sh 105 | # Create a simple TCP relay 106 | localrelay new -host -destination 107 | 108 | # Create HTTP relay 109 | localrelay new -host -destination -http 110 | 111 | # Create HTTPS relay 112 | localrelay new -host -destination -https -certificate=cert.pem key=key.pem 113 | 114 | # Use proxy 115 | localrelay new -host -destination -proxy 116 | 117 | # Set custom output config file 118 | localrelay new -host -destination -output ./config.toml 119 | 120 | # Create a failover TCP relay 121 | localrelay new -host -destination , -failover 122 | ``` 123 | 124 | #### Examples 125 | 126 | ```sh 127 | # Create a simple TCP relay 128 | localrelay new example.com -host 127.0.0.1:8080 -destination example.com:80 129 | 130 | # Create HTTP relay 131 | localrelay new example.com -host 127.0.0.1:8080 -destination http://example.com -http 132 | 133 | # Create HTTPS relay 134 | localrelay new example.com -host 127.0.0.1:8080 -destination https://example.com -https -certificate=cert.pem key=key.pem 135 | 136 | # Create a TCP relay and store it in the config dir to auto start on system boot (daemon required) 137 | sudo localrelay new example.com -host 127.0.0.1:8080 -destination example.com:80 -store 138 | 139 | # Use proxy 140 | localrelay new onion -host 127.0.0.1:8080 -destination 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80 -proxy socks5://127.0.0.1:9050 141 | 142 | # Create a failover TCP relay with one remote accessed via Tor 143 | localrelay new onion -host 127.0.0.1:8080 -destination 192.168.1.240:80,2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80 -failover -ignore_proxy=0 -proxy socks5://127.0.0.1:9050 144 | ``` 145 | 146 |
147 |
148 | 149 | **[Installation And Usage Guide On The Wiki](https://github.com/go-compile/localrelay/wiki)** 150 | 151 |
-------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## **IMPORTANT** 4 | - DO NOT OPEN A GITHUB ISSUE OR PULL REQUEST *(unless following reporting method B)*. 5 | - REPORT IT PRIVATELY. 6 | - PROVIDE REQUIRED DETAILS *(listed bellow)*. 7 | 8 | ## Supported Versions 9 | 10 | Only the latest version is supported for security updates. 11 | 12 | ## Reporting a Vulnerability 13 | When reporting a vulnerability (vuln) you **must** provide at-lest the following: 14 | - Vulnerability type/classification 15 | - Is it a dependency? (true/false) 16 | - Affected component (source file, function or route) 17 | - Impact (what happens when exploited) 18 | - Justify importance (**only** if the vuln is obscure and has no [CWE](https://cwe.mitre.org/data/published/cwe_latest.pdf) identifier) 19 | - Impacted version (version number or git commit hash) 20 | - Impacted platforms (windows, mac, linux, openbsd etc) 21 | 22 | **Recommended** including: 23 | - Poof of Concept 24 | - Explanation of attack and how to reproduce 25 | - Patch 26 | 27 | ### CVEs 28 | If you obtain a CVE for a vulnerability found in this repository please contact me with the CVE identifier. 29 | 30 | ### Reporting Method A 31 | To report a vulnerability, navigate the Localrelay's Github repository. Here you will find a tab called "security", next privately submit a vulnerability via Github's built in system. 32 | 33 | ### Reporting Method B 34 | Alternatively, find my contact details are (signed and) provided at . However, there is guarantee no I will see your message. Resulting, you are recommended to make a public issue which only asks for my contact information. **Do not:** (1) make a public issue disclosing the vuln, (2) make a public issue stating there is a vuln. 35 | 36 | ## Full-Disclosure 37 | Full disclosure, opposed to responsible disclosure (privately reporting and awaiting a patch), is ill-advised. Upon notification of a vulnerability (in-private), a patch will be issued with upmost urgency in a timely manor, thus no need for full-disclosure. However, if you do opt for full-disclosure, please still contact me and provide access to the publication material. -------------------------------------------------------------------------------- /cmd/localrelay/args.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/go-compile/localrelay/v2" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | var ( 16 | // ErrFailedCheckUpdate is returned when the latest version could not be fetched 17 | ErrFailedCheckUpdate = errors.New("failed to check for updates") 18 | ) 19 | 20 | type options struct { 21 | host string 22 | destination string 23 | proxyType localrelay.ProxyType 24 | proxy Proxy 25 | output string 26 | proxyIgnore []int 27 | logs string 28 | 29 | certificate string 30 | key string 31 | 32 | commands []string 33 | detach bool 34 | loadbalance bool 35 | 36 | isFork bool 37 | DisableAutoStart bool 38 | store bool 39 | 40 | ipcPipe string 41 | 42 | interval time.Duration 43 | } 44 | 45 | /* 46 | localrelay new nextcloud -host 127.0.0.1:87 -destination example.com -tcp -proxy socks5://127.0.0.1:9050 47 | 48 | localrelay run ./nextcloud.toml ./git.toml 49 | 50 | > Overwrite options such as logs for all profiles 51 | localrelay run ./nextcloud.toml ./git.toml -metrics=5s -logs=./relay.log 52 | */ 53 | func parseArgs() (*options, error) { 54 | args := os.Args[1:] 55 | 56 | opt := &options{ 57 | logs: "default", 58 | interval: time.Second, 59 | proxyType: localrelay.ProxyTCP, 60 | } 61 | 62 | for i := 0; i < len(args); i++ { 63 | if !strings.HasPrefix(args[i], "-") { 64 | opt.commands = append(opt.commands, args[i]) 65 | continue 66 | } 67 | 68 | // Strip prefix 69 | arg := strings.SplitN(args[i][1:], "=", 2) 70 | 71 | switch strings.ToLower(arg[0]) { 72 | case forkIdentifier: 73 | opt.isFork = true 74 | case "version": 75 | version() 76 | return nil, nil 77 | case "interval", "refresh": 78 | value, err := getAnswer(args, arg, &i) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | dur, err := time.ParseDuration(value) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | opt.interval = dur 89 | case "host", "lhost": 90 | value, err := getAnswer(args, arg, &i) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | opt.host = value 96 | case "log", "logs": 97 | value, err := getAnswer(args, arg, &i) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | opt.logs = value 103 | case "cert", "certificate": 104 | value, err := getAnswer(args, arg, &i) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | opt.certificate = value 110 | case "key": 111 | value, err := getAnswer(args, arg, &i) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | opt.key = value 117 | case "disable_autostart", "disable_auto_start", "nostart", "noauto": 118 | opt.DisableAutoStart = true 119 | case "ipc-stream-io-pipe": 120 | value, err := getAnswer(args, arg, &i) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | opt.ipcPipe = value 126 | case "detach", "bg", "d": 127 | opt.detach = true 128 | case "loadbalance", "lb": 129 | opt.loadbalance = true 130 | case "store", "s": 131 | opt.store = true 132 | case "timeout": 133 | value, err := getAnswer(args, arg, &i) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | dur, err := time.ParseDuration(value) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | Printf("Timeout set to: %dms\n", dur.Milliseconds()) 144 | localrelay.Timeout = dur 145 | case "destination", "dst", "rhost": 146 | value, err := getAnswer(args, arg, &i) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | opt.destination = value 152 | case "output", "o": 153 | value, err := getAnswer(args, arg, &i) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | opt.output = value 159 | case "proxyignore", "proxy_ignore": 160 | value, err := getAnswer(args, arg, &i) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | var ignored []int 166 | for _, index := range strings.Split(value, ",") { 167 | i, err := strconv.Atoi(index) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | ignored = append(ignored, i) 173 | } 174 | 175 | opt.proxyIgnore = ignored 176 | case "proxy": 177 | value, err := getAnswer(args, arg, &i) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // Parse proxy url 183 | // socks5://127.0.0.1:9050 184 | prox, err := url.Parse(value) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | pw, _ := prox.User.Password() 190 | 191 | opt.proxy.Protocol = prox.Scheme 192 | opt.proxy.Address = prox.Host 193 | opt.proxy.Username = prox.User.Username() 194 | opt.proxy.Password = pw 195 | case "tcp": 196 | opt.proxyType = localrelay.ProxyTCP 197 | case "udp": 198 | opt.proxyType = localrelay.ProxyUDP 199 | case "http": 200 | opt.proxyType = localrelay.ProxyHTTP 201 | case "https": 202 | opt.proxyType = localrelay.ProxyHTTPS 203 | case "help", "h", "?": 204 | help() 205 | if len(os.Args) >= 3 { 206 | Println("\n\n[Warn] It looks like you accidentally used -h instead of -host") 207 | } 208 | return nil, nil 209 | default: 210 | Printf("Unknown argument %q\n", arg) 211 | return nil, nil 212 | } 213 | 214 | } 215 | 216 | return opt, nil 217 | } 218 | 219 | func getAnswer(args []string, arg []string, i *int) (string, error) { 220 | // If arg is a KV pair 221 | if len(arg) == 2 { 222 | return arg[1], nil 223 | } 224 | 225 | // Check if there are any more key values 226 | if len(args)-1 <= *i { 227 | return "", errors.New("Expected value to be paired with argument") 228 | } 229 | 230 | // Skip next argument as we are going to use it now 231 | *i++ 232 | 233 | // Check if next value is a argument 234 | if x := args[*i]; len(x) > 0 && x[0] == '-' { 235 | return "", errors.New("A value can not be a argument") 236 | } 237 | 238 | return args[*i], nil 239 | } 240 | 241 | func help() { 242 | Printf("LocalRelay CLI - %s\n", VERSION) 243 | Println() 244 | Println("Usage:") 245 | Println(" localrelay new -host 127.0.0.1:8080 -destination example.com:80") 246 | Println(" -output= -tcp -http -https -proxy socks5://127.0.0.1:9050") 247 | Println() 248 | Println(" localrelay run ") 249 | Println(" localrelay run -detach") 250 | Println(" localrelay run ...") 251 | Println() 252 | Println(" localrelay start") 253 | Println(" localrelay status") 254 | Println(" localrelay monitor") 255 | Println(" localrelay connections") 256 | Println(" localrelay connections ") 257 | Println(" localrelay ips") 258 | Println(" localrelay drop") 259 | Println(" localrelay dropip ") 260 | Println(" localrelay droprelay ") 261 | Println(" localrelay stop") 262 | Println(" localrelay stop ") 263 | Println(" localrelay restart") 264 | Println(" localrelay install") 265 | Println(" localrelay uninstall") 266 | Println() 267 | Println("Arguments:") 268 | Printf(" %-28s %s\n", "-host, -lhost", "Set listen host") 269 | Printf(" %-28s %s\n", "-destination, -dst, -rhost", "Set forward address") 270 | Printf(" %-28s %s\n", "-tcp", "Set relay to TCP relay") 271 | Printf(" %-28s %s\n", "-udp", "Set relay to UDP relay") 272 | Printf(" %-28s %s\n", "-http", "Set relay to HTTP relay") 273 | Printf(" %-28s %s\n", "-https", "Set relay to HTTPS relay") 274 | Printf(" %-28s %s\n", "-proxy", "Set socks5 proxy via URL") 275 | Printf(" %-28s %s\n", "-loadbalance", "Enables load balancing") 276 | Printf(" %-28s %s\n", "-output, -o", "Set output file path") 277 | Printf(" %-28s %s\n", "-proxy_ignore", "Destination indexes to ignore proxy settings") 278 | Printf(" %-28s %s\n", "-version", "View version page") 279 | Printf(" %-28s %s\n", "-timeout", "Set dial timeout for non proxied relays") 280 | Printf(" %-28s %s\n", "-detach", "Run relay service in background") 281 | Printf(" %-28s %s\n", "-log", "Specify the file to write logs to") 282 | Printf(" %-28s %s\n", "-cert", "Set TLS certificate file") 283 | Printf(" %-28s %s\n", "-key", "Set TLS key file") 284 | Printf(" %-28s %s\n", "-noauto", "Set relay to not autostart with daemon") 285 | Printf(" %-28s %s\n", "-store", "Output relay configs to config dir") 286 | Printf(" %-28s %s\n", "-interval", "Metrics refresh interval") 287 | } 288 | 289 | func version() { 290 | Printf("LocalRelay CLI - %s (%s.%s) [%s]\n", VERSION, BRANCH, COMMIT, runtime.Version()) 291 | Println() 292 | Println(" A reverse proxying program to allow services e.g. Nextcloud, Bitwarden etc to\n" + 293 | " be accessed over Tor (SOCKS5) even when the client app do not support\n" + 294 | " SOCKS proxies.") 295 | Println() 296 | Println() 297 | Println(" github.com/go-compile/localrelay") 298 | 299 | checkForUpdates() 300 | } 301 | -------------------------------------------------------------------------------- /cmd/localrelay/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/go-compile/localrelay/v2" 4 | 5 | // Relay is a config for a relay server 6 | type Relay struct { 7 | Name string 8 | Listener localrelay.TargetLink 9 | // DisableAutoStart will stop the daemon from auto starting this relay 10 | AutoRestart bool 11 | // Logging; stdout, ./filename.log 12 | Logging string 13 | 14 | Destinations []localrelay.TargetLink 15 | 16 | Tls TLS 17 | Proxies map[string]Proxy 18 | 19 | Loadbalance Loadbalance 20 | } 21 | 22 | // TLS is used when configuring https proxies 23 | type TLS struct { 24 | Certificate string 25 | Private string 26 | } 27 | 28 | // Proxy is used for relay forwarding 29 | type Proxy struct { 30 | Protocol string 31 | Address string 32 | Username string 33 | Password string 34 | } 35 | 36 | type Loadbalance struct { 37 | Enabled bool 38 | } 39 | 40 | // IsSet returns true if a proxy has been set 41 | func (p *Proxy) IsSet() bool { 42 | return p.Address != "" 43 | } 44 | -------------------------------------------------------------------------------- /cmd/localrelay/conns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/containerd/console" 9 | "github.com/go-compile/localrelay/pkg/api" 10 | ) 11 | 12 | func displayOpenConns(opt *options, onlyIPS bool) error { 13 | // make terminal raw to allow the use of colour on windows terminals 14 | current, _ := console.ConsoleFromFile(os.Stdout) 15 | // NOTE: Docker healthchecks will panic "provided file is not a console" 16 | 17 | if current != nil { 18 | defer current.Reset() 19 | } 20 | 21 | if current != nil { 22 | if err := current.SetRaw(); err != nil { 23 | log.Println(err) 24 | } 25 | } 26 | 27 | // we don't set terminal to raw here because print statements don't use 28 | // carriage returns 29 | conns, err := activeConnections() 30 | if err != nil { 31 | Printf("Daemon: \x1b[31m [OFFLINE] \x1b[0m\r\n") 32 | return err 33 | } 34 | 35 | filteredRelays := []string{} 36 | 37 | // build filter list for relays 38 | if len(opt.commands) > 1 { 39 | for _, relayName := range opt.commands[1:] { 40 | if !validateName(relayName) { 41 | Println("[WARN] Invalid relay name.") 42 | return nil 43 | } 44 | 45 | filteredRelays = append(filteredRelays, relayName) 46 | } 47 | } 48 | 49 | for _, conn := range conns { 50 | if len(filteredRelays) != 0 { 51 | if arrayContains(filteredRelays, conn.RelayName) { 52 | printConn(conn, onlyIPS) 53 | } 54 | } else { 55 | printConn(conn, onlyIPS) 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func printConn(conn api.Connection, onlyIPS bool) { 63 | if onlyIPS { 64 | Printf("%s\r\n", conn.RemoteAddr) 65 | return 66 | } 67 | 68 | Printf("%s -> %s (%s) (%s)\r\n", conn.RemoteAddr, conn.ForwardedAddr, conn.RelayName, formatDuration(time.Since(time.Unix(conn.Opened, 0)))) 69 | } 70 | 71 | func arrayContains(arr []string, element string) bool { 72 | for i := 0; i < len(arr); i++ { 73 | if arr[i] == element { 74 | return true 75 | } 76 | } 77 | 78 | return false 79 | } 80 | 81 | func dropConns(opt *options) error { 82 | // make terminal raw to allow the use of colour on windows terminals 83 | current, _ := console.ConsoleFromFile(os.Stdout) 84 | // NOTE: Docker healthchecks will panic "provided file is not a console" 85 | 86 | if current != nil { 87 | defer current.Reset() 88 | } 89 | 90 | if current != nil { 91 | if err := current.SetRaw(); err != nil { 92 | log.Println(err) 93 | } 94 | } 95 | 96 | return dropAll() 97 | } 98 | 99 | func dropConnsIP(opt *options) error { 100 | // make terminal raw to allow the use of colour on windows terminals 101 | current, _ := console.ConsoleFromFile(os.Stdout) 102 | // NOTE: Docker healthchecks will panic "provided file is not a console" 103 | 104 | if current != nil { 105 | defer current.Reset() 106 | } 107 | 108 | if current != nil { 109 | if err := current.SetRaw(); err != nil { 110 | log.Println(err) 111 | } 112 | } 113 | 114 | if len(opt.commands) < 2 { 115 | Println("Provide an ip address.") 116 | return nil 117 | } 118 | 119 | return dropIP(opt.commands[1]) 120 | } 121 | 122 | func dropConnsRelay(opt *options) error { 123 | // make terminal raw to allow the use of colour on windows terminals 124 | current, _ := console.ConsoleFromFile(os.Stdout) 125 | // NOTE: Docker healthchecks will panic "provided file is not a console" 126 | 127 | if current != nil { 128 | defer current.Reset() 129 | } 130 | 131 | if current != nil { 132 | if err := current.SetRaw(); err != nil { 133 | log.Println(err) 134 | } 135 | } 136 | 137 | if len(opt.commands) < 2 { 138 | Println("Provide a relay name.") 139 | return nil 140 | } 141 | 142 | return dropRelay(opt.commands[1]) 143 | } 144 | -------------------------------------------------------------------------------- /cmd/localrelay/daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | var ( 10 | // ErrIPCShutdownFail is returned when the daemon fails to shutdown when being 11 | // requested via IPC 12 | ErrIPCShutdownFail = errors.New("failed to shutdown daemon process via IPC") 13 | 14 | // ErrIPCForkFail is returned when trying to re-fork the daemon process 15 | ErrIPCForkFail = errors.New("ipc fork failed") 16 | 17 | ipcTimeout = time.Second 18 | 19 | // daemonStarted stores the time when the daemon was created 20 | daemonStarted time.Time 21 | ) 22 | -------------------------------------------------------------------------------- /cmd/localrelay/fork_posix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || !windows 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris !windows 3 | 4 | package main 5 | 6 | import ( 7 | "io" 8 | "net" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func createTmpIPC(connCh chan net.Conn) (string, io.Closer, error) { 14 | return "", nil, errors.New("not supported on your platform") 15 | } 16 | 17 | func elevatePrivileges(args []string) error { 18 | return errors.New("not supported on your platform") 19 | } 20 | 21 | func fork() error { 22 | return errors.New("not supported on your platform") 23 | } 24 | 25 | func forwardIO(opt *options) (net.Conn, error) { 26 | return nil, nil 27 | } 28 | -------------------------------------------------------------------------------- /cmd/localrelay/fork_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "io" 7 | "log" 8 | "net" 9 | "os" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "golang.org/x/sys/windows" 15 | "gopkg.in/natefinch/npipe.v2" 16 | ) 17 | 18 | // fork requests elevated privileges via UAC then forks the process and provides an 19 | // IPC pipe to communicate back to the master process. 20 | func fork() error { 21 | // create a IPC listener from the unprivileged process 22 | connCh := make(chan net.Conn) 23 | pipe, l, err := createTmpIPC(connCh) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | // close IPC listener 29 | defer l.Close() 30 | 31 | // request UAC to create new process and provide IPC pipe 32 | if err := elevatePrivileges(append(os.Args[1:], []string{"-ipc-stream-io-pipe", pipe}...)); err != nil { 33 | return err 34 | } 35 | 36 | // create new IPC timeout 37 | timeout := time.NewTicker(time.Second * 30) 38 | 39 | select { 40 | case <-timeout.C: 41 | timeout.Stop() 42 | return ErrIPCTimeout 43 | case conn := <-connCh: 44 | timeout.Stop() 45 | 46 | defer conn.Close() 47 | 48 | // stream elevated's stdout to us and our stdin to elevated process 49 | go io.Copy(conn, os.Stdin) 50 | io.Copy(os.Stdout, conn) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // elevatePrivileges 57 | func elevatePrivileges(args []string) error { 58 | verb := "runas" 59 | exe, err := os.Executable() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | cwd, err := os.Getwd() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | cmdArgs := strings.Join(args, " ") 70 | 71 | verbPtr, _ := syscall.UTF16PtrFromString(verb) 72 | exePtr, _ := syscall.UTF16PtrFromString(exe) 73 | cwdPtr, _ := syscall.UTF16PtrFromString(cwd) 74 | argPtr, _ := syscall.UTF16PtrFromString(cmdArgs) 75 | 76 | showCmd := int32(0) 77 | return windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd) 78 | } 79 | 80 | // createTmpIPC is used to forward io from a newly connected process to an existing one 81 | func createTmpIPC(connCh chan net.Conn) (string, io.Closer, error) { 82 | randBuf := make([]byte, 16) 83 | _, err := rand.Read(randBuf) 84 | if err != nil { 85 | return "", nil, err 86 | } 87 | 88 | pipe := `\\.\pipe\` + "localrelay-stream." + hex.EncodeToString(randBuf) 89 | 90 | // create a new name pipe with a unique name 91 | l, err := npipe.Listen(pipe) 92 | if err != nil { 93 | return pipe, l, err 94 | } 95 | 96 | // asynchronously wait for a connection then push to channel 97 | go func(l *npipe.PipeListener, connCh chan net.Conn) { 98 | conn, _ := l.Accept() 99 | 100 | connCh <- conn 101 | }(l, connCh) 102 | 103 | return pipe, l, nil 104 | } 105 | 106 | func forwardIO(opt *options) (net.Conn, error) { 107 | conn, err := npipe.DialTimeout(opt.ipcPipe, time.Second*5) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | stdout = conn 113 | log.SetOutput(conn) 114 | 115 | return conn, nil 116 | } 117 | -------------------------------------------------------------------------------- /cmd/localrelay/format_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestFormatBytes(t *testing.T) { 9 | fmt.Println(formatBytes(283)) 10 | fmt.Println(formatBytes(324235)) 11 | fmt.Println(formatBytes(3242335)) 12 | fmt.Println(formatBytes(2124235)) 13 | fmt.Println(formatBytes(3242321352355)) 14 | } 15 | -------------------------------------------------------------------------------- /cmd/localrelay/ipc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | type daemon struct{} 12 | 13 | const ( 14 | serviceName = "localrelayd" 15 | ipcSocket = "localrelay.ipc.socket" 16 | serviceDescription = "Localrelay daemon relay runner" 17 | ) 18 | 19 | var ( 20 | ipcListener io.Closer 21 | ErrIPCTimeout = errors.New("timed out waiting for IPC to connect") 22 | ) 23 | 24 | // handleConn takes a conn and handles each command 25 | func handleConn(conn net.Conn, srv *fasthttp.Server, l io.Closer) { 26 | defer conn.Close() 27 | 28 | srv.ServeConn(conn) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/localrelay/ipcClient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/go-compile/localrelay/pkg/api" 5 | ) 6 | 7 | // serviceRun takes paths to relay config files and then connects via IPC to 8 | // instruct the service to run these relays 9 | func serviceRun(relays []string) error { 10 | c, err := api.Connect() 11 | if err != nil { 12 | return err 13 | } 14 | 15 | defer c.Close() 16 | 17 | for _, relay := range relays { 18 | r, err := c.StartRelay(relay) 19 | 20 | for _, v := range r { 21 | Println(v) 22 | } 23 | 24 | if err != nil { 25 | return err 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func serviceStatus() (*api.Status, error) { 33 | c, err := api.Connect() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | defer c.Close() 39 | 40 | status, err := c.GetStatus() 41 | return status, err 42 | } 43 | 44 | func stopRelay(relayName string) error { 45 | c, err := api.Connect() 46 | if err != nil { 47 | return err 48 | } 49 | 50 | defer c.Close() 51 | 52 | err = c.StopRelay(relayName) 53 | if err == nil { 54 | Printf("Relay %q has been stopped.\n", relayName) 55 | return nil 56 | } 57 | 58 | return err 59 | } 60 | 61 | func activeConnections() ([]api.Connection, error) { 62 | c, err := api.Connect() 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | defer c.Close() 68 | 69 | return c.GetConnections() 70 | } 71 | 72 | func dropAll() error { 73 | c, err := api.Connect() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | defer c.Close() 79 | 80 | return c.DropAll() 81 | } 82 | 83 | func dropIP(ip string) error { 84 | c, err := api.Connect() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | defer c.Close() 90 | 91 | if err := c.DropIP(ip); err == nil { 92 | Printf("All connections from %q have been dropped.\r\n", ip) 93 | return nil 94 | } 95 | 96 | Printf("Failed to drop connections. Err: %s.\n", err) 97 | return nil 98 | } 99 | 100 | func dropRelay(relay string) error { 101 | c, err := api.Connect() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | defer c.Close() 107 | 108 | if err := c.DropRelay(relay); err == nil { 109 | Printf("All connections from %q have been dropped.\r\n", relay) 110 | return nil 111 | } else { 112 | Printf("Failed to drop connections. Status code: %s.\n", err) 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /cmd/localrelay/ipcServer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/fasthttp/router" 12 | "github.com/go-compile/localrelay/pkg/api" 13 | "github.com/go-compile/localrelay/v2" 14 | "github.com/naoina/toml" 15 | 16 | "github.com/valyala/fasthttp" 17 | ) 18 | 19 | type msgResponse struct { 20 | Message string `json:"message"` 21 | } 22 | 23 | func newIPCServer() *fasthttp.Server { 24 | r := router.New() 25 | assignIPCRoutes(r) 26 | 27 | return &fasthttp.Server{ 28 | Handler: ipcHeadersMiddleware(r.Handler), 29 | Name: "localrelay-ipc", 30 | ReadTimeout: time.Second * 60, 31 | WriteTimeout: time.Second * 60, 32 | } 33 | } 34 | 35 | func assignIPCRoutes(r *router.Router) { 36 | r.GET("/", ipcRouteRoot) 37 | r.GET("/stop/{relay}", ipcRouteStop) 38 | r.POST("/run", ipcRouteRun) 39 | r.GET("/status", ipcRouteStatus) 40 | r.GET("/connections", ipcRouteConns) 41 | r.GET("/drop", ipcRouteDropAll) 42 | r.GET("/drop/ip/{ip}", ipcRouteDropIP) 43 | r.GET("/drop/relay/{relay}", ipcRouteDropRelay) 44 | } 45 | 46 | func ipcHeadersMiddleware(handler fasthttp.RequestHandler) fasthttp.RequestHandler { 47 | return func(ctx *fasthttp.RequestCtx) { 48 | ctx.SetContentType("application/json") 49 | handler(ctx) 50 | } 51 | } 52 | 53 | func ipcRouteRoot(ctx *fasthttp.RequestCtx) { 54 | ctx.Write([]byte(`{"version":"` + VERSION + `","commit":"` + COMMIT + `"}`)) 55 | } 56 | 57 | func ipcRouteStop(ctx *fasthttp.RequestCtx) { 58 | relayName := ctx.UserValue("relay").(string) 59 | 60 | var relay *localrelay.Relay 61 | for _, r := range runningRelays() { 62 | if r.Name == strings.ToLower(relayName) { 63 | relay = r 64 | break 65 | } 66 | } 67 | 68 | // relay not found 69 | if relay == nil { 70 | ctx.SetStatusCode(404) 71 | ctx.Write([]byte(`{"message":"Relay not found."}`)) 72 | return 73 | } 74 | 75 | if err := relay.Close(); err != nil { 76 | ctx.SetStatusCode(500) 77 | ctx.Write([]byte(`{"message":"Error encountered when trying to close the relay."}`)) 78 | return 79 | } 80 | 81 | // send success 82 | ctx.SetStatusCode(200) 83 | ctx.Write([]byte(`{"message":"Relay has been closed."}`)) 84 | return 85 | } 86 | 87 | func ipcRouteRun(ctx *fasthttp.RequestCtx) { 88 | var files []string 89 | 90 | if err := json.Unmarshal(ctx.Request.Body(), &files); err != nil { 91 | ctx.SetStatusCode(400) 92 | ctx.Write([]byte(`{"message":"Invalid json body."}`)) 93 | return 94 | } 95 | 96 | // TODO: support run multiple files 97 | if len(files) != 1 { 98 | ctx.SetStatusCode(400) 99 | ctx.Write([]byte(`{"message":"Endpoint currently requires at maximum and minimum one relay."}`)) 100 | return 101 | } 102 | 103 | relayFile := files[0] 104 | 105 | exists, err := pathExists(relayFile) 106 | if err != nil { 107 | ctx.SetStatusCode(500) 108 | ctx.Write([]byte(`{"message":"Relay path could not be verified."}`)) 109 | return 110 | } 111 | 112 | if !exists { 113 | ctx.SetStatusCode(404) 114 | ctx.Write([]byte(`{"message":"Relay file does not exist."}`)) 115 | return 116 | } 117 | 118 | f, err := os.Open(relayFile) 119 | if err != nil { 120 | ctx.SetStatusCode(500) 121 | ctx.Write([]byte(`{"message":"Failed to open relay config."}`)) 122 | return 123 | } 124 | 125 | var relay Relay 126 | if err := toml.NewDecoder(f).Decode(&relay); err != nil { 127 | f.Close() 128 | ctx.SetStatusCode(500) 129 | ctx.Write([]byte(`{"message":"Failed to decode relay config."}`)) 130 | return 131 | } 132 | 133 | f.Close() 134 | 135 | if isRunning(relay.Name) { 136 | ctx.SetStatusCode(500) 137 | ctx.Write([]byte(`{"message":"Relay is already running."}`)) 138 | return 139 | } 140 | 141 | if err := launchRelays([]Relay{relay}, false); err != nil { 142 | ctx.SetStatusCode(500) 143 | ctx.Write([]byte(`{"message":` + strconv.Quote("Error launching relay. "+err.Error()) + `}`)) 144 | return 145 | } 146 | 147 | ctx.SetStatusCode(200) 148 | ctx.Write([]byte(`{"message":"Relay successfully launched."}`)) 149 | return 150 | } 151 | 152 | func ipcRouteStatus(ctx *fasthttp.RequestCtx) { 153 | relayMetrics := make(map[string]api.Metrics) 154 | 155 | relays := runningRelaysCopy() 156 | for _, r := range relays { 157 | active, total := r.Metrics.Connections() 158 | relayMetrics[r.Name] = api.Metrics{ 159 | In: r.Metrics.Download(), 160 | Out: r.Metrics.Upload(), 161 | Active: active, 162 | DialAvg: r.DialerAvg(), 163 | TotalConns: total, 164 | TotalRequests: r.Metrics.Requests(), 165 | } 166 | } 167 | 168 | ctx.SetStatusCode(200) 169 | json.NewEncoder(ctx).Encode(&api.Status{ 170 | Relays: relays, 171 | Pid: os.Getpid(), 172 | Version: VERSION, 173 | Started: daemonStarted.Unix(), 174 | 175 | Metrics: relayMetrics, 176 | }) 177 | } 178 | 179 | func ipcRouteConns(ctx *fasthttp.RequestCtx) { 180 | relayConns := make([]api.Connection, 0, 200) 181 | 182 | relays := runningRelaysCopy() 183 | for _, r := range relays { 184 | for _, conn := range r.GetConns() { 185 | relayConns = append(relayConns, api.Connection{ 186 | LocalAddr: conn.Conn.LocalAddr().String(), 187 | RemoteAddr: conn.Conn.RemoteAddr().String(), 188 | Network: conn.Conn.LocalAddr().Network(), 189 | 190 | RelayName: r.Name, 191 | RelayHost: string(r.Listener), 192 | ForwardedAddr: conn.RemoteAddr, 193 | 194 | Opened: conn.Opened.Unix(), 195 | }) 196 | } 197 | } 198 | 199 | ctx.SetStatusCode(200) 200 | json.NewEncoder(ctx).Encode(relayConns) 201 | } 202 | 203 | func ipcRouteDropAll(ctx *fasthttp.RequestCtx) { 204 | relays := runningRelaysCopy() 205 | // iterate through all relays and close every connection 206 | for _, r := range relays { 207 | for _, conn := range r.GetConns() { 208 | go conn.Conn.Close() 209 | } 210 | } 211 | } 212 | 213 | func ipcRouteDropIP(ctx *fasthttp.RequestCtx) { 214 | ip := ctx.UserValue("ip").(string) 215 | 216 | relays := runningRelaysCopy() 217 | // iterate through all relays and close every connection 218 | for _, r := range relays { 219 | for _, conn := range r.GetConns() { 220 | host, _, err := net.SplitHostPort(conn.Conn.RemoteAddr().String()) 221 | if err != nil { 222 | // ignore error 223 | continue 224 | } 225 | 226 | if host == ip { 227 | go conn.Conn.Close() 228 | } 229 | } 230 | } 231 | } 232 | 233 | func ipcRouteDropRelay(ctx *fasthttp.RequestCtx) { 234 | relay := ctx.UserValue("relay").(string) 235 | 236 | relays := runningRelaysCopy() 237 | // iterate through all relays and close every connection 238 | for _, r := range relays { 239 | if r.Name != relay { 240 | continue 241 | } 242 | 243 | for _, conn := range r.GetConns() { 244 | go conn.Conn.Close() 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /cmd/localrelay/ipc_posix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || !windows 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris !windows 3 | 4 | package main 5 | 6 | import ( 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "syscall" 13 | ) 14 | 15 | var ( 16 | // ipcPathPrefix is the dir which comes before the unix socket 17 | ipcPathPrefix = "/var/run/" 18 | ) 19 | 20 | type logger struct { 21 | w io.WriteCloser 22 | relayName string 23 | } 24 | 25 | func fileOwnership(stat os.FileInfo) (string, error) { 26 | s := stat.Sys().(*syscall.Stat_t) 27 | uid := s.Uid 28 | gid := s.Gid 29 | 30 | if uid != gid { 31 | return strconv.Itoa(int(uid)) + "," + strconv.Itoa(int(gid)), nil 32 | } 33 | 34 | return strconv.Itoa(int(uid)), nil 35 | } 36 | 37 | func runningAsRoot() bool { 38 | return os.Geteuid() == 0 39 | } 40 | 41 | func (l *logger) Write(b []byte) (int, error) { 42 | return l.w.Write(b) 43 | } 44 | 45 | func (l *logger) Close() error { 46 | return l.w.Close() 47 | } 48 | 49 | func newLogger(relayName string) *logger { 50 | f, err := os.OpenFile(filepath.Join("/var/log/localrelay/", relayName+".log"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | return &logger{ 56 | w: f, 57 | relayName: relayName, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cmd/localrelay/ipc_posix_test.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || !windows 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris !windows 3 | 4 | package main 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-compile/localrelay/internal/ipc" 11 | ) 12 | 13 | func TestIPCPosix(t *testing.T) { 14 | ipc.SetPathPrefix("./") 15 | 16 | go func() { 17 | l, err := ipc.NewListener() 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | defer l.Close() 23 | 24 | ipcListener = l 25 | 26 | err = ipc.ListenServe(l, newIPCServer()) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | }() 31 | 32 | time.Sleep(time.Second) 33 | 34 | // due to the above embeded function being in a different 35 | // gorutine, t.Fatal will only effect the above subroutine. 36 | // Hence needing to check if ipcListener is nil. 37 | if ipcListener == nil { 38 | t.Fatal("ipc listener could not startup") 39 | } 40 | 41 | _, err := serviceStatus() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | // close IPC listener 47 | ipcListener.Close() 48 | } 49 | -------------------------------------------------------------------------------- /cmd/localrelay/ipc_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "golang.org/x/sys/windows" 8 | "golang.org/x/sys/windows/svc/eventlog" 9 | ) 10 | 11 | type logger struct { 12 | w *eventlog.Log 13 | relayName string 14 | } 15 | 16 | func fileOwnership(stat os.FileInfo) (string, error) { 17 | // TODO: get owner of file on windows 18 | return "", nil 19 | } 20 | 21 | func runningAsRoot() bool { 22 | token := windows.GetCurrentProcessToken() 23 | defer token.Close() 24 | 25 | return token.IsElevated() 26 | } 27 | 28 | func (l *logger) Write(b []byte) (int, error) { 29 | return len(b), l.w.Info(1, string(b)) 30 | } 31 | 32 | func (l *logger) Close() error { 33 | return l.w.Close() 34 | } 35 | 36 | func newLogger(relayName string) *logger { 37 | w, err := eventlog.Open("localrelayd") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | return &logger{ 43 | w: w, 44 | relayName: relayName, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/localrelay/ipc_windows_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/go-compile/localrelay/internal/ipc" 8 | ) 9 | 10 | func TestIPCWindows(t *testing.T) { 11 | go func() { 12 | // if IPC listen fails make sure your host system isn't 13 | // already running localrelay. Run localrelay stop 14 | l, err := ipc.NewListener() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | defer l.Close() 20 | 21 | ipcListener = l 22 | 23 | err = ipc.ListenServe(l, newIPCServer()) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | }() 28 | 29 | time.Sleep(time.Millisecond * 300) 30 | 31 | // due to the above embeded function being in a different 32 | // gorutine, t.Fatal will only effect the above subroutine. 33 | // Hence needing to check if ipcListener is nil. 34 | if ipcListener == nil { 35 | t.Fatal("ipc listener could not startup") 36 | } 37 | 38 | _, err := serviceStatus() 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | // close IPC listener 44 | ipcListener.Close() 45 | } 46 | -------------------------------------------------------------------------------- /cmd/localrelay/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/cmd/localrelay/logo.ico -------------------------------------------------------------------------------- /cmd/localrelay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "runtime" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-compile/localrelay/internal/httperror" 11 | "github.com/kardianos/service" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | var ( 16 | // VERSION uses semantic versioning 17 | VERSION = "(Unknown Version)" 18 | COMMIT = "0000000" 19 | BRANCH = "(Unknown Branch)" 20 | ) 21 | 22 | var ( 23 | daemonService service.Service 24 | ) 25 | 26 | func main() { 27 | // set default output, may be changed if ipc stream is enabled 28 | stdout = os.Stdout 29 | 30 | serviceConfig := &service.Config{ 31 | Name: serviceName, 32 | DisplayName: serviceName, 33 | Description: serviceDescription, 34 | Arguments: []string{"start-service-daemon"}, 35 | Option: service.KeyValue{ 36 | "DelayedAutoStart": false, 37 | "OnFailure": "restart", 38 | "OnFailureDelayDuration": "5s", 39 | }, 40 | } 41 | 42 | prg := &daemon{} 43 | s, err := service.New(prg, serviceConfig) 44 | if err != nil { 45 | log.Fatalf("[Error] Failed to create service: %s\n", err) 46 | } 47 | 48 | daemonService = s 49 | 50 | httperror.SetVersion(VERSION) 51 | 52 | opt, err := parseArgs() 53 | if err == nil && opt == nil { 54 | return 55 | } 56 | 57 | if err != nil { 58 | Println(err) 59 | return 60 | } 61 | 62 | // if process was forked forward stdout 63 | if len(opt.ipcPipe) != 0 { 64 | conn, err := forwardIO(opt) 65 | if err != nil { 66 | Println(err) 67 | return 68 | } 69 | 70 | defer conn.Close() 71 | } 72 | 73 | if len(opt.commands) == 0 { 74 | help() 75 | return 76 | } 77 | 78 | for i := 0; i < len(opt.commands); i++ { 79 | 80 | switch strings.ToLower(opt.commands[i]) { 81 | case "help", "h", "?": 82 | help() 83 | return 84 | case "version", "v": 85 | version() 86 | return 87 | case "new": 88 | if err := newRelay(opt, i, opt.commands); err != nil { 89 | Println(err) 90 | } 91 | 92 | return 93 | case "run": 94 | // detach requires admin to run 95 | if opt.detach { 96 | if !privCommand(true) { 97 | return 98 | } 99 | } 100 | 101 | if err := runRelays(opt, i, opt.commands); err != nil { 102 | Println(err) 103 | } 104 | 105 | return 106 | // stop will shutdown the daemon service 107 | case "stop": 108 | if !privCommand(true) { 109 | return 110 | } 111 | 112 | if len(opt.commands) == 1 { 113 | if err := s.Stop(); err != nil { 114 | log.Fatalf("[Error] Failed to stop service: %s\n", err) 115 | } 116 | 117 | Println("Daemon has been shutdown") 118 | return 119 | } 120 | 121 | if err := stopRelay(opt.commands[1]); err != nil { 122 | log.Fatalf("[Error] Failed to stop service: %s\n", err) 123 | } 124 | 125 | return 126 | 127 | // install will register the daemon service 128 | case "install": 129 | secure, msg, err := securityCheckBinary() 130 | if err != nil { 131 | log.Fatal(errors.Wrap(err, "checking binary security")) 132 | } 133 | 134 | if !secure { 135 | Printf("WARNING!\n Security issues detected. Installation Blocked!\n"+ 136 | "Please follow localrelay's service installation guide to avoid inadvertently"+ 137 | "exposing your system to security vulnerabilities. It is likely your binary has"+ 138 | "insecure permissions.\n\nAudit Results:\n%s\n", msg) 139 | return 140 | } 141 | 142 | if !privCommand(true) { 143 | return 144 | } 145 | 146 | if err := s.Install(); err != nil { 147 | log.Fatalf("[Error] Failed to install service: %s\n", err) 148 | } 149 | 150 | Println("Daemon service has been installed.") 151 | 152 | return 153 | case "uninstall": 154 | if !privCommand(true) { 155 | return 156 | } 157 | 158 | if err := s.Uninstall(); err != nil { 159 | log.Fatalf("[Error] Failed to uninstall service: %s\n", err) 160 | } 161 | 162 | Println("Daemon service has been uninstalled.") 163 | 164 | return 165 | // start-service-daemon will run as the service daemon 166 | case "start-service-daemon": 167 | log.Printf("[Version] %s (%s.%s)\n", VERSION, BRANCH, COMMIT) 168 | daemonStarted = time.Now() 169 | if err := s.Run(); err != nil { 170 | log.Fatalf("[Error] Failed to run service: %s\n", err) 171 | } 172 | 173 | return 174 | // restart will rerun the service but will not restore previously ran relays 175 | case "restart": 176 | if !privCommand(true) { 177 | return 178 | } 179 | 180 | if err := s.Restart(); err != nil { 181 | log.Fatalf("[Error] Failed to restart service: %s\n", err) 182 | } 183 | 184 | Println("Daemon has been restarted") 185 | case "start": 186 | if !privCommand(true) { 187 | return 188 | } 189 | 190 | if err := s.Start(); err != nil { 191 | log.Fatalf("[Error] Failed to start service: %s\n", err) 192 | } 193 | 194 | Println("Daemon has been started") 195 | return 196 | case "conns", "connections": 197 | if !privCommand(true) { 198 | return 199 | } 200 | 201 | if err := displayOpenConns(opt, false); err != nil { 202 | Println(err) 203 | os.Exit(1) 204 | } 205 | 206 | return 207 | case "ips": 208 | if !privCommand(true) { 209 | return 210 | } 211 | 212 | if err := displayOpenConns(opt, true); err != nil { 213 | Println(err) 214 | os.Exit(1) 215 | } 216 | 217 | return 218 | case "status": 219 | if !privCommand(true) { 220 | return 221 | } 222 | 223 | if err := relayStatus(); err != nil { 224 | Println(err) 225 | os.Exit(1) 226 | } 227 | 228 | return 229 | // TODO: add logs. Connect via IPC and show live view 230 | case "metrics", "monitor": 231 | if !privCommand(false) { 232 | return 233 | } 234 | 235 | if err := relayMetrics(opt); err != nil { 236 | Println(err) 237 | } 238 | 239 | return 240 | case "drop": 241 | if !privCommand(true) { 242 | return 243 | } 244 | 245 | if err := dropConns(opt); err != nil { 246 | Println(err) 247 | } 248 | return 249 | case "dropip": 250 | if !privCommand(true) { 251 | return 252 | } 253 | 254 | if err := dropConnsIP(opt); err != nil { 255 | Println(err) 256 | } 257 | return 258 | case "droprelay": 259 | if !privCommand(true) { 260 | return 261 | } 262 | 263 | if err := dropConnsRelay(opt); err != nil { 264 | Println(err) 265 | } 266 | return 267 | default: 268 | Printf("Unrecognised command %q\n", opt.commands[i]) 269 | return 270 | } 271 | } 272 | } 273 | 274 | // privCommand will handle permission elevation. 275 | // If bool is false exit program without errors. 276 | // If bool true the user has permission. 277 | func privCommand(autoFork bool) bool { 278 | if !runningAsRoot() { 279 | if runtime.GOOS == "windows" && autoFork { 280 | // UAC prompt 281 | if err := fork(); err != nil { 282 | Println(err) 283 | return false 284 | } 285 | } else { 286 | Println("Elevated privileges required.") 287 | } 288 | 289 | return false 290 | } 291 | 292 | return true 293 | } 294 | -------------------------------------------------------------------------------- /cmd/localrelay/metrics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "runtime" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | "github.com/containerd/console" 14 | "github.com/go-compile/localrelay/pkg/api" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | var ( 19 | // ErrRelayNotRunning is returned if the selected relay isn't running 20 | ErrRelayNotRunning = errors.New("relay not running") 21 | ) 22 | 23 | type namedMetrics struct { 24 | name string 25 | api.Metrics 26 | } 27 | 28 | func relayMetrics(opt *options) error { 29 | client, err := api.Connect() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | defer client.Close() 35 | 36 | status, err := client.GetStatus() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // setting terminal to raw on linux results in SIGTERM not registering 42 | if runtime.GOOS == "windows" { 43 | // make terminal raw to allow the use of colour on windows terminals 44 | current := console.Current() 45 | defer current.Reset() 46 | 47 | if err := current.SetRaw(); err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | 52 | relays := []string{} 53 | 54 | // build filter list for relays 55 | if len(opt.commands) > 1 { 56 | for _, relayName := range opt.commands[1:] { 57 | if !validateName(relayName) { 58 | Printf("[WARN] Invalid relay name.") 59 | return nil 60 | } 61 | 62 | if _, ok := status.Metrics[strings.ToLower(relayName)]; !ok { 63 | Printf("Relay %q is not running.\n", relayName) 64 | return nil 65 | } 66 | 67 | relays = append(relays, relayName) 68 | } 69 | } 70 | 71 | sig := make(chan os.Signal, 1) 72 | go func() { 73 | signal.Notify(sig, os.Interrupt) 74 | 75 | if runtime.GOOS == "windows" { 76 | // listen for interrupt 77 | for { 78 | buf := make([]byte, 4) 79 | n, err := os.Stdin.Read(buf) 80 | if err != nil { 81 | os.Exit(0) 82 | return 83 | } 84 | 85 | // break on keys "q", "ESC", "CTRL + C" etc 86 | if bytes.Equal(buf[:n], []byte{3}) || bytes.Equal(buf[:n], []byte{8}) || 87 | bytes.Equal(buf[:n], []byte{113}) || bytes.Equal(buf[:n], []byte{27}) { 88 | close(sig) 89 | break 90 | } 91 | } 92 | } 93 | }() 94 | 95 | running := 0 96 | // set ticker to micro second to triger metrics to render instantly 97 | // then change tricker within case statement to correct interval 98 | ticker := time.NewTicker(time.Microsecond) 99 | defer ticker.Stop() 100 | 101 | for { 102 | select { 103 | case <-sig: 104 | // make a guess how far to move the cursor 105 | if len(relays) != 0 { 106 | Printf("\x1b[%dB", (len(relays)*2)+2) 107 | } else { 108 | Printf("\x1b[%dB", (len(status.Metrics)*2)+2) 109 | } 110 | 111 | return nil 112 | case <-ticker.C: 113 | ticker.Reset(opt.interval) 114 | status, err := client.GetStatus() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | // if relay has gone offline clear bottom of screen 120 | if x := len(status.Metrics); x < running { 121 | Printf("\x1b[0J") 122 | } 123 | 124 | running = len(status.Metrics) 125 | totalInOut := [2]int{} 126 | 127 | metrics := make([]namedMetrics, 0, running) 128 | for k, m := range status.Metrics { 129 | metrics = append(metrics, namedMetrics{k, m}) 130 | totalInOut[0] += m.In 131 | totalInOut[1] += m.Out 132 | } 133 | 134 | // sort alphabetically 135 | sort.SliceStable(metrics, func(i, j int) bool { 136 | return metrics[i].name < metrics[j].name 137 | }) 138 | 139 | count := len(relays) 140 | 141 | // if not filter present, show all 142 | if len(relays) == 0 { 143 | count = len(metrics) 144 | 145 | for _, m := range metrics { 146 | printMetrics(m.name, m.Metrics) 147 | } 148 | } else { 149 | // sort will be based on order of input args 150 | for _, v := range relays { 151 | m, ok := status.Metrics[v] 152 | if !ok { 153 | return errors.Wrapf(ErrRelayNotRunning, "%s", v) 154 | } 155 | 156 | printMetrics(v, m) 157 | } 158 | } 159 | 160 | Printf("\r\n\x1b[2K [Running Relays: %d] [In/Out: %s/%s]\r\n", running, formatBytes(totalInOut[0]), formatBytes(totalInOut[1])) 161 | Printf("\x1b[%dA", (count*2)+2) 162 | } 163 | } 164 | } 165 | 166 | func printMetrics(name string, m api.Metrics) { 167 | Printf("\x1b[2K \x1b[90m%s\x1b[0m\r\n\x1b[2K [In/Out:%s/%s] [DialAvg:%dms] [Active:%d] [Total:%d]\r\n", name, formatBytes(m.In), formatBytes(m.Out), m.DialAvg, m.Active, m.TotalConns+m.TotalRequests) 168 | } 169 | -------------------------------------------------------------------------------- /cmd/localrelay/new.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/chzyer/readline" 11 | "github.com/go-compile/localrelay/v2" 12 | "github.com/naoina/toml" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | var ( 17 | // ErrParseBool is returned when a boolean can not be parsed 18 | ErrParseBool = errors.New("cannot parse boolean") 19 | 20 | relayNameFormat = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") 21 | ) 22 | 23 | func newRelay(opt *options, i int, cmd []string) error { 24 | if len(opt.commands)-1 <= i { 25 | Println("[WARN] Relay name was not provided.") 26 | return nil 27 | } 28 | 29 | if err := createConfigDir(); err != nil { 30 | Printf("[WARN] Failed to create config dir: %s\n", err) 31 | } 32 | 33 | name := cmd[i+1] 34 | if !validateName(name) { 35 | Println("[WARN] Invalid relay name.") 36 | return nil 37 | } 38 | 39 | if opt.host == "" { 40 | Println("[WARN] Host was not set.") 41 | return nil 42 | } 43 | 44 | if opt.destination == "" { 45 | Println("[WARN] Destination was not set.") 46 | return nil 47 | } 48 | 49 | switch strings.ToLower(opt.proxy.Protocol) { 50 | case "", "socks5": 51 | // validate socks5 or empty is ok 52 | default: 53 | Println("[WARN] Unsupported proxy type.") 54 | return nil 55 | } 56 | 57 | listener := localrelay.TargetLink(string(opt.proxyType) + "://" + opt.host) 58 | 59 | relay := Relay{ 60 | Name: name, 61 | Listener: listener, 62 | Logging: opt.logs, 63 | 64 | Tls: TLS{ 65 | Certificate: opt.certificate, 66 | Private: opt.key, 67 | }, 68 | 69 | Loadbalance: Loadbalance{ 70 | Enabled: opt.loadbalance, 71 | }, 72 | 73 | Proxies: make(map[string]Proxy), 74 | AutoRestart: !opt.DisableAutoStart, 75 | } 76 | 77 | // assign the mutliple remotes 78 | dsts := strings.Split(opt.destination, ",") 79 | for di, dst := range dsts { 80 | destination := localrelay.TargetLink(string(opt.proxyType) + "://" + dst) 81 | if opt.proxy.IsSet() && !contains(opt.proxyIgnore, di+1) { 82 | destination += "/?proxy=proxy-a" 83 | } 84 | 85 | relay.Destinations = append(relay.Destinations, destination) 86 | } 87 | 88 | if opt.proxy.IsSet() { 89 | relay.Proxies["proxy-a"] = opt.proxy 90 | } 91 | 92 | filename := name + ".toml" 93 | // If output file has been set use that instead 94 | if opt.output != "" { 95 | filename = opt.output 96 | } else if opt.store { 97 | // store config file in daemon config dir 98 | filename = filepath.Join(relaysDir(), filename) 99 | } 100 | 101 | f, err := os.OpenFile(filename, os.O_WRONLY, os.ModeExclusive) 102 | if err != nil { 103 | if os.IsNotExist(err) { 104 | 105 | f, err := os.Create(filename) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if err := toml.NewEncoder(f).Encode(relay); err != nil { 111 | return err 112 | } 113 | 114 | if err := f.Close(); err != nil { 115 | return err 116 | } 117 | 118 | Printf("[Info] Relay config written to %s\n", filename) 119 | 120 | return nil 121 | } 122 | 123 | return errors.Wrap(err, "opening file") 124 | } 125 | 126 | defer f.Close() 127 | 128 | prompt, err := readline.New("> ") 129 | if err != nil { 130 | return err 131 | } 132 | 133 | Println("File already exits, do you want to overwrite it?") 134 | prompt.SetPrompt("Overwrite (y/n): ") 135 | overwrite, err := prompt.ReadlineWithDefault("n") 136 | if err != nil { 137 | return err 138 | } 139 | 140 | ow, err := parseBool(overwrite) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | if !ow { 146 | Println("[Info] Aborting, file was not overwritten") 147 | return nil 148 | } 149 | 150 | if err := f.Truncate(0); err != nil { 151 | return err 152 | } 153 | 154 | if err := toml.NewEncoder(f).Encode(relay); err != nil { 155 | return err 156 | } 157 | 158 | Printf("[Info] Relay config written to %s\n", filename) 159 | 160 | return nil 161 | } 162 | 163 | func parseBool(input string) (bool, error) { 164 | switch strings.ToLower(input) { 165 | case "true", "1", "yes", "on", "active", "y": 166 | return true, nil 167 | case "false", "0", "no", "off", "disabled", "n": 168 | return false, nil 169 | default: 170 | return false, ErrParseBool 171 | } 172 | } 173 | 174 | func createConfigDir() error { 175 | home := configSystemDir() 176 | dir := filepath.Join(home, configDirSuffix) 177 | 178 | exists, err := pathExists(dir) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | // already exists, don't recreate it 184 | if !exists { 185 | if err := os.Mkdir(dir, 0644); err != nil { 186 | return err 187 | } 188 | } 189 | 190 | return createLogDir() 191 | } 192 | 193 | func createLogDir() error { 194 | if runtime.GOOS == "windows" { 195 | return nil 196 | } 197 | 198 | dir := "/var/log/localrelay/" 199 | 200 | exists, err := pathExists(dir) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | // already exists, don't recreate it 206 | if exists { 207 | return nil 208 | } 209 | 210 | return os.MkdirAll(dir, 0644) 211 | } 212 | 213 | func pathExists(path string) (bool, error) { 214 | _, err := os.Stat(path) 215 | if err == nil { 216 | return true, nil 217 | } 218 | 219 | return !os.IsNotExist(err), nil 220 | } 221 | 222 | func validateName(name string) bool { 223 | return relayNameFormat.MatchString(name) 224 | } 225 | 226 | func contains(l []int, x int) bool { 227 | for i := 0; i < len(l); i++ { 228 | if l[i] == x { 229 | return true 230 | } 231 | } 232 | 233 | return false 234 | } 235 | -------------------------------------------------------------------------------- /cmd/localrelay/print.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | var stdout io.Writer 9 | 10 | // Println formats using the default formats for its operands and writes to standard output. 11 | // Spaces are always added between operands and a newline is appended. 12 | // It returns the number of bytes written and any write error encountered. 13 | func Println(a ...interface{}) (n int, err error) { 14 | return fmt.Fprintln(stdout, a...) 15 | } 16 | 17 | // Printf formats according to a format specifier and writes to standard output. 18 | // It returns the number of bytes written and any write error encountered. 19 | func Printf(format string, a ...interface{}) (n int, err error) { 20 | return fmt.Fprintf(stdout, format, a...) 21 | } 22 | -------------------------------------------------------------------------------- /cmd/localrelay/rsrc_windows_386.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/cmd/localrelay/rsrc_windows_386.syso -------------------------------------------------------------------------------- /cmd/localrelay/rsrc_windows_amd64.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/cmd/localrelay/rsrc_windows_amd64.syso -------------------------------------------------------------------------------- /cmd/localrelay/run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/go-compile/localrelay/v2" 15 | "github.com/kardianos/service" 16 | "github.com/naoina/toml" 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | var ( 21 | // activeRelays is a list of relays being ran 22 | activeRelays map[string]*localrelay.Relay 23 | activeRelaysM sync.Mutex 24 | 25 | // logDescriptors is a list of relay name to file descriptor 26 | // this is used when shutting down. 27 | logDescriptors map[string]*io.Closer 28 | 29 | forkIdentifier = "exec.signal-forked-process-true" 30 | 31 | // configDirSuffix is prepended with the user's home dir. 32 | // This is where the relay configs are stored. 33 | configDirSuffix = "localrelay/" 34 | 35 | ErrInvalidRelayName = errors.New("invalid relay name") 36 | ) 37 | 38 | func init() { 39 | activeRelays = make(map[string]*localrelay.Relay, 3) 40 | logDescriptors = make(map[string]*io.Closer, 3) 41 | } 42 | 43 | func runRelays(opt *options, i int, cmd []string) error { 44 | home := configSystemDir() 45 | 46 | relayPaths := make([]string, 0, len(cmd[i+1:])) 47 | 48 | // Read all relay config files and decode them 49 | relays := make([]Relay, 0, len(cmd[i+1:])) 50 | for _, file := range cmd[i+1:] { 51 | 52 | // if @ used as prefix grab the file from the user profile's 53 | // config location 54 | if strings.HasPrefix(file, "@") { 55 | file = filepath.Join(home, configDirSuffix, file[1:]) 56 | } 57 | 58 | file, err := filepath.Abs(file) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | relay, err := readRelayConfig(file) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | relays = append(relays, *relay) 69 | // append path here so we validate the config first before sending to 70 | // service. 71 | relayPaths = append(relayPaths, file) 72 | 73 | } 74 | 75 | if len(relays) == 0 { 76 | Println("[WARN] No relay configs provided.") 77 | return nil 78 | } 79 | 80 | Printf("Loaded: %d relays\n", len(relays)) 81 | 82 | // if detach is enable fork process and start daemon 83 | if opt.detach { 84 | running, err := daemonService.Status() 85 | if err != nil { 86 | return errors.Wrap(err, "fetching service status") 87 | } 88 | 89 | if running != service.StatusRunning { 90 | Println("[Info] Service not running.") 91 | 92 | if err := daemonService.Start(); err != nil { 93 | log.Fatalf("[Error] Failed to start service: %s\n", err) 94 | } 95 | 96 | Println("[Info] Service has been started.") 97 | 98 | // wait for process to launch 99 | time.Sleep(time.Millisecond * 50) 100 | } 101 | 102 | return serviceRun(relayPaths) 103 | } 104 | 105 | return launchRelays(relays, true) 106 | } 107 | 108 | func launchRelays(relays []Relay, wait bool) error { 109 | // TODO: listen for sigterm signal and softly shutdown 110 | 111 | wg := sync.WaitGroup{} 112 | 113 | for i := range relays { 114 | r := relays[i] 115 | 116 | if !validateName(r.Name) { 117 | return ErrInvalidRelayName 118 | } 119 | 120 | Printf("[Info] [Relay:%d] Starting %q on %q\n", i+1, r.Name, r.Listener) 121 | 122 | w := io.MultiWriter(newLogger(r.Name), os.Stdout) 123 | if !isService { 124 | // not running as a serivce 125 | w = os.Stdout 126 | } 127 | 128 | // was a custom file provided? 129 | if r.Logging != "stdout" && r.Logging != "default" && r.Logging != "" { 130 | if isService { 131 | Printf("[Info] [Relay:%s] Custom log files are not permitted when running in daemon mode\n", r.Name) 132 | } else { 133 | Printf("[Info] [Relay:%s] Log output writing to: %q\n", r.Name, r.Logging) 134 | 135 | f, err := os.OpenFile(r.Logging, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | addLogDescriptor(f, r.Name) 141 | w = io.MultiWriter(f, os.Stdout) 142 | } 143 | } 144 | 145 | relay, err := localrelay.New(r.Name, w, r.Listener, r.Destinations...) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | // ===== set proxies 151 | proxMap := make(map[string]localrelay.ProxyURL) 152 | for proxyName, proxyConf := range r.Proxies { 153 | if strings.ToLower(proxyConf.Protocol) != "socks5" { 154 | return errors.New("Socks5 is the only supported proxy type") 155 | } 156 | 157 | proxyURL, err := url.Parse(proxyConf.Protocol + "://" + proxyConf.Address) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | if len(proxyConf.Username) > 0 || len(proxyConf.Password) > 0 { 163 | proxyURL.User = url.UserPassword(proxyConf.Username, proxyConf.Password) 164 | } 165 | 166 | proxMap[proxyName] = localrelay.NewProxyURL(proxyURL) 167 | } 168 | 169 | if len(proxMap) > 0 { 170 | relay.SetProxy(proxMap) 171 | } 172 | 173 | if r.Loadbalance.Enabled { 174 | relay.SetLoadbalance(true) 175 | } 176 | 177 | switch r.Listener.ProxyType() { 178 | case localrelay.ProxyTCP, localrelay.ProxyUDP: 179 | addRelay(relay) 180 | wg.Add(1) 181 | go func(relay *localrelay.Relay) { 182 | if err := relay.ListenServe(); err != nil { 183 | log.Println("[Error] ", err) 184 | } 185 | 186 | removeRelay(relay.Name) 187 | removeLogDescriptor(r.Name) 188 | wg.Done() 189 | }(relay) 190 | case localrelay.ProxyHTTP, localrelay.ProxyHTTPS: 191 | // Convert the relay from the default: TCP to a HTTP server 192 | err := relay.SetHTTP(&http.Server{ 193 | // Middle ware can be set here 194 | Handler: localrelay.HandleHTTP(relay), 195 | 196 | ReadTimeout: time.Second * 60, 197 | WriteTimeout: time.Second * 60, 198 | IdleTimeout: time.Second * 120, 199 | }) 200 | 201 | if err != nil { 202 | panic(err) 203 | } 204 | 205 | if relay.Listener.ProxyType() == localrelay.ProxyHTTPS { 206 | // Set TLS certificates & make relay HTTPS 207 | relay.SetTLS(r.Tls.Certificate, r.Tls.Private) 208 | } 209 | 210 | addRelay(relay) 211 | wg.Add(1) 212 | go func(relay *localrelay.Relay) { 213 | if err := relay.ListenServe(); err != nil { 214 | log.Println("[Error] ", err) 215 | } 216 | 217 | removeRelay(relay.Name) 218 | removeLogDescriptor(r.Name) 219 | wg.Done() 220 | }(relay) 221 | default: 222 | return errors.New("unknown listener type") 223 | } 224 | } 225 | 226 | if wait { 227 | wg.Wait() 228 | Println("[Info] All relays closed.") 229 | } 230 | 231 | return nil 232 | } 233 | 234 | func addRelay(r *localrelay.Relay) { 235 | activeRelaysM.Lock() 236 | activeRelays[r.Name] = r 237 | activeRelaysM.Unlock() 238 | } 239 | 240 | func removeRelay(name string) { 241 | activeRelaysM.Lock() 242 | delete(activeRelays, name) 243 | activeRelaysM.Unlock() 244 | } 245 | 246 | func addLogDescriptor(c io.Closer, name string) { 247 | activeRelaysM.Lock() 248 | logDescriptors[name] = &c 249 | activeRelaysM.Unlock() 250 | } 251 | 252 | func removeLogDescriptor(name string) { 253 | activeRelaysM.Lock() 254 | delete(logDescriptors, name) 255 | activeRelaysM.Unlock() 256 | } 257 | 258 | func closeLogDescriptors() { 259 | activeRelaysM.Lock() 260 | for _, c := range logDescriptors { 261 | closer := *c 262 | 263 | closer.Close() 264 | } 265 | activeRelaysM.Unlock() 266 | } 267 | 268 | func isRunning(relay string) bool { 269 | activeRelaysM.Lock() 270 | defer activeRelaysM.Unlock() 271 | 272 | _, found := activeRelays[relay] 273 | return found 274 | } 275 | 276 | func runningRelays() []*localrelay.Relay { 277 | activeRelaysM.Lock() 278 | 279 | relays := make([]*localrelay.Relay, 0, len(activeRelays)) 280 | for _, r := range activeRelays { 281 | relays = append(relays, r) 282 | } 283 | activeRelaysM.Unlock() 284 | 285 | return relays 286 | } 287 | 288 | func closeDescriptor(path string) error { 289 | activeRelaysM.Lock() 290 | defer activeRelaysM.Unlock() 291 | 292 | closer := *logDescriptors[path] 293 | return closer.Close() 294 | } 295 | 296 | // runningRelaysCopy makes a copy instead of returning the 297 | // pointers 298 | func runningRelaysCopy() []localrelay.Relay { 299 | activeRelaysM.Lock() 300 | 301 | relays := make([]localrelay.Relay, 0, len(activeRelays)) 302 | for _, r := range activeRelays { 303 | relays = append(relays, *r) 304 | } 305 | activeRelaysM.Unlock() 306 | 307 | return relays 308 | } 309 | 310 | func readRelayConfig(file string) (*Relay, error) { 311 | f, err := os.Open(file) 312 | if err != nil { 313 | return nil, errors.Wrapf(err, "file:%q", file) 314 | } 315 | 316 | defer f.Close() 317 | 318 | var relay Relay 319 | if err := toml.NewDecoder(f).Decode(&relay); err != nil { 320 | return nil, errors.Wrapf(err, "file:%q", file) 321 | } 322 | 323 | return &relay, nil 324 | } 325 | -------------------------------------------------------------------------------- /cmd/localrelay/service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/go-compile/localrelay/internal/ipc" 11 | "github.com/kardianos/service" 12 | ) 13 | 14 | var isService = false 15 | 16 | func (p daemon) Start(s service.Service) error { 17 | Println(s.String() + " started") 18 | go p.run() 19 | return nil 20 | } 21 | 22 | func (p daemon) Stop(s service.Service) error { 23 | for _, r := range runningRelays() { 24 | log.Printf("[Info] Closing relay: %s\n", r.Name) 25 | if err := r.Close(); err != nil { 26 | log.Printf("[Error] Closing relay: %s with error: %s\n", r.Name, err) 27 | } 28 | } 29 | 30 | log.Printf("[Info] All relays closed:\n") 31 | 32 | closeLogDescriptors() 33 | 34 | ipcListener.Close() 35 | 36 | Println(s.String() + " stopped") 37 | return nil 38 | } 39 | 40 | func (p daemon) run() { 41 | isService = true 42 | // TODO: listen to signals for reload from systemctl 43 | 44 | if err := launchAutoStartRelays(); err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | l, err := ipc.NewListener() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | ipcListener = l 54 | 55 | err = ipc.ListenServe(l, newIPCServer()) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | } 60 | 61 | func launchAutoStartRelays() error { 62 | if err := createConfigDir(); err != nil { 63 | return err 64 | } 65 | 66 | home := configSystemDir() 67 | prefix := filepath.Join(home, configDirSuffix) 68 | 69 | // read config dir in home folder 70 | dir, err := os.ReadDir(prefix) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | relays := make([]Relay, 0, len(dir)) 76 | 77 | for _, entry := range dir { 78 | // ignore all none toml files 79 | if filepath.Ext(entry.Name()) != ".toml" || entry.IsDir() { 80 | continue 81 | } 82 | 83 | file := filepath.Join(home, configDirSuffix, entry.Name()) 84 | 85 | relay, err := readRelayConfig(file) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | relays = append(relays, *relay) 91 | 92 | if !relay.AutoRestart { 93 | log.Printf("[Ignoring Relay] %q\n", relay.Name) 94 | continue 95 | } 96 | 97 | log.Printf("[Launching relay] %q\n", relay.Name) 98 | } 99 | 100 | if len(relays) == 0 { 101 | return nil 102 | } 103 | 104 | return launchRelays(relays, false) 105 | } 106 | 107 | // configSystemDir returns the parent config dir depending on the system. 108 | // A additional folder will be created within as a child inode. 109 | func configSystemDir() string { 110 | if runtime.GOOS == "windows" { 111 | return "C:\\ProgramData" 112 | } 113 | 114 | return "/etc" 115 | } 116 | 117 | func relaysDir() string { 118 | return filepath.Join(configSystemDir(), configDirSuffix) 119 | } 120 | 121 | // securityCheckBinary checks for common issues yet is not comprehensive 122 | func securityCheckBinary() (bool, string, error) { 123 | exe, err := os.Executable() 124 | if err != nil { 125 | return false, "", err 126 | } 127 | 128 | if runtime.GOOS != "windows" { 129 | // validate file location 130 | if !(strings.HasPrefix(exe, "/usr/bin/") || 131 | strings.HasPrefix(exe, "/usr/sbin/") || 132 | strings.HasPrefix(exe, "/usr/local/bin") || 133 | strings.HasPrefix(exe, "/bin/") || 134 | strings.HasPrefix(exe, "/sbin/")) { 135 | return false, "Binary is outside of an appropriate bin directory.", nil 136 | } 137 | } 138 | 139 | fileStat, err := os.Stat(exe) 140 | if err != nil { 141 | return false, "Could not sta binary.", err 142 | } 143 | 144 | owner, err := fileOwnership(fileStat) 145 | if err != nil { 146 | return false, "Could not attain file ownership information.", err 147 | } 148 | 149 | // if file owned by root 150 | if owner != "0" && runtime.GOOS != "windows" { 151 | return false, "Binary is not solely owned by root/administrator.", err 152 | } 153 | 154 | // TODO: check if non-owners have write access 155 | 156 | return true, "", nil 157 | } 158 | -------------------------------------------------------------------------------- /cmd/localrelay/status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "sort" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/containerd/console" 11 | "github.com/go-compile/localrelay/v2" 12 | ) 13 | 14 | func relayStatus() error { 15 | // make terminal raw to allow the use of colour on windows terminals 16 | current, _ := console.ConsoleFromFile(os.Stdout) 17 | // NOTE: Docker healthchecks will panic "provided file is not a console" 18 | 19 | if current != nil { 20 | defer current.Reset() 21 | } 22 | 23 | if current != nil { 24 | if err := current.SetRaw(); err != nil { 25 | log.Println(err) 26 | } 27 | } 28 | 29 | // we don't set terminal to raw here because print statements don't use 30 | // carriage returns 31 | s, err := serviceStatus() 32 | if err != nil { 33 | Printf("Daemon: \x1b[31m [OFFLINE] \x1b[0m\r\n") 34 | return err 35 | } 36 | 37 | Printf("\r\nDaemon: \x1b[102m\x1b[30m [RUNNING] \x1b[0m\r\n") 38 | Printf("PID: [%d]\r\n", s.Pid) 39 | Printf("Version: [%s]\r\n", s.Version) 40 | Printf("Relays: [%d]\r\n", len(s.Relays)) 41 | 42 | totalConns := 0 43 | totalRequests := 0 44 | in := 0 45 | out := 0 46 | active := 0 47 | 48 | for _, m := range s.Metrics { 49 | totalConns += int(m.TotalConns) 50 | totalRequests += int(m.TotalRequests) 51 | in += int(m.In) 52 | out += int(m.Out) 53 | active += int(m.Active) 54 | } 55 | 56 | Println("\r") 57 | Printf("Total Conns: [%d] Total Requests: [%d]\r\n", totalConns, totalRequests) 58 | Printf("Active: [%d]\r\n", active) 59 | Printf("In/Out: [%s/%s]\r\n", formatBytes(in), formatBytes(out)) 60 | Printf("Uptime: [%s]\r\n", formatDuration(time.Since(time.Unix(s.Started, 0)))) 61 | 62 | // sort alphabetically 63 | sort.SliceStable(s.Relays, func(i, j int) bool { 64 | return s.Relays[i].Name < s.Relays[j].Name 65 | }) 66 | 67 | for i := range s.Relays { 68 | badges := "" 69 | 70 | switch s.Relays[i].Listener.ProxyType() { 71 | case localrelay.ProxyTCP: 72 | badges += "\x1b[90m [TCP] \x1b[0m" 73 | case localrelay.ProxyHTTP: 74 | badges += "\x1b[90m [HTTP] \x1b[0m" 75 | case localrelay.ProxyHTTPS: 76 | badges += "\x1b[90m [HTTPS] \x1b[0m" 77 | } 78 | 79 | if s.Relays[i].ProxyEnabled { 80 | badges += "\x1b[92m [PROXY] \x1b[0m" 81 | } 82 | 83 | if s.Relays[i].Loadbalancer() { 84 | badges += "\x1b[93m [LOAD BALANCER] \x1b[0m" 85 | } 86 | 87 | if s.Relays[i].Failover() { 88 | badges += "\x1b[93m [FAILOVER] \x1b[0m" 89 | } 90 | 91 | Printf(" \x1b[90m%.2d\x1b[0m: %s %s\r\n %s -> %s\r\n", i+1, s.Relays[i].Name, badges, s.Relays[i].Listener, fmtDestination(s.Relays[i].Destination, 6)) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func formatBytes(bytes int) string { 98 | if unit := 1000; bytes < unit { 99 | return strconv.Itoa(bytes) + "bytes" 100 | } 101 | 102 | if unit := 1000000; bytes < unit { 103 | return strconv.FormatFloat(float64(bytes)/1000, 'f', 2, 64) + "kb" 104 | } 105 | 106 | if unit := 1000000000; bytes < unit { 107 | return strconv.FormatFloat(float64(bytes)/1000000, 'f', 2, 64) + "mb" 108 | } 109 | 110 | return strconv.FormatFloat(float64(bytes)/1000000000, 'f', 2, 64) + "gb" 111 | 112 | } 113 | 114 | func formatDuration(d time.Duration) string { 115 | if d.Minutes() < 1 { 116 | return strconv.FormatFloat(d.Seconds(), 'f', 2, 64) + " secs" 117 | } 118 | 119 | if d.Hours() < 1 { 120 | return strconv.FormatFloat(d.Minutes(), 'f', 2, 64) + " minutes" 121 | } 122 | 123 | if d.Hours()/24 < 1 { 124 | return strconv.FormatFloat(d.Hours(), 'f', 2, 64) + " hours" 125 | } 126 | 127 | return strconv.FormatFloat(d.Hours()/24, 'f', 2, 64) + " days" 128 | } 129 | 130 | func fmtDestination(targets []localrelay.TargetLink, limit int) string { 131 | if len(targets) > limit { 132 | return "[" + joinTargets(targets[:limit], "\x1b[90m,\x1b[0m ") + " \x1b[90m... n=" + strconv.Itoa(len(targets)) + "\x1b[0m]" 133 | } 134 | 135 | return "[" + joinTargets(targets, " ") + "]" 136 | } 137 | 138 | func joinTargets(targets []localrelay.TargetLink, sep string) (str string) { 139 | for i := 0; i < len(targets); i++ { 140 | if len(str) == 0 { 141 | str += targets[i].Print() 142 | continue 143 | } 144 | 145 | str += sep + targets[i].Print() 146 | } 147 | 148 | return str 149 | } 150 | -------------------------------------------------------------------------------- /cmd/localrelay/updates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type githubRelease struct { 12 | URL string `json:"url"` 13 | AssetsURL string `json:"assets_url"` 14 | UploadURL string `json:"upload_url"` 15 | HTMLURL string `json:"html_url"` 16 | ID int `json:"id"` 17 | Author struct { 18 | Login string `json:"login"` 19 | ID int `json:"id"` 20 | NodeID string `json:"node_id"` 21 | AvatarURL string `json:"avatar_url"` 22 | GravatarID string `json:"gravatar_id"` 23 | URL string `json:"url"` 24 | HTMLURL string `json:"html_url"` 25 | FollowersURL string `json:"followers_url"` 26 | FollowingURL string `json:"following_url"` 27 | GistsURL string `json:"gists_url"` 28 | StarredURL string `json:"starred_url"` 29 | SubscriptionsURL string `json:"subscriptions_url"` 30 | OrganizationsURL string `json:"organizations_url"` 31 | ReposURL string `json:"repos_url"` 32 | EventsURL string `json:"events_url"` 33 | ReceivedEventsURL string `json:"received_events_url"` 34 | Type string `json:"type"` 35 | SiteAdmin bool `json:"site_admin"` 36 | } `json:"author"` 37 | NodeID string `json:"node_id"` 38 | TagName string `json:"tag_name"` 39 | TargetCommitish string `json:"target_commitish"` 40 | Name string `json:"name"` 41 | Draft bool `json:"draft"` 42 | Prerelease bool `json:"prerelease"` 43 | CreatedAt time.Time `json:"created_at"` 44 | PublishedAt time.Time `json:"published_at"` 45 | Assets []struct { 46 | URL string `json:"url"` 47 | ID int `json:"id"` 48 | NodeID string `json:"node_id"` 49 | Name string `json:"name"` 50 | Label interface{} `json:"label"` 51 | Uploader struct { 52 | Login string `json:"login"` 53 | ID int `json:"id"` 54 | NodeID string `json:"node_id"` 55 | AvatarURL string `json:"avatar_url"` 56 | GravatarID string `json:"gravatar_id"` 57 | URL string `json:"url"` 58 | HTMLURL string `json:"html_url"` 59 | FollowersURL string `json:"followers_url"` 60 | FollowingURL string `json:"following_url"` 61 | GistsURL string `json:"gists_url"` 62 | StarredURL string `json:"starred_url"` 63 | SubscriptionsURL string `json:"subscriptions_url"` 64 | OrganizationsURL string `json:"organizations_url"` 65 | ReposURL string `json:"repos_url"` 66 | EventsURL string `json:"events_url"` 67 | ReceivedEventsURL string `json:"received_events_url"` 68 | Type string `json:"type"` 69 | SiteAdmin bool `json:"site_admin"` 70 | } `json:"uploader"` 71 | ContentType string `json:"content_type"` 72 | State string `json:"state"` 73 | Size int `json:"size"` 74 | DownloadCount int `json:"download_count"` 75 | CreatedAt time.Time `json:"created_at"` 76 | UpdatedAt time.Time `json:"updated_at"` 77 | BrowserDownloadURL string `json:"browser_download_url"` 78 | } `json:"assets"` 79 | TarballURL string `json:"tarball_url"` 80 | ZipballURL string `json:"zipball_url"` 81 | Body string `json:"body"` 82 | } 83 | 84 | func checkForUpdates() error { 85 | 86 | frames := []string{"|", "/", "-", "\\", "|", "/"} 87 | 88 | // when finished change to true. Use mutex to keep it thread safe 89 | finished := false 90 | fm := sync.Mutex{} 91 | 92 | go func() { 93 | for i := 0; true; i++ { 94 | fm.Lock() 95 | if finished { 96 | fm.Unlock() 97 | return 98 | } 99 | fm.Unlock() 100 | 101 | Printf("\r Latest version: %s", frames[i%len(frames)]) 102 | time.Sleep(time.Millisecond * 60) 103 | } 104 | }() 105 | 106 | r, err := http.Get("https://api.github.com/repos/go-compile/localrelay/releases/latest") 107 | if err != nil { 108 | fmt.Print("\r") 109 | Printf(" └ Latest version: %s\n", "unknown") 110 | return err 111 | } 112 | 113 | fm.Lock() 114 | finished = false 115 | fm.Unlock() 116 | fmt.Print("\r") 117 | 118 | if r.StatusCode != 200 { 119 | return ErrFailedCheckUpdate 120 | } 121 | 122 | var release githubRelease 123 | if err := json.NewDecoder(r.Body).Decode(&release); err != nil { 124 | return err 125 | } 126 | 127 | if release.Prerelease { 128 | Printf(" ├ Latest version: %s (Pre Release)\n", release.Name) 129 | } else { 130 | Printf(" ├ Latest version: %s\n", release.Name) 131 | } 132 | 133 | Printf(" └ Published: %s\n", release.PublishedAt.Format("January 2 2006")) 134 | 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /dialer.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func defaultDialer() net.Dialer { 8 | return net.Dialer{ 9 | DualStack: false, 10 | KeepAlive: 15, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | localrelay: 4 | container_name: localrelay 5 | image: gocompile/localrelay:latest 6 | network_mode: "host" 7 | restart: unless-stopped 8 | volumes: 9 | - ./localrelay:/etc/localrelay:ro 10 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-compile/localrelay/v2" 7 | ) 8 | 9 | func main() { 10 | // Create new relay 11 | // nextcloud is the name of the relay. Note this can be called anything 12 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 13 | // localhost:8080 is the destination address, this can be a remote server 14 | r, err := localrelay.New("nextcloud", os.Stdout, "tcp://127.0.0.1:90", "tcp://localhost:8080") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | // Starts the relay server 20 | panic(r.ListenServe()) 21 | } 22 | -------------------------------------------------------------------------------- /examples/certificate-pinning/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/tam7t/hpkp" 11 | 12 | "github.com/go-compile/localrelay/v2" 13 | ) 14 | 15 | func main() { 16 | // Create new relay 17 | // nextcloud is the name of the relay. Note this can be called anything 18 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 19 | // http://example.com is the destination address, this can be a remote server 20 | r, err := localrelay.New("http-relay", os.Stdout, "http://127.0.0.1:90", "https://example.com") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | // Convert the relay from the default: TCP to a HTTP server 26 | err = r.SetHTTP(&http.Server{ 27 | // Middle ware can be set here 28 | Handler: localrelay.HandleHTTP(r), 29 | 30 | ReadTimeout: time.Second * 15, 31 | WriteTimeout: time.Second * 15, 32 | IdleTimeout: time.Second * 30, 33 | }) 34 | 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // Certificate pinning via https://github.com/tam7t/hpkp 40 | s := hpkp.NewMemStorage() 41 | s.Add("example.com", &hpkp.Header{ 42 | Permanent: true, 43 | Sha256Pins: []string{ 44 | "Xs+pjRp23QkmXeH31KEAjM1aWvxpHT6vYy+q2ltqtaM=", 45 | "RQeZkB42znUfsDIIFWIRiYEcKl7nHwNFwWCrnMMJbVc=", 46 | }, 47 | }) 48 | 49 | client := &http.Client{} 50 | dialConf := &hpkp.DialerConfig{ 51 | Storage: s, 52 | PinOnly: true, 53 | TLSConfig: nil, 54 | Reporter: func(p *hpkp.PinFailure, reportUri string) { 55 | fmt.Printf("Certificate did not match locked certificate. Expected: %s got %s\n", 56 | s.Lookup("example.com").Sha256Pins, returnedCertificatePin(), 57 | ) 58 | }, 59 | } 60 | 61 | client.Transport = &http.Transport{ 62 | DialTLS: dialConf.NewDialer(), 63 | } 64 | 65 | // Set the http client for the relay 66 | r.SetClient(client) 67 | 68 | // Starts the relay server 69 | panic(r.ListenServe()) 70 | } 71 | 72 | func returnedCertificatePin() (fingerprints []string) { 73 | conn, err := tls.Dial("tcp", "example.com:443", &tls.Config{ 74 | InsecureSkipVerify: true, 75 | }) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | for _, cert := range conn.ConnectionState().PeerCertificates { 81 | fingerprints = append(fingerprints, hpkp.Fingerprint(cert)) 82 | } 83 | 84 | return fingerprints 85 | } 86 | -------------------------------------------------------------------------------- /examples/close/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/go-compile/localrelay/v2" 9 | ) 10 | 11 | func main() { 12 | // Create new relay 13 | r, err := localrelay.New("nextcloud", os.Stdout, "tcp://127.0.0.1:90", "tcp://localhost:8080") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | // Close relay after 15 seconds 19 | go func() { 20 | time.Sleep(time.Second * 15) 21 | r.Close() 22 | }() 23 | 24 | // Start the relay and handle requests 25 | if err := r.ListenServe(); err != nil { 26 | panic(err) 27 | } 28 | 29 | fmt.Println("Server closed") 30 | } 31 | -------------------------------------------------------------------------------- /examples/failover/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/go-compile/localrelay/v2" 7 | ) 8 | 9 | func main() { 10 | // Create new relay 11 | // nextcloud is the name of the relay. Note this can be called anything 12 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 13 | // localhost:8080 is the destination address, if this fails to dial it will call: 14 | // localhost:445 15 | r, err := localrelay.New("nextcloud", os.Stdout, "tcp://127.0.0.1:90", "tcp://localhost:440", "tcp://localhost:449") 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | // Starts the relay server 21 | panic(r.ListenServe()) 22 | } 23 | -------------------------------------------------------------------------------- /examples/http-privacy/access-tor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/examples/http-privacy/access-tor.png -------------------------------------------------------------------------------- /examples/http-privacy/ifconfig.me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/examples/http-privacy/ifconfig.me.png -------------------------------------------------------------------------------- /examples/http-privacy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "os" 7 | "time" 8 | 9 | "github.com/go-compile/localrelay/v2" 10 | ) 11 | 12 | func main() { 13 | // Create new relay 14 | // nextcloud is the name of the relay. Note this can be called anything 15 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 16 | // https://check.torproject.org is the destination address, this can be a remote server 17 | r, err := localrelay.New("http-spoof", os.Stdout, "http://127.0.0.1:90", "https://check.torproject.org?proxy=tor") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | // Route traffic through Tor 23 | torProxy, err := url.Parse("socks5://127.0.0.1:9050") 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | r.SetProxy(map[string]localrelay.ProxyURL{"tor": { 29 | URL: torProxy, 30 | }}) 31 | 32 | // Convert the relay from the default: TCP to a HTTP server 33 | err = r.SetHTTP(&http.Server{ 34 | // On each request this middleware will be executed 35 | // changing the useragent and the accept language 36 | Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 37 | // Spoof user-agent 38 | req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4891.171 Safari/537.36") 39 | 40 | // Spoof accept language to en-US 41 | req.Header.Set("Accept-Language", "en-US,en;q=0.5") 42 | 43 | // Then send request to localrelay 44 | localrelay.HandleHTTP(r)(w, req) 45 | }), 46 | 47 | ReadTimeout: time.Second * 15, 48 | WriteTimeout: time.Second * 15, 49 | IdleTimeout: time.Second * 30, 50 | }) 51 | 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | // Starts the relay server 57 | panic(r.ListenServe()) 58 | } 59 | -------------------------------------------------------------------------------- /examples/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "time" 7 | 8 | "github.com/go-compile/localrelay" 9 | ) 10 | 11 | func main() { 12 | // Create new relay 13 | // nextcloud is the name of the relay. Note this can be called anything 14 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 15 | // http://example.com is the destination address, this can be a remote server 16 | r := localrelay.New("http-relay", "127.0.0.1:90", "http://example.com", os.Stdout) 17 | 18 | // Convert the relay from the default: TCP to a HTTP server 19 | err := r.SetHTTP(http.Server{ 20 | // Middle ware can be set here 21 | Handler: localrelay.HandleHTTP(r), 22 | 23 | ReadTimeout: time.Second * 15, 24 | WriteTimeout: time.Second * 15, 25 | IdleTimeout: time.Second * 30, 26 | }) 27 | 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // Starts the relay server 33 | panic(r.ListenServe()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/https/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "time" 7 | 8 | "github.com/go-compile/localrelay/v2" 9 | ) 10 | 11 | func main() { 12 | // Create new relay 13 | // nextcloud is the name of the relay. Note this can be called anything 14 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 15 | // http://example.com is the destination address, this can be a remote server 16 | r, err := localrelay.New("https-relay", os.Stdout, "https://127.0.0.1:90", "https://example.com") 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // Convert the relay from the default: TCP to a HTTP server 22 | err = r.SetHTTP(&http.Server{ 23 | // Middle ware can be set here 24 | Handler: localrelay.HandleHTTP(r), 25 | 26 | ReadTimeout: time.Second * 15, 27 | WriteTimeout: time.Second * 15, 28 | IdleTimeout: time.Second * 30, 29 | }) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | // Set TLS certificates & make relay HTTPS 35 | r.SetTLS("certificate.crt", "key.pem") 36 | 37 | // Starts the relay server 38 | panic(r.ListenServe()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/metrics/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "time" 8 | 9 | "github.com/go-compile/localrelay/v2" 10 | ) 11 | 12 | func main() { 13 | // Create new relay 14 | // onion-service is the name of the relay. Note this can be called anything 15 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 16 | // 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80 this can be a normal IP 17 | // address or even a onion if you're using Tor 18 | r, err := localrelay.New("onion-service", os.Stdout, "tcp://127.0.0.1:90", "tcp://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80?proxy=tor") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | // Create a new SOCKS5 proxy 24 | 25 | // 127.0.0.1:9050 is the Tor SOCKS5 proxy address on all opperating systems 26 | // other than Windows. On windows it's 9150 however, if you run Tor as a 27 | // service on Windows (tor.exe not the whole Tor Browser Bundle) the address 28 | // will be 9050 29 | // Route traffic through Tor 30 | torProxy, err := url.Parse("socks5://127.0.0.1:9050") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | r.SetProxy(map[string]localrelay.ProxyURL{"tor": { 36 | URL: torProxy, 37 | }}) 38 | 39 | // Prints metrics every 5 seconds 40 | go func() { 41 | for { 42 | time.Sleep(time.Second * 5) 43 | 44 | active, total := r.Metrics.Connections() 45 | fmt.Printf("[In/Out: %d/%d] [Active: %d] [Total: %d] [Dialer Avg: %dms]\n", r.Metrics.Download(), r.Metrics.Upload(), active, total, r.Metrics.DialerAvg()) 46 | } 47 | }() 48 | 49 | // Start the relay server 50 | panic(r.ListenServe()) 51 | } 52 | -------------------------------------------------------------------------------- /examples/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "os" 6 | 7 | "github.com/go-compile/localrelay/v2" 8 | ) 9 | 10 | func main() { 11 | // Create new relay 12 | // onion-service is the name of the relay. Note this can be called anything 13 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 14 | // 2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80 this can be a normal IP 15 | // address or even a onion if you're using Tor 16 | r, err := localrelay.New("onion-service", os.Stdout, "tcp://127.0.0.1:90", "tcp://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80?proxy=tor") 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // Create a new SOCKS5 proxy 22 | 23 | // 127.0.0.1:9050 is the Tor SOCKS5 proxy address on all opperating systems 24 | // other than Windows. On windows it's 9150 however, if you run Tor as a 25 | // service on Windows (tor.exe not the whole Tor Browser Bundle) the address 26 | // will be 9050 27 | // Route traffic through Tor 28 | torProxy, err := url.Parse("socks5://127.0.0.1:9050") 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | r.SetProxy(map[string]localrelay.ProxyURL{"tor": { 34 | URL: torProxy, 35 | }}) 36 | 37 | // Start the relay server 38 | panic(r.ListenServe()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/proxy/tor hidden service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/examples/proxy/tor hidden service.png -------------------------------------------------------------------------------- /examples/timeout/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/go-compile/localrelay/v2" 8 | ) 9 | 10 | func main() { 11 | // Set the remote dial time out globally. 12 | // Remote dial is only used for remotes when proxies aren't in use 13 | // or are being ignored. 14 | localrelay.Timeout = time.Second * 2 15 | 16 | // Create new relay 17 | // nextcloud is the name of the relay. Note this can be called anything 18 | // 127.0.0.1:90 is the address the relay will listen on. E.g. you connect via localhost:90 19 | // localhost:8080 is the destination address, this can be a remote server 20 | r, err := localrelay.New("nextcloud", os.Stdout, "tcp://127.0.0.1:90", "tcp://localhost:8080") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | // Starts the relay server 26 | panic(r.ListenServe()) 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-compile/localrelay 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/chzyer/readline v1.5.1 7 | github.com/containerd/console v1.0.3 8 | github.com/fasthttp/router v1.5.1 9 | github.com/go-compile/localrelay/v2 v2.0.0-20240119152158-a43947254ed3 10 | github.com/kardianos/service v1.2.2 11 | github.com/naoina/toml v0.1.1 12 | github.com/pkg/errors v0.9.1 13 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 14 | github.com/valyala/fasthttp v1.54.0 15 | golang.org/x/net v0.26.0 16 | golang.org/x/sys v0.21.0 17 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce 18 | ) 19 | 20 | require ( 21 | github.com/andybalholm/brotli v1.1.0 // indirect 22 | github.com/klauspost/compress v1.17.7 // indirect 23 | github.com/kylelemons/godebug v1.1.0 // indirect 24 | github.com/mroth/weightedrand v1.0.0 // indirect 25 | github.com/naoina/go-stringutil v0.1.0 // indirect 26 | github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect 27 | github.com/valyala/bytebufferpool v1.0.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 3 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 4 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 5 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 6 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 7 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 8 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 9 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 10 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 11 | github.com/fasthttp/router v1.5.1 h1:uViy8UYYhm5npJSKEZ4b/ozM//NGzVCfJbh6VJ0VKr8= 12 | github.com/fasthttp/router v1.5.1/go.mod h1:WrmsLo3mrerZP2VEXRV1E8nL8ymJFYCDTr4HmnB8+Zs= 13 | github.com/go-compile/localrelay/v2 v2.0.0-20240119152158-a43947254ed3 h1:h4F3Bl+mg/nWX7LAtPjr2D6EWmom9UN3yzJ65BRrmuE= 14 | github.com/go-compile/localrelay/v2 v2.0.0-20240119152158-a43947254ed3/go.mod h1:JyvuvQHWQ9UhLF3NLgSvA9+w2/4p6M0R3Zv/74dNpbw= 15 | github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= 16 | github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= 17 | github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= 18 | github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 19 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 20 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 21 | github.com/mroth/weightedrand v1.0.0 h1:V8JeHChvl2MP1sAoXq4brElOcza+jxLkRuwvtQu8L3E= 22 | github.com/mroth/weightedrand v1.0.0/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= 23 | github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= 24 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= 25 | github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= 26 | github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= 27 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 28 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 29 | github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= 30 | github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= 31 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= 32 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= 33 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 34 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 35 | github.com/valyala/fasthttp v1.54.0 h1:cCL+ZZR3z3HPLMVfEYVUMtJqVaui0+gu7Lx63unHwS0= 36 | github.com/valyala/fasthttp v1.54.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= 37 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 38 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 39 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 43 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= 45 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 46 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.21 2 | 3 | use ( 4 | . 5 | ./v2 6 | ) -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 2 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 3 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 6 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 7 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 8 | golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 9 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 11 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 12 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 13 | golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 14 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 15 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 16 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-compile/localrelay/fa1d248f6aee7ff20320e665b03477cef09b09f6/icon.png -------------------------------------------------------------------------------- /internal/httperror/503.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 503 Service Unavailable 8 | 36 | 37 | 38 | 39 |
40 |

503 Service Unavailable

41 |

The remote relay could not be dialed. This may be due the remote being down or a proxy failing.

42 |
43 |
44 |
45 | Localrelay HTTP(s) Relay 46 |
47 |
48 | {(VERSION)} 49 |
50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /internal/httperror/pages.go: -------------------------------------------------------------------------------- 1 | package httperror 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | ) 7 | 8 | //go:embed 503.html 9 | var error503 []byte 10 | 11 | var ( 12 | templateVerson = []byte("{(VERSION)}") 13 | 14 | version = "v2" 15 | ) 16 | 17 | // SetVersion allows you to set the current version of the program which will 18 | // be displayed in the http error pages. 19 | func SetVersion(v string) { 20 | version = v 21 | } 22 | 23 | // Get503 returns a rendered Error 503 service unavaliable error page 24 | func Get503() []byte { 25 | return bytes.Replace(error503, templateVerson, []byte(version), -1) 26 | } 27 | -------------------------------------------------------------------------------- /internal/ipc/connect_posix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || !windows 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris !windows 3 | 4 | package ipc 5 | 6 | import ( 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | // ipcPathPrefix is the dir which comes before the unix socket 13 | ipcPathPrefix = "/var/run/" 14 | ) 15 | 16 | // ipcConnect will use name pipes to communicate to the daemon 17 | func Connect() (*http.Client, net.Conn, error) { 18 | conn, err := net.DialTimeout("unix", ipcPathPrefix+ipcSocket, ipcTimeout) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | 23 | httpClient := newHTTPClient(conn) 24 | 25 | return httpClient, conn, nil 26 | } 27 | 28 | // SetPathPrefix will determin where the IPC listener binds and connects 29 | func SetPathPrefix(prefix string) { 30 | ipcPathPrefix = prefix 31 | } 32 | -------------------------------------------------------------------------------- /internal/ipc/connect_windows.go: -------------------------------------------------------------------------------- 1 | package ipc 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "gopkg.in/natefinch/npipe.v2" 8 | ) 9 | 10 | // ipcConnect will use name pipes to communicate to the daemon 11 | func Connect() (*http.Client, net.Conn, error) { 12 | conn, err := npipe.DialTimeout(`\\.\pipe\`+serviceName, ipcTimeout) 13 | if err != nil { 14 | return nil, nil, err 15 | } 16 | 17 | httpClient := newHTTPClient(conn) 18 | 19 | return httpClient, conn, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/ipc/ipc.go: -------------------------------------------------------------------------------- 1 | package ipc 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | const ( 13 | serviceName = "localrelayd" 14 | ipcSocket = "localrelay.ipc.socket" 15 | serviceDescription = "Localrelay daemon relay runner" 16 | ipcTimeout = time.Second 17 | ) 18 | 19 | // handleConn takes a conn and handles each command 20 | func handleConn(conn net.Conn, srv *fasthttp.Server, l io.Closer) { 21 | defer conn.Close() 22 | srv.ServeConn(conn) 23 | } 24 | 25 | // newHTTPClient makes a http client which always uses the socket. 26 | // When making a HTTP request provide any host, it does not need to exist. 27 | // 28 | // Example: 29 | // 30 | // http://lr/status 31 | func newHTTPClient(conn net.Conn) *http.Client { 32 | return &http.Client{ 33 | Transport: &http.Transport{Dial: func(network, addr string) (net.Conn, error) { 34 | return conn, nil 35 | }}, 36 | Timeout: time.Second * 15, 37 | } 38 | } 39 | 40 | func ListenServe(l net.Listener, srv *fasthttp.Server) error { 41 | defer l.Close() 42 | 43 | for { 44 | conn, err := l.Accept() 45 | if err != nil { 46 | if err == net.ErrClosed { 47 | return err 48 | } 49 | 50 | continue 51 | } 52 | 53 | go handleConn(conn, srv, l) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/ipc/listen_posix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || !windows 2 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris !windows 3 | 4 | package ipc 5 | 6 | import "net" 7 | 8 | // NewListener for windows uses name pipes to communicate 9 | func NewListener() (net.Listener, error) { 10 | return net.Listen("unix", ipcPathPrefix+ipcSocket) 11 | } 12 | -------------------------------------------------------------------------------- /internal/ipc/listen_windows.go: -------------------------------------------------------------------------------- 1 | package ipc 2 | 3 | import ( 4 | "net" 5 | 6 | "gopkg.in/natefinch/npipe.v2" 7 | ) 8 | 9 | // NewListener for windows uses name pipes to communicate 10 | func NewListener() (net.Listener, error) { 11 | return npipe.Listen(`\\.\pipe\` + serviceName) 12 | } 13 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | // Logger is used for logging debug information such as 9 | // connections being created, dropped etc 10 | type Logger struct { 11 | Info *log.Logger 12 | Warning *log.Logger 13 | Error *log.Logger 14 | } 15 | 16 | // NewLogger creates a new logging system 17 | func NewLogger(w io.Writer, name string) *Logger { 18 | return &Logger{ 19 | Info: log.New(w, "[INFO] ["+name+"] ", log.Lshortfile|log.Lmicroseconds), 20 | Warning: log.New(w, "[WARNING] ["+name+"] ", log.Lshortfile|log.Lmicroseconds), 21 | Error: log.New(w, "[ERROR] ["+name+"] ", log.Lshortfile|log.Lmicroseconds), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Metrics stores information such as bandwidth usage 9 | // conn stats etc 10 | type Metrics struct { 11 | up, down int 12 | dialFail, dialSuccess uint64 13 | activeConns int 14 | totalConns uint64 15 | totalRequests uint64 16 | 17 | // dialTimes holds recent durations of how long it takes a 18 | // relay to dial a remote 19 | dialTimes []int64 20 | 21 | m sync.RWMutex 22 | } 23 | 24 | // Upload returns the amount of bytes uploaded through the relay 25 | func (m *Metrics) Upload() int { 26 | m.m.RLock() 27 | defer m.m.RUnlock() 28 | 29 | return m.up 30 | } 31 | 32 | // Download returns the amount of bytes downloaded through the relay 33 | func (m *Metrics) Download() int { 34 | m.m.RLock() 35 | defer m.m.RUnlock() 36 | 37 | return m.down 38 | } 39 | 40 | // Connections returns the amount of active and total connections 41 | func (m *Metrics) Connections() (active int, total uint64) { 42 | m.m.RLock() 43 | defer m.m.RUnlock() 44 | 45 | return m.activeConns, m.totalConns 46 | } 47 | 48 | // Requests returns the amount of requests made via http 49 | func (m *Metrics) Requests() uint64 { 50 | m.m.RLock() 51 | defer m.m.RUnlock() 52 | 53 | return m.totalRequests 54 | } 55 | 56 | // Dialer returns the successful dials and failed dials 57 | func (m *Metrics) Dialer() (success, failed uint64) { 58 | m.m.RLock() 59 | defer m.m.RUnlock() 60 | 61 | return m.dialSuccess, m.dialFail 62 | } 63 | 64 | // DialerAvg returns the 10 point average dial time 65 | // this average includes failed dials 66 | func (m *Metrics) DialerAvg() (milliseconds int) { 67 | m.m.RLock() 68 | defer m.m.RUnlock() 69 | 70 | if len(m.dialTimes) == 0 { 71 | return 0 72 | } 73 | 74 | x := int64(0) 75 | for i := 0; i < len(m.dialTimes); i++ { 76 | x += m.dialTimes[i] 77 | } 78 | 79 | return int(x) / len(m.dialTimes) 80 | } 81 | 82 | // bandwidth will increment the bandwidth statistics 83 | func (m *Metrics) bandwidth(up, down int) { 84 | m.m.Lock() 85 | defer m.m.Unlock() 86 | 87 | m.up += up 88 | m.down += down 89 | } 90 | 91 | // dial will increment the dialer success/fail statistics 92 | func (m *Metrics) dial(success, failed uint64, t time.Time) { 93 | m.m.Lock() 94 | defer m.m.Unlock() 95 | 96 | m.dialSuccess += success 97 | m.dialFail += failed 98 | 99 | // 10 point moving average 100 | if len(m.dialTimes) >= 10 { 101 | m.dialTimes = append(m.dialTimes[1:], time.Since(t).Milliseconds()) 102 | } else { 103 | m.dialTimes = append(m.dialTimes, time.Since(t).Milliseconds()) 104 | } 105 | } 106 | 107 | // connections will update the active connections metric 108 | func (m *Metrics) connections(delta int) { 109 | m.m.Lock() 110 | defer m.m.Unlock() 111 | 112 | // Calculate total connections 113 | if delta > 0 { 114 | m.totalConns += uint64(delta) 115 | } 116 | 117 | m.activeConns += delta 118 | } 119 | 120 | // requests will update the requests metric 121 | func (m *Metrics) requests(delta int) { 122 | m.m.Lock() 123 | defer m.m.Unlock() 124 | 125 | m.totalRequests += uint64(delta) 126 | } 127 | -------------------------------------------------------------------------------- /pkg/api/client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | 12 | "github.com/go-compile/localrelay/internal/ipc" 13 | ) 14 | 15 | var ( 16 | ErrNotOk = errors.New("status code not ok") 17 | ErrFailure = errors.New("localrelay failed executing the requested action") 18 | ErrNotFound = errors.New("relay not found") 19 | ) 20 | 21 | type Client struct { 22 | conn net.Conn 23 | hc *http.Client 24 | } 25 | 26 | type msgResponse struct { 27 | Message string `json:"message"` 28 | } 29 | 30 | // Connect establishes a connection to the IPC socket 31 | func Connect() (*Client, error) { 32 | hc, conn, err := ipc.Connect() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return &Client{ 38 | conn: conn, 39 | hc: hc, 40 | }, nil 41 | } 42 | 43 | // Close disconnects from the IPC socket 44 | func (c *Client) Close() error { 45 | return c.conn.Close() 46 | } 47 | 48 | func (c *Client) GetStatus() (*Status, error) { 49 | resp, err := c.hc.Get("http://lr/status") 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | if resp.StatusCode != 200 { 55 | return nil, ErrNotOk 56 | } 57 | 58 | var status Status 59 | if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { 60 | return nil, err 61 | } 62 | 63 | return &status, nil 64 | } 65 | 66 | func (c *Client) GetConnections() ([]Connection, error) { 67 | resp, err := c.hc.Get("http://lr/connections") 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | if resp.StatusCode != 200 { 73 | return nil, ErrNotOk 74 | } 75 | 76 | var pool []Connection 77 | if err := json.NewDecoder(resp.Body).Decode(&pool); err != nil { 78 | return nil, err 79 | } 80 | 81 | return pool, nil 82 | } 83 | 84 | func (c *Client) DropRelay(relay string) error { 85 | resp, err := c.hc.Get("http://lr/drop/relay/" + url.PathEscape(relay)) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | if resp.StatusCode != 200 { 91 | return ErrNotOk 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (c *Client) DropIP(ip string) error { 98 | resp, err := c.hc.Get("http://lr/drop/ip/" + url.PathEscape(ip)) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | if resp.StatusCode != 200 { 104 | return ErrNotOk 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (c *Client) DropAll() error { 111 | resp, err := c.hc.Get("http://lr/drop") 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if resp.StatusCode != 200 { 117 | return ErrNotOk 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func (c *Client) StopRelay(relay string) error { 124 | resp, err := c.hc.Get("http://lr/stop/" + url.PathEscape(relay)) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | switch resp.StatusCode { 130 | case 200: 131 | return nil 132 | case 500: 133 | return ErrFailure 134 | case 404: 135 | return ErrNotFound 136 | default: 137 | return errors.New("unknown respose code") 138 | } 139 | } 140 | 141 | func (c *Client) StartRelay(relays ...string) (responses []string, err error) { 142 | for _, relay := range relays { 143 | // make post request to run relay. Use strconv instead of json encoding for performance 144 | resp, err := c.hc.Post("http://lr/run", "application/json", bytes.NewBuffer([]byte("["+strconv.Quote(relay)+"]"))) 145 | if err != nil { 146 | return responses, err 147 | } 148 | 149 | var response msgResponse 150 | if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { 151 | return responses, err 152 | } 153 | 154 | responses = append(responses, response.Message) 155 | 156 | switch resp.StatusCode { 157 | case 404: 158 | return responses, ErrNotFound 159 | case 500: 160 | return responses, ErrFailure 161 | } 162 | } 163 | 164 | return responses, nil 165 | } 166 | -------------------------------------------------------------------------------- /pkg/api/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | This package enables the communication between a client and Localrelay daemon. 3 | */ 4 | package api 5 | -------------------------------------------------------------------------------- /pkg/api/models.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/go-compile/localrelay/v2" 4 | 5 | type Status struct { 6 | Relays []localrelay.Relay 7 | Pid int 8 | Version string 9 | // Metrics contains relay name as the index 10 | Metrics map[string]Metrics 11 | // Started is a unix timestamp of when the daemon was created 12 | Started int64 13 | } 14 | 15 | type Metrics struct { 16 | In, Out, Active, DialAvg int 17 | TotalConns, TotalRequests uint64 18 | } 19 | 20 | type Connection struct { 21 | LocalAddr string 22 | RemoteAddr string 23 | Network string 24 | 25 | RelayName string 26 | RelayHost string 27 | 28 | ForwardedAddr string 29 | 30 | // Opened is a unix timestamp 31 | Opened int64 32 | } 33 | -------------------------------------------------------------------------------- /relay.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "golang.org/x/net/proxy" 13 | ) 14 | 15 | // ProxyType represents what type of proxy the relay is. 16 | // 17 | // Raw TCP is used for just forwarding the raw connection 18 | // to the remote address. 19 | type ProxyType uint8 20 | 21 | // Relay represents a reverse proxy and all of its settings 22 | type Relay struct { 23 | // Name is a generic name which can be assigned to this relay 24 | Name string 25 | // Host is the address to listen on 26 | Host string 27 | 28 | // ForwardAddr is the destination to send the connection. 29 | // When using a relay type which accept multipule destinations 30 | // use a comma seperated list. 31 | ForwardAddr string 32 | // ProxyType is used to forward or manipulate the connection 33 | ProxyType ProxyType 34 | 35 | // ProxyEnabled is set to true when a proxy has been set for this relay 36 | ProxyEnabled bool 37 | proxies []*proxy.Dialer 38 | // remoteProxyIgnore is a list of indexes in the ForwardAddr array 39 | // to which the proxy settings should be ignored 40 | remoteProxyIgnore []int 41 | 42 | logger *Logger 43 | 44 | // close is linked to the listener 45 | close io.Closer 46 | 47 | // Metrics is used to store information such as upload/download 48 | // and other statistics 49 | *Metrics 50 | 51 | // http relay section 52 | server http.Server 53 | httpClient *http.Client 54 | 55 | // TLS settings 56 | certificateFile string 57 | keyFile string 58 | 59 | running bool 60 | m sync.Mutex 61 | 62 | protocolSwitching map[int]string 63 | 64 | // connPool contains a list of ACTIVE connections 65 | connPool []*PooledConn 66 | } 67 | 68 | // PooledConn allows meta data to be attached to a connection 69 | type PooledConn struct { 70 | Conn net.Conn 71 | RemoteAddr string 72 | Opened time.Time 73 | } 74 | 75 | const ( 76 | // ProxyTCP is for raw TCP forwarding 77 | ProxyTCP ProxyType = iota 78 | // ProxyHTTP creates a HTTP server and forwards the traffic to 79 | // either a HTTP or HTTPs server 80 | ProxyHTTP 81 | // ProxyHTTPS is the same as HTTP but listens on TLS 82 | ProxyHTTPS 83 | 84 | // ProxyFailOverTCP acts like the TCP proxy however if it cannot connect 85 | // it will use a failover address instead. 86 | ProxyFailOverTCP 87 | 88 | // ProxyUDP forwards UDP traffic 89 | ProxyUDP 90 | 91 | // VERSION uses semantic versioning 92 | // this version number is for the library not the CLI 93 | VERSION = "v1.4.0" 94 | ) 95 | 96 | var ( 97 | // ErrUnknownProxyType is returned when a relay has a proxy type which is invalid 98 | ErrUnknownProxyType = errors.New("unknown proxytype used in creation of relay") 99 | // ErrAddrNotMatch is returned when a server object has a addr which is not nil 100 | // and does not equal the relay's address 101 | ErrAddrNotMatch = errors.New("addr does not match the relays host address") 102 | ) 103 | 104 | // New creates a new TCP relay 105 | func New(name, host, destination string, logger io.Writer) *Relay { 106 | 107 | return &Relay{ 108 | Name: name, 109 | Host: host, 110 | ForwardAddr: destination, 111 | ProxyType: ProxyTCP, 112 | 113 | Metrics: &Metrics{ 114 | // Preallocate array with capacity of 10 115 | dialTimes: make([]int64, 0, 10), 116 | }, 117 | 118 | httpClient: http.DefaultClient, 119 | 120 | logger: NewLogger(logger, name), 121 | protocolSwitching: make(map[int]string, strings.Count(destination, ",")), 122 | } 123 | } 124 | 125 | // Running returns true if relay is running 126 | func (r *Relay) Running() bool { 127 | r.m.Lock() 128 | defer r.m.Unlock() 129 | 130 | return r.running 131 | } 132 | 133 | func (r *Relay) setRunning(toggle bool) { 134 | r.m.Lock() 135 | defer r.m.Unlock() 136 | 137 | r.running = toggle 138 | } 139 | 140 | // DisableProxy will disable the proxy settings when connecting 141 | // to the remote at the index provided. 142 | // 143 | // OPTION ONLY AVAILABLE FOR FAIL OVER TCP PROXY TYPE! 144 | func (r *Relay) DisableProxy(remoteIndex ...int) { 145 | r.remoteProxyIgnore = remoteIndex 146 | } 147 | 148 | // ignoreProxySettings returns true if the proxy should be disabled 149 | // for this remote index 150 | func (r *Relay) ignoreProxySettings(remoteIndex int) bool { 151 | for _, v := range r.remoteProxyIgnore { 152 | if v == remoteIndex { 153 | return true 154 | } 155 | } 156 | 157 | return false 158 | } 159 | 160 | // SetFailOverTCP will make the relay type TCP and support 161 | // multipule destinations. If one destination fails to dial 162 | // the next will be attempted. 163 | func (r *Relay) SetFailOverTCP() { 164 | r.ProxyType = ProxyFailOverTCP 165 | } 166 | 167 | // SetProtocolSwitch allows you to switch the outgoing protocol 168 | // NOTE: If a proxy is enabled protocol switching is disabled 169 | func (r *Relay) SetProtocolSwitch(index int, protocol string) { 170 | r.protocolSwitching[index] = protocol 171 | } 172 | 173 | // SetHTTP is used to set the relay as a type HTTP relay 174 | // addr will auto be set in the server object if left blank 175 | func (r *Relay) SetHTTP(server http.Server) error { 176 | r.ProxyType = ProxyHTTP 177 | 178 | // Auto set addr if left blank 179 | if server.Addr == "" { 180 | server.Addr = r.Host 181 | } else if server.Addr != r.Host { 182 | return ErrAddrNotMatch 183 | } 184 | 185 | // if there is a trailing slash strip it 186 | if len(r.ForwardAddr) > 1 && r.ForwardAddr[len(r.ForwardAddr)-1] == '/' { 187 | r.ForwardAddr = r.ForwardAddr[:len(r.ForwardAddr)-1] 188 | } 189 | 190 | r.server = server 191 | 192 | return nil 193 | } 194 | 195 | // SetClient will set the http client used by the relay 196 | func (r *Relay) SetClient(client *http.Client) { 197 | r.httpClient = client 198 | 199 | if r.httpClient.Transport != nil { 200 | r.ProxyEnabled = true 201 | } 202 | } 203 | 204 | // SetTLS sets the TLS certificates for use in the ProxyHTTPS relay. 205 | // This function will upgrade this relay to a HTTPS relay 206 | func (r *Relay) SetTLS(certificateFile, keyFile string) { 207 | r.certificateFile = certificateFile 208 | r.keyFile = keyFile 209 | 210 | r.ProxyType = ProxyHTTPS 211 | } 212 | 213 | // SetProxy sets the proxy dialer to be used 214 | // proxy.SOCKS5() can be used to setup a socks5 proxy 215 | // or a list of proxies 216 | func (r *Relay) SetProxy(dialer ...*proxy.Dialer) { 217 | r.proxies = dialer 218 | r.ProxyEnabled = true 219 | } 220 | 221 | // Close will close the relay's listener 222 | func (r *Relay) Close() error { 223 | return r.close.Close() 224 | } 225 | 226 | // ListenServe will start a listener and handle the incoming requests 227 | func (r *Relay) ListenServe() error { 228 | 229 | defer func() { 230 | r.logger.Info.Printf("STOPPING: %q on %q\n", r.Name, r.Host) 231 | r.setRunning(false) 232 | }() 233 | 234 | r.setRunning(true) 235 | 236 | r.logger.Info.Printf("STARTING: %q on %q\n", r.Name, r.Host) 237 | 238 | l, err := listener(r) 239 | if err != nil { 240 | return err 241 | } 242 | 243 | switch r.ProxyType { 244 | case ProxyTCP: 245 | r.close = l 246 | 247 | return relayTCP(r, l) 248 | case ProxyUDP: 249 | r.close = l 250 | 251 | return relayUDP(r, l) 252 | case ProxyHTTP: 253 | r.close = l 254 | 255 | return relayHTTP(r, l) 256 | case ProxyHTTPS: 257 | r.close = l 258 | 259 | return relayHTTPS(r, l) 260 | case ProxyFailOverTCP: 261 | r.close = l 262 | 263 | return relayFailOverTCP(r, l) 264 | default: 265 | l.Close() 266 | 267 | return ErrUnknownProxyType 268 | } 269 | } 270 | 271 | // Serve lets you set your own listener and then serve on it 272 | func (r *Relay) Serve(l net.Listener) error { 273 | defer func() { 274 | r.logger.Info.Printf("STOPPING: %q on %q\n", r.Name, r.Host) 275 | r.setRunning(false) 276 | }() 277 | 278 | r.setRunning(true) 279 | 280 | r.logger.Info.Printf("STARTING: %q on %q\n", r.Name, r.Host) 281 | r.close = l 282 | 283 | switch r.ProxyType { 284 | case ProxyTCP: 285 | return relayTCP(r, l) 286 | case ProxyUDP: 287 | return relayUDP(r, l) 288 | case ProxyHTTP: 289 | return relayHTTP(r, l) 290 | case ProxyHTTPS: 291 | return relayHTTPS(r, l) 292 | case ProxyFailOverTCP: 293 | return relayFailOverTCP(r, l) 294 | default: 295 | return ErrUnknownProxyType 296 | } 297 | } 298 | 299 | // storeConn places the provided net.Conn into the connPoll. 300 | // To remove this conn from the pool, provide it to popConn() 301 | func (r *Relay) storeConn(conn net.Conn) { 302 | r.m.Lock() 303 | defer r.m.Unlock() 304 | 305 | r.connPool = append(r.connPool, &PooledConn{conn, "", time.Now()}) 306 | } 307 | 308 | // popConn removes the provided connection from the conn pool 309 | func (r *Relay) popConn(conn net.Conn) { 310 | r.m.Lock() 311 | defer r.m.Unlock() 312 | 313 | for i := 0; i < len(r.connPool); i++ { 314 | if r.connPool[i].Conn == conn { 315 | // remove conn 316 | r.connPool = append(r.connPool[:i], r.connPool[i+1:]...) 317 | return 318 | } 319 | } 320 | } 321 | 322 | // setConnRemote will update the conn pool with the remote 323 | func (r *Relay) setConnRemote(conn net.Conn, remote net.Addr) { 324 | r.m.Lock() 325 | defer r.m.Unlock() 326 | 327 | for i := 0; i < len(r.connPool); i++ { 328 | if r.connPool[i].Conn == conn { 329 | // remove conn 330 | r.connPool[i].RemoteAddr = remote.String() 331 | return 332 | } 333 | } 334 | } 335 | 336 | // GetConns returns all the active connections to this relay 337 | func (r *Relay) GetConns() []*PooledConn { 338 | r.m.Lock() 339 | defer r.m.Unlock() 340 | 341 | return r.connPool 342 | } 343 | -------------------------------------------------------------------------------- /relay_test.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestConnPoolBasic(t *testing.T) { 12 | conns := []net.Conn{} 13 | connAmount := 50 14 | relay := New("test-relay", "127.0.0.1:23838", "127.0.0.1:23838", io.Discard) 15 | 16 | for i := 0; i < connAmount; i++ { 17 | conn := &net.TCPConn{} 18 | 19 | conns = append(conns, conn) 20 | relay.storeConn(conn) 21 | } 22 | 23 | for i := 0; i < connAmount; i++ { 24 | relay.popConn(conns[i]) 25 | } 26 | 27 | if len(relay.connPool) != 0 { 28 | t.Fatal("connPool is not empty") 29 | } 30 | } 31 | 32 | func TestConnPool(t *testing.T) { 33 | // create channel to receive errors from another goroutine 34 | errCh := make(chan error) 35 | go startTCPServer(errCh) 36 | 37 | // wait for error or nil error indicating server launched fine 38 | if err := <-errCh; err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | relay := New("test-relay", "127.0.0.1:23838", "127.0.0.1:23838", io.Discard) 43 | 44 | wg := sync.WaitGroup{} 45 | 46 | // open 10 conns and append to the conn pool 47 | for i := 0; i < 10; i++ { 48 | wg.Add(1) 49 | 50 | conn, err := net.Dial("tcp", "127.0.0.1:23838") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | relay.storeConn(conn) 56 | 57 | // handle conn 58 | go func(conn net.Conn, i int) { 59 | for { 60 | time.Sleep(time.Millisecond * (10 * time.Duration(i))) 61 | _, err := conn.Write([]byte("test")) 62 | if err != nil { 63 | relay.popConn(conn) 64 | 65 | for _, c := range relay.connPool { 66 | if c.Conn == conn { 67 | t.Fatal("correct conn was not removed") 68 | } 69 | } 70 | 71 | wg.Done() 72 | return 73 | } 74 | } 75 | }(conn, i) 76 | } 77 | 78 | wg.Wait() 79 | } 80 | 81 | func startTCPServer(errCh chan error) { 82 | l, err := net.Listen("tcp", ":23838") 83 | if err != nil { 84 | errCh <- err 85 | return 86 | } 87 | 88 | errCh <- nil 89 | 90 | for { 91 | conn, err := l.Accept() 92 | if err != nil { 93 | continue 94 | } 95 | 96 | // handle conn with echo server 97 | go func(conn net.Conn) { 98 | for i := 0; i <= 5; i++ { 99 | buf := make([]byte, 1048) 100 | n, err := conn.Read(buf) 101 | if err != nil { 102 | conn.Close() 103 | return 104 | } 105 | 106 | conn.Write(buf[:n]) 107 | } 108 | 109 | // close conn after 5 messages 110 | conn.Close() 111 | }(conn) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /relayfailovertcp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var ( 11 | // ErrFailConnect will be returned if the remote failed to dial 12 | ErrFailConnect = errors.New("failed to dial remote") 13 | // Timeout is only used when dialling without a proxy 14 | Timeout = time.Second * 5 15 | ) 16 | 17 | func relayFailOverTCP(r *Relay, l net.Listener) error { 18 | 19 | r.logger.Info.Println("STARTING FAIL OVER TCP RELAY") 20 | 21 | for { 22 | conn, err := l.Accept() 23 | if err != nil { 24 | if errors.Is(err, net.ErrClosed) { 25 | r.logger.Warning.Println("LISTENER CLOSED") 26 | return nil 27 | } 28 | 29 | r.logger.Warning.Println("ACCEPT FAILED: ", err) 30 | continue 31 | } 32 | 33 | go handleFailOver(r, conn, "tcp") 34 | } 35 | } 36 | 37 | func handleFailOver(r *Relay, conn net.Conn, network string) { 38 | r.storeConn(conn) 39 | 40 | defer func() { 41 | conn.Close() 42 | 43 | // remove conn from connPool 44 | r.popConn(conn) 45 | 46 | r.Metrics.connections(-1) 47 | }() 48 | 49 | r.Metrics.connections(1) 50 | 51 | r.logger.Info.Printf("NEW CONNECTION %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 52 | 53 | start := time.Now() 54 | 55 | // If using a proxy dial with proxy 56 | if r.proxies != nil && len(r.protocolSwitching) == 0 { 57 | r.logger.Info.Println("CREATING PROXY DIALER") 58 | 59 | // Use proxies list as failover list 60 | for i := 0; i < len(r.proxies); i++ { 61 | dialer := *r.proxies[i] 62 | 63 | for x, remoteAddress := range strings.Split(r.ForwardAddr, ",") { 64 | if r.ignoreProxySettings(x) { 65 | r.logger.Info.Printf("REMOTE [%d] IGNORING PROXY %d\n", x+1, i+1) 66 | 67 | if err := dial(r, conn, remoteAddress, x, network, start); err != nil { 68 | continue 69 | } 70 | 71 | // if no error dialling then exit 72 | return 73 | } 74 | 75 | r.logger.Info.Printf("DIALLING FORWARD ADDRESS [%d] THROUGH PROXY %d\n", x+1, i+1) 76 | 77 | c, err := dialer.Dial("tcp", remoteAddress) 78 | if err != nil { 79 | r.Metrics.dial(0, 1, start) 80 | 81 | r.logger.Error.Printf("DIAL FORWARD ADDR: %s\n", err) 82 | continue 83 | } 84 | 85 | r.Metrics.dial(1, 0, start) 86 | 87 | r.logger.Info.Printf("CONNECTED TO %s\n", remoteAddress) 88 | streamConns(conn, c, r.Metrics) 89 | 90 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 91 | return 92 | } 93 | } 94 | 95 | r.logger.Error.Printf("CONNECTION CLOSED %q ON %q AFTER DIALLING WITH PROXY FAILED\n", conn.RemoteAddr(), conn.LocalAddr()) 96 | return 97 | } 98 | 99 | // Not using proxy so dial with standard dialer 100 | for i, remoteAddress := range strings.Split(r.ForwardAddr, ",") { 101 | proto := network 102 | 103 | // if protocol switching enabled set the new network 104 | if protocol, ok := r.protocolSwitching[i]; ok { 105 | r.logger.Info.Printf("SWITCHING PROTOCOL FROM %q TO %q\n", network, protocol) 106 | proto = protocol 107 | } 108 | 109 | if err := dial(r, conn, remoteAddress, i, proto, start); err != nil { 110 | // dial next host 111 | continue 112 | } 113 | 114 | return 115 | } 116 | } 117 | 118 | func dial(r *Relay, conn net.Conn, remoteAddress string, i int, network string, start time.Time) error { 119 | r.logger.Info.Printf("DIALLING FORWARD ADDRESS [%d]\n", i+1) 120 | 121 | c, err := net.DialTimeout(network, remoteAddress, Timeout) 122 | if err != nil { 123 | r.Metrics.dial(0, 1, start) 124 | 125 | r.logger.Error.Printf("DIAL FORWARD ADDR: %s\n", err) 126 | return ErrFailConnect 127 | } 128 | 129 | r.Metrics.dial(1, 0, start) 130 | 131 | r.logger.Info.Printf("CONNECTED TO %s\n", remoteAddress) 132 | streamConns(conn, c, r.Metrics) 133 | 134 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /relayhttp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func relayHTTP(r *Relay, l net.Listener) error { 12 | 13 | r.logger.Info.Println("STARTING HTTP RELAY") 14 | 15 | return r.server.Serve(l) 16 | } 17 | 18 | // HandleHTTP is to be used as the HTTP relay's handler set in the 19 | // http.Server object 20 | func HandleHTTP(relay *Relay) http.HandlerFunc { 21 | // Forwards relay object to request handler 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | handleHTTP(w, r, relay) 24 | } 25 | } 26 | 27 | func handleHTTP(w http.ResponseWriter, r *http.Request, re *Relay) { 28 | re.Metrics.requests(1) 29 | 30 | remoteURL := re.ForwardAddr + r.URL.Path + "?" + r.URL.Query().Encode() 31 | 32 | // BUG: sometimes requests redirect and cause a loop (Loop is auto stopped) 33 | req, err := http.NewRequest(r.Method, remoteURL, r.Body) 34 | if err != nil { 35 | re.logger.Error.Println("BUILD REQUEST ERROR: ", err) 36 | return 37 | } 38 | 39 | re.Metrics.bandwidth(int(req.ContentLength)+len(remoteURL), 0) 40 | 41 | // Append request headers 42 | for k, v := range r.Header { 43 | req.Header.Set(k, strings.Join(v, ",")) 44 | } 45 | 46 | start := time.Now() 47 | response, err := re.httpClient.Do(req) 48 | if err != nil { 49 | re.logger.Error.Println("FORWARD REQUEST ERROR: ", err) 50 | re.Metrics.dial(0, 1, start) 51 | return 52 | } 53 | 54 | re.Metrics.dial(1, 0, start) 55 | 56 | defer response.Body.Close() 57 | 58 | // Append response headers 59 | for k, v := range response.Header { 60 | w.Header().Set(k, strings.Join(v, ",")) 61 | } 62 | 63 | w.WriteHeader(response.StatusCode) 64 | 65 | in, _ := io.Copy(w, response.Body) 66 | re.Metrics.bandwidth(0, int(in)) 67 | 68 | } 69 | -------------------------------------------------------------------------------- /relayhttps.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func relayHTTPS(r *Relay, l net.Listener) error { 8 | r.logger.Info.Println("STARTING HTTPS RELAY") 9 | 10 | return r.server.ServeTLS(l, r.certificateFile, r.keyFile) 11 | } 12 | -------------------------------------------------------------------------------- /relaytcp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func listener(r *Relay) (net.Listener, error) { 13 | 14 | network := "tcp" 15 | if r.ProxyType == ProxyUDP { 16 | network = "udp" 17 | } 18 | 19 | l, err := net.Listen(network, r.Host) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return l, nil 25 | } 26 | 27 | func relayTCP(r *Relay, l net.Listener) error { 28 | 29 | r.logger.Info.Println("STARTING TCP RELAY") 30 | 31 | for { 32 | conn, err := l.Accept() 33 | if err != nil { 34 | if errors.Is(err, net.ErrClosed) { 35 | r.logger.Warning.Println("LISTENER CLOSED") 36 | return nil 37 | } 38 | 39 | r.logger.Warning.Println("ACCEPT FAILED: ", err) 40 | continue 41 | } 42 | 43 | go handleConn(r, conn, "tcp") 44 | } 45 | } 46 | 47 | func handleConn(r *Relay, conn net.Conn, network string) { 48 | r.storeConn(conn) 49 | 50 | defer func() { 51 | conn.Close() 52 | 53 | // remove conn from connPool 54 | r.popConn(conn) 55 | 56 | r.Metrics.connections(-1) 57 | }() 58 | 59 | r.Metrics.connections(1) 60 | 61 | r.logger.Info.Printf("NEW CONNECTION %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 62 | 63 | start := time.Now() 64 | 65 | // If using a proxy dial with proxy 66 | if r.proxies != nil { 67 | r.logger.Info.Println("CREATING PROXY DIALER") 68 | 69 | // Use proxies list as failover list 70 | for i := 0; i < len(r.proxies); i++ { 71 | dialer := *r.proxies[i] 72 | 73 | r.logger.Info.Printf("DIALLING FORWARD ADDRESS THROUGH PROXY %d\n", i+1) 74 | 75 | c, err := dialer.Dial("tcp", r.ForwardAddr) 76 | if err != nil { 77 | r.Metrics.dial(0, 1, start) 78 | 79 | r.logger.Error.Printf("DIAL FORWARD ADDR: %s\n", err) 80 | continue 81 | } 82 | 83 | r.Metrics.dial(1, 0, start) 84 | 85 | r.logger.Info.Printf("CONNECTED TO %s\n", r.ForwardAddr) 86 | r.setConnRemote(conn, c.RemoteAddr()) 87 | 88 | err = streamConns(conn, c, r.Metrics) 89 | if err != nil { 90 | r.logger.Info.Printf("ERROR FROM %q ON %q: ERR=%s\n", conn.RemoteAddr(), conn.LocalAddr(), err) 91 | } 92 | 93 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 94 | return 95 | } 96 | 97 | r.logger.Error.Printf("CONNECTION CLOSED %q ON %q AFTER DIALLING WITH PROXY FAILED\n", conn.RemoteAddr(), conn.LocalAddr()) 98 | return 99 | } 100 | 101 | // Not using proxy so dial with standard dialer 102 | 103 | r.logger.Info.Println("DIALLING FORWARD ADDRESS") 104 | 105 | proto := network 106 | 107 | // if protocol switching enabled set the new network 108 | if protocol, ok := r.protocolSwitching[0]; ok { 109 | r.logger.Info.Printf("SWITCHING PROTOCOL FROM %q TO %q\n", network, protocol) 110 | proto = protocol 111 | } 112 | 113 | c, err := net.DialTimeout(proto, r.ForwardAddr, Timeout) 114 | if err != nil { 115 | r.Metrics.dial(0, 1, start) 116 | 117 | r.logger.Error.Printf("DIAL FORWARD ADDR: %s\n", err) 118 | return 119 | } 120 | 121 | r.Metrics.dial(1, 0, start) 122 | 123 | r.logger.Info.Printf("CONNECTED TO %s\n", r.ForwardAddr) 124 | r.setConnRemote(conn, c.RemoteAddr()) 125 | 126 | err = streamConns(conn, c, r.Metrics) 127 | if err != nil { 128 | r.logger.Info.Printf("ERROR FROM %q ON %q: ERR=%s\n", conn.RemoteAddr(), conn.LocalAddr(), err) 129 | } 130 | 131 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 132 | } 133 | 134 | func streamConns(client net.Conn, remote net.Conn, m *Metrics) error { 135 | wg := sync.WaitGroup{} 136 | 137 | var copyInErr error 138 | 139 | wg.Add(1) 140 | go func() { 141 | copyInErr = copierIn(client, remote, 128, m) 142 | wg.Done() 143 | }() 144 | 145 | wg.Add(1) 146 | err := copierOut(client, remote, 128, m) 147 | wg.Done() 148 | 149 | // if error is reporting that the conn is closed ignore both 150 | if errors.Is(copyInErr, io.EOF) || errors.Is(err, io.EOF) || errors.Is(copyInErr, net.ErrClosed) { 151 | // if any of the errors are EOFs or ErrClosed we are not 152 | // bothered with any additional errors. 153 | return nil 154 | } 155 | 156 | // else propagate error 157 | return err 158 | } 159 | 160 | // NOTE: static function for maximum performance 161 | func copierIn(client net.Conn, dst net.Conn, buffer int, m *Metrics) error { 162 | 163 | buf := make([]byte, buffer) 164 | for { 165 | n, err := dst.Read(buf) 166 | m.bandwidth(0, n) 167 | if err != nil { 168 | 169 | var err1 error 170 | // if we read some data, flush it then return a error 171 | if n > 0 { 172 | _, err1 = dst.Write(buf[:n]) 173 | } 174 | 175 | err2 := client.Close() 176 | err3 := dst.Close() 177 | 178 | // wrap all errors into one 179 | if err1 != nil { 180 | err = errors.Wrap(err, err1.Error()) 181 | } 182 | 183 | // wrap all errors into one 184 | if err2 != nil { 185 | err = errors.Wrap(err, err2.Error()) 186 | } 187 | 188 | // wrap all errors into one 189 | if err3 != nil { 190 | err = errors.Wrap(err, err3.Error()) 191 | } 192 | 193 | return errors.WithStack(err) 194 | } 195 | 196 | if n2, err := client.Write(buf[:n]); err != nil || n2 != n { 197 | err1 := client.Close() 198 | err2 := dst.Close() 199 | 200 | // wrap all errors into one 201 | if err1 != nil { 202 | err = errors.Wrap(err, err1.Error()) 203 | } 204 | 205 | // wrap all errors into one 206 | if err2 != nil { 207 | err = errors.Wrap(err, err2.Error()) 208 | } 209 | 210 | return errors.WithStack(err) 211 | } 212 | } 213 | } 214 | 215 | // NOTE: static function for maximum performance 216 | func copierOut(client net.Conn, dst net.Conn, buffer int, m *Metrics) error { 217 | 218 | buf := make([]byte, buffer) 219 | for { 220 | 221 | n, err := client.Read(buf) 222 | m.bandwidth(n, 0) 223 | if err != nil { 224 | 225 | var err1 error 226 | // if we read some data, flush it then return a error 227 | if n > 0 { 228 | _, err1 = dst.Write(buf[:n]) 229 | } 230 | 231 | err2 := client.Close() 232 | err3 := dst.Close() 233 | 234 | // wrap all errors into one 235 | if err1 != nil { 236 | err = errors.Wrap(err, err1.Error()) 237 | } 238 | 239 | // wrap all errors into one 240 | if err2 != nil { 241 | err = errors.Wrap(err, err2.Error()) 242 | } 243 | 244 | // wrap all errors into one 245 | if err3 != nil { 246 | err = errors.Wrap(err, err3.Error()) 247 | } 248 | 249 | return errors.WithStack(err) 250 | } 251 | 252 | if n2, err := dst.Write(buf[:n]); err != nil || n2 != n { 253 | err1 := client.Close() 254 | err2 := dst.Close() 255 | 256 | // wrap all errors into one 257 | if err1 != nil { 258 | err = errors.Wrap(err, err1.Error()) 259 | } 260 | 261 | // wrap all errors into one 262 | if err2 != nil { 263 | err = errors.Wrap(err, err2.Error()) 264 | } 265 | 266 | return errors.WithStack(err) 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /relayudp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | ) 7 | 8 | func relayUDP(r *Relay, l net.Listener) error { 9 | 10 | r.logger.Info.Println("STARTING UDP RELAY") 11 | 12 | for { 13 | conn, err := l.Accept() 14 | if err != nil { 15 | if errors.Is(err, net.ErrClosed) { 16 | r.logger.Warning.Println("LISTENER CLOSED") 17 | return nil 18 | } 19 | 20 | r.logger.Warning.Println("ACCEPT FAILED: ", err) 21 | continue 22 | } 23 | 24 | go handleConn(r, conn, "udp") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/wix/README.md: -------------------------------------------------------------------------------- 1 | # Wix Installer 2 | 3 | Wix is a utility which enables the creation of windows `.msi` installer files. 4 | 5 | ### Requirements 6 | 7 | You must have wix command line installed. 8 | https://wixtoolset.org/docs/intro/ 9 | 10 | ``` 11 | dotnet tool install --global wix 12 | ``` -------------------------------------------------------------------------------- /scripts/wix/localrelay.template.wxs: -------------------------------------------------------------------------------- 1 |  2 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 28 | 30 | 31 | 32 | 34 | 35 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-compile/localrelay/v2 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/pkg/errors v0.9.1 7 | golang.org/x/net v0.26.0 8 | ) 9 | 10 | require github.com/mroth/weightedrand v1.0.0 // indirect 11 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/mroth/weightedrand v1.0.0 h1:V8JeHChvl2MP1sAoXq4brElOcza+jxLkRuwvtQu8L3E= 2 | github.com/mroth/weightedrand v1.0.0/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 6 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 7 | -------------------------------------------------------------------------------- /v2/logger.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "log" 6 | ) 7 | 8 | // Logger is used for logging debug information such as 9 | // connections being created, dropped etc 10 | type Logger struct { 11 | Info *log.Logger 12 | Warning *log.Logger 13 | Error *log.Logger 14 | } 15 | 16 | // NewLogger creates a new logging system 17 | func NewLogger(w io.Writer, name string) *Logger { 18 | return &Logger{ 19 | Info: log.New(w, "[INFO] ["+name+"] ", log.Lshortfile|log.Lmicroseconds), 20 | Warning: log.New(w, "[WARNING] ["+name+"] ", log.Lshortfile|log.Lmicroseconds), 21 | Error: log.New(w, "[ERROR] ["+name+"] ", log.Lshortfile|log.Lmicroseconds), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /v2/metrics.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Metrics stores information such as bandwidth usage 9 | // conn stats etc 10 | type Metrics struct { 11 | up, down int 12 | dialFail, dialSuccess uint64 13 | activeConns int 14 | totalConns uint64 15 | totalRequests uint64 16 | 17 | // dialTimes holds recent durations of how long it takes a 18 | // relay to dial a remote 19 | dialTimes []int64 20 | 21 | m sync.RWMutex 22 | } 23 | 24 | // Upload returns the amount of bytes uploaded through the relay 25 | func (m *Metrics) Upload() int { 26 | m.m.RLock() 27 | defer m.m.RUnlock() 28 | 29 | return m.up 30 | } 31 | 32 | // Download returns the amount of bytes downloaded through the relay 33 | func (m *Metrics) Download() int { 34 | m.m.RLock() 35 | defer m.m.RUnlock() 36 | 37 | return m.down 38 | } 39 | 40 | // Connections returns the amount of active and total connections 41 | func (m *Metrics) Connections() (active int, total uint64) { 42 | m.m.RLock() 43 | defer m.m.RUnlock() 44 | 45 | return m.activeConns, m.totalConns 46 | } 47 | 48 | // Requests returns the amount of requests made via http 49 | func (m *Metrics) Requests() uint64 { 50 | m.m.RLock() 51 | defer m.m.RUnlock() 52 | 53 | return m.totalRequests 54 | } 55 | 56 | // Dialer returns the successful dials and failed dials 57 | func (m *Metrics) Dialer() (success, failed uint64) { 58 | m.m.RLock() 59 | defer m.m.RUnlock() 60 | 61 | return m.dialSuccess, m.dialFail 62 | } 63 | 64 | // DialerAvg returns the 10 point average dial time 65 | // this average includes failed dials 66 | func (m *Metrics) DialerAvg() (milliseconds int) { 67 | m.m.RLock() 68 | defer m.m.RUnlock() 69 | 70 | if len(m.dialTimes) == 0 { 71 | return 0 72 | } 73 | 74 | x := int64(0) 75 | for i := 0; i < len(m.dialTimes); i++ { 76 | x += m.dialTimes[i] 77 | } 78 | 79 | return int(x) / len(m.dialTimes) 80 | } 81 | 82 | // bandwidth will increment the bandwidth statistics 83 | func (m *Metrics) bandwidth(up, down int) { 84 | m.m.Lock() 85 | defer m.m.Unlock() 86 | 87 | m.up += up 88 | m.down += down 89 | } 90 | 91 | // dial will increment the dialer success/fail statistics 92 | func (m *Metrics) dial(success, failed uint64, t time.Time) { 93 | m.m.Lock() 94 | defer m.m.Unlock() 95 | 96 | m.dialSuccess += success 97 | m.dialFail += failed 98 | 99 | // 10 point moving average 100 | if len(m.dialTimes) >= 10 { 101 | m.dialTimes = append(m.dialTimes[1:], time.Since(t).Milliseconds()) 102 | } else { 103 | m.dialTimes = append(m.dialTimes, time.Since(t).Milliseconds()) 104 | } 105 | } 106 | 107 | // connections will update the active connections metric 108 | func (m *Metrics) connections(delta int) { 109 | m.m.Lock() 110 | defer m.m.Unlock() 111 | 112 | // Calculate total connections 113 | if delta > 0 { 114 | m.totalConns += uint64(delta) 115 | } 116 | 117 | m.activeConns += delta 118 | } 119 | 120 | // requests will update the requests metric 121 | func (m *Metrics) requests(delta int) { 122 | m.m.Lock() 123 | defer m.m.Unlock() 124 | 125 | m.totalRequests += uint64(delta) 126 | } 127 | -------------------------------------------------------------------------------- /v2/relay.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "sync" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "golang.org/x/net/proxy" 14 | ) 15 | 16 | // ProxyType represents what type of proxy the relay is. 17 | // 18 | // Raw TCP is used for just forwarding the raw connection 19 | // to the remote address. 20 | type ProxyType string 21 | 22 | // Relay represents a reverse proxy and all of its settings 23 | type Relay struct { 24 | // Name is a generic name which can be assigned to this relay 25 | Name string 26 | // Listener is the address and protocol to listen on. 27 | // Example A (listen on loopback): 28 | // tcp://127.0.0.1:443 29 | // Example B (listen on all interfaces): 30 | // tcp://0.0.0.0:443 31 | Listener TargetLink 32 | 33 | // Destination is an array of connection URLs. 34 | // Example A: 35 | // tcp://127.0.0.1:443 36 | // Example B: 37 | // udp://127.0.0.1:23 38 | // Example C: 39 | // tcp://example.com:443?proxy=tor 40 | Destination []TargetLink 41 | 42 | // ProxyEnabled is set to true when a proxy has been set for this relay 43 | ProxyEnabled bool 44 | proxies map[string]ProxyURL 45 | 46 | logger *Logger 47 | 48 | // close is linked to the listener 49 | close io.Closer 50 | 51 | // Metrics is used to store information such as upload/download 52 | // and other statistics 53 | *Metrics 54 | 55 | // http relay section 56 | httpServer *http.Server 57 | httpClient *http.Client 58 | 59 | // TLS settings 60 | certificateFile string 61 | keyFile string 62 | 63 | loadbalance Loadbalance 64 | 65 | running bool 66 | m sync.Mutex 67 | 68 | // connPool contains a list of ACTIVE connections 69 | connPool []*PooledConn 70 | 71 | // Tags are used to propogate relay properties to the API client/CLI 72 | Targs map[string]struct{} 73 | } 74 | 75 | type Loadbalance struct { 76 | Enabled bool 77 | Algorithm string 78 | } 79 | 80 | // PooledConn allows meta data to be attached to a connection 81 | type PooledConn struct { 82 | Conn net.Conn 83 | RemoteAddr string 84 | Opened time.Time 85 | } 86 | 87 | type ProxyURL struct { 88 | *url.URL 89 | } 90 | 91 | const ( 92 | // ProxyTCP is for raw TCP forwarding 93 | ProxyTCP ProxyType = "tcp" 94 | // ProxyUDP forwards UDP traffic 95 | ProxyUDP ProxyType = "udp" 96 | // ProxyHTTP creates a HTTP server and forwards the traffic to 97 | // either a HTTP or HTTPs server 98 | ProxyHTTP ProxyType = "http" 99 | // ProxyHTTPS is the same as HTTP but listens on TLS 100 | ProxyHTTPS ProxyType = "https" 101 | 102 | // VERSION uses semantic versioning 103 | // this version number is for the library not the CLI 104 | VERSION = "v2.0.0" 105 | ) 106 | 107 | var ( 108 | // ErrUnknownProxyType is returned when a relay has a proxy type which is invalid 109 | ErrUnknownProxyType = errors.New("unknown proxytype used in creation of relay") 110 | // ErrAddrNotMatch is returned when a server object has a addr which is not nil 111 | // and does not equal the relay's address 112 | ErrAddrNotMatch = errors.New("addr does not match the relays host address") 113 | // ErrNoDestination is returned when the user did not provide a destination 114 | ErrNoDestination = errors.New("at least one destination must be set") 115 | // ErrManyDestinations is returned if attempting to use more than one destination 116 | // on a http(s) relay. 117 | ErrManyDestinations = errors.New("too many destinations for this relay type") 118 | ) 119 | 120 | // New creates a new TCP relay 121 | func New(name string, logger io.Writer, listener TargetLink, destination ...TargetLink) (*Relay, error) { 122 | if len(destination) == 0 { 123 | return nil, ErrNoDestination 124 | } 125 | 126 | tags := make(map[string]struct{}) 127 | if len(destination) > 1 { 128 | tags["failover"] = struct{}{} 129 | } 130 | 131 | // if a http(s) proxy enforce one destination only policy 132 | if t := destination[0].ProxyType(); t == ProxyHTTP || t == ProxyHTTPS { 133 | if len(destination) > 1 { 134 | return nil, ErrManyDestinations 135 | } 136 | } 137 | 138 | if logger == nil { 139 | logger = os.Stdout 140 | } 141 | 142 | return &Relay{ 143 | Name: name, 144 | Listener: listener, 145 | Destination: destination, 146 | 147 | Metrics: &Metrics{ 148 | // Preallocate array with capacity of 10 149 | dialTimes: make([]int64, 0, 10), 150 | }, 151 | 152 | httpClient: http.DefaultClient, 153 | proxies: make(map[string]ProxyURL), 154 | 155 | logger: NewLogger(logger, name), 156 | Targs: tags, 157 | }, nil 158 | } 159 | 160 | // Running returns true if relay is running 161 | func (r *Relay) Running() bool { 162 | r.m.Lock() 163 | defer r.m.Unlock() 164 | 165 | return r.running 166 | } 167 | 168 | func (r *Relay) setRunning(toggle bool) { 169 | r.m.Lock() 170 | defer r.m.Unlock() 171 | 172 | r.running = toggle 173 | } 174 | 175 | // SetHTTP is used to set the relay as a type HTTP relay 176 | // addr will auto be set in the server object if left blank 177 | func (r *Relay) SetHTTP(server *http.Server) error { 178 | // Auto set addr if left blank 179 | if server.Addr == "" { 180 | server.Addr = r.Listener.Addr() 181 | } else if server.Addr != r.Listener.Addr() { 182 | return ErrAddrNotMatch 183 | } 184 | 185 | r.httpServer = server 186 | 187 | return nil 188 | } 189 | 190 | // SetClient will set the http client used by the relay 191 | func (r *Relay) SetClient(client *http.Client) { 192 | r.httpClient = client 193 | 194 | if r.httpClient.Transport != nil { 195 | r.ProxyEnabled = true 196 | } 197 | } 198 | 199 | // SetTLS sets the TLS certificates for use in the ProxyHTTPS relay. 200 | // This function will upgrade this relay to a HTTPS relay 201 | func (r *Relay) SetTLS(certificateFile, keyFile string) { 202 | r.certificateFile = certificateFile 203 | r.keyFile = keyFile 204 | } 205 | 206 | // SetProxy sets the proxy dialer to be used 207 | // proxy.SOCKS5() can be used to setup a socks5 proxy 208 | // or a list of proxies 209 | func (r *Relay) SetProxy(proxies map[string]ProxyURL) { 210 | r.proxies = proxies 211 | r.ProxyEnabled = true 212 | } 213 | 214 | func (r *Relay) SetLoadbalance(enabled bool) { 215 | r.loadbalance.Enabled = true 216 | 217 | if enabled { 218 | r.Targs["load-balancer"] = struct{}{} 219 | delete(r.Targs, "failover") 220 | } else { 221 | delete(r.Targs, "load-balancer") 222 | 223 | if len(r.Destination) < 2 { 224 | delete(r.Targs, "failover") 225 | } 226 | } 227 | } 228 | 229 | // Loadbalancer returns true if the relay is a load balancer 230 | func (r *Relay) Loadbalancer() bool { 231 | return r.loadbalance.Enabled || hasTag(r.Targs, "load-balancer") 232 | } 233 | 234 | // Failover returns true if the relay offers failover but is not a load balancer 235 | func (r *Relay) Failover() bool { 236 | return hasTag(r.Targs, "failover") 237 | } 238 | 239 | // Close will close the relay's listener 240 | func (r *Relay) Close() error { 241 | return r.close.Close() 242 | } 243 | 244 | // ListenServe will start a listener and handle the incoming requests 245 | func (r *Relay) ListenServe() error { 246 | defer func() { 247 | r.logger.Info.Printf("STOPPING: %q on %q\n", r.Name, r.Listener) 248 | r.setRunning(false) 249 | }() 250 | 251 | r.setRunning(true) 252 | 253 | r.logger.Info.Printf("STARTING: %q on %q\n", r.Name, r.Listener) 254 | 255 | l, err := listener(r) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | switch r.Listener.ProxyType() { 261 | case ProxyTCP: 262 | r.close = l 263 | 264 | return relayTCP(r, l) 265 | case ProxyUDP: 266 | r.close = l 267 | 268 | return relayUDP(r, l) 269 | case ProxyHTTP: 270 | r.close = l 271 | 272 | return relayHTTP(r, l) 273 | case ProxyHTTPS: 274 | r.close = l 275 | 276 | return relayHTTPS(r, l) 277 | default: 278 | l.Close() 279 | 280 | return ErrUnknownProxyType 281 | } 282 | } 283 | 284 | // Serve lets you set your own listener and then serve on it 285 | func (r *Relay) Serve(l net.Listener) error { 286 | defer func() { 287 | r.logger.Info.Printf("STOPPING: %q on %q\n", r.Name, r.Listener) 288 | r.setRunning(false) 289 | }() 290 | 291 | r.setRunning(true) 292 | 293 | r.logger.Info.Printf("STARTING: %q on %q\n", r.Name, r.Listener) 294 | r.close = l 295 | 296 | switch r.Listener.ProxyType() { 297 | case ProxyTCP: 298 | return relayTCP(r, l) 299 | case ProxyUDP: 300 | return relayUDP(r, l) 301 | case ProxyHTTP: 302 | return relayHTTP(r, l) 303 | case ProxyHTTPS: 304 | return relayHTTPS(r, l) 305 | default: 306 | return ErrUnknownProxyType 307 | } 308 | } 309 | 310 | // storeConn places the provided net.Conn into the connPoll. 311 | // To remove this conn from the pool, provide it to popConn() 312 | func (r *Relay) storeConn(conn net.Conn) { 313 | r.m.Lock() 314 | defer r.m.Unlock() 315 | 316 | r.connPool = append(r.connPool, &PooledConn{conn, "\x1b[92mdialing\x1b[0m", time.Now()}) 317 | } 318 | 319 | // popConn removes the provided connection from the conn pool 320 | func (r *Relay) popConn(conn net.Conn) { 321 | r.m.Lock() 322 | defer r.m.Unlock() 323 | 324 | for i := 0; i < len(r.connPool); i++ { 325 | if r.connPool[i].Conn == conn { 326 | // remove conn 327 | r.connPool = append(r.connPool[:i], r.connPool[i+1:]...) 328 | return 329 | } 330 | } 331 | } 332 | 333 | // setConnRemote will update the conn pool with the remote 334 | func (r *Relay) setConnRemote(conn net.Conn, remote net.Addr) { 335 | r.m.Lock() 336 | defer r.m.Unlock() 337 | 338 | for i := 0; i < len(r.connPool); i++ { 339 | if r.connPool[i].Conn == conn { 340 | // remove conn 341 | r.connPool[i].RemoteAddr = remote.String() 342 | return 343 | } 344 | } 345 | } 346 | 347 | // GetConns returns all the active connections to this relay 348 | func (r *Relay) GetConns() []*PooledConn { 349 | r.m.Lock() 350 | defer r.m.Unlock() 351 | 352 | return r.connPool 353 | } 354 | 355 | func NewProxyURL(u *url.URL) ProxyURL { 356 | return ProxyURL{u} 357 | } 358 | 359 | func (p *ProxyURL) Dialer() proxy.Dialer { 360 | pwd, set := p.User.Password() 361 | auth := &proxy.Auth{ 362 | User: p.User.Username(), 363 | Password: pwd, 364 | } 365 | 366 | if !set || len(auth.User) < 1 { 367 | auth = nil 368 | } 369 | 370 | prox, _ := proxy.SOCKS5("tcp", p.Host, auth, nil) 371 | return prox 372 | } 373 | 374 | func (p *ProxyURL) HttpProxyURL() { 375 | http.ProxyURL(&url.URL{}) 376 | } 377 | 378 | func hasTag(tags map[string]struct{}, tag string) bool { 379 | _, ok := tags[tag] 380 | return ok 381 | } 382 | -------------------------------------------------------------------------------- /v2/relay_test.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestConnPoolBasic(t *testing.T) { 12 | conns := []net.Conn{} 13 | connAmount := 50 14 | relay, err := New("test-relay", io.Discard, "127.0.0.1:23832", "127.0.0.1:23838") 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | 19 | for i := 0; i < connAmount; i++ { 20 | conn := &net.TCPConn{} 21 | 22 | conns = append(conns, conn) 23 | relay.storeConn(conn) 24 | } 25 | 26 | for i := 0; i < connAmount; i++ { 27 | relay.popConn(conns[i]) 28 | } 29 | 30 | if len(relay.connPool) != 0 { 31 | t.Fatal("connPool is not empty") 32 | } 33 | } 34 | 35 | func TestConnPool(t *testing.T) { 36 | // create channel to receive errors from another goroutine 37 | errCh := make(chan error) 38 | go startTCPServer(errCh) 39 | 40 | // wait for error or nil error indicating server launched fine 41 | if err := <-errCh; err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | relay, err := New("test-relay", io.Discard, "127.0.0.1:23838", "127.0.0.1:23838") 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | 50 | wg := sync.WaitGroup{} 51 | 52 | // open 10 conns and append to the conn pool 53 | for i := 0; i < 10; i++ { 54 | wg.Add(1) 55 | 56 | conn, err := net.Dial("tcp", "127.0.0.1:23838") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | relay.storeConn(conn) 62 | 63 | // handle conn 64 | go func(conn net.Conn, i int) { 65 | for { 66 | time.Sleep(time.Millisecond * (10 * time.Duration(i))) 67 | _, err := conn.Write([]byte("test")) 68 | if err != nil { 69 | relay.popConn(conn) 70 | 71 | for _, c := range relay.connPool { 72 | if c.Conn == conn { 73 | t.Fatal("correct conn was not removed") 74 | } 75 | } 76 | 77 | wg.Done() 78 | return 79 | } 80 | } 81 | }(conn, i) 82 | } 83 | 84 | wg.Wait() 85 | } 86 | 87 | func startTCPServer(errCh chan error) { 88 | l, err := net.Listen("tcp", ":23838") 89 | if err != nil { 90 | errCh <- err 91 | return 92 | } 93 | 94 | errCh <- nil 95 | 96 | for { 97 | conn, err := l.Accept() 98 | if err != nil { 99 | continue 100 | } 101 | 102 | // handle conn with echo server 103 | go func(conn net.Conn) { 104 | for i := 0; i <= 5; i++ { 105 | buf := make([]byte, 1048) 106 | n, err := conn.Read(buf) 107 | if err != nil { 108 | conn.Close() 109 | return 110 | } 111 | 112 | conn.Write(buf[:n]) 113 | } 114 | 115 | // close conn after 5 messages 116 | conn.Close() 117 | }(conn) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /v2/relayfailovertcp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // ErrFailConnect will be returned if the remote failed to dial 11 | ErrFailConnect = errors.New("failed to dial remote") 12 | // Timeout is only used when dialling without a proxy 13 | Timeout = time.Second * 5 14 | ) 15 | 16 | func dial(r *Relay, conn net.Conn, remoteAddress string, i int, network string, start time.Time) error { 17 | r.logger.Info.Printf("DIALLING FORWARD ADDRESS [%d]\n", i+1) 18 | 19 | c, err := net.DialTimeout(network, remoteAddress, Timeout) 20 | if err != nil { 21 | r.Metrics.dial(0, 1, start) 22 | 23 | r.logger.Error.Printf("DIAL FORWARD ADDR: %s\n", err) 24 | return ErrFailConnect 25 | } 26 | 27 | r.setConnRemote(conn, c.RemoteAddr()) 28 | 29 | r.Metrics.dial(1, 0, start) 30 | 31 | r.logger.Info.Printf("CONNECTED TO %s\n", remoteAddress) 32 | err = streamConns(conn, c, r.Metrics) 33 | if err != nil { 34 | r.logger.Error.Printf("STREAM ERROR %q for %q\n", err, conn.RemoteAddr()) 35 | } 36 | 37 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /v2/relayhttp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-compile/localrelay/internal/httperror" 11 | ) 12 | 13 | func relayHTTP(r *Relay, l net.Listener) error { 14 | 15 | r.logger.Info.Println("STARTING HTTP RELAY") 16 | 17 | return r.httpServer.Serve(l) 18 | } 19 | 20 | // HandleHTTP is to be used as the HTTP relay's handler set in the 21 | // http.Server object 22 | func HandleHTTP(relay *Relay) http.HandlerFunc { 23 | // Forwards relay object to request handler 24 | return func(w http.ResponseWriter, r *http.Request) { 25 | handleHTTP(w, r, relay) 26 | } 27 | } 28 | 29 | func handleHTTP(w http.ResponseWriter, r *http.Request, re *Relay) { 30 | re.Metrics.requests(1) 31 | 32 | destination := re.Destination[0] 33 | 34 | remoteURL := destination.Protocol() + "://" + destination.Addr() + r.URL.Path + "?" + r.URL.Query().Encode() 35 | 36 | // BUG: sometimes requests redirect and cause a loop (Loop is auto stopped) 37 | req, err := http.NewRequest(r.Method, remoteURL, r.Body) 38 | if err != nil { 39 | re.logger.Error.Println("BUILD REQUEST ERROR: ", err) 40 | serviceUnavaliable(w, r) 41 | return 42 | } 43 | 44 | re.Metrics.bandwidth(int(req.ContentLength)+len(remoteURL), 0) 45 | 46 | // Append request headers 47 | for k, v := range r.Header { 48 | req.Header.Set(k, strings.Join(v, ",")) 49 | } 50 | 51 | // used to record dial time 52 | start := time.Now() 53 | 54 | // clone http client, as to not cause a race condition when we apply a proxy 55 | hclient := cloneHttpClient(*re.httpClient) 56 | 57 | proxyStrings, proxyNames, err := destination.Proxy(re) 58 | if err != nil { 59 | re.logger.Error.Printf("destination proxy error: %s\n", err) 60 | serviceUnavaliable(w, r) 61 | return 62 | } 63 | 64 | if len(proxyNames) == 0 { 65 | if !forwardHttp(&hclient, re, req, w, start) { 66 | serviceUnavaliable(w, r) 67 | } 68 | 69 | return 70 | } 71 | 72 | for _, proxyString := range proxyStrings { 73 | hclient.Transport = &http.Transport{ 74 | Proxy: http.ProxyURL(proxyString.URL), 75 | } 76 | 77 | if forwardHttp(&hclient, re, req, w, start) { 78 | // success 79 | return 80 | } 81 | } 82 | 83 | serviceUnavaliable(w, r) 84 | } 85 | 86 | func forwardHttp(hclient *http.Client, re *Relay, req *http.Request, w http.ResponseWriter, start time.Time) bool { 87 | response, err := hclient.Do(req) 88 | if err != nil { 89 | re.logger.Error.Println("FORWARD REQUEST ERROR: ", err) 90 | re.Metrics.dial(0, 1, start) 91 | return false 92 | } 93 | 94 | re.Metrics.dial(1, 0, start) 95 | 96 | defer response.Body.Close() 97 | 98 | // Append response headers 99 | for k, v := range response.Header { 100 | w.Header().Set(k, strings.Join(v, ",")) 101 | } 102 | 103 | w.WriteHeader(response.StatusCode) 104 | 105 | in, _ := io.Copy(w, response.Body) 106 | re.Metrics.bandwidth(0, int(in)) 107 | 108 | return true 109 | } 110 | 111 | func cloneHttpClient(client http.Client) http.Client { 112 | c := client 113 | return c 114 | } 115 | 116 | func serviceUnavaliable(w http.ResponseWriter, r *http.Request) { 117 | w.WriteHeader(503) 118 | w.Write(httperror.Get503()) 119 | } 120 | -------------------------------------------------------------------------------- /v2/relayhttps.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func relayHTTPS(r *Relay, l net.Listener) error { 8 | r.logger.Info.Println("STARTING HTTPS RELAY") 9 | 10 | return r.httpServer.ServeTLS(l, r.certificateFile, r.keyFile) 11 | } 12 | -------------------------------------------------------------------------------- /v2/relaytcp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func listener(r *Relay) (net.Listener, error) { 14 | 15 | network := "tcp" 16 | if r.Listener.ProxyType() == ProxyUDP { 17 | network = "udp" 18 | } 19 | 20 | l, err := net.Listen(network, r.Listener.Addr()) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return l, nil 26 | } 27 | 28 | func relayTCP(r *Relay, l net.Listener) error { 29 | r.logger.Info.Println("STARTING TCP RELAY") 30 | 31 | for { 32 | conn, err := l.Accept() 33 | if err != nil { 34 | if errors.Is(err, net.ErrClosed) { 35 | r.logger.Warning.Println("LISTENER CLOSED") 36 | return nil 37 | } 38 | 39 | r.logger.Warning.Println("ACCEPT FAILED: ", err) 40 | continue 41 | } 42 | 43 | go handleConn(r, conn, "tcp") 44 | } 45 | } 46 | 47 | func handleConn(r *Relay, conn net.Conn, network string) { 48 | r.storeConn(conn) 49 | 50 | defer func() { 51 | conn.Close() 52 | 53 | // remove conn from connPool 54 | r.popConn(conn) 55 | 56 | r.Metrics.connections(-1) 57 | }() 58 | 59 | r.Metrics.connections(1) 60 | 61 | r.logger.Info.Printf("NEW CONNECTION %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 62 | 63 | start := time.Now() 64 | 65 | destinationCandiates := make([]TargetLink, len(r.Destination)) 66 | copy(destinationCandiates, r.Destination) 67 | 68 | for i := 0; len(destinationCandiates) > 0; i++ { 69 | di, destination, err := nextDestination(r, destinationCandiates) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | destinationCandiates = removeTargetlink(destinationCandiates, di) 75 | 76 | // Retrieve proxy config for destination 77 | proxies, proxyNames, err := destination.Proxy(r) 78 | if err != nil { 79 | r.logger.Error.Printf("A PROXY FOR DESTINATION %q WAS REFERENCED BUT NOT DEFINED\n", destination) 80 | return 81 | } 82 | 83 | // if no proxy is set direct dial 84 | if proxies == nil { 85 | r.logger.Info.Printf("DIALING REMOTE [%s]\n", destination) 86 | 87 | if err := dial(r, conn, destination.Addr(), i, destination.Protocol(), start); err != nil { 88 | r.logger.Info.Printf("FAILED DIALING REMOTE [%s]\n", destination) 89 | // errored dialing, continue to try next destination 90 | continue 91 | } 92 | 93 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 94 | // close connection 95 | return 96 | } 97 | 98 | // proxies are set for this destination 99 | for pi, proxy := range proxies { 100 | r.logger.Info.Printf("DIALLING DESTINATION [%d] ADDRESS [%s] THROUGH PROXY %q\n", i+1, destination, proxyNames[pi]) 101 | 102 | // Dial destination through proxy 103 | c, err := proxy.Dialer().Dial(destination.Protocol(), destination.Addr()) 104 | if err != nil { 105 | r.Metrics.dial(0, 1, start) 106 | 107 | r.logger.Error.Printf("FAILED TO DIAL DESTINATION ADDR: %s\n", err) 108 | // try next proxy 109 | continue 110 | } 111 | 112 | r.setConnRemote(conn, c.RemoteAddr()) 113 | 114 | r.Metrics.dial(1, 0, start) 115 | 116 | r.logger.Info.Printf("CONNECTED TO %s\n", destination) 117 | err = streamConns(conn, c, r.Metrics) 118 | if err != nil { 119 | r.logger.Error.Printf("STREAM ERROR %q for %q\n", err, conn.RemoteAddr()) 120 | } 121 | 122 | r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 123 | // close connection 124 | return 125 | } 126 | } 127 | 128 | // for i, destination := range r.Destination { 129 | // proxies, proxyNames, err := destination.Proxy(r) 130 | // if err != nil { 131 | // r.logger.Error.Printf("A PROXY FOR DESTINATION %q WAS REFERENCED BUT NOT DEFINED\n", destination) 132 | // return 133 | // } 134 | 135 | // // if no proxy is set 136 | // if proxies == nil { 137 | // r.logger.Info.Printf("DAILING REMOTE [%s]\n", destination) 138 | 139 | // if err := dial(r, conn, destination.Addr(), i+1, destination.Protocol(), start); err != nil { 140 | // r.logger.Info.Printf("FAILED DAILING REMOTE [%s]\n", destination) 141 | // // errored dialing, continue to try next destination 142 | // continue 143 | // } 144 | 145 | // r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 146 | // // close connection 147 | // return 148 | // } 149 | 150 | // // proxies are set for this destination 151 | // for pi, proxy := range proxies { 152 | // r.logger.Info.Printf("DIALLING DESTINATION [%d] ADDRESS [%s] THROUGH PROXY %q\n", i+1, destination, proxyNames[pi]) 153 | 154 | // // Dial destination through proxy 155 | // c, err := proxy.Dialer().Dial(destination.Protocol(), destination.Addr()) 156 | // if err != nil { 157 | // r.Metrics.dial(0, 1, start) 158 | 159 | // r.logger.Error.Printf("FAILED TO DIAL DESTINATION ADDR: %s\n", err) 160 | // // try next proxy 161 | // continue 162 | // } 163 | 164 | // // TODO: test validate 165 | // r.setConnRemote(c, c.RemoteAddr()) 166 | 167 | // r.Metrics.dial(1, 0, start) 168 | 169 | // r.logger.Info.Printf("CONNECTED TO %s\n", destination) 170 | // streamConns(conn, c, r.Metrics) 171 | 172 | // r.logger.Info.Printf("CONNECTION CLOSED %q ON %q\n", conn.RemoteAddr(), conn.LocalAddr()) 173 | // // close connection 174 | // return 175 | // } 176 | // } 177 | 178 | r.logger.Info.Printf("UNABLE TO MAKE A CONNECTION FROM %q TO %q\n", conn.RemoteAddr(), conn.LocalAddr()) 179 | } 180 | 181 | func streamConns(client net.Conn, remote net.Conn, m *Metrics) error { 182 | wg := sync.WaitGroup{} 183 | 184 | var copyInErr error 185 | 186 | wg.Add(1) 187 | go func() { 188 | copyInErr = copierIn(client, remote, 128, m) 189 | wg.Done() 190 | }() 191 | 192 | wg.Add(1) 193 | err := copierOut(client, remote, 128, m) 194 | wg.Done() 195 | 196 | wg.Wait() 197 | 198 | // if error is reporting that the conn is closed ignore both 199 | if errors.Is(copyInErr, io.EOF) || errors.Is(err, io.EOF) || errors.Is(copyInErr, net.ErrClosed) { 200 | // if any of the errors are EOFs or ErrClosed we are not 201 | // bothered with any additional errors. 202 | return nil 203 | } 204 | 205 | // else propagate error 206 | return err 207 | } 208 | 209 | // NOTE: static function for maximum performance 210 | func copierIn(client net.Conn, dst net.Conn, buffer int, m *Metrics) error { 211 | 212 | buf := make([]byte, buffer) 213 | for { 214 | n, err := dst.Read(buf) 215 | m.bandwidth(0, n) 216 | if err != nil { 217 | 218 | var err1 error 219 | // if we read some data, flush it then return a error 220 | if n > 0 { 221 | _, err1 = dst.Write(buf[:n]) 222 | } 223 | 224 | err2 := client.Close() 225 | err3 := dst.Close() 226 | 227 | // wrap all errors into one 228 | if err1 != nil { 229 | err = errors.Wrap(err, err1.Error()) 230 | } 231 | 232 | // wrap all errors into one 233 | if err2 != nil { 234 | err = errors.Wrap(err, err2.Error()) 235 | } 236 | 237 | // wrap all errors into one 238 | if err3 != nil { 239 | err = errors.Wrap(err, err3.Error()) 240 | } 241 | 242 | return errors.WithStack(err) 243 | } 244 | 245 | if n2, err := client.Write(buf[:n]); err != nil || n2 != n { 246 | err1 := client.Close() 247 | err2 := dst.Close() 248 | 249 | // wrap all errors into one 250 | if err1 != nil { 251 | err = errors.Wrap(err, err1.Error()) 252 | } 253 | 254 | // wrap all errors into one 255 | if err2 != nil { 256 | err = errors.Wrap(err, err2.Error()) 257 | } 258 | 259 | return errors.WithStack(err) 260 | } 261 | } 262 | } 263 | 264 | // NOTE: static function for maximum performance 265 | func copierOut(client net.Conn, dst net.Conn, buffer int, m *Metrics) error { 266 | 267 | buf := make([]byte, buffer) 268 | for { 269 | 270 | n, err := client.Read(buf) 271 | m.bandwidth(n, 0) 272 | if err != nil { 273 | 274 | var err1 error 275 | // if we read some data, flush it then return a error 276 | if n > 0 { 277 | _, err1 = dst.Write(buf[:n]) 278 | } 279 | 280 | err2 := client.Close() 281 | err3 := dst.Close() 282 | 283 | // wrap all errors into one 284 | if err1 != nil { 285 | err = errors.Wrap(err, err1.Error()) 286 | } 287 | 288 | // wrap all errors into one 289 | if err2 != nil { 290 | err = errors.Wrap(err, err2.Error()) 291 | } 292 | 293 | // wrap all errors into one 294 | if err3 != nil { 295 | err = errors.Wrap(err, err3.Error()) 296 | } 297 | 298 | return errors.WithStack(err) 299 | } 300 | 301 | if n2, err := dst.Write(buf[:n]); err != nil || n2 != n { 302 | err1 := client.Close() 303 | err2 := dst.Close() 304 | 305 | // wrap all errors into one 306 | if err1 != nil { 307 | err = errors.Wrap(err, err1.Error()) 308 | } 309 | 310 | // wrap all errors into one 311 | if err2 != nil { 312 | err = errors.Wrap(err, err2.Error()) 313 | } 314 | 315 | return errors.WithStack(err) 316 | } 317 | } 318 | } 319 | 320 | func removeTargetlink(slice []TargetLink, s int) []TargetLink { 321 | return append(slice[:s], slice[s+1:]...) 322 | } 323 | -------------------------------------------------------------------------------- /v2/relayudp.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | ) 7 | 8 | func relayUDP(r *Relay, l net.Listener) error { 9 | 10 | r.logger.Info.Println("STARTING UDP RELAY") 11 | 12 | for { 13 | conn, err := l.Accept() 14 | if err != nil { 15 | if errors.Is(err, net.ErrClosed) { 16 | r.logger.Warning.Println("LISTENER CLOSED") 17 | return nil 18 | } 19 | 20 | r.logger.Warning.Println("ACCEPT FAILED: ", err) 21 | continue 22 | } 23 | 24 | go handleConn(r, conn, "udp") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /v2/target.go: -------------------------------------------------------------------------------- 1 | package localrelay 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/mroth/weightedrand" 11 | ) 12 | 13 | var ( 14 | ErrProxyDefine = errors.New("proxy is not defined") 15 | ) 16 | 17 | type TargetLink string 18 | 19 | func (t *TargetLink) String() string { 20 | u, _ := url.Parse(string(*t)) 21 | return u.Host 22 | } 23 | 24 | // ProxyType returns the protocol as a ProxyType 25 | func (t *TargetLink) ProxyType() ProxyType { 26 | return ProxyType(t.Protocol()) 27 | } 28 | 29 | // Addr returns the address within the target link. 30 | // Example: 127.0.0.1:443 31 | func (t *TargetLink) Addr() string { 32 | u, _ := url.Parse(string(*t)) 33 | return u.Hostname() + ":" + t.Port() 34 | } 35 | 36 | // Host returns the host/ip of the target 37 | func (t *TargetLink) Host() string { 38 | u, _ := url.Parse(string(*t)) 39 | return u.Hostname() 40 | } 41 | 42 | // Port returns the port number of the target 43 | func (t *TargetLink) Port() string { 44 | u, _ := url.Parse(string(*t)) 45 | 46 | if len(u.Port()) > 0 { 47 | return u.Port() 48 | } 49 | switch t.Protocol() { 50 | case "https": 51 | return "443" 52 | case "http": 53 | return "80" 54 | default: 55 | return "" 56 | } 57 | } 58 | 59 | // Protocol returns the protocol of the target 60 | func (t *TargetLink) Protocol() string { 61 | u, _ := url.Parse(string(*t)) 62 | return strings.ToLower(u.Scheme) 63 | } 64 | 65 | // Proxy parses the TargetLink and uses the relay to lookup proxy dialers. 66 | // The returned array is in the same order as written. 67 | func (t *TargetLink) Proxy(r *Relay) ([]ProxyURL, []string, error) { 68 | u, _ := url.Parse(string(*t)) 69 | 70 | // get ?proxy= from TargetLink and split into comma seperated array 71 | proxieNames := strings.Split(u.Query().Get("proxy"), ",") 72 | if len(proxieNames) == 0 || len(proxieNames[0]) == 0 { 73 | return nil, nil, nil 74 | } 75 | 76 | proxies := make([]ProxyURL, len(proxieNames)) 77 | for i := 0; i < len(proxies); i++ { 78 | proxy, found := r.proxies[proxieNames[i]] 79 | if !found { 80 | return proxies, proxieNames, ErrProxyDefine 81 | } 82 | 83 | proxies[i] = proxy 84 | } 85 | 86 | return proxies, proxieNames, nil 87 | } 88 | 89 | // Print returns the targetlink string 90 | func (t *TargetLink) Print() string { 91 | return string(*t) 92 | } 93 | 94 | // LbWeight returns the weight provided or the default value of 100 95 | func (t *TargetLink) LbWeight() uint { 96 | u, _ := url.Parse(string(*t)) 97 | weight := u.Query().Get("lb_weight") 98 | 99 | if weight == "" { 100 | return 100 101 | } 102 | 103 | n, err := strconv.Atoi(weight) 104 | if err != nil { 105 | log.Fatalf("Weight could not be parsed for %s\n", t) 106 | } 107 | 108 | return uint(n) 109 | } 110 | 111 | // Lb returns true if loadbalancing is enabled 112 | func (t *TargetLink) Lb() bool { 113 | u, _ := url.Parse(string(*t)) 114 | switch strings.ToLower(u.Query().Get("lb")) { 115 | case "false", "off", "disabled", "inactive", "0", "no": 116 | return false 117 | default: 118 | return true 119 | } 120 | } 121 | 122 | // nextDestination using the provided list of potential destinations, find the appripriate 123 | // next one to try based on the relay config, e.g. loadbalance and failovers. 124 | func nextDestination(r *Relay, dsts []TargetLink) (int, TargetLink, error) { 125 | candiates := []TargetLink{} 126 | 127 | if r.loadbalance.Enabled { 128 | choices := []weightedrand.Choice{} 129 | // Remove all non loadbalanced dsts 130 | for i := 0; i < len(dsts); i++ { 131 | if dsts[i].Lb() { 132 | candiates = append(candiates, dsts[i]) 133 | choices = append(choices, weightedrand.NewChoice(i, dsts[i].LbWeight())) 134 | } 135 | } 136 | 137 | // there are no load balanced relays left, use the failovers 138 | if len(candiates) == 0 { 139 | return 0, dsts[0], nil 140 | } 141 | 142 | chooser, err := weightedrand.NewChooser(choices...) 143 | if err != nil { 144 | return 0, "", err 145 | } 146 | 147 | dstI := chooser.Pick().(int) 148 | 149 | return dstI, dsts[dstI], nil 150 | } 151 | 152 | // return the first destination 153 | return 0, dsts[0], nil 154 | } 155 | -------------------------------------------------------------------------------- /v2/target_test.go: -------------------------------------------------------------------------------- 1 | package localrelay_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-compile/localrelay/v2" 7 | ) 8 | 9 | func TestTargetParseTCP(t *testing.T) { 10 | target := localrelay.TargetLink("tcp://127.0.0.1:443") 11 | 12 | if x := target.Addr(); x != "127.0.0.1:443" { 13 | t.Errorf("unexpected target address: %s", x) 14 | } 15 | 16 | if x := target.Host(); x != "127.0.0.1" { 17 | t.Errorf("unexpected target host: %s", x) 18 | } 19 | 20 | if x := target.Port(); x != "443" { 21 | t.Errorf("unexpected target port: %s", x) 22 | } 23 | 24 | if x := target.Protocol(); x != "tcp" { 25 | t.Errorf("unexpected target protocol: %s", x) 26 | } 27 | } 28 | 29 | func TestTargetParseHTTPS(t *testing.T) { 30 | target := localrelay.TargetLink("https://example.com") 31 | 32 | if target.Addr() != "example.com:443" { 33 | t.Error("unexpected target address") 34 | } 35 | 36 | if target.Host() != "example.com" { 37 | t.Error("unexpected target host") 38 | } 39 | 40 | if target.Port() != "443" { 41 | t.Error("unexpected target port") 42 | } 43 | 44 | if target.Protocol() != "https" { 45 | t.Error("unexpected target protocol") 46 | } 47 | } 48 | --------------------------------------------------------------------------------