1 iptable的四表五链
Netfilter 的实现可以简单地归纳为四表五链
五链:在内核协议栈的各个重要关卡,埋下了五个HOOK(钩子函数)。每一个钩子函数对应的一些列的规则,以链表的形式存在,所以俗称为五链
五链主要体现在内核的接收,发送,和转发上面
2 内核的接收过程
- Linux内核网络包接收在IP层的入口函数是ip_rcv。
- 网络包在这里碰到的第一个HOOK就是PREROUTING。
- 当PREROUTING处理完后,会进行路由选择,如果发现是本设备的网络包。进入ip_local_deliver中,在这里又会遇到INPUT钩子
//file: net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, ......){
......
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
}
//file: net/ipv4/ip_input.c
static int ip_rcv_finish(struct sk_buff *skb){
...
if (!skb_dst(skb)) {
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, skb->dev);
...
}
...
return dst_input(skb);
}
如果发现是本地设备,就会执行ip_local_deliver函数,接着执行LOCAL_IN钩子函数
int ip_route_input_noref(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev)
{
.......
res = ip_route_input_slow(skb, daddr, saddr, tos, dev);
rcu_read_unlock();
return res;
}
static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
u8 tos, struct net_device *dev)
{
.......
res.fi = NULL;
res.table = NULL;
if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0))
goto brd_input;
/* Accept zero addresses only to limited broadcast;
* I even do not know to fix it or not. Waiting for complains :-)
*/
if (ipv4_is_zeronet(saddr))
goto martian_source;
if (ipv4_is_zeronet(daddr))
goto martian_destination;
/* Following code try to avoid calling IN_DEV_NET_ROUTE_LOCALNET(),
* and call it once if daddr or/and saddr are loopback addresses
*/
if (ipv4_is_loopback(daddr)) {
if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
goto martian_destination;
} else if (ipv4_is_loopback(saddr)) {
if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
goto martian_source;
}
out: return err;
brd_input:
if (skb->protocol != htons(ETH_P_IP))
goto e_inval;
if (!ipv4_is_zeronet(saddr)) {
err = fib_validate_source(skb, saddr, 0, tos, 0, dev,
in_dev, &itag);
if (err < 0)
goto martian_source;
}
flags |= RTCF_BROADCAST;
res.type = RTN_BROADCAST;
RT_CACHE_STAT_INC(in_brd);
local_input:
do_cache = false;
if (res.fi) {
if (!itag) {
rth = rcu_dereference(FIB_RES_NH(res).nh_rth_input);
if (rt_cache_valid(rth)) {
skb_dst_set_noref(skb, &rth->dst);
err = 0;
goto out;
}
do_cache = true;
}
}
// local_input
rth = rt_dst_alloc(net->loopback_dev, flags | RTCF_LOCAL, res.type,
IN_DEV_CONF_GET(in_dev, NOPOLICY), false, do_cache);
if (!rth)
goto e_nobufs;
.......
}
static struct rtable *rt_dst_alloc(struct net_device *dev,
unsigned int flags, u16 type,
bool nopolicy, bool noxfrm, bool will_cache)
{
struct rtable *rt;
rt = dst_alloc(&ipv4_dst_ops, dev, 1, DST_OBSOLETE_FORCE_CHK,
(will_cache ? 0 : (DST_HOST | DST_NOCACHE)) |
(nopolicy ? DST_NOPOLICY : 0) |
(noxfrm ? DST_NOXFRM : 0));
if (rt) {
rt->rt_genid = rt_genid_ipv4(dev_net(dev));
rt->rt_flags = flags;
rt->rt_type = type;
rt->rt_is_input = 0;
rt->rt_iif = 0;
rt->rt_pmtu = 0;
rt->rt_mtu_locked = 0;
rt->rt_gateway = 0;
rt->rt_uses_gateway = 0;
rt->rt_table_id = 0;
INIT_LIST_HEAD(&rt->rt_uncached);
rt->dst.output = ip_output;
if (flags & RTCF_LOCAL)
// 本地路由
rt->dst.input = ip_local_deliver;
}
return rt;
}
/*
* Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb->dev);
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
// 执行NF_INET_LOCAL_IN钩子函数
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
接收总结:网卡数据进入协议栈->PREROUTING->路由判断(本机)->INPUT链->....
3 发送数据过程
- Linux在发送网络数据包过程中,进入协议栈后,进行路由选择。第一个HOOK钩子就是OUTPUT,然后进入POSTRUTING链
//file: net/ipv4/ip_output.c
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
// 路由选择过程
// 选择完后记录路由信息到 skb 上
rt = (struct rtable *)__sk_dst_check(sk, 0);
if (rt == NULL) {
// 没有缓存则查找路由项
rt = ip_route_output_ports(...);
sk_setup_caps(sk, &rt->dst);
}
skb_dst_set_noref(skb, &rt->dst);
...
//发送
ip_local_out(skb);
}
//file: net/ipv4/ip_output.c
int __ip_local_out(struct sk_buff *skb)
{
struct iphdr *iph = ip_hdr(skb);
iph->tot_len = htons(skb->len);
ip_send_check(iph);
// 执行NF_HOOK 将数据包发送到NF_INET_LOCAL_OUT(OUTPUT)
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
skb_dst(skb)->dev, dst_output);
}
执行完进入到dst_output
总结发送过程:路由选择->OUTPUT链->POSTRUTING链->....
4 转发过程
- Linux可以像路由器一样来工作。接收到不属于自己的包,根据路由表现在合适的网卡设备,将数据转发出去
- 这个过程需要经过接收数据的前半段,在ip_rcv中经过PRERUTING链,然后路由发现不是自己的本设备的包,就进入ip_forward进行转发。
- 在这里又会遇到FORWARD链路。最后进入ip_output进行真正的发送,遇到POSTROUTING链
//file: net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, ......){
......
// NF_INET_PRE_ROUTING 链上规则处理完后进入ip_rcv_finish,在这里进行路由然后进入des_input
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
}
//file: include/net/dst.h
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
转发的过程这几步和接收的过程一模一样,不过内核路径就要从上面的input方法调用开始分道扬镳,非本设备的不会进入ip_local_deliver而是进入ip_forward
//file: net/ipv4/ip_forward.c
int ip_forward(struct sk_buff *skb)
{
......
// 这里的ip_forward_finish路面会发生到IP层的ip_output
return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
rt->dst.dev, ip_forward_finish);
}
//file: net/ipv4/ip_output.c
int ip_output(struct sk_buff *skb)
{
...
//再次交给 netfilter,完毕后回调 ip_finish_output
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
总结转发的过程:PRERUTING->路由判断(非本机)->FORWARD->POSTRUTING
5 IPtable汇总
总结:接收过程1,2 发送的过程是4, 5 ,转发的过程是1,3,5
iptables 中的五个链。在每一个链上都可能是由许多个规则组成的。在 NF_HOOK 执行到这个链的时候,就会把规则按照优先级挨个过一遍。如果有符合条件的规则,则执行规则对应的动作。
不同的规则又分为四个部分:raw mangle nat filter
raw: 表的作用是将命中规则的包,跳过其他表的处理,它的优先级最高
mangle: 表的作用是根据规则的修改数据包的一些标志位。比如TTL
nat: 表的作用是实现网络地址转换
filter: 表的重要是过来某一些数据包,防火墙的基础
Raw 表目的是跳过其它表,所以只需要在接收和发送两大过程的最开头处把关,所以只需要用到 PREROUTING 和 OUTPUT 两个钩子。
Mangle 表有可能会在任意位置都有可能会修改网络包,所以它是用到了全部的钩子位置。
NAT 分为 SNAT(Source NAT)和 DNAT(Destination NAT)两种,可能会工作在 PREROUTING、INPUT、OUTPUT、POSTROUTING 四个位置。
Filter 只在 INPUT、OUTPUT 和 FORWARD 这三步中工作就够了。