It requires some doing, but it can be done.
First things first, proxy_protocol has to be supported by the backend you're connecting to for it to work. It uses http headers and thus only works for http connections (it won't work for dns requests to the pihole).
A transparent proxy is what you want; Linux kernel support makes this rather trivial, though the application will have to support it. If it doesn't, then a little hoop jumping will be needed to achieve the same thing.
As previously recommended you could set up a transparent proxy following the steps outlined here:
https://www.nginx.com/blog/ip-transparency-direct-server-return-nginx-plus-transparent-proxy/
I followed this guide using docker containers, though there are a couple of caveats:
-
You don't need to run nginx worker processes as root.
-
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
should be:
iptables -t nat -A POSTROUTING -s {container network subnet here} -o eth0 -j MASQUERADE
Here's a rough sketch of setting it up:
./nginx/nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
# I'm assuming you're comfortable reverse proxying the web UI
}
stream {
# work around for nginx shutting down if the upstream isn't resolvable at runtime
map $remote_addr $dns_upstream {
default "pihole:53";
}
server {
resolver 127.0.0.11 valid=30s;
listen 53;
listen 53 udp;
proxy_bind $remote_addr transparent;
proxy_responses 1;
proxy_timeout 1s;
proxy_pass $dns_upstream;
}
}
./nginx/nginx.dockerfile
FROM arm32v7/nginx:stable as dev
RUN apt-get update &&\
apt-get install -qq iproute2 iptables &&\
rm -rf /var/cache/apt/archives /var/lib/apt/lists/*
CMD SUBNET=$(ip route | grep -oP "default via \K(\d*\.){3}") &&\
MASK=$(ip route | grep -oP "${SUBNET}0/\K(\d*)") &&\
ip rule add fwmark 1 lookup 100 &&\
ip route add local 0.0.0.0/0 dev lo table 100 &&\
iptables -t mangle -A PREROUTING -p tcp -s "${SUBNET}0/${MASK}" --sport 80 -j MARK --set-xmark 0x1/0xffffffff &&\
iptables -t mangle -A PREROUTING -p udp -s "${SUBNET}0/${MASK}" --sport 53 -j MARK --set-xmark 0x1/0xffffffff &&\
iptables -t mangle -A PREROUTING -p tcp -s "${SUBNET}0/${MASK}" --sport 53 -j MARK --set-xmark 0x1/0xffffffff &&\
iptables -t nat -A POSTROUTING -s "${SUBNET}0/${MASK}" -o eth0 -j MASQUERADE &&\
nginx -g 'daemon off;'
./pihole/pihole.dockerfile
FROM pihole/pihole:4.3.2-1_armhf as dev
RUN sed -i '1 a \
SUBNET=$(ip route | grep -oP "default via \\K(\\d*\\.){3}")\n\
ip route del default\n\
ip route add default via "${SUBNET}2"\n\
/opt/dnscrypt-proxy -service start\n\
' /etc/cont-init.d/20-start.sh
./docker-compose.yml
version: '3.7'
services:
tproxy:
build:
context: ./nginx
dockerfile: nginx.dockerfile
cap_add:
- NET_ADMIN
container_name: tproxy
image: tproxy:latest
networks:
lan:
ipv4_address: 172.16.0.2
ports:
- mode: host
protocol: udp
published: 53
target: 53
- mode: host
protocol: tcp
published: 53
target: 53
- mode: host
protocol: tcp
published: 80
target: 80
restart: unless-stopped
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:cached
pihole:
build:
context: ./pihole
dockerfile: pihole.dockerfile
image: proxied_pihole:latest
cap_add:
- NET_ADMIN
container_name: pihole
depends_on:
- tproxy
environment:
DNSMASQ_LISTENING: all
networks:
- lan
restart: unless-stopped
networks:
lan:
ipam:
config:
- subnet: 172.16.0.0/24
driver: default
You bake iptables and iproute into the nginx image and then exec the desired commands at container start up. I grep the subnet/mask at runtime rather than hard code it because it depends on how you define the network in docker-compose.yml
.
Note, I'm using armv7 images here, you may need to change that.
docker-compose up -d
brings it all up
docker-compose down
tears it down
docker logs <container id/name>
brings up the associated logs
docker container ls -a
lists the names and statuses of the running containers