Linux下使用原始套接字實現ping命令

2021-07-24 11:59:40 字數 4441 閱讀 3961

一、背景需求

客戶端程通過透明**訪問遠端伺服器,**需要以snat去修改源位址源埠,一般寫法是add snat、connect、del snat;

那麼問題來了,加snat規則時需要 -s $ip --sport $port (避免多個客戶端互相混淆),若正好**機器上存在多個位址時,呼叫connect之前socket並不知道需要繫結哪個出口位址,那怎麼獲取到$ip、$port呢?

我的思路是需要在connect動作之前,目的伺服器位址是已知的,通過傳送icmp echo 來確定本機的出口位址;

二、相關知識

2.1 ip路由選擇過程

假設當前linux具備多個網絡卡若干個位址,那麼在路由表上將存在各個網段的預設路由(route -n / netstat -nr);

《ccna》p257-261中提到了乙個最簡單的ip路由選擇的過程就是ping操作,大致步驟就是:

1)icmp建立回應請求資料報,ip協議建立分組;

2)ip協議判斷目的ip位址為本地網路還是遠端網路;

3)若目的為遠端網路,分組需要先傳送給預設閘道器(以預設閘道器的mac位址傳送,幀的形式);

5)迴圈第4步驟,最後伺服器收到分組(網路層)完成目的位址的匹配,生成乙個新的有效荷載遞交到icmp;

6)上述icmp需要成功返回到最初的客戶端,完成乙個ping的過程;

2.2 icmp結構

icmp包含在ip分組中,所以整體結構是20位元組的ip頭+8位元組icmp頭+icmp載荷

ip datagram

bits 0–7

bits 8–15

bits 16–23

bits 24–31

ip header

(20 bytes)

version/ihl

type of service

length

identification

flags and offset

time to live (ttl)

protocol

checksum

source ip address

destination ip address

icmp header

(8 bytes)

type of message

code

checksum

header data

icmp payload

(optional)

payload data

我們在ping程式中使用的是icmp echo request / reply 資訊,注意request.type=8,reply.type=0 00

0102

0304

0506

0708

0910

1112

1314

1516

1718

1920

2122

2324

2526

2728

2930

31type = 8

code = 0

header checksum

identifier

sequence number

payload 00

0102

0304

0506

0708

0910

1112

1314

1516

1718

1920

2122

2324

2526

2728

2930

31type = 0

code = 0

header checksum

identifier

sequence number

payload

三、程式設計實現

ping的程式設計傳送三層ip分組時,需要用到原始套接字(raw socket),參考《unp》書中的方法

「socket(pf_inet, sock_raw, ipproto_icmp」

int ping(char *dst_ip)

;

struct ip *ip = null;

struct sockaddr_in dst_addr = ;

struct icmp icmp_packet = ;

struct timeval tm = ;

fd_set rdfds;

if ( !dst_ip )

sd = socket(pf_inet, sock_raw, ipproto_icmp);

if ( sd < 0 )

dst_addr.sin_family = af_inet;

dst_addr.sin_addr.s_addr = inet_addr(dst_ip);

ret = gen_icmp_packet(&icmp_packet, 8, 1);

if ( success != ret )

ret = sendto(sd, &icmp_packet, sizeof(struct icmp), 0,

(struct sockaddr *)&dst_addr, sizeof(struct sockaddr_in));

if ( ret < 0 )

printf("send ping sucess!\n");

/* timeout 1s to recv icmp */

fd_zero(&rdfds);

fd_set(sd, &rdfds);

ret = select(sd + 1, &rdfds, null, null, &tm);

if ( -1 == ret && eintr != errno )

else if ( 0 == ret )

if ( fd_isset(sd, &rdfds) )

ip = (struct ip *)buf;

printf("from: %s\n", inet_ntoa(ip->ip_src));

printf(" to: %s\n", inet_ntoa(ip->ip_dst));

}ret = success;

_e2:

close_sock(sd);

_e1:

return ret;

}

以上該注意的是並不是傳送出icmp echo request 就結束了,別忘了需求是獲取出口位址;

所以,又結合 select + recv 的方式,超時1秒去等待 icmp echo reply,然後再獲取出口位址;

由於未使用「setsockopt (..., ipproto_ip, ip_hdrincl, ...);」 ,由系統自動填充ip頭,所以我們只需要 gen_icmp_packet 去填充 icmp內容即可;

int gen_icmp_packet(struct icmp *icmp_packet, int type, int seq)

icmp_packet->icmp_type = type;

icmp_packet->icmp_code = 0;

icmp_packet->icmp_cksum = 0;

icmp_packet->icmp_id = htons(getpid());

icmp_packet->icmp_seq = htons(seq);

gettimeofday((struct timeval *)icmp_packet->icmp_data, null);

icmp_packet->icmp_cksum = api_checksum16((unsigned short *)icmp_packet, sizeof(struct icmp));

return success;

}

同時需要進行crc16的校驗碼

u16 api_checksum16(u16 *buffer, int size)

while ( size > 1 )

if ( size )

printf("2. cksum: 0x%08x\n", cksum);

/* 32 bit change to 16 bit */

while ( cksum >> 16 )

return (u16)(~cksum);

}

所以,乙個基礎的ping命令就完成了。

四、總結

利用icmp可以獲取出口位址,透明**就可以針對目的位址進行乙個出口位址的快取。

若icmp不可達,也不一定表示icmp request 未送達目的,是存在 icmp reply 回不來的可能性的,那麼又如何獲取到出口位址呢?

是否有直接查詢路由表的程式設計方法?

平行思考,nginx upstream的時候,是不是也有類似的選路過程?

Linux 原始套接字

原始套接字可以用來自行組裝ip資料報,然後將資料報傳送到其他終端。必須在管理員許可權下才能使用原始套接字。總結自 unix網路程式設計 卷1 套接字聯網api 1 原始套接字的建立 int sockfd socket af inet,sock raw,ipproto 後面的 可以是icmp,udp,...

linux原始套接字

通常情況下程式設計師接所接觸到的套接字 socket 為兩類 1 流式套接字 sock stream 一種面向連線的 socket,針對於面向連線的tcp 服務應用 2 資料報式套接字 sock dgram 一種無連線的 socket,對應於無連線的 udp 服務應用。從使用者的角度來看,sock ...

使用原始套接字Raw Socket實現資料報嗅探

網路上隨時都流通了大量的資料報,我們要想實現抓包並分析,實現思路思路大概是 在合適的時候捕獲資料報,儲存到緩衝區,作為備用 然後,按照一定的結構和格式去讀取緩衝區的內容。由於各種公開的網路協議是已知的,所以對於資料報的分析就比較簡單。通常我們都是使用類似wireshark的抓包軟體嗅探資料報,這些抓...