Dockerized OpenVPN Client with Proxy Integration

A containerized OpenVPN client solution featuring killswitch implementation using iptables and seamless proxy integration through SOCKS5. Includes complete Docker configurations, routing policies and traffic splitting strategies with Clash proxy rules.

This article was published 1378 days ago, some content may be outdated. If you have any questions, please leave a comment.

To resolve conflicts between OpenVPN and proxy software

wfg/docker-openvpn-client: OpenVPN client with killswitch and proxy servers; built on Alpine (github.com)

Build Files

Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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" ]

killswitch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

# Block all traffic not going through tun0 interface
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 allowed subnets
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

# Whitelist OpenVPN server addresses
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
    # Process each remote entry
    IFS=" " read -ra remote <<< "${line%%\#*}"
    address=${remote[0]}
    port=${remote[1]:-${global_port:-1194}}
    protocol=${remote[2]:-${global_protocol:-udp}}

    # Allow IP addresses and handle domain resolution
    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"

entry.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/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

Build and Start

docker-compose.yaml

  • Public image: ghcr.io/wfg/openvpn-client

  • Build image:

1
  docker compose build
  • Start:
1
  docker compose up -d
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
services:
  openvpn-client:
    image: openvpn-client:latest  # ghcr.io/wfg/openvpn-client
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: openvpn-client
    cap_add:
      - NET_ADMIN
    environment:
      # - HTTP_PROXY=on
      - SOCKS_PROXY=on
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ./local:/config
      - ./local:/data/vpn
    ports:
      # - 8080:8080  # HTTP_PROXY port
      - 1080:1080  # SOCKS_PROXY port
    restart: unless-stopped

Using with Proxy Software

Utilizing the Rule Configuration routing policy in Clash software

1
2
3
4
5
6
7
proxies:
	- { name: OpenVPN, server: 127.0.0.1, port: 1080, type: socks5 }

rules:
	- IP-CIDR,192.168.142.0/24,OpenVPN
	- IP-CIDR,10.10.10.0/24,OpenVPN
	- IP-CIDR,172.16.0.0/24,OpenVPN

image-20231201162440422

One More Thing

In addition to OpenVPN, other campus VPN clients like EasyConnect (used for free CNKI access) can be deployed in Docker containers. Expose HTTP/SOCKS proxies and configure routing rules to achieve elegant cross-network environment access.

1
2
    - { name: 'CNKI', type: socks5, server: 10.10.10.45, port: 57080}
    - 'DOMAIN-SUFFIX,cnki.net,CNKI'
Facing the sea with spring blossoms.
Built with Hugo
Theme Stack designed by Jimmy