├── .dockerignore ├── .github ├── FUNDING.yml ├── wireproxy-releaser.yml └── workflows │ ├── build.yml │ ├── container.yml │ ├── golangci-lint.yml │ ├── test.yml │ └── wireproxy.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── UseWithVPN.md ├── cmd └── wireproxy │ └── main.go ├── config.go ├── config_test.go ├── go.mod ├── go.sum ├── http.go ├── net.go ├── routine.go ├── systemd ├── README.md └── wireproxy.service ├── test_config.sh ├── util.go └── wireguard.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .github 3 | .gitignore 4 | Dockerfile 5 | LICENSE 6 | README.md 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | liberapay: octeep 4 | -------------------------------------------------------------------------------- /.github/wireproxy-releaser.yml: -------------------------------------------------------------------------------- 1 | project_name: Build wireproxy 2 | before: 3 | hooks: 4 | - go mod tidy -v 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | ldflags: 9 | - -s -w -X main.version={{.Version}} -X main.arch={{.Arch}} 10 | goos: 11 | - linux 12 | - windows 13 | - darwin 14 | goarch: 15 | - arm 16 | - arm64 17 | - 386 18 | - amd64 19 | - mips 20 | - mipsle 21 | - s390x 22 | - riscv64 23 | gomips: 24 | - softfloat 25 | ignore: 26 | - goos: windows 27 | goarch: arm 28 | - goos: windows 29 | goarch: arm64 30 | main: ./cmd/wireproxy/ 31 | binary: wireproxy 32 | universal_binaries: 33 | - name_template: "wireproxy" 34 | replace: false 35 | checksum: 36 | name_template: "checksums.txt" 37 | snapshot: 38 | name_template: "wireproxy" 39 | archives: 40 | - name_template: "wireproxy_{{ .Os }}_{{ .Arch }}" 41 | files: 42 | - none* 43 | changelog: 44 | sort: asc 45 | filters: 46 | exclude: 47 | - "^docs:" 48 | - "^test:" 49 | - "^chore" 50 | - Merge pull request 51 | - Merge branch 52 | - go mod tidy 53 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | pull_request: 7 | branches: 8 | - '**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | windowsAmd64Build: 13 | name: Build Windows amd64 Version 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setting up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "1.23" 21 | - name: Building Windows amd64 Version 22 | run: | 23 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o WireProxy_amd64.exe -v ./cmd/wireproxy 24 | mkdir release_windows_amd64 25 | mv WireProxy_amd64.exe wireproxy.exe 26 | cp wireproxy.exe release_windows_amd64/wireproxy.exe 27 | - name: Upload Windows amd64 Version 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: WireProxy_windows_amd64 31 | path: release_windows_amd64 32 | windowsArm64Build: 33 | name: Build Windows arm64 Version 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Setting up Go 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version: "1.23" 41 | - name: Building Windows arm64 Version 42 | run: | 43 | CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o WireProxy_arm64.exe -v ./cmd/wireproxy 44 | mkdir release_windows_arm64 45 | mv WireProxy_arm64.exe wireproxy.exe 46 | cp wireproxy.exe release_windows_arm64/wireproxy.exe 47 | - name: Upload Windows arm64 Version 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: WireProxy_windows_arm64 51 | path: release_windows_arm64 52 | linuxAmd64Build: 53 | name: Build Linux amd64 Version 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Setting up Go 58 | uses: actions/setup-go@v5 59 | with: 60 | go-version: "1.23" 61 | - name: Building Linux amd64 Version 62 | run: | 63 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o WireProxy_amd64 -v ./cmd/wireproxy 64 | mkdir release_linux_amd64 65 | mv WireProxy_amd64 wireproxy 66 | cp wireproxy release_linux_amd64/wireproxy 67 | - name: Upload Linux amd64 Version 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: WireProxy_linux_amd64 71 | path: release_linux_amd64 72 | linuxArm64Build: 73 | name: Build Linux arm64 Version 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Setting up Go 78 | uses: actions/setup-go@v5 79 | with: 80 | go-version: "1.23" 81 | - name: Building Linux arm64 Version 82 | run: | 83 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o WireProxy_arm64 -v ./cmd/wireproxy 84 | mkdir release_linux_arm64 85 | mv WireProxy_arm64 wireproxy 86 | cp wireproxy release_linux_arm64/wireproxy 87 | - name: Upload Linux arm64 Version 88 | uses: actions/upload-artifact@v4 89 | with: 90 | name: WireProxy_linux_arm64 91 | path: release_linux_arm64 92 | linuxS390xBuild: 93 | name: Build Linux s390x Version 94 | runs-on: ubuntu-latest 95 | steps: 96 | - uses: actions/checkout@v4 97 | - name: Setting up Go 98 | uses: actions/setup-go@v5 99 | with: 100 | go-version: "1.23" 101 | - name: Building Linux s390x Version 102 | run: | 103 | CGO_ENABLED=0 GOOS=linux GOARCH=s390x go build -o WireProxy_s390x -v ./cmd/wireproxy 104 | mkdir release_linux_s390x 105 | mv WireProxy_s390x wireproxy 106 | cp wireproxy release_linux_s390x/wireproxy 107 | - name: Upload Linux s390x Version 108 | uses: actions/upload-artifact@v4 109 | with: 110 | name: WireProxy_linux_s390x 111 | path: release_linux_s390x 112 | darwinAmd64Build: 113 | name: Build Darwin amd64 Version 114 | runs-on: ubuntu-latest 115 | steps: 116 | - uses: actions/checkout@v4 117 | - name: Setting up Go 118 | uses: actions/setup-go@v5 119 | with: 120 | go-version: "1.23" 121 | - name: Building Darwin amd64 Version 122 | run: | 123 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o WireProxy_amd64 -v ./cmd/wireproxy 124 | mkdir release_darwin_amd64 125 | mv WireProxy_amd64 wireproxy 126 | cp wireproxy release_darwin_amd64/wireproxy 127 | - name: Upload Darwin amd64 Version 128 | uses: actions/upload-artifact@v4 129 | with: 130 | name: WireProxy_darwin_amd64 131 | path: release_darwin_amd64 132 | darwinArm64Build: 133 | name: Build Darwin arm64 Version 134 | runs-on: ubuntu-latest 135 | steps: 136 | - uses: actions/checkout@v4 137 | - name: Setting up Go 138 | uses: actions/setup-go@v5 139 | with: 140 | go-version: "1.23" 141 | - name: Building Darwin arm64 Version 142 | run: | 143 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o WireProxy_arm64 -v ./cmd/wireproxy 144 | mkdir release_darwin_arm64 145 | mv WireProxy_arm64 wireproxy 146 | cp wireproxy release_darwin_arm64/wireproxy 147 | - name: Upload Darwin arm64 Version 148 | uses: actions/upload-artifact@v4 149 | with: 150 | name: WireProxy_darwin_arm64 151 | path: release_darwin_arm64 152 | -------------------------------------------------------------------------------- /.github/workflows/container.yml: -------------------------------------------------------------------------------- 1 | name: Build container 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | # Allow for manually running 9 | workflow_dispatch: 10 | inputs: 11 | container_tag: 12 | description: Tag for container 13 | default: "latest" 14 | required: true 15 | 16 | permissions: 17 | packages: write 18 | 19 | jobs: 20 | container: 21 | runs-on: ubuntu-20.04 22 | 23 | env: 24 | CONTAINER_NAME: ghcr.io/${{ github.repository }} 25 | BUILD_PLATFORMS: linux/amd64,linux/arm,linux/arm64,linux/ppc64le,linux/s390x 26 | RAW_CONTAINER_TAG: ${{ github.event.inputs.container_tag || github.event.pull_request.head.ref || 'latest' }} 27 | RAW_REF_NAME: ${{ github.event.pull_request.head.ref || github.ref }} 28 | 29 | steps: 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Set up Docker Buildx 34 | id: buildx 35 | uses: docker/setup-buildx-action@v3.0.0 36 | 37 | - name: Login to GitHub Container Registry 38 | uses: docker/login-action@v3 39 | with: 40 | registry: ghcr.io 41 | username: ${{ github.actor }} 42 | password: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - uses: actions/checkout@v4 45 | with: 46 | submodules: recursive 47 | 48 | # Needed for buildx gha cache to work 49 | - name: Expose GitHub Runtime 50 | uses: crazy-max/ghaction-github-runtime@v3 51 | 52 | - name: Build container 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: | 56 | CONTAINER_TAG=$(echo "$RAW_CONTAINER_TAG" | sed 's/[^a-zA-Z0-9]\+/-/') 57 | REF_NAME=$(echo "$RAW_REF_NAME" | sed -r 's#^refs/(heads|tags)/##') 58 | 59 | docker buildx build \ 60 | --platform "$BUILD_PLATFORMS" \ 61 | --tag "$CONTAINER_NAME:$CONTAINER_TAG" \ 62 | --tag "$CONTAINER_NAME:$GITHUB_SHA" \ 63 | --label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \ 64 | --label "org.opencontainers.image.documentation=${{ github.server_url }}/${{ github.repository }}" \ 65 | --label "org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}/packages" \ 66 | --label "org.opencontainers.image.ref.name=$REF_NAME" \ 67 | --label "org.opencontainers.image.revision=${{ github.sha }}" \ 68 | --label "org.opencontainers.image.vendor=${{ github.repository_owner }}" \ 69 | --label "org.opencontainers.image.created=$(date -u --rfc-3339=seconds)" \ 70 | --cache-from type=gha \ 71 | --cache-to type=gha,mode=max \ 72 | --pull ${{ github.event_name == 'push' && '--push' || '' }} . 73 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | pull_request: 7 | branches: 8 | - '**' 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.23' 21 | - uses: actions/checkout@v4 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v4 24 | with: 25 | version: latest 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | pull_request: 7 | branches: 8 | - '**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | name: Test wireproxy 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setting up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: "1.23" 21 | - name: Install dependencies 22 | run: sudo apt install wireguard curl 23 | - name: Building wireproxy 24 | run: | 25 | git tag dev 26 | make 27 | - name: Generate test config 28 | run: ./test_config.sh 29 | - name: Start wireproxy 30 | run: ./wireproxy -c test.conf & sleep 1 31 | - name: Test socks5 32 | run: curl --proxy socks5://localhost:64423 http://zx2c4.com/ip | grep -q "demo.wireguard.com" 33 | - name: Test http 34 | run: curl --proxy http://localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com" 35 | - name: Test http with password 36 | run: curl --proxy http://peter:hunter123@localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com" 37 | - name: Test http with wrong password 38 | run: | 39 | set +e 40 | curl -s --fail --proxy http://peter:wrongpass@localhost:64425 http://zx2c4.com/ip 41 | if [[ $? == 0 ]]; then exit 1; fi 42 | -------------------------------------------------------------------------------- /.github/workflows/wireproxy.yml: -------------------------------------------------------------------------------- 1 | name: Cross compile WireProxy 2 | 3 | on: 4 | workflow_dispatch: 5 | create: 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | WireProxy: 11 | 12 | name: Cross compile WireProxy 13 | 14 | runs-on: ubuntu-20.04 15 | 16 | env: 17 | workdir: ./WireProxy 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@master 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Git clone WireProxy 26 | run: | 27 | git clone https://github.com/artem-russkikh/wireproxy-awg.git ${{ env.workdir }} 28 | cp ./.github/wireproxy-releaser.yml ${{ env.workdir }}/.goreleaser.yml 29 | 30 | - name: Set up GoReleaser 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: "1.23" 34 | 35 | - name: Run GoReleaser 36 | uses: goreleaser/goreleaser-action@v5 37 | with: 38 | distribution: goreleaser 39 | workdir: ${{ env.workdir }} 40 | version: latest 41 | args: release --clean 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Release binaries 46 | uses: softprops/action-gh-release@v1 47 | with: 48 | tag_name: wireproxy 49 | files: ${{ env.workdir }}/dist/*.tar.gz 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /main 2 | /wireproxy 3 | *.sw? 4 | /.idea 5 | .goreleaser.yml 6 | *.conf 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start by building the application. 2 | FROM docker.io/golang:1.23 as build 3 | 4 | WORKDIR /usr/src/wireproxy 5 | COPY . . 6 | 7 | RUN make 8 | 9 | # Now copy it into our base image. 10 | FROM gcr.io/distroless/static-debian11:nonroot 11 | COPY --from=build /usr/src/wireproxy/wireproxy /usr/bin/wireproxy 12 | 13 | VOLUME [ "/etc/wireproxy"] 14 | ENTRYPOINT [ "/usr/bin/wireproxy" ] 15 | CMD [ "--config", "/etc/wireproxy/config" ] 16 | 17 | LABEL org.opencontainers.image.title="wireproxy" 18 | LABEL org.opencontainers.image.description="Wireguard client that exposes itself as a socks5 proxy" 19 | LABEL org.opencontainers.image.licenses="ISC" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Wind Wong 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO ?= go 2 | export CGO_ENABLED = 0 3 | 4 | TAG := $(shell git describe --always --tags $(git rev-list --tags --max-count=1) --match v*) 5 | 6 | .PHONY: all 7 | all: wireproxy 8 | 9 | .PHONY: wireproxy 10 | wireproxy: 11 | ${GO} build -trimpath -ldflags "-s -w -X 'main.version=${TAG}'" ./cmd/wireproxy 12 | 13 | .PHONY: clean 14 | clean: 15 | ${RM} wireproxy 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wireproxy 2 | [![ISC licensed](https://img.shields.io/badge/license-ISC-blue)](./LICENSE) 3 | [![Build status](https://github.com/octeep/wireproxy/actions/workflows/build.yml/badge.svg)](https://github.com/octeep/wireproxy/actions) 4 | [![Documentation](https://img.shields.io/badge/godoc-wireproxy-blue)](https://pkg.go.dev/github.com/octeep/wireproxy) 5 | 6 | AmneziaWG compatible wireguard client that exposes itself as a socks5/http proxy or tunnels. Forked from [wireproxy](https://github.com/pufferffish/wireproxy) 7 | 8 | # What is this 9 | `wireproxy` is a completely userspace application that connects to a wireguard peer, 10 | and exposes a socks5/http proxy or tunnels on the machine. This can be useful if you need 11 | to connect to certain sites via a wireguard peer, but can't be bothered to setup a new network 12 | interface for whatever reasons. 13 | 14 | # Why you might want this 15 | - You simply want to use wireguard as a way to proxy some traffic. 16 | - You don't want root permission just to change wireguard settings. 17 | 18 | Currently, I'm running wireproxy connected to a wireguard server in another country, 19 | and configured my browser to use wireproxy for certain sites. It's pretty useful since 20 | wireproxy is completely isolated from my network interfaces, and I don't need root to configure 21 | anything. 22 | 23 | Users who want something similar but for Amnezia VPN can use [this fork](https://github.com/artem-russkikh/wireproxy-awg) 24 | of wireproxy by [@artem-russkikh](https://github.com/artem-russkikh). 25 | 26 | # Feature 27 | - TCP static routing for client and server 28 | - SOCKS5/HTTP proxy (currently only CONNECT is supported) 29 | 30 | # TODO 31 | - UDP Support in SOCKS5 32 | - UDP static routing 33 | 34 | # Usage 35 | ``` 36 | ./wireproxy [-c path to config] 37 | ``` 38 | 39 | ``` 40 | usage: wireproxy [-h|--help] [-c|--config ""] [-s|--silent] 41 | [-d|--daemon] [-i|--info ""] [-v|--version] 42 | [-n|--configtest] 43 | 44 | Userspace wireguard client for proxying 45 | 46 | Arguments: 47 | 48 | -h --help Print help information 49 | -c --config Path of configuration file 50 | Default paths: /etc/wireproxy/wireproxy.conf, $HOME/.config/wireproxy.conf 51 | -s --silent Silent mode 52 | -d --daemon Make wireproxy run in background 53 | -i --info Specify the address and port for exposing health status 54 | -v --version Print version 55 | -n --configtest Configtest mode. Only check the configuration file for 56 | validity. 57 | 58 | ``` 59 | 60 | # Build instruction 61 | ``` 62 | git clone https://github.com/octeep/wireproxy 63 | cd wireproxy 64 | make 65 | ``` 66 | 67 | # Use with VPN 68 | Instructions for using wireproxy with Firefox container tabs and auto-start on MacOS can be found [here](/UseWithVPN.md). 69 | 70 | # Sample config file 71 | ``` 72 | # The [Interface] and [Peer] configurations follow the same semantics and meaning 73 | # of a wg-quick configuration. To understand what these fields mean, please refer to: 74 | # https://wiki.archlinux.org/title/WireGuard#Persistent_configuration 75 | # https://www.wireguard.com/#simple-network-interface 76 | [Interface] 77 | Address = 10.200.200.2/32 # The subnet should be /32 and /128 for IPv4 and v6 respectively 78 | # MTU = 1420 (optional) 79 | PrivateKey = uCTIK+56CPyCvwJxmU5dBfuyJvPuSXAq1FzHdnIxe1Q= 80 | # PrivateKey = $MY_WIREGUARD_PRIVATE_KEY # Alternatively, reference environment variables 81 | DNS = 10.200.200.1 82 | 83 | [Peer] 84 | PublicKey = QP+A67Z2UBrMgvNIdHv8gPel5URWNLS4B3ZQ2hQIZlg= 85 | # PresharedKey = UItQuvLsyh50ucXHfjF0bbR4IIpVBd74lwKc8uIPXXs= (optional) 86 | Endpoint = my.ddns.example.com:51820 87 | # PersistentKeepalive = 25 (optional) 88 | 89 | # TCPClientTunnel is a tunnel listening on your machine, 90 | # and it forwards any TCP traffic received to the specified target via wireguard. 91 | # Flow: 92 | # --> localhost:25565 --(wireguard)--> play.cubecraft.net:25565 93 | [TCPClientTunnel] 94 | BindAddress = 127.0.0.1:25565 95 | Target = play.cubecraft.net:25565 96 | 97 | # TCPServerTunnel is a tunnel listening on wireguard, 98 | # and it forwards any TCP traffic received to the specified target via local network. 99 | # Flow: 100 | # --(wireguard)--> 172.16.31.2:3422 --> localhost:25545 101 | [TCPServerTunnel] 102 | ListenPort = 3422 103 | Target = localhost:25545 104 | 105 | # STDIOTunnel is a tunnel connecting the standard input and output of the wireproxy 106 | # process to the specified TCP target via wireguard. 107 | # This is especially useful to use wireproxy as a ProxyCommand parameter in openssh 108 | # For example: 109 | # ssh -o ProxyCommand='wireproxy -c myconfig.conf' ssh.myserver.net 110 | # Flow: 111 | # Piped command -->(wireguard)--> ssh.myserver.net:22 112 | [STDIOTunnel] 113 | Target = ssh.myserver.net:22 114 | 115 | # Socks5 creates a socks5 proxy on your LAN, and all traffic would be routed via wireguard. 116 | [Socks5] 117 | BindAddress = 127.0.0.1:25344 118 | 119 | # Socks5 authentication parameters, specifying username and password enables 120 | # proxy authentication. 121 | #Username = ... 122 | # Avoid using spaces in the password field 123 | #Password = ... 124 | 125 | # http creates a http proxy on your LAN, and all traffic would be routed via wireguard. 126 | [http] 127 | BindAddress = 127.0.0.1:25345 128 | 129 | # HTTP authentication parameters, specifying username and password enables 130 | # proxy authentication. 131 | #Username = ... 132 | # Avoid using spaces in the password field 133 | #Password = ... 134 | ``` 135 | 136 | Alternatively, if you already have a wireguard config, you can import it in the 137 | wireproxy config file like this: 138 | ``` 139 | WGConfig = 140 | 141 | # Same semantics as above 142 | [TCPClientTunnel] 143 | ... 144 | 145 | [TCPServerTunnel] 146 | ... 147 | 148 | [Socks5] 149 | ... 150 | ``` 151 | 152 | Having multiple peers is also supported. `AllowedIPs` would need to be specified 153 | such that wireproxy would know which peer to forward to. 154 | ``` 155 | [Interface] 156 | Address = 10.254.254.40/32 157 | PrivateKey = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX= 158 | 159 | [Peer] 160 | Endpoint = 192.168.0.204:51820 161 | PublicKey = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY= 162 | AllowedIPs = 10.254.254.100/32 163 | PersistentKeepalive = 25 164 | 165 | [Peer] 166 | PublicKey = ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ= 167 | AllowedIPs = 10.254.254.1/32, fdee:1337:c000:d00d::1/128 168 | Endpoint = 172.16.0.185:44044 169 | PersistentKeepalive = 25 170 | 171 | 172 | [TCPServerTunnel] 173 | ListenPort = 5000 174 | Target = service-one.servicenet:5000 175 | 176 | [TCPServerTunnel] 177 | ListenPort = 5001 178 | Target = service-two.servicenet:5001 179 | 180 | [TCPServerTunnel] 181 | ListenPort = 5080 182 | Target = service-three.servicenet:80 183 | ``` 184 | 185 | Wireproxy can also allow peers to connect to it: 186 | ``` 187 | [Interface] 188 | ListenPort = 5400 189 | ... 190 | 191 | [Peer] 192 | PublicKey = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY= 193 | AllowedIPs = 10.254.254.100/32 194 | # Note there is no Endpoint defined here. 195 | ``` 196 | # Health endpoint 197 | Wireproxy supports exposing a health endpoint for monitoring purposes. 198 | The argument `--info/-i` specifies an address and port (e.g. `localhost:9080`), which exposes a HTTP server that provides health status metric of the server. 199 | 200 | Currently two endpoints are implemented: 201 | 202 | `/metrics`: Exposes information of the wireguard daemon, this provides the same information you would get with `wg show`. [This](https://www.wireguard.com/xplatform/#example-dialog) shows an example of what the response would look like. 203 | 204 | `/readyz`: This responds with a json which shows the last time a pong is received from an IP specified with `CheckAlive`. When `CheckAlive` is set, a ping is sent out to addresses in `CheckAlive` per `CheckAliveInterval` seconds (defaults to 5) via wireguard. If a pong has not been received from one of the addresses within the last `CheckAliveInterval` seconds (+2 seconds for some leeway to account for latency), then it would respond with a 503, otherwise a 200. 205 | 206 | For example: 207 | ``` 208 | [Interface] 209 | PrivateKey = censored 210 | Address = 10.2.0.2/32 211 | DNS = 10.2.0.1 212 | CheckAlive = 1.1.1.1, 3.3.3.3 213 | CheckAliveInterval = 3 214 | 215 | [Peer] 216 | PublicKey = censored 217 | AllowedIPs = 0.0.0.0/0 218 | Endpoint = 149.34.244.174:51820 219 | 220 | [Socks5] 221 | BindAddress = 127.0.0.1:25344 222 | ``` 223 | `/readyz` would respond with 224 | ``` 225 | < HTTP/1.1 503 Service Unavailable 226 | < Date: Thu, 11 Apr 2024 00:54:59 GMT 227 | < Content-Length: 35 228 | < Content-Type: text/plain; charset=utf-8 229 | < 230 | {"1.1.1.1":1712796899,"3.3.3.3":0} 231 | ``` 232 | 233 | And for: 234 | ``` 235 | [Interface] 236 | PrivateKey = censored 237 | Address = 10.2.0.2/32 238 | DNS = 10.2.0.1 239 | CheckAlive = 1.1.1.1 240 | ``` 241 | `/readyz` would respond with 242 | ``` 243 | < HTTP/1.1 200 OK 244 | < Date: Thu, 11 Apr 2024 00:56:21 GMT 245 | < Content-Length: 23 246 | < Content-Type: text/plain; charset=utf-8 247 | < 248 | {"1.1.1.1":1712796979} 249 | ``` 250 | 251 | If nothing is set for `CheckAlive`, an empty JSON object with 200 will be the response. 252 | 253 | The peer which the ICMP ping packet is routed to depends on the `AllowedIPs` set for each peers. 254 | 255 | # Stargazers over time 256 | [![Stargazers over time](https://starchart.cc/octeep/wireproxy.svg)](https://starchart.cc/octeep/wireproxy) 257 | -------------------------------------------------------------------------------- /UseWithVPN.md: -------------------------------------------------------------------------------- 1 | # Getting a Wireguard Server 2 | You can create your own wireguard server using a host service like DigitalOcean, 3 | or you can get a VPN service that provides WireGuard configs. 4 | 5 | I recommend ProtonVPN, because it is highly secure and has a great WireGuard 6 | config generator. 7 | 8 | Simply go to https://account.protonvpn.com/downloads and scroll down to the 9 | wireguard section to generate your configs, then paste into the appropriate 10 | section below. 11 | 12 | # Simple Setup for multiple SOCKS configs for firefox 13 | 14 | Create a folder for your configs and startup scripts. Can be the same place as 15 | this code. That path you will use below. For reference this text uses 16 | `/Users/jonny/vpntabs` 17 | 18 | For each VPN you want to run, you will download your wireguard config and name 19 | it appropriately (e.g. `ProtonUS.adblock.server.conf`) and then create two new 20 | files from those below with similar names (e.g. `ProtonUS.adblock.conf` and 21 | `ProtonUS.adblock.sh`) 22 | 23 | You will also create a launch script, the reference below is only for macOS. The 24 | naming should also be similar (e.g. 25 | `/Users/jonny/Library/LaunchAgents/com.ProtonUS.adblock.plist`) 26 | 27 | ## Config File 28 | Make sure you use a unique port for every separate server 29 | I recommend you set proxy authentication, you can use the same user/pass for all 30 | ``` 31 | # Link to the Downloaded config 32 | WGConfig = /Users/jonny/vpntabs/ProtonUS.adblock.server.conf 33 | 34 | # Used for firefox containers 35 | [Socks5] 36 | BindAddress = 127.0.0.1:25344 # Update the port here for each new server 37 | 38 | # Socks5 authentication parameters, specifying username and password enables 39 | # proxy authentication. 40 | #Username = ... 41 | # Avoid using spaces in the password field 42 | #Password = ... 43 | ``` 44 | 45 | ## Startup Script File 46 | This is a bash script to facilitate startup, not strictly essential, but adds 47 | ease. 48 | Note, you MUST update the first path to wherever you installed this code to. 49 | Make sure you use the path for the config file above, not the one you downloaded 50 | from e.g. protonvpn. 51 | ``` 52 | #!/bin/bash 53 | /Users/jonny/wireproxy/wireproxy -c /Users/jonny/vpntabs/ProtonUS.adblock.conf 54 | ``` 55 | 56 | ## MacOS LaunchAgent 57 | To make it run every time you start your computer, you can create a launch agent 58 | in `$HOME/Library/LaunchAgents`. Name reference above. 59 | 60 | That file should contain the following, the label should be the same as the file 61 | name and the paths should be set correctly: 62 | 63 | ``` 64 | 65 | 66 | 67 | 68 | Label 69 | com.ProtonUS.adblock 70 | Program 71 | /Users/jonny/vpntabs/ProtonUS.adblock.sh 72 | RunAtLoad 73 | 74 | KeepAlive 75 | 76 | 77 | 78 | ``` 79 | 80 | To enable it, run 81 | `launchctl load ~/Library/LaunchAgents/com.ProtonUS.adblock.plist` and 82 | `launchtl start ~/Library/LaunchAgents/com.PortonUS.adblock.plist` 83 | 84 | # Firefox Setup 85 | You will need to enable the Multi Account Container Tabs extension and a proxy extension, I 86 | recommend Sideberry, but Container Proxy also works. 87 | 88 | Create a container to be dedicated to this VPN, and then add the IP, port, 89 | username, and password from above. 90 | -------------------------------------------------------------------------------- /cmd/wireproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/landlock-lsm/go-landlock/landlock" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "os/signal" 13 | "strconv" 14 | "syscall" 15 | 16 | "github.com/akamensky/argparse" 17 | "github.com/amnezia-vpn/amneziawg-go/device" 18 | wireproxyawg "github.com/artem-russkikh/wireproxy-awg" 19 | "suah.dev/protect" 20 | ) 21 | 22 | // an argument to denote that this process was spawned by -d 23 | const daemonProcess = "daemon-process" 24 | 25 | // default paths for wireproxy config file 26 | var default_config_paths = []string { 27 | "/etc/wireproxy/wireproxy.conf", 28 | os.Getenv("HOME")+"/.config/wireproxy.conf", 29 | } 30 | 31 | var version = "1.0.11-dev" 32 | 33 | func panicIfError(err error) { 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | } 38 | 39 | // attempts to pledge and panic if it fails 40 | // this does nothing on non-OpenBSD systems 41 | func pledgeOrPanic(promises string) { 42 | panicIfError(protect.Pledge(promises)) 43 | } 44 | 45 | // attempts to unveil and panic if it fails 46 | // this does nothing on non-OpenBSD systems 47 | func unveilOrPanic(path string, flags string) { 48 | panicIfError(protect.Unveil(path, flags)) 49 | } 50 | 51 | // get the executable path via syscalls or infer it from argv 52 | func executablePath() string { 53 | programPath, err := os.Executable() 54 | if err != nil { 55 | return os.Args[0] 56 | } 57 | return programPath 58 | } 59 | 60 | // check if default config file paths exist 61 | func configFilePath() (string, bool) { 62 | for _, path := range default_config_paths { 63 | if _, err := os.Stat(path); err == nil { 64 | return path, true 65 | } 66 | } 67 | return "", false 68 | } 69 | 70 | func lock(stage string) { 71 | switch stage { 72 | case "boot": 73 | exePath := executablePath() 74 | // OpenBSD 75 | unveilOrPanic("/", "r") 76 | unveilOrPanic(exePath, "x") 77 | // only allow standard stdio operation, file reading, networking, and exec 78 | // also remove unveil permission to lock unveil 79 | pledgeOrPanic("stdio rpath inet dns proc exec") 80 | // Linux 81 | panicIfError(landlock.V1.BestEffort().RestrictPaths( 82 | landlock.RODirs("/"), 83 | )) 84 | case "boot-daemon": 85 | case "read-config": 86 | // OpenBSD 87 | pledgeOrPanic("stdio rpath inet dns") 88 | case "ready": 89 | // no file access is allowed from now on, only networking 90 | // OpenBSD 91 | pledgeOrPanic("stdio inet dns") 92 | // Linux 93 | net.DefaultResolver.PreferGo = true // needed to lock down dependencies 94 | panicIfError(landlock.V1.BestEffort().RestrictPaths( 95 | landlock.ROFiles("/etc/resolv.conf").IgnoreIfMissing(), 96 | landlock.ROFiles("/dev/fd").IgnoreIfMissing(), 97 | landlock.ROFiles("/dev/zero").IgnoreIfMissing(), 98 | landlock.ROFiles("/dev/urandom").IgnoreIfMissing(), 99 | landlock.ROFiles("/etc/localtime").IgnoreIfMissing(), 100 | landlock.ROFiles("/proc/self/stat").IgnoreIfMissing(), 101 | landlock.ROFiles("/proc/self/status").IgnoreIfMissing(), 102 | landlock.ROFiles("/usr/share/locale").IgnoreIfMissing(), 103 | landlock.ROFiles("/proc/self/cmdline").IgnoreIfMissing(), 104 | landlock.ROFiles("/usr/share/zoneinfo").IgnoreIfMissing(), 105 | landlock.ROFiles("/proc/sys/kernel/version").IgnoreIfMissing(), 106 | landlock.ROFiles("/proc/sys/kernel/ngroups_max").IgnoreIfMissing(), 107 | landlock.ROFiles("/proc/sys/kernel/cap_last_cap").IgnoreIfMissing(), 108 | landlock.ROFiles("/proc/sys/vm/overcommit_memory").IgnoreIfMissing(), 109 | landlock.RWFiles("/dev/log").IgnoreIfMissing(), 110 | landlock.RWFiles("/dev/null").IgnoreIfMissing(), 111 | landlock.RWFiles("/dev/full").IgnoreIfMissing(), 112 | landlock.RWFiles("/proc/self/fd").IgnoreIfMissing(), 113 | )) 114 | default: 115 | panic("invalid stage") 116 | } 117 | } 118 | 119 | func extractPort(addr string) uint16 { 120 | _, portStr, err := net.SplitHostPort(addr) 121 | if err != nil { 122 | panic(fmt.Errorf("failed to extract port from %s: %w", addr, err)) 123 | } 124 | 125 | port, err := strconv.Atoi(portStr) 126 | if err != nil { 127 | panic(fmt.Errorf("failed to extract port from %s: %w", addr, err)) 128 | } 129 | 130 | return uint16(port) 131 | } 132 | 133 | func lockNetwork(sections []wireproxyawg.RoutineSpawner, infoAddr *string) { 134 | var rules []landlock.Rule 135 | if infoAddr != nil && *infoAddr != "" { 136 | rules = append(rules, landlock.BindTCP(extractPort(*infoAddr))) 137 | } 138 | 139 | for _, section := range sections { 140 | switch section := section.(type) { 141 | case *wireproxyawg.TCPServerTunnelConfig: 142 | rules = append(rules, landlock.ConnectTCP(extractPort(section.Target))) 143 | case *wireproxyawg.HTTPConfig: 144 | rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress))) 145 | case *wireproxyawg.TCPClientTunnelConfig: 146 | rules = append(rules, landlock.ConnectTCP(uint16(section.BindAddress.Port))) 147 | case *wireproxyawg.Socks5Config: 148 | rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress))) 149 | } 150 | } 151 | 152 | panicIfError(landlock.V4.BestEffort().RestrictNet(rules...)) 153 | } 154 | 155 | func main() { 156 | s := make(chan os.Signal, 1) 157 | signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT) 158 | ctx, cancel := context.WithCancel(context.Background()) 159 | 160 | go func() { 161 | <-s 162 | cancel() 163 | }() 164 | 165 | exePath := executablePath() 166 | lock("boot") 167 | 168 | isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess 169 | args := os.Args 170 | if isDaemonProcess { 171 | lock("boot-daemon") 172 | args = []string{args[0]} 173 | args = append(args, os.Args[2:]...) 174 | } 175 | parser := argparse.NewParser("wireproxy", "Userspace wireguard client for proxying") 176 | 177 | config := parser.String("c", "config", &argparse.Options{Help: "Path of configuration file"}) 178 | silent := parser.Flag("s", "silent", &argparse.Options{Help: "Silent mode"}) 179 | daemon := parser.Flag("d", "daemon", &argparse.Options{Help: "Make wireproxy run in background"}) 180 | info := parser.String("i", "info", &argparse.Options{Help: "Specify the address and port for exposing health status"}) 181 | printVerison := parser.Flag("v", "version", &argparse.Options{Help: "Print version"}) 182 | configTest := parser.Flag("n", "configtest", &argparse.Options{Help: "Configtest mode. Only check the configuration file for validity."}) 183 | 184 | err := parser.Parse(args) 185 | if err != nil { 186 | fmt.Print(parser.Usage(err)) 187 | return 188 | } 189 | 190 | if *printVerison { 191 | fmt.Printf("wireproxy, version %s\n", version) 192 | return 193 | } 194 | 195 | if *config == "" { 196 | if path, config_exist := configFilePath(); config_exist { 197 | *config = path 198 | } else { 199 | fmt.Println("configuration path is required") 200 | return 201 | } 202 | } 203 | 204 | if !*daemon { 205 | lock("read-config") 206 | } 207 | 208 | conf, err := wireproxyawg.ParseConfig(*config) 209 | if err != nil { 210 | log.Fatal(err) 211 | } 212 | 213 | if *configTest { 214 | fmt.Println("Config OK") 215 | return 216 | } 217 | 218 | lockNetwork(conf.Routines, info) 219 | 220 | if isDaemonProcess { 221 | os.Stdout, _ = os.Open(os.DevNull) 222 | os.Stderr, _ = os.Open(os.DevNull) 223 | *daemon = false 224 | } 225 | 226 | if *daemon { 227 | args[0] = daemonProcess 228 | cmd := exec.Command(exePath, args...) 229 | err = cmd.Start() 230 | if err != nil { 231 | fmt.Println(err.Error()) 232 | } 233 | return 234 | } 235 | 236 | // Wireguard doesn't allow configuring which FD to use for logging 237 | // https://github.com/WireGuard/wireguard-go/blob/master/device/logger.go#L39 238 | // so redirect STDOUT to STDERR, we don't want to print anything to STDOUT anyways 239 | os.Stdout = os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") 240 | logLevel := device.LogLevelVerbose 241 | if *silent { 242 | logLevel = device.LogLevelSilent 243 | } 244 | 245 | lock("ready") 246 | 247 | tun, err := wireproxyawg.StartWireguard(conf.Device, logLevel) 248 | if err != nil { 249 | log.Fatal(err) 250 | } 251 | 252 | for _, spawner := range conf.Routines { 253 | go spawner.SpawnRoutine(tun) 254 | } 255 | 256 | tun.StartPingIPs() 257 | 258 | if *info != "" { 259 | go func() { 260 | err := http.ListenAndServe(*info, tun) 261 | if err != nil { 262 | panic(err) 263 | } 264 | }() 265 | } 266 | 267 | <-ctx.Done() 268 | } 269 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package wireproxy 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "errors" 7 | "net" 8 | "os" 9 | "strings" 10 | 11 | "github.com/go-ini/ini" 12 | 13 | "net/netip" 14 | ) 15 | 16 | type PeerConfig struct { 17 | PublicKey string 18 | PreSharedKey string 19 | Endpoint *string 20 | KeepAlive int 21 | AllowedIPs []netip.Prefix 22 | } 23 | 24 | type ASecConfigType struct { 25 | junkPacketCount int // Jc 26 | junkPacketMinSize int // Jmin 27 | junkPacketMaxSize int // Jmax 28 | initPacketJunkSize int // s1 29 | responsePacketJunkSize int // s2 30 | initPacketMagicHeader uint32 // h1 31 | responsePacketMagicHeader uint32 // h2 32 | underloadPacketMagicHeader uint32 // h3 33 | transportPacketMagicHeader uint32 // h4 34 | } 35 | 36 | // DeviceConfig contains the information to initiate a wireguard connection 37 | type DeviceConfig struct { 38 | SecretKey string 39 | Endpoint []netip.Addr 40 | Peers []PeerConfig 41 | DNS []netip.Addr 42 | MTU int 43 | ListenPort *int 44 | CheckAlive []netip.Addr 45 | CheckAliveInterval int 46 | ASecConfig *ASecConfigType 47 | } 48 | 49 | type TCPClientTunnelConfig struct { 50 | BindAddress *net.TCPAddr 51 | Target string 52 | } 53 | 54 | type STDIOTunnelConfig struct { 55 | Target string 56 | } 57 | 58 | type TCPServerTunnelConfig struct { 59 | ListenPort int 60 | Target string 61 | } 62 | 63 | type Socks5Config struct { 64 | BindAddress string 65 | Username string 66 | Password string 67 | } 68 | 69 | type HTTPConfig struct { 70 | BindAddress string 71 | Username string 72 | Password string 73 | } 74 | 75 | type Configuration struct { 76 | Device *DeviceConfig 77 | Routines []RoutineSpawner 78 | } 79 | 80 | func parseString(section *ini.Section, keyName string) (string, error) { 81 | key := section.Key(strings.ToLower(keyName)) 82 | if key == nil { 83 | return "", errors.New(keyName + " should not be empty") 84 | } 85 | value := key.String() 86 | if strings.HasPrefix(value, "$") { 87 | if strings.HasPrefix(value, "$$") { 88 | return strings.Replace(value, "$$", "$", 1), nil 89 | } 90 | var ok bool 91 | value, ok = os.LookupEnv(strings.TrimPrefix(value, "$")) 92 | if !ok { 93 | return "", errors.New(keyName + " references unset environment variable " + key.String()) 94 | } 95 | return value, nil 96 | } 97 | return key.String(), nil 98 | } 99 | 100 | func parsePort(section *ini.Section, keyName string) (int, error) { 101 | key := section.Key(keyName) 102 | if key == nil { 103 | return 0, errors.New(keyName + " should not be empty") 104 | } 105 | 106 | port, err := key.Int() 107 | if err != nil { 108 | return 0, err 109 | } 110 | 111 | if !(port >= 0 && port < 65536) { 112 | return 0, errors.New("port should be >= 0 and < 65536") 113 | } 114 | 115 | return port, nil 116 | } 117 | 118 | func parseTCPAddr(section *ini.Section, keyName string) (*net.TCPAddr, error) { 119 | addrStr, err := parseString(section, keyName) 120 | if err != nil { 121 | return nil, err 122 | } 123 | return net.ResolveTCPAddr("tcp", addrStr) 124 | } 125 | 126 | func parseBase64KeyToHex(section *ini.Section, keyName string) (string, error) { 127 | key, err := parseString(section, keyName) 128 | if err != nil { 129 | return "", err 130 | } 131 | result, err := encodeBase64ToHex(key) 132 | if err != nil { 133 | return result, err 134 | } 135 | 136 | return result, nil 137 | } 138 | 139 | func encodeBase64ToHex(key string) (string, error) { 140 | decoded, err := base64.StdEncoding.DecodeString(key) 141 | if err != nil { 142 | return "", errors.New("invalid base64 string: " + key) 143 | } 144 | if len(decoded) != 32 { 145 | return "", errors.New("key should be 32 bytes: " + key) 146 | } 147 | return hex.EncodeToString(decoded), nil 148 | } 149 | 150 | func parseNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) { 151 | key, err := parseString(section, keyName) 152 | if err != nil { 153 | if strings.Contains(err.Error(), "should not be empty") { 154 | return []netip.Addr{}, nil 155 | } 156 | return nil, err 157 | } 158 | 159 | keys := strings.Split(key, ",") 160 | var ips = make([]netip.Addr, 0, len(keys)) 161 | for _, str := range keys { 162 | str = strings.TrimSpace(str) 163 | if len(str) == 0 { 164 | continue 165 | } 166 | ip, err := netip.ParseAddr(str) 167 | if err != nil { 168 | return nil, err 169 | } 170 | ips = append(ips, ip) 171 | } 172 | return ips, nil 173 | } 174 | 175 | func parseCIDRNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) { 176 | key, err := parseString(section, keyName) 177 | if err != nil { 178 | if strings.Contains(err.Error(), "should not be empty") { 179 | return []netip.Addr{}, nil 180 | } 181 | return nil, err 182 | } 183 | 184 | keys := strings.Split(key, ",") 185 | var ips = make([]netip.Addr, 0, len(keys)) 186 | for _, str := range keys { 187 | str = strings.TrimSpace(str) 188 | if len(str) == 0 { 189 | continue 190 | } 191 | 192 | if addr, err := netip.ParseAddr(str); err == nil { 193 | ips = append(ips, addr) 194 | } else { 195 | prefix, err := netip.ParsePrefix(str) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | addr := prefix.Addr() 201 | ips = append(ips, addr) 202 | } 203 | } 204 | return ips, nil 205 | } 206 | 207 | func parseAllowedIPs(section *ini.Section) ([]netip.Prefix, error) { 208 | key, err := parseString(section, "AllowedIPs") 209 | if err != nil { 210 | if strings.Contains(err.Error(), "should not be empty") { 211 | return []netip.Prefix{}, nil 212 | } 213 | return nil, err 214 | } 215 | 216 | keys := strings.Split(key, ",") 217 | var ips = make([]netip.Prefix, 0, len(keys)) 218 | for _, str := range keys { 219 | str = strings.TrimSpace(str) 220 | if len(str) == 0 { 221 | continue 222 | } 223 | prefix, err := netip.ParsePrefix(str) 224 | if err != nil { 225 | return nil, err 226 | } 227 | 228 | ips = append(ips, prefix) 229 | } 230 | return ips, nil 231 | } 232 | 233 | func resolveIP(ip string) (*net.IPAddr, error) { 234 | return net.ResolveIPAddr("ip", ip) 235 | } 236 | 237 | func resolveIPPAndPort(addr string) (string, error) { 238 | host, port, err := net.SplitHostPort(addr) 239 | if err != nil { 240 | return "", err 241 | } 242 | 243 | ip, err := resolveIP(host) 244 | if err != nil { 245 | return "", err 246 | } 247 | return net.JoinHostPort(ip.String(), port), nil 248 | } 249 | 250 | // ParseInterface parses the [Interface] section and extract the information into `device` 251 | func ParseInterface(cfg *ini.File, device *DeviceConfig) error { 252 | sections, err := cfg.SectionsByName("Interface") 253 | if len(sections) != 1 || err != nil { 254 | return errors.New("one and only one [Interface] is expected") 255 | } 256 | section := sections[0] 257 | 258 | address, err := parseCIDRNetIP(section, "Address") 259 | if err != nil { 260 | return err 261 | } 262 | 263 | device.Endpoint = address 264 | 265 | privKey, err := parseBase64KeyToHex(section, "PrivateKey") 266 | if err != nil { 267 | return err 268 | } 269 | device.SecretKey = privKey 270 | 271 | dns, err := parseNetIP(section, "DNS") 272 | if err != nil { 273 | return err 274 | } 275 | device.DNS = dns 276 | 277 | if sectionKey, err := section.GetKey("MTU"); err == nil { 278 | value, err := sectionKey.Int() 279 | if err != nil { 280 | return err 281 | } 282 | device.MTU = value 283 | } 284 | 285 | if sectionKey, err := section.GetKey("ListenPort"); err == nil { 286 | value, err := sectionKey.Int() 287 | if err != nil { 288 | return err 289 | } 290 | device.ListenPort = &value 291 | } 292 | 293 | checkAlive, err := parseNetIP(section, "CheckAlive") 294 | if err != nil { 295 | return err 296 | } 297 | device.CheckAlive = checkAlive 298 | 299 | device.CheckAliveInterval = 5 300 | if sectionKey, err := section.GetKey("CheckAliveInterval"); err == nil { 301 | value, err := sectionKey.Int() 302 | if err != nil { 303 | return err 304 | } 305 | if len(checkAlive) == 0 { 306 | return errors.New("CheckAliveInterval is only valid when CheckAlive is set") 307 | } 308 | 309 | device.CheckAliveInterval = value 310 | } 311 | 312 | aSecConfig, err := ParseASecConfig(section) 313 | if err != nil { 314 | return err 315 | } 316 | device.ASecConfig = aSecConfig 317 | 318 | return nil 319 | } 320 | 321 | func ParseASecConfig(section *ini.Section) (*ASecConfigType, error) { 322 | var aSecConfig *ASecConfigType 323 | 324 | if sectionKey, err := section.GetKey("Jc"); err == nil { 325 | value, err := sectionKey.Int() 326 | if err != nil { 327 | return nil, err 328 | } 329 | if aSecConfig == nil { 330 | aSecConfig = &ASecConfigType{} 331 | } 332 | aSecConfig.junkPacketCount = value 333 | } 334 | 335 | if sectionKey, err := section.GetKey("Jmin"); err == nil { 336 | value, err := sectionKey.Int() 337 | if err != nil { 338 | return nil, err 339 | } 340 | if aSecConfig == nil { 341 | aSecConfig = &ASecConfigType{} 342 | } 343 | aSecConfig.junkPacketMinSize = value 344 | } 345 | 346 | if sectionKey, err := section.GetKey("Jmax"); err == nil { 347 | value, err := sectionKey.Int() 348 | if err != nil { 349 | return nil, err 350 | } 351 | if aSecConfig == nil { 352 | aSecConfig = &ASecConfigType{} 353 | } 354 | aSecConfig.junkPacketMaxSize = value 355 | } 356 | 357 | if sectionKey, err := section.GetKey("S1"); err == nil { 358 | value, err := sectionKey.Int() 359 | if err != nil { 360 | return nil, err 361 | } 362 | if aSecConfig == nil { 363 | aSecConfig = &ASecConfigType{} 364 | } 365 | aSecConfig.initPacketJunkSize = value 366 | } 367 | 368 | if sectionKey, err := section.GetKey("S2"); err == nil { 369 | value, err := sectionKey.Int() 370 | if err != nil { 371 | return nil, err 372 | } 373 | if aSecConfig == nil { 374 | aSecConfig = &ASecConfigType{} 375 | } 376 | aSecConfig.responsePacketJunkSize = value 377 | } 378 | 379 | if sectionKey, err := section.GetKey("H1"); err == nil { 380 | value, err := sectionKey.Uint() 381 | if err != nil { 382 | return nil, err 383 | } 384 | if aSecConfig == nil { 385 | aSecConfig = &ASecConfigType{} 386 | } 387 | aSecConfig.initPacketMagicHeader = uint32(value) 388 | } 389 | 390 | if sectionKey, err := section.GetKey("H2"); err == nil { 391 | value, err := sectionKey.Uint() 392 | if err != nil { 393 | return nil, err 394 | } 395 | if aSecConfig == nil { 396 | aSecConfig = &ASecConfigType{} 397 | } 398 | aSecConfig.responsePacketMagicHeader = uint32(value) 399 | } 400 | 401 | if sectionKey, err := section.GetKey("H3"); err == nil { 402 | value, err := sectionKey.Uint() 403 | if err != nil { 404 | return nil, err 405 | } 406 | if aSecConfig == nil { 407 | aSecConfig = &ASecConfigType{} 408 | } 409 | aSecConfig.underloadPacketMagicHeader = uint32(value) 410 | } 411 | 412 | if sectionKey, err := section.GetKey("H4"); err == nil { 413 | value, err := sectionKey.Uint() 414 | if err != nil { 415 | return nil, err 416 | } 417 | if aSecConfig == nil { 418 | aSecConfig = &ASecConfigType{} 419 | } 420 | aSecConfig.transportPacketMagicHeader = uint32(value) 421 | } 422 | 423 | if err := ValidateASecConfig(aSecConfig); err != nil { 424 | return nil, err 425 | } 426 | 427 | return aSecConfig, nil 428 | } 429 | 430 | func ValidateASecConfig(config *ASecConfigType) error { 431 | if config == nil { 432 | return nil 433 | } 434 | jc := config.junkPacketCount 435 | jmin := config.junkPacketMinSize 436 | jmax := config.junkPacketMaxSize 437 | if jc < 1 || jc > 128 { 438 | return errors.New("value of the Jc field must be within the range of 1 to 128") 439 | } 440 | if jmin > jmax { 441 | return errors.New("value of the Jmin field must be less than or equal to Jmax field value") 442 | } 443 | if jmax > 1280 { 444 | return errors.New("value of the Jmax field must be less than or equal 1280") 445 | } 446 | 447 | s1 := config.initPacketJunkSize 448 | s2 := config.responsePacketJunkSize 449 | const messageInitiationSize = 148 450 | const messageResponseSize = 92 451 | if messageInitiationSize+s1 == messageResponseSize+s2 { 452 | return errors.New( 453 | "value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)", 454 | ) 455 | } 456 | 457 | h1 := config.initPacketMagicHeader 458 | h2 := config.responsePacketMagicHeader 459 | h3 := config.underloadPacketMagicHeader 460 | h4 := config.transportPacketMagicHeader 461 | if (h1 == h2) || (h1 == h3) || (h1 == h4) || (h2 == h3) || (h2 == h4) || (h3 == h4) { 462 | return errors.New("values of the H1-H4 fields must be unique") 463 | } 464 | 465 | return nil 466 | } 467 | 468 | // ParsePeers parses the [Peer] section and extract the information into `peers` 469 | func ParsePeers(cfg *ini.File, peers *[]PeerConfig) error { 470 | sections, err := cfg.SectionsByName("Peer") 471 | if len(sections) < 1 || err != nil { 472 | return errors.New("at least one [Peer] is expected") 473 | } 474 | 475 | for _, section := range sections { 476 | peer := PeerConfig{ 477 | PreSharedKey: "0000000000000000000000000000000000000000000000000000000000000000", 478 | KeepAlive: 0, 479 | } 480 | 481 | decoded, err := parseBase64KeyToHex(section, "PublicKey") 482 | if err != nil { 483 | return err 484 | } 485 | peer.PublicKey = decoded 486 | 487 | if sectionKey, err := section.GetKey("PreSharedKey"); err == nil { 488 | value, err := encodeBase64ToHex(sectionKey.String()) 489 | if err != nil { 490 | return err 491 | } 492 | peer.PreSharedKey = value 493 | } 494 | 495 | if value, err := parseString(section, "Endpoint"); err == nil { 496 | decoded, err = resolveIPPAndPort(strings.ToLower(value)) 497 | if err != nil { 498 | return err 499 | } 500 | peer.Endpoint = &decoded 501 | } 502 | 503 | if sectionKey, err := section.GetKey("PersistentKeepalive"); err == nil { 504 | value, err := sectionKey.Int() 505 | if err != nil { 506 | return err 507 | } 508 | peer.KeepAlive = value 509 | } 510 | 511 | peer.AllowedIPs, err = parseAllowedIPs(section) 512 | if err != nil { 513 | return err 514 | } 515 | 516 | *peers = append(*peers, peer) 517 | } 518 | return nil 519 | } 520 | 521 | func parseTCPClientTunnelConfig(section *ini.Section) (RoutineSpawner, error) { 522 | config := &TCPClientTunnelConfig{} 523 | tcpAddr, err := parseTCPAddr(section, "BindAddress") 524 | if err != nil { 525 | return nil, err 526 | } 527 | config.BindAddress = tcpAddr 528 | 529 | targetSection, err := parseString(section, "Target") 530 | if err != nil { 531 | return nil, err 532 | } 533 | config.Target = targetSection 534 | 535 | return config, nil 536 | } 537 | 538 | func parseSTDIOTunnelConfig(section *ini.Section) (RoutineSpawner, error) { 539 | config := &STDIOTunnelConfig{} 540 | targetSection, err := parseString(section, "Target") 541 | if err != nil { 542 | return nil, err 543 | } 544 | config.Target = targetSection 545 | 546 | return config, nil 547 | } 548 | 549 | func parseTCPServerTunnelConfig(section *ini.Section) (RoutineSpawner, error) { 550 | config := &TCPServerTunnelConfig{} 551 | 552 | listenPort, err := parsePort(section, "ListenPort") 553 | if err != nil { 554 | return nil, err 555 | } 556 | config.ListenPort = listenPort 557 | 558 | target, err := parseString(section, "Target") 559 | if err != nil { 560 | return nil, err 561 | } 562 | config.Target = target 563 | 564 | return config, nil 565 | } 566 | 567 | func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) { 568 | config := &Socks5Config{} 569 | 570 | bindAddress, err := parseString(section, "BindAddress") 571 | if err != nil { 572 | return nil, err 573 | } 574 | config.BindAddress = bindAddress 575 | 576 | username, _ := parseString(section, "Username") 577 | config.Username = username 578 | 579 | password, _ := parseString(section, "Password") 580 | config.Password = password 581 | 582 | return config, nil 583 | } 584 | 585 | func parseHTTPConfig(section *ini.Section) (RoutineSpawner, error) { 586 | config := &HTTPConfig{} 587 | 588 | bindAddress, err := parseString(section, "BindAddress") 589 | if err != nil { 590 | return nil, err 591 | } 592 | config.BindAddress = bindAddress 593 | 594 | username, _ := parseString(section, "Username") 595 | config.Username = username 596 | 597 | password, _ := parseString(section, "Password") 598 | config.Password = password 599 | 600 | return config, nil 601 | } 602 | 603 | // Takes a function that parses an individual section into a config, and apply it on all 604 | // specified sections 605 | func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error { 606 | sections, err := cfg.SectionsByName(sectionName) 607 | if err != nil { 608 | return nil 609 | } 610 | 611 | for _, section := range sections { 612 | config, err := f(section) 613 | if err != nil { 614 | return err 615 | } 616 | 617 | *routines = append(*routines, config) 618 | } 619 | 620 | return nil 621 | } 622 | 623 | // ParseConfig takes the path of a configuration file and parses it into Configuration 624 | func ParseConfig(path string) (*Configuration, error) { 625 | iniOpt := ini.LoadOptions{ 626 | Insensitive: true, 627 | AllowShadows: true, 628 | AllowNonUniqueSections: true, 629 | } 630 | 631 | cfg, err := ini.LoadSources(iniOpt, path) 632 | if err != nil { 633 | return nil, err 634 | } 635 | 636 | device := &DeviceConfig{ 637 | MTU: 1420, 638 | } 639 | 640 | root := cfg.Section("") 641 | wgConf, err := root.GetKey("WGConfig") 642 | wgCfg := cfg 643 | if err == nil { 644 | wgCfg, err = ini.LoadSources(iniOpt, wgConf.String()) 645 | if err != nil { 646 | return nil, err 647 | } 648 | } 649 | 650 | err = ParseInterface(wgCfg, device) 651 | if err != nil { 652 | return nil, err 653 | } 654 | 655 | err = ParsePeers(wgCfg, &device.Peers) 656 | if err != nil { 657 | return nil, err 658 | } 659 | 660 | var routinesSpawners []RoutineSpawner 661 | 662 | err = parseRoutinesConfig(&routinesSpawners, cfg, "TCPClientTunnel", parseTCPClientTunnelConfig) 663 | if err != nil { 664 | return nil, err 665 | } 666 | 667 | err = parseRoutinesConfig(&routinesSpawners, cfg, "STDIOTunnel", parseSTDIOTunnelConfig) 668 | if err != nil { 669 | return nil, err 670 | } 671 | 672 | err = parseRoutinesConfig(&routinesSpawners, cfg, "TCPServerTunnel", parseTCPServerTunnelConfig) 673 | if err != nil { 674 | return nil, err 675 | } 676 | 677 | err = parseRoutinesConfig(&routinesSpawners, cfg, "Socks5", parseSocks5Config) 678 | if err != nil { 679 | return nil, err 680 | } 681 | 682 | err = parseRoutinesConfig(&routinesSpawners, cfg, "http", parseHTTPConfig) 683 | if err != nil { 684 | return nil, err 685 | } 686 | 687 | return &Configuration{ 688 | Device: device, 689 | Routines: routinesSpawners, 690 | }, nil 691 | } 692 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package wireproxy 2 | 3 | import ( 4 | "github.com/go-ini/ini" 5 | "testing" 6 | ) 7 | 8 | func loadIniConfig(config string) (*ini.File, error) { 9 | iniOpt := ini.LoadOptions{ 10 | Insensitive: true, 11 | AllowShadows: true, 12 | AllowNonUniqueSections: true, 13 | } 14 | 15 | return ini.LoadSources(iniOpt, []byte(config)) 16 | } 17 | 18 | func TestWireguardConfWithoutSubnet(t *testing.T) { 19 | const config = ` 20 | [Interface] 21 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 22 | Address = 10.5.0.2 23 | DNS = 1.1.1.1 24 | 25 | [Peer] 26 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 27 | AllowedIPs = 0.0.0.0/0, ::/0 28 | Endpoint = 94.140.11.15:51820 29 | PersistentKeepalive = 25` 30 | var cfg DeviceConfig 31 | iniData, err := loadIniConfig(config) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | err = ParseInterface(iniData, &cfg) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | 42 | func TestWireguardConfWithSubnet(t *testing.T) { 43 | const config = ` 44 | [Interface] 45 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 46 | Address = 10.5.0.2/23 47 | DNS = 1.1.1.1 48 | 49 | [Peer] 50 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 51 | AllowedIPs = 0.0.0.0/0, ::/0 52 | Endpoint = 94.140.11.15:51820 53 | PersistentKeepalive = 25` 54 | var cfg DeviceConfig 55 | iniData, err := loadIniConfig(config) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | err = ParseInterface(iniData, &cfg) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | } 65 | 66 | func TestWireguardConfWithAWGParams(t *testing.T) { 67 | const config = ` 68 | [Interface] 69 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 70 | Address = 10.5.0.2 71 | DNS = 1.1.1.1 72 | Jc = 5 73 | Jmin = 10 74 | Jmax = 50 75 | S1 = 0 76 | S2 = 0 77 | H1 = 1 78 | H2 = 2 79 | H3 = 3 80 | H4 = 4 81 | 82 | [Peer] 83 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 84 | AllowedIPs = 0.0.0.0/0, ::/0 85 | Endpoint = 94.140.11.15:51820 86 | PersistentKeepalive = 25` 87 | var cfg DeviceConfig 88 | iniData, err := loadIniConfig(config) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | err = ParseInterface(iniData, &cfg) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | } 98 | 99 | func TestWireguardConfWithInvalid1AWGParams(t *testing.T) { 100 | const config = ` 101 | [Interface] 102 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 103 | Address = 10.5.0.2 104 | DNS = 1.1.1.1 105 | Jc = 200 106 | Jmin = 10 107 | Jmax = 50 108 | S1 = 0 109 | S2 = 0 110 | H1 = 1 111 | H2 = 2 112 | H3 = 3 113 | H4 = 4 114 | 115 | [Peer] 116 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 117 | AllowedIPs = 0.0.0.0/0, ::/0 118 | Endpoint = 94.140.11.15:51820 119 | PersistentKeepalive = 25` 120 | var cfg DeviceConfig 121 | iniData, err := loadIniConfig(config) 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | 126 | expectedError := "value of the Jc field must be within the range of 1 to 128" 127 | err = ParseInterface(iniData, &cfg) 128 | if err == nil { 129 | t.Fatal("error expected") 130 | } 131 | if err != nil && err.Error() != expectedError { 132 | t.Fatalf("error expected: %s, got: %s", expectedError, err.Error()) 133 | } 134 | } 135 | 136 | func TestWireguardConfWithInvalid2AWGParams(t *testing.T) { 137 | const config = ` 138 | [Interface] 139 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 140 | Address = 10.5.0.2 141 | DNS = 1.1.1.1 142 | Jc = 5 143 | Jmin = 55 144 | Jmax = 50 145 | S1 = 0 146 | S2 = 0 147 | H1 = 1 148 | H2 = 2 149 | H3 = 3 150 | H4 = 4 151 | 152 | [Peer] 153 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 154 | AllowedIPs = 0.0.0.0/0, ::/0 155 | Endpoint = 94.140.11.15:51820 156 | PersistentKeepalive = 25` 157 | var cfg DeviceConfig 158 | iniData, err := loadIniConfig(config) 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | 163 | expectedError := "value of the Jmin field must be less than or equal to Jmax field value" 164 | err = ParseInterface(iniData, &cfg) 165 | if err == nil { 166 | t.Fatal("error expected") 167 | } 168 | if err != nil && err.Error() != expectedError { 169 | t.Fatalf("error expected: %s, got: %s", expectedError, err.Error()) 170 | } 171 | } 172 | 173 | func TestWireguardConfWithInvalid3AWGParams(t *testing.T) { 174 | const config = ` 175 | [Interface] 176 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 177 | Address = 10.5.0.2 178 | DNS = 1.1.1.1 179 | Jc = 5 180 | Jmin = 10 181 | Jmax = 1300 182 | S1 = 0 183 | S2 = 0 184 | H1 = 1 185 | H2 = 2 186 | H3 = 3 187 | H4 = 4 188 | 189 | [Peer] 190 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 191 | AllowedIPs = 0.0.0.0/0, ::/0 192 | Endpoint = 94.140.11.15:51820 193 | PersistentKeepalive = 25` 194 | var cfg DeviceConfig 195 | iniData, err := loadIniConfig(config) 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | 200 | expectedError := "value of the Jmax field must be less than or equal 1280" 201 | err = ParseInterface(iniData, &cfg) 202 | if err == nil { 203 | t.Fatal("error expected") 204 | } 205 | if err != nil && err.Error() != expectedError { 206 | t.Fatalf("error expected: %s, got: %s", expectedError, err.Error()) 207 | } 208 | } 209 | 210 | func TestWireguardConfWithInvalid4AWGParams(t *testing.T) { 211 | const config = ` 212 | [Interface] 213 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 214 | Address = 10.5.0.2 215 | DNS = 1.1.1.1 216 | Jc = 5 217 | Jmin = 10 218 | Jmax = 50 219 | S1 = 0 220 | S2 = 56 221 | H1 = 1 222 | H2 = 2 223 | H3 = 3 224 | H4 = 4 225 | 226 | [Peer] 227 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 228 | AllowedIPs = 0.0.0.0/0, ::/0 229 | Endpoint = 94.140.11.15:51820 230 | PersistentKeepalive = 25` 231 | var cfg DeviceConfig 232 | iniData, err := loadIniConfig(config) 233 | if err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | expectedError := "value of the field S1 + message initiation size (148) must not equal S2 + message response size (92)" 238 | err = ParseInterface(iniData, &cfg) 239 | if err == nil { 240 | t.Fatal("error expected") 241 | } 242 | if err != nil && err.Error() != expectedError { 243 | t.Fatalf("error expected: %s, got: %s", expectedError, err.Error()) 244 | } 245 | } 246 | 247 | func TestWireguardConfWithInvalid5AWGParams(t *testing.T) { 248 | const config = ` 249 | [Interface] 250 | PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0= 251 | Address = 10.5.0.2 252 | DNS = 1.1.1.1 253 | Jc = 5 254 | Jmin = 10 255 | Jmax = 50 256 | S1 = 0 257 | S2 = 0 258 | H1 = 1 259 | H2 = 2 260 | H3 = 2 261 | H4 = 4 262 | 263 | [Peer] 264 | PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w= 265 | AllowedIPs = 0.0.0.0/0, ::/0 266 | Endpoint = 94.140.11.15:51820 267 | PersistentKeepalive = 25` 268 | var cfg DeviceConfig 269 | iniData, err := loadIniConfig(config) 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | 274 | expectedError := "values of the H1-H4 fields must be unique" 275 | err = ParseInterface(iniData, &cfg) 276 | if err == nil { 277 | t.Fatal("error expected") 278 | } 279 | if err != nil && err.Error() != expectedError { 280 | t.Fatalf("error expected: %s, got: %s", expectedError, err.Error()) 281 | } 282 | } 283 | 284 | func TestWireguardConfWithManyAddress(t *testing.T) { 285 | const config = ` 286 | [Interface] 287 | PrivateKey = mBsVDahr1XIu9PPd17UmsDdB6E53nvmS47NbNqQCiFM= 288 | Address = 100.96.0.190,2606:B300:FFFF:fe8a:2ac6:c7e8:b021:6f5f/128 289 | DNS = 198.18.0.1,198.18.0.2 290 | 291 | [Peer] 292 | PublicKey = SHnh4C2aDXhp1gjIqceGhJrhOLSeNYcqWLKcYnzj00U= 293 | AllowedIPs = 0.0.0.0/0,::/0 294 | Endpoint = 192.200.144.22:51820` 295 | var cfg DeviceConfig 296 | iniData, err := loadIniConfig(config) 297 | if err != nil { 298 | t.Fatal(err) 299 | } 300 | 301 | err = ParseInterface(iniData, &cfg) 302 | if err != nil { 303 | t.Fatal(err) 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/artem-russkikh/wireproxy-awg 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/MakeNowJust/heredoc/v2 v2.0.1 7 | github.com/akamensky/argparse v1.4.0 8 | github.com/amnezia-vpn/amneziawg-go v0.2.12 9 | github.com/go-ini/ini v1.67.0 10 | github.com/landlock-lsm/go-landlock v0.0.0-20240715193425-db0c8d6f1dff 11 | github.com/sourcegraph/conc v0.3.0 12 | github.com/things-go/go-socks5 v0.0.5 13 | golang.org/x/net v0.28.0 14 | suah.dev/protect v1.2.4 15 | ) 16 | 17 | require ( 18 | github.com/google/btree v1.1.3 // indirect 19 | github.com/stretchr/testify v1.9.0 // indirect 20 | github.com/tevino/abool/v2 v2.1.0 // indirect 21 | golang.org/x/crypto v0.26.0 // indirect 22 | golang.org/x/sys v0.24.0 // indirect 23 | golang.org/x/time v0.6.0 // indirect 24 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 25 | gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect 26 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= 2 | github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= 3 | github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc= 4 | github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= 5 | github.com/amnezia-vpn/amneziawg-go v0.2.12 h1:CxIQETy5kZ0ip/dFBpmnDxAcS/KuIQaJkOxDv5OQhVI= 6 | github.com/amnezia-vpn/amneziawg-go v0.2.12/go.mod h1:d7WpNfzCRLy7ufGElJBYpD58WRmNjyLyt3IDHPY8AmM= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 10 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 11 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 12 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 13 | github.com/landlock-lsm/go-landlock v0.0.0-20240715193425-db0c8d6f1dff h1:x3f7WnTbCmOl/pCqbb5UDFH1PqRNAAkn/xy5mwZJgoo= 14 | github.com/landlock-lsm/go-landlock v0.0.0-20240715193425-db0c8d6f1dff/go.mod h1:ln1YHTUL4mGdRe14d/8nDEGF0ikfpiWK1yk20Txy490= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 18 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 19 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 20 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c= 22 | github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY= 23 | github.com/things-go/go-socks5 v0.0.5 h1:qvKaGcBkfDrUL33SchHN93srAmYGzb4CxSM2DPYufe8= 24 | github.com/things-go/go-socks5 v0.0.5/go.mod h1:mtzInf8v5xmsBpHZVbIw2YQYhc4K0jRwzfsH64Uh0IQ= 25 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 26 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 27 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 28 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 29 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 31 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 32 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 33 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 34 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 35 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 36 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 37 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= 39 | gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= 40 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI= 41 | kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= 42 | suah.dev/protect v1.2.4 h1:iVZG/zQB63FKNpITDYM/cXoAeCTIjCiXHuFVByJFDzg= 43 | suah.dev/protect v1.2.4/go.mod h1:vVrquYO3u1Ep9Ez2z8x+6N6/czm+TBmWKZfiXU2tb54= 44 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package wireproxy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net" 12 | "net/http" 13 | "strings" 14 | 15 | "github.com/sourcegraph/conc" 16 | ) 17 | 18 | const proxyAuthHeaderKey = "Proxy-Authorization" 19 | 20 | type HTTPServer struct { 21 | config *HTTPConfig 22 | 23 | auth CredentialValidator 24 | dial func(network, address string) (net.Conn, error) 25 | 26 | authRequired bool 27 | } 28 | 29 | func (s *HTTPServer) authenticate(req *http.Request) (int, error) { 30 | if !s.authRequired { 31 | return 0, nil 32 | } 33 | 34 | auth := req.Header.Get(proxyAuthHeaderKey) 35 | if auth == "" { 36 | return http.StatusProxyAuthRequired, errors.New(http.StatusText(http.StatusProxyAuthRequired)) 37 | } 38 | 39 | enc := strings.TrimPrefix(auth, "Basic ") 40 | str, err := base64.StdEncoding.DecodeString(enc) 41 | if err != nil { 42 | return http.StatusNotAcceptable, fmt.Errorf("decode username and password failed: %w", err) 43 | } 44 | pairs := bytes.SplitN(str, []byte(":"), 2) 45 | if len(pairs) != 2 { 46 | return http.StatusLengthRequired, fmt.Errorf("username and password format invalid") 47 | } 48 | if s.auth.Valid(string(pairs[0]), string(pairs[1])) { 49 | return 0, nil 50 | } 51 | return http.StatusUnauthorized, fmt.Errorf("username and password not matching") 52 | } 53 | 54 | func (s *HTTPServer) handleConn(req *http.Request, conn net.Conn) (peer net.Conn, err error) { 55 | addr := req.Host 56 | if !strings.Contains(addr, ":") { 57 | port := "443" 58 | addr = net.JoinHostPort(addr, port) 59 | } 60 | 61 | peer, err = s.dial("tcp", addr) 62 | if err != nil { 63 | return peer, fmt.Errorf("tun tcp dial failed: %w", err) 64 | } 65 | 66 | _, err = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) 67 | if err != nil { 68 | _ = peer.Close() 69 | peer = nil 70 | } 71 | 72 | return 73 | } 74 | 75 | func (s *HTTPServer) handle(req *http.Request) (peer net.Conn, err error) { 76 | addr := req.Host 77 | if !strings.Contains(addr, ":") { 78 | port := "80" 79 | addr = net.JoinHostPort(addr, port) 80 | } 81 | 82 | peer, err = s.dial("tcp", addr) 83 | if err != nil { 84 | return peer, fmt.Errorf("tun tcp dial failed: %w", err) 85 | } 86 | 87 | err = req.Write(peer) 88 | if err != nil { 89 | _ = peer.Close() 90 | peer = nil 91 | return peer, fmt.Errorf("conn write failed: %w", err) 92 | } 93 | 94 | return 95 | } 96 | 97 | func (s *HTTPServer) serve(conn net.Conn) { 98 | var rd = bufio.NewReader(conn) 99 | req, err := http.ReadRequest(rd) 100 | if err != nil { 101 | log.Printf("read request failed: %s\n", err) 102 | return 103 | } 104 | 105 | code, err := s.authenticate(req) 106 | if err != nil { 107 | resp := responseWith(req, code) 108 | if code == http.StatusProxyAuthRequired { 109 | resp.Header.Set("Proxy-Authenticate", "Basic realm=\"Proxy\"") 110 | } 111 | _ = resp.Write(conn) 112 | log.Println(err) 113 | return 114 | } 115 | 116 | var peer net.Conn 117 | switch req.Method { 118 | case http.MethodConnect: 119 | peer, err = s.handleConn(req, conn) 120 | case http.MethodGet: 121 | peer, err = s.handle(req) 122 | default: 123 | _ = responseWith(req, http.StatusMethodNotAllowed).Write(conn) 124 | log.Printf("unsupported protocol: %s\n", req.Method) 125 | return 126 | } 127 | if err != nil { 128 | log.Printf("dial proxy failed: %s\n", err) 129 | return 130 | } 131 | if peer == nil { 132 | log.Println("dial proxy failed: peer nil") 133 | return 134 | } 135 | go func() { 136 | wg := conc.NewWaitGroup() 137 | wg.Go(func() { 138 | _, err = io.Copy(conn, peer) 139 | _ = conn.Close() 140 | }) 141 | wg.Go(func() { 142 | _, err = io.Copy(peer, conn) 143 | _ = peer.Close() 144 | }) 145 | wg.Wait() 146 | }() 147 | } 148 | 149 | // ListenAndServe is used to create a listener and serve on it 150 | func (s *HTTPServer) ListenAndServe(network, addr string) error { 151 | server, err := net.Listen(network, addr) 152 | if err != nil { 153 | return fmt.Errorf("listen tcp failed: %w", err) 154 | } 155 | defer func(server net.Listener) { 156 | _ = server.Close() 157 | }(server) 158 | for { 159 | conn, err := server.Accept() 160 | if err != nil { 161 | return fmt.Errorf("accept request failed: %w", err) 162 | } 163 | go func(conn net.Conn) { 164 | s.serve(conn) 165 | }(conn) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /net.go: -------------------------------------------------------------------------------- 1 | // will delete when upgrading to go 1.18 2 | 3 | package wireproxy 4 | 5 | import ( 6 | "net" 7 | "net/netip" 8 | ) 9 | 10 | func TCPAddrFromAddrPort(addr netip.AddrPort) *net.TCPAddr { 11 | return &net.TCPAddr{ 12 | IP: addr.Addr().AsSlice(), 13 | Zone: addr.Addr().Zone(), 14 | Port: int(addr.Port()), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /routine.go: -------------------------------------------------------------------------------- 1 | package wireproxy 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | srand "crypto/rand" 7 | "crypto/subtle" 8 | "encoding/binary" 9 | "encoding/json" 10 | "errors" 11 | "github.com/amnezia-vpn/amneziawg-go/device" 12 | "golang.org/x/net/icmp" 13 | "golang.org/x/net/ipv4" 14 | "golang.org/x/net/ipv6" 15 | "io" 16 | "log" 17 | "math/rand" 18 | "net" 19 | "net/http" 20 | "os" 21 | "path" 22 | "strconv" 23 | "strings" 24 | "time" 25 | 26 | "github.com/sourcegraph/conc" 27 | "github.com/things-go/go-socks5" 28 | "github.com/things-go/go-socks5/bufferpool" 29 | 30 | "net/netip" 31 | 32 | "github.com/amnezia-vpn/amneziawg-go/tun/netstack" 33 | ) 34 | 35 | // errorLogger is the logger to print error message 36 | var errorLogger = log.New(os.Stderr, "ERROR: ", log.LstdFlags) 37 | 38 | // CredentialValidator stores the authentication data of a socks5 proxy 39 | type CredentialValidator struct { 40 | username string 41 | password string 42 | } 43 | 44 | // VirtualTun stores a reference to netstack network and DNS configuration 45 | type VirtualTun struct { 46 | Tnet *netstack.Net 47 | Dev *device.Device 48 | SystemDNS bool 49 | Conf *DeviceConfig 50 | // PingRecord stores the last time an IP was pinged 51 | PingRecord map[string]uint64 52 | } 53 | 54 | // RoutineSpawner spawns a routine (e.g. socks5, tcp static routes) after the configuration is parsed 55 | type RoutineSpawner interface { 56 | SpawnRoutine(vt *VirtualTun) 57 | } 58 | 59 | type addressPort struct { 60 | address string 61 | port uint16 62 | } 63 | 64 | // LookupAddr lookups a hostname. 65 | // DNS traffic may or may not be routed depending on VirtualTun's setting 66 | func (d VirtualTun) LookupAddr(ctx context.Context, name string) ([]string, error) { 67 | if d.SystemDNS { 68 | return net.DefaultResolver.LookupHost(ctx, name) 69 | } 70 | return d.Tnet.LookupContextHost(ctx, name) 71 | } 72 | 73 | // ResolveAddrWithContext resolves a hostname and returns an AddrPort. 74 | // DNS traffic may or may not be routed depending on VirtualTun's setting 75 | func (d VirtualTun) ResolveAddrWithContext(ctx context.Context, name string) (*netip.Addr, error) { 76 | addrs, err := d.LookupAddr(ctx, name) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | size := len(addrs) 82 | if size == 0 { 83 | return nil, errors.New("no address found for: " + name) 84 | } 85 | 86 | rand.Shuffle(size, func(i, j int) { 87 | addrs[i], addrs[j] = addrs[j], addrs[i] 88 | }) 89 | 90 | var addr netip.Addr 91 | for _, saddr := range addrs { 92 | addr, err = netip.ParseAddr(saddr) 93 | if err == nil { 94 | break 95 | } 96 | } 97 | 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | return &addr, nil 103 | } 104 | 105 | // Resolve resolves a hostname and returns an IP. 106 | // DNS traffic may or may not be routed depending on VirtualTun's setting 107 | func (d VirtualTun) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { 108 | addr, err := d.ResolveAddrWithContext(ctx, name) 109 | if err != nil { 110 | return nil, nil, err 111 | } 112 | 113 | return ctx, addr.AsSlice(), nil 114 | } 115 | 116 | func parseAddressPort(endpoint string) (*addressPort, error) { 117 | name, sport, err := net.SplitHostPort(endpoint) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | port, err := strconv.Atoi(sport) 123 | if err != nil || port < 0 || port > 65535 { 124 | return nil, &net.OpError{Op: "dial", Err: errors.New("port must be numeric")} 125 | } 126 | 127 | return &addressPort{address: name, port: uint16(port)}, nil 128 | } 129 | 130 | func (d VirtualTun) resolveToAddrPort(endpoint *addressPort) (*netip.AddrPort, error) { 131 | addr, err := d.ResolveAddrWithContext(context.Background(), endpoint.address) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | addrPort := netip.AddrPortFrom(*addr, endpoint.port) 137 | return &addrPort, nil 138 | } 139 | 140 | // SpawnRoutine spawns a socks5 server. 141 | func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) { 142 | var authMethods []socks5.Authenticator 143 | if username := config.Username; username != "" { 144 | authMethods = append(authMethods, socks5.UserPassAuthenticator{ 145 | Credentials: socks5.StaticCredentials{username: config.Password}, 146 | }) 147 | } else { 148 | authMethods = append(authMethods, socks5.NoAuthAuthenticator{}) 149 | } 150 | 151 | options := []socks5.Option{ 152 | socks5.WithDial(vt.Tnet.DialContext), 153 | socks5.WithResolver(vt), 154 | socks5.WithAuthMethods(authMethods), 155 | socks5.WithBufferPool(bufferpool.NewPool(256 * 1024)), 156 | } 157 | 158 | server := socks5.NewServer(options...) 159 | 160 | if err := server.ListenAndServe("tcp", config.BindAddress); err != nil { 161 | log.Fatal(err) 162 | } 163 | } 164 | 165 | // SpawnRoutine spawns a http server. 166 | func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) { 167 | server := &HTTPServer{ 168 | config: config, 169 | dial: vt.Tnet.Dial, 170 | auth: CredentialValidator{config.Username, config.Password}, 171 | } 172 | if config.Username != "" || config.Password != "" { 173 | server.authRequired = true 174 | } 175 | 176 | if err := server.ListenAndServe("tcp", config.BindAddress); err != nil { 177 | log.Fatal(err) 178 | } 179 | } 180 | 181 | // Valid checks the authentication data in CredentialValidator and compare them 182 | // to username and password in constant time. 183 | func (c CredentialValidator) Valid(username, password string) bool { 184 | u := subtle.ConstantTimeCompare([]byte(c.username), []byte(username)) 185 | p := subtle.ConstantTimeCompare([]byte(c.password), []byte(password)) 186 | return u&p == 1 187 | } 188 | 189 | // connForward copy data from `from` to `to` 190 | func connForward(from io.ReadWriteCloser, to io.ReadWriteCloser) { 191 | _, err := io.Copy(to, from) 192 | if err != nil { 193 | errorLogger.Printf("Cannot forward traffic: %s\n", err.Error()) 194 | } 195 | } 196 | 197 | // tcpClientForward starts a new connection via wireguard and forward traffic from `conn` 198 | func tcpClientForward(vt *VirtualTun, raddr *addressPort, conn net.Conn) { 199 | target, err := vt.resolveToAddrPort(raddr) 200 | if err != nil { 201 | errorLogger.Printf("TCP Server Tunnel to %s: %s\n", target, err.Error()) 202 | return 203 | } 204 | 205 | tcpAddr := TCPAddrFromAddrPort(*target) 206 | 207 | sconn, err := vt.Tnet.DialTCP(tcpAddr) 208 | if err != nil { 209 | errorLogger.Printf("TCP Client Tunnel to %s: %s\n", target, err.Error()) 210 | return 211 | } 212 | 213 | go func() { 214 | wg := conc.NewWaitGroup() 215 | wg.Go(func() { 216 | connForward(sconn, conn) 217 | }) 218 | wg.Go(func() { 219 | connForward(conn, sconn) 220 | }) 221 | wg.Wait() 222 | _ = sconn.Close() 223 | _ = conn.Close() 224 | sconn = nil 225 | conn = nil 226 | }() 227 | } 228 | 229 | // STDIOTcpForward starts a new connection via wireguard and forward traffic from `conn` 230 | func STDIOTcpForward(vt *VirtualTun, raddr *addressPort) { 231 | target, err := vt.resolveToAddrPort(raddr) 232 | if err != nil { 233 | errorLogger.Printf("Name resolution error for %s: %s\n", raddr.address, err.Error()) 234 | return 235 | } 236 | 237 | // os.Stdout has previously been remapped to stderr, se we can't use it 238 | stdout, err := os.OpenFile("/dev/stdout", os.O_WRONLY, 0) 239 | if err != nil { 240 | errorLogger.Printf("Failed to open /dev/stdout: %s\n", err.Error()) 241 | return 242 | } 243 | 244 | tcpAddr := TCPAddrFromAddrPort(*target) 245 | sconn, err := vt.Tnet.DialTCP(tcpAddr) 246 | if err != nil { 247 | errorLogger.Printf("TCP Client Tunnel to %s (%s): %s\n", target, tcpAddr, err.Error()) 248 | return 249 | } 250 | 251 | go func() { 252 | wg := conc.NewWaitGroup() 253 | wg.Go(func() { 254 | connForward(os.Stdin, sconn) 255 | }) 256 | wg.Go(func() { 257 | connForward(sconn, stdout) 258 | }) 259 | wg.Wait() 260 | _ = sconn.Close() 261 | sconn = nil 262 | }() 263 | } 264 | 265 | // SpawnRoutine spawns a local TCP server which acts as a proxy to the specified target 266 | func (conf *TCPClientTunnelConfig) SpawnRoutine(vt *VirtualTun) { 267 | raddr, err := parseAddressPort(conf.Target) 268 | if err != nil { 269 | log.Fatal(err) 270 | } 271 | 272 | server, err := net.ListenTCP("tcp", conf.BindAddress) 273 | if err != nil { 274 | log.Fatal(err) 275 | } 276 | 277 | for { 278 | conn, err := server.Accept() 279 | if err != nil { 280 | log.Fatal(err) 281 | } 282 | go tcpClientForward(vt, raddr, conn) 283 | } 284 | } 285 | 286 | // SpawnRoutine connects to the specified target and plumbs it to STDIN / STDOUT 287 | func (conf *STDIOTunnelConfig) SpawnRoutine(vt *VirtualTun) { 288 | raddr, err := parseAddressPort(conf.Target) 289 | if err != nil { 290 | log.Fatal(err) 291 | } 292 | 293 | go STDIOTcpForward(vt, raddr) 294 | } 295 | 296 | // tcpServerForward starts a new connection locally and forward traffic from `conn` 297 | func tcpServerForward(vt *VirtualTun, raddr *addressPort, conn net.Conn) { 298 | target, err := vt.resolveToAddrPort(raddr) 299 | if err != nil { 300 | errorLogger.Printf("TCP Server Tunnel to %s: %s\n", target, err.Error()) 301 | return 302 | } 303 | 304 | tcpAddr := TCPAddrFromAddrPort(*target) 305 | 306 | sconn, err := net.DialTCP("tcp", nil, tcpAddr) 307 | if err != nil { 308 | errorLogger.Printf("TCP Server Tunnel to %s: %s\n", target, err.Error()) 309 | return 310 | } 311 | 312 | go func() { 313 | gr := conc.NewWaitGroup() 314 | gr.Go(func() { 315 | connForward(sconn, conn) 316 | }) 317 | gr.Go(func() { 318 | connForward(conn, sconn) 319 | }) 320 | gr.Wait() 321 | _ = sconn.Close() 322 | _ = conn.Close() 323 | sconn = nil 324 | conn = nil 325 | }() 326 | } 327 | 328 | // SpawnRoutine spawns a TCP server on wireguard which acts as a proxy to the specified target 329 | func (conf *TCPServerTunnelConfig) SpawnRoutine(vt *VirtualTun) { 330 | raddr, err := parseAddressPort(conf.Target) 331 | if err != nil { 332 | log.Fatal(err) 333 | } 334 | 335 | addr := &net.TCPAddr{Port: conf.ListenPort} 336 | server, err := vt.Tnet.ListenTCP(addr) 337 | if err != nil { 338 | log.Fatal(err) 339 | } 340 | 341 | for { 342 | conn, err := server.Accept() 343 | if err != nil { 344 | log.Fatal(err) 345 | } 346 | go tcpServerForward(vt, raddr, conn) 347 | } 348 | } 349 | 350 | func (d VirtualTun) ServeHTTP(w http.ResponseWriter, r *http.Request) { 351 | log.Printf("Health metric request: %s\n", r.URL.Path) 352 | switch path.Clean(r.URL.Path) { 353 | case "/readyz": 354 | body, err := json.Marshal(d.PingRecord) 355 | if err != nil { 356 | errorLogger.Printf("Failed to get device metrics: %s\n", err.Error()) 357 | w.WriteHeader(http.StatusInternalServerError) 358 | return 359 | } 360 | 361 | status := http.StatusOK 362 | for _, record := range d.PingRecord { 363 | lastPong := time.Unix(int64(record), 0) 364 | // +2 seconds to account for the time it takes to ping the IP 365 | if time.Since(lastPong) > time.Duration(d.Conf.CheckAliveInterval+2)*time.Second { 366 | status = http.StatusServiceUnavailable 367 | break 368 | } 369 | } 370 | 371 | w.WriteHeader(status) 372 | _, _ = w.Write(body) 373 | _, _ = w.Write([]byte("\n")) 374 | case "/metrics": 375 | get, err := d.Dev.IpcGet() 376 | if err != nil { 377 | errorLogger.Printf("Failed to get device metrics: %s\n", err.Error()) 378 | w.WriteHeader(http.StatusInternalServerError) 379 | return 380 | } 381 | var buf bytes.Buffer 382 | for _, peer := range strings.Split(get, "\n") { 383 | pair := strings.SplitN(peer, "=", 2) 384 | if len(pair) != 2 { 385 | buf.WriteString(peer) 386 | continue 387 | } 388 | if pair[0] == "private_key" || pair[0] == "preshared_key" { 389 | pair[1] = "REDACTED" 390 | } 391 | buf.WriteString(pair[0]) 392 | buf.WriteString("=") 393 | buf.WriteString(pair[1]) 394 | buf.WriteString("\n") 395 | } 396 | 397 | w.WriteHeader(http.StatusOK) 398 | _, _ = w.Write(buf.Bytes()) 399 | default: 400 | w.WriteHeader(http.StatusNotFound) 401 | } 402 | } 403 | 404 | func (d VirtualTun) pingIPs() { 405 | for _, addr := range d.Conf.CheckAlive { 406 | socket, err := d.Tnet.Dial("ping", addr.String()) 407 | if err != nil { 408 | errorLogger.Printf("Failed to ping %s: %s\n", addr, err.Error()) 409 | continue 410 | } 411 | 412 | data := make([]byte, 16) 413 | _, _ = srand.Read(data) 414 | 415 | requestPing := icmp.Echo{ 416 | Seq: rand.Intn(1 << 16), 417 | Data: data, 418 | } 419 | 420 | var icmpBytes []byte 421 | if addr.Is4() { 422 | icmpBytes, _ = (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil) 423 | } else if addr.Is6() { 424 | icmpBytes, _ = (&icmp.Message{Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &requestPing}).Marshal(nil) 425 | } else { 426 | errorLogger.Printf("Failed to ping %s: invalid address: %s\n", addr, addr.String()) 427 | continue 428 | } 429 | 430 | _ = socket.SetReadDeadline(time.Now().Add(time.Duration(d.Conf.CheckAliveInterval) * time.Second)) 431 | _, err = socket.Write(icmpBytes) 432 | if err != nil { 433 | errorLogger.Printf("Failed to ping %s: %s\n", addr, err.Error()) 434 | continue 435 | } 436 | 437 | addr := addr 438 | go func() { 439 | n, err := socket.Read(icmpBytes[:]) 440 | if err != nil { 441 | errorLogger.Printf("Failed to read ping response from %s: %s\n", addr, err.Error()) 442 | return 443 | } 444 | 445 | replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n]) 446 | if err != nil { 447 | errorLogger.Printf("Failed to parse ping response from %s: %s\n", addr, err.Error()) 448 | return 449 | } 450 | 451 | if addr.Is4() { 452 | replyPing, ok := replyPacket.Body.(*icmp.Echo) 453 | if !ok { 454 | errorLogger.Printf("Failed to parse ping response from %s: invalid reply type: %s\n", addr, replyPacket.Type) 455 | return 456 | } 457 | if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq { 458 | errorLogger.Printf("Failed to parse ping response from %s: invalid ping reply: %v\n", addr, replyPing) 459 | return 460 | } 461 | } 462 | 463 | if addr.Is6() { 464 | replyPing, ok := replyPacket.Body.(*icmp.RawBody) 465 | if !ok { 466 | errorLogger.Printf("Failed to parse ping response from %s: invalid reply type: %s\n", addr, replyPacket.Type) 467 | return 468 | } 469 | 470 | seq := binary.BigEndian.Uint16(replyPing.Data[2:4]) 471 | pongBody := replyPing.Data[4:] 472 | if !bytes.Equal(pongBody, requestPing.Data) || int(seq) != requestPing.Seq { 473 | errorLogger.Printf("Failed to parse ping response from %s: invalid ping reply: %v\n", addr, replyPing) 474 | return 475 | } 476 | } 477 | 478 | d.PingRecord[addr.String()] = uint64(time.Now().Unix()) 479 | 480 | defer socket.Close() 481 | }() 482 | } 483 | } 484 | 485 | func (d VirtualTun) StartPingIPs() { 486 | for _, addr := range d.Conf.CheckAlive { 487 | d.PingRecord[addr.String()] = 0 488 | } 489 | 490 | go func() { 491 | for { 492 | d.pingIPs() 493 | time.Sleep(time.Duration(d.Conf.CheckAliveInterval) * time.Second) 494 | } 495 | }() 496 | } 497 | -------------------------------------------------------------------------------- /systemd/README.md: -------------------------------------------------------------------------------- 1 | # Running wireproxy with systemd 2 | 3 | If you're on a systemd-based distro, you'll most likely want to run Wireproxy as a systemd unit. 4 | 5 | The provided systemd unit assumes you have the wireproxy executable installed on `/opt/wireproxy/wireproxy` and a configuration file stored at `/etc/wireproxy.conf`. These paths can be customized by editing the unit file. 6 | 7 | # Setting up the unit 8 | 9 | 1. Copy the `wireproxy.service` file from this directory to `/etc/systemd/system/`, or use the following cURL command to download it: 10 | ```bash 11 | sudo curl https://raw.githubusercontent.com/artem-russkikh/wireproxy-awg/master/systemd/wireproxy.service > /etc/systemd/system/wireproxy.service 12 | ``` 13 | 14 | 2. If necessary, customize the unit. 15 | 16 | Edit the parts with `LoadCredential`, `ExecStartPre=` and `ExecStart=` to point to the executable and the configuration file. For example, if wireproxy is installed on `/usr/bin` and the configuration file is located in `/opt/myfiles/wireproxy.conf` do the following change: 17 | ```service 18 | LoadCredential=conf:/opt/myfiles/wireproxy.conf 19 | ExecStartPre=/usr/bin/wireproxy -n -c ${CREDENTIALS_DIRECTORY}/conf 20 | ExecStart=/usr/bin/wireproxy -c ${CREDENTIALS_DIRECTORY}/conf 21 | ``` 22 | 23 | 4. Reload systemd and enable the unit. 24 | ```bash 25 | sudo systemctl daemon-reload 26 | sudo systemctl enable --now wireproxy.service 27 | ``` 28 | 29 | 5. Make sure it's working correctly. 30 | 31 | Finally, check out the unit status to confirm `wireproxy.service` has started without problems. You can use commands like `systemctl status wireproxy.service` and/or `sudo journalctl -u wireproxy.service`. 32 | 33 | # Additional notes 34 | 35 | If you want to disable the extensive logging that's done by Wireproxy, simply add `-s` parameter to `ExecStart=`. This will enable the silent mode that was implemented with [pull/67](https://github.com/pufferffish/wireproxy/pull/67). 36 | -------------------------------------------------------------------------------- /systemd/wireproxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Wireproxy socks5/http tunnel 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | User=wireproxy 8 | Group=wireproxy 9 | SyslogIdentifier=wireproxy 10 | Type=simple 11 | Restart=on-failure 12 | RestartSec=30s 13 | 14 | DynamicUser=yes 15 | LoadCredential=conf:/etc/wireproxy.conf 16 | ExecStartPre=/opt/wireproxy/wireproxy -n -c ${CREDENTIALS_DIRECTORY}/conf 17 | ExecStart=/opt/wireproxy/wireproxy -c ${CREDENTIALS_DIRECTORY}/conf 18 | 19 | # Required if <1024 port 20 | #AmbientCapabilities=CAP_NET_BIND_SERVICE 21 | #CapabilityBoundingSet=CAP_NET_BIND_SERVICE 22 | LimitNPROC=64 23 | LockPersonality=true 24 | MemoryDenyWriteExecute=true 25 | NoNewPrivileges=true 26 | PrivateDevices=true 27 | PrivateTmp=true 28 | PrivateUsers=true 29 | ProcSubset=pid 30 | ProtectClock=true 31 | ProtectControlGroups=true 32 | ProtectHome=true 33 | ProtectHostname=true 34 | ProtectKernelLogs=true 35 | ProtectKernelModules=true 36 | ProtectKernelTunables=true 37 | ProtectProc=invisible 38 | ProtectSystem=strict 39 | RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK 40 | RestrictNamespaces=true 41 | RestrictRealtime=true 42 | SystemCallArchitectures=native 43 | SystemCallFilter=@system-service @sandbox 44 | 45 | [Install] 46 | WantedBy=multi-user.target 47 | -------------------------------------------------------------------------------- /test_config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | exec 3<>/dev/tcp/demo.wireguard.com/42912 4 | privatekey="$(wg genkey)" 5 | wg pubkey <<<"$privatekey" >&3 6 | IFS=: read -r status server_pubkey server_port internal_ip <&3 7 | [[ $status == OK ]] 8 | cat >test.conf < 0 { 67 | for _, ip := range peer.AllowedIPs { 68 | request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip.String())) 69 | } 70 | } else { 71 | request.WriteString(heredoc.Doc(` 72 | allowed_ip=0.0.0.0/0 73 | allowed_ip=::0/0 74 | `)) 75 | } 76 | } 77 | 78 | setting := &DeviceSetting{IpcRequest: request.String(), DNS: conf.DNS, DeviceAddr: conf.Endpoint, MTU: conf.MTU} 79 | return setting, nil 80 | } 81 | 82 | // StartWireguard creates a tun interface on netstack given a configuration 83 | func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) { 84 | setting, err := CreateIPCRequest(conf) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | tun, tnet, err := netstack.CreateNetTUN(setting.DeviceAddr, setting.DNS, setting.MTU) 90 | if err != nil { 91 | return nil, err 92 | } 93 | dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(logLevel, "")) 94 | err = dev.IpcSet(setting.IpcRequest) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | err = dev.Up() 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return &VirtualTun{ 105 | Tnet: tnet, 106 | Dev: dev, 107 | Conf: conf, 108 | SystemDNS: len(setting.DNS) == 0, 109 | PingRecord: make(map[string]uint64), 110 | }, nil 111 | } 112 | --------------------------------------------------------------------------------