diff --git a/Dockerfile b/Dockerfile index c6f42c8..d91e2a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,45 @@ -FROM silarsis/base -MAINTAINER Kevin Littlejohn -RUN apt-get -yq update +FROM ubuntu:trusty-20190515 -RUN apt-get -yq install squid iptables -ADD squid.conf /etc/squid3/squid.conf -ADD start_squid.sh /usr/local/bin/start_squid.sh +MAINTAINER Kevin Littlejohn , \ + Alex Fraser -EXPOSE 3128 +# Install base dependencies. +WORKDIR /root +RUN sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list +RUN export DEBIAN_FRONTEND=noninteractive TERM=linux \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + dpkg-dev \ + iptables \ + libssl-dev \ + patch \ + squid-langpack \ + ssl-cert \ + && apt-get source -y squid3 squid-langpack \ + && apt-get build-dep -y squid3 squid-langpack + +# Customise and build Squid. +# It's silly, but run dpkg-buildpackage again if it fails the first time. This +# is needed because sometimes the `configure` script is busy when building in +# Docker after autoconf sets its mode +x. +COPY squid3.patch mime.conf /root/ +RUN cd squid3-3.* \ + && patch -p1 < /root/squid3.patch \ + && export NUM_PROCS=`grep -c ^processor /proc/cpuinfo` \ + && (dpkg-buildpackage -b -j${NUM_PROCS} \ + || dpkg-buildpackage -b -j${NUM_PROCS}) \ + && DEBIAN_FRONTEND=noninteractive TERM=linux dpkg -i \ + ../squid3-common_3.*_all.deb \ + ../squid3_3.*.deb \ + && mkdir -p /etc/squid3/ssl_cert \ + && cat /root/mime.conf >> /usr/share/squid3/mime.conf + +COPY squid.conf /etc/squid3/squid.conf +COPY start_squid.sh /usr/local/bin/start_squid.sh + +VOLUME /var/spool/squid3 /etc/squid3/ssl_cert +EXPOSE 3128 3129 3130 CMD ["/usr/local/bin/start_squid.sh"] diff --git a/README.md b/README.md index 6b2976b..e2b92ca 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,104 @@ -docker-proxy -============ +# docker-proxy -Transparent proxy for docker containers, run in a docker container - https://hub.docker.com/r/silarsis/docker-proxy/ +Transparent caching proxy server for Docker containers, run in a Docker +container. It can speed up the dependency-fetching part of your application +build process. ## Instructions for Use -The "run.sh" script will run the container and setup the appropriate iptables -and ip routing rules. +First check out the code. Then build with: -NOTE: This project is _not_ designed to be run with a simple "docker run" - it -requires the "run.sh" script to be run on the docker host, so it can adjust the -routing rules. You will need to check this code out from -https://github.com/silarsis/docker-proxy/ -and run the "run.sh" script on the host (for OS X, that's on your boot2docker or -similar host). +``` +sudo docker build -t docker-proxy . +``` + +Then run with: + +``` +./run.sh +``` + +The script will start the container and set up the appropriate +routing rules. Your other Docker containers will automatically use +the proxy, whether or not they were already running. When you are finished, +just press CtrlC to stop the proxy. + +NOTE: This project is _not_ designed to be run with a simple `docker run` - it +requires `run.sh` to be run on the docker host, so it can adjust the +routing rules. You will need to check this code out +and run `run.sh` on the host. For OS X, that's on your [boot2docker], +Docker Machine or similar host). To start under [Docker Machine] on OS X: + +``` +docker-machine scp run.sh default:/home/docker/run.sh +docker-machine ssh default +sh ./run.sh +``` + +[boot2docker]: http://boot2docker.io/ +[Docker Machine]: https://docs.docker.com/machine/get-started/ ## Overview -The run.sh script will fire up a docker container running squid, with -appropriate iptables rules in place for transparent proxying. It will also +`run.sh` will fire up a Docker container running Squid, with +appropriate firewall rules in place for transparent proxying. It will also configure port-based routing on the main host such that any traffic from a -docker container to port 80 routes via the transparent proxy container. +Docker container to port 80 routes via the transparent proxy container. It +requires `sudo` access to perform the firewall changes, and it will prompt you +for your password as appropriate. -The run.sh script is designed to run in the foreground, because when the +`run.sh` is designed to run in the foreground, because when the container terminates it needs to remove the rules that were redirecting the traffic. -If you want to see squid in operation, you can (in another terminal) attach -to the squid container - it is tailing the access log, so will show a record -of requests made. +If you want to see Squid in operation, you can (in another terminal) attach +to the `docker-proxy` container - it is tailing the access log, so will show a +record of requests made. -## Notes +## HTTPS Support + +The proxy server supports HTTPS caching via Squid's [SSL Bump] feature. To +enable it, start with: + +``` +./run.sh ssl +``` -* This script assumes you have "sudo" access to perform the iptables changes +The server will decrypt traffic from the server and encrypt it again using its +own root certificate. HTTPS connections from your other Docker containers will +fail until you install the root certificate. To install it: + + 1. Install the `ca-certificates` package (Debian/Ubuntu images) + 2. Run [`detect-proxy.sh`] + +Those steps can be performed in a running container (for testing), or you can +add them to your `Dockerfile`. `detect-proxy.sh` can be run after you install +your OS packages with apt, because apt shouldn't need HTTPS. However, adding +PPAs with `add-apt-repository` will fail until the certificate is installed. See +[`test/Dockerfile`] for an example. + +Some programs don't use the OS's primary key store, such as `npm` and `pip`. +You may need to take extra steps for those programs. + +To test HTTPS support, do this in another console after starting the proxy: + +``` +cd test +sudo docker build -t test-proxy . +sudo docker run --rm test-proxy +# Should print "All tests passed" +``` + +[SSL Bump]: http://wiki.squid-cache.org/Features/SslBump +[`detect-proxy.sh`]: test/detect-proxy.sh +[`test/Dockerfile`]: test/Dockerfile + +## Notes -* the format of the run.sh script is a bit strange - that's because it's also used as part of a larger framework on my own machine. +This proxy configuration is intended to be used solely to speed +up development of Docker applications. **Do not** attempt to use this to +eavesdrop on other people's connections. -* There exists a real possibility this script will break your iptables or ip rules in some unexpected way. I've tried to be reasonably tidy and careful, but be aware that if things go wrong, the potential exists for all containers to lose the ability to download anything. +There exists a real possibility this script will break your `iptables` or `ip` +rules in some unexpected way. Be aware that if things go wrong, the potential +exists for all containers to lose the ability to download anything. diff --git a/mime.conf b/mime.conf new file mode 100644 index 0000000..46db037 --- /dev/null +++ b/mime.conf @@ -0,0 +1,5 @@ +# These dummy types configure Squid to serve the certificate as a static file. +# This allows clients to download the certificate from the proxy server. +# The .pem file is the certificate; the .der file is for browsers. +\.pem$ application/x-pem-file1 ca.pem - ascii +download +\.der$ application/x-der-file1 ca.der - image +download diff --git a/run.sh b/run.sh index 2bf43fa..d997f65 100755 --- a/run.sh +++ b/run.sh @@ -3,18 +3,19 @@ # Script to maintain ip rules on the host when starting up a transparent # proxy server for docker. -CACHEDIR="/tmp/squid3" # Change this to place the cache somewhere else +CACHEDIR=${CACHEDIR:-/var/lib/docker-proxy/cache} +CERTDIR=${CERTDIR:-/var/lib/docker-proxy/ssl} +CONTAINER_NAME=${CONTAINER_NAME:-docker-proxy} +if [ "$1" = 'ssl' ]; then + WITH_SSL=yes +else + WITH_SSL=no +fi set -e -# Guard for my own scripts -# Note, if you're running this script direct, it will rebuild if it can't see -# the image. -[ -z ${RUNNING_DRUN} ] && { - RUN_DOCKER="docker run" - CONTAINER_NAME='docker-proxy' - docker images | grep "^${CONTAINER_NAME} " >/dev/null || docker build -q --rm -t ${CONTAINER_NAME} "$(dirname $0)" -} +sudo docker images | grep -q "^${CONTAINER_NAME} " \ + || (echo "Build ${CONTAINER_NAME} image first" && exit 1) start_routing () { # Add a new route table that routes everything marked through the new container @@ -28,38 +29,49 @@ start_routing () { sudo ln -s /usr/local/etc/iproute2/rt_tables /etc/iproute2/rt_tables fi fi - ([ -e /etc/iproute2/rt_tables ] && grep TRANSPROXY /etc/iproute2/rt_tables >/dev/null) || \ - sudo sh -c "echo '1 TRANSPROXY' >> /etc/iproute2/rt_tables" - ip rule show | grep TRANSPROXY >/dev/null || \ - sudo ip rule add from all fwmark 0x1 lookup TRANSPROXY + ([ -e /etc/iproute2/rt_tables ] && grep -q TRANSPROXY /etc/iproute2/rt_tables) \ + || sudo sh -c "echo '1 TRANSPROXY' >> /etc/iproute2/rt_tables" + ip rule show | grep -q TRANSPROXY \ + || sudo ip rule add from all fwmark 0x1 lookup TRANSPROXY sudo ip route add default via "${IPADDR}" dev docker0 table TRANSPROXY - # Mark packets to port 80 external, so they route through the new route table - sudo iptables -t mangle -I PREROUTING -p tcp --dport 80 \! -s "${IPADDR}" -i docker0 -j MARK --set-mark 1 + # Mark packets to port 80 and 443 external, so they route through the new + # route table + COMMON_RULES="-t mangle -I PREROUTING -p tcp -i docker0 ! -s ${IPADDR} + -j MARK --set-mark 1" + echo "Redirecting HTTP to docker-proxy" + sudo iptables $COMMON_RULES --dport 80 + if [ "$WITH_SSL" = 'yes' ]; then + echo "Redirecting HTTPS to docker-proxy" + sudo iptables $COMMON_RULES --dport 443 + else + echo "Not redirecting HTTPS. To enable, re-run with the argument 'ssl'" + echo "CA certificate will be generated anyway, but it won't be used" + fi # Exemption rule to stop docker from masquerading traffic routed to the # transparent proxy sudo iptables -t nat -I POSTROUTING -o docker0 -s 172.17.0.0/16 -j ACCEPT } stop_routing () { - # Remove the appropriate rules - that is, those that mention the IP Address. - set +e - [ "x$IPADDR" != "x" ] && { - ip route show table TRANSPROXY | grep default >/dev/null && \ - sudo ip route del default table TRANSPROXY - sudo iptables -t mangle -L PREROUTING -n | grep 'tcp dpt:80 MARK set 0x1' >/dev/null && \ - sudo iptables -t mangle -D PREROUTING -p tcp --dport 80 \! -s "${IPADDR}" -i docker0 -j MARK --set-mark 1 + # Remove iptables rules. + set +e + ip route show table TRANSPROXY | grep -q default \ + && sudo ip route del default table TRANSPROXY + while true; do + rule_num=$(sudo iptables -t mangle -L PREROUTING -n --line-numbers \ + | grep -E 'MARK.*172\.17.*tcp \S+ MARK set 0x1' \ + | awk '{print $1}' \ + | head -n1) + [ -z "$rule_num" ] && break + sudo iptables -t mangle -D PREROUTING "$rule_num" + done sudo iptables -t nat -D POSTROUTING -o docker0 -s 172.17.0.0/16 -j ACCEPT 2>/dev/null - } - set -e + set -e } stop () { - # Ideally we'd leave the container around and re-use it, but I really - # need a nice way to query for a named container first. Doesn't cost much - # to create a new container anyway, especially given the cache volume is mapped. set +e - docker kill ${CONTAINER_NAME} >/dev/null 2>&1 - docker rm ${CONTAINER_NAME} >/dev/null 2>&1 + sudo docker rm -fv ${CONTAINER_NAME} >/dev/null 2>&1 set -e stop_routing } @@ -81,22 +93,27 @@ terminated () { run () { # Make sure we have a cache dir - if you're running in vbox you should # probably map this through to the host machine for persistence - mkdir -p "${CACHEDIR}" + mkdir -p "${CACHEDIR}" "${CERTDIR}" # Because we're named, make sure the container doesn't already exist stop - # Run and find the IP for the running container - CID=$(${RUN_DOCKER} --privileged -d -v "${CACHEDIR}":/var/spool/squid3 --name ${CONTAINER_NAME} ${CONTAINER_NAME}) - IPADDR=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${CID}) + # Run and find the IP for the running container. Bind the forward proxy port + # so clients can get the CA certificate. + CID=$(sudo docker run --privileged -d \ + --name ${CONTAINER_NAME} \ + --volume="${CACHEDIR}":/var/spool/squid3 \ + --volume="${CERTDIR}":/etc/squid3/ssl_cert \ + --publish=3128:3128 \ + ${CONTAINER_NAME}) + IPADDR=$(sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' ${CID}) start_routing # Run at console, kill cleanly if ctrl-c is hit trap interrupted INT trap terminated TERM echo 'Now entering wait, please hit "ctrl-c" to kill proxy and undo routing' - docker logs -f "${CID}" + sudo docker logs -f "${CID}" echo 'Squid exited unexpectedly, cleaning up...' stop } -# Guard so I can include this script into my own scripts -[ -z ${RUNNING_DRUN} ] && run +run echo diff --git a/squid.conf b/squid.conf index 23fc6ab..ab46a5d 100644 --- a/squid.conf +++ b/squid.conf @@ -10,6 +10,7 @@ acl Safe_ports port 488 # gss-http acl Safe_ports port 591 # filemaker acl Safe_ports port 777 # multiling http acl CONNECT method CONNECT + http_access deny !Safe_ports http_access deny CONNECT !SSL_ports http_access allow localhost manager @@ -17,15 +18,26 @@ http_access deny manager http_access allow localhost http_access allow all #http_access deny all -http_port 3128 transparent +ssl_bump server-first all + +# Port 3128 is the HTTP Forwarding port, which allows for serving of error pages +# and icons, and in our case, the local CA cert. The other ports are used to +# intercept regular traffic and cache responses using firewall rules (listed +# elsewhere). +http_port 3128 +http_port 3129 intercept +https_port 3130 cert=/etc/squid3/ssl_cert/ca.pem key=/etc/squid3/ssl_cert/privkey.pem generate-host-certificates=on intercept ssl-bump + maximum_object_size 512 MB -cache_dir ufs /var/spool/squid3 2024 2 8 +cache_dir aufs /var/spool/squid3 2024 2 8 cache_mem 200 MB maximum_object_size_in_memory 100 MB cache_replacement_policy heap LFUDA + refresh_pattern Packages\.bz2$ 0 20% 4320 refresh-ims refresh_pattern Sources\.bz2$ 0 20% 4320 refresh-ims refresh_pattern Release\.gpg$ 0 20% 4320 refresh-ims refresh_pattern Release$ 0 20% 4320 refresh-ims refresh_pattern -i .(udeb|tar.gz|deb|rpm|exe|zip|tar|tgz|bz2|ram|rar|bin)$ 129600 100% 129600 override-expire ignore-no-cache ignore-no-store +refresh_pattern -i .(jar|pom)$ 129600 100% 129600 override-expire ignore-no-cache ignore-no-store ignore-must-revalidate refresh_pattern . 0 20% 4320 diff --git a/squid3.patch b/squid3.patch new file mode 100644 index 0000000..49a903a --- /dev/null +++ b/squid3.patch @@ -0,0 +1,13 @@ +--- a/debian/rules 2014-02-18 03:13:28.000000000 +0000 ++++ b/debian/rules 2016-04-04 02:03:23.732296279 +0000 +@@ -41,8 +41,10 @@ + --enable-icmp \ + --enable-zph-qos \ + --enable-ecap \ ++ --enable-ssl \ + --disable-translation \ + --with-swapdir=/var/spool/squid3 \ ++ --with-open-ssl=/etc/ssl/openssl.cnf \ + --with-logdir=/var/log/squid3 \ + --with-pidfile=/var/run/squid3.pid \ + --with-filedescriptors=65536 \ diff --git a/start_squid.sh b/start_squid.sh index cf78b03..e86250c 100755 --- a/start_squid.sh +++ b/start_squid.sh @@ -1,16 +1,43 @@ #!/bin/bash -# Setup the NAT rule that enables transparent proxying -IPADDR=$(/sbin/ip -o -f inet addr show eth0 | awk '{ sub(/\/.+/,"",$4); print $4 }') -iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination ${IPADDR}:3128 +function gen-cert() { + pushd /etc/squid3/ssl_cert > /dev/null + if [ ! -f ca.pem ]; then + openssl req -new -newkey rsa:2048 -sha256 -days 365 -nodes \ + -x509 -keyout privkey.pem -out ca.pem \ + -subj '/CN=docker-proxy/O=NULL/C=AU' + chown proxy.proxy privkey.pem + chmod 600 privkey.pem + openssl x509 -in ca.pem -outform DER -out ca.der + else + echo "Reusing existing certificate" + fi + openssl x509 -sha1 -in ca.pem -noout -fingerprint + # Make CA certificate available for download via HTTP Forwarding port + # e.g. GET http://docker-proxy:3128/squid-internal-static/icons/ca.pem + cp `pwd`/ca.* /usr/share/squid3/icons/ + popd > /dev/null + return $? +} -# Make sure our cache is setup -chown proxy.proxy /var/spool/squid3 -[ -e /var/spool/squid3/swap.state ] || squid3 -z 2>/dev/null +function start-routing() { + # Setup the NAT rule that enables transparent proxying + IPADDR=$(/sbin/ip -o -f inet addr show eth0 | awk '{ sub(/\/.+/,"",$4); print $4 }') + iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination ${IPADDR}:3129 + iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination ${IPADDR}:3130 + return $? +} + +function init-cache() { + # Make sure our cache is setup + touch /var/log/squid3/access.log /var/log/squid3/cache.log + chown proxy.proxy -R /var/spool/squid3 /var/log/squid3 + [ -e /var/spool/squid3/swap.state ] || squid3 -z 2>/dev/null +} + +gen-cert || exit 1 +start-routing || exit 1 +init-cache -# Run squid and tail the logs squid3 -while true; do - [ -e /var/log/squid3/access.log ] && tail -f /var/log/squid3/access.log - sleep 1 -done +tail -f /var/log/squid3/access.log /var/log/squid3/cache.log diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..1cb5554 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,22 @@ +FROM ubuntu:14.04 + +MAINTAINER Alex Fraser + +# Install ca-certificates so we can install the proxy's certificate. curl and +# Java are only needed for running the test, not for installing the +# certificate. +WORKDIR /root +RUN export DEBIAN_FRONTEND=noninteractive TERM=linux \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + default-jdk \ + && rm -rf /var/lib/apt/lists/* + +# Install the proxy's CA certificate. +COPY detect-proxy.sh test-proxy.sh HttpTest.java /root/ +RUN javac HttpTest.java \ + && ./detect-proxy.sh start + +CMD /root/test-proxy.sh diff --git a/test/HttpTest.java b/test/HttpTest.java new file mode 100644 index 0000000..7575e22 --- /dev/null +++ b/test/HttpTest.java @@ -0,0 +1,9 @@ +import java.io.*; +import java.net.*; + +public class HttpTest { + public static void main(String[] args) throws Exception { + URL url = new URL(args[0]); + url.openConnection().getContent(); + } +} diff --git a/test/detect-proxy.sh b/test/detect-proxy.sh new file mode 100755 index 0000000..f6326eb --- /dev/null +++ b/test/detect-proxy.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +function download-cert() { + # The proxy server hosts the root certificate over HTTP, on a port that is + # published on the host. + AWK_SPLIT=' + !fout && /^\r?$/ { fout="docker-proxy.pem"; next } + fout { print > fout } + !fout { print } + ' + + HOST_IP=$(route -n | awk '/^0.0.0.0/ {print $2}') + echo -e 'GET /squid-internal-static/icons/ca.pem\r\n' \ + | nc -q -1 "$HOST_IP" 3128 \ + | awk "$AWK_SPLIT" \ + | grep -q 'Server: squid' +} + +download-cert +if [ $? -ne 0 ]; then + echo "No proxy server detected" + exit 0 +fi + +grep -q '\-----BEGIN CERTIFICATE-----' docker-proxy.pem +if [ $? -ne 0 ]; then + echo "Proxy detected" + exit 0 +fi + +echo "SSL-caching proxy server detected. Installing certificate." + +# Install CA cert into OS key store +cp docker-proxy.pem /usr/local/share/ca-certificates/docker-proxy.crt +update-ca-certificates diff --git a/test/test-proxy.sh b/test/test-proxy.sh new file mode 100755 index 0000000..b8c3148 --- /dev/null +++ b/test/test-proxy.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +curl -sS -o /dev/null https://httpbin.org/get 2>&1 || exit 1 + +curl -sS -v -o /dev/null https://httpbin.org/get 2>&1 \ + | grep -q 'X-Cache:' + +if [ $? -ne 0 ]; then + echo "Request succeeded but response was not cached" >&2 + exit 1 +fi + +# Test with Java too, because it may get its certificates from a different key +# store. +java HttpTest https://httpbin.org/get || exit 1 + +echo "All tests passed"