Linux 双网卡 Server 与直连 Device 通信:标准路由与 Proxy ARP 两种方案(含勘误)

九月 20, 2026 [网络与安全] #Linux #网络 #routing

背景

有一台 Linux Server,拥有两块网卡:

                 Server
          ┌─────────────────┐
          │                 │
      eth0│                 │eth1
          │                 │
          └─────────────────┘

其中:

Device ───────── eth1 ───────── Server
                                  │
                                  │ eth0
                                  ▼
                              192.168.1.10
                              HTTP Server

这里有一个非常重要的概念:

HTTP 服务绑定在哪个网卡的 IP 上,并不意味着只有从这个网卡进入的数据包才能访问这个服务。

Linux 判断一个 IP 数据包是否属于本机,主要看:

目标 IP 是否是本机地址

而不是:

数据包是不是从拥有这个 IP 的网卡进入

因此,只要 Device 能够把 dst = 192.168.1.10 的数据包送到 Server,Server 就可以把它交给 192.168.1.10:80 上的 HTTP 服务。

真正需要解决的问题是:

Device 如何把目标为 192.168.1.10 的数据包送到 Server 的 eth1?

下面介绍两种不同的方案。

方案一:不同网段 + 默认网关

这是最传统、最标准的三层网络设计。

网络配置:

Server:
  eth0 = 192.168.1.10/24
  eth1 = 192.168.100.1/24

Device:
  eth1 = 192.168.100.2/24
  default gateway = 192.168.100.1

拓扑:

                         Server
                  ┌──────────────────┐
                  │                  │
LAN ──────────────┤ eth0             │
192.168.1.0/24    │ 192.168.1.10     │
                  │                  │
                  │ eth1             │
                  │ 192.168.100.1    │
                  └────────┬─────────┘
                           │
                           │ Ethernet
                           │
                    192.168.100.2
                         Device

Device 和 Server 的 eth1 属于 192.168.100.0/24,Server 的 eth0 属于 192.168.1.0/24,两个接口处于不同 IP 网络。

Device 的路由表

Device 配置:

ip addr add 192.168.100.2/24 dev ethX
ip route add default via 192.168.100.1

路由表可以理解为:

192.168.100.0/24 dev ethX
default via 192.168.100.1

当 Device 访问 192.168.1.10 时,目标不属于 192.168.100.0/24,因此匹配 default via 192.168.100.1:

192.168.1.10
      │
      ▼
下一跳:192.168.100.1
      │
      ▼
Server eth1

ARP 的作用

Device 已经知道下一跳是 192.168.100.1,但 Ethernet 发送数据需要 MAC 地址,所以先发 ARP:

Who has 192.168.100.1?

Server 的 eth1 回复:

192.168.100.1 is-at <Server eth1 MAC>

然后 Device 才真正发送 IP 数据包:

Ethernet:
    src MAC = Device MAC
    dst MAC = Server eth1 MAC

IP:
    src = 192.168.100.2
    dst = 192.168.1.10

TCP:
    src port = random
    dst port = 80

注意:ARP 解决的是「下一跳 IP 对应哪个 MAC」,并不负责决定 IP 数据包最终应该去哪里——IP 路由已经在前面决定好了。

Server 收到数据包之后

Server eth1 收到 src = 192.168.100.2, dst = 192.168.1.10。

很多人会认为 eth1 → eth0 所以必须开启 IP forwarding,实际上不需要。因为 192.168.1.10 是 Server 自己的 IP,Linux 发现目标属于本机后直接交给本机协议栈:

eth1
  │
  ▼
Linux IP stack
  │
  ▼
发现目标 IP 是本机地址
  │
  ▼
TCP
  │
  ▼
192.168.1.10:80
  │
  ▼
HTTP Server

这里不是 eth1 → routing → eth0,而是 eth1 → 本机协议栈 → HTTP Server。因此访问 Server 自己的服务时,net.ipv4.ip_forward 不需要开启。

Server 的回包

HTTP Server 回复 src = 192.168.1.10, dst = 192.168.100.2,Server 查路由:

192.168.100.0/24 dev eth1

于是回包从 eth1 发出,直接到达 Device。完整 TCP 流程:

Device                         Server
192.168.100.2                  eth1: 192.168.100.1
      │                              │
      │ TCP SYN                      │
      │ dst=192.168.1.10:80          │
      ├─────────────────────────────>│
      │                              │
      │                         HTTP Server
      │                              │
      │ TCP SYN/ACK                  │
      │<─────────────────────────────┤
      │                              │
      │ TCP ACK                      │
      ├─────────────────────────────>│
      │                              │

方案一的特点

方案二:同一个地址空间 + /30 精确网络 + ARP 代答

第二种方案:

Server:
  eth0 = 192.168.1.10/24
  eth1 = 192.168.1.101/30

Device:
  192.168.1.102/24

拓扑:

                         Server
                  ┌──────────────────┐
                  │                  │
LAN ──────────────┤ eth0             │
                  │ 192.168.1.10/24  │
                  │                  │
                  │ eth1             │
                  │ 192.168.1.101/30 │
                  └────────┬─────────┘
                           │
                           │ Ethernet
                           │
                    192.168.1.102/24
                         Device

这个方案看起来很奇怪:为什么让 eth0 和 Device 处在同一个 /24 里?

核心目的就是:

让 Device 认为 192.168.1.10 和自己处于同一个 /24 网络,从而不需要配置 Gateway 或静态路由。

而 Server 又通过 eth1 的 /30 形成一个非常小、更加精确的直连网络:

Server eth1              Device
192.168.1.101 ───────── 192.168.1.102
       \                    /
        \──── /30 ─────────/

为什么 eth1 使用 /30?

192.168.1.101/30 对应的网络是 192.168.1.100/30:

192.168.1.100  network
192.168.1.101  Server
192.168.1.102  Device
192.168.1.103  broadcast

这是点对点链路中 /30 的经典用法。

Server 的路由表与最长前缀匹配

Server 同时拥有:

192.168.1.0/24 dev eth0
192.168.1.100/30 dev eth1

两个网络存在包含关系:

192.168.1.0/24
└─────────────────────────────────────┐
                                      │
                    192.168.1.100/30 │
                    └──────────────┐  │
                    .100 .101 .102 .103

当 Server 访问 192.168.1.102 时,两个路由都能匹配,但 /30 比 /24 更具体,按 Longest Prefix Match(最长前缀匹配) 选择:

Server 192.168.1.10
      │
      │ dst = 192.168.1.102
      ▼
Longest Prefix Match
      │
      ▼
192.168.1.100/30 dev eth1

这就是 /30 在这个设计里的作用:让 Server 到 Device 的回程明确走 eth1。

Device 为什么可以直接访问 192.168.1.10?

因为 Device 配置的是 192.168.1.102/24 而不是 /30,它认为 192.168.1.0/24 是自己的本地网络,192.168.1.10 属于其中。所以它不找 Gateway、不查静态路由,而是直接 ARP:

Who has 192.168.1.10?

勘误:代答 ARP 的其实不是 proxy_arp

本文最初的解释是「在 eth1 上开 proxy_arp,Server 就会替 192.168.1.10 回答 ARP」——这是同类教程里非常常见的一种说法,核对内核实际行为、并在隔离 netns 中实测复核后发现并不成立:目标 IP 就是 Server 本机地址(.10 配在 eth0 上)时,代答与 proxy_arp 无关。

误区是怎么来的

方案二确实能通,而 proxy_arp 又经常出现在同类方案里,于是「能通」被归因给了这个开关。实际上:

这种「先动手、后归因」的顺序很容易制造错误的因果:开关打开时网络是通的,不等于这个开关是通的必要条件。

内核实际怎么处理

Linux 收到 ARP 请求后,先判断目标 IP 是不是本机地址:

由此还能得到两个反直觉的结论:

一句话判定

判断一个场景到底需不需要 proxy_arp,只看目标 IP:目标是本机地址 → 看 arp_ignore;目标不是本机地址 → 才看 proxy_arp + ip_forward。 文末「如何验证方案二」里给出了可复现的判定实验。

注:proxy ARP 并不是把 ARP 广播从 eth1 转发到 eth0,而是 Server 自己代替目标 IP 回答 ARP。

proxy_arp 真正用在哪

既然目标为本机地址时轮不到 proxy_arp,那它到底解决什么问题?它解决的是:

发送方认为目标和自己同一子网、直接 ARP 目标 IP,但 ARP 广播到不了目标,而代理主机能路由到目标。

Linux 上的完整触发链:

  1. 入接口收到 ARP 请求;
  2. 目标 IP 不是本机地址(是本机地址就走 arp_ignore 那条分支,见上文勘误);
  3. 入接口 proxy_arp=1,并且处于转发状态(通常即 ip_forward=1)——实测把 ip_forward 关掉后,proxy_arp=1 也不再代答,所以「只开 proxy_arp」是不够的;
  4. 本机路由表能到达目标(目标若就在入接口的二层网段上,本来就会由目标自己应答,不需要代答);
  5. 本机用自己的 MAC 替目标应答,发送方把帧发给本机,之后由数据面转发到目标。

真实场景:

这些场景的共同点是:发送方都「以为目标在本地」,且没有(或不能用)正确的网关。如果发送方正确配置了默认网关,跨网段时它 ARP 的是网关而不是目标,根本不会触发 proxy_arp。

现代网络里它很少被主动使用:proxy_arp 本质上是「合法 ARP 欺骗」,有中间人风险,还会带来 ARP 表膨胀、拓扑不透明、排查困难等问题。能用网关、路由协议、隧道或桥接解决时,都不该用它兜底。

完整数据流

Device                              Server
  │                                    │
  │ ARP: Who has 192.168.1.10?         │
  ├───────────────────────────────────>│
  │                                    │
  │ ARP Reply: .10 is-at eth1 MAC      │
  │ (arp_ignore=0 的默认行为)         │
  │<───────────────────────────────────┤
  │                                    │
  │ TCP SYN                            │
  │ dst=.10:80                         │
  ├───────────────────────────────────>│
  │                                    │
  │                         HTTP Server│
  │                                    │
  │ TCP SYN/ACK                        │
  │<───────────────────────────────────┤
  │                                    │
  │ TCP ACK                            │
  ├───────────────────────────────────>│

Server 收到 dst = 192.168.1.10 后同样不需要 forwarding(目标是本机地址),直接交给本机 HTTP 服务;回包 src = 192.168.1.10, dst = 192.168.1.102 经最长前缀匹配走 /30 的 eth1 回到 Device。

两种方案的核心区别

两种方案最终都实现 Device → Server 192.168.1.10:80,但 Device 到 Server 的「第一跳」完全不同。

方案一

Device 192.168.100.2/24
      │
      │ 目标不在本地网段
      ▼
Default Gateway 192.168.100.1
      │
      ▼
Server eth1

核心:IP Routing。

方案二

Device 192.168.1.102/24
      │
      │ 目标在本地网段(自认为)
      ▼
ARP
      │
      ▼
Server eth1

Device 以为 .10 就在本地网段,直接 ARP;Server 侧靠默认的 arp_ignore=0 代答,回包再靠最长前缀匹配走 /30 的 eth1。

核心:同网段 ARP 代答 + 精确路由。

两种方案对比

项目方案一方案二
Server eth0192.168.1.10/24192.168.1.10/24
Server eth1192.168.100.1/24192.168.1.101/30
Device192.168.100.2/24192.168.1.102/24
Device Gateway192.168.100.1不需要
Device 静态路由不需要额外路由不需要
ARP 代答不需要需要(默认 arp_ignore=0 即可)
proxy_arp不需要也不需要(目标为本机地址)
Device 访问 .10 的方式通过 Gateway直接 ARP
Server 回 Deviceeth1 的 /24 路由eth1 的 /30 更具体路由
两个接口地址空间不重叠有重叠
网络模型标准三层路由同网段 ARP 代答 + 精确路由
配置复杂度较低较高,且依赖默认 ARP 行为

如何验证方案二

查看路由与最长前缀匹配

ip route
# 192.168.1.0/24 dev eth0
# 192.168.1.100/30 dev eth1

ip route get 192.168.1.102
# 192.168.1.102 dev eth1 src 192.168.1.101

ip route get 是验证 /30 优先于 /24 最直接的方法。

查看 ARP 相关参数

sysctl net.ipv4.conf.eth1.arp_ignore   # 默认 0
sysctl net.ipv4.conf.eth1.proxy_arp    # 默认 0

抓 ARP

Server 上:

sudo tcpdump -ni eth1 arp

Device 上 ping:

ping 192.168.1.10

可以看到:

ARP, Request who-has 192.168.1.10 tell 192.168.1.102
ARP, Reply 192.168.1.10 is-at <Server eth1 MAC>

这可以直接证明 Device → eth1 → ARP 代答 这条路径成立。

复现勘误:证明代答与 proxy_arp 无关

# 1. Server 上确认 proxy_arp 是关的(默认就是 0)
sysctl net.ipv4.conf.eth1.proxy_arp

# 2. Device 上清掉 ARP 缓存再试,依然能通
ip neigh del 192.168.1.10 dev ethX
ping -c 1 192.168.1.10

# 3. 反证:把 arp_ignore 改成 1,代答立即消失
sudo sysctl -w net.ipv4.conf.eth1.arp_ignore=1
# Device 上再清一次缓存
ip neigh del 192.168.1.10 dev ethX
ping -c 1 192.168.1.10        # 不通:ARP 无人应答

# 4. 实验完恢复默认
sudo sysctl -w net.ipv4.conf.eth1.arp_ignore=0

第 2 步通了、第 3 步不通,就足以推翻「必须开 proxy_arp」的归因:如果代答真由 proxy_arp 提供,proxy_arp=0 时第 2 步就该失败;而改变 arp_ignore 能立刻开关代答,说明真正起作用的是它。

以上结论在隔离的 netns 里按本文拓扑实测复核过(三个命名空间分别模拟 Server、Device 和 eth0 侧的远端主机;下表 proxy_arp 指 eth1 的开关,B 组回程另在 eth0 开了 proxy_arp):

A 组:目标 .10 是 Server 本机地址(配在 eth0 上)

proxy_arparp_ignoreip_forwardping .10Device 的 ARP 表
0(默认)0(默认)0(默认)OK.10 → eth1 MAC
100OK.10 → eth1 MAC
010FAILINCOMPLETE
111FAILINCOMPLETE

B 组:目标 .50 非本机,经 eth0 可达

proxy_arparp_ignoreip_forwardping .50Device 的 ARP 表
001FAILINCOMPLETE
101OK.50 → eth1 MAC
100FAILINCOMPLETE

上面的对照实验分别证明:

  1. 全默认(proxy_arp=0、arp_ignore=0、ip_forward=0)就能代答本机地址——方案二不需要 proxy_arp;
  2. 本机地址的代答完全由 arp_ignore 控制,arp_ignore=1 后代答消失,且 proxy_arp=1 + ip_forward=1 也救不回来;
  3. proxy_arp 只在目标非本机时生效(B1 → B2);
  4. 即便如此,proxy_arp 也必须配合 ip_forward=1 才能代答(B3)。

抓 TCP

Server 上:

sudo tcpdump -ni eth1 'tcp port 80'
sudo tcpdump -ni eth0 'tcp port 80'

可以看到 eth1 上有 Device 到 192.168.1.10:80 的 TCP SYN,而 eth0 上通常看不到这条连接——因为它是 eth1 → 本机 TCP socket,而不是 eth1 → eth0 转发。

这能直接证明:

「服务绑定在 eth0 地址」和「数据包必须从 eth0 进入」是两件完全不同的事情。

注意事项:方案二属于非典型网络设计

方案二能工作,但依赖若干默认行为,在复杂环境中要特别小心:

总结

这个问题实际上涉及 Linux 网络中的三个不同层次:

┌──────────────────────────────────────┐
│              Application             │
│          HTTP 192.168.1.10:80        │
└──────────────────┬───────────────────┘
                   │
┌──────────────────▼───────────────────┐
│                  IP                  │
│       Route / Longest Prefix         │
└──────────────────┬───────────────────┘
                   │
┌──────────────────▼───────────────────┐
│              Ethernet                │
│                 ARP                  │
└──────────────────────────────────────┘

方案一:不同网段 + 默认网关,靠 IP Routing 把包交给 Server,标准三层设计。

方案二:Device 用 /24 把 .10 认成同网段邻居,直接 ARP;Server 用默认的 arp_ignore=0 在 eth1 上代答,再用 /30 的最长前缀匹配把回程钉在 eth1 上。

最后要记住三点:

  1. 服务绑定在哪个接口的 IP 上,和「数据包从哪个接口进入」无关——只要目标 IP 是本机地址,内核就交给本机协议栈,不需要 forwarding;
  2. 方案二里代答 ARP 的是默认的 arp_ignore=0(ARP flux),不是 proxy_arp;proxy_arp 只在目标 IP 非本机、需要经其他接口路由时才起作用,而且要求 ip_forward=1;
  3. 验证时「先关掉可疑开关再看」——默认行为的功劳很容易被记到刚打开的开关上,本文对 proxy_arp 的误判就是这么来的。