From ee1489880596e5fe335d86e04b6d9678abb104c4 Mon Sep 17 00:00:00 2001 From: Wyatt Gill Date: Tue, 14 Jun 2022 19:20:14 -0500 Subject: [PATCH] Overhaul --- .dockerignore | 2 +- .github/workflows/publish.yml | 85 ++--- .gitignore | 2 +- .pre-commit-config.yaml | 16 +- CHANGELOG.md | 9 + Dockerfile | 31 +- README.md | 73 ++-- build-variables | 1 + build.py | 20 + .../http-proxy.conf} | 11 +- data/config/socks-proxy.conf | 26 ++ data/scripts/dante_wrapper.sh | 9 - data/scripts/entry.sh | 341 +++++++----------- data/scripts/run-http-proxy.sh | 30 ++ data/scripts/run-socks-proxy.sh | 26 ++ data/scripts/tinyproxy_wrapper.sh | 20 - data/sockd.conf | 65 ---- docker-compose.yml | 30 +- 18 files changed, 372 insertions(+), 425 deletions(-) create mode 100644 build-variables create mode 100755 build.py rename data/{tinyproxy.conf => config/http-proxy.conf} (76%) create mode 100644 data/config/socks-proxy.conf delete mode 100755 data/scripts/dante_wrapper.sh create mode 100755 data/scripts/run-http-proxy.sh create mode 100755 data/scripts/run-socks-proxy.sh delete mode 100755 data/scripts/tinyproxy_wrapper.sh delete mode 100644 data/sockd.conf diff --git a/.dockerignore b/.dockerignore index d37a05c..549a195 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,2 @@ * -!data/ \ No newline at end of file +!data/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0d5bed5..035a246 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish on: push: tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+' env: IMAGE_NAME: openvpn-client @@ -13,47 +13,42 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check out repository - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Log in to registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Create tags - id: tags - uses: docker/metadata-action@v3 - with: - images: ghcr.io/wfg/openvpn-client - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - - name: Create build args - id: build-args - run: | - ref=${{ github.ref }} - vpatch=${ref##refs/*/} - patch=${vpatch#v} - echo "::set-output name=date::$(date --utc --iso-8601=seconds)" - echo "::set-output name=version::$patch" - - - name: Build and push - uses: docker/build-push-action@v2 - with: - platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 - tags: ${{ steps.tags.outputs.tags }} - build-args: | - BUILD_DATE=${{ steps.build-args.outputs.date }} - IMAGE_VERSION=${{ steps.build-args.outputs.version }} - push: true + - uses: actions/checkout@v3 + + - run: cat build-variables >> $GITHUB_ENV + + - uses: docker/setup-qemu-action@v2 + + - uses: docker/setup-buildx-action@v2 + + - uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: tags + uses: docker/metadata-action@v4 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} + + - id: build-args + run: | + ref=${{ github.ref }} + vpatch=${ref##refs/*/} + patch=${vpatch#v} + echo "::set-output name=date::$(date --utc --iso-8601=seconds)" + echo "::set-output name=version::$patch" + + - uses: docker/build-push-action@v3 + with: + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 + tags: ${{ steps.tags.outputs.tags }} + build-args: | + BUILD_DATE=${{ steps.build-args.outputs.date }} + IMAGE_VERSION=${{ steps.build-args.outputs.version }} + push: true diff --git a/.gitignore b/.gitignore index aa052db..a47f082 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -local/ \ No newline at end of file +.local/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 793b942..de4e43a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ repos: - - repo: https://github.com/norwoodj/helm-docs - rev: v1.6.0 - hooks: - - id: helm-docs - args: - - --chart-search-root=chart - - --template-files=./_templates.gotmpl - - --template-files=README.md.gotmpl +- repo: https://github.com/norwoodj/helm-docs + rev: v1.6.0 + hooks: + - id: helm-docs + args: + - --chart-search-root=chart + - --template-files=./_templates.gotmpl + - --template-files=README.md.gotmpl diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef93f6..0899d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Version 3.0.0 - 2022-06-14 +### Changed +- Refactored scripts + - Renamed a lot of variables ([PLEASE see docs](README.md#environment-variables)) + - Updated logic used to select the OpenVPN configuration file + - Switched to `nftables` +- Updated to Alpine 3.16 +- Fixed outdated proxy configuration files + ## Version 2.1.0 - 2022-03-06 ### Added - `VPN_CONFIG_PATTERN` environment variable. diff --git a/Dockerfile b/Dockerfile index b65d232..5d7d196 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,27 @@ -FROM alpine:3.15 - -ARG IMAGE_VERSION -ARG BUILD_DATE - -LABEL org.opencontainers.image.created="$BUILD_DATE" -LABEL org.opencontainers.image.source="github.com/wfg/docker-openvpn-client" -LABEL org.opencontainers.image.version="$IMAGE_VERSION" - -ENV KILL_SWITCH=on \ - VPN_LOG_LEVEL=3 \ - HTTP_PROXY=off \ - SOCKS_PROXY=off +FROM alpine:3.16 RUN apk add --no-cache \ bash \ bind-tools \ dante-server \ + nftables \ openvpn \ tinyproxy -RUN mkdir -p /data/vpn +COPY data/ /data/ -COPY data/ /data +ENV KILL_SWITCH=on +ENV USE_VPN_DNS=on +ENV VPN_LOG_LEVEL=3 + +ARG BUILD_DATE +ARG IMAGE_VERSION + +LABEL build-date=$BUILD_DATE +LABEL image-version=$IMAGE_VERSION HEALTHCHECK CMD ping -c 3 1.1.1.1 || exit 1 -ENTRYPOINT ["/data/scripts/entry.sh"] +WORKDIR /data + +ENTRYPOINT [ "scripts/entry.sh" ] diff --git a/README.md b/README.md index c5b1752..196ff54 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # OpenVPN Client for Docker ## What is this and what does it do? [`ghcr.io/wfg/openvpn-client`](https://github.com/users/wfg/packages/container/package/openvpn-client) is a containerized OpenVPN client. -It has a kill switch built with `iptables` that kills Internet connectivity to the container if the VPN tunnel goes down for any reason. +It has a kill switch built with `nftables` that kills Internet connectivity to the container if the VPN tunnel goes down for any reason. It also includes an HTTP proxy server ([Tinyproxy](https://tinyproxy.github.io/)) and a SOCKS proxy server ([Dante](https://www.inet.no/dante/index.html)). This allows hosts and non-containerized applications to use the VPN without having to run VPN clients on those hosts. @@ -14,10 +14,6 @@ If you find something that doesn't work or have an idea for a new feature, issue Having a containerized VPN client lets you use container networking to easily choose which applications you want using the VPN instead of having to set up split tunnelling. It also keeps you from having to install an OpenVPN client on the underlying host. -The idea for this image came from a similar project by [qdm12](https://github.com/qdm12) that has since evolved into something bigger and more complex than I wanted to use. -I decided to dissect it and take it in my own direction. -I plan to keep everything here well-documented so this is not only a learning experience for me, but also anyone else that uses it. - ## How do I use it? ### Getting the image You can either pull it from GitHub Container Registry or build it yourself. @@ -62,22 +58,28 @@ services: restart: unless-stopped ``` -#### Environment variables (alphabetical) +#### Environment variables | Variable | Default (blank is unset) | Description | | --- | --- | --- | -| `HTTP_PROXY` | `off` | The on/off status of Tinyproxy, the built-in HTTP proxy server. To enable, set to `on`. Any other value (including unset) will cause the proxy server to not start. It listens on port 8080. | -| `KEEP_DNS_UNCHANGED` | `off` | If `off`, the VPN server you connect to might override the DNS server used by the container. If `on`, the container will always use the DNS settings it had before connecting to the server. Usually, containers use the Docker internal DNS server by default. It allows to resolve IP addreses from container names, service names and Docker-specific names such as `host.docker.internal`. If DNS server is overriden, you won't be able to resolve such names. | -| `KILL_SWITCH` | `on` | The on/off status of the network kill switch. | -| `LISTEN_ON` | | Address the proxies will be listening on. Set to `0.0.0.0` to listen on all IP addresses. | -| `PROXY_PASSWORD` | | Credentials for accessing the proxies. If `PROXY_PASSWORD` is specified, you must also specify `PROXY_USERNAME`. | -| `PROXY_PASSWORD_SECRET` | | Docker secrets that contain the credentials for accessing the proxies. If `PROXY_PASSWORD_SECRET` is specified, you must also specify `PROXY_USERNAME_SECRET`. | -| `PROXY_USERNAME` | | Credentials for accessing the proxies. If `PROXY_USERNAME` is specified, you must also specify `PROXY_PASSWORD`. | -| `PROXY_USERNAME_SECRET` | | Docker secrets that contain the credentials for accessing the proxies. If `PROXY_USERNAME_SECRET` is specified, you must also specify `PROXY_PASSWORD_SECRET`. | -| `SOCKS_PROXY` | `off` | The on/off status of Dante, the built-in SOCKS proxy server. To enable, set to `on`. Any other value (including unset) will cause the proxy server to not start. It listens on port 1080. | -| `SUBNETS` | | A list of one or more comma-separated subnets (e.g. `192.168.0.0/24,192.168.1.0/24`) to allow outside of the VPN tunnel. | +| `USE_VPN_DNS` | `on` | Whether or not to use the DNS servers pushed from the VPN server. It's best to leave this enabled unless you have a good reason to disable it. | +| `VPN_CONFIG_FILE` | | The OpenVPN configuration file to use. If unset, the `VPN_CONFIG_PATTERN` is used. | +| `VPN_CONFIG_PATTERN` | | The search pattern to use when looking for an OpenVPN configuration file. If unset, the search will include `*.conf` and `*.ovpn`. | | `VPN_AUTH_SECRET` | | Docker secret that contain the credentials for accessing the VPN. | -| `VPN_CONFIG_FILE` | | The OpenVPN config file to use. If this is unset, the first file with the extension .conf will be used. | -| `VPN_LOG_LEVEL` | `3` | OpenVPN verbosity (`1`-`11`) | +| `VPN_LOG_LEVEL` | `3` | OpenVPN logging verbosity (`1`-`11`) | +| `SUBNETS` | | A list of one or more comma-separated subnets (e.g. `192.168.0.0/24,192.168.1.0/24`) to allow outside of the VPN tunnel. | +| `KILL_SWITCH` | `on` | Whether or not to enable the network kill switch. | +| `HTTP_PROXY` | | Whether or not to enable the built-in HTTP proxy server. To enable, set to any "truthy" value (see below the table). Any other value (including unset) will cause the proxy server to not run. It listens on port 8080. | +| `HTTP_PROXY_USERNAME` | | Credentials for accessing the HTTP proxy. If `HTTP_PROXY_USERNAME` is specified, you should also specify `HTTP_PROXY_PASSWORD`. | +| `HTTP_PROXY_PASSWORD` | | Credentials for accessing the HTTP proxy. If `HTTP_PROXY_PASSWORD` is specified, you should also specify `HTTP_PROXY_USERNAME`. | +| `HTTP_PROXY_USERNAME_SECRET` | | Docker secrets that contain the credentials for accessing the HTTP proxy. If `HTTP_PROXY_USERNAME_SECRET` is specified, you should also specify `HTTP_PROXY_PASSWORD_SECRET`. | +| `HTTP_PROXY_PASSWORD_SECRET` | | Docker secrets that contain the credentials for accessing the HTTP proxy. If `HTTP_PROXY_PASSWORD_SECRET` is specified, you should also specify `HTTP_PROXY_USERNAME_SECRET`. | +| `SOCKS_PROXY` | | Whether or not to enable the built-in SOCKS proxy server. To enable, set to any "truthy" value (see below the table). Any other value (including unset) will cause the proxy server to not run. It listens on port 1080. | +| `SOCKS_LISTEN_ON` | | Address the proxies will be listening on. Set to `0.0.0.0` to listen on all IP addresses. | +| `SOCKS_PROXY_USERNAME` | | Credentials for accessing the proxies. If `SOCKS_PROXY_USERNAME` is specified, you should also specify `SOCKS_PROXY_PASSWORD`. | +| `SOCKS_PROXY_PASSWORD` | | Credentials for accessing the proxies. If `SOCKS_PROXY_PASSWORD` is specified, you should also specify `SOCKS_PROXY_USERNAME`. | +| `SOCKS_PROXY_USERNAME_SECRET` | | Docker secrets that contain the credentials for accessing the proxies. If `SOCKS_PROXY_USERNAME_SECRET` is specified, you should also specify `SOCKS_PROXY_PASSWORD_SECRET`. | +| `SOCKS_PROXY_PASSWORD_SECRET` | | Docker secrets that contain the credentials for accessing the proxies. If `SOCKS_PROXY_PASSWORD_SECRET` is specified, you should also specify `SOCKS_PROXY_USERNAME_SECRET`. | +"Truthy" values are the following: `true`, `t`, `yes`, `y`, `1`, `on`, `enable`, or `enabled`. ##### Environment variable considerations ###### `HTTP_PROXY` and `SOCKS_PROXY` @@ -90,25 +92,10 @@ ports: - :1080 ``` -##### `PROXY_USERNAME_SECRET`, `PROXY_PASSWORD_SECRET`, and `VPN_AUTH_SECRET` +##### `*_PROXY_USERNAME_SECRET`, `*_PROXY_PASSWORD_SECRET`, and `VPN_AUTH_SECRET` Compose has support for [Docker secrets](https://docs.docker.com/engine/swarm/secrets/#use-secrets-in-compose). See the [Compose file](docker-compose.yml) in this repository for example usage of passing proxy credentials as Docker secrets. -### VPN Authentication -To provide the VPN user and password credentials, create a file called `passfile` in the config folder being mounted, right next to the vpn .conf (or .ovpn) file. - -In the passfile, enter the username in the first line and password in the second line. For example: -``` -gilbert -p@sswd123 -``` -Now in the vpn configuration file, such as my_vpn.ovpn, create a new line and enter the following: -``` -auth-user-pass passfile -``` -That should be enough. You can refer to this link for more details: https://help.yeastar.com/en/s-series/topic/openvpn-username-password-authentication.html#openvpn-create-account-password-for-each-client__section_uyg_sps_33b - - ### Using with other containers Once you have your `openvpn-client` container up and running, you can tell other containers to use `openvpn-client`'s network stack which gives them the ability to utilize the VPN tunnel. There are a few ways to accomplish this depending how how your container is created. @@ -137,3 +124,21 @@ You should see an IP address owned by your VPN provider. ```bash docker run --rm -it --network=container:openvpn-client alpine wget -qO - ifconfig.me ``` + +### Troubleshooting +#### VPN authentication +Your OpenVPN configuration file may not come with authentication baked in. +To provide OpenVPN the necessary credentials, create a file (any name will work, but this example will use `credentials.txt`) next to the OpenVPN configuration file with your username on the first line and your password on the second line. + +For example: +``` +vpn_username +vpn_password +``` + +In the OpenVPN configuration file, add the following line: +``` +auth-user-pass credentials.txt +``` + +This will tell OpenVPN to read `credentials.txt` whenever it needs credentials. diff --git a/build-variables b/build-variables new file mode 100644 index 0000000..a0b6d44 --- /dev/null +++ b/build-variables @@ -0,0 +1 @@ +IMAGE_NAME=ghcr.io/wfg/openvpn-client diff --git a/build.py b/build.py new file mode 100755 index 0000000..0978ceb --- /dev/null +++ b/build.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import argparse +import datetime +import subprocess + + +parser = argparse.ArgumentParser() +parser.add_argument('image_version', type=str) +args = parser.parse_args() + +docker_build_cmd = [ + 'docker', 'build', + '--build-arg', f'BUILD_DATE={str(datetime.datetime.now())}', + '--build-arg', f'IMAGE_VERSION={args.image_version}', + '--tag', f'ghcr.io/wfg/openvpn-client:{args.image_version}', + '--tag', 'ghcr.io/wfg/openvpn-client:latest', + '.', +] +subprocess.run(docker_build_cmd) diff --git a/data/tinyproxy.conf b/data/config/http-proxy.conf similarity index 76% rename from data/tinyproxy.conf rename to data/config/http-proxy.conf index 222ff5c..3fe0637 100644 --- a/data/tinyproxy.conf +++ b/data/config/http-proxy.conf @@ -4,16 +4,11 @@ Group tinyproxy Port 8080 Listen Bind - Timeout 600 +LogLevel Info +LogFile "/var/log/tinyproxy/tinyproxy.log" DefaultErrorFile "/usr/share/tinyproxy/default.html" StatFile "/usr/share/tinyproxy/stats.html" -LogFile "/var/log/tinyproxy/tinyproxy.log" - -LogLevel Info -MaxClients 100 -MinSpareServers 5 -MaxSpareServers 15 -StartServers 10 \ No newline at end of file +DisableViaHeader yes diff --git a/data/config/socks-proxy.conf b/data/config/socks-proxy.conf new file mode 100644 index 0000000..50494c6 --- /dev/null +++ b/data/config/socks-proxy.conf @@ -0,0 +1,26 @@ +logoutput: /var/log/sockd.log +errorlog: stderr + +internal: eth0 port = 1080 +external: tun0 + +socksmethod: none + +user.unprivileged: sockd + +client pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + log: error connect disconnect +} + +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + command: bind connect udpassociate + log: error connect disconnect +} + +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 + command: bindreply udpreply + log: error connect disconnect +} diff --git a/data/scripts/dante_wrapper.sh b/data/scripts/dante_wrapper.sh deleted file mode 100755 index 4e9d566..0000000 --- a/data/scripts/dante_wrapper.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo -e "Running Dante SOCKS proxy server.\n" - -until ip link show tun0 2>&1 | grep -qv "does not exist"; do - sleep 1 -done - -sockd -f /data/sockd.conf diff --git a/data/scripts/entry.sh b/data/scripts/entry.sh index c52b104..df2886e 100755 --- a/data/scripts/entry.sh +++ b/data/scripts/entry.sh @@ -1,242 +1,177 @@ #!/usr/bin/env bash -cleanup() { - # When you run `docker stop` or any equivalent, a SIGTERM signal is sent to PID 1. - # A process running as PID 1 inside a container is treated specially by Linux: - # it ignores any signal with the default action. As a result, the process will - # not terminate on SIGINT or SIGTERM unless it is coded to do so. Because of this, - # I've defined behavior for when SIGINT and SIGTERM is received. - if [[ -n "$openvpn_child" ]]; then - echo "Stopping OpenVPN..." - kill -TERM "$openvpn_child" - fi +set -e + - sleep 1 - rm "$config_file_modified" - echo "Exiting." - exit 0 +is_enabled() { + [[ ${1,,} =~ ^(true|t|yes|y|1|on|enable|enabled)$ ]] } -# OpenVPN log levels are 1-11. -# shellcheck disable=SC2153 -if [[ "$VPN_LOG_LEVEL" -lt 1 || "$VPN_LOG_LEVEL" -gt 11 ]]; then - echo "WARNING: Invalid log level $VPN_LOG_LEVEL. Setting to default." - vpn_log_level=3 -else - vpn_log_level=$VPN_LOG_LEVEL -fi +mkdir -p /data/{config,scripts,vpn} echo " ----- Running with the following variables ---- -Kill switch: ${KILL_SWITCH:-off} -HTTP proxy: ${HTTP_PROXY:-off} -SOCKS proxy: ${SOCKS_PROXY:-off} -Keep DNS settings unchanged: ${KEEP_DNS_UNCHANGED:-off} -Proxy username secret: ${PROXY_PASSWORD_SECRET:-none} -Proxy password secret: ${PROXY_USERNAME_SECRET:-none} -Allowing subnets: ${SUBNETS:-none} -Using OpenVPN log level: $vpn_log_level -Listening on: ${LISTEN_ON:-none}" - -if [[ -n "$VPN_CONFIG_FILE" ]]; then - config_file_original="/data/vpn/$VPN_CONFIG_FILE" -elif [[ -n "$VPN_CONFIG_PATTERN" ]]; then - # Capture the filename of the random .conf file according to the pattern to use as OpenVPN config. - config_file_original=$(find /data/vpn -name "$VPN_CONFIG_PATTERN" 2> /dev/null | sort | shuf -n 1) -else - # Capture the filename of the random .conf file to use as the OpenVPN config. - config_file_original=$(find /data/vpn -name "*.conf" 2> /dev/null | sort | shuf -n 1) -fi +--- Running with the following variables ---" -if [[ -z "$config_file_original" ]]; then - >&2 echo "ERROR: No configuration file found. Please check your mount and file permissions. Exiting." - exit 1 +if [[ $VPN_CONFIG_FILE ]]; then + echo "VPN configuration file: $VPN_CONFIG_FILE" fi - -echo "Using configuration file: $config_file_original" - -# Create a new configuration file to modify so the original is left untouched. -config_file_modified="${config_file_original}.modified" - -echo "Creating $config_file_modified and making required changes to that file." -grep -Ev "(^up\s|^down\s)" "$config_file_original" > "$config_file_modified" - -# These configuration file changes are required by Alpine. -# Also replace carriage return char for conversion of CRLF to LF line endings -sed -i \ - -e 's/^proto udp$/proto udp4/' \ - -e 's/^proto tcp$/proto tcp4/' \ - -e 's/\r$//' \ - "$config_file_modified" - -if [[ "$KEEP_DNS_UNCHANGED" != "on" ]]; then - echo "up /etc/openvpn/up.sh" >> "$config_file_modified" - echo "down /etc/openvpn/down.sh" >> "$config_file_modified" +if [[ $VPN_CONFIG_PATTERN ]]; then + echo "VPN configuration file name pattern: $VPN_CONFIG_PATTERN" fi -echo -e "Changes made.\n" - -trap cleanup INT TERM - -default_gateway=$(ip r | grep 'default via' | cut -d " " -f 3) -if [[ "$KILL_SWITCH" == "on" ]]; then - local_subnet=$(ip r | grep -v 'default via' | grep eth0 | tail -n 1 | cut -d " " -f 1) +echo "Use default resolv.conf: ${USE_VPN_DNS:-off} +Allowing subnets: ${SUBNETS:-none} +Kill switch: $KILL_SWITCH +Using OpenVPN log level: $VPN_LOG_LEVEL" + +if is_enabled "$HTTP_PROXY"; then + echo "HTTP proxy: $HTTP_PROXY" + if is_enabled "$HTTP_PROXY_USERNAME"; then + echo "HTTP proxy username: $HTTP_PROXY_USERNAME" + elif is_enabled "$HTTP_PROXY_USERNAME_SECRET"; then + echo "HTTP proxy username secret: $HTTP_PROXY_USERNAME_SECRET" + fi +fi +if is_enabled "$SOCKS_PROXY"; then + echo "SOCKS proxy: $SOCKS_PROXY" + if [[ $SOCKS_LISTEN_ON ]]; then + echo "Listening on: $SOCKS_LISTEN_ON" + fi + if is_enabled "$SOCKS_PROXY_USERNAME"; then + echo "SOCKS proxy username: $SOCKS_PROXY_USERNAME" + elif is_enabled "$SOCKS_PROXY_USERNAME_SECRET"; then + echo "SOCKS proxy username secret: $SOCKS_PROXY_USERNAME_SECRET" + fi +fi - echo "Creating VPN kill switch and local routes." +echo "--- +" - echo "Allowing established and related connections..." - iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT +if [[ $VPN_CONFIG_FILE ]]; then + original_config_file=vpn/$VPN_CONFIG_FILE +elif [[ $VPN_CONFIG_PATTERN ]]; then + original_config_file=$(find vpn -name "$VPN_CONFIG_PATTERN" 2> /dev/null | sort | shuf -n 1) +else + original_config_file=$(find vpn -name '*.conf' -o -name '*.ovpn' 2> /dev/null | sort | shuf -n 1) +fi - echo "Allowing loopback connections..." - iptables -A INPUT -i lo -j ACCEPT - iptables -A OUTPUT -o lo -j ACCEPT +if [[ -z $original_config_file ]]; then + >&2 echo 'erro: no vpn configuration file found' + exit 1 +fi - echo "Allowing Docker network connections..." - iptables -A INPUT -s "$local_subnet" -j ACCEPT - iptables -A OUTPUT -d "$local_subnet" -j ACCEPT +echo "info: original configuration file: $original_config_file" - echo "Allowing specified subnets..." - # for every specified subnet... - for subnet in ${SUBNETS//,/ }; do - # create a route to it and... - ip route add "$subnet" via "$default_gateway" dev eth0 - # allow connections - iptables -A INPUT -s "$subnet" -j ACCEPT - iptables -A OUTPUT -d "$subnet" -j ACCEPT - done +# Create a new configuration file to modify so the original is left untouched. +modified_config_file=vpn/openvpn.$(tr -dc A-Za-z0-9 "$modified_config_file" + +# Remove carriage returns (\r) from the config file +sed -i 's/\r$//g' "$modified_config_file" + + +default_gateway=$(ip -4 route | grep 'default via' | awk '{print $3}') +if is_enabled "$KILL_SWITCH" ; then + echo "info: kill switch is on" + + nftables_config_file=config/nftables.conf + + local_subnet=$(ip -4 route | grep 'scope link' | awk '{print $1}') + + printf '%s\n' \ + '#!/usr/bin/nft' '' \ + 'flush ruleset' '' \ + '# base ruleset' \ + 'add table inet killswitch' '' \ + 'add chain inet killswitch incoming { type filter hook input priority 0; policy drop; }' \ + 'add rule inet killswitch incoming ct state established,related accept' \ + 'add rule inet killswitch incoming iifname lo accept' '' \ + 'add chain inet killswitch outgoing { type filter hook output priority 0; policy drop; }' \ + 'add rule inet killswitch outgoing ct state established,related accept' \ + 'add rule inet killswitch outgoing oifname lo accept' '' > $nftables_config_file + + printf '%s\n' \ + '# allow traffic to/from the Docker subnet' \ + "add rule inet killswitch incoming ip saddr $local_subnet accept" \ + "add rule inet killswitch outgoing ip daddr $local_subnet accept" '' >> $nftables_config_file + + if [[ $SUBNETS ]]; then + printf '# allow traffic to/from the specified subnets\n' >> $nftables_config_file + for subnet in ${SUBNETS//,/ }; do + ip route add "$subnet" via "$default_gateway" dev eth0 + printf '%s\n' \ + "add rule inet killswitch incoming ip saddr $subnet accept" \ + "add rule inet killswitch outgoing ip daddr $subnet accept" '' >> $nftables_config_file + done + fi - echo "Allowing remote servers in configuration file..." - global_port=$(grep "port " "$config_file_modified" | cut -d " " -f 2) - global_protocol=$(grep "proto " "$config_file_modified" | cut -d " " -f 2 | cut -c1-3) - remotes=$(grep "remote " "$config_file_modified") - - echo " Using:" - comment_regex='^[[:space:]]*[#;]' - echo "$remotes" | while IFS= read -r line; do - # Ignore comments. - if ! [[ "$line" =~ $comment_regex ]]; then - # Remove the line prefix 'remote '. - line=${line#remote } - - # Remove any trailing comments. - line=${line%%#*} - - # Split the line into an array. - # The first element is an address (IP or domain), the second is a port, - # and the fourth is a protocol. - IFS=' ' read -r -a remote <<< "$line" - address=${remote[0]} - # Use port from 'remote' line, then 'port' line, then '1194'. - port=${remote[1]:-${global_port:-1194}} - # Use protocol from 'remote' line, then 'proto' line, then 'udp'. - protocol=${remote[2]:-${global_protocol:-udp}} - - # Map from OpenVPN tcp-client config option to tcp for iptables - if [[ $protocol == "tcp-client" ]]; then - protocol='tcp' - fi - - ip_regex='^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$' - if [[ "$address" =~ $ip_regex ]]; then - echo " IP: $address PORT: $port PROTOCOL: $protocol" - iptables -A OUTPUT -o eth0 -d "$address" -p "$protocol" --dport "$port" -j ACCEPT - else - for ip in $(dig -4 +short "$address"); do - echo " $address (IP: $ip PORT: $port PROTOCOL: $protocol)" - iptables -A OUTPUT -o eth0 -d "$ip" -p "$protocol" --dport "$port" -j ACCEPT - echo "$ip $address" >> /etc/hosts - done - fi + global_port=$(grep "^port " "$modified_config_file" | awk '{print $2}') + global_protocol=$(grep "^proto " "$modified_config_file" | awk '{print $2}') # {$2 = substr($2, 1, 3)} 2 + remotes=$(grep "^remote " "$modified_config_file" | awk '{print $2, $3, $4}') + + printf '# allow traffic to the VPN server(s)\n' >> $nftables_config_file + ip_regex='^(([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))\.){3}([1-9]?[0-9]|1[0-9][0-9]|2([0-4][0-9]|5[0-5]))$' + while IFS= read -r line; do + IFS=' ' read -ra remote <<< "$line" + address=${remote[0]} + port=${remote[1]:-${global_port:-1194}} + protocol=${remote[2]:-${global_protocol:-udp}} + + if [[ $address =~ $ip_regex ]]; then + printf '%s\n' \ + "add rule inet killswitch outgoing oifname eth0 ip daddr $address $protocol dport $port accept" '' >> $nftables_config_file + else + for ip in $(dig -4 +short "$address"); do + printf '%s\n' \ + "add rule inet killswitch outgoing oifname eth0 ip daddr $ip $protocol dport $port accept" '' >> $nftables_config_file + printf "%s %s\n" "$ip" "$address" >> /etc/hosts + done fi - done - - echo "Allowing connections over VPN interface..." - iptables -A INPUT -i tun0 -j ACCEPT - iptables -A OUTPUT -o tun0 -j ACCEPT + done <<< "$remotes" - echo "Preventing anything else..." - iptables -P INPUT DROP - iptables -P OUTPUT DROP - iptables -P FORWARD DROP + printf '%s\n' \ + '# allow traffic over the VPN interface' \ + "add rule inet killswitch incoming iifname tun0 accept" \ + "add rule inet killswitch outgoing oifname tun0 accept" >> $nftables_config_file - echo -e "iptables rules created and routes configured.\n" + nft -f $nftables_config_file else - echo -e "WARNING: VPN kill switch is disabled. Traffic will be allowed outside of the tunnel if the connection is lost.\n" - echo "Creating routes to specified subnets..." + echo "info: kill switch is off" for subnet in ${SUBNETS//,/ }; do ip route add "$subnet" via "$default_gateway" dev eth0 done - echo -e "Routes created.\n" fi -if [[ "$HTTP_PROXY" == "on" ]]; then - if [[ -n "$PROXY_USERNAME" ]]; then - if [[ -n "$PROXY_PASSWORD" ]]; then - echo "Configuring HTTP proxy authentication." - echo -e "\nBasicAuth $PROXY_USERNAME $PROXY_PASSWORD" >> /data/tinyproxy.conf - else - echo "WARNING: Proxy username supplied without password. Starting HTTP proxy without credentials." - fi - elif [[ -f "/run/secrets/$PROXY_USERNAME_SECRET" ]]; then - if [[ -f "/run/secrets/$PROXY_PASSWORD_SECRET" ]]; then - echo "Configuring proxy authentication." - echo -e "\nBasicAuth $(cat /run/secrets/$PROXY_USERNAME_SECRET) $(cat /run/secrets/$PROXY_PASSWORD_SECRET)" >> /data/tinyproxy.conf - else - echo "WARNING: Credentials secrets not read. Starting HTTP proxy without credentials." - fi - fi - /data/scripts/tinyproxy_wrapper.sh & +if is_enabled "$HTTP_PROXY" ; then + scripts/run-http-proxy.sh & fi -if [[ "$SOCKS_PROXY" == "on" ]]; then - if [[ -n "$LISTEN_ON" ]]; then - sed -i "s/internal: eth0/internal: $LISTEN_ON/" /data/sockd.conf - fi - if [[ -n "$PROXY_USERNAME" ]]; then - if [[ -n "$PROXY_PASSWORD" ]]; then - echo "Configuring SOCKS proxy authentication." - adduser -S -D -g "$PROXY_USERNAME" -H -h /dev/null "$PROXY_USERNAME" - echo "$PROXY_USERNAME:$PROXY_PASSWORD" | chpasswd 2> /dev/null - sed -i 's/socksmethod: none/socksmethod: username/' /data/sockd.conf - else - echo "WARNING: Proxy username supplied without password. Starting SOCKS proxy without credentials." - fi - elif [[ -f "/run/secrets/$PROXY_USERNAME_SECRET" ]]; then - if [[ -f "/run/secrets/$PROXY_PASSWORD_SECRET" ]]; then - echo "Configuring proxy authentication." - adduser -S -D -g "$(cat /run/secrets/$PROXY_USERNAME_SECRET)" -H -h /dev/null "$(cat /run/secrets/$PROXY_USERNAME_SECRET)" - echo "$(cat /run/secrets/$PROXY_USERNAME_SECRET):$(cat /run/secrets/$PROXY_PASSWORD_SECRET)" | chpasswd 2> /dev/null - sed -i 's/socksmethod: none/socksmethod: username/' /data/sockd.conf - else - echo "WARNING: Credentials secrets not present. Starting SOCKS proxy without credentials." - fi - fi - /data/scripts/dante_wrapper.sh & +if is_enabled "$SOCKS_PROXY" ; then + scripts/run-socks-proxy.sh & fi openvpn_args=( - "--config" "$config_file_modified" + "--config" "$modified_config_file" "--auth-nocache" - "--cd" "/data/vpn" - "--pull-filter" "ignore" "ifconfig-ipv6" - "--pull-filter" "ignore" "route-ipv6" + "--cd" "vpn" + "--pull-filter" "ignore" "ifconfig-ipv6 " + "--pull-filter" "ignore" "route-ipv6 " "--script-security" "2" "--up-restart" - "--verb" "$vpn_log_level" + "--verb" "$VPN_LOG_LEVEL" ) -if [[ -n "$VPN_AUTH_SECRET" ]]; then - if [[ -f "/run/secrets/$VPN_AUTH_SECRET" ]]; then - echo "Configuring OpenVPN authentication." - openvpn_args+=("--auth-user-pass" "/run/secrets/$VPN_AUTH_SECRET") - else - echo "WARNING: OpenVPN credentials secrets not present." - fi +if is_enabled "$USE_VPN_DNS" ; then + openvpn_args+=( + "--up" "/etc/openvpn/up.sh" + "--down" "/etc/openvpn/down.sh" + ) fi -echo -e "Running OpenVPN client.\n" - -openvpn "${openvpn_args[@]}" & -openvpn_child=$! +if [[ $VPN_AUTH_SECRET ]]; then + openvpn_args+=("--auth-user-pass" "/run/secrets/$VPN_AUTH_SECRET") +fi -wait $openvpn_child +exec openvpn "${openvpn_args[@]}" diff --git a/data/scripts/run-http-proxy.sh b/data/scripts/run-http-proxy.sh new file mode 100755 index 0000000..1266808 --- /dev/null +++ b/data/scripts/run-http-proxy.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e + +until ip link show tun0 2>&1 | grep -qv "does not exist"; do + sleep 1 +done + +proxy_config_file=config/http-proxy.conf + +addr_eth0=$(ip address show eth0 | grep 'inet' | awk '{split($2, inet, "/"); print inet[1]}') +addr_tun0=$(ip address show tun0 | grep 'inet' | awk '{split($2, inet, "/"); print inet[1]}') +sed -i \ + -e "/Listen/c Listen $addr_eth0" \ + -e "/Bind/c Bind $addr_tun0" \ + $proxy_config_file + +if [[ $HTTP_PROXY_USERNAME && $HTTP_PROXY_PASSWORD ]]; then + echo 'info: starting http proxy with credentials' + printf 'BasicAuth %s %s\n' "$HTTP_PROXY_USERNAME" "$HTTP_PROXY_PASSWORD" >> $proxy_config_file +elif [[ -f "/run/secrets/$HTTP_PROXY_USERNAME_SECRET" && -f "/run/secrets/$HTTP_PROXY_PASSWORD_SECRET" ]]; then + echo 'info: starting http proxy with credentials' + printf 'BasicAuth %s %s\n' \ + "$(cat /run/secrets/"$HTTP_PROXY_USERNAME_SECRET")" \ + "$(cat /run/secrets/"$HTTP_PROXY_PASSWORD_SECRET")" >> $proxy_config_file +else + echo 'info: starting http proxy without credentials' +fi + +exec tinyproxy -d -c $proxy_config_file diff --git a/data/scripts/run-socks-proxy.sh b/data/scripts/run-socks-proxy.sh new file mode 100755 index 0000000..bef5e84 --- /dev/null +++ b/data/scripts/run-socks-proxy.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -e + +until ip link show tun0 2>&1 | grep -qv "does not exist"; do + sleep 1 +done + +proxy_config_file=config/socks-proxy.conf + +if [[ $SOCKS_LISTEN_ON ]]; then + sed -i "/internal: /c internal: $SOCKS_LISTEN_ON port = 1080" $proxy_config_file +fi +if [[ $SOCKS_PROXY_USERNAME && $SOCKS_PROXY_PASSWORD ]]; then + printf 'info: starting socks proxy with credentials\n' + useradd "$SOCKS_PROXY_USERNAME" -s /bin/false -M -p "$(mkpasswd "$SOCKS_PROXY_PASSWORD")" + sed -i "/method: /c method: username" $proxy_config_file +elif [[ -f "/run/secrets/$SOCKS_PROXY_USERNAME_SECRET" && -f "/run/secrets/$SOCKS_PROXY_PASSWORD_SECRET" ]]; then + printf 'info: starting socks proxy with credentials\n' + useradd "$(cat /run/secrets/"$SOCKS_PROXY_USERNAME_SECRET")" -s /bin/false -M -p "$(mkpasswd "$(cat /run/secrets/"$SOCKS_PROXY_PASSWORD_SECRET")")" + sed -i "/method: /c method: username" $proxy_config_file +else + printf 'info: starting socks proxy without credentials\n' +fi + +exec sockd -f $proxy_config_file diff --git a/data/scripts/tinyproxy_wrapper.sh b/data/scripts/tinyproxy_wrapper.sh deleted file mode 100755 index eb4ef6b..0000000 --- a/data/scripts/tinyproxy_wrapper.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -echo -e "Running Tinyproxy HTTP proxy server.\n" - -until ip link show tun0 2>&1 | grep -qv "does not exist"; do - sleep 1 -done - -get_addr() { - ip a show dev "$1" | grep inet | cut -d " " -f 6 | cut -d "/" -f 1 -} - -addr_eth=${LISTEN_ON:-$(get_addr eth0)} -addr_tun=$(get_addr tun0) -sed -i \ - -e "/Listen/c Listen $addr_eth" \ - -e "/Bind/c Bind $addr_tun" \ - /data/tinyproxy.conf - -tinyproxy -d -c /data/tinyproxy.conf diff --git a/data/sockd.conf b/data/sockd.conf deleted file mode 100644 index 78166c2..0000000 --- a/data/sockd.conf +++ /dev/null @@ -1,65 +0,0 @@ -# Logging -logoutput: /var/log/sockd.log -errorlog: stderr - -# Server address specification -internal: eth0 port = 1080 -external: tun0 - -# Authentication methods -clientmethod: none -socksmethod: none - -# Server identities -user.unprivileged: sockd - -## -## SOCKS client access rules -## -# Rule processing stops at the first match; no match results in blocking - -# Block access to socks server from 192.0.2.22 -# client block { -# # Block connections from 192.0.2.22/32 -# from: 192.0.2.22/24 to: 0.0.0.0/0 -# log: error # connect disconnect -# } - -# Allow all connections -client pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 - log: error connect disconnect -} - -## -## SOCKS command rules -## -# Rule processing stops at the first match; no match results in blocking - -# Block communication with www.example.org -# socks block { -# from: 0.0.0.0/0 to: www.example.org -# command: bind connect udpassociate -# log: error # connect disconnect iooperation -# } - -# Generic pass statement - bind/outgoing traffic -socks pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 - command: bind connect udpassociate - log: error connect disconnect # iooperation -} - -# Block incoming connections/packets from ftp.example.org -# socks block { -# from: ftp.example.org to: 0.0.0.0/0 -# command: bindreply udpreply -# log: error # connect disconnect iooperation -# } - -# Generic pass statement for incoming connections/packets -socks pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 - command: bindreply udpreply - log: error connect disconnect # iooperation -} diff --git a/docker-compose.yml b/docker-compose.yml index 2dc6cbe..0d9381a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,24 +6,24 @@ services: image: ghcr.io/wfg/openvpn-client # build: . container_name: openvpn-client - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun environment: - # - SUBNETS=192.168.10.0/24 - - HTTP_PROXY=on - - SOCKS_PROXY=on - # - PROXY_USERNAME_SECRET=username # <-- If used, these must match the name of a - # - PROXY_PASSWORD_SECRET=password # <-- secret (NOT the file used by the secret) - # volumes: - # - ~/local/vpn:/data/vpn + # - SUBNETS=192.168.10.0/24 + - HTTP_PROXY=on + - SOCKS_PROXY=on + # - PROXY_USERNAME_SECRET=username # <-- If used, these must match the name of a + # - PROXY_PASSWORD_SECRET=password # <-- secret (NOT the file used by the secret) + # volumes: + # - ~/local/vpn:/data/vpn ports: - - 1080:1080 - - 8088:8080 + - 1080:1080 + - 8088:8080 # secrets: - # - username - # - password + # - username + # - password # secrets: # username: