]> mj.ucw.cz Git - netgrind.git/blob - netgrind/netgrind.c
Hopefully complete TCP analyser.
[netgrind.git] / netgrind / netgrind.c
1 /*
2  *      Netgrind -- The Network Traffic Analyser
3  *
4  *      (c) 2003 Martin Mares <mj@ucw.cz>
5  *
6  *      This software may be freely distributed and used according to the terms
7  *      of the GNU General Public License.
8  */
9
10 #define LOCAL_DEBUG
11
12 #include "lib/lib.h"
13 #include "lib/heap.h"
14 #include "netgrind/netgrind.h"
15 #include "netgrind/pkt.h"
16
17 #include <stdio.h>
18 #include <stdarg.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <net/ethernet.h>
22 #include <netinet/in.h>
23 #include <netinet/ip.h>
24 #include <netinet/tcp.h>
25
26 #include <pcap.h>
27
28 void die(byte *msg, ...)
29 {
30   va_list args;
31
32   va_start(args, msg);
33   fputs("netgrind: ", stderr);
34   vfprintf(stderr, msg, args);
35   fputs("\n", stderr);
36   exit(1);
37 }
38
39 /*** CHECKSUMMING ***/
40
41 static uns tcpip_calc_checksum(void *data, uns len, uns csum)
42 {
43   byte *x = data;
44
45   while (len >= 2)
46     {
47       csum += (x[0] << 8) | x[1];
48       if (csum & 0xffff0000)
49         {
50           csum &= 0x0000ffff;
51           csum++;
52         }
53       x += 2;
54       len -= 2;
55     }
56   if (len)
57     {
58       csum += x[0];
59       if (csum & 0xffff0000)
60         {
61           csum &= 0x0000ffff;
62           csum++;
63         }
64     }
65   return csum;
66 }
67
68 static inline uns tcpip_verify_checksum(uns csum)
69 {
70   return (csum == 0xffff);
71 }
72
73 /*** FLOW ANALYSIS ***/
74
75 enum close_cause {
76   CAUSE_CLOSE,
77   CAUSE_RESET,
78   CAUSE_TIMEOUT,
79   CAUSE_DOOMSDAY
80 };
81
82 struct flow;
83
84 struct appl_hooks {
85   void (*open)(struct flow *f);
86   void (*input)(struct flow *f, int dir, struct pkt *p);
87   void (*close)(struct flow *f, int cause);
88 };
89
90 static void sink_open(struct flow *f)
91 {
92 }
93
94 static void sink_close(struct flow *f, int cause)
95 {
96 }
97
98 static void sink_input(struct flow *f, int dir, struct pkt *p)
99 {
100   pkt_free(p);
101 }
102
103 struct appl_hooks appl_sink = {
104   .open = sink_open,
105   .input = sink_input,
106   .close = sink_close
107 };
108
109 /*** TCP LAYER ***/
110
111 static struct pkt_stats stat_tcp_in, stat_tcp_invalid, stat_tcp_badsum, stat_tcp_unmatched,
112   stat_tcp_on_closed;
113
114 struct pipe {
115   list queue;                           /* incoming packets */
116   u32 last_acked_seq;                   /* last sequence number for which I sent ACK */
117   u32 syn_or_fin_seq;                   /* sequence number of SYN/FIN I sent */
118   enum {                                /* very simplified TCP state machine */
119     FLOW_IDLE,
120     FLOW_SYN_SENT,                      /* sent SYN, waiting for SYN ACK */
121     FLOW_SYN_SENT_ACK,                  /* sent SYN ACK, waiting for first ACK */
122     FLOW_ESTABLISHED,                   /* established state including waiting for ACK of SYN ACK */
123     FLOW_FIN_SENT,                      /* sent FIN, waiting for its ACK */
124     FLOW_FINISHED                       /* closed, ignoring further packets */
125   } state;
126   struct pkt_stats stat_in;
127 };
128
129 static byte *pipe_state_names[] = { "IDLE", "SYNSENT", "SYNACK", "ESTAB", "FINSENT", "FINISH" };
130
131 struct flow {
132   struct flow *hash_next;
133   u32 saddr, daddr, sport, dport;
134   u32 timeout;
135   uns heap_pos;
136   struct appl_hooks *appl;
137   void *appl_data;
138   struct pipe pipe[2];
139 };
140
141 static uns num_flows, max_flows;
142 static struct flow **flow_hash;
143 static struct flow **flow_heap;
144
145 static uns flow_calc_hash(u32 saddr, u32 daddr, u32 sport, u32 dport)
146 {
147   saddr = (saddr >> 16) | (saddr << 16);
148   daddr = (daddr >>  8) | (daddr << 24);
149   sport <<= 7;
150   dport <<= 21;
151   return (saddr + daddr + sport + dport) % max_flows;
152 }
153
154 #define FLOW_HEAP_LESS(a,b) (a->timeout < b->timeout)
155 #define FLOW_HEAP_SWAP(h,a,b,t) do { t=h[a]; h[a]=h[b]; h[b]=t; h[a]->heap_pos=a; h[b]->heap_pos=b; } while(0)
156
157 static void flow_rehash(void)
158 {
159   uns omax = max_flows;
160   struct flow **ohash = flow_hash;
161
162   if (flow_heap)
163     xfree(flow_heap);
164   if (max_flows)
165     max_flows = nextprime(2*max_flows);
166   else
167     max_flows = 3;
168   DBG("Rehashing to %d buckets\n", max_flows);
169   flow_hash = xmalloc_zero(sizeof(struct flow *) * max_flows);
170   flow_heap = xmalloc_zero(sizeof(struct flow *) * (max_flows+1));
171   num_flows = 0;
172   for (uns i=0; i<omax; i++)
173     {
174       struct flow *f = ohash[i];
175       while (f)
176         {
177           struct flow *n = f->hash_next;
178           uns h = flow_calc_hash(f->saddr, f->daddr, f->sport, f->dport);
179           f->hash_next = flow_hash[h];
180           flow_hash[h] = f;
181           flow_heap[++num_flows] = f;
182           f->heap_pos = num_flows;
183           f = n;
184         }
185     }
186   if (ohash)
187     xfree(ohash);
188   HEAP_INIT(struct flow *, flow_heap, num_flows, FLOW_HEAP_LESS, FLOW_HEAP_SWAP);
189 }
190
191 static struct flow *flow_lookup(u32 saddr, u32 daddr, u32 sport, u32 dport)
192 {
193   uns h = flow_calc_hash(saddr, daddr, sport, dport);
194   for (struct flow *f = flow_hash[h]; f; f=f->hash_next)
195     if (f->saddr == saddr && f->daddr == daddr &&
196         f->sport == sport && f->dport == dport)
197       return f;
198   return NULL;
199 }
200
201 static struct flow *flow_create(u32 saddr, u32 daddr, u32 sport, u32 dport)
202 {
203   if (num_flows >= max_flows)
204     flow_rehash();
205   uns h = flow_calc_hash(saddr, daddr, sport, dport);
206   struct flow *f = xmalloc_zero(sizeof(struct flow));
207   f->saddr = saddr;
208   f->daddr = daddr;
209   f->sport = sport;
210   f->dport = dport;
211   f->timeout = ~0U;
212   f->hash_next = flow_hash[h];
213   flow_hash[h] = f;
214   flow_heap[++num_flows] = f;
215   f->heap_pos = num_flows;
216   return f;
217 }
218
219 static void flow_set_timeout(struct flow *f, u32 when)
220 {
221   f->timeout = when;
222   HEAP_CHANGE(struct flow *, flow_heap, num_flows, FLOW_HEAP_LESS, FLOW_HEAP_SWAP, f->heap_pos);
223 }
224
225 static uns flow_now(struct pkt *p)
226 {
227   return p->timestamp >> 20;
228 }
229
230 static inline int tcp_seq_le(u32 a, u32 b)
231 {
232   return ((b - a) < 0x80000000);
233 }
234
235 static inline int tcp_seq_lt(u32 a, u32 b)
236 {
237   return (a != b && tcp_seq_le(a, b));
238 }
239
240 static void tcp_time_step(uns now)
241 {
242   while (num_flows && flow_heap[1]->timeout <= now)
243     {
244       struct flow *f = flow_heap[1];
245       HEAP_DELMIN(struct flow *, flow_heap, num_flows, FLOW_HEAP_LESS, FLOW_HEAP_SWAP);
246       DBG("TIMEOUT for flow %p(%s/%s)\n", f, pipe_state_names[f->pipe[0].state], pipe_state_names[f->pipe[1].state]);
247       if (f->pipe[0].state != FLOW_FINISHED || f->pipe[1].state != FLOW_FINISHED)
248         f->appl->close(f, (now == ~0U) ? CAUSE_DOOMSDAY : CAUSE_TIMEOUT);
249       uns h = flow_calc_hash(f->saddr, f->daddr, f->sport, f->dport);
250       struct flow **gg = &flow_hash[h];
251       for(;;)
252         {
253           ASSERT(*gg);
254           if (*gg == f)
255             {
256               *gg = f->hash_next;
257               break;
258             }
259           gg = &(*gg)->hash_next;
260         }
261       xfree(f);
262     }
263 }
264
265 static void tcp_enqueue_data(struct pipe *b, struct pkt *p)
266 {
267   struct pkt *q, *prev, *new;
268   u32 last_seq;
269
270   DBG("DATA:");
271   if (tcp_seq_lt(b->last_acked_seq, p->seq) && p->seq - b->last_acked_seq >= 0x40000)
272     {
273       DBG(" OUT OF WINDOW (last-ack=%u)\n", b->last_acked_seq);
274       pkt_free(p);
275       return;
276     }
277   prev = (struct pkt *) &b->queue.head;
278   last_seq = b->last_acked_seq;
279   while (p)
280     {
281       if (tcp_seq_lt(p->seq, last_seq))
282         {
283           if (tcp_seq_le(p->seq + pkt_len(p), last_seq))
284             {
285               DBG(" have\n");
286               pkt_free(p);
287               return;
288             }
289           pkt_pop(p, p->seq + pkt_len(p) - last_seq);
290           p->seq = last_seq;
291           DBG(" clip");
292         }
293       q = list_next(&b->queue, &prev->n);
294       if (q && tcp_seq_le(q->seq, p->seq))
295         {
296           /* next packet starts before us => skip it */
297           prev = q;
298           last_seq = q->seq + pkt_len(q);
299         }
300       else
301         {
302           new = NULL;
303           if (q && tcp_seq_lt(q->seq, p->seq + pkt_len(p)))
304             {
305               /* overlap with next packet => split */
306               DBG(" split");
307               uns keeplen = q->seq - p->seq;
308               uns newlen = pkt_len(p) - keeplen;
309               new = pkt_new(0, newlen);
310               memcpy(pkt_append(new, newlen), pkt_unappend(p, newlen), newlen);
311               new->seq = p->seq + keeplen;
312             }
313           DBG(" insert");
314           list_insert(&p->n, &prev->n);
315           prev = p;
316           last_seq = p->seq + pkt_len(p);
317           p = new;
318         }
319     }
320   DBG("\n");
321 }
322
323 static void tcp_got_packet(struct iphdr *iph, struct pkt *p)
324 {
325   struct tcphdr *tcph;
326   struct {
327     u32 src;
328     u32 dst;
329     byte zero;
330     byte proto;
331     u16 len;
332   } fakehdr;
333   uns now = flow_now(p);
334
335   tcp_time_step(now);
336
337   pkt_account(&stat_tcp_in, p);
338   if (!(tcph = pkt_peek(p, sizeof(*tcph))))
339     goto invalid;
340   uns hdrlen = 4*tcph->doff;
341   if (hdrlen < sizeof(*tcph) || hdrlen > pkt_len(p))
342     goto invalid;
343   fakehdr.src = iph->saddr;
344   fakehdr.dst = iph->daddr;
345   fakehdr.zero = 0;
346   fakehdr.proto = IPPROTO_TCP;
347   fakehdr.len = htons(pkt_len(p));
348   uns sum = tcpip_calc_checksum(&fakehdr, sizeof(fakehdr), 0);
349   sum = tcpip_calc_checksum(p->data, pkt_len(p), sum);
350   if (!tcpip_verify_checksum(sum))
351     {
352       pkt_account(&stat_tcp_badsum, p);
353       goto drop;
354     }
355   /* XXX: Check TCP options? */
356   pkt_pop(p, hdrlen);
357
358   u32 seq = ntohl(tcph->seq);
359   u32 ack = ntohl(tcph->ack_seq);
360   DBG("TCP %08x %08x %04x %04x seq=%u+%u ack=%u%s%s%s%s%s%s\n",
361       ntohl(iph->saddr), ntohl(iph->daddr), ntohs(tcph->source), ntohs(tcph->dest), seq, pkt_len(p), ack,
362       (tcph->fin ? " FIN" : ""),
363       (tcph->syn ? " SYN" : ""),
364       (tcph->rst ? " RST" : ""),
365       (tcph->psh ? " PSH" : ""),
366       (tcph->ack ? " ACK" : ""),
367       (tcph->urg ? " URG" : ""));
368
369   struct flow *f;
370   struct pipe *a, *b;
371   if (f = flow_lookup(iph->saddr, iph->daddr, tcph->source, tcph->dest))
372     {
373       a = &f->pipe[0];
374       b = &f->pipe[1];
375     }
376   else if (f = flow_lookup(iph->daddr, iph->saddr, tcph->dest, tcph->source))
377     {
378       a = &f->pipe[1];
379       b = &f->pipe[0];
380     }
381   else
382     {
383       /* Flow not found, if it's a SYN packet, go create it */
384       if (tcph->syn && !tcph->ack && !tcph->rst && !tcph->fin)
385         {
386           f = flow_create(iph->saddr, iph->daddr, tcph->source, tcph->dest);
387           f->appl = &appl_sink;
388           f->appl->open(f);
389           a = &f->pipe[0];
390           b = &f->pipe[1];
391           list_init(&a->queue);
392           a->syn_or_fin_seq = a->last_acked_seq = seq;
393           a->state = FLOW_SYN_SENT;
394           list_init(&b->queue);
395           b->state = FLOW_IDLE;
396           DBG("\t%p NEW\n", f);
397           goto drop;
398         }
399       DBG("\tUnmatched\n");
400       pkt_account(&stat_tcp_unmatched, p);
401       goto drop;
402     }
403
404   DBG("\t%p %s (%s/%s) ", f, (a == &f->pipe[0] ? "A->B" : "B->A"), pipe_state_names[f->pipe[0].state], pipe_state_names[f->pipe[1].state]);
405   if (a->state == FLOW_FINISHED && b->state == FLOW_FINISHED)
406     {
407       DBG("closed\n");
408       pkt_account(&stat_tcp_on_closed, p);
409       goto drop;
410     }
411
412   if (tcph->rst)
413     {
414       DBG("RESET\n");
415       f->appl->close(f, CAUSE_RESET);
416       a->state = b->state = FLOW_FINISHED;
417       flow_set_timeout(f, now + 300); /* FIXME */
418       goto drop;
419     }
420
421   flow_set_timeout(f, now + 600); /* FIXME */
422
423   if (tcph->syn)
424     {
425       if (tcph->fin || pkt_len(p))
426         goto inval;
427       if (tcph->ack)
428         {                       /* SYN ACK */
429           if (b->state == FLOW_SYN_SENT && b->syn_or_fin_seq+1 == ack)
430             {
431               DBG("SYN ACK\n");
432               a->last_acked_seq = ack;
433               a->syn_or_fin_seq = seq;
434               a->state = FLOW_SYN_SENT_ACK;
435               b->last_acked_seq = seq;
436               goto drop;
437             }
438           else if (b->state == FLOW_ESTABLISHED)
439             goto dup;
440           else
441             goto unex;
442         }
443       else
444         goto dup; /* otherwise SYN on already existing connection gets ignored */
445     }
446
447   if (tcph->ack)
448     {
449       if (tcp_seq_le(ack, a->last_acked_seq))
450         DBG("DUP ACK, ");
451       else
452         {
453           struct pkt *q;
454           a->last_acked_seq = ack;
455           while ((q = list_head(&a->queue)) && tcp_seq_le(q->seq+pkt_len(q), ack))
456             {
457               list_remove(&q->n);
458               DBG("data(%Ld-%Ld), ", a->stat_in.bytes, a->stat_in.bytes+pkt_len(q)-1);
459               pkt_account(&a->stat_in, q);
460               f->appl->input(f, (a == &f->pipe[1]), q);
461             }
462           if (b->state == FLOW_SYN_SENT_ACK && b->syn_or_fin_seq+1 == ack)
463             {
464               a->state = b->state = FLOW_ESTABLISHED;
465               DBG("ACKED SYN, ");
466             }
467           else if (b->state == FLOW_FIN_SENT && b->syn_or_fin_seq+1 == ack)
468             {
469               b->state = FLOW_FINISHED;
470               if (a->state == FLOW_FINISHED)
471                 {
472                   DBG("CLOSED BOTH WAYS\n");
473                   f->appl->close(f, CAUSE_CLOSE);
474                   flow_set_timeout(f, now + 300); /* FIXME */
475                   goto drop;
476                 }
477               else
478                 DBG("CLOSED ONE-WAY, ");
479             }
480           else if ((q = list_head(&a->queue)) && tcp_seq_lt(ack, q->seq))
481             {
482               DBG("DAMNED, ACK FOR UNCAUGHT DATA!\n");
483               goto invalid;
484             }
485           else if (b->state == FLOW_SYN_SENT_ACK || b->state == FLOW_SYN_SENT)
486             goto unex;
487         }
488     }
489
490   if (tcph->fin)
491     {
492       if (a->state == FLOW_ESTABLISHED)
493         {
494           a->state = FLOW_FIN_SENT;
495           a->syn_or_fin_seq = seq;
496           DBG("FIN SENT, waiting for FIN ACK, ");
497         }
498       else if (a->state == FLOW_FIN_SENT)
499         ;
500       else
501         goto unex;
502     }
503
504   if (!pkt_len(p))
505     {
506       DBG("EMPTY\n");
507       goto drop;
508     }
509
510   if (b->state == FLOW_ESTABLISHED || b->state == FLOW_FIN_SENT || b->state == FLOW_FINISHED)
511     {
512       p->seq = seq;
513       tcp_enqueue_data(b, p);
514       return;
515     }
516   else
517     goto unex;
518
519  drop:
520   pkt_free(p);
521   return;
522
523  dup:
524   DBG("DUP\n");
525   goto drop;
526
527  unex:
528   DBG("UNEXPECTED\n");
529   goto drop;
530
531  inval:
532   DBG("???\n");
533  invalid:
534   pkt_account(&stat_tcp_invalid, p);
535   goto drop;
536 }
537
538 /*** IP LAYER ***/
539
540 static struct pkt_stats stat_ip_in, stat_ip_invalid, stat_ip_uninteresting, stat_ip_fragmented, stat_ip_badsum;
541
542 static void ip_got_packet(struct pkt *p)
543 {
544   struct iphdr *iph;
545
546   pkt_account(&stat_ip_in, p);
547   if (!(iph = pkt_peek(p, sizeof(*iph))))
548     goto invalid;
549   if (iph->ihl < 5)
550     goto invalid;
551   if (iph->version != 4)
552     goto invalid;
553   uns hdrlen = 4*iph->ihl;
554   if (pkt_len(p) < hdrlen)
555     goto invalid;
556   if (!tcpip_verify_checksum(tcpip_calc_checksum(p->data, hdrlen, 0)))
557     {
558       pkt_account(&stat_ip_badsum, p);
559       goto drop;
560     }
561   uns len = ntohs(iph->tot_len);
562   if (len < hdrlen || len > pkt_len(p))
563     goto invalid;
564   pkt_unappend(p, pkt_len(p) - len);
565   pkt_pop(p, hdrlen);
566
567   if (iph->protocol != IPPROTO_TCP)
568     {
569       pkt_account(&stat_ip_uninteresting, p);
570       goto drop;
571     }
572   /* XXX: Fragmentation not supported yet, but well-behaved TCP stacks don't use it anyway */
573   if (ntohs(iph->frag_off) & 0x3fff)
574     {
575       pkt_account(&stat_ip_fragmented, p);
576       goto drop;
577     }
578   tcp_got_packet(iph, p);
579   return;
580
581  invalid:
582   pkt_account(&stat_ip_invalid, p);
583  drop:
584   pkt_free(p);
585 }
586
587 /*** LINK LAYER ***/
588
589 static struct pkt_stats stat_link_dwarf, stat_link_in, stat_link_unknown, stat_link_arp;
590
591 static void link_eth_got_packet(struct pkt *p)
592 {
593   struct ether_header *eth;
594   uns etype;
595
596   pkt_account(&stat_link_in, p);
597   if (!(eth = pkt_pop(p, sizeof(*eth))))
598     {
599       pkt_account(&stat_link_dwarf, p);
600       return;
601     }
602   etype = ntohs(eth->ether_type);
603   switch (etype)
604     {
605     case ETHERTYPE_IP:
606       ip_got_packet(p);
607       break;
608     case ETHERTYPE_ARP:
609       pkt_account(&stat_link_arp, p);
610       pkt_free(p);
611       break;
612     default:
613       // printf("Unknown ethertype: %04x\n", etype);
614       pkt_account(&stat_link_unknown, p);
615       pkt_free(p);
616     }
617 }
618
619 /*** PCAP INTERFACE ***/
620
621 static void (*link_handler)(struct pkt *);
622 static struct pkt_stats stat_pcap_incomplete;
623
624 static int link_setup_handler(int dlt)
625 {
626   switch (dlt)
627     {
628     case DLT_EN10MB:    link_handler = link_eth_got_packet; return 1;
629     default:            return 0;
630     }
631 }
632
633 static void got_pcap_packet(u_char *userdata UNUSED, const struct pcap_pkthdr *hdr, const u_char *pkt)
634 {
635   if (hdr->caplen != hdr->len)
636     {
637       stat_pcap_incomplete.packets++;
638       stat_pcap_incomplete.bytes += hdr->len - hdr->caplen;
639       return;
640     }
641   struct pkt *p = pkt_new(0, hdr->len);
642   memcpy(pkt_append(p, hdr->len), pkt, hdr->len);
643   p->timestamp = (u64)hdr->ts.tv_sec * 1000000 + hdr->ts.tv_usec;
644   link_handler(p);
645 }
646
647 int main(int argc, char **argv)
648 {
649   char errbuf[PCAP_ERRBUF_SIZE];
650   pcap_t *pcap;
651   int dlt;
652
653   if (argc != 2)
654     die("Usage: netgrind <capture-file>");
655
656   flow_rehash();
657
658   if (!(pcap = pcap_open_offline(argv[1], errbuf)))
659     die("Unable to open %s: %s", argv[1], errbuf);
660   dlt = pcap_datalink(pcap);
661   if (!link_setup_handler(dlt))
662     die("Don't know how to handle data link type %d", dlt);
663   if (pcap_loop(pcap, -1, got_pcap_packet, NULL) < 0)
664     die("Capture failed: %s", pcap_geterr(pcap));
665   tcp_time_step(~0U);
666 #if 0
667   struct pcap_stat stats;
668   if (pcap_stats(pcap, &stats))
669     die("pcap_stats: %s", pcap_geterr(pcap));
670   printf("libpcap stats: %d packets received, %d dropped\n", stats.ps_recv, stats.ps_drop);
671 #endif
672   printf("Pcap: %Ld(%Ld) incomplete\n",
673          stat_pcap_incomplete.packets, stat_pcap_incomplete.bytes);
674   printf("Link: %Ld(%Ld) in, %Ld(%Ld) dwarves, %Ld(%Ld) strangers, %Ld(%Ld) ARPs\n",
675          stat_link_in.packets, stat_link_in.bytes,
676          stat_link_dwarf.packets, stat_link_dwarf.bytes,
677          stat_link_unknown.packets, stat_link_unknown.bytes,
678          stat_link_arp.packets, stat_link_arp.bytes);
679   printf("IP: %Ld(%Ld) in, %Ld(%Ld) invalid, %Ld(%Ld) boring, %Ld(%Ld) fragmented, %Ld(%Ld) bad checksum\n",
680          stat_ip_in.packets, stat_ip_in.bytes,
681          stat_ip_invalid.packets, stat_ip_invalid.bytes,
682          stat_ip_uninteresting.packets, stat_ip_uninteresting.bytes,
683          stat_ip_fragmented.packets, stat_ip_fragmented.bytes,
684          stat_ip_badsum.packets, stat_ip_badsum.bytes);
685   printf("TCP: %Ld(%Ld) in, %Ld(%Ld) invalid, %Ld(%Ld) bad checksum\n",
686          stat_tcp_in.packets, stat_tcp_in.bytes,
687          stat_tcp_invalid.packets, stat_tcp_invalid.bytes,
688          stat_tcp_badsum.packets, stat_tcp_badsum.bytes);
689   pcap_close(pcap);
690   return 0;
691 }