In practical applications, a problem was found where, in certain countries or regions with specific ISPs, the program sometimes fails to obtain the server IP due to DNS timeout. This happens because the ISPâs DNS experiences slow recursive queries, leading to DNS request timeouts.
The solution for users is: Please do not use the DNS server automatically assigned by the ISP, but rather switch to 8.8.8.8, and it will be resolved.
However, configuring it this way for users is too cumbersome and not user-friendly. So, I started thinking: Can I implement my own DNS service, where if the ISPâs DNS request times out or fails, those DNS requests can be made directly to 8.8.8.8 internally?
If you were to use gethostbyname()
and getaddrinfo()
as a solution to this problem, the approach would be to modify the contents of /etc/resolve.conf
. But this is not the correct method, as it is imprecise and can affect other DNS requests in the system. A feasible solution is to construct DNS requests on our own and parse them independently to obtain the IP information we need.
This article is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Originally published at: https://segmentfault.com/a/1190000009369381, which is also in the authorâs own column.
Reference
Even though DNS is considered a relatively simple protocol within network interconnection, none of the reference materials seem to cover the knowledge points I needed for such a straightforward requirementâŠ
I did my own packet capturing as well, and when capturing packets, itâs advised not to directly query authoritative DNS servers, but rather aim at servers providing DNS relay such as gateways, routers, etc., to gather more information than whatâs provided by the last reference material below.
- âInternetworking with TCP/IP (5th Edition) â Principles, Protocols, and Architecture (5th Edition)â, Douglas E. Comer
- âComputer Networks (5th Edition)â, Andrew S. Tannenbaum, David J. Wetherall: The legendary Professor Tanenbaum!
- DNS Protocol
- DNS Reference Information: Contains descriptions of various types
- Domain Name System (DNS) Parameters: A complete collection of various parameters
- DNS Name Notation and Message Compression Technique
- RFC-1035
- Understanding DNS Messages
- DNS Message Parsing: This article also thoroughly explains DNS message structures, with illustrations for those who prefer visuals.
- Analyzing DNS Protocol with WireShark
Basic Concepts of DNS
Briefly organize some points related to this article:
The essence of DNS is that it invented a hierarchical, domain-based naming scheme and implemented it with a distributed database system. The primary function of DNS is to map host names to IP addresses.
The initiator of DNS resolution is usually the client end in the Internet Server/Client model (hereinafter referred to as the client end, referring to the DNS resolution initiator). Most C language client ends now use getaddrinfo()
. It used to be gethostbyname()
which is not recommended anymore due to certain reasons and it only supports IPv4.
In DNS resolution, the DNS server should open port 53. When the client makes a request, the server returns not just the IP information, but resource records associated with the domain name.
We cannot differentiate between a domain name and a host name based only on a domain URL. The total length of a domain name should be less than or equal to 255 bytes
, and each segment of the domain name must be less than or equal to 63 bytes
.
DNS Message Format
The format of DNS requests and responses are similar, so they are not explained separately. Starting from the main part of the UDP packet, the structure of a DNS message is as follows:
Data Type |
Name in Ethereal |
Description |
---|---|---|
|
Transaction ID |
Identifier. Explained below |
|
Flags |
Flags. Explained below |
|
Questions |
Number of questions in the query list |
|
Answer RRs |
Number of (direct) answers |
|
Authority RRs |
Number of authority records (only in response packet) |
|
Additional RRs |
Number of additional information (only in response packet) |
variable |
Queries |
Main content of the request. Only present in the request packet. The response packet also contains the original request data |
variable |
Answers |
Main content of the response |
variable |
Authortative name servers |
Name server data for the domain |
variable |
Additional records |
Additional data |
- Transaction ID: This is an identifier specified by the client; the DNS server returns this field as is, allowing the client to distinguish between different DNS requests.
- RR: Abbreviation for Resource Record
Flags
The 16 bits value is structured as follows (in order, bit number, Ethereal name, description):
- Bit 15, Response: 0 indicates a query, 1 indicates a response (query/response)
- Bit 14~11, Opcode: Query typeâapplicable to both request and response packets:
0
: Standard query (most common)1
: Inverse query2
: Server status request3
: Notify4
: Update (seemingly used in DDNS?)- Bit 10, Authoritative: Used in response packets to indicate if the server is an authoritative domain server
- Bit 9, Truncated: Whether the message has been truncated. Used in both send and receive packets
- Bit 8, Recursion desired: Used in both send and receive packets, indicates whether recursion is needed. Itâs best to set it to 1 as a client; otherwise, DNS will not perform recursive queries, leading to incomplete data retrieval
- Bit 7, Recursion available: Used in response packets to indicate if the server can perform recursive queries
- Bit 6: Ethereal says itâs a reserve bit, while books indicate itâs whether the data is authenticatedâconfirmation needed
- Bit 5, Answer authenticated: Indicates if the data has been authenticated by the server (seemingly 0 in captured packets)
- Bit 4, Reserved
- Bit 3~0, Reply code: Response status codes are as follows (see Microsoft documentation âDNS update message flags fieldâ section):
0
: OK1
: Query format error2
: Server internal error3
: Name does not exist4
: This error code is not supported5
: Request refused6
: Name appears when it should not (what?)7
: RR set does not exist8
: RR set should exist but does not (what?)9
: Server lacks authority over zone10
: Name is not in zone
Format of Resource Records (RR)
The format of each RR is as follows:
Data Type |
Name in Ethereal |
Description |
---|---|---|
variable |
Name |
Domain name of the resourceâalready mentioned earlier |
|
Type |
Type. Explained below |
|
Class |
Mostly 0x0001, representing |
|
Time to Live |
TTL seconds |
|
Data length |
Length of the remaining part of this RR |
variable |
Main data of the RR |
If it is request data, then TTL, Data Length, and main RR data are not necessary.
Most Type
values are defined in RFC-1035, with extra definitions elsewhere (e.g., IPv6). The ones Iâll use include:
1
: âA
â, indicates IPv4 address2
: âNS
â, name of the domain name server28
: âAAAA
â, indicates IPv6 address5
: âCNAME
â, canonical name, often followed by a set of A and AAAA
Domain Name Compression Display
This section directly references RFC-1035 section â4.1.4. Message Compressionâ.
In RRsâ Name field, there are three representations (unofficially classified by me):
Full Domain Name Representation
For example, the full domain name âwww.google.comâ requires 16 bytes as follows:
B0 |
B1 |
B2 |
B3 |
B4 |
B5 |
B6 |
B7 |
B8 |
B9 |
B10 |
B11 |
B12 |
B13 |
B14 |
B15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note that it does not simply copy Googleâs URL using a char *
string, but rather splits each segment. In this example, the domain name is split into three segments: www
, google
, com
. Each segment is preceded by one byte indicating the byte length of the following segment. When \0
is encountered, it signifies the end of the data (which is slightly different from the meaning of char *
\0
, although their forms are the same).
Labeled Representation
As mentioned earlier, each domain segment should not exceed 63 bytes, thus the highest two bits
( 0xC0
) of the byte indicating segment length must be 0. This leads to the second use case here.
This representation is like a pointer, referring to a segment of the domain in the DNS message. When parsing an RR data segment, size determination logic is:
- if the highest two bits are 00, it indicates the first representation
- if the highest two bits are 11, it indicates a compressed representation. This byte, with its highest two bits cleared revealing the remaining 6 bits, along with the next 8 bits totaling 14 bits of data, points to a segment of the domain in the DNS message (not necessarily a full domain, see the third case).
For example, 0xC150 indicates it refers to the domain segment found at offset = 0x0150
in the DNS packet. 0x0150 is derived from clearing the highest two bits of 0xC150.
Mixed Representation
This is essentially a combination of the previous two representations. For instance, assuming the domain segment representing www.google.com
in its entirety is at offset 0x20, the following usages apply:
0xC020
: naturally refers towww.google.com
.0xC024
: starts from the second segment of the full domain, referring togoogle.com
.0x016DC024
: where0x6d
represents the characterm
;0x016D
alone refers tom
; the second segment0xC024
refers togoogle.com
, thus representingm.google.com
.
Analysis Tools
In addition to Ethereal, recommended analysis tools include:
- Wireshark: A packet capturing tool
- BIND: A DNS server that can be installed in your development environment to observe and generate DNS responses. FTP address:
ftp.isc.org/isc/bind9/
, Simple tutorial
Code Implementation
The code implementation is in a branch I used to study epoll()
. GitHub repository here, licensed under LGPL.
In terms of logic, itâs quite straightforwardâimplementing according to the principles mentioned above. Most of the code is unrelated to this article; just look at the AMCDns.c / h files.
This code can completely replace the blocking getaddrinfo()
function, and it can also integrate into asynchronous I/O libraries. The usage process is as follows:
- Call
socket()
to create a UDP socket andbind()
- Call
AMCDns_GetDefaultServer()
to get the systemâs default DNS server configuration - If not using the systemâs default DNS server, specify it using the struct addrinfo type.
- Call
AMCDns_SendRequest()
to request IP information for a specified domain - Call
AMCDns_RecvAndResolve()
to get either summary or full response. - Call
AMCDns_FreeResult()
to clear DNS response data to avoid memory leaks Close()
the socket