inet_res(3)

πŸ“¦
version 29.0.6 Β· 3 versions
, in ubuntu stonking

πŸ¦‹ inet_res β€” the Erlang manual page, bestie edition πŸ¦‹

okay so this is inet_res(3) from the Erlang Module Definition section, and we are gonna get through it together, no stress.


✨ NAME ✨

inet_res β€” a rudimentary DNS client. yes, rudimentary, she said it herself, we're being honest here πŸ’…


🌸 DESCRIPTION 🌸

so this module does DNS name resolving, and it points that at recursive name servers. that's the whole vibe.

also go look at the ERTS User's Guide: Inet Configuration if you wanna know how to set up an Erlang runtime system for IP communication, and how to turn this DNS client on by defining <purple>'dns'</purple> as a lookup method. once you do that, this DNS client becomes the backend for the resolving functions over in inet. slay.

and here's a cute thing: this DNS client can resolve DNS records even if it's not being used for normal name resolving in the node. she contains multitudes πŸ’–

WARNING 🚨 β€” this is NOT a full-fledged resolver. it's just a DNS client that relies on being in a trusted network, shielded from DNS reply spoofing by firewalls, and on only asking trusted recursive name servers. if the Crypto application is loaded, this implementation does its best to not get fooled by spoofed DNS replies. but ngl that's still a meager remedy compared to using encrypted and signed DNS methods like TSIG or DNSSEC, the way full-fledged resolvers do. so like… know that going in 😭
NOTE 🫢 β€” if the Crypto application is loaded, this resolver client uses cryptographically random transaction IDs and port numbers, which should make the probability of a successfully brute-force spoofed reply too low to be a usable exploit. an Erlang node installed in an exposed network environment should already have Crypto loaded anyway, like, for running the SSL application for example. if that's not your situation, you can explicitly load Crypto with application:load(crypto).

πŸ“‘ Name Resolving πŸ“‘

UDP queries are the default, unless resolver option usevc is true, which forces TCP queries. and if the query is too large for UDP, TCP gets used instead. for regular DNS queries, 512 bytes is the size limit.

when EDNS is enabled (that's resolver option edns set to the EDNS version β€” so 0 instead of false), resolver option udp_payload_size sets the payload size limit. if a name server replies with the TC bit set (that's truncation, bestie), indicating the answer is incomplete, the query is retried towards the same name server using TCP. resolver option udp_payload_size also sets the advertised size for the maximum allowed reply size, if EDNS is enabled β€” otherwise the name server uses the limit 512 bytes. if the reply is larger, it gets truncated, forcing a TCP requery. it's giving "square up, try again" πŸ”₯

for UDP queries, resolver options timeout and retry control retransmission. each name server in the nameservers list is tried with a time-out of timeout/retry. then all name servers are tried again, doubling the time-out, for a total of retry times.

but hold on β€” before all name servers are tried again, there's a (user configurable) time-out: servfail_retry_timeout. the whole point of this is to prevent the new query from being handled by a server's servfail cache (a client that's too eager will literally only get what's in the servfail cache πŸ™ˆ). and if there's too little time left of the resolver call's time-out to do a retry, the resolver call may return before the call's time-out has expired. so, heads up on that.

for queries not using the search list: if the query to all nameservers results in {error,nxdomain} or an empty answer, the same query is tried for alt_nameservers.

and finally β€” if randomization of transaction ID:s and port numbers is too costly for an Erlang node in a trusted network shielded from DNS reply spoofing, the legacy behaviour (reuse the UDP socket on retries, use sequential transaction ID:s) can be configured by setting resolver option random to false. living in the past is a choice and it's available πŸ’€


🧚 Resolver Types 🧚

the following data types concern the resolver β€” they're all waiting for you down in DATA TYPES below πŸ“œ


🌈 DNS Types 🌈

the following data types concern the DNS client β€” also down in DATA TYPES, keep scrolling bestie πŸ’–


⭐ Example ⭐

this access functions example shows how lookup/3 can be implemented using resolve/3 from outside the module:

example_lookup(Name, Class, Type) ->
    case inet_res:resolve(Name, Class, Type) of
        {ok,Msg} ->
            [inet_dns:rr(RR, data)
             || RR <- inet_dns:msg(Msg, anlist),
                 inet_dns:rr(RR, type) =:= Type,
                 inet_dns:rr(RR, class) =:= Class];
        {error,_} ->
            []
     end.

πŸ‘‘ DATA TYPES πŸ‘‘

dns_class() :: in | chaos | hs | any.

dns_data() ::
          dns_name() |
          inet:ip4_address() |
          inet:ip6_address() |
          {MName :: dns_name(),
           RName :: dns_name(),
           Serial :: integer(),
           Refresh :: integer(),
           Retry :: integer(),
           Expiry :: integer(),
           Minimum :: integer()} |
          {inet:ip4_address(), Proto :: integer(), BitMap :: binary()} |
          {CpuString :: string(), OsString :: string()} |
          {RM :: dns_name(), EM :: dns_name()} |
          {Prio :: integer(), dns_name()} |
          {Prio :: integer(),
           Weight :: integer(),
           Port :: integer(),
           dns_name()} |
          {Order :: integer(),
           Preference :: integer(),
           Flags :: string(),
           Services :: string(),
           Regexp :: string(),
           dns_name()} |
          [string()] |
          binary().

DNS record data (content), periodt. πŸ’…

the basic type of each data element is specified in this type.

ngl, Regexp is a string/0 that's UTF-8. the other string/0s are actually Latin-1 strings. yes, that's a whole thing, and yes, you needed to know it.


dns_msg() :: term().

a DNS message. ✨

this is the start of a hierarchy of opaque data structures that can be examined with access functions in inet_dns, which return lists of {Field,Value} tuples. the arity 2 functions return the value for a specified field. so basically: vibes in, tuples out.

dns_msg() = DnsMsg
    inet_dns:msg(DnsMsg) ->
        [ {header, dns_header()}
        | {qdlist, dns_query()}
        | {anlist, dns_rr()}
        | {nslist, dns_rr()}
        | {arlist, dns_rr()} ]
    inet_dns:msg(DnsMsg, header) -> dns_header() % for example
    inet_dns:msg(DnsMsg, Field) -> Value

dns_header() = DnsHeader
    inet_dns:header(DnsHeader) ->
        [ {id, integer()}
        | {qr, boolean()}
        | {opcode, query | iquery | status | integer()}
        | {aa, boolean()}
        | {tc, boolean()}
        | {rd, boolean()}
        | {ra, boolean()}
        | {pr, boolean()}
        | {rcode, integer(0..16)} ]
    inet_dns:header(DnsHeader, Field) -> Value

query_type() = axfr | mailb | maila | any | dns_rr_type()

dns_query() = DnsQuery
    inet_dns:dns_query(DnsQuery) ->
        [ {domain, dns_name()}
        | {type, query_type()}
        | {class, dns_class()} ]
    inet_dns:dns_query(DnsQuery, Field) -> Value

dns_rr() = DnsRr
    inet_dns:rr(DnsRr) -> DnsRrFields | DnsRrOptFields
    DnsRrFields = [ {domain, dns_name()}
                  | {type, dns_rr_type()}
                  | {class, dns_class()}
                  | {ttl, integer()}
                  | {data, dns_data()} ]
    DnsRrOptFields = [ {domain, dns_name()}
                     | {type, opt}
                     | {udp_payload_size, integer()}
                     | {ext_rcode, integer()}
                     | {version, integer()}
                     | {z, integer()}
                     | {data, dns_data()} ]
    inet_dns:rr(DnsRr, Field) -> Value

and there's an information function for the types above πŸŽ€

inet_dns:record_type(dns_msg()) -> msg;
inet_dns:record_type(dns_header()) -> header;
inet_dns:record_type(dns_query()) -> dns_query;
inet_dns:record_type(dns_rr()) -> rr;
inet_dns:record_type(_) -> undefined.

so, inet_dns:(inet_dns:record_type(X))(X) converts any of these data structures into a {Field,Value} list. iconic, honestly. 🌟


dns_name() :: string().

a string with no adjacent dots. that's the whole rule, don't test her.


dns_rr_type() ::
          a | aaaa | caa | cname | gid | hinfo | ns | mb | md | mg |
          mf | minfo | mx | naptr | null | ptr | soa | spf | srv | txt |
          uid | uinfo | unspec | uri | wks.

hostent() ::
          inet:hostent() |
          {hostent,
           H_name :: inet:hostname(),
           H_aliases :: [inet:hostname()],
           H_addrtype :: dns_rr_type(),
           H_length :: non_neg_integer(),
           H_addr_list :: [dns_data()]}.

extended variant of inet:hostent/0. πŸ¦‹

it allows dns_rr_type/0 for the #hostent{}.h_addrtype field, and [dns_data/0] for the #hostent{}.h_addr_list field. so much more room for activities.


nameserver() :: {inet:ip_address(), Port :: 1..65535}.

res_error() ::
          formerr | qfmterror | servfail | nxdomain | notimp | refused |
          badvers | timeout.

res_option() ::
          {alt_nameservers, [nameserver()]} |
          {edns, 0 | false} |
          {inet6, boolean()} |
          {nameservers, [nameserver()]} |
          {recurse, boolean()} |
          {retry, integer()} |
          {timeout, integer()} |
          {udp_payload_size, integer()} |
          {dnssec_ok, boolean()} |
          {usevc, boolean()} |
          {random, boolean()} |
          {nxdomain_reply, boolean()}.

okay that's part 1, and there's more coming, so do NOT go anywhere β€” part 2 is right behind me 🫢

✨ FUNCTIONS ✨

okay bestie, this is the good part πŸ’… every function you can call, with its whole signature, spelled out character for character. no skipping, no "and so on", we're doing all of it 🫢


getbyname

getbyname(Name, Type, Opts, Timeout) ->
                     {ok, Hostent} | {error, Reason}
                     when
                         Name :: dns_name(),
                         Type :: dns_rr_type(),
                         Opts :: [Opt],
                         Opt :: res_option() | verbose | atom(),
                         Timeout :: timeout(),
                         Hostent :: inet:hostent() | hostent(),
                         Reason :: inet:posix() | res_error().

<cute>Resolve a DNS query.</cute> that's the whole job description, iconic.

It resolves a DNS query of the given Type for the given host, of class in. On success β€” when you're resolving a Type = a or aaaa DNS record β€” you get back a #hostent{} record where #hostent.h_addrtype = inet or inet6 respectively. peek at inet:hostent/0 for the shape πŸ’–

When you're resolving other Type = dns_rr_type():s (still of class in), you also get a #hostent{} record back, but this time dns_rr_type/0 shows up in #hostent.h_addrtype, and the resolved dns_data/0 lives in #hostent.h_addr_list. the hostent/0 docs have the deets 🌸

This function uses the resolver option search, which is literally a list of domain names. Here's the tea on how it gets used:

  • if the name you're resolving has no dots, it gets prepended to each domain name in the search list, and those are tried in order
  • if the name does have dots, it's tried first as an absolute name, and if that fails, the search list kicks in
  • if the name has a trailing dot, it's supposed to be absolute, so the search list is not used. periodt.

For all the Opts lore, go read resolve/5 πŸ‘‘


gethostbyaddr

gethostbyaddr(Address, Timeout) -> {ok, Hostent} | {error, Reason}
                           when
                               Address :: inet:ip_address(),
                               Timeout :: timeout(),
                               Hostent :: inet:hostent(),
                               Reason :: inet:posix() | res_error().

Backend function used by inet:gethostbyaddr/1. it's the plumbing behind the curtain 🧚

gethostbyaddr(Address, Opts, Timeout) ->
                           {ok, Hostent} | {error, Reason}
                           when
                               Address :: inet:ip_address(),
                               Opts :: [Opt],
                               Opt :: res_option() | verbose | atom(),
                               Timeout :: timeout(),
                               Hostent :: inet:hostent(),
                               Reason :: inet:posix() | res_error().

Also a backend function used by inet:gethostbyaddr/1. same energy, now with Opts πŸŽ€


gethostbyname

gethostbyname(Name) -> {ok, Hostent} | {error, Reason}
                           when
                               Name :: dns_name(),
                               Hostent :: inet:hostent(),
                               Reason :: inet:posix() | res_error().

Backend functions used by inet:gethostbyname/1,2.

Here's the vibe check: if the resolver option inet6 is true, this is equivalent to gethostbyname(Name, inet6, infinity), otherwise it's gethostbyname(Name, inet, infinity). lowkey just picking a family for you πŸ’œ

gethostbyname(Name, Family, Opts, Timeout) ->
                           {ok, Hostent} | {error, Reason}
                           when
                               Name :: dns_name(),
                               Family :: inet:address_family(),
                               Opts :: [Opt],
                               Opt :: res_option() | verbose | atom(),
                               Timeout :: timeout(),
                               Hostent :: inet:hostent(),
                               Reason :: inet:posix() | res_error().

Backend functions used by inet:gethostbyname/1,2. (yes, again, they're the same two functions.)

This one uses the resolver option search in exactly the same way as getbyname/2,3 🌈


lookup

lookup(Name, Class, Type, Opts, Timeout) -> [dns_data()]
                    when
                        Name :: dns_name() | inet:ip_address(),
                        Class :: dns_class(),
                        Type :: dns_rr_type(),
                        Opts :: [res_option() | verbose],
                        Timeout :: timeout().

Look up DNS data. simple as that.

It resolves the DNS data for the record Name of the specified Type and Class. On success it filters out the answer records that have the correct Class and Type, and hands you back a list of their data fields. so heads up: a lookup for type any gives you an empty answer πŸ’€ because the answer records have specific types, and none of those are any. an empty answer, or a lookup that just failed, returns an empty list.

It calls resolve/* with the same arguments and then filters the result β€” so yeah, the Opts storyline is the one written up for those functions ✨


nnslookup

nnslookup(Name, Class, Type, Nameservers, Timeout) ->
                     {ok, dns_msg()} | {error, Reason}
                     when
                         Name :: dns_name() | inet:ip_address(),
                         Class :: dns_class(),
                         Type :: dns_rr_type(),
                         Timeout :: timeout(),
                         Nameservers :: [nameserver()],
                         Reason :: inet:posix().

<cute>Resolve a DNS query.</cute>

It's like nslookup/4 but it calls resolve/5 with both the arguments Opts = [{nameservers, Nameservers}] and Timeout. cute, efficient, we love her πŸŽ€


nslookup

nslookup(Name, Class, Type, Timeout) ->
                    {ok, dns_msg()} | {error, Reason}
                    when
                        Name :: dns_name() | inet:ip_address(),
                        Class :: dns_class(),
                        Type :: dns_rr_type(),
                        Timeout :: timeout(),
                        Reason :: inet:posix() | res_error();
                (Name, Class, Type, Nameservers) ->
                    {ok, dns_msg()} | {error, Reason}
                    when
                        Name :: dns_name() | inet:ip_address(),
                        Class :: dns_class(),
                        Type :: dns_rr_type(),
                        Nameservers :: [nameserver()],
                        Reason :: inet:posix() | res_error().

<cute>Resolve a DNS query.</cute> (we say this a lot here, it's the family motto fr)

This function is a legacy wrapper to resolve/5 that simplifies error matching: {error, {Reason, _}} gets squished down into {error, Reason} or {error, einval}. the glow-up of error messages πŸ’…

  • when you call it with the Timeout argument, it calls resolve/5 with Opts = []
  • when you call it with the Nameservers argument, it calls resolve/5 with Opts = [{nameservers, Nameservers}] and Timeout = infinity

resolve

resolve(Name, Class, Type, Opts, Timeout) ->
                       {ok, dns_msg()} | Error
                       when
                           Name :: dns_name() | inet:ip_address(),
                           Class :: dns_class(),
                           Type :: dns_rr_type(),
                           Opts :: [Opt],
                           Opt :: res_option() | verbose | atom(),
                           Timeout :: timeout(),
                           Error ::
                               {error, Reason} | {error, {Reason, dns_msg()}},
                           Reason :: inet:posix() | res_error().

<cute>Resolve a DNS query.</cute> the main character of this whole section, honestly πŸ‘‘

It resolves a DNS query for the specified Type, Class, and Name, into a DNS message that possibly contains Resource Records. that returned dns_msg/0 can be poked at with the access functions in inet_db, as described in the section on DNS Types πŸ¦‹

If Name is an ip_address(), the domain name that gets queried is generated as the standard reverse name β€” .IN-ADDR.ARPA. for an IPv4 address, or .IP6.ARPA. for an IPv6 address. and in that case, you most probably want to use Class = in and Type = ptr, but it is not done automatically. you gotta say it yourself, bestie πŸ™ˆ

Opts overrides the corresponding resolver options. If the option nameservers is specified, it's assumed to be the complete list of name servers, so the resolver option alt_nameserves gets ignored. however β€” if the option alt_nameserves is also specified to this function, then it is used πŸŽ€

Option verbose (or rather {verbose,true}) causes diagnostics printout through io:format/2 of queries, replies, retransmissions, and so on, similar to utilities such as dig and nslookup. it's giving very chatty, very extra ✨

Option nxdomain_reply (or rather {nxdomain_reply, true}) causes NXDOMAIN errors from DNS servers to be returned as {error, {nxdomain, dns_msg()}}. the dns_msg/0 contains the additional sections that were included by the answering server. this is mainly useful to inspect the SOA record to get the TTL for negative caching πŸ“

If Opt is any atom, it's interpreted as {Opt,true} β€” unless the atom string starts with "no", which flips the interpretation to {Opt,false}. for example, usevc is an alias for {usevc, true} and nousevc is an alias for {usevc, false} πŸ’–

⚠️ okay important one: option inet6 has no effect on this function. you probably want to use Type = a | aaaa instead. don't do the thing, do the other thing πŸ’€

🌸 that's the whole page, bestie 🌸

no cap, that was Ericsson AB's manual page for inet_res(3), from kernel 11.0.3. go forth and resolve things responsibly, you absolute icon πŸ’…πŸ‘‘πŸ«Ά

πŸ“¦ where this page comes from