SEEDLab-VPN隧道

SEEDLab-VPN隧道

【实验目的】

本实验通过从零构建一个简单的VPN隧道,深入理解虚拟专用网络(VPN)的工作原理。实验涵盖TUN/TAP虚拟网络接口的创建与配置、IP隧道技术(将原始IP数据包封装在UDP载荷中传输)、路由配置与双向流量转发等核心内容,最终实现主机U通过VPN隧道安全访问专用网络内部主机V的功能。

【实验原理】

VPN与IP隧道。 虚拟专用网络(VPN)建立在公共网络之上,使得远程主机能够像在专用网络中一样安全通信。IP隧道是VPN的核心技术之一,其实质是将一个IP数据包作为载荷封装在另一个IP数据包的UDP(或TCP)段中进行传输。发送端从虚拟网络接口读取原始IP数据包,将其放入UDP数据报的载荷字段,通过公共网络发送到VPN服务器;服务器收到后从UDP载荷中还原原始IP数据包,再通过虚拟网络接口注入内核协议栈,由内核路由到最终目的地。

TUN/TAP虚拟接口。 TUN和TAP是Linux内核提供的虚拟网络驱动程序。TUN工作在第3层(网络层),处理IP数据包;TAP工作在第2层(数据链路层),处理以太网帧。用户空间程序通过文件描述符与TUN/TAP接口交互:从接口读取的数据包是内核协议栈转发来的,向接口写入的数据包则被注入内核,如同从外部网络收到的一样。这种机制使得用户空间程序能够在不修改内核代码的前提下拦截和注入网络数据包。

select()多路复用。 当程序需要同时监听TUN接口和UDP套接字两个文件描述符时,使用select()系统调用可以阻塞等待任一接口上有数据到达,避免忙轮询浪费CPU资源。select()返回就绪的文件描述符集合,程序据此判断数据来源并分别处理。

路由转发。 VPN服务器需要启用IP转发功能(net.ipv4.ip_forward=1),使其在TUN接口和物理网络接口之间转发数据包,充当网关角色。专用网络内的主机也需要正确配置路由表,确保返回数据包能通过VPN服务器路由回隧道另一端。

Task 1: 设置网络环境

实验网络拓扑图

本实验需要三台机器:VPN客户端(主机U,10.9.0.5)、VPN服务器/路由器(10.9.0.11和192.168.60.11)和专用网络内的主机V(192.168.60.5)。VPN客户端和服务器通过模拟互联网的10.9.0.0/24网络连接,主机V通过192.168.60.0/24网络连接到VPN服务器。

使用docker-compose启动实验环境后,首先验证各主机之间的基本连通性。在VPN客户端容器中测试能否ping通VPN服务器(10.9.0.11),以及能否ping通主机V(192.168.60.5)。预期结果是客户端能与服务器通信,但无法直接与主机V通信,因为主机V位于专用网络内部,不能从"互联网"直接访问。

在VPN服务器(Router)上使用tcpdump分别在两个网络接口上抓包,验证可以捕获到两个网络上的数据包,确保网络环境搭建正确。

网络连通性测试结果

在Router上使用tcpdump捕获数据包

Task 2: 创建和配置TUN接口

2.a 创建TUN接口

TUN接口的创建通过打开/dev/net/tun设备文件,使用ioctl系统调用设置IFF_TUN标志来完成。程序使用姓氏拼音(高)作为接口名称前缀,内核会自动分配序号。运行tun.py程序后,在另一个终端中执行ip address命令,可以看到系统中多出了一个名为gao0的TUN接口。

1
2
3
4
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'gao%d', IFF_TUN | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tun, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")

创建TUN接口并查看接口名称

2.b 配置TUN接口

新创建的TUN接口处于down状态且没有IP地址,需要手动配置。通过ip addr add为接口分配IP地址192.168.53.99/24,再通过ip link set将其设置为up状态。配置完成后,TUN接口在系统中处于激活状态,可以收发数据包。

1
2
os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

设置TUN接口

2.c 从TUN接口读取数据包

修改tun.py,将while循环改为从TUN接口读取数据包并解析为Scapy IP对象。当在主机U上ping 192.168.53.0/24网络中的地址时,由于该网段与TUN接口的IP地址在同一子网内,内核会将数据包路由到TUN接口,tun.py程序便能读取到这些ICMP echo request数据包并打印其摘要信息。

然而,当ping 192.168.60.0/24网络中的主机时,tun.py不会打印任何内容。这是因为去往192.168.60.0/24的流量默认走物理接口而非TUN接口,除非手动添加路由规则将其引导到TUN接口。在图6中可以看到这两种情况的区别。

从TUN接口读取ICMP数据包

2.d 将数据包写入TUN接口

从TUN接口读取到ICMP echo request后,程序构造对应的ICMP echo reply数据包写回TUN接口。写入的数据包被内核当作从外部网络收到的报文处理,因此ping命令能够收到回复。这证明了TUN接口的双向数据传递能力:读取的数据来自内核,写入的数据注入内核。

1
2
3
if ICMP in ip and ip[ICMP].type == 8:
reply = IP(src=ip.dst, dst=ip.src) / ICMP(type=0, code=0) / ip[ICMP].payload
os.write(tun, bytes(reply))

构造ICMP echo reply写入TUN接口

当向TUN接口写入任意非IP数据时,内核会报错,因为TUN接口只接受有效的IP数据包。这验证了TUN接口工作在网络层(第3层),写入的数据必须是完整的IP数据包。

向TUN接口写入任意数据的报错结果

Task 3: 通过隧道将IP数据包发送到VPN服务器

(1)VPN服务器程序

在VPN服务器上运行tun_server.py,该程序创建一个UDP套接字监听9090端口。收到数据后,将UDP载荷解析为Scapy IP对象,打印出源IP和目的IP地址。

1
2
3
4
5
6
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 9090))
while True:
data, (ip, port) = sock.recvfrom(2048)
pkt = IP(data)
print(" Inside: {} --> {}".format(pkt.src, pkt.dst))

(2)VPN客户端程序

在客户端上运行tun_client.py,程序从TUN接口读取IP数据包后,通过UDP套接字将其发送到VPN服务器(10.9.0.11:9090)。当在客户端ping 192.168.53.0/24网络中的地址时,ICMP数据包被路由到TUN接口,客户端程序将其封装在UDP中发送到服务器,服务器成功接收并打印。

通过隧道将数据包发送到VPN服务器(ping 192.168.53.5)

(3)添加路由规则

当ping主机V(192.168.60.5)时,ICMP数据包不会通过隧道发送,因为内核不知道去往192.168.60.0/24网络的流量应该路由到TUN接口。需要手动添加路由规则:

1
ip route add 192.168.60.0/24 dev gao0

添加路由后,去往192.168.60.0/24的数据包被引导到TUN接口,客户端程序读取后通过UDP隧道发送到服务器。VPN服务器成功接收到这些ICMP数据包,证明IP隧道在去程方向上工作正常。

设置路由后通过隧道发送数据包到主机V

Task 4: 设置VPN服务器

服务器从隧道接收到数据包后,需要将其注入内核以便路由到最终目的地。为此,服务器也需要创建一个TUN接口,将从UDP套接字接收到的数据写入TUN接口。内核收到数据包后,通过路由表查找目的地址,将其转发到主机V。

同时,VPN服务器需要启用IP转发功能,使其在TUN接口和物理网络接口之间转发数据包。在docker-compose.yml中已通过sysctls配置了net.ipv4.ip_forward=1。

设置VPN服务器后通过隧道将数据包转发到主机V

1
2
3
# 服务器端:从socket接收数据,写入TUN接口
data, (ip, port) = sock.recvfrom(2048)
os.write(tun, data)

Task 5: 处理双向流量

此前的隧道仅实现了单向通信(客户端到服务器)。为实现双向流量,客户端和服务器都需要同时监听TUN接口和UDP套接字。使用select()系统调用可以同时监视多个文件描述符,当任一接口有数据到达时唤醒进程进行处理。

1
2
3
4
5
6
7
8
9
while True:
ready, _, _ = select.select([sock, tun], [], [])
for fd in ready:
if fd is sock:
data, (ip, port) = sock.recvfrom(2048)
os.write(tun, data) # 从隧道到TUN
if fd is tun:
packet = os.read(tun, 2048)
sock.sendto(packet, (SERVER_IP, SERVER_PORT)) # 从TUN到隧道

双向流量处理完成后,主机U可以成功ping通主机V,ICMP echo request和echo reply都能通过隧道往返传输。

双向隧道完成后ping主机V成功

进一步使用telnet登录主机V,输入用户名和密码后成功建立远程登录会话,验证了TCP连接也能通过VPN隧道正常工作。

通过VPN隧道telnet连接主机V

通过Wireshark抓包可以观察到,telnet的数据包在VPN客户端处被封装在UDP中,经由10.9.0.0/24网络到达VPN服务器后被解封装,再转发到192.168.60.0/24网络中的主机V。返回数据的流程则相反。

telnet连接的数据包流向观察

Task 6: 隧道中断实验

首先建立双方的VPN连接,通过telnet输入"hello",在保持telnet连接的同时,终止tun_client.py程序以中断VPN隧道。此时在telnet窗口中继续输入字符’h’,可以看到输入的内容无法显示,因为TCP数据包无法通过已中断的隧道传输。如图15所示。

如果在隧道中断后较短时间内重新启动客户端和服务器程序并恢复路由配置,已建立的TCP连接可以自动恢复,之前输入的字符全部一次出现在telnet会话中,telnet会话中的输入重新变得可响应。这是因为TCP连接的状态仍保留在两端主机的内核中,隧道恢复后数据包可以继续正常传输。如图16所示。

隧道中断后telnet无法输入字符

隧道重新建立后telnet输入恢复

Task 7: 主机V上的路由实验

在真实VPN场景中,返回流量必须经由同一隧道返回。为模拟这种情况,从主机V中删除默认路由条目,然后添加一条更具体的静态路由,确保去往隧道另一端的数据包被路由到VPN服务器。

首先查看主机V的当前路由表,确认存在默认路由条目。然后删除默认路由:

1
ip route del default

删除默认路由后,主机V无法与10.9.0.0/24网络通信,ping主机V的ICMP数据包可以到达主机V,但返回的数据包因没有路由而无法送达。

删除默认路由后无法ping通主机V

添加一条具体的路由条目,将去往10.9.0.0/24网络的数据包路由到VPN服务器(192.168.60.11):

1
ip route add 10.9.0.0/24 via 192.168.60.11

配置完成后,主机V能够正确将返回数据包路由到VPN服务器,通信恢复正常。

添加具体路由后ping通主机V

Task 8: 专用网络之间的VPN

在真实应用中,VPN常用于连接两个分布在不同地点的专用网络。本任务使用docker-compose2.yml搭建包含两个专用网络的实验环境:站点A(192.168.50.0/24)和站点B(192.168.60.0/24),两个站点各有一台VPN路由器通过模拟互联网(10.9.0.0/24)相连。

两个专用网络间的VPN网络拓扑图

需要在两台VPN路由器上分别运行tun_client.py和tun_server.py,并配置正确的路由规则,使得两个专用网络之间的数据包能够通过VPN隧道传输。具体需要在站点A的路由器上添加去往192.168.60.0/24的路由指向TUN接口,在站点B的路由器上添加去往192.168.50.0/24的路由指向TUN接口。

从站点A的主机ping站点B的主机,可以成功通信,验证了两个专用网络之间的VPN隧道工作正常。

两个专用网络之间通过VPN隧道ping通

通过抓包可以观察到,两个专用网络之间的数据包确实经过了VPN隧道封装传输。

两个专用网络之间VPN通信的抓包信息

Task 9: 用TAP接口实验

TAP接口与TUN接口的创建方式类似,区别在于TAP使用IFF_TAP标志,工作在第2层(数据链路层)。从TAP接口读取的数据是以太网帧,包含MAC头部,可以使用Scapy的Ether对象进行解析。

1
2
3
tap = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tap%d', IFF_TAP | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tap, TUNSETIFF, ifr)

当对TAP接口所在网段的IP地址执行ping操作时,系统首先需要通过ARP协议获取目标IP对应的MAC地址。由于TAP接口背后没有真实的ARP响应者,ARP请求会被TAP程序捕获。

从TAP接口读取以太网帧

程序检测到ARP请求后,构造伪造的ARP响应写回TAP接口,使得上层协议认为目标MAC地址已被解析,后续的IP数据包便可以正常封装和发送。使用arping命令可以验证伪造的ARP响应是否生效。

1
2
3
4
5
6
7
if ARP in ether and ether[ARP].op == 1:
arp = ether[ARP]
newether = Ether(dst=ether.src, src=FAKE_MAC)
newarp = ARP(psrc=arp.pdst, hwsrc=FAKE_MAC,
pdst=arp.psrc, hwdst=ether.src, op=2)
newpkt = newether/newarp
os.write(tap, bytes(newpkt))

伪造ARP响应并验证

【思考与总结】

通过本次VPN隧道实验,对VPN的核心技术——TUN/TAP虚拟接口、IP隧道和路由配置——有了深入的理解。

TUN/TAP接口是VPN实现的基石。TUN接口工作在网络层,用户空间程序通过它读写IP数据包;TAP接口工作在数据链路层,处理完整的以太网帧。两者的创建方式几乎相同,仅IFF_TUN与IFF_TAP标志不同。这种虚拟接口机制使得用户空间程序无需修改内核即可拦截和注入网络数据包,是构建VPN、网络仿真等工具的基础。

IP隧道的本质是封装:将原始IP数据包作为载荷放入另一个IP数据包的UDP段中传输。这种"包中有包"的结构虽然增加了少量头部开销,但使得原始数据包可以在不兼容的网络之间穿越。实验中,从主机U发出的ICMP数据包被封装在UDP中穿越10.9.0.0/24网络到达VPN服务器,再被解封装转发到192.168.60.0/24网络中的主机V,完整展示了隧道的封装与解封装过程。

路由配置是VPN正确工作的关键。实验中多次遇到因路由缺失导致通信失败的情况:未将192.168.60.0/24的流量引导到TUN接口时数据包无法进入隧道,主机V删除默认路由后返回数据包无法送达。这些问题深刻说明了路由表在网络通信中的核心地位——即使隧道本身工作正常,错误的路由配置也会导致通信失败。

select()多路复用机制是实现双向隧道的关键技术。通过同时监视TUN接口和UDP套接字,程序能够在任一方向有数据时及时处理,避免了忙轮询的低效。在实际的VPN产品中,这一机制被广泛用于处理多接口并发数据。

隧道中断实验揭示了VPN对上层TCP连接的影响。隧道中断时TCP数据包无法传输,连接进入超时等待;隧道恢复后TCP连接可以自动恢复,因为TCP的状态仍然保留在两端内核中。这说明VPN隧道对上层协议是透明的——隧道的建立和断开不影响已建立的TCP连接状态。

从专用网络之间的VPN实验可以看到,两个隔离的专用网络可以通过VPN隧道实现互通。在实际的企业网络中,这种技术被广泛用于连接分布在不同地点的办公网络,员工无论身处何地都能安全访问公司内部资源。

在实际应用中,VPN还需要对隧道中的数据进行加密以保证安全性。本实验仅关注隧道部分,未涉及加密。完整的VPN还需要使用TLS等协议保护隧道中的数据,防止中间人攻击和数据泄露。

【附录-完整源代码】

tun.py — TUN接口基础实验程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'gao%d', IFF_TUN | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

while True:
packet = os.read(tun, 2048)
if not packet:
continue

ip = IP(packet)
print("==> Received packet: {}".format(ip.summary()))
if ip.haslayer(ICMP) and ip[ICMP].type == 8:
print(" Detected ICMP Echo Request. Constructing Reply...")
newip = IP(src=ip.dst, dst=ip.src)
newicmp = ICMP(type=0, id=ip[ICMP].id, seq=ip[ICMP].seq)
newpkt = newip / newicmp / ip[ICMP].payload

os.write(tun, bytes(newpkt))
print("<== Sent ICMP Echo Reply: {}".format(newpkt.summary()))

tun_client.py — VPN客户端程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import fcntl
import struct
import os
import socket
import select
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_NO_PI = 0x1000

SERVER_IP = "10.9.0.11"
SERVER_PORT = 9090

tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'gao%d', IFF_TUN | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tun, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Client TUN Interface Name: {}".format(ifname))

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

print("Client is running using select()...")

while True:
# 同时监控 sock 和 tun,谁有数据就唤醒谁
ready, _, _ = select.select([sock, tun], [], [])

for fd in ready:
# 收到来自 VPN 服务器的 UDP 隧道回程包
if fd is sock:
data, (ip, port) = sock.recvfrom(2048)
pkt = IP(data)
print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
os.write(tun, data)

# 收到来自本地内核发送给虚拟网卡的包
if fd is tun:
packet = os.read(tun, 2048)
pkt = IP(packet)
print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
sock.sendto(packet, (SERVER_IP, SERVER_PORT))

tun_server.py — VPN服务器程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import fcntl
import struct
import os
import socket
import select
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_NO_PI = 0x1000

tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'gao%d', IFF_TUN | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tun, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Server TUN Interface Name: {}".format(ifname))

os.system("ip addr add 192.168.53.1/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

IP_A = "0.0.0.0"
PORT = 9090
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

client_addr = None

print("Server is running using select()...")

while True:
ready, _, _ = select.select([sock, tun], [], [])

for fd in ready:
# 收到来自客户端的 UDP 隧道请求包
if fd is sock:
data, (ip, port) = sock.recvfrom(2048)
client_addr = (ip, port) # 记录/刷新客户端的真实物理网络地址

pkt = IP(data)
print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
# 将内层原始 IP 包写入服务器 TUN 接口,交给服务器内核转发
os.write(tun, data)

# 收到来自私网返回给客户端的包
if fd is tun:
packet = os.read(tun, 2048)
pkt = IP(packet)
print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
# 只有在知晓客户端物理地址后,才能通过隧道传回
if client_addr:
sock.sendto(packet, client_addr)
else:
print("Warning: Received packet from TUN, but no client has connected yet.")

tap_read.py — TAP接口实验程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
import fcntl
import struct
import os
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000

# 1. 创建 TAP 接口
tap = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tap%d', IFF_TAP | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tap, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

# 2. 配置 TAP 接口 IP 并启动
os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

print("TAP interface is UP. Listening for Ethernet frames...")

# 3. 循环读取二层帧
while True:
packet = os.read(tap, 2048)
if packet:
# 注意:这里转成 Ether 对象,而不是 IP 对象
ether = Ether(packet)
print(ether.summary())

tap_arp_spoof.py — TAP接口ARP欺骗实验程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import fcntl
import struct
import os
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000

# 1. 创建并配置 TAP 接口
tap = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tap%d', IFF_TAP | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tap, TUNSETIFF, ifr)
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

# 配置网卡 IP 并启动
os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

FAKE_MAC = "aa:bb:cc:dd:ee:ff"
print("TAP Spoof Server is running...")

# 2. 核心循环:捕获 ARP 请求并响应
while True:
try:
# 修正后的标准写法:从 TAP 描述符中读取 2048 字节的二层以太网帧
packet = os.read(tap, 2048)
if not packet:
continue

ether = Ether(packet)

# 检查是否为 ARP 包,且 op == 1 (ARP Request)
if ARP in ether and ether[ARP].op == 1:
print("--------------------------------")
print("Received ARP Request: " + ether.summary())
arp = ether[ARP]

# 构造新的以太网头部 (目的MAC为发送方,源MAC为我们伪造的MAC)
newether = Ether(dst=ether.src, src=FAKE_MAC)

# 构造新的 ARP 响应体 (op=2 代表 ARP Reply)
newarp = ARP(
psrc=arp.pdst, # 把请求者询问的目标 IP,作为我们回复的源 IP
hwsrc=FAKE_MAC, # 告诉对方,这个 IP 对应的 MAC 是我们伪造的
pdst=arp.psrc, # 目的 IP 设为请求者的 IP
hwdst=ether.src, # 目的 MAC 设为请求者的 MAC
op=2 # 2 代表 Reply
)

# 组装成二层以太网帧
newpkt = newether / newarp
print("***** Sending Fake ARP Response: {}".format(newpkt.summary()))

# 写回 TAP 接口,注入内核
os.write(tap, bytes(newpkt))

except KeyboardInterrupt:
print("\nExiting TAP Server...")
break
except Exception as e:
print("Error: {}".format(e))

SEEDLab-VPN隧道
https://eleco.top/2026/06/02/SEEDLab-VPN隧道/
作者
Eleco
发布于
2026年6月2日
许可协议