├── Dockerfile ├── LICENSE ├── README.md ├── custom-entrypoint.sh ├── docker-compose.yml └── haproxy.cfg /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use 1.8.8 for now until https://github.com/docker-library/haproxy/issues/68 is fixed 2 | FROM haproxy:1.8.8-alpine 3 | 4 | ENV CONNECTION_TIMEOUT 5m 5 | 6 | COPY custom-entrypoint.sh / 7 | COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg.tpl 8 | 9 | ENTRYPOINT ["/custom-entrypoint.sh"] 10 | CMD ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Martin Honermeyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swarm TCP proxy 2 | 3 | This is a Docker image which is designed to act as a TCP load balancer service for another backend swarm service. 4 | 5 | It supports the [Proxy Protocol](https://www.haproxy.com/blog/haproxy/proxy-protocol/) for passing through information about the original connection. 6 | 7 | ## What's the problem? 8 | 9 | Every service running on a Docker swarm cluster has its own virtual IP address. The service containers are getting IP addresses from an overlay network with private IP addresses. 10 | 11 | As [every connection is routed through an ingress network](https://github.com/moby/moby/issues/25526), a service container will see an address from the ingress network for each incoming connection. It is thus unable to find out the real remote address. 12 | 13 | As a workaround, the service can publish a port [directly on the host](https://github.com/moby/moby/issues/25526#issuecomment-336363408). As this is only effective on the host the container is running on, the service has thus to be made [global](https://github.com/moby/moby/issues/25526#issuecomment-275292393) so it will be started on every host in the cluster. 14 | 15 | For cases where this is not a feasible solution, Swarm TCP proxy comes into play. 16 | 17 | ## How does it work? 18 | 19 | Swarm TCP proxy is deployed as a global service, i.e. on all nodes in the cluster. As each instance is just a [HAProxy](https://www.haproxy.com/) instance, this has quite a small footprint. 20 | 21 | It will forward each incoming connection to the virtual IP of the real service, which is then routed to the final container by Docker swarm itself. 22 | 23 | In order to for the container to see the original remote address, Swarm TCP proxy uses the [Proxy Protocol](https://www.haproxy.com/). 24 | 25 | This means that the deployed application has to support the Proxy Protocol as well. See [this introductory post](https://www.haproxy.com/blog/haproxy/proxy-protocol/) for which software already supports the protocol. 26 | 27 | ## Usage 28 | 29 | See the supplied [docker-compose.yml](docker-compose.yml) for an example. It uses the sample [proxy-responder service](https://github.com/djmaze/proxy-responder) which just returns the remote IP and closes the connection. 30 | 31 | Example output: 32 | 33 | $ docker-compose up -d 34 | $ telnet localhost 8000 35 | Trying ::1... 36 | Connected to localhost. 37 | Escape character is '^]'. 38 | Hello, 192.168.80.1:37174 39 | Connection closed by foreign host. 40 | 41 | The example proxy runs on port 8080. 42 | 43 | ### Configuration 44 | 45 | The proxy is able to front multiple backend services listening at different addresses. It is configured through some mandatory ENV variables: 46 | 47 | * `SERVICES`: a list of `:` combinations which should be proxied, separated by spaces 48 | 49 | The proxy will listen on the same port as each given service. 50 | 51 | Other settings: 52 | 53 | * `DISABLE_PROXY_PROTOCOL`: do not use the proxy protocol (just forward data) 54 | * `CONNECTION_TIMEOUT`: connection timeout to client as well as server (default: "5m") 55 | 56 | ### Deprecated syntax 57 | 58 | With the deprecated syntax, the proxy will front a single service while listening on port `8000`. 59 | 60 | * `SERVICE_NAME`: name of the connected Swarm service 61 | * `SERVICE_PORT`: port on the service which should be proxied 62 | 63 | -------------------------------------------------------------------------------- /custom-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail 3 | 4 | haproxy_cfg="/usr/local/etc/haproxy/haproxy.cfg" 5 | send_proxy_arg="send-proxy" 6 | 7 | SERVICES="${SERVICES:-}" 8 | DISABLE_PROXY_PROTOCOL="${DISABLE_PROXY_PROTOCOL:-}" 9 | CONNECTION_TIMEOUT="${CONNECTION_TIMEOUT:-}" 10 | 11 | if [[ -n "$SERVICES" ]]; then 12 | num=0 13 | for service in $SERVICES; do 14 | let "num++" || true 15 | name="$(echo $service | cut -d ":" -f 1)" 16 | port="$(echo $service | cut -d ":" -f 2)" 17 | sed "s/\${NUM}/${num}/; \ 18 | s/\${PROXY_PORT}/${port}/; \ 19 | s/\${SERVICE_NAME}/${name}/; \ 20 | s/\${SERVICE_PORT}/${port}/; \ 21 | s/\${CONNECTION_TIMEOUT}/${CONNECTION_TIMEOUT}/g" \ 22 | "$haproxy_cfg.tpl" >>"$haproxy_cfg" 23 | done 24 | else 25 | sed "s/\${NUM}/1/; \ 26 | s/\${PROXY_PORT}/8000/; \ 27 | s/\${SERVICE_NAME}/${SERVICE_NAME}/; \ 28 | s/\${SERVICE_PORT}/${SERVICE_PORT}/; \ 29 | s/\${CONNECTION_TIMEOUT}/${CONNECTION_TIMEOUT}/g" \ 30 | "$haproxy_cfg.tpl" >"$haproxy_cfg" 31 | fi 32 | 33 | if [ -n "$DISABLE_PROXY_PROTOCOL" ]; then 34 | send_proxy_arg="" 35 | fi 36 | sed -i "s/\${SEND_PROXY}/${send_proxy_arg}/" \ 37 | "$haproxy_cfg" 38 | 39 | exec /docker-entrypoint.sh $* 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | proxy: 5 | build: . 6 | image: decentralize/swarm-tcp-proxy 7 | environment: 8 | SERVICES: "app:8080" 9 | CONNECTION_TIMEOUT: 5m 10 | ports: 11 | - target: 8080 12 | published: 8080 13 | protocol: tcp 14 | mode: host 15 | networks: 16 | - backend 17 | deploy: 18 | mode: global 19 | 20 | app: 21 | image: decentralize/proxy-responder 22 | networks: 23 | - backend 24 | 25 | networks: 26 | backend: 27 | -------------------------------------------------------------------------------- /haproxy.cfg: -------------------------------------------------------------------------------- 1 | resolvers dns${NUM} 2 | nameserver public-0 127.0.0.11:53 3 | hold valid 10s 4 | 5 | frontend ft${NUM} 6 | bind 0.0.0.0:${PROXY_PORT} 7 | mode tcp 8 | no option http-server-close 9 | timeout client ${CONNECTION_TIMEOUT} 10 | default_backend bk${NUM} 11 | 12 | backend bk${NUM} 13 | mode tcp 14 | no option http-server-close 15 | timeout server ${CONNECTION_TIMEOUT} 16 | timeout connect 5s 17 | server server ${SERVICE_NAME}:${SERVICE_PORT} ${SEND_PROXY} resolvers dns${NUM} 18 | 19 | --------------------------------------------------------------------------------