source: trunk/libtransmission/tr-dht.c

Last change on this file was 14536, checked in by mikedld, 18 months ago

Bump DHT version used in CMake scripts (includes latest Win32 fixes)

  • Property svn:keywords set to Date Rev Author Id
File size: 20.0 KB
Line 
1/*
2 * Copyright (c) 2009-2010 by Juliusz Chroboczek
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 *
22 * $Id: tr-dht.c 14536 2015-06-08 19:54:51Z mikedld $
23 *
24 */
25
26/* ansi */
27#include <errno.h>
28#include <stdio.h>
29#include <string.h> /* memcpy (), memset (), memchr (), strlen () */
30#include <stdlib.h> /* atoi () */
31
32/* posix */
33#include <signal.h> /* sig_atomic_t */
34
35#ifdef _WIN32
36  #include <inttypes.h>
37  #include <ws2tcpip.h>
38  #undef gai_strerror
39  #define gai_strerror gai_strerrorA
40#else
41  #include <sys/time.h>
42  #include <sys/types.h>
43  #include <sys/socket.h> /* socket (), bind () */
44  #include <netdb.h>
45  #include <netinet/in.h> /* sockaddr_in */
46#endif
47
48/* third party */
49#include <event2/event.h>
50#include <dht/dht.h>
51
52/* libT */
53#include "transmission.h"
54#include "crypto-utils.h"
55#include "file.h"
56#include "log.h"
57#include "net.h"
58#include "peer-mgr.h" /* tr_peerMgrCompactToPex () */
59#include "platform.h" /* tr_threadNew () */
60#include "session.h"
61#include "torrent.h" /* tr_torrentFindFromHash () */
62#include "tr-dht.h"
63#include "trevent.h" /* tr_runInEventThread () */
64#include "utils.h"
65#include "variant.h"
66
67static struct event *dht_timer = NULL;
68static unsigned char myid[20];
69static tr_session *session = NULL;
70
71static void timer_callback (evutil_socket_t s, short type, void *ignore);
72
73struct bootstrap_closure {
74    tr_session *session;
75    uint8_t *nodes;
76    uint8_t *nodes6;
77    size_t len, len6;
78};
79
80static bool
81bootstrap_done (tr_session *session, int af)
82{
83    int status;
84
85    if (af == 0)
86        return
87            bootstrap_done (session, AF_INET) &&
88            bootstrap_done (session, AF_INET6);
89
90    status = tr_dhtStatus (session, af, NULL);
91    return status == TR_DHT_STOPPED || status >= TR_DHT_FIREWALLED;
92}
93
94static void
95nap (int roughly_sec)
96{
97    const int roughly_msec = roughly_sec * 1000;
98    const int msec = roughly_msec/2 + tr_rand_int_weak (roughly_msec);
99    tr_wait_msec (msec);
100}
101
102static int
103bootstrap_af (tr_session *session)
104{
105    if (bootstrap_done (session, AF_INET6))
106        return AF_INET;
107    else if (bootstrap_done (session, AF_INET))
108        return AF_INET6;
109    else
110        return 0;
111}
112
113static void
114bootstrap_from_name (const char *name, tr_port port, int af)
115{
116    struct addrinfo hints, *info, *infop;
117    char pp[10];
118    int rc;
119
120    memset (&hints, 0, sizeof (hints));
121    hints.ai_socktype = SOCK_DGRAM;
122    hints.ai_family = af;
123    /* No, just passing p + 1 to gai won't work. */
124    tr_snprintf (pp, sizeof (pp), "%d", (int)port);
125
126    rc = getaddrinfo (name, pp, &hints, &info);
127    if (rc != 0) {
128        tr_logAddNamedError ("DHT", "%s:%s: %s", name, pp, gai_strerror (rc));
129        return;
130    }
131
132    infop = info;
133    while (infop) {
134        dht_ping_node (infop->ai_addr, infop->ai_addrlen);
135
136        nap (15);
137
138        if (bootstrap_done (session, af))
139            break;
140        infop = infop->ai_next;
141    }
142    freeaddrinfo (info);
143}
144
145static void
146dht_bootstrap (void *closure)
147{
148    struct bootstrap_closure *cl = closure;
149    int i;
150    int num = cl->len / 6, num6 = cl->len6 / 18;
151
152    if (session != cl->session)
153        return;
154
155    if (cl->len > 0)
156        tr_logAddNamedInfo ("DHT", "Bootstrapping from %d IPv4 nodes", num);
157
158    if (cl->len6 > 0)
159        tr_logAddNamedInfo ("DHT", "Bootstrapping from %d IPv6 nodes", num6);
160
161
162    for (i = 0; i < MAX (num, num6); i++) {
163        if (i < num && !bootstrap_done (cl->session, AF_INET)) {
164            tr_port port;
165            struct tr_address addr;
166
167            memset (&addr, 0, sizeof (addr));
168            addr.type = TR_AF_INET;
169            memcpy (&addr.addr.addr4, &cl->nodes[i * 6], 4);
170            memcpy (&port, &cl->nodes[i * 6 + 4], 2);
171            port = ntohs (port);
172            tr_dhtAddNode (cl->session, &addr, port, 1);
173        }
174        if (i < num6 && !bootstrap_done (cl->session, AF_INET6)) {
175            tr_port port;
176            struct tr_address addr;
177
178            memset (&addr, 0, sizeof (addr));
179            addr.type = TR_AF_INET6;
180            memcpy (&addr.addr.addr6, &cl->nodes6[i * 18], 16);
181            memcpy (&port, &cl->nodes6[i * 18 + 16], 2);
182            port = ntohs (port);
183            tr_dhtAddNode (cl->session, &addr, port, 1);
184        }
185
186        /* Our DHT code is able to take up to 9 nodes in a row without
187           dropping any. After that, it takes some time to split buckets.
188           So ping the first 8 nodes quickly, then slow down. */
189        if (i < 8)
190            nap (2);
191        else
192            nap (15);
193
194        if (bootstrap_done (session, 0))
195            break;
196    }
197
198    if (!bootstrap_done (cl->session, 0)) {
199        char *bootstrap_file;
200        tr_sys_file_t f = TR_BAD_SYS_FILE;
201
202        bootstrap_file =
203            tr_buildPath (cl->session->configDir, "dht.bootstrap", NULL);
204
205        if (bootstrap_file)
206            f = tr_sys_file_open (bootstrap_file, TR_SYS_FILE_READ, 0, NULL);
207        if (f != TR_BAD_SYS_FILE) {
208            tr_logAddNamedInfo ("DHT", "Attempting manual bootstrap");
209            for (;;) {
210                char buf[201];
211                char *p;
212                int port = 0;
213
214                if (!tr_sys_file_read_line (f, buf, 200, NULL))
215                    break;
216
217                p = memchr (buf, ' ', strlen (buf));
218                if (p != NULL)
219                    port = atoi (p + 1);
220                if (p == NULL || port <= 0 || port >= 0x10000) {
221                    tr_logAddNamedError ("DHT", "Couldn't parse %s", buf);
222                    continue;
223                }
224
225                *p = '\0';
226
227                bootstrap_from_name (buf, port, bootstrap_af (session));
228
229                if (bootstrap_done (cl->session, 0))
230                    break;
231            }
232            tr_sys_file_close (f, NULL);
233        }
234
235        tr_free (bootstrap_file);
236    }
237
238    if (!bootstrap_done (cl->session, 0)) {
239        for (i = 0; i < 6; i++) {
240            /* We don't want to abuse our bootstrap nodes, so be very
241               slow.  The initial wait is to give other nodes a chance
242               to contact us before we attempt to contact a bootstrap
243               node, for example because we've just been restarted. */
244            nap (40);
245            if (bootstrap_done (cl->session, 0))
246                break;
247            if (i == 0)
248                tr_logAddNamedInfo ("DHT",
249                        "Attempting bootstrap from dht.transmissionbt.com");
250            bootstrap_from_name ("dht.transmissionbt.com", 6881,
251                                 bootstrap_af (session));
252        }
253    }
254
255    if (cl->nodes)
256        tr_free (cl->nodes);
257    if (cl->nodes6)
258        tr_free (cl->nodes6);
259    tr_free (closure);
260    tr_logAddNamedDbg ("DHT", "Finished bootstrapping");
261}
262
263int
264tr_dhtInit (tr_session *ss)
265{
266    tr_variant benc;
267    int rc;
268    bool have_id = false;
269    char * dat_file;
270    uint8_t * nodes = NULL, * nodes6 = NULL;
271    const uint8_t * raw;
272    size_t len, len6;
273    struct bootstrap_closure * cl;
274
275    if (session) /* already initialized */
276        return -1;
277
278    tr_logAddNamedDbg ("DHT", "Initializing DHT");
279
280    if (tr_env_key_exists ("TR_DHT_VERBOSE"))
281        dht_debug = stderr;
282
283    dat_file = tr_buildPath (ss->configDir, "dht.dat", NULL);
284    rc = tr_variantFromFile (&benc, TR_VARIANT_FMT_BENC, dat_file, NULL) ? 0 : -1;
285    tr_free (dat_file);
286    if (rc == 0) {
287        have_id = tr_variantDictFindRaw (&benc, TR_KEY_id, &raw, &len);
288        if (have_id && len==20)
289            memcpy (myid, raw, len);
290        if (ss->udp_socket != TR_BAD_SOCKET &&
291            tr_variantDictFindRaw (&benc, TR_KEY_nodes, &raw, &len) && ! (len%6)) {
292                nodes = tr_memdup (raw, len);
293        }
294        if (ss->udp6_socket != TR_BAD_SOCKET &&
295            tr_variantDictFindRaw (&benc, TR_KEY_nodes6, &raw, &len6) && ! (len6%18)) {
296            nodes6 = tr_memdup (raw, len6);
297        }
298        tr_variantFree (&benc);
299    }
300
301    if (nodes == NULL)
302        len = 0;
303    if (nodes6 == NULL)
304        len6 = 0;
305
306    if (have_id)
307        tr_logAddNamedInfo ("DHT", "Reusing old id");
308    else {
309        /* Note that DHT ids need to be distributed uniformly,
310         * so it should be something truly random. */
311        tr_logAddNamedInfo ("DHT", "Generating new id");
312        tr_rand_buffer (myid, 20);
313    }
314
315    rc = dht_init (ss->udp_socket, ss->udp6_socket, myid, NULL);
316    if (rc < 0)
317        goto fail;
318
319    session = ss;
320
321    cl = tr_new (struct bootstrap_closure, 1);
322    cl->session = session;
323    cl->nodes = nodes;
324    cl->nodes6 = nodes6;
325    cl->len = len;
326    cl->len6 = len6;
327    tr_threadNew (dht_bootstrap, cl);
328
329    dht_timer = evtimer_new (session->event_base, timer_callback, session);
330    tr_timerAdd (dht_timer, 0, tr_rand_int_weak (1000000));
331
332    tr_logAddNamedDbg ("DHT", "DHT initialized");
333
334    return 1;
335
336 fail:
337    tr_logAddNamedDbg ("DHT", "DHT initialization failed (errno = %d)", errno);
338    session = NULL;
339    return -1;
340}
341
342void
343tr_dhtUninit (tr_session *ss)
344{
345    if (session != ss)
346        return;
347
348    tr_logAddNamedDbg ("DHT", "Uninitializing DHT");
349
350    if (dht_timer != NULL) {
351        event_free (dht_timer);
352        dht_timer = NULL;
353    }
354
355    /* Since we only save known good nodes, avoid erasing older data if we
356       don't know enough nodes. */
357    if ((tr_dhtStatus (ss, AF_INET, NULL) < TR_DHT_FIREWALLED) &&
358      (tr_dhtStatus (ss, AF_INET6, NULL) < TR_DHT_FIREWALLED)) {
359        tr_logAddNamedInfo ("DHT", "Not saving nodes, DHT not ready");
360    } else {
361        tr_variant benc;
362        struct sockaddr_in sins[300];
363        struct sockaddr_in6 sins6[300];
364        char compact[300 * 6], compact6[300 * 18];
365        char *dat_file;
366        int i, j, num = 300, num6 = 300;
367        int n = dht_get_nodes (sins, &num, sins6, &num6);
368
369        tr_logAddNamedInfo ("DHT", "Saving %d (%d + %d) nodes", n, num, num6);
370
371        j = 0;
372        for (i=0; i<num; ++i) {
373            memcpy (compact + j, &sins[i].sin_addr, 4);
374            memcpy (compact + j + 4, &sins[i].sin_port, 2);
375            j += 6;
376        }
377        j = 0;
378        for (i=0; i<num6; ++i) {
379            memcpy (compact6 + j, &sins6[i].sin6_addr, 16);
380            memcpy (compact6 + j + 16, &sins6[i].sin6_port, 2);
381            j += 18;
382        }
383        tr_variantInitDict (&benc, 3);
384        tr_variantDictAddRaw (&benc, TR_KEY_id, myid, 20);
385        if (num > 0)
386            tr_variantDictAddRaw (&benc, TR_KEY_nodes, compact, num * 6);
387        if (num6 > 0)
388            tr_variantDictAddRaw (&benc, TR_KEY_nodes6, compact6, num6 * 18);
389        dat_file = tr_buildPath (ss->configDir, "dht.dat", NULL);
390        tr_variantToFile (&benc, TR_VARIANT_FMT_BENC, dat_file);
391        tr_variantFree (&benc);
392        tr_free (dat_file);
393    }
394
395    dht_uninit ();
396    tr_logAddNamedDbg ("DHT", "Done uninitializing DHT");
397
398    session = NULL;
399}
400
401bool
402tr_dhtEnabled (const tr_session * ss)
403{
404    return ss && (ss == session);
405}
406
407struct getstatus_closure
408{
409    int af;
410    sig_atomic_t status;
411    sig_atomic_t count;
412};
413
414static void
415getstatus (void * cl)
416{
417    struct getstatus_closure * closure = cl;
418    int good, dubious, incoming;
419
420    dht_nodes (closure->af, &good, &dubious, NULL, &incoming);
421
422    closure->count = good + dubious;
423
424    if (good < 4 || good + dubious <= 8)
425        closure->status = TR_DHT_BROKEN;
426    else if (good < 40)
427        closure->status = TR_DHT_POOR;
428    else if (incoming < 8)
429        closure->status = TR_DHT_FIREWALLED;
430    else
431        closure->status = TR_DHT_GOOD;
432}
433
434int
435tr_dhtStatus (tr_session * session, int af, int * nodes_return)
436{
437    struct getstatus_closure closure = { af, -1, -1 };
438
439    if (!tr_dhtEnabled (session) ||
440      (af == AF_INET && session->udp_socket == TR_BAD_SOCKET) ||
441      (af == AF_INET6 && session->udp6_socket == TR_BAD_SOCKET)) {
442        if (nodes_return)
443            *nodes_return = 0;
444        return TR_DHT_STOPPED;
445    }
446
447    tr_runInEventThread (session, getstatus, &closure);
448    while (closure.status < 0)
449        tr_wait_msec (50 /*msec*/);
450
451    if (nodes_return)
452        *nodes_return = closure.count;
453
454    return closure.status;
455}
456
457tr_port
458tr_dhtPort (tr_session *ss)
459{
460    return tr_dhtEnabled (ss) ? ss->udp_port : 0;
461}
462
463bool
464tr_dhtAddNode (tr_session       * ss,
465               const tr_address * address,
466               tr_port            port,
467               bool            bootstrap)
468{
469    int af = address->type == TR_AF_INET ? AF_INET : AF_INET6;
470
471    if (!tr_dhtEnabled (ss))
472        return false;
473
474    /* Since we don't want to abuse our bootstrap nodes,
475     * we don't ping them if the DHT is in a good state. */
476
477    if (bootstrap) {
478        if (tr_dhtStatus (ss, af, NULL) >= TR_DHT_FIREWALLED)
479            return false;
480    }
481
482    if (address->type == TR_AF_INET) {
483        struct sockaddr_in sin;
484        memset (&sin, 0, sizeof (sin));
485        sin.sin_family = AF_INET;
486        memcpy (&sin.sin_addr, &address->addr.addr4, 4);
487        sin.sin_port = htons (port);
488        dht_ping_node ((struct sockaddr*)&sin, sizeof (sin));
489        return true;
490    } else if (address->type == TR_AF_INET6) {
491        struct sockaddr_in6 sin6;
492        memset (&sin6, 0, sizeof (sin6));
493        sin6.sin6_family = AF_INET6;
494        memcpy (&sin6.sin6_addr, &address->addr.addr6, 16);
495        sin6.sin6_port = htons (port);
496        dht_ping_node ((struct sockaddr*)&sin6, sizeof (sin6));
497        return true;
498    }
499
500    return false;
501}
502
503const char *
504tr_dhtPrintableStatus (int status)
505{
506    switch (status) {
507    case TR_DHT_STOPPED: return "stopped";
508    case TR_DHT_BROKEN: return "broken";
509    case TR_DHT_POOR: return "poor";
510    case TR_DHT_FIREWALLED: return "firewalled";
511    case TR_DHT_GOOD: return "good";
512    default: return "???";
513    }
514}
515
516static void
517callback (void *ignore UNUSED, int event,
518          const unsigned char *info_hash, const void *data, size_t data_len)
519{
520    if (event == DHT_EVENT_VALUES || event == DHT_EVENT_VALUES6) {
521        tr_torrent *tor;
522        tr_sessionLock (session);
523        tor = tr_torrentFindFromHash (session, info_hash);
524        if (tor && tr_torrentAllowsDHT (tor))
525        {
526            size_t i, n;
527            tr_pex * pex;
528            if (event == DHT_EVENT_VALUES)
529                pex = tr_peerMgrCompactToPex (data, data_len, NULL, 0, &n);
530            else
531                pex = tr_peerMgrCompact6ToPex (data, data_len, NULL, 0, &n);
532            for (i=0; i<n; ++i)
533                tr_peerMgrAddPex (tor, TR_PEER_FROM_DHT, pex+i, -1);
534            tr_free (pex);
535            tr_logAddTorDbg (tor, "Learned %d %s peers from DHT",
536                    (int)n,
537                      event == DHT_EVENT_VALUES6 ? "IPv6" : "IPv4");
538        }
539        tr_sessionUnlock (session);
540    } else if (event == DHT_EVENT_SEARCH_DONE ||
541               event == DHT_EVENT_SEARCH_DONE6) {
542        tr_torrent * tor = tr_torrentFindFromHash (session, info_hash);
543        if (tor) {
544            if (event == DHT_EVENT_SEARCH_DONE) {
545                tr_logAddTorInfo (tor, "%s", "IPv4 DHT announce done");
546                tor->dhtAnnounceInProgress = 0;
547            } else {
548                tr_logAddTorInfo (tor, "%s", "IPv6 DHT announce done");
549                tor->dhtAnnounce6InProgress = 0;
550            }
551        }
552    }
553}
554
555static int
556tr_dhtAnnounce (tr_torrent *tor, int af, bool announce)
557{
558    int rc, status, numnodes, ret = 0;
559
560    if (!tr_torrentAllowsDHT (tor))
561        return -1;
562
563    status = tr_dhtStatus (tor->session, af, &numnodes);
564
565    if (status == TR_DHT_STOPPED) {
566        /* Let the caller believe everything is all right. */
567        return 1;
568    }
569
570    if (status >= TR_DHT_POOR) {
571        rc = dht_search (tor->info.hash,
572                         announce ? tr_sessionGetPeerPort (session) : 0,
573                         af, callback, NULL);
574        if (rc >= 1) {
575            tr_logAddTorInfo (tor, "Starting %s DHT announce (%s, %d nodes)",
576                      af == AF_INET6 ? "IPv6" : "IPv4",
577                      tr_dhtPrintableStatus (status), numnodes);
578            if (af == AF_INET)
579                tor->dhtAnnounceInProgress = true;
580            else
581                tor->dhtAnnounce6InProgress = true;
582            ret = 1;
583        } else {
584            tr_logAddTorErr (tor, "%s DHT announce failed (%s, %d nodes): %s",
585                      af == AF_INET6 ? "IPv6" : "IPv4",
586                      tr_dhtPrintableStatus (status), numnodes,
587                      tr_strerror (errno));
588        }
589    } else {
590        tr_logAddTorDbg (tor, "%s DHT not ready (%s, %d nodes)",
591                  af == AF_INET6 ? "IPv6" : "IPv4",
592                  tr_dhtPrintableStatus (status), numnodes);
593    }
594
595    return ret;
596}
597
598void
599tr_dhtUpkeep (tr_session * session)
600{
601    tr_torrent * tor = NULL;
602    const time_t now = tr_time ();
603
604    while ((tor = tr_torrentNext (session, tor)))
605    {
606        if (!tor->isRunning || !tr_torrentAllowsDHT (tor))
607            continue;
608
609        if (tor->dhtAnnounceAt <= now)
610        {
611            const int rc = tr_dhtAnnounce (tor, AF_INET, 1);
612
613            tor->dhtAnnounceAt = now + ((rc == 0)
614                                     ? 5 + tr_rand_int_weak (5)
615                                     : 25 * 60 + tr_rand_int_weak (3*60));
616        }
617
618        if (tor->dhtAnnounce6At <= now)
619        {
620            const int rc = tr_dhtAnnounce (tor, AF_INET6, 1);
621
622            tor->dhtAnnounce6At = now + ((rc == 0)
623                                      ? 5 + tr_rand_int_weak (5)
624                                      : 25 * 60 + tr_rand_int_weak (3*60));
625        }
626    }
627}
628
629void
630tr_dhtCallback (unsigned char *buf, int buflen,
631               struct sockaddr *from, socklen_t fromlen,
632               void *sv)
633{
634    time_t tosleep;
635    int rc;
636
637    assert (tr_isSession (sv));
638
639    if (sv != session)
640        return;
641
642    rc = dht_periodic (buf, buflen, from, fromlen,
643                       &tosleep, callback, NULL);
644    if (rc < 0) {
645        if (errno == EINTR) {
646            tosleep = 0;
647        } else {
648            tr_logAddNamedError ("DHT", "dht_periodic failed: %s", tr_strerror (errno));
649            if (errno == EINVAL || errno == EFAULT)
650                    abort ();
651            tosleep = 1;
652        }
653    }
654
655    /* Being slightly late is fine,
656       and has the added benefit of adding some jitter. */
657    tr_timerAdd (dht_timer, tosleep, tr_rand_int_weak (1000000));
658}
659
660static void
661timer_callback (evutil_socket_t s UNUSED, short type UNUSED, void *session)
662{
663    tr_dhtCallback (NULL, 0, NULL, 0, session);
664}
665
666/* This function should return true when a node is blacklisted.  We do
667   not support using a blacklist with the DHT in Transmission, since
668   massive (ab)use of this feature could harm the DHT.  However, feel
669   free to add support to your private copy as long as you don't
670   redistribute it. */
671
672int
673dht_blacklisted (const struct sockaddr *sa UNUSED, int salen UNUSED)
674{
675    return 0;
676}
677
678void
679dht_hash (void *hash_return, int hash_size,
680         const void *v1, int len1,
681         const void *v2, int len2,
682         const void *v3, int len3)
683{
684    unsigned char sha1[SHA_DIGEST_LENGTH];
685    tr_sha1 (sha1, v1, len1, v2, len2, v3, len3, NULL);
686    memset (hash_return, 0, hash_size);
687    memcpy (hash_return, sha1, MIN (hash_size, SHA_DIGEST_LENGTH));
688}
689
690int
691dht_random_bytes (void * buf, size_t size)
692{
693    tr_rand_buffer (buf, size);
694    return size;
695}
696
697#if defined (_WIN32) && !defined (__MINGW32__)
698int
699dht_gettimeofday (struct timeval * tv, struct timezone * tz)
700{
701  assert (tz == NULL);
702  return tr_gettimeofday (tv);
703}
704#endif
Note: See TracBrowser for help on using the repository browser.