Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 13868bb

Browse files
author
yacht7
committed
Rewrite README.md and clean up entry.sh
- Added trap and cleanup function for cleaner and faster container stops. - A copy of the OpenVPN config file is made to keep from modifying original. - Slight reorganization and additional comments/logging
1 parent 1c473f3 commit 13868bb

File tree

2 files changed

+130
-90
lines changed

2 files changed

+130
-90
lines changed

README.md

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
# OpenVPN Client for Docker
2+
## What is this and what does it do?
3+
`yacht7/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 also includes two types of proxy: HTTP (Tinyproxy) and SOCKS5 (Shadowsocks). These allow hosts and non-containerized applications to use the VPN without having to run VPN clients on every host.
4+
5+
This image requires the user to supply the necessary OpenVPN configuration file(s). Because of this, any VPN provider should work (however, if you find something that doesn't, please open an issue for it).
6+
27
## Why?
3-
Using this image will allow you establish a VPN connection usable by other containers (and hosts via the built-in proxies) without having to install a VPN client on the host. The image requires the user to supply the necessary OpenVPN configuration files, so (probably) any VPN provider will work.
8+
Having a containerized VPN client lets you easy choose exactly what you want to use the VPN at the Docker level instead of having to set up split tunnelling at the VPN level. It also keeps you from having to install an OpenVPN client on the underlying host.
9+
10+
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 because I want this to be a learning experience for both me and hopefully anyone else that uses it.
411

5-
It has a VPN kill switch enabled by default, so if the VPN connection is lost at any time, all internet connectivity to the container and connected clients is lost.
12+
## How do I use it?
13+
### Getting the image
14+
You can either pull it from Docker Hub or build it yourself.
615

7-
Please note that the kill switch does allow connections to the VPN server address/port combinations specified in the configuration file outside of the VPN tunnel in order to establish connection.
16+
To pull from [Docker Hub](https://hub.docker.com/r/yacht7/openvpn-client), run `docker pull yacht7/openvpn-client`.
817

9-
## Creating
10-
### `docker run`
18+
To build it yourself, do the following:
19+
```bash
20+
git clone https://github.com/yacht7/docker-openvpn-client.git
21+
cd docker-openvpn-client
22+
docker build -t yacht7/openvpn-client .
1123
```
24+
25+
### Creating and running a container
26+
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 sections below to learn how to use the [proxies](#shadowsocks-and-tinyproxy) and have [other containers use `openvpn-client`'s network stack](#using-with-other-containers).
27+
28+
#### `docker run`
29+
```bash
1230
docker run -d \
1331
--name=openvpn-client \
1432
--cap-add=NET_ADMIN \
@@ -17,8 +35,8 @@ docker run -d \
1735
yacht7/openvpn-client
1836
```
1937

20-
### `docker-compose`
21-
```
38+
#### `docker-compose`
39+
```yaml
2240
version: '2'
2341

2442
services:
@@ -33,58 +51,59 @@ services:
3351
- <path/to/config>:/data/vpn
3452
restart: unless-stopped
3553
```
36-
#### Considerations
37-
##### Tinyproxy and Shadowsocks
38-
If enabling Tinyproxy or Shadowsocks, you'll want to publish the proxy's port in order to access the proxy. To do that using `docker run`, add `-p <host_port>:<container_port>` where `<host_port>` and `<container_port>` are whatever port your proxy is using (8888 and 8388 by default for Tinyproxy and Shadowsocks). If you're using `docker-compose`, add the below snippet to the `openvpn-client` service definition in your Compose file.
39-
```
40-
ports:
41-
- <host_port>:<container_port>
42-
```
43-
44-
##### Handling ports intended for connected containers
45-
If you plan on having [other containers use `openvpn-client`'s network stack](#using-with-other-containers) and those containers have web UIs, you'll want to publish the web UI ports on `openvpn-client` instead of the connected container. To do that, add `-p <host_port>:<container_port>` if you're using `docker run`, or add the below snippet to the `openvpn-client` service definition in your Compose file if using `docker-compose`.
46-
```
47-
ports:
48-
- <host_port>:<container_port>
49-
```
50-
In both cases, replace `<host_port>` and `<container_port>` with the port used by your connected container.
51-
52-
### Environment variables
5354
55+
#### Environment variables
5456
| Variable | Default (blank is unset) | Description |
5557
| --- | --- | --- |
5658
| `KILL_SWITCH` | `on` | The on/off status of VPN kill switch. To disable, set to any value besides `on`. |
57-
| `SUBNETS` | | A comma-separated (no whitespaces) list of LAN subnets (e.g. `192.168.0.0/24,192.168.1.0/24`). |
59+
| `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. See important note about this [below](#subnets). |
5860
| `FORWARDED_PORTS` | | Port(s) forwarded by your VPN provider (e.g. `12345` or `9876,54321`) |
5961
| `VPN_LOG_LEVEL` | `3` | OpenVPN verbosity (`1`-`11`) |
6062
| `SHADOWSOCKS` | | The on/off status of Shadowsocks. To enable, set to `on`. Any other value, including leaving it unset, will cause the proxy to not start. |
61-
| `SHADOWSOCKS_PORT` | `8388` | The port that Shadowsocks listens on. If manually specified, choose a port over 1024. |
62-
| `SHADOWSOCKS_PASS` | `password` | Required to start Shadowsocks, so a default is specified. |
63+
| `SHADOWSOCKS_PORT` | `8388` | The port on which Shadowsocks listens. If manually specified, choose a port over 1024. |
64+
| `SHADOWSOCKS_PASS` | `password` | A password is required to start Shadowsocks, so a default is specified. I recommend you change this if Shadowsocks is enabled. |
6365
| `TINYPROXY` | | The on/off status of Tinyproxy. To enable, set to `on`. Any other value, including leaving it unset, will cause the proxy to not start. |
64-
| `TINYPROXY_PORT` | `8888` | The port that Tinyproxy listens on. If manually specified, choose a port over 1024. |
65-
| `TINYPROXY_USER` | | Setting `TINYPROXY_USER` and `TINYPROXY_PASS` will restrict access to the proxy server to only the specified username and password. |
66-
| `TINYPROXY_PASS` | | Setting `TINYPROXY_USER` and `TINYPROXY_PASS` will restrict access to the proxy server to only the specified username and password. |
66+
| `TINYPROXY_PORT` | `8888` | The port on which Tinyproxy listens. If manually specified, choose a port over 1024. |
67+
| `TINYPROXY_USER` | | Credentials for accessing Tinyproxy. If `TINYPROXY_USER` is specified, you must also specify `TINYPROXY_PASS`. |
68+
| `TINYPROXY_PASS` | | Credentials for accessing Tinyproxy. If `TINYPROXY_PASS` is specified, you must also specify `TINYPROXY_USER`. |
6769

68-
#### `SUBNETS`
69-
**Important note about this variable**: the DNS server used by this container prior to VPN connection must be included in the value specified. For example, if your underlying host is using 192.168.1.1 as a DNS server, then this address must be included in `SUBNETS` (`192.168.1.1` or `192.168.1.0/24` would be acceptable). This is necessary because the kill switch will block traffic outside of the VPN tunnel before it's actually established. If the DNS server is not whitelisted, the server addresses in the VPN configuration will not resolve.
70+
##### Environment variable considerations
71+
###### `KILL_SWITCH`
72+
The kill switch allows connections outside of the VPN tunnel to the following two places: 1) the VPN server(s) specified in the configuration file and 2) all addresses specified in `SUBNETS`.
73+
74+
###### `SUBNETS`
75+
**Important**: The DNS server used by this container prior to VPN connection must be included in the value specified. For example, if your container is using 192.168.1.1 as a DNS server, then this address or an appropriate CIDR block must be included in `SUBNETS`. This is necessary because the kill switch blocks traffic outside of the VPN tunnel before it's actually established. If the DNS server is not whitelisted, the server addresses in the VPN configuration will not resolve.
7076

7177
The subnets specified will have routes created and whitelists added in the firewall for them which allows for connectivity to and from hosts on the subnets.
7278

73-
## Running
74-
### Verifying functionality
75-
Once you have container running `yacht7/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.
76-
```
77-
docker run --rm -it --network=container:openvpn-client alpine wget -qO - ifconfig.me
79+
###### `SHADOWSOCKS` and `TINYPROXY`
80+
If enabling Shadowsocks or Tinyproxy, you'll want to publish the proxy's port in order to access the proxy. To do that using `docker run`, add `-p <host_port>:<container_port>` where `<host_port>` and `<container_port>` are whatever port your proxy is using (8388 and 8888 by default for Shadowsocks and Tinyproxy). If you're using `docker-compose`, add the below snippet to the `openvpn-client` service definition in your Compose file.
81+
```yaml
82+
ports:
83+
- <host_port>:<container_port>
7884
```
7985

8086
### Using with other containers
81-
Once you have your OpenVPN client container up and running, you can tell other containers to use `openvpn-client`'s network stack which gives any container the ability to utilize the VPN tunnel. There are a few ways to accomplish this depending how how your container is created.
87+
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.
8288

8389
If your container is being created with
8490
1. the same Compose YAML file as `openvpn-client`, add `network_mode: service:openvpn-client` to the container's service definition.
85-
2. a different Compose YAML file as `openvpn-client`, add `network_mode: container:openvpn-client` to the container's service definition.
91+
2. a different Compose YAML file than `openvpn-client`, add `network_mode: container:openvpn-client` to the container's service definition.
8692
3. `docker run`, add `--network=container:openvpn-client` as an option to `docker run`.
8793

8894
Once running and provided your container has `wget` or `curl`, you can run `docker exec <container_name> wget -qO - ifconfig.me` or `docker exec <container_name> curl -s ifconfig.me` to get the public IP of the container and make sure everything is working as expected. This IP should match the one of `openvpn-client`.
8995

90-
If the connected container needs to publish ports, see [this](#handling-ports-intended-for-connected-containers) section.
96+
#### Handling ports intended for connected containers
97+
If you have a connected container and you need to access a port that container, you'll want to publish that port on the `openvpn-client` container instead of the connected container. To do that, add `-p <host_port>:<container_port>` if you're using `docker run`, or add the below snippet to the `openvpn-client` service definition in your Compose file if using `docker-compose`.
98+
```yaml
99+
ports:
100+
- <host_port>:<container_port>
101+
```
102+
In both cases, replace `<host_port>` and `<container_port>` with the port used by your connected container.
103+
104+
### Verifying functionality
105+
Once you have container running `yacht7/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.
106+
```bash
107+
docker run --rm -it --network=container:openvpn-client alpine wget -qO - ifconfig.me
108+
```
109+

data/entry.sh

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,72 @@
11
#!/bin/sh
22

3-
################################################################################
4-
5-
config_file=$(ls -1 /data/vpn/*.conf 2>/dev/null | head -1)
6-
if [ -z $config_file ]; then
3+
# When you run `docker stop` or any equivalent, a SIGTERM signal is sent to PID 1.
4+
# A process running as PID 1 inside a container is treated specially by Linux:
5+
# it ignores any signal with the default action. As a result, the process will
6+
# not terminate on SIGINT or SIGTERM unless it is coded to do so. Because of this,
7+
# I've defined behavior for when SIGINT and SIGTERM is received.
8+
cleanup() {
9+
if [ $openvpn_child ]; then
10+
echo "Stopping OpenVPN..."
11+
kill -TERM $openvpn_child
12+
fi
13+
14+
sleep 1
15+
rm $config_file_modified
16+
echo "Exiting."
17+
exit 0
18+
}
19+
20+
# Capture the filename of the first .conf file to use as the OpenVPN config.
21+
config_file_original=$(ls -1 /data/vpn/*.conf 2> /dev/null | head -1)
22+
if [ -z $config_file_original ]; then
723
>&2 echo "[ERRO] No configuration file found. Please check your mount and file permissions. Exiting."
824
exit 1
925
fi
1026

11-
vpn_log_level=${LOG_LEVEL:-3}
27+
vpn_log_level=${VPN_LOG_LEVEL:-3}
1228
if ! $(echo $vpn_log_level | grep -Eq '^([1-9]|1[0-1])$'); then
1329
echo "[WARN] Invalid log level $vpn_log_level. Setting to default."
1430
vpn_log_level=3
1531
fi
1632

1733
echo -e "\n---- Details ----
18-
Kill switch: $KILL_SWITCH
19-
Tinyproxy: $TINYPROXY
20-
Shadowsocks: $SHADOWSOCKS
34+
Kill switch: ${KILL_SWITCH:-off}
35+
Tinyproxy: ${TINYPROXY:-off}
36+
Shadowsocks: ${SHADOWSOCKS:-off}
2137
Whitelisting subnets: ${SUBNETS:-none}
22-
Using configuration file: $config_file
38+
Using configuration file: $config_file_original
2339
Using OpenVPN log level: $vpn_log_level"
2440

2541
################################################################################
2642

2743
echo -e "\n---- OpenVPN Configuration ----"
2844

45+
# Create a new configuration file to modify so the original is left untouched.
46+
config_file_modified=${config_file_original}.modified
47+
cp $config_file_original $config_file_modified
48+
2949
# These configuration file changes are required by Alpine.
3050
echo "Making required changes to the configuration file."
3151
sed -i \
3252
-e '/up /c up \/etc\/openvpn\/up.sh' \
3353
-e '/down /c down \/etc\/openvpn\/down.sh' \
3454
-e 's/^proto udp$/proto udp4/' \
3555
-e 's/^proto tcp$/proto tcp4/' \
36-
$config_file
56+
$config_file_modified
3757

38-
if ! grep -q 'pull-filter ignore "route-ipv6"' $config_file; then
39-
printf '\npull-filter ignore "route-ipv6"' >> $config_file
58+
if ! grep -q 'pull-filter ignore "route-ipv6"' $config_file_modified; then
59+
printf '\npull-filter ignore "route-ipv6"' >> $config_file_modified
4060
fi
4161

42-
if ! grep -q 'pull-filter ignore "ifconfig-ipv6"' $config_file; then
43-
printf '\npull-filter ignore "ifconfig-ipv6"' >> $config_file
62+
if ! grep -q 'pull-filter ignore "ifconfig-ipv6"' $config_file_modified; then
63+
printf '\npull-filter ignore "ifconfig-ipv6"' >> $config_file_modified
4464
fi
4565

46-
cp /data/vpn/* /etc/openvpn
66+
echo "[INFO] Changes made."
4767

48-
echo "[INFO] Changes made and files moved into place."
68+
# Upon receiving a SIGINT or SIGTERM, run the cleanup function.
69+
trap cleanup INT TERM
4970

5071
################################################################################
5172

@@ -57,18 +78,19 @@ if [ $KILL_SWITCH = "on" ]; then
5778

5879
echo "Creating VPN kill switch and local routes."
5980

60-
# allows established and related connections
81+
echo "Allowing established and related connections..."
6182
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
6283
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
6384

64-
# allows loopback connections
85+
echo "Allowing loopback connections..."
6586
iptables -A INPUT -i lo -j ACCEPT
6687
iptables -A OUTPUT -o lo -j ACCEPT
6788

68-
# allows Docker network connections
89+
echo "Allowing Docker network connections..."
6990
iptables -A INPUT -s $local_subnet -j ACCEPT
7091
iptables -A OUTPUT -d $local_subnet -j ACCEPT
7192

93+
echo "Allowing specified subnets..."
7294
# for every specified subnet...
7395
for subnet in ${SUBNETS//,/ }; do
7496
# create a route to it and...
@@ -78,28 +100,27 @@ if [ $KILL_SWITCH = "on" ]; then
78100
iptables -A OUTPUT -d $subnet -j ACCEPT
79101
done
80102

81-
# allows OpenVPN connections only to addresses in configuration file
82-
remote_port=$(grep "port " $config_file | cut -d " " -f 2)
83-
remote_proto=$(grep "proto " $config_file | cut -d " " -f 2 | cut -c1-3)
84-
remotes=$(grep "remote " $config_file | cut -d " " -f 2-4)
85-
103+
echo "Allowing remote servers in configuration file..."
104+
remote_port=$(grep "port " $config_file_modified | cut -d " " -f 2)
105+
remote_proto=$(grep "proto " $config_file_modified | cut -d " " -f 2 | cut -c1-3)
106+
remotes=$(grep "remote " $config_file_modified | cut -d " " -f 2-4)
86107

87-
echo "Using remote servers:"
108+
echo " Using:"
88109
echo "$remotes" | while IFS= read line; do
89110
domain=$(echo "$line" | cut -d " " -f 1)
90111
port=$(echo "$line" | cut -d " " -f 2)
91112
proto=$(echo "$line" | cut -d " " -f 3 | cut -c1-3)
92113
for ip in $(nslookup $domain localhost | tail -n +4 | grep -Eo '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' | sort | uniq); do
93-
echo "$domain (IP:$ip PORT:$port)"
114+
echo " $domain (IP:$ip PORT:$port)"
94115
iptables -A OUTPUT -o eth0 -d $ip -p ${proto:-$remote_proto} --dport ${port:-$remote_port} -j ACCEPT
95116
done
96117
done
97118

98-
# allow connections over VPN interface...
119+
echo "Allowing connections over VPN interface..."
99120
iptables -A INPUT -i tun0 -j ACCEPT
100121
iptables -A OUTPUT -o tun0 -j ACCEPT
101122

102-
# and over VPN interface to forwarded ports
123+
echo "Allowing connections over VPN interface to forwarded ports..."
103124
if [ ! -z $FORWARDED_PORTS ]; then
104125
for port in ${FORWARDED_PORTS//,/ }; do
105126
if [ $port -lt 1024 ] || [ $port -gt 65535 ]; then
@@ -110,6 +131,7 @@ if [ $KILL_SWITCH = "on" ]; then
110131
done
111132
fi
112133

134+
echo "Preventing anything else..."
113135
iptables -P INPUT DROP
114136
iptables -P OUTPUT DROP
115137
iptables -P FORWARD DROP
@@ -119,7 +141,22 @@ else
119141
echo "[WARN] VPN kill switch is disabled. Traffic will be allowed outside of the tunnel if the connection is lost."
120142
fi
121143

122-
################################################################################
144+
if [ "$SHADOWSOCKS" = "on" ]; then
145+
{
146+
echo "[INFO] Running Shadowsocks"
147+
while ! ping -c 1 1.1.1.1 > /dev/null 2>&1; do
148+
sleep 1
149+
done
150+
151+
sed -i \
152+
-e "/server_port/c\ \"server_port\": ${SHADOWSOCKS_PORT:-8388}," \
153+
-e "/password/c\ \"password\": \"${SHADOWSOCKS_PASS:-password}\"," \
154+
/data/shadowsocks.conf
155+
156+
sleep 1
157+
ss-server -c /data/shadowsocks.conf
158+
} &
159+
fi
123160

124161
if [ "$TINYPROXY" = "on" ]; then
125162
# start list of commands to run Tinyproxy
@@ -147,29 +184,13 @@ if [ "$TINYPROXY" = "on" ]; then
147184
fi
148185

149186
sleep 1
150-
tinyproxy -c /data/tinyproxy.conf
151-
} &
152-
fi
153-
154-
if [ "$SHADOWSOCKS" = "on" ]; then
155-
{
156-
echo "[INFO] Running Shadowsocks"
157-
while ! ping -c 1 1.1.1.1 > /dev/null 2>&1; do
158-
sleep 1
159-
done
160-
161-
sed -i \
162-
-e "/server_port/c\ \"server_port\": ${SHADOWSOCKS_PORT:-8388}," \
163-
-e "/password/c\ \"password\": \"${SHADOWSOCKS_PASS:-password}\"," \
164-
/data/shadowsocks.conf
165-
166-
sleep 1
167-
ss-server -c /data/shadowsocks.conf
187+
tinyproxy -c /data/tinyproxy.conf &
168188
} &
169189
fi
170190

171-
cd /etc/openvpn
172-
173191
echo "[INFO] Running OpenVPN"
174192

175-
openvpn --verb $vpn_log_level --auth-nocache --config $config_file
193+
openvpn --verb $vpn_log_level --auth-nocache --cd /data/vpn --config $config_file_modified &
194+
195+
openvpn_child=$!
196+
wait $openvpn_child

0 commit comments

Comments
 (0)