├── .dockerignore
├── .github
├── stale.yml
└── workflows
│ ├── build.yml
│ └── docker-ci.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── clock
└── clock.go
├── dialer
├── fixed.go
├── resolver.go
└── upstream.go
├── go.mod
├── go.sum
├── handler
├── handler.go
└── socks.go
├── log
├── condlog.go
└── logwriter.go
├── main.go
├── seclient
├── csrand.go
├── hash.go
├── jar.go
├── messages.go
├── randutils.go
└── seclient.go
└── snapcraft.yaml
/.dockerignore:
--------------------------------------------------------------------------------
1 | bin/
2 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 |
14 | steps:
15 | -
16 | name: Checkout
17 | uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 | -
21 | name: Setup Go
22 | uses: actions/setup-go@v4
23 | with:
24 | go-version: 'stable'
25 | -
26 | name: Read tag
27 | id: tag
28 | run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
29 | -
30 | name: Build
31 | run: >-
32 | make -j $(nproc) allplus
33 | NDK_CC_ARM64="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
34 | NDK_CC_ARM="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
35 | VERSION=${{steps.tag.outputs.tag}}
36 | -
37 | name: Release
38 | uses: softprops/action-gh-release@v1
39 | with:
40 | files: bin/*
41 | fail_on_unmatched_files: true
42 | generate_release_notes: true
43 |
--------------------------------------------------------------------------------
/.github/workflows/docker-ci.yml:
--------------------------------------------------------------------------------
1 | name: docker-ci
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | docker:
10 | runs-on: ubuntu-latest
11 | steps:
12 | -
13 | name: Checkout
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 | -
18 | name: Find Git Tag
19 | id: tagger
20 | uses: jimschubert/query-tag-action@v2
21 | with:
22 | include: 'v*'
23 | exclude: '*-rc*'
24 | commit-ish: 'HEAD'
25 | skip-unshallow: 'true'
26 | abbrev: 7
27 | - name: Docker meta
28 | id: meta
29 | uses: docker/metadata-action@v5
30 | with:
31 | # list of Docker images to use as base name for tags
32 | images: |
33 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
34 | # generate Docker tags based on the following events/attributes
35 | tags: |
36 | type=semver,pattern={{version}}
37 | type=semver,pattern={{major}}.{{minor}}
38 | type=semver,pattern={{major}}
39 | type=sha
40 | -
41 | name: Set up QEMU
42 | uses: docker/setup-qemu-action@v3
43 | -
44 | name: Set up Docker Buildx
45 | uses: docker/setup-buildx-action@v3
46 | -
47 | name: Login to DockerHub
48 | uses: docker/login-action@v3
49 | with:
50 | username: ${{ secrets.DOCKERHUB_USERNAME }}
51 | password: ${{ secrets.DOCKERHUB_TOKEN }}
52 | -
53 | name: Build and push
54 | id: docker_build
55 | uses: docker/build-push-action@v5
56 | with:
57 | context: .
58 | platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
59 | push: true
60 | tags: ${{ steps.meta.outputs.tags }}
61 | labels: ${{ steps.meta.outputs.labels }}
62 | build-args: 'GIT_DESC=${{steps.tagger.outputs.tag}}'
63 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 | bin/
17 | *.snap
18 | opera-proxy
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=$BUILDPLATFORM golang:1 AS build
2 |
3 | ARG GIT_DESC=undefined
4 |
5 | WORKDIR /go/src/github.com/Snawoot/opera-proxy
6 | COPY . .
7 | ARG TARGETOS TARGETARCH
8 | RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static" -X main.version='"$GIT_DESC"
9 | ADD https://curl.haxx.se/ca/cacert.pem /certs.crt
10 | RUN chmod 0644 /certs.crt
11 |
12 | FROM scratch AS arrange
13 | COPY --from=build /go/src/github.com/Snawoot/opera-proxy/opera-proxy /
14 | COPY --from=build /certs.crt /etc/ssl/certs/ca-certificates.crt
15 |
16 | FROM scratch
17 | COPY --from=arrange / /
18 | USER 9999:9999
19 | EXPOSE 18080/tcp
20 | ENTRYPOINT ["/opera-proxy", "-bind-address", "0.0.0.0:18080"]
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Snawoot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROGNAME = opera-proxy
2 | OUTSUFFIX = bin/$(PROGNAME)
3 | VERSION := $(shell git describe)
4 | BUILDOPTS = -a -tags netgo -trimpath -asmflags -trimpath
5 | LDFLAGS = -ldflags '-s -w -extldflags "-static" -X main.version=$(VERSION)'
6 | LDFLAGS_NATIVE = -ldflags '-s -w -X main.version=$(VERSION)'
7 |
8 | NDK_CC_ARM = $(abspath ../../ndk-toolchain-arm/bin/arm-linux-androideabi-gcc)
9 | NDK_CC_ARM64 = $(abspath ../../ndk-toolchain-arm64/bin/aarch64-linux-android21-clang)
10 |
11 | GO := go
12 |
13 | src = $(wildcard *.go */*.go */*/*.go) go.mod go.sum
14 |
15 | native: bin-native
16 | all: bin-linux-amd64 bin-linux-386 bin-linux-arm bin-linux-arm64 \
17 | bin-linux-mips bin-linux-mipsle bin-linux-mips64 bin-linux-mips64le \
18 | bin-freebsd-amd64 bin-freebsd-386 bin-freebsd-arm bin-freebsd-arm64 \
19 | bin-netbsd-amd64 bin-netbsd-386 bin-netbsd-arm bin-netbsd-arm64 \
20 | bin-openbsd-amd64 bin-openbsd-386 bin-openbsd-arm bin-openbsd-arm64 \
21 | bin-darwin-amd64 bin-darwin-arm64 \
22 | bin-windows-amd64 bin-windows-386 bin-windows-arm
23 |
24 | allplus: all \
25 | bin-android-arm bin-android-arm64
26 |
27 | bin-native: $(OUTSUFFIX)
28 | bin-linux-amd64: $(OUTSUFFIX).linux-amd64
29 | bin-linux-386: $(OUTSUFFIX).linux-386
30 | bin-linux-arm: $(OUTSUFFIX).linux-arm
31 | bin-linux-arm64: $(OUTSUFFIX).linux-arm64
32 | bin-linux-mips: $(OUTSUFFIX).linux-mips
33 | bin-linux-mipsle: $(OUTSUFFIX).linux-mipsle
34 | bin-linux-mips64: $(OUTSUFFIX).linux-mips64
35 | bin-linux-mips64le: $(OUTSUFFIX).linux-mips64le
36 | bin-freebsd-amd64: $(OUTSUFFIX).freebsd-amd64
37 | bin-freebsd-386: $(OUTSUFFIX).freebsd-386
38 | bin-freebsd-arm: $(OUTSUFFIX).freebsd-arm
39 | bin-freebsd-arm64: $(OUTSUFFIX).freebsd-arm64
40 | bin-netbsd-amd64: $(OUTSUFFIX).netbsd-amd64
41 | bin-netbsd-386: $(OUTSUFFIX).netbsd-386
42 | bin-netbsd-arm: $(OUTSUFFIX).netbsd-arm
43 | bin-netbsd-arm64: $(OUTSUFFIX).netbsd-arm64
44 | bin-openbsd-amd64: $(OUTSUFFIX).openbsd-amd64
45 | bin-openbsd-386: $(OUTSUFFIX).openbsd-386
46 | bin-openbsd-arm: $(OUTSUFFIX).openbsd-arm
47 | bin-openbsd-arm64: $(OUTSUFFIX).openbsd-arm64
48 | bin-darwin-amd64: $(OUTSUFFIX).darwin-amd64
49 | bin-darwin-arm64: $(OUTSUFFIX).darwin-arm64
50 | bin-windows-amd64: $(OUTSUFFIX).windows-amd64.exe
51 | bin-windows-386: $(OUTSUFFIX).windows-386.exe
52 | bin-windows-arm: $(OUTSUFFIX).windows-arm.exe
53 | bin-android-arm: $(OUTSUFFIX).android-arm
54 | bin-android-arm64: $(OUTSUFFIX).android-arm64
55 |
56 | $(OUTSUFFIX): $(src)
57 | $(GO) build $(LDFLAGS_NATIVE) -o $@
58 |
59 | $(OUTSUFFIX).linux-amd64: $(src)
60 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
61 |
62 | $(OUTSUFFIX).linux-386: $(src)
63 | CGO_ENABLED=0 GOOS=linux GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
64 |
65 | $(OUTSUFFIX).linux-arm: $(src)
66 | CGO_ENABLED=0 GOOS=linux GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
67 |
68 | $(OUTSUFFIX).linux-arm64: $(src)
69 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
70 |
71 | $(OUTSUFFIX).linux-mips: $(src)
72 | CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
73 |
74 | $(OUTSUFFIX).linux-mips64: $(src)
75 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64 GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
76 |
77 | $(OUTSUFFIX).linux-mipsle: $(src)
78 | CGO_ENABLED=0 GOOS=linux GOARCH=mipsle GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
79 |
80 | $(OUTSUFFIX).linux-mips64le: $(src)
81 | CGO_ENABLED=0 GOOS=linux GOARCH=mips64le GOMIPS=softfloat $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
82 |
83 | $(OUTSUFFIX).freebsd-amd64: $(src)
84 | CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
85 |
86 | $(OUTSUFFIX).freebsd-386: $(src)
87 | CGO_ENABLED=0 GOOS=freebsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
88 |
89 | $(OUTSUFFIX).freebsd-arm: $(src)
90 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
91 |
92 | $(OUTSUFFIX).freebsd-arm64: $(src)
93 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
94 |
95 | $(OUTSUFFIX).netbsd-amd64: $(src)
96 | CGO_ENABLED=0 GOOS=netbsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
97 |
98 | $(OUTSUFFIX).netbsd-386: $(src)
99 | CGO_ENABLED=0 GOOS=netbsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
100 |
101 | $(OUTSUFFIX).netbsd-arm: $(src)
102 | CGO_ENABLED=0 GOOS=netbsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
103 |
104 | $(OUTSUFFIX).netbsd-arm64: $(src)
105 | CGO_ENABLED=0 GOOS=netbsd GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
106 |
107 | $(OUTSUFFIX).openbsd-amd64: $(src)
108 | CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
109 |
110 | $(OUTSUFFIX).openbsd-386: $(src)
111 | CGO_ENABLED=0 GOOS=openbsd GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
112 |
113 | $(OUTSUFFIX).openbsd-arm: $(src)
114 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
115 |
116 | $(OUTSUFFIX).openbsd-arm64: $(src)
117 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
118 |
119 | $(OUTSUFFIX).darwin-amd64: $(src)
120 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
121 |
122 | $(OUTSUFFIX).darwin-arm64: $(src)
123 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
124 |
125 | $(OUTSUFFIX).windows-amd64.exe: $(src)
126 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
127 |
128 | $(OUTSUFFIX).windows-386.exe: $(src)
129 | CGO_ENABLED=0 GOOS=windows GOARCH=386 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
130 |
131 | $(OUTSUFFIX).windows-arm.exe: $(src)
132 | CGO_ENABLED=0 GOOS=windows GOARCH=arm GOARM=7 $(GO) build $(BUILDOPTS) $(LDFLAGS) -o $@
133 |
134 | $(OUTSUFFIX).android-arm: $(src)
135 | CC=$(NDK_CC_ARM) CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 $(GO) build $(LDFLAGS_NATIVE) -o $@
136 |
137 | $(OUTSUFFIX).android-arm64: $(src)
138 | CC=$(NDK_CC_ARM64) CGO_ENABLED=1 GOOS=android GOARCH=arm64 $(GO) build $(LDFLAGS_NATIVE) -o $@
139 |
140 | clean:
141 | rm -f bin/*
142 |
143 | fmt:
144 | $(GO) fmt ./...
145 |
146 | run:
147 | $(GO) run $(LDFLAGS) .
148 |
149 | install:
150 | $(GO) install $(LDFLAGS_NATIVE) .
151 |
152 | .PHONY: clean all native fmt install \
153 | bin-native \
154 | bin-linux-amd64 \
155 | bin-linux-386 \
156 | bin-linux-arm \
157 | bin-linux-arm64 \
158 | bin-linux-mips \
159 | bin-linux-mipsle \
160 | bin-linux-mips64 \
161 | bin-linux-mips64le \
162 | bin-freebsd-amd64 \
163 | bin-freebsd-386 \
164 | bin-freebsd-arm \
165 | bin-freebsd-arm64 \
166 | bin-netbsd-amd64 \
167 | bin-netbsd-386 \
168 | bin-netbsd-arm \
169 | bin-netbsd-arm64 \
170 | bin-openbsd-amd64 \
171 | bin-openbsd-386 \
172 | bin-openbsd-arm \
173 | bin-openbsd-arm64 \
174 | bin-darwin-amd64 \
175 | bin-darwin-arm64 \
176 | bin-windows-amd64 \
177 | bin-windows-386 \
178 | bin-windows-arm \
179 | bin-android-arm \
180 | bin-android-arm64
181 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | opera-proxy
2 | ===========
3 |
4 | [](https://snapcraft.io/opera-proxy)
5 |
6 |
7 |
8 | Standalone Opera VPN client. Younger brother of [hola-proxy](https://github.com/Snawoot/hola-proxy/).
9 |
10 | Just run it and it'll start a plain HTTP proxy server forwarding traffic through "Opera VPN" proxies of your choice.
11 | By default the application listens on 127.0.0.1:18080.
12 |
13 | ## Features
14 |
15 | * Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD)
16 | * Uses TLS for secure communication with upstream proxies
17 | * Zero configuration
18 | * Simple and straightforward
19 |
20 | ## Installation
21 |
22 | #### Binaries
23 |
24 | Pre-built binaries are available [here](https://github.com/Snawoot/opera-proxy/releases/latest).
25 |
26 | #### Build from source
27 |
28 | Alternatively, you may install opera-proxy from source. Run the following within the source directory:
29 |
30 | ```
31 | make install
32 | ```
33 |
34 | #### Docker
35 |
36 | A docker image is available as well. Here is an example of running opera-proxy as a background service:
37 |
38 | ```sh
39 | docker run -d \
40 | --security-opt no-new-privileges \
41 | -p 127.0.0.1:18080:18080 \
42 | --restart unless-stopped \
43 | --name opera-proxy \
44 | yarmak/opera-proxy
45 | ```
46 |
47 | #### Snap Store
48 |
49 | [](https://snapcraft.io/opera-proxy)
50 |
51 | ```bash
52 | sudo snap install opera-proxy
53 | ```
54 |
55 | ## Usage
56 |
57 | List available countries:
58 |
59 | ```
60 | $ ./opera-proxy -list-countries
61 | country code,country name
62 | EU,Europe
63 | AS,Asia
64 | AM,Americas
65 | ```
66 |
67 | Run proxy via country of your choice:
68 |
69 | ```
70 | $ ./opera-proxy -country EU
71 | ```
72 |
73 | Also it is possible to export proxy addresses and credentials:
74 |
75 | ```
76 | $ ./opera-proxy -country EU -list-proxies
77 | Proxy login: ABCF206831D0BDC0C8C3AE5283F99EF6726444B3
78 | Proxy password: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb3VudHJ5IjoidWEiLCJpYXQiOjE2MTY4MDkxMTIsImlkIjoic2UwMzE2LTYweGY3aTBxMGhoOWQ1MWF0emd0IiwiaXAiOiI3Ny4xMTEuMjQ3LjE3IiwidnBuX2xvZ2luIjoiSzJYdmJ5R0tUb3JLbkpOaDNtUGlGSTJvSytyVTA5bXMraGt2c2UwRWJBcz1Ac2UwMzE2LmJlc3QudnBuIn0.ZhqqzVyKmc3hZG6VVwWfn4nvVIPuZvaEfOLXfTppyvo
79 | Proxy-Authorization: Basic QUJDRjIwNjgzMUQwQkRDMEM4QzNBRTUyODNGOTlFRjY3MjY0NDRCMzpleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKamIzVnVkSEo1SWpvaWRXRWlMQ0pwWVhRaU9qRTJNVFk0TURreE1USXNJbWxrSWpvaWMyVXdNekUyTFRZd2VHWTNhVEJ4TUdob09XUTFNV0YwZW1kMElpd2lhWEFpT2lJM055NHhNVEV1TWpRM0xqRTNJaXdpZG5CdVgyeHZaMmx1SWpvaVN6SllkbUo1UjB0VWIzSkxia3BPYUROdFVHbEdTVEp2U3l0eVZUQTViWE1yYUd0MmMyVXdSV0pCY3oxQWMyVXdNekUyTG1KbGMzUXVkbkJ1SW4wLlpocXF6VnlLbWMzaFpHNlZWd1dmbjRudlZJUHVadmFFZk9MWGZUcHB5dm8=
80 |
81 | host,ip_address,port
82 | eu0.sec-tunnel.com,77.111.244.26,443
83 | eu1.sec-tunnel.com,77.111.244.67,443
84 | eu2.sec-tunnel.com,77.111.247.51,443
85 | eu3.sec-tunnel.com,77.111.244.22,443
86 | ```
87 |
88 | ## List of arguments
89 |
90 | | Argument | Type | Description |
91 | | -------- | ---- | ----------- |
92 | | api-address | String | override IP address of api2.sec-tunnel.com |
93 | | api-client-type | String | client type reported to SurfEasy API (default "se0316") |
94 | | api-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
95 | | api-login | String | SurfEasy API login (default "se0316") |
96 | | api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
97 | | api-user-agent | String | user agent reported to SurfEasy API (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0") |
98 | | bind-address | String | proxy listen address (default "127.0.0.1:18080") |
99 | | bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT/DoQ resolvers for initial discovery of SurfEasy API address. See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. Examples: `https://1.1.1.1/dns-query`, `quic://dns.adguard.com` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) |
100 | | cafile | String | use custom CA certificate bundle file |
101 | | certchain-workaround | Boolean | add bundled cross-signed intermediate cert to certchain to make it check out on old systems (default true) |
102 | | country | String | desired proxy location (default "EU") |
103 | | fake-SNI | String | domain name to use as SNI in communications with servers |
104 | | init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
105 | | init-retry-interval | Duration | delay between initialization retries (default 5s) |
106 | | list-countries | - | list available countries and exit |
107 | | list-proxies | - | output proxy list and exit |
108 | | override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
109 | | proxy | String | sets base proxy to use for all dial-outs. Format: `://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` |
110 | | refresh | Duration | login refresh interval (default 4h0m0s) |
111 | | refresh-retry | Duration | login refresh retry interval (default 5s) |
112 | | timeout | Duration | timeout for network operations (default 10s) |
113 | | verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
114 | | version | - | show program version and exit |
115 | | socks-mode | - | listen for SOCKS requests instead of HTTP |
116 |
117 | ## See also
118 |
119 | * [Project wiki](https://github.com/Snawoot/opera-proxy/wiki)
120 | * [Community in Telegram](https://t.me/alternative_proxy)
121 |
--------------------------------------------------------------------------------
/clock/clock.go:
--------------------------------------------------------------------------------
1 | package clock
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | const WALLCLOCK_PRECISION = 1 * time.Second
9 |
10 | func AfterWallClock(d time.Duration) <-chan time.Time {
11 | ch := make(chan time.Time, 1)
12 | deadline := time.Now().Add(d).Truncate(0)
13 | after_ch := time.After(d)
14 | ticker := time.NewTicker(WALLCLOCK_PRECISION)
15 | go func() {
16 | var t time.Time
17 | defer ticker.Stop()
18 | for {
19 | select {
20 | case t = <-after_ch:
21 | ch <- t
22 | return
23 | case t = <-ticker.C:
24 | if t.After(deadline) {
25 | ch <- t
26 | return
27 | }
28 | }
29 | }
30 | }()
31 | return ch
32 | }
33 |
34 | func RunTicker(ctx context.Context, interval, retryInterval time.Duration, cb func(context.Context) error) {
35 | go func() {
36 | var err error
37 | for {
38 | nextInterval := interval
39 | if err != nil {
40 | nextInterval = retryInterval
41 | }
42 | select {
43 | case <-ctx.Done():
44 | return
45 | case <-AfterWallClock(nextInterval):
46 | err = cb(ctx)
47 | }
48 | }
49 | }()
50 | }
51 |
--------------------------------------------------------------------------------
/dialer/fixed.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "context"
5 | "net"
6 | )
7 |
8 | type FixedDialer struct {
9 | fixedAddress string
10 | next ContextDialer
11 | }
12 |
13 | func NewFixedDialer(address string, next ContextDialer) *FixedDialer {
14 | return &FixedDialer{
15 | fixedAddress: address,
16 | next: next,
17 | }
18 | }
19 |
20 | func (d *FixedDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
21 | _, port, err := net.SplitHostPort(address)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | return d.next.DialContext(ctx, network, net.JoinHostPort(d.fixedAddress, port))
27 | }
28 |
29 | func (d *FixedDialer) Dial(network, address string) (net.Conn, error) {
30 | return d.DialContext(context.Background(), network, address)
31 | }
32 |
--------------------------------------------------------------------------------
/dialer/resolver.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "net"
8 | "net/netip"
9 | "time"
10 |
11 | "github.com/AdguardTeam/dnsproxy/upstream"
12 | "github.com/hashicorp/go-multierror"
13 | )
14 |
15 | type Resolver struct {
16 | resolvers upstream.ParallelResolver
17 | timeout time.Duration
18 | }
19 |
20 | func NewResolver(addresses []string, timeout time.Duration) (*Resolver, error) {
21 | resolvers := make([]upstream.Resolver, 0, len(addresses))
22 | opts := &upstream.Options{
23 | Timeout: timeout,
24 | }
25 | for _, addr := range addresses {
26 | u, err := upstream.AddressToUpstream(addr, opts)
27 | if err != nil {
28 | return nil, fmt.Errorf("unable to construct upstream resolver from string %q: %w",
29 | addr, err)
30 | }
31 | resolvers = append(resolvers, &upstream.UpstreamResolver{Upstream: u})
32 | }
33 | return &Resolver{
34 | resolvers: resolvers,
35 | timeout: timeout,
36 | }, nil
37 | }
38 |
39 | func (r *Resolver) LookupNetIP(ctx context.Context, network string, host string) (addrs []netip.Addr, err error) {
40 | return r.resolvers.LookupNetIP(ctx, network, host)
41 | }
42 |
43 | func (r *Resolver) Close() error {
44 | var res error
45 | for _, resolver := range r.resolvers {
46 | if closer, ok := resolver.(io.Closer); ok {
47 | if err := closer.Close(); err != nil {
48 | res = multierror.Append(res, err)
49 | }
50 | }
51 | }
52 | return res
53 | }
54 |
55 | type LookupNetIPer interface {
56 | LookupNetIP(context.Context, string, string) ([]netip.Addr, error)
57 | }
58 |
59 | type ResolvingDialer struct {
60 | lookup LookupNetIPer
61 | next ContextDialer
62 | }
63 |
64 | func NewResolvingDialer(lookup LookupNetIPer, next ContextDialer) *ResolvingDialer {
65 | return &ResolvingDialer{
66 | lookup: lookup,
67 | next: next,
68 | }
69 | }
70 |
71 | func (d *ResolvingDialer) Dial(network, address string) (net.Conn, error) {
72 | return d.DialContext(context.Background(), network, address)
73 | }
74 |
75 | func (d *ResolvingDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
76 | host, port, err := net.SplitHostPort(address)
77 | if err != nil {
78 | return nil, fmt.Errorf("failed to extract host and port from %s: %w", address, err)
79 | }
80 |
81 | var resolveNetwork string
82 | switch network {
83 | case "udp4", "tcp4", "ip4":
84 | resolveNetwork = "ip4"
85 | case "udp6", "tcp6", "ip6":
86 | resolveNetwork = "ip6"
87 | case "udp", "tcp", "ip":
88 | resolveNetwork = "ip"
89 | default:
90 | return nil, fmt.Errorf("resolving dial %q: unsupported network %q", address, network)
91 | }
92 | resolved, err := d.lookup.LookupNetIP(ctx, resolveNetwork, host)
93 | if err != nil {
94 | return nil, fmt.Errorf("dial failed on address lookup: %w", err)
95 | }
96 |
97 | var conn net.Conn
98 | for _, ip := range resolved {
99 | conn, err = d.next.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
100 | if err == nil {
101 | return conn, nil
102 | }
103 | }
104 |
105 | return nil, fmt.Errorf("failed to dial %s: %w", address, err)
106 | }
107 |
--------------------------------------------------------------------------------
/dialer/upstream.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "context"
7 | "crypto/tls"
8 | "crypto/x509"
9 | "encoding/base64"
10 | "encoding/pem"
11 | "errors"
12 | "fmt"
13 | "io"
14 | "net"
15 | "net/http"
16 | "net/http/httputil"
17 | "net/url"
18 | "strings"
19 | )
20 |
21 | const (
22 | PROXY_CONNECT_METHOD = "CONNECT"
23 | PROXY_HOST_HEADER = "Host"
24 | PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"
25 | MISSING_CHAIN_CERT = `-----BEGIN CERTIFICATE-----
26 | MIID0zCCArugAwIBAgIQVmcdBOpPmUxvEIFHWdJ1lDANBgkqhkiG9w0BAQwFADB7
27 | MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
28 | VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
29 | AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
30 | MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
31 | MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
32 | ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0
33 | aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEGqxUWqn5aCPnetUkb1PGWthL
34 | q8bVttHmc3Gu3ZzWDGH926CJA7gFFOxXzu5dP+Ihs8731Ip54KODfi2X0GHE8Znc
35 | JZFjq38wo7Rw4sehM5zzvy5cU7Ffs30yf4o043l5o4HyMIHvMB8GA1UdIwQYMBaA
36 | FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1
37 | xmNjmjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI
38 | MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j
39 | b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG
40 | CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM
41 | BQADggEBABns652JLCALBIAdGN5CmXKZFjK9Dpx1WywV4ilAbe7/ctvbq5AfjJXy
42 | ij0IckKJUAfiORVsAYfZFhr1wHUrxeZWEQff2Ji8fJ8ZOd+LygBkc7xGEJuTI42+
43 | FsMuCIKchjN0djsoTI0DQoWz4rIjQtUfenVqGtF8qmchxDM6OW1TyaLtYiKou+JV
44 | bJlsQ2uRl9EMC5MCHdK8aXdJ5htN978UeAOwproLtOGFfy/cQjutdAFI3tZs4RmY
45 | CV4Ks2dH/hzg1cEo70qLRDEmBDeNiXQ2Lu+lIg+DdEmSx/cQwgwp+7e9un/jX9Wf
46 | 8qn0dNW44bOwgeThpWOjzOoEeJBuv/c=
47 | -----END CERTIFICATE-----
48 | `
49 | )
50 |
51 | var missingLinkDER, _ = pem.Decode([]byte(MISSING_CHAIN_CERT))
52 | var missingLink, _ = x509.ParseCertificate(missingLinkDER.Bytes)
53 |
54 | type stringCb = func() (string, error)
55 |
56 | type Dialer interface {
57 | Dial(network, address string) (net.Conn, error)
58 | }
59 |
60 | type ContextDialer interface {
61 | Dialer
62 | DialContext(ctx context.Context, network, address string) (net.Conn, error)
63 | }
64 |
65 | type ProxyDialer struct {
66 | address stringCb
67 | tlsServerName stringCb
68 | fakeSNI stringCb
69 | auth stringCb
70 | next ContextDialer
71 | intermediateWorkaround bool
72 | caPool *x509.CertPool
73 | }
74 |
75 | func NewProxyDialer(address, tlsServerName, fakeSNI, auth stringCb, intermediateWorkaround bool, caPool *x509.CertPool, nextDialer ContextDialer) *ProxyDialer {
76 | return &ProxyDialer{
77 | address: address,
78 | tlsServerName: tlsServerName,
79 | fakeSNI: fakeSNI,
80 | auth: auth,
81 | next: nextDialer,
82 | intermediateWorkaround: intermediateWorkaround,
83 | caPool: caPool,
84 | }
85 | }
86 |
87 | func ProxyDialerFromURL(u *url.URL, next ContextDialer) (*ProxyDialer, error) {
88 | host := u.Hostname()
89 | port := u.Port()
90 | tlsServerName := ""
91 | var auth stringCb = nil
92 |
93 | switch strings.ToLower(u.Scheme) {
94 | case "http":
95 | if port == "" {
96 | port = "80"
97 | }
98 | case "https":
99 | if port == "" {
100 | port = "443"
101 | }
102 | tlsServerName = host
103 | default:
104 | return nil, errors.New("unsupported proxy type")
105 | }
106 |
107 | address := net.JoinHostPort(host, port)
108 |
109 | if u.User != nil {
110 | username := u.User.Username()
111 | password, _ := u.User.Password()
112 | auth = WrapStringToCb(BasicAuthHeader(username, password))
113 | }
114 | return NewProxyDialer(
115 | WrapStringToCb(address),
116 | WrapStringToCb(tlsServerName),
117 | WrapStringToCb(tlsServerName),
118 | auth,
119 | false,
120 | nil,
121 | next), nil
122 | }
123 |
124 | func (d *ProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
125 | switch network {
126 | case "tcp", "tcp4", "tcp6":
127 | default:
128 | return nil, errors.New("bad network specified for DialContext: only tcp is supported")
129 | }
130 |
131 | uAddress, err := d.address()
132 | if err != nil {
133 | return nil, err
134 | }
135 | conn, err := d.next.DialContext(ctx, "tcp", uAddress)
136 | if err != nil {
137 | return nil, err
138 | }
139 |
140 | uTLSServerName, err := d.tlsServerName()
141 | if err != nil {
142 | return nil, err
143 | }
144 | fakeSNI, err := d.fakeSNI()
145 | if err != nil {
146 | return nil, err
147 | }
148 | if uTLSServerName != "" {
149 | // Custom cert verification logic:
150 | // DO NOT send SNI extension of TLS ClientHello
151 | // DO peer certificate verification against specified servername
152 | conn = tls.Client(conn, &tls.Config{
153 | ServerName: fakeSNI,
154 | InsecureSkipVerify: true,
155 | VerifyConnection: func(cs tls.ConnectionState) error {
156 | opts := x509.VerifyOptions{
157 | DNSName: uTLSServerName,
158 | Intermediates: x509.NewCertPool(),
159 | Roots: d.caPool,
160 | }
161 | waRequired := false
162 | for _, cert := range cs.PeerCertificates[1:] {
163 | opts.Intermediates.AddCert(cert)
164 | if d.intermediateWorkaround && !waRequired &&
165 | bytes.Compare(cert.AuthorityKeyId, missingLink.SubjectKeyId) == 0 {
166 | waRequired = true
167 | }
168 | }
169 | if waRequired {
170 | opts.Intermediates.AddCert(missingLink)
171 | }
172 | _, err := cs.PeerCertificates[0].Verify(opts)
173 | return err
174 | },
175 | })
176 | }
177 |
178 | req := &http.Request{
179 | Method: PROXY_CONNECT_METHOD,
180 | Proto: "HTTP/1.1",
181 | ProtoMajor: 1,
182 | ProtoMinor: 1,
183 | RequestURI: address,
184 | Host: address,
185 | Header: http.Header{
186 | PROXY_HOST_HEADER: []string{address},
187 | },
188 | }
189 |
190 | if d.auth != nil {
191 | auth, err := d.auth()
192 | if err != nil {
193 | return nil, err
194 | }
195 | req.Header.Set(PROXY_AUTHORIZATION_HEADER, auth)
196 | }
197 |
198 | rawreq, err := httputil.DumpRequest(req, false)
199 | if err != nil {
200 | return nil, err
201 | }
202 |
203 | _, err = conn.Write(rawreq)
204 | if err != nil {
205 | return nil, err
206 | }
207 |
208 | proxyResp, err := readResponse(conn, req)
209 | if err != nil {
210 | return nil, err
211 | }
212 |
213 | if proxyResp.StatusCode != http.StatusOK {
214 | return nil, errors.New(fmt.Sprintf("bad response from upstream proxy server: %s", proxyResp.Status))
215 | }
216 |
217 | return conn, nil
218 | }
219 |
220 | func (d *ProxyDialer) Dial(network, address string) (net.Conn, error) {
221 | return d.DialContext(context.Background(), network, address)
222 | }
223 |
224 | func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
225 | endOfResponse := []byte("\r\n\r\n")
226 | buf := &bytes.Buffer{}
227 | b := make([]byte, 1)
228 | for {
229 | n, err := r.Read(b)
230 | if n < 1 && err == nil {
231 | continue
232 | }
233 |
234 | buf.Write(b)
235 | sl := buf.Bytes()
236 | if len(sl) < len(endOfResponse) {
237 | continue
238 | }
239 |
240 | if bytes.Equal(sl[len(sl)-4:], endOfResponse) {
241 | break
242 | }
243 |
244 | if err != nil {
245 | return nil, err
246 | }
247 | }
248 | return http.ReadResponse(bufio.NewReader(buf), req)
249 | }
250 |
251 | func BasicAuthHeader(login, password string) string {
252 | return "Basic " + base64.StdEncoding.EncodeToString(
253 | []byte(login+":"+password))
254 | }
255 |
256 | func WrapStringToCb(s string) func() (string, error) {
257 | return func() (string, error) {
258 | return s, nil
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Snawoot/opera-proxy
2 |
3 | go 1.24.1
4 |
5 | toolchain go1.24.2
6 |
7 | require (
8 | github.com/AdguardTeam/dnsproxy v0.75.2
9 | github.com/Snawoot/go-http-digest-auth-client v1.1.3
10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
11 | github.com/hashicorp/go-multierror v1.1.1
12 | golang.org/x/net v0.39.0
13 | )
14 |
15 | require (
16 | github.com/AdguardTeam/golibs v0.32.7 // indirect
17 | github.com/ameshkov/dnscrypt/v2 v2.4.0 // indirect
18 | github.com/ameshkov/dnsstamps v1.0.3 // indirect
19 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
20 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
21 | github.com/hashicorp/errwrap v1.1.0 // indirect
22 | github.com/miekg/dns v1.1.65 // indirect
23 | github.com/onsi/ginkgo/v2 v2.23.4 // indirect
24 | github.com/quic-go/qpack v0.5.1 // indirect
25 | github.com/quic-go/quic-go v0.50.1 // indirect
26 | go.uber.org/automaxprocs v1.6.0 // indirect
27 | go.uber.org/mock v0.5.1 // indirect
28 | golang.org/x/crypto v0.37.0 // indirect
29 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
30 | golang.org/x/mod v0.24.0 // indirect
31 | golang.org/x/sync v0.13.0 // indirect
32 | golang.org/x/sys v0.32.0 // indirect
33 | golang.org/x/text v0.24.0 // indirect
34 | golang.org/x/tools v0.32.0 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/AdguardTeam/dnsproxy v0.75.2 h1:bciOkzQh/GG8vcZGdFn6+rS3pu+2Npt9tbA4bNA/rsc=
2 | github.com/AdguardTeam/dnsproxy v0.75.2/go.mod h1:U/ouLftmXMIrkTAf8JepqbPuoQzsbXJo0Vxxn+LAdgA=
3 | github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4=
4 | github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA=
5 | github.com/Snawoot/go-http-digest-auth-client v1.1.3 h1:Xd/SNBuIUJqotzmxRpbXovBJxmlVZOT19IZZdMdrJ0Q=
6 | github.com/Snawoot/go-http-digest-auth-client v1.1.3/go.mod h1:WiwNiPXTRGyjTGpBtSQJlM2wDPRRPpFGhMkMWpV4uqg=
7 | github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o=
8 | github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
9 | github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
10 | github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
17 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
18 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
19 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
20 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
21 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
22 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
23 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
24 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
25 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
26 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
27 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
28 | github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
29 | github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
30 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
31 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
32 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
33 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
36 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
37 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
38 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
39 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
40 | github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
41 | github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
42 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
43 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
44 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
45 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
46 | go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
47 | go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
48 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
49 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
50 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
51 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
52 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
53 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
54 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
55 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
56 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
57 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
58 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
59 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
60 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
61 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
62 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
63 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
64 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
65 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
66 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
67 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
68 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
70 |
--------------------------------------------------------------------------------
/handler/handler.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "net"
10 | "net/http"
11 | "strings"
12 | "sync"
13 | "time"
14 |
15 | "github.com/Snawoot/opera-proxy/dialer"
16 | clog "github.com/Snawoot/opera-proxy/log"
17 | )
18 |
19 | const (
20 | COPY_BUF = 128 * 1024
21 | BAD_REQ_MSG = "Bad Request\n"
22 | )
23 |
24 | type ProxyHandler struct {
25 | logger *clog.CondLogger
26 | dialer dialer.ContextDialer
27 | httptransport http.RoundTripper
28 | }
29 |
30 | func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger) *ProxyHandler {
31 | httptransport := &http.Transport{
32 | MaxIdleConns: 100,
33 | IdleConnTimeout: 90 * time.Second,
34 | TLSHandshakeTimeout: 10 * time.Second,
35 | ExpectContinueTimeout: 1 * time.Second,
36 | DialContext: dialer.DialContext,
37 | }
38 | return &ProxyHandler{
39 | logger: logger,
40 | dialer: dialer,
41 | httptransport: httptransport,
42 | }
43 | }
44 |
45 | func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
46 | ctx := req.Context()
47 | conn, err := s.dialer.DialContext(ctx, "tcp", req.RequestURI)
48 | if err != nil {
49 | s.logger.Error("Can't satisfy CONNECT request: %v", err)
50 | http.Error(wr, "Can't satisfy CONNECT request", http.StatusBadGateway)
51 | return
52 | }
53 |
54 | if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
55 | // Upgrade client connection
56 | localconn, _, err := hijack(wr)
57 | if err != nil {
58 | s.logger.Error("Can't hijack client connection: %v", err)
59 | http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError)
60 | return
61 | }
62 | defer localconn.Close()
63 |
64 | // Inform client connection is built
65 | fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
66 |
67 | proxy(req.Context(), localconn, conn)
68 | } else if req.ProtoMajor == 2 {
69 | wr.Header()["Date"] = nil
70 | wr.WriteHeader(http.StatusOK)
71 | flush(wr)
72 | proxyh2(req.Context(), req.Body, wr, conn)
73 | } else {
74 | s.logger.Error("Unsupported protocol version: %s", req.Proto)
75 | http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest)
76 | return
77 | }
78 | }
79 |
80 | func (s *ProxyHandler) HandleRequest(wr http.ResponseWriter, req *http.Request) {
81 | req.RequestURI = ""
82 | if req.ProtoMajor == 2 {
83 | req.URL.Scheme = "http" // We can't access :scheme pseudo-header, so assume http
84 | req.URL.Host = req.Host
85 | }
86 | resp, err := s.httptransport.RoundTrip(req)
87 | if err != nil {
88 | s.logger.Error("HTTP fetch error: %v", err)
89 | http.Error(wr, "Server Error", http.StatusInternalServerError)
90 | return
91 | }
92 | defer resp.Body.Close()
93 | s.logger.Info("%v %v %v %v", req.RemoteAddr, req.Method, req.URL, resp.Status)
94 | delHopHeaders(resp.Header)
95 | copyHeader(wr.Header(), resp.Header)
96 | wr.WriteHeader(resp.StatusCode)
97 | flush(wr)
98 | copyBody(wr, resp.Body)
99 | }
100 |
101 | func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
102 | s.logger.Info("Request: %v %v %v %v", req.RemoteAddr, req.Proto, req.Method, req.URL)
103 |
104 | isConnect := strings.ToUpper(req.Method) == "CONNECT"
105 | if (req.URL.Host == "" || req.URL.Scheme == "" && !isConnect) && req.ProtoMajor < 2 ||
106 | req.Host == "" && req.ProtoMajor == 2 {
107 | http.Error(wr, BAD_REQ_MSG, http.StatusBadRequest)
108 | return
109 | }
110 | delHopHeaders(req.Header)
111 | if isConnect {
112 | s.HandleTunnel(wr, req)
113 | } else {
114 | s.HandleRequest(wr, req)
115 | }
116 | }
117 |
118 | func proxy(ctx context.Context, left, right net.Conn) {
119 | wg := sync.WaitGroup{}
120 | cpy := func(dst, src net.Conn) {
121 | defer wg.Done()
122 | io.Copy(dst, src)
123 | dst.Close()
124 | }
125 | wg.Add(2)
126 | go cpy(left, right)
127 | go cpy(right, left)
128 | groupdone := make(chan struct{})
129 | go func() {
130 | wg.Wait()
131 | groupdone <- struct{}{}
132 | }()
133 | select {
134 | case <-ctx.Done():
135 | left.Close()
136 | right.Close()
137 | case <-groupdone:
138 | return
139 | }
140 | <-groupdone
141 | return
142 | }
143 |
144 | func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn) {
145 | wg := sync.WaitGroup{}
146 | ltr := func(dst net.Conn, src io.Reader) {
147 | defer wg.Done()
148 | io.Copy(dst, src)
149 | dst.Close()
150 | }
151 | rtl := func(dst io.Writer, src io.Reader) {
152 | defer wg.Done()
153 | copyBody(dst, src)
154 | }
155 | wg.Add(2)
156 | go ltr(right, leftreader)
157 | go rtl(leftwriter, right)
158 | groupdone := make(chan struct{}, 1)
159 | go func() {
160 | wg.Wait()
161 | groupdone <- struct{}{}
162 | }()
163 | select {
164 | case <-ctx.Done():
165 | leftreader.Close()
166 | right.Close()
167 | case <-groupdone:
168 | return
169 | }
170 | <-groupdone
171 | return
172 | }
173 |
174 | // Hop-by-hop headers. These are removed when sent to the backend.
175 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
176 | var hopHeaders = []string{
177 | "Connection",
178 | "Keep-Alive",
179 | "Proxy-Authenticate",
180 | "Proxy-Connection",
181 | "Te", // canonicalized version of "TE"
182 | "Trailers",
183 | "Transfer-Encoding",
184 | "Upgrade",
185 | }
186 |
187 | func copyHeader(dst, src http.Header) {
188 | for k, vv := range src {
189 | for _, v := range vv {
190 | dst.Add(k, v)
191 | }
192 | }
193 | }
194 |
195 | func delHopHeaders(header http.Header) {
196 | for _, h := range hopHeaders {
197 | header.Del(h)
198 | }
199 | }
200 |
201 | func hijack(hijackable interface{}) (net.Conn, *bufio.ReadWriter, error) {
202 | hj, ok := hijackable.(http.Hijacker)
203 | if !ok {
204 | return nil, nil, errors.New("Connection doesn't support hijacking")
205 | }
206 | conn, rw, err := hj.Hijack()
207 | if err != nil {
208 | return nil, nil, err
209 | }
210 | var emptytime time.Time
211 | err = conn.SetDeadline(emptytime)
212 | if err != nil {
213 | conn.Close()
214 | return nil, nil, err
215 | }
216 | return conn, rw, nil
217 | }
218 |
219 | func flush(flusher interface{}) bool {
220 | f, ok := flusher.(http.Flusher)
221 | if !ok {
222 | return false
223 | }
224 | f.Flush()
225 | return true
226 | }
227 |
228 | func copyBody(wr io.Writer, body io.Reader) {
229 | buf := make([]byte, COPY_BUF)
230 | for {
231 | bread, read_err := body.Read(buf)
232 | var write_err error
233 | if bread > 0 {
234 | _, write_err = wr.Write(buf[:bread])
235 | flush(wr)
236 | }
237 | if read_err != nil || write_err != nil {
238 | break
239 | }
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/handler/socks.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/Snawoot/opera-proxy/dialer"
5 | "github.com/armon/go-socks5"
6 | "log"
7 | )
8 |
9 | func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Server, error) {
10 | return socks5.New(&socks5.Config{
11 | Rules: &socks5.PermitCommand{
12 | EnableConnect: true,
13 | },
14 | Logger: logger,
15 | Dial: dialer.DialContext,
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/log/condlog.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | )
7 |
8 | const (
9 | CRITICAL = 50
10 | ERROR = 40
11 | WARNING = 30
12 | INFO = 20
13 | DEBUG = 10
14 | NOTSET = 0
15 | )
16 |
17 | type CondLogger struct {
18 | logger *log.Logger
19 | verbosity int
20 | }
21 |
22 | func (cl *CondLogger) Log(verb int, format string, v ...interface{}) error {
23 | if verb >= cl.verbosity {
24 | return cl.logger.Output(2, fmt.Sprintf(format, v...))
25 | }
26 | return nil
27 | }
28 |
29 | func (cl *CondLogger) log(verb int, format string, v ...interface{}) error {
30 | if verb >= cl.verbosity {
31 | return cl.logger.Output(3, fmt.Sprintf(format, v...))
32 | }
33 | return nil
34 | }
35 |
36 | func (cl *CondLogger) Critical(s string, v ...interface{}) error {
37 | return cl.log(CRITICAL, "CRITICAL "+s, v...)
38 | }
39 |
40 | func (cl *CondLogger) Error(s string, v ...interface{}) error {
41 | return cl.log(ERROR, "ERROR "+s, v...)
42 | }
43 |
44 | func (cl *CondLogger) Warning(s string, v ...interface{}) error {
45 | return cl.log(WARNING, "WARNING "+s, v...)
46 | }
47 |
48 | func (cl *CondLogger) Info(s string, v ...interface{}) error {
49 | return cl.log(INFO, "INFO "+s, v...)
50 | }
51 |
52 | func (cl *CondLogger) Debug(s string, v ...interface{}) error {
53 | return cl.log(DEBUG, "DEBUG "+s, v...)
54 | }
55 |
56 | func NewCondLogger(logger *log.Logger, verbosity int) *CondLogger {
57 | return &CondLogger{verbosity: verbosity, logger: logger}
58 | }
59 |
--------------------------------------------------------------------------------
/log/logwriter.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "time"
7 | )
8 |
9 | const MAX_LOG_QLEN = 128
10 | const QUEUE_SHUTDOWN_TIMEOUT = 500 * time.Millisecond
11 |
12 | type LogWriter struct {
13 | writer io.Writer
14 | ch chan []byte
15 | done chan struct{}
16 | }
17 |
18 | func (lw *LogWriter) Write(p []byte) (int, error) {
19 | if p == nil {
20 | return 0, errors.New("Can't write nil byte slice")
21 | }
22 | buf := make([]byte, len(p))
23 | copy(buf, p)
24 | select {
25 | case lw.ch <- buf:
26 | return len(p), nil
27 | default:
28 | return 0, errors.New("Writer queue overflow")
29 | }
30 | }
31 |
32 | func NewLogWriter(writer io.Writer) *LogWriter {
33 | lw := &LogWriter{writer,
34 | make(chan []byte, MAX_LOG_QLEN),
35 | make(chan struct{})}
36 | go lw.loop()
37 | return lw
38 | }
39 |
40 | func (lw *LogWriter) loop() {
41 | for p := range lw.ch {
42 | if p == nil {
43 | break
44 | }
45 | lw.writer.Write(p)
46 | }
47 | lw.done <- struct{}{}
48 | }
49 |
50 | func (lw *LogWriter) Close() {
51 | lw.ch <- nil
52 | timer := time.After(QUEUE_SHUTDOWN_TIMEOUT)
53 | select {
54 | case <-timer:
55 | case <-lw.done:
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "encoding/csv"
9 | "errors"
10 | "flag"
11 | "fmt"
12 | "io"
13 | "io/ioutil"
14 | "log"
15 | "net"
16 | "net/http"
17 | "net/url"
18 | "os"
19 | "strings"
20 | "time"
21 |
22 | xproxy "golang.org/x/net/proxy"
23 |
24 | "github.com/Snawoot/opera-proxy/clock"
25 | "github.com/Snawoot/opera-proxy/dialer"
26 | "github.com/Snawoot/opera-proxy/handler"
27 | clog "github.com/Snawoot/opera-proxy/log"
28 | se "github.com/Snawoot/opera-proxy/seclient"
29 | )
30 |
31 | const (
32 | API_DOMAIN = "api2.sec-tunnel.com"
33 | PROXY_SUFFIX = "sec-tunnel.com"
34 | )
35 |
36 | var (
37 | version = "undefined"
38 | )
39 |
40 | func perror(msg string) {
41 | fmt.Fprintln(os.Stderr, "")
42 | fmt.Fprintln(os.Stderr, msg)
43 | }
44 |
45 | func arg_fail(msg string) {
46 | perror(msg)
47 | perror("Usage:")
48 | flag.PrintDefaults()
49 | os.Exit(2)
50 | }
51 |
52 | type CSVArg struct {
53 | values []string
54 | }
55 |
56 | func (a *CSVArg) String() string {
57 | if len(a.values) == 0 {
58 | return ""
59 | }
60 | buf := new(bytes.Buffer)
61 | wr := csv.NewWriter(buf)
62 | wr.Write(a.values)
63 | wr.Flush()
64 | return strings.TrimRight(buf.String(), "\n")
65 | }
66 |
67 | func (a *CSVArg) Set(line string) error {
68 | rd := csv.NewReader(strings.NewReader(line))
69 | rd.FieldsPerRecord = -1
70 | rd.TrimLeadingSpace = true
71 | values, err := rd.Read()
72 | if err == io.EOF {
73 | a.values = nil
74 | return nil
75 | }
76 | if err != nil {
77 | return fmt.Errorf("unable to parse comma-separated argument: %w", err)
78 | }
79 | a.values = values
80 | return nil
81 | }
82 |
83 | type CLIArgs struct {
84 | country string
85 | listCountries bool
86 | listProxies bool
87 | bindAddress string
88 | socksMode bool
89 | verbosity int
90 | timeout time.Duration
91 | showVersion bool
92 | proxy string
93 | apiLogin string
94 | apiPassword string
95 | apiAddress string
96 | apiClientType string
97 | apiClientVersion string
98 | apiUserAgent string
99 | bootstrapDNS *CSVArg
100 | refresh time.Duration
101 | refreshRetry time.Duration
102 | initRetries int
103 | initRetryInterval time.Duration
104 | certChainWorkaround bool
105 | caFile string
106 | fakeSNI string
107 | overrideProxyAddress string
108 | }
109 |
110 | func parse_args() *CLIArgs {
111 | args := &CLIArgs{
112 | bootstrapDNS: &CSVArg{
113 | values: []string{
114 | "https://1.1.1.3/dns-query",
115 | "https://8.8.8.8/dns-query",
116 | "https://dns.google/dns-query",
117 | "https://security.cloudflare-dns.com/dns-query",
118 | "https://fidelity.vm-0.com/q",
119 | "https://wikimedia-dns.org/dns-query",
120 | "https://dns.adguard-dns.com/dns-query",
121 | "https://dns.quad9.net/dns-query",
122 | "https://doh.cleanbrowsing.org/doh/adult-filter/",
123 | },
124 | },
125 | }
126 | flag.StringVar(&args.country, "country", "EU", "desired proxy location")
127 | flag.BoolVar(&args.listCountries, "list-countries", false, "list available countries and exit")
128 | flag.BoolVar(&args.listProxies, "list-proxies", false, "output proxy list and exit")
129 | flag.StringVar(&args.bindAddress, "bind-address", "127.0.0.1:18080", "proxy listen address")
130 | flag.BoolVar(&args.socksMode, "socks-mode", false, "listen for SOCKS requests instead of HTTP")
131 | flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
132 | "(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)")
133 | flag.DurationVar(&args.timeout, "timeout", 10*time.Second, "timeout for network operations")
134 | flag.BoolVar(&args.showVersion, "version", false, "show program version and exit")
135 | flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. "+
136 | "Format: ://[login:password@]host[:port] "+
137 | "Examples: http://user:password@192.168.1.1:3128, socks5://10.0.0.1:1080")
138 | flag.StringVar(&args.apiClientVersion, "api-client-version", se.DefaultSESettings.ClientVersion, "client version reported to SurfEasy API")
139 | flag.StringVar(&args.apiClientType, "api-client-type", se.DefaultSESettings.ClientType, "client type reported to SurfEasy API")
140 | flag.StringVar(&args.apiUserAgent, "api-user-agent", se.DefaultSESettings.UserAgent, "user agent reported to SurfEasy API")
141 | flag.StringVar(&args.apiLogin, "api-login", "se0316", "SurfEasy API login")
142 | flag.StringVar(&args.apiPassword, "api-password", "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II", "SurfEasy API password")
143 | flag.StringVar(&args.apiAddress, "api-address", "", fmt.Sprintf("override IP address of %s", API_DOMAIN))
144 | flag.Var(args.bootstrapDNS, "bootstrap-dns",
145 | "comma-separated list of DNS/DoH/DoT/DoQ resolvers for initial discovery of SurfEasy API address. "+
146 | "See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. "+
147 | "Examples: https://1.1.1.1/dns-query,quic://dns.adguard.com")
148 | flag.DurationVar(&args.refresh, "refresh", 4*time.Hour, "login refresh interval")
149 | flag.DurationVar(&args.refreshRetry, "refresh-retry", 5*time.Second, "login refresh retry interval")
150 | flag.IntVar(&args.initRetries, "init-retries", 0, "number of attempts for initialization steps, zero for unlimited retry")
151 | flag.DurationVar(&args.initRetryInterval, "init-retry-interval", 5*time.Second, "delay between initialization retries")
152 | flag.BoolVar(&args.certChainWorkaround, "certchain-workaround", true,
153 | "add bundled cross-signed intermediate cert to certchain to make it check out on old systems")
154 | flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file")
155 | flag.StringVar(&args.fakeSNI, "fake-SNI", "", "domain name to use as SNI in communications with servers")
156 | flag.StringVar(&args.overrideProxyAddress, "override-proxy-address", "", "use fixed proxy address instead of server address returned by SurfEasy API")
157 | flag.Parse()
158 | if args.country == "" {
159 | arg_fail("Country can't be empty string.")
160 | }
161 | if args.listCountries && args.listProxies {
162 | arg_fail("list-countries and list-proxies flags are mutually exclusive")
163 | }
164 | return args
165 | }
166 |
167 | func proxyFromURLWrapper(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) {
168 | cdialer, ok := next.(dialer.ContextDialer)
169 | if !ok {
170 | return nil, errors.New("only context dialers are accepted")
171 | }
172 |
173 | return dialer.ProxyDialerFromURL(u, cdialer)
174 | }
175 |
176 | func run() int {
177 | args := parse_args()
178 | if args.showVersion {
179 | fmt.Println(version)
180 | return 0
181 | }
182 |
183 | logWriter := clog.NewLogWriter(os.Stderr)
184 | defer logWriter.Close()
185 |
186 | mainLogger := clog.NewCondLogger(log.New(logWriter, "MAIN : ",
187 | log.LstdFlags|log.Lshortfile),
188 | args.verbosity)
189 | proxyLogger := clog.NewCondLogger(log.New(logWriter, "PROXY : ",
190 | log.LstdFlags|log.Lshortfile),
191 | args.verbosity)
192 | socksLogger := log.New(logWriter, "SOCKS : ",
193 | log.LstdFlags|log.Lshortfile)
194 |
195 | mainLogger.Info("opera-proxy client version %s is starting...", version)
196 |
197 | var d dialer.ContextDialer = &net.Dialer{
198 | Timeout: 30 * time.Second,
199 | KeepAlive: 30 * time.Second,
200 | }
201 |
202 | if args.proxy != "" {
203 | xproxy.RegisterDialerType("http", proxyFromURLWrapper)
204 | xproxy.RegisterDialerType("https", proxyFromURLWrapper)
205 | proxyURL, err := url.Parse(args.proxy)
206 | if err != nil {
207 | mainLogger.Critical("Unable to parse base proxy URL: %v", err)
208 | return 6
209 | }
210 | pxDialer, err := xproxy.FromURL(proxyURL, d)
211 | if err != nil {
212 | mainLogger.Critical("Unable to instantiate base proxy dialer: %v", err)
213 | return 7
214 | }
215 | d = pxDialer.(dialer.ContextDialer)
216 | }
217 |
218 | seclientDialer := d
219 | if args.apiAddress != "" {
220 | mainLogger.Info("Using fixed API host IP address = %s", args.apiAddress)
221 | seclientDialer = dialer.NewFixedDialer(args.apiAddress, d)
222 | } else if len(args.bootstrapDNS.values) > 0 {
223 | resolver, err := dialer.NewResolver(args.bootstrapDNS.values, args.timeout)
224 | if err != nil {
225 | mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
226 | return 4
227 | }
228 | defer resolver.Close()
229 | seclientDialer = dialer.NewResolvingDialer(resolver, d)
230 | }
231 |
232 | // Dialing w/o SNI, receiving self-signed certificate, so skip verification.
233 | // Either way we'll validate certificate of actual proxy server.
234 | tlsConfig := &tls.Config{
235 | ServerName: args.fakeSNI,
236 | InsecureSkipVerify: true,
237 | }
238 | seclient, err := se.NewSEClient(args.apiLogin, args.apiPassword, &http.Transport{
239 | DialContext: seclientDialer.DialContext,
240 | DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
241 | conn, err := seclientDialer.DialContext(ctx, network, addr)
242 | if err != nil {
243 | return conn, err
244 | }
245 | return tls.Client(conn, tlsConfig), nil
246 | },
247 | ForceAttemptHTTP2: true,
248 | MaxIdleConns: 100,
249 | IdleConnTimeout: 90 * time.Second,
250 | TLSHandshakeTimeout: 10 * time.Second,
251 | ExpectContinueTimeout: 1 * time.Second,
252 | })
253 | if err != nil {
254 | mainLogger.Critical("Unable to construct SEClient: %v", err)
255 | return 8
256 | }
257 | seclient.Settings.ClientType = args.apiClientType
258 | seclient.Settings.ClientVersion = args.apiClientVersion
259 | seclient.Settings.UserAgent = args.apiUserAgent
260 |
261 | try := retryPolicy(args.initRetries, args.initRetryInterval, mainLogger)
262 |
263 | err = try("anonymous registration", func() error {
264 | ctx, cl := context.WithTimeout(context.Background(), args.timeout)
265 | defer cl()
266 | return seclient.AnonRegister(ctx)
267 | })
268 | if err != nil {
269 | return 9
270 | }
271 |
272 | err = try("device registration", func() error {
273 | ctx, cl := context.WithTimeout(context.Background(), args.timeout)
274 | defer cl()
275 | return seclient.RegisterDevice(ctx)
276 | })
277 | if err != nil {
278 | return 10
279 | }
280 |
281 | if args.listCountries {
282 | return printCountries(try, mainLogger, args.timeout, seclient)
283 | }
284 |
285 | var ips []se.SEIPEntry
286 | err = try("discover", func() error {
287 | ctx, cl := context.WithTimeout(context.Background(), args.timeout)
288 | defer cl()
289 | // TODO: learn about requested_geo value format
290 | res, err := seclient.Discover(ctx, fmt.Sprintf("\"%s\",,", args.country))
291 | ips = res
292 | return err
293 | })
294 | if err != nil {
295 | return 12
296 | }
297 |
298 | if args.listProxies {
299 | return printProxies(ips, seclient)
300 | }
301 |
302 | if len(ips) == 0 {
303 | mainLogger.Critical("Empty endpoint list!")
304 | return 13
305 | }
306 |
307 | clock.RunTicker(context.Background(), args.refresh, args.refreshRetry, func(ctx context.Context) error {
308 | mainLogger.Info("Refreshing login...")
309 | reqCtx, cl := context.WithTimeout(ctx, args.timeout)
310 | defer cl()
311 | err := seclient.Login(reqCtx)
312 | if err != nil {
313 | mainLogger.Error("Login refresh failed: %v", err)
314 | return err
315 | }
316 | mainLogger.Info("Login refreshed.")
317 |
318 | mainLogger.Info("Refreshing device password...")
319 | reqCtx, cl = context.WithTimeout(ctx, args.timeout)
320 | defer cl()
321 | err = seclient.DeviceGeneratePassword(reqCtx)
322 | if err != nil {
323 | mainLogger.Error("Device password refresh failed: %v", err)
324 | return err
325 | }
326 | mainLogger.Info("Device password refreshed.")
327 | return nil
328 | })
329 |
330 | endpoint := ips[0]
331 |
332 | var caPool *x509.CertPool
333 | if args.caFile != "" {
334 | caPool = x509.NewCertPool()
335 | certs, err := ioutil.ReadFile(args.caFile)
336 | if err != nil {
337 | mainLogger.Error("Can't load CA file: %v", err)
338 | return 15
339 | }
340 | if ok := caPool.AppendCertsFromPEM(certs); !ok {
341 | mainLogger.Error("Can't load certificates from CA file")
342 | return 15
343 | }
344 | }
345 |
346 | var handlerBaseDialer = d
347 | if args.overrideProxyAddress != "" {
348 | mainLogger.Info("Original endpoint: %s", endpoint.IP)
349 | handlerBaseDialer = dialer.NewFixedDialer(args.overrideProxyAddress, handlerBaseDialer)
350 | mainLogger.Info("Endpoint override: %s", args.overrideProxyAddress)
351 | } else {
352 | mainLogger.Info("Endpoint: %s", endpoint.NetAddr())
353 | }
354 | handlerDialer := dialer.NewProxyDialer(
355 | dialer.WrapStringToCb(endpoint.NetAddr()),
356 | dialer.WrapStringToCb(fmt.Sprintf("%s0.%s", args.country, PROXY_SUFFIX)),
357 | dialer.WrapStringToCb(args.fakeSNI),
358 | func() (string, error) {
359 | return dialer.BasicAuthHeader(seclient.GetProxyCredentials()), nil
360 | },
361 | args.certChainWorkaround,
362 | caPool,
363 | handlerBaseDialer)
364 | mainLogger.Info("Starting proxy server...")
365 | if args.socksMode {
366 | socks, initError := handler.NewSocksServer(handlerDialer, socksLogger)
367 | if initError != nil {
368 | mainLogger.Critical("Failed to start: %v", err)
369 | return 16
370 | }
371 | mainLogger.Info("Init complete.")
372 | err = socks.ListenAndServe("tcp", args.bindAddress)
373 | } else {
374 | h := handler.NewProxyHandler(handlerDialer, proxyLogger)
375 | mainLogger.Info("Init complete.")
376 | err = http.ListenAndServe(args.bindAddress, h)
377 | }
378 | mainLogger.Critical("Server terminated with a reason: %v", err)
379 | mainLogger.Info("Shutting down...")
380 | return 0
381 | }
382 |
383 | func printCountries(try func(string, func() error) error, logger *clog.CondLogger, timeout time.Duration, seclient *se.SEClient) int {
384 | var list []se.SEGeoEntry
385 | err := try("geolist", func() error {
386 | ctx, cl := context.WithTimeout(context.Background(), timeout)
387 | defer cl()
388 | l, err := seclient.GeoList(ctx)
389 | list = l
390 | return err
391 | })
392 | if err != nil {
393 | return 11
394 | }
395 |
396 | wr := csv.NewWriter(os.Stdout)
397 | defer wr.Flush()
398 | wr.Write([]string{"country code", "country name"})
399 | for _, country := range list {
400 | wr.Write([]string{country.CountryCode, country.Country})
401 | }
402 | return 0
403 | }
404 |
405 | func printProxies(ips []se.SEIPEntry, seclient *se.SEClient) int {
406 | wr := csv.NewWriter(os.Stdout)
407 | defer wr.Flush()
408 | login, password := seclient.GetProxyCredentials()
409 | fmt.Println("Proxy login:", login)
410 | fmt.Println("Proxy password:", password)
411 | fmt.Println("Proxy-Authorization:", dialer.BasicAuthHeader(login, password))
412 | fmt.Println("")
413 | wr.Write([]string{"host", "ip_address", "port"})
414 | for i, ip := range ips {
415 | for _, port := range ip.Ports {
416 | wr.Write([]string{
417 | fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX),
418 | ip.IP,
419 | fmt.Sprintf("%d", port),
420 | })
421 | }
422 | }
423 | return 0
424 | }
425 |
426 | func main() {
427 | os.Exit(run())
428 | }
429 |
430 | func retryPolicy(retries int, retryInterval time.Duration, logger *clog.CondLogger) func(string, func() error) error {
431 | return func(name string, f func() error) error {
432 | var err error
433 | for i := 1; retries <= 0 || i <= retries; i++ {
434 | if i > 1 {
435 | logger.Warning("Retrying action %q in %v...", name, retryInterval)
436 | time.Sleep(retryInterval)
437 | }
438 | logger.Info("Attempting action %q, attempt #%d...", name, i)
439 | err = f()
440 | if err == nil {
441 | logger.Info("Action %q succeeded on attempt #%d", name, i)
442 | return nil
443 | }
444 | logger.Warning("Action %q failed: %v", name, err)
445 | }
446 | logger.Critical("All attempts for action %q have failed. Last error: %v", name, err)
447 | return err
448 | }
449 | }
450 |
--------------------------------------------------------------------------------
/seclient/csrand.go:
--------------------------------------------------------------------------------
1 | package seclient
2 |
3 | import (
4 | crand "crypto/rand"
5 | "math/big"
6 | )
7 |
8 | type secureRandomSource struct{}
9 |
10 | var RandomSource secureRandomSource
11 |
12 | var int63Limit = big.NewInt(0).Lsh(big.NewInt(1), 63)
13 |
14 | func (_ secureRandomSource) Seed(_ int64) {
15 | }
16 |
17 | func (_ secureRandomSource) Int63() int64 {
18 | randNum, err := crand.Int(crand.Reader, int63Limit)
19 | if err != nil {
20 | panic(err)
21 | }
22 | return randNum.Int64()
23 | }
24 |
--------------------------------------------------------------------------------
/seclient/hash.go:
--------------------------------------------------------------------------------
1 | package seclient
2 |
3 | import (
4 | "crypto/sha1"
5 | "encoding/hex"
6 | "strings"
7 | )
8 |
9 | func capitalHexSHA1(input string) string {
10 | h := sha1.Sum([]byte(input))
11 | return strings.ToUpper(hex.EncodeToString(h[:]))
12 | }
13 |
--------------------------------------------------------------------------------
/seclient/jar.go:
--------------------------------------------------------------------------------
1 | package seclient
2 |
3 | import (
4 | "net/http"
5 | "net/http/cookiejar"
6 | "net/url"
7 | "sync"
8 |
9 | "golang.org/x/net/publicsuffix"
10 | )
11 |
12 | type StdJar struct {
13 | jar *cookiejar.Jar
14 | mux sync.RWMutex
15 | }
16 |
17 | func NewStdJar() (*StdJar, error) {
18 | var jar StdJar
19 |
20 | err := jar.Reset()
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | return &jar, nil
26 | }
27 |
28 | func (j *StdJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
29 | j.mux.RLock()
30 | j.jar.SetCookies(u, cookies)
31 | j.mux.RUnlock()
32 | }
33 |
34 | func (j *StdJar) Cookies(u *url.URL) []*http.Cookie {
35 | j.mux.RLock()
36 | c := j.jar.Cookies(u)
37 | j.mux.RUnlock()
38 | return c
39 | }
40 |
41 | func (j *StdJar) Reset() error {
42 | jar, err := cookiejar.New(&cookiejar.Options{
43 | PublicSuffixList: publicsuffix.List,
44 | })
45 | if err != nil {
46 | return err
47 | }
48 |
49 | j.mux.Lock()
50 | j.jar = jar
51 | j.mux.Unlock()
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/seclient/messages.go:
--------------------------------------------------------------------------------
1 | package seclient
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "net"
8 | "strconv"
9 | )
10 |
11 | const (
12 | SE_STATUS_OK int64 = 0
13 | )
14 |
15 | type SEStatusPair struct {
16 | Code int64
17 | Message string
18 | }
19 |
20 | func (p *SEStatusPair) UnmarshalJSON(b []byte) error {
21 | var tmp map[string]string
22 | err := json.Unmarshal(b, &tmp)
23 | if err != nil {
24 | return err
25 | }
26 |
27 | if len(tmp) != 1 {
28 | return errors.New("ambiguous status")
29 | }
30 |
31 | var strCode, strStatus string
32 | for k, v := range tmp {
33 | strCode = k
34 | strStatus = v
35 | }
36 |
37 | code, err := strconv.ParseInt(strCode, 10, 64)
38 | if err != nil {
39 | return err
40 | }
41 |
42 | *p = SEStatusPair{
43 | Code: code,
44 | Message: strStatus,
45 | }
46 | return nil
47 | }
48 |
49 | type SERegisterSubscriberResponse struct {
50 | Data interface{} `json:"data"`
51 | Status SEStatusPair `json:"return_code"`
52 | }
53 |
54 | type SERegisterDeviceData struct {
55 | ClientType string `json:"client_type"`
56 | DeviceID string `json:"device_id"`
57 | DevicePassword string `json:"device_password"`
58 | }
59 |
60 | type SERegisterDeviceResponse struct {
61 | Data SERegisterDeviceData `json:"data"`
62 | Status SEStatusPair `json:"return_code"`
63 | }
64 |
65 | type SEDeviceGeneratePasswordData struct {
66 | DevicePassword string `json:"device_password"`
67 | }
68 |
69 | type SEDeviceGeneratePasswordResponse struct {
70 | Data SEDeviceGeneratePasswordData `json:"data"`
71 | Status SEStatusPair `json:"return_code"`
72 | }
73 |
74 | type SEGeoEntry struct {
75 | Country string `json:"country,omitempty"`
76 | CountryCode string `json:"country_code"`
77 | }
78 |
79 | type SEGeoListResponse struct {
80 | Data struct {
81 | Geos []SEGeoEntry `json:"geos"`
82 | } `json:"data"`
83 | Status SEStatusPair `json:"return_code"`
84 | }
85 |
86 | type SEIPEntry struct {
87 | Geo SEGeoEntry `json:"geo"`
88 | IP string `json:"ip"`
89 | Ports []uint16 `json:"ports"`
90 | }
91 |
92 | func (e *SEIPEntry) NetAddr() string {
93 | if len(e.Ports) == 0 {
94 | return net.JoinHostPort(e.IP, "443")
95 | } else {
96 | return net.JoinHostPort(e.IP, fmt.Sprintf("%d", e.Ports[0]))
97 | }
98 | }
99 |
100 | type SEDiscoverResponse struct {
101 | Data struct {
102 | IPs []SEIPEntry `json:"ips"`
103 | } `json:"data"`
104 | Status SEStatusPair `json:"return_code"`
105 | }
106 |
107 | type SESubscriberLoginResponse SERegisterSubscriberResponse
108 |
--------------------------------------------------------------------------------
/seclient/randutils.go:
--------------------------------------------------------------------------------
1 | package seclient
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/hex"
6 | "io"
7 | "strings"
8 | )
9 |
10 | func randomEmailLocalPart(rng io.Reader) (string, error) {
11 | b := make([]byte, ANON_EMAIL_LOCALPART_BYTES)
12 | _, err := rng.Read(b)
13 | if err != nil {
14 | return "", err
15 | }
16 | return base64.StdEncoding.EncodeToString(b), nil
17 | }
18 |
19 | func randomCapitalHexString(rng io.Reader, length int) (string, error) {
20 | b := make([]byte, length)
21 | _, err := rng.Read(b)
22 | if err != nil {
23 | return "", err
24 | }
25 | return strings.ToUpper(hex.EncodeToString(b)), nil
26 | }
27 |
--------------------------------------------------------------------------------
/seclient/seclient.go:
--------------------------------------------------------------------------------
1 | package seclient
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "math/rand"
10 | "net/http"
11 | "net/url"
12 | "strings"
13 | "sync"
14 |
15 | dac "github.com/Snawoot/go-http-digest-auth-client"
16 | )
17 |
18 | const (
19 | ANON_EMAIL_LOCALPART_BYTES = 32
20 | ANON_PASSWORD_BYTES = 20
21 | DEVICE_ID_BYTES = 20
22 | READ_LIMIT int64 = 128 * 1024
23 | )
24 |
25 | type SEEndpoints struct {
26 | RegisterSubscriber string
27 | SubscriberLogin string
28 | RegisterDevice string
29 | DeviceGeneratePassword string
30 | GeoList string
31 | Discover string
32 | }
33 |
34 | var DefaultSEEndpoints = SEEndpoints{
35 | RegisterSubscriber: "https://api2.sec-tunnel.com/v4/register_subscriber",
36 | SubscriberLogin: "https://api2.sec-tunnel.com/v4/subscriber_login",
37 | RegisterDevice: "https://api2.sec-tunnel.com/v4/register_device",
38 | DeviceGeneratePassword: "https://api2.sec-tunnel.com/v4/device_generate_password",
39 | GeoList: "https://api2.sec-tunnel.com/v4/geo_list",
40 | Discover: "https://api2.sec-tunnel.com/v4/discover",
41 | }
42 |
43 | type SESettings struct {
44 | ClientVersion string
45 | ClientType string
46 | DeviceName string
47 | OperatingSystem string
48 | UserAgent string
49 | Endpoints SEEndpoints
50 | }
51 |
52 | var DefaultSESettings = SESettings{
53 | ClientVersion: "Stable 114.0.5282.21",
54 | ClientType: "se0316",
55 | UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0",
56 | DeviceName: "Opera-Browser-Client",
57 | OperatingSystem: "Windows",
58 | Endpoints: DefaultSEEndpoints,
59 | }
60 |
61 | type SEClient struct {
62 | httpClient *http.Client
63 | Settings SESettings
64 | SubscriberEmail string
65 | SubscriberPassword string
66 | DeviceID string
67 | AssignedDeviceID string
68 | AssignedDeviceIDHash string
69 | DevicePassword string
70 | Mux sync.Mutex
71 | rng *rand.Rand
72 | }
73 |
74 | type StrKV map[string]string
75 |
76 | // Instantiates SurfEasy client with default settings and given API keys.
77 | // Optional `transport` parameter allows to override HTTP transport used
78 | // for HTTP calls
79 | func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) {
80 | if transport == nil {
81 | transport = http.DefaultTransport
82 | }
83 |
84 | rng := rand.New(RandomSource)
85 |
86 | device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
87 | if err != nil {
88 | return nil, err
89 | }
90 |
91 | jar, err := NewStdJar()
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | res := &SEClient{
97 | httpClient: &http.Client{
98 | Jar: jar,
99 | Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
100 | },
101 | Settings: DefaultSESettings,
102 | rng: rng,
103 | DeviceID: device_id,
104 | }
105 |
106 | return res, nil
107 | }
108 |
109 | func (c *SEClient) ResetCookies() error {
110 | c.Mux.Lock()
111 | defer c.Mux.Unlock()
112 | return c.resetCookies()
113 | }
114 |
115 | func (c *SEClient) resetCookies() error {
116 | return (c.httpClient.Jar.(*StdJar)).Reset()
117 | }
118 |
119 | func (c *SEClient) AnonRegister(ctx context.Context) error {
120 | c.Mux.Lock()
121 | defer c.Mux.Unlock()
122 |
123 | localPart, err := randomEmailLocalPart(c.rng)
124 | if err != nil {
125 | return err
126 | }
127 |
128 | c.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType)
129 | c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail)
130 |
131 | return c.register(ctx)
132 | }
133 |
134 | func (c *SEClient) Register(ctx context.Context) error {
135 | c.Mux.Lock()
136 | defer c.Mux.Unlock()
137 | return c.register(ctx)
138 | }
139 |
140 | func (c *SEClient) register(ctx context.Context) error {
141 | err := c.resetCookies()
142 | if err != nil {
143 | return err
144 | }
145 |
146 | var regRes SERegisterSubscriberResponse
147 | err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
148 | "email": c.SubscriberEmail,
149 | "password": c.SubscriberPassword,
150 | }, ®Res)
151 | if err != nil {
152 | return err
153 | }
154 |
155 | if regRes.Status.Code != SE_STATUS_OK {
156 | return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
157 | regRes.Status.Code, regRes.Status.Message)
158 | }
159 | return nil
160 | }
161 |
162 | func (c *SEClient) RegisterDevice(ctx context.Context) error {
163 | c.Mux.Lock()
164 | defer c.Mux.Unlock()
165 |
166 | var regRes SERegisterDeviceResponse
167 | err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterDevice, StrKV{
168 | "client_type": c.Settings.ClientType,
169 | "device_hash": c.DeviceID,
170 | "device_name": c.Settings.DeviceName,
171 | }, ®Res)
172 | if err != nil {
173 | return err
174 | }
175 |
176 | if regRes.Status.Code != SE_STATUS_OK {
177 | return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
178 | regRes.Status.Code, regRes.Status.Message)
179 | }
180 |
181 | c.AssignedDeviceID = regRes.Data.DeviceID
182 | c.DevicePassword = regRes.Data.DevicePassword
183 | c.AssignedDeviceIDHash = capitalHexSHA1(regRes.Data.DeviceID)
184 | return nil
185 | }
186 |
187 | func (c *SEClient) GeoList(ctx context.Context) ([]SEGeoEntry, error) {
188 | c.Mux.Lock()
189 | defer c.Mux.Unlock()
190 |
191 | var geoListRes SEGeoListResponse
192 | err := c.rpcCall(ctx, c.Settings.Endpoints.GeoList, StrKV{
193 | "device_id": c.AssignedDeviceIDHash,
194 | }, &geoListRes)
195 | if err != nil {
196 | return nil, err
197 | }
198 |
199 | if geoListRes.Status.Code != SE_STATUS_OK {
200 | return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
201 | geoListRes.Status.Code, geoListRes.Status.Message)
202 | }
203 |
204 | return geoListRes.Data.Geos, nil
205 | }
206 |
207 | func (c *SEClient) Discover(ctx context.Context, requestedGeo string) ([]SEIPEntry, error) {
208 | c.Mux.Lock()
209 | defer c.Mux.Unlock()
210 |
211 | var discoverRes SEDiscoverResponse
212 | err := c.rpcCall(ctx, c.Settings.Endpoints.Discover, StrKV{
213 | "serial_no": c.AssignedDeviceIDHash,
214 | "requested_geo": requestedGeo,
215 | }, &discoverRes)
216 | if err != nil {
217 | return nil, err
218 | }
219 |
220 | if discoverRes.Status.Code != SE_STATUS_OK {
221 | return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
222 | discoverRes.Status.Code, discoverRes.Status.Message)
223 | }
224 |
225 | return discoverRes.Data.IPs, nil
226 | }
227 |
228 | func (c *SEClient) Login(ctx context.Context) error {
229 | c.Mux.Lock()
230 | defer c.Mux.Unlock()
231 |
232 | err := c.resetCookies()
233 | if err != nil {
234 | return err
235 | }
236 |
237 | var loginRes SESubscriberLoginResponse
238 | err = c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
239 | "login": c.SubscriberEmail,
240 | "password": c.SubscriberPassword,
241 | "client_type": c.Settings.ClientType,
242 | }, &loginRes)
243 | if err != nil {
244 | return err
245 | }
246 |
247 | if loginRes.Status.Code != SE_STATUS_OK {
248 | return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
249 | loginRes.Status.Code, loginRes.Status.Message)
250 | }
251 | return nil
252 | }
253 |
254 | func (c *SEClient) DeviceGeneratePassword(ctx context.Context) error {
255 | c.Mux.Lock()
256 | defer c.Mux.Unlock()
257 |
258 | var genRes SEDeviceGeneratePasswordResponse
259 | err := c.rpcCall(ctx, c.Settings.Endpoints.DeviceGeneratePassword, StrKV{
260 | "device_id": c.AssignedDeviceID,
261 | }, &genRes)
262 | if err != nil {
263 | return err
264 | }
265 |
266 | if genRes.Status.Code != SE_STATUS_OK {
267 | return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"",
268 | genRes.Status.Code, genRes.Status.Message)
269 | }
270 |
271 | c.DevicePassword = genRes.Data.DevicePassword
272 | return nil
273 | }
274 |
275 | func (c *SEClient) GetProxyCredentials() (string, string) {
276 | c.Mux.Lock()
277 | defer c.Mux.Unlock()
278 |
279 | return c.AssignedDeviceIDHash, c.DevicePassword
280 | }
281 |
282 | func (c *SEClient) populateRequest(req *http.Request) {
283 | req.Header["SE-Client-Version"] = []string{c.Settings.ClientVersion}
284 | req.Header["SE-Operating-System"] = []string{c.Settings.OperatingSystem}
285 | req.Header["User-Agent"] = []string{c.Settings.UserAgent}
286 | }
287 |
288 | func (c *SEClient) RpcCall(ctx context.Context, endpoint string, params map[string]string, res interface{}) error {
289 | c.Mux.Lock()
290 | defer c.Mux.Unlock()
291 |
292 | return c.rpcCall(ctx, endpoint, params, res)
293 | }
294 |
295 | func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[string]string, res interface{}) error {
296 | input := make(url.Values)
297 | for k, v := range params {
298 | input[k] = []string{v}
299 | }
300 | req, err := http.NewRequestWithContext(
301 | ctx,
302 | "POST",
303 | endpoint,
304 | strings.NewReader(input.Encode()),
305 | )
306 | if err != nil {
307 | return err
308 | }
309 | c.populateRequest(req)
310 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
311 | req.Header.Set("Accept", "application/json")
312 |
313 | resp, err := c.httpClient.Do(req)
314 | if err != nil {
315 | return err
316 | }
317 |
318 | if resp.StatusCode != http.StatusOK {
319 | return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header)
320 | }
321 |
322 | decoder := json.NewDecoder(resp.Body)
323 | err = decoder.Decode(res)
324 | cleanupBody(resp.Body)
325 |
326 | if err != nil {
327 | return err
328 | }
329 |
330 | return nil
331 | }
332 |
333 | // Does cleanup of HTTP response in order to make it reusable by keep-alive
334 | // logic of HTTP client
335 | func cleanupBody(body io.ReadCloser) {
336 | io.Copy(ioutil.Discard, &io.LimitedReader{
337 | R: body,
338 | N: READ_LIMIT,
339 | })
340 | body.Close()
341 | }
342 |
--------------------------------------------------------------------------------
/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: opera-proxy
2 | summary: Standalone Opera VPN proxies client.
3 | description: |
4 | Standalone Opera VPN proxies client. Just run it and it'll start plain HTTP proxy server forwarding traffic via proxies of your choice.
5 |
6 | confinement: strict
7 | base: core22
8 | adopt-info: opera-proxy
9 |
10 | parts:
11 | opera-proxy:
12 | plugin: go
13 | build-snaps: [go/latest/stable]
14 | build-packages:
15 | - make
16 | - git-core
17 | source: https://github.com/Snawoot/opera-proxy
18 | source-type: git
19 | override-pull: |
20 | craftctl default
21 | craftctl set version="$(git describe --long --tags --always --match=v*.*.* | sed 's/v//')"
22 | override-build:
23 | make &&
24 | cp bin/opera-proxy "$SNAPCRAFT_PART_INSTALL"
25 | stage:
26 | - opera-proxy
27 |
28 | apps:
29 | opera-proxy:
30 | command: opera-proxy
31 | plugs:
32 | - network
33 | - network-bind
34 |
--------------------------------------------------------------------------------