GO语言实战:创建TUN/TAP接口与报文收发详解‌

以下内容将从概念入手,逐步带你使用 Go 语言创建并操作 TUN/TAP 接口,演示如何收发报文。正文包含完整的代码示例、文字说明,以及配合 ASCII “图解”以便理解网络数据流向。建议边看边实践。


1. 概述

在很多网络编程场景中,需要在用户态与内核态之间搭建一个“虚拟网卡”以便自定义地收发 IP 数据包。Linux 下常见的方式是通过 TUN/TAP 设备:

  • TUN(Network TUNnel):工作在三层(L3),只收发 IP 报文(非以太网帧)。
  • TAP (Network TAP):工作在二层(L2),收发完整以太网帧。

本文聚焦于 TUN 设备,因为它更直接与 IP 报文打交道,更易于理解和演示。我们将使用第三方库 github.com/songgao/water(下文简称 water),它封装了底层创建 TUN/TAP 的系统调用,使得 Go 代码更简洁。

1.1 适用场景

  • 自定义 VPN 软件(例如基于 TUN 的用户态路由)。
  • 虚拟网络实验:自行处理 IP 报文,实现简单路由、NAT、隧道等。
  • 学习和调试 IP 协议栈:通过 TUN 将报文导出到用户态,分析后再注入回去。

1.2 环境准备

  • 操作系统:Linux(若在 macOS 下创建 TUN,需额外安装 tuntaposx 驱动;本文以 Linux 为例)。
  • Go 版本:Go 1.18 及以上。
  • 根(root)权限:创建和配置 TUN 设备通常需要 root 权限,或者将程序的可执行文件授权给 CAP\_NET\_ADMIN 权限(本文假设你以 root 身份运行)。

安装 water

go get -u github.com/songgao/water

2. TUN/TAP 原理与流程

在深入代码之前,先简单梳理 TUN 的工作机制与数据流向。

2.1 TUN 设备在内核中的角色

  1. 用户进程通过 open("/dev/net/tun", ...) 请求创建一个 TUN 设备(如 tun0)。
  2. 内核分配并注册一个名为 tun0 的网络接口,类型为 TUN。此时系统会在 /sys/class/net/ 下生成相应目录,用户还需手动或脚本方式赋予 IP 地址、路由等配置。
  3. 用户态程序通过文件描述符 fd(即 /dev/net/tun 的打开句柄)读写。写入的数据应当是“原始 IP 数据报”(不含以太网头部);读到的数据同样是内核向用户态投递的 IP 数据报。
  4. 内核会将用户态写入的 IP 报文当作从该接口发出,再交给内核 IP 栈启动路由转发;反之,内核路由到该接口的 IP 数据包则从文件描述符中被用户态读到。

换句话说,TUN 设备是内核⟷用户的“IP 隧道”,通信示意图如下(图解用 ASCII 表示):

+-------------------------------------------+
|                 用户态程序                 |
|   ┌────────┐      ┌───────────────────┐   |
|   │  read  │◀─────│ 内核: routable IP  │   |
|   │  write │─────▶│    packet (L3)    │   |
|   └────────┘      └───────────────────┘   |
|      │  ^                                       
|      │  |                                       
|      ▼  |                                       
|  /dev/net/tun (fd)                             
+-------------------------------------------+

而内核视角可理解为:

             ┌───────────────────────────────┐
             │      内核网络协议栈 (IP)       │
             └───────────────────────────────┘
       ▲                    ▲           ▲
       │                    │           │
 from tun0             to tun0     to real NIC
(read(): deliver to user)    (write(): receive from user)

当用户程序向 TUN 设备写入一个完整 IP 报文后,内核“看到”仿佛收到了该接口的报文,会进入 IP 层进行路由、转发或本地处理。用户态从 TUN 设备 read 得到内核要发往该接口的 IP 报文,通常是从其他主机发往本机或在内核进行隧道转发时产生。


3. 使用 Go 创建 TUN 设备

下面我们一步步用 Go 代码创建 TUN 设备,并查看其文件描述符。整个示例放在 main.go 中。

package main

import (
    "fmt"
    "log"
    "github.com/songgao/water"
)

func main() {
    // 1. 配置 TUN 接口的参数
    config := water.Config{
        DeviceType: water.TUN,
    }
    config.Name = "tun0" // 希望的接口名;如果为空,则内核自动分配

    // 2. 创建 TUN 接口
    ifce, err := water.New(config)
    if err != nil {
        log.Fatalf("创建 TUN 设备失败: %v", err)
    }

    // 3. 打印分配到的接口名称(若未显式指定则可查看)
    fmt.Printf("TUN 接口已创建:%s\n", ifce.Name())

    // 4. 待实现:后续可直接用 ifce.Read() / ifce.Write() 进行 IO
    //    这里只做最简单的示例
    select {}  // 阻塞,防止程序退出
}

3.1 代码解读

  1. 导入包

    import "github.com/songgao/water"

    water 库封装了 Linux 下 /dev/net/tun 的创建与配置细节,让我们能够更直观地用 Go 操作 TUN/TAP。

  2. 配置 water.Config

    config := water.Config{ DeviceType: water.TUN }
    config.Name = "tun0"
    • DeviceType: water.TUN 表示申请一个 TUN 设备(而非 TAP)。
    • config.Name = "tun0":期望的网卡名称。如果此网卡名已被占用,或者不设置(留空),内核会自动分配一个类似 tunX 的名字。
  3. 调用 water.New(config)
    该函数最终会调用 open("/dev/net/tun", ...) 并通过 ioctl(TUNSETIFF) 将设备类型与名字传入内核。如果成功,返回一个实现了 io.ReadWriteCloser 接口的 *water.Interface,和一个名为 Name() 的方法,用以获取实际分配到的接口名。
  4. 保持程序运行
    select {} 是一种“空阻塞”写法,仅为了让程序不因主 goroutine 结束而退出。后续会在同一程序中读取/写入数据。

3.2 启动与权限

注意:

  • root 权限:通常 water.New 会报错 permission denied,因为打开 /dev/net/tun 需要 CAP\_NET\_ADMIN 权限。可以通过 sudo ./main 运行,或给可执行文件打 setcap cap_net_admin+ep main 后以普通用户运行。
  • 内核模块:若运行时报错“tun: No such device”或“/dev/net/tun: No such file or directory”,请检查是否加载了 tun 模块:

    sudo modprobe tun
    ls /dev/net/tun  # 应存在

4. 为 TUN 接口分配 IP 并配置路由

创建接口后,系统仅在逻辑上“有”一个网卡,尚未分配 IP、路由等。我们需要在终端执行以下命令(或在 Go 程序中通过 os/exec 调用):

# 以 root 身份运行:
ip link set dev tun0 up
ip addr add 10.0.0.1/24 dev tun0
  • ip link set dev tun0 up:启动该接口。
  • ip addr add 10.0.0.1/24 dev tun0:分配 IPv4 地址及子网掩码(示例用 10.0.0.1/24)。

此时,执行 ip addr show tun0 会看到类似:

4: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 ...
    inet 10.0.0.1/24 scope global tun0
    ...

可以用 ip route 查看当前路由表,若想让某些目标 IP 走此接口,可根据需求添加路由。例如:

ip route add 10.0.0.0/24 dev tun0

假设后续有机器 A 通过虚拟隧道访问 10.0.0.x,所有发往该网段的报文会被内核路由至 tun0,进而从 Go 程序的 ifce.Read() 中读到。


5. 读取与发送 IP 报文

下面,我们在 Go 程序中实现“读报文”和“回显”示例。收到的每个 IP 数据报,解析后简单回显一个 ICMP Reply,等同“Ping 应答”。整体流程:

  1. 程序创建并启动 tun0(已配置 IP)。
  2. 喂入一台主机(如另一容器或本机)发送 ping 10.0.0.1
  3. 内核将 ICMP Echo Request 从 tun0 投递给用户态程序。
  4. 程序解析 ICMP 报文,构造 Echo Reply,并写回 tun0
  5. 内核接收 Reply 并发给源主机,用户就看到正常回复。

5.1 完整示例代码

package main

import (
    "encoding/binary"
    "fmt"
    "log"
    "net"
    "os"
    "syscall"
    "github.com/songgao/water"
)

// IP 头最小长度(20 字节)
const (
    IPHeaderLen  = 20
    ICMPProtoNum = 1
)

// IP 头结构(仅解析常用字段)
type IPHeader struct {
    VersionIHL   uint8  // 版本 + 头长
    TypeOfSvc    uint8
    TotalLen     uint16
    Identification uint16
    FlagsFragOff uint16
    TTL          uint8
    Protocol     uint8
    HeaderChecksum uint16
    SrcIP        net.IP
    DstIP        net.IP
    // 省略 Options
}

// ICMP 头结构(仅解析常用字段)
type ICMPHeader struct {
    Type     uint8
    Code     uint8
    Checksum uint16
    ID       uint16
    Seq      uint16
    // 省略后续 Payload
}

func main() {
    // 1. 创建 TUN
    config := water.Config{DeviceType: water.TUN}
    config.Name = "tun0"
    ifce, err := water.New(config)
    if err != nil {
        log.Fatalf("创建 TUN 失败: %v", err)
    }
    fmt.Printf("已创建 TUN 设备:%s\n", ifce.Name())

    // 2. 程序需在外部配置 tun0 IP(如 10.0.0.1/24)并 ip link up
    fmt.Println("请确保已在外部将 tun0 配置为 up 并分配 IP,比如:")
    fmt.Println("  sudo ip link set dev tun0 up")
    fmt.Println("  sudo ip addr add 10.0.0.1/24 dev tun0")
    fmt.Println("等待几秒继续...")
    // 小小延迟,让用户有时间执行
    // time.Sleep(5 * time.Second)

    buf := make([]byte, 1500) // MTU 大小一般为 1500

    for {
        // 3. 从 TUN 设备读取一帧(实际上是 IP 数据报)
        n, err := ifce.Read(buf)
        if err != nil {
            log.Fatalf("读取数据失败: %v", err)
        }
        packet := buf[:n]

        // 4. 解析 IP 头
        if len(packet) < IPHeaderLen {
            continue // 过短,舍弃
        }
        ipHdr := parseIPHeader(packet[:IPHeaderLen])

        // 仅处理 ICMP 协议
        if ipHdr.Protocol != ICMPProtoNum {
            continue
        }

        // 5. 解析 ICMP 负载
        icmpStart := int((ipHdr.VersionIHL&0x0F) * 4) // IHL 字段给出头长
        if len(packet) < icmpStart+8 {
            continue
        }
        icmpHdr := parseICMPHeader(packet[icmpStart : icmpStart+8])
        // 仅处理 ICMP Echo Request (Type=8)
        if icmpHdr.Type != 8 {
            continue
        }

        fmt.Printf("收到 ICMP Echo Request: 从 %s 到 %s, ID=%d Seq=%d\n",
            ipHdr.SrcIP, ipHdr.DstIP, icmpHdr.ID, icmpHdr.Seq)

        // 6. 构造 ICMP Echo Reply
        reply := buildICMPEchoReply(ipHdr, packet[icmpStart:], n-icmpStart)

        // 7. 写回 TUN,内核转发给源主机
        if _, err := ifce.Write(reply); err != nil {
            log.Printf("写入数据失败: %v", err)
        } else {
            fmt.Printf("已发送 ICMP Echo Reply: %s -> %s\n", ipHdr.DstIP, ipHdr.SrcIP)
        }
    }
}

// 解析 IP Header
func parseIPHeader(data []byte) *IPHeader {
    return &IPHeader{
        VersionIHL:     data[0],
        TypeOfSvc:      data[1],
        TotalLen:       binary.BigEndian.Uint16(data[2:4]),
        Identification: binary.BigEndian.Uint16(data[4:6]),
        FlagsFragOff:   binary.BigEndian.Uint16(data[6:8]),
        TTL:            data[8],
        Protocol:       data[9],
        HeaderChecksum: binary.BigEndian.Uint16(data[10:12]),
        SrcIP:          net.IPv4(data[12], data[13], data[14], data[15]),
        DstIP:          net.IPv4(data[16], data[17], data[18], data[19]),
    }
}

// 解析 ICMP Header
func parseICMPHeader(data []byte) *ICMPHeader {
    return &ICMPHeader{
        Type:     data[0],
        Code:     data[1],
        Checksum: binary.BigEndian.Uint16(data[2:4]),
        ID:       binary.BigEndian.Uint16(data[4:6]),
        Seq:      binary.BigEndian.Uint16(data[6:8]),
    }
}

// 计算校验和(针对 ICMP 或 IP 特定区域)
func checksum(data []byte) uint16 {
    var sum uint32
    length := len(data)
    for i := 0; i < length-1; i += 2 {
        sum += uint32(binary.BigEndian.Uint16(data[i : i+2]))
    }
    if length%2 == 1 {
        sum += uint32(data[length-1]) << 8
    }
    for (sum >> 16) > 0 {
        sum = (sum >> 16) + (sum & 0xFFFF)
    }
    return ^uint16(sum)
}

// 构建 ICMP Echo Reply 报文(含 IP 头 + ICMP Payload)
func buildICMPEchoReply(ipHdr *IPHeader, icmpPayload []byte, icmpLen int) []byte {
    // 1. 准备新的缓冲区:IP 头 + ICMP 头 + 数据
    totalLen := IPHeaderLen + icmpLen
    buf := make([]byte, totalLen)

    // 2. 构造 IP 头
    // 版本(4) + 头长(5,即 20 字节) = 0x45
    buf[0] = 0x45
    buf[1] = 0                         // TOS
    binary.BigEndian.PutUint16(buf[2:4], uint16(totalLen))
    binary.BigEndian.PutUint16(buf[4:6], 0)      // Identification,可随意
    binary.BigEndian.PutUint16(buf[6:8], 0)      // Flags + Fragment offset
    buf[8] = 64                                // TTL
    buf[9] = ICMPProtoNum                      // Protocol: ICMP = 1
    // 源和目的 IP 交换
    copy(buf[12:16], ipHdr.DstIP.To4())
    copy(buf[16:20], ipHdr.SrcIP.To4())
    // 计算 IP 头校验和
    ipCsum := checksum(buf[:IPHeaderLen])
    binary.BigEndian.PutUint16(buf[10:12], ipCsum)

    // 3. 构造 ICMP 负载:Type=0 (Echo Reply), Code=0
    // 把原始请求的 ID、Seq 复制过来,数据部分原封不动
    buf[IPHeaderLen+0] = 0   // Type: Echo Reply
    buf[IPHeaderLen+1] = 0   // Code: 0
    // 校验和字段先置 0
    binary.BigEndian.PutUint16(buf[IPHeaderLen+2:IPHeaderLen+4], 0)
    // ID & Seq
    copy(buf[IPHeaderLen+4:IPHeaderLen+6], icmpPayload[4:6])
    copy(buf[IPHeaderLen+6:IPHeaderLen+8], icmpPayload[6:8])
    // 剩余数据(原请求的 Payload)
    if icmpLen > 8 {
        copy(buf[IPHeaderLen+8:], icmpPayload[8:icmpLen])
    }
    // 计算 ICMP 校验和(覆盖整个 ICMP 包,包括头+数据)
    icmpCsum := checksum(buf[IPHeaderLen : IPHeaderLen+icmpLen])
    binary.BigEndian.PutUint16(buf[IPHeaderLen+2:IPHeaderLen+4], icmpCsum)

    return buf
}

5.1.1 关键步骤说明

  1. 解析 IP 报文

    • parseIPHeader 只提取常用字段以便后续根据源、目的地址、协议等判断。
    • 注意 IP 头的长度由 VersionIHL & 0x0F(IHL 字段)给出,单位为 32 位字(4 字节)。最小值为 5(即 20 字节)。
  2. 解析 ICMP 报文

    • parseICMPHeader 提取 ICMP 类型(Type)、代码(Code)、校验和以及标识符(ID)和序列号(Seq)。
    • 只对 Echo Request(Type = 8)做回复,其它类型忽略。
  3. 校验和计算

    • IP 头和 ICMP 包各自都要计算 16 位校验和,需按照 RFC 791 / RFC 792 的算法:将 16 位当作无符号数求和,若出现进位再加回,最后按位取反。
    • 函数 checksum(data []byte) uint16 将整个切片两两字节累加,若长度为奇数则最后一个字节左移 8 位与上一步累加。
  4. 构造 Echo Reply

    • IP 层:交换源/目的地址,TTL 设为 64,Protocol 填写 1(ICMP)。其余字段可设置为默认或随机。
    • ICMP 层:将原请求的 ID、序列号原样保留,将 Type 改为 0(Echo Reply)。校验和先置 0,再计算并写入。
  5. Read/Write

    • ifce.Read(buf):从 tun0 阻塞读取“原始 IP 数据报”。
    • ifce.Write(reply):将自己构造的 IP 数据报写回 tun0,内核就会发送给对端。

5.2 演示流程

  1. 启动 Go 程序(假设编译为 tun-ping):

    sudo ./tun-ping

    程序会提示创建了 tun0 并等待你在外部配置 IP。

  2. 在另一个终端(同一台主机或虚拟机)执行:

    sudo ip link set dev tun0 up
    sudo ip addr add 10.0.0.1/24 dev tun0

    这样 loeth0 等以外,又出现了一个逻辑上的 “tun0”。

  3. 依照你的网络环境,主动 ping 该 IP:

    ping -c 4 10.0.0.1

    你会看到类似:

    PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
    64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.045 ms
    64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.032 ms
    ...

    同时,Go 程序终端会打印:

    收到 ICMP Echo Request: 从 10.0.0.x 到 10.0.0.1, ID=xxxx Seq=1
    已发送 ICMP Echo Reply: 10.0.0.1 -> 10.0.0.x

    由此证明读写流程成功。


6. 图解:TUN 数据流向

下面用 ASCII 图示演示一下,便于理解用户态收发报文的整个过程。

                              +----------------+
                              |   真实主机 A   |
                              |  (如:10.0.0.2) |
                              +--------+-------+
                                       |
                                       | ping 10.0.0.1 (ICMP Echo Request)
                                       v
                            ┌───────────────────────┐
                            │    系统内核网络栈      │
                            │   (收到来自 A 的 ICMP)  │
                            └─────────┬─────────────┘
                                      │
                                      │ 路由匹配:目标 10.0.0.1/24 -> tun0
                                      ▼
                             ┌─────────────────────┐
                             │    TUN 设备 (tun0)   │
                             │ /dev/net/tun 文件描述符 │
                             └─────────┬───────────┘
                                       │
                      Read() 返回给 Go 程序 │
                                       ▼
                   ┌────────────────────────────────┐
                   │      用户态 Go 程序 (tun-ping)   │
                   │ ① ifce.Read() 得到 IP 数据报       │
                   │ ② 解析后构造 ICMP Echo Reply       │
                   │ ③ ifce.Write(reply) 写回 tun0      │
                   └───────────▲───────────────────────┘
                               │
                               │ 写入tun0后由内核当做“发出”报文处理
                               │
                               ▼
                             ┌─────────────────────┐
                             │    TUN 设备 (tun0)   │
                             └─────────┬───────────┘
                                       │
                                       │ 内核再将 ICMP Reply 发给 A
                                       ▼
                            ┌───────────────────────┐
                            │    系统内核网络栈      │
                            └─────────┬─────────────┘
                                      │
                                      ▼
                              +----------------+
                              |   真实主机 A   |
                              +----------------+
  • 箭头说明

    • 上半部分由 A 向 10.0.0.1(即 tun0)发送 ICMP 请求(Echo Request),被内核路由到 TUN。
    • 下半部分 Go 程序处理后写回 TUN,内核再发往 A,完成一次完整的请求-应答。

7. 可选:在 Go 中动态配置 IP 与路由

如果想将“创建 TUN”与“配置 IP、启动接口、添加路由”这几步都放在 Go 代码内完成,可以调用 os/exec 执行系统命令(或使用 golang.org/x/sys/unix 接口发起 syscall)。下面示例演示最简单的用 exec.Command 的方式:

package main

import (
    "fmt"
    "log"
    "os/exec"
    "time"

    "github.com/songgao/water"
)

func main() {
    config := water.Config{DeviceType: water.TUN}
    config.Name = "tun0"
    ifce, err := water.New(config)
    if err != nil {
        log.Fatalf("创建 TUN 失败: %v", err)
    }
    fmt.Printf("已创建 TUN 设备:%s\n", ifce.Name())

    // 延迟几百毫秒,让系统有时间挂载设备
    time.Sleep(200 * time.Millisecond)

    // 1. 打开接口
    cmd := exec.Command("ip", "link", "set", "dev", ifce.Name(), "up")
    if out, err := cmd.CombinedOutput(); err != nil {
        log.Fatalf("执行 ip link up 失败: %v, 输出: %s", err, string(out))
    }

    // 2. 分配 IP 地址
    cmd = exec.Command("ip", "addr", "add", "10.0.0.1/24", "dev", ifce.Name())
    if out, err := cmd.CombinedOutput(); err != nil {
        log.Fatalf("执行 ip addr add 失败: %v, 输出: %s", err, string(out))
    }

    fmt.Println("接口已配置为 UP 并分配了 IP 10.0.0.1/24")
    // 后续可同前面示例一样进行 Read/Write 循环
    select {}
}

Tip:

  • 若想添加路由:

    exec.Command("ip", "route", "add", "192.168.1.0/24", "dev", ifce.Name())

    这样可让发往 192.168.1.x 的流量都走 tun0。

  • 优点:一步到位,程序启动后即可完成所有配置。
  • 缺点:依赖系统的 ip 命令,跨平台兼容性较差;且需要捕获命令行输出并处理错误。

8. 常见问题及调试技巧

  1. 程序报 “permission denied”

    • 原因:缺少 CAP\_NET\_ADMIN 权限,无法打开 /dev/net/tun
    • 解决:以 root 运行,或对可执行文件执行:

      sudo setcap cap_net_admin+ep /path/to/your/binary

      然后普通用户也能创建 TUN。

  2. water.New 返回 “device not found”

    • 原因:内核未加载 tun 模块。
    • 解决:sudo modprobe tun,然后再试。
  3. Ping 时无响应

    • 检查 ip addr show tun0 是否已经 UP 且分配了正确 IP。
    • 确保主机对目标 IP 的路由正确指向 tun0(使用 ip route 查看)。
    • 查看 Go 日志,确认是否有收到 ICMP 请求。如果程序未读到报文,则说明 TUN 未正确配置或路由有误。
  4. MTU 不匹配导致分片/丢包

    • 默认 TUN 接口 MTU 1500。若你分配的 IP 段在底层网络 MTU 比较小,可能需要 ip link set dev tun0 mtu 1400 之类命令调整。
  5. Windows/macOS 平台

    • 本文示例基于 Linux。macOS 需先用 tuntaposx 安装 TUN/TAP 驱动,接口名称通常为 utun0。Go 中需要相应修改创建过程。
    • Windows 上 TUN/TAP 通常通过 OpenVPN TAP-Windows 驱动,创建和读写方式也有差异,需要使用相应 Windows API 或封装库。

9. 进阶:处理更复杂的 IP 与 UDP/TCP 流量

上面示例只演示了最简单的 ICMP 回显。你还可以在用户态处理任意 IP(IPv4/IPv6)流量。例如:

  • XOR/加密隧道:在写入 tun0 之前,对整个 IP 数据包做加密,接收方程序再解密后写入 tun0,形成加密隧道。
  • 自定义路由逻辑:收到从 tun0 的 IP 报文后,解析出目标 IP,然后用 net.DialUDPnet.DialTCP 等函数将真实数据转发到远端服务器;反之,将远端返回数据封装成 IP 报文再写回 tun0,实现简单 VPN。
  • 转发给用户态 HTTP/HTTPS 流量:比如将本地设为默认网关,然后将所有 80/443 流量串到自己实现的用户态代理,做流量分析或缓存。

以上都依赖 ifce.Read() 拿到完整 L3 报文后,自行解析(也可以用现有的包,如 golang.org/x/net/ipv4),并自行封装或处理。


10. 小结

本文从零开始介绍了如何用 Go 语言创建并操作 TUN 设备,演示了如何在用户态读写 L3 报文,并给出了 ICMP Ping 回显的完整示例。关键要点包括:

  1. 使用 github.com/songgao/water 库简化创建 TUN 接口的步骤。
  2. 在系统中配置 TUN 的 IP 与路由(可用命令行或在 Go 中调用外部命令)。
  3. ifce.Read() 拿到的就是“原始 IP 数据报”,可解析后自行处理。
  4. 构造好新的 IP 报文,ifce.Write() 即可将数据注入内核网络栈,实现网络交互。
  5. ASCII 图解帮助理解用户态与内核态之间的流程。

后续练习建议

  • 改写示例,将 Echo Reply 之外的其它 ICMP 类型也做处理。
  • 实现一个简单的用户态路由器:收到来自 tun0 的 UDP 数据包,转发到指定真实服务端,返回时再封装进 tun0。
  • 将示例移植到 macOS 或 Windows 平台,理解不同 OS 下 TUN/TAP 驱动的差异。
最后修改于:2025年06月07日 16:26

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日