Docker 容器内部网络穿透的一些想法

前言

本来呢是为了学校做了一个 Docker 应用,但是某年暑假之后这个接口突然不对公访问了,只能在校园网里访问了。

如果是普通的应用的话还好说,各种穿透方案都有,但是对于 Docker 容器就比较麻烦了。

网上的各种教程里都说在 Dockerfile 里设置 proxy 环境变量,但是在我这个 Docker 镜像里运行的这个应用并不是很喜欢这个环境变量(aka 不会走 proxy 指向的代理),这就必须得采取更高级别的措施了。

如果折腾过 Linux 下网络穿透的小伙伴兴许玩过 iptables + redsocks 转发流量的方法,此法虽简单暴力但 It works(狗头)。话说回来,可不可以利用此法转发原 Docker 镜像中的流量呢?

即:对于任意一给定的 Docker 镜像,在不修改其 Dockerfile 的情况下强行让流量走隧道,进而实现流量转发的目的

探索

  1. Docker + iptables 相关
    在 Docker 容器内其实并不能直接乱玩 iptables, 因为 Linux capabilities 的缘故需要赋予一个容器以权限才能在容器内折腾 iptables。

    对于旧版的 Docker 来说,需要在 docker 启动命令里加上 --privileged 参数来给予容器以最高权限。但 Docker 开发者也考虑到这样直接给最高权限似乎不太安全,所以在后续的版本中增加了 --cap-add--cap-drop 参数用于细化容器所能持有的 capabilities。所以现在,如果想在容器内启用 iptables 支持,只需要在启动命令行中加入 --cap_add NET_ADMIN 就好啦

  2. 配置 iptables 让全局流量走特定端口

    这个有很多教程都已经提到过了,这边只简单说一下。
    即在 iptables 的 NAT 中新建一个 chain, 将需要 bypass 的地址先加入其中(一般是私有地址),最后的规则即将流量转发到某个特定端口。

    假设这个 chain 的名字为 REDSOCKS, iptables 配置命令如下:

    # 新建一个 chain
    iptables -t nat -N REDSOCKS
    # 绕过私有地址
    iptables -t nat -A REDSOCKS -d 0.0.0.0/8 -j RETURN
    iptables -t nat -A REDSOCKS -d 10.0.0.0/8 -j RETURN
    iptables -t nat -A REDSOCKS -d 127.0.0.0/8 -j RETURN
    iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
    iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
    iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
    iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
    iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN
    # 将流量转发到本地的 12345 端口
    iptables -t nat -A REDSOCKS -p tcp -j REDIRECT --to-ports 12345 
    
  3. 使用 redsocks 将 TCP packet 转发至 Socks5 Proxy

    上文中我们看到 iptables 会将所有数据转发到12345端口,可是此时的报文都是 TCP 报文,这时候我们就需要 redsocks 将 TCP 流转发至 Socks5 代理

    只需要将 redsocks 配置文件中的 redsocks 段修改就行

    redsocks {
        local_ip = 127.0.0.1;
        // local_port 为监听 TCP 报文的端口
        local_port = 12345;
        // ip 为 socks5 服务器地址, port 为端口
        ip = vPROXY-SERVER;
        port = vPROXY-PORT;
        type = socks5;
    }
    
  4. 重头戏:如何让原 Docker 容器的流量由 iptables 转发强制走 redsocks

    我们发现上面一番操作之后并没有什么用——因为在不改变原 Docker 镜像的情况下,我们几乎没有办法在原 Docker 镜像里安装 iptables + redsocks。

    好在万能的 Docker 在 network 中悄悄提供了一个参数。让我们看下 Docker 的 --network 里都有什么可选项

    --network="bridge" : Connect a container to a network
              'bridge': create a network stack on the default Docker bridge
              'none': no networking
              'container:<name|id>': reuse another container's network stack
              'host': use the Docker host network stack
              '<network-name>|<network-id>': connect to a user-defined network
    

    会发现其中有一个 container flag 特别有意思,虽然官方文档里只有简简单单一句复用他之容器的网络,网络上关于这个选项的文档也不是很多,但这个 flag 就是整个 tactic 里最重要的一环。他的作用就是,复用另外一个容器的网络,相当于两个容器是在同一个网络下运行的。

    我们完全可以自己再做一个 Docker 镜像,装上 iptables + redsocks,配置好转发,之后再将原 Docker 镜像的 network 参数改成 container 并指向新建的 Docker 容器就好啦。

流程

原 Docker 镜像(my-image) -> iptables NAT(redsocks) -> redsocks(redsocks) -> socks5 server(socks5-server) -> Private Network

实战

因为涉及到多个 Docker 容器的内部网络联络,且手动输入 docker run 冗长的命令行也过于繁琐,这里我们采用 docker-compose 这一神器。

docker-compose 的方便之处不光在于可以一次性启动多个容器,免去长长的启动命令行,更重要的是它可以把一个 docker-compose.yml 中的所有容器都统一归入一个内网中方便互联,免去手动创建网络的烦恼。

  1. 先在一个空白文件夹内新建 docker-compose.yml, 内容如下
    version: '3'
    services:
        my-image:
            image: milkice/icebox-project:latest
            restart: always
            depends_on:
                - redsocks
            network_mode: "service:redsocks"
        redsocks:
            build: redsocks-docker-global 
            image: redsocks-global
            container_name: redsocks-proxy
            depends_on:
                - socks5-server
            cap_add:
                - NET_ADMIN
            environment:
                - PROXY_SERVER=socks5-server
                - PROXY_PORT=1080
            volumes:
                - "/home/milkice/redsocks/etc/:/etc/"
        socks5-server:
            image: "socks5/server"
    

    上述文件中请将 my-image 及其中的内容替换成自己的原 Docker image,再将 redsocks 中的 volumes 改成自己将要存放的配置文件的位置。

    配置样例中的 socks5-server 是搭建于本地的 socks5 服务器,有哪些大家都清楚,在使用前需要自行修改这一段。如果使用的 socks5 服务器是外网的,那么修改redsocks里的PROXY_SERVERPROXY_PORT即可。

  2. 创建自定义的代理绕过名单
    虽然我们的目的是将目标 Docker 容器的全局流量转发至 socks5 服务器,但同时我们也想要部分流量不转发至远端服务器,比如 socks5 服务器的远端地址,如果不取消转发就会导致死循环。

    但是将这些ip写死在 Docker 镜像里也不好,不方便修改,所以把这个配置文件放在 Docker 以外的文件系统里,需要修改的时候直接修改该文件再重启 Docker 镜像即可。

    上述配置文件中奶冰将这个配置文件存放于 /home/milkice/redsocks/etc/,请根据需要自行修改文件夹位置。末了在该文件夹下新建 redsocks-proxy-ip.txt 文件,并填入下述信息(可以写多行ip网段):

    210.32.0.125/24
    210.32.0.182/24
    

    即可针对这些地址绕过代理

  3. 在 docker-compose.yml 同一个文件夹下新建 redsocks-docker-global 文件夹,在里面新建几个文件

    1. Dockerfile:
      FROM alpine:latest
      
      WORKDIR /app
      ADD . /app
      
      ENV PROXY_SERVER=localhost
      ENV PROXY_PORT=3128
      
      RUN set -ex; \
          apk update; \
          apk upgrade; \
          apk add --no-cache redsocks iptables bash; \
          chmod +x *
      
      ENTRYPOINT ["/bin/sh","redsocks.sh"]
      
    2. redsocks.conf:
      base {
          log_info = on;
          daemon = on;
          redirector = iptables;
      }
      
      redsocks {
          /* `local_ip' defaults to 127.0.0.1 for security reasons,
           * use 0.0.0.0 if you want to listen on every interface.
           * `local_*' are used as port to redirect to.
           */
          local_ip = 127.0.0.1;
          local_port = 12345;
      
          ip = vPROXY-SERVER;
          port = vPROXY-PORT;
      
          type = socks5;
      
      }
      
    3. redsocks-fw.sh:
      #!/bin/sh
      
      ##########################
      # Setup the Firewall rules
      ##########################
      fw_setup() {
      
        # First we added a new chain called 'REDSOCKS' to the 'nat' table.
        iptables -t nat -N REDSOCKS
      
        iptables -t nat -A REDSOCKS -d 0.0.0.0/8 -j RETURN
        iptables -t nat -A REDSOCKS -d 10.0.0.0/8 -j RETURN
        iptables -t nat -A REDSOCKS -d 127.0.0.0/8 -j RETURN
        iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
        iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
        iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
        iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
        iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN
      
        # Add the ip that need to be proxied to PREROUTING
        while read item; do
            iptables -t nat -A REDSOCKS -d $item -p tcp -j RETURN
        done < /etc/redsocks-proxy-ip.txt
      
        # We then told iptables to redirect all port 80 connections to the http-relay redsocks port and all other connections to the http-connect redsocks port.
        iptables -t nat -A REDSOCKS -p tcp -j REDIRECT --to-ports 12345
        # TODO Supports ports forwarding to other machines
      
        # Finally we tell iptables to use the ‘REDSOCKS’ chain for all outgoing connection in the network interface ‘eth0′.
        iptables -t nat -A OUTPUT -p tcp -j REDSOCKS
      }
      
      ##########################
      # Clear the Firewall rules
      ##########################
      fw_clear() {
        iptables-save | grep -v REDSOCKS | iptables-restore
        #iptables -L -t nat --line-numbers
        #iptables -t nat -D PREROUTING 2
      }
      
      case "$1" in
          start)
              echo -n "Setting REDSOCKS firewall rules..."
              fw_clear
              fw_setup
              echo "done."
              ;;
          stop)
              echo -n "Cleaning REDSOCKS firewall rules..."
              fw_clear
              echo "done."
              ;;
          *)
              echo "Usage: $0 {start|stop}"
              exit 1
              ;;
      esac
      exit 0
      
    4. redsocks.conf
      #!/bin/bash
      echo "Configuration:"
      echo "PROXY_SERVER=$PROXY_SERVER"
      echo "PROXY_PORT=$PROXY_PORT"
      echo "Setting config variables"
      sed -i "s/vPROXY-SERVER/$PROXY_SERVER/g" redsocks.conf
      sed -i "s/vPROXY-PORT/$PROXY_PORT/g" redsocks.conf
      
      echo "Activating iptables rules..."
      sh ./redsocks-fw.sh start
      
      pid=0
      
      term_handler() {
          if [ $pid -ne 0 ]; then
              echo "Term signal catched. Shutdown redsocks and disable iptables rules..."
              kill -SIGTERM "$pid"
              wait "$pid"
              sh ./redsocks-fw.sh stop
          fi
          exit 143; # 128 + 15 -- SIGTERM
      }
      
      trap 'kill ${!}; term_handler' SIGTERM
      
      echo "Starting redsocks..."
      /usr/bin/redsocks -c redsocks.conf &
      pid="$!"
      
      while true
      do
          tail -f /dev/null & wait ${!}
      done
      
  4. 启动!
    如果一切都没有问题的话,执行docker-compose up -d,此时原 Docker 容器的流量都应流入 socks5 代理之后进入特定网络,也就实现了穿透的目的。

参考资料

点赞

发表评论