16 Commits

Author SHA1 Message Date
Vladislav Yarmak
6ebcb66a57 fix auth-secret command line argument 2021-06-25 23:33:20 +03:00
Vladislav Yarmak
378547283b fix makefile 2021-06-25 23:04:16 +03:00
Vladislav Yarmak
4fe260ff46 fix dockerfile 2021-06-25 22:34:54 +03:00
Vladislav Yarmak
07faf6dfea add doc; fix dockerfile 2021-06-25 21:07:29 +03:00
Vladislav Yarmak
f3d2b06f48 ci: docker 2021-06-25 20:44:37 +03:00
Vladislav Yarmak
abdbbbe687 resolver: finished 2021-06-25 19:49:06 +03:00
Vladislav Yarmak
d0b50bc52b resolver: WIP 2021-06-25 17:58:15 +03:00
Vladislav Yarmak
cfaf0bd449 cleanup 2021-06-25 14:54:57 +03:00
Vladislav Yarmak
b11ab2192e cleanup 2021-06-25 14:31:47 +03:00
Vladislav Yarmak
4d74c597a9 proper nosni dialer 2021-06-25 14:25:36 +03:00
Vladislav Yarmak
9011828de5 fix config fd leak 2021-06-25 13:58:35 +03:00
Vladislav Yarmak
ed775ab070 main: add location picking 2021-06-25 04:53:53 +03:00
Vladislav Yarmak
abb10a90d6 first working version 2021-06-25 04:16:41 +03:00
Vladislav Yarmak
c96b7ca763 wndclient: add BestLocation method 2021-06-25 03:39:42 +03:00
Vladislav Yarmak
857d435bf9 main: list locations 2021-06-25 02:36:37 +03:00
Vladislav Yarmak
b016b8a4d3 main: list proxies 2021-06-25 01:53:51 +03:00
12 changed files with 563 additions and 156 deletions

58
.github/workflows/docker-ci.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: docker-ci
on:
push:
branches:
- 'master'
release:
types: [published]
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Find Git Tag
id: tagger
uses: jimschubert/query-tag-action@v2
with:
include: 'v*'
exclude: '*-rc*'
commit-ish: 'HEAD'
skip-unshallow: 'true'
abbrev: 7
-
name: Determine image tag type
uses: haya14busa/action-cond@v1
id: imgtag
with:
cond: ${{ github.event_name == 'release' }}
if_true: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ github.event.release.tag_name }},${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest
if_false: ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7
push: true
tags: ${{ steps.imgtag.outputs.value }}
build-args: 'GIT_DESC=${{steps.tagger.outputs.tag}}'

View File

@@ -7,13 +7,15 @@ COPY . .
RUN CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static" -X main.version='"$GIT_DESC"
ADD https://curl.haxx.se/ca/cacert.pem /certs.crt
RUN chmod 0644 /certs.crt
RUN mkdir /state
FROM scratch AS arrange
COPY --from=build /go/src/github.com/Snawoot/windscribe-proxy/windscribe-proxy /
COPY --from=build /certs.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build --chown=9999:9999 /state /state
FROM scratch
COPY --from=arrange / /
USER 9999:9999
EXPOSE 18080/tcp
ENTRYPOINT ["/windscribe-proxy", "-bind-address", "0.0.0.0:28080"]
ENTRYPOINT ["/windscribe-proxy", "-state-file", "/state/wndstate.json", "-bind-address", "0.0.0.0:28080"]

View File

@@ -10,7 +10,7 @@ NDK_CC_ARM64 = $(abspath ../../ndk-toolchain-arm64/bin/aarch64-linux-android21-c
GO := go
src = $(wildcard *.go)
src = $(wildcard *.go */*.go)
native: bin-native
all: bin-linux-amd64 bin-linux-386 bin-linux-arm \

View File

@@ -1,3 +1,82 @@
# windscribe-proxy
windscribe-proxy
================
Work in progress...
Standalone Windscribe proxy client. Younger brother of [opera-proxy](https://github.com/Snawoot/opera-proxy/).
Just run it and it'll start a plain HTTP proxy server forwarding traffic through Windscribe proxies of your choice.
By default the application listens on 127.0.0.1:28080.
## Features
* Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD)
* Uses TLS for secure communication with upstream proxies
* Zero configuration
* Simple and straightforward
## Installation
#### Binaries
Pre-built binaries are available [here](https://github.com/Snawoot/windscribe-proxy/releases/latest).
#### Build from source
Alternatively, you may install windscribe-proxy from source. Run the following within the source directory:
```
make install
```
#### Docker
A docker image is available as well. Here is an example of running windscribe-proxy as a background service:
```sh
docker run -d \
--security-opt no-new-privileges \
-p 127.0.0.1:28080:28080 \
--restart unless-stopped \
--name windscribe-proxy \
yarmak/windscribe-proxy
```
## Usage
List available locations:
```
windscribe-proxy -list-locations
```
Run proxy via location of your choice:
```
windscribe-proxy -location Germany/Frankfurt
```
Also it is possible to export proxy addresses and credentials:
```
windscribe-proxy -list-proxies
```
## List of arguments
| Argument | Type | Description |
| -------- | ---- | ----------- |
| auth-secret | String | client auth secret (default `952b4412f002315aa50751032fcaab03`) |
| bind-address | String | HTTP proxy listen address (default `127.0.0.1:28080`) |
| cafile | String | use custom CA certificate bundle file |
| list-locations | - | list available locations and exit |
| list-proxies | - | output proxy list and exit |
| location | String | desired proxy location. Default: best location |
| proxy | String | sets base proxy to use for all dial-outs. Format: `<http\|https\|socks5\|socks5h>://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` |
| resolver | String | Use DNS/DoH/DoT/DoQ resolver for all dial-outs. See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. Examples: `https://1.1.1.1/dns-query`, `quic://dns.adguard.com` |
| state-file | String | file name used to persist Windscribe API client state. Default: `wndstate.json` |
| timeout | Duration | timeout for network operations. Default: `10s` |
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical). Default: `20` |
| version | - | show program version and exit |
## See also
* [Project wiki](https://github.com/Snawoot/windscribe-proxy/wiki)

1
go.mod
View File

@@ -4,6 +4,7 @@ go 1.16
require (
github.com/AdguardTeam/dnsproxy v0.37.7
github.com/ReneKroon/ttlcache/v2 v2.7.0
github.com/miekg/dns v1.1.43
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
)

21
go.sum
View File

@@ -13,6 +13,8 @@ github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKU
github.com/AdguardTeam/golibs v0.4.4 h1:cM9UySQiYFW79zo5XRwnaIWVzfW4eNXmZktMrWbthpw=
github.com/AdguardTeam/golibs v0.4.4/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ReneKroon/ttlcache/v2 v2.7.0 h1:sZeaSwA2UN/y/h7CvkW15Kovd2Oiy76CBDORiOwHPwI=
github.com/ReneKroon/ttlcache/v2 v2.7.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
@@ -155,14 +157,19 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -178,7 +185,11 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -195,6 +206,7 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
@@ -208,6 +220,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -225,6 +238,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -245,9 +259,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

275
main.go
View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/csv"
"encoding/json"
@@ -11,11 +10,13 @@ import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"os"
//"strings"
"sort"
"strconv"
"time"
xproxy "golang.org/x/net/proxy"
@@ -24,8 +25,8 @@ import (
)
const (
DEFAULT_CLIENT_AUTH_SECRET = "952b4412f002315aa50751032fcaab03"
ASSUMED_PROXY_PORT = 443
DEFAULT_CLIENT_AUTH_SECRET = "952b4412f002315aa50751032fcaab03"
ASSUMED_PROXY_PORT uint16 = 443
)
var (
@@ -45,17 +46,15 @@ func arg_fail(msg string) {
}
type CLIArgs struct {
country string
listCountries bool
location string
listLocations bool
listProxies bool
bindAddress string
verbosity int
timeout time.Duration
showVersion bool
proxy string
bootstrapDNS string
refresh time.Duration
refreshRetry time.Duration
resolver string
caFile string
clientAuthSecret string
stateFile string
@@ -63,8 +62,8 @@ type CLIArgs struct {
func parse_args() CLIArgs {
var args CLIArgs
flag.StringVar(&args.country, "country", "EU", "desired proxy location")
flag.BoolVar(&args.listCountries, "list-countries", false, "list available countries and exit")
flag.StringVar(&args.location, "location", "", "desired proxy location. Default: best location")
flag.BoolVar(&args.listLocations, "list-locations", false, "list available locations and exit")
flag.BoolVar(&args.listProxies, "list-proxies", false, "output proxy list and exit")
flag.StringVar(&args.bindAddress, "bind-address", "127.0.0.1:28080", "HTTP proxy listen address")
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
@@ -75,22 +74,17 @@ func parse_args() CLIArgs {
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] "+
"Examples: http://user:password@192.168.1.1:3128, socks5://10.0.0.1:1080")
// TODO: implement DNS resolving or remove it
flag.StringVar(&args.bootstrapDNS, "bootstrap-dns", "",
"DNS/DoH/DoT/DoQ resolver for initial discovering of SurfEasy API address. "+
flag.StringVar(&args.resolver, "resolver", "",
"Use DNS/DoH/DoT/DoQ resolver for all dial-outs. "+
"See https://github.com/ameshkov/dnslookup/ for upstream DNS URL format. "+
"Examples: https://1.1.1.1/dns-query, quic://dns.adguard.com")
flag.DurationVar(&args.refresh, "refresh", 4*time.Hour, "login refresh interval")
flag.DurationVar(&args.refreshRetry, "refresh-retry", 5*time.Second, "login refresh retry interval")
flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file")
flag.StringVar(&args.clientAuthSecret, "auth-secret", DEFAULT_CLIENT_AUTH_SECRET, "client auth secret")
flag.StringVar(&args.stateFile, "state-file", "wndstate.json", "file name used to persist "+
"Windscribe API client state")
flag.Parse()
if args.country == "" {
arg_fail("Country can't be empty string.")
}
if args.listCountries && args.listProxies {
arg_fail("list-countries and list-proxies flags are mutually exclusive")
if args.listLocations && args.listProxies {
arg_fail("list-locations and list-proxies flags are mutually exclusive")
}
return args
}
@@ -105,6 +99,7 @@ func proxyFromURLWrapper(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error)
}
func run() int {
var err error
args := parse_args()
if args.showVersion {
fmt.Println(version)
@@ -120,6 +115,9 @@ func run() int {
proxyLogger := NewCondLogger(log.New(logWriter, "PROXY : ",
log.LstdFlags|log.Lshortfile),
args.verbosity)
resolverLogger := NewCondLogger(log.New(logWriter, "RESOLVER: ",
log.LstdFlags|log.Lshortfile),
args.verbosity)
mainLogger.Info("windscribe-proxy client version %s is starting...", version)
@@ -128,6 +126,20 @@ func run() int {
KeepAlive: 30 * time.Second,
}
var caPool *x509.CertPool
if args.caFile != "" {
caPool = x509.NewCertPool()
certs, err := ioutil.ReadFile(args.caFile)
if err != nil {
mainLogger.Error("Can't load CA file: %v", err)
return 15
}
if ok := caPool.AppendCertsFromPEM(certs); !ok {
mainLogger.Error("Can't load certificates from CA file")
return 15
}
}
if args.proxy != "" {
xproxy.RegisterDialerType("http", proxyFromURLWrapper)
xproxy.RegisterDialerType("https", proxyFromURLWrapper)
@@ -144,22 +156,19 @@ func run() int {
dialer = pxDialer.(ContextDialer)
}
if args.resolver != "" {
dialer, err = NewResolvingDialer(args.resolver, args.timeout, dialer, resolverLogger)
if err != nil {
mainLogger.Critical("Unable to instantiate resolver: %v", err)
return 5
}
}
wndclientDialer := dialer
// TODO: properly validate cert, move TLSDialer to utils
tlsConfig := &tls.Config{
ServerName: "",
InsecureSkipVerify: true,
}
wndclient, err := wndclient.NewWndClient(&http.Transport{
DialContext: wndclientDialer.DialContext,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := wndclientDialer.DialContext(ctx, network, addr)
if err != nil {
return conn, err
}
return tls.Client(conn, tlsConfig), nil
},
wndc, err := wndclient.NewWndClient(&http.Transport{
DialContext: wndclientDialer.DialContext,
DialTLSContext: NewNoSNIDialer(caPool, wndclientDialer).DialTLSContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
@@ -170,95 +179,118 @@ func run() int {
mainLogger.Critical("Unable to construct WndClient: %v", err)
return 8
}
wndc.Mux.Lock()
wndc.State.Settings.ClientAuthSecret = args.clientAuthSecret
wndc.Mux.Unlock()
// Try ressurect state
state, err := loadState(args.stateFile)
if err != nil {
mainLogger.Warning("Failed to load client state: %v. Performing cold init...", err)
err = coldInit(wndclient, args.timeout)
err = coldInit(wndc, args.timeout)
if err != nil {
mainLogger.Critical("Cold init failed: %v", err)
return 9
}
err = saveState(args.stateFile, &wndclient.State)
err = saveState(args.stateFile, &wndc.State)
if err != nil {
mainLogger.Error("Unable to save state file! Error: %v", err)
}
} else {
wndclient.State = *state
wndc.Mux.Lock()
wndc.State = *state
wndc.Mux.Unlock()
}
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
serverList, err := wndclient.ServerList(ctx)
cl()
if err != nil {
mainLogger.Critical("Server list retrieve failed: %v", err)
return 12
var serverList wndclient.ServerList
if args.listProxies || args.listLocations || args.location != "" {
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
serverList, err = wndc.ServerList(ctx)
cl()
if err != nil {
mainLogger.Critical("Server list retrieve failed: %v", err)
return 12
}
}
if args.listProxies {
username, password := wndclient.GetProxyCredentials()
username, password := wndc.GetProxyCredentials()
return printProxies(username, password, serverList)
}
//if len(ips) == 0 {
// mainLogger.Critical("Empty endpoint list!")
// return 13
//}
//runTicker(context.Background(), args.refresh, args.refreshRetry, func(ctx context.Context) error {
// mainLogger.Info("Refreshing login...")
// reqCtx, cl := context.WithTimeout(ctx, args.timeout)
// defer cl()
// err := wndclient.Login(reqCtx)
// if err != nil {
// mainLogger.Error("Login refresh failed: %v", err)
// return err
// }
// mainLogger.Info("Login refreshed.")
// mainLogger.Info("Refreshing device password...")
// reqCtx, cl = context.WithTimeout(ctx, args.timeout)
// defer cl()
// err = wndclient.DeviceGeneratePassword(reqCtx)
// if err != nil {
// mainLogger.Error("Device password refresh failed: %v", err)
// return err
// }
// mainLogger.Info("Device password refreshed.")
// return nil
//})
//endpoint := ips[0]
auth := func() string {
return basic_auth_header(wndclient.GetProxyCredentials())
if args.listLocations {
return printLocations(serverList)
}
var caPool *x509.CertPool
if args.caFile != "" {
caPool = x509.NewCertPool()
certs, err := ioutil.ReadFile(args.caFile)
var proxyHostname string
if args.location == "" {
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
bestLocation, err := wndc.BestLocation(ctx)
cl()
if err != nil {
mainLogger.Error("Can't load CA file: %v", err)
return 15
mainLogger.Critical("Unable to get best location endpoint: %v", err)
return 13
}
if ok := caPool.AppendCertsFromPEM(certs); !ok {
mainLogger.Error("Can't load certificates from CA file")
return 15
proxyHostname = bestLocation.Hostname
} else {
proxyHostname = pickServer(serverList, args.location)
if proxyHostname == "" {
mainLogger.Critical("Server for location \"%s\" not found.", args.location)
return 13
}
}
// TODO: set servername
//handlerDialer := NewProxyDialer(endpoint.NetAddr(), "", auth, caPool, dialer)
//mainLogger.Info("Endpoint: %s", endpoint.NetAddr())
//mainLogger.Info("Starting proxy server...")
//handler := NewProxyHandler(handlerDialer, proxyLogger)
//mainLogger.Info("Init complete.")
//err = http.ListenAndServe(args.bindAddress, handler)
//mainLogger.Critical("Server terminated with a reason: %v", err)
//mainLogger.Info("Shutting down...")
_ = proxyLogger
_ = auth
auth := func() string {
return basic_auth_header(wndc.GetProxyCredentials())
}
proxyNetAddr := net.JoinHostPort(proxyHostname, strconv.FormatUint(uint64(ASSUMED_PROXY_PORT), 10))
handlerDialer := NewProxyDialer(proxyNetAddr, proxyHostname, auth, caPool, dialer)
mainLogger.Info("Endpoint: %s", proxyNetAddr)
mainLogger.Info("Starting proxy server...")
handler := NewProxyHandler(handlerDialer, proxyLogger)
mainLogger.Info("Init complete.")
err = http.ListenAndServe(args.bindAddress, handler)
mainLogger.Critical("Server terminated with a reason: %v", err)
mainLogger.Info("Shutting down...")
return 0
}
type locationPair struct {
country string
city string
}
func printLocations(serverList wndclient.ServerList) int {
var locs []locationPair
for _, country := range serverList {
for _, group := range country.Groups {
if len(group.Hosts) > 1 {
locs = append(locs, locationPair{country.Name, group.City})
}
}
}
if len(locs) == 0 {
return 0
}
sort.Slice(locs, func(i, j int) bool {
if locs[i].country < locs[j].country {
return true
}
if locs[i].country == locs[j].country && locs[i].city < locs[j].city {
return true
}
return false
})
var prevLoc locationPair
for _, loc := range locs {
if loc != prevLoc {
fmt.Println(loc.country + "/" + loc.city)
prevLoc = loc
}
}
return 0
}
@@ -269,24 +301,48 @@ func printProxies(username, password string, serverList wndclient.ServerList) in
fmt.Println("Proxy password:", password)
fmt.Println("Proxy-Authorization:", basic_auth_header(username, password))
fmt.Println("")
//wr.Write([]string{"host", "ip_address", "port"})
//for i, ip := range ips {
// for _, port := range ip.Ports {
// wr.Write([]string{
// fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX),
// ip.IP,
// fmt.Sprintf("%d", port),
// })
// }
//}
wr.Write([]string{"location", "hostname", "port"})
for _, country := range serverList {
for _, group := range country.Groups {
for _, host := range group.Hosts {
wr.Write([]string{
country.Name + "/" + group.City,
host.Hostname,
strconv.FormatUint(uint64(ASSUMED_PROXY_PORT), 10),
})
}
}
}
return 0
}
func pickServer(serverList wndclient.ServerList, location string) string {
var candidates []string
for _, country := range serverList {
for _, group := range country.Groups {
for _, host := range group.Hosts {
if country.Name+"/"+group.City == location {
candidates = append(candidates, host.Hostname)
}
}
}
}
if len(candidates) == 0 {
return ""
}
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
return candidates[rnd.Intn(len(candidates))]
}
func loadState(filename string) (*wndclient.WndClientState, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var state wndclient.WndClientState
dec := json.NewDecoder(file)
@@ -303,6 +359,7 @@ func saveState(filename string, state *wndclient.WndClientState) error {
if err != nil {
return err
}
defer file.Close()
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
@@ -310,23 +367,23 @@ func saveState(filename string, state *wndclient.WndClientState) error {
return err
}
func coldInit(wndclient *wndclient.WndClient, timeout time.Duration) error {
func coldInit(wndc *wndclient.WndClient, timeout time.Duration) error {
ctx, cl := context.WithTimeout(context.Background(), timeout)
err := wndclient.RegisterToken(ctx)
err := wndc.RegisterToken(ctx)
cl()
if err != nil {
return err
}
ctx, cl = context.WithTimeout(context.Background(), timeout)
err = wndclient.Users(ctx)
err = wndc.Users(ctx)
cl()
if err != nil {
return err
}
ctx, cl = context.WithTimeout(context.Background(), timeout)
err = wndclient.ServerCredentials(ctx)
err = wndc.ServerCredentials(ctx)
cl()
if err != nil {
return err

View File

@@ -1,16 +1,25 @@
package main
import (
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns"
"context"
"errors"
"net"
"strings"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/ReneKroon/ttlcache/v2"
"github.com/miekg/dns"
)
type Resolver struct {
upstream upstream.Upstream
}
const DOT = 0x2e
const (
DOT = 0x2e
DNS_CACHE_SIZE_LIMIT = 1024
)
func NewResolver(address string, timeout time.Duration) (*Resolver, error) {
opts := upstream.Options{Timeout: timeout}
@@ -26,9 +35,8 @@ func (r *Resolver) ResolveA(domain string) []string {
if len(domain) == 0 {
return res
}
if domain[len(domain)-1] != DOT {
domain = domain + "."
}
domain = absDomain(domain)
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
@@ -52,9 +60,8 @@ func (r *Resolver) ResolveAAAA(domain string) []string {
if len(domain) == 0 {
return res
}
if domain[len(domain)-1] != DOT {
domain = domain + "."
}
domain = absDomain(domain)
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
@@ -80,3 +87,128 @@ func (r *Resolver) Resolve(domain string) []string {
}
return res
}
type ResolvingDialer struct {
next ContextDialer
upstream upstream.Upstream
cache4 *ttlcache.Cache
cache6 *ttlcache.Cache
logger *CondLogger
}
func NewResolvingDialer(resolverAddress string, timeout time.Duration, next ContextDialer, logger *CondLogger) (*ResolvingDialer, error) {
opts := upstream.Options{Timeout: timeout}
u, err := upstream.AddressToUpstream(resolverAddress, opts)
if err != nil {
return nil, err
}
cache4 := ttlcache.NewCache()
cache6 := ttlcache.NewCache()
d := &ResolvingDialer{
upstream: u,
next: next,
cache4: cache4,
cache6: cache6,
logger: logger,
}
cache4.SetLoaderFunction(d.resolveA)
cache6.SetLoaderFunction(d.resolveAAAA)
cache4.SetCacheSizeLimit(DNS_CACHE_SIZE_LIMIT)
cache6.SetCacheSizeLimit(DNS_CACHE_SIZE_LIMIT)
cache4.SkipTTLExtensionOnHit(true)
cache6.SkipTTLExtensionOnHit(true)
return d, nil
}
func (d *ResolvingDialer) resolveA(domain string) (interface{}, time.Duration, error) {
d.logger.Debug("resolveA(%#v)", domain)
return d.resolve(domain, dns.TypeA)
}
func (d *ResolvingDialer) resolveAAAA(domain string) (interface{}, time.Duration, error) {
d.logger.Debug("resolveAAAA(%#v)", domain)
return d.resolve(domain, dns.TypeAAAA)
}
func (d *ResolvingDialer) resolve(domain string, typ uint16) (string, time.Duration, error) {
if len(domain) == 0 {
return "", 0, errors.New("empty domain name")
}
domain = absDomain(domain)
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: domain, Qtype: typ, Qclass: dns.ClassINET},
}
reply, err := d.upstream.Exchange(&req)
if err != nil {
return "", 0, err
}
for _, rr := range reply.Answer {
if a, ok := rr.(*dns.A); ok {
return a.A.String(), (time.Second * time.Duration(a.Hdr.Ttl)), nil
}
}
return "", 0, errors.New("no data in DNS response")
}
func (d *ResolvingDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
name, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
if net.ParseIP(name) != nil || len(name) == 0 {
// Address is already in numeric form
return d.next.DialContext(ctx, network, address)
}
if len(network) == 0 {
return d.next.DialContext(ctx, network, address)
}
name = absDomain(name)
switch network[len(network)-1] {
case '4':
res, err := d.cache4.Get(name)
if err != nil {
return nil, err
}
name = res.(string)
case '6':
res, err := d.cache6.Get(name)
if err != nil {
return nil, err
}
name = res.(string)
default:
res, err := d.cache4.Get(name)
if err != nil {
res, err = d.cache6.Get(name)
if err != nil {
return nil, err
}
}
name = res.(string)
}
newAddress := net.JoinHostPort(name, port)
d.logger.Debug("resolve rewrite: %s => %s", address, newAddress)
return d.next.DialContext(ctx, network, newAddress)
}
func (d *ResolvingDialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
func absDomain(domain string) string {
if domain == "" {
return ""
}
if domain[len(domain)-1] != DOT {
domain = domain + "."
}
return strings.ToLower(domain)
}

View File

@@ -1,27 +0,0 @@
name: windscribe-proxy
version: '1.0.0'
summary: Standalone Windscribe proxies client.
description: |
Standalone Windscribe proxies client. Just run it and it'll start plain HTTP proxy server forwarding traffic via proxies of your choice.
confinement: strict
base: core18
parts:
windscribe-proxy:
plugin: go
source: .
build-packages:
- gcc
override-build:
make &&
cp bin/windscribe-proxy "$SNAPCRAFT_PART_INSTALL"
stage:
- windscribe-proxy
apps:
windscribe-proxy:
command: windscribe-proxy
plugs:
- network
- network-bind

View File

@@ -32,12 +32,11 @@ type ContextDialer interface {
}
type ProxyDialer struct {
address string
tlsServerName string
auth AuthProvider
next ContextDialer
intermediateWorkaround bool
caPool *x509.CertPool
address string
tlsServerName string
auth AuthProvider
next ContextDialer
caPool *x509.CertPool
}
func NewProxyDialer(address, tlsServerName string, auth AuthProvider, caPool *x509.CertPool, nextDialer ContextDialer) *ProxyDialer {
@@ -185,3 +184,45 @@ func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
}
return http.ReadResponse(bufio.NewReader(buf), req)
}
type NoSNIDialer struct {
caPool *x509.CertPool
next ContextDialer
}
func NewNoSNIDialer(caPool *x509.CertPool, nextDialer ContextDialer) *NoSNIDialer {
return &NoSNIDialer{
caPool: caPool,
next: nextDialer,
}
}
func (d *NoSNIDialer) DialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := d.next.DialContext(ctx, network, addr)
name, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
ServerName: "",
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
opts := x509.VerifyOptions{
DNSName: name,
Intermediates: x509.NewCertPool(),
Roots: d.caPool,
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
_, err := cs.PeerCertificates[0].Verify(opts)
return err
},
}
if err != nil {
return conn, err
}
return tls.Client(conn, tlsConfig), nil
}

View File

@@ -82,3 +82,17 @@ type ServerListGroupHost struct {
Hostname string `json:"hostname"`
Weight float64 `json:"weight"`
}
type BestLocation struct {
CountryCode string `json:"country_code"`
ShortName string `json:"short_name"`
LocationName string `json:"location_name"`
CityName string `json:"city_name"`
DCID int `json:"dc_id"`
ServerID int `json:"server_id"`
Hostname string `json:"hostname"`
}
type BestLocationResponse struct {
Data *BestLocation `json:"data"`
}

View File

@@ -33,6 +33,7 @@ type WndEndpoints struct {
Users string `json:"Users"`
ServerList string `json:"serverlist"`
ServerCredentials string `json:"ServerCredentials"`
BestLocation string `json:"BestLocation"`
}
var DefaultWndEndpoints = WndEndpoints{
@@ -40,6 +41,7 @@ var DefaultWndEndpoints = WndEndpoints{
Users: "https://api.windscribe.com/Users",
ServerList: "https://assets.windscribe.com/serverlist",
ServerCredentials: "https://api.windscribe.com/ServerCredentials",
BestLocation: "https://api.windscribe.com/BestLocation",
}
type WndSettings struct {
@@ -193,6 +195,35 @@ func (c *WndClient) ServerCredentials(ctx context.Context) error {
return nil
}
func (c *WndClient) BestLocation(ctx context.Context) (*BestLocation, error) {
c.Mux.Lock()
defer c.Mux.Unlock()
clientAuthHash, authTime := MakeAuthHash(c.State.Settings.ClientAuthSecret)
requestUrl, err := url.Parse(c.State.Settings.Endpoints.BestLocation)
if err != nil {
return nil, err
}
queryValues := requestUrl.Query()
queryValues.Set("client_auth_hash", clientAuthHash)
queryValues.Set("session_auth_hash", c.State.SessionAuthHash)
queryValues.Set("time", strconv.FormatInt(authTime, 10))
requestUrl.RawQuery = queryValues.Encode()
var output BestLocationResponse
err = c.getJSON(ctx, requestUrl.String(), &output)
if err != nil {
return nil, err
}
if output.Data == nil {
return nil, ErrNoDataInResponse
}
return output.Data, nil
}
func (c *WndClient) ServerList(ctx context.Context) (ServerList, error) {
c.Mux.Lock()
defer c.Mux.Unlock()