├── .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 | [](https://github.com/go-compile/localrelay/releases)
4 | [](https://goreportcard.com/report/go-compile/localrelay)
5 | [](https://pkg.go.dev/github.com/go-compile/localrelay)
6 | [](https://hub.docker.com/r/gocompile/localrelay/)
7 | [](https://hub.docker.com/r/gocompile/localrelay/)
8 | 
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 | |  |
80 | |  |
81 | |  |
82 | | |
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 |
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 |
--------------------------------------------------------------------------------