概述
基於字節流套接字(SOCK_STREAM)和數據報套接字(SOCK_DGRAM)不可以訪問傳輸層協議,只是對應用層的報文進行操作,傳輸層的數據報格式都是由系統提供的協議棧實現,用戶只需要填充相應的應用層報文,由系統完成底層報文首部的填充併發送。原始套接字(SOCK_RAW)可以訪問位於基層的傳輸層協議,原始套接字沒有端口號。
原始套接字(SOCK_RAW)是一種不同於 SOCK_STREAM、SOCK_DGRAM 的套接字,它實現於系統核心。原始套接字使進程可以讀與寫 ICMP、IGMP 等網絡報文;也可以處理特殊的 IPv4 報文;進程還可以通過設置 IP_HDRINCL 套接字選項由用戶自行構造 IP 首部。原始套接字可以用來自行組裝 IP 數據報,然後將數據報發送到其他終端。但是隻有管理員權限才能使用原始套接字,可防止普通用戶往網絡寫入它們自行構造的 IP 數據報。
原始套接字創建
調用 socket 函數創建套接字時,指定套接字類型爲 SOCK_RAW 以創建一個原始套接字。
int sockfd;
/* 創建一個 IPv4 的原始套接字 */
sockfd = socket(AF_INET, SOCK_RAW, protocol);
創建原始套接字之後,可以選擇是否開啓 IP_HDRINCL 套接字選項。
const int on = 1;
if(setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0)
/* 接下來是一些錯誤處理程序 */
原始套接字輸出
原始套接字的輸出遵循以下規則:
- 若套接字已經連接,則可以調用 write、writev 或 send 函數輸出,否則,普通輸出只能調用 sendto 或 sendmsg 函數並指定目的 IP 地址完成輸出;
- 進程讓內核所發送數據的起始地址:
- 若沒有開啓 P_HDRINCL 套接字選項,則起始地址是 IP 首部之後的第一個字節,因爲此時,IP 首部由內核構造,並把它放在來自進程的數據之前;
- 若開啓 P_HDRINCL 套接字選項,則起始地址是 IP 首部的第一個字節,因爲此時,IP 首部由進程構造,所以進程數據包含 IP 首部;
- 內核會對超出外出接口 MTU(最大傳輸單元)的原始分組進行分片;
原始套接字輸入
原始套接字遵循以下規則:
- 接收到的 UDP 分組和 TCP 分組絕不傳遞到任何原始套接字;
- 大多數的 ICMP 分組在內核處理完其中的 ICMP 消息後傳遞到原始套接字;
- 所有的 IGMP 分組在內核處理完其中的 IGMP 消息後傳遞到原始套接字;
- 內核無法識別的協議字段的所有 IP 數據報傳遞到原始套接字;
- 在數據報的所有分片到達之前,不傳遞任何分片到原始套接字;
內核在傳遞 IP 數據報到原始套接字之前,必須對所有進程上的所有原始套接字進行匹配檢測,若匹配成功,才把 IP 數據報的副本傳遞到匹配的原始套接字。檢測匹配步驟如下:
- 創建原始套接字時 socket 函數的第三個參數必須指定非 0 值;
- 若原始套接字 bind 綁定到某個本地 IP 地址,則該本地 IP 地址必須與 IP 數據報的目的 IP 地址匹配;
- 若原始套接字已由 connect 調用指定某個外地 IP 地址,則該外地 IP 地址必須與 IP 數據報的源 IP 地址匹配;
Ping 程序
ping 程序的操作比較簡單,當源主機向目標主機發送了 ICMP 回顯請求數據報後,它期待着目標主機的回答。目標主機在收到一個 ICMP 回顯請求數據報後,它會交換源、目的主機的地址,然後將收到的 ICMP 回顯請求數據報中的數據部分原封不動地封裝在自己的 ICMP 回顯應答數據報中,然後發回給發送 ICMP 回顯請求的一方。如果校驗正確,發送者便認爲目標主機的回顯服務正常,也即物理連接暢通。
ping 程序編程需要用到 ICMP 協議,有關 ICMP 協議的知識可以參考前面的文章《ICMP 協議》。ping 命令只使用衆多 ICMP 報文中的兩種:"請求(ICMP_ECHO)"和"迴應(ICMP_ECHOREPLY)",這兩種 ICMP 報文格式如下圖所示:
首先看下系統自帶 ping 程序的輸出:
$ ping www.github.com
PING github.com (192.30.252.128) 56(84) bytes of data.
64 bytes from github.com (192.30.252.128): icmp_req=1 ttl=45 time=269 ms
64 bytes from github.com (192.30.252.128): icmp_req=2 ttl=45 time=274 ms
64 bytes from github.com (192.30.252.128): icmp_req=3 ttl=45 time=270 ms
64 bytes from github.com (192.30.252.128): icmp_req=4 ttl=45 time=281 ms
64 bytes from github.com (192.30.252.128): icmp_req=5 ttl=45 time=283 ms
64 bytes from github.com (192.30.252.128): icmp_req=6 ttl=45 time=249 ms
64 bytes from github.com (192.30.252.128): icmp_req=7 ttl=45 time=253 ms
^C
--- github.com ping statistics ---
7 packets transmitted, 7 received, 0% packet loss, time 6006ms
rtt min/avg/max/mdev = 249.472/269.010/283.945/12.186 ms
ping 程序的編程步驟:
1) 創建類型爲 SOCK_RAW 的原始套接字,同時設定協議爲 IPPROTO_ICMP;
2) 創建並初始化 ICMP 首部;
3) 調用 sendto 函數,將 ICMP 請求發給遠程主機;
4) 調用 recvform函數,以接收任何 ICMP 響應;
Linux 中<netinet/ip_icmp.h> ICMP 的數據結構定義如下:
struct icmp
{
u_int8_t icmp_type; /* type of message, see below */
u_int8_t icmp_code; /* type sub code */
u_int16_t icmp_cksum; /* ones complement checksum of struct */
union
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* gateway address */
struct ih_idseq /* echo datagram */
{
u_int16_t icd_id;
u_int16_t icd_seq;
} ih_idseq;
u_int32_t ih_void;
/* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
struct ih_pmtu
{
u_int16_t ipm_void;
u_int16_t ipm_nextmtu;
} ih_pmtu;
struct ih_rtradv
{
u_int8_t irt_num_addrs;
u_int8_t irt_wpa;
u_int16_t irt_lifetime;
} ih_rtradv;
} icmp_hun;
#define icmp_pptr icmp_hun.ih_pptr
#define icmp_gwaddr icmp_hun.ih_gwaddr
#define icmp_id icmp_hun.ih_idseq.icd_id
#define icmp_seq icmp_hun.ih_idseq.icd_seq
#define icmp_void icmp_hun.ih_void
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
#define icmp_nextmtu icmp_hun.ih_pmtu.ipm_nextmtu
#define icmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs
#define icmp_wpa icmp_hun.ih_rtradv.irt_wpa
#define icmp_lifetime icmp_hun.ih_rtradv.irt_lifetime
union
{
struct
{
u_int32_t its_otime;
u_int32_t its_rtime;
u_int32_t its_ttime;
} id_ts;
struct
{
struct ip idi_ip;
/* options and then 64 bits of data */
} id_ip;
struct icmp_ra_addr id_radv;
u_int32_t id_mask;
u_int8_t id_data[1];
} icmp_dun;
#define icmp_otime icmp_dun.id_ts.its_otime
#define icmp_rtime icmp_dun.id_ts.its_rtime
#define icmp_ttime icmp_dun.id_ts.its_ttime
#define icmp_ip icmp_dun.id_ip.idi_ip
#define icmp_radv icmp_dun.id_radv
#define icmp_mask icmp_dun.id_mask
#define icmp_data icmp_dun.id_data
};
構成 ping 程序的各個函數及其關係如下圖所示:
首先定義一個頭文件:
#ifndef PING_H
#define PING_H
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <sys/socket.h>
#include <signal.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/types.h>
#define BUFSIZE 4096
char sendbuf[BUFSIZE];
extern int datalen; /* # bytes of data following ICMP header */
char *host;
int nsent; /* add 1 for each sendto() */
int nrecv; /* add 1 for each recvmsg() */
pid_t pid; /* our PID */
int sockfd;
int verbose;
/* function prototypes */
void init_v6(void);
void proc_v4(char *, ssize_t, struct msghdr *, struct timeval *);
void proc_v6(char *, ssize_t, struct msghdr *, struct timeval *);
void send_v4(void);
void send_v6(void);
void readloop(void);
void sig_alrm(int);
void tv_sub(struct timeval *, struct timeval *);
/* 這個結構主要是爲了處理IPv4與IPv6之間的差異 */
struct proto
{
/* 3個函數指針 */
void (*fproc)(char *, ssize_t, struct msghdr *, struct timeval *);
void (*fsend)(void);
void (*finit)(void);
/* 2個套接字地址結構指針 */
struct sockaddr *sasend; /* sockaddr{} for send, from getaddrinfo */
struct sockaddr *sarecv; /* sockaddr for receiving */
socklen_t salen; /* length of sockaddr{}s */
/* ICMP 協議值 */
int icmpprot; /* IPPROTO_xxx value for ICMP */
} *pr;
#ifdef IPV6
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#endif
#endif
接着看下主函數:
#include "ping.h"
/* 初始化IPv4結構 */
struct proto proto_v4 = {proc_v4, send_v4, NULL, NULL, NULL, 0, IPPROTO_ICMP};
#ifdef IPV6
/* 若存在IPv6,則初始化IPv6結構 */
struct proto proto_v6 = {proc_v6, send_v6, init_v6, NULL, NULL, 0, IPPROTO_ICMPV6};
#endif
typedef void Sigfunc(int);
extern int datalen = 56; /* data that goes with ICMP echo request */
extern Sigfunc *MySignal(int signo, Sigfunc *func);
extern struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype);
extern char *Sock_ntop_host(const struct sockaddr *sa, socklen_t salen);
extern void *Calloc(size_t n, size_t size);
void statistics(int signo);
int main(int argc, char **argv)
{
int n;
struct addrinfo *ai;
char *h;
opterr = 0; /* don't want getopt() writing to stderr */
/* 只實現ping的一個參數選項-v供查詢 */
/* 有關getopt函數的使用可以查閱相關資料 */
while( (n = getopt(argc, argv, "v")) != -1)
{
switch(n)
{
case 'v':
verbose++;
break;
case '?':
printf("unrecognize option: %c\n", n);
exit(1);
}
}
if(optind != argc-1)
{
perror("usage: ping [ -v ] <hostname>");
exit(1);
}
host = argv[optind];
pid = getpid() & 0xffff; /* ICMP ID field is 16 bits */
MySignal(SIGALRM, sig_alrm);
MySignal(SIGINT, statistics);
/* 將主機名和服務名映射到一個地址,並返回指向addrinfo的指針 */
ai = host_serv(host, NULL, 0, 0);
/* 將網絡字節序的地址轉換爲字符串格式地址,並返回該字符串的指針 */
h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen);
/* 顯示PING的主機名、地址與數據字節數 */
printf("PING %s (%s) %d bytes of data.\n", ai->ai_canonname ? ai->ai_canonname : h, h, datalen);
/* initialize according to protocol */
if(ai->ai_family == AF_INET)
{
pr = &proto_v4;/* proto結構指針pr指向對應域的結構,這裏是IPv4域的結構 */
#ifdef IPV6
}else if(ai->family == AF_INET6)
{
pr = &proc_v6;
if(IN6_IS_ADDR_V4MAPPED(&(((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr)))
{
perror("connot ping IPv4-mapped IPv6 address");
exit(1);
}
#endif
}else
{
printf("unknown address family %d", ai->ai_family);
exit(1);
}
pr->sasend = ai->ai_addr;/* 發送地址賦值 */
pr->sarecv = (struct sockaddr *)Calloc(1, ai->ai_addrlen);
pr->salen = ai->ai_addrlen;/* 地址的大小 */
/* 處理數據 */
readloop();
exit(0);
}
/* 顯示發送和接收數據報的個數,並計算丟包率 */
void statistics(int signo)
{
printf("\n----------- %s ping statistics -----------\n", Sock_ntop_host(pr->sarecv, pr->salen));
int lost = 100*(nsent-nrecv)/nsent;
printf("%d packets transmitted, %d received, %d packet lost\n", nsent, nrecv, lost);
close(sockfd);
exit(1);
}
數據處理函數
#include "ping.h"
void readloop()
{
int size;
char recvbuf[BUFSIZE];
char controlbuf[BUFSIZE];
struct msghdr msg;
struct iovec iov;
ssize_t n;
struct timeval tval;
/* 創建ICMP的原始套接字,必須是root權限 */
if( (sockfd = socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpprot)) < 0)
{
perror("socket error");
exit(1);
}
/* 回收root權限,設置當前用戶權限 */
setuid(getuid());
/* 初始化IPv6 */
if(pr->finit)
(*pr->finit)();
size = 60 * 1024;
/* 設置接收緩衝區的大小爲60k,主要爲了減小接收緩衝區溢出 */
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
/* 發送第一個數據包 */
sig_alrm(SIGALRM);
/* 初始化接收緩衝區 */
iov.iov_base = recvbuf;
iov.iov_len = sizeof(recvbuf);
msg.msg_name = pr->sarecv;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = controlbuf;
for( ; ;)
{
/* 接收ICMP數據包 */
msg.msg_namelen = pr->salen;
msg.msg_controllen = sizeof(controlbuf);
/* 從套接字接收數據 */
n = recvmsg(sockfd, &msg, 0);
if(n < 0)
{
if(errno == EINTR)
continue;
else
{
perror("recvmsg error");
exit(1);
}
}
/* 記錄接收時間 */
gettimeofday(&tval, NULL);
/* 調用處理函數 */
(*pr->fproc)(recvbuf, n, &msg, &tval);
}
}
發送回顯請求數據報:
#include "ping.h"
/* 在IPv4域中發送數據包 */
extern uint16_t in_cksum(uint16_t *addr, int len);
void
send_v4(void)
{
int len;
struct icmp *icmp;
/* 設置ICMP報頭 */
icmp = (struct icmp *) sendbuf;
icmp->icmp_type = ICMP_ECHO;/* 回顯請求 */
icmp->icmp_code = 0;
icmp->icmp_id = pid;
icmp->icmp_seq = nsent++;
memset(icmp->icmp_data, 0xa5, datalen); /* fill with pattern */
gettimeofday((struct timeval *) icmp->icmp_data, NULL);/* 記錄發送時間 */
len = 8 + datalen; /* checksum ICMP header and data */
icmp->icmp_cksum = 0;
/* 檢驗和算法 */
icmp->icmp_cksum = in_cksum((u_short *) icmp, len);
/* 發送數據包 */
if( len != sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen))
{
perror("sendto error");
exit(1);
}
}
接收數據,並顯示回顯應答數據報:
#include "ping.h"
extern char *Sock_ntop_host(const struct sockaddr *sa, socklen_t salen);
void
proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv)
{
int hlen1, icmplen;
double rtt;
struct ip *ip;
struct icmp *icmp;
struct timeval *tvsend;
ip = (struct ip *) ptr; /* start of IP header */
/* IP報文首部長度,即IP報文首部的長度標誌乘以4 */
hlen1 = ip->ip_hl << 2; /* length of IP header */
if (ip->ip_p != IPPROTO_ICMP)
return; /* not ICMP */
/* 越過IP報頭,指向ICMP報頭 */
icmp = (struct icmp *) (ptr + hlen1); /* start of ICMP header */
/* ICMP報頭及ICMP數據報的總長度,若小於8,則不合理 */
if ( (icmplen = len - hlen1) < 8)
return; /* malformed packet */
/* 確保所有接收的數據報是ICMP回顯應答 */
if (icmp->icmp_type == ICMP_ECHOREPLY) {
if (icmp->icmp_id != pid)
return; /* not a response to our ECHO_REQUEST */
if (icmplen < 16)
return; /* not enough data to use */
tvsend = (struct timeval *) icmp->icmp_data;
/* 計算接收和發送的時間差 */
tv_sub(tvrecv, tvsend);
/* 以毫秒爲單位計算rtt */
rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;
/* 打印相關信息 */
printf("%d bytes from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n",
icmplen, Sock_ntop_host(pr->sarecv, pr->salen),
icmp->icmp_seq, ip->ip_ttl, rtt);
nrecv++;
} else if (verbose) {
printf(" %d bytes from %s: icmp_type = %d, icmp_code = %d\n",
icmplen, Sock_ntop_host(pr->sarecv, pr->salen),
icmp->icmp_type, icmp->icmp_code);
}
}
計算檢驗和函數:
#include <stdint.h>
/* 檢驗和算法 */
uint16_t
in_cksum(uint16_t *addr, int len)
{
int nleft = len;
uint32_t sum = 0;
uint16_t *w = addr;
uint16_t answer = 0;
/*
* Our algorithm is simple, using a 32 bit accumulator (sum), we add
* sequential 16 bit words to it, and at the end, fold back all the
* carry bits from the top 16 bits into the lower 16 bits.
*/
/* 把ICMP報頭二進制數據以2字節爲單位進行累加 */
while (nleft > 1) {
sum += *w++;
nleft -= 2;
}
/* 4mop up an odd byte, if necessary */
if (nleft == 1) {/* 若ICMP報頭爲奇數個字節,把最後一個字節視爲2字節數據的高字節,則低字節爲0,繼續累加 */
*(unsigned char *)(&answer) = *(unsigned char *)w ;
sum += answer;
}
/* 4add back carry outs from top 16 bits to low 16 bits */
sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
sum += (sum >> 16); /* add carry */
answer = ~sum; /* truncate to 16 bits */
return(answer);
}
信號處理函數:
#include "ping.h"
/* 發送數據包,並設置鬧鐘,一秒鐘後給所在進程發送SIGALRM信號 */
void
sig_alrm(int signo)
{
(*pr->fsend)();
alarm(1);
return;
}
編譯步驟:
sudo make
sudo chmod u+s Ping
測試:
$ ./Ping www.github.com
PING github.com (192.30.252.129) 56 bytes of data.
64 bytes from 192.30.252.129: icmp_seq=0 ttl=45 rtt=303.057 ms
64 bytes from 192.30.252.129: icmp_seq=1 ttl=45 rtt=301.416 ms
64 bytes from 192.30.252.129: icmp_seq=2 ttl=45 rtt=301.614 ms
64 bytes from 192.30.252.129: icmp_seq=3 ttl=45 rtt=301.727 ms
64 bytes from 192.30.252.129: icmp_seq=4 ttl=45 rtt=308.911 ms
64 bytes from 192.30.252.129: icmp_seq=5 ttl=45 rtt=303.088 ms
64 bytes from 192.30.252.129: icmp_seq=6 ttl=45 rtt=305.763 ms
^C
----------- 192.30.252.129 ping statistics -----------
7 packets transmitted, 7 received, 0 packet lost
注:這裏只是給出 IPv4 實現的 ping 程序,完整程序可以到我的github下載,下載地址《Ping完整程序》。 參考資料:
《Unix 網絡編程》