Skip to content

Commit

Permalink
Import the repo initially
Browse files Browse the repository at this point in the history
  • Loading branch information
nolar committed Sep 20, 2020
1 parent 9122f7c commit af4ece2
Show file tree
Hide file tree
Showing 22 changed files with 946 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/cache/
/transmission-files/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/cache/
/transmission-files/
/transmission-state/
!/transmission-state/settings.json
11 changes: 11 additions & 0 deletions Dockerfile
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/*
19 changes: 19 additions & 0 deletions LICENSE.md
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.
121 changes: 121 additions & 0 deletions README.md
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.

![](screenshots/protected.png)

---

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.

![](screenshots/disconnected.png)

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.

![](screenshots/exposed.png)

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.
22 changes: 22 additions & 0 deletions apply-firewall.sh
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 added cache/.keep
Empty file.
180 changes: 180 additions & 0 deletions docker-compose.yaml
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:
Loading

0 comments on commit af4ece2

Please sign in to comment.