# ICMP
本章將重點介紹位於 ISO 模型第7層的 ICMP 協定,並詳細解析該協定的數據包在系統內核中的處理過程。在用戶空間層面,使用者能夠通過 socket API 來傳送 ICMP 數據包,其中,`ping` 命令是一個廣為人知且常用的例子。
建議讀者先閱讀 [RFC 792](https://datatracker.ietf.org/doc/html/rfc792) 再閱讀本章。
## ICMPv4
ICMPv4 訊息主要可以分為兩個類型: [RFC 1812](https://datatracker.ietf.org/doc/html/rfc1812#page-52) 中列出 ICMP 支援的各種訊息類別以及其功能
1. Error message
2. Information message
:::info
[Internet Control Message Protocol (ICMP) Parameters](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xml) 中提供了所有 ICMP 訊息的 RFC
:::
ICMPv4 主要是用於診斷工具中,例如 `ping` 以及 `traceroute` 等等的工具。`ping` 主要使用 raw socket 傳送 `ICMP_ECHO` 訊息,並且等待`ICMP_REPLY` 訊息,驗證主機與目標之間是否能夠連線。`traceroute` 則是用於找出主機與目標間的最短路由路徑,主要是使用封包中帶著的參數 Time To Live(TTL) 來完成,首先發送 TTL = 1 的封包,每收到帶著 `ICMP_TIME_EXCEED` 的 `ICMP_DEST_UNREACH` 回復,它就將TTL增加1, 直到收到目的地回傳的 `ICMP_REPLY` 訊息為止。另外,在 Windows 中的 `traceroute` 預設使用的是 ICMP 協定,但是 Unix-like 則是預設使用 UDP 協定,並且 ICMPv4 不能成為一個單獨的模組,原因有下列幾點。
- 核心網絡功能:ICMPv4 是處理 IP 層錯誤消息和控制消息的關鍵部分。由於它負責處理網絡不可達、重定向、超時等多種重要情況,這些功能被視為核心網絡堆疊的一部分,因此被緊密集成在核心內。
- 性能考量:作為網絡協議棧中的基本組件,ICMPv4 需要高效運行。將其作為模塊實現可能會導致額外的開銷,例如模塊加載和卸載的開銷,這可能會影響到網絡的性能。
- 安全性和穩定性:將 ICMPv4 實現為模塊可能會增加系統的複雜性,從而影響安全性和穩定性。由於 ICMPv4 與 IP 層的交互是網絡運作的基礎,任何模塊化可能引入的錯誤或不穩定都可能導致整個網絡堆疊的故障。
- 可用性:ICMPv4 是網絡協議中必不可少的部分,因此它必須始終可用,以便即使在系統壓力較大或處於非標準狀態時也能報告問題。如果 ICMPv4 是一個模塊,那麼在模塊未加載的情況下,核心網絡功能可能無法正常工作。
### ICMPv4 Initialization
ICMPv4 的初始化是在啟動時透過 [`inet_init`](https://github.com/torvalds/linux/blob/master/net/ipv4/af_inet.c#L1902),該方法會使用到 [`icmp_init`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1498) 然後其中再呼叫 [`register_pernet_subsys`](https://github.com/torvalds/linux/blob/master/net/core/net_namespace.c#L1357) 從而呼叫 [`icmp_sk_init`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1465) 來建立一個核心的 ICMP socket 用於傳送 ICMP 訊息,同時也初始化一些 ICMP procfs 變數為預設值。另外,在 `inet_init` 中還會註冊其餘的 IPv4 協定。
在核心中,ICMP 協定的[定義](https://github.com/torvalds/linux/blob/master/net/ipv4/af_inet.c#L1716C1-L1720C3)如下
```clike
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
};
```
- `handler`: 如果收到的封包型態是 IPPROTO_ICMP(0x1),就會用來處理該封包
- `err_handler`
- `no_policy`: 預設為 1,用來表示這個協定不需要提供 IPsec policy 的檢查。舉例來說,因為 `no_pollicy` 設定為1,所以 [`ip_local_deliver_rcu`](https://github.com/torvalds/linux/blob/master/net/ipv4/ip_input.c#L197) 中不會呼叫 `xfrm4_policy_check`
在 `icmp_sk_init` 中會為每個 CPU 建立一個 ICMPv4 socket 並且存在一個陣列中。我們可以透過 `icmp_sk` 來取得該 ICMPv4 物件的 socket buffer。這些 socket 在 [`icmp_push_reply`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L366) 中會被使用到。
### ICMPv4 Header
關於 ICMPv4 header 建議直接閱讀 [RFC 792](https://datatracker.ietf.org/doc/html/rfc792),下圖為 ICMPv4 header 的結構。根據要傳送的訊息類型不同,Payload 會有不同的格式。
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Code | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Variables according to type/code |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data(Optional) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
在核心中,使用 [icmphdr](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/include/uapi/linux/icmp.h#L89C1-L106C1) 結構代表 ICMPv4 header 如下列。其中的 `reserved` 是為了在 ICMPv4 收到的訊息包含 [RFC 4884](https://datatracker.ietf.org/doc/html/rfc4884) 定義的擴充功能,並且需要轉換到 ICMPv6 的 SIG or GRE tunnel 時使用的,詳情請見 [commit](https://github.com/torvalds/linux/commit/20e1954fe238dbe5f8d3a979e593fe352bd703cf)
```clike
struct icmphdr {
__u8 type;
__u8 code;
__sum16 checksum;
union {
struct {
__be16 id;
__be16 sequence;
} echo;
__be32 gateway;
struct {
__be16 __unused;
__be16 mtu;
} frag;
__u8 reserved[4];
} un;
};
```
ICMPv4 模組定義一個名為 [`icmp_pointers`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/icmp.c#L196) 陣列,由 [`icmp_control`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/icmp.c#L191C1-L194C3) 物件組成。
```clike
struct icmp_control {
enum skb_drop_reason (*handler)(struct sk_buff *skb);
short error; /* This ICMP is classed as an error message */
};
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES+1];
```
- `handler`: 用來回覆丟棄封包的原因為何,會透過 function pointer 指向 `icmp_discard` 等等的功能
- `error`:
- 0: Information message
- 1: Error message
在 Linux Kernel 3.0 之前的 ICMP socket 中,我們需要自己建立 socket 以傳送 ping request,然後[`ping_rcv`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/ping.c#L967) 會處理收到的 `ICMP_ECHOREPLY` 訊息。ICMP 訊息都是由 `icmp_rcv` 處理,這個功能被放在 `icmp_protocol` 的 `handler` 中,`icmp_protocol` 在 `inet_init` 時註冊到 inet 供之後收到封包時使用。關於 `ping` 會在後面有更深的講解。
我們可以透過 `icmp_send` 來傳送訊息,以 ICMP_TIME_EXCEEDED 訊息為例,這個訊息會在下列兩種情境下傳送出去
1. 在 `ip_forward` 中,當 TTL 小於0的時候
`icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0)`
2. 在 `ip_expire` 中,有 fragment timeout 的時候
`icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_FRAGTIME, 0)`
另外,在進入 ICMPv4 Messages 之前,要先介紹 `icmp_reply` 以及 `icmp_send` 都會使用到的 [`icmp_bxm`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/icmp.c#L103C1-L114C3) 結構
```clike
struct icmp_bxm {
struct sk_buff *skb;
int offset;
int data_len;
struct {
struct icmphdr icmph;
__be32 times[3];
} data;
int head_len;
struct ip_options_data replyopts;
};
```
- `skb`: socket buffer
- in `icmp_reply`: this `skb` is the request packet and `icmp_bxm` is built from it
- in `icmp_send`: this `skb` is the one that triggered sending an ICMPv4 message due to some conditions
- `offset`: `skb_network_handler` 跟 `skb->data` 的距離
- `data_len`: ICMPv4 封包 Data 的長度
- `icmph`: ICMPv4 header
- `times`: `icmp_timestamp` 使用的成員,其中有三個 timestamp
- `head_len`: Size of the ICMPv4 header. 在 `icmp_timestamp` 中會有比其他種 message 多 12 bytes,因為把 `times` 算在 header 裡面
- `replyopts`: [`ip_options_data`]() 物件,最多40個 bytes,用於啟用進階功能如 strict routing/loose routing, record routing, time stamping 等等,這些進階功能是在 `ip_optinos_echo` 初始化,會在第四章講解。
### Receiving ICMPv4 Messages
當我們收到 ICMP 封包時,[`ip_local_deliver_finish`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/ip_input.c#L227) 負責處理封包,這個功能在 [`ip_local_deliver`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/ip_input.c#L242) 註冊來處理所有 IPv4 的封包,然後會根據收到封包的 `ip_hdr(skb)->protocol` 來絕決定要用哪個通訊協定做後續的處理,所以這邊會呼叫到在 [`icmp_protocol`](https://github.com/torvalds/linux/blob/2bfcfd584ff5ccc8bb7acde19b42570414bf880b/net/ipv4/af_inet.c#L1716C1-L1720C3) 中的 [`icmp_rcv`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1193)。
在 `icmp_rcv` 中會處理以下事情,要注意的是,在這個功能中發現錯誤時,並不會回傳錯誤訊息,只會丟棄該封包。這是因為收到有問題的 ICMP 並不需要做任何事。其他協定處理封包時回傳錯誤是為了要對封包做更多的處理。
1. Increase `InMsgs` SNMP counter(`ICMP_MIB_INGSGS`)
2. 確認 checksum 沒錯
1. 如果有問題的話 `ICMP_MIB_CSUMERRORS` 和 `ICMP_MIB_INERRORS` 都會加一
2. 釋放 SKB 使用的記憶體
3. 回傳 `NET_RX_DROP`
4. 使用 [`ICMPMSGIN_INC_STATS`](https://github.com/torvalds/linux/blob/master/include/net/icmp.h#L32) 把 `/proc` 中對應的 counter(每個 type 都有) 加一
5. 檢查 type 是否在定義的範圍內
6. 超出範圍就會把 `ICMP_MIB_INERRORS` 加一
7. 釋放 SKB 使用的記憶體
8. 回傳 `NET_RX_DROP`
8. 如果收到的封包 flag 中,[包含](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1266)了 `RTCF_BROADCAST ` 或 `RTCF_MULTICAST` 就根據 `net->ipv4.sysctl_icmp_echo_ignore_broadcasts` 的設定來決定要不要丟棄封包。如果不是合理的 type 也會丟棄封包
這個設定可以透過 `/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts` 來設定,預設值為1,代表要忽略這樣的封包
6. 超出範圍就會把 `ICMP_MIB_INERRORS` 加一
7. 釋放 SKB 使用的記憶體
8. 回傳 `NET_RX_DROP`
7. 根據收到的 type 呼叫 [`icmp_pointers`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1394) 中的 handler。例如收到 `ICMP_ECHOREPLY`(`ping` 使用的 ICMP 類型) 就會呼叫 `ping_rcv`,收到 `ICMP_EXT_ECHO` 就會呼叫到 [`icmp_echo`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1005),之後已 `icmp_echo` 為例
8. 檢查 `net->ipv4.sysctl_icmp_echo_ignore_all`
9. 如果為1,就會直接回傳 `SKB_NOT_DROPPED_YET`
10. 呼叫 [`icmp_reply`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L400),`ICMP_TIMESTAMP` 在回覆時也是透過這個功能
11. 把訊息送出
[`icmp_push_reply`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L366) -> [`ip_append_data`](https://github.com/torvalds/linux/blob/master/net/ipv4/ip_output.c#L1344) -> [`ip_flush_pending_frames`](https://github.com/torvalds/linux/blob/master/net/ipv4/ip_output.c#L1530)
### Sending ICMPv4 Messages: "Desitnation Unreachable"
有兩種方法可以發送 ICMPv4 訊息
1. `icmp_reply`,用來發送 ICMP request, ICMP_ECHO 以及 ICMP_TIMESTAMP 的訊息
2. `icmp_send`
這兩個方法都會使用到 [`icmp_push_reply`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L366) 將訊息送出。並且,都透過呼叫 [`icmpv4_xrlim_allow`](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/icmp.c#L310) 支援傳輸速率的限制,如果傳輸速率被允許的話,就會送出封包。不過要注意的是,並不是每一種類型的 traffic 都可以使用 rate limiting,以下是使用速率限制的要求,符合以下任意一點,都沒辦法使用 rate limiting。
1. The message type is unknown
2. The packet is of PMTU discovery
3. The device is a loopback device
4. The ICMP type is not enabled in the rate mask
如果支援 rate limiting,該功能真正的實作在 [`inet_peer_xrlim_allow`](https://github.com/torvalds/linux/blob/master/net/ipv4/inetpeer.c#L267) 中。
```clike
void __icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info, const struct ip_options *opt)
```
- `skb_in`: 呼叫 `icmp_send` 的 SKB
- `type`: ICMPv4 的 type
- `code`: ICMPv4 的 code
- `info`: 根據不同的 type 會有不一樣的定義
- ICMP_PARAMETERPROB message type: IPv4 header 到 parse 時出現問題點的 offset
- ICMP_DEST_UNREACH + ICMP_FRAG_NEEDED: 當前的 MTU 資訊
- ICMP_REDIRECT + ICMP_REDIR_HOST: 目的地的 IP 地址
深入了解 `icmp_send` 會發現有很多完整性的檢查。
1. multicast/broadcast 的封包會被拒絕
2. [檢查 `frag_off`](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/icmp.c#L645) 確認是否要對封包進行切割,要的話就只傳送 ICMPv4 message 的第一個分割
3. 根據 [RFC 1812 4.3.2.7](https://datatracker.ietf.org/doc/html/rfc1812#section-4.3.2.7) 所述,收到 ICMP error message 時,不能再傳送 ICMP error message 來回應。所以要傳送 ICMP error message 時,會[確認該 SKB 是否是收到 ICMP error message 的 SKB](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/icmp.c#L651),是的話就不會傳送
4. [SKB 指向未知的 ICMPv4 type](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/icmp.c#L674) 就不會傳送
檢查完後會根據 `net->ipv4.sysctl_icmp_errors_use_inbound_ifaddr` 來確認要不要把 IPv4 header 中的 IP options 放到 SKB 中。
最後
1. `icmp_bxm` 分配並初始化一個 `icmp_param`
2. 使用 `icmp_route_lookup` 確認路徑
3. `icmp_push_reply` 送出訊息
1. 先取得 network namespace
5. `icmp_sk` fetches the socket,因為 SMP 的關係,所以每個 CPU 都有一個 socket
6. `ip_append_data` 把 packet 移動到 IP layer
1. 如果失敗就呼叫 `ip_flush_pending_frames` 將 SKB 釋放
8. `ip_push_pending_frames` 把封包送出去
本小節說明了幾個送出 "Destination Unreachable" 訊息的案例
#### Code2: ICMP_PROT_UNREACH
這個判斷寫在 [`ip_protocol_deliver_rcu`](https://github.com/torvalds/linux/blob/e0cce98fe279b64f4a7d81b7f5c3a23d80b92fbc/net/ipv4/ip_input.c#L187C6-L187C29) 中。當 IP Protocol 收到的封包中,protocol 段落的內容是未定義的協定,就會送出 ICMPv4 訊息中的 "Destination Unreachable" 給發送端,ICMP_PROT_UNREACH 就是在說明"收到的協定無法送達"。
#### Code3: ICMP_PORT_UNREACH
在 [`__udp4_lib_rcv`](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/udp.c#L2383) 中,會負責處理收到的 UDPv4 封包。如果找不到對應的 UDP socket 並且 checksum 沒錯的話,就會送出 ICMPv4 的 "Destination Unreachable" 並且[附帶 ICMP_PORT_UNREACH](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/udp.c#L2457)。但若 checksum 錯誤,就會直接丟棄封包,做 silent drop。
#### Code4: ICMP_FRAG_NEEDED
負責[轉發封包訊息](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/ip_forward.c#L83)時,如果訊息大於 MTU 導致需要做 fragment,但是該封包的 Don't Fragment(DF) Flag 有啟用,就會丟棄該封包並且[傳送 ICMP_FRAG_NEEDED 給發送端](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/ip_forward.c#L137)。
#### Code5: ICMP_SR_RAILED
轉發的訊息中,若[有限制 routing 選項,並且發現要使用的 routing 有用到 gateway](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/ip_forward.c#L128),就會[傳送 ICMP_SR_FAILED](https://github.com/torvalds/linux/blob/4a4be1ad3a6efea16c56615f31117590fd881358/net/ipv4/ip_forward.c#L170)。
## ICMPv6
ICMPv6 與 ICMPv4 相同,都是為了在網路層(L3)回報錯誤訊息而生,除此之外,ICMPv6 還被賦予了更多的任務,從 [RFC 4443](https://datatracker.ietf.org/doc/html/rfc4443) 中了解第一手細節,如果直接閱讀原始碼會發現許多關於 [RFC 1885](https://datatracker.ietf.org/doc/html/rfc1885) 的實作,這是因為他是 ICMPv6 的第一個版本的設計,之後又有了 [RFC 2463](https://datatracker.ietf.org/doc/html/rfc2463) 最後才有 RFC 4443。ICMPv6 是基於 IPv4 而設計得,但是更加複雜,本小節會討論其中的改動。
根據 RFC 4443 可以知道 ICMPv6 協定使用的 header value 是58,而且 ICMPv6 是 IPv6 的一部分,每個 IPv6 節點都必須實作 ICMPv6 的功能。ICMPv6 除了負責傳遞錯誤訊息,還被用於 [Neighbour Discovery(ND) Protocol](https://zh.wikipedia.org/zh-tw/%E9%82%BB%E5%B1%85%E5%8F%91%E7%8E%B0%E5%8D%8F%E8%AE%AE),該協定取代了 IPv4 中的 ARP 協定;也使用於 [Multicast Listener Discovery(MLD) Protocol](https://en.wikipedia.org/wiki/Multicast_Listener_Discovery),對應於 IPv4 中的 [IGMP Protocol](https://zh.wikipedia.org/zh-tw/%E5%9B%A0%E7%89%B9%E7%BD%91%E7%BB%84%E7%AE%A1%E7%90%86%E5%8D%8F%E8%AE%AE)

顯而易見的,ICMPv4 中的實作在 ICMPv6 中也都找的到,所以有 `traceroute6` 使用了 ICMPv6 實作路由追蹤。ICMPv6 的主要實作可以在 `net/ipv6/icmp.c` 中找到,但是,同樣的,ICMPv6 也不能當作一個單獨的 Kernel module。
### ICMPv6 Initialization
ICMPv6 的初始化是透過 [`icmpv6_init`](https://github.com/torvalds/linux/blob/a693b9c95abd4947c2d06e05733de5d470ab6586/net/ipv6/icmp.c#L1051),這個方法在 [`inet6_init`](https://github.com/torvalds/linux/blob/master/net/ipv6/af_inet6.c#L1138) 中被呼叫到,`inet6_init` 就是拿來初始化 IPv6 的。
其中,會使用到 [`icmpv6_protocol`](https://github.com/torvalds/linux/blob/a693b9c95abd4947c2d06e05733de5d470ab6586/net/ipv6/icmp.c#L96),這是個 `inet6_protocol` 結構,用於指定收到訊息 `handler` 以及發生錯醋時的 `handler`。
```clike
static const struct inet6_protocol icmpv6_protocol = {
.handler = icmpv6_rcv,
.err_handler = icmpv6_err,
.flags = INET6_PROTO_NOPOLICY|INET6_PROTO_FINAL,
};
int __init icmpv6_init(void)
{
...
if (inet6_add_protocol(&icmpv6_protocol, IPPROTO_ICMPV6) < 0)
goto fail;
...
fail:
pr_err("Failed to register ICMP6 protocol\n");
return err;
}
```
如果發現 `INET6_PROTO_NOPOLICY` 有被啟用,就代表 IPsec 的 Policy 中部允許使用 IPv6 封包。例如在 [`ip6_input_finish` 中](https://github.com/torvalds/linux/blob/a693b9c95abd4947c2d06e05733de5d470ab6586/net/ipv6/ip6_input.c#L430)就會丟棄該封包。
:::warning
本書撰寫的時候還是使用 `icmpv6_sk_init` 建立 `sock`,但現在都是使用 `inet_ctl_sock_create` 建立 `sock`,所以後續是我自己追蹤的部分,有誤或者說的不夠清楚的部分請留言分享或登入修改。
:::
1. `icmpv6_init` 會為每個 CPU 透過 `inet_ctl_sock_create` 建立一個 `sock`
2. `inet_ctl_sock_create` 透過 `sock_create_kern`(就是 `__sock_create`) 建立 Kernel 中的 Sock
3. `__socket_create` 中會負責檢查各項參數並建立 `sock`
1. 檢查要建立的 Protocol number and type 是否有支援
2. 透過 [`security_socket_create`](https://github.com/torvalds/linux/blob/master/security/security.c#L4363) 確認是否可以建立 `sock`
3. `sock_alloc` 建立新的一個 `sock`
4. 透過 `rcu_dereference` 查找 `net_families`,其中有建立 ICMPv6 sock 的方法,然後呼叫該方法
### ICMPv6 Header

ICMPv6 header 的實作是 [`icmp6hdr`](https://github.com/torvalds/linux/blob/c3f38fa61af77b49866b006939479069cd451173/include/uapi/linux/icmpv6.h#L8),內容非常的豐富。其中的 `icmp6_type` 用來表示訊息的類型,如果是 $0~127$ 則代表是錯誤訊息,否則就是普通訊息。下表為常見的 ICMPv6 的訊息類型、對應的訊息編號以及其 Kernel Symbol,[Internet Control Message Protocol version 6 (ICMPv6) Parameters](https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xml) 列出了所有的類型可以查閱。
|Type|Kernel Symbol|Error/Info|Description
|---|---|---|---
1|ICMPV6_DEST_UNREACH|Error|Destination Unreachable
2|ICMPV6_PKT_TOOBIG|Error|Packet too big
3|ICMPV6_TIME_EXCEED|Error|Time Exceeded
4|ICMPV6_PARAMPROB|Error|Parameter probolem
128|ICMPV6_ECHO_REQUEST|Info|Echo Request
129|ICMPV6_ECHO_REPLY|Info|Echo Reply
130|ICMPV6_MGM_QUERY|Info|Multicast group membership management query
131|ICMPV6_MGM_REPORT|Info|Multicast group membership management report
132|ICMPV6_MGM_REDUCTION|Info|Multicast group membership management reduction
133|NDISC_ROUTER_SOLICITATION|Info|Router solicitation
134|NDISC_ROUTER_ADVERTISEMENT|Info|Router advertisement
135|NDISC_NEIGHBOUR_SOLICITATION|Info|Neighbour solicitation
136|NDISC_NEIGHBOUR_ADVERTISEMENT|Info|Neighbour advertisement
137|NDISC_REDIRECT|Info|Neighbour redirect
從中我們可以注意到除了 `echo request/reply` 之外, ICMPv6 還包含了 Router, Neighbour 相關的訊息,這些就是 ICMPv4 沒有提供而 ICMPv6 新增的功能們。
### Receiving ICMPv6 Messages
核心透過 [`icmpv6_rcv`](https://github.com/torvalds/linux/blob/c3f38fa61af77b49866b006939479069cd451173/net/ipv6/icmp.c#L881) 處理收到的封包,流程如下圖。
```graphviz
digraph icmpv6_rcv{
M[label = "icmpv6_rcv", shape = box]
I[label = "Pass sanity checks?", shape = diamond]
D[label = "Discard packet", shape = box]
X[label = "Extract the ICMPv6 type from the IPv6 header", shape = box]
E[label = "icmpv6_echo_reply", shape = box]
N[label = "ndisc_rcv", shape = box]
R[label = "igmp6_event_report", shape = box]
{rank=same M}
{rank=same D,I}
M -> I -> X
X -> E[label = "ICMPV6_ECHO_REQUEST"]
X -> N[label = "NDISC_ROUTER_SOLICITATION"]
X -> R[label = "ICMPV6_MGM_REPORT"]
I -> D[label = "No"]
}
```
`icmpv6_rcv` 在收到封包後會
1. 進行一系列的檢查
- 通過: ICMP6_MIB_INMSGS 加一
- 失敗: ICMP6_MIB_INERRORS 加一,釋放 SKB
1. 透過 ~~`ICMP6MSGIN_INC_STATS_BH`~~ 為不同的 type 加一。在這個 [commit](https://github.com/torvalds/linux/commit/f3832ed2c27e7ad13300791db4089a7d4304f500) 之後,改成了 [`ICMP6MSGIN_INC_STATS`](https://github.com/torvalds/linux/blob/2ab79514109578fc4b6df90633d500cf281eb689/include/net/ipv6.h#L283),這個統計量可以在 `/proc/net/snmp6` 裡面找到
2. 使用 switch(type) 執行各個不同類型的訊息處理。在 [`icmp_rcv`](https://github.com/torvalds/linux/blob/master/net/ipv4/icmp.c#L1288) 中使用的是一個 table 記住每個類型的 handler,這裡不是這樣的實作方式
- Echo Request(ICMPV6_ECHO_REQUEST): `icmpv6_echo_reply`
- Echo Reply(ICMPV6_ECHO_REPLY): `ping_rcv`,ICMPv4 也是透過該功能處理這種訊息
- Packet too big(ICMPV6_PKT_TOOBIG):
1. 透過 [`pskb_may_pull`](https://github.com/torvalds/linux/blob/master/include/linux/skbuff.h#L2752) 檢查 `skb->data` 是否包含超過 ICMP header 大小的資料,否則直接丟棄封包
2. 透過 [`icmpv6_notify`](https://github.com/torvalds/linux/blob/2ab79514109578fc4b6df90633d500cf281eb689/net/ipv6/icmp.c#L823) 讓註冊的 socket 處理這個 ICMP 訊息
- Destination Unreachable, Time Exceeded and Parameter Problem: 也都是透過 `icmpv6_notify` 處理的
- Neighbour Discovery(ND) messages: 通通由 [`ndisc_rcv`](https://github.com/torvalds/linux/blob/master/net/ipv6/ndisc.c#L1828) 負責處理
- NDISC_ROUTE_SOLICITATION: 通常是送給所有 router 的,使用的 multicast address 是 $FF02::2$ 然後透過 router advertisements 回應
- NDISC_ROUTER_ADVERTISEMENT: router 會定期發送的訊息,或者收到 solicitation 的時候也會立即回應。
- NDISC_NEIGHBOUR_SOLICITATION: 取代 IPv4 中的 ARP request
- NDISC_NEIGHBOUG_ADVERTISEMENT: 取代 IPv4 中的 ARP reply
- NDISC_REDIRECT: 用來跟 host 講有更好的 first hop
- Multicast Listener Query(ICMPV6_MGM_QUERY): [`igmp6_event_query`](https://github.com/torvalds/linux/blob/master/net/ipv6/mcast.c#L1374)
- Multicast Listener Report(ICMPV6_MGM_REPORT): [`icmp6_event_report`](https://github.com/torvalds/linux/blob/master/net/ipv6/mcast.c#L1542)
- 其餘有定義的 type 都不會處理
- ICMPV6_MGM_REDUCTION: [`igmp6_leave_group`]() 時會發出的訊息
- ICMPV6_MLD2_REPORT: MLDv2 Multicast Listener Report packet; 通常用來傳送給所有 MLDv2-capable routers Multicast Group Address($FF02LL16$)
- ICMPV6_NI_QUERY: ICMP NOde Information Query
- ICMPV6_NI_REPLY: ICMP Node Information Response
- ICMPV6_DHAAD_REQUEST: ICMP Home Agent Address Discovery Request Message
- ICMPV6_DHAAD_REPLY: ICMP Home Agent Address Reply Message
- ICMPV6_MOBILE_PREFIX_SOL: ICMP Mobile Prefix Solicitation Message Format
- ICMPV6_MOBILE_PREFIX_ADV: ICMP Mobile Prefix Advertisement Message Format
- unknown type 都由 `icmpv6_notify` 處理
### Sending ICMPv6 Messages
我們主要透過 [`__icmpv6_send`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv6/ip6_icmp.c#L36) 來發送 ICPMv6 訊息,本小節會說明在那些情境下,我們才能使用這個功能。另外,核心中還有 [`icmpv6_echo_reply`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv6/icmp.c#L712) 專門發送回應給發送了 ICMPV6_ECHO_REQUEST 的 `ping` 訊息。
`icmpv6_send` 支援 rate limit,稱為 [`icmpv6_xrlim_allow`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv6/icmp.c#L193),與 ICMPv4 相同的,不是所有的流量都自動被限制,如果符合以下任一項情境,就不會被限制流量
- Informational messages
- PMTU discovery
- Loopback device
如果封包不符合上述的描述,透過 [`inet_peer_xrlim_allow`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv4/inetpeer.c#L267) 來限制流量。ICMPv4 與 ICMPv6 一樣都是透過這個方法來限制流量,但是與 IPv4 不同的是,~~IPv6 不能自己設定 rate mask,這並不是 RFC 4443 的限制,只是沒被實作而已~~。在 [commit](https://github.com/torvalds/linux/commit/0bc199854405543b0debe67c735c0aae94f1d319) 中,實作了 IPv6 的 rate mask。
#### Example: Sending "Hop Limit Time Exceeded" ICMPv6 Messages
Hop Limit 就是 IPv6 版本的 TTL,當其值為0時,收到該封包的機器就要負責使用 `icmpv6_send` 傳送包含了 ICMPV6_EXC_HOPLIMIT 的 ICMPV6_TIME_EXCEED [訊息](https://github.com/torvalds/linux/blob/master/net/ipv6/ip6_output.c#L547)給發算端。
```clike=
int ip6_forward(struct sk_buff *skb)
{
...
if (hdr->hop_limit <= 1) {
icmpv6_send(skb, ICMPV6_TIME_EXCEED, ICMPV6_EXC_HOPLIMIT, 0);
__IP6_INC_STATS(net, idev, IPSTATS_MIB_INHDRERRORS);
kfree_skb_reason(skb, SKB_DROP_REASON_IP_INHDR);
return -ETIMEDOUT;
}
...
}
```
#### Example: Sending "Fragment Reassembly Time Exceeded" ICMPv6 Messages
當有個 fragment 發生 time out 的時候,則需要發送包含 ICMPV6_EXC_FRAGTIME 的 ICMPV6_TIME_EXCEED [訊息](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/include/net/ipv6_frag.h#L105)
#### Example: Sending "Destination Unreachable"/"Port Unreachable" ICMPv6 Message
如果收到的 UDPv6 封包找不到所屬的 socket,並且 checksum 驗證沒錯,就會送出 "Destination Unreachable" / "Port Unreachable" 的 ICMPv6 [訊息](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv6/udp.c#L1034)。
```clike
int __udp6_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
int proto)
{
...
no_sk:
...
icmpv6_send(skb, ICMPV6_DEST_UNREACH, ICMPV6_PORT_UNREACH, 0);
...
}
```
#### Example: Send "Fragmentation Needed" ICMPv6 Messages
如果收到的封包大於 MTU 需要分割傳送,但是 SKB 中的 `local_df` 又沒有啟用,就會把該封包丟棄並且傳送 ICMPV6_PKT_TOOBIG [訊息](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv6/ip6_output.c#L640)給發送端。要注意這裡的行為與 IPv4 不同,IPv4 傳送的是包含 ICMP_FRAG_NEEDED 的 ICMP_DEST_UNREACH;而在 IPv6 中,則是發送 ICMPV6_PKT_TOOBIG 而不是 ICMPV6_DEST_UNREACH 訊息。這個訊息類型是 ICMPv6 獨有的。
```clike=
int ip6_forward(struct sk_buff *skb)
{
...
if (ip6_pkt_too_big(skb, mtu)) {
/* Again, force OUTPUT device used as source address */
skb->dev = dst->dev;
icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
__IP6_INC_STATS(net, idev, IPSTATS_MIB_INTOOBIGERRORS);
__IP6_INC_STATS(net, ip6_dst_idev(dst),
IPSTATS_MIB_FRAGFAILS);
kfree_skb_reason(skb, SKB_DROP_REASON_PKT_TOO_BIG);
return -EMSGSIZE;
}
...
}
```
#### Example: Sending "Parameter Problem" ICMPv6 Messages
如果在解析 extension header 遇到問題,就會傳送包含 ICMPV6_UNK_OPTION 的 ICMPV6_PARAMPROB [訊息](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv6/exthdrs.c#L65)給發送端。
## ICMP Sockets ("Ping sockets")
Openwall GNU/\*/Linux distribution(Owl) [新增了 IPPROTO_ICMP socket type](https://github.com/torvalds/linux/commit/c319b4d76b9e583a5d88d6bf190e079c4e43213d),用來提供比其他 distro 更安全的 ping socket 版本。我們可以在 [Mac OS X](https://www.manpagez.com/man/4/icmp/) 中看到相似的實作。
使用者可以透過 `socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)` 建立 ICMPv4 socket,如果要建立 ICMPv6 socket 則是 `socket(PF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6)`。
Linux Kernel 預設是不能使用 ICMP socket 的,如果要使用就要設定 `/proc/sys/net/ipv4/ping_group_range`,其中的內容預設是 "1 0",代表的是沒有人可以建立 ping socket。如果要允許使用者建立該 socket 的話,就要把 uid 以及 gid 寫入 `/proc/sys/net/ipv4/ping_group_range` 中。然後該使用者就可以建立 ping socket。如果是要允許系統使用者建立該 socket 的話,就要寫入"0 2147483647",2147483647是 `GID_T_MAX` 的值。
另外,不管是 IPv4 還是 IPv6 都是透過 `/proc/sys/net/ipv4/ping_group_range` 設定。而在傳送的 socket 中,ICMP message code 必須是0。
```clike
static inline int ping_supported(int family, int type, int code)
{
return (family == AF_INET && type == ICMP_ECHO && code == 0) ||
(family == AF_INET && type == ICMP_EXT_ECHO && code == 0) ||
(family == AF_INET6 && type == ICMPV6_ECHO_REQUEST && code == 0) ||
(family == AF_INET6 && type == ICMPV6_EXT_ECHO_REQUEST && code == 0);
}
```
## Covered Methods
- [`icmp_rcv`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/net/ipv4/icmp.c#L1193): 用於處理收到的 ICMPv4 訊息
- [`icmp_send`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/include/net/icmp.h#L41): 發送 ICMPv4 訊息
- [`icmp6_hdr`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/include/linux/icmpv6.h#L9): 取得 ICMPv6 的 header 地址
- [`icmpv6_send`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/include/linux/icmpv6.h#L47): 發送 ICMPv6 訊息
- [`icmpv6_param_prob`](https://github.com/torvalds/linux/blob/32f88d65f01bf6f45476d7edbe675e44fb9e1d58/include/linux/icmpv6.h#L93): 發送包含丟棄封包原因的訊息
## procfs entries
Linux 核心提供了在 userspace 設定各種子系統的方法,這些設定都可以在 `/proc` 中找到,而這些方法就稱為 procfs entries。ICMPv4 的 procfs entries 也可以在 [`netns_ipv4`](https://github.com/torvalds/linux/blob/master/include/net/netns/ipv4.h#L43) 結構中找到,以下列出幾個設定的功能以及預設值,詳情可見 [Documentation/networking/ip-sysctl.rst](https://github.com/torvalds/linux/blob/master/Documentation/networking/ip-sysctl.rst)。
- `sysctl_icmp_echo_ignore_all`: 如果有啟用,就不會回覆任何的 echo request
- `sysctl_icmp_echo_ignore_broadcasts`: 如果有啟用,就不會回覆 broadcast/multicast 的 echo request,也不會回覆 timestamp message
- `sysctl_icmp_ignore_bogus_error_responses`: 有些路由器沒有遵循 [RFC1122](https://datatracker.ietf.org/doc/html/rfc1122) 的規範傳送出假的回覆。所以在 `icmp_unreach` 中,會檢查這個功能有沒有啟用,啟用的話就不會記錄`"<IPv4Addr>sent an invalid ICMP type. . .”`警告。
- `sysctl_icmp_rate_limit`: 有啟用的話,就可以限制發送 ICMP 封包的流量
- `sysctl_icmp_ratemask`: 用來設定 ICMPv4 的 rate mask
- `sysctl_icmp_errors_use_inbound_ifaddr`: 控制 ICMP 錯誤訊息要使用哪個網路接口的 IP 來發送,如果有啟用的話,就會使用發送接口的 IP,否則會使用接收到這個 ICMP 錯誤的網路接口 IP
## Creating "Destination Unreachable" Messages with `iptables`
`iptables` 允許使用者設定規則,讓核心過濾網路封包。使用者可以使用 `reject` 規則的設定來拒絕連線,並且透過 `--reject-with` 設定要送出的錯誤訊息,下列為拒絕連線後,傳送 "Destination Unreachable" ICMPv4 訊息。
```bash
iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
```
下列為可以設定的 ICMPv4 訊息
- `icmp-net-unreachable`: ICMP_NET_UNREACH
- `icmp-host-unreachable`: ICMP_HOST_UNREACH
- `icmp-port-unreachable`: ICMP_PORT_UNREACH
- `icmp-proto-unreachable`: ICMP_PROT_UNREACH
- `icmp-net-prohibited`: ICMP_NET_ANO
- `icmp-host-prohibited`: ICMP_HOST_ANO
- `icmp-admin-prohibited`: ICMP_PKT_FILTERED
下列為可以設定的 ICMPv6 訊息
- `no-route`/`icmp6-no-route`: ICMPV6_NOROUTE
- `adm-prohibited`/`icmp6-adm-prohibited`: ICMPV6_ADM_PROHIBITED
- `port-unreach`/`icmp6-port-unreachable`: ICMPV6_NOT_NEIGHBOUR
- `addr-unreach`/`icmp6-addr-unreachable`: ICMPV6_ADDR_UNREACH
也可以使用 `tcp-reset` 傳送 TCP RST 風包給傳送訊息過來的主機。
## Reference
- [RFC 1812](https://datatracker.ietf.org/doc/html/rfc1812#page-52): Detail of ICMP messages