traceroute程式剖析
traceroute允許我們確定IP資料報從本地主機遊歷到某個遠端主機所經過的路徑。
我們先來說明tranceroute的工作原理:是IP路由過程中對資料包TTL(Time to Live,存活時間)的處理。當路由器收到一個IP包時,會修改IP包的TTL(及由此造成的頭部檢測和checksum變化)。每收到一個包,檢查這個的TTL是否是0或1.如果是,表明這個包還沒有到達目的地,而且剩餘時間不多了,肯定是到不了目的地了。這樣路由器就簡單地丟棄這個包,並給源主機發送ICMP通知,說明這個包已經過時了。ICMP的通知資訊裡包含當前路由器傳送時所用的IP。
這樣就可以通過構造資料包,來間接檢查到達一個主機時經過了哪些路由器。一開始傳送一個TTL為1的包,這樣到達第一個路由器的時侯就已經超時了,第一個路由器就發通知說包超時,這樣就可以記錄下所經過的第一個路由器所的IP。然後TTL加1,安全通過第一個路由器,而第二個路由器的處理與第一個相同,丟包,發通知說包超時了。這樣記錄下的第二個路由器IP,由此可以一直下去,直到這個資料包到達目標主機,由此列印所有經過的路由器。
在通訊中,IP層只負責資料的路由與傳輸,並不處理資料包的內容。例如ICMP,或TCP,UDP,這些協議是依賴IP層的傳輸功能來傳輸資料的。在通訊雙方的主機中,收到這些協議的資料包後,一般在通訊的對應主機上,會有程式來處理這些資料。因此traceroute程式傳送一個UDP包來試探。對路由器來說,UDP資料報只是IP資料報的一種,它並不關心UDP資料報的具體內容。直到這個包到達目的埠的主機,目的主機的核心會解析UDP資料報,並查詢資料報中要求的埠是否已經有程序在使用。如果找到,則通知程序有資料到達。而如果找不到,則傳送一個“目的埠不可達”的ICMP錯誤資料回到源主機。
這樣就可以完全確定下來。trcertroute建立一個UDP資料包,不斷修改TTL值併發送出去,如果收到"超時錯",表示剛剛到達的是路由器,而如果收到的是"埠不可達"錯誤,表示剛剛到達的就是目的主機。這樣路由跟蹤完成,程式結束。
下面給出部分原始碼(主要針對IPv4):
trace.h:
#include "unp.h" #include <netinet/in_systm.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <netinet/udp.h> #define BUFSIZE 1500 struct rec { /* format of outgoing UDP data */ u_short rec_seq; /* sequence number */ u_short rec_ttl; /* TTL packet left with */ struct timeval rec_tv; /* time packet left */ }; /* globals */ char recvbuf[BUFSIZE]; char sendbuf[BUFSIZE]; int datalen; /* # bytes of data following ICMP header */ char *host; u_short sport, dport; int nsent; /* add 1 for each sendto() */ pid_t pid; /* our PID */ int probe, nprobes; int sendfd, recvfd; /* send on UDP sock, read on raw ICMP sock */ int ttl, max_ttl; int verbose; /* function prototypes */ const char *icmpcode_v4(int); int recv_v4(int, struct timeval *); void sig_alrm(int); void traceloop(void); void tv_sub(struct timeval *, struct timeval *); struct proto { const char *(*icmpcode)(int); int (*recv)(int, struct timeval *); struct sockaddr *sasend; /* sockaddr{} for send, from getaddrinfo */ struct sockaddr *sarecv; /* sockaddr{} for receiving */ struct sockaddr *salast; /* last sockaddr{} for receiving */ struct sockaddr *sabind; /* sockaddr{} for binding source port */ socklen_t salen; /* length of sockaddr{}s */ int icmpproto; /* IPPROTO_xxx value for ICMP */ int ttllevel; /* setsockopt() level to set TTL */ int ttloptname; /* setsockopt() name to set TTL */ } *pr; #ifdef IPV6 #include <netinet/ip6.h> #include <netinet/icmp6.h> #endif
main.c:
#include "trace.h"
struct proto proto_v4 = { icmpcode_v4, recv_v4, NULL, NULL, NULL, NULL, 0,
IPPROTO_ICMP, IPPROTO_IP, IP_TTL };
int datalen = sizeof(struct rec); /* defaults */
int max_ttl = 30;
int nprobes = 3;
u_short dport = 32768 + 666;
int
main(int argc, char **argv)
{
int c;
struct addrinfo *ai;
char *h;
opterr = 0; /* don't want getopt() writing to stderr */
//對命令列引數的處理
while ( (c = getopt(argc, argv, "m:v")) != -1) {
switch (c) {
case 'm':
if ( (max_ttl = atoi(optarg)) <= 1)
err_quit("invalid -m value");
break;
case 'v':
verbose++;
break;
case '?':
err_quit("unrecognized option: %c", c);
}
}
if (optind != argc-1)
err_quit("usage: traceroute [ -m <maxttl> -v ] <hostname>");
host = argv[optind];
pid = getpid();
Signal(SIGALRM, sig_alrm);
//處理目的主機或ip,返回一個指向addrinfo結構體的指標
ai = Host_serv(host, NULL, 0, 0);
h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen);
printf("traceroute to %s (%s): %d hops max, %d data bytes\n",
ai->ai_canonname ? ai->ai_canonname : h,
h, max_ttl, datalen);
/* initialize according to protocol */
if (ai->ai_family == AF_INET) {
//當協議為IPv4時,初始化協議結構體
pr = &proto_v4;
} else
err_quit("unknown address family %d", ai->ai_family);
pr->sasend = ai->ai_addr; /* contains destination address */
pr->sarecv = Calloc(1, ai->ai_addrlen);
pr->salast = Calloc(1, ai->ai_addrlen);
pr->sabind = Calloc(1, ai->ai_addrlen);
pr->salen = ai->ai_addrlen;
traceloop();
exit(0);
}
traceloop.c:
#include "trace.h"
void
traceloop(void)
{
int seq, code, done;
double rtt;
struct rec *rec;
struct timeval tvrecv;
//建立原始套接字
recvfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);
setuid(getuid()); /* don't need special permissions anymore */
#ifdef IPV6
if (pr->sasend->sa_family == AF_INET6 && verbose == 0) {
struct icmp6_filter myfilt;
ICMP6_FILTER_SETBLOCKALL(&myfilt);
ICMP6_FILTER_SETPASS(ICMP6_TIME_EXCEEDED, &myfilt);
ICMP6_FILTER_SETPASS(ICMP6_DST_UNREACH, &myfilt);
setsockopt(recvfd, IPPROTO_IPV6, ICMP6_FILTER,
&myfilt, sizeof(myfilt));
}
#endif
//建立資料報套接字
sendfd = Socket(pr->sasend->sa_family, SOCK_DGRAM, 0);
pr->sabind->sa_family = pr->sasend->sa_family;
sport = (getpid() & 0xffff) | 0x8000; /* our source UDP port # */
//設定埠
sock_set_port(pr->sabind, pr->salen, htons(sport));
//監聽udp資料報的套接字
Bind(sendfd, pr->sabind, pr->salen);
sig_alrm(SIGALRM);
seq = 0;
done = 0;
for (ttl = 1; ttl <= max_ttl && done == 0; ttl++) {
//進入迴圈之後每次首先設定生存時間,pr->ttllevel = IPPROTO_IP
//pr->ttloptname = IP_TTL
Setsockopt(sendfd, pr->ttllevel, pr->ttloptname, &ttl, sizeof(int));
//清空pr->salast指向的套接字結構
bzero(pr->salast, pr->salen);
printf("%2d ", ttl);
fflush(stdout);
//nprobes = 3
for (probe = 0; probe < nprobes; probe++) {
rec = (struct rec *) sendbuf;
//設定序列號
rec->rec_seq = ++seq;
//設定跳數
rec->rec_ttl = ttl;
//獲取當前時間
Gettimeofday(&rec->rec_tv, NULL);
//設定傳送套接字的埠
sock_set_port(pr->sasend, pr->salen, htons(dport + seq));
//傳送資料
Sendto(sendfd, sendbuf, datalen, 0, pr->sasend, pr->salen);
if ( (code = (*pr->recv)(seq, &tvrecv)) == -3)
printf(" *"); /* timeout, no reply */
else {
char str[NI_MAXHOST];
//如果傳送應答icmp的節點ip地址發生變化,顯示應答傳送主機的主機名和ip地址
if (sock_cmp_addr(pr->sarecv, pr->salast, pr->salen) != 0) {
if (getnameinfo(pr->sarecv, pr->salen, str, sizeof(str),
NULL, 0, 0) == 0)
printf(" %s (%s)", str,
Sock_ntop_host(pr->sarecv, pr->salen));
else
printf(" %s",
Sock_ntop_host(pr->sarecv, pr->salen));
memcpy(pr->salast, pr->sarecv, pr->salen);
}
//計算往返時間
tv_sub(&tvrecv, &rec->rec_tv);
rtt = tvrecv.tv_sec * 1000.0 + tvrecv.tv_usec / 1000.0;
printf(" %.3f ms", rtt);
//顯示ICMP程式碼值
if (code == -1) /* port unreachable; at destination */
done++;
else if (code >= 0)
printf(" (ICMP %s)", (*pr->icmpcode)(code));
}
fflush(stdout);
}
printf("\n");
}
}
recv_v4.c:
#include "trace.h"
extern int gotalarm;
/*
* Return: -3 on timeout
* -2 on ICMP time exceeded in transit (caller keeps going)
* -1 on ICMP port unreachable (caller is done)
* >= 0 return value is some other ICMP unreachable code
*/
int
recv_v4(int seq, struct timeval *tv)
{
int hlen1, hlen2, icmplen, ret;
socklen_t len;
ssize_t n;
struct ip *ip, *hip;
struct icmp *icmp;
struct udphdr *udp;
gotalarm = 0;
alarm(3);
for ( ; ; ) {
if (gotalarm)
return(-3); /* alarm expired */
len = pr->salen;
//接收資料
n = recvfrom(recvfd, recvbuf, sizeof(recvbuf), 0, pr->sarecv, &len);
if (n < 0) {
if (errno == EINTR)
continue;
else
err_sys("recvfrom error");
}
//獲取ip的頭
ip = (struct ip *) recvbuf; /* start of IP header */
//獲取ip報文的長度
hlen1 = ip->ip_hl << 2; /* length of IP header */
//獲取icmp報文的頭部
icmp = (struct icmp *) (recvbuf + hlen1); /* start of ICMP header */
if ( (icmplen = n - hlen1) < 8)
continue; /* not enough to look at ICMP header */
if (icmp->icmp_type == ICMP_TIMXCEED &&
icmp->icmp_code == ICMP_TIMXCEED_INTRANS) {
//第一種情況處理ICMP傳輸中超時錯誤,“time exceeded in transmit”
if (icmplen < 8 + sizeof(struct ip))
continue; /* not enough data to look at inner IP */
//將hip指向在ICMP訊息中返回的IPv4首部,它跟在8位元組的ICMP首部之後
hip = (struct ip *) (recvbuf + hlen1 + 8);
hlen2 = hip->ip_hl << 2;
if (icmplen < 8 + hlen2 + 4)
continue; /* not enough data to look at UDP ports */
//udp指向跟在這個IPv4首部之後的UDP首部
udp = (struct udphdr *) (recvbuf + hlen1 + 8 + hlen2);
//如果ICMP返回的錯誤是由於某個UDP資料報引起的,並且UDP資料報的源埠和目的埠是本程序傳送的值
//那麼它是某個中間路由器的響應我們的探測分組的一個應答
if (hip->ip_p == IPPROTO_UDP &&
udp->source == htons(sport) &&
udp->dest == htons(dport + seq)) {
ret = -2; /* we hit an intermediate router */
//返回-2
break;
}
} else if (icmp->icmp_type == ICMP_UNREACH) {
//當返回ICMP為埠不可達時
if (icmplen < 8 + sizeof(struct ip))
continue; /* not enough data to look at inner IP */
//將hip指向在ICMP訊息中返回的IPv4首部,它跟在8位元組的ICMP首部之後
hip = (struct ip *) (recvbuf + hlen1 + 8);
hlen2 = hip->ip_hl << 2;
if (icmplen < 8 + hlen2 + 4)
continue; /* not enough data to look at UDP ports */
//udp指向跟在這個IPv4首部之後的UDP首部
udp = (struct udphdr *) (recvbuf + hlen1 + 8 + hlen2);
if (hip->ip_p == IPPROTO_UDP &&
//如果ICMP返回的錯誤是由於某個UDP資料報引起的,並且UDP資料報的源埠和目的埠是本程序傳送的值
//那麼它是某個中間路由器的響應我們的探測分組的一個應答
//
udp->source == htons(sport) &&
udp->dest == htons(dport + seq)) {
if (icmp->icmp_code == ICMP_UNREACH_PORT)
//如果ICMP的程式碼為"port unreachable"
//返回-1,因為其探測分組已經到達最終目的地
ret = -1; /* have reached destination */
else
//否則返回ICMP程式碼值
ret = icmp->icmp_code; /* 0, 1, 2, ... */
break;
}
}
if (verbose) {
printf(" (from %s: type = %d, code = %d)\n",
Sock_ntop_host(pr->sarecv, pr->salen),
icmp->icmp_type, icmp->icmp_code);
}
/* Some other ICMP error, recvfrom() again */
}
alarm(0); /* don't leave alarm running */
Gettimeofday(tv, NULL); /* get time of packet arrival */
return(ret);
}
下面就是一些小的函式:
sock_set_port.c:
#include "unp.h"
void
sock_set_port(struct sockaddr *sa, socklen_t salen, int port)
{
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *) sa;
sin->sin_port = port;
return;
}
#ifdef IPV6
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
sin6->sin6_port = port;
return;
}
#endif
}
return;
}
host_serv.c:
/* include host_serv */
#include "unp.h"
struct addrinfo *
host_serv(const char *host, const char *serv, int family, int socktype)
{
int n;
struct addrinfo hints, *res;
//清零
bzero(&hints, sizeof(struct addrinfo));
//用於返回主機的規範名稱
hints.ai_flags = AI_CANONNAME; /* always return canonical name */
//其值為0代表:協議無關
hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6, etc. */
hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */
if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
return(NULL);
return(res); /* return pointer to first on linked list */
}
/* end host_serv */
/*
* There is no easy way to pass back the integer return code from
* getaddrinfo() in the function above, short of adding another argument
* that is a pointer, so the easiest way to provide the wrapper function
* is just to duplicate the simple function as we do here.
*/
struct addrinfo *
Host_serv(const char *host, const char *serv, int family, int socktype)
{
int n;
struct addrinfo hints, *res;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_flags = AI_CANONNAME; /* always return canonical name */
hints.ai_family = family; /* 0, AF_INET, AF_INET6, etc. */
hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */
if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
err_quit("host_serv error for %s, %s: %s",
(host == NULL) ? "(no hostname)" : host,
(serv == NULL) ? "(no service name)" : serv,
gai_strerror(n));
return(res); /* return pointer to first on linked list */
}
sock_set_port.c:
#include "unp.h"
void
sock_set_port(struct sockaddr *sa, socklen_t salen, int port)
{
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *) sa;
sin->sin_port = port;
return;
}
#ifdef IPV6
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
sin6->sin6_port = port;
return;
}
#endif
}
return;
}
sock_ntop.c:
#include "unp.h"
#ifdef HAVE_SOCKADDR_DL_STRUCT
#include <net/if_dl.h>
#endif
/* include sock_ntop */
char *
sock_ntop(const struct sockaddr *sa, socklen_t salen)
{
char portstr[8];
static char str[128]; /* Unix domain is largest */
switch (sa->sa_family) {
//當是IPv4協議時
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *) sa;
//點分十進位制與二進位制的轉化
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return(NULL);
//將埠的網路位元組序轉換為主機位元組序
if (ntohs(sin->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return(str);
}
/* end sock_ntop */
#ifdef IPV6
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
str[0] = '[';
if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
return(NULL);
if (ntohs(sin6->sin6_port) != 0) {
snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
strcat(str, portstr);
return(str);
}
return (str + 1);
}
#endif
#ifdef AF_UNIX
case AF_UNIX: {
struct sockaddr_un *unp = (struct sockaddr_un *) sa;
/* OK to have no pathname bound to the socket: happens on
every connect() unless client calls bind() first. */
if (unp->sun_path[0] == 0)
strcpy(str, "(no pathname bound)");
else
snprintf(str, sizeof(str), "%s", unp->sun_path);
return(str);
}
#endif
#ifdef HAVE_SOCKADDR_DL_STRUCT
case AF_LINK: {
struct sockaddr_dl *sdl = (struct sockaddr_dl *) sa;
if (sdl->sdl_nlen > 0)
snprintf(str, sizeof(str), "%*s (index %d)",
sdl->sdl_nlen, &sdl->sdl_data[0], sdl->sdl_index);
else
snprintf(str, sizeof(str), "AF_LINK, index=%d", sdl->sdl_index);
return(str);
}
#endif
default:
snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d, len %d",
sa->sa_family, salen);
return(str);
}
return (NULL);
}
char *
Sock_ntop(const struct sockaddr *sa, socklen_t salen)
{
char *ptr;
if ( (ptr = sock_ntop(sa, salen)) == NULL)
err_sys("sock_ntop error"); /* inet_ntop() sets errno */
return(ptr);
}
sock_cmp_addr.c:
#include "unp.h"
#ifdef HAVE_SOCKADDR_DL_STRUCT
#include <net/if_dl.h>
#endif
int
sock_cmp_addr(const struct sockaddr *sa1, const struct sockaddr *sa2,
socklen_t salen)
{
if (sa1->sa_family != sa2->sa_family)
return(-1);
switch (sa1->sa_family) {
case AF_INET: {
return(memcmp( &((struct sockaddr_in *) sa1)->sin_addr,
&((struct sockaddr_in *) sa2)->sin_addr,
sizeof(struct in_addr)));
}
#ifdef IPV6
case AF_INET6: {
return(memcmp( &((struct sockaddr_in6 *) sa1)->sin6_addr,
&((struct sockaddr_in6 *) sa2)->sin6_addr,
sizeof(struct in6_addr)));
}
#endif
#ifdef AF_UNIX
case AF_UNIX: {
return(strcmp( ((struct sockaddr_un *) sa1)->sun_path,
((struct sockaddr_un *) sa2)->sun_path));
}
#endif
#ifdef HAVE_SOCKADDR_DL_STRUCT
case AF_LINK: {
return(-1); /* no idea what to compare here ? */
}
#endif
}
return (-1);
}
sig_alrm.c:
#include "trace.h"
int gotalarm;
void
sig_alrm(int signo)
{
gotalarm = 1; /* set flag to note that alarm occurred */
return; /* and interrupt the recvfrom() */
}
tv_sub.c:
#include "unp.h"
void
tv_sub(struct timeval *out, struct timeval *in)
{
if ( (out->tv_usec -= in->tv_usec) < 0) { /* out -= in */
--out->tv_sec;
out->tv_usec += 1000000;
}
out->tv_sec -= in->tv_sec;
}
icmpcode_v4.c:
#include "trace.h"
const char *
icmpcode_v4(int code)
{
static char errbuf[100];
switch (code) {
case 0: return("network unreachable");
case 1: return("host unreachable");
case 2: return("protocol unreachable");
case 3: return("port unreachable");
case 4: return("fragmentation required but DF bit set");
case 5: return("source route failed");
case 6: return("destination network unknown");
case 7: return("destination host unknown");
case 8: return("source host isolated (obsolete)");
case 9: return("destination network administratively prohibited");
case 10: return("destination host administratively prohibited");
case 11: return("network unreachable for TOS");
case 12: return("host unreachable for TOS");
case 13: return("communication administratively prohibited by filtering");
case 14: return("host recedence violation");
case 15: return("precedence cutoff in effect");
default: sprintf(errbuf, "[unknown code %d]", code);
return errbuf;
}
}
首先我們來看程式的執行結果:
上圖分別展示了自己的traceroute程式和系統的traceroute程式的執行結果。。。。