内核代码
内核中对绑定非本地地址的相关判断代码,位于net/ipv4/af_inet.c中:
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
chk_addr_ret = inet_addr_type(net, addr->sin_addr.s_addr);
if (!net->ipv4_sysctl_ip_nonlocal_bind &&
!(inet->freebind || inet->transparent) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL &&
chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out;
}
由代码可见,需要满足以下三个条件中的一个,才能绑定成功:
1)此socket所属的net namespace设置了全局的ipv4_sysctl_ip_nonlocal_bind;
2)此socket设置了IP_FREEBIND选项;
3)此socket设置了IP_TRANSPARENT选项;
echo 1 > /proc/sys/net/ipv4/ip_nonlocal_bind
int on = 1;
setsockopt(sock, SOL_IP, IP_FREEBIND, (char *)&on, sizeof(on))
setsockopt(sock, SOL_IP, IP_TRANSPARENT, (char *)&on, sizeof(on))
三者的区别
IP_TRANSPARENT不仅是允许绑定非本地地址,更重要的是与netfilter一起实现透明代理功能。
透明代理
a) 接收
利用transparent选项,本机已经建立了socket监听客户端到服务器的连接,需要iptables在IP层把外出流量导入到本机。
iptables -t mangle -N DIVERT
iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
在PREROUTING hook点上,提前检查是否有本机socket在监听此连接,transparent选项忽略未设置此选项的socket。另外一个有用的选项--nowildcard,可用于将此连接关联到监听在INADDR_ANY的socket上。如果找到socket,设置mark等于1,路由到本机。
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
b)发送
主要涉及到查找出口路由函数,其中要对源地址做检查,非本机地址导致查找失败,代码文件net/ipv4/route.c:
struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4)
{
if (fl4->saddr) {
...
if (!(fl4->flowi4_flags & FLOWI_FLAG_ANYSRC)) {
/* It is equivalent to inet_addr_type(saddr) == RTN_LOCAL */
if (!__ip_dev_find(net, fl4->saddr, false))
goto out;
}
}
}
针对此问题,IP_TRANSPARENT选项关闭了源地址检查include/net/inet_sock.h:
static inline __u8 inet_sk_flowi_flags(const struct sock *sk)
{
__u8 flags = 0;
if (inet_sk(sk)->transparent || inet_sk(sk)->hdrincl)
flags |= FLOWI_FLAG_ANYSRC;
return flags;
}
c)重定向
利用TPROXY目标,将目的端口为80的tcp连接重定向到本机监听在192.168.1.1:50080上的sock,不改变数据包的内容,即透明模式。
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-ip 192.168.1.1 --on-port 50080
另外一种改变数据包目的地址的重定向为nat方式,如下:
iptables -t nat -N MY_HTTP #在nat表上新建名为MY_HTTP自定义链
iptables -t nat -p tcp -A MY_HTTP -j REDIRECT --to-ports 50080 #将进入MY_HTTP链的数据包端口重定向到50080上
iptables -t nat -N MY_NAT #在nat表上新建名为MY_NAT自定义链
iptables -t nat -A PREROUTING -p tcp -j MY_NAT #将MY_NAT加入到PREROUTING链后
iptables -t nat -A MY_NAT -p tcp -m multiport --dports 80 -j MY_HTTP #在端口为80时执行MY_HTTP链
内核版本
Linux-3.10.0