使用原始套接字 (SOCK_RAW, IPPROTO_ICMP) 手工構造/解析 ICMP 封包,實現 ping 核心功能:每秒發送 Echo Request、接收 Echo Reply、計算 RTT。
struct proto {
struct sockaddr *sasend; // 發送目標位址
struct sockaddr *sarecv; // 接收來源位址
socklen_t salen; // 位址長度
int icmpproto; // IPPROTO_ICMP
};
pid_t g_pid; // 進程 ID 低 16 位作為 ICMP 識別碼
int g_sockfd; // 原始套接字
struct proto g_proto = { NULL, NULL, 0, IPPROTO_ICMP };
main():解析主機名 → getaddrinfo → 設定 g_proto → signal(SIGALRM, sig_alrm) → readloop()readloop():建立原始套接字 (需 root) → setuid(getuid()) 放棄權限 → 設定接收緩衝區 → 首次 sig_alrm 發送 → recvmsg 迴圈接收 → proc_msg 處理sig_alrm():每秒觸發 → send_msg() → alarm(1) 重設定時器send_msg():填充 ICMP 標頭 (Type=8, Code=0, ID=pid, Seq=nsent++)、資料區填入 gettimeofday 時間戳 → 計算校驗和 → sendtoproc_msg():剝離 IP 標頭 → 驗證 ICMP 類型/ID/序號 → 取出時間戳 → tv_sub 計算 RTT → 輸出
g_sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); // 需 CAP_NET_RAW (root)
setuid(getuid()); // 發送後立即降權,安全原則
uint16_t in_cksum(uint16_t *addr, int len) {
uint32_t sum = 0;
while (len > 1) { sum += *addr++; len -= 2; }
if (len == 1) { sum += *(uint8_t*)addr; }
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
return ~sum;
}
void tv_sub(struct timeval *out, struct timeval *in) { // out -= in
if ((out->tv_usec -= in->tv_usec) < 0) {
--out->tv_sec;
out->tv_usec += 1000000;
}
out->tv_sec -= in->tv_sec;
}
char *sock_ntop_host(const struct sockaddr *sa, socklen_t salen) {
static char str[128];
struct sockaddr_in *sin = (struct sockaddr_in *)sa;
inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str));
return str;
}
gcc -o myping myping.c
sudo ./myping www.baidu.com
PING www.a.shifen.com (115.239.210.26): 56 data bytes 64 bytes from 115.239.210.26: seq=0, ttl=128, rtt=31.272 ms 64 bytes from 115.239.210.26: seq=1, ttl=128, rtt=34.722 ms ...
-c count、-i interval、-s packetsize、IPv6 (ICMPV6)、統計摘要 (min/avg/max/mdev)capabilities (setcap cap_net_raw+ep) 或 setuid 二進制