2EXP / 2exp.github.io

8 stars 3 forks source link

Transparent Proxy 不同平台的实现方式 #2

Open 2EXP opened 5 years ago

2EXP commented 5 years ago

本文是对不同平台不同 IP packet 过滤程序透明代理实现的一些思考,主要考虑过滤规则以及如何在代理程序中获取客户端网络请求的原目标 IP 地址。

参数、函数的解释

dst_addr:所需获取的目标地址;

client_addr:客户端请求的源地址,可通过 accept() 获取;

local_addr:代理程序绑定的地址,在 bind() 调用中传入的地址;

log_printf():类比 printf(),只不过用来输出信息至日志文件,这里没有给出具体的实现。

Netfilter

应用于 Linux kernel 2.4+。

过滤规则

TCP 过滤规则用了 REDIRECT 规则,UDP 过滤规则用了 TPROXY 规则,TCP 同样也可以使用 TPROXY 规则过滤。需要注意的是,使用 TPROXY 规则时,在写代理程序时需要 setsockopt(IP_TRANSPARENT),具体见下文 TPROXY 方案获取目标 IP 地址。

设置规则可参阅 iptables(8)Netfilter 文档

# Create TRANSPARENT_PROXY chain
iptables -t nat -N TRANSPARENT_PROXY
iptables -t mangle -N TRANSPARENT_PROXY

# Bypass remote proxy server addresses
iptables -t nat -A TRANSPARENT_PROXY -d $SERVER_IP -j RETURN
iptables -t mangle -A TRANSPARENT_PROXY -d $SERVER_IP -j RETURN

# Bypass LAN and any other addresses if desired
# Here is an example to bypass local identification ip addresses
iptables -t nat -A TRANSPARENT_PROXY -d 0.0.0.0/8 -j RETURN
iptables -t mangle -A TRANSPARENT_PROXY -d 0.0.0.0/8 -j RETURN

# The remainder should be redirect to local proxy port
# Here assumes local proxy server is running on localhost
## Port forwarding for TCP
iptables -t nat -A TRANSPARENT_PROXY -p tcp -j REDIRECT --to-ports $PROXY_PORT
## Port forwarding for UDP
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
iptables -t mangle -A TRANSPARENT_PROXY -p udp -j TPROXY --on-port $PROXY_PORT --tproxy-mark 0x1/0x1

# Add TRANSPARENT_PROXY chain to iptables PREROUTING chain
iptables -t nat -A PREROUTING -p tcp -j TRANSPARENT_PROXY
iptables -t mangle -A PREROUTING -p udp -j TRANSPARENT_PROXY

获取目标 IP 地址

使用 TPROXY 方案,获取原目标 IP 地址只需简单调用 getsockname(),与下文通过 IPFW 的实现透明代理方案一样。

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

static int set_transparent(int fd) {
  int opt = 1;

  if (setsockopt(fd, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt))) {
    log_printf("%s - setsockopt(IP_TRANSPARENT): %s\n", __func__, strerror(errno));
    return -1;
  }
  return 0;
}

static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen) {
  if (getsockname(fd, dst_addr, dst_addrlen)) {
    log_printf("%s - getsockname(dst_addr): %s\n", __func__, strerror(errno));
    return -1;
  }
  return 0;
}

使用普通方案,则可通过调用 getsockopt(SO_ORIGINAL_DST)(IPv4 地址)和 getsockopt(IP6T_SO_ORIGINAL_DST)(IPv6 地址)获取。

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netfilter_ipv6/ip6_tables.h>

static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen, sa_family_t sa_type) {
  if (sa_type == AF_INET) {
    if (getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, dst_addr, dst_addrlen)) {
      log_printf("%s - getsockopt(SO_ORIGINAL_DST): %s\n", __func__, strerror(errno));
      return -1;
    }
  } else {
#ifdef IP6T_SO_ORIGINAL_DST
    if (getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, dst_addr, dst_addrlen)) {
      log_printf("%s - getsockopt(IP6T_SO_ORIGINAL_DST): %s\n", __func__, strerror(errno));
      return -1;
    }
#else
    log_printf("The Netfilter does not support IPv6 NAT lookup.\n");
    return -1;
#endif
  }
  return 0;
}

Packet Filter

出自 OpenBSD,应用于 OpenBSD 3.0+、FreeBSD 5.3+、NetBSD 3.0+、Solaris 11.3+、macOS 10.7+、iOS 和 QNX。

过滤规则

以下规则应用于 TCP 过滤,UDP 过滤可以用同样方式设置,PF 规则具体设置见 pf.conf(5)FreeBSD 相关文档。FreeBSD 中使用的 PF 是 OpenBSD 4.5 的版本。OpenBSD 可以在 pass 规则增加 rdr-to 规则,可省去第一条规则,可参阅 OpenBSD 文档。macOS 中的 PF 与 FreeBSD 的更加相似。

# Here it assumes that local network interface is en0.
# rdr rules could not work on local outbound traffic directly.
# Route en0's traffic to lo0, then the original outbound traffic becomes inbound traffic, and rdr rules work.

# Redirect traffic to proxy port
rdr on lo0 proto tcp from en0 to any -> 127.0.0.1 port $PROXY_PORT

# Bypass remote proxy server addresses
pass out quick on en0 proto tcp from any to $SERVER_IP
# Route en0's traffic to lo0
pass out quick on en0 route-to lo0 proto tcp from any to any

获取目标 IP 地址

调用 open() 以只读方式打开 PF 设备文件,如果需要动态地加入过滤规则可以使用可读写方式。之后,主要通过 ioctl(DIOCNATLOOK) 实现,其它与 PF 设备相关动态的操作可参阅 pf(4)。实际上文档中的内容还是不够详细,直接在 net/pfvar.h 看结构体的声明反而更加直观。

#include <errno.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#ifdef __APPLE__
#define PRIVATE
#endif
#include <net/pfvar.h>
#ifdef __APPLE__
#undef PRIVATE
#endif

static int pffd = -1;

static int
pf_init(void) {
  pffd = open("/dev/pf", O_RDONLY);
  if (pffd) {
    log_printf("%s - open(\"/dev/pf\"): %s\n", __func__, strerror(errno));
    return -1;
  }
  if (fcntl(pffd, F_SETFD, fcntl(pffd, F_GETFD) | FD_CLOEXEC)) {
    log_printf("%s - fcntl(F_SETFD): %s\n", __func__, strerror(errno));
    return -1;
  }
  return 0;
}

static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen,
             struct sockaddr *client_addr, struct sockaddr *local_addr,
             sa_family_t sa_type) {
#ifdef __APPLE__
#define sport sxport.port
#define dport dxport.port
#define rdport rdxport.port
#ifdef v4addr // XNU 4570.1.46 and later (macOS 10.13+, iOS 11+)
#define v4 v4addr
#define v6 v6addr
#endif // XNU 4570.1.46 and later (macOS 10.13+, iOS 11+)
#endif // __APPLE__
  struct pfioc_natlook pnl;

  bzero(&pnl, sizeof(pnl));
  pnl.af = sa_type;
  if (sa_type == AF_INET) {
    struct sockaddr_in *src_addr = (struct sockaddr_in *)client_addr;
    struct sockaddr_in *bind_addr = (struct sockaddr_in *)local_addr;
    bcopy(&src_addr->sin_addr, &pnl.saddr.v4, sizeof(pnl.saddr.v4));
    pnl.sport = src_addr->sin_port;
    bcopy(&bind_addr->sin_addr, &pnl.daddr.v4, sizeof(pnl.daddr.v4));
    pnl.dport = bind_addr->sin_port;
  } else if (sa_type == AF_INET6) {
    struct sockaddr_in6 *src_addr = (struct sockaddr_in6 *)client_addr;
    struct sockaddr_in6 *bind_addr = (struct sockaddr_in6 *)local_addr;
    bcopy(&src_addr->sin6_addr, &pnl.saddr.v6, sizeof(pnl.saddr.v6));
    pnl.sport = src_addr->sin6_port;
    bcopy(&bind_addr->sin6_addr, &pnl.daddr.v6, sizeof(pnl.daddr.v6));
    pnl.dport = bind_addr->sin6_port;
  }
  pnl.proto = IPPROTO_TCP;
  pnl.direction = PF_OUT;

  if (ioctl(pffd, DIOCNATLOOK, &pnl)) {
    log_printf("%s - ioctl(DIOCNATLOOK): %s\n", __func__, strerror(errno));
    return -1;
  }

  if (sa_type == AF_INET) {
    struct sockaddr_in *dst_addr_in = (struct sockaddr_in *)dst_addr;
    dst_addr_in->sin_family = sa_type;
    bcopy(&pnl.rdaddr.v4, &dst_addr_in->sin_addr, sizeof(dst_addr_in->sin_addr));
    dst_addr_in->sin_port = pnl.rdport;
  } else if (sa_type == AF_INET6) {
    struct sockaddr_in6 *dst_addr_in6 = (struct sockaddr_in6 *)dst_addr;
    dst_addr_in6->sin6_family = sa_type;
    bcopy(&pnl.rdaddr.v6, &dst_addr_in6->sin6_addr, sizeof(dst_addr_in6->sin6_addr));
    dst_addr_in6->sin6_port = pnl.rdport;
  }

  return 0;
#ifdef __APPLE__
#undef sport
#undef dport
#undef rdport
#ifdef v4addr // XNU 4570.1.46 and later (macOS 10.13+, iOS 11+)
#undef v4
#undef v6
#endif // XNU 4570.1.46 and later (macOS 10.13+, iOS 11+)
#endif // __APPLE__
}

IPFilter

应用于 FreeBSD 2.2+、NetBSD 1.2+、Solaris 10+、illumos 和 QNX。

IPFilter 过滤规则语法看起来与 PF 差不多,我还没仔细看,FreeBSD 文档也可以作为参考。

获取目标 IP 地址流程也差不多,打开设备文件,然后 ioctl(SIOCGNATL) ,参阅 ipnat(4)

#include <errno.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ipl.h>
#include <netinet/ip_compat.h>
#include <netinet/ip_fil.h>
#include <netinet/ip_nat.h>

static int ipfilter_fd = -1;

static int
ipfilter_init(void) {
  ipfilter_fd = open(IPNAT_NAME, O_RDONLY);
  if (ipfilter_fd) {
    log_printf("%s - open(IPNAT_NAME): %s\n", __func__, strerror(errno));
    return -1;
  }
  if (fcntl(ipfilter_fd, F_SETFD, fcntl(ipfilter_fd, F_GETFD) | FD_CLOEXEC)) {
    log_printf("%s - fcntl(F_SETFD): %s\n", __func__, strerror(errno));
    return -1;
  }
  return 0;
}

static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen,
             struct sockaddr *client_addr, struct sockaddr *local_addr,
             sa_family_t sa_type) {
  struct natlookup nl;
  struct ipfobj ipfo;

  bzero(&nl, sizeof(nl));
  if (sa_type == AF_INET) {
    struct sockaddr_in *src_addr = (struct sockaddr_in *)client_addr;
    struct sockaddr_in *bind_addr = (struct sockaddr_in *)local_addr;
    nl.nl_outip = src_addr->sin_addr;
    nl.nl_outport = src_addr->sin_port;
    nl.nl_inip = bind_addr->sin_addr;
    nl.nl_inport = bind_addr->sin_port;
  } else {
    log_printf("The IPFilter does not support IPv6 NAT lookup.\n");
    return -1;
  }
  nl.nl_flags = IPN_TCP;

  bzero(&ipfo, sizeof(ipfo));
  ipfo.ipfo_rev = IPFILTER_VERSION;
  ipfo.ipfo_size = sizeof(nl);
  ipfo.ipfo_ptr = &nl;
  ipfo.ipfo_type = IPFOBJ_NATLOOKUP;

  if (ioctl(ipfilter_fd, SIOCGNATL, &ipfo)) {
    log_printf("%s - ioctl(SIOCGNATL): %s\n", __func__, strerror(errno));
    return -1;
  }

  struct sockaddr_in *dst_addr_in = (struct sockaddr_in *)dst_addr;
  dst_addr_in->sin_family = AF_INET;
  dst_addr_in->sin_addr = nl.nl_realip;
  dst_addr_in->sin_port = nl.nl_realport;

  return 0;
}

IPFW

出自 FreeBSD,应用于 FreeBSD、macOS 10.6-(macOS 10.10 完全移除),也曾被用于 Linux kernel 1.1,为 Linux 第一代防火墙。

IPFW 过滤规则暂时还没看,可参考 FreeBSD IPFW 相关文档

原目标 IP 地址的获取与上文 Netfilter 的 TPROXY 方案一样。

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>

static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen) {
  if (getsockname(fd, dst_addr, dst_addrlen)) {
    log_printf("%s - getsockname(dst_addr): %s\n", __func__, strerror(errno));
    return -1;
  }
  return 0;
}

NPF

出自 NetBSD,应用于 NetBSD 6.0+。

规则设置文档

提供的部分 API 可参考文档 libnpf(3),但是并不齐全,以下用到的 npf_nat_lookup() 就没有列举在其中,直接看源码——lib/libnpf/npf.hlib/libnpf/npf.c

以下 get_dst_addr() 中的 dst_addr 为 inout 参数,应传入 client_addr,调用函数后其值为所需的 dst_addr。

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/pfil.h>
#include <npf.h>

static int
get_dst_addr(int fd, struct sockaddr *dst_addr, struct sockaddr *local_addr, sa_family_t sa_type) {
  npf_addr_t *addr[2];
  in_port_t port[2];

  if (sa_type == AF_INET) {
    struct sockaddr_in dst_addr_in = (struct sockaddr_in *)dst_addr;
    struct sockaddr_in local_addr_in = (struct sockaddr_in *)local_addr;
    addr[0] = (npf_addr_t *)&dst_addr_in->sin_addr;
    port[0] = dst_addr_in->sin_port;
    addr[1] = (npf_addr_t *)&local_addr_in->sin_addr;
    port[1] = local_addr_in->sin_port;

    if (npf_nat_lookup(fd, AF_INET, addr, port, IPPROTO_TCP, PFIL_OUT)) {
      log_printf("%s - npf_nat_lookup(): %s\n", __func__, strerror(errno));
      return -1;
    }
    dst_addr_in->sin_port = port[0];
    local_addr_in->sin_port = port[1];
  } else if (sa_type == AF_INET6) {
    struct sockaddr_in6 dst_addr_in6 = (struct sockaddr_in6 *)dst_addr;
    struct sockaddr_in6 local_addr_in6 = (struct sockaddr_in6 *)local_addr;
    addr[0] = (npf_addr_t *)&dst_addr_in6->sin6_addr;
    port[0] = dst_addr_in6->sin6_port;
    addr[1] = (npf_addr_t *)&local_addr_in6->sin6_addr;
    port[1] = local_addr_in6->sin6_port;

    if (npf_nat_lookup(fd, AF_INET, addr, port, IPPROTO_TCP, PFIL_OUT)) {
      log_printf("%s - npf_nat_lookup(): %s\n", __func__, strerror(errno));
      return -1;
    }
    dst_addr_in6->sin6_port = port[0];
    local_addr_in6->sin6_port = port[1];
  }
  return 0;
}

WFP

Windows 平台暂时没发现简单易用的透明代理方案,但是或许可以通过 Windows Filtering Platform 提供的 API 解决。

WFP 相关文档

caobug commented 3 years ago

谢谢。根据测试,尽管pfioc_natlook设置了 proto=IPPROTO_UDP,但在 macOS 上仍提示找不到匹配项。

sudo pfctl -ss倒可以列出待查找UDP,或许需要其它设定?

zonyitoo commented 3 years ago

macOS上的pf应该是不支持udp,目前没找到办法

ezh commented 3 years ago

Привет парни! Ничего ничего что вы тут на Китайском, а я на Русском? Можно успешно получить состояния для proto=IPPROTO_UDP из macos через DIOCGETSTATES pfioc_states pfsync_state. Это не очень эффективно, но работает. 👌

zonyitoo commented 2 years ago

Привет парни! Ничего ничего что вы тут на Китайском, а я на Русском? Можно успешно получить состояния для proto=IPPROTO_UDP из macos через DIOCGETSTATES pfioc_states pfsync_state. Это не очень эффективно, но работает. 👌

Wow. Is it actually working on macOS? Do you have any document references or code pieces about how to make it work?

ezh commented 2 years ago

@zonyitoo I'm writing in Go. But language doesn't matter. MacOS pf looks like 💩 But you get an idea

// LookUpUDP looks up for the specific UDP IP in the NAT table
func (p *PFCTL) LookUpUDP(srcIP net.IP, srcPort int, dstIP net.IP, dstPort int) (net.IP, int, error) {
    records, err := p.LookUpTable()
    if err != nil {
        return nil, 0, fmt.Errorf("unable to look up states table: %w", err)
    }
    for _, state := range records {
        if state.Proto == syscall.IPPROTO_UDP &&
            int(state.Lan.Port) == dstPort && int(state.ExtGwy.Port) == srcPort &&
            bytes.Equal(state.Lan.Addr[:4], dstIP.To4()) && bytes.Equal(state.ExtGwy.Addr[:4], srcIP.To4()) {
            toIP := net.IPv4(state.Gwy.Addr[0], state.Gwy.Addr[1], state.Gwy.Addr[2], state.Gwy.Addr[3])
            return toIP, int(state.Gwy.Port), nil
        }
    }
    return nil, 0, os.NewSyscallError("ioctl", fmt.Errorf("%d", int(syscall.ENOENT)))
}

// LookUpTable gets list of PF NAT states
func (p *PFCTL) LookUpTable() ([]*State, error) {
    records := []*State{}
    states := PFIOCStates{
        BufferLength: StateBufferSize,
        Buffer:       &StateBuffer,
    }
    err := p.Read(DIOCGETSTATES, unsafe.Pointer(&states))
    if err != nil {
        return nil, err
    }
    for i := 0; i < int(states.BufferLength)/StateSize; i++ {
        state := State{}
        err = binary.Read(bytes.NewReader(states.Buffer[i*StateSize:(i+1)*StateSize]), binary.BigEndian, &state)
        if err != nil {
            return records, fmt.Errorf("unable to read state %d: %w", i, err)
        }
        records = append(records, &state)
    }
    return records, nil
}
ezh commented 2 years ago

p.Read is

// Read reads IOCTL data
func (c *IOCTL) Read(cmd uint32, ptr unsafe.Pointer) error {
    _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, c.File.Fd(), uintptr(cmd), uintptr(ptr))
    if errno != 0 {
        return os.NewSyscallError("ioctl", fmt.Errorf("%d", int(errno)))
    }
    return nil
}