mirror of
https://github.com/whyvl/wireproxy.git
synced 2025-06-28 17:08:01 +02:00
Compare commits
No commits in common. "v1.0.0" and "master" have entirely different histories.
32 changed files with 1831 additions and 1295 deletions
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.dockerignore
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
LICENSE
|
||||||
|
README.md
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
liberapay: octeep
|
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
|
@ -6,17 +6,18 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
windowsAmd64Build:
|
windowsAmd64Build:
|
||||||
name: Build Windows amd64 Version
|
name: Build Windows amd64 Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Windows amd64 Version
|
- name: Building Windows amd64 Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o WireProxy_amd64.exe -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o WireProxy_amd64.exe -v ./cmd/wireproxy
|
||||||
|
@ -24,7 +25,7 @@ jobs:
|
||||||
mv WireProxy_amd64.exe wireproxy.exe
|
mv WireProxy_amd64.exe wireproxy.exe
|
||||||
cp wireproxy.exe release_windows_amd64/wireproxy.exe
|
cp wireproxy.exe release_windows_amd64/wireproxy.exe
|
||||||
- name: Upload Windows amd64 Version
|
- name: Upload Windows amd64 Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_windows_amd64
|
name: WireProxy_windows_amd64
|
||||||
path: release_windows_amd64
|
path: release_windows_amd64
|
||||||
|
@ -32,11 +33,11 @@ jobs:
|
||||||
name: Build Windows arm64 Version
|
name: Build Windows arm64 Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Windows arm64 Version
|
- name: Building Windows arm64 Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o WireProxy_arm64.exe -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o WireProxy_arm64.exe -v ./cmd/wireproxy
|
||||||
|
@ -44,7 +45,7 @@ jobs:
|
||||||
mv WireProxy_arm64.exe wireproxy.exe
|
mv WireProxy_arm64.exe wireproxy.exe
|
||||||
cp wireproxy.exe release_windows_arm64/wireproxy.exe
|
cp wireproxy.exe release_windows_arm64/wireproxy.exe
|
||||||
- name: Upload Windows arm64 Version
|
- name: Upload Windows arm64 Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_windows_arm64
|
name: WireProxy_windows_arm64
|
||||||
path: release_windows_arm64
|
path: release_windows_arm64
|
||||||
|
@ -52,11 +53,11 @@ jobs:
|
||||||
name: Build Linux amd64 Version
|
name: Build Linux amd64 Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Linux amd64 Version
|
- name: Building Linux amd64 Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o WireProxy_amd64 -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o WireProxy_amd64 -v ./cmd/wireproxy
|
||||||
|
@ -64,7 +65,7 @@ jobs:
|
||||||
mv WireProxy_amd64 wireproxy
|
mv WireProxy_amd64 wireproxy
|
||||||
cp wireproxy release_linux_amd64/wireproxy
|
cp wireproxy release_linux_amd64/wireproxy
|
||||||
- name: Upload Linux amd64 Version
|
- name: Upload Linux amd64 Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_linux_amd64
|
name: WireProxy_linux_amd64
|
||||||
path: release_linux_amd64
|
path: release_linux_amd64
|
||||||
|
@ -72,11 +73,11 @@ jobs:
|
||||||
name: Build Linux arm64 Version
|
name: Build Linux arm64 Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Linux arm64 Version
|
- name: Building Linux arm64 Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o WireProxy_arm64 -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o WireProxy_arm64 -v ./cmd/wireproxy
|
||||||
|
@ -84,7 +85,7 @@ jobs:
|
||||||
mv WireProxy_arm64 wireproxy
|
mv WireProxy_arm64 wireproxy
|
||||||
cp wireproxy release_linux_arm64/wireproxy
|
cp wireproxy release_linux_arm64/wireproxy
|
||||||
- name: Upload Linux arm64 Version
|
- name: Upload Linux arm64 Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_linux_arm64
|
name: WireProxy_linux_arm64
|
||||||
path: release_linux_arm64
|
path: release_linux_arm64
|
||||||
|
@ -92,11 +93,11 @@ jobs:
|
||||||
name: Build Linux s390x Version
|
name: Build Linux s390x Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Linux s390x Version
|
- name: Building Linux s390x Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=s390x go build -o WireProxy_s390x -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=linux GOARCH=s390x go build -o WireProxy_s390x -v ./cmd/wireproxy
|
||||||
|
@ -104,7 +105,7 @@ jobs:
|
||||||
mv WireProxy_s390x wireproxy
|
mv WireProxy_s390x wireproxy
|
||||||
cp wireproxy release_linux_s390x/wireproxy
|
cp wireproxy release_linux_s390x/wireproxy
|
||||||
- name: Upload Linux s390x Version
|
- name: Upload Linux s390x Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_linux_s390x
|
name: WireProxy_linux_s390x
|
||||||
path: release_linux_s390x
|
path: release_linux_s390x
|
||||||
|
@ -112,11 +113,11 @@ jobs:
|
||||||
name: Build Darwin amd64 Version
|
name: Build Darwin amd64 Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Darwin amd64 Version
|
- name: Building Darwin amd64 Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o WireProxy_amd64 -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o WireProxy_amd64 -v ./cmd/wireproxy
|
||||||
|
@ -124,7 +125,7 @@ jobs:
|
||||||
mv WireProxy_amd64 wireproxy
|
mv WireProxy_amd64 wireproxy
|
||||||
cp wireproxy release_darwin_amd64/wireproxy
|
cp wireproxy release_darwin_amd64/wireproxy
|
||||||
- name: Upload Darwin amd64 Version
|
- name: Upload Darwin amd64 Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_darwin_amd64
|
name: WireProxy_darwin_amd64
|
||||||
path: release_darwin_amd64
|
path: release_darwin_amd64
|
||||||
|
@ -132,11 +133,11 @@ jobs:
|
||||||
name: Build Darwin arm64 Version
|
name: Build Darwin arm64 Version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setting up Go
|
- name: Setting up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: "1.21"
|
||||||
- name: Building Darwin arm64 Version
|
- name: Building Darwin arm64 Version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o WireProxy_arm64 -v ./cmd/wireproxy
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o WireProxy_arm64 -v ./cmd/wireproxy
|
||||||
|
@ -144,7 +145,7 @@ jobs:
|
||||||
mv WireProxy_arm64 wireproxy
|
mv WireProxy_arm64 wireproxy
|
||||||
cp wireproxy release_darwin_arm64/wireproxy
|
cp wireproxy release_darwin_arm64/wireproxy
|
||||||
- name: Upload Darwin arm64 Version
|
- name: Upload Darwin arm64 Version
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: WireProxy_darwin_arm64
|
name: WireProxy_darwin_arm64
|
||||||
path: release_darwin_arm64
|
path: release_darwin_arm64
|
||||||
|
|
72
.github/workflows/container.yml
vendored
Normal file
72
.github/workflows/container.yml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
name: Build container
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
# Allow for manually running
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
container_tag:
|
||||||
|
description: Tag for container
|
||||||
|
default: "latest"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
container:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
|
env:
|
||||||
|
CONTAINER_NAME: ghcr.io/${{ github.repository }}
|
||||||
|
BUILD_PLATFORMS: linux/amd64,linux/arm,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
|
RAW_CONTAINER_TAG: ${{ github.event.inputs.container_tag || github.event.pull_request.head.ref || 'latest' }}
|
||||||
|
RAW_REF_NAME: ${{ github.event.pull_request.head.ref || github.ref }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
# Needed for buildx gha cache to work
|
||||||
|
- name: Expose GitHub Runtime
|
||||||
|
uses: crazy-max/ghaction-github-runtime@v3
|
||||||
|
|
||||||
|
- name: Build container
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
CONTAINER_TAG=$(echo "$RAW_CONTAINER_TAG" | sed 's/[^a-zA-Z0-9]\+/-/')
|
||||||
|
REF_NAME=$(echo "$RAW_REF_NAME" | sed -r 's#^refs/(heads|tags)/##')
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--platform "$BUILD_PLATFORMS" \
|
||||||
|
--tag "$CONTAINER_NAME:$CONTAINER_TAG" \
|
||||||
|
--tag "$CONTAINER_NAME:$GITHUB_SHA" \
|
||||||
|
--label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \
|
||||||
|
--label "org.opencontainers.image.documentation=${{ github.server_url }}/${{ github.repository }}" \
|
||||||
|
--label "org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}/packages" \
|
||||||
|
--label "org.opencontainers.image.ref.name=$REF_NAME" \
|
||||||
|
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||||
|
--label "org.opencontainers.image.vendor=${{ github.repository_owner }}" \
|
||||||
|
--label "org.opencontainers.image.created=$(date -u --rfc-3339=seconds)" \
|
||||||
|
--cache-from type=gha \
|
||||||
|
--cache-to type=gha,mode=max \
|
||||||
|
--pull ${{ github.event_name == 'push' && '--push' || '' }} .
|
12
.github/workflows/golangci-lint.yml
vendored
12
.github/workflows/golangci-lint.yml
vendored
|
@ -6,6 +6,8 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -13,7 +15,11 @@ jobs:
|
||||||
name: lint
|
name: lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
- uses: actions/checkout@v2
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v4
|
||||||
|
with:
|
||||||
|
version: latest
|
41
.github/workflows/test.yml
vendored
Normal file
41
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
name: Test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test wireproxy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setting up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.21"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt install wireguard curl
|
||||||
|
- name: Building wireproxy
|
||||||
|
run: |
|
||||||
|
git tag dev
|
||||||
|
make
|
||||||
|
- name: Generate test config
|
||||||
|
run: ./test_config.sh
|
||||||
|
- name: Start wireproxy
|
||||||
|
run: ./wireproxy -c test.conf & sleep 1
|
||||||
|
- name: Test socks5
|
||||||
|
run: curl --proxy socks5://localhost:64423 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
|
||||||
|
- name: Test http
|
||||||
|
run: curl --proxy http://localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
|
||||||
|
- name: Test http with password
|
||||||
|
run: curl --proxy http://peter:hunter123@localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
|
||||||
|
- name: Test http with wrong password
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
curl -s --fail --proxy http://peter:wrongpass@localhost:64425 http://zx2c4.com/ip
|
||||||
|
if [[ $? == 0 ]]; then exit 1; fi
|
27
.github/workflows/wireproxy.yml
vendored
27
.github/workflows/wireproxy.yml
vendored
|
@ -1,17 +1,20 @@
|
||||||
name: Cross compile WirePorxy
|
name: Cross compile WireProxy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
create:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
WirePorxy:
|
WireProxy:
|
||||||
|
|
||||||
name: Cross compile WirePorxy
|
name: Cross compile WireProxy
|
||||||
|
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
env:
|
env:
|
||||||
workdir: ./WirePorxy
|
workdir: ./WireProxy
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
@ -19,23 +22,25 @@ jobs:
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Git clone WirePorxy
|
- name: Git clone WireProxy
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/octeep/wireproxy.git ${{ env.workdir }}
|
git clone https://github.com/pufferffish/wireproxy.git ${{ env.workdir }}
|
||||||
cp ./.github/wireproxy-releaser.yml ${{ env.workdir }}/.goreleaser.yml
|
cp ./.github/wireproxy-releaser.yml ${{ env.workdir }}/.goreleaser.yml
|
||||||
|
|
||||||
- name: Set up GoReleaser
|
- name: Set up GoReleaser
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.17"
|
go-version: "1.21"
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v5
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
workdir: ${{ env.workdir }}
|
workdir: ${{ env.workdir }}
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Release binaries
|
- name: Release binaries
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
@ -43,4 +48,4 @@ jobs:
|
||||||
tag_name: wireproxy
|
tag_name: wireproxy
|
||||||
files: ${{ env.workdir }}/dist/*.tar.gz
|
files: ${{ env.workdir }}/dist/*.tar.gz
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
||||||
/main
|
/main
|
||||||
/wireproxy
|
/wireproxy
|
||||||
*.sw?
|
*.sw?
|
||||||
|
/.idea
|
||||||
|
.goreleaser.yml
|
||||||
|
*.conf
|
||||||
|
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Start by building the application.
|
||||||
|
FROM docker.io/golang:1.21 as build
|
||||||
|
|
||||||
|
WORKDIR /usr/src/wireproxy
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN make
|
||||||
|
|
||||||
|
# Now copy it into our base image.
|
||||||
|
FROM gcr.io/distroless/static-debian11:nonroot
|
||||||
|
COPY --from=build /usr/src/wireproxy/wireproxy /usr/bin/wireproxy
|
||||||
|
|
||||||
|
VOLUME [ "/etc/wireproxy"]
|
||||||
|
ENTRYPOINT [ "/usr/bin/wireproxy" ]
|
||||||
|
CMD [ "--config", "/etc/wireproxy/config" ]
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="wireproxy"
|
||||||
|
LABEL org.opencontainers.image.description="Wireguard client that exposes itself as a socks5 proxy"
|
||||||
|
LABEL org.opencontainers.image.licenses="ISC"
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2022 Wind T.F. Wong <octeep@pm.me>
|
Copyright (c) 2024 Wind Wong <me@windtfw.com>
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
Permission to use, copy, modify, and distribute this software for any
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
|
15
Makefile
Normal file
15
Makefile
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export GO ?= go
|
||||||
|
export CGO_ENABLED = 0
|
||||||
|
|
||||||
|
TAG := $(shell git describe --always --tags $(git rev-list --tags --max-count=1) --match v*)
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: wireproxy
|
||||||
|
|
||||||
|
.PHONY: wireproxy
|
||||||
|
wireproxy:
|
||||||
|
${GO} build -trimpath -ldflags "-s -w -X 'main.version=${TAG}'" ./cmd/wireproxy
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
${RM} wireproxy
|
199
README.md
199
README.md
|
@ -1,13 +1,20 @@
|
||||||
# wireproxy
|
# wireproxy
|
||||||
A wireguard client that exposes itself as a socks5 proxy or tunnels.
|
|
||||||
|
[](./LICENSE)
|
||||||
|
[](https://github.com/octeep/wireproxy/actions)
|
||||||
|
[](https://pkg.go.dev/github.com/octeep/wireproxy)
|
||||||
|
|
||||||
|
A wireguard client that exposes itself as a socks5/http proxy or tunnels.
|
||||||
|
|
||||||
# What is this
|
# What is this
|
||||||
|
|
||||||
`wireproxy` is a completely userspace application that connects to a wireguard peer,
|
`wireproxy` is a completely userspace application that connects to a wireguard peer,
|
||||||
and exposes a socks5 proxy or tunnels on the machine. This can be useful if you need
|
and exposes a socks5/http proxy or tunnels on the machine. This can be useful if you need
|
||||||
to connect to certain sites via a wireguard peer, but can't be bothered to setup a new network
|
to connect to certain sites via a wireguard peer, but can't be bothered to setup a new network
|
||||||
interface for whatever reasons.
|
interface for whatever reasons.
|
||||||
|
|
||||||
# Why you might want this
|
# Why you might want this
|
||||||
|
|
||||||
- You simply want to use wireguard as a way to proxy some traffic.
|
- You simply want to use wireguard as a way to proxy some traffic.
|
||||||
- You don't want root permission just to change wireguard settings.
|
- You don't want root permission just to change wireguard settings.
|
||||||
|
|
||||||
|
@ -16,17 +23,35 @@ and configured my browser to use wireproxy for certain sites. It's pretty useful
|
||||||
wireproxy is completely isolated from my network interfaces, and I don't need root to configure
|
wireproxy is completely isolated from my network interfaces, and I don't need root to configure
|
||||||
anything.
|
anything.
|
||||||
|
|
||||||
|
Users who want something similar but for Amnezia VPN can use [this fork](https://github.com/artem-russkikh/wireproxy-awg)
|
||||||
|
of wireproxy by [@artem-russkikh](https://github.com/artem-russkikh).
|
||||||
|
|
||||||
|
# Sponsor
|
||||||
|
|
||||||
|
This project is supported by [IPRoyal](https://iproyal.com/?r=795836). You can get premium quality proxies at unbeatable prices
|
||||||
|
with a discount using [this referral link](https://iproyal.com/?r=795836)! 🚀
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
# Feature
|
# Feature
|
||||||
|
|
||||||
- TCP static routing for client and server
|
- TCP static routing for client and server
|
||||||
- SOCKS5 proxy (currently only CONNECT is supported)
|
- SOCKS5/HTTP proxy (currently only CONNECT is supported)
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
- UDP Support in SOCKS5
|
||||||
|
- UDP static routing
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
```
|
|
||||||
./wireproxy -c [path to config]
|
```bash
|
||||||
|
./wireproxy [-c path to config]
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```bash
|
||||||
usage: wireproxy [-h|--help] -c|--config "<value>" [-d|--daemon]
|
usage: wireproxy [-h|--help] [-c|--config "<value>"] [-s|--silent]
|
||||||
|
[-d|--daemon] [-i|--info "<value>"] [-v|--version]
|
||||||
[-n|--configtest]
|
[-n|--configtest]
|
||||||
|
|
||||||
Userspace wireguard client for proxying
|
Userspace wireguard client for proxying
|
||||||
|
@ -35,20 +60,36 @@ Arguments:
|
||||||
|
|
||||||
-h --help Print help information
|
-h --help Print help information
|
||||||
-c --config Path of configuration file
|
-c --config Path of configuration file
|
||||||
|
Default paths: /etc/wireproxy/wireproxy.conf, $HOME/.config/wireproxy.conf
|
||||||
|
-s --silent Silent mode
|
||||||
-d --daemon Make wireproxy run in background
|
-d --daemon Make wireproxy run in background
|
||||||
|
-i --info Specify the address and port for exposing health status
|
||||||
|
-v --version Print version
|
||||||
-n --configtest Configtest mode. Only check the configuration file for
|
-n --configtest Configtest mode. Only check the configuration file for
|
||||||
validity.
|
validity.
|
||||||
```
|
```
|
||||||
|
|
||||||
# Build instruction
|
# Build instruction
|
||||||
```
|
|
||||||
|
```bash
|
||||||
git clone https://github.com/octeep/wireproxy
|
git clone https://github.com/octeep/wireproxy
|
||||||
cd wireproxy
|
cd wireproxy
|
||||||
go build ./cmd/wireproxy
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
# Sample config file
|
# Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/pufferffish/wireproxy/cmd/wireproxy@v1.0.9 # or @latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Use with VPN
|
||||||
|
|
||||||
|
Instructions for using wireproxy with Firefox container tabs and auto-start on MacOS can be found [here](/UseWithVPN.md).
|
||||||
|
|
||||||
|
# Sample config file
|
||||||
|
|
||||||
|
```ini
|
||||||
# The [Interface] and [Peer] configurations follow the same semantics and meaning
|
# The [Interface] and [Peer] configurations follow the same semantics and meaning
|
||||||
# of a wg-quick configuration. To understand what these fields mean, please refer to:
|
# of a wg-quick configuration. To understand what these fields mean, please refer to:
|
||||||
# https://wiki.archlinux.org/title/WireGuard#Persistent_configuration
|
# https://wiki.archlinux.org/title/WireGuard#Persistent_configuration
|
||||||
|
@ -57,6 +98,7 @@ go build ./cmd/wireproxy
|
||||||
Address = 10.200.200.2/32 # The subnet should be /32 and /128 for IPv4 and v6 respectively
|
Address = 10.200.200.2/32 # The subnet should be /32 and /128 for IPv4 and v6 respectively
|
||||||
# MTU = 1420 (optional)
|
# MTU = 1420 (optional)
|
||||||
PrivateKey = uCTIK+56CPyCvwJxmU5dBfuyJvPuSXAq1FzHdnIxe1Q=
|
PrivateKey = uCTIK+56CPyCvwJxmU5dBfuyJvPuSXAq1FzHdnIxe1Q=
|
||||||
|
# PrivateKey = $MY_WIREGUARD_PRIVATE_KEY # Alternatively, reference environment variables
|
||||||
DNS = 10.200.200.1
|
DNS = 10.200.200.1
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
|
@ -81,6 +123,16 @@ Target = play.cubecraft.net:25565
|
||||||
ListenPort = 3422
|
ListenPort = 3422
|
||||||
Target = localhost:25545
|
Target = localhost:25545
|
||||||
|
|
||||||
|
# STDIOTunnel is a tunnel connecting the standard input and output of the wireproxy
|
||||||
|
# process to the specified TCP target via wireguard.
|
||||||
|
# This is especially useful to use wireproxy as a ProxyCommand parameter in openssh
|
||||||
|
# For example:
|
||||||
|
# ssh -o ProxyCommand='wireproxy -c myconfig.conf' ssh.myserver.net
|
||||||
|
# Flow:
|
||||||
|
# Piped command -->(wireguard)--> ssh.myserver.net:22
|
||||||
|
[STDIOTunnel]
|
||||||
|
Target = ssh.myserver.net:22
|
||||||
|
|
||||||
# Socks5 creates a socks5 proxy on your LAN, and all traffic would be routed via wireguard.
|
# Socks5 creates a socks5 proxy on your LAN, and all traffic would be routed via wireguard.
|
||||||
[Socks5]
|
[Socks5]
|
||||||
BindAddress = 127.0.0.1:25344
|
BindAddress = 127.0.0.1:25344
|
||||||
|
@ -90,11 +142,22 @@ BindAddress = 127.0.0.1:25344
|
||||||
#Username = ...
|
#Username = ...
|
||||||
# Avoid using spaces in the password field
|
# Avoid using spaces in the password field
|
||||||
#Password = ...
|
#Password = ...
|
||||||
|
|
||||||
|
# http creates a http proxy on your LAN, and all traffic would be routed via wireguard.
|
||||||
|
[http]
|
||||||
|
BindAddress = 127.0.0.1:25345
|
||||||
|
|
||||||
|
# HTTP authentication parameters, specifying username and password enables
|
||||||
|
# proxy authentication.
|
||||||
|
#Username = ...
|
||||||
|
# Avoid using spaces in the password field
|
||||||
|
#Password = ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, if you already have a wireguard config, you can import it in the
|
Alternatively, if you already have a wireguard config, you can import it in the
|
||||||
wireproxy config file like this:
|
wireproxy config file like this:
|
||||||
```
|
|
||||||
|
```ini
|
||||||
WGConfig = <path to the wireguard config>
|
WGConfig = <path to the wireguard config>
|
||||||
|
|
||||||
# Same semantics as above
|
# Same semantics as above
|
||||||
|
@ -108,7 +171,119 @@ WGConfig = <path to the wireguard config>
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Having multiple peers is also supported. `AllowedIPs` would need to be specified
|
||||||
|
such that wireproxy would know which peer to forward to.
|
||||||
|
|
||||||
## Stargazers over time
|
```ini
|
||||||
|
[Interface]
|
||||||
|
Address = 10.254.254.40/32
|
||||||
|
PrivateKey = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
Endpoint = 192.168.0.204:51820
|
||||||
|
PublicKey = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY=
|
||||||
|
AllowedIPs = 10.254.254.100/32
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ=
|
||||||
|
AllowedIPs = 10.254.254.1/32, fdee:1337:c000:d00d::1/128
|
||||||
|
Endpoint = 172.16.0.185:44044
|
||||||
|
PersistentKeepalive = 25
|
||||||
|
|
||||||
|
|
||||||
|
[TCPServerTunnel]
|
||||||
|
ListenPort = 5000
|
||||||
|
Target = service-one.servicenet:5000
|
||||||
|
|
||||||
|
[TCPServerTunnel]
|
||||||
|
ListenPort = 5001
|
||||||
|
Target = service-two.servicenet:5001
|
||||||
|
|
||||||
|
[TCPServerTunnel]
|
||||||
|
ListenPort = 5080
|
||||||
|
Target = service-three.servicenet:80
|
||||||
|
```
|
||||||
|
|
||||||
|
Wireproxy can also allow peers to connect to it:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
ListenPort = 5400
|
||||||
|
...
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY=
|
||||||
|
AllowedIPs = 10.254.254.100/32
|
||||||
|
# Note there is no Endpoint defined here.
|
||||||
|
```
|
||||||
|
|
||||||
|
# Health endpoint
|
||||||
|
|
||||||
|
Wireproxy supports exposing a health endpoint for monitoring purposes.
|
||||||
|
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.
|
||||||
|
|
||||||
|
Currently two endpoints are implemented:
|
||||||
|
|
||||||
|
`/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.
|
||||||
|
|
||||||
|
`/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.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = censored
|
||||||
|
Address = 10.2.0.2/32
|
||||||
|
DNS = 10.2.0.1
|
||||||
|
CheckAlive = 1.1.1.1, 3.3.3.3
|
||||||
|
CheckAliveInterval = 3
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = censored
|
||||||
|
AllowedIPs = 0.0.0.0/0
|
||||||
|
Endpoint = 149.34.244.174:51820
|
||||||
|
|
||||||
|
[Socks5]
|
||||||
|
BindAddress = 127.0.0.1:25344
|
||||||
|
```
|
||||||
|
|
||||||
|
`/readyz` would respond with
|
||||||
|
|
||||||
|
```text
|
||||||
|
< HTTP/1.1 503 Service Unavailable
|
||||||
|
< Date: Thu, 11 Apr 2024 00:54:59 GMT
|
||||||
|
< Content-Length: 35
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{"1.1.1.1":1712796899,"3.3.3.3":0}
|
||||||
|
```
|
||||||
|
|
||||||
|
And for:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = censored
|
||||||
|
Address = 10.2.0.2/32
|
||||||
|
DNS = 10.2.0.1
|
||||||
|
CheckAlive = 1.1.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
`/readyz` would respond with
|
||||||
|
|
||||||
|
```text
|
||||||
|
< HTTP/1.1 200 OK
|
||||||
|
< Date: Thu, 11 Apr 2024 00:56:21 GMT
|
||||||
|
< Content-Length: 23
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{"1.1.1.1":1712796979}
|
||||||
|
```
|
||||||
|
|
||||||
|
If nothing is set for `CheckAlive`, an empty JSON object with 200 will be the response.
|
||||||
|
|
||||||
|
The peer which the ICMP ping packet is routed to depends on the `AllowedIPs` set for each peers.
|
||||||
|
|
||||||
|
# Stargazers over time
|
||||||
|
|
||||||
[](https://starchart.cc/octeep/wireproxy)
|
[](https://starchart.cc/octeep/wireproxy)
|
||||||
|
|
96
UseWithVPN.md
Normal file
96
UseWithVPN.md
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
# Getting a Wireguard Server
|
||||||
|
|
||||||
|
You can create your own wireguard server using a host service like DigitalOcean,
|
||||||
|
or you can get a VPN service that provides WireGuard configs.
|
||||||
|
|
||||||
|
I recommend ProtonVPN, because it is highly secure and has a great WireGuard
|
||||||
|
config generator.
|
||||||
|
|
||||||
|
Simply go to <https://account.protonvpn.com/downloads> and scroll down to the
|
||||||
|
wireguard section to generate your configs, then paste into the appropriate
|
||||||
|
section below.
|
||||||
|
|
||||||
|
# Simple Setup for multiple SOCKS configs for firefox
|
||||||
|
|
||||||
|
Create a folder for your configs and startup scripts. Can be the same place as
|
||||||
|
this code. That path you will use below. For reference this text uses
|
||||||
|
`/Users/jonny/vpntabs`
|
||||||
|
|
||||||
|
For each VPN you want to run, you will download your wireguard config and name
|
||||||
|
it appropriately (e.g. `ProtonUS.adblock.server.conf`) and then create two new
|
||||||
|
files from those below with similar names (e.g. `ProtonUS.adblock.conf` and
|
||||||
|
`ProtonUS.adblock.sh`)
|
||||||
|
|
||||||
|
You will also create a launch script, the reference below is only for macOS. The
|
||||||
|
naming should also be similar (e.g.
|
||||||
|
`/Users/jonny/Library/LaunchAgents/com.ProtonUS.adblock.plist`)
|
||||||
|
|
||||||
|
## Config File
|
||||||
|
|
||||||
|
Make sure you use a unique port for every separate server
|
||||||
|
I recommend you set proxy authentication, you can use the same user/pass for all
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Link to the Downloaded config
|
||||||
|
WGConfig = /Users/jonny/vpntabs/ProtonUS.adblock.server.conf
|
||||||
|
|
||||||
|
# Used for firefox containers
|
||||||
|
[Socks5]
|
||||||
|
BindAddress = 127.0.0.1:25344 # Update the port here for each new server
|
||||||
|
|
||||||
|
# Socks5 authentication parameters, specifying username and password enables
|
||||||
|
# proxy authentication.
|
||||||
|
#Username = ...
|
||||||
|
# Avoid using spaces in the password field
|
||||||
|
#Password = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Startup Script File
|
||||||
|
|
||||||
|
This is a bash script to facilitate startup, not strictly essential, but adds
|
||||||
|
ease.
|
||||||
|
Note, you MUST update the first path to wherever you installed this code to.
|
||||||
|
Make sure you use the path for the config file above, not the one you downloaded
|
||||||
|
from e.g. protonvpn.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
/Users/jonny/wireproxy/wireproxy -c /Users/jonny/vpntabs/ProtonUS.adblock.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
## MacOS LaunchAgent
|
||||||
|
|
||||||
|
To make it run every time you start your computer, you can create a launch agent
|
||||||
|
in `$HOME/Library/LaunchAgents`. Name reference above.
|
||||||
|
|
||||||
|
That file should contain the following, the label should be the same as the file
|
||||||
|
name and the paths should be set correctly:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.ProtonUS.adblock</string>
|
||||||
|
<key>Program</key>
|
||||||
|
<string>/Users/jonny/vpntabs/ProtonUS.adblock.sh</string>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
```
|
||||||
|
|
||||||
|
To enable it, run
|
||||||
|
`launchctl load ~/Library/LaunchAgents/com.ProtonUS.adblock.plist` and
|
||||||
|
`launchtl start ~/Library/LaunchAgents/com.PortonUS.adblock.plist`
|
||||||
|
|
||||||
|
# Firefox Setup
|
||||||
|
|
||||||
|
You will need to enable the Multi Account Container Tabs extension and a proxy extension, I
|
||||||
|
recommend Sideberry, but Container Proxy also works.
|
||||||
|
|
||||||
|
Create a container to be dedicated to this VPN, and then add the IP, port,
|
||||||
|
username, and password from above.
|
BIN
assets/iproyal.png
Normal file
BIN
assets/iproyal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
|
@ -1,29 +1,184 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/landlock-lsm/go-landlock/landlock"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/akamensky/argparse"
|
"github.com/akamensky/argparse"
|
||||||
"github.com/octeep/wireproxy"
|
"github.com/pufferffish/wireproxy"
|
||||||
|
"golang.zx2c4.com/wireguard/device"
|
||||||
|
"suah.dev/protect"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// an argument to denote that this process was spawned by -d
|
||||||
const daemonProcess = "daemon-process"
|
const daemonProcess = "daemon-process"
|
||||||
|
|
||||||
|
// default paths for wireproxy config file
|
||||||
|
var default_config_paths = []string {
|
||||||
|
"/etc/wireproxy/wireproxy.conf",
|
||||||
|
os.Getenv("HOME")+"/.config/wireproxy.conf",
|
||||||
|
}
|
||||||
|
|
||||||
|
var version = "1.0.8-dev"
|
||||||
|
|
||||||
|
func panicIfError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempts to pledge and panic if it fails
|
||||||
|
// this does nothing on non-OpenBSD systems
|
||||||
|
func pledgeOrPanic(promises string) {
|
||||||
|
panicIfError(protect.Pledge(promises))
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempts to unveil and panic if it fails
|
||||||
|
// this does nothing on non-OpenBSD systems
|
||||||
|
func unveilOrPanic(path string, flags string) {
|
||||||
|
panicIfError(protect.Unveil(path, flags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the executable path via syscalls or infer it from argv
|
||||||
|
func executablePath() string {
|
||||||
|
programPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return os.Args[0]
|
||||||
|
}
|
||||||
|
return programPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if default config file paths exist
|
||||||
|
func configFilePath() (string, bool) {
|
||||||
|
for _, path := range default_config_paths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func lock(stage string) {
|
||||||
|
switch stage {
|
||||||
|
case "boot":
|
||||||
|
exePath := executablePath()
|
||||||
|
// OpenBSD
|
||||||
|
unveilOrPanic("/", "r")
|
||||||
|
unveilOrPanic(exePath, "x")
|
||||||
|
// only allow standard stdio operation, file reading, networking, and exec
|
||||||
|
// also remove unveil permission to lock unveil
|
||||||
|
pledgeOrPanic("stdio rpath inet dns proc exec")
|
||||||
|
// Linux
|
||||||
|
panicIfError(landlock.V1.BestEffort().RestrictPaths(
|
||||||
|
landlock.RODirs("/"),
|
||||||
|
))
|
||||||
|
case "boot-daemon":
|
||||||
|
case "read-config":
|
||||||
|
// OpenBSD
|
||||||
|
pledgeOrPanic("stdio rpath inet dns")
|
||||||
|
case "ready":
|
||||||
|
// no file access is allowed from now on, only networking
|
||||||
|
// OpenBSD
|
||||||
|
pledgeOrPanic("stdio inet dns")
|
||||||
|
// Linux
|
||||||
|
net.DefaultResolver.PreferGo = true // needed to lock down dependencies
|
||||||
|
panicIfError(landlock.V1.BestEffort().RestrictPaths(
|
||||||
|
landlock.ROFiles("/etc/resolv.conf").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/dev/fd").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/dev/zero").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/dev/urandom").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/etc/localtime").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/self/stat").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/self/status").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/usr/share/locale").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/self/cmdline").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/usr/share/zoneinfo").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/sys/kernel/version").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/sys/kernel/ngroups_max").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/sys/kernel/cap_last_cap").IgnoreIfMissing(),
|
||||||
|
landlock.ROFiles("/proc/sys/vm/overcommit_memory").IgnoreIfMissing(),
|
||||||
|
landlock.RWFiles("/dev/log").IgnoreIfMissing(),
|
||||||
|
landlock.RWFiles("/dev/null").IgnoreIfMissing(),
|
||||||
|
landlock.RWFiles("/dev/full").IgnoreIfMissing(),
|
||||||
|
landlock.RWFiles("/proc/self/fd").IgnoreIfMissing(),
|
||||||
|
))
|
||||||
|
default:
|
||||||
|
panic("invalid stage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPort(addr string) uint16 {
|
||||||
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to extract port from %s: %w", addr, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("failed to extract port from %s: %w", addr, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint16(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lockNetwork(sections []wireproxy.RoutineSpawner, infoAddr *string) {
|
||||||
|
var rules []landlock.Rule
|
||||||
|
if infoAddr != nil && *infoAddr != "" {
|
||||||
|
rules = append(rules, landlock.BindTCP(extractPort(*infoAddr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range sections {
|
||||||
|
switch section := section.(type) {
|
||||||
|
case *wireproxy.TCPServerTunnelConfig:
|
||||||
|
rules = append(rules, landlock.ConnectTCP(extractPort(section.Target)))
|
||||||
|
case *wireproxy.HTTPConfig:
|
||||||
|
rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
|
||||||
|
case *wireproxy.TCPClientTunnelConfig:
|
||||||
|
rules = append(rules, landlock.ConnectTCP(uint16(section.BindAddress.Port)))
|
||||||
|
case *wireproxy.Socks5Config:
|
||||||
|
rules = append(rules, landlock.BindTCP(extractPort(section.BindAddress)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panicIfError(landlock.V4.BestEffort().RestrictNet(rules...))
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
s := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(s, syscall.SIGINT, syscall.SIGQUIT)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-s
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
exePath := executablePath()
|
||||||
|
lock("boot")
|
||||||
|
|
||||||
isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess
|
isDaemonProcess := len(os.Args) > 1 && os.Args[1] == daemonProcess
|
||||||
args := os.Args
|
args := os.Args
|
||||||
if isDaemonProcess {
|
if isDaemonProcess {
|
||||||
|
lock("boot-daemon")
|
||||||
args = []string{args[0]}
|
args = []string{args[0]}
|
||||||
args = append(args, os.Args[2:]...)
|
args = append(args, os.Args[2:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
parser := argparse.NewParser("wireproxy", "Userspace wireguard client for proxying")
|
parser := argparse.NewParser("wireproxy", "Userspace wireguard client for proxying")
|
||||||
|
|
||||||
config := parser.String("c", "config", &argparse.Options{Required: true, Help: "Path of configuration file"})
|
config := parser.String("c", "config", &argparse.Options{Help: "Path of configuration file"})
|
||||||
|
silent := parser.Flag("s", "silent", &argparse.Options{Help: "Silent mode"})
|
||||||
daemon := parser.Flag("d", "daemon", &argparse.Options{Help: "Make wireproxy run in background"})
|
daemon := parser.Flag("d", "daemon", &argparse.Options{Help: "Make wireproxy run in background"})
|
||||||
|
info := parser.String("i", "info", &argparse.Options{Help: "Specify the address and port for exposing health status"})
|
||||||
|
printVerison := parser.Flag("v", "version", &argparse.Options{Help: "Print version"})
|
||||||
configTest := parser.Flag("n", "configtest", &argparse.Options{Help: "Configtest mode. Only check the configuration file for validity."})
|
configTest := parser.Flag("n", "configtest", &argparse.Options{Help: "Configtest mode. Only check the configuration file for validity."})
|
||||||
|
|
||||||
err := parser.Parse(args)
|
err := parser.Parse(args)
|
||||||
|
@ -32,9 +187,27 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *printVerison {
|
||||||
|
fmt.Printf("wireproxy, version %s\n", version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *config == "" {
|
||||||
|
if path, config_exist := configFilePath(); config_exist {
|
||||||
|
*config = path
|
||||||
|
} else {
|
||||||
|
fmt.Println("configuration path is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*daemon {
|
||||||
|
lock("read-config")
|
||||||
|
}
|
||||||
|
|
||||||
conf, err := wireproxy.ParseConfig(*config)
|
conf, err := wireproxy.ParseConfig(*config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *configTest {
|
if *configTest {
|
||||||
|
@ -42,6 +215,8 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lockNetwork(conf.Routines, info)
|
||||||
|
|
||||||
if isDaemonProcess {
|
if isDaemonProcess {
|
||||||
os.Stdout, _ = os.Open(os.DevNull)
|
os.Stdout, _ = os.Open(os.DevNull)
|
||||||
os.Stderr, _ = os.Open(os.DevNull)
|
os.Stderr, _ = os.Open(os.DevNull)
|
||||||
|
@ -49,14 +224,8 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if *daemon {
|
if *daemon {
|
||||||
programPath, err := os.Executable()
|
args[0] = daemonProcess
|
||||||
if err != nil {
|
cmd := exec.Command(exePath, args...)
|
||||||
programPath = args[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
newArgs := []string{daemonProcess}
|
|
||||||
newArgs = append(newArgs, args[1:]...)
|
|
||||||
cmd := exec.Command(programPath, newArgs...)
|
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
|
@ -64,14 +233,36 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tnet, err := wireproxy.StartWireguard(conf.Device)
|
// Wireguard doesn't allow configuring which FD to use for logging
|
||||||
|
// https://github.com/WireGuard/wireguard-go/blob/master/device/logger.go#L39
|
||||||
|
// so redirect STDOUT to STDERR, we don't want to print anything to STDOUT anyways
|
||||||
|
os.Stdout = os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
|
||||||
|
logLevel := device.LogLevelVerbose
|
||||||
|
if *silent {
|
||||||
|
logLevel = device.LogLevelSilent
|
||||||
|
}
|
||||||
|
|
||||||
|
lock("ready")
|
||||||
|
|
||||||
|
tun, err := wireproxy.StartWireguard(conf.Device, logLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, spawner := range conf.Routines {
|
for _, spawner := range conf.Routines {
|
||||||
go spawner.SpawnRoutine(tnet)
|
go spawner.SpawnRoutine(tun)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {} // sleep eternally
|
tun.StartPingIPs()
|
||||||
|
|
||||||
|
if *info != "" {
|
||||||
|
go func() {
|
||||||
|
err := http.ListenAndServe(*info, tun)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
}
|
}
|
||||||
|
|
229
config.go
229
config.go
|
@ -5,22 +5,32 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-ini/ini"
|
"github.com/go-ini/ini"
|
||||||
|
|
||||||
"golang.zx2c4.com/go118/netip"
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeviceConfig struct {
|
type PeerConfig struct {
|
||||||
SelfSecretKey string
|
PublicKey string
|
||||||
SelfEndpoint []netip.Addr
|
|
||||||
PeerPublicKey string
|
|
||||||
PeerEndpoint string
|
|
||||||
DNS []netip.Addr
|
|
||||||
KeepAlive int
|
|
||||||
PreSharedKey string
|
PreSharedKey string
|
||||||
|
Endpoint *string
|
||||||
|
KeepAlive int
|
||||||
|
AllowedIPs []netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceConfig contains the information to initiate a wireguard connection
|
||||||
|
type DeviceConfig struct {
|
||||||
|
SecretKey string
|
||||||
|
Endpoint []netip.Addr
|
||||||
|
Peers []PeerConfig
|
||||||
|
DNS []netip.Addr
|
||||||
MTU int
|
MTU int
|
||||||
|
ListenPort *int
|
||||||
|
CheckAlive []netip.Addr
|
||||||
|
CheckAliveInterval int
|
||||||
}
|
}
|
||||||
|
|
||||||
type TCPClientTunnelConfig struct {
|
type TCPClientTunnelConfig struct {
|
||||||
|
@ -28,6 +38,10 @@ type TCPClientTunnelConfig struct {
|
||||||
Target string
|
Target string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type STDIOTunnelConfig struct {
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
type TCPServerTunnelConfig struct {
|
type TCPServerTunnelConfig struct {
|
||||||
ListenPort int
|
ListenPort int
|
||||||
Target string
|
Target string
|
||||||
|
@ -39,6 +53,12 @@ type Socks5Config struct {
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTPConfig struct {
|
||||||
|
BindAddress string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Device *DeviceConfig
|
Device *DeviceConfig
|
||||||
Routines []RoutineSpawner
|
Routines []RoutineSpawner
|
||||||
|
@ -49,6 +69,18 @@ func parseString(section *ini.Section, keyName string) (string, error) {
|
||||||
if key == nil {
|
if key == nil {
|
||||||
return "", errors.New(keyName + " should not be empty")
|
return "", errors.New(keyName + " should not be empty")
|
||||||
}
|
}
|
||||||
|
value := key.String()
|
||||||
|
if strings.HasPrefix(value, "$") {
|
||||||
|
if strings.HasPrefix(value, "$$") {
|
||||||
|
return strings.Replace(value, "$$", "$", 1), nil
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
value, ok = os.LookupEnv(strings.TrimPrefix(value, "$"))
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New(keyName + " references unset environment variable " + key.String())
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
return key.String(), nil
|
return key.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,14 +135,21 @@ func encodeBase64ToHex(key string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) {
|
func parseNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) {
|
||||||
key := section.Key(keyName)
|
key, err := parseString(section, keyName)
|
||||||
if key == nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "should not be empty") {
|
||||||
return []netip.Addr{}, nil
|
return []netip.Addr{}, nil
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ips := []netip.Addr{}
|
keys := strings.Split(key, ",")
|
||||||
for _, str := range key.StringsWithShadows(",") {
|
var ips = make([]netip.Addr, 0, len(keys))
|
||||||
|
for _, str := range keys {
|
||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
|
if len(str) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ip, err := netip.ParseAddr(str)
|
ip, err := netip.ParseAddr(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -121,24 +160,59 @@ func parseNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCIDRNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) {
|
func parseCIDRNetIP(section *ini.Section, keyName string) ([]netip.Addr, error) {
|
||||||
key := section.Key(keyName)
|
key, err := parseString(section, keyName)
|
||||||
if key == nil {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "should not be empty") {
|
||||||
return []netip.Addr{}, nil
|
return []netip.Addr{}, nil
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ips := []netip.Addr{}
|
keys := strings.Split(key, ",")
|
||||||
for _, str := range key.StringsWithShadows(",") {
|
var ips = make([]netip.Addr, 0, len(keys))
|
||||||
|
for _, str := range keys {
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
if len(str) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr, err := netip.ParseAddr(str); err == nil {
|
||||||
|
ips = append(ips, addr)
|
||||||
|
} else {
|
||||||
prefix, err := netip.ParsePrefix(str)
|
prefix, err := netip.ParsePrefix(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := prefix.Addr()
|
addr := prefix.Addr()
|
||||||
if prefix.Bits() != addr.BitLen() {
|
ips = append(ips, addr)
|
||||||
return nil, errors.New("interface address subnet should be /32 for IPv4 and /128 for IPv6")
|
}
|
||||||
|
}
|
||||||
|
return ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAllowedIPs(section *ini.Section) ([]netip.Prefix, error) {
|
||||||
|
key, err := parseString(section, "AllowedIPs")
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "should not be empty") {
|
||||||
|
return []netip.Prefix{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ips = append(ips, addr)
|
keys := strings.Split(key, ",")
|
||||||
|
var ips = make([]netip.Prefix, 0, len(keys))
|
||||||
|
for _, str := range keys {
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
if len(str) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prefix, err := netip.ParsePrefix(str)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ips = append(ips, prefix)
|
||||||
}
|
}
|
||||||
return ips, nil
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
@ -160,6 +234,7 @@ func resolveIPPAndPort(addr string) (string, error) {
|
||||||
return net.JoinHostPort(ip.String(), port), nil
|
return net.JoinHostPort(ip.String(), port), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseInterface parses the [Interface] section and extract the information into `device`
|
||||||
func ParseInterface(cfg *ini.File, device *DeviceConfig) error {
|
func ParseInterface(cfg *ini.File, device *DeviceConfig) error {
|
||||||
sections, err := cfg.SectionsByName("Interface")
|
sections, err := cfg.SectionsByName("Interface")
|
||||||
if len(sections) != 1 || err != nil {
|
if len(sections) != 1 || err != nil {
|
||||||
|
@ -172,13 +247,13 @@ func ParseInterface(cfg *ini.File, device *DeviceConfig) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
device.SelfEndpoint = address
|
device.Endpoint = address
|
||||||
|
|
||||||
privKey, err := parseBase64KeyToHex(section, "PrivateKey")
|
privKey, err := parseBase64KeyToHex(section, "PrivateKey")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
device.SelfSecretKey = privKey
|
device.SecretKey = privKey
|
||||||
|
|
||||||
dns, err := parseNetIP(section, "DNS")
|
dns, err := parseNetIP(section, "DNS")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -194,48 +269,87 @@ func ParseInterface(cfg *ini.File, device *DeviceConfig) error {
|
||||||
device.MTU = value
|
device.MTU = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sectionKey, err := section.GetKey("ListenPort"); err == nil {
|
||||||
|
value, err := sectionKey.Int()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
device.ListenPort = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAlive, err := parseNetIP(section, "CheckAlive")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
device.CheckAlive = checkAlive
|
||||||
|
|
||||||
|
device.CheckAliveInterval = 5
|
||||||
|
if sectionKey, err := section.GetKey("CheckAliveInterval"); err == nil {
|
||||||
|
value, err := sectionKey.Int()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(checkAlive) == 0 {
|
||||||
|
return errors.New("CheckAliveInterval is only valid when CheckAlive is set")
|
||||||
|
}
|
||||||
|
|
||||||
|
device.CheckAliveInterval = value
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParsePeer(cfg *ini.File, device *DeviceConfig) error {
|
// ParsePeers parses the [Peer] section and extract the information into `peers`
|
||||||
|
func ParsePeers(cfg *ini.File, peers *[]PeerConfig) error {
|
||||||
sections, err := cfg.SectionsByName("Peer")
|
sections, err := cfg.SectionsByName("Peer")
|
||||||
if len(sections) != 1 || err != nil {
|
if len(sections) < 1 || err != nil {
|
||||||
return errors.New("one and only one [Peer] is expected")
|
return errors.New("at least one [Peer] is expected")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range sections {
|
||||||
|
peer := PeerConfig{
|
||||||
|
PreSharedKey: "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
KeepAlive: 0,
|
||||||
}
|
}
|
||||||
section := sections[0]
|
|
||||||
|
|
||||||
decoded, err := parseBase64KeyToHex(section, "PublicKey")
|
decoded, err := parseBase64KeyToHex(section, "PublicKey")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
device.PeerPublicKey = decoded
|
peer.PublicKey = decoded
|
||||||
|
|
||||||
if sectionKey, err := section.GetKey("PreSharedKey"); err == nil {
|
if sectionKey, err := section.GetKey("PreSharedKey"); err == nil {
|
||||||
value, err := encodeBase64ToHex(sectionKey.String())
|
value, err := encodeBase64ToHex(sectionKey.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
device.PreSharedKey = value
|
peer.PreSharedKey = value
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded, err = parseString(section, "Endpoint")
|
if sectionKey, err := section.GetKey("Endpoint"); err == nil {
|
||||||
|
value := sectionKey.String()
|
||||||
|
decoded, err = resolveIPPAndPort(strings.ToLower(value))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
decoded, err = resolveIPPAndPort(decoded)
|
peer.Endpoint = &decoded
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
device.PeerEndpoint = decoded
|
|
||||||
|
|
||||||
if sectionKey, err := section.GetKey("PersistentKeepalive"); err == nil {
|
if sectionKey, err := section.GetKey("PersistentKeepalive"); err == nil {
|
||||||
value, err := sectionKey.Int()
|
value, err := sectionKey.Int()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
device.KeepAlive = value
|
peer.KeepAlive = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peer.AllowedIPs, err = parseAllowedIPs(section)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*peers = append(*peers, peer)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,6 +370,17 @@ func parseTCPClientTunnelConfig(section *ini.Section) (RoutineSpawner, error) {
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSTDIOTunnelConfig(section *ini.Section) (RoutineSpawner, error) {
|
||||||
|
config := &STDIOTunnelConfig{}
|
||||||
|
targetSection, err := parseString(section, "Target")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.Target = targetSection
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseTCPServerTunnelConfig(section *ini.Section) (RoutineSpawner, error) {
|
func parseTCPServerTunnelConfig(section *ini.Section) (RoutineSpawner, error) {
|
||||||
config := &TCPServerTunnelConfig{}
|
config := &TCPServerTunnelConfig{}
|
||||||
|
|
||||||
|
@ -292,6 +417,26 @@ func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseHTTPConfig(section *ini.Section) (RoutineSpawner, error) {
|
||||||
|
config := &HTTPConfig{}
|
||||||
|
|
||||||
|
bindAddress, err := parseString(section, "BindAddress")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.BindAddress = bindAddress
|
||||||
|
|
||||||
|
username, _ := parseString(section, "Username")
|
||||||
|
config.Username = username
|
||||||
|
|
||||||
|
password, _ := parseString(section, "Password")
|
||||||
|
config.Password = password
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes a function that parses an individual section into a config, and apply it on all
|
||||||
|
// specified sections
|
||||||
func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error {
|
func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error {
|
||||||
sections, err := cfg.SectionsByName(sectionName)
|
sections, err := cfg.SectionsByName(sectionName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -310,10 +455,12 @@ func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseConfig takes the path of a configuration file and parses it into Configuration
|
||||||
func ParseConfig(path string) (*Configuration, error) {
|
func ParseConfig(path string) (*Configuration, error) {
|
||||||
iniOpt := ini.LoadOptions{
|
iniOpt := ini.LoadOptions{
|
||||||
Insensitive: true,
|
Insensitive: true,
|
||||||
AllowShadows: true,
|
AllowShadows: true,
|
||||||
|
AllowNonUniqueSections: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := ini.LoadSources(iniOpt, path)
|
cfg, err := ini.LoadSources(iniOpt, path)
|
||||||
|
@ -322,8 +469,6 @@ func ParseConfig(path string) (*Configuration, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
device := &DeviceConfig{
|
device := &DeviceConfig{
|
||||||
PreSharedKey: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
KeepAlive: 0,
|
|
||||||
MTU: 1420,
|
MTU: 1420,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,18 +487,23 @@ func ParseConfig(path string) (*Configuration, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ParsePeer(wgCfg, device)
|
err = ParsePeers(wgCfg, &device.Peers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
routinesSpawners := []RoutineSpawner{}
|
var routinesSpawners []RoutineSpawner
|
||||||
|
|
||||||
err = parseRoutinesConfig(&routinesSpawners, cfg, "TCPClientTunnel", parseTCPClientTunnelConfig)
|
err = parseRoutinesConfig(&routinesSpawners, cfg, "TCPClientTunnel", parseTCPClientTunnelConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = parseRoutinesConfig(&routinesSpawners, cfg, "STDIOTunnel", parseSTDIOTunnelConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
err = parseRoutinesConfig(&routinesSpawners, cfg, "TCPServerTunnel", parseTCPServerTunnelConfig)
|
err = parseRoutinesConfig(&routinesSpawners, cfg, "TCPServerTunnel", parseTCPServerTunnelConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -364,6 +514,11 @@ func ParseConfig(path string) (*Configuration, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = parseRoutinesConfig(&routinesSpawners, cfg, "http", parseHTTPConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Configuration{
|
return &Configuration{
|
||||||
Device: device,
|
Device: device,
|
||||||
Routines: routinesSpawners,
|
Routines: routinesSpawners,
|
||||||
|
|
87
config_test.go
Normal file
87
config_test.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package wireproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-ini/ini"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadIniConfig(config string) (*ini.File, error) {
|
||||||
|
iniOpt := ini.LoadOptions{
|
||||||
|
Insensitive: true,
|
||||||
|
AllowShadows: true,
|
||||||
|
AllowNonUniqueSections: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ini.LoadSources(iniOpt, []byte(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWireguardConfWithoutSubnet(t *testing.T) {
|
||||||
|
const config = `
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0=
|
||||||
|
Address = 10.5.0.2
|
||||||
|
DNS = 1.1.1.1
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w=
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::/0
|
||||||
|
Endpoint = 94.140.11.15:51820
|
||||||
|
PersistentKeepalive = 25`
|
||||||
|
var cfg DeviceConfig
|
||||||
|
iniData, err := loadIniConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ParseInterface(iniData, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWireguardConfWithSubnet(t *testing.T) {
|
||||||
|
const config = `
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = LAr1aNSNF9d0MjwUgAVC4020T0N/E5NUtqVv5EnsSz0=
|
||||||
|
Address = 10.5.0.2/23
|
||||||
|
DNS = 1.1.1.1
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = e8LKAc+f9xEzq9Ar7+MfKRrs+gZ/4yzvpRJLRJ/VJ1w=
|
||||||
|
AllowedIPs = 0.0.0.0/0, ::/0
|
||||||
|
Endpoint = 94.140.11.15:51820
|
||||||
|
PersistentKeepalive = 25`
|
||||||
|
var cfg DeviceConfig
|
||||||
|
iniData, err := loadIniConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ParseInterface(iniData, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWireguardConfWithManyAddress(t *testing.T) {
|
||||||
|
const config = `
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = mBsVDahr1XIu9PPd17UmsDdB6E53nvmS47NbNqQCiFM=
|
||||||
|
Address = 100.96.0.190,2606:B300:FFFF:fe8a:2ac6:c7e8:b021:6f5f/128
|
||||||
|
DNS = 198.18.0.1,198.18.0.2
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = SHnh4C2aDXhp1gjIqceGhJrhOLSeNYcqWLKcYnzj00U=
|
||||||
|
AllowedIPs = 0.0.0.0/0,::/0
|
||||||
|
Endpoint = 192.200.144.22:51820`
|
||||||
|
var cfg DeviceConfig
|
||||||
|
iniData, err := loadIniConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ParseInterface(iniData, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
FROM golang:alpine AS go-build
|
|
||||||
|
|
||||||
RUN apk --no-cache add --update git
|
|
||||||
RUN git clone https://github.com/octeep/wireproxy.git
|
|
||||||
RUN cd ./wireproxy && go build ./cmd/wireproxy
|
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
RUN apk upgrade
|
|
||||||
COPY --from=go-build /go/wireproxy/wireproxy /usr/bin/
|
|
||||||
|
|
||||||
VOLUME [ "/etc/wireproxy"]
|
|
||||||
ENTRYPOINT [ "/usr/bin/wireproxy", "/etc/wireproxy/config" ]
|
|
|
@ -1,10 +0,0 @@
|
||||||
build:
|
|
||||||
docker build -t wireproxy .
|
|
||||||
|
|
||||||
run:
|
|
||||||
docker run \
|
|
||||||
--rm --tty --interactive \
|
|
||||||
--name=wireproxy \
|
|
||||||
--publish 2534:2534 \
|
|
||||||
--volume "${PWD}/config:/etc/wireproxy/config:ro" \
|
|
||||||
wireproxy
|
|
|
@ -1,12 +0,0 @@
|
||||||
[Interface]
|
|
||||||
Address = ###Interface - Address###
|
|
||||||
PrivateKey = ###Interface - PrivateKey###
|
|
||||||
DNS = ###Interface - DNS###
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ###Peer - PublicKey###
|
|
||||||
Endpoint = ###Peer - Endpoint###
|
|
||||||
|
|
||||||
# Socks5 create a socks5 proxy on your LAN, and any traffic would be routed via wireguard
|
|
||||||
[Socks5]
|
|
||||||
BindAddress = 0.0.0.0:2534
|
|
36
go.mod
36
go.mod
|
@ -1,24 +1,26 @@
|
||||||
module github.com/octeep/wireproxy
|
module github.com/pufferffish/wireproxy
|
||||||
|
|
||||||
go 1.17
|
go 1.21.1
|
||||||
|
|
||||||
|
toolchain go1.21.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||||
github.com/go-ini/ini v1.66.4
|
github.com/akamensky/argparse v1.4.0
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
github.com/go-ini/ini v1.67.0
|
||||||
golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d
|
github.com/landlock-lsm/go-landlock v0.0.0-20240216195629-efb66220540a
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20220202223031-3b95c81cc178
|
github.com/things-go/go-socks5 v0.0.5
|
||||||
golang.zx2c4.com/wireguard/tun/netstack v0.0.0-20220310012736-ae6bc4dd64e1
|
golang.org/x/net v0.33.0
|
||||||
gvisor.dev/gvisor v0.0.0-20211020211948-f76a604701b6
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||||
|
suah.dev/protect v1.2.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/akamensky/argparse v1.3.1 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/btree v1.0.1 // indirect
|
golang.org/x/crypto v0.31.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
166
http.go
Normal file
166
http.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package wireproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const proxyAuthHeaderKey = "Proxy-Authorization"
|
||||||
|
|
||||||
|
type HTTPServer struct {
|
||||||
|
config *HTTPConfig
|
||||||
|
|
||||||
|
auth CredentialValidator
|
||||||
|
dial func(network, address string) (net.Conn, error)
|
||||||
|
|
||||||
|
authRequired bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) authenticate(req *http.Request) (int, error) {
|
||||||
|
if !s.authRequired {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := req.Header.Get(proxyAuthHeaderKey)
|
||||||
|
if auth == "" {
|
||||||
|
return http.StatusProxyAuthRequired, fmt.Errorf("%s", http.StatusText(http.StatusProxyAuthRequired))
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := strings.TrimPrefix(auth, "Basic ")
|
||||||
|
str, err := base64.StdEncoding.DecodeString(enc)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusNotAcceptable, fmt.Errorf("decode username and password failed: %w", err)
|
||||||
|
}
|
||||||
|
pairs := bytes.SplitN(str, []byte(":"), 2)
|
||||||
|
if len(pairs) != 2 {
|
||||||
|
return http.StatusLengthRequired, fmt.Errorf("username and password format invalid")
|
||||||
|
}
|
||||||
|
if s.auth.Valid(string(pairs[0]), string(pairs[1])) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return http.StatusUnauthorized, fmt.Errorf("username and password not matching")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) handleConn(req *http.Request, conn net.Conn) (peer net.Conn, err error) {
|
||||||
|
addr := req.Host
|
||||||
|
if !strings.Contains(addr, ":") {
|
||||||
|
port := "443"
|
||||||
|
addr = net.JoinHostPort(addr, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err = s.dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
|
||||||
|
if err != nil {
|
||||||
|
_ = peer.Close()
|
||||||
|
peer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) handle(req *http.Request) (peer net.Conn, err error) {
|
||||||
|
addr := req.Host
|
||||||
|
if !strings.Contains(addr, ":") {
|
||||||
|
port := "80"
|
||||||
|
addr = net.JoinHostPort(addr, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err = s.dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = req.Write(peer)
|
||||||
|
if err != nil {
|
||||||
|
_ = peer.Close()
|
||||||
|
peer = nil
|
||||||
|
return peer, fmt.Errorf("conn write failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) serve(conn net.Conn) {
|
||||||
|
var rd = bufio.NewReader(conn)
|
||||||
|
req, err := http.ReadRequest(rd)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("read request failed: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := s.authenticate(req)
|
||||||
|
if err != nil {
|
||||||
|
resp := responseWith(req, code)
|
||||||
|
if code == http.StatusProxyAuthRequired {
|
||||||
|
resp.Header.Set("Proxy-Authenticate", "Basic realm=\"Proxy\"")
|
||||||
|
}
|
||||||
|
_ = resp.Write(conn)
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var peer net.Conn
|
||||||
|
switch req.Method {
|
||||||
|
case http.MethodConnect:
|
||||||
|
peer, err = s.handleConn(req, conn)
|
||||||
|
case http.MethodGet:
|
||||||
|
peer, err = s.handle(req)
|
||||||
|
default:
|
||||||
|
_ = responseWith(req, http.StatusMethodNotAllowed).Write(conn)
|
||||||
|
log.Printf("unsupported protocol: %s\n", req.Method)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("dial proxy failed: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if peer == nil {
|
||||||
|
log.Println("dial proxy failed: peer nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer conn.Close()
|
||||||
|
defer peer.Close()
|
||||||
|
|
||||||
|
_, _ = io.Copy(conn, peer)
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer conn.Close()
|
||||||
|
defer peer.Close()
|
||||||
|
|
||||||
|
_, _ = io.Copy(peer, conn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe is used to create a listener and serve on it
|
||||||
|
func (s *HTTPServer) ListenAndServe(network, addr string) error {
|
||||||
|
server, err := net.Listen(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen tcp failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func(server net.Listener) {
|
||||||
|
_ = server.Close()
|
||||||
|
}(server)
|
||||||
|
for {
|
||||||
|
conn, err := server.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("accept request failed: %w", err)
|
||||||
|
}
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
s.serve(conn)
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
10
net.go
10
net.go
|
@ -3,8 +3,8 @@
|
||||||
package wireproxy
|
package wireproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"golang.zx2c4.com/go118/netip"
|
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TCPAddrFromAddrPort(addr netip.AddrPort) *net.TCPAddr {
|
func TCPAddrFromAddrPort(addr netip.AddrPort) *net.TCPAddr {
|
||||||
|
@ -14,11 +14,3 @@ func TCPAddrFromAddrPort(addr netip.AddrPort) *net.TCPAddr {
|
||||||
Port: int(addr.Port()),
|
Port: int(addr.Port()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UDPAddrFromAddrPort(addr netip.AddrPort) *net.UDPAddr {
|
|
||||||
return &net.UDPAddr{
|
|
||||||
IP: addr.Addr().AsSlice(),
|
|
||||||
Zone: addr.Addr().Zone(),
|
|
||||||
Port: int(addr.Port()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
21
rc.d/README.md
Normal file
21
rc.d/README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Running wireproxy with rc.d
|
||||||
|
|
||||||
|
If you're on a rc.d-based distro, you'll most likely want to run Wireproxy as a systemd unit.
|
||||||
|
|
||||||
|
The provided systemd unit assumes you have the wireproxy executable installed on `/bin/wireproxy` and a configuration file stored at `/etc/wireproxy.conf`. These paths can be customized by editing the unit file.
|
||||||
|
|
||||||
|
# Setting up the unit
|
||||||
|
|
||||||
|
1. Copy the `wireproxy` file from this directory to `/usr/local/etc/rc.d`.
|
||||||
|
|
||||||
|
2. If necessary, customize the unit.
|
||||||
|
Edit the parts with `procname`, `command`, `wireproxy_conf` to point to the executable and the configuration file.
|
||||||
|
|
||||||
|
4. Add the following lines to `/etc/rc.conf` to enable wireproxy
|
||||||
|
`wireproxy_enable="YES"`
|
||||||
|
|
||||||
|
5. Start wireproxy service and check status
|
||||||
|
```
|
||||||
|
sudo service wireproxy start
|
||||||
|
sudo service wireproxy status
|
||||||
|
```
|
30
rc.d/wireproxy
Normal file
30
rc.d/wireproxy
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PROVIDE: wireproxy
|
||||||
|
# REQUIRE: DAEMON
|
||||||
|
# KEYWORD: nojail
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add the following lines to /etc/rc.conf to enable wireproxy:
|
||||||
|
#
|
||||||
|
#wireproxy_enable="YES"
|
||||||
|
#
|
||||||
|
|
||||||
|
. /etc/rc.subr
|
||||||
|
|
||||||
|
name=wireproxy
|
||||||
|
rcvar=wireproxy_enable
|
||||||
|
|
||||||
|
load_rc_config $name
|
||||||
|
procname="/bin/wireproxy"
|
||||||
|
|
||||||
|
wireproxy_enable=${wireproxy_enable:-"NO"}
|
||||||
|
|
||||||
|
wireproxy_bin=/bin/wireproxy
|
||||||
|
wireproxy_conf=/etc/wireproxy.conf
|
||||||
|
|
||||||
|
command=${wireproxy_bin}
|
||||||
|
command_args="-s -d -c ${wireproxy_conf}"
|
||||||
|
|
||||||
|
run_rc_command "$1"
|
424
routine.go
424
routine.go
|
@ -1,51 +1,80 @@
|
||||||
package wireproxy
|
package wireproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
srand "crypto/rand"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"golang.org/x/net/icmp"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
|
"golang.zx2c4.com/wireguard/device"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/armon/go-socks5"
|
"github.com/things-go/go-socks5"
|
||||||
|
"github.com/things-go/go-socks5/bufferpool"
|
||||||
|
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"golang.zx2c4.com/go118/netip"
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// errorLogger is the logger to print error message
|
||||||
|
var errorLogger = log.New(os.Stderr, "ERROR: ", log.LstdFlags)
|
||||||
|
|
||||||
|
// CredentialValidator stores the authentication data of a socks5 proxy
|
||||||
type CredentialValidator struct {
|
type CredentialValidator struct {
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VirtualTun stores a reference to netstack network and DNS configuration
|
||||||
type VirtualTun struct {
|
type VirtualTun struct {
|
||||||
tnet *netstack.Net
|
Tnet *netstack.Net
|
||||||
systemDNS bool
|
Dev *device.Device
|
||||||
|
SystemDNS bool
|
||||||
|
Conf *DeviceConfig
|
||||||
|
// PingRecord stores the last time an IP was pinged
|
||||||
|
PingRecord map[string]uint64
|
||||||
|
PingRecordLock *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RoutineSpawner spawns a routine (e.g. socks5, tcp static routes) after the configuration is parsed
|
||||||
type RoutineSpawner interface {
|
type RoutineSpawner interface {
|
||||||
SpawnRoutine(vt *VirtualTun)
|
SpawnRoutine(vt *VirtualTun)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d VirtualTun) LookupAddr(ctx context.Context, name string) ([]string, error) {
|
type addressPort struct {
|
||||||
if d.systemDNS {
|
address string
|
||||||
return net.DefaultResolver.LookupHost(ctx, name)
|
port uint16
|
||||||
} else {
|
|
||||||
return d.tnet.LookupContextHost(ctx, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d VirtualTun) ResolveAddrPort(saddr string) (*netip.AddrPort, error) {
|
// LookupAddr lookups a hostname.
|
||||||
name, sport, err := net.SplitHostPort(saddr)
|
// DNS traffic may or may not be routed depending on VirtualTun's setting
|
||||||
if err != nil {
|
func (d VirtualTun) LookupAddr(ctx context.Context, name string) ([]string, error) {
|
||||||
return nil, err
|
if d.SystemDNS {
|
||||||
|
return net.DefaultResolver.LookupHost(ctx, name)
|
||||||
}
|
}
|
||||||
|
return d.Tnet.LookupContextHost(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
addrs, err := d.LookupAddr(context.Background(), name)
|
// ResolveAddrWithContext resolves a hostname and returns an AddrPort.
|
||||||
|
// DNS traffic may or may not be routed depending on VirtualTun's setting
|
||||||
|
func (d VirtualTun) ResolveAddrWithContext(ctx context.Context, name string) (*netip.Addr, error) {
|
||||||
|
addrs, err := d.LookupAddr(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -55,7 +84,38 @@ func (d VirtualTun) ResolveAddrPort(saddr string) (*netip.AddrPort, error) {
|
||||||
return nil, errors.New("no address found for: " + name)
|
return nil, errors.New("no address found for: " + name)
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := netip.ParseAddr(addrs[rand.Intn(size)])
|
rand.Shuffle(size, func(i, j int) {
|
||||||
|
addrs[i], addrs[j] = addrs[j], addrs[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
var addr netip.Addr
|
||||||
|
for _, saddr := range addrs {
|
||||||
|
addr, err = netip.ParseAddr(saddr)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve resolves a hostname and returns an IP.
|
||||||
|
// DNS traffic may or may not be routed depending on VirtualTun's setting
|
||||||
|
func (d VirtualTun) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
|
||||||
|
addr, err := d.ResolveAddrWithContext(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, addr.AsSlice(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAddressPort(endpoint string) (*addressPort, error) {
|
||||||
|
name, sport, err := net.SplitHostPort(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -65,127 +125,345 @@ func (d VirtualTun) ResolveAddrPort(saddr string) (*netip.AddrPort, error) {
|
||||||
return nil, &net.OpError{Op: "dial", Err: errors.New("port must be numeric")}
|
return nil, &net.OpError{Op: "dial", Err: errors.New("port must be numeric")}
|
||||||
}
|
}
|
||||||
|
|
||||||
addrPort := netip.AddrPortFrom(addr, uint16(port))
|
return &addressPort{address: name, port: uint16(port)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d VirtualTun) resolveToAddrPort(endpoint *addressPort) (*netip.AddrPort, error) {
|
||||||
|
addr, err := d.ResolveAddrWithContext(context.Background(), endpoint.address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrPort := netip.AddrPortFrom(*addr, endpoint.port)
|
||||||
return &addrPort, nil
|
return &addrPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d VirtualTun) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
|
// SpawnRoutine spawns a socks5 server.
|
||||||
var addrs []string
|
func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
|
||||||
var err error
|
var authMethods []socks5.Authenticator
|
||||||
|
if username := config.Username; username != "" {
|
||||||
addrs, err = d.LookupAddr(ctx, name)
|
authMethods = append(authMethods, socks5.UserPassAuthenticator{
|
||||||
|
Credentials: socks5.StaticCredentials{username: config.Password},
|
||||||
if err != nil {
|
})
|
||||||
return ctx, nil, err
|
} else {
|
||||||
|
authMethods = append(authMethods, socks5.NoAuthAuthenticator{})
|
||||||
}
|
}
|
||||||
|
|
||||||
size := len(addrs)
|
options := []socks5.Option{
|
||||||
if size == 0 {
|
socks5.WithDial(vt.Tnet.DialContext),
|
||||||
return ctx, nil, errors.New("no address found for: " + name)
|
socks5.WithResolver(vt),
|
||||||
|
socks5.WithAuthMethods(authMethods),
|
||||||
|
socks5.WithBufferPool(bufferpool.NewPool(256 * 1024)),
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := addrs[rand.Intn(size)]
|
server := socks5.NewServer(options...)
|
||||||
ip := net.ParseIP(addr)
|
|
||||||
if ip == nil {
|
|
||||||
return ctx, nil, errors.New("invalid address: " + addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx, ip, err
|
if err := server.ListenAndServe("tcp", config.BindAddress); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
|
// SpawnRoutine spawns a http server.
|
||||||
conf := &socks5.Config{Dial: vt.tnet.DialContext, Resolver: vt}
|
func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) {
|
||||||
if username := config.Username; username != "" {
|
server := &HTTPServer{
|
||||||
validator := CredentialValidator{username: username}
|
config: config,
|
||||||
validator.password = config.Password
|
dial: vt.Tnet.Dial,
|
||||||
conf.Credentials = validator
|
auth: CredentialValidator{config.Username, config.Password},
|
||||||
}
|
}
|
||||||
server, err := socks5.New(conf)
|
if config.Username != "" || config.Password != "" {
|
||||||
if err != nil {
|
server.authRequired = true
|
||||||
log.Panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.ListenAndServe("tcp", config.BindAddress); err != nil {
|
if err := server.ListenAndServe("tcp", config.BindAddress); err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valid checks the authentication data in CredentialValidator and compare them
|
||||||
|
// to username and password in constant time.
|
||||||
func (c CredentialValidator) Valid(username, password string) bool {
|
func (c CredentialValidator) Valid(username, password string) bool {
|
||||||
u := subtle.ConstantTimeCompare([]byte(c.username), []byte(username))
|
u := subtle.ConstantTimeCompare([]byte(c.username), []byte(username))
|
||||||
p := subtle.ConstantTimeCompare([]byte(c.password), []byte(password))
|
p := subtle.ConstantTimeCompare([]byte(c.password), []byte(password))
|
||||||
return u&p == 1
|
return u&p == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func connForward(bufSize int, from io.ReadWriteCloser, to io.ReadWriteCloser) {
|
// connForward copy data from `from` to `to`
|
||||||
buf := make([]byte, bufSize)
|
func connForward(from io.ReadWriteCloser, to io.ReadWriteCloser) {
|
||||||
_, err := io.CopyBuffer(to, from, buf)
|
defer from.Close()
|
||||||
|
defer to.Close()
|
||||||
|
|
||||||
|
_, err := io.Copy(to, from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
to.Close()
|
errorLogger.Printf("Cannot forward traffic: %s\n", err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpClientForward(tnet *netstack.Net, target *net.TCPAddr, conn net.Conn) {
|
// tcpClientForward starts a new connection via wireguard and forward traffic from `conn`
|
||||||
sconn, err := tnet.DialTCP(target)
|
func tcpClientForward(vt *VirtualTun, raddr *addressPort, conn net.Conn) {
|
||||||
|
target, err := vt.resolveToAddrPort(raddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] TCP Client Tunnel to %s: %s\n", target, err.Error())
|
errorLogger.Printf("TCP Server Tunnel to %s: %s\n", target, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go connForward(1024, sconn, conn)
|
tcpAddr := TCPAddrFromAddrPort(*target)
|
||||||
go connForward(1024, conn, sconn)
|
|
||||||
|
sconn, err := vt.Tnet.DialTCP(tcpAddr)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("TCP Client Tunnel to %s: %s\n", target, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go connForward(sconn, conn)
|
||||||
|
go connForward(conn, sconn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// STDIOTcpForward starts a new connection via wireguard and forward traffic from `conn`
|
||||||
|
func STDIOTcpForward(vt *VirtualTun, raddr *addressPort) {
|
||||||
|
target, err := vt.resolveToAddrPort(raddr)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Name resolution error for %s: %s\n", raddr.address, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// os.Stdout has previously been remapped to stderr, se we can't use it
|
||||||
|
stdout, err := os.OpenFile("/dev/stdout", os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to open /dev/stdout: %s\n", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpAddr := TCPAddrFromAddrPort(*target)
|
||||||
|
sconn, err := vt.Tnet.DialTCP(tcpAddr)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("TCP Client Tunnel to %s (%s): %s\n", target, tcpAddr, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go connForward(os.Stdin, sconn)
|
||||||
|
go connForward(sconn, stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpawnRoutine spawns a local TCP server which acts as a proxy to the specified target
|
||||||
func (conf *TCPClientTunnelConfig) SpawnRoutine(vt *VirtualTun) {
|
func (conf *TCPClientTunnelConfig) SpawnRoutine(vt *VirtualTun) {
|
||||||
raddr, err := vt.ResolveAddrPort(conf.Target)
|
raddr, err := parseAddressPort(conf.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
tcpAddr := TCPAddrFromAddrPort(*raddr)
|
|
||||||
|
|
||||||
server, err := net.ListenTCP("tcp", conf.BindAddress)
|
server, err := net.ListenTCP("tcp", conf.BindAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := server.Accept()
|
conn, err := server.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
go tcpClientForward(vt.tnet, tcpAddr, conn)
|
go tcpClientForward(vt, raddr, conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpServerForward(target *net.TCPAddr, conn net.Conn) {
|
// SpawnRoutine connects to the specified target and plumbs it to STDIN / STDOUT
|
||||||
sconn, err := net.DialTCP("tcp", nil, target)
|
func (conf *STDIOTunnelConfig) SpawnRoutine(vt *VirtualTun) {
|
||||||
|
raddr, err := parseAddressPort(conf.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[ERROR] TCP Server Tunnel to %s: %s\n", target, err.Error())
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go STDIOTcpForward(vt, raddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tcpServerForward starts a new connection locally and forward traffic from `conn`
|
||||||
|
func tcpServerForward(vt *VirtualTun, raddr *addressPort, conn net.Conn) {
|
||||||
|
target, err := vt.resolveToAddrPort(raddr)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("TCP Server Tunnel to %s: %s\n", target, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go connForward(1024, sconn, conn)
|
tcpAddr := TCPAddrFromAddrPort(*target)
|
||||||
go connForward(1024, conn, sconn)
|
|
||||||
|
sconn, err := net.DialTCP("tcp", nil, tcpAddr)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("TCP Server Tunnel to %s: %s\n", target, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go connForward(sconn, conn)
|
||||||
|
go connForward(conn, sconn)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SpawnRoutine spawns a TCP server on wireguard which acts as a proxy to the specified target
|
||||||
func (conf *TCPServerTunnelConfig) SpawnRoutine(vt *VirtualTun) {
|
func (conf *TCPServerTunnelConfig) SpawnRoutine(vt *VirtualTun) {
|
||||||
raddr, err := vt.ResolveAddrPort(conf.Target)
|
raddr, err := parseAddressPort(conf.Target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
tcpAddr := TCPAddrFromAddrPort(*raddr)
|
|
||||||
|
|
||||||
addr := &net.TCPAddr{Port: conf.ListenPort}
|
addr := &net.TCPAddr{Port: conf.ListenPort}
|
||||||
server, err := vt.tnet.ListenTCP(addr)
|
server, err := vt.Tnet.ListenTCP(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := server.Accept()
|
conn, err := server.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
go tcpServerForward(tcpAddr, conn)
|
go tcpServerForward(vt, raddr, conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d VirtualTun) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("Health metric request: %s\n", r.URL.Path)
|
||||||
|
switch path.Clean(r.URL.Path) {
|
||||||
|
case "/readyz":
|
||||||
|
body, err := json.Marshal(d.PingRecord)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to get device metrics: %s\n", err.Error())
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := http.StatusOK
|
||||||
|
for _, record := range d.PingRecord {
|
||||||
|
lastPong := time.Unix(int64(record), 0)
|
||||||
|
// +2 seconds to account for the time it takes to ping the IP
|
||||||
|
if time.Since(lastPong) > time.Duration(d.Conf.CheckAliveInterval+2)*time.Second {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_, _ = w.Write(body)
|
||||||
|
_, _ = w.Write([]byte("\n"))
|
||||||
|
case "/metrics":
|
||||||
|
get, err := d.Dev.IpcGet()
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to get device metrics: %s\n", err.Error())
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, peer := range strings.Split(get, "\n") {
|
||||||
|
pair := strings.SplitN(peer, "=", 2)
|
||||||
|
if len(pair) != 2 {
|
||||||
|
buf.WriteString(peer)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pair[0] == "private_key" || pair[0] == "preshared_key" {
|
||||||
|
pair[1] = "REDACTED"
|
||||||
|
}
|
||||||
|
buf.WriteString(pair[0])
|
||||||
|
buf.WriteString("=")
|
||||||
|
buf.WriteString(pair[1])
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(buf.Bytes())
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d VirtualTun) pingIPs() {
|
||||||
|
for _, addr := range d.Conf.CheckAlive {
|
||||||
|
socket, err := d.Tnet.Dial("ping", addr.String())
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to ping %s: %s\n", addr, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, 16)
|
||||||
|
_, _ = srand.Read(data)
|
||||||
|
|
||||||
|
requestPing := icmp.Echo{
|
||||||
|
Seq: rand.Intn(1 << 16),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
var icmpBytes []byte
|
||||||
|
if addr.Is4() {
|
||||||
|
icmpBytes, _ = (&icmp.Message{Type: ipv4.ICMPTypeEcho, Code: 0, Body: &requestPing}).Marshal(nil)
|
||||||
|
} else if addr.Is6() {
|
||||||
|
icmpBytes, _ = (&icmp.Message{Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &requestPing}).Marshal(nil)
|
||||||
|
} else {
|
||||||
|
errorLogger.Printf("Failed to ping %s: invalid address: %s\n", addr, addr.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = socket.SetReadDeadline(time.Now().Add(time.Duration(d.Conf.CheckAliveInterval) * time.Second))
|
||||||
|
_, err = socket.Write(icmpBytes)
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to ping %s: %s\n", addr, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := addr
|
||||||
|
go func() {
|
||||||
|
n, err := socket.Read(icmpBytes[:])
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to read ping response from %s: %s\n", addr, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
replyPacket, err := icmp.ParseMessage(1, icmpBytes[:n])
|
||||||
|
if err != nil {
|
||||||
|
errorLogger.Printf("Failed to parse ping response from %s: %s\n", addr, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Is4() {
|
||||||
|
replyPing, ok := replyPacket.Body.(*icmp.Echo)
|
||||||
|
if !ok {
|
||||||
|
errorLogger.Printf("Failed to parse ping response from %s: invalid reply type: %s\n", addr, replyPacket.Type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !bytes.Equal(replyPing.Data, requestPing.Data) || replyPing.Seq != requestPing.Seq {
|
||||||
|
errorLogger.Printf("Failed to parse ping response from %s: invalid ping reply: %v\n", addr, replyPing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Is6() {
|
||||||
|
replyPing, ok := replyPacket.Body.(*icmp.RawBody)
|
||||||
|
if !ok {
|
||||||
|
errorLogger.Printf("Failed to parse ping response from %s: invalid reply type: %s\n", addr, replyPacket.Type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seq := binary.BigEndian.Uint16(replyPing.Data[2:4])
|
||||||
|
pongBody := replyPing.Data[4:]
|
||||||
|
if !bytes.Equal(pongBody, requestPing.Data) || int(seq) != requestPing.Seq {
|
||||||
|
errorLogger.Printf("Failed to parse ping response from %s: invalid ping reply: %v\n", addr, replyPing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.PingRecordLock.Lock()
|
||||||
|
d.PingRecord[addr.String()] = uint64(time.Now().Unix())
|
||||||
|
d.PingRecordLock.Unlock()
|
||||||
|
|
||||||
|
defer socket.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d VirtualTun) StartPingIPs() {
|
||||||
|
for _, addr := range d.Conf.CheckAlive {
|
||||||
|
d.PingRecord[addr.String()] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
d.pingIPs()
|
||||||
|
time.Sleep(time.Duration(d.Conf.CheckAliveInterval) * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
35
systemd/README.md
Normal file
35
systemd/README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Running wireproxy with systemd
|
||||||
|
|
||||||
|
If you're on a systemd-based distro, you'll most likely want to run Wireproxy as a systemd unit.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
# Setting up the unit
|
||||||
|
|
||||||
|
1. Copy the `wireproxy.service` file from this directory to `/etc/systemd/system/`, or use the following cURL command to download it:
|
||||||
|
```bash
|
||||||
|
curl https://raw.githubusercontent.com/pufferffish/wireproxy/master/systemd/wireproxy.service | sudo tee /etc/systemd/system/wireproxy.service
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If necessary, customize the unit.
|
||||||
|
|
||||||
|
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:
|
||||||
|
```service
|
||||||
|
LoadCredential=conf:/opt/myfiles/wireproxy.conf
|
||||||
|
ExecStartPre=/usr/bin/wireproxy -n -c ${CREDENTIALS_DIRECTORY}/conf
|
||||||
|
ExecStart=/usr/bin/wireproxy -c ${CREDENTIALS_DIRECTORY}/conf
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Reload systemd and enable the unit.
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now wireproxy.service
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Make sure it's working correctly.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
# Additional notes
|
||||||
|
|
||||||
|
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).
|
46
systemd/wireproxy.service
Normal file
46
systemd/wireproxy.service
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Wireproxy socks5/http tunnel
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=wireproxy
|
||||||
|
Group=wireproxy
|
||||||
|
SyslogIdentifier=wireproxy
|
||||||
|
Type=simple
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30s
|
||||||
|
|
||||||
|
DynamicUser=yes
|
||||||
|
LoadCredential=conf:/etc/wireproxy.conf
|
||||||
|
ExecStartPre=/opt/wireproxy/wireproxy -n -c ${CREDENTIALS_DIRECTORY}/conf
|
||||||
|
ExecStart=/opt/wireproxy/wireproxy -c ${CREDENTIALS_DIRECTORY}/conf
|
||||||
|
|
||||||
|
# Required if <1024 port
|
||||||
|
#AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
LimitNPROC=64
|
||||||
|
LockPersonality=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateUsers=true
|
||||||
|
ProcSubset=pid
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectProc=invisible
|
||||||
|
ProtectSystem=strict
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
SystemCallFilter=@system-service @sandbox
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
28
test_config.sh
Executable file
28
test_config.sh
Executable file
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
exec 3<>/dev/tcp/demo.wireguard.com/42912
|
||||||
|
privatekey="$(wg genkey)"
|
||||||
|
wg pubkey <<<"$privatekey" >&3
|
||||||
|
IFS=: read -r status server_pubkey server_port internal_ip <&3
|
||||||
|
[[ $status == OK ]]
|
||||||
|
cat >test.conf <<EOL
|
||||||
|
[Interface]
|
||||||
|
Address = $internal_ip/32
|
||||||
|
PrivateKey = $privatekey
|
||||||
|
DNS = 8.8.8.8
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = $server_pubkey
|
||||||
|
Endpoint = demo.wireguard.com:$server_port
|
||||||
|
|
||||||
|
[Socks5]
|
||||||
|
BindAddress = 127.0.0.1:64423
|
||||||
|
|
||||||
|
[http]
|
||||||
|
BindAddress = 127.0.0.1:64424
|
||||||
|
|
||||||
|
[http]
|
||||||
|
BindAddress = 127.0.0.1:64425
|
||||||
|
Username = peter
|
||||||
|
Password = hunter123
|
||||||
|
EOL
|
25
util.go
Normal file
25
util.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package wireproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const space = " "
|
||||||
|
|
||||||
|
func responseWith(req *http.Request, statusCode int) *http.Response {
|
||||||
|
statusText := http.StatusText(statusCode)
|
||||||
|
body := "wireproxy:" + space + req.Proto + space + strconv.Itoa(statusCode) + space + statusText + "\r\n"
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Status: statusText,
|
||||||
|
Proto: req.Proto,
|
||||||
|
ProtoMajor: req.ProtoMajor,
|
||||||
|
ProtoMinor: req.ProtoMinor,
|
||||||
|
Header: http.Header{},
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString(body)),
|
||||||
|
}
|
||||||
|
}
|
76
wireguard.go
76
wireguard.go
|
@ -1,45 +1,77 @@
|
||||||
package wireproxy
|
package wireproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"golang.zx2c4.com/go118/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/MakeNowJust/heredoc/v2"
|
||||||
"golang.zx2c4.com/wireguard/conn"
|
"golang.zx2c4.com/wireguard/conn"
|
||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DeviceSetting contains the parameters for setting up a tun interface
|
||||||
type DeviceSetting struct {
|
type DeviceSetting struct {
|
||||||
ipcRequest string
|
IpcRequest string
|
||||||
dns []netip.Addr
|
DNS []netip.Addr
|
||||||
deviceAddr []netip.Addr
|
DeviceAddr []netip.Addr
|
||||||
mtu int
|
MTU int
|
||||||
}
|
}
|
||||||
|
|
||||||
func createIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) {
|
// CreateIPCRequest serialize the config into an IPC request and DeviceSetting
|
||||||
request := fmt.Sprintf(`private_key=%s
|
func CreateIPCRequest(conf *DeviceConfig) (*DeviceSetting, error) {
|
||||||
public_key=%s
|
var request bytes.Buffer
|
||||||
endpoint=%s
|
|
||||||
persistent_keepalive_interval=%d
|
|
||||||
preshared_key=%s
|
|
||||||
allowed_ip=0.0.0.0/0`, conf.SelfSecretKey, conf.PeerPublicKey, conf.PeerEndpoint, conf.KeepAlive, conf.PreSharedKey)
|
|
||||||
|
|
||||||
setting := &DeviceSetting{ipcRequest: request, dns: conf.DNS, deviceAddr: conf.SelfEndpoint, mtu: conf.MTU}
|
request.WriteString(fmt.Sprintf("private_key=%s\n", conf.SecretKey))
|
||||||
|
|
||||||
|
if conf.ListenPort != nil {
|
||||||
|
request.WriteString(fmt.Sprintf("listen_port=%d\n", *conf.ListenPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range conf.Peers {
|
||||||
|
request.WriteString(fmt.Sprintf(heredoc.Doc(`
|
||||||
|
public_key=%s
|
||||||
|
persistent_keepalive_interval=%d
|
||||||
|
preshared_key=%s
|
||||||
|
`),
|
||||||
|
peer.PublicKey, peer.KeepAlive, peer.PreSharedKey,
|
||||||
|
))
|
||||||
|
if peer.Endpoint != nil {
|
||||||
|
request.WriteString(fmt.Sprintf("endpoint=%s\n", *peer.Endpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peer.AllowedIPs) > 0 {
|
||||||
|
for _, ip := range peer.AllowedIPs {
|
||||||
|
request.WriteString(fmt.Sprintf("allowed_ip=%s\n", ip.String()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
request.WriteString(heredoc.Doc(`
|
||||||
|
allowed_ip=0.0.0.0/0
|
||||||
|
allowed_ip=::0/0
|
||||||
|
`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setting := &DeviceSetting{IpcRequest: request.String(), DNS: conf.DNS, DeviceAddr: conf.Endpoint, MTU: conf.MTU}
|
||||||
return setting, nil
|
return setting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartWireguard(conf *DeviceConfig) (*VirtualTun, error) {
|
// StartWireguard creates a tun interface on netstack given a configuration
|
||||||
setting, err := createIPCRequest(conf)
|
func StartWireguard(conf *DeviceConfig, logLevel int) (*VirtualTun, error) {
|
||||||
|
setting, err := CreateIPCRequest(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tun, tnet, err := netstack.CreateNetTUN(setting.deviceAddr, setting.dns, setting.mtu)
|
tun, tnet, err := netstack.CreateNetTUN(setting.DeviceAddr, setting.DNS, setting.MTU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelVerbose, ""))
|
dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(logLevel, ""))
|
||||||
err = dev.IpcSet(setting.ipcRequest)
|
err = dev.IpcSet(setting.IpcRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -50,7 +82,11 @@ func StartWireguard(conf *DeviceConfig) (*VirtualTun, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &VirtualTun{
|
return &VirtualTun{
|
||||||
tnet: tnet,
|
Tnet: tnet,
|
||||||
systemDNS: len(setting.dns) == 0,
|
Dev: dev,
|
||||||
|
Conf: conf,
|
||||||
|
SystemDNS: len(setting.DNS) == 0,
|
||||||
|
PingRecord: make(map[string]uint64),
|
||||||
|
PingRecordLock: new(sync.Mutex),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue