bpf_cmd:
BPF_MAP_CREATE: 創建一個新的BPF映射。
BPF_MAP_LOOKUP_ELEM: 在BPF映射中查找一個元素。
BPF_MAP_UPDATE_ELEM: 更新BPF映射中的一個元素。
BPF_MAP_DELETE_ELEM: 從BPF映射中刪除一個元素。
BPF_MAP_GET_NEXT_KEY: 獲取BPF映射中下一個鍵的迭代器。
BPF_PROG_LOAD: 加載一個新的BPF程序到內核中。
BPF_OBJ_PIN: 將BPF對象(如映射或程序)固定到文件系統中,以便後續檢索。
BPF_OBJ_GET: 從文件系統中檢索固定的BPF對象。
BPF_PROG_ATTACH: 將BPF程序附加到某個內核事件或對象上。
BPF_PROG_DETACH: 將BPF程序從之前附加的內核事件或對象上分離。
BPF_PROG_TEST_RUN/BPF_PROG_RUN: 測試或運行BPF程序。
BPF_PROG_GET_NEXT_ID: 獲取下一個BPF程序ID的迭代器。
BPF_MAP_GET_NEXT_ID: 獲取下一個BPF映射ID的迭代器。
BPF_PROG_GET_FD_BY_ID/BPF_MAP_GET_FD_BY_ID: 通過ID獲取BPF程序或映射的文件描述符。
BPF_OBJ_GET_INFO_BY_FD: 通過文件描述符獲取BPF對象的信息。
BPF_PROG_QUERY: 查詢BPF程序的信息。
BPF_RAW_TRACEPOINT_OPEN: 打開一個原始跟蹤點。
BPF_BTF_LOAD: 加載BPF類型信息(BTF)。
BPF_BTF_GET_FD_BY_ID: 通過ID獲取BTF的文件描述符。
BPF_TASK_FD_QUERY: 查詢與任務相關的文件描述符信息。
BPF_MAP_LOOKUP_AND_DELETE_ELEM: 查找並刪除BPF映射中的一個元素。
BPF_MAP_FREEZE: 凍結BPF映射,防止進一步更新。
BPF_BTF_GET_NEXT_ID: 獲取下一個BTF ID的迭代器。
BPF_MAP_LOOKUP_BATCH/BPF_MAP_LOOKUP_AND_DELETE_BATCH/BPF_MAP_UPDATE_BATCH/BPF_MAP_DELETE_BATCH: 批量操作BPF映射的元素。
BPF_LINK_CREATE/BPF_LINK_UPDATE/BPF_LINK_GET_FD_BY_ID/BPF_LINK_GET_NEXT_ID: 創建、更新、通過ID獲取文件描述符、獲取下一個鏈接ID的BPF鏈接操作。
BPF_ENABLE_STATS: 啓用BPF統計信息。
BPF_ITER_CREATE: 創建BPF迭代器。
BPF_LINK_DETACH: 分離BPF鏈接。
BPF_PROG_BIND_MAP: 將BPF程序綁定到映射上。
bpf_map_type
BPF_MAP_TYPE_UNSPEC: 未指定的映射類型。
BPF_MAP_TYPE_HASH: 哈希表映射。
BPF_MAP_TYPE_ARRAY: 數組映射。
BPF_MAP_TYPE_PROG_ARRAY: 程序數組映射,用於存儲BPF程序的數組。
BPF_MAP_TYPE_PERF_EVENT_ARRAY: 性能事件數組映射。
BPF_MAP_TYPE_PERCPU_HASH/BPF_MAP_TYPE_PERCPU_ARRAY: 每個CPU的哈希表/數組映射。
BPF_MAP_TYPE_STACK_TRACE: 堆棧跟蹤映射。
BPF_MAP_TYPE_CGROUP_ARRAY: 控制組數組映射。
BPF_MAP_TYPE_LRU_HASH/BPF_MAP_TYPE_LRU_PERCPU_HASH: LRU(最近最少使用)哈希表映射,可選地每個CPU。
BPF_MAP_TYPE_LPM_TRIE: LPM(最長前綴匹配)樹映射。
BPF_MAP_TYPE_ARRAY_OF_MAPS/BPF_MAP_TYPE_HASH_OF_MAPS: 映射的數組/哈希表映射。
BPF_MAP_TYPE_DEVMAP/BPF_MAP_TYPE_SOCKMAP/BPF_MAP_TYPE_CPUMAP: 設備/套接字/CPU映射,用於網絡和設備編程。
BPF_MAP_TYPE_XSKMAP: XDP套接字映射。
BPF_MAP_TYPE_SOCKHASH: 套接字哈希映射。
BPF_MAP_TYPE_CGROUP_STORAGE/BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE: 控制組存儲映射,可選地每個CPU。注意這些類型已棄用,建議使用BPF_MAP_TYPE_CGRP_STORAGE。
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY: 重用端口套接字數組映射。
BPF_MAP_TYPE_QUEUE/BPF_MAP_TYPE_STACK: 隊列/堆棧映射。
BPF_MAP_TYPE_SK_STORAGE: 套接字存儲映射。
BPF_MAP_TYPE_DEVMAP_HASH: 設備哈希映射。
BPF_MAP_TYPE_STRUCT_OPS: 結構體操作映射,用於修改內核函數的行為。
BPF_MAP_TYPE_RINGBUF: 環形緩衝區映射。
BPF_MAP_TYPE_INODE_STORAGE/BPF_MAP_TYPE_TASK_STORAGE: inode/任務存儲映射。
BPF_MAP_TYPE_BLOOM_FILTER: 布隆過濾器映射。
BPF_MAP_TYPE_USER_RINGBUF: 用户空間環形緩衝區映射。
BPF_MAP_TYPE_CGRP_STORAGE: 控制組存儲映射,支持更廣泛的用例。
struct xdp_md {
__u32 data;
__u32 data_end;
__u32 data_meta;
/* Below access go through struct xdp_rxq_info */
這個成員表示接收數據包的網絡接口的索引(ifindex)。它是通過 rxq->dev->ifindex 獲得的,其中 rxq 是指向接收隊列的指針,dev 是指向網絡設備的指針。這個索引可以用來識別數據包是從哪個網絡接口進入系統的。
__u32 ingress_ifindex; /* rxq->dev->ifindex */
這個成員表示接收數據包時所在的接收隊列的索引。它是通過 rxq->queue_index 獲得的,這個索引對於理解數據包在接收過程中的調度和負載均衡可能是有用的。
__u32 rx_queue_index; /* rxq->queue_index */
這個成員表示如果數據包被 XDP 程序修改並重新發送時,應該使用的發送網絡接口的索引。然而,需要注意的是,在 XDP 程序的上下文中,egress_ifindex 的值可能並不總是有效或可用,因為它通常與數據包的發送路徑相關,而 XDP 程序主要關注於數據包的接收和可能的修改。在某些情況下,這個成員可能被忽略或具有特定的默認值。
__u32 egress_ifindex; /* txq->dev->ifindex */
};
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP 程序決定不對數據包進行任何修改,並將其直接傳遞給網絡棧的其餘部分進行正常處理時,它會返回 XDP_PASS。這是 XDP 程序在不需要對數據包進行特殊處理時的默認行為。
XDP_PASS,
//如果 XDP 程序想要將數據包發送到另一個網絡接口(即將其轉發或發送到另一個地方),它會返回 XDP_TX 並可能伴隨一個指向 xdp_tx_metadata 結構體的指針(這取決於具體的內核版本和 XDP 程序的實現)。這個操作允許數據包在內核內部被重新路由,而不需要將它們發送回用户空間進行處理。
XDP_TX,
XDP_REDIRECT,
};
如果是tcp 通信的話,XDP_DROP 了之後, 客户端會收到通知嗎 ?
在TCP通信中,如果數據包在XDP層被XDP_DROP處理,客户端通常不會直接收到關於數據包被丟棄的通知。這是因為XDP(eXpress Data Path)是在網絡數據包到達網絡棧之前,即在TCP/IP協議棧的更低層次(如網卡驅動層)進行處理的。XDP的主要目的是在網絡棧的早期階段對數據包進行快速處理,以減少處理延遲和提高性能
過濾掉特定端口的數據包
- 例如丟棄所有目的端口為 80(HTTP)的數據包。
- 基於地理位置過濾, 獲取 IP 地址的地理位置數據庫:需要一個包含 IP 地址和對應地理位置的數據庫,如 MaxMind 的 GeoIP 數據庫。這個數據庫可以提供 IP 地址所屬的國家、地區、城市等信息。
加載地理位置數據庫:在用户空間程序中加載地理位置數據庫,並將其映射到內核空間的 eBPF 程序中。
eBPF 程序過濾邏輯:在 eBPF 程序中,通過查找地理位置數據庫,判斷數據包的源 IP 地址是否來自指定的國家或地區,並根據結果決定是否丟棄數據包。
//
// Created by putao on 2024/9/5.
//
// packet_filter.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <netinet/in.h>
SEC("xdp")
int packet_filter(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
struct tcphdr *tcp;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
if (ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end) {
return XDP_DROP;
}
if (tcp->dest == __constant_htons(80)) {
return XDP_DROP; // 丟棄目的端口為 80 的數據包
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
將所有數據包鏡像到另一個網絡接口
網絡監控和分析:可以將數據包鏡像到一個專用的監控接口或設備,用於實時分析網絡流量,檢測異常行為或入侵。
流量複製:在測試環境中,可以將生產環境的流量複製到測試環境,以便進行性能測試或故障排除。
數據包捕獲:在網絡故障排除或性能分析時,可以將數據包鏡像到一個捕獲設備或系統,用於詳細分析。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
// 定義一個映射,用於存儲目標接口索引
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32);
} ifindex_map SEC(".maps");
SEC("xdp")
int packet_mirror(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
__u32 key = 0;
__u32 *ifindex = bpf_map_lookup_elem(&ifindex_map, &key);
if (ifindex) {
// 將數據包鏡像到指定的網絡接口
bpf_clone_redirect(ctx, *ifindex, 0);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
將數據包負載均衡到不同的後端服務器。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <netinet/in.h>
// 定義一個映射,用於存儲後端服務器的 IP 地址
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, __u32);
} backend_servers SEC(".maps");
SEC("xdp")
int packet_lb(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
struct tcphdr *tcp;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
if (ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end) {
return XDP_DROP;
}
__u32 src_ip = ip->saddr;
__u32 *backend_ip = bpf_map_lookup_elem(&backend_servers, &src_ip);
if (backend_ip) {
ip->daddr = *backend_ip;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
對特定 IP 地址的數據包進行限速。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} ip_rate_limit SEC(".maps");
SEC("xdp")
int packet_rate_limit(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
__u32 src_ip = ip->saddr;
__u64 *last_time = bpf_map_lookup_elem(&ip_rate_limit, &src_ip);
__u64 current_time = bpf_ktime_get_ns();
if (last_time) {
if (current_time - *last_time < 1000000000) { // 1 秒內限速
return XDP_DROP;
}
}
bpf_map_update_elem(&ip_rate_limit, &src_ip, ¤t_time, BPF_ANY);
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
DDoS 防護
分佈式拒絕服務(DDoS)攻擊是通過大量請求導致目標服務不可用的攻擊方式。DDoS 防護可以通過速率限制、流量分析和過濾等方式來實現。基於 IP 地址的速率限制,同上。
深度包檢測(DPI)
深度包檢測(DPI)是一種網絡流量分析技術,通過檢查數據包的內容(包括應用層數據),可以識別特定的協議、應用或內容。DPI 可以用於內容過濾、入侵檢測、帶寬管理等。
-
基於url過濾
#include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/in.h> #define HTTP_PORT 80 #define MAX_URL_LEN 256 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 256); __type(key, char[MAX_URL_LEN]); __type(value, __u8); } blocked_urls SEC(".maps"); SEC("xdp") int http_filter(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; struct iphdr *ip; struct tcphdr *tcp; char *http_data; char url[MAX_URL_LEN] = {0}; if (data + sizeof(*eth) > data_end) { return XDP_DROP; } eth = data; if (eth->h_proto != __constant_htons(ETH_P_IP)) { return XDP_PASS; } ip = data + sizeof(*eth); if ((void *)ip + sizeof(*ip) > data_end) { return XDP_DROP; } if (ip->protocol != IPPROTO_TCP) { return XDP_PASS; } tcp = (void *)ip + sizeof(*ip); if ((void *)tcp + sizeof(*tcp) > data_end) { return XDP_DROP; } if (tcp->dest != __constant_htons(HTTP_PORT)) { return XDP_PASS; } http_data = (char *)tcp + sizeof(*tcp); if (http_data + 4 > (char *)data_end) { return XDP_DROP; } if (http_data[0] == 'G' && http_data[1] == 'E' && http_data[2] == 'T' && http_data[3] == ' ') { char *url_start = http_data + 4; char *url_end = url_start; while (url_end < (char *)data_end && *url_end != ' ' && *url_end != '\r' && *url_end != '\n') { url_end++; } if (url_end - url_start < MAX_URL_LEN) { __builtin_memcpy(url, url_start, url_end - url_start); url[url_end - url_start] = '\0'; } __u8 *blocked = bpf_map_lookup_elem(&blocked_urls, url); if (blocked) { return XDP_DROP; } } return XDP_PASS; } char _license[] SEC("license") = "GPL"; - 基於ua過濾
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#define HTTP_PORT 80
#define MAX_HEADER_LEN 256
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 256);
__type(key, char[MAX_HEADER_LEN]);
__type(value, __u8);
} blocked_user_agents SEC(".maps");
SEC("xdp")
int http_user_agent_filter(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
struct tcphdr *tcp;
char *http_data;
char user_agent[MAX_HEADER_LEN] = {0};
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
if (ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end) {
return XDP_DROP;
}
if (tcp->dest != __constant_htons(HTTP_PORT)) {
return XDP_PASS;
}
http_data = (char *)tcp + sizeof(*tcp);
if (http_data + 4 > (char *)data_end) {
return XDP_DROP;
}
// 查找 User-Agent 頭
char *header_start = http_data;
while (header_start < (char *)data_end) {
if (header_start + 10 < (char *)data_end &&
header_start[0] == 'U' && header_start[1] == 's' && header_start[2] == 'e' &&
header_start[3] == 'r' && header_start[4] == '-' && header_start[5] == 'A' &&
header_start[6] == 'g' && header_start[7] == 'e' && header_start[8] == 'n' &&
header_start[9] == 't' && header_start[10] == ':') {
char *ua_start = header_start + 12;
char *ua_end = ua_start;
while (ua_end < (char *)data_end && *ua_end != '\r' && *ua_end != '\n') {
ua_end++;
}
if (ua_end - ua_start < MAX_HEADER_LEN) {
__builtin_memcpy(user_agent, ua_start, ua_end - ua_start);
user_agent[ua_end - ua_start] = '\0';
}
break;
}
header_start++;
}
__u8 *blocked = bpf_map_lookup_elem(&blocked_user_agents, user_agent);
if (blocked) {
return XDP_DROP;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
雖然SNI不是SSL/TLS協議中的必要部分,但在現代網絡環境中,它對於支持多域名服務器、共享IP地址的虛擬主機、CDN和企業級應用等場景至關重要。
- 多域名服務器:當一台服務器配置了多個SSL證書,每個證書對應一個不同的域名時,SNI允許客户端在TLS握手過程中向服務器指明它希望連接的域名。這樣,服務器就能根據SNI信息返回正確的證書,完成SSL/TLS握手。
- 共享IP地址的虛擬主機:在雲環境和虛擬主機環境中,多個域名可能共享同一個IP地址。SNI使得這些域名能夠在同一個IP地址上安全地提供HTTPS服務,而無需為每個域名分配一個獨立的IP地址。
- 內容分發網絡(CDN):CDN提供商經常需要為不同的客户提供HTTPS服務,而客户之間可能共享CDN的IP地址。SNI允許CDN根據客户端的請求動態地選擇並返回正確的證書。
- 企業級應用:在大型企業中,可能存在多個內部或外部的應用和服務,它們共享同一個服務器或集羣。SNI可以幫助這些應用和服務安全地提供HTTPS服務,同時保持網絡的整潔和高效。
- 提升用户體驗:使用SNI可以避免在不支持SNI的客户端上出現“證書不匹配”的錯誤,從而提升用户體驗。這是因為沒有SNI時,服務器可能無法確定客户端請求的域名,從而返回錯誤的證書。
SNI 過濾的意義主要體現在以下幾個方面:
- 企業網絡:企業可以通過 SNI 過濾來阻止員工訪問某些社交媒體、視頻流媒體或其他不相關的站點。
- 公共網絡:公共 Wi-Fi 提供者可以通過 SNI 過濾來阻止訪問某些不適當的內容。
- 在某些國家或地區,法律法規要求運營商或網絡提供者阻止訪問某些特定的網站或服務。通過 SNI 過濾,可以實現對這些網站的訪問控制,確保合規性。
- 通過 SNI 過濾,網絡管理員可以更好地管理網絡資源,防止帶寬被濫用。例如,可以阻止大文件下載或視頻流媒體服務,以確保網絡帶寬的合理分配。
如果 SNI 被過濾,客户端通常無法與目標服務器建立 HTTPS 連接。為了更好地理解這個過程,我們需要了解 SSL/TLS 連接的建立過程。
以下是 SSL/TLS 連接建立的基本過程:
- 客户端發起連接(ClientHello)
客户端向服務器發送一個 ClientHello 消息,其中包括以下信息:
支持的協議版本(如 TLS 1.2 或 TLS 1.3)
支持的加密套件(Cipher Suites)
支持的壓縮方法
隨機數(用於生成會話密鑰)
擴展字段(包括 SNI) - 服務器響應(ServerHello)
服務器接收到 ClientHello 消息後,發送 ServerHello 消息,其中包括:
協商的協議版本
選擇的加密套件
選擇的壓縮方法
隨機數
服務器證書(用於驗證服務器身份)
服務器公鑰(用於加密會話密鑰)
其他擴展字段 - 服務器證書驗證
客户端接收到 ServerHello 和服務器證書後,會驗證服務器證書的有效性。這包括檢查證書的簽名、有效期和頒發機構等。 - 客户端密鑰交換(Client Key Exchange)
客户端生成一個預主密鑰(Pre-Master Secret),並使用服務器的公鑰對其進行加密,然後發送給服務器。 - 生成會話密鑰
客户端和服務器使用之前的隨機數和預主密鑰生成會話密鑰(Session Key),用於加密後續的通信。 - 結束握手(Finished)
客户端和服務器分別發送 Finished 消息,表示握手過程結束。此時,客户端和服務器之間的通信將使用會話密鑰進行加密。
+---------------------------------------------------------+
| Record Layer |
+----------------+----------------+-----------------------+
| Content Type | Version | Length |
| (1 byte) | (2 bytes) | (2 bytes) |
+----------------+----------------+-----------------------+
+---------------------------------------------------------+
| Handshake Layer |
+----------------+----------------+-----------------------+
| Message Type | Length | Version |
| (1 byte) | (3 bytes) | (2 bytes) |
+----------------+----------------+-----------------------+
| Random (32 bytes) |
+---------------------------------------------------------+
| Session ID Length | Session ID (可變長度) |
| (1 byte) | |
+-------------------+-------------------------------------+
| Cipher Suites Length | Cipher Suites (可變長度) |
| (2 bytes) | |
+----------------------+----------------------------------+
| Compression Methods Length | Compression Methods (可變長度) |
| (1 byte) | |
+---------------------------+-----------------------------+
| Extensions Length | Extensions (可變長度) |
| (2 bytes) | |
+---------------------------+-----------------------------+
+---------------------------------------------------------+
| Extensions |
+----------------+----------------+-----------------------+
| Extension Type | Extension Length | Extension Data |
| (2 bytes) | (2 bytes) | (可變長度) |
+----------------+------------------+---------------------+
+---------------------------------------------------------+
| SNI 擴展 |
+----------------+----------------+-----------------------+
| Extension Type | Extension Length | SNI List Length |
| (2 bytes) | (2 bytes) | (2 bytes) |
+----------------+------------------+---------------------+
| SNI Type | Host Name Length | Host Name (可變長度)|
| (1 byte) | (2 bytes) | |
+----------------+------------------+---------------------+