Compare commits

..

No commits in common. "master" and "v3.1.0" have entirely different histories.

29 changed files with 943 additions and 147 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
*
!data/

View file

@ -1,15 +1,24 @@
name: Build
name: Publish
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
env:
IMAGE_NAME: openvpn-client
jobs:
build:
publish:
runs-on: ubuntu-latest
steps:
- 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
@ -21,7 +30,7 @@ jobs:
- id: tags
uses: docker/metadata-action@v4
with:
images: ghcr.io/wfg/openvpn-client
images: ${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@ -37,10 +46,9 @@ jobs:
- uses: docker/build-push-action@v3
with:
context: "{{defaultContext}}:build"
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 }}
tags: ${{ steps.tags.outputs.tags }}
push: true

3
.gitignore vendored
View file

@ -1,2 +1 @@
# Anything used during development should be put in local/ to prevent accidental committing.
local/
.local/

9
.pre-commit-config.yaml Normal file
View file

@ -0,0 +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

28
CHANGELOG.md Normal file
View file

@ -0,0 +1,28 @@
# Changelog
## Version 3.1.0 - 2022-06-30
### Changed
- `KILL_SWITCH` now requires `iptables` or `nftables` to be enabled. It defaults to `iptables`. See documentation for more information.
### Added
- Modified OpenVPN configuration file cleanup function.
## 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.
## Version 2.0.0 - 2022-01-02
### Changed
- `OPENVPN_AUTH_SECRET` changed to `VPN_AUTH_SECRET` for consistency.
### Fixed
- Commented remotes are no longer processed.

29
Dockerfile Normal file
View file

@ -0,0 +1,29 @@
FROM alpine:3.16
RUN apk add --no-cache \
bash \
bind-tools \
dante-server \
iptables \
openvpn \
nftables \
shadow \
tinyproxy
COPY data/ /data/
ENV KILL_SWITCH=iptables
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
WORKDIR /data
ENTRYPOINT [ "scripts/entry.sh" ]

View file

@ -1,15 +1,14 @@
# OpenVPN Client for Docker
Archived in favor of [a WireGuard version](https://github.com/wfg/docker-wireguard).
## 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.
This image requires you to supply the necessary OpenVPN configuration file(s).
Because of this, any VPN provider should work.
If you find something that doesn't work or have an idea for a new feature, issues and **pull requests are welcome** (however, I'm not promising they will be merged).
If you find something that doesn't work or have an idea for a new feature, issues and **pull requests are welcome**.
## Why?
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.
@ -20,27 +19,27 @@ It also keeps you from having to install an OpenVPN client on the underlying hos
You can either pull it from GitHub Container Registry or build it yourself.
To pull it from GitHub Container Registry, run
```
```bash
docker pull ghcr.io/wfg/openvpn-client
```
To build it yourself, run
```
docker build -t ghcr.io/wfg/openvpn-client https://github.com/wfg/docker-openvpn-client.git#:build
```bash
docker build -t ghcr.io/wfg/openvpn-client https://github.com/wfg/docker-openvpn-client.git
```
### Creating and running a container
The image requires the container be created with the `NET_ADMIN` capability and `/dev/net/tun` accessible.
Below are bare-bones examples for `docker run` and Compose; however, you'll probably want to do more than just run the VPN client.
See the below to learn how to have [other containers use `openvpn-client`'s network stack](#using-with-other-containers).
See the sections below to learn how to use the [proxies](#http_proxy-and-socks_proxy) and have [other containers use `openvpn-client`'s network stack](#using-with-other-containers).
#### `docker run`
```
```bash
docker run --detach \
--name=openvpn-client \
--cap-add=NET_ADMIN \
--device=/dev/net/tun \
--volume <path/to/config/dir>:/config \
--volume <path/to/config/dir>:/data/vpn \
ghcr.io/wfg/openvpn-client
```
@ -55,26 +54,45 @@ services:
devices:
- /dev/net/tun
volumes:
- <path/to/config/dir>:/config
- <path/to/config/dir>:/data/vpn
restart: unless-stopped
```
#### Environment variables
| Variable | Default (blank is unset) | Description |
| --- | --- | --- |
| `ALLOWED_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. |
| `AUTH_SECRET` | | Docker secret that contains the credentials for accessing the VPN. |
| `CONFIG_FILE` | | The OpenVPN configuration file or search pattern. If unset, a random `.conf` or `.ovpn` file will be selected. |
| `KILL_SWITCH` | `on` | Whether or not to enable the kill switch. Set to any "truthy" value[1] to enable. |
[1] "Truthy" values in this context are the following: `true`, `t`, `yes`, `y`, `1`, `on`, `enable`, or `enabled`.
| `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_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` | `iptables` | Which packet filterer to use for the kill switch. This value likely depends on your underlying host. Recommended to leave default unless you have problems. Acceptable values are `iptables` and `nftables`. To disable the kill switch, set to any other value. |
| `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
###### `ALLOWED_SUBNETS`
If you intend on connecting to containers that use the OpenVPN container's network stack (which you probably do), **you will probably want to use this variable**.
Regardless of whether or not you're using the kill switch, the entrypoint script also adds routes to each of the `ALLOWED_SUBNETS` to allow network connectivity from outside of Docker.
###### `HTTP_PROXY` and `SOCKS_PROXY`
If enabling the the proxy server(s), you'll want to publish the appropriate port(s) in order to access the server(s).
To do that using `docker run`, add `-p <host_port>:8080` and/or `-p <host_port>:1080` where `<host_port>` is whatever port you want to use on the host.
If you're using `docker-compose`, add the relevant port specification(s) from the snippet below to the `openvpn-client` service definition in your Compose file.
```yaml
ports:
- <host_port>:8080
- <host_port>:1080
```
##### `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.
@ -103,11 +121,18 @@ In both cases, replace `<host_port>` and `<container_port>` with the port used b
Once you have container running `ghcr.io/wfg/openvpn-client`, run the following command to spin up a temporary container using `openvpn-client` for networking.
The `wget -qO - ifconfig.me` bit will return the public IP of the container (and anything else using `openvpn-client` for networking).
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
#### `can't initialize iptables`
If you see a message like the below in your logs, try setting `KILL_SWITCH` to `nftables`:
```
iptables v1.8.8 (legacy): can't initialize iptables table `filter': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.
```
#### 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.

1
build-variables Normal file
View file

@ -0,0 +1 @@
IMAGE_NAME=ghcr.io/wfg/openvpn-client

20
build.py Executable file
View file

@ -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)

View file

@ -1 +0,0 @@
Dockerfile

View file

@ -1,14 +0,0 @@
FROM alpine:3.17
RUN apk add --no-cache \
bash \
bind-tools \
iptables \
ip6tables \
openvpn
COPY . /usr/local/bin
ENV KILL_SWITCH=on
ENTRYPOINT [ "entry.sh" ]

View file

@ -1,50 +0,0 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
cleanup() {
kill TERM "$openvpn_pid"
exit 0
}
is_enabled() {
[[ ${1,,} =~ ^(true|t|yes|y|1|on|enable|enabled)$ ]]
}
# Either a specific file name or a pattern.
if [[ $CONFIG_FILE ]]; then
config_file=$(find /config -name "$CONFIG_FILE" 2> /dev/null | sort | shuf -n 1)
else
config_file=$(find /config -name '*.conf' -o -name '*.ovpn' 2> /dev/null | sort | shuf -n 1)
fi
if [[ -z $config_file ]]; then
echo "no openvpn configuration file found" >&2
exit 1
fi
echo "using openvpn configuration file: $config_file"
openvpn_args=(
"--config" "$config_file"
"--cd" "/config"
)
if is_enabled "$KILL_SWITCH"; then
openvpn_args+=("--route-up" "/usr/local/bin/killswitch.sh $ALLOWED_SUBNETS")
fi
# Docker secret that contains the credentials for accessing the VPN.
if [[ $AUTH_SECRET ]]; then
openvpn_args+=("--auth-user-pass" "/run/secrets/$AUTH_SECRET")
fi
openvpn "${openvpn_args[@]}" &
openvpn_pid=$!
trap cleanup TERM
wait $openvpn_pid

View file

@ -1,44 +0,0 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
iptables --insert OUTPUT \
! --out-interface tun0 \
--match addrtype ! --dst-type LOCAL \
! --destination "$(ip -4 -oneline addr show dev eth0 | awk 'NR == 1 { print $4 }')" \
--jump REJECT
# Create static routes for any ALLOWED_SUBNETS and punch holes in the firewall
# (ALLOWED_SUBNETS is passed as $1 from entry.sh)
default_gateway=$(ip -4 route | awk '$1 == "default" { print $3 }')
for subnet in ${1//,/ }; do
ip route add "$subnet" via "$default_gateway"
iptables --insert OUTPUT --destination "$subnet" --jump ACCEPT
done
# Punch holes in the firewall for the OpenVPN server addresses
# $config is set by OpenVPN:
# "Name of first --config file. Set on program initiation and reset on SIGHUP."
global_port=$(awk '$1 == "port" { print $2 }' "${config:?"config file not found by kill switch"}")
global_protocol=$(awk '$1 == "proto" { print $2 }' "${config:?"config file not found by kill switch"}")
remotes=$(awk '$1 == "remote" { print $2, $3, $4 }' "${config:?"config file not found by kill switch"}")
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
# Read a comment-stripped version of the line
# Fixes #84
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
iptables --insert OUTPUT --destination "$address" --protocol "$protocol" --destination-port "$port" --jump ACCEPT
else
for ip in $(dig -4 +short "$address"); do
iptables --insert OUTPUT --destination "$ip" --protocol "$protocol" --destination-port "$port" --jump ACCEPT
echo "$ip $address" >> /etc/hosts
done
fi
done <<< "$remotes"

23
chart/.helmignore Normal file
View file

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

15
chart/Chart.yaml Normal file
View file

@ -0,0 +1,15 @@
apiVersion: v2
name: openvpn-client
description: A Helm chart for an OpenVPN client with HTTP and SOCKS5 proxies
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v1.2.1"

42
chart/README.md Normal file
View file

@ -0,0 +1,42 @@
# openvpn-client
![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.2.1](https://img.shields.io/badge/AppVersion-v1.2.1-informational?style=flat-square)
A Helm chart for an OpenVPN client with HTTP and SOCKS5 proxies
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| auth.enabled | bool | `false` | Whether to turn on authentication for the proxies |
| auth.existingSecret | string | `""` | Existing secret containing the credentials for accessing the proxies. |
| auth.proxyPassword | string | `""` | |
| auth.proxyUsername | string | `""` | |
| autoscaling.enabled | bool | `false` | |
| autoscaling.maxReplicas | int | `100` | |
| autoscaling.minReplicas | int | `1` | |
| autoscaling.targetCPUUtilizationPercentage | int | `80` | |
| configFiles.files | object | `{}` | OpenVPN config files |
| configFiles.openVPNConfig | string | `""` | The OpenVPN config file to use. If this is unset, the first file with the extension `.conf` will be used. |
| fullnameOverride | string | `""` | |
| httpProxy.enabled | bool | `false` | The on/off status of Tinyproxy, the built-in HTTP proxy server. |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"ghcr.io/wfg/openvpn-client"` | |
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
| keepDNSUnchanged.enabled | bool | `false` | Keep existing DNS configuration |
| killSwitch.enabled | bool | `true` | The on/off status of the network kill switch. |
| listenOn | string | `""` | Address the proxies will be listening on. Set to `0.0.0.0` to allow all IP addresses. |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
| replicaCount | int | `1` | |
| resources | object | `{}` | |
| service.type | string | `"ClusterIP"` | |
| socksProxy.enabled | bool | `false` | The on/off status of Dante, the built-in SOCKS proxy server. |
| subnets | list | `[]` | A list of one or more subnets to allow outside of the VPN tunnel. |
| tolerations | list | `[]` | |
| vpnLogLevel | int | `3` | OpenVPN verbosity (`1`-`11`) |
----------------------------------------------
Autogenerated from chart metadata using [helm-docs v1.7.0](https://github.com/norwoodj/helm-docs/releases/v1.7.0)

View file

@ -0,0 +1,76 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "openvpn-client.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "openvpn-client.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "openvpn-client.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "openvpn-client.labels" -}}
helm.sh/chart: {{ include "openvpn-client.chart" . }}
{{ include "openvpn-client.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "openvpn-client.selectorLabels" -}}
app.kubernetes.io/name: {{ include "openvpn-client.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Convert boolean to on/off
*/}}
{{- define "openvpn-client.boolean" -}}
{{- if .enabled }} "on" {{- else }} "off" {{- end }}
{{- end }}
{{/*
Define auth secret name
*/}}
{{- define "openvpn-client.authSecretName" -}}
{{- if .Values.auth.existingSecret -}}
{{- .Values.auth.existingSecret -}}
{{- else -}}
{{- include "openvpn-client.fullname" . | printf "%s-auth" }}
{{- end -}}
{{- end -}}
{{/*
Define config secret name
*/}}
{{- define "openvpn-client.configSecretName" -}}
{{- include "openvpn-client.fullname" . | printf "%s-config" }}
{{- end -}}

View file

@ -0,0 +1,12 @@
{{- if and .Values.auth.enabled (not .Values.auth.existingSecret) (or .Values.httpProxy.enabled .Values.socksProxy.enabled) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "openvpn-client.authSecretName" . }}
labels:
{{- include "openvpn-client.labels" . | nindent 4 }}
type: kubernetes.io/basic-auth
data:
username: {{ .Values.auth.proxyUsername | b64enc | quote }}
password: {{ .Values.auth.proxyPassword | b64enc | quote }}
{{- end -}}

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "openvpn-client.configSecretName" . }}
labels:
{{- include "openvpn-client.labels" . | nindent 4 }}
type: Opaque
data:
{{- range $fileName, $fileContent := $.Values.configFiles.files }}
{{ $fileName }}: |-
{{- $fileContent | b64enc | nindent 4 }}
{{- end }}

View file

@ -0,0 +1,108 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "openvpn-client.fullname" . }}
labels:
{{- include "openvpn-client.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "openvpn-client.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "openvpn-client.selectorLabels" . | nindent 8 }}
spec:
initContainers:
- name: copy
image: busybox
command: ["/bin/sh", "-c", "cp -r /from/. /to"]
volumeMounts:
- name: openvpn-client
mountPath: /from
- name: configs
mountPath: /to
containers:
- name: {{ .Chart.Name }}
securityContext:
capabilities:
add:
- NET_ADMIN
privileged: true
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
{{- if .Values.socksProxy.enabled }}
- containerPort: 1080
{{- end }}
{{- if .Values.httpProxy.enabled }}
- containerPort: 8080
{{- end }}
readinessProbe:
exec:
command: ["ping", "-c", "3", "1.1.1.1"]
env:
- name: VPN_LOG_LEVEL
value: {{ .Values.vpnLogLevel | quote }}
- name: SOCKS_PROXY
value: {{- include "openvpn-client.boolean" .Values.socksProxy }}
- name: HTTP_PROXY
value: {{- include "openvpn-client.boolean" .Values.httpProxy }}
- name: KILL_SWITCH
value: {{- include "openvpn-client.boolean" .Values.killSwitch }}
- name: KEEP_DNS_UNCHANGED
value: {{- include "openvpn-client.boolean" .Values.keepDNSUnchanged }}
{{- if .Values.listenOn }}
- name: LISTEN_ON
value: {{ .Values.listenOn }}
{{- end }}
{{- if .Values.subnets }}
- name: SUBNETS
value: {{ join "," .Values.subnets | quote }}
{{- end }}
{{- if .Values.auth.enabled }}
- name: PROXY_USERNAME
valueFrom:
secretKeyRef:
name: {{ include "openvpn-client.authSecretName" . }}
key: username
- name: PROXY_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "openvpn-client.authSecretName" . }}
key: password
{{- end }}
{{- with .Values.configFiles.openVPNConfig }}
- name: VPN_CONFIG_FILE
value: {{ . }}
{{- end }}
volumeMounts:
- mountPath: /data/vpn
name: configs
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: openvpn-client
secret:
secretName: {{ include "openvpn-client.configSecretName" . }}
- name: configs
emptyDir: {}

28
chart/templates/hpa.yaml Normal file
View file

@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "openvpn-client.fullname" . }}
labels:
{{- include "openvpn-client.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "openvpn-client.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,23 @@
{{- if or .Values.socksProxy.enabled .Values.httpProxy.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "openvpn-client.fullname" . }}
labels:
{{- include "openvpn-client.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
{{- if .Values.socksProxy.enabled }}
- port: 1080
protocol: TCP
name: socks5
{{- end }}
{{- if .Values.httpProxy.enabled }}
- port: 8080
protocol: TCP
name: http
{{- end }}
selector:
{{- include "openvpn-client.selectorLabels" . | nindent 4 }}
{{- end }}

88
chart/values.yaml Normal file
View file

@ -0,0 +1,88 @@
# Default values for openvpn-client.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: ghcr.io/wfg/openvpn-client
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion.
tag: ""
nameOverride: ""
fullnameOverride: ""
podAnnotations: {}
service:
type: ClusterIP
resources:
{}
# limits:
# cpu: 10m
# memory: 64Mi
# requests:
# cpu: 10m
# memory: 64Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
httpProxy:
# -- The on/off status of Tinyproxy, the built-in HTTP proxy server.
enabled: false
socksProxy:
# -- The on/off status of Dante, the built-in SOCKS proxy server.
enabled: false
# -- Address the proxies will be listening on. Set to `0.0.0.0` to allow all IP addresses.
listenOn: ""
auth:
# -- Whether to turn on authentication for the proxies
enabled: false
# -- Existing secret containing the credentials for accessing the proxies.
existingSecret: ""
proxyUsername: ""
proxyPassword: ""
# -- OpenVPN verbosity (`1`-`11`)
vpnLogLevel: 3
# -- A list of one or more subnets to allow outside of the VPN tunnel.
subnets:
[]
# - "192.168.0.0/24"
# - "192.168.1.0/24"
killSwitch:
# -- The on/off status of the network kill switch.
enabled: true
keepDNSUnchanged:
# -- Keep existing DNS configuration
enabled: false
configFiles:
# -- The OpenVPN config file to use. If this is unset, the first file with the extension `.conf` will be used.
openVPNConfig: ""
# -- OpenVPN config files
files:
{}
# example.conf: |-
# # Your OpenVPN configuration file
# Line 1
# Line 2

View file

@ -0,0 +1,14 @@
User tinyproxy
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"
DisableViaHeader yes

View file

@ -0,0 +1,26 @@
logoutput: /var/log/dante.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
}

242
data/scripts/entry.sh Executable file
View file

@ -0,0 +1,242 @@
#!/usr/bin/env bash
set -e
cleanup() {
if [[ $openvpn_child ]]; then
kill SIGTERM "$openvpn_child"
fi
sleep 0.5
rm -f "$modified_config_file"
echo "info: exiting"
exit 0
}
is_enabled() {
[[ ${1,,} =~ ^(true|t|yes|y|1|on|enable|enabled)$ ]]
}
mkdir -p /data/{config,scripts,vpn}
echo "
--- Running with the following variables ---"
if [[ $VPN_CONFIG_FILE ]]; then
echo "VPN configuration file: $VPN_CONFIG_FILE"
fi
if [[ $VPN_CONFIG_PATTERN ]]; then
echo "VPN configuration file name pattern: $VPN_CONFIG_PATTERN"
fi
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 "---
"
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
if [[ -z $original_config_file ]]; then
>&2 echo 'erro: no vpn configuration file found'
exit 1
fi
echo "info: original configuration file: $original_config_file"
# Create a new configuration file to modify so the original is left untouched.
modified_config_file=vpn/openvpn.$(tr -dc A-Za-z0-9 </dev/urandom | head -c8).conf
trap cleanup SIGTERM
echo "info: modified configuration file: $modified_config_file"
grep -Ev '(^up\s|^down\s)' "$original_config_file" > "$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}')
case "$KILL_SWITCH" in
'iptables')
echo "info: kill switch is using iptables"
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
local_subnet=$(ip -4 route | grep 'scope link' | awk '{print $1}')
iptables -A INPUT -s "$local_subnet" -j ACCEPT
iptables -A OUTPUT -d "$local_subnet" -j ACCEPT
if [[ $SUBNETS ]]; then
for subnet in ${SUBNETS//,/ }; do
ip route add "$subnet" via "$default_gateway" dev eth0
iptables -A INPUT -s "$subnet" -j ACCEPT
iptables -A OUTPUT -d "$subnet" -j ACCEPT
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}')
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
iptables -A OUTPUT -o eth0 -d "$address" -p "$protocol" --dport "$port" -j ACCEPT
else
for ip in $(dig -4 +short "$address"); do
iptables -A OUTPUT -o eth0 -d "$ip" -p "$protocol" --dport "$port" -j ACCEPT
printf "%s %s\n" "$ip" "$address" >> /etc/hosts
done
fi
done <<< "$remotes"
iptables -A INPUT -i tun0 -j ACCEPT
iptables -A OUTPUT -o tun0 -j ACCEPT
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -P FORWARD DROP
iptables-save > config/iptables.conf
;;
'nftables')
echo "info: kill switch is using nftables"
nftables_config_file=config/nftables.conf
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
local_subnet=$(ip -4 route | grep 'scope link' | awk '{print $1}')
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
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 <<< "$remotes"
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
nft -f $nftables_config_file
;;
*)
echo "info: kill switch is off"
for subnet in ${SUBNETS//,/ }; do
ip route add "$subnet" via "$default_gateway" dev eth0
done
;;
esac
if is_enabled "$HTTP_PROXY" ; then
scripts/run-http-proxy.sh &
fi
if is_enabled "$SOCKS_PROXY" ; then
scripts/run-socks-proxy.sh &
fi
openvpn_args=(
"--config" "$modified_config_file"
"--auth-nocache"
"--cd" "vpn"
"--pull-filter" "ignore" "ifconfig-ipv6 "
"--pull-filter" "ignore" "route-ipv6 "
"--script-security" "2"
"--up-restart"
"--verb" "$VPN_LOG_LEVEL"
)
if is_enabled "$USE_VPN_DNS" ; then
openvpn_args+=(
"--up" "/etc/openvpn/up.sh"
"--down" "/etc/openvpn/down.sh"
)
fi
if [[ $VPN_AUTH_SECRET ]]; then
openvpn_args+=("--auth-user-pass" "/run/secrets/$VPN_AUTH_SECRET")
fi
openvpn "${openvpn_args[@]}" &
openvpn_child=$!
wait $openvpn_child

30
data/scripts/run-http-proxy.sh Executable file
View file

@ -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

26
data/scripts/run-socks-proxy.sh Executable file
View file

@ -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

View file

@ -1,13 +1,32 @@
# Use this file as an example if you need help writing your Compose files.
# The commented-out parts may or may not be relevant to your setup.
services:
openvpn-client:
image: ghcr.io/wfg/openvpn-client:latest
vpn:
image: ghcr.io/wfg/openvpn-client
# build: .
container_name: openvpn-client
cap_add:
- NET_ADMIN
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
- /dev/net/tun:/dev/net/tun
environment:
- ALLOWED_SUBNETS=192.168.10.0/24
volumes:
- ./local:/config
restart: unless-stopped
# - 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
# secrets:
# - username
# - password
# secrets:
# username:
# file: ~/local/secrets/username
# password:
# file: ~/local/secrets/password