# 实验五

## 关于debug以及bug

首先，请按照以下操作来做，否则你就看不到任何你打的log。

2025.5.2更新：

1. 终端无法显示`log`等函数打印的信息：找到对应的拓扑文件`router_topo1.py, router_topo2.py`，删除路由器节点执行`router`可执行文件的代码`r.cmd(./router &)`，之后使用 `sudo mnexec -a <router pid> bash`打开路由器节点的终端，在其中执行`./router`即可。

具体操作是这样的：

1.开很多个终端（有多少个节点就多少个终端）

2.把router\_topo\_1.py和router\_topo\_2.py都注释掉一句话，如图

<figure><img src="/files/Y4OYGwUuOWI3qZnin48r" alt=""><figcaption></figcaption></figure>

3.第一个终端执行sudo python3 router\_topo1（或2）py，进入mininet

4.第二个终端啥也不做，只负责make和make clean(是的，你不用重新进入mininet就能重新编译)

5.第一个终端dump,知道所有节点的pid

6.其他终端对不同pid依次 sudo mnexec -a pid bash

7.对所有进入pid的路由器节点（指除了h1-eth0,h2-eth0这种）终端，执行./router(如果你重新make了，这些终端都要ctrl+c退出./router,然后再执行./router)

8.在第一个终端执行ping，你就能在相应的pid的终端看到你的log了，包传到哪里，调用了哪个函数，你打的log就会出现在你对应的节点的终端里。

9.为防止oj测评错误，记得测试完把log都注释掉。

10.`base.h`中函数参数定义不一致问题：找到`base.h`文件，把`iface_send_packet`的声明换成`void iface_send_packet(iface_info_t *iface, const char *packet, int len);`，即在`char *packet`前加一个`const`。

***

## 关于代码

首先，你要读懂路由器和路由表转发的基础知识。

实验文档写的很清楚，这里就多解释一下那几张图的箭头。

<figure><img src="/files/jq0E8BdO9dcWjl8gLzwd" alt=""><figcaption></figcaption></figure>

AI回答：

#### 🖧 网络拓扑结构：

图中有4个设备：

* **h1（主机1）**
* **r1（路由器1）**
* **r2（路由器2）**
* **h2（主机2）**

它们通过网线连接在一起，形成以下的网络结构：

```
css复制编辑h1 ↔ r1 ↔ r2 ↔ h2
```

每个设备的接口（`eth0`, `eth1`）上都有一个IP地址，例如：

* h1 的 IP 是 `10.0.1.11`
* r1 的 eth0 是 `10.0.1.1`，eth1 是 `10.0.3.1`
* r2 的 eth0 是 `10.0.2.1`，eth1 是 `10.0.3.2`
* h2 的 IP 是 `10.0.2.22`

***

#### 🔁 通信流程（红色虚线表示）：

想要实现从 `h1` ↔ `h2` 的通信，必须确保所有设备知道如何将数据包正确转发。这就需要手动添加**路由规则**。

***

#### 📌 静态路由配置（红框中的命令）：

1. **r1 上的命令**：

```bash
bash复制编辑route add -net 10.0.2.0 netmask 255.255.255.0 gw 10.0.3.2 dev r1-eth1
```

解释：

* 让 r1 知道：如果目标是 `10.0.2.0/24` 网络（即 h2 所在的网络），应该通过网关 `10.0.3.2`（也就是 r2）来转发，使用接口 `r1-eth1`。

2. **r2 上的命令**：

```bash
bash复制编辑route add -net 10.0.1.0 netmask 255.255.255.0 gw 10.0.3.1 dev r2-eth1
```

解释：

* 让 r2 知道：如果目标是 `10.0.1.0/24` 网络（即 h1 所在的网络），应该通过网关 `10.0.3.1`（也就是 r1）来转发，使用接口 `r2-eth1`。

***

#### ✅ 总结：

这个图说明了如何通过**静态路由**让两个不同子网（10.0.1.0/24 和 10.0.2.0/24）之间的主机（h1 和 h2）互相通信：

* h1 → r1 → r2 → h2
* h2 → r2 → r1 → h1

***

## 如何补函数

详尽的注释已经在实验文档里阐明，我这里只负责理一下思路。

首先，你ping的时候是通过一跳一跳的路由发arp包和icmp包给对应的destination的，在本实验的简化，icmp包只负责ping和traceroute。

然后是一个细节问题：traceroute是如何记录的？是不断发TTL=i(i从1开始递增)的icmp包直到达到dest地址，每经过一跳TTL会-1，如果TTL归0会发回ICMP\_TIME\_EXCEEDED包，这样就可以通过这个包的src地址知道路径了。

还是和之前实验一样，你需要先看懂.h里的struct定义和函数以及define,实验文档和代码里的注释都很清楚。如果你遇到了问题，打log观察发了哪些包（或者tcpdump抓包，不会问ai），函数调用如何。

补函数的时候如果你看不懂注释，你就要结合下面这张图，ICMP包的构成，header里有什么，arp包的格式，实验文档的额外注释来反复结合观看，如果你对ip层的网络平面比较了解，那么你应该能看懂。

<figure><img src="/files/FLPBCtGmrNqXI4UfGMZw" alt=""><figcaption></figcaption></figure>

下面给出每个函数的一些重点要素：

handle\_arp\_packet:记得用ntohl,ntohs进行序列转换，获取ether\_arp头用packet\_to\_ether\_arp函数，丢弃包在ping同一子网但不存在的ip时有用。

arp有两种包，分类调用函数即可，有宏ARPOP\_REQUEST和ARPOP\_REPLY来做判断。

不合法和被丢弃的包要free掉。

arp\_send\_reply,arp\_send\_request:按照arp包要求填充，记得用htonl,htons等函数，以及memcpy,sizeof,还有ETHER\_HDR\_SIZE,ETH\_ALEN,ARPOP\_REPLY,ETH\_P\_ARP,ARPHRD\_ETHER,ETH\_P\_IP,ARPOP\_REQUES等宏，按照实验文档的包格式来就行。

iface\_send\_packet\_by\_arp：先把iface->mac地址写到包里的shost,然后调用lookup,找到了把返回的dst的mac写到包里的dhost里，然后iface\_send\_packet;没找到调用这俩：arpcache\_append\_packet(iface, dst\_ip, packet, len);arp\_send\_request(iface, dst\_ip);

以上是arp.c里的函数，都不要加锁。

arpchche.c里的函数:

arpcache\_lookup：加锁\&arpcache.lock,然后从0到MAX\_ARP\_SIZE，如果arpcache.entries\[i].valid && arpcache.entries\[i].ip4==ip4，把mac放进去，return 1(true),记得解锁要不死锁了。

arpcache\_insert：上锁，然后按照注释做就行，给个list\_for\_each\_entry\_safe的示例（记得free packet）：

```c
struct arp_req *req_entry, *req_q;
    list_for_each_entry_safe(req_entry, req_q, &arpcache.req_list, list) {
        if (req_entry->ip4 == ip4) {
            struct cached_pkt *pkt_entry, *pkt_q;
            list_for_each_entry_safe(pkt_entry, pkt_q, &req_entry->cached_packets, list) {
                struct ether_header *eh = (struct ether_header *)pkt_entry->packet;
                memcpy(eh->ether_dhost, mac, ETH_ALEN);
                iface_send_packet(req_entry->iface, pkt_entry->packet, pkt_entry->len);
                list_delete_entry(&pkt_entry->list);
                free(pkt_entry);
            }
            list_delete_entry(&req_entry->list);
            free(req_entry);
        }
	}
```

arpcache\_append\_packet:加锁，处理好双层链表（请自行探索）。

arpcache\_sweep:加锁，但是---

{% hint style="warning" %}
会和别的加锁函数冲突死锁！（这是一个递归循环调用了ICMP\_send\_packet的问题，因此发ICMP包前要解锁）！（这不禁让你思考，一把大锁保平安永远是对的吗？）
{% endhint %}

```c
void *arpcache_sweep(void *arg) 
{
    while (1) {
        sleep(1);
        pthread_mutex_lock(&arpcache.lock);
        time_t now = time(NULL);
        
        // 清理超时的ARP表项
       //...

        struct arp_req *req_entry, *req_q;
        list_for_each_entry_safe(req_entry, req_q, &arpcache.req_list, list) {
            if (now - req_entry->sent >= 1) {
                if (req_entry->retries < ARP_REQUEST_MAX_RETRIES) {
                    arp_send_request(req_entry->iface, req_entry->ip4);
                    req_entry->sent = now;
                    req_entry->retries++;
                }
                else {
                    // 在发送ICMP之前,先保存需要的信息
                   //...
                    
                    // 计算需要处理的包数量
                   //...
                        
                        // 保存所有包的信息
                       //...
                    // 删除ARP请求项
                   ...
                    // 解锁
                    pthread_mutex_unlock(&arpcache.lock);
                    
                    // 发送所有ICMP包
                    for (int i = 0; i < packet_count; i++) {
                        icmp_send_packet(packets[i], packet_lens[i], 
                                     ICMP_DEST_UNREACH, ICMP_HOST_UNREACH);
                        free(packets[i]);
                    }
                    
                    // 释放临时数组
                   ...
                    
                    // 重新加锁继续处理其他项
                    pthread_mutex_lock(&arpcache.lock);
                    continue;
                }
            }
        }

        pthread_mutex_unlock(&arpcache.lock);
    }
    return NULL;
}

```

如果你发送包前不解锁，也就是找到了该发出去的包的一瞬间就发包，那么你在ping不存在的ip时很容易死锁。

icmp.c里的函数：

icmp\_send\_packet：最重要的函数，长度（最重要的，请自行思考），以太网头部，ip头部都要设置好，还有字节序和网络序的问题

```c
void icmp_send_packet(const char *in_pkt, int len, u8 type, u8 code)
{
    log(DEBUG, "Sending ICMP packet, type: %d, code: %d\n", type, code);
	struct iphdr *in_ip=packet_to_ip_hdr(in_pkt);
	int icmp_len;
	char *packet;

	if(type==ICMP_ECHOREPLY){
		//...
	}
	else{
		//...
	}

	// 设置以太网头部
    //...
    // MAC地址将在 ip_send_packet 中通过 ARP 设置
    //最长路径匹配
     //...
    //设置ip头部（packet_to_ip_hdr,ip_init_hdr）
    //...
     //icmp头设置
    struct icmphdr *icmp = (struct icmphdr *)IP_DATA(ip);
	//设置...
    
    if (type == ICMP_ECHOREPLY) {
        // 对于 ECHO REPLY，复制原始 ICMP 数据包的内容
        //...
       
    } else {
        // 对于错误消息，复制原始 IP 头和部分数据
        //...
        
    }
    //最后再checksum
	icmp->checksum=icmp_checksum(icmp,icmp_len);
	ip_send_packet(packet,ETHER_HDR_SIZE+IP_BASE_HDR_SIZE+icmp_len);
	//assert(0 && "TODO: function icmp_send_packet not implemented!");
}

```

ip\_base.c里的函数：

longest\_prefix\_match：基本功，不用我说吧...，一个list\_for\_each\_entry的事，注意子网掩码（加不加rt\_lock那个锁都行，但是建议加上，对实验7有用）

ip\_send\_packet：先最长路径匹配，找不到free packet,找到了用gw判断下一跳在那里送给iface\_send\_packet\_by\_arp当参数即可。

```c
 u32 next_hop = rt_entry->gw ? rt_entry->gw : ntohl(ip->daddr);
```

ip.c里的函数：

handle\_ip\_packet：分两类包即可，发完ICMP包就可以free了。

ip\_forward\_packet：先判断TTL，到点了发ICMP包，free packet（traceroute的关键），否则和ip\_send\_packet类似找路径，下一跳，调用函数。

***

## 其他小问题

ICMP Destination Host Unreachable必须所有发的icmp包都返回才是对的，如果你的icmp\_seq不是从1开始返回这个，无法通过测试。

这次打log比较麻烦，请理解哪个节点在干什么。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://blueroaring-njus-organization.gitbook.io/njucs_25spring_network-exp_additional/shi-yan-wu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
