LCOV - code coverage report
Current view: top level - waltz/resolv - fd_adns.c (source / functions) Hit Total Coverage
Test: cov.lcov Lines: 0 250 0.0 %
Date: 2026-10-22 18:10:43 Functions: 0 11 0.0 %

          Line data    Source code
       1             : #include "fd_adns.h"
       2             : #include "fd_netdb.h"
       3             : #include "fd_lookup.h"
       4             : #include "../../util/log/fd_log.h"
       5             : #include "../../util/io/fd_io.h"
       6             : 
       7             : #include <errno.h>
       8             : #include <unistd.h>
       9             : #include <sys/epoll.h>
      10             : #include <sys/socket.h>
      11             : #include <netinet/in.h>
      12             : 
      13             : /* Queries advertise EDNS0 with this UDP payload capacity (the DNS
      14             :    flag day 2020 recommendation: large enough that an A answer never
      15             :    truncates, small enough to avoid IP fragmentation). */
      16             : #define ANSWER_MTU (1232UL)
      17             : 
      18             : /* EDNS0 OPT pseudo-RR appended to each query: root name, TYPE=OPT,
      19             :    CLASS=payload capacity, TTL=0 (no extended flags), RDLEN=0. */
      20           0 : #define OPT_RR_SZ (11UL)
      21             : static uchar const opt_rr[ OPT_RR_SZ ] = { 0, 0, 41, (uchar)(ANSWER_MTU>>8), (uchar)(ANSWER_MTU&0xFF), 0, 0, 0, 0, 0, 0 };
      22             : 
      23             : /* Bound receive work per advance so a UDP flood at our ephemeral
      24             :    port cannot starve the caller's run loop. */
      25           0 : #define RECV_BURST_MAX (64UL)
      26             : 
      27             : FD_TL ushort fd_adns_ns_port = 53;
      28             : 
      29           0 : #define NS_MAX (3UL) /* MAXNS */
      30             : 
      31           0 : #define REQ_STATE_IDLE    (0)
      32           0 : #define REQ_STATE_PENDING (1)
      33           0 : #define REQ_STATE_DONE    (2)
      34             : 
      35             : struct fd_adns_req {
      36             :   int              state;
      37             :   uchar            query[ FD_DNS_QUERY_MTU ];
      38             :   int              qlen;
      39             :   int              no_edns;         /* server answered FORMERR/NOTIMP to EDNS0 */
      40             :   long             deadline_nanos;  /* next (re)send */
      41             :   uint             sends_left;
      42             :   fd_adns_result_t result;
      43             : };
      44             : 
      45             : typedef struct fd_adns_req fd_adns_req_t;
      46             : 
      47             : struct fd_adns_private {
      48             :   int   fd;
      49             : 
      50             :   uint  ns[ NS_MAX ];
      51             :   ulong ns_cnt;
      52             :   long  retry_nanos;
      53             :   uint  attempts;
      54             : 
      55             :   ulong           max;
      56             :   ulong           active_cnt;  /* PENDING+DONE */
      57             :   ulong           pending_cnt; /* PENDING only */
      58             :   fd_adns_req_t * reqs;
      59             : 
      60             :   ulong magic;
      61             : };
      62             : 
      63             : FD_FN_CONST ulong
      64           0 : fd_adns_align( void ) {
      65           0 :   return fd_ulong_max( alignof(fd_adns_t), alignof(fd_adns_req_t) );
      66           0 : }
      67             : 
      68             : FD_FN_CONST ulong
      69           0 : fd_adns_footprint( ulong max_reqs ) {
      70           0 :   ulong l = FD_LAYOUT_INIT;
      71           0 :   l = FD_LAYOUT_APPEND( l, alignof(fd_adns_t),     sizeof(fd_adns_t) );
      72           0 :   l = FD_LAYOUT_APPEND( l, alignof(fd_adns_req_t), max_reqs*sizeof(fd_adns_req_t) );
      73           0 :   return FD_LAYOUT_FINI( l, fd_adns_align() );
      74           0 : }
      75             : 
      76             : void *
      77             : fd_adns_new( void * shmem,
      78             :              ulong  max_reqs,
      79           0 :              int    epoll_fd ) {
      80           0 :   if( FD_UNLIKELY( !shmem ) ) {
      81           0 :     FD_LOG_WARNING(( "NULL shmem" ));
      82           0 :     return NULL;
      83           0 :   }
      84             : 
      85           0 :   if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shmem, fd_adns_align() ) ) ) {
      86           0 :     FD_LOG_WARNING(( "unaligned shmem" ));
      87           0 :     return NULL;
      88           0 :   }
      89             : 
      90           0 :   if( FD_UNLIKELY( !max_reqs ) ) {
      91           0 :     FD_LOG_WARNING(( "max_reqs must be at least 1" ));
      92           0 :     return NULL;
      93           0 :   }
      94             : 
      95           0 :   if( FD_UNLIKELY( epoll_fd==-1 ) ) {
      96           0 :     FD_LOG_WARNING(( "invalid epoll_fd" ));
      97           0 :     return NULL;
      98           0 :   }
      99             : 
     100           0 :   FD_SCRATCH_ALLOC_INIT( l, shmem );
     101           0 :   fd_adns_t * adns = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_adns_t),     sizeof(fd_adns_t) );
     102           0 :   adns->reqs       = FD_SCRATCH_ALLOC_APPEND( l, alignof(fd_adns_req_t), max_reqs*sizeof(fd_adns_req_t) );
     103             : 
     104           0 :   adns->max         = max_reqs;
     105           0 :   adns->active_cnt  = 0UL;
     106           0 :   adns->pending_cnt = 0UL;
     107           0 :   for( ulong i=0UL; i<max_reqs; i++ ) adns->reqs[ i ].state = REQ_STATE_IDLE;
     108             : 
     109             :   /* resolv.conf: nameservers plus retry cadence (timeout is the total
     110             :      budget spread over attempts, like the blocking resolver). */
     111           0 :   uint ns[ NS_MAX ];
     112           0 :   uint timeout  = 5U;
     113           0 :   uint attempts = 2U;
     114           0 :   int  ns_cnt = fd_dns_nameservers( ns, NS_MAX, &timeout, &attempts );
     115           0 :   if( FD_UNLIKELY( ns_cnt<=0 ) ) {
     116           0 :     FD_LOG_WARNING(( "no usable IPv4 nameservers in /etc/resolv.conf" ));
     117           0 :     ns_cnt = 0;
     118           0 :   }
     119           0 :   adns->ns_cnt = (ulong)ns_cnt;
     120           0 :   for( ulong i=0UL; i<adns->ns_cnt; i++ ) adns->ns[ i ] = ns[ i ];
     121           0 :   adns->attempts    = fd_uint_max( attempts, 1U );
     122           0 :   adns->retry_nanos = ((long)timeout*1000L*1000L*1000L)/(long)adns->attempts;
     123             : 
     124           0 :   adns->fd = socket( AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0 );
     125           0 :   if( FD_UNLIKELY( -1==adns->fd ) ) FD_LOG_ERR(( "socket() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
     126           0 :   struct epoll_event ev = { .events = EPOLLIN, .data.fd = adns->fd };
     127           0 :   if( FD_UNLIKELY( -1==epoll_ctl( epoll_fd, EPOLL_CTL_ADD, adns->fd, &ev ) ) ) FD_LOG_ERR(( "epoll_ctl(ADD) failed (%i-%s)", errno, fd_io_strerror( errno ) ));
     128             : 
     129           0 :   FD_COMPILER_MFENCE();
     130           0 :   FD_VOLATILE( adns->magic ) = FD_ADNS_MAGIC;
     131           0 :   FD_COMPILER_MFENCE();
     132             : 
     133           0 :   return (void *)adns;
     134           0 : }
     135             : 
     136             : fd_adns_t *
     137           0 : fd_adns_join( void * shadns ) {
     138           0 :   if( FD_UNLIKELY( !shadns ) ) {
     139           0 :     FD_LOG_WARNING(( "NULL shadns" ));
     140           0 :     return NULL;
     141           0 :   }
     142             : 
     143           0 :   if( FD_UNLIKELY( !fd_ulong_is_aligned( (ulong)shadns, fd_adns_align() ) ) ) {
     144           0 :     FD_LOG_WARNING(( "unaligned shadns" ));
     145           0 :     return NULL;
     146           0 :   }
     147             : 
     148           0 :   fd_adns_t * adns = (fd_adns_t *)shadns;
     149           0 :   if( FD_UNLIKELY( adns->magic!=FD_ADNS_MAGIC ) ) {
     150           0 :     FD_LOG_WARNING(( "bad magic" ));
     151           0 :     return NULL;
     152           0 :   }
     153             : 
     154           0 :   return adns;
     155           0 : }
     156             : 
     157             : void *
     158           0 : fd_adns_leave( fd_adns_t * adns ) {
     159           0 :   return (void *)adns;
     160           0 : }
     161             : 
     162             : void *
     163           0 : fd_adns_delete( void * shadns ) {
     164           0 :   fd_adns_t * adns = (fd_adns_t *)shadns;
     165           0 :   if( FD_UNLIKELY( !adns ) ) return NULL;
     166           0 :   if( FD_UNLIKELY( adns->magic!=FD_ADNS_MAGIC ) ) {
     167           0 :     FD_LOG_WARNING(( "bad magic" ));
     168           0 :     return NULL;
     169           0 :   }
     170             : 
     171           0 :   if( FD_UNLIKELY( -1==close( adns->fd ) ) ) FD_LOG_ERR(( "close() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
     172             : 
     173           0 :   FD_COMPILER_MFENCE();
     174           0 :   FD_VOLATILE( adns->magic ) = 0UL;
     175           0 :   FD_COMPILER_MFENCE();
     176             : 
     177           0 :   return shadns;
     178           0 : }
     179             : 
     180             : int
     181             : fd_adns_resolve( fd_adns_t *  adns,
     182             :                  char const * name,
     183           0 :                  ulong        req_id ) {
     184           0 :   if( FD_UNLIKELY( adns->active_cnt>=adns->max ) ) return -1;
     185             : 
     186           0 :   fd_adns_req_t * req = NULL;
     187           0 :   for( ulong i=0UL; i<adns->max; i++ ) {
     188           0 :     if( FD_LIKELY( adns->reqs[ i ].state==REQ_STATE_IDLE ) ) { req = &adns->reqs[ i ]; break; }
     189           0 :   }
     190           0 :   FD_TEST( req );
     191             : 
     192           0 :   req->result.req_id   = req_id;
     193           0 :   req->result.err      = 0;
     194           0 :   req->result.addr_cnt = 0UL;
     195           0 :   req->no_edns         = 0;
     196             : 
     197             :   /* Local names (literals, /etc/hosts) complete without network I/O */
     198           0 :   int cnt = fd_dns_ip4_local( name, req->result.addrs, FD_ADNS_ADDR_MAX );
     199           0 :   if( FD_UNLIKELY( cnt ) ) {
     200           0 :     if( FD_LIKELY( cnt>0 ) ) req->result.addr_cnt = (ulong)cnt;
     201           0 :     else                     req->result.err      = cnt;
     202           0 :     req->state = REQ_STATE_DONE;
     203           0 :     adns->active_cnt++;
     204           0 :     return 0;
     205           0 :   }
     206             : 
     207           0 :   req->qlen = fd_dns_ip4_query( name, req->query );
     208           0 :   if( FD_UNLIKELY( req->qlen<0 ) ) {
     209           0 :     req->result.err = FD_EAI_NONAME;
     210           0 :     req->state      = REQ_STATE_DONE;
     211           0 :     adns->active_cnt++;
     212           0 :     return 0;
     213           0 :   }
     214             : 
     215             :   /* Answers are matched by query id; make ids distinct. */
     216           0 :   for( ulong i=0UL; i<adns->max; i++ ) {
     217           0 :     if( FD_UNLIKELY( adns->reqs[ i ].state!=REQ_STATE_PENDING ) ) continue;
     218           0 :     if( FD_UNLIKELY( adns->reqs[ i ].query[ 0 ]==req->query[ 0 ] && adns->reqs[ i ].query[ 1 ]==req->query[ 1 ] ) ) {
     219           0 :       req->query[ 1 ]++;
     220           0 :       i = (ulong)-1L; /* restart scan */
     221           0 :     }
     222           0 :   }
     223             : 
     224           0 :   req->deadline_nanos = 0L; /* due immediately */
     225           0 :   req->sends_left     = adns->attempts;
     226           0 :   req->state          = REQ_STATE_PENDING;
     227           0 :   adns->active_cnt++;
     228           0 :   adns->pending_cnt++;
     229           0 :   return 0;
     230           0 : }
     231             : 
     232             : static void
     233             : drain_answers( fd_adns_t * adns );
     234             : 
     235             : static void
     236             : pending_io( fd_adns_t * adns,
     237           0 :             long        now ) {
     238           0 :   drain_answers( adns );
     239             : 
     240           0 :   if( FD_LIKELY( !adns->pending_cnt ) ) return;
     241             : 
     242             :   /* Send due (re)tries to all nameservers in parallel */
     243           0 :   for( ulong i=0UL; i<adns->max; i++ ) {
     244           0 :     fd_adns_req_t * req = &adns->reqs[ i ];
     245           0 :     if( FD_LIKELY( req->state!=REQ_STATE_PENDING ) ) continue;
     246           0 :     if( FD_LIKELY( req->deadline_nanos>now ) )       continue;
     247             : 
     248           0 :     if( FD_UNLIKELY( !req->sends_left || !adns->ns_cnt ) ) {
     249           0 :       req->result.err = FD_EAI_AGAIN;
     250           0 :       req->state      = REQ_STATE_DONE;
     251           0 :       adns->pending_cnt--;
     252           0 :       continue;
     253           0 :     }
     254             : 
     255             :     /* req->query holds the bare question packet (the part answers
     256             :        echo); the wire packet adds the EDNS0 OPT RR unless the server
     257             :        rejected it */
     258           0 :     uchar pkt[ FD_DNS_QUERY_MTU+OPT_RR_SZ ];
     259           0 :     fd_memcpy( pkt, req->query, (ulong)req->qlen );
     260           0 :     ulong pkt_sz = (ulong)req->qlen;
     261           0 :     if( FD_LIKELY( !req->no_edns ) ) {
     262           0 :       fd_memcpy( pkt+pkt_sz, opt_rr, OPT_RR_SZ );
     263           0 :       pkt[ 11 ] = 1; /* arcount */
     264           0 :       pkt_sz += OPT_RR_SZ;
     265           0 :     }
     266             : 
     267           0 :     int sent_any = 0, all_eagain = 1;
     268           0 :     for( ulong j=0UL; j<adns->ns_cnt; j++ ) {
     269           0 :       struct sockaddr_in ns_addr = {
     270           0 :         .sin_family = AF_INET,
     271           0 :         .sin_port   = fd_ushort_bswap( fd_adns_ns_port ),
     272           0 :         .sin_addr   = { .s_addr = adns->ns[ j ] }
     273           0 :       };
     274           0 :       long sent;
     275           0 :       do sent = sendto( adns->fd, pkt, pkt_sz, MSG_NOSIGNAL, fd_type_pun( &ns_addr ), sizeof(ns_addr) );
     276           0 :       while( FD_UNLIKELY( -1L==sent && errno==EINTR ) ); /* retry before charging the attempt */
     277           0 :       if( FD_UNLIKELY( -1L==sent ) ) {
     278           0 :         if( FD_UNLIKELY( errno!=EAGAIN && errno!=ECONNREFUSED && errno!=ENETUNREACH && errno!=EHOSTUNREACH ) )
     279           0 :           FD_LOG_ERR(( "sendto() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
     280           0 :         if( FD_LIKELY( errno!=EAGAIN ) ) all_eagain = 0;
     281           0 :       } else {
     282           0 :         sent_any = 1;
     283           0 :       }
     284           0 :     }
     285             :     /* Local socket backpressure put nothing on the wire: stay due and
     286             :        retry next advance without consuming the attempt */
     287           0 :     if( FD_UNLIKELY( !sent_any && all_eagain && adns->ns_cnt ) ) { req->deadline_nanos = now; continue; }
     288           0 :     req->sends_left--;
     289           0 :     req->deadline_nanos = now+adns->retry_nanos;
     290           0 :   }
     291           0 : }
     292             : 
     293             : static void
     294           0 : drain_answers( fd_adns_t * adns ) {
     295           0 :   for( ulong burst=0UL; burst<RECV_BURST_MAX; burst++ ) {
     296           0 :     uchar answer[ ANSWER_MTU ];
     297           0 :     struct sockaddr_in sa;
     298           0 :     struct iovec iov = { .iov_base = answer, .iov_len = sizeof(answer) };
     299           0 :     struct msghdr mh = {
     300           0 :       .msg_name    = &sa,
     301           0 :       .msg_namelen = sizeof(sa),
     302           0 :       .msg_iov     = &iov,
     303           0 :       .msg_iovlen  = 1
     304           0 :     };
     305           0 :     long rlen = recvmsg( adns->fd, &mh, 0 );
     306           0 :     if( FD_LIKELY( rlen<0L ) ) {
     307           0 :       if( FD_LIKELY( errno==EAGAIN || errno==EWOULDBLOCK ) ) break;
     308           0 :       if( FD_LIKELY( errno==ECONNREFUSED || errno==EINTR ) ) continue;
     309           0 :       FD_LOG_ERR(( "recvmsg() failed (%i-%s)", errno, fd_io_strerror( errno ) ));
     310           0 :     }
     311           0 :     if( FD_UNLIKELY( rlen<12L ) ) continue;
     312             : 
     313             :     /* Ignore replies from addresses we didn't send to */
     314           0 :     ulong j;
     315           0 :     for( j=0UL; j<adns->ns_cnt; j++ ) {
     316           0 :       if( FD_LIKELY( sa.sin_family==AF_INET && sa.sin_addr.s_addr==adns->ns[ j ] && sa.sin_port==fd_ushort_bswap( fd_adns_ns_port ) ) ) break;
     317           0 :     }
     318           0 :     if( FD_UNLIKELY( j==adns->ns_cnt ) ) continue;
     319             : 
     320             :     /* Must be a standard-query response */
     321           0 :     if( FD_UNLIKELY( (answer[ 2 ]&0x80)!=0x80 || (answer[ 2 ]&0x78)!=0 ) ) continue;
     322             : 
     323           0 :     fd_adns_req_t * req = NULL;
     324           0 :     for( ulong i=0UL; i<adns->max; i++ ) {
     325           0 :       if( FD_LIKELY( adns->reqs[ i ].state==REQ_STATE_PENDING && adns->reqs[ i ].query[ 0 ]==answer[ 0 ] && adns->reqs[ i ].query[ 1 ]==answer[ 1 ] ) ) {
     326           0 :         req = &adns->reqs[ i ];
     327           0 :         break;
     328           0 :       }
     329           0 :     }
     330           0 :     if( FD_UNLIKELY( !req ) ) continue;
     331           0 :     if( FD_UNLIKELY( rlen<req->qlen ) ) continue;
     332           0 :     if( FD_UNLIKELY( answer[ 4 ]!=req->query[ 4 ] || answer[ 5 ]!=req->query[ 5 ] ) ) continue;
     333           0 :     if( FD_UNLIKELY( memcmp( answer+12, req->query+12, (ulong)req->qlen-12UL ) ) ) continue;
     334             : 
     335             :     /* FORMERR/NOTIMP from an EDNS0-unaware server: resend plain.
     336             :        Guarantee a send remains, else attempts:1 expires the request
     337             :        at the sends_left check instead of retrying. */
     338           0 :     if( FD_UNLIKELY( ((answer[ 3 ]&15)==1 || (answer[ 3 ]&15)==4) && !req->no_edns ) ) {
     339           0 :       req->no_edns        = 1;
     340           0 :       req->sends_left     = fd_uint_max( req->sends_left, 1U );
     341           0 :       req->deadline_nanos = 0L; /* due immediately */
     342           0 :       continue;
     343           0 :     }
     344             : 
     345           0 :     int truncated = !!(answer[ 2 ]&2) | !!(mh.msg_flags&MSG_TRUNC);
     346             : 
     347           0 :     int cnt = fd_dns_ip4_answer( answer, (ulong)rlen, req->result.addrs, FD_ADNS_ADDR_MAX );
     348           0 :     if( FD_UNLIKELY( cnt==FD_EAI_AGAIN && !truncated ) ) continue; /* SERVFAIL: leave pending for retry */
     349             :     /* Truncated and nothing parseable: with EDNS0 advertising 1232B
     350             :        this means a pathological answer; the leading records we need
     351             :        (up to FD_ADNS_ADDR_MAX) essentially always fit, so this is a
     352             :        terminal failure rather than a TCP fallback. */
     353           0 :     if( FD_UNLIKELY( cnt<=0 && truncated ) ) cnt = FD_EAI_FAIL;
     354             : 
     355           0 :     if( FD_LIKELY( cnt>0 ) ) req->result.addr_cnt = (ulong)cnt;
     356           0 :     else                     req->result.err      = cnt;
     357           0 :     req->state = REQ_STATE_DONE;
     358           0 :     adns->pending_cnt--;
     359           0 :   }
     360           0 : }
     361             : 
     362             : long
     363           0 : fd_adns_next_deadline( fd_adns_t const * adns ) {
     364           0 :   long next = LONG_MAX;
     365           0 :   if( FD_LIKELY( !adns->pending_cnt ) ) return next;
     366             : 
     367           0 :   for( ulong i=0UL; i<adns->max; i++ ) {
     368           0 :     fd_adns_req_t const * req = &adns->reqs[ i ];
     369           0 :     if( FD_LIKELY( req->state!=REQ_STATE_PENDING ) ) continue;
     370             : 
     371           0 :     next = fd_long_min( next, req->deadline_nanos );
     372           0 :   }
     373             : 
     374           0 :   return next;
     375           0 : }
     376             : 
     377             : int
     378             : fd_adns_advance( fd_adns_t *        adns,
     379             :                  long               now,
     380           0 :                  fd_adns_result_t * result ) {
     381           0 :   pending_io( adns, now );
     382             : 
     383           0 :   if( FD_LIKELY( !adns->active_cnt ) ) return 0;
     384             : 
     385           0 :   for( ulong i=0UL; i<adns->max; i++ ) {
     386           0 :     fd_adns_req_t * req = &adns->reqs[ i ];
     387           0 :     if( FD_LIKELY( req->state!=REQ_STATE_DONE ) ) continue;
     388           0 :     *result    = req->result;
     389           0 :     req->state = REQ_STATE_IDLE;
     390           0 :     adns->active_cnt--;
     391           0 :     return 1;
     392           0 :   }
     393           0 :   return 0;
     394           0 : }

Generated by: LCOV version 1.14