diff options
| author | hc <hc@a.nub.ninja> | 2026-02-07 12:15:01 +0000 |
|---|---|---|
| committer | hc <hc@a.nub.ninja> | 2026-02-07 12:15:01 +0000 |
| commit | f6cdeabe2f57b97299308e16486958ed122315b9 (patch) | |
| tree | c570f1b5ef373eb251c8504992b95f1b7789746f | |
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Dockerfile.mullvad | 20 | ||||
| -rw-r--r-- | docker-compose.yml | 14 | ||||
| -rw-r--r-- | docs | 35 | ||||
| -rw-r--r-- | entrypoint.sh | 82 | ||||
| -rw-r--r-- | torrent/Dockerfile | 14 | ||||
| -rw-r--r-- | torrent/docker-compose.yml | 8 | ||||
| -rw-r--r-- | wg0.conf.example | 17 |
8 files changed, 191 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c4cb47 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| wg0.conf | |||
diff --git a/Dockerfile.mullvad b/Dockerfile.mullvad new file mode 100644 index 0000000..9998d60 --- /dev/null +++ b/Dockerfile.mullvad | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | FROM rockylinux/rockylinux:10 | ||
| 2 | |||
| 3 | RUN dnf install -y epel-release && \ | ||
| 4 | dnf install -y \ | ||
| 5 | wireguard-tools \ | ||
| 6 | iptables \ | ||
| 7 | iproute \ | ||
| 8 | curl \ | ||
| 9 | procps-ng \ | ||
| 10 | && dnf clean all | ||
| 11 | |||
| 12 | # Copy WireGuard config (exported from Mullvad website) | ||
| 13 | COPY wg0.conf /etc/wireguard/wg0.conf | ||
| 14 | RUN chmod 600 /etc/wireguard/wg0.conf | ||
| 15 | |||
| 16 | # Kill switch: only allow traffic through the VPN tunnel | ||
| 17 | COPY entrypoint.sh /entrypoint.sh | ||
| 18 | RUN chmod +x /entrypoint.sh | ||
| 19 | |||
| 20 | ENTRYPOINT ["/entrypoint.sh"] | ||
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..96437e5 --- /dev/null +++ b/docker-compose.yml | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | services: | ||
| 2 | mullvad: | ||
| 3 | build: { context: ., dockerfile: Dockerfile.mullvad } | ||
| 4 | container_name: mullvad-vpn | ||
| 5 | cap_add: [NET_ADMIN] | ||
| 6 | devices: [/dev/net/tun:/dev/net/tun] | ||
| 7 | restart: unless-stopped | ||
| 8 | |||
| 9 | # Example: route any container through the VPN | ||
| 10 | app1: | ||
| 11 | image: rockylinux/rockylinux:10 | ||
| 12 | network_mode: "container:mullvad-vpn" | ||
| 13 | depends_on: [mullvad] | ||
| 14 | command: bash -c "dnf install -y curl && curl -s https://am.i.mullvad.net/connected && curl -s https://am.i.mullvad.net/ip && curl -s https://am.i.mullvad.net/country && curl -s https://am.i.mullvad.net/city" | ||
| @@ -0,0 +1,35 @@ | |||
| 1 | ## Setup | ||
| 2 | |||
| 3 | Both must use --in-pod=false so the torrent container can attach to the VPN container's network. | ||
| 4 | Downloads appear in /root/downloads on the host. | ||
| 5 | |||
| 6 | cd /root/mullvad-docker && podman compose --in-pod=false up -d | ||
| 7 | cd /root/mullvad-docker/torrent && podman compose --in-pod=false up -d | ||
| 8 | |||
| 9 | ## Shell into container | ||
| 10 | |||
| 11 | podman exec -it torrent bash | ||
| 12 | |||
| 13 | ## aria2p commands | ||
| 14 | |||
| 15 | # Add downloads | ||
| 16 | aria2p add "magnet:?xt=urn:btih:..." | ||
| 17 | aria2p add "https://example.com/file.zip" | ||
| 18 | aria2p add /path/to/file.torrent | ||
| 19 | |||
| 20 | # Monitor | ||
| 21 | aria2p show # list all downloads | ||
| 22 | aria2p top # live TUI | ||
| 23 | |||
| 24 | # Control | ||
| 25 | aria2p pause <GID> # pause one | ||
| 26 | aria2p resume <GID> # resume one | ||
| 27 | aria2p remove <GID> # remove one | ||
| 28 | aria2p pause-all | ||
| 29 | aria2p resume-all | ||
| 30 | |||
| 31 | # Cleanup | ||
| 32 | aria2p purge # clear completed/errored from list | ||
| 33 | |||
| 34 | GID is the hex ID from aria2p show. | ||
| 35 | |||
diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..2316e01 --- /dev/null +++ b/entrypoint.sh | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | #!/bin/bash | ||
| 2 | set -e | ||
| 3 | |||
| 4 | WG_CONF="/etc/wireguard/wg0.conf" | ||
| 5 | |||
| 6 | # Parse the config file | ||
| 7 | PRIVATE_KEY=$(grep -oP 'PrivateKey\s*=\s*\K.*' "$WG_CONF" | tr -d ' ') | ||
| 8 | ADDRESS_V4=$(grep -oP 'Address\s*=\s*\K[^,]+' "$WG_CONF" | grep -v ':' | tr -d ' ') | ||
| 9 | ADDRESS_V6=$(grep -oP 'Address\s*=\s*\K.*' "$WG_CONF" | grep -oP '[^,]*::[^,]*' | tr -d ' ') | ||
| 10 | DNS_SERVERS=$(grep -oP 'DNS\s*=\s*\K.*' "$WG_CONF" | tr ',' '\n' | tr -d ' ') | ||
| 11 | PEER_PUBKEY=$(grep -oP 'PublicKey\s*=\s*\K.*' "$WG_CONF" | tr -d ' ') | ||
| 12 | ENDPOINT=$(grep -oP 'Endpoint\s*=\s*\K.*' "$WG_CONF" | tr -d ' ') | ||
| 13 | WG_ENDPOINT=$(echo "$ENDPOINT" | cut -d: -f1) | ||
| 14 | WG_PORT=$(echo "$ENDPOINT" | cut -d: -f2) | ||
| 15 | |||
| 16 | # Set DNS manually | ||
| 17 | if [ -n "$DNS_SERVERS" ]; then | ||
| 18 | : > /etc/resolv.conf | ||
| 19 | for dns in $DNS_SERVERS; do | ||
| 20 | echo "nameserver $dns" >> /etc/resolv.conf | ||
| 21 | done | ||
| 22 | fi | ||
| 23 | |||
| 24 | DEFAULT_IF=$(ip route | awk '/default/ {print $5; exit}') | ||
| 25 | DEFAULT_GW=$(ip route | awk '/default/ {print $3; exit}') | ||
| 26 | |||
| 27 | # --- IPv4 kill switch --- | ||
| 28 | iptables -A INPUT -i lo -j ACCEPT | ||
| 29 | iptables -A OUTPUT -o lo -j ACCEPT | ||
| 30 | iptables -A OUTPUT -d "$WG_ENDPOINT" -p udp --dport "$WG_PORT" -j ACCEPT | ||
| 31 | iptables -A INPUT -s "$WG_ENDPOINT" -p udp --sport "$WG_PORT" -j ACCEPT | ||
| 32 | iptables -A INPUT -i wg0 -j ACCEPT | ||
| 33 | iptables -A OUTPUT -o wg0 -j ACCEPT | ||
| 34 | iptables -A INPUT -i "$DEFAULT_IF" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT | ||
| 35 | # Allow container-to-container traffic | ||
| 36 | iptables -A INPUT -i eth0 -j ACCEPT | ||
| 37 | iptables -A OUTPUT -o eth0 -d 172.16.0.0/12 -j ACCEPT | ||
| 38 | iptables -A OUTPUT -o eth0 -d 10.0.0.0/8 -j ACCEPT | ||
| 39 | iptables -A OUTPUT -o eth0 -d 192.168.0.0/16 -j ACCEPT | ||
| 40 | iptables -A INPUT -j DROP | ||
| 41 | iptables -A OUTPUT -j DROP | ||
| 42 | |||
| 43 | # --- IPv6 kill switch --- | ||
| 44 | ip6tables -A INPUT -i lo -j ACCEPT | ||
| 45 | ip6tables -A OUTPUT -o lo -j ACCEPT | ||
| 46 | ip6tables -A INPUT -i wg0 -j ACCEPT | ||
| 47 | ip6tables -A OUTPUT -o wg0 -j ACCEPT | ||
| 48 | ip6tables -A INPUT -j DROP | ||
| 49 | ip6tables -A OUTPUT -j DROP | ||
| 50 | |||
| 51 | # --- Bring up WireGuard manually (no wg-quick) --- | ||
| 52 | ip link add wg0 type wireguard | ||
| 53 | echo "$PRIVATE_KEY" | wg set wg0 private-key /dev/stdin peer "$PEER_PUBKEY" endpoint "$ENDPOINT" allowed-ips 0.0.0.0/0,::/0 | ||
| 54 | |||
| 55 | [ -n "$ADDRESS_V4" ] && ip addr add "$ADDRESS_V4" dev wg0 | ||
| 56 | [ -n "$ADDRESS_V6" ] && ip addr add "$ADDRESS_V6" dev wg0 | ||
| 57 | |||
| 58 | ip link set wg0 up | ||
| 59 | |||
| 60 | # Route the WireGuard endpoint via the real gateway (avoid routing loop) | ||
| 61 | ip route add "$WG_ENDPOINT"/32 via "$DEFAULT_GW" dev "$DEFAULT_IF" | ||
| 62 | |||
| 63 | # Route all other traffic through the tunnel | ||
| 64 | ip route add 0.0.0.0/1 dev wg0 | ||
| 65 | ip route add 128.0.0.0/1 dev wg0 | ||
| 66 | |||
| 67 | # IPv6 routes through the tunnel | ||
| 68 | if [ -n "$ADDRESS_V6" ]; then | ||
| 69 | ip -6 route add ::/1 dev wg0 | ||
| 70 | ip -6 route add 8000::/1 dev wg0 | ||
| 71 | fi | ||
| 72 | |||
| 73 | echo "VPN is up. Checking connection..." | ||
| 74 | curl -s --max-time 10 https://am.i.mullvad.net/connected || echo "Warning: could not verify Mullvad connection" | ||
| 75 | echo "Public IP: $(curl -s --max-time 10 https://am.i.mullvad.net/ip) | Location: $(curl -s --max-time 10 https://am.i.mullvad.net/country), $(curl -s --max-time 10 https://am.i.mullvad.net/city)" | ||
| 76 | |||
| 77 | echo "VPN gateway ready." | ||
| 78 | |||
| 79 | # Keep the container running, exit gracefully on SIGTERM | ||
| 80 | trap 'echo "Shutting down VPN..."; ip link del wg0 2>/dev/null; exit 0' SIGTERM SIGINT | ||
| 81 | sleep infinity & | ||
| 82 | wait $! | ||
diff --git a/torrent/Dockerfile b/torrent/Dockerfile new file mode 100644 index 0000000..3a4fd53 --- /dev/null +++ b/torrent/Dockerfile | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | FROM rockylinux/rockylinux:10 | ||
| 2 | |||
| 3 | RUN dnf install -y epel-release && \ | ||
| 4 | dnf install -y aria2 python3-pip curl tmux bmon && \ | ||
| 5 | pip install aria2p[tui] && \ | ||
| 6 | dnf clean all | ||
| 7 | |||
| 8 | RUN echo 'alias ls="ls --color=auto"' >> /root/.bashrc && \ | ||
| 9 | echo 'alias ll="ls -lah --color=auto"' >> /root/.bashrc && \ | ||
| 10 | echo 'export PS1="[\u@torrent \W]\$ "' >> /root/.bashrc | ||
| 11 | |||
| 12 | WORKDIR /downloads | ||
| 13 | |||
| 14 | ENTRYPOINT ["aria2c", "--dir=/downloads", "--enable-rpc", "--rpc-listen-all", "--file-allocation=trunc", "--bt-tracker=udp://tracker.opentrackr.org:1337/announce,udp://open.stealth.si:80/announce,udp://tracker.torrent.eu.org:451/announce,udp://exodus.desync.com:6969/announce,udp://tracker.openbittorrent.com:6969/announce"] | ||
diff --git a/torrent/docker-compose.yml b/torrent/docker-compose.yml new file mode 100644 index 0000000..967934c --- /dev/null +++ b/torrent/docker-compose.yml | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | services: | ||
| 2 | torrent: | ||
| 3 | build: . | ||
| 4 | container_name: torrent | ||
| 5 | network_mode: "container:mullvad-vpn" | ||
| 6 | volumes: | ||
| 7 | - /root/downloads:/downloads:z | ||
| 8 | restart: unless-stopped | ||
diff --git a/wg0.conf.example b/wg0.conf.example new file mode 100644 index 0000000..893b9ee --- /dev/null +++ b/wg0.conf.example | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | # Download this from: https://mullvad.net/en/account/wireguard-config | ||
| 2 | # 1. Log into your Mullvad account | ||
| 3 | # 2. Go to WireGuard configuration | ||
| 4 | # 3. Generate a key and download a config file | ||
| 5 | # 4. Rename the downloaded file to wg0.conf and place it in this directory | ||
| 6 | # | ||
| 7 | # It will look something like this: | ||
| 8 | |||
| 9 | [Interface] | ||
| 10 | PrivateKey = YOUR_PRIVATE_KEY_HERE | ||
| 11 | Address = 10.x.x.x/32,fc00:bbbb:bbbb:bb01::x:xxxx/128 | ||
| 12 | DNS = 10.64.0.1 | ||
| 13 | |||
| 14 | [Peer] | ||
| 15 | PublicKey = SERVER_PUBLIC_KEY_HERE | ||
| 16 | AllowedIPs = 0.0.0.0/0,::0/0 | ||
| 17 | Endpoint = xxx.xxx.xxx.xxx:51820 | ||
