-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
946 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/cache/ | ||
/transmission-files/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/cache/ | ||
/transmission-files/ | ||
/transmission-state/ | ||
!/transmission-state/settings.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
FROM ubuntu:20.04 | ||
RUN export DEBIAN_FRONTEND=noninteractive \ | ||
&& apt-get update -y -qq \ | ||
&& apt-get install -y \ | ||
curl jq toilet colorized-logs rsync \ | ||
dnsutils iputils-ping traceroute iproute2 iptables tcpdump \ | ||
openvpn \ | ||
transmission-daemon \ | ||
&& apt-get autoremove -y \ | ||
&& apt-get clean -y \ | ||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2020 Sergey Vasilyev | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# VPN-in-Docker with a network lock | ||
|
||
It is ogranised as a collection of containers, each doing its job: | ||
|
||
* **Network** — a shared networking/firewalling namespace for all containers. | ||
* **OpenVPN** — tunnels the traffic through VPN (openvpn-client). | ||
* **Firewall** — blocks the untunnelled traffic with a firewall (iptables). | ||
* **RuleMaker** — generates the firewall rules to be applied atomically. | ||
* **Status** — monitors the status of the setup and prints it to stdout. | ||
* **WebView** — publishes the monitor's status via HTTP (static nginx). | ||
|
||
Any amount of other containers can be added to run arbitrary application: | ||
|
||
* **Transmission** — run securely as a sample application. | ||
|
||
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). | ||
|
||
The setup does not affect other containers or applications running in the same Docker. | ||
|
||
Only IPv4 addresses and traffic are currently supported. IPv6 is disabled and blocked. | ||
|
||
[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). | ||
|
||
|
||
## Usage | ||
|
||
To start: | ||
|
||
```shell script | ||
docker-compose build | ||
docker-compose up | ||
``` | ||
|
||
Then, open: | ||
|
||
* http://localhost:9090/ | ||
* http://localhost:9091/ | ||
|
||
Or download and install [transmission-remote-gui](https://github.com/transmission-remote-gui/transgui) and configure a connection with `localhost` as the hostname. | ||
|
||
Download [Ubuntu via BitTorrent](https://ubuntu.com/download/alternative-downloads) (either server or desktop, any version). | ||
|
||
Stop with Ctrl+C (docker-compose will stop the containers). | ||
|
||
To clean it up: | ||
|
||
```shell script | ||
docker-compose down --volumes --remove-orphans | ||
``` | ||
|
||
|
||
## Monitoring | ||
|
||
When the network is fully secured, you will see this status: | ||
|
||
* The VPN's detected country is in green (acceptable). | ||
* The default next-hop IP address is in green (acceptable). | ||
* `eth*` interfaces show "Operation not permitted". | ||
* `tun*` interfaces show some pinging and timing. | ||
|
||
 | ||
|
||
--- | ||
|
||
When VPN is down, but the traffic is still secured: | ||
|
||
* The VPN's detected country is absent (acceptable). | ||
* The default next-hop IP address is absent (acceptable). | ||
* `eth*` interfaces show "Operation not permitted". | ||
* `tun*` interfaces are absent. | ||
|
||
 | ||
|
||
To simulate: | ||
|
||
```shell script | ||
docker-compose stop openvpn | ||
``` | ||
|
||
To restore: | ||
|
||
```shell script | ||
docker-compose start openvpn | ||
``` | ||
|
||
--- | ||
|
||
When the network is exposed, the status reporting looks like this: | ||
|
||
* The VPN's detected country is in red and flashing (compromised). | ||
* The default next-hop IP address is in red (compromised). | ||
* `eth*` interfaces show some pinging and timing (they must not). | ||
* `tun*` interfaces are either absent or show something. | ||
|
||
 | ||
|
||
To simulate: | ||
|
||
```shell script | ||
docker-compose stop openvpn firewall | ||
docker-compose exec network iptables -F | ||
docker-compose exec network iptables -P INPUT ACCEPT | ||
docker-compose exec network iptables -P OUTPUT ACCEPT | ||
``` | ||
|
||
To restore: | ||
|
||
```shell script | ||
docker-compose start openvpn firewall | ||
``` | ||
|
||
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. | ||
|
||
|
||
## Implementation details | ||
|
||
### Shared network container | ||
|
||
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. | ||
|
||
**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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
#!/usr/bin/env bash | ||
# Apply the firewall atomically as soon as it appears. | ||
# | ||
# The firewall files are generated by `iptables-save`/`ip6tables-save` | ||
# in a separate container for building the firewall rules. | ||
# | ||
|
||
# Where to get the iptables dump files. | ||
: ${IPTABLES_FILE_V4:="/tmp/iptables.txt"} | ||
: ${IPTABLES_FILE_V6:="/tmp/ip6tables.txt"} | ||
|
||
if [[ -e "${IPTABLES_FILE_V4}" ]]; then | ||
iptables-restore <"${IPTABLES_FILE_V4}" | ||
rm -f "${IPTABLES_FILE_V4}" | ||
echo "The firewall is applied (v4)." | ||
fi | ||
|
||
if [[ -e "${IPTABLES_FILE_V6}" ]]; then | ||
ip6tables-restore <"${IPTABLES_FILE_V6}" | ||
rm -f "${IPTABLES_FILE_V6}" | ||
echo "The firewall is applied (v6)." | ||
fi |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
version: "3" | ||
|
||
networks: | ||
# A non-default network is needed to control the IP address ranges (used in | ||
# some configs), and to avoid affecting other containers in the same Docker. | ||
vpn-network: | ||
driver: bridge | ||
ipam: | ||
driver: default | ||
config: | ||
- subnet: "172.30.172.0/24" | ||
enable_ipv6: false | ||
|
||
services: | ||
|
||
# A sample application that runs securely only through the VPN, not directly. | ||
# It will not actually start until the firewall rules are applied (either | ||
# for the first time on creation, or pre-existing block-rules on restarts). | ||
# There can any amount of apps configured in the same setup: 0, 1, 2, so on. | ||
# If stopped, nothing will happen - the VPN remains available for other apps. | ||
transmission: | ||
build: . | ||
entrypoint: [/app/wait-for-safety.sh] | ||
command: [/app/transmission.sh] | ||
environment: | ||
LOCAL_IPS: 172.30.172.* | ||
PEER_PORT: 46112 # as (and if) configured in the VPN provider | ||
volumes: | ||
- ./:/app | ||
- ./transmission-state:/var/lib/transmission | ||
- ./transmission-files:/mnt/files | ||
- ~/Downloads:/mnt/downloads | ||
- ~/Movies:/mnt/movies | ||
cap_add: [NET_ADMIN] # needed for the `wait-for-safety.sh` script | ||
restart: unless-stopped | ||
stop_signal: SIGTERM | ||
network_mode: service:network # CRITICALLY IMPORTANT! | ||
|
||
# A shared container that is used as a network. It does nothing but sleeps. | ||
# Native Docker's networks cannot share the iptables rules cross containers. | ||
# The ports of all containers are shared here, as the network-bound containers | ||
# cannot share their own ports (including the VPN-secured application). | ||
network: | ||
build: . | ||
command: sleep infinity | ||
cap_add: [NET_ADMIN] # needed only for debugging and README's simulations | ||
stop_signal: SIGKILL | ||
restart: always | ||
dns: [8.8.4.4] | ||
ports: | ||
- "127.0.0.1:9091:9091" # application's ports | ||
networks: | ||
- vpn-network | ||
|
||
# Evaluates the status of the setup, and prints a colorful message about that. | ||
# It also generates an HTML file that is later served by the web-view server. | ||
# If stopped, the status is not checked and not updated, the old one is shown. | ||
status: | ||
build: . | ||
command: | ||
- bash | ||
- -c | ||
- | | ||
while true; do | ||
/report-status.sh | ||
cat /status/index.ansi | ||
sleep 5 | ||
done | ||
environment: | ||
NS: 8.8.4.4 | ||
TZ: Europe/Berlin | ||
STATUS_DIR: /status | ||
env_file: | ||
- ipstack.env | ||
volumes: | ||
- ./report-status.sh:/report-status.sh:ro | ||
- html:/status:rw | ||
restart: unless-stopped | ||
stop_signal: SIGKILL | ||
network_mode: service:network # CRITICALLY IMPORTANT! | ||
|
||
# Connects and reconnects to the remote VPN server, creates the `tun` device, | ||
# configures the default traffic routing through VPN (only when connected). | ||
# If stopped, the `tun` device disappears for all other containers, | ||
# and the traffic is routed through the default `eth` device (if not blocked). | ||
openvpn: | ||
build: . | ||
command: ["openvpn", "--config", "client.conf"] | ||
volumes: | ||
- ./openvpn:/etc/openvpn:ro | ||
working_dir: /etc/openvpn/airvpn | ||
devices: [/dev/net/tun] | ||
cap_add: [NET_ADMIN] | ||
restart: unless-stopped | ||
stop_signal: SIGTERM | ||
network_mode: service:network # CRITICALLY IMPORTANT! | ||
|
||
# Applies the firewall rules to block the traffic from going around VPN. | ||
# If stopped, the iptables rules remain applied, but are not re-applied, | ||
# which allows their modification manually (incl. unblocking the traffic). | ||
firewall: | ||
build: . | ||
command: | ||
- bash | ||
- -c | ||
- | | ||
while true; do | ||
/apply-firewall.sh | ||
sleep 1s | ||
done | ||
environment: | ||
IPTABLES_FILE_V4: /iptables/iptables-v4.txt | ||
IPTABLES_FILE_V6: /iptables/iptables-v6.txt | ||
volumes: | ||
- ./apply-firewall.sh:/apply-firewall.sh:ro | ||
- iptables:/iptables | ||
cap_add: [NET_ADMIN] | ||
restart: unless-stopped | ||
stop_signal: SIGKILL | ||
network_mode: service:network # CRITICALLY IMPORTANT! | ||
|
||
# Generates the firewall rules to be atmomically applied in another container. | ||
# It also resolves the IP addresses of the VPN provider into an allow-list, | ||
# so that the firewall would not block it on the default `eth` interface. | ||
# See the notes in `generate-firewall.sh` on why this needs to be isolated. | ||
# If stopped, the dump files are not generated, so they will not be applied. | ||
rulemaker: | ||
build: . | ||
command: | ||
- bash | ||
- -c | ||
- | | ||
IPTABLES_FILE_V4=/tmp/null4 \ | ||
IPTABLES_FILE_V6=/tmp/null6 \ | ||
ALLOWED_IPS_FILE= ALLOWED_IPS_DIR= \ | ||
/generate-firewall.sh # silent insta-block! | ||
while true; do | ||
/update-airvpn-ips.sh | ||
/generate-firewall.sh | ||
sleep 600 | ||
done | ||
environment: | ||
IPTABLES_FILE_V4: /iptables/iptables-v4.txt | ||
IPTABLES_FILE_V6: /iptables/iptables-v6.txt | ||
ALLOWED_IPS_FILE: /cache/all.txt | ||
ALLOWED_IPS_DIR: /cache | ||
LOCAL_IPS: 172.30.172.0/24 | ||
STATUS_IP: 139.130.4.5 | ||
NS: 8.8.4.4 | ||
volumes: | ||
- ./cache:/cache | ||
- ./update-airvpn-ips.sh:/update-airvpn-ips.sh:ro | ||
- ./generate-firewall.sh:/generate-firewall.sh:ro | ||
- iptables:/iptables | ||
dns: [8.8.4.4, 8.8.8.8] | ||
cap_add: [NET_ADMIN] | ||
restart: unless-stopped | ||
stop_signal: SIGKILL | ||
network_mode: bridge # NB! See the comment above, and generate-firewall.sh. | ||
|
||
# A supplimentary web server to publish the HTML status page. | ||
# If stopped, the status will not be served via HTTP, but will be shown | ||
# in the output anyway; the HTML page will also be generated anyway. | ||
# Note: it is not a part of the firewalled network, as there is no need | ||
# for utilities to be firewalled. And so, it can have its own ports exposed. | ||
# TODO: Is there a proper "Docker way" to run nginx in the "status" container? | ||
webview: | ||
image: nginx | ||
volumes: | ||
- ./nginx-no-access-log.conf:/etc/nginx/conf.d/nginx-no-access-log.conf:ro | ||
- html:/usr/share/nginx/html:ro | ||
restart: unless-stopped | ||
stop_signal: SIGTERM | ||
ports: | ||
- "127.0.0.1:9090:80" | ||
|
||
volumes: | ||
iptables: | ||
html: |
Oops, something went wrong.