Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095623925a | ||
|
|
ded635bd56 | ||
|
|
66667f94e1 | ||
|
|
77c6eeb765 | ||
|
|
040b5afca6 | ||
|
|
321579333d | ||
|
|
a76aa5276d | ||
|
|
0264f8726a | ||
|
|
247dc01f8a | ||
|
|
6734779e90 | ||
|
|
e527f14bd2 | ||
|
|
a40f68f1df | ||
|
|
84f49c5827 | ||
|
|
792f70ffa7 | ||
|
|
7f35daa418 | ||
|
|
86ed6736a5 | ||
|
|
6620ba52d2 | ||
|
|
1f873e7d66 | ||
|
|
fc9ebd561c | ||
|
|
63fd72524e | ||
|
|
ed5a90ef25 | ||
|
|
7f103b2749 | ||
|
|
69796e1ff9 | ||
|
|
6a9cd7ed9c | ||
|
|
64649039d9 | ||
|
|
3de4ffcf66 | ||
|
|
60a69f316b | ||
|
|
9b26a39690 | ||
|
|
73cef63e73 | ||
|
|
90f506d2b7 | ||
|
|
07cb909061 | ||
|
|
af5c7c648d | ||
|
|
fd248098a6 | ||
|
|
a21bb009e5 | ||
|
|
8b313cf211 | ||
|
|
adf82d844a | ||
|
|
0af0632304 | ||
|
|
9a2d0ec3ef |
62
.devcontainer/devcontainer.json
Normal file
62
.devcontainer/devcontainer.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
15
.devcontainer/docker-compose.yml
Normal file
15
.devcontainer/docker-compose.yml
Normal file
@@ -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"
|
||||
@@ -1,4 +1,10 @@
|
||||
.git
|
||||
.vscode
|
||||
readme
|
||||
*.yml
|
||||
*.md
|
||||
.gitignore
|
||||
.travis.yml
|
||||
ci.sh
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
README.md
|
||||
Dockerfile
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [qdm12]
|
||||
20
.travis.yml
Normal file
20
.travis.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
dist: xenial
|
||||
sudo: required
|
||||
git:
|
||||
quiet: true
|
||||
depth: 1
|
||||
env:
|
||||
global:
|
||||
- DOCKER_REPO=qmcgaw/private-internet-access
|
||||
before_install:
|
||||
- curl -fsSL https://get.docker.com | sh
|
||||
- echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json
|
||||
- mkdir -p $HOME/.docker
|
||||
- echo '{"experimental":"enabled"}' | sudo tee $HOME/.docker/config.json
|
||||
- sudo service docker start
|
||||
install:
|
||||
- docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- docker buildx create --name xbuilder --use
|
||||
script: bash ci.sh
|
||||
after_success:
|
||||
- curl -X POST https://hooks.microbadger.com/images/$DOCKER_REPO/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
|
||||
82
Dockerfile
82
Dockerfile
@@ -1,37 +1,55 @@
|
||||
ARG BASE_IMAGE=alpine
|
||||
ARG ALPINE_VERSION=3.10
|
||||
ARG ALPINE_VERSION=3.11
|
||||
ARG GO_VERSION=1.13.7
|
||||
|
||||
FROM ${BASE_IMAGE}:${ALPINE_VERSION}
|
||||
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="" \
|
||||
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" \
|
||||
image-size="23.3MB" \
|
||||
ram-usage="13MB to 80MB" \
|
||||
cpu-usage="Low to Medium"
|
||||
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=no \
|
||||
DOT=on \
|
||||
BLOCK_MALICIOUS=off \
|
||||
BLOCK_NSA=off \
|
||||
DOT_PROVIDERS=cloudflare \
|
||||
DOT_PRIVATE_ADDRESS=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 \
|
||||
DOT_VERBOSITY=1 \
|
||||
DOT_VERBOSITY_DETAILS=0 \
|
||||
DOT_VALIDATION_LOGLEVEL=0 \
|
||||
DOT_CACHING=on \
|
||||
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=Critical \
|
||||
TINYPROXY_LOG=Info \
|
||||
TINYPROXY_PORT=8888 \
|
||||
TINYPROXY_USER= \
|
||||
TINYPROXY_PASSWORD= \
|
||||
@@ -40,42 +58,14 @@ ENV USER= \
|
||||
SHADOWSOCKS_PORT=8388 \
|
||||
SHADOWSOCKS_PASSWORD= \
|
||||
TZ=
|
||||
ENTRYPOINT /entrypoint.sh
|
||||
ENTRYPOINT /entrypoint
|
||||
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 && \
|
||||
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 && \
|
||||
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
|
||||
chown nonrootuser -R /etc/unbound /etc/tinyproxy && \
|
||||
chmod 700 /etc/unbound /etc/tinyproxy
|
||||
COPY --from=builder --chown=1000:1000 /tmp/gobuild/entrypoint /entrypoint
|
||||
216
README.md
216
README.md
@@ -1,41 +1,47 @@
|
||||
# Private Internet Access Client (OpenVPN+Iptables+DNS over TLS on Alpine Linux)
|
||||
# Private Internet Access Client
|
||||
|
||||
*Lightweight VPN client to tunnel to private internet access servers*
|
||||
*Lightweight swiss-knife-like VPN client to tunnel to private internet access servers, using OpenVPN, iptables, DNS over TLS, ShadowSocks, Tinyproxy and more*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access/)
|
||||
**ANNOUCEMENT**: *Total rewrite in Go: see the new features [below](#Features)* (in case something break use the image with tag `:old`)
|
||||
|
||||
<a href="https://hub.docker.com/r/qmcgaw/private-internet-access">
|
||||
<img width="100%" height="320" src="https://raw.githubusercontent.com/qdm12/private-internet-access-docker/master/title.svg?sanitize=true">
|
||||
</a>
|
||||
|
||||
[](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc)
|
||||
[](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://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://hub.docker.com/r/qmcgaw/private-internet-access)
|
||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
||||
[](https://hub.docker.com/r/qmcgaw/private-internet-access)
|
||||
|
||||
[](https://microbadger.com/images/qmcgaw/private-internet-access)
|
||||
[](https://microbadger.com/images/qmcgaw/private-internet-access)
|
||||
|
||||
| Image size | RAM usage | CPU usage |
|
||||
| --- | --- | --- |
|
||||
| 23.3MB | 14MB to 80MB | Low to Medium |
|
||||
[](https://join.slack.com/t/qdm12/shared_invite/enQtOTE0NjcxNTM1ODc5LTYyZmVlOTM3MGI4ZWU0YmJkMjUxNmQ4ODQ2OTAwYzMxMTlhY2Q1MWQyOWUyNjc2ODliNjFjMDUxNWNmNzk5MDk)
|
||||
|
||||
<details><summary>Click to show base components</summary><p>
|
||||
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
</p></details>
|
||||
|
||||
## 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
|
||||
- <details><summary>Configure everything with environment variables</summary><p>
|
||||
|
||||
- [Destination region](https://www.privateinternetaccess.com/pages/network)
|
||||
@@ -43,82 +49,48 @@
|
||||
- Level of encryption
|
||||
- PIA Username and password
|
||||
- DNS over TLS
|
||||
- Malicious DNS blocking
|
||||
- DNS blocking: ads, malicious, surveillance
|
||||
- Internal firewall
|
||||
- Socks5 proxy
|
||||
- Web HTTP proxy
|
||||
- Run openvpn without root
|
||||
|
||||
</p></details>
|
||||
- Connect other containers to it, [see this](https://github.com/qdm12/private-internet-access-docker#connect-to-it)
|
||||
- **ARM** compatible
|
||||
- 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
|
||||
- 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 can run *without root* but this disallows OpenVPN reconnecting, it can be set with `NONROOT=yes`
|
||||
- Connect your LAN devices
|
||||
- HTTP Web proxy *tinyproxy*
|
||||
- SOCKS5 proxy *shadowsocks*
|
||||
- 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. <details><summary>Requirements</summary><p>
|
||||
|
||||
- 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`
|
||||
- <details><summary>External firewall requirements, if you have one</summary><p>
|
||||
|
||||
- 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
|
||||
|
||||
</p></details>
|
||||
|
||||
1. Ensure `/dev/net/tun` is setup on your host with either:
|
||||
|
||||
```sh
|
||||
insmod /lib/modules/tun.ko
|
||||
# or...
|
||||
modprobe tun
|
||||
```
|
||||
|
||||
1. <details><summary>CLICK IF YOU HAVE AN ARM DEVICE</summary><p>
|
||||
|
||||
- If you have a ARM 32 bit v6 architecture
|
||||
|
||||
```sh
|
||||
docker build -t qmcgaw/private-internet-access \
|
||||
--build-arg BASE_IMAGE=arm32v6/alpine \
|
||||
https://github.com/qdm12/private-internet-access-docker.git
|
||||
```
|
||||
|
||||
- If you have a ARM 32 bit v7 architecture
|
||||
|
||||
```sh
|
||||
docker build -t qmcgaw/private-internet-access \
|
||||
--build-arg BASE_IMAGE=arm32v7/alpine \
|
||||
https://github.com/qdm12/private-internet-access-docker.git
|
||||
```
|
||||
|
||||
- If you have a ARM 64 bit v8 architecture
|
||||
|
||||
```sh
|
||||
docker build -t qmcgaw/private-internet-access \
|
||||
--build-arg BASE_IMAGE=arm64v8/alpine \
|
||||
https://github.com/qdm12/private-internet-access-docker.git
|
||||
```
|
||||
|
||||
</p></details>
|
||||
|
||||
1. Launch the container with:
|
||||
|
||||
```bash
|
||||
docker run -d --init --name=pia --cap-add=NET_ADMIN --device=/dev/net/tun \
|
||||
docker run -d --init --name=pia --cap-add=NET_ADMIN \
|
||||
-e REGION="CA Montreal" -e USER=js89ds7 -e PASSWORD=8fd9s239G \
|
||||
qmcgaw/private-internet-access
|
||||
```
|
||||
@@ -134,6 +106,8 @@
|
||||
- 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
|
||||
|
||||
@@ -152,24 +126,30 @@ docker run --rm --network=container:pia alpine:3.10 wget -qO- https://ipinfo.io
|
||||
| `ENCRYPTION` | `strong` | `normal` or `strong` |
|
||||
| `USER` | | Your PIA username |
|
||||
| `PASSWORD` | | Your PIA password |
|
||||
| `NONROOT` | `no` | 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 |
|
||||
| `DOT_PROVIDERS` | `cloudflare` | Comma delimited list of DNS over TLS providers from `cloudflare`, `google`, `quad9`, `quadrant`, `cleanbrowsing`, `securedns`, `libredns` |
|
||||
| `DOT_CACHING` | `on` | Unbound caching feature, `on` or `off` |
|
||||
| `DOT_PRIVATE_ADDRESS` | All IPv4 and IPv6 CIDRs private ranges | Comma separated list of CIDRs or single IP addresses. Note that the default setting prevents DNS rebinding |
|
||||
| `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` | `Critical` | `Info`, `Warning`, `Error` or `Critical` |
|
||||
| `TINYPROXY` | `off` | `on` or `off`, to enable the internal HTTP proxy tinyproxy |
|
||||
| `TINYPROXY_LOG` | `Info` | `Info`, `Connect`, `Notice`, `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` | `off` | `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` |
|
||||
| `TZ` | | Specify a timezone to use i.e. `Europe/London` |
|
||||
|
||||
## Connect to it
|
||||
|
||||
@@ -192,13 +172,14 @@ There are various ways to achieve this, depending on your use case.
|
||||
</p></details>
|
||||
- <details><summary>Connect LAN devices through the built-in HTTP proxy *Tinyproxy* (i.e. with Chrome, Kodi, etc.)</summary><p>
|
||||
|
||||
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, merged with the OpenVPN logs.
|
||||
`TINYPROXY_LOG` defaults to `Critical` to avoid logging everything, for privacy purposes.
|
||||
1. If you set `TINYPROXY_LOG` to `Info`, more information will be logged in the Docker logs
|
||||
|
||||
</p></details>
|
||||
- <details><summary>Connect LAN devices through the built-in SOCKS5 proxy *Shadowsocks* (per app, system wide, etc.)</summary><p>
|
||||
@@ -217,7 +198,7 @@ There are various ways to achieve this, depending on your use case.
|
||||
- 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.
|
||||
1. If you set `SHADOWSOCKS_LOG` to `on`, more information will be logged in the Docker logs
|
||||
|
||||
</p></details>
|
||||
- <details><summary>Access ports of containers connected to PIA</summary><p>
|
||||
@@ -240,8 +221,6 @@ There are various ways to achieve this, depending on your use case.
|
||||
init: true
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun
|
||||
environment:
|
||||
- USER=js89ds7
|
||||
- PASSWORD=8fd9s239G
|
||||
@@ -270,15 +249,19 @@ 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:
|
||||
- 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 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)
|
||||
- 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)
|
||||
@@ -286,11 +269,16 @@ Note that not all regions support port forwarding.
|
||||
|
||||
## 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
|
||||
- If openvpn fails to start, you may need to:
|
||||
- Install the tun kernel module on your host with `insmod /lib/modules/tun.ko` or `modprobe tun`
|
||||
- Add `--device=/dev/net/tun` to your docker run command (equivalent for docker-compose, kubernetes, etc.)
|
||||
|
||||
- Fallback to a previous Docker image tags:
|
||||
- `v1` tag, stable shell scripting based (no support)
|
||||
- `old` tag, latest shell scripting version (no support)
|
||||
- `v2`... waiting for `latest` to become more stable
|
||||
|
||||
- Fallback to a precise previous version
|
||||
1. Clone the repository on your machine
|
||||
|
||||
```sh
|
||||
@@ -311,13 +299,41 @@ Note that not all regions support port forwarding.
|
||||
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
|
||||
|
||||
- Shadowsocks
|
||||
- Get logs from file and merge with docker stdout
|
||||
- Mix Logs of Unbound
|
||||
- Maybe use `--inactive 3600 --ping 10 --ping-exit 60` as default behavior
|
||||
- Try without tun
|
||||
- Support other VPN providers
|
||||
- Mullvad
|
||||
- Windscribe
|
||||
- Gotify support for notificactions
|
||||
- Periodic update of malicious block lists with Unbound restart
|
||||
- Improve healthcheck
|
||||
- Check IP address belongs to selected region
|
||||
- Check for DNS provider somehow if this is even possible
|
||||
- Support for other VPN protocols
|
||||
- Wireguard (wireguard-go)
|
||||
- Show new versions/commits at start
|
||||
- Colors & emojis
|
||||
- Setup
|
||||
- Logging streams
|
||||
- More unit tests
|
||||
- Write in Go
|
||||
- DNS over TLS to replace Unbound
|
||||
- HTTP proxy to replace tinyproxy
|
||||
- use [go-Shadowsocks2](https://github.com/shadowsocks/go-shadowsocks2)
|
||||
- DNS over HTTPS, maybe use [github.com/likexian/doh-go](https://github.com/likexian/doh-go)
|
||||
- use [iptables-go](https://github.com/coreos/go-iptables) to replace iptables
|
||||
- wireguard-go
|
||||
- Openvpn to replace openvpn
|
||||
|
||||
## License
|
||||
|
||||
|
||||
21
ci.sh
Normal file
21
ci.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$TRAVIS_PULL_REQUEST" = "true" ] || [ "$TRAVIS_BRANCH" != "master" ]; then
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6,linux/ppc64le,linux/s390x \
|
||||
.
|
||||
exit $?
|
||||
fi
|
||||
echo $DOCKER_PASSWORD | docker login -u qmcgaw --password-stdin &> /dev/null
|
||||
TAG="${TRAVIS_TAG:-latest}"
|
||||
echo "Building Docker images for \"$DOCKER_REPO:$TAG\""
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6,linux/ppc64le,linux/s390x \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=$TAG \
|
||||
-t $DOCKER_REPO:$TAG \
|
||||
--push \
|
||||
.
|
||||
190
cmd/main.go
Normal file
190
cmd/main.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/command"
|
||||
"github.com/qdm12/golibs/files"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/signals"
|
||||
"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/healthcheck"
|
||||
"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)
|
||||
}
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
if err := healthcheck.HealthCheck(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
paramsReader := params.NewParamsReader(logger)
|
||||
fmt.Println(splash.Splash(paramsReader))
|
||||
e := env.New(logger)
|
||||
client := network.NewClient(15 * 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())
|
||||
|
||||
if err := ovpnConf.CheckTUN(); err != nil {
|
||||
logger.Warn(err)
|
||||
err = ovpnConf.CreateTUN()
|
||||
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)
|
||||
|
||||
go func() {
|
||||
// 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)
|
||||
}()
|
||||
|
||||
if allSettings.DNS.Enabled {
|
||||
initialDNSToUse := constants.DNSProviderMapping()[allSettings.DNS.Providers[0]]
|
||||
dnsConf.UseDNSInternally(initialDNSToUse.IPs[0])
|
||||
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, waitFn, err := dnsConf.Start(allSettings.DNS.VerbosityDetailsLevel)
|
||||
e.FatalOnError(err)
|
||||
go func() {
|
||||
e.FatalOnError(waitFn())
|
||||
}()
|
||||
go streamMerger.Merge("unbound", stream)
|
||||
dnsConf.UseDNSInternally(net.IP{127, 0, 0, 1}) // use Unbound
|
||||
err = dnsConf.UseDNSSystemWide(net.IP{127, 0, 0, 1}) // use Unbound
|
||||
e.FatalOnError(err)
|
||||
err = dnsConf.WaitForUnbound()
|
||||
e.FatalOnError(err)
|
||||
}
|
||||
|
||||
VPNIPs, port, err := piaConf.BuildConf(allSettings.PIA.Region, allSettings.OpenVPN.NetworkProtocol, allSettings.PIA.Encryption, uid, gid)
|
||||
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(constants.TUN, 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.TinyProxy.Port, allSettings.TinyProxy.User, allSettings.TinyProxy.Password, uid, gid)
|
||||
e.FatalOnError(err)
|
||||
err = firewallConf.AllowAnyIncomingOnPort(allSettings.TinyProxy.Port)
|
||||
e.FatalOnError(err)
|
||||
stream, waitFn, err := tinyProxyConf.Start()
|
||||
e.FatalOnError(err)
|
||||
go func() {
|
||||
if err := waitFn(); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}()
|
||||
go streamMerger.Merge("tinyproxy", stream)
|
||||
}
|
||||
|
||||
if allSettings.ShadowSocks.Enabled {
|
||||
err = shadowsocksConf.MakeConf(allSettings.ShadowSocks.Port, allSettings.ShadowSocks.Password, uid, gid)
|
||||
e.FatalOnError(err)
|
||||
err = firewallConf.AllowAnyIncomingOnPort(allSettings.ShadowSocks.Port)
|
||||
e.FatalOnError(err)
|
||||
stream, waitFn, err := shadowsocksConf.Start("0.0.0.0", allSettings.ShadowSocks.Port, allSettings.ShadowSocks.Password, allSettings.ShadowSocks.Log)
|
||||
e.FatalOnError(err)
|
||||
go func() {
|
||||
if err := waitFn(); err != nil {
|
||||
logger.Error(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(constants.TUN, port); err != nil {
|
||||
logger.Error("port forwarding:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stream, waitFn, err := ovpnConf.Start()
|
||||
e.FatalOnError(err)
|
||||
go streamMerger.Merge("openvpn", stream)
|
||||
go signals.WaitForExit(func(signal string) int {
|
||||
logger.Warn("Caught OS signal %s, shutting down", signal)
|
||||
time.Sleep(100 * time.Millisecond) // wait for other processes to exit
|
||||
return 0
|
||||
})
|
||||
e.FatalOnError(waitFn())
|
||||
}
|
||||
@@ -6,8 +6,6 @@ services:
|
||||
container_name: pia
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun
|
||||
network_mode: bridge
|
||||
init: true
|
||||
ports:
|
||||
@@ -21,18 +19,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
|
||||
|
||||
492
entrypoint.sh
492
entrypoint.sh
@@ -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_LOG "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"
|
||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
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-20200208153322-66b2eb719e21
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d
|
||||
)
|
||||
113
go.sum
Normal file
113
go.sum
Normal file
@@ -0,0 +1,113 @@
|
||||
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 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
|
||||
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 h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
||||
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 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
|
||||
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 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
|
||||
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/loads v0.17.0 h1:H22nMs3GDQk4SwAaFQ+jLNw+0xoFeCueawhZlv8MBYs=
|
||||
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 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
|
||||
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
||||
github.com/go-openapi/spec v0.17.0 h1:XNvrt8FlSVP8T1WuhbAFF6QDhJc0zsoWzX4wXARhhpE=
|
||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/strfmt v0.17.0 h1:1isAxYf//QDTnVzbLAMrUK++0k1EjeLJU/gTOR0o3Mc=
|
||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
|
||||
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 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
|
||||
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 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
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-20200208153322-66b2eb719e21 h1:Nza/Ar6tPYhDzkiNzbaJZHl4+GUXTqbtjGXuWenkqpQ=
|
||||
github.com/qdm12/golibs v0.0.0-20200208153322-66b2eb719e21/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 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
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 h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
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=
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
-t $IMAGE_NAME .
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
curl -X POST https://hooks.microbadger.com/images/qmcgaw/${DOCKER_REPO}/tQFy7AxtSUNANPe6aoVChYdsI_I= || exit 0
|
||||
83
internal/constants/dns.go
Normal file
83
internal/constants/dns.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// DNSProviderMapping returns a constant mapping of dns provider name
|
||||
// to their data such as IP addresses or TLS host name.
|
||||
func DNSProviderMapping() map[models.DNSProvider]models.DNSProviderData {
|
||||
return map[models.DNSProvider]models.DNSProviderData{
|
||||
Cloudflare: models.DNSProviderData{
|
||||
IPs: []net.IP{{1, 1, 1, 1}, {1, 0, 0, 1}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("cloudflare-dns.com"),
|
||||
},
|
||||
Google: models.DNSProviderData{
|
||||
IPs: []net.IP{{8, 8, 8, 8}, {8, 8, 4, 4}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("dns.google"),
|
||||
},
|
||||
Quad9: models.DNSProviderData{
|
||||
IPs: []net.IP{{9, 9, 9, 9}, {149, 112, 112, 112}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("dns.quad9.net"),
|
||||
},
|
||||
Quadrant: models.DNSProviderData{
|
||||
IPs: []net.IP{{12, 159, 2, 159}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("dns-tls.qis.io"),
|
||||
},
|
||||
CleanBrowsing: models.DNSProviderData{
|
||||
IPs: []net.IP{{185, 228, 168, 9}, {185, 228, 169, 9}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("security-filter-dns.cleanbrowsing.org"),
|
||||
},
|
||||
SecureDNS: models.DNSProviderData{
|
||||
IPs: []net.IP{{146, 185, 167, 43}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("dot.securedns.eu"),
|
||||
},
|
||||
LibreDNS: models.DNSProviderData{
|
||||
IPs: []net.IP{{116, 203, 115, 192}},
|
||||
SupportsTLS: true,
|
||||
Host: models.DNSHost("dot.libredns.gr"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
)
|
||||
10
internal/constants/openvpn.go
Normal file
10
internal/constants/openvpn.go
Normal file
@@ -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"
|
||||
)
|
||||
30
internal/constants/paths.go
Normal file
30
internal/constants/paths.go
Normal file
@@ -0,0 +1,30 @@
|
||||
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"
|
||||
// CACertificates is the file path to the CA certificates file
|
||||
CACertificates models.Filepath = "/etc/ssl/certs/ca-certificates.crt"
|
||||
// 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"
|
||||
)
|
||||
90
internal/constants/pia.go
Normal file
90
internal/constants/pia.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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 (
|
||||
PIAX509CRL_NORMAL = "MIICWDCCAUAwDQYJKoZIhvcNAQENBQAwgegxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRlaW50ZXJuZXRhY2Nlc3MuY29tFw0xNjA3MDgxOTAwNDZaFw0zNjA3MDMxOTAwNDZaMCYwEQIBARcMMTYwNzA4MTkwMDQ2MBECAQYXDDE2MDcwODE5MDA0NjANBgkqhkiG9w0BAQ0FAAOCAQEAQZo9X97ci8EcPYu/uK2HB152OZbeZCINmYyluLDOdcSvg6B5jI+ffKN3laDvczsG6CxmY3jNyc79XVpEYUnq4rT3FfveW1+Ralf+Vf38HdpwB8EWB4hZlQ205+21CALLvZvR8HcPxC9KEnev1mU46wkTiov0EKc+EdRxkj5yMgv0V2Reze7AP+NQ9ykvDScH4eYCsmufNpIjBLhpLE2cuZZXBLcPhuRzVoU3l7A9lvzG9mjA5YijHJGHNjlWFqyrn1CfYS6koa4TGEPngBoAziWRbDGdhEgJABHrpoaFYaL61zqyMR6jC0K2ps9qyZAN74LEBedEfK7tBOzWMwr58A=="
|
||||
PIAX509CRL_STRONG = "MIIDWDCCAUAwDQYJKoZIhvcNAQENBQAwgegxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRlaW50ZXJuZXRhY2Nlc3MuY29tFw0xNjA3MDgxOTAwNDZaFw0zNjA3MDMxOTAwNDZaMCYwEQIBARcMMTYwNzA4MTkwMDQ2MBECAQYXDDE2MDcwODE5MDA0NjANBgkqhkiG9w0BAQ0FAAOCAgEAppFfEpGsasjB1QgJcosGpzbf2kfRhM84o2TlqY1ua+Gi5TMdKydA3LJcNTjlI9a0TYAJfeRX5IkpoglSUuHuJgXhP3nEvX10mjXDpcu/YvM8TdE5JV2+EGqZ80kFtBeOq94WcpiVKFTR4fO+VkOK9zwspFfb1cNs9rHvgJ1QMkRUF8PpLN6AkntHY0+6DnigtSaKqldqjKTDTv2OeH3nPoh80SGrt0oCOmYKfWTJGpggMGKvIdvU3vH9+EuILZKKIskt+1dwdfA5Bkz1GLmiQG7+9ZZBQUjBG9Dos4hfX/rwJ3eU8oUIm4WoTz9rb71SOEuUUjP5NPy9HNx2vx+cVvLsTF4ZDZaUztW9o9JmIURDtbeyqxuHN3prlPWB6aj73IIm2dsDQvs3XXwRIxs8NwLbJ6CyEuvEOVCskdM8rdADWx1J0lRNlOJ0Z8ieLLEmYAA834VN1SboB6wJIAPxQU3rcBhXqO9y8aa2oRMg8NxZ5gr+PnKVMqag1x0IxbIgLxtkXQvxXxQHEMSODzvcOfK/nBRBsqTj30P+R87sU8titOoxNeRnBDRNhdEy/QGAqGh62ShPpQUCJdnKRiRTjnil9hMQHevoSuFKeEMO30FQL7BZyo37GFU+q1WPCplVZgCP9hC8Rn5K2+f6KLFo5bhtowSmu+GY1yZtg+RTtsA="
|
||||
PIACertificate_NORMAL = "MIIFqzCCBJOgAwIBAgIJAKZ7D5Yv87qDMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzM1MThaFw0zNDA0MTIxNzM1MThaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPXDL1L9tX6DGf36liA7UBTy5I869z0UVo3lImfOs/GSiFKPtInlesP65577nd7UNzzXlH/P/CnFPdBWlLp5ze3HRBCc/Avgr5CdMRkEsySL5GHBZsx6w2cayQ2EcRhVTwWpcdldeNO+pPr9rIgPrtXqT4SWViTQRBeGM8CDxAyTopTsobjSiYZCF9Ta1gunl0G/8Vfp+SXfYCC+ZzWvP+L1pFhPRqzQQ8k+wMZIovObK1s+nlwPaLyayzw9a8sUnvWB/5rGPdIYnQWPgoNlLN9HpSmsAcw2z8DXI9pIxbr74cb3/HSfuYGOLkRqrOk6h4RCOfuWoTrZup1uEOn+fw8CAwEAAaOCAVQwggFQMB0GA1UdDgQWBBQv63nQ/pJAt5tLy8VJcbHe22ZOsjCCAR8GA1UdIwSCARYwggESgBQv63nQ/pJAt5tLy8VJcbHe22ZOsqGB7qSB6zCB6DELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRMwEQYDVQQHEwpMb3NBbmdlbGVzMSAwHgYDVQQKExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UECxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAMTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQpExdQcml2YXRlIEludGVybmV0IEFjY2VzczEvMC0GCSqGSIb3DQEJARYgc2VjdXJlQHByaXZhdGVpbnRlcm5ldGFjY2Vzcy5jb22CCQCmew+WL/O6gzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4IBAQAna5PgrtxfwTumD4+3/SYvwoD66cB8IcK//h1mCzAduU8KgUXocLx7QgJWo9lnZ8xUryXvWab2usg4fqk7FPi00bED4f4qVQFVfGfPZIH9QQ7/48bPM9RyfzImZWUCenK37pdw4Bvgoys2rHLHbGen7f28knT2j/cbMxd78tQc20TIObGjo8+ISTRclSTRBtyCGohseKYpTS9himFERpUgNtefvYHbn70mIOzfOJFTVqfrptf9jXa9N8Mpy3ayfodz1wiqdteqFXkTYoSDctgKMiZ6GdocK9nMroQipIQtpnwd4yBDWIyC6Bvlkrq5TQUtYDQ8z9v+DMO6iwyIDRiU"
|
||||
PIACertificate_STRONG = "MIIHqzCCBZOgAwIBAgIJAJ0u+vODZJntMA0GCSqGSIb3DQEBDQUAMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTAeFw0xNDA0MTcxNzQwMzNaFw0zNDA0MTIxNzQwMzNaMIHoMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEzARBgNVBAcTCkxvc0FuZ2VsZXMxIDAeBgNVBAoTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQLExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEAxMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBCkTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMS8wLQYJKoZIhvcNAQkBFiBzZWN1cmVAcHJpdmF0ZWludGVybmV0YWNjZXNzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVkhjumaqBbL8aSgj6xbX1QPTfTd1qHsAZd2B97m8Vw31c/2yQgZNf5qZY0+jOIHULNDe4R9TIvyBEbvnAg/OkPw8n/+ScgYOeH876VUXzjLDBnDb8DLr/+w9oVsuDeFJ9KV2UFM1OYX0SnkHnrYAN2QLF98ESK4NCSU01h5zkcgmQ+qKSfA9Ny0/UpsKPBFqsQ25NvjDWFhCpeqCHKUJ4Be27CDbSl7lAkBuHMPHJs8f8xPgAbHRXZOxVCpayZ2SNDfCwsnGWpWFoMGvdMbygngCn6jA/W1VSFOlRlfLuuGe7QFfDwA0jaLCxuWt/BgZylp7tAzYKR8lnWmtUCPm4+BtjyVDYtDCiGBD9Z4P13RFWvJHw5aapx/5W/CuvVyI7pKwvc2IT+KPxCUhH1XI8ca5RN3C9NoPJJf6qpg4g0rJH3aaWkoMRrYvQ+5PXXYUzjtRHImghRGd/ydERYoAZXuGSbPkm9Y/p2X8unLcW+F0xpJD98+ZI+tzSsI99Zs5wijSUGYr9/j18KHFTMQ8n+1jauc5bCCegN27dPeKXNSZ5riXFL2XX6BkY68y58UaNzmeGMiUL9BOV1iV+PMb7B7PYs7oFLjAhh0EdyvfHkrh/ZV9BEhtFa7yXp8XR0J6vz1YV9R6DYJmLjOEbhU8N0gc3tZm4Qz39lIIG6w3FDAgMBAAGjggFUMIIBUDAdBgNVHQ4EFgQUrsRtyWJftjpdRM0+925Y6Cl08SUwggEfBgNVHSMEggEWMIIBEoAUrsRtyWJftjpdRM0+925Y6Cl08SWhge6kgeswgegxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTETMBEGA1UEBxMKTG9zQW5nZWxlczEgMB4GA1UEChMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxIDAeBgNVBAsTF1ByaXZhdGUgSW50ZXJuZXQgQWNjZXNzMSAwHgYDVQQDExdQcml2YXRlIEludGVybmV0IEFjY2VzczEgMB4GA1UEKRMXUHJpdmF0ZSBJbnRlcm5ldCBBY2Nlc3MxLzAtBgkqhkiG9w0BCQEWIHNlY3VyZUBwcml2YXRlaW50ZXJuZXRhY2Nlc3MuY29tggkAnS7684Nkme0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOCAgEAJsfhsPk3r8kLXLxY+v+vHzbr4ufNtqnL9/1Uuf8NrsCtpXAoyZ0YqfbkWx3NHTZ7OE9ZRhdMP/RqHQE1p4N4Sa1nZKhTKasV6KhHDqSCt/dvEm89xWm2MVA7nyzQxVlHa9AkcBaemcXEiyT19XdpiXOP4Vhs+J1R5m8zQOxZlV1GtF9vsXmJqWZpOVPmZ8f35BCsYPvv4yMewnrtAC8PFEK/bOPeYcKN50bol22QYaZuLfpkHfNiFTnfMh8sl/ablPyNY7DUNiP5DRcMdIwmfGQxR5WEQoHL3yPJ42LkB5zs6jIm26DGNXfwura/mi105+ENH1CaROtRYwkiHb08U6qLXXJz80mWJkT90nr8Asj35xN2cUppg74nG3YVav/38P48T56hG1NHbYF5uOCske19F6wi9maUoto/3vEr0rnXJUp2KODmKdvBI7co245lHBABWikk8VfejQSlCtDBXn644ZMtAdoxKNfR2WTFVEwJiyd1Fzx0yujuiXDROLhISLQDRjVVAvawrAtLZWYK31bY7KlezPlQnl/D9Asxe85l8jO5+0LdJ6VyOs/Hd4w52alDW/MFySDZSfQHMTIc30hLBJ8OnCEIvluVQQ2UQvoW+no177N9L2Y+M9TcTA62ZyMXShHQGeh20rb4kK8f+iFX8NxtdHVSkxMEFSfDDyQ="
|
||||
)
|
||||
|
||||
func PIAGeoChoices() []string {
|
||||
return []string{"AU Melbourne", "AU Perth", "AU Sydney", "Austria", "Belgium", "CA Montreal", "CA Toronto", "CA Vancouver", "Czech Republic", "DE Berlin", "DE Frankfurt", "Denmark", "Finland", "France", "Hong Kong", "Hungary", "India", "Ireland", "Israel", "Italy", "Japan", "Luxembourg", "Mexico", "Netherlands", "New Zealand", "Norway", "Poland", "Romania", "Singapore", "Spain", "Sweden", "Switzerland", "UAE", "UK London", "UK Manchester", "UK Southampton", "US Atlanta", "US California", "US Chicago", "US Denver", "US East", "US Florida", "US Houston", "US Las Vegas", "US New York City", "US Seattle", "US Silicon Valley", "US Texas", "US Washington DC", "US West"}
|
||||
}
|
||||
|
||||
func PIAGeoToSubdomainMapping(region models.PIARegion) (subdomain string, err error) {
|
||||
mapping := map[models.PIARegion]string{
|
||||
models.PIARegion("AU Melbourne"): "au-melbourne",
|
||||
models.PIARegion("AU Perth"): "au-perth",
|
||||
models.PIARegion("AU Sydney"): "au-sydney",
|
||||
models.PIARegion("Austria"): "austria",
|
||||
models.PIARegion("Belgium"): "belgium",
|
||||
models.PIARegion("CA Montreal"): "ca-montreal",
|
||||
models.PIARegion("CA Toronto"): "ca-toronto",
|
||||
models.PIARegion("CA Vancouver"): "ca-vancouver",
|
||||
models.PIARegion("Czech Republic"): "czech",
|
||||
models.PIARegion("DE Berlin"): "de-berlin",
|
||||
models.PIARegion("DE Frankfurt"): "de-frankfurt",
|
||||
models.PIARegion("Denmark"): "denmark",
|
||||
models.PIARegion("Finland"): "fi",
|
||||
models.PIARegion("France"): "france",
|
||||
models.PIARegion("Hong Kong"): "hk",
|
||||
models.PIARegion("Hungary"): "hungary",
|
||||
models.PIARegion("India"): "in",
|
||||
models.PIARegion("Ireland"): "ireland",
|
||||
models.PIARegion("Israel"): "israel",
|
||||
models.PIARegion("Italy"): "italy",
|
||||
models.PIARegion("Japan"): "japan",
|
||||
models.PIARegion("Luxembourg"): "lu",
|
||||
models.PIARegion("Mexico"): "mexico",
|
||||
models.PIARegion("Netherlands"): "nl",
|
||||
models.PIARegion("New Zealand"): "nz",
|
||||
models.PIARegion("Norway"): "no",
|
||||
models.PIARegion("Poland"): "poland",
|
||||
models.PIARegion("Romania"): "ro",
|
||||
models.PIARegion("Singapore"): "sg",
|
||||
models.PIARegion("Spain"): "spain",
|
||||
models.PIARegion("Sweden"): "sweden",
|
||||
models.PIARegion("Switzerland"): "swiss",
|
||||
models.PIARegion("UAE"): "ae",
|
||||
models.PIARegion("UK London"): "uk-london",
|
||||
models.PIARegion("UK Manchester"): "uk-manchester",
|
||||
models.PIARegion("UK Southampton"): "uk-southampton",
|
||||
models.PIARegion("US Atlanta"): "us-atlanta",
|
||||
models.PIARegion("US California"): "us-california",
|
||||
models.PIARegion("US Chicago"): "us-chicago",
|
||||
models.PIARegion("US Denver"): "us-denver",
|
||||
models.PIARegion("US East"): "us-east",
|
||||
models.PIARegion("US Florida"): "us-florida",
|
||||
models.PIARegion("US Houston"): "us-houston",
|
||||
models.PIARegion("US Las Vegas"): "us-lasvegas",
|
||||
models.PIARegion("US New York City"): "us-newyorkcity",
|
||||
models.PIARegion("US Seattle"): "us-seattle",
|
||||
models.PIARegion("US Silicon Valley"): "us-siliconvalley",
|
||||
models.PIARegion("US Texas"): "us-texas",
|
||||
models.PIARegion("US Washington DC"): "us-washingtondc",
|
||||
models.PIARegion("US West"): "us-west",
|
||||
}
|
||||
subdomain, ok := mapping[region]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("PIA region %q does not exist and can only be one of ", strings.Join(PIAGeoChoices(), ","))
|
||||
}
|
||||
return subdomain, nil
|
||||
}
|
||||
|
||||
const (
|
||||
PIAPortForwardURL models.URL = "http://209.222.18.222:2000"
|
||||
)
|
||||
13
internal/constants/splash.go
Normal file
13
internal/constants/splash.go
Normal file
@@ -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"
|
||||
)
|
||||
20
internal/constants/tinyproxy.go
Normal file
20
internal/constants/tinyproxy.go
Normal file
@@ -0,0 +1,20 @@
|
||||
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"
|
||||
// TinyProxyConnectLevel is the info log level for TinyProxy
|
||||
TinyProxyConnectLevel models.TinyProxyLogLevel = "Connect"
|
||||
// TinyProxyNoticeLevel is the info log level for TinyProxy
|
||||
TinyProxyNoticeLevel models.TinyProxyLogLevel = "Notice"
|
||||
// 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"
|
||||
)
|
||||
21
internal/constants/vpn.go
Normal file
21
internal/constants/vpn.go
Normal file
@@ -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"
|
||||
)
|
||||
40
internal/dns/command.go
Normal file
40
internal/dns/command.go
Normal file
@@ -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, waitFn func() error, 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, waitFn, err = c.commander.Start("unbound", args...)
|
||||
return stdout, waitFn, 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
|
||||
}
|
||||
70
internal/dns/command_test.go
Normal file
70
internal/dns/command_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
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, waitFn, err := c.Start(2)
|
||||
assert.Nil(t, stdout)
|
||||
assert.Nil(t, waitFn)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
288
internal/dns/conf.go
Normal file
288
internal/dns/conf.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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.Ownership(uid, gid),
|
||||
files.Permissions(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": fmt.Sprintf("%q", constants.CACertificates),
|
||||
"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",
|
||||
}
|
||||
if settings.Caching {
|
||||
forwardZoneSection["forward-no-cache"] = "no"
|
||||
} else {
|
||||
forwardZoneSection["forward-no-cache"] = "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 {
|
||||
providerData, ok := constants.DNSProviderMapping()[provider]
|
||||
if !ok {
|
||||
return nil, warnings, fmt.Errorf("DNS provider %q does not have associated data", provider)
|
||||
} else if !providerData.SupportsTLS {
|
||||
return nil, warnings, fmt.Errorf("DNS provider %q does not support DNS over TLS", provider)
|
||||
}
|
||||
for _, IP := range providerData.IPs {
|
||||
forwardZoneLines = append(forwardZoneLines,
|
||||
fmt.Sprintf(" forward-addr: %s@853#%s", IP.String(), providerData.Host))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
520
internal/dns/conf_test.go
Normal file
520
internal/dns/conf_test.go
Normal file
@@ -0,0 +1,520 @@
|
||||
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,
|
||||
Caching: true,
|
||||
}
|
||||
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-no-cache: no
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
43
internal/dns/dns.go
Normal file
43
internal/dns/dns.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"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)
|
||||
UseDNSInternally(IP net.IP)
|
||||
UseDNSSystemWide(IP net.IP) error
|
||||
Start(logLevel uint8) (stdout io.ReadCloser, waitFn func() error, err error)
|
||||
WaitForUnbound() (err error)
|
||||
Version() (version string, err error)
|
||||
}
|
||||
|
||||
type configurator struct {
|
||||
logger logging.Logger
|
||||
client network.Client
|
||||
fileManager files.FileManager
|
||||
commander command.Commander
|
||||
lookupIP func(host string) ([]net.IP, error)
|
||||
}
|
||||
|
||||
func NewConfigurator(logger logging.Logger, client network.Client, fileManager files.FileManager) Configurator {
|
||||
return &configurator{
|
||||
logger: logger,
|
||||
client: client,
|
||||
fileManager: fileManager,
|
||||
commander: command.NewCommander(),
|
||||
lookupIP: net.LookupIP,
|
||||
}
|
||||
}
|
||||
47
internal/dns/nameserver.go
Normal file
47
internal/dns/nameserver.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/private-internet-access-docker/internal/constants"
|
||||
)
|
||||
|
||||
// UseDNSInternally is to change the Go program DNS only
|
||||
func (c *configurator) UseDNSInternally(IP net.IP) {
|
||||
c.logger.Info("%s: using DNS address %s internally", logPrefix, IP.String())
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{}
|
||||
return d.DialContext(ctx, "udp", net.JoinHostPort(IP.String(), "53"))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// UseDNSSystemWide changes the nameserver to use for DNS system wide
|
||||
func (c *configurator) UseDNSSystemWide(IP net.IP) error {
|
||||
c.logger.Info("%s: using DNS address %s system wide", logPrefix, IP.String())
|
||||
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 " + IP.String()
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
lines = append(lines, "nameserver "+IP.String())
|
||||
}
|
||||
data = []byte(strings.Join(lines, "\n"))
|
||||
return c.fileManager.WriteToFile(string(constants.ResolvConf), data)
|
||||
}
|
||||
73
internal/dns/nameserver_test.go
Normal file
73
internal/dns/nameserver_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"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_UseDNSSystemWide(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: using DNS address %s system wide", logPrefix, "127.0.0.1").Once()
|
||||
c := &configurator{
|
||||
fileManager: fileManager,
|
||||
logger: logger,
|
||||
}
|
||||
err := c.UseDNSSystemWide(net.IP{127, 0, 0, 1})
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
38
internal/dns/roots.go
Normal file
38
internal/dns/roots.go
Normal file
@@ -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.Ownership(uid, gid),
|
||||
files.Permissions(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.Ownership(uid, gid),
|
||||
files.Permissions(0400))
|
||||
}
|
||||
144
internal/dns/roots_test.go
Normal file
144
internal/dns/roots_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
20
internal/dns/wait.go
Normal file
20
internal/dns/wait.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *configurator) WaitForUnbound() (err error) {
|
||||
const maxTries = 10
|
||||
const hostToResolve = "github.com"
|
||||
for try := 1; try <= maxTries; try++ {
|
||||
_, err := c.lookupIP(hostToResolve)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
c.logger.Warn("could not resolve %s (try %d of %d)", hostToResolve, try, maxTries)
|
||||
time.Sleep(time.Duration(maxTries * 50 * time.Millisecond))
|
||||
}
|
||||
return fmt.Errorf("Unbound does not seem to be working after %d tries", maxTries)
|
||||
}
|
||||
40
internal/env/env.go
vendored
Normal file
40
internal/env/env.go
vendored
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
90
internal/env/env_test.go
vendored
Normal file
90
internal/env/env_test.go
vendored
Normal file
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
43
internal/firewall/firewall.go
Normal file
43
internal/firewall/firewall.go
Normal file
@@ -0,0 +1,43 @@
|
||||
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
|
||||
AllowAnyIncomingOnPort(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,
|
||||
}
|
||||
}
|
||||
138
internal/firewall/iptables.go
Normal file
138
internal/firewall/iptables.go
Normal file
@@ -0,0 +1,138 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *configurator) AllowAnyIncomingOnPort(port uint16) error {
|
||||
c.logger.Info("%s: accepting any input traffic on port %d", logPrefix, port)
|
||||
return c.runIptablesInstructions([]string{
|
||||
fmt.Sprintf("-A INPUT -p tcp --dport %d -j ACCEPT", port),
|
||||
fmt.Sprintf("-A INPUT -p udp --dport %d -j ACCEPT", port),
|
||||
})
|
||||
}
|
||||
88
internal/firewall/route.go
Normal file
88
internal/firewall/route.go
Normal file
@@ -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
|
||||
}
|
||||
171
internal/firewall/route_test.go
Normal file
171
internal/firewall/route_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
24
internal/healthcheck/healthcheck.go
Normal file
24
internal/healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func HealthCheck() error {
|
||||
// DNS, HTTP and HTTPs check on github.com
|
||||
connectivty := network.NewConnectivity(3 * time.Second)
|
||||
errs := connectivty.Checks("github.com")
|
||||
if len(errs) > 0 {
|
||||
var errsStr []string
|
||||
for _, err := range errs {
|
||||
errsStr = append(errsStr, err.Error())
|
||||
}
|
||||
return fmt.Errorf("Multiple errors: %s", strings.Join(errsStr, "; "))
|
||||
}
|
||||
// TODO check IP address is in the right region
|
||||
return nil
|
||||
}
|
||||
24
internal/models/alias.go
Normal file
24
internal/models/alias.go
Normal file
@@ -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
|
||||
// DNSHost is the DNS host to use for TLS validation
|
||||
DNSHost string
|
||||
// PIAEncryption defines the level of encryption for communication with PIA servers
|
||||
PIAEncryption string
|
||||
// PIARegion is used to define 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
|
||||
)
|
||||
10
internal/models/dns.go
Normal file
10
internal/models/dns.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
import "net"
|
||||
|
||||
// DNSProviderData contains information for a DNS provider
|
||||
type DNSProviderData struct {
|
||||
IPs []net.IP
|
||||
SupportsTLS bool
|
||||
Host DNSHost
|
||||
}
|
||||
23
internal/openvpn/auth.go
Normal file
23
internal/openvpn/auth.go
Normal file
@@ -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.Ownership(uid, gid),
|
||||
files.Permissions(0400))
|
||||
}
|
||||
28
internal/openvpn/command.go
Normal file
28
internal/openvpn/command.go
Normal file
@@ -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, waitFn func() error, err error) {
|
||||
c.logger.Info("%s: starting openvpn", logPrefix)
|
||||
stdout, _, waitFn, err = c.commander.Start("openvpn", "--config", string(constants.OpenVPNConf))
|
||||
return stdout, waitFn, 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
|
||||
}
|
||||
41
internal/openvpn/openvpn.go
Normal file
41
internal/openvpn/openvpn.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/golibs/command"
|
||||
"github.com/qdm12/golibs/files"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const logPrefix = "openvpn configurator"
|
||||
|
||||
type Configurator interface {
|
||||
Version() (string, error)
|
||||
WriteAuthFile(user, password string, uid, gid int) error
|
||||
CheckTUN() error
|
||||
CreateTUN() error
|
||||
Start() (stdout io.ReadCloser, waitFn func() error, 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)
|
||||
mkDev func(major uint32, minor uint32) uint64
|
||||
mkNod func(path string, mode uint32, dev int) error
|
||||
}
|
||||
|
||||
func NewConfigurator(logger logging.Logger, fileManager files.FileManager) Configurator {
|
||||
return &configurator{
|
||||
fileManager: fileManager,
|
||||
logger: logger,
|
||||
commander: command.NewCommander(),
|
||||
openFile: os.OpenFile,
|
||||
mkDev: unix.Mkdev,
|
||||
mkNod: unix.Mknod,
|
||||
}
|
||||
}
|
||||
37
internal/openvpn/tun.go
Normal file
37
internal/openvpn/tun.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/private-internet-access-docker/internal/constants"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (c *configurator) CreateTUN() error {
|
||||
c.logger.Info("%s: creating %s", logPrefix, constants.TunnelDevice)
|
||||
if err := c.fileManager.CreateDir("/dev/net"); err != nil {
|
||||
return err
|
||||
}
|
||||
dev := c.mkDev(10, 200)
|
||||
if err := c.mkNod(string(constants.TunnelDevice), unix.S_IFCHR, int(dev)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.fileManager.SetUserPermissions(string(constants.TunnelDevice), 666); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
118
internal/params/dns.go
Normal file
118
internal/params/dns.go
Normal file
@@ -0,0 +1,118 @@
|
||||
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_SURVEILLANCE
|
||||
// and BLOCK_NSA for retrocompatibility
|
||||
func (p *paramsReader) GetDNSSurveillanceBlocking() (blocking bool, err error) {
|
||||
// Retro-compatibility
|
||||
s, err := p.envParams.GetEnv("BLOCK_NSA")
|
||||
if err != nil {
|
||||
return false, err
|
||||
} else if len(s) != 0 {
|
||||
p.logger.Warn("You are using the old environment variable BLOCK_NSA, please consider changing it to BLOCK_SURVEILLANCE")
|
||||
return p.envParams.GetOnOff("BLOCK_NSA", libparams.Compulsory())
|
||||
}
|
||||
return p.envParams.GetOnOff("BLOCK_SURVEILLANCE", 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
|
||||
}
|
||||
|
||||
// GetDNSOverTLSCaching obtains if Unbound caching should be enable or not
|
||||
// from the environment variable DOT_CACHING
|
||||
func (p *paramsReader) GetDNSOverTLSCaching() (caching bool, err error) {
|
||||
return p.envParams.GetOnOff("DOT_CACHING")
|
||||
}
|
||||
|
||||
// GetDNSOverTLSPrivateAddresses obtains if Unbound caching should be enable or not
|
||||
// from the environment variable DOT_PRIVATE_ADDRESS
|
||||
func (p *paramsReader) GetDNSOverTLSPrivateAddresses() (privateAddresses []string) {
|
||||
s, _ := p.envParams.GetEnv("DOT_PRIVATE_ADDRESS")
|
||||
for _, s := range strings.Split(s, ",") {
|
||||
privateAddresses = append(privateAddresses, s)
|
||||
}
|
||||
return privateAddresses
|
||||
}
|
||||
29
internal/params/firewall.go
Normal file
29
internal/params/firewall.go
Normal file
@@ -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
|
||||
}
|
||||
13
internal/params/openvpn.go
Normal file
13
internal/params/openvpn.go
Normal file
@@ -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
|
||||
}
|
||||
77
internal/params/params.go
Normal file
77
internal/params/params.go
Normal file
@@ -0,0 +1,77 @@
|
||||
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)
|
||||
GetDNSOverTLSCaching() (caching bool, 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)
|
||||
GetDNSOverTLSPrivateAddresses() (privateAddresses []string)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewParamsReader returns a paramsReadeer object to read parameters from
|
||||
// environment variables
|
||||
func NewParamsReader(logger logging.Logger) ParamsReader {
|
||||
return ¶msReader{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
logger: logger,
|
||||
verifier: verification.NewVerifier(),
|
||||
unsetEnv: os.Unsetenv,
|
||||
}
|
||||
}
|
||||
85
internal/params/pia.go
Normal file
85
internal/params/pia.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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 := constants.PIAGeoChoices()
|
||||
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
|
||||
}
|
||||
40
internal/params/shadowsocks.go
Normal file
40
internal/params/shadowsocks.go
Normal file
@@ -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")
|
||||
}
|
||||
94
internal/params/tinyproxy.go
Normal file
94
internal/params/tinyproxy.go
Normal file
@@ -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", "Connect", "Notice", "Warning", "Error", "Critical"}, libparams.Compulsory())
|
||||
return models.TinyProxyLogLevel(s), err
|
||||
}
|
||||
s, err = p.envParams.GetValueIfInside("TINYPROXY_LOG", []string{"Info", "Connect", "Notice", "Warning", "Error", "Critical"}, libparams.Default("Connect"))
|
||||
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")
|
||||
}
|
||||
20
internal/params/version.go
Normal file
20
internal/params/version.go
Normal file
@@ -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
|
||||
}
|
||||
90
internal/pia/conf.go
Normal file
90
internal/pia/conf.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package pia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"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) BuildConf(region models.PIARegion, protocol models.NetworkProtocol,
|
||||
encryption models.PIAEncryption, uid, gid int) (IPs []net.IP, port uint16, err error) {
|
||||
var X509CRL, certificate string // depends on encryption
|
||||
var cipherAlgo, authAlgo string // depends on encryption
|
||||
if encryption == constants.PIAEncryptionNormal {
|
||||
cipherAlgo = "aes-128-cbc"
|
||||
authAlgo = "sha1"
|
||||
X509CRL = constants.PIAX509CRL_NORMAL
|
||||
certificate = constants.PIACertificate_NORMAL
|
||||
if protocol == constants.UDP {
|
||||
port = 1198
|
||||
} else {
|
||||
port = 502
|
||||
}
|
||||
} else { // strong
|
||||
cipherAlgo = "aes-256-cbc"
|
||||
authAlgo = "sha256"
|
||||
X509CRL = constants.PIAX509CRL_STRONG
|
||||
certificate = constants.PIACertificate_STRONG
|
||||
if protocol == constants.UDP {
|
||||
port = 1197
|
||||
} else {
|
||||
port = 501
|
||||
}
|
||||
}
|
||||
subdomain, err := constants.PIAGeoToSubdomainMapping(region)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
IPs, err = c.lookupIP(subdomain + ".privateinternetaccess.com")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
lines := []string{
|
||||
"client",
|
||||
"dev tun",
|
||||
"nobind",
|
||||
"persist-key",
|
||||
"persist-tun",
|
||||
"tls-client",
|
||||
"remote-cert-tls server",
|
||||
"compress",
|
||||
"verb 1", // TODO env variable
|
||||
"reneg-sec 0",
|
||||
// Added constant values
|
||||
"mute-replay-warnings",
|
||||
"user nonrootuser",
|
||||
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
|
||||
"auth-retry nointeract",
|
||||
"disable-occ",
|
||||
"remote-random",
|
||||
|
||||
// Modified variables
|
||||
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
|
||||
fmt.Sprintf("proto %s", string(protocol)),
|
||||
fmt.Sprintf("cipher %s", cipherAlgo),
|
||||
fmt.Sprintf("auth %s", authAlgo),
|
||||
}
|
||||
for _, IP := range IPs {
|
||||
lines = append(lines, fmt.Sprintf("remote %s %d", IP.String(), port))
|
||||
}
|
||||
lines = append(lines, []string{
|
||||
"<crl-verify>",
|
||||
"-----BEGIN X509 CRL-----",
|
||||
X509CRL,
|
||||
"-----END X509 CRL-----",
|
||||
"</crl-verify>",
|
||||
}...)
|
||||
lines = append(lines, []string{
|
||||
"<ca>",
|
||||
"-----BEGIN CERTIFICATE-----",
|
||||
certificate,
|
||||
"-----END CERTIFICATE-----",
|
||||
"</ca>",
|
||||
"",
|
||||
}...)
|
||||
err = c.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400))
|
||||
return IPs, port, err
|
||||
}
|
||||
39
internal/pia/pia.go
Normal file
39
internal/pia/pia.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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 {
|
||||
BuildConf(region models.PIARegion, protocol models.NetworkProtocol,
|
||||
encryption models.PIAEncryption, uid, gid int) (IPs []net.IP, port uint16, err error)
|
||||
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}
|
||||
}
|
||||
46
internal/pia/portforward.go
Normal file
46
internal/pia/portforward.go
Normal file
@@ -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, body.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)
|
||||
}
|
||||
107
internal/settings/dns.go
Normal file
107
internal/settings/dns.go
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
Caching bool
|
||||
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"
|
||||
}
|
||||
caching, blockMalicious, blockSurveillance, blockAds := "disabled", "disabed", "disabed", "disabed"
|
||||
if d.Caching {
|
||||
caching = "enabled"
|
||||
}
|
||||
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 |--"),
|
||||
"Caching: " + caching,
|
||||
"Block malicious: " + blockMalicious,
|
||||
"Block surveillance: " + blockSurveillance,
|
||||
"Block ads: " + blockAds,
|
||||
"Allowed hostnames:\n |--" + strings.Join(d.AllowedHostnames, "\n |--"),
|
||||
"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.Caching, err = params.GetDNSOverTLSCaching()
|
||||
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 = params.GetDNSOverTLSPrivateAddresses()
|
||||
return settings, nil
|
||||
}
|
||||
34
internal/settings/firewall.go
Normal file
34
internal/settings/firewall.go
Normal file
@@ -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
|
||||
}
|
||||
30
internal/settings/openvpn.go
Normal file
30
internal/settings/openvpn.go
Normal file
@@ -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|--")
|
||||
}
|
||||
72
internal/settings/pia.go
Normal file
72
internal/settings/pia.go
Normal file
@@ -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
|
||||
}
|
||||
60
internal/settings/settings.go
Normal file
60
internal/settings/settings.go
Normal file
@@ -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
|
||||
}
|
||||
48
internal/settings/shadowsocks.go
Normal file
48
internal/settings/shadowsocks.go
Normal file
@@ -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
|
||||
}
|
||||
59
internal/settings/tinyproxy.go
Normal file
59
internal/settings/tinyproxy.go
Normal file
@@ -0,0 +1,59 @@
|
||||
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{
|
||||
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
|
||||
}
|
||||
40
internal/shadowsocks/command.go
Normal file
40
internal/shadowsocks/command.go
Normal file
@@ -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, waitFn func() error, 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, _, waitFn, err = c.commander.Start("ss-server", args...)
|
||||
return stdout, waitFn, 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
|
||||
}
|
||||
49
internal/shadowsocks/conf.go
Normal file
49
internal/shadowsocks/conf.go
Normal file
@@ -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.Ownership(uid, gid),
|
||||
files.Permissions(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
|
||||
}
|
||||
79
internal/shadowsocks/conf_test.go
Normal file
79
internal/shadowsocks/conf_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
27
internal/shadowsocks/shadowsocks.go
Normal file
27
internal/shadowsocks/shadowsocks.go
Normal file
@@ -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, waitFn func() error, 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()}
|
||||
}
|
||||
56
internal/splash/splash.go
Normal file
56
internal/splash/splash.go
Normal file
@@ -0,0 +1,56 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// Splash returns the welcome spash message
|
||||
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(":mega: ") + 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",
|
||||
}
|
||||
}
|
||||
26
internal/tinyproxy/command.go
Normal file
26
internal/tinyproxy/command.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package tinyproxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *configurator) Start() (stdout io.ReadCloser, waitFn func() error, err error) {
|
||||
c.logger.Info("%s: starting tinyproxy server", logPrefix)
|
||||
stdout, _, waitFn, err = c.commander.Start("tinyproxy", "-d")
|
||||
return stdout, waitFn, 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
|
||||
}
|
||||
48
internal/tinyproxy/conf.go
Normal file
48
internal/tinyproxy/conf.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package tinyproxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"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.Ownership(uid, gid),
|
||||
files.Permissions(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)
|
||||
}
|
||||
sort.Slice(lines, func(i, j int) bool {
|
||||
return lines[i] < lines[j]
|
||||
})
|
||||
return lines
|
||||
}
|
||||
68
internal/tinyproxy/conf_test.go
Normal file
68
internal/tinyproxy/conf_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package tinyproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/private-internet-access-docker/internal/constants"
|
||||
"github.com/qdm12/private-internet-access-docker/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_generateConf(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
logLevel models.TinyProxyLogLevel
|
||||
port uint16
|
||||
user string
|
||||
password string
|
||||
lines []string
|
||||
}{
|
||||
"No credentials": {
|
||||
logLevel: constants.TinyProxyInfoLevel,
|
||||
port: 2000,
|
||||
lines: []string{
|
||||
"DefaultErrorFile \"/usr/share/tinyproxy/default.html\"",
|
||||
"DisableViaHeader Yes",
|
||||
"Group tinyproxy",
|
||||
"LogLevel Info",
|
||||
"MaxClients 100",
|
||||
"MaxRequestsPerChild 0",
|
||||
"MaxSpareServers 20",
|
||||
"MinSpareServers 5",
|
||||
"Port 2000",
|
||||
"StartServers 10",
|
||||
"Timeout 600",
|
||||
"User nonrootuser",
|
||||
},
|
||||
},
|
||||
"With credentials": {
|
||||
logLevel: constants.TinyProxyErrorLevel,
|
||||
port: 2000,
|
||||
user: "abc",
|
||||
password: "def",
|
||||
lines: []string{
|
||||
"BasicAuth abc def",
|
||||
"DefaultErrorFile \"/usr/share/tinyproxy/default.html\"",
|
||||
"DisableViaHeader Yes",
|
||||
"Group tinyproxy",
|
||||
"LogLevel Error",
|
||||
"MaxClients 100",
|
||||
"MaxRequestsPerChild 0",
|
||||
"MaxSpareServers 20",
|
||||
"MinSpareServers 5",
|
||||
"Port 2000",
|
||||
"StartServers 10",
|
||||
"Timeout 600",
|
||||
"User nonrootuser",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lines := generateConf(tc.logLevel, tc.port, tc.user, tc.password)
|
||||
assert.Equal(t, tc.lines, lines)
|
||||
})
|
||||
}
|
||||
}
|
||||
28
internal/tinyproxy/tinyproxy.go
Normal file
28
internal/tinyproxy/tinyproxy.go
Normal file
@@ -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, waitFn func() error, 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()}
|
||||
}
|
||||
@@ -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"
|
||||
BIN
readme/title.png
BIN
readme/title.png
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
1582
title.svg
Normal file
1582
title.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 98 KiB |
58
unbound.conf
58
unbound.conf
@@ -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
|
||||
Reference in New Issue
Block a user