《網絡編程》原始套接字 ---ping程序實現

概述

        基於字節流套接字(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)
/* 接下來是一些錯誤處理程序 */

原始套接字輸出

原始套接字的輸出遵循以下規則:

  1. 若套接字已經連接,則可以調用 write、writev 或 send 函數輸出,否則,普通輸出只能調用 sendto 或 sendmsg 函數並指定目的 IP 地址完成輸出;
  2. 進程讓內核所發送數據的起始地址:
    • 沒有開啓 P_HDRINCL 套接字選項,則起始地址是 IP 首部之後的第一個字節,因爲此時,IP 首部由內核構造,並把它放在來自進程的數據之前;
    • 開啓 P_HDRINCL 套接字選項,則起始地址是 IP 首部的第一個字節,因爲此時,IP 首部由進程構造,所以進程數據包含 IP 首部;
  3. 內核會對超出外出接口 MTU(最大傳輸單元)的原始分組進行分片;

原始套接字輸入

原始套接字遵循以下規則:

  1. 接收到的 UDP 分組和 TCP 分組絕不傳遞到任何原始套接字;
  2. 大多數的 ICMP 分組在內核處理完其中的 ICMP 消息後傳遞到原始套接字;
  3. 所有的 IGMP 分組在內核處理完其中的 IGMP 消息後傳遞到原始套接字;
  4. 內核無法識別的協議字段的所有 IP 數據報傳遞到原始套接字;
  5. 在數據報的所有分片到達之前,不傳遞任何分片到原始套接字;

         內核在傳遞 IP 數據報到原始套接字之前,必須對所有進程上的所有原始套接字進行匹配檢測,若匹配成功,才把 IP 數據報的副本傳遞到匹配的原始套接字。檢測匹配步驟如下:

  1. 創建原始套接字時 socket 函數的第三個參數必須指定非 0 值;
  2. 若原始套接字 bind 綁定到某個本地 IP 地址,則該本地 IP 地址必須與 IP 數據報的目的 IP 地址匹配;
  3. 若原始套接字已由 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 網絡編程》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章