我如何拦截套接字绑定调用以映射到另一个端口?

我如何拦截套接字绑定调用以映射到另一个端口?

假设我有一些可执行文件foo,并且我有bar。两者都被硬编码为在端口 80 上监听。假设我们有 root。

我怎样才能让两者在不同的端口上运行?或者其中一个?你可以假设我没有源代码。

我显然可以想到以下几种方法:

  1. 在虚拟机上运行它;将主机端口映射到此
  2. 类似地,在容器(Docker 等)上运行它,并让主机映射指向内部
  3. 您能用或类似的东西bind来挂接系统调用吗?ptrace
  4. 在二进制中运行十六进制搜索80并修改绑定的参数

您可以使用 Docker 的网络命名空间来做到这一点吗?

答案1

Linux 程序很少直接使用系统调用。通常有一个同名函数。有一种标准方式可以覆盖(以及任何其他动态库)函数:

  • 你编写了一个小型动态库来重新定义你想要的函数(例如绑定)。在这个库中,你可以使用 访问上一个函数定义dlsym(RLTD_NEXT, "function_name")
  • 将动态库添加到LD_PRELOAD环境变量中。

对于您的具体问题,您可以创建一个文件libaltbind.c

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

int find_bind(int fd, const struct sockaddr *addr, socklen_t addrlen);
typedef int bindfn_type(int fd, const struct sockaddr *addr, socklen_t addrlen);

static bindfn_type* real_bind = find_bind;

int find_bind(int fd, const struct sockaddr *addr, socklen_t addrlen) {
  bindfn_type* found;
  found = (bindfn_type*) dlsym(RTLD_NEXT, "bind");
  if (found == NULL) {
    fprintf(stderr, "Didn't find the real bind().\n");
    return -1;
  }
  real_bind = found;
  return real_bind(fd,addr,addrlen);
}

int bind(int fd, const struct sockaddr *addr, socklen_t addrlen) {
  if (addr->sa_family == AF_INET) {
    struct sockaddr_in *addr_in = (struct sockaddr_in*) addr;
    if (ntohs(addr_in->sin_port) == 80) {
      char* env = getenv("ALT_PORT");
      if (env != NULL) {
    int new_port = atoi(env);
    if (new_port != -1) {
      addr_in->sin_port = htons(new_port);
    }
      }
    }
  }
  return real_bind(fd, addr, addrlen);
}

并将其转换为动态库:

gcc -c -fPIC -Wall -Werror -o libaltbind.o libaltbind.c
gcc -shared -ldl -o libaltbind.so libaltbind.o

现在,您要做的就是将程序绑定到端口,8080而不是80运行:

ALT_PORT=8080 LD_PRELOAD=/full/path/to/libaltbind.so program

答案2

从内核 2.6.29 开始,Linux 的网络命名空间功能使程序可以在自己的网络空间中运行,该程序可以使用自己的链接、地址和路由构建,也可以通过路由连接到主机的主网络空间。这可以通过用户空间程序来实现,而无需使用 docker 或其他容器框架。

因此,如果您想运行一个程序,该程序在所有接口上侦听另一个程序使用的端口,那么您可以在其自己的接口上同时运行它。

例如,设置一个名为 的网络命名空间island。添加一个接口 veth-island 并将其放在 10.1.0.0 网络中。将其虚拟以太网接口连接到主网络空间中的对等虚拟接口 veth-host。

ip netns add island
ip link add veth-island type veth peer name veth-host
ip link set veth-island netns island
ip netns exec island ip link set veth-island up
ip netns exec island ip link set lo up
ip link set veth-host up
ip netns exec island ip address add 10.1.0.2/16 dev veth-island
ip addr add 10.1.0.1/16 dev veth-host

现在 island 命名空间有了自己的环回接口和一个名为 veth-island 的以太网接口,地址为 10.1.0.2,可以到达 10.1.0.1 上的 veth-host。

您可以使用以下方式检查:

ip netns exec island ip address show

从主主机,您可以访问 10.1.0.2 以及在其上监听的所有内容。

因此,如果您有一个在所有接口上监听端口 8080 的程序,那么您可以使用以下命令启动它:
ip netns exec island program arguments,并且在主主机上您可以通过 10.1.0.2 端口 8080 连接到它,而不能连接到其他任何地方。

您可以这样做ip netns exec island netstat -plnt,然后您将看到程序正在监听命名空间岛中的所有接口。

如果不使用 iptables,这大概就是你能达到的极限了。主要的限制是namespace 无法与外界建立出站连接和名称解析。在生产机器上更改这一点可能不是您想要的,因为路由会对接受或不接受哪些 IP 功能的方式进行细微更改。

因此基本上要设置出站功能(eth0用您的出站接口替换):

#Activate router functions
echo 1 > /proc/sys/net/ipv4/ip_forward
#Set a gateway for the island namespace
ip netns exec island ip route add default via 10.1.0.1
#Masquerade outgoing connections (you can limit to tcp with `-p tcp`)
iptables -t nat -A POSTROUTING -s 10.1.0.0/16 -o eth0 -j MASQUERADE
#Let packets move from the outward interface to the virtual ethernet pair and vice versa
iptables -A FORWARD -i eth0 -o veth-host -j ACCEPT
iptables -A FORWARD -o eth0 -i veth-host -j ACCEPT
#Setup a resolver (replace with your own DNS, does not work with a loopback resolver)
mkdir -p /etc/netns/island
echo nameserver dns-ip > /etc/netns/island/resolv.conf
#Maybe give it its own hosts file, to do edits
cp /etc/hosts /etc/netns/island/hosts

现在你可以测试ip netns exec island ping example.com

您可以使用类似的方法socat将服务器映射到主命名空间中选定的端口。

socat tcp-listen:8080,reuseaddr,fork tcp-connect:10.1.0.2:80

或者你可以使用 iptables 作为某种 TCP 代理。这也涉及 sysctl 设置。

# Proxy connections on localhost at a given port of main namespace to a chosen port on other namespace
# You have to set a sysctl and SNAT if you want to connect to loopback in main namespace and get packets returned from island namespace
# This sets up DNAT for packets output from a locally executed program in the main namespace
iptables -t nat -A OUTPUT -o lo -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.1.0.2:80
# You can omit SNAT if you connect to any interface different from loopback
iptables -t nat -A POSTROUTING -d 10.1.0.2/32 -p tcp -m tcp --dport 80 -j SNAT --to-source 10.1.0.1
# Allow routing of localhost output packets to veth-host (only needed for connections on loopback)
sysctl -w net.ipv4.conf.veth-host.route_localnet=1

iptables 解决方案比较棘手,因为 netstat 不会向您显示主命名空间中监听 DNAT 端口的任何内容。您可以启动一个绑定到主命名空间中相同端口的程序。使用给定的命令,这意味着例如来自外部的连接将到达监听主命名空间端口的程序,但主命名空间中本地执行的程序将连接到岛命名空间中的 DNAT 端口。如果您想从外部连接,则必须添加 PREROUTING DNAT 规则。只是说,如果更多人在这台机器上工作,可能会造成混乱。

警告。对于某些应用程序,虚拟以太网接口可能不可靠。我曾经在使用 VPN 应用程序时遇到过这种情况。它工作正常,但速度慢得令人难以置信。可能是 ipsec,也可能是其他传输层。

相关内容