Skip to content

Commit af4ece2

Browse files
committed
Import the repo initially
1 parent 9122f7c commit af4ece2

22 files changed

+946
-0
lines changed

.dockerignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/cache/
2+
/transmission-files/

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/cache/
2+
/transmission-files/
3+
/transmission-state/
4+
!/transmission-state/settings.json

Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM ubuntu:20.04
2+
RUN export DEBIAN_FRONTEND=noninteractive \
3+
&& apt-get update -y -qq \
4+
&& apt-get install -y \
5+
curl jq toilet colorized-logs rsync \
6+
dnsutils iputils-ping traceroute iproute2 iptables tcpdump \
7+
openvpn \
8+
transmission-daemon \
9+
&& apt-get autoremove -y \
10+
&& apt-get clean -y \
11+
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*

LICENSE.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2020 Sergey Vasilyev
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# VPN-in-Docker with a network lock
2+
3+
It is ogranised as a collection of containers, each doing its job:
4+
5+
* **Network** — a shared networking/firewalling namespace for all containers.
6+
* **OpenVPN** — tunnels the traffic through VPN (openvpn-client).
7+
* **Firewall** — blocks the untunnelled traffic with a firewall (iptables).
8+
* **RuleMaker** — generates the firewall rules to be applied atomically.
9+
* **Status** — monitors the status of the setup and prints it to stdout.
10+
* **WebView** — publishes the monitor's status via HTTP (static nginx).
11+
12+
Any amount of other containers can be added to run arbitrary application:
13+
14+
* **Transmission** — run securely as a sample application.
15+
16+
All components are optional and can be disabled. Though, without some of them, the solution makes no sense and will not function (the traffic will be blocked, or the apps will never start).
17+
18+
The setup does not affect other containers or applications running in the same Docker.
19+
20+
Only IPv4 addresses and traffic are currently supported. IPv6 is disabled and blocked.
21+
22+
[AirVPN](https://airvpn.org/) is used as a VPN provider, but any other OpenVPN-compatible one can be used (if you have a config file for OpenVPN and know their IP ranges for monitoring/alerting).
23+
24+
25+
## Usage
26+
27+
To start:
28+
29+
```shell script
30+
docker-compose build
31+
docker-compose up
32+
```
33+
34+
Then, open:
35+
36+
* http://localhost:9090/
37+
* http://localhost:9091/
38+
39+
Or download and install [transmission-remote-gui](https://github.com/transmission-remote-gui/transgui) and configure a connection with `localhost` as the hostname.
40+
41+
Download [Ubuntu via BitTorrent](https://ubuntu.com/download/alternative-downloads) (either server or desktop, any version).
42+
43+
Stop with Ctrl+C (docker-compose will stop the containers).
44+
45+
To clean it up:
46+
47+
```shell script
48+
docker-compose down --volumes --remove-orphans
49+
```
50+
51+
52+
## Monitoring
53+
54+
When the network is fully secured, you will see this status:
55+
56+
* The VPN's detected country is in green (acceptable).
57+
* The default next-hop IP address is in green (acceptable).
58+
* `eth*` interfaces show "Operation not permitted".
59+
* `tun*` interfaces show some pinging and timing.
60+
61+
![](screenshots/protected.png)
62+
63+
---
64+
65+
When VPN is down, but the traffic is still secured:
66+
67+
* The VPN's detected country is absent (acceptable).
68+
* The default next-hop IP address is absent (acceptable).
69+
* `eth*` interfaces show "Operation not permitted".
70+
* `tun*` interfaces are absent.
71+
72+
![](screenshots/disconnected.png)
73+
74+
To simulate:
75+
76+
```shell script
77+
docker-compose stop openvpn
78+
```
79+
80+
To restore:
81+
82+
```shell script
83+
docker-compose start openvpn
84+
```
85+
86+
---
87+
88+
When the network is exposed, the status reporting looks like this:
89+
90+
* The VPN's detected country is in red and flashing (compromised).
91+
* The default next-hop IP address is in red (compromised).
92+
* `eth*` interfaces show some pinging and timing (they must not).
93+
* `tun*` interfaces are either absent or show something.
94+
95+
![](screenshots/exposed.png)
96+
97+
To simulate:
98+
99+
```shell script
100+
docker-compose stop openvpn firewall
101+
docker-compose exec network iptables -F
102+
docker-compose exec network iptables -P INPUT ACCEPT
103+
docker-compose exec network iptables -P OUTPUT ACCEPT
104+
```
105+
106+
To restore:
107+
108+
```shell script
109+
docker-compose start openvpn firewall
110+
```
111+
112+
Please note that to expose yourself, you need to do both: configure the firewall to pass the traffic **AND** shut down the VPN connection. As long as the VPN connection is alive, the traffic goes through it even if the firewall is in the permissive mode.
113+
114+
115+
## Implementation details
116+
117+
### Shared network container
118+
119+
All of the containers use the shared network space of a special pseudo-container: it sleeps forever, and is only used as a shared network namespace with iptables.
120+
121+
**Why not Docker networks?** In that case, each container has its own iptables namespace, and so the firewall rules do not apply to all of them equally. With the shared container's network, they all run in the same networking context.

apply-firewall.sh

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
# Apply the firewall atomically as soon as it appears.
3+
#
4+
# The firewall files are generated by `iptables-save`/`ip6tables-save`
5+
# in a separate container for building the firewall rules.
6+
#
7+
8+
# Where to get the iptables dump files.
9+
: ${IPTABLES_FILE_V4:="/tmp/iptables.txt"}
10+
: ${IPTABLES_FILE_V6:="/tmp/ip6tables.txt"}
11+
12+
if [[ -e "${IPTABLES_FILE_V4}" ]]; then
13+
iptables-restore <"${IPTABLES_FILE_V4}"
14+
rm -f "${IPTABLES_FILE_V4}"
15+
echo "The firewall is applied (v4)."
16+
fi
17+
18+
if [[ -e "${IPTABLES_FILE_V6}" ]]; then
19+
ip6tables-restore <"${IPTABLES_FILE_V6}"
20+
rm -f "${IPTABLES_FILE_V6}"
21+
echo "The firewall is applied (v6)."
22+
fi

cache/.keep

Whitespace-only changes.

docker-compose.yaml

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
version: "3"
2+
3+
networks:
4+
# A non-default network is needed to control the IP address ranges (used in
5+
# some configs), and to avoid affecting other containers in the same Docker.
6+
vpn-network:
7+
driver: bridge
8+
ipam:
9+
driver: default
10+
config:
11+
- subnet: "172.30.172.0/24"
12+
enable_ipv6: false
13+
14+
services:
15+
16+
# A sample application that runs securely only through the VPN, not directly.
17+
# It will not actually start until the firewall rules are applied (either
18+
# for the first time on creation, or pre-existing block-rules on restarts).
19+
# There can any amount of apps configured in the same setup: 0, 1, 2, so on.
20+
# If stopped, nothing will happen - the VPN remains available for other apps.
21+
transmission:
22+
build: .
23+
entrypoint: [/app/wait-for-safety.sh]
24+
command: [/app/transmission.sh]
25+
environment:
26+
LOCAL_IPS: 172.30.172.*
27+
PEER_PORT: 46112 # as (and if) configured in the VPN provider
28+
volumes:
29+
- ./:/app
30+
- ./transmission-state:/var/lib/transmission
31+
- ./transmission-files:/mnt/files
32+
- ~/Downloads:/mnt/downloads
33+
- ~/Movies:/mnt/movies
34+
cap_add: [NET_ADMIN] # needed for the `wait-for-safety.sh` script
35+
restart: unless-stopped
36+
stop_signal: SIGTERM
37+
network_mode: service:network # CRITICALLY IMPORTANT!
38+
39+
# A shared container that is used as a network. It does nothing but sleeps.
40+
# Native Docker's networks cannot share the iptables rules cross containers.
41+
# The ports of all containers are shared here, as the network-bound containers
42+
# cannot share their own ports (including the VPN-secured application).
43+
network:
44+
build: .
45+
command: sleep infinity
46+
cap_add: [NET_ADMIN] # needed only for debugging and README's simulations
47+
stop_signal: SIGKILL
48+
restart: always
49+
dns: [8.8.4.4]
50+
ports:
51+
- "127.0.0.1:9091:9091" # application's ports
52+
networks:
53+
- vpn-network
54+
55+
# Evaluates the status of the setup, and prints a colorful message about that.
56+
# It also generates an HTML file that is later served by the web-view server.
57+
# If stopped, the status is not checked and not updated, the old one is shown.
58+
status:
59+
build: .
60+
command:
61+
- bash
62+
- -c
63+
- |
64+
while true; do
65+
/report-status.sh
66+
cat /status/index.ansi
67+
sleep 5
68+
done
69+
environment:
70+
NS: 8.8.4.4
71+
TZ: Europe/Berlin
72+
STATUS_DIR: /status
73+
env_file:
74+
- ipstack.env
75+
volumes:
76+
- ./report-status.sh:/report-status.sh:ro
77+
- html:/status:rw
78+
restart: unless-stopped
79+
stop_signal: SIGKILL
80+
network_mode: service:network # CRITICALLY IMPORTANT!
81+
82+
# Connects and reconnects to the remote VPN server, creates the `tun` device,
83+
# configures the default traffic routing through VPN (only when connected).
84+
# If stopped, the `tun` device disappears for all other containers,
85+
# and the traffic is routed through the default `eth` device (if not blocked).
86+
openvpn:
87+
build: .
88+
command: ["openvpn", "--config", "client.conf"]
89+
volumes:
90+
- ./openvpn:/etc/openvpn:ro
91+
working_dir: /etc/openvpn/airvpn
92+
devices: [/dev/net/tun]
93+
cap_add: [NET_ADMIN]
94+
restart: unless-stopped
95+
stop_signal: SIGTERM
96+
network_mode: service:network # CRITICALLY IMPORTANT!
97+
98+
# Applies the firewall rules to block the traffic from going around VPN.
99+
# If stopped, the iptables rules remain applied, but are not re-applied,
100+
# which allows their modification manually (incl. unblocking the traffic).
101+
firewall:
102+
build: .
103+
command:
104+
- bash
105+
- -c
106+
- |
107+
while true; do
108+
/apply-firewall.sh
109+
sleep 1s
110+
done
111+
environment:
112+
IPTABLES_FILE_V4: /iptables/iptables-v4.txt
113+
IPTABLES_FILE_V6: /iptables/iptables-v6.txt
114+
volumes:
115+
- ./apply-firewall.sh:/apply-firewall.sh:ro
116+
- iptables:/iptables
117+
cap_add: [NET_ADMIN]
118+
restart: unless-stopped
119+
stop_signal: SIGKILL
120+
network_mode: service:network # CRITICALLY IMPORTANT!
121+
122+
# Generates the firewall rules to be atmomically applied in another container.
123+
# It also resolves the IP addresses of the VPN provider into an allow-list,
124+
# so that the firewall would not block it on the default `eth` interface.
125+
# See the notes in `generate-firewall.sh` on why this needs to be isolated.
126+
# If stopped, the dump files are not generated, so they will not be applied.
127+
rulemaker:
128+
build: .
129+
command:
130+
- bash
131+
- -c
132+
- |
133+
IPTABLES_FILE_V4=/tmp/null4 \
134+
IPTABLES_FILE_V6=/tmp/null6 \
135+
ALLOWED_IPS_FILE= ALLOWED_IPS_DIR= \
136+
/generate-firewall.sh # silent insta-block!
137+
138+
while true; do
139+
/update-airvpn-ips.sh
140+
/generate-firewall.sh
141+
sleep 600
142+
done
143+
environment:
144+
IPTABLES_FILE_V4: /iptables/iptables-v4.txt
145+
IPTABLES_FILE_V6: /iptables/iptables-v6.txt
146+
ALLOWED_IPS_FILE: /cache/all.txt
147+
ALLOWED_IPS_DIR: /cache
148+
LOCAL_IPS: 172.30.172.0/24
149+
STATUS_IP: 139.130.4.5
150+
NS: 8.8.4.4
151+
volumes:
152+
- ./cache:/cache
153+
- ./update-airvpn-ips.sh:/update-airvpn-ips.sh:ro
154+
- ./generate-firewall.sh:/generate-firewall.sh:ro
155+
- iptables:/iptables
156+
dns: [8.8.4.4, 8.8.8.8]
157+
cap_add: [NET_ADMIN]
158+
restart: unless-stopped
159+
stop_signal: SIGKILL
160+
network_mode: bridge # NB! See the comment above, and generate-firewall.sh.
161+
162+
# A supplimentary web server to publish the HTML status page.
163+
# If stopped, the status will not be served via HTTP, but will be shown
164+
# in the output anyway; the HTML page will also be generated anyway.
165+
# Note: it is not a part of the firewalled network, as there is no need
166+
# for utilities to be firewalled. And so, it can have its own ports exposed.
167+
# TODO: Is there a proper "Docker way" to run nginx in the "status" container?
168+
webview:
169+
image: nginx
170+
volumes:
171+
- ./nginx-no-access-log.conf:/etc/nginx/conf.d/nginx-no-access-log.conf:ro
172+
- html:/usr/share/nginx/html:ro
173+
restart: unless-stopped
174+
stop_signal: SIGTERM
175+
ports:
176+
- "127.0.0.1:9090:80"
177+
178+
volumes:
179+
iptables:
180+
html:

0 commit comments

Comments
 (0)