分析“关于Linux内核引入的accept_local参数的一个问题”

本文深入解析Linux内核中Ping本地IP的复杂过程,包括路由策略、策略路由、arp处理、rp_filter机制及策略路由的应用。通过调整内核参数和策略路由,成功实现了从特定接口Ping通本地另一接口的IP。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、概述

二、分析

2.1 问题描述

2.2 分析流程


一、概述

参考关于Linux内核引入的accept_local参数的一个问题,自己分析一下

二、分析

2.1 问题描述

ip link add veth0 type veth peer name veth1
ip addr add dev veth0 1.1.1.2/24
ip addr add dev veth1 1.1.1.1/24
ip link set veth0 up
ip link set veth1 up
# 以下一条为关键,down掉loopback
# ip link set dev lo down 

要求: 以源IP 1.1.1.2, ping 通 1.1.1.1,不使用namespace,docker等技术

2.2 分析流程

这里先执行ping 1.1.1.2发现是通的,但是抓包发现,报文的发送和接受都是从lo口进行的:

[root@localhost a]# tcpdump -i lo -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
18:44:43.510145 IP 1.1.1.1 > 1.1.1.1: ICMP echo request, id 5013, seq 125, length 64
18:44:43.510186 IP 1.1.1.1 > 1.1.1.1: ICMP echo reply, id 5013, seq 125, length 64

这不满足题目的要求,我们先分析一下为什么会产生上述结果。先分析 ping 1.1.1.1过程,报文先查路由,我们就先分析一下路由的情况,一般的,Linux有如下不同策略的路由表:

[root@localhost a]# ip rule 
0:            from all lookup local 
32766:    from all lookup main 
32767:    from all lookup default 

上述每一行代表一条策略,从上到下优先级降低,报文匹配到策略就会去查找对应的路由表。观察上图,每条报文都会依次查local,main,default表,对于ping本机的报文来说,一定会命中local表。因为在配置IP地址时(以veth1为例),生成如下路由:

首先查路由,在local表命中,linux路由查找返回local,于是端口替换为loopback口,可以看到,源IP并不是1.1.1.2,因为在未指定源IP的情况下,源IP设置成与目的IP一致:

icmp_route_lookup-> ip_route_output_key_hash->__ip_route_output_key_hash:

 

if (res.type == RTN_LOCAL) {
        if (!fl4->saddr) {
            if (res.fi->fib_prefsrc)
                fl4->saddr = res.fi->fib_prefsrc;
            else
                fl4->saddr = fl4->daddr;
        }

        dev_out = l3mdev_master_dev_rcu(FIB_RES_DEV(*res)) ? :
                          net->loopback_dev;

}

而loopback发送直接调用netif_rx,所以这时候会收到loopback来的报文。

那如何指定由veth0(1.1.1.2发送呢),可以使用ping 1.1.1.1 -I veth0,结果发现是不通的。下面我们来分析不通的原因,抓包发现veth1收到veth0的arp request而没有返回,这是由于arp reply时查路由失败。

[root@localhost a]# tcpdump -i veth1 -nn
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
19:38:21.755358 ARP, Request who-has 1.1.1.1 tell 1.1.1.2, length 28
19:38:22.755686 ARP, Request who-has 1.1.1.1 tell 1.1.1.2, length 28

参照代码

if (!skb_valid_dst(skb)) {
		int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
					       iph->tos, dev);

这时候1.1.1.1查找结果一定是LOCAL,而上述函数对于LOCAL的路由有一个源地址校验::

	if (res.type == RTN_LOCAL) {
		err = fib_validate_source(skb, saddr, daddr, tos,
					  0, dev, in_dev, &itag);
		if (err < 0)
			goto martian_source;
		goto local_input;
	}

具体为:

    fl4.daddr = src;
    fl4.saddr = dst;

    if (fib_lookup(net, &fl4, &res, 0))
        goto last_resort;
    if (res.type != RTN_UNICAST &&
        (res.type != RTN_LOCAL || !IN_DEV_ACCEPT_LOCAL(idev)))
        goto e_inval;

这里进行了反向路由查找,就是将源、目的地址调换再次查找路由,显然1.1.1.2也是local的,在没有配置accept local的情况下,就会出错。

我们将local_accept选项打开:

  • sysctl -w net.ipv4.conf.veth0.accept_local=1
  • sysctl -w net.ipv4.conf.veth1.accept_local=1

为什么会这样呢,我参考了概述中的文章里面的一句话:

任何从非loopback网卡进来的任何数据包的源地址不能是本机地址

这应该算作一个设计约束,防止非法报文进来。

那么这里深究一下:从loopback进来的报文为什么没有被这个条件拦住呢?看下loopback的实现:在loopback接口发送报文时:

[drivers/net/loopback.c]

static netdev_tx_t loopback_xmit(struct sk_buff *skb,
                 struct net_device *dev)
{
    /* Before queueing this packet to netif_rx(),
     * make sure dst is refcounted.
     */
    skb_dst_force(skb);

    skb->protocol = eth_type_trans(skb, dev);

    if (likely(netif_rx(skb) == NET_RX_SUCCESS)) {

    }

    return NETDEV_TX_OK;
}

loopback在发送时 skb_dst_force,这样在报文接收时,就可以跳过查找输入路由,从而不会在检查时出错!

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{

    if (!skb_valid_dst(skb)) {
        int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
                           iph->tos, dev);

}

修改了accept local ping还是不通,这是为什么呢,这个和linux反向过滤机制有关(rp_filter),反向查找以后,如果出口和入口不一致,则认为反向过滤失败,进行丢包,使用1.1.1.2查找出接口会命中local表项,出口为veth0,报文是从veth1来的,所以这里会失败,如下:

    if (FIB_RES_DEV(res) == dev)  //不匹配
        dev_match = true;
    if (dev_match) {
        ret = FIB_RES_NH(res).nh_scope >= RT_SCOPE_HOST;
        return ret;
    }
    if (no_addr)
        goto last_resort;
    if (rpf == 1)        //如果启用非rp_filter,进入失败流程
        goto e_rpf;

那我们将rp_filter关掉。

  • sysctl -w net.ipv4.conf.veth0.rp_filter=0
  • sysctl -w net.ipv4.conf.veth1.rp_filter=0
  • sysctl -w net.ipv4.conf.default.rp_filter=0
  • sysctl -w net.ipv4.conf.all.rp_filter=0

修改完成后还是不通,这时候抓包发现arp问题已经没有了,两个口抓到icmp request,但是没有回复

[root@localhost ~]# tcpdump -i veth1 -qnnvv
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
00:27:05.040687 IP (tos 0x0, ttl 64, id 26224, offset 0, flags [DF], proto ICMP (1), length 84)
    1.1.1.2 > 1.1.1.1: ICMP echo request, id 4901, seq 2966, length 64
00:27:06.040804 IP (tos 0x0, ttl 64, id 26332, offset 0, flags [DF], proto ICMP (1), length 84)
    1.1.1.2 > 1.1.1.1: ICMP echo request, id 4901, seq 2967, length 64

这时候input方向应该是没有限制了,我们考虑icmp response,这时候没办法向前面那样指定发送端口了,而根据我们前面的分析,本地地址会自动使用loopback口替换,在lo抓包:

[root@localhost ~]# tcpdump -i lo -qnnvv
tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
00:31:20.068443 IP (tos 0x0, ttl 64, id 38157, offset 0, flags [none], proto ICMP (1), length 84)
    1.1.1.1 > 1.1.1.2: ICMP echo reply, id 4901, seq 3221, length 64
00:31:21.069468 IP (tos 0x0, ttl 64, id 39082, offset 0, flags [none], proto ICMP (1), length 84)
    1.1.1.1 > 1.1.1.2: ICMP echo reply, id 4901, seq 3222, length 64

那么如何让icmp response包通过veth0口回复呢,我们知道在查路由结果如果时local才会将端口替换成lo,那么我们使用一种方法使查询结果不是local就好了,怎么办,使用策略路由!提供一个比local表优先级更高的表项:

[root@localhost ~]# ip rule show
0:      from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default

可以看到默认情况下,优先级最高的是0,对应local表,所以先将local表的优先级降低一下:

  • ip rule add pref 1000 tab local
  • ip rule del pref 0 tab local

接下来加入我们的策略

  • ip rule add fwmark 100 pref 100 tab 100
  • ip route add 1.1.1.2/32 dev veth1 src 1.1.1.1 tab 100

此时看下rule

[root@localhost a]# ip rule 
100:	from all fwmark 0x64 lookup 100 
1000:	from all lookup local 
32766:	from all lookup main 
32767:	from all lookup default

表100

[root@localhost a]# ip route show table 100
1.1.1.2 dev veth1 scope link src 1.1.1.1

注意,查上面的表是不会返回local的,因此不会有lo的替换,而是按照table 100指定的口进行发包。最后使用iptable将ping response打上fwmark标记,使之能进入table 100查路由。

iptables -t mangle -A OUTPUT -d 1.1.1.2/32 -j MARK --set-mark 100

此时可以ping通了

[root@localhost a]# ping 1.1.1.1 -I veth0
PING 1.1.1.1 (1.1.1.1) from 1.1.1.2 veth0: 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.081 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.150 ms

当然,原文通过-m也匹配table100,而不通过-I veth0也是可以的,在table 100加额外的一条路由

  • ip route add 1.1.1.1/32 dev veth0 src 1.1.1.2 tab 100
[root@localhost a]# ip route show table 100
1.1.1.1 dev veth0 scope link src 1.1.1.2 
1.1.1.2 dev veth1 scope link src 1.1.1.1 

此时也是通的:

[root@localhost a]# ping 1.1.1.1 -m 100
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.083 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.114 ms

最后作者提到将iptable mangle换成如下指定:

  • ip rule add to 1.1.1.2 pref 101 tab 100

这样是不行的,因为arp reply的时候会查路由,只有在结果是local的才会回复,而从1.1.1.1到1.1.1.2的arp在处理时会命中table100,table 100没有local,从而失败。

arp_process

    if (arp->ar_op == htons(ARPOP_REQUEST) &&
        ip_route_input_noref(skb, tip, sip, 0, dev) == 0) {

        rt = skb_rtable(skb);
        addr_type = rt->rt_type;

        if (addr_type == RTN_LOCAL) {
            int dont_send;

            dont_send = arp_ignore(in_dev, sip, tip);
            if (!dont_send && IN_DEV_ARPFILTER(in_dev))
                dont_send = arp_filter(sip, tip, dev);
            if (!dont_send) {
                n = neigh_event_ns(&arp_tbl, sha, &sip, dev);
                if (n) {
                    arp_send_dst(ARPOP_REPLY, ETH_P_ARP,
                             sip, dev, tip, sha,
                             dev->dev_addr, sha,
                             reply_dst);
                    neigh_release(n);
                }
            }

另外,作者在配置table 100的路由时,一开始写成

  • ip route add 1.1.1.1/32 dev veth1 src 1.1.1.2 tab 100
  • ip route add 1.1.1.2/32 dev veth0 src 1.1.1.1 tab 100

即将dev和src指反了,不匹配实际配置,这时候ping(ping 1.1.1.1 -m 100)是通的!

来分析一下原因,当ping发生时,命中上述第一条路由,但是出口选错了,本应该从veth0发出,但是查路由后却是veth1

口,这导致arp request从veth1口发出,下面是我用systemtap抓到的:

 0xffffffffc06f94a0 : veth_xmit+0x0/0x60 [veth]
 0xffffffff90beb166 : dev_hard_start_xmit+0x246/0x3b0 [kernel]
 0xffffffff90bee0d9 : __dev_queue_xmit+0x529/0x660 [kernel]
 0xffffffff90bee243 : dev_queue_xmit_sk+0x13/0x20 [kernel]
 0xffffffff90c65ae3 : arp_xmit+0x33/0xa0 [kernel]
 0xffffffff90c65b95 : arp_send_dst.part.16+0x45/0x50 [kernel]
 0xffffffff90c65d0a : arp_solicit+0x12a/0x2e0 [kernel]
 0xffffffff90bf8240 : neigh_probe+0x50/0x70 [kernel]
 0xffffffff90bf93ae : __neigh_event_send+0xae/0x260 [kernel]
 0xffffffff90bf9c7b : neigh_resolve_output+0x13b/0x220 [kernel]
 0xffffffff90c3717c : ip_finish_output+0x2ac/0x7a0 [kernel]
 0xffffffff90c37973 : ip_output+0x73/0xe0 [kernel]
 0xffffffff90c35567 : ip_local_out_sk+0x37/0x40 [kernel]
 0xffffffff90c383c6 : ip_send_skb+0x16/0x50 [kernel]
 0xffffffff90c38433 : ip_push_pending_frames+0x33/0x40 [kernel]
 0xffffffff90c5efa9 : raw_sendmsg+0x879/0xa30 [kernel]
 0xffffffff90c6e5a9 : inet_sendmsg+0x69/0xb0 [kernel]
 0xffffffff90bcc396 : sock_sendmsg+0xb6/0xf0 [kernel]
 0xffffffff90bccaa1 : SYSC_sendto+0x121/0x1c0 [kernel]
 0xffffffff90bce42e : SyS_sendto+0xe/0x10 [kernel]
 veth_xmit dev_name:veth1

这样arp请求就会从veth0口收到,回复的时候用veth0口的mac进行回复,这样会导致veth0,veth1学到的mac地址是相反的!

16:36:42.700706 2a:59:93:9e:c5:d6 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 1.1.1.1 tell 1.1.1.2, length 28
16:36:42.700787 6a:61:8e:4b:71:80 > 2a:59:93:9e:c5:d6, ethertype ARP (0x0806), length 42: Reply 1.1.1.1 is-at 6a:61:8e:4b:71:80, length 28
16:36:42.700928 2a:59:93:9e:c5:d6 > 6a:61:8e:4b:71:80, ethertype IPv4 (0x0800), length 98: 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6754, seq 1, length 64
16:36:42.701010 6a:61:8e:4b:71:80 > 2a:59:93:9e:c5:d6, ethertype IPv4 (0x0800), length 98: 1.1.1.1 > 1.1.1.2: ICMP echo reply, id 6754, seq 1, length 64
16:36:47.705842 6a:61:8e:4b:71:80 > 2a:59:93:9e:c5:d6, ethertype ARP (0x0806), length 42: Request who-has 1.1.1.2 tell 1.1.1.2, length 28
16:36:47.706204 2a:59:93:9e:c5:d6 > 6a:61:8e:4b:71:80, ethertype ARP (0x0806), length 42: Reply 1.1.1.2 is-at 2a:59:93:9e:c5:d6, length 28

由于两端是对称的,这样处理起来也不会有什么错误。当然了,你用ping 1.1.1.1 -I veth0 肯定就不通了,对吧。

 

# 全局配置段(影响整个Nginx服务) user nginx; # 运行用户(建议使用专用用户如nginx) worker_processes auto; # 工作进程数(通常设为CPU核心数或auto) error_log /var/log/nginx/error.log warn; # 错误日志路径及级别 pid /var/run/nginx.pid; # 进程PID文件位置 # 事件驱动模块配置 events { worker_connections 1024; # 单个工作进程的最大连接数 multi_accept on; # 允许同时接收多个连接 use epoll; # 使用高效事件模型(Linux建议epoll) } # HTTP核心模块配置 http { include /etc/nginx/mime.types; # 包含MIME类型定义文件 default_type application/octet-stream; # 默认MIME类型 # 日志格式定义 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; # 访问日志路径及格式 sendfile on; # 启用高效文件传输模式 tcp_nopush on; # 优化数据包发送(减少网络报文数) tcp_nodelay on; # 禁用Nagle算法(降低延迟) keepalive_timeout 65; # 客户端长连接超时时间(秒) types_hash_max_size 2048; # MIME类型哈希表大小 # 开启Gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # 包含其他配置文件(如虚拟主机配置) include /etc/nginx/conf.d/*.conf; # 示例:反向代理配置(可写在conf.d/目录下的单独文件) server { listen 80; # 监听端口 server_name message.zgxhwy.com; # 域名或IP # 静态资源服务 location /static/ { alias /path/to/static/files/; # 静态文件目录 expires 30d; # 缓存过期时间 access_log off; # 关闭访问日志(可选) } # 反向代理到后端应用 location / { proxy_pass http://localhost:8080; # 后端服务地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
最新发布
03-28
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值