diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..1f3c7898
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,62 @@
+{
+ "name": "pia-dev",
+ "dockerComposeFile": ["docker-compose.yml"],
+ "service": "vscode",
+ "runServices": ["vscode"],
+ "shutdownAction": "stopCompose",
+ // "postCreateCommand": "go mod download",
+ "workspaceFolder": "/workspace",
+ "extensions": [
+ "ms-vscode.go",
+ "IBM.output-colorizer",
+ "eamodio.gitlens",
+ "mhutchie.git-graph",
+ "davidanson.vscode-markdownlint",
+ "shardulm94.trailing-spaces",
+ "alefragnani.Bookmarks",
+ "Gruntfuggly.todo-tree",
+ "mohsen1.prettify-json",
+ "quicktype.quicktype",
+ "spikespaz.vscode-smoothtype",
+ "stkb.rewrap",
+ "vscode-icons-team.vscode-icons"
+ ],
+ "settings": {
+ // General settings
+ "files.eol": "\n",
+ // Docker
+ "remote.extensionKind": {
+ "ms-azuretools.vscode-docker": "workspace"
+ },
+ // Golang general settings
+ "go.useLanguageServer": true,
+ "go.autocompleteUnimportedPackages": true,
+ "go.gotoSymbol.includeImports": true,
+ "go.gotoSymbol.includeGoroot": true,
+ "gopls": {
+ "completeUnimported": true,
+ "deepCompletion": true,
+ "usePlaceholders": false
+ },
+ // Golang on save
+ "go.buildOnSave": "package",
+ "go.lintOnSave": "package",
+ "go.vetOnSave": "package",
+ "editor.formatOnSave": true,
+ "[go]": {
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": true
+ }
+ },
+ // Golang testing
+ "go.toolsEnvVars": {
+ "GOFLAGS": "-tags=integration"
+ },
+ "gopls.env": {
+ "GOFLAGS": "-tags=integration"
+ },
+ "go.testEnvVars": {},
+ "go.testFlags": ["-v"],
+ "go.testTimeout": "600s"
+ }
+}
\ No newline at end of file
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
new file mode 100644
index 00000000..98206c29
--- /dev/null
+++ b/.devcontainer/docker-compose.yml
@@ -0,0 +1,15 @@
+version: "3.7"
+
+services:
+ vscode:
+ image: qmcgaw/godevcontainer
+ volumes:
+ - ../:/workspace
+ - ~/.ssh:/home/vscode/.ssh:ro
+ - ~/.ssh:/root/.ssh:ro
+ - /var/run/docker.sock:/var/run/docker.sock
+ cap_add:
+ - SYS_PTRACE
+ security_opt:
+ - seccomp:unconfined
+ entrypoint: zsh -c "while sleep 1000; do :; done"
diff --git a/.dockerignore b/.dockerignore
index 1727b566..7596341d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,3 +7,4 @@ ci.sh
docker-compose.yml
LICENSE
README.md
+Dockerfile
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 4e29d54f..8575af0e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,78 +1,66 @@
-ARG ALPINE_VERSION=3.10
-
-FROM alpine:${ALPINE_VERSION}
-ARG VERSION
-ARG BUILD_DATE
-ARG VCS_REF
-LABEL \
- org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
- org.opencontainers.image.created=$BUILD_DATE \
- org.opencontainers.image.version=$VERSION \
- org.opencontainers.image.revision=$VCS_REF \
- org.opencontainers.image.url="https://github.com/qdm12/private-internet-access-docker" \
- org.opencontainers.image.documentation="https://github.com/qdm12/private-internet-access-docker" \
- org.opencontainers.image.source="https://github.com/qdm12/private-internet-access-docker" \
- org.opencontainers.image.title="PIA client" \
- org.opencontainers.image.description="VPN client to tunnel to private internet access servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux"
-ENV USER= \
- PASSWORD= \
- ENCRYPTION=strong \
- PROTOCOL=udp \
- REGION="CA Montreal" \
- NONROOT=yes \
- DOT=on \
- BLOCK_MALICIOUS=off \
- BLOCK_NSA=off \
- UNBLOCK= \
- EXTRA_SUBNETS= \
- PORT_FORWARDING=off \
- PORT_FORWARDING_STATUS_FILE="/forwarded_port" \
- TINYPROXY=off \
- TINYPROXY_LOG=Critical \
- TINYPROXY_PORT=8888 \
- TINYPROXY_USER= \
- TINYPROXY_PASSWORD= \
- SHADOWSOCKS=off \
- SHADOWSOCKS_LOG=on \
- SHADOWSOCKS_PORT=8388 \
- SHADOWSOCKS_PASSWORD= \
- TZ=
-ENTRYPOINT /entrypoint.sh
-EXPOSE 8888/tcp 8388/tcp 8388/udp
-HEALTHCHECK --interval=3m --timeout=3s --start-period=20s --retries=1 CMD /healthcheck.sh
-RUN apk add -q --progress --no-cache --update openvpn wget ca-certificates iptables unbound unzip tinyproxy jq tzdata && \
- echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
- apk add -q --progress --no-cache --update shadowsocks-libev && \
- wget -q https://www.privateinternetaccess.com/openvpn/openvpn.zip \
- https://www.privateinternetaccess.com/openvpn/openvpn-strong.zip \
- https://www.privateinternetaccess.com/openvpn/openvpn-tcp.zip \
- https://www.privateinternetaccess.com/openvpn/openvpn-strong-tcp.zip && \
- mkdir -p /openvpn/target && \
- unzip -q openvpn.zip -d /openvpn/udp-normal && \
- unzip -q openvpn-strong.zip -d /openvpn/udp-strong && \
- unzip -q openvpn-tcp.zip -d /openvpn/tcp-normal && \
- unzip -q openvpn-strong-tcp.zip -d /openvpn/tcp-strong && \
- apk del -q --progress --purge unzip && \
- rm -rf /*.zip /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-anchor /usr/sbin/unbound-checkconf /usr/sbin/unbound-control /usr/sbin/unbound-control-setup /usr/sbin/unbound-host /etc/tinyproxy/tinyproxy.conf && \
- adduser nonrootuser -D -H --uid 1000 && \
- wget -q https://raw.githubusercontent.com/qdm12/files/master/named.root.updated -O /etc/unbound/root.hints && \
- wget -q https://raw.githubusercontent.com/qdm12/files/master/root.key.updated -O /etc/unbound/root.key && \
- cd /tmp && \
- wget -q https://raw.githubusercontent.com/qdm12/files/master/malicious-hostnames.updated -O malicious-hostnames && \
- wget -q https://raw.githubusercontent.com/qdm12/files/master/surveillance-hostnames.updated -O nsa-hostnames && \
- wget -q https://raw.githubusercontent.com/qdm12/files/master/malicious-ips.updated -O malicious-ips && \
- while read hostname; do echo "local-zone: \""$hostname"\" static" >> blocks-malicious.conf; done < malicious-hostnames && \
- while read ip; do echo "private-address: $ip" >> blocks-malicious.conf; done < malicious-ips && \
- tar -cjf /etc/unbound/blocks-malicious.bz2 blocks-malicious.conf && \
- while read hostname; do echo "local-zone: \""$hostname"\" static" >> blocks-nsa.conf; done < nsa-hostnames && \
- tar -cjf /etc/unbound/blocks-nsa.bz2 blocks-nsa.conf && \
- rm -f /tmp/*
-COPY unbound.conf /etc/unbound/unbound.conf
-COPY tinyproxy.conf /etc/tinyproxy/tinyproxy.conf
-COPY shadowsocks.json /etc/shadowsocks.json
-COPY entrypoint.sh healthcheck.sh portforward.sh /
-RUN chown nonrootuser -R /etc/unbound /etc/tinyproxy && \
- chmod 700 /etc/unbound /etc/tinyproxy && \
- chmod 600 /etc/unbound/unbound.conf /etc/tinyproxy/tinyproxy.conf /etc/shadowsocks.json && \
- chmod 500 /entrypoint.sh /healthcheck.sh /portforward.sh && \
- chmod 400 /etc/unbound/root.hints /etc/unbound/root.key /etc/unbound/*.bz2
+ARG ALPINE_VERSION=3.11
+ARG GO_VERSION=1.13.7
+
+FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
+RUN apk --update add git
+WORKDIR /tmp/gobuild
+ENV CGO_ENABLED=0
+COPY go.mod go.sum ./
+RUN go mod download 2>&1
+COPY internal/ ./internal/
+COPY cmd/main.go .
+RUN go test ./...
+RUN go build -ldflags="-s -w" -o entrypoint main.go
+
+FROM alpine:${ALPINE_VERSION}
+ARG VERSION
+ARG BUILD_DATE
+ARG VCS_REF
+ENV VERSION=$VERSION \
+ BUILD_DATE=$BUILD_DATE \
+ VCS_REF=$VCS_REF
+LABEL \
+ org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
+ org.opencontainers.image.created=$BUILD_DATE \
+ org.opencontainers.image.version=$VERSION \
+ org.opencontainers.image.revision=$VCS_REF \
+ org.opencontainers.image.url="https://github.com/qdm12/private-internet-access-docker" \
+ org.opencontainers.image.documentation="https://github.com/qdm12/private-internet-access-docker" \
+ org.opencontainers.image.source="https://github.com/qdm12/private-internet-access-docker" \
+ org.opencontainers.image.title="PIA client" \
+ org.opencontainers.image.description="VPN client to tunnel to private internet access servers using OpenVPN, IPtables, DNS over TLS and Alpine Linux"
+ENV USER= \
+ PASSWORD= \
+ ENCRYPTION=strong \
+ PROTOCOL=udp \
+ REGION="CA Montreal" \
+ DOT=on \
+ DOT_PROVIDERS=cloudflare \
+ BLOCK_MALICIOUS=on \
+ BLOCK_SURVEILLANCE=off \
+ BLOCK_ADS=off \
+ UNBLOCK= \
+ EXTRA_SUBNETS= \
+ PORT_FORWARDING=off \
+ PORT_FORWARDING_STATUS_FILE="/forwarded_port" \
+ TINYPROXY=off \
+ TINYPROXY_LOG=Info \
+ TINYPROXY_PORT=8888 \
+ TINYPROXY_USER= \
+ TINYPROXY_PASSWORD= \
+ SHADOWSOCKS=off \
+ SHADOWSOCKS_LOG=on \
+ SHADOWSOCKS_PORT=8388 \
+ SHADOWSOCKS_PASSWORD= \
+ TZ=
+ENTRYPOINT /entrypoint
+EXPOSE 8888/tcp 8388/tcp 8388/udp
+# HEALTHCHECK --interval=3m --timeout=3s --start-period=20s --retries=1 CMD /entrypoint -healthcheck
+RUN apk add -q --progress --no-cache --update openvpn ca-certificates iptables unbound tinyproxy tzdata && \
+ echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
+ apk add -q --progress --no-cache --update shadowsocks-libev && \
+ rm -rf /*.zip /var/cache/apk/* /etc/unbound/* /usr/sbin/unbound-anchor /usr/sbin/unbound-checkconf /usr/sbin/unbound-control /usr/sbin/unbound-control-setup /usr/sbin/unbound-host /etc/tinyproxy/tinyproxy.conf && \
+ adduser nonrootuser -D -H --uid 1000 && \
+ chown nonrootuser -R /etc/unbound /etc/tinyproxy && \
+ chmod 700 /etc/unbound /etc/tinyproxy
+COPY --from=builder --chown=1000:1000 /tmp/gobuild/entrypoint /entrypoint
\ No newline at end of file
diff --git a/README.md b/README.md
index 55729afb..e8d357b8 100644
--- a/README.md
+++ b/README.md
@@ -1,295 +1,329 @@
-# Private Internet Access Client
-
-*Lightweight swiss-knife-like VPN client to tunnel to private internet access servers, using OpenVPN, iptables, DNS over TLS, ShadowSocks, Tinyproxy and more*
-
-**ANNOUCEMENT**: I just published [*Kape acquisition of Private Internet Access: not worry you must*](https://link.medium.com/e70B1j0wz2)
-
-
-
-
-
-[](https://travis-ci.org/qdm12/private-internet-access-docker)
-[](https://hub.docker.com/r/qmcgaw/private-internet-access)
-[](https://hub.docker.com/r/qmcgaw/private-internet-access)
-
-[](https://github.com/qdm12/private-internet-access-docker/issues)
-[](https://github.com/qdm12/private-internet-access-docker/issues)
-[](https://github.com/qdm12/private-internet-access-docker/issues)
-
-[](https://microbadger.com/images/qmcgaw/private-internet-access)
-[](https://microbadger.com/images/qmcgaw/private-internet-access)
-[](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
-
-Click to show base components
-
-- [Alpine 3.10](https://alpinelinux.org) for a tiny image
-- [OpenVPN 2.4.7](https://pkgs.alpinelinux.org/package/v3.10/main/x86_64/openvpn) to tunnel to PIA servers
-- [IPtables 1.8.3](https://pkgs.alpinelinux.org/package/v3.10/main/x86_64/iptables) enforces the container to communicate only through the VPN or with other containers in its virtual network (acts as a killswitch)
-- [Unbound 1.9.1](https://pkgs.alpinelinux.org/package/v3.10/main/x86_64/unbound) configured with Cloudflare's [1.1.1.1](https://1.1.1.1) DNS over TLS
-- [Files and blocking lists built periodically](https://github.com/qdm12/updated/tree/master/files) used with Unbound (see `BLOCK_MALICIOUS` and `BLOCK_NSA` environment variables)
-- [TinyProxy 1.10.0](https://pkgs.alpinelinux.org/package/v3.10/main/x86_64/tinyproxy)
-
-
-
-## Features
-
-- Configure everything with environment variables
-
- - [Destination region](https://www.privateinternetaccess.com/pages/network)
- - Internet protocol
- - Level of encryption
- - PIA Username and password
- - DNS over TLS
- - Malicious DNS blocking
- - Internal firewall
- - Web HTTP proxy
- - Run openvpn without root
-
-
-- Connect other containers to it, [see this](https://github.com/qdm12/private-internet-access-docker#connect-to-it)
-- Compatible with amd64, i686 (32 bit), ARM 64 bit, ARM 32 bit v6 and v7, ppc64le and even that s390x 🎆
-- Port forwarding
-- The *iptables* firewall allows traffic only with needed PIA servers (IP addresses, port, protocol) combinations
-- OpenVPN reconnects automatically on failure
-- Docker healthcheck pings the DNS 1.1.1.1 to verify the connection is up
-- Unbound DNS runs *without root*
-- OpenVPN runs *without root* by default. You can run it with root with the environment variable `NONROOT=no`
-- Connect your LAN devices
- - HTTP Web proxy *tinyproxy*
- - SOCKS5 proxy *shadowsocks* (better as it does UDP too)
-
-## Setup
-
-1. Requirements
-
- - A Private Internet Access **username** and **password** - [Sign up](https://www.privateinternetaccess.com/pages/buy-vpn/)
- - External firewall requirements, if you have one
- - Allow outbound TCP 853 to 1.1.1.1 to allow Unbound to resolve the PIA domain name at start. You can then block it once the container is started.
- - For UDP strong encryption, allow outbound UDP 1197
- - For UDP normal encryption, allow outbound UDP 1198
- - For TCP strong encryption, allow outbound TCP 501
- - For TCP normal encryption, allow outbound TCP 502
- - For the built-in web HTTP proxy, allow inbound TCP 8888
- - For the built-in SOCKS5 proxy, allow inbound TCP 8388 and UDP 8388
- - Docker API 1.25 to support `init`
- - If you use Docker Compose, docker-compose >= 1.22.0, to support `init: true`
-
-
-
-1. Ensure `/dev/net/tun` is setup on your host with either:
-
- ```sh
- insmod /lib/modules/tun.ko
- # or...
- modprobe tun
- ```
-
-1. Launch the container with:
-
- ```bash
- docker run -d --init --name=pia --cap-add=NET_ADMIN --device=/dev/net/tun \
- -e REGION="CA Montreal" -e USER=js89ds7 -e PASSWORD=8fd9s239G \
- qmcgaw/private-internet-access
- ```
-
- or use [docker-compose.yml](https://github.com/qdm12/private-internet-access-docker/blob/master/docker-compose.yml) with:
-
- ```bash
- docker-compose up -d
- ```
-
- Note that you can:
- - Change the many [environment variables](#environment-variables) available
- - Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable)
- - Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the SOCKS5 proxy (and put your LAN in `EXTRA_SUBNETS` environment variable)
- - Pass additional arguments to *openvpn* using Docker's command function (commands after the image name)
-1. You can update the image with `docker pull qmcgaw/private-internet-access:latest`. There are also docker tags available:
- - `qmcgaw/private-internet-access:v1` linked to the [v1 release](https://github.com/qdm12/private-internet-access-docker/releases/tag/v1.0)
-
-## Testing
-
-Check the PIA IP address matches your expectations
-
-```sh
-docker run --rm --network=container:pia alpine:3.10 wget -qO- https://ipinfo.io
-```
-
-## Environment variables
-
-| Environment variable | Default | Description |
-| --- | --- | --- |
-| `REGION` | `CA Montreal` | One of the [PIA regions](https://www.privateinternetaccess.com/pages/network/) |
-| `PROTOCOL` | `udp` | `tcp` or `udp` |
-| `ENCRYPTION` | `strong` | `normal` or `strong` |
-| `USER` | | Your PIA username |
-| `PASSWORD` | | Your PIA password |
-| `NONROOT` | `yes` | Run OpenVPN without root, `yes` or `no` |
-| `DOT` | `on` | `on` or `off`, to activate DNS over TLS to 1.1.1.1 |
-| `BLOCK_MALICIOUS` | `off` | `on` or `off`, blocks malicious hostnames and IPs |
-| `BLOCK_NSA` | `off` | `on` or `off`, blocks NSA hostnames |
-| `UNBLOCK` | | comma separated string (i.e. `web.com,web2.ca`) to unblock hostnames |
-| `EXTRA_SUBNETS` | | comma separated subnets allowed in the container firewall (i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28`) |
-| `PORT_FORWARDING` | `off` | Set to `on` to forward a port on PIA server |
-| `PORT_FORWARDING_STATUS_FILE` | `/forwarded_port` | File path to store the forwarded port number |
-| `TINYPROXY` | `on` | `on` or `off`, to enable the internal HTTP proxy tinyproxy |
-| `TINYPROXY_LOG` | `Critical` | `Info`, `Warning`, `Error` or `Critical` |
-| `TINYPROXY_PORT` | `8888` | `1024` to `65535` internal port for HTTP proxy |
-| `TINYPROXY_USER` | | Username to use to connect to the HTTP proxy |
-| `TINYPROXY_PASSWORD` | | Passsword to use to connect to the HTTP proxy |
-| `SHADOWSOCKS` | `on` | `on` or `off`, to enable the internal SOCKS5 proxy Shadowsocks |
-| `SHADOWSOCKS_LOG` | `on` | `on` or `off` to enable logging for Shadowsocks |
-| `SHADOWSOCKS_PORT` | `8388` | `1024` to `65535` internal port for SOCKS5 proxy |
-| `SHADOWSOCKS_PASSWORD` | | Passsword to use to connect to the SOCKS5 proxy |
-| `TZ` | | Specify a timezone to use e.g. `Europe/London` |
-
-## Connect to it
-
-There are various ways to achieve this, depending on your use case.
-
-- Connect containers in the same docker-compose.yml as PIA
-
- Add `network_mode: "service:pia"` to your *docker-compose.yml* (no need for `depends_on`)
-
-
-- Connect other containers to PIA
-
- Add `--network=container:pia` when launching the container, provided PIA is already running
-
-
-- Connect containers from another docker-compose.yml
-
- Add `network_mode: "container:pia"` to your *docker-compose.yml*, provided PIA is already running
-
-
-- Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)
-
- 1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en)
- 1. Ensure the PIA container is launched with:
- - port `8888` published `-p 8888:8888/tcp`
- - your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
- 1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`.
- 1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs, merged with the OpenVPN logs.
- `TINYPROXY_LOG` defaults to `Critical` to avoid logging everything, for privacy purposes.
-
-
-- Connect LAN devices through the built-in SOCKS5 proxy *Shadowsocks* (per app, system wide, etc.)
-
- 1. Setup a SOCKS5 proxy client, there is a list of [ShadowSocks clients for **all platforms**](https://shadowsocks.org/en/download/clients.html)
- - **note** some clients do not tunnel UDP so your DNS queries will be done locally and not through PIA and its built in DNS over TLS
- - Clients that support such UDP tunneling are, as far as I know:
- - iOS: Potatso Lite
- - OSX: ShadowsocksX
- - Android: Shadowsocks by Max Lv
- 1. Ensure the PIA container is launched with:
- - port `8388` published `-p 8388:8388/tcp -p 8388:8388/udp`
- - your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
- 1. With your SOCKS5 proxy client
- - Enter the Docker host (i.e. `192.168.1.10`) as the server IP
- - Enter port TCP (and UDP, if available) `8388` as the server port
- - Use the password you have set with `SHADOWSOCKS_PASSWORD`
- - Choose the encryption method/algorithm `chacha20-ietf-poly1305`
- 1. If you set `SHADOWSOCKS_LOG` to `on`, more information will be logged in the Docker logs, merged with the OpenVPN logs.
-
-
-- Access ports of containers connected to PIA
-
- In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to PIA,
- publish ports `8000` and `9000` for the PIA container and access them as you would with any other container
-
-
-- Access ports of containers connected to PIA, all in the same docker-compose.yml
-
- In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to PIA, publish port `8000` and `9000` for the PIA container.
- The docker-compose.yml file would look like:
-
- ```yml
- version: '3.7'
- services:
- pia:
- image: qmcgaw/private-internet-access
- container_name: pia
- init: true
- cap_add:
- - NET_ADMIN
- devices:
- - /dev/net/tun
- environment:
- - USER=js89ds7
- - PASSWORD=8fd9s239G
- ports:
- - 8000:8000/tcp
- - 9000:9000/tcp
- abc:
- image: abc
- container_name: abc
- network_mode: "service:pia"
- xyz:
- image: xyz
- container_name: xyz
- network_mode: "service:pia"
- ```
-
-
-
-## Port forwarding
-
-By setting `PORT_FORWARDING` environment variable to `on`, the forwarded port will be read and written to the file specified in `PORT_FORWARDING_STATUS_FILE` (by default, this is set to `/forwarded_port`). If the location for this file does not exist, it will be created automatically.
-
-You can mount this file as a volume to read it from other containers.
-
-Note that not all regions support port forwarding.
-
-## For the paranoids
-
-- You can review the code which essential consists in the [Dockerfile](https://github.com/qdm12/private-internet-access-docker/blob/master/Dockerfile) and [entrypoint.sh](https://github.com/qdm12/private-internet-access-docker/blob/master/entrypoint.sh)
-- Build the images yourself:
-
- ```bash
- docker build -t qmcgaw/private-internet-access https://github.com/qdm12/private-internet-access-docker.git
- ```
-
-- The download and unziping of PIA openvpn files is done at build for the ones not able to download the zip files
-- Checksums for PIA openvpn zip files are not used as these files change often (but HTTPS is used)
-- Use `-e ENCRYPTION=strong -e BLOCK_MALICIOUS=on`
-- You can test DNSSEC using [internet.nl/connection](https://www.internet.nl/connection/)
-- Check DNS leak tests with [https://www.dnsleaktest.com](https://www.dnsleaktest.com)
-- DNS Leaks tests might not work because of [this](https://github.com/qdm12/cloudflare-dns-server#verify-dns-connection) (*TLDR*: DNS server is a local caching intermediary)
-
-## Troubleshooting
-
-- Password problems `AUTH: Received control message: AUTH_FAILED`
- - Your password may contain a special character such as `$`.
- You need to escape it with `\` in your run command or docker-compose.yml.
- For example you would set `-e PASSWORD=mypa\$\$word`.
-- Fallback to a previous version
- 1. Clone the repository on your machine
-
- ```sh
- git clone https://github.com/qdm12/private-internet-access-docker.git pia
- cd pia
- ```
-
- 1. Look up which commit you want to go back to [here](https://github.com/qdm12/private-internet-access-docker/commits/master), i.e. `942cc7d4d10545b6f5f89c907b7dd1dbc39368e0`
- 1. Revert to this commit locally
-
- ```sh
- git reset --hard 942cc7d4d10545b6f5f89c907b7dd1dbc39368e0
- ```
-
- 1. Build the Docker image
-
- ```sh
- docker build -t qmcgaw/private-internet-access .
- ```
-
-## TODOs
-
-- Golang binary to setup the container at start, and:
- - Mix logs of unbound, tinyproxy, shadowsocks and openvpn together somehow
- - support other VPN providers
-- Maybe use `--inactive 3600 --ping 10 --ping-exit 60` as default behavior
-- Try without tun
-
-## License
-
-This repository is under an [MIT license](https://github.com/qdm12/private-internet-access-docker/master/license)
+# Private Internet Access Client
+
+*Lightweight swiss-knife-like VPN client to tunnel to private internet access servers, using OpenVPN, iptables, DNS over TLS, ShadowSocks, Tinyproxy and more*
+
+**ANNOUCEMENT**: *Total rewrite in Go: see the new features [below](#Features)* (in case something break use the image with tag `:old`)
+
+
+
+
+
+[](https://travis-ci.org/qdm12/private-internet-access-docker)
+[](https://hub.docker.com/r/qmcgaw/private-internet-access)
+[](https://hub.docker.com/r/qmcgaw/private-internet-access)
+
+[](https://github.com/qdm12/private-internet-access-docker/issues)
+[](https://github.com/qdm12/private-internet-access-docker/issues)
+[](https://github.com/qdm12/private-internet-access-docker/issues)
+
+[](https://microbadger.com/images/qmcgaw/private-internet-access)
+[](https://microbadger.com/images/qmcgaw/private-internet-access)
+[](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
+
+Click to show base components
+
+- [Alpine 3.11](https://alpinelinux.org) for a tiny image (37MB of packages, 6.7MB of Go binary and 5.6MB for Alpine)
+- [OpenVPN 2.4.8](https://pkgs.alpinelinux.org/package/v3.11/main/x86_64/openvpn) to tunnel to PIA servers
+- [IPtables 1.8.3](https://pkgs.alpinelinux.org/package/v3.11/main/x86_64/iptables) enforces the container to communicate only through the VPN or with other containers in its virtual network (acts as a killswitch)
+- [Unbound 1.9.6](https://pkgs.alpinelinux.org/package/v3.11/main/x86_64/unbound) configured with Cloudflare's [1.1.1.1](https://1.1.1.1) DNS over TLS (configurable with 5 different providers)
+- [Files and blocking lists built periodically](https://github.com/qdm12/updated/tree/master/files) used with Unbound (see `BLOCK_MALICIOUS`, `BLOCK_SURVEILLANCE` and `BLOCK_ADS` environment variables)
+- [TinyProxy 1.10.0](https://pkgs.alpinelinux.org/package/v3.11/main/x86_64/tinyproxy)
+- [Shadowsocks 3.3.4](https://pkgs.alpinelinux.org/package/edge/testing/x86/shadowsocks-libev)
+
+
+
+## Features
+
+- **New features**
+ - Choice to block ads, malicious and surveillance at the DNS level
+ - All program output streams are merged (openvpn, unbound, shadowsocks, tinyproxy, etc.)
+ - Choice of DNS over TLS provider(s)
+ - Possibility of split horizon DNS by selecting multiple DNS over TLS providers
+ - Download block lists and cryptographic files at start instead of at build time
+ - Can work as a Kubernetes sidecar container, thanks @rorph
+ - Pick a random region if no region is given, thanks @rorph
+- Configure everything with environment variables
+
+ - [Destination region](https://www.privateinternetaccess.com/pages/network)
+ - Internet protocol
+ - Level of encryption
+ - PIA Username and password
+ - DNS over TLS
+ - DNS blocking: ads, malicious, surveillance
+ - Internal firewall
+ - Socks5 proxy
+ - Web HTTP proxy
+
+
+- Connect
+ - [Other containers to it](https://github.com/qdm12/private-internet-access-docker#connect-to-it)
+ - [LAN devices to it](https://github.com/qdm12/private-internet-access-docker#connect-to-it)
+- Killswitch using *iptables* to allow traffic only with needed PIA servers and LAN devices
+- Port forwarding
+- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, ppc64le and even that s390x 🎆
+- Sub programs drop root privileges once launched: Openvpn, Unbound, Shadowsocks, Tinyproxy
+
+## Setup
+
+1. Requirements
+
+ - A Private Internet Access **username** and **password** - [Sign up](https://www.privateinternetaccess.com/pages/buy-vpn/)
+ - Docker API 1.25 to support `init`
+ - If you use Docker Compose, docker-compose >= 1.22.0, to support `init: true`
+ - External firewall requirements, if you have one
+
+ - At start only
+ - Allow outbound TCP 443 to github.com and privateinternetaccess.com
+ - If `DOT=on`, allow outbound TCP 853 to 1.1.1.1 to allow Unbound to resolve the PIA domain name.
+ - If `DOT=off`, allow outbound UDP 53 to your DNS provider to resolve the PIA domain name.
+ - For UDP strong encryption, allow outbound UDP 1197 to the corresponding VPN server IPs
+ - For UDP normal encryption, allow outbound UDP 1198 to the corresponding VPN server IPs
+ - For TCP strong encryption, allow outbound TCP 501 to the corresponding VPN server IPs
+ - For TCP normal encryption, allow outbound TCP 502 to the corresponding VPN server IPs
+ - If `SHADOWSOCKS=on`, allow inbound TCP 8388 and UDP 8388 from your LAN
+ - If `TINYPROXY=on`, allow inbound TCP 8888 from your LAN
+
+
+
+
+
+1. Ensure `/dev/net/tun` is setup on your host with either:
+
+ ```sh
+ insmod /lib/modules/tun.ko
+ # or...
+ modprobe tun
+ ```
+
+1. Launch the container with:
+
+ ```bash
+ docker run -d --init --name=pia --cap-add=NET_ADMIN --device=/dev/net/tun \
+ -e REGION="CA Montreal" -e USER=js89ds7 -e PASSWORD=8fd9s239G \
+ qmcgaw/private-internet-access
+ ```
+
+ or use [docker-compose.yml](https://github.com/qdm12/private-internet-access-docker/blob/master/docker-compose.yml) with:
+
+ ```bash
+ docker-compose up -d
+ ```
+
+ Note that you can:
+ - Change the many [environment variables](#environment-variables) available
+ - Use `-p 8888:8888/tcp` to access the HTTP web proxy (and put your LAN in `EXTRA_SUBNETS` environment variable)
+ - Use `-p 8388:8388/tcp -p 8388:8388/udp` to access the SOCKS5 proxy (and put your LAN in `EXTRA_SUBNETS` environment variable)
+ - Pass additional arguments to *openvpn* using Docker's command function (commands after the image name)
+1. You can update the image with `docker pull qmcgaw/private-internet-access:latest`. There are also docker tags available:
+ - `qmcgaw/private-internet-access:v1` linked to the [v1 release](https://github.com/qdm12/private-internet-access-docker/releases/tag/v1.0)
+
+## Testing
+
+Check the PIA IP address matches your expectations
+
+```sh
+docker run --rm --network=container:pia alpine:3.10 wget -qO- https://ipinfo.io
+```
+
+## Environment variables
+
+| Environment variable | Default | Description |
+| --- | --- | --- |
+| `REGION` | `CA Montreal` | One of the [PIA regions](https://www.privateinternetaccess.com/pages/network/) |
+| `PROTOCOL` | `udp` | `tcp` or `udp` |
+| `ENCRYPTION` | `strong` | `normal` or `strong` |
+| `USER` | | Your PIA username |
+| `PASSWORD` | | Your PIA password |
+| `DOT` | `on` | `on` or `off`, to activate DNS over TLS to 1.1.1.1 |
+| `DOT_PROVIDERS` | `cloudflare` | Comma delimited list of DNS over TLS providers from `cloudflare`, `google`, `quad9`, `quadrant`, `cleanbrowsing`, `securedns`, `libredns` |
+| `DOT_VERBOSITY` | `1` | Unbound verbosity level from `0` to `5` (full debug) |
+| `DOT_VERBOSITY_DETAILS` | `0` | Unbound details verbosity level from `0` to `4` |
+| `DOT_VALIDATION_LOGLEVEL` | `0` | Unbound validation log level from `0` to `2` |
+| `BLOCK_MALICIOUS` | `on` | `on` or `off`, blocks malicious hostnames and IPs |
+| `BLOCK_SURVEILLANCE` | `off` | `on` or `off`, blocks surveillance hostnames and IPs |
+| `BLOCK_ADS` | `off` | `on` or `off`, blocks ads hostnames and IPs |
+| `UNBLOCK` | | comma separated string (i.e. `web.com,web2.ca`) to unblock hostnames |
+| `EXTRA_SUBNETS` | | comma separated subnets allowed in the container firewall (i.e. `192.168.1.0/24,192.168.10.121,10.0.0.5/28`) |
+| `PORT_FORWARDING` | `off` | Set to `on` to forward a port on PIA server |
+| `PORT_FORWARDING_STATUS_FILE` | `/forwarded_port` | File path to store the forwarded port number |
+| `TINYPROXY` | `on` | `on` or `off`, to enable the internal HTTP proxy tinyproxy |
+| `TINYPROXY_LOG` | `Info` | `Info`, `Warning`, `Error` or `Critical` |
+| `TINYPROXY_PORT` | `8888` | `1024` to `65535` internal port for HTTP proxy |
+| `TINYPROXY_USER` | | Username to use to connect to the HTTP proxy |
+| `TINYPROXY_PASSWORD` | | Passsword to use to connect to the HTTP proxy |
+| `SHADOWSOCKS` | `on` | `on` or `off`, to enable the internal SOCKS5 proxy Shadowsocks |
+| `SHADOWSOCKS_LOG` | `on` | `on` or `off` to enable logging for Shadowsocks |
+| `SHADOWSOCKS_PORT` | `8388` | `1024` to `65535` internal port for SOCKS5 proxy |
+| `SHADOWSOCKS_PASSWORD` | | Passsword to use to connect to the SOCKS5 proxy |
+| `TZ` | | Specify a timezone to use i.e. `Europe/London` |
+
+## Connect to it
+
+There are various ways to achieve this, depending on your use case.
+
+- Connect containers in the same docker-compose.yml as PIA
+
+ Add `network_mode: "service:pia"` to your *docker-compose.yml* (no need for `depends_on`)
+
+
+- Connect other containers to PIA
+
+ Add `--network=container:pia` when launching the container, provided PIA is already running
+
+
+- Connect containers from another docker-compose.yml
+
+ Add `network_mode: "container:pia"` to your *docker-compose.yml*, provided PIA is already running
+
+
+- Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)
+
+ You might want to use Shadowsocks instead which tunnels UDP as well as TCP, whereas Tinyproxy only tunnels TCP.
+
+ 1. Setup a HTTP proxy client, such as [SwitchyOmega for Chrome](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?hl=en)
+ 1. Ensure the PIA container is launched with:
+ - port `8888` published `-p 8888:8888/tcp`
+ - your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
+ 1. With your HTTP proxy client, connect to the Docker host (i.e. `192.168.1.10`) on port `8888`. You need to enter your credentials if you set them with `TINYPROXY_USER` and `TINYPROXY_PASSWORD`.
+ 1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs
+
+
+- Connect LAN devices through the built-in SOCKS5 proxy *Shadowsocks* (per app, system wide, etc.)
+
+ 1. Setup a SOCKS5 proxy client, there is a list of [ShadowSocks clients for **all platforms**](https://shadowsocks.org/en/download/clients.html)
+ - **note** some clients do not tunnel UDP so your DNS queries will be done locally and not through PIA and its built in DNS over TLS
+ - Clients that support such UDP tunneling are, as far as I know:
+ - iOS: Potatso Lite
+ - OSX: ShadowsocksX
+ - Android: Shadowsocks by Max Lv
+ 1. Ensure the PIA container is launched with:
+ - port `8388` published `-p 8388:8388/tcp -p 8388:8388/udp`
+ - your LAN subnet, i.e. `192.168.1.0/24`, set as `-e EXTRA_SUBNETS=192.168.1.0/24`
+ 1. With your SOCKS5 proxy client
+ - Enter the Docker host (i.e. `192.168.1.10`) as the server IP
+ - Enter port TCP (and UDP, if available) `8388` as the server port
+ - Use the password you have set with `SHADOWSOCKS_PASSWORD`
+ - Choose the encryption method/algorithm `chacha20-ietf-poly1305`
+ 1. If you set `SHADOWSOCKS_LOG` to `on`, more information will be logged in the Docker logs
+
+
+- Access ports of containers connected to PIA
+
+ In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to PIA,
+ publish ports `8000` and `9000` for the PIA container and access them as you would with any other container
+
+
+- Access ports of containers connected to PIA, all in the same docker-compose.yml
+
+ In example, to access port `8000` of container `xyz` and `9000` of container `abc` connected to PIA, publish port `8000` and `9000` for the PIA container.
+ The docker-compose.yml file would look like:
+
+ ```yml
+ version: '3.7'
+ services:
+ pia:
+ image: qmcgaw/private-internet-access
+ container_name: pia
+ init: true
+ cap_add:
+ - NET_ADMIN
+ devices:
+ - /dev/net/tun
+ environment:
+ - USER=js89ds7
+ - PASSWORD=8fd9s239G
+ ports:
+ - 8000:8000/tcp
+ - 9000:9000/tcp
+ abc:
+ image: abc
+ container_name: abc
+ network_mode: "service:pia"
+ xyz:
+ image: xyz
+ container_name: xyz
+ network_mode: "service:pia"
+ ```
+
+
+
+## Port forwarding
+
+By setting `PORT_FORWARDING` environment variable to `on`, the forwarded port will be read and written to the file specified in `PORT_FORWARDING_STATUS_FILE` (by default, this is set to `/forwarded_port`). If the location for this file does not exist, it will be created automatically.
+
+You can mount this file as a volume to read it from other containers.
+
+Note that not all regions support port forwarding.
+
+## For the paranoids
+
+- You can review the code which consists in:
+ - [Dockerfile](https://github.com/qdm12/private-internet-access-docker/blob/master/Dockerfile)
+ - [main.go](https://github.com/qdm12/private-internet-access-docker/blob/master/cmd/main.go), the main code entrypoint
+ - [internal package](https://github.com/qdm12/private-internet-access-docker/blob/master/internal)
+ - [github.com/qdm12/golibs](https://github.com/qdm12/golibs) dependency
+ - [github.com/qdm12/files](https://github.com/qdm12/files) for files downloaded at start (DNS root hints, block lists, etc.)
+- Build the image yourself:
+
+ ```bash
+ docker build -t qmcgaw/private-internet-access https://github.com/qdm12/private-internet-access-docker.git
+ ```
+
+- The download and parsing of all needed files is done at start (openvpn config files, Unbound files, block lists, etc.)
+- Use `-e ENCRYPTION=strong -e BLOCK_MALICIOUS=on`
+- You can test DNSSEC using [internet.nl/connection](https://www.internet.nl/connection/)
+- Check DNS leak tests with [https://www.dnsleaktest.com](https://www.dnsleaktest.com)
+- DNS Leaks tests might not work because of [this](https://github.com/qdm12/cloudflare-dns-server#verify-dns-connection) (*TLDR*: DNS server is a local caching intermediary)
+
+## Troubleshooting
+
+- Fallback to a previous version
+ 1. Clone the repository on your machine
+
+ ```sh
+ git clone https://github.com/qdm12/private-internet-access-docker.git pia
+ cd pia
+ ```
+
+ 1. Look up which commit you want to go back to [here](https://github.com/qdm12/private-internet-access-docker/commits/master), i.e. `942cc7d4d10545b6f5f89c907b7dd1dbc39368e0`
+ 1. Revert to this commit locally
+
+ ```sh
+ git reset --hard 942cc7d4d10545b6f5f89c907b7dd1dbc39368e0
+ ```
+
+ 1. Build the Docker image
+
+ ```sh
+ docker build -t qmcgaw/private-internet-access .
+ ```
+
+## Development
+
+### Using VSCode and Docker
+
+1. Install [Docker](https://docs.docker.com/install)
+ - On Windows, share a drive with Docker Desktop and have the project on that partition
+1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
+1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
+1. Your dev environment is ready to go!... and it's running in a container :+1:
+
+## TODOs
+
+- Healthcheck checking for IP address, DNS leaks etc.
+- Periodic update of malicious block lists with Unbound restart
+- Support other VPN providers
+ - Mullvad
+ - Windscribe
+- Support for other VPN protocols
+ - Wireguard (wireguard-go)
+- Show new versions/commits at start
+- Colors & emojis
+ - Setup
+ - Logging streams
+- More unit tests
+- Switch to iptables-go instead of using the shell iptables
+
+## License
+
+This repository is under an [MIT license](https://github.com/qdm12/private-internet-access-docker/master/license)
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 00000000..82bd2a75
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,150 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/qdm12/golibs/command"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/network"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/dns"
+ "github.com/qdm12/private-internet-access-docker/internal/env"
+ "github.com/qdm12/private-internet-access-docker/internal/firewall"
+ "github.com/qdm12/private-internet-access-docker/internal/openvpn"
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+ "github.com/qdm12/private-internet-access-docker/internal/pia"
+ "github.com/qdm12/private-internet-access-docker/internal/settings"
+ "github.com/qdm12/private-internet-access-docker/internal/shadowsocks"
+ "github.com/qdm12/private-internet-access-docker/internal/splash"
+ "github.com/qdm12/private-internet-access-docker/internal/tinyproxy"
+)
+
+const (
+ uid, gid = 1000, 1000
+)
+
+func main() {
+ logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
+ if err != nil {
+ panic(err)
+ }
+ paramsReader := params.NewParamsReader(logger)
+ fmt.Println(splash.Splash(paramsReader))
+ e := env.New(logger)
+ client := network.NewClient(3 * time.Second)
+ // Create configurators
+ fileManager := files.NewFileManager()
+ ovpnConf := openvpn.NewConfigurator(logger, fileManager)
+ dnsConf := dns.NewConfigurator(logger, client, fileManager)
+ firewallConf := firewall.NewConfigurator(logger, fileManager)
+ piaConf := pia.NewConfigurator(client, fileManager, firewallConf, logger)
+ tinyProxyConf := tinyproxy.NewConfigurator(fileManager, logger)
+ shadowsocksConf := shadowsocks.NewConfigurator(fileManager, logger)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ streamMerger := command.NewStreamMerger(ctx)
+
+ e.PrintVersion("OpenVPN", ovpnConf.Version)
+ e.PrintVersion("Unbound", dnsConf.Version)
+ e.PrintVersion("IPtables", firewallConf.Version)
+ e.PrintVersion("TinyProxy", tinyProxyConf.Version)
+ e.PrintVersion("ShadowSocks", shadowsocksConf.Version)
+
+ allSettings, err := settings.GetAllSettings(paramsReader)
+ e.FatalOnError(err)
+ logger.Info(allSettings.String())
+
+ err = ovpnConf.CheckTUN()
+ e.FatalOnError(err)
+
+ err = ovpnConf.WriteAuthFile(allSettings.PIA.User, allSettings.PIA.Password, uid, gid)
+ e.FatalOnError(err)
+
+ // Temporarily reset chain policies allowing Kubernetes sidecar to
+ // successfully restart the container. Without this, the existing rules will
+ // pre-exist, preventing the nslookup of the PIA region address. These will
+ // simply be redundant at Docker runtime as they will already be set this way
+ // Thanks to @npawelek https://github.com/npawelek
+ err = firewallConf.AcceptAll()
+ e.FatalOnError(err)
+
+ if allSettings.DNS.Enabled {
+ err = dnsConf.DownloadRootHints(uid, gid)
+ e.FatalOnError(err)
+ err = dnsConf.DownloadRootKey(uid, gid)
+ e.FatalOnError(err)
+ err = dnsConf.MakeUnboundConf(allSettings.DNS, uid, gid)
+ e.FatalOnError(err)
+ stream, err := dnsConf.Start(allSettings.DNS.VerbosityDetailsLevel)
+ e.FatalOnError(err)
+ go streamMerger.Merge("unbound", stream)
+ err = dnsConf.SetLocalNameserver()
+ e.FatalOnError(err)
+ }
+
+ lines, err := piaConf.DownloadOvpnConfig(allSettings.PIA.Encryption, allSettings.OpenVPN.NetworkProtocol, allSettings.PIA.Region)
+ e.FatalOnError(err)
+ VPNIPs, port, VPNDevice, err := piaConf.ParseConfig(lines)
+ e.FatalOnError(err)
+ lines = piaConf.ModifyLines(lines, VPNIPs, port)
+ fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines)
+ e.FatalOnError(err)
+
+ defaultInterface, defaultGateway, defaultSubnet, err := firewallConf.GetDefaultRoute()
+ e.FatalOnError(err)
+ err = firewallConf.AddRoutesVia(allSettings.Firewall.AllowedSubnets, defaultGateway, defaultInterface)
+ e.FatalOnError(err)
+ err = firewallConf.Clear()
+ e.FatalOnError(err)
+ err = firewallConf.BlockAll()
+ e.FatalOnError(err)
+ err = firewallConf.CreateGeneralRules()
+ e.FatalOnError(err)
+ err = firewallConf.CreateVPNRules(VPNDevice, VPNIPs, defaultInterface, port, allSettings.OpenVPN.NetworkProtocol)
+ e.FatalOnError(err)
+ err = firewallConf.CreateLocalSubnetsRules(defaultSubnet, allSettings.Firewall.AllowedSubnets, defaultInterface)
+ e.FatalOnError(err)
+
+ if allSettings.TinyProxy.Enabled {
+ err = tinyProxyConf.MakeConf(allSettings.TinyProxy.LogLevel, allSettings.ShadowSocks.Port, allSettings.TinyProxy.User, allSettings.TinyProxy.Password, uid, gid)
+ e.FatalOnError(err)
+ stream, err := tinyProxyConf.Start()
+ e.FatalOnError(err)
+ go streamMerger.Merge("tinyproxy", stream)
+ }
+
+ if allSettings.ShadowSocks.Enabled {
+ err = shadowsocksConf.MakeConf(allSettings.ShadowSocks.Port, allSettings.TinyProxy.Password, uid, gid)
+ e.FatalOnError(err)
+ stream, err := shadowsocksConf.Start("0.0.0.0", allSettings.ShadowSocks.Port, allSettings.ShadowSocks.Password, allSettings.ShadowSocks.Log)
+ e.FatalOnError(err)
+ go streamMerger.Merge("shadowsocks", stream)
+ }
+
+ if allSettings.PIA.PortForwarding.Enabled {
+ time.AfterFunc(10*time.Second, func() {
+ port, err := piaConf.GetPortForward()
+ if err != nil {
+ logger.Error("port forwarding:", err)
+ }
+ if err := piaConf.WritePortForward(allSettings.PIA.PortForwarding.Filepath, port); err != nil {
+ logger.Error("port forwarding:", err)
+ }
+ if err := piaConf.AllowPortForwardFirewall(VPNDevice, port); err != nil {
+ logger.Error("port forwarding:", err)
+ }
+ })
+ }
+
+ stream, err := ovpnConf.Start()
+ e.FatalOnError(err)
+ go streamMerger.Merge("openvpn", stream)
+
+ // Blocking line merging reader for all programs: openvpn, tinyproxy, unbound and shadowsocks
+ logger.Info("Launching standard output merger")
+ err = streamMerger.CollectLines(func(line string) { logger.Info(line) })
+ e.FatalOnError(err)
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index ef176fae..f0fa9174 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,18 +21,19 @@ services:
- ENCRYPTION=strong
- PROTOCOL=udp
- REGION=CA Montreal
- - NONROOT=no
- DOT=on
+ - DOT_PROVIDERS=cloudflare
- BLOCK_MALICIOUS=on
- - BLOCK_NSA=off
+ - BLOCK_SURVEILLANCE=off
+ - BLOCK_ADS=off
- UNBLOCK=
- - FIREWALL=on
- EXTRA_SUBNETS=
- - TINYPROXY=on
- - TINYPROXY_LOG=Critical
+ - TINYPROXY=off
+ - TINYPROXY_LOG=Info
- TINYPROXY_USER=
- TINYPROXY_PASSWORD=
- - SHADOWSOCKS=on
+ - SHADOWSOCKS=off
- SHADOWSOCKS_LOG=on
+ - SHADOWSOCKS_PORT=8388
- SHADOWSOCKS_PASSWORD=
restart: always
diff --git a/entrypoint.sh b/entrypoint.sh
deleted file mode 100644
index 7d6e7509..00000000
--- a/entrypoint.sh
+++ /dev/null
@@ -1,492 +0,0 @@
-#!/bin/sh
-
-exitOnError(){
- # $1 must be set to $?
- status=$1
- message=$2
- [ "$message" != "" ] || message="Undefined error"
- if [ $status != 0 ]; then
- printf "[ERROR] $message, with status $status\n"
- exit $status
- fi
-}
-
-exitIfUnset(){
- # $1 is the name of the variable to check - not the variable itself
- var="$(eval echo "\$$1")"
- if [ -z "$var" ]; then
- printf "[ERROR] Environment variable $1 is not set\n"
- exit 1
- fi
-}
-
-exitIfNotIn(){
- # $1 is the name of the variable to check - not the variable itself
- # $2 is a string of comma separated possible values
- var="$(eval echo "\$$1")"
- for value in ${2//,/ }
- do
- if [ "$var" = "$value" ]; then
- return 0
- fi
- done
- printf "[ERROR] Environment variable $1 cannot be '$var' and must be one of the following: "
- for value in ${2//,/ }
- do
- printf "$value "
- done
- printf "\n"
- exit 1
-}
-
-printf " =========================================\n"
-printf " =========================================\n"
-printf " ============= PIA CONTAINER =============\n"
-printf " =========================================\n"
-printf " =========================================\n"
-printf " == by github.com/qdm12 - Quentin McGaw ==\n\n"
-
-printf "OpenVPN version: $(openvpn --version | head -n 1 | grep -oE "OpenVPN [0-9\.]* " | cut -d" " -f2)\n"
-printf "Unbound version: $(unbound -h | grep "Version" | cut -d" " -f2)\n"
-printf "Iptables version: $(iptables --version | cut -d" " -f2)\n"
-printf "TinyProxy version: $(tinyproxy -v | cut -d" " -f2)\n"
-printf "ShadowSocks version: $(ss-server --help | head -n 2 | tail -n 1 | cut -d" " -f 2)\n"
-
-############################################
-# BACKWARD COMPATIBILITY PARAMETERS
-############################################
-[ "$PORT_FORWARDING" == "false" ] && PORT_FORWARDING=on
-[ "$PORT_FORWARDING" == "true" ] && PORT_FORWARDING=off
-if [ -z $TINYPROXY ] && [ ! -z $PROXY ]; then
- TINYPROXY=$PROXY
-fi
-if [ -z $TINYPROXY_LOG ] && [ ! -z $PROXY_LOG_LEVEL ]; then
- TINYPROXY_LOG=$PROXY_LOG_LEVEL
-fi
-if [ -z $TINYPROXY_PORT ] && [ ! -z $PROXY_PORT ]; then
- TINYPROXY_PORT=$PROXY_PORT
-fi
-if [ -z $TINYPROXY_USER ] && [ ! -z $PROXY_USER ]; then
- TINYPROXY_USER=$PROXY_USER
-fi
-if [ -z $TINYPROXY_PASSWORD ] && [ ! -z $PROXY_PASSWORD ]; then
- TINYPROXY_PASSWORD=$PROXY_PASSWORD
-fi
-
-############################################
-# CHECK PARAMETERS
-############################################
-exitIfUnset USER
-exitIfUnset PASSWORD
-exitIfNotIn ENCRYPTION "normal,strong"
-exitIfNotIn PROTOCOL "tcp,udp"
-exitIfNotIn NONROOT "yes,no"
-cat "/openvpn/$PROTOCOL-$ENCRYPTION/$REGION.ovpn" &> /dev/null
-exitOnError $? "/openvpn/$PROTOCOL-$ENCRYPTION/$REGION.ovpn is not accessible"
-for EXTRA_SUBNET in ${EXTRA_SUBNETS//,/ }; do
- if [ $(echo "$EXTRA_SUBNET" | grep -Eo '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/([0-2]?[0-9])|([3]?[0-1]))?$') = "" ]; then
- printf "Extra subnet $EXTRA_SUBNET is not a valid IPv4 subnet of the form 255.255.255.255/31 or 255.255.255.255\n"
- exit 1
- fi
-done
-exitIfNotIn DOT "on,off"
-exitIfNotIn BLOCK_MALICIOUS "on,off"
-exitIfNotIn BLOCK_NSA "on,off"
-if [ "$DOT" == "off" ]; then
- if [ "$BLOCK_MALICIOUS" == "on" ]; then
- printf "DOT is off so BLOCK_MALICIOUS cannot be on\n"
- exit 1
- elif [ "$BLOCK_NSA" == "on" ]; then
- printf "DOT is off so BLOCK_NSA cannot be on\n"
- exit 1
- fi
-fi
-exitIfNotIn PORT_FORWARDING "on,off"
-if [ "$PORT_FORWARDING" == "on" ] && [ -z "$PORT_FORWARDING_STATUS_FILE" ]; then
- printf "PORT_FORWARDING is on but PORT_FORWARDING_STATUS_FILE is not set\n"
- exit 1
-fi
-exitIfNotIn TINYPROXY "on,off"
-if [ "$TINYPROXY" == "on" ]; then
- exitIfNotIn TINYPROXY_LOG "Info,Warning,Error,Critical"
- if [ -z $TINYPROXY_PORT ]; then
- TINYPROXY_PORT=8888
- fi
- if [ `echo $TINYPROXY_PORT | grep -E "^[0-9]+$"` != $TINYPROXY_PORT ]; then
- printf "TINYPROXY_PORT is not a valid number\n"
- exit 1
- elif [ $TINYPROXY_PORT -lt 1024 ]; then
- printf "TINYPROXY_PORT cannot be a privileged port under port 1024\n"
- exit 1
- elif [ $TINYPROXY_PORT -gt 65535 ]; then
- printf "TINYPROXY_PORT cannot be a port higher than the maximum port 65535\n"
- exit 1
- fi
- if [ ! -z "$TINYPROXY_USER" ] && [ -z "$TINYPROXY_PASSWORD" ]; then
- printf "TINYPROXY_USER is set but TINYPROXY_PASSWORD is not set\n"
- exit 1
- elif [ -z "$TINYPROXY_USER" ] && [ ! -z "$TINYPROXY_PASSWORD" ]; then
- printf "TINYPROXY_USER is not set but TINYPROXY_PASSWORD is set\n"
- exit 1
- fi
-fi
-exitIfNotIn SHADOWSOCKS "on,off"
-if [ "$SHADOWSOCKS" == "on" ]; then
- exitIfNotIn SHADOWSOCKS "on,off"
- if [ -z $SHADOWSOCKS_PORT ]; then
- SHADOWSOCKS_PORT=8388
- fi
- if [ `echo $SHADOWSOCKS_PORT | grep -E "^[0-9]+$"` != $SHADOWSOCKS_PORT ]; then
- printf "SHADOWSOCKS_PORT is not a valid number\n"
- exit 1
- elif [ $SHADOWSOCKS_PORT -lt 1024 ]; then
- printf "SHADOWSOCKS_PORT cannot be a privileged port under port 1024\n"
- exit 1
- elif [ $SHADOWSOCKS_PORT -gt 65535 ]; then
- printf "SHADOWSOCKS_PORT cannot be a port higher than the maximum port 65535\n"
- exit 1
- fi
- if [ -z $SHADOWSOCKS_PASSWORD ]; then
- printf "SHADOWSOCKS_PASSWORD is not set\n"
- exit 1
- fi
-fi
-
-############################################
-# SHOW PARAMETERS
-############################################
-printf "\n"
-printf "OpenVPN parameters:\n"
-printf " * Region: $REGION\n"
-printf " * Encryption: $ENCRYPTION\n"
-printf " * Protocol: $PROTOCOL\n"
-printf " * Running without root: $NONROOT\n"
-printf "DNS over TLS:\n"
-printf " * Activated: $DOT\n"
-if [ "$DOT" = "on" ]; then
- printf " * Malicious hostnames DNS blocking: $BLOCK_MALICIOUS\n"
- printf " * NSA related DNS blocking: $BLOCK_NSA\n"
- printf " * Unblocked hostnames: $UNBLOCK\n"
-fi
-printf "Local network parameters:\n"
-printf " * Extra subnets: $EXTRA_SUBNETS\n"
-printf " * Tinyproxy HTTP proxy: $TINYPROXY\n"
-if [ "$TINYPROXY" == "on" ]; then
- printf " * Tinyproxy port: $TINYPROXY_PORT\n"
- tinyproxy_auth=yes
- if [ -z $TINYPROXY_USER ]; then
- tinyproxy_auth=no
- fi
- printf " * Tinyproxy has authentication: $tinyproxy_auth\n"
- unset -v tinyproxy_auth
-fi
-printf " * ShadowSocks SOCKS5 proxy: $SHADOWSOCKS\n"
-printf "PIA parameters:\n"
-printf " * Remote port forwarding: $PORT_FORWARDING\n"
-[ "$PORT_FORWARDING" == "on" ] && printf " * Remote port forwarding status file: $PORT_FORWARDING_STATUS_FILE\n"
-printf "\n"
-
-#####################################################
-# Writes to protected file and remove USER, PASSWORD
-#####################################################
-if [ -f /auth.conf ]; then
- printf "[INFO] /auth.conf already exists\n"
-else
- printf "[INFO] Writing USER and PASSWORD to protected file /auth.conf..."
- echo "$USER" > /auth.conf
- exitOnError $?
- echo "$PASSWORD" >> /auth.conf
- exitOnError $?
- chown nonrootuser /auth.conf
- exitOnError $?
- chmod 400 /auth.conf
- exitOnError $?
- printf "DONE\n"
- printf "[INFO] Clearing environment variables USER and PASSWORD..."
- unset -v USER
- unset -v PASSWORD
- printf "DONE\n"
-fi
-
-############################################
-# CHECK FOR TUN DEVICE
-############################################
-if [ "$(cat /dev/net/tun 2>&1 /dev/null)" != "cat: read error: File descriptor in bad state" ]; then
- printf "[WARNING] TUN device is not available, creating it..."
- mkdir -p /dev/net
- mknod /dev/net/tun c 10 200
- exitOnError $?
- chmod 0666 /dev/net/tun
- printf "DONE\n"
-fi
-
-############################################
-# BLOCKING MALICIOUS HOSTNAMES AND IPs WITH UNBOUND
-############################################
-if [ "$DOT" == "on" ]; then
- rm -f /etc/unbound/blocks-malicious.conf
- if [ "$BLOCK_MALICIOUS" = "on" ]; then
- tar -xjf /etc/unbound/blocks-malicious.bz2 -C /etc/unbound/
- printf "[INFO] $(cat /etc/unbound/blocks-malicious.conf | grep "local-zone" | wc -l ) malicious hostnames and $(cat /etc/unbound/blocks-malicious.conf | grep "private-address" | wc -l) malicious IP addresses blacklisted\n"
- else
- echo "" > /etc/unbound/blocks-malicious.conf
- fi
- if [ "$BLOCK_NSA" = "on" ]; then
- tar -xjf /etc/unbound/blocks-nsa.bz2 -C /etc/unbound/
- printf "[INFO] $(cat /etc/unbound/blocks-nsa.conf | grep "local-zone" | wc -l ) NSA hostnames blacklisted\n"
- cat /etc/unbound/blocks-nsa.conf >> /etc/unbound/blocks-malicious.conf
- rm /etc/unbound/blocks-nsa.conf
- sort -u -o /etc/unbound/blocks-malicious.conf /etc/unbound/blocks-malicious.conf
- fi
- for hostname in ${UNBLOCK//,/ }
- do
- printf "[INFO] Unblocking hostname $hostname\n"
- sed -i "/$hostname/d" /etc/unbound/blocks-malicious.conf
- done
-fi
-
-############################################
-# SETTING DNS OVER TLS TO 1.1.1.1 / 1.0.0.1
-############################################
-if [ "$DOT" == "on" ]; then
- printf "[INFO] Launching Unbound to connect to Cloudflare DNS 1.1.1.1 over TLS..."
- unbound
- exitOnError $?
- printf "DONE\n"
- printf "[INFO] Changing DNS to localhost..."
- printf "`sed '/^nameserver /d' /etc/resolv.conf`\nnameserver 127.0.0.1\n" > /etc/resolv.conf
- exitOnError $?
- printf "DONE\n"
-fi
-
-############################################
-# Reading chosen OpenVPN configuration
-############################################
-printf "[INFO] Reading OpenVPN configuration...\n"
-CONNECTIONSTRING=$(grep -i "/openvpn/$PROTOCOL-$ENCRYPTION/$REGION.ovpn" -e 'privateinternetaccess.com')
-exitOnError $?
-PORT=$(echo $CONNECTIONSTRING | cut -d' ' -f3)
-if [ "$PORT" = "" ]; then
- printf "[ERROR] Port not found in /openvpn/$PROTOCOL-$ENCRYPTION/$REGION.ovpn\n"
- exit 1
-fi
-PIADOMAIN=$(echo $CONNECTIONSTRING | cut -d' ' -f2)
-if [ "$PIADOMAIN" = "" ]; then
- printf "[ERROR] Domain not found in /openvpn/$PROTOCOL-$ENCRYPTION/$REGION.ovpn\n"
- exit 1
-fi
-printf " * Port: $PORT\n"
-printf " * Domain: $PIADOMAIN\n"
-printf "[INFO] Detecting IP addresses corresponding to $PIADOMAIN...\n"
-VPNIPS=$(nslookup $PIADOMAIN localhost | tail -n +3 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}')
-exitOnError $?
-for ip in $VPNIPS; do
- printf " $ip\n";
-done
-
-############################################
-# Writing target OpenVPN files
-############################################
-TARGET_PATH="/openvpn/target"
-printf "[INFO] Creating target OpenVPN files in $TARGET_PATH..."
-rm -rf $TARGET_PATH/*
-cd "/openvpn/$PROTOCOL-$ENCRYPTION"
-cp -f *.crt "$TARGET_PATH"
-exitOnError $? "Cannot copy crt file to $TARGET_PATH"
-cp -f *.pem "$TARGET_PATH"
-exitOnError $? "Cannot copy pem file to $TARGET_PATH"
-cp -f "$REGION.ovpn" "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot copy $REGION.ovpn file to $TARGET_PATH"
-sed -i "/$CONNECTIONSTRING/d" "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot delete '$CONNECTIONSTRING' from $TARGET_PATH/config.ovpn"
-sed -i '/resolv-retry/d' "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot delete 'resolv-retry' from $TARGET_PATH/config.ovpn"
-for ip in $VPNIPS; do
- echo "remote $ip $PORT" >> "$TARGET_PATH/config.ovpn"
- exitOnError $? "Cannot add 'remote $ip $PORT' to $TARGET_PATH/config.ovpn"
-done
-# Uses the username/password from this file to get the token from PIA
-echo "auth-user-pass /auth.conf" >> "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot add 'auth-user-pass /auth.conf' to $TARGET_PATH/config.ovpn"
-# Reconnects automatically on failure
-echo "auth-retry nointeract" >> "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot add 'auth-retry nointeract' to $TARGET_PATH/config.ovpn"
-# Prevents auth_failed infinite loops - make it interact? Remove persist-tun? nobind?
-echo "pull-filter ignore \"auth-token\"" >> "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot add 'pull-filter ignore \"auth-token\"' to $TARGET_PATH/config.ovpn"
-# Runs openvpn without root, as nonrootuser if specified
-if [ "$NONROOT" = "yes" ]; then
- echo "user nonrootuser" >> "$TARGET_PATH/config.ovpn"
- exitOnError $? "Cannot add 'user nonrootuser' to $TARGET_PATH/config.ovpn"
-fi
-echo "mute-replay-warnings" >> "$TARGET_PATH/config.ovpn"
-exitOnError $? "Cannot add 'mute-replay-warnings' to $TARGET_PATH/config.ovpn"
-# Note: TUN device re-opening will restart the container due to permissions
-printf "DONE\n"
-
-############################################
-# NETWORKING
-############################################
-printf "[INFO] Finding network properties...\n"
-printf " * Detecting default gateway..."
-DEFAULT_GATEWAY=$(ip r | grep 'default via' | cut -d" " -f 3)
-exitOnError $?
-printf "$DEFAULT_GATEWAY\n"
-printf " * Detecting local interface..."
-INTERFACE=$(ip r | grep 'default via' | cut -d" " -f 5)
-exitOnError $?
-printf "$INTERFACE\n"
-printf " * Detecting local subnet..."
-SUBNET=$(ip r | grep -v 'default via' | grep $INTERFACE | tail -n 1 | cut -d" " -f 1)
-exitOnError $?
-printf "$SUBNET\n"
-for EXTRASUBNET in ${EXTRA_SUBNETS//,/ }
-do
- printf " * Adding $EXTRASUBNET as route via $INTERFACE..."
- ip route add $EXTRASUBNET via $DEFAULT_GATEWAY dev $INTERFACE
- exitOnError $?
- printf "DONE\n"
-done
-printf " * Detecting target VPN interface..."
-VPN_DEVICE=$(cat $TARGET_PATH/config.ovpn | grep 'dev ' | cut -d" " -f 2)0
-exitOnError $?
-printf "$VPN_DEVICE\n"
-
-
-############################################
-# FIREWALL
-############################################
-printf "[INFO] Setting firewall\n"
-printf " * Blocking everyting\n"
-printf " * Deleting all iptables rules..."
-iptables --flush
-exitOnError $?
-iptables --delete-chain
-exitOnError $?
-iptables -t nat --flush
-exitOnError $?
-iptables -t nat --delete-chain
-exitOnError $?
-printf "DONE\n"
-printf " * Block input traffic..."
-iptables -P INPUT DROP
-exitOnError $?
-printf "DONE\n"
-printf " * Block output traffic..."
-iptables -F OUTPUT
-exitOnError $?
-iptables -P OUTPUT DROP
-exitOnError $?
-printf "DONE\n"
-printf " * Block forward traffic..."
-iptables -P FORWARD DROP
-exitOnError $?
-printf "DONE\n"
-
-printf " * Creating general rules\n"
-printf " * Accept established and related input and output traffic..."
-iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-exitOnError $?
-iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-exitOnError $?
-printf "DONE\n"
-printf " * Accept local loopback input and output traffic..."
-iptables -A OUTPUT -o lo -j ACCEPT
-exitOnError $?
-iptables -A INPUT -i lo -j ACCEPT
-exitOnError $?
-printf "DONE\n"
-
-printf " * Creating VPN rules\n"
-for ip in $VPNIPS; do
- printf " * Accept output traffic to VPN server $ip through $INTERFACE, port $PROTOCOL $PORT..."
- iptables -A OUTPUT -d $ip -o $INTERFACE -p $PROTOCOL -m $PROTOCOL --dport $PORT -j ACCEPT
- exitOnError $?
- printf "DONE\n"
-done
-printf " * Accept all output traffic through $VPN_DEVICE..."
-iptables -A OUTPUT -o $VPN_DEVICE -j ACCEPT
-exitOnError $?
-printf "DONE\n"
-
-printf " * Creating local subnet rules\n"
-printf " * Accept input and output traffic to and from $SUBNET..."
-iptables -A INPUT -s $SUBNET -d $SUBNET -j ACCEPT
-iptables -A OUTPUT -s $SUBNET -d $SUBNET -j ACCEPT
-printf "DONE\n"
-for EXTRASUBNET in ${EXTRA_SUBNETS//,/ }
-do
- printf " * Accept input traffic through $INTERFACE from $EXTRASUBNET to $SUBNET..."
- iptables -A INPUT -i $INTERFACE -s $EXTRASUBNET -d $SUBNET -j ACCEPT
- exitOnError $?
- printf "DONE\n"
- # iptables -A OUTPUT -d $EXTRASUBNET -j ACCEPT
- # iptables -A OUTPUT -o $INTERFACE -s $SUBNET -d $EXTRASUBNET -j ACCEPT
-done
-
-############################################
-# TINYPROXY LAUNCH
-############################################
-if [ "$TINYPROXY" == "on" ]; then
- printf "[INFO] Setting TinyProxy log level to $TINYPROXY_LOG..."
- sed -i "/LogLevel /c\LogLevel $TINYPROXY_LOG" /etc/tinyproxy/tinyproxy.conf
- exitOnError $?
- printf "DONE\n"
- printf "[INFO] Setting TinyProxy port to $TINYPROXY_PORT..."
- sed -i "/Port /c\Port $TINYPROXY_PORT" /etc/tinyproxy/tinyproxy.conf
- exitOnError $?
- printf "DONE\n"
- if [ ! -z "$TINYPROXY_USER" ]; then
- printf "[INFO] Setting TinyProxy credentials..."
- echo "BasicAuth $TINYPROXY_USER $TINYPROXY_PASSWORD" >> /etc/tinyproxy/tinyproxy.conf
- unset -v TINYPROXY_USER
- unset -v TINYPROXY_PASSWORD
- printf "DONE\n"
- fi
- tinyproxy -d &
-fi
-
-############################################
-# SHADOWSOCKS
-############################################
-if [ "$SHADOWSOCKS" == "on" ]; then
- ARGS="-c /etc/shadowsocks.json"
- if [ "$SHADOWSOCKS_LOG" == " on" ]; then
- printf "[INFO] Setting ShadowSocks logging..."
- ARGS="$ARGS -v"
- printf "DONE\n"
- fi
- printf "[INFO] Setting ShadowSocks port to $SHADOWSOCKS_PORT..."
- jq ".port_password = {\"$SHADOWSOCKS_PORT\":\"\"}" /etc/shadowsocks.json > /tmp/shadowsocks.json && mv /tmp/shadowsocks.json /etc/shadowsocks.json
- exitOnError $?
- printf "DONE\n"
- printf "[INFO] Setting ShadowSocks password..."
- jq ".port_password[\"$SHADOWSOCKS_PORT\"] = \"$SHADOWSOCKS_PASSWORD\"" /etc/shadowsocks.json > /tmp/shadowsocks.json && mv /tmp/shadowsocks.json /etc/shadowsocks.json
- exitOnError $?
- printf "DONE\n"
- ARGS="$ARGS -s `jq --raw-output '.server' /etc/shadowsocks.json`"
- unset -v SERVER
- ARGS="$ARGS -p $SHADOWSOCKS_PORT"
- ARGS="$ARGS -k $SHADOWSOCKS_PASSWORD"
- ss-server $ARGS &
- unset -v ARGS
-fi
-
-############################################
-# READ FORWARDED PORT
-############################################
-
-if [ "$PORT_FORWARDING" == "on" ]; then
- sleep 10 && /portforward.sh &
-fi
-
-############################################
-# OPENVPN LAUNCH
-############################################
-printf "[INFO] Launching OpenVPN\n"
-cd "$TARGET_PATH"
-openvpn --config config.ovpn "$@"
-status=$?
-printf "\n =========================================\n"
-printf " OpenVPN exit with status $status\n"
-printf " =========================================\n\n"
diff --git a/go.mod b/go.mod
new file mode 100644
index 00000000..4f4e51fa
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,9 @@
+module github.com/qdm12/private-internet-access-docker
+
+go 1.13
+
+require (
+ github.com/kyokomi/emoji v2.1.0+incompatible
+ github.com/qdm12/golibs v0.0.0-20200206031633-f9e9b1bea1db
+ github.com/stretchr/testify v1.4.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 00000000..4ac930c9
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,93 @@
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
+github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
+github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
+github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
+github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
+github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
+github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
+github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
+github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/qdm12/golibs v0.0.0-20200206031633-f9e9b1bea1db h1:Q3bR2GWwvDxQ0EqLKUupzHswsBsEi3eMWxAPfvzKTbM=
+github.com/qdm12/golibs v0.0.0-20200206031633-f9e9b1bea1db/go.mod h1:YULaFjj6VGmhjak6f35sUWwEleHUmngN5IQ3kdvd6XE=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
diff --git a/healthcheck.sh b/healthcheck.sh
deleted file mode 100644
index 4f384e84..00000000
--- a/healthcheck.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-
-out="$(ping -W 3 -c 1 -q -s 8 1.1.1.1)"
-[ $? != 0 ] || exit 0
-printf "$out"
-exit 1
\ No newline at end of file
diff --git a/internal/constants/dns.go b/internal/constants/dns.go
new file mode 100644
index 00000000..61f25d74
--- /dev/null
+++ b/internal/constants/dns.go
@@ -0,0 +1,63 @@
+package constants
+
+import (
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const (
+ // Cloudflare is a DNS over TLS provider
+ Cloudflare models.DNSProvider = "cloudflare"
+ // Google is a DNS over TLS provider
+ Google models.DNSProvider = "google"
+ // Quad9 is a DNS over TLS provider
+ Quad9 models.DNSProvider = "quad9"
+ // Quadrant is a DNS over TLS provider
+ Quadrant models.DNSProvider = "quadrant"
+ // CleanBrowsing is a DNS over TLS provider
+ CleanBrowsing models.DNSProvider = "cleanbrowsing"
+ // SecureDNS is a DNS over TLS provider
+ SecureDNS models.DNSProvider = "securedns"
+ // LibreDNS is a DNS over TLS provider
+ LibreDNS models.DNSProvider = "libredns"
+)
+
+const (
+ CloudflareAddress1 models.DNSForwardAddress = "1.1.1.1@853#cloudflare-dns.com"
+ CloudflareAddress2 models.DNSForwardAddress = "1.0.0.1@853#cloudflare-dns.com"
+ GoogleAddress1 models.DNSForwardAddress = "8.8.8.8@853#dns.google"
+ GoogleAddress2 models.DNSForwardAddress = "8.8.4.4@853#dns.google"
+ Quad9Address1 models.DNSForwardAddress = "9.9.9.9@853#dns.quad9.net"
+ Quad9Address2 models.DNSForwardAddress = "149.112.112.112@853#dns.quad9.net"
+ QuadrantAddress models.DNSForwardAddress = "12.159.2.159@853#dns-tls.qis.io"
+ CleanBrowsingAddress1 models.DNSForwardAddress = "185.228.168.9@853#security-filter-dns.cleanbrowsing.org"
+ CleanBrowsingAddress2 models.DNSForwardAddress = "185.228.169.9@853#security-filter-dns.cleanbrowsing.org"
+ SecureDNSAddress models.DNSForwardAddress = "146.185.167.43@853#dot.securedns.eu"
+ LibreDNSAddress models.DNSForwardAddress = "116.203.115.192@853#dot.libredns.gr"
+)
+
+var DNSAddressesMapping = map[models.DNSProvider][]models.DNSForwardAddress{
+ Cloudflare: []models.DNSForwardAddress{CloudflareAddress1, CloudflareAddress2},
+ Google: []models.DNSForwardAddress{GoogleAddress1, GoogleAddress2},
+ Quad9: []models.DNSForwardAddress{Quad9Address1, Quad9Address2},
+ Quadrant: []models.DNSForwardAddress{QuadrantAddress},
+ CleanBrowsing: []models.DNSForwardAddress{CleanBrowsingAddress1, CleanBrowsingAddress2},
+ SecureDNS: []models.DNSForwardAddress{SecureDNSAddress},
+ LibreDNS: []models.DNSForwardAddress{LibreDNSAddress},
+}
+
+// Block lists URLs
+const (
+ AdsBlockListHostnamesURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/ads-hostnames.updated"
+ AdsBlockListIPsURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/ads-ips.updated"
+ MaliciousBlockListHostnamesURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/malicious-hostnames.updated"
+ MaliciousBlockListIPsURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/malicious-ips.updated"
+ SurveillanceBlockListHostnamesURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/surveillance-hostnames.updated"
+ SurveillanceBlockListIPsURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/surveillance-ips.updated"
+)
+
+// DNS certificates to fetch
+// TODO obtain from source directly, see qdm12/updated)
+const (
+ NamedRootURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/named.root.updated"
+ RootKeyURL models.URL = "https://raw.githubusercontent.com/qdm12/files/master/root.key.updated"
+)
diff --git a/internal/constants/openvpn.go b/internal/constants/openvpn.go
new file mode 100644
index 00000000..d878805f
--- /dev/null
+++ b/internal/constants/openvpn.go
@@ -0,0 +1,10 @@
+package constants
+
+import (
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const (
+ TUN models.VPNDevice = "tun0"
+ TAP models.VPNDevice = "tap0"
+)
diff --git a/internal/constants/paths.go b/internal/constants/paths.go
new file mode 100644
index 00000000..b318eeb6
--- /dev/null
+++ b/internal/constants/paths.go
@@ -0,0 +1,28 @@
+package constants
+
+import (
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const (
+ // UnboundConf is the file path to the Unbound configuration file
+ UnboundConf models.Filepath = "/etc/unbound/unbound.conf"
+ // ResolvConf is the file path to the system resolv.conf file
+ ResolvConf models.Filepath = "/etc/resolv.conf"
+ // OpenVPNAuthConf is the file path to the OpenVPN auth file
+ OpenVPNAuthConf models.Filepath = "/etc/openvpn/auth.conf"
+ // OpenVPNConf is the file path to the OpenVPN client configuration file
+ OpenVPNConf models.Filepath = "/etc/openvpn/target.ovpn"
+ // TunnelDevice is the file path to tun device
+ TunnelDevice models.Filepath = "/dev/net/tun"
+ // NetRoute is the path to the file containing information on the network route
+ NetRoute models.Filepath = "/proc/net/route"
+ // TinyProxyConf is the filepath to the tinyproxy configuration file
+ TinyProxyConf models.Filepath = "/etc/tinyproxy/tinyproxy.conf"
+ // ShadowsocksConf is the filepath to the shadowsocks configuration file
+ ShadowsocksConf models.Filepath = "/etc/shadowsocks.json"
+ // RootHints is the filepath to the root.hints file used by Unbound
+ RootHints models.Filepath = "/etc/unbound/root.hints"
+ // RootKey is the filepath to the root.key file used by Unbound
+ RootKey models.Filepath = "/etc/unbound/root.key"
+)
diff --git a/internal/constants/pia.go b/internal/constants/pia.go
new file mode 100644
index 00000000..7be742d3
--- /dev/null
+++ b/internal/constants/pia.go
@@ -0,0 +1,70 @@
+package constants
+
+import (
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const (
+ // PIAEncryptionNormal is the normal level of encryption for communication with PIA servers
+ PIAEncryptionNormal models.PIAEncryption = "normal"
+ // PIAEncryptionStrong is the strong level of encryption for communication with PIA servers
+ PIAEncryptionStrong models.PIAEncryption = "strong"
+)
+
+const (
+ AUMelbourne models.PIARegion = "AU Melbourne"
+ AUPerth models.PIARegion = "AU Perth"
+ AUSydney models.PIARegion = "AU Sydney"
+ Austria models.PIARegion = "Austria"
+ Belgium models.PIARegion = "Belgium"
+ CAMontreal models.PIARegion = "CA Montreal"
+ CAToronto models.PIARegion = "CA Toronto"
+ CAVancouver models.PIARegion = "CA Vancouver"
+ CzechRepublic models.PIARegion = "Czech Republic"
+ DEBerlin models.PIARegion = "DE Berlin"
+ DEFrankfurt models.PIARegion = "DE Frankfurt"
+ Denmark models.PIARegion = "Denmark"
+ Finland models.PIARegion = "Finland"
+ France models.PIARegion = "France"
+ HongKong models.PIARegion = "Hong Kong"
+ Hungary models.PIARegion = "Hungary"
+ India models.PIARegion = "India"
+ Ireland models.PIARegion = "Ireland"
+ Israel models.PIARegion = "Israel"
+ Italy models.PIARegion = "Italy"
+ Japan models.PIARegion = "Japan"
+ Luxembourg models.PIARegion = "Luxembourg"
+ Mexico models.PIARegion = "Mexico"
+ Netherlands models.PIARegion = "Netherlands"
+ NewZealand models.PIARegion = "New Zealand"
+ Norway models.PIARegion = "Norway"
+ Poland models.PIARegion = "Poland"
+ Romania models.PIARegion = "Romania"
+ Singapore models.PIARegion = "Singapore"
+ Spain models.PIARegion = "Spain"
+ Sweden models.PIARegion = "Sweden"
+ Switzerland models.PIARegion = "Switzerland"
+ UAE models.PIARegion = "UAE"
+ UKLondon models.PIARegion = "UK London"
+ UKManchester models.PIARegion = "UK Manchester"
+ UKSouthampton models.PIARegion = "UK Southampton"
+ USAtlanta models.PIARegion = "US Atlanta"
+ USCalifornia models.PIARegion = "US California"
+ USChicago models.PIARegion = "US Chicago"
+ USDenver models.PIARegion = "US Denver"
+ USEast models.PIARegion = "US East"
+ USFlorida models.PIARegion = "US Florida"
+ USHouston models.PIARegion = "US Houston"
+ USLasVegas models.PIARegion = "US Las Vegas"
+ USNewYorkCity models.PIARegion = "US New York City"
+ USSeattle models.PIARegion = "US Seattle"
+ USSiliconValley models.PIARegion = "US Silicon Valley"
+ USTexas models.PIARegion = "US Texas"
+ USWashingtonDC models.PIARegion = "US Washington DC"
+ USWest models.PIARegion = "US West"
+)
+
+const (
+ PIAOpenVPNURL models.URL = "https://www.privateinternetaccess.com/openvpn"
+ PIAPortForwardURL models.URL = "http://209.222.18.222:2000"
+)
diff --git a/internal/constants/splash.go b/internal/constants/splash.go
new file mode 100644
index 00000000..32b5d773
--- /dev/null
+++ b/internal/constants/splash.go
@@ -0,0 +1,13 @@
+package constants
+
+const (
+ // Annoucement is a message annoucement
+ Annoucement = "Total rewrite in Go with many new features"
+ // AnnoucementExpiration is the expiration time of the annoucement in unix timestamp
+ AnnoucementExpiration = 1582761600
+)
+
+const (
+ // IssueLink is the link for users to use to create issues
+ IssueLink = "https://github.com/qdm12/private-internet-access-docker/issues/new"
+)
diff --git a/internal/constants/tinyproxy.go b/internal/constants/tinyproxy.go
new file mode 100644
index 00000000..90cd7100
--- /dev/null
+++ b/internal/constants/tinyproxy.go
@@ -0,0 +1,16 @@
+package constants
+
+import (
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const (
+ // TinyProxyInfoLevel is the info log level for TinyProxy
+ TinyProxyInfoLevel models.TinyProxyLogLevel = "Info"
+ // TinyProxyWarnLevel is the warning log level for TinyProxy
+ TinyProxyWarnLevel models.TinyProxyLogLevel = "Warning"
+ // TinyProxyErrorLevel is the error log level for TinyProxy
+ TinyProxyErrorLevel models.TinyProxyLogLevel = "Error"
+ // TinyProxyCriticalLevel is the critical log level for TinyProxy
+ TinyProxyCriticalLevel models.TinyProxyLogLevel = "Critical"
+)
diff --git a/internal/constants/vpn.go b/internal/constants/vpn.go
new file mode 100644
index 00000000..9f672e81
--- /dev/null
+++ b/internal/constants/vpn.go
@@ -0,0 +1,21 @@
+package constants
+
+import (
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const (
+ // PrivateInternetAccess is a VPN provider
+ PrivateInternetAccess models.VPNProvider = "private internet access"
+ // Mullvad is a VPN provider
+ Mullvad models.VPNProvider = "mullvad"
+ // Windscribe is a VPN provider
+ Windscribe models.VPNProvider = "windscribe"
+)
+
+const (
+ // TCP is a network protocol (reliable and slower than UDP)
+ TCP models.NetworkProtocol = "tcp"
+ // UDP is a network protocol (unreliable and faster than TCP)
+ UDP models.NetworkProtocol = "udp"
+)
diff --git a/internal/dns/command.go b/internal/dns/command.go
new file mode 100644
index 00000000..42c68d7a
--- /dev/null
+++ b/internal/dns/command.go
@@ -0,0 +1,40 @@
+package dns
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) Start(verbosityDetailsLevel uint8) (stdout io.ReadCloser, err error) {
+ c.logger.Info("%s: starting unbound", logPrefix)
+ args := []string{"-d", "-c", string(constants.UnboundConf)}
+ if verbosityDetailsLevel > 0 {
+ args = append(args, "-"+strings.Repeat("v", int(verbosityDetailsLevel)))
+ }
+ // Only logs to stderr
+ _, stdout, _, err = c.commander.Start("unbound", args...)
+ return stdout, err
+}
+
+func (c *configurator) Version() (version string, err error) {
+ output, err := c.commander.Run("unbound", "-V")
+ if err != nil {
+ return "", fmt.Errorf("unbound version: %w", err)
+ }
+ for _, line := range strings.Split(output, "\n") {
+ if strings.Contains(line, "Version ") {
+ words := strings.Fields(line)
+ if len(words) < 2 {
+ continue
+ }
+ version = words[1]
+ }
+ }
+ if version == "" {
+ return "", fmt.Errorf("unbound version was not found in %q", output)
+ }
+ return version, nil
+}
diff --git a/internal/dns/command_test.go b/internal/dns/command_test.go
new file mode 100644
index 00000000..e230c849
--- /dev/null
+++ b/internal/dns/command_test.go
@@ -0,0 +1,69 @@
+package dns
+
+import (
+ "fmt"
+ "testing"
+
+ commandMocks "github.com/qdm12/golibs/command/mocks"
+ loggingMocks "github.com/qdm12/golibs/logging/mocks"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func Test_Start(t *testing.T) {
+ t.Parallel()
+ logger := &loggingMocks.Logger{}
+ logger.On("Info", "%s: starting unbound", logPrefix).Once()
+ commander := &commandMocks.Commander{}
+ commander.On("Start", "unbound", "-d", "-c", string(constants.UnboundConf), "-vv").
+ Return(nil, nil, nil, nil).Once()
+ c := &configurator{commander: commander, logger: logger}
+ stdout, err := c.Start(2)
+ assert.Nil(t, stdout)
+ assert.NoError(t, err)
+ logger.AssertExpectations(t)
+ commander.AssertExpectations(t)
+}
+
+func Test_Version(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ runOutput string
+ runErr error
+ version string
+ err error
+ }{
+ "no data": {
+ err: fmt.Errorf(`unbound version was not found in ""`),
+ },
+ "2 lines with version": {
+ runOutput: "Version \nVersion 1.0-a hello\n",
+ version: "1.0-a",
+ },
+ "run error": {
+ runErr: fmt.Errorf("error"),
+ err: fmt.Errorf("unbound version: error"),
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ commander := &commandMocks.Commander{}
+ commander.On("Run", "unbound", "-V").
+ Return(tc.runOutput, tc.runErr).Once()
+ c := &configurator{commander: commander}
+ version, err := c.Version()
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, tc.version, version)
+ commander.AssertExpectations(t)
+ })
+ }
+}
diff --git a/internal/dns/conf.go b/internal/dns/conf.go
new file mode 100644
index 00000000..86a77e79
--- /dev/null
+++ b/internal/dns/conf.go
@@ -0,0 +1,280 @@
+package dns
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/network"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/settings"
+)
+
+func (c *configurator) MakeUnboundConf(settings settings.DNS, uid, gid int) (err error) {
+ c.logger.Info("%s: generating Unbound configuration", logPrefix)
+ lines, warnings, err := generateUnboundConf(settings, c.client, c.logger)
+ for _, warning := range warnings {
+ c.logger.Warn(warning)
+ }
+ if err != nil {
+ return err
+ }
+ return c.fileManager.WriteLinesToFile(
+ string(constants.UnboundConf),
+ lines,
+ files.FileOwnership(uid, gid),
+ files.FilePermissions(0400))
+}
+
+// MakeUnboundConf generates an Unbound configuration from the user provided settings
+func generateUnboundConf(settings settings.DNS, client network.Client, logger logging.Logger) (lines []string, warnings []error, err error) {
+ serverSection := map[string]string{
+ // Logging
+ "verbosity": fmt.Sprintf("%d", settings.VerbosityLevel),
+ "val-log-level": fmt.Sprintf("%d", settings.ValidationLogLevel),
+ "use-syslog": "no",
+ // Performance
+ "num-threads": "1",
+ "prefetch": "yes",
+ "prefetch-key": "yes",
+ "key-cache-size": "16m",
+ "key-cache-slabs": "4",
+ "msg-cache-size": "4m",
+ "msg-cache-slabs": "4",
+ "rrset-cache-size": "4m",
+ "rrset-cache-slabs": "4",
+ "cache-min-ttl": "3600",
+ "cache-max-ttl": "9000",
+ // Privacy
+ "rrset-roundrobin": "yes",
+ "hide-identity": "yes",
+ "hide-version": "yes",
+ // Security
+ "tls-cert-bundle": "\"/etc/ssl/certs/ca-certificates.crt\"",
+ "root-hints": fmt.Sprintf("%q", constants.RootHints),
+ "trust-anchor-file": fmt.Sprintf("%q", constants.RootKey),
+ "harden-below-nxdomain": "yes",
+ "harden-referral-path": "yes",
+ "harden-algo-downgrade": "yes",
+ // Network
+ "do-ip4": "yes",
+ "do-ip6": "no",
+ "interface": "127.0.0.1",
+ "port": "53",
+ // Other
+ "username": "\"nonrootuser\"",
+ }
+
+ // Block lists
+ hostnamesLines, ipsLines, warnings := buildBlocked(client,
+ settings.BlockMalicious, settings.BlockAds, settings.BlockSurveillance,
+ settings.AllowedHostnames, settings.PrivateAddresses,
+ )
+ logger.Info("%s: %d hostnames blocked overall", logPrefix, len(hostnamesLines))
+ logger.Info("%s: %d IP addresses blocked overall", logPrefix, len(ipsLines))
+ sort.Slice(hostnamesLines, func(i, j int) bool { // for unit tests really
+ return hostnamesLines[i] < hostnamesLines[j]
+ })
+ sort.Slice(ipsLines, func(i, j int) bool { // for unit tests really
+ return ipsLines[i] < ipsLines[j]
+ })
+
+ // Server
+ lines = append(lines, "server:")
+ var serverLines []string
+ for k, v := range serverSection {
+ serverLines = append(serverLines, " "+k+": "+v)
+ }
+ sort.Slice(serverLines, func(i, j int) bool {
+ return serverLines[i] < serverLines[j]
+ })
+ lines = append(lines, serverLines...)
+ lines = append(lines, hostnamesLines...)
+ lines = append(lines, ipsLines...)
+
+ // Forward zone
+ lines = append(lines, "forward-zone:")
+ forwardZoneSection := map[string]string{
+ "name": "\".\"",
+ "forward-tls-upstream": "yes",
+ }
+ var forwardZoneLines []string
+ for k, v := range forwardZoneSection {
+ forwardZoneLines = append(forwardZoneLines, " "+k+": "+v)
+ }
+ sort.Slice(forwardZoneLines, func(i, j int) bool {
+ return forwardZoneLines[i] < forwardZoneLines[j]
+ })
+ for _, provider := range settings.Providers {
+ forwardAddresses, ok := constants.DNSAddressesMapping[provider]
+ if !ok || len(forwardAddresses) == 0 {
+ return nil, warnings, fmt.Errorf("DNS provider %q does not have any matching forward addresses", provider)
+ }
+ for _, forwardAddress := range forwardAddresses {
+ forwardZoneLines = append(forwardZoneLines, fmt.Sprintf(" forward-addr: %s", forwardAddress))
+ }
+ }
+ lines = append(lines, forwardZoneLines...)
+ return lines, warnings, nil
+}
+
+func buildBlocked(client network.Client, blockMalicious, blockAds, blockSurveillance bool,
+ allowedHostnames, privateAddresses []string) (hostnamesLines, ipsLines []string, errs []error) {
+ chHostnames := make(chan []string)
+ chIPs := make(chan []string)
+ chErrors := make(chan []error)
+ go func() {
+ lines, errs := buildBlockedHostnames(client, blockMalicious, blockAds, blockSurveillance, allowedHostnames)
+ chHostnames <- lines
+ chErrors <- errs
+ }()
+ go func() {
+ lines, errs := buildBlockedIPs(client, blockMalicious, blockAds, blockSurveillance, privateAddresses)
+ chIPs <- lines
+ chErrors <- errs
+ }()
+ n := 2
+ for n > 0 {
+ select {
+ case lines := <-chHostnames:
+ hostnamesLines = append(hostnamesLines, lines...)
+ case lines := <-chIPs:
+ ipsLines = append(ipsLines, lines...)
+ case routineErrs := <-chErrors:
+ errs = append(errs, routineErrs...)
+ n--
+ }
+ }
+ return hostnamesLines, ipsLines, errs
+}
+
+func getList(client network.Client, URL string) (results []string, err error) {
+ content, status, err := client.GetContent(URL)
+ if err != nil {
+ return nil, err
+ } else if status != 200 {
+ return nil, fmt.Errorf("HTTP status code is %d and not 200", status)
+ }
+ results = strings.Split(string(content), "\n")
+
+ // remove empty lines
+ last := len(results) - 1
+ for i := range results {
+ if len(results[i]) == 0 {
+ results[i] = results[last]
+ last--
+ }
+ }
+ results = results[:last+1]
+
+ if len(results) == 0 {
+ return nil, nil
+ }
+ return results, nil
+}
+
+func buildBlockedHostnames(client network.Client, blockMalicious, blockAds, blockSurveillance bool,
+ allowedHostnames []string) (lines []string, errs []error) {
+ chResults := make(chan []string)
+ chError := make(chan error)
+ listsLeftToFetch := 0
+ if blockMalicious {
+ listsLeftToFetch++
+ go func() {
+ results, err := getList(client, string(constants.MaliciousBlockListHostnamesURL))
+ chResults <- results
+ chError <- err
+ }()
+ }
+ if blockAds {
+ listsLeftToFetch++
+ go func() {
+ results, err := getList(client, string(constants.AdsBlockListHostnamesURL))
+ chResults <- results
+ chError <- err
+ }()
+ }
+ if blockSurveillance {
+ listsLeftToFetch++
+ go func() {
+ results, err := getList(client, string(constants.SurveillanceBlockListHostnamesURL))
+ chResults <- results
+ chError <- err
+ }()
+ }
+ uniqueResults := make(map[string]struct{})
+ for listsLeftToFetch > 0 {
+ select {
+ case results := <-chResults:
+ for _, result := range results {
+ uniqueResults[result] = struct{}{}
+ }
+ case err := <-chError:
+ listsLeftToFetch--
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ }
+ for _, allowedHostname := range allowedHostnames {
+ delete(uniqueResults, allowedHostname)
+ }
+ for result := range uniqueResults {
+ lines = append(lines, " local-zone: \""+result+"\" static")
+ }
+ return lines, errs
+}
+
+func buildBlockedIPs(client network.Client, blockMalicious, blockAds, blockSurveillance bool,
+ privateAddresses []string) (lines []string, errs []error) {
+ chResults := make(chan []string)
+ chError := make(chan error)
+ listsLeftToFetch := 0
+ if blockMalicious {
+ listsLeftToFetch++
+ go func() {
+ results, err := getList(client, string(constants.MaliciousBlockListIPsURL))
+ chResults <- results
+ chError <- err
+ }()
+ }
+ if blockAds {
+ listsLeftToFetch++
+ go func() {
+ results, err := getList(client, string(constants.AdsBlockListIPsURL))
+ chResults <- results
+ chError <- err
+ }()
+ }
+ if blockSurveillance {
+ listsLeftToFetch++
+ go func() {
+ results, err := getList(client, string(constants.SurveillanceBlockListIPsURL))
+ chResults <- results
+ chError <- err
+ }()
+ }
+ uniqueResults := make(map[string]struct{})
+ for listsLeftToFetch > 0 {
+ select {
+ case results := <-chResults:
+ for _, result := range results {
+ uniqueResults[result] = struct{}{}
+ }
+ case err := <-chError:
+ listsLeftToFetch--
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ }
+ for _, privateAddress := range privateAddresses {
+ uniqueResults[privateAddress] = struct{}{}
+ }
+ for result := range uniqueResults {
+ lines = append(lines, " private-address: "+result)
+ }
+ return lines, errs
+}
diff --git a/internal/dns/conf_test.go b/internal/dns/conf_test.go
new file mode 100644
index 00000000..ec036be2
--- /dev/null
+++ b/internal/dns/conf_test.go
@@ -0,0 +1,518 @@
+package dns
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/network/mocks"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+ "github.com/qdm12/private-internet-access-docker/internal/settings"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_generateUnboundConf(t *testing.T) {
+ t.Parallel()
+ settings := settings.DNS{
+ Providers: []models.DNSProvider{constants.Cloudflare, constants.Quad9},
+ AllowedHostnames: []string{"a"},
+ PrivateAddresses: []string{"9.9.9.9"},
+ BlockMalicious: true,
+ BlockSurveillance: false,
+ BlockAds: false,
+ VerbosityLevel: 2,
+ ValidationLogLevel: 3,
+ }
+ client := &mocks.Client{}
+ client.On("GetContent", string(constants.MaliciousBlockListHostnamesURL)).
+ Return([]byte("b\na\nc"), 200, nil).Once()
+ client.On("GetContent", string(constants.MaliciousBlockListIPsURL)).
+ Return([]byte("c\nd\n"), 200, nil).Once()
+ emptyLogger, err := logging.NewEmptyLogger()
+ require.NoError(t, err)
+ lines, warnings, err := generateUnboundConf(settings, client, emptyLogger)
+ require.Len(t, warnings, 0)
+ require.NoError(t, err)
+ client.AssertExpectations(t)
+ expected := `
+server:
+ cache-max-ttl: 9000
+ cache-min-ttl: 3600
+ do-ip4: yes
+ do-ip6: no
+ harden-algo-downgrade: yes
+ harden-below-nxdomain: yes
+ harden-referral-path: yes
+ hide-identity: yes
+ hide-version: yes
+ interface: 127.0.0.1
+ key-cache-size: 16m
+ key-cache-slabs: 4
+ msg-cache-size: 4m
+ msg-cache-slabs: 4
+ num-threads: 1
+ port: 53
+ prefetch-key: yes
+ prefetch: yes
+ root-hints: "/etc/unbound/root.hints"
+ rrset-cache-size: 4m
+ rrset-cache-slabs: 4
+ rrset-roundrobin: yes
+ tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
+ trust-anchor-file: "/etc/unbound/root.key"
+ use-syslog: no
+ username: "nonrootuser"
+ val-log-level: 3
+ verbosity: 2
+ local-zone: "b" static
+ local-zone: "c" static
+ private-address: 9.9.9.9
+ private-address: c
+ private-address: d
+forward-zone:
+ forward-tls-upstream: yes
+ name: "."
+ forward-addr: 1.1.1.1@853#cloudflare-dns.com
+ forward-addr: 1.0.0.1@853#cloudflare-dns.com
+ forward-addr: 9.9.9.9@853#dns.quad9.net
+ forward-addr: 149.112.112.112@853#dns.quad9.net`
+ assert.Equal(t, expected, "\n"+strings.Join(lines, "\n"))
+}
+
+func Test_buildBlocked(t *testing.T) {
+ t.Parallel()
+ type blockParams struct {
+ blocked bool
+ content []byte
+ clientErr error
+ }
+ tests := map[string]struct {
+ malicious blockParams
+ ads blockParams
+ surveillance blockParams
+ allowedHostnames []string
+ privateAddresses []string
+ hostnamesLines []string
+ ipsLines []string
+ errsString []string
+ }{
+ "none blocked": {},
+ "all blocked without lists": {
+ malicious: blockParams{
+ blocked: true,
+ },
+ ads: blockParams{
+ blocked: true,
+ },
+ surveillance: blockParams{
+ blocked: true,
+ },
+ },
+ "all blocked with lists": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("malicious"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("ads"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ content: []byte("surveillance"),
+ },
+ hostnamesLines: []string{
+ " local-zone: \"ads\" static",
+ " local-zone: \"malicious\" static",
+ " local-zone: \"surveillance\" static"},
+ ipsLines: []string{
+ " private-address: ads",
+ " private-address: malicious",
+ " private-address: surveillance"},
+ },
+ "all blocked with allowed hostnames": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("malicious"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("ads"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ content: []byte("surveillance"),
+ },
+ allowedHostnames: []string{"ads"},
+ hostnamesLines: []string{
+ " local-zone: \"malicious\" static",
+ " local-zone: \"surveillance\" static"},
+ ipsLines: []string{
+ " private-address: ads",
+ " private-address: malicious",
+ " private-address: surveillance"},
+ },
+ "all blocked with private addresses": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("malicious"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("ads"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ content: []byte("surveillance"),
+ },
+ privateAddresses: []string{"ads", "192.100.1.5"},
+ hostnamesLines: []string{
+ " local-zone: \"ads\" static",
+ " local-zone: \"malicious\" static",
+ " local-zone: \"surveillance\" static"},
+ ipsLines: []string{
+ " private-address: 192.100.1.5",
+ " private-address: ads",
+ " private-address: malicious",
+ " private-address: surveillance"},
+ },
+ "all blocked with lists and one error": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("malicious"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("ads"),
+ clientErr: fmt.Errorf("ads error"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ content: []byte("surveillance"),
+ },
+ hostnamesLines: []string{
+ " local-zone: \"malicious\" static",
+ " local-zone: \"surveillance\" static"},
+ ipsLines: []string{
+ " private-address: malicious",
+ " private-address: surveillance"},
+ errsString: []string{"ads error", "ads error"},
+ },
+ "all blocked with errors": {
+ malicious: blockParams{
+ blocked: true,
+ clientErr: fmt.Errorf("malicious"),
+ },
+ ads: blockParams{
+ blocked: true,
+ clientErr: fmt.Errorf("ads"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ clientErr: fmt.Errorf("surveillance"),
+ },
+ errsString: []string{"malicious", "malicious", "ads", "ads", "surveillance", "surveillance"},
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ client := &mocks.Client{}
+ if tc.malicious.blocked {
+ client.On("GetContent", string(constants.MaliciousBlockListHostnamesURL)).
+ Return(tc.malicious.content, 200, tc.malicious.clientErr).Once()
+ client.On("GetContent", string(constants.MaliciousBlockListIPsURL)).
+ Return(tc.malicious.content, 200, tc.malicious.clientErr).Once()
+ }
+ if tc.ads.blocked {
+ client.On("GetContent", string(constants.AdsBlockListHostnamesURL)).
+ Return(tc.ads.content, 200, tc.ads.clientErr).Once()
+ client.On("GetContent", string(constants.AdsBlockListIPsURL)).
+ Return(tc.ads.content, 200, tc.ads.clientErr).Once()
+ }
+ if tc.surveillance.blocked {
+ client.On("GetContent", string(constants.SurveillanceBlockListHostnamesURL)).
+ Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Once()
+ client.On("GetContent", string(constants.SurveillanceBlockListIPsURL)).
+ Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Once()
+ }
+ hostnamesLines, ipsLines, errs := buildBlocked(client, tc.malicious.blocked, tc.ads.blocked, tc.surveillance.blocked,
+ tc.allowedHostnames, tc.privateAddresses)
+ var errsString []string
+ for _, err := range errs {
+ errsString = append(errsString, err.Error())
+ }
+ assert.ElementsMatch(t, tc.errsString, errsString)
+ assert.ElementsMatch(t, tc.hostnamesLines, hostnamesLines)
+ assert.ElementsMatch(t, tc.ipsLines, ipsLines)
+ client.AssertExpectations(t)
+ })
+ }
+}
+
+func Test_getList(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ content []byte
+ status int
+ clientErr error
+ results []string
+ err error
+ }{
+ "no result": {nil, 200, nil, nil, nil},
+ "bad status": {nil, 500, nil, nil, fmt.Errorf("HTTP status code is 500 and not 200")},
+ "network error": {nil, 200, fmt.Errorf("error"), nil, fmt.Errorf("error")},
+ "results": {[]byte("a\nb\nc\n"), 200, nil, []string{"a", "b", "c"}, nil},
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ client := &mocks.Client{}
+ client.On("GetContent", "irrelevant_url").Return(
+ tc.content, tc.status, tc.clientErr,
+ ).Once()
+ results, err := getList(client, "irrelevant_url")
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, tc.results, results)
+ client.AssertExpectations(t)
+ })
+ }
+}
+
+func Test_buildBlockedHostnames(t *testing.T) {
+ t.Parallel()
+ type blockParams struct {
+ blocked bool
+ content []byte
+ clientErr error
+ }
+ tests := map[string]struct {
+ malicious blockParams
+ ads blockParams
+ surveillance blockParams
+ allowedHostnames []string
+ lines []string
+ errsString []string
+ }{
+ "nothing blocked": {
+ lines: nil,
+ errsString: nil,
+ },
+ "only malicious blocked": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ clientErr: nil,
+ },
+ lines: []string{
+ " local-zone: \"site_a\" static",
+ " local-zone: \"site_b\" static"},
+ errsString: nil,
+ },
+ "all blocked with some duplicates": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_c"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ content: []byte("site_c\nsite_a"),
+ },
+ lines: []string{
+ " local-zone: \"site_a\" static",
+ " local-zone: \"site_b\" static",
+ " local-zone: \"site_c\" static"},
+ errsString: nil,
+ },
+ "all blocked with one errored": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_c"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ clientErr: fmt.Errorf("surveillance error"),
+ },
+ lines: []string{
+ " local-zone: \"site_a\" static",
+ " local-zone: \"site_b\" static",
+ " local-zone: \"site_c\" static"},
+ errsString: []string{"surveillance error"},
+ },
+ "blocked with allowed hostnames": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("site_c\nsite_d"),
+ },
+ allowedHostnames: []string{"site_b", "site_c"},
+ lines: []string{
+ " local-zone: \"site_a\" static",
+ " local-zone: \"site_d\" static"},
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ client := &mocks.Client{}
+ if tc.malicious.blocked {
+ client.On("GetContent", string(constants.MaliciousBlockListHostnamesURL)).
+ Return(tc.malicious.content, 200, tc.malicious.clientErr).Once()
+ }
+ if tc.ads.blocked {
+ client.On("GetContent", string(constants.AdsBlockListHostnamesURL)).
+ Return(tc.ads.content, 200, tc.ads.clientErr).Once()
+ }
+ if tc.surveillance.blocked {
+ client.On("GetContent", string(constants.SurveillanceBlockListHostnamesURL)).
+ Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Once()
+ }
+ lines, errs := buildBlockedHostnames(client,
+ tc.malicious.blocked, tc.ads.blocked, tc.surveillance.blocked, tc.allowedHostnames)
+ var errsString []string
+ for _, err := range errs {
+ errsString = append(errsString, err.Error())
+ }
+ assert.ElementsMatch(t, tc.errsString, errsString)
+ assert.ElementsMatch(t, tc.lines, lines)
+ client.AssertExpectations(t)
+ })
+ }
+}
+
+func Test_buildBlockedIPs(t *testing.T) {
+ t.Parallel()
+ type blockParams struct {
+ blocked bool
+ content []byte
+ clientErr error
+ }
+ tests := map[string]struct {
+ malicious blockParams
+ ads blockParams
+ surveillance blockParams
+ privateAddresses []string
+ lines []string
+ errsString []string
+ }{
+ "nothing blocked": {
+ lines: nil,
+ errsString: nil,
+ },
+ "only malicious blocked": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ clientErr: nil,
+ },
+ lines: []string{
+ " private-address: site_a",
+ " private-address: site_b"},
+ errsString: nil,
+ },
+ "all blocked with some duplicates": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_c"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ content: []byte("site_c\nsite_a"),
+ },
+ lines: []string{
+ " private-address: site_a",
+ " private-address: site_b",
+ " private-address: site_c"},
+ errsString: nil,
+ },
+ "all blocked with one errored": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_c"),
+ },
+ surveillance: blockParams{
+ blocked: true,
+ clientErr: fmt.Errorf("surveillance error"),
+ },
+ lines: []string{
+ " private-address: site_a",
+ " private-address: site_b",
+ " private-address: site_c"},
+ errsString: []string{"surveillance error"},
+ },
+ "blocked with private addresses": {
+ malicious: blockParams{
+ blocked: true,
+ content: []byte("site_a\nsite_b"),
+ },
+ ads: blockParams{
+ blocked: true,
+ content: []byte("site_c"),
+ },
+ privateAddresses: []string{"site_c", "site_d"},
+ lines: []string{
+ " private-address: site_a",
+ " private-address: site_b",
+ " private-address: site_c",
+ " private-address: site_d"},
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ client := &mocks.Client{}
+ if tc.malicious.blocked {
+ client.On("GetContent", string(constants.MaliciousBlockListIPsURL)).
+ Return(tc.malicious.content, 200, tc.malicious.clientErr).Once()
+ }
+ if tc.ads.blocked {
+ client.On("GetContent", string(constants.AdsBlockListIPsURL)).
+ Return(tc.ads.content, 200, tc.ads.clientErr).Once()
+ }
+ if tc.surveillance.blocked {
+ client.On("GetContent", string(constants.SurveillanceBlockListIPsURL)).
+ Return(tc.surveillance.content, 200, tc.surveillance.clientErr).Once()
+ }
+ lines, errs := buildBlockedIPs(client,
+ tc.malicious.blocked, tc.ads.blocked, tc.surveillance.blocked, tc.privateAddresses)
+ var errsString []string
+ for _, err := range errs {
+ errsString = append(errsString, err.Error())
+ }
+ assert.ElementsMatch(t, tc.errsString, errsString)
+ assert.ElementsMatch(t, tc.lines, lines)
+ client.AssertExpectations(t)
+ })
+ }
+}
diff --git a/internal/dns/dns.go b/internal/dns/dns.go
new file mode 100644
index 00000000..6b637f5f
--- /dev/null
+++ b/internal/dns/dns.go
@@ -0,0 +1,38 @@
+package dns
+
+import (
+ "io"
+
+ "github.com/qdm12/golibs/command"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/network"
+ "github.com/qdm12/private-internet-access-docker/internal/settings"
+)
+
+const logPrefix = "dns configurator"
+
+type Configurator interface {
+ DownloadRootHints(uid, gid int) error
+ DownloadRootKey(uid, gid int) error
+ MakeUnboundConf(settings settings.DNS, uid, gid int) (err error)
+ SetLocalNameserver() error
+ Start(logLevel uint8) (stdout io.ReadCloser, err error)
+ Version() (version string, err error)
+}
+
+type configurator struct {
+ logger logging.Logger
+ client network.Client
+ fileManager files.FileManager
+ commander command.Commander
+}
+
+func NewConfigurator(logger logging.Logger, client network.Client, fileManager files.FileManager) Configurator {
+ return &configurator{
+ logger: logger,
+ client: client,
+ fileManager: fileManager,
+ commander: command.NewCommander(),
+ }
+}
diff --git a/internal/dns/os.go b/internal/dns/os.go
new file mode 100644
index 00000000..6a0afda2
--- /dev/null
+++ b/internal/dns/os.go
@@ -0,0 +1,32 @@
+package dns
+
+import (
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) SetLocalNameserver() error {
+ c.logger.Info("%s: setting local nameserver to 127.0.0.1", logPrefix)
+ data, err := c.fileManager.ReadFile(string(constants.ResolvConf))
+ if err != nil {
+ return err
+ }
+ s := strings.TrimSuffix(string(data), "\n")
+ lines := strings.Split(s, "\n")
+ if len(lines) == 1 && lines[0] == "" {
+ lines = nil
+ }
+ found := false
+ for i := range lines {
+ if strings.HasPrefix(lines[i], "nameserver ") {
+ lines[i] = "nameserver 127.0.0.1"
+ found = true
+ }
+ }
+ if !found {
+ lines = append(lines, "nameserver 127.0.0.1")
+ }
+ data = []byte(strings.Join(lines, "\n"))
+ return c.fileManager.WriteToFile(string(constants.ResolvConf), data)
+}
diff --git a/internal/dns/os_test.go b/internal/dns/os_test.go
new file mode 100644
index 00000000..f6201d1f
--- /dev/null
+++ b/internal/dns/os_test.go
@@ -0,0 +1,72 @@
+package dns
+
+import (
+ "fmt"
+ "testing"
+
+ filesmocks "github.com/qdm12/golibs/files/mocks"
+ loggingmocks "github.com/qdm12/golibs/logging/mocks"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_SetLocalNameserver(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ data []byte
+ writtenData []byte
+ readErr error
+ writeErr error
+ err error
+ }{
+ "no data": {
+ writtenData: []byte("nameserver 127.0.0.1"),
+ },
+ "read error": {
+ readErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ "write error": {
+ writtenData: []byte("nameserver 127.0.0.1"),
+ writeErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ "lines without nameserver": {
+ data: []byte("abc\ndef\n"),
+ writtenData: []byte("abc\ndef\nnameserver 127.0.0.1"),
+ },
+ "lines with nameserver": {
+ data: []byte("abc\nnameserver abc def\ndef\n"),
+ writtenData: []byte("abc\nnameserver 127.0.0.1\ndef"),
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ fileManager := &filesmocks.FileManager{}
+ fileManager.On("ReadFile", string(constants.ResolvConf)).
+ Return(tc.data, tc.readErr).Once()
+ if tc.readErr == nil {
+ fileManager.On("WriteToFile", string(constants.ResolvConf), tc.writtenData).
+ Return(tc.writeErr).Once()
+ }
+ logger := &loggingmocks.Logger{}
+ logger.On("Info", "%s: setting local nameserver to 127.0.0.1", logPrefix).Once()
+ c := &configurator{
+ fileManager: fileManager,
+ logger: logger,
+ }
+ err := c.SetLocalNameserver()
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ fileManager.AssertExpectations(t)
+ logger.AssertExpectations(t)
+ })
+ }
+}
diff --git a/internal/dns/roots.go b/internal/dns/roots.go
new file mode 100644
index 00000000..ad15a996
--- /dev/null
+++ b/internal/dns/roots.go
@@ -0,0 +1,38 @@
+package dns
+
+import (
+ "fmt"
+
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) DownloadRootHints(uid, gid int) error {
+ c.logger.Info("%s: downloading root hints from %s", logPrefix, constants.NamedRootURL)
+ content, status, err := c.client.GetContent(string(constants.NamedRootURL))
+ if err != nil {
+ return err
+ } else if status != 200 {
+ return fmt.Errorf("HTTP status code is %d for %s", status, constants.NamedRootURL)
+ }
+ return c.fileManager.WriteToFile(
+ string(constants.RootHints),
+ content,
+ files.FileOwnership(uid, gid),
+ files.FilePermissions(0400))
+}
+
+func (c *configurator) DownloadRootKey(uid, gid int) error {
+ c.logger.Info("%s: downloading root key from %s", logPrefix, constants.RootKeyURL)
+ content, status, err := c.client.GetContent(string(constants.RootKeyURL))
+ if err != nil {
+ return err
+ } else if status != 200 {
+ return fmt.Errorf("HTTP status code is %d for %s", status, constants.RootKeyURL)
+ }
+ return c.fileManager.WriteToFile(
+ string(constants.RootKey),
+ content,
+ files.FileOwnership(uid, gid),
+ files.FilePermissions(0400))
+}
diff --git a/internal/dns/roots_test.go b/internal/dns/roots_test.go
new file mode 100644
index 00000000..5a84a7d3
--- /dev/null
+++ b/internal/dns/roots_test.go
@@ -0,0 +1,144 @@
+package dns
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ filesMocks "github.com/qdm12/golibs/files/mocks"
+ loggingMocks "github.com/qdm12/golibs/logging/mocks"
+ networkMocks "github.com/qdm12/golibs/network/mocks"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func Test_DownloadRootHints(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ content []byte
+ status int
+ clientErr error
+ writeErr error
+ err error
+ }{
+ "no data": {
+ status: http.StatusOK,
+ },
+ "bad status": {
+ status: http.StatusBadRequest,
+ err: fmt.Errorf("HTTP status code is 400 for https://raw.githubusercontent.com/qdm12/files/master/named.root.updated"),
+ },
+ "client error": {
+ clientErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ "write error": {
+ status: http.StatusOK,
+ writeErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ "data": {
+ content: []byte("content"),
+ status: http.StatusOK,
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ logger := &loggingMocks.Logger{}
+ logger.On("Info", "%s: downloading root hints from %s", logPrefix, constants.NamedRootURL).Once()
+ client := &networkMocks.Client{}
+ client.On("GetContent", string(constants.NamedRootURL)).
+ Return(tc.content, tc.status, tc.clientErr).Once()
+ fileManager := &filesMocks.FileManager{}
+ if tc.clientErr == nil && tc.status == http.StatusOK {
+ fileManager.On(
+ "WriteToFile",
+ string(constants.RootHints),
+ tc.content,
+ mock.AnythingOfType("files.WriteOptionSetter"),
+ mock.AnythingOfType("files.WriteOptionSetter")).
+ Return(tc.writeErr).Once()
+ }
+ c := &configurator{logger: logger, client: client, fileManager: fileManager}
+ err := c.DownloadRootHints(1000, 1000)
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ logger.AssertExpectations(t)
+ client.AssertExpectations(t)
+ fileManager.AssertExpectations(t)
+ })
+ }
+}
+
+func Test_DownloadRootKey(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ content []byte
+ status int
+ clientErr error
+ writeErr error
+ err error
+ }{
+ "no data": {
+ status: http.StatusOK,
+ },
+ "bad status": {
+ status: http.StatusBadRequest,
+ err: fmt.Errorf("HTTP status code is 400 for https://raw.githubusercontent.com/qdm12/files/master/root.key.updated"),
+ },
+ "client error": {
+ clientErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ "write error": {
+ status: http.StatusOK,
+ writeErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ "data": {
+ content: []byte("content"),
+ status: http.StatusOK,
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ logger := &loggingMocks.Logger{}
+ logger.On("Info", "%s: downloading root key from %s", logPrefix, constants.RootKeyURL).Once()
+ client := &networkMocks.Client{}
+ client.On("GetContent", string(constants.RootKeyURL)).
+ Return(tc.content, tc.status, tc.clientErr).Once()
+ fileManager := &filesMocks.FileManager{}
+ if tc.clientErr == nil && tc.status == http.StatusOK {
+ fileManager.On(
+ "WriteToFile",
+ string(constants.RootKey),
+ tc.content,
+ mock.AnythingOfType("files.WriteOptionSetter"),
+ mock.AnythingOfType("files.WriteOptionSetter"),
+ ).Return(tc.writeErr).Once()
+ }
+ c := &configurator{logger: logger, client: client, fileManager: fileManager}
+ err := c.DownloadRootKey(1000, 1001)
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ logger.AssertExpectations(t)
+ client.AssertExpectations(t)
+ fileManager.AssertExpectations(t)
+ })
+ }
+}
diff --git a/internal/env/env.go b/internal/env/env.go
new file mode 100644
index 00000000..46643388
--- /dev/null
+++ b/internal/env/env.go
@@ -0,0 +1,40 @@
+package env
+
+import (
+ "os"
+
+ "github.com/qdm12/golibs/logging"
+)
+
+type Env interface {
+ FatalOnError(err error)
+ PrintVersion(program string, commandFn func() (string, error))
+}
+
+type env struct {
+ logger logging.Logger
+ osExit func(n int)
+}
+
+func New(logger logging.Logger) Env {
+ return &env{
+ logger: logger,
+ osExit: os.Exit,
+ }
+}
+
+func (e *env) FatalOnError(err error) {
+ if err != nil {
+ e.logger.Error(err)
+ e.osExit(1)
+ }
+}
+
+func (e *env) PrintVersion(program string, commandFn func() (string, error)) {
+ version, err := commandFn()
+ if err != nil {
+ e.logger.Error(err)
+ } else {
+ e.logger.Info("%s version: %s", program, version)
+ }
+}
diff --git a/internal/env/env_test.go b/internal/env/env_test.go
new file mode 100644
index 00000000..5a585f02
--- /dev/null
+++ b/internal/env/env_test.go
@@ -0,0 +1,90 @@
+package env
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/qdm12/golibs/logging/mocks"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func Test_FatalOnError(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ err error
+ }{
+ "nil": {},
+ "err": {fmt.Errorf("error")},
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ var logged string
+ var exitCode int
+ logger := &mocks.Logger{}
+ if tc.err != nil {
+ logger.On("Error", tc.err).
+ Run(func(args mock.Arguments) {
+ err := args.Get(0).(error)
+ logged = err.Error()
+ }).Once()
+ }
+ osExit := func(n int) { exitCode = n }
+ e := &env{logger, osExit}
+ e.FatalOnError(tc.err)
+ if tc.err != nil {
+ assert.Equal(t, logged, tc.err.Error())
+ assert.Equal(t, exitCode, 1)
+ } else {
+ assert.Empty(t, logged)
+ assert.Zero(t, exitCode)
+ }
+ })
+ }
+}
+
+func Test_PrintVersion(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ program string
+ commandVersion string
+ commandErr error
+ }{
+ "no data": {},
+ "data": {"binu", "2.3-5", nil},
+ "error": {"binu", "", fmt.Errorf("error")},
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ var logged string
+ logger := &mocks.Logger{}
+ if tc.commandErr != nil {
+ logger.On("Error", tc.commandErr).
+ Run(func(args mock.Arguments) {
+ err := args.Get(0).(error)
+ logged = err.Error()
+ }).Once()
+ } else {
+ logger.On("Info", "%s version: %s", tc.program, tc.commandVersion).
+ Run(func(args mock.Arguments) {
+ format := args.Get(0).(string)
+ program := args.Get(1).(string)
+ version := args.Get(2).(string)
+ logged = fmt.Sprintf(format, program, version)
+ }).Once()
+ }
+ e := &env{logger: logger}
+ commandFn := func() (string, error) { return tc.commandVersion, tc.commandErr }
+ e.PrintVersion(tc.program, commandFn)
+ if tc.commandErr != nil {
+ assert.Equal(t, logged, tc.commandErr.Error())
+ } else {
+ assert.Equal(t, logged, fmt.Sprintf("%s version: %s", tc.program, tc.commandVersion))
+ }
+ })
+ }
+}
diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go
new file mode 100644
index 00000000..2c191036
--- /dev/null
+++ b/internal/firewall/firewall.go
@@ -0,0 +1,42 @@
+package firewall
+
+import (
+ "net"
+
+ "github.com/qdm12/golibs/command"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const logPrefix = "firewall configurator"
+
+// Configurator allows to change firewall rules and modify network routes
+type Configurator interface {
+ Version() (string, error)
+ AcceptAll() error
+ Clear() error
+ BlockAll() error
+ CreateGeneralRules() error
+ CreateVPNRules(dev models.VPNDevice, serverIPs []net.IP, defaultInterface string,
+ port uint16, protocol models.NetworkProtocol) error
+ CreateLocalSubnetsRules(subnet net.IPNet, extraSubnets []net.IPNet, defaultInterface string) error
+ AddRoutesVia(subnets []net.IPNet, defaultGateway net.IP, defaultInterface string) error
+ GetDefaultRoute() (defaultInterface string, defaultGateway net.IP, defaultSubnet net.IPNet, err error)
+ AllowInputTrafficOnPort(device models.VPNDevice, port uint16) error
+}
+
+type configurator struct {
+ commander command.Commander
+ logger logging.Logger
+ fileManager files.FileManager
+}
+
+// NewConfigurator creates a new Configurator instance
+func NewConfigurator(logger logging.Logger, fileManager files.FileManager) Configurator {
+ return &configurator{
+ commander: command.NewCommander(),
+ logger: logger,
+ fileManager: fileManager,
+ }
+}
diff --git a/internal/firewall/iptables.go b/internal/firewall/iptables.go
new file mode 100644
index 00000000..7aa4d32a
--- /dev/null
+++ b/internal/firewall/iptables.go
@@ -0,0 +1,130 @@
+package firewall
+
+import (
+ "fmt"
+ "net"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+// Version obtains the version of the installed iptables
+func (c *configurator) Version() (string, error) {
+ output, err := c.commander.Run("iptables", "--version")
+ if err != nil {
+ return "", err
+ }
+ words := strings.Fields(output)
+ if len(words) < 2 {
+ return "", fmt.Errorf("iptables --version: output is too short: %q", output)
+ }
+ return words[1], nil
+}
+
+func (c *configurator) runIptablesInstructions(instructions []string) error {
+ for _, instruction := range instructions {
+ if err := c.runIptablesInstruction(instruction); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *configurator) runIptablesInstruction(instruction string) error {
+ flags := strings.Fields(instruction)
+ if output, err := c.commander.Run("iptables", flags...); err != nil {
+ return fmt.Errorf("failed executing %q: %s: %w", instruction, output, err)
+ }
+ return nil
+}
+
+func (c *configurator) Clear() error {
+ c.logger.Info("%s: clearing all rules", logPrefix)
+ return c.runIptablesInstructions([]string{
+ "--flush",
+ "--delete-chain",
+ "-t nat --flush",
+ "-t nat --delete-chain",
+ })
+}
+
+func (c *configurator) AcceptAll() error {
+ c.logger.Info("%s: accepting all traffic", logPrefix)
+ return c.runIptablesInstructions([]string{
+ "-P INPUT ACCEPT",
+ "-P OUTPUT ACCEPT",
+ "-P FORWARD ACCEPT",
+ })
+}
+
+func (c *configurator) BlockAll() error {
+ c.logger.Info("%s: blocking all traffic", logPrefix)
+ return c.runIptablesInstructions([]string{
+ "-P INPUT DROP",
+ "-F OUTPUT",
+ "-P OUTPUT DROP",
+ "-P FORWARD DROP",
+ })
+}
+
+func (c *configurator) CreateGeneralRules() error {
+ c.logger.Info("%s: creating general rules", logPrefix)
+ return c.runIptablesInstructions([]string{
+ "-A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT",
+ "-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT",
+ "-A OUTPUT -o lo -j ACCEPT",
+ "-A INPUT -i lo -j ACCEPT",
+ })
+}
+
+func (c *configurator) CreateVPNRules(dev models.VPNDevice, serverIPs []net.IP,
+ defaultInterface string, port uint16, protocol models.NetworkProtocol) error {
+ for _, serverIP := range serverIPs {
+ c.logger.Info("%s: allowing output traffic to VPN server %s through %s on port %s %d",
+ logPrefix, serverIP, defaultInterface, protocol, port)
+ if err := c.runIptablesInstruction(
+ fmt.Sprintf("-A OUTPUT -d %s -o %s -p %s -m %s --dport %d -j ACCEPT",
+ serverIP, defaultInterface, protocol, protocol, port)); err != nil {
+ return err
+ }
+ }
+ if err := c.runIptablesInstruction(fmt.Sprintf("-A OUTPUT -o %s -j ACCEPT", dev)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *configurator) CreateLocalSubnetsRules(subnet net.IPNet, extraSubnets []net.IPNet, defaultInterface string) error {
+ subnetStr := subnet.String()
+ c.logger.Info("%s: accepting input and output traffic for %s", logPrefix, subnetStr)
+ if err := c.runIptablesInstructions([]string{
+ fmt.Sprintf("-A INPUT -s %s -d %s -j ACCEPT", subnetStr, subnetStr),
+ fmt.Sprintf("-A OUTPUT -s %s -d %s -j ACCEPT", subnetStr, subnetStr),
+ }); err != nil {
+ return err
+ }
+ for _, extraSubnet := range extraSubnets {
+ extraSubnetStr := extraSubnet.String()
+ c.logger.Info("%s: accepting input traffic through %s from %s to %s", logPrefix, defaultInterface, extraSubnetStr, subnetStr)
+ if err := c.runIptablesInstruction(
+ fmt.Sprintf("-A INPUT -i %s -s %s -d %s -j ACCEPT", defaultInterface, extraSubnetStr, subnetStr)); err != nil {
+ return err
+ }
+ // Thanks to @npawelek
+ c.logger.Info("%s: accepting output traffic through %s from %s to %s", logPrefix, defaultInterface, subnetStr, extraSubnetStr)
+ if err := c.runIptablesInstruction(
+ fmt.Sprintf("-A OUTPUT -o %s -s %s -d %s -j ACCEPT", defaultInterface, subnetStr, extraSubnetStr)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Used for port forwarding
+func (c *configurator) AllowInputTrafficOnPort(device models.VPNDevice, port uint16) error {
+ c.logger.Info("%s: accepting input traffic through %s on port %d", logPrefix, device, port)
+ return c.runIptablesInstructions([]string{
+ fmt.Sprintf("-A INPUT -i %s -p tcp --dport %d -j ACCEPT", device, port),
+ fmt.Sprintf("-A INPUT -i %s -p udp --dport %d -j ACCEPT", device, port),
+ })
+}
diff --git a/internal/firewall/route.go b/internal/firewall/route.go
new file mode 100644
index 00000000..28b87b8f
--- /dev/null
+++ b/internal/firewall/route.go
@@ -0,0 +1,88 @@
+package firewall
+
+import (
+ "encoding/hex"
+ "net"
+
+ "fmt"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) AddRoutesVia(subnets []net.IPNet, defaultGateway net.IP, defaultInterface string) error {
+ for _, subnet := range subnets {
+ subnetStr := subnet.String()
+ output, err := c.commander.Run("ip", "route", "show", subnetStr)
+ if err != nil {
+ return fmt.Errorf("cannot read route %s: %s: %w", subnetStr, output, err)
+ } else if len(output) > 0 { // thanks to @npawelek https://github.com/npawelek
+ continue // already exists
+ // TODO remove it instead and continue execution below
+ }
+ c.logger.Info("%s: adding %s as route via %s", logPrefix, subnetStr, defaultInterface)
+ output, err = c.commander.Run("ip", "route", "add", subnetStr, "via", defaultGateway.String(), "dev", defaultInterface)
+ if err != nil {
+ return fmt.Errorf("cannot add route for %s via %s %s %s: %s: %w", subnetStr, defaultGateway.String(), "dev", defaultInterface, output, err)
+ }
+ }
+ return nil
+}
+
+func (c *configurator) GetDefaultRoute() (defaultInterface string, defaultGateway net.IP, defaultSubnet net.IPNet, err error) {
+ c.logger.Info("%s: detecting default network route", logPrefix)
+ data, err := c.fileManager.ReadFile(string(constants.NetRoute))
+ if err != nil {
+ return "", nil, defaultSubnet, err
+ }
+ // Verify number of lines and fields
+ lines := strings.Split(string(data), "\n")
+ if len(lines) < 3 {
+ return "", nil, defaultSubnet, fmt.Errorf("not enough lines (%d) found in %s", len(lines), constants.NetRoute)
+ }
+ fieldsLine1 := strings.Fields(lines[1])
+ if len(fieldsLine1) < 3 {
+ return "", nil, defaultSubnet, fmt.Errorf("not enough fields in %q", lines[1])
+ }
+ fieldsLine2 := strings.Fields(lines[2])
+ if len(fieldsLine2) < 8 {
+ return "", nil, defaultSubnet, fmt.Errorf("not enough fields in %q", lines[2])
+ }
+ // get information
+ defaultInterface = fieldsLine1[0]
+ defaultGateway, err = reversedHexToIPv4(fieldsLine1[2])
+ if err != nil {
+ return "", nil, defaultSubnet, err
+ }
+ netNumber, err := reversedHexToIPv4(fieldsLine2[1])
+ if err != nil {
+ return "", nil, defaultSubnet, err
+ }
+ netMask, err := hexToIPv4Mask(fieldsLine2[7])
+ if err != nil {
+ return "", nil, defaultSubnet, err
+ }
+ subnet := net.IPNet{IP: netNumber, Mask: netMask}
+ c.logger.Info("%s: default route found: interface %s, gateway %s, subnet %s", logPrefix, defaultInterface, defaultGateway.String(), subnet.String())
+ return defaultInterface, defaultGateway, subnet, nil
+}
+
+func reversedHexToIPv4(reversedHex string) (IP net.IP, err error) {
+ bytes, err := hex.DecodeString(reversedHex)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse reversed IP hex %q: %s", reversedHex, err)
+ } else if len(bytes) != 4 {
+ return nil, fmt.Errorf("hex string contains %d bytes instead of 4", len(bytes))
+ }
+ return []byte{bytes[3], bytes[2], bytes[1], bytes[0]}, nil
+}
+
+func hexToIPv4Mask(hexString string) (mask net.IPMask, err error) {
+ bytes, err := hex.DecodeString(hexString)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse hex mask %q: %s", hexString, err)
+ } else if len(bytes) != 4 {
+ return nil, fmt.Errorf("hex string contains %d bytes instead of 4", len(bytes))
+ }
+ return []byte{bytes[3], bytes[2], bytes[1], bytes[0]}, nil
+}
diff --git a/internal/firewall/route_test.go b/internal/firewall/route_test.go
new file mode 100644
index 00000000..e1f2292f
--- /dev/null
+++ b/internal/firewall/route_test.go
@@ -0,0 +1,171 @@
+package firewall
+
+import (
+ "fmt"
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ filesmocks "github.com/qdm12/golibs/files/mocks"
+ loggingmocks "github.com/qdm12/golibs/logging/mocks"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func Test_getDefaultRoute(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ data []byte
+ readErr error
+ defaultInterface string
+ defaultGateway net.IP
+ defaultSubnet net.IPNet
+ err error
+ }{
+ "no data": {
+ err: fmt.Errorf("not enough lines (1) found in %s", constants.NetRoute)},
+ "read error": {
+ readErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error")},
+ "not enough fields line 1": {
+ data: []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
+eth0 00000000
+eth0 000011AC 00000000 0001 0 0 0 0000FFFF 0 0 0`),
+ err: fmt.Errorf("not enough fields in \"eth0 00000000\"")},
+ "not enough fields line 2": {
+ data: []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
+eth0 00000000 010011AC 0003 0 0 0 00000000 0 0 0
+eth0 000011AC 00000000 0001 0 0 0`),
+ err: fmt.Errorf("not enough fields in \"eth0 000011AC 00000000 0001 0 0 0\"")},
+ "bad gateway": {
+ data: []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
+eth0 00000000 x 0003 0 0 0 00000000 0 0 0
+eth0 000011AC 00000000 0001 0 0 0 0000FFFF 0 0 0`),
+ err: fmt.Errorf("cannot parse reversed IP hex \"x\": encoding/hex: invalid byte: U+0078 'x'")},
+ "bad net number": {
+ data: []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
+eth0 00000000 010011AC 0003 0 0 0 00000000 0 0 0
+eth0 x 00000000 0001 0 0 0 0000FFFF 0 0 0`),
+ err: fmt.Errorf("cannot parse reversed IP hex \"x\": encoding/hex: invalid byte: U+0078 'x'")},
+ "bad net mask": {
+ data: []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
+eth0 00000000 010011AC 0003 0 0 0 00000000 0 0 0
+eth0 000011AC 00000000 0001 0 0 0 x 0 0 0`),
+ err: fmt.Errorf("cannot parse hex mask \"x\": encoding/hex: invalid byte: U+0078 'x'")},
+ "success": {
+ data: []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
+eth0 00000000 010011AC 0003 0 0 0 00000000 0 0 0
+eth0 000011AC 00000000 0001 0 0 0 0000FFFF 0 0 0`),
+ defaultInterface: "eth0",
+ defaultGateway: net.IP{0xac, 0x11, 0x0, 0x1},
+ defaultSubnet: net.IPNet{
+ IP: net.IP{0xac, 0x11, 0x0, 0x0},
+ Mask: net.IPMask{0xff, 0xff, 0x0, 0x0},
+ }},
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ fileManager := &filesmocks.FileManager{}
+ fileManager.On("ReadFile", string(constants.NetRoute)).
+ Return(tc.data, tc.readErr).Once()
+ logger := &loggingmocks.Logger{}
+ logger.On("Info", "%s: detecting default network route", logPrefix).Once()
+ if tc.err == nil {
+ logger.On("Info", "%s: default route found: interface %s, gateway %s, subnet %s",
+ logPrefix, tc.defaultInterface, tc.defaultGateway.String(), tc.defaultSubnet.String()).Once()
+ }
+ c := &configurator{logger: logger, fileManager: fileManager}
+ defaultInterface, defaultGateway, defaultSubnet, err := c.GetDefaultRoute()
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, tc.defaultInterface, defaultInterface)
+ assert.Equal(t, tc.defaultGateway, defaultGateway)
+ assert.Equal(t, tc.defaultSubnet, defaultSubnet)
+ fileManager.AssertExpectations(t)
+ logger.AssertExpectations(t)
+ })
+ }
+}
+
+func Test_reversedHexToIPv4(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ reversedHex string
+ IP net.IP
+ err error
+ }{
+ "empty hex": {
+ err: fmt.Errorf("hex string contains 0 bytes instead of 4")},
+ "bad hex": {
+ reversedHex: "x",
+ err: fmt.Errorf("cannot parse reversed IP hex \"x\": encoding/hex: invalid byte: U+0078 'x'")},
+ "3 bytes hex": {
+ reversedHex: "9abcde",
+ err: fmt.Errorf("hex string contains 3 bytes instead of 4")},
+ "correct hex": {
+ reversedHex: "010011AC",
+ IP: []byte{0xac, 0x11, 0x0, 0x1},
+ err: nil},
+ "correct hex 2": {
+ reversedHex: "000011AC",
+ IP: []byte{0xac, 0x11, 0x0, 0x0},
+ err: nil},
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ IP, err := reversedHexToIPv4(tc.reversedHex)
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, tc.IP, IP)
+ })
+ }
+}
+
+func Test_hexMaskToDecMask(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ hexString string
+ mask net.IPMask
+ err error
+ }{
+ "empty hex": {
+ err: fmt.Errorf("hex string contains 0 bytes instead of 4")},
+ "bad hex": {
+ hexString: "x",
+ err: fmt.Errorf("cannot parse hex mask \"x\": encoding/hex: invalid byte: U+0078 'x'")},
+ "3 bytes hex": {
+ hexString: "9abcde",
+ err: fmt.Errorf("hex string contains 3 bytes instead of 4")},
+ "16": {
+ hexString: "0000FFFF",
+ mask: []byte{0xff, 0xff, 0x0, 0x0},
+ err: nil},
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ mask, err := hexToIPv4Mask(tc.hexString)
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, tc.mask, mask)
+ })
+ }
+}
diff --git a/internal/models/alias.go b/internal/models/alias.go
new file mode 100644
index 00000000..e6c9df33
--- /dev/null
+++ b/internal/models/alias.go
@@ -0,0 +1,24 @@
+package models
+
+type (
+ // VPNDevice is the device name used to tunnel using Openvpn
+ VPNDevice string
+ // DNSProvider is a DNS over TLS server provider name
+ DNSProvider string
+ // DNSForwardAddress is the Unbound formatted forward address
+ DNSForwardAddress string
+ // PIAEncryption defines the level of encryption for communication with PIA servers
+ PIAEncryption string
+ // PIARegion contains the list of regions available for PIA
+ PIARegion string
+ // URL is an HTTP(s) URL address
+ URL string
+ // Filepath is a local filesytem file path
+ Filepath string
+ // TinyProxyLogLevel is the log level for TinyProxy
+ TinyProxyLogLevel string
+ // VPNProvider is the name of the VPN provider to be used
+ VPNProvider string
+ // NetworkProtocol contains the network protocol to be used to communicate with the VPN servers
+ NetworkProtocol string
+)
diff --git a/internal/openvpn/auth.go b/internal/openvpn/auth.go
new file mode 100644
index 00000000..0e022f2d
--- /dev/null
+++ b/internal/openvpn/auth.go
@@ -0,0 +1,23 @@
+package openvpn
+
+import (
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+// WriteAuthFile writes the OpenVPN auth file to disk with the right permissions
+func (c *configurator) WriteAuthFile(user, password string, uid, gid int) error {
+ authExists, err := c.fileManager.FileExists(string(constants.OpenVPNAuthConf))
+ if err != nil {
+ return err
+ } else if authExists { // in case of container stop/start
+ c.logger.Info("%s: %s already exists", logPrefix, constants.OpenVPNAuthConf)
+ return nil
+ }
+ c.logger.Info("%s: writing auth file %s", logPrefix, constants.OpenVPNAuthConf)
+ return c.fileManager.WriteLinesToFile(
+ string(constants.OpenVPNAuthConf),
+ []string{user, password},
+ files.FileOwnership(uid, gid),
+ files.FilePermissions(0400))
+}
diff --git a/internal/openvpn/command.go b/internal/openvpn/command.go
new file mode 100644
index 00000000..947c86c3
--- /dev/null
+++ b/internal/openvpn/command.go
@@ -0,0 +1,28 @@
+package openvpn
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) Start() (stdout io.ReadCloser, err error) {
+ c.logger.Info("%s: starting openvpn", logPrefix)
+ stdout, _, _, err = c.commander.Start("openvpn", "--config", string(constants.OpenVPNConf))
+ return stdout, err
+}
+
+func (c *configurator) Version() (string, error) {
+ output, err := c.commander.Run("openvpn", "--version")
+ if err != nil && err.Error() != "exit status 1" {
+ return "", err
+ }
+ firstLine := strings.Split(output, "\n")[0]
+ words := strings.Fields(firstLine)
+ if len(words) < 2 {
+ return "", fmt.Errorf("openvpn --version: first line is too short: %q", firstLine)
+ }
+ return words[1], nil
+}
diff --git a/internal/openvpn/openvpn.go b/internal/openvpn/openvpn.go
new file mode 100644
index 00000000..c7bfa141
--- /dev/null
+++ b/internal/openvpn/openvpn.go
@@ -0,0 +1,35 @@
+package openvpn
+
+import (
+ "io"
+ "os"
+
+ "github.com/qdm12/golibs/command"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+)
+
+const logPrefix = "openvpn configurator"
+
+type Configurator interface {
+ Version() (string, error)
+ WriteAuthFile(user, password string, uid, gid int) error
+ CheckTUN() error
+ Start() (stdout io.ReadCloser, err error)
+}
+
+type configurator struct {
+ fileManager files.FileManager
+ logger logging.Logger
+ commander command.Commander
+ openFile func(name string, flag int, perm os.FileMode) (*os.File, error)
+}
+
+func NewConfigurator(logger logging.Logger, fileManager files.FileManager) Configurator {
+ return &configurator{
+ fileManager: fileManager,
+ logger: logger,
+ commander: command.NewCommander(),
+ openFile: os.OpenFile,
+ }
+}
diff --git a/internal/openvpn/tun.go b/internal/openvpn/tun.go
new file mode 100644
index 00000000..5ccf7d39
--- /dev/null
+++ b/internal/openvpn/tun.go
@@ -0,0 +1,21 @@
+package openvpn
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+// CheckTUN checks the tunnel device is present and accessible
+func (c *configurator) CheckTUN() error {
+ c.logger.Info("%s: checking for device %s", logPrefix, constants.TunnelDevice)
+ f, err := c.openFile(string(constants.TunnelDevice), os.O_RDWR, 0)
+ if err != nil {
+ return fmt.Errorf("TUN device is not available: %w", err)
+ }
+ if err := f.Close(); err != nil {
+ c.logger.Warn("Could not close TUN device file: %s", err)
+ }
+ return nil
+}
diff --git a/internal/params/dns.go b/internal/params/dns.go
new file mode 100644
index 00000000..1a60e622
--- /dev/null
+++ b/internal/params/dns.go
@@ -0,0 +1,93 @@
+package params
+
+import (
+ "fmt"
+ "strings"
+
+ libparams "github.com/qdm12/golibs/params"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+// GetDNSOverTLS obtains if the DNS over TLS should be enabled
+// from the environment variable DOT
+func (p *paramsReader) GetDNSOverTLS() (DNSOverTLS bool, err error) {
+ return p.envParams.GetOnOff("DOT", libparams.Default("on"))
+}
+
+// GetDNSOverTLSProviders obtains the DNS over TLS providers to use
+// from the environment variable DOT_PROVIDERS
+func (p *paramsReader) GetDNSOverTLSProviders() (providers []models.DNSProvider, err error) {
+ s, err := p.envParams.GetEnv("DOT_PROVIDERS", libparams.Default("cloudflare"))
+ if err != nil {
+ return nil, err
+ }
+ for _, word := range strings.Split(s, ",") {
+ provider := models.DNSProvider(word)
+ switch provider {
+ case constants.Cloudflare, constants.Google, constants.Quad9, constants.Quadrant, constants.CleanBrowsing, constants.SecureDNS, constants.LibreDNS:
+ providers = append(providers, provider)
+ default:
+ return nil, fmt.Errorf("DNS over TLS provider %q is not valid", provider)
+ }
+ }
+ return providers, nil
+}
+
+// GetDNSOverTLSVerbosity obtains the verbosity level to use for Unbound
+// from the environment variable DOT_VERBOSITY
+func (p *paramsReader) GetDNSOverTLSVerbosity() (verbosityLevel uint8, err error) {
+ n, err := p.envParams.GetEnvIntRange("DOT_VERBOSITY", 0, 5, libparams.Default("1"))
+ return uint8(n), err
+}
+
+// GetDNSOverTLSVerbosityDetails obtains the log level to use for Unbound
+// from the environment variable DOT_VERBOSITY_DETAILS
+func (p *paramsReader) GetDNSOverTLSVerbosityDetails() (verbosityDetailsLevel uint8, err error) {
+ n, err := p.envParams.GetEnvIntRange("DOT_VERBOSITY_DETAILS", 0, 4, libparams.Default("0"))
+ return uint8(n), err
+}
+
+// GetDNSOverTLSValidationLogLevel obtains the log level to use for Unbound DOT validation
+// from the environment variable DOT_VALIDATION_LOGLEVEL
+func (p *paramsReader) GetDNSOverTLSValidationLogLevel() (validationLogLevel uint8, err error) {
+ n, err := p.envParams.GetEnvIntRange("DOT_VALIDATION_LOGLEVEL", 0, 2, libparams.Default("0"))
+ return uint8(n), err
+}
+
+// GetDNSMaliciousBlocking obtains if malicious hostnames/IPs should be blocked
+// from being resolved by Unbound, using the environment variable BLOCK_MALICIOUS
+func (p *paramsReader) GetDNSMaliciousBlocking() (blocking bool, err error) {
+ return p.envParams.GetOnOff("BLOCK_MALICIOUS", libparams.Default("on"))
+}
+
+// GetDNSSurveillanceBlocking obtains if surveillance hostnames/IPs should be blocked
+// from being resolved by Unbound, using the environment variable BLOCK_NSA
+func (p *paramsReader) GetDNSSurveillanceBlocking() (blocking bool, err error) {
+ return p.envParams.GetOnOff("BLOCK_NSA", libparams.Default("off"))
+}
+
+// GetDNSAdsBlocking obtains if ads hostnames/IPs should be blocked
+// from being resolved by Unbound, using the environment variable BLOCK_ADS
+func (p *paramsReader) GetDNSAdsBlocking() (blocking bool, err error) {
+ return p.envParams.GetOnOff("BLOCK_ADS", libparams.Default("off"))
+}
+
+// GetDNSUnblockedHostnames obtains a list of hostnames to unblock from block lists
+// from the comma separated list for the environment variable UNBLOCK
+func (p *paramsReader) GetDNSUnblockedHostnames() (hostnames []string, err error) {
+ s, err := p.envParams.GetEnv("UNBLOCK")
+ if err != nil {
+ return nil, err
+ }
+ if len(s) == 0 {
+ return nil, nil
+ }
+ hostnames = strings.Split(s, ",")
+ for _, hostname := range hostnames {
+ if !p.verifier.MatchHostname(hostname) {
+ return nil, fmt.Errorf("hostname %q does not seem valid", hostname)
+ }
+ }
+ return hostnames, nil
+}
diff --git a/internal/params/firewall.go b/internal/params/firewall.go
new file mode 100644
index 00000000..2618ea89
--- /dev/null
+++ b/internal/params/firewall.go
@@ -0,0 +1,29 @@
+package params
+
+import (
+ "fmt"
+ "net"
+ "strings"
+)
+
+// GetExtraSubnets obtains the CIDR subnets from the comma separated list of the
+// environment variable EXTRA_SUBNETS
+func (p *paramsReader) GetExtraSubnets() (extraSubnets []net.IPNet, err error) {
+ s, err := p.envParams.GetEnv("EXTRA_SUBNETS")
+ if err != nil {
+ return nil, err
+ } else if s == "" {
+ return nil, nil
+ }
+ subnets := strings.Split(s, ",")
+ for _, subnet := range subnets {
+ _, cidr, err := net.ParseCIDR(subnet)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse subnet %q from environment variable with key EXTRA_SUBNETS: %w", subnet, err)
+ } else if cidr == nil {
+ return nil, fmt.Errorf("parsing subnet %q resulted in a nil CIDR", subnet)
+ }
+ extraSubnets = append(extraSubnets, *cidr)
+ }
+ return extraSubnets, nil
+}
diff --git a/internal/params/openvpn.go b/internal/params/openvpn.go
new file mode 100644
index 00000000..4d589a36
--- /dev/null
+++ b/internal/params/openvpn.go
@@ -0,0 +1,13 @@
+package params
+
+import (
+ libparams "github.com/qdm12/golibs/params"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+// GetNetworkProtocol obtains the network protocol to use to connect to the
+// VPN servers from the environment variable PROTOCOL
+func (p *paramsReader) GetNetworkProtocol() (protocol models.NetworkProtocol, err error) {
+ s, err := p.envParams.GetValueIfInside("PROTOCOL", []string{"tcp", "udp"}, libparams.Default("udp"))
+ return models.NetworkProtocol(s), err
+}
diff --git a/internal/params/params.go b/internal/params/params.go
new file mode 100644
index 00000000..f493c351
--- /dev/null
+++ b/internal/params/params.go
@@ -0,0 +1,73 @@
+package params
+
+import (
+ "net"
+ "os"
+
+ "github.com/qdm12/golibs/logging"
+ libparams "github.com/qdm12/golibs/params"
+ "github.com/qdm12/golibs/verification"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+// ParamsReader contains methods to obtain parameters
+type ParamsReader interface {
+ // DNS over TLS getters
+ GetDNSOverTLS() (DNSOverTLS bool, err error)
+ GetDNSOverTLSProviders() (providers []models.DNSProvider, err error)
+ GetDNSOverTLSVerbosity() (verbosityLevel uint8, err error)
+ GetDNSOverTLSVerbosityDetails() (verbosityDetailsLevel uint8, err error)
+ GetDNSOverTLSValidationLogLevel() (validationLogLevel uint8, err error)
+ GetDNSMaliciousBlocking() (blocking bool, err error)
+ GetDNSSurveillanceBlocking() (blocking bool, err error)
+ GetDNSAdsBlocking() (blocking bool, err error)
+ GetDNSUnblockedHostnames() (hostnames []string, err error)
+
+ // Firewall getters
+ GetExtraSubnets() (extraSubnets []net.IPNet, err error)
+
+ // VPN getters
+ GetNetworkProtocol() (protocol models.NetworkProtocol, err error)
+
+ // PIA getters
+ GetUser() (s string, err error)
+ GetPassword() (s string, err error)
+ GetPortForwarding() (activated bool, err error)
+ GetPortForwardingStatusFilepath() (filepath models.Filepath, err error)
+ GetPIAEncryption() (models.PIAEncryption, error)
+ GetPIARegion() (models.PIARegion, error)
+
+ // Shadowsocks getters
+ GetShadowSocks() (activated bool, err error)
+ GetShadowSocksLog() (activated bool, err error)
+ GetShadowSocksPort() (port uint16, err error)
+ GetShadowSocksPassword() (password string, err error)
+
+ // Tinyproxy getters
+ GetTinyProxy() (activated bool, err error)
+ GetTinyProxyLog() (models.TinyProxyLogLevel, error)
+ GetTinyProxyPort() (port uint16, err error)
+ GetTinyProxyUser() (user string, err error)
+ GetTinyProxyPassword() (password string, err error)
+
+ // Version getters
+ GetVersion() string
+ GetBuildDate() string
+ GetVcsRef() string
+}
+
+type paramsReader struct {
+ envParams libparams.EnvParams
+ logger logging.Logger
+ verifier verification.Verifier
+ unsetEnv func(key string) error
+}
+
+func NewParamsReader(logger logging.Logger) ParamsReader {
+ return ¶msReader{
+ envParams: libparams.NewEnvParams(),
+ logger: logger,
+ verifier: verification.NewVerifier(),
+ unsetEnv: os.Unsetenv,
+ }
+}
diff --git a/internal/params/pia.go b/internal/params/pia.go
new file mode 100644
index 00000000..c66edff7
--- /dev/null
+++ b/internal/params/pia.go
@@ -0,0 +1,87 @@
+package params
+
+import (
+ "fmt"
+ "math/rand"
+
+ libparams "github.com/qdm12/golibs/params"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+// GetUser obtains the user to use to connect to the VPN servers
+func (p *paramsReader) GetUser() (s string, err error) {
+ defer func() {
+ unsetenvErr := p.unsetEnv("USER")
+ if err == nil {
+ err = unsetenvErr
+ }
+ }()
+ s, err = p.envParams.GetEnv("USER")
+ if err != nil {
+ return "", err
+ } else if len(s) == 0 {
+ return s, fmt.Errorf("USER environment variable cannot be empty")
+ }
+ return s, nil
+}
+
+// GetPassword obtains the password to use to connect to the VPN servers
+func (p *paramsReader) GetPassword() (s string, err error) {
+ defer func() {
+ unsetenvErr := p.unsetEnv("PASSWORD")
+ if err == nil {
+ err = unsetenvErr
+ }
+ }()
+ s, err = p.envParams.GetEnv("PASSWORD")
+ if err != nil {
+ return "", err
+ } else if len(s) == 0 {
+ return s, fmt.Errorf("PASSWORD environment variable cannot be empty")
+ }
+ return s, nil
+}
+
+// GetPortForwarding obtains if port forwarding on the VPN provider server
+// side is enabled or not from the environment variable PORT_FORWARDING
+func (p *paramsReader) GetPortForwarding() (activated bool, err error) {
+ s, err := p.envParams.GetEnv("PORT_FORWARDING", libparams.Default("off"))
+ if err != nil {
+ return false, err
+ }
+ // Custom for retro-compatibility
+ if s == "false" || s == "off" {
+ return false, nil
+ } else if s == "true" || s == "on" {
+ return true, nil
+ }
+ return false, fmt.Errorf("PORT_FORWARDING can only be \"on\" or \"off\"")
+}
+
+// GetPortForwardingStatusFilepath obtains the port forwarding status file path
+// from the environment variable PORT_FORWARDING_STATUS_FILE
+func (p *paramsReader) GetPortForwardingStatusFilepath() (filepath models.Filepath, err error) {
+ filepathStr, err := p.envParams.GetPath("PORT_FORWARDING_STATUS_FILE", libparams.Default("/forwarded_port"))
+ return models.Filepath(filepathStr), err
+}
+
+// GetPIAEncryption obtains the encryption level for the PIA connection
+// from the environment variable ENCRYPTION
+func (p *paramsReader) GetPIAEncryption() (models.PIAEncryption, error) {
+ s, err := p.envParams.GetValueIfInside("ENCRYPTION", []string{"normal", "strong"}, libparams.Default("strong"))
+ return models.PIAEncryption(s), err
+}
+
+// GetPIARegion obtains the region for the PIA server from the
+// environment variable REGION
+func (p *paramsReader) GetPIARegion() (region models.PIARegion, err error) {
+ choices := []string{
+ string(constants.AUMelbourne), string(constants.AUPerth), string(constants.AUSydney), string(constants.Austria), string(constants.Belgium), string(constants.CAMontreal), string(constants.CAToronto), string(constants.CAVancouver), string(constants.CzechRepublic), string(constants.DEBerlin), string(constants.DEFrankfurt), string(constants.Denmark), string(constants.Finland), string(constants.France), string(constants.HongKong), string(constants.Hungary), string(constants.India), string(constants.Ireland), string(constants.Israel), string(constants.Italy), string(constants.Japan), string(constants.Luxembourg), string(constants.Mexico), string(constants.Netherlands), string(constants.NewZealand), string(constants.Norway), string(constants.Poland), string(constants.Romania), string(constants.Singapore), string(constants.Spain), string(constants.Sweden), string(constants.Switzerland), string(constants.UAE), string(constants.UKLondon), string(constants.UKManchester), string(constants.UKSouthampton), string(constants.USAtlanta), string(constants.USCalifornia), string(constants.USChicago), string(constants.USDenver), string(constants.USEast), string(constants.USFlorida), string(constants.USHouston), string(constants.USLasVegas), string(constants.USNewYorkCity), string(constants.USSeattle), string(constants.USSiliconValley), string(constants.USTexas), string(constants.USWashingtonDC), string(constants.USWest),
+ }
+ s, err := p.envParams.GetValueIfInside("REGION", choices)
+ if len(s) == 0 { // Suggestion by @rorph https://github.com/rorph
+ s = choices[rand.Int()%len(choices)]
+ }
+ return models.PIARegion(s), err
+}
diff --git a/internal/params/shadowsocks.go b/internal/params/shadowsocks.go
new file mode 100644
index 00000000..aee2f340
--- /dev/null
+++ b/internal/params/shadowsocks.go
@@ -0,0 +1,40 @@
+package params
+
+import (
+ "strconv"
+
+ libparams "github.com/qdm12/golibs/params"
+)
+
+// GetShadowSocks obtains if ShadowSocks is on from the environment variable
+// SHADOWSOCKS
+func (p *paramsReader) GetShadowSocks() (activated bool, err error) {
+ return p.envParams.GetOnOff("SHADOWSOCKS", libparams.Default("off"))
+}
+
+// GetShadowSocksLog obtains the ShadowSocks log level from the environment variable
+// SHADOWSOCKS_LOG
+func (p *paramsReader) GetShadowSocksLog() (activated bool, err error) {
+ return p.envParams.GetOnOff("SHADOWSOCKS_LOG", libparams.Default("off"))
+}
+
+// GetShadowSocksPort obtains the ShadowSocks listening port from the environment variable
+// SHADOWSOCKS_PORT
+func (p *paramsReader) GetShadowSocksPort() (port uint16, err error) {
+ portStr, err := p.envParams.GetEnv("SHADOWSOCKS_PORT", libparams.Default("8388"))
+ if err != nil {
+ return 0, err
+ }
+ if err := p.verifier.VerifyPort(portStr); err != nil {
+ return 0, err
+ }
+ portUint64, err := strconv.ParseUint(portStr, 10, 16)
+ return uint16(portUint64), err
+}
+
+// GetShadowSocksPassword obtains the ShadowSocks server password from the environment variable
+// SHADOWSOCKS_PASSWORD
+func (p *paramsReader) GetShadowSocksPassword() (password string, err error) {
+ defer p.unsetEnv("SHADOWSOCKS_PASSWORD")
+ return p.envParams.GetEnv("SHADOWSOCKS_PASSWORD")
+}
diff --git a/internal/params/tinyproxy.go b/internal/params/tinyproxy.go
new file mode 100644
index 00000000..2d66b073
--- /dev/null
+++ b/internal/params/tinyproxy.go
@@ -0,0 +1,94 @@
+package params
+
+import (
+ "strconv"
+
+ libparams "github.com/qdm12/golibs/params"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+// GetTinyProxy obtains if TinyProxy is on from the environment variable
+// TINYPROXY, and using PROXY as a retro-compatibility name
+func (p *paramsReader) GetTinyProxy() (activated bool, err error) {
+ // Retro-compatibility
+ s, err := p.envParams.GetEnv("PROXY")
+ if err != nil {
+ return false, err
+ } else if len(s) != 0 {
+ p.logger.Warn("You are using the old environment variable PROXY, please consider changing it to TINYPROXY")
+ return p.envParams.GetOnOff("PROXY", libparams.Compulsory())
+ }
+ return p.envParams.GetOnOff("TINYPROXY", libparams.Default("off"))
+}
+
+// GetTinyProxyLog obtains the TinyProxy log level from the environment variable
+// TINYPROXY_LOG, and using PROXY_LOG_LEVEL as a retro-compatibility name
+func (p *paramsReader) GetTinyProxyLog() (models.TinyProxyLogLevel, error) {
+ // Retro-compatibility
+ s, err := p.envParams.GetEnv("PROXY_LOG_LEVEL")
+ if err != nil {
+ return models.TinyProxyLogLevel(s), err
+ } else if len(s) != 0 {
+ p.logger.Warn("You are using the old environment variable PROXY_LOG_LEVEL, please consider changing it to TINYPROXY_LOG")
+ s, err = p.envParams.GetValueIfInside("PROXY_LOG_LEVEL", []string{"info", "warning", "error", "critical"}, libparams.Compulsory())
+ return models.TinyProxyLogLevel(s), err
+ }
+ s, err = p.envParams.GetValueIfInside("TINYPROXY_LOG", []string{"info", "warning", "error", "critical"}, libparams.Default("info"))
+ return models.TinyProxyLogLevel(s), err
+}
+
+// GetTinyProxyPort obtains the TinyProxy listening port from the environment variable
+// TINYPROXY_PORT, and using PROXY_PORT as a retro-compatibility name
+func (p *paramsReader) GetTinyProxyPort() (port uint16, err error) {
+ // Retro-compatibility
+ portStr, err := p.envParams.GetEnv("PROXY_PORT")
+ if err != nil {
+ return 0, err
+ } else if len(portStr) != 0 {
+ p.logger.Warn("You are using the old environment variable PROXY_PORT, please consider changing it to TINYPROXY_PORT")
+ } else {
+ portStr, err = p.envParams.GetEnv("TINYPROXY_PORT", libparams.Default("8888"))
+ if err != nil {
+ return 0, err
+ }
+ }
+ if err := p.verifier.VerifyPort(portStr); err != nil {
+ return 0, err
+ }
+ portUint64, err := strconv.ParseUint(portStr, 10, 16)
+ return uint16(portUint64), err
+}
+
+// GetTinyProxyUser obtains the TinyProxy server user from the environment variable
+// TINYPROXY_USER, and using PROXY_USER as a retro-compatibility name
+func (p *paramsReader) GetTinyProxyUser() (user string, err error) {
+ defer p.unsetEnv("PROXY_USER")
+ defer p.unsetEnv("TINYPROXY_USER")
+ // Retro-compatibility
+ user, err = p.envParams.GetEnv("PROXY_USER")
+ if err != nil {
+ return user, err
+ }
+ if len(user) != 0 {
+ p.logger.Warn("You are using the old environment variable PROXY_USER, please consider changing it to TINYPROXY_USER")
+ return user, nil
+ }
+ return p.envParams.GetEnv("TINYPROXY_USER")
+}
+
+// GetTinyProxyPassword obtains the TinyProxy server password from the environment variable
+// TINYPROXY_PASSWORD, and using PROXY_PASSWORD as a retro-compatibility name
+func (p *paramsReader) GetTinyProxyPassword() (password string, err error) {
+ defer p.unsetEnv("PROXY_PASSWORD")
+ defer p.unsetEnv("TINYPROXY_PASSWORD")
+ // Retro-compatibility
+ password, err = p.envParams.GetEnv("PROXY_PASSWORD")
+ if err != nil {
+ return password, err
+ }
+ if len(password) != 0 {
+ p.logger.Warn("You are using the old environment variable PROXY_PASSWORD, please consider changing it to TINYPROXY_PASSWORD")
+ return password, nil
+ }
+ return p.envParams.GetEnv("TINYPROXY_PASSWORD")
+}
diff --git a/internal/params/version.go b/internal/params/version.go
new file mode 100644
index 00000000..02780887
--- /dev/null
+++ b/internal/params/version.go
@@ -0,0 +1,20 @@
+package params
+
+import (
+ "github.com/qdm12/golibs/params"
+)
+
+func (p *paramsReader) GetVersion() string {
+ version, _ := p.envParams.GetEnv("VERSION", params.Default("?"))
+ return version
+}
+
+func (p *paramsReader) GetBuildDate() string {
+ buildDate, _ := p.envParams.GetEnv("BUILD_DATE", params.Default("?"))
+ return buildDate
+}
+
+func (p *paramsReader) GetVcsRef() string {
+ buildDate, _ := p.envParams.GetEnv("VCS_REF", params.Default("?"))
+ return buildDate
+}
diff --git a/internal/pia/download.go b/internal/pia/download.go
new file mode 100644
index 00000000..e9236d91
--- /dev/null
+++ b/internal/pia/download.go
@@ -0,0 +1,61 @@
+package pia
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+func (c *configurator) DownloadOvpnConfig(encryption models.PIAEncryption,
+ protocol models.NetworkProtocol, region models.PIARegion) (lines []string, err error) {
+ c.logger.Info("%s: downloading openvpn configuration files", logPrefix)
+ URL := buildZipURL(encryption, protocol)
+ content, status, err := c.client.GetContent(URL)
+ if err != nil {
+ return nil, err
+ } else if status != 200 {
+ return nil, fmt.Errorf("HTTP Get %s resulted in HTTP status code %d", URL, status)
+ }
+ filename := fmt.Sprintf("%s.ovpn", region)
+ fileContent, err := getFileInZip(content, filename)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", URL, err)
+ }
+ lines = strings.Split(string(fileContent), "\n")
+ return lines, nil
+}
+
+func buildZipURL(encryption models.PIAEncryption, protocol models.NetworkProtocol) (URL string) {
+ URL = string(constants.PIAOpenVPNURL) + "/openvpn"
+ if encryption == constants.PIAEncryptionStrong {
+ URL += "-strong"
+ }
+ if protocol == constants.TCP {
+ URL += "-tcp"
+ }
+ return URL + ".zip"
+}
+
+func getFileInZip(zipContent []byte, filename string) (fileContent []byte, err error) {
+ contentLength := int64(len(zipContent))
+ r, err := zip.NewReader(bytes.NewReader(zipContent), contentLength)
+ if err != nil {
+ return nil, err
+ }
+ for _, f := range r.File {
+ if f.Name == filename {
+ readCloser, err := f.Open()
+ if err != nil {
+ return nil, err
+ }
+ defer readCloser.Close()
+ return ioutil.ReadAll(readCloser)
+ }
+ }
+ return nil, fmt.Errorf("%s not found in zip archive file", filename)
+}
diff --git a/internal/pia/modify.go b/internal/pia/modify.go
new file mode 100644
index 00000000..ce6c9131
--- /dev/null
+++ b/internal/pia/modify.go
@@ -0,0 +1,31 @@
+package pia
+
+import (
+ "fmt"
+ "net"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) ModifyLines(lines []string, IPs []net.IP, port uint16) (modifiedLines []string) {
+ c.logger.Info("%s: adapting openvpn configuration for server IP addresses and port %d", logPrefix, port)
+ // Remove lines
+ for _, line := range lines {
+ if strings.Contains(line, "privateinternetaccess.com") ||
+ strings.Contains(line, "resolv-retry") {
+ continue
+ }
+ modifiedLines = append(modifiedLines, line)
+ }
+ // Add lines
+ for _, IP := range IPs {
+ modifiedLines = append(modifiedLines, fmt.Sprintf("remote %s %d", IP.String(), port))
+ }
+ modifiedLines = append(modifiedLines, "auth-user-pass "+string(constants.OpenVPNAuthConf))
+ modifiedLines = append(modifiedLines, "auth-retry nointeract")
+ modifiedLines = append(modifiedLines, "pull-filter ignore \"auth-token\"") // prevent auth failed loops
+ modifiedLines = append(modifiedLines, "user nonrootuser")
+ modifiedLines = append(modifiedLines, "mute-replay-warnings")
+ return modifiedLines
+}
diff --git a/internal/pia/modify_test.go b/internal/pia/modify_test.go
new file mode 100644
index 00000000..01c1e676
--- /dev/null
+++ b/internal/pia/modify_test.go
@@ -0,0 +1,31 @@
+package pia
+
+import (
+ "io/ioutil"
+ "net"
+ "strings"
+ "testing"
+
+ loggingMocks "github.com/qdm12/golibs/logging/mocks"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_ModifyLines(t *testing.T) {
+ t.Parallel()
+ original, err := ioutil.ReadFile("testdata/ovpn.golden")
+ require.NoError(t, err)
+ originalLines := strings.Split(string(original), "\n")
+ expected, err := ioutil.ReadFile("testdata/ovpn.modified.golden")
+ require.NoError(t, err)
+ expectedLines := strings.Split(string(expected), "\n")
+
+ var port uint16 = 3000
+ IPs := []net.IP{net.IP{100, 10, 10, 10}, net.IP{100, 20, 20, 20}}
+ logger := &loggingMocks.Logger{}
+ logger.On("Info", "%s: adapting openvpn configuration for server IP addresses and port %d", logPrefix, port).Once()
+ c := &configurator{logger: logger}
+ modifiedLines := c.ModifyLines(originalLines, IPs, port)
+ assert.Equal(t, expectedLines, modifiedLines)
+ logger.AssertExpectations(t)
+}
diff --git a/internal/pia/parse.go b/internal/pia/parse.go
new file mode 100644
index 00000000..9b5e8285
--- /dev/null
+++ b/internal/pia/parse.go
@@ -0,0 +1,54 @@
+package pia
+
+import (
+ "fmt"
+ "net"
+ "strconv"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+func (c *configurator) ParseConfig(lines []string) (IPs []net.IP, port uint16, device models.VPNDevice, err error) {
+ c.logger.Info("%s: parsing openvpn configuration", logPrefix)
+ remoteLineFound := false
+ deviceLineFound := false
+ for _, line := range lines {
+ if strings.HasPrefix(line, "remote ") {
+ remoteLineFound = true
+ words := strings.Fields(line)
+ if len(words) != 3 {
+ return nil, 0, "", fmt.Errorf("line %q misses information", line)
+ }
+ host := words[1]
+ if err := c.verifyPort(words[2]); err != nil {
+ return nil, 0, "", fmt.Errorf("line %q has an invalid port: %w", line, err)
+ }
+ portUint64, _ := strconv.ParseUint(words[2], 10, 16)
+ port = uint16(portUint64)
+ IPs, err = c.lookupIP(host)
+ if err != nil {
+ return nil, 0, "", err
+ }
+ } else if strings.HasPrefix(line, "dev ") {
+ deviceLineFound = true
+ fields := strings.Fields(line)
+ if len(fields) != 2 {
+ return nil, 0, "", fmt.Errorf("line %q misses information", line)
+ }
+ device = models.VPNDevice(fields[1] + "0")
+ if device != constants.TUN && device != constants.TAP {
+ return nil, 0, "", fmt.Errorf("device %q is not valid", device)
+ }
+ }
+ }
+ if remoteLineFound && deviceLineFound {
+ c.logger.Info("%s: Found %d PIA server IP addresses, port %d and device %s", logPrefix, len(IPs), port, device)
+ return IPs, port, device, nil
+ } else if !remoteLineFound {
+ return nil, 0, "", fmt.Errorf("remote line not found in Openvpn configuration")
+ } else {
+ return nil, 0, "", fmt.Errorf("device line not found in Openvpn configuration")
+ }
+}
diff --git a/internal/pia/parse_test.go b/internal/pia/parse_test.go
new file mode 100644
index 00000000..d0a2d0e9
--- /dev/null
+++ b/internal/pia/parse_test.go
@@ -0,0 +1,99 @@
+package pia
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net"
+ "strings"
+ "testing"
+
+ loggingMocks "github.com/qdm12/golibs/logging/mocks"
+ "github.com/qdm12/golibs/verification"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_ParseConfig(t *testing.T) {
+ t.Parallel()
+ original, err := ioutil.ReadFile("testdata/ovpn.golden")
+ require.NoError(t, err)
+ exampleLines := strings.Split(string(original), "\n")
+ tests := map[string]struct {
+ lines []string
+ lookupIPs []net.IP
+ lookupIPErr error
+ IPs []net.IP
+ port uint16
+ device models.VPNDevice
+ err error
+ }{
+ "no data": {
+ err: fmt.Errorf("remote line not found in Openvpn configuration"),
+ },
+ "bad remote line": {
+ lines: []string{"remote field2"},
+ err: fmt.Errorf("line \"remote field2\" misses information"),
+ },
+ "bad remote port": {
+ lines: []string{"remote field2 port"},
+ err: fmt.Errorf("line \"remote field2 port\" has an invalid port: port \"port\" is not a valid integer"),
+ },
+ "lookupIP error": {
+ lines: []string{"remote host 1000"},
+ lookupIPErr: fmt.Errorf("lookup error"),
+ err: fmt.Errorf("lookup error"),
+ },
+ "missing dev line": {
+ lines: []string{"remote host 1994"},
+ err: fmt.Errorf("device line not found in Openvpn configuration"),
+ },
+ "bad dev line": {
+ lines: []string{"dev field2 field3"},
+ err: fmt.Errorf("line \"dev field2 field3\" misses information"),
+ },
+ "bad device": {
+ lines: []string{"dev xx"},
+ err: fmt.Errorf("device \"xx0\" is not valid"),
+ },
+ "valid lines": {
+ lines: []string{"remote host 1194", "dev tap", "blabla"},
+ port: 1194,
+ device: constants.TAP,
+ },
+ "real data": {
+ lines: exampleLines,
+ lookupIPs: []net.IP{{100, 100, 100, 100}, {100, 100, 200, 200}},
+ IPs: []net.IP{{100, 100, 100, 100}, {100, 100, 200, 200}},
+ port: 1198,
+ device: constants.TUN,
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ logger := &loggingMocks.Logger{}
+ logger.On("Info", "%s: parsing openvpn configuration", logPrefix).Once()
+ if tc.err == nil {
+ logger.On("Info", "%s: Found %d PIA server IP addresses, port %d and device %s", logPrefix, len(tc.IPs), tc.port, tc.device).Once()
+ }
+ lookupIP := func(host string) ([]net.IP, error) {
+ return tc.lookupIPs, tc.lookupIPErr
+ }
+ c := &configurator{logger: logger, verifyPort: verification.NewVerifier().VerifyPort, lookupIP: lookupIP}
+ IPs, port, device, err := c.ParseConfig(tc.lines)
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ assert.Equal(t, tc.IPs, IPs)
+ assert.Equal(t, tc.port, port)
+ assert.Equal(t, tc.device, device)
+ logger.AssertExpectations(t)
+ })
+ }
+}
diff --git a/internal/pia/pia.go b/internal/pia/pia.go
new file mode 100644
index 00000000..c0f96df5
--- /dev/null
+++ b/internal/pia/pia.go
@@ -0,0 +1,41 @@
+package pia
+
+import (
+ "net"
+
+ "github.com/qdm12/golibs/crypto/random"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/golibs/network"
+ "github.com/qdm12/golibs/verification"
+ "github.com/qdm12/private-internet-access-docker/internal/firewall"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const logPrefix = "PIA configurator"
+
+// Configurator contains methods to download, read and modify the openvpn configuration to connect as a client
+type Configurator interface {
+ DownloadOvpnConfig(encryption models.PIAEncryption,
+ protocol models.NetworkProtocol, region models.PIARegion) (lines []string, err error)
+ ParseConfig(lines []string) (IPs []net.IP, port uint16, device models.VPNDevice, err error)
+ ModifyLines(lines []string, IPs []net.IP, port uint16) (modifiedLines []string)
+ GetPortForward() (port uint16, err error)
+ WritePortForward(filepath models.Filepath, port uint16) (err error)
+ AllowPortForwardFirewall(device models.VPNDevice, port uint16) (err error)
+}
+
+type configurator struct {
+ client network.Client
+ fileManager files.FileManager
+ firewall firewall.Configurator
+ logger logging.Logger
+ random random.Random
+ verifyPort func(port string) error
+ lookupIP func(host string) ([]net.IP, error)
+}
+
+// NewConfigurator returns a new Configurator object
+func NewConfigurator(client network.Client, fileManager files.FileManager, firewall firewall.Configurator, logger logging.Logger) Configurator {
+ return &configurator{client, fileManager, firewall, logger, random.NewRandom(), verification.NewVerifier().VerifyPort, net.LookupIP}
+}
diff --git a/internal/pia/portforward.go b/internal/pia/portforward.go
new file mode 100644
index 00000000..5e8d73e3
--- /dev/null
+++ b/internal/pia/portforward.go
@@ -0,0 +1,46 @@
+package pia
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+func (c *configurator) GetPortForward() (port uint16, err error) {
+ c.logger.Info("%s: Obtaining port to be forwarded", logPrefix)
+ b, err := c.random.GenerateRandomBytes(32)
+ if err != nil {
+ return 0, err
+ }
+ clientID := hex.EncodeToString(b)
+ url := fmt.Sprintf("%s/?client_id=%s", constants.PIAPortForwardURL, clientID)
+ content, status, err := c.client.GetContent(url)
+ if err != nil {
+ return 0, err
+ } else if status != 200 {
+ return 0, fmt.Errorf("status is %d for %s; does your PIA server support port forwarding?", status, url)
+ } else if len(content) == 0 {
+ return 0, fmt.Errorf("port forwarding is already activated on this connection, has expired, or you are not connected to a PIA region that supports port forwarding")
+ }
+ body := struct {
+ Port uint16 `json:"port"`
+ }{}
+ if err := json.Unmarshal(content, &body); err != nil {
+ return 0, fmt.Errorf("port forwarding response: %w", err)
+ }
+ c.logger.Info("%s: Port forwarded is %d", logPrefix, port)
+ return body.Port, nil
+}
+
+func (c *configurator) WritePortForward(filepath models.Filepath, port uint16) (err error) {
+ c.logger.Info("%s: Writing forwarded port to %s", logPrefix, filepath)
+ return c.fileManager.WriteLinesToFile(string(filepath), []string{fmt.Sprintf("%d", port)})
+}
+
+func (c *configurator) AllowPortForwardFirewall(device models.VPNDevice, port uint16) (err error) {
+ c.logger.Info("%s: Allowing forwarded port %d through firewall", logPrefix, port)
+ return c.firewall.AllowInputTrafficOnPort(device, port)
+}
diff --git a/internal/pia/testdata/ovpn.golden b/internal/pia/testdata/ovpn.golden
new file mode 100644
index 00000000..2a80fd55
--- /dev/null
+++ b/internal/pia/testdata/ovpn.golden
@@ -0,0 +1,72 @@
+client
+dev tun
+proto udp
+remote belgium.privateinternetaccess.com 1198
+resolv-retry infinite
+nobind
+persist-key
+persist-tun
+cipher aes-128-cbc
+auth sha1
+tls-client
+remote-cert-tls server
+
+auth-user-pass
+compress
+verb 1
+reneg-sec 0
+
+-----BEGIN X509 CRL-----
+MIICWDCCAUAwDQYJKoZIhvcNAQENBQAwgegxCzAJBgNVBAYTAlVTMQswCQYDVQQI
+EwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRl
+cm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAw
+HgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0
+ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRl
+aW50ZXJuZXRhY2Nlc3MuY29tFw0xNjA3MDgxOTAwNDZaFw0zNjA3MDMxOTAwNDZa
+MCYwEQIBARcMMTYwNzA4MTkwMDQ2MBECAQYXDDE2MDcwODE5MDA0NjANBgkqhkiG
+9w0BAQ0FAAOCAQEAQZo9X97ci8EcPYu/uK2HB152OZbeZCINmYyluLDOdcSvg6B5
+jI+ffKN3laDvczsG6CxmY3jNyc79XVpEYUnq4rT3FfveW1+Ralf+Vf38HdpwB8EW
+B4hZlQ205+21CALLvZvR8HcPxC9KEnev1mU46wkTiov0EKc+EdRxkj5yMgv0V2Re
+ze7AP+NQ9ykvDScH4eYCsmufNpIjBLhpLE2cuZZXBLcPhuRzVoU3l7A9lvzG9mjA
+5YijHJGHNjlWFqyrn1CfYS6koa4TGEPngBoAziWRbDGdhEgJABHrpoaFYaL61zqy
+MR6jC0K2ps9qyZAN74LEBedEfK7tBOzWMwr58A==
+-----END X509 CRL-----
+
+
+
+-----BEGIN CERTIFICATE-----
+MIIFqzCCBJOgAwIBAgIJAKZ7D5Yv87qDMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD
+VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV
+BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu
+dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx
+IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB
+FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzM1
+MThaFw0zNDA0MTIxNzM1MThaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
+EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg
+QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE
+AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50
+ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy
+bmV0YWNjZXNzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPXD
+L1L9tX6DGf36liA7UBTy5I869z0UVo3lImfOs/GSiFKPtInlesP65577nd7UNzzX
+lH/P/CnFPdBWlLp5ze3HRBCc/Avgr5CdMRkEsySL5GHBZsx6w2cayQ2EcRhVTwWp
+cdldeNO+pPr9rIgPrtXqT4SWViTQRBeGM8CDxAyTopTsobjSiYZCF9Ta1gunl0G/
+8Vfp+SXfYCC+ZzWvP+L1pFhPRqzQQ8k+wMZIovObK1s+nlwPaLyayzw9a8sUnvWB
+/5rGPdIYnQWPgoNlLN9HpSmsAcw2z8DXI9pIxbr74cb3/HSfuYGOLkRqrOk6h4RC
+OfuWoTrZup1uEOn+fw8CAwEAAaOCAVQwggFQMB0GA1UdDgQWBBQv63nQ/pJAt5tL
+y8VJcbHe22ZOsjCCAR8GA1UdIwSCARYwggESgBQv63nQ/pJAt5tLy8VJcbHe22ZO
+sqGB7qSB6zCB6DELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRMwEQYDVQQHEwpM
+b3NBbmdlbGVzMSAwHgYDVQQKExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4G
+A1UECxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAMTF1ByaXZhdGUg
+SW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQpExdQcml2YXRlIEludGVybmV0IEFjY2Vz
+czEvMC0GCSqGSIb3DQEJARYgc2VjdXJlQHByaXZhdGVpbnRlcm5ldGFjY2Vzcy5j
+b22CCQCmew+WL/O6gzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IBAQAn
+a5PgrtxfwTumD4+3/SYvwoD66cB8IcK//h1mCzAduU8KgUXocLx7QgJWo9lnZ8xU
+ryXvWab2usg4fqk7FPi00bED4f4qVQFVfGfPZIH9QQ7/48bPM9RyfzImZWUCenK3
+7pdw4Bvgoys2rHLHbGen7f28knT2j/cbMxd78tQc20TIObGjo8+ISTRclSTRBtyC
+GohseKYpTS9himFERpUgNtefvYHbn70mIOzfOJFTVqfrptf9jXa9N8Mpy3ayfodz
+1wiqdteqFXkTYoSDctgKMiZ6GdocK9nMroQipIQtpnwd4yBDWIyC6Bvlkrq5TQUt
+YDQ8z9v+DMO6iwyIDRiU
+-----END CERTIFICATE-----
+
+
+disable-occ
diff --git a/internal/pia/testdata/ovpn.modified.golden b/internal/pia/testdata/ovpn.modified.golden
new file mode 100644
index 00000000..dfd10da6
--- /dev/null
+++ b/internal/pia/testdata/ovpn.modified.golden
@@ -0,0 +1,78 @@
+client
+dev tun
+proto udp
+nobind
+persist-key
+persist-tun
+cipher aes-128-cbc
+auth sha1
+tls-client
+remote-cert-tls server
+
+auth-user-pass
+compress
+verb 1
+reneg-sec 0
+
+-----BEGIN X509 CRL-----
+MIICWDCCAUAwDQYJKoZIhvcNAQENBQAwgegxCzAJBgNVBAYTAlVTMQswCQYDVQQI
+EwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRl
+cm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAw
+HgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0
+ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRl
+aW50ZXJuZXRhY2Nlc3MuY29tFw0xNjA3MDgxOTAwNDZaFw0zNjA3MDMxOTAwNDZa
+MCYwEQIBARcMMTYwNzA4MTkwMDQ2MBECAQYXDDE2MDcwODE5MDA0NjANBgkqhkiG
+9w0BAQ0FAAOCAQEAQZo9X97ci8EcPYu/uK2HB152OZbeZCINmYyluLDOdcSvg6B5
+jI+ffKN3laDvczsG6CxmY3jNyc79XVpEYUnq4rT3FfveW1+Ralf+Vf38HdpwB8EW
+B4hZlQ205+21CALLvZvR8HcPxC9KEnev1mU46wkTiov0EKc+EdRxkj5yMgv0V2Re
+ze7AP+NQ9ykvDScH4eYCsmufNpIjBLhpLE2cuZZXBLcPhuRzVoU3l7A9lvzG9mjA
+5YijHJGHNjlWFqyrn1CfYS6koa4TGEPngBoAziWRbDGdhEgJABHrpoaFYaL61zqy
+MR6jC0K2ps9qyZAN74LEBedEfK7tBOzWMwr58A==
+-----END X509 CRL-----
+
+
+
+-----BEGIN CERTIFICATE-----
+MIIFqzCCBJOgAwIBAgIJAKZ7D5Yv87qDMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYD
+VQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNV
+BAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIElu
+dGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3Mx
+IDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkB
+FiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzM1
+MThaFw0zNDA0MTIxNzM1MThaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0Ex
+EzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQg
+QWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UE
+AxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50
+ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVy
+bmV0YWNjZXNzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPXD
+L1L9tX6DGf36liA7UBTy5I869z0UVo3lImfOs/GSiFKPtInlesP65577nd7UNzzX
+lH/P/CnFPdBWlLp5ze3HRBCc/Avgr5CdMRkEsySL5GHBZsx6w2cayQ2EcRhVTwWp
+cdldeNO+pPr9rIgPrtXqT4SWViTQRBeGM8CDxAyTopTsobjSiYZCF9Ta1gunl0G/
+8Vfp+SXfYCC+ZzWvP+L1pFhPRqzQQ8k+wMZIovObK1s+nlwPaLyayzw9a8sUnvWB
+/5rGPdIYnQWPgoNlLN9HpSmsAcw2z8DXI9pIxbr74cb3/HSfuYGOLkRqrOk6h4RC
+OfuWoTrZup1uEOn+fw8CAwEAAaOCAVQwggFQMB0GA1UdDgQWBBQv63nQ/pJAt5tL
+y8VJcbHe22ZOsjCCAR8GA1UdIwSCARYwggESgBQv63nQ/pJAt5tLy8VJcbHe22ZO
+sqGB7qSB6zCB6DELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRMwEQYDVQQHEwpM
+b3NBbmdlbGVzMSAwHgYDVQQKExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4G
+A1UECxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAMTF1ByaXZhdGUg
+SW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQpExdQcml2YXRlIEludGVybmV0IEFjY2Vz
+czEvMC0GCSqGSIb3DQEJARYgc2VjdXJlQHByaXZhdGVpbnRlcm5ldGFjY2Vzcy5j
+b22CCQCmew+WL/O6gzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IBAQAn
+a5PgrtxfwTumD4+3/SYvwoD66cB8IcK//h1mCzAduU8KgUXocLx7QgJWo9lnZ8xU
+ryXvWab2usg4fqk7FPi00bED4f4qVQFVfGfPZIH9QQ7/48bPM9RyfzImZWUCenK3
+7pdw4Bvgoys2rHLHbGen7f28knT2j/cbMxd78tQc20TIObGjo8+ISTRclSTRBtyC
+GohseKYpTS9himFERpUgNtefvYHbn70mIOzfOJFTVqfrptf9jXa9N8Mpy3ayfodz
+1wiqdteqFXkTYoSDctgKMiZ6GdocK9nMroQipIQtpnwd4yBDWIyC6Bvlkrq5TQUt
+YDQ8z9v+DMO6iwyIDRiU
+-----END CERTIFICATE-----
+
+
+disable-occ
+
+remote 100.10.10.10 3000
+remote 100.20.20.20 3000
+auth-user-pass /etc/openvpn/auth.conf
+auth-retry nointeract
+pull-filter ignore "auth-token"
+user nonrootuser
+mute-replay-warnings
\ No newline at end of file
diff --git a/internal/settings/dns.go b/internal/settings/dns.go
new file mode 100644
index 00000000..6d725f9e
--- /dev/null
+++ b/internal/settings/dns.go
@@ -0,0 +1,108 @@
+package settings
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// DNS contains settings to configure Unbound for DNS over TLS operation
+type DNS struct {
+ Enabled bool
+ Providers []models.DNSProvider
+ AllowedHostnames []string
+ PrivateAddresses []string
+ BlockMalicious bool
+ BlockSurveillance bool
+ BlockAds bool
+ VerbosityLevel uint8
+ VerbosityDetailsLevel uint8
+ ValidationLogLevel uint8
+}
+
+func (d *DNS) String() string {
+ if !d.Enabled {
+ return "DNS over TLS settings: disabled"
+ }
+ blockMalicious, blockSurveillance, blockAds := "disabed", "disabed", "disabed"
+ if d.BlockMalicious {
+ blockMalicious = "enabled"
+ }
+ if d.BlockSurveillance {
+ blockSurveillance = "enabled"
+ }
+ if d.BlockAds {
+ blockAds = "enabled"
+ }
+ var providersStr []string
+ for _, provider := range d.Providers {
+ providersStr = append(providersStr, string(provider))
+ }
+ settingsList := []string{
+ "DNS over TLS settings:",
+ "DNS over TLS provider: \n |--" + strings.Join(providersStr, "\n |--"),
+ "Block malicious: " + blockMalicious,
+ "Block surveillance: " + blockSurveillance,
+ "Block ads: " + blockAds,
+ "Allowed hostnames: " + strings.Join(d.AllowedHostnames, ", "),
+ "Private addresses:\n |--" + strings.Join(d.PrivateAddresses, "\n |--"),
+ "Verbosity level: " + fmt.Sprintf("%d/5", d.VerbosityLevel),
+ "Verbosity details level: " + fmt.Sprintf("%d/4", d.VerbosityDetailsLevel),
+ "Validation log level: " + fmt.Sprintf("%d/2", d.ValidationLogLevel),
+ }
+ return strings.Join(settingsList, "\n |--")
+}
+
+// GetDNSSettings obtains DNS over TLS settings from environment variables using the params package.
+func GetDNSSettings(params params.ParamsReader) (settings DNS, err error) {
+ settings.Enabled, err = params.GetDNSOverTLS()
+ if err != nil || !settings.Enabled {
+ return settings, err
+ }
+ settings.Providers, err = params.GetDNSOverTLSProviders()
+ if err != nil {
+ return settings, err
+ }
+ settings.AllowedHostnames, err = params.GetDNSUnblockedHostnames()
+ if err != nil {
+ return settings, err
+ }
+ settings.BlockMalicious, err = params.GetDNSMaliciousBlocking()
+ if err != nil {
+ return settings, err
+ }
+ settings.BlockSurveillance, err = params.GetDNSSurveillanceBlocking()
+ if err != nil {
+ return settings, err
+ }
+ settings.BlockAds, err = params.GetDNSAdsBlocking()
+ if err != nil {
+ return settings, err
+ }
+ settings.VerbosityLevel, err = params.GetDNSOverTLSVerbosity()
+ if err != nil {
+ return settings, err
+ }
+ settings.VerbosityDetailsLevel, err = params.GetDNSOverTLSVerbosityDetails()
+ if err != nil {
+ return settings, err
+ }
+ settings.ValidationLogLevel, err = params.GetDNSOverTLSValidationLogLevel()
+ if err != nil {
+ return settings, err
+ }
+ settings.PrivateAddresses = []string{ // TODO make env variable
+ "127.0.0.1/8",
+ "10.0.0.0/8",
+ "172.16.0.0/12",
+ "192.168.0.0/16",
+ "169.254.0.0/16",
+ "::1/128",
+ "fc00::/7",
+ "fe80::/10",
+ "::ffff:0:0/96",
+ }
+ return settings, nil
+}
diff --git a/internal/settings/firewall.go b/internal/settings/firewall.go
new file mode 100644
index 00000000..841416d0
--- /dev/null
+++ b/internal/settings/firewall.go
@@ -0,0 +1,34 @@
+package settings
+
+import (
+ "net"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// Firewall contains settings to customize the firewall operation
+type Firewall struct {
+ AllowedSubnets []net.IPNet
+}
+
+func (f *Firewall) String() string {
+ var allowedSubnets []string
+ for _, net := range f.AllowedSubnets {
+ allowedSubnets = append(allowedSubnets, net.String())
+ }
+ settingsList := []string{
+ "Firewall settings:",
+ "Allowed subnets: " + strings.Join(allowedSubnets, ", "),
+ }
+ return strings.Join(settingsList, "\n |--")
+}
+
+// GetFirewallSettings obtains firewall settings from environment variables using the params package.
+func GetFirewallSettings(params params.ParamsReader) (settings Firewall, err error) {
+ settings.AllowedSubnets, err = params.GetExtraSubnets()
+ if err != nil {
+ return settings, err
+ }
+ return settings, nil
+}
diff --git a/internal/settings/openvpn.go b/internal/settings/openvpn.go
new file mode 100644
index 00000000..21e07872
--- /dev/null
+++ b/internal/settings/openvpn.go
@@ -0,0 +1,30 @@
+package settings
+
+import (
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// OpenVPN contains settings to configure the OpenVPN client
+type OpenVPN struct {
+ NetworkProtocol models.NetworkProtocol
+}
+
+// GetOpenVPNSettings obtains the OpenVPN settings using the params functions
+func GetOpenVPNSettings(params params.ParamsReader) (settings OpenVPN, err error) {
+ settings.NetworkProtocol, err = params.GetNetworkProtocol()
+ if err != nil {
+ return settings, err
+ }
+ return settings, nil
+}
+
+func (o *OpenVPN) String() string {
+ settingsList := []string{
+ "OpenVPN settings:",
+ "Network protocol: " + string(o.NetworkProtocol),
+ }
+ return strings.Join(settingsList, "\n|--")
+}
diff --git a/internal/settings/pia.go b/internal/settings/pia.go
new file mode 100644
index 00000000..7455e7d2
--- /dev/null
+++ b/internal/settings/pia.go
@@ -0,0 +1,72 @@
+package settings
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// PIA contains the settings to connect to a PIA server
+type PIA struct {
+ User string
+ Password string
+ Encryption models.PIAEncryption
+ Region models.PIARegion
+ PortForwarding PortForwarding
+}
+
+// PortForwarding contains settings for port forwarding
+type PortForwarding struct {
+ Enabled bool
+ Filepath models.Filepath
+}
+
+func (p *PortForwarding) String() string {
+ if p.Enabled {
+ return fmt.Sprintf("on, saved in %s", p.Filepath)
+ }
+ return "off"
+}
+
+func (p *PIA) String() string {
+ settingsList := []string{
+ "PIA settings:",
+ "Region: " + string(p.Region),
+ "Encryption: " + string(p.Encryption),
+ "Port forwarding: " + p.PortForwarding.String(),
+ }
+ return strings.Join(settingsList, "\n |--")
+}
+
+// GetPIASettings obtains PIA settings from environment variables using the params package.
+func GetPIASettings(params params.ParamsReader) (settings PIA, err error) {
+ settings.User, err = params.GetUser()
+ if err != nil {
+ return settings, err
+ }
+ settings.Password, err = params.GetPassword()
+ if err != nil {
+ return settings, err
+ }
+ settings.Encryption, err = params.GetPIAEncryption()
+ if err != nil {
+ return settings, err
+ }
+ settings.Region, err = params.GetPIARegion()
+ if err != nil {
+ return settings, err
+ }
+ settings.PortForwarding.Enabled, err = params.GetPortForwarding()
+ if err != nil {
+ return settings, err
+ }
+ if settings.PortForwarding.Enabled {
+ settings.PortForwarding.Filepath, err = params.GetPortForwardingStatusFilepath()
+ if err != nil {
+ return settings, err
+ }
+ }
+ return settings, nil
+}
diff --git a/internal/settings/settings.go b/internal/settings/settings.go
new file mode 100644
index 00000000..7b56dc19
--- /dev/null
+++ b/internal/settings/settings.go
@@ -0,0 +1,60 @@
+package settings
+
+import (
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// Settings contains all settings for the program to run
+type Settings struct {
+ OpenVPN OpenVPN
+ PIA PIA
+ DNS DNS
+ Firewall Firewall
+ TinyProxy TinyProxy
+ ShadowSocks ShadowSocks
+}
+
+func (s *Settings) String() string {
+ return strings.Join([]string{
+ "Settings summary below:",
+ s.OpenVPN.String(),
+ s.PIA.String(),
+ s.DNS.String(),
+ s.Firewall.String(),
+ s.TinyProxy.String(),
+ s.ShadowSocks.String(),
+ "", // new line at the end
+ }, "\n")
+}
+
+// GetAllSettings obtains all settings for the program and returns an error as soon
+// as an error is encountered reading them.
+func GetAllSettings(params params.ParamsReader) (settings Settings, err error) {
+ settings.OpenVPN, err = GetOpenVPNSettings(params)
+ if err != nil {
+ return settings, err
+ }
+ settings.PIA, err = GetPIASettings(params)
+ if err != nil {
+ return settings, err
+ }
+ settings.DNS, err = GetDNSSettings(params)
+ if err != nil {
+ return settings, err
+ }
+ settings.Firewall, err = GetFirewallSettings(params)
+ if err != nil {
+ return settings, err
+ }
+ settings.TinyProxy, err = GetTinyProxySettings(params)
+ if err != nil {
+ return settings, err
+ }
+ settings.ShadowSocks, err = GetShadowSocksSettings(params)
+ if err != nil {
+ return settings, err
+ }
+ return settings, nil
+}
diff --git a/internal/settings/shadowsocks.go b/internal/settings/shadowsocks.go
new file mode 100644
index 00000000..63a77bfe
--- /dev/null
+++ b/internal/settings/shadowsocks.go
@@ -0,0 +1,48 @@
+package settings
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// ShadowSocks contains settings to configure the Shadowsocks server
+type ShadowSocks struct {
+ Enabled bool
+ Password string
+ Log bool
+ Port uint16
+}
+
+func (s *ShadowSocks) String() string {
+ if !s.Enabled {
+ return "ShadowSocks settings: disabled"
+ }
+ settingsList := []string{
+ "ShadowSocks settings:",
+ fmt.Sprintf("Port: %d", s.Port),
+ }
+ return strings.Join(settingsList, "\n |--")
+}
+
+// GetShadowSocksSettings obtains ShadowSocks settings from environment variables using the params package.
+func GetShadowSocksSettings(params params.ParamsReader) (settings ShadowSocks, err error) {
+ settings.Enabled, err = params.GetShadowSocks()
+ if err != nil || !settings.Enabled {
+ return settings, err
+ }
+ settings.Port, err = params.GetShadowSocksPort()
+ if err != nil {
+ return settings, err
+ }
+ settings.Password, err = params.GetShadowSocksPassword()
+ if err != nil {
+ return settings, err
+ }
+ settings.Log, err = params.GetShadowSocksLog()
+ if err != nil {
+ return settings, err
+ }
+ return settings, nil
+}
diff --git a/internal/settings/tinyproxy.go b/internal/settings/tinyproxy.go
new file mode 100644
index 00000000..1eb5b527
--- /dev/null
+++ b/internal/settings/tinyproxy.go
@@ -0,0 +1,60 @@
+package settings
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+// TinyProxy contains settings to configure TinyProxy
+type TinyProxy struct {
+ Enabled bool
+ User string
+ Password string
+ Port uint16
+ LogLevel models.TinyProxyLogLevel
+}
+
+func (t *TinyProxy) String() string {
+ if !t.Enabled {
+ return "TinyProxy settings: disabled"
+ }
+ auth := "disabled"
+ if t.User != "" {
+ auth = "enabled"
+ }
+ settingsList := []string{
+ "TinyProxy settings:",
+ fmt.Sprintf("Port: %d", t.Port),
+ "Authentication: " + auth,
+ "Log level: " + string(t.LogLevel),
+ }
+ return "TinyProxy settings:\n" + strings.Join(settingsList, "\n |--")
+}
+
+// GetTinyProxySettings obtains TinyProxy settings from environment variables using the params package.
+func GetTinyProxySettings(params params.ParamsReader) (settings TinyProxy, err error) {
+ settings.Enabled, err = params.GetTinyProxy()
+ if err != nil || !settings.Enabled {
+ return settings, err
+ }
+ settings.User, err = params.GetTinyProxyUser()
+ if err != nil {
+ return settings, err
+ }
+ settings.Password, err = params.GetTinyProxyPassword()
+ if err != nil {
+ return settings, err
+ }
+ settings.Port, err = params.GetTinyProxyPort()
+ if err != nil {
+ return settings, err
+ }
+ settings.LogLevel, err = params.GetTinyProxyLog()
+ if err != nil {
+ return settings, err
+ }
+ return settings, nil
+}
diff --git a/internal/shadowsocks/command.go b/internal/shadowsocks/command.go
new file mode 100644
index 00000000..9684e63d
--- /dev/null
+++ b/internal/shadowsocks/command.go
@@ -0,0 +1,40 @@
+package shadowsocks
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) Start(server string, port uint16, password string, log bool) (stdout io.ReadCloser, err error) {
+ c.logger.Info("%s: starting shadowsocks server", logPrefix)
+ args := []string{
+ "-c", string(constants.ShadowsocksConf),
+ "-p", fmt.Sprintf("%d", port),
+ "-k", password,
+ }
+ if log {
+ args = append(args, "-v")
+ }
+ stdout, _, _, err = c.commander.Start("ss-server", args...)
+ return stdout, err
+}
+
+// Version obtains the version of the installed shadowsocks server
+func (c *configurator) Version() (string, error) {
+ output, err := c.commander.Run("ss-server", "-h")
+ if err != nil {
+ return "", err
+ }
+ lines := strings.Split(output, "\n")
+ if len(lines) < 2 {
+ return "", fmt.Errorf("ss-server -h: not enough lines in %q", output)
+ }
+ words := strings.Fields(lines[1])
+ if len(words) < 2 {
+ return "", fmt.Errorf("ss-server -h: line 2 is too short: %q", lines[1])
+ }
+ return words[1], nil
+}
diff --git a/internal/shadowsocks/conf.go b/internal/shadowsocks/conf.go
new file mode 100644
index 00000000..f8e3b107
--- /dev/null
+++ b/internal/shadowsocks/conf.go
@@ -0,0 +1,49 @@
+package shadowsocks
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+)
+
+func (c *configurator) MakeConf(port uint16, password string, uid, gid int) (err error) {
+ c.logger.Info("%s: generating configuration file", logPrefix)
+ data := generateConf(port, password)
+ return c.fileManager.WriteToFile(
+ string(constants.ShadowsocksConf),
+ data,
+ files.FileOwnership(uid, gid),
+ files.FilePermissions(0400))
+}
+
+func generateConf(port uint16, password string) (data []byte) {
+ conf := struct {
+ Server string `json:"server"`
+ User string `json:"user"`
+ Method string `json:"method"`
+ Timeout uint `json:"timeout"`
+ FastOpen bool `json:"fast_open"`
+ Mode string `json:"mode"`
+ PortPassword map[string]string `json:"port_password"`
+ Workers uint `json:"workers"`
+ Interface string `json:"interface"`
+ Nameserver string `json:"nameserver"`
+ }{
+ Server: "0.0.0.0",
+ User: "nonrootuser",
+ Method: "chacha20-ietf-poly1305",
+ Timeout: 30,
+ FastOpen: false,
+ Mode: "tcp_and_udp",
+ PortPassword: map[string]string{
+ fmt.Sprintf("%d", port): password,
+ },
+ Workers: 2,
+ Interface: "tun",
+ Nameserver: "127.0.0.1",
+ }
+ data, _ = json.Marshal(conf)
+ return data
+}
diff --git a/internal/shadowsocks/conf_test.go b/internal/shadowsocks/conf_test.go
new file mode 100644
index 00000000..a7a05788
--- /dev/null
+++ b/internal/shadowsocks/conf_test.go
@@ -0,0 +1,79 @@
+package shadowsocks
+
+import (
+ "fmt"
+ "testing"
+
+ filesMocks "github.com/qdm12/golibs/files/mocks"
+ loggingMocks "github.com/qdm12/golibs/logging/mocks"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_generateConf(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ port uint16
+ password string
+ data []byte
+ }{
+ "no data": {
+ data: []byte(`{"server":"0.0.0.0","user":"nonrootuser","method":"chacha20-ietf-poly1305","timeout":30,"fast_open":false,"mode":"tcp_and_udp","port_password":{"0":""},"workers":2,"interface":"tun","nameserver":"127.0.0.1"}`),
+ },
+ "data": {
+ port: 2000,
+ password: "abcde",
+ data: []byte(`{"server":"0.0.0.0","user":"nonrootuser","method":"chacha20-ietf-poly1305","timeout":30,"fast_open":false,"mode":"tcp_and_udp","port_password":{"2000":"abcde"},"workers":2,"interface":"tun","nameserver":"127.0.0.1"}`),
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ data := generateConf(tc.port, tc.password)
+ assert.Equal(t, tc.data, data)
+ })
+ }
+}
+
+func Test_MakeConf(t *testing.T) {
+ t.Parallel()
+ tests := map[string]struct {
+ writeErr error
+ err error
+ }{
+ "no write error": {},
+ "write error": {
+ writeErr: fmt.Errorf("error"),
+ err: fmt.Errorf("error"),
+ },
+ }
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ logger := &loggingMocks.Logger{}
+ logger.On("Info", "%s: generating configuration file", logPrefix).Once()
+ fileManager := &filesMocks.FileManager{}
+ fileManager.On("WriteToFile",
+ string(constants.ShadowsocksConf),
+ []byte(`{"server":"0.0.0.0","user":"nonrootuser","method":"chacha20-ietf-poly1305","timeout":30,"fast_open":false,"mode":"tcp_and_udp","port_password":{"2000":"abcde"},"workers":2,"interface":"tun","nameserver":"127.0.0.1"}`),
+ mock.AnythingOfType("files.WriteOptionSetter"),
+ mock.AnythingOfType("files.WriteOptionSetter"),
+ ).
+ Return(tc.writeErr).Once()
+ c := &configurator{logger: logger, fileManager: fileManager}
+ err := c.MakeConf(2000, "abcde", 1000, 1001)
+ if tc.err != nil {
+ require.Error(t, err)
+ assert.Equal(t, tc.err.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+ logger.AssertExpectations(t)
+ fileManager.AssertExpectations(t)
+ })
+ }
+}
diff --git a/internal/shadowsocks/shadowsocks.go b/internal/shadowsocks/shadowsocks.go
new file mode 100644
index 00000000..52241128
--- /dev/null
+++ b/internal/shadowsocks/shadowsocks.go
@@ -0,0 +1,27 @@
+package shadowsocks
+
+import (
+ "io"
+
+ "github.com/qdm12/golibs/command"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+)
+
+const logPrefix = "shadowsocks configurator"
+
+type Configurator interface {
+ Version() (string, error)
+ MakeConf(port uint16, password string, uid, gid int) (err error)
+ Start(server string, port uint16, password string, log bool) (stdout io.ReadCloser, err error)
+}
+
+type configurator struct {
+ fileManager files.FileManager
+ logger logging.Logger
+ commander command.Commander
+}
+
+func NewConfigurator(fileManager files.FileManager, logger logging.Logger) Configurator {
+ return &configurator{fileManager, logger, command.NewCommander()}
+}
diff --git a/internal/splash/splash.go b/internal/splash/splash.go
new file mode 100644
index 00000000..a63791e9
--- /dev/null
+++ b/internal/splash/splash.go
@@ -0,0 +1,55 @@
+package splash
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/kyokomi/emoji"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/params"
+)
+
+func Splash(paramsReader params.ParamsReader) string {
+ version := paramsReader.GetVersion()
+ vcsRef := paramsReader.GetVcsRef()
+ buildDate := paramsReader.GetBuildDate()
+ lines := title()
+ lines = append(lines, "")
+ lines = append(lines, fmt.Sprintf("Running version %s built on %s (commit %s)", version, buildDate, vcsRef))
+ lines = append(lines, "")
+ lines = append(lines, annoucement()...)
+ lines = append(lines, "")
+ lines = append(lines, links()...)
+ return strings.Join(lines, "\n")
+}
+
+func title() []string {
+ return []string{
+ "=========================================",
+ "============= PIA container =============",
+ "========== An exquisite mix of ==========",
+ "==== OpenVPN, Unbound, DNS over TLS, ====",
+ "===== Shadowsocks, Tinyproxy and Go =====",
+ "=========================================",
+ "=== Made with " + emoji.Sprint(":heart:") + " by github.com/qdm12 ====",
+ "=========================================",
+ }
+}
+
+func annoucement() []string {
+ timestamp := time.Now().UnixNano() / 1000000000
+ if timestamp < constants.AnnoucementExpiration {
+ return []string{emoji.Sprint(":rotating_light: ") + constants.Annoucement}
+ }
+ return nil
+}
+
+func links() []string {
+ return []string{
+ emoji.Sprint(":wrench: ") + "Need help? " + constants.IssueLink,
+ emoji.Sprint(":computer: ") + "Email? quentin.mcgaw@gmail.com",
+ emoji.Sprint(":coffee: ") + "Slack? Join from the Slack button on Github",
+ emoji.Sprint(":money_with_wings: ") + "Help me? https://github.com/sponsors/qdm12",
+ }
+}
diff --git a/internal/tinyproxy/command.go b/internal/tinyproxy/command.go
new file mode 100644
index 00000000..15786e28
--- /dev/null
+++ b/internal/tinyproxy/command.go
@@ -0,0 +1,26 @@
+package tinyproxy
+
+import (
+ "fmt"
+ "io"
+ "strings"
+)
+
+func (c *configurator) Start() (stdout io.ReadCloser, err error) {
+ c.logger.Info("%s: starting tinyproxy server", logPrefix)
+ stdout, _, _, err = c.commander.Start("tinyproxy", "-d")
+ return stdout, err
+}
+
+// Version obtains the version of the installed Tinyproxy server
+func (c *configurator) Version() (string, error) {
+ output, err := c.commander.Run("tinyproxy", "-v")
+ if err != nil {
+ return "", err
+ }
+ words := strings.Fields(output)
+ if len(words) < 2 {
+ return "", fmt.Errorf("tinyproxy -v: output is too short: %q", output)
+ }
+ return words[1], nil
+}
diff --git a/internal/tinyproxy/conf.go b/internal/tinyproxy/conf.go
new file mode 100644
index 00000000..73d06933
--- /dev/null
+++ b/internal/tinyproxy/conf.go
@@ -0,0 +1,44 @@
+package tinyproxy
+
+import (
+ "fmt"
+
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/private-internet-access-docker/internal/constants"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+func (c *configurator) MakeConf(logLevel models.TinyProxyLogLevel, port uint16, user, password string, uid, gid int) error {
+ c.logger.Info("%s: generating tinyproxy configuration file", logPrefix)
+ lines := generateConf(logLevel, port, user, password)
+ return c.fileManager.WriteLinesToFile(string(constants.TinyProxyConf),
+ lines,
+ files.FileOwnership(uid, gid),
+ files.FilePermissions(0400))
+}
+
+func generateConf(logLevel models.TinyProxyLogLevel, port uint16, user, password string) (lines []string) {
+ confMapping := map[string]string{
+ "User": "nonrootuser",
+ "Group": "tinyproxy",
+ "Port": fmt.Sprintf("%d", port),
+ "Timeout": "600",
+ "DefaultErrorFile": "/usr/share/tinyproxy/default.html",
+ "MaxClients": "100",
+ "MinSpareServers": "5",
+ "MaxSpareServers": "20",
+ "StartServers": "10",
+ "MaxRequestsPerChild": "0",
+ "DisableViaHeader": "Yes",
+ "LogLevel": string(logLevel),
+ // "StatFile": "/usr/share/tinyproxy/stats.html",
+ }
+ if len(user) > 0 {
+ confMapping["BasicAuth"] = fmt.Sprintf("%s %s", user, password)
+ }
+ for k, v := range confMapping {
+ line := fmt.Sprintf("%s %s", k, v)
+ lines = append(lines, line)
+ }
+ return lines
+}
diff --git a/internal/tinyproxy/tinyproxy.go b/internal/tinyproxy/tinyproxy.go
new file mode 100644
index 00000000..663389a0
--- /dev/null
+++ b/internal/tinyproxy/tinyproxy.go
@@ -0,0 +1,28 @@
+package tinyproxy
+
+import (
+ "io"
+
+ "github.com/qdm12/golibs/command"
+ "github.com/qdm12/golibs/files"
+ "github.com/qdm12/golibs/logging"
+ "github.com/qdm12/private-internet-access-docker/internal/models"
+)
+
+const logPrefix = "tinyproxy configurator"
+
+type Configurator interface {
+ Version() (string, error)
+ MakeConf(logLevel models.TinyProxyLogLevel, port uint16, user, password string, uid, gid int) error
+ Start() (stdout io.ReadCloser, err error)
+}
+
+type configurator struct {
+ fileManager files.FileManager
+ logger logging.Logger
+ commander command.Commander
+}
+
+func NewConfigurator(fileManager files.FileManager, logger logging.Logger) Configurator {
+ return &configurator{fileManager, logger, command.NewCommander()}
+}
diff --git a/portforward.sh b/portforward.sh
deleted file mode 100644
index 23205407..00000000
--- a/portforward.sh
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/bin/sh
-
-exitOnError(){
- # $1 must be set to $?
- status=$1
- message=$2
- [ "$message" != "" ] || message="Undefined error"
- if [ $status != 0 ]; then
- printf "[ERROR] $message, with status $status)\n"
- exit $status
- fi
-}
-
-warnOnError(){
- # $1 must be set to $?
- status=$1
- message=$2
- [ "$message" != "" ] || message="Undefined error"
- if [ $status != 0 ]; then
- printf "[WARNING] $message, with status $status)\n"
- fi
-}
-
-printf "[INFO] Reading forwarded port\n"
-printf " * Generating client ID...\n"
-client_id=`head -n 100 /dev/urandom | sha256sum | tr -d " -"`
-exitOnError $? "Unable to generate Client ID"
-printf " * Obtaining forward port from PIA server...\n"
-json=`wget -qO- "http://209.222.18.222:2000/?client_id=$client_id"`
-exitOnError $? "Could not obtain response from PIA server (does your PIA server support port forwarding?)"
-if [ "$json" == "" ]; then
- printf "[ERROR] Port forwarding is already activated on this connection, has expired, or you are not connected to a PIA region that supports port forwarding\n"
- exit 1
-fi
-printf " * Parsing JSON response...\n"
-port=`echo $json | jq .port`
-exitOnError $? "Cannot find port in JSON response"
-printf " * Writing forwarded port to file...\n"
-port_status_folder=`dirname "${PORT_FORWARDING_STATUS_FILE}"`
-warnOnError $? "Cannot find parent directory of ${PORT_FORWARDING_STATUS_FILE}"
-mkdir -p "${port_status_folder}"
-warnOnError $? "Cannot create containing directory ${port_status_folder}"
-echo "$port" > "${PORT_FORWARDING_STATUS_FILE}"
-warnOnError $? "Cannot write port to ${PORT_FORWARDING_STATUS_FILE}"
-printf " * Detecting current VPN IP address...\n"
-ip=`wget -qO- https://duckduckgo.com/\?q=ip | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"`
-warnOnError $? "Cannot detect remote VPN IP on https://duckduckgo.com"
-printf " * Forwarded port accessible at $ip:$port\n"
-printf " * Detecting target VPN interface...\n"
-vpn_device=$(cat /openvpn/target/config.ovpn | grep 'dev ' | cut -d" " -f 2)0
-exitOnError $? "Unable to find VPN interface in /openvpn/target/config.ovpn"
-printf " * Accepting input traffic through $vpn_device to port $port...\n"
-iptables -A INPUT -i $vpn_device -p tcp --dport $port -j ACCEPT
-exitOnError $? "Unable to allow the forwarded port in TCP"
-iptables -A INPUT -i $vpn_device -p udp --dport $port -j ACCEPT
-exitOnError $? "Unable to allow the forwarded port in UDP"
-printf "[INFO] Port forwarded successfully\n"
diff --git a/shadowsocks.json b/shadowsocks.json
deleted file mode 100644
index 7293bf8d..00000000
--- a/shadowsocks.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "server": "0.0.0.0",
- "user": "nonrootuser",
- "method": "chacha20-ietf-poly1305",
- "timeout": 30,
- "fast_open": false,
- "mode": "tcp_and_udp",
- "port_password": {
- "8388": ""
- },
- "workers": 2,
- "interface": "tun",
- "nameserver": "127.0.0.1"
-}
\ No newline at end of file
diff --git a/tinyproxy.conf b/tinyproxy.conf
deleted file mode 100644
index 515f09f2..00000000
--- a/tinyproxy.conf
+++ /dev/null
@@ -1,13 +0,0 @@
-User tinyproxy
-Group tinyproxy
-Port 8888
-Timeout 600
-DefaultErrorFile "/usr/share/tinyproxy/default.html"
-MaxClients 100
-MinSpareServers 5
-MaxSpareServers 20
-StartServers 10
-MaxRequestsPerChild 0
-DisableViaHeader Yes
-LogLevel Critical
-# StatFile "/usr/share/tinyproxy/stats.html"
\ No newline at end of file
diff --git a/unbound.conf b/unbound.conf
deleted file mode 100644
index d39fd577..00000000
--- a/unbound.conf
+++ /dev/null
@@ -1,58 +0,0 @@
-server:
- # See https://www.nlnetlabs.nl/documentation/unbound/unbound.conf/
- # logging
- verbosity: 0
- val-log-level: 0
- use-syslog: yes
-
- # performance
- num-threads: 1
- prefetch: yes
- prefetch-key: yes
- key-cache-size: 16m
- key-cache-slabs: 4
- msg-cache-size: 4m
- msg-cache-slabs: 4
- rrset-cache-size: 4m
- rrset-cache-slabs: 4
- cache-min-ttl: 3600
- cache-max-ttl: 9000
-
- # privacy
- rrset-roundrobin: yes
- hide-identity: yes
- hide-version: yes
-
- # security
- tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt"
- root-hints: "/etc/unbound/root.hints"
- trust-anchor-file: "/etc/unbound/root.key"
- harden-below-nxdomain: yes
- harden-referral-path: yes
- harden-algo-downgrade: yes
- # set above to no if there is any problem
- # Prevent DNS rebinding
- private-address: 127.0.0.1/8
- private-address: 10.0.0.0/8
- private-address: 172.16.0.0/12
- private-address: 192.168.0.0/16
- private-address: 169.254.0.0/16
- private-address: ::1/128
- private-address: fc00::/7
- private-address: fe80::/10
- private-address: ::ffff:0:0/96
-
- # network
- do-ip4: yes
- do-ip6: no
- interface: 127.0.0.1
- port: 53
- username: "nonrootuser"
-
- # other files
- include: "/etc/unbound/blocks-malicious.conf"
-forward-zone:
- name: "."
- forward-addr: 1.1.1.1@853#cloudflare-dns.com
- forward-addr: 1.0.0.1@853#cloudflare-dns.com
- forward-tls-upstream: yes