← System Programming DUJINLABS.COM

Socket · Linux userspace / kernel ABI

getaddrinfo와 이름 해석

DNS만이 아닌 NSS 정책, IPv4/IPv6 address list, service name, blocking resolver를 connection 시도와 분리합니다.

Series
34 / 37
Build
cc -std=c17 -Wall -Wextra -O2 resolve.c -o resolve
Run
./resolve localhost 80
Kernel
Linux 6.18.37 LTS

getaddrinfo 결과의 첫 주소만 연결하면 충분한가?

getaddrinfo는 DNS 전용 함수가 아니다. /etc/nsswitch.conf 정책에 따라 files, DNS, mDNS 같은 source를 조회하고 address family와 socket type 조건에 맞는 addrinfo list를 만든다.

반환 순서는 destination address selection policy를 반영하지만 첫 주소가 도달 가능하다는 보장은 없다. 각 result를 시도하고 전체 timeout을 관리하거나 IPv6/IPv4 candidate를 병렬화한다.

구조 그림

그림 1. 하나의 이름에서 만들어지는 connection candidate 목록
순서familysockaddr시도 상태소유 fd 1AF_INET6[2001:db8::20]:443SYN-SENT · 250 msfd 62AF_INET192.0.2.20:44350 ms 뒤 시작fd 73AF_INET6[2001:db8::21]:443대기 후보없음winnerAF_INET192.0.2.20:443ESTABLISHEDfd 7cleanupAF_INET6첫 시도 취소closefd 6 해제

addrinfo 결과는 후보 목록이다. 첫 행이 실패하거나 지연되면 다른 family/address를 전체 deadline 안에서 시도한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
host/service 문자열 입력
NSS files/dns 등 조회
addrinfo list family/type/protocol
connect attempts 각 sockaddr 사용
selected peer 성공 fd + canonical info

이름 해석 결과와 실제 network connection 결과를 다른 cache/state로 관리한다. address list 하나가 service instance 하나를 뜻하지 않는다.

그림 3. 커널 내부에서 지나가는 주요 지점
resolver libc NSS module
DNS socket UDP/TCP query 가능
routing candidate별 route
connect family별 socket
peer 4/6 tuple 확정

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
net/socket.c __sys_connect() resolver가 만든 sockaddr를 실제 socket operation에 사용
net/ipv6/af_inet6.c inet6_create(), inet6_bind() IPv6 socket family 경로
net/ipv4/af_inet.c inet_create() IPv4 socket family 경로와 protocol 선택

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 resolve.c -o resolve
01#define _POSIX_C_SOURCE 200809L
02#include <arpa/inet.h>
03#include <netdb.h>
04#include <stdio.h>
05#include <string.h>
06
07int main(int argc, char **argv)
08{
09    if (argc != 3)
10        return 2;
11    struct addrinfo hints;
12    memset(&hints, 0, sizeof(hints));
13    hints.ai_family = AF_UNSPEC;
14    hints.ai_socktype = SOCK_STREAM;
15    hints.ai_protocol = IPPROTO_TCP;
16
17    struct addrinfo *results;
18    int error = getaddrinfo(argv[1], argv[2], &hints, &results);
19    if (error != 0) {
20        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error));
21        return 1;
22    }
23    for (const struct addrinfo *item = results; item != NULL; item = item->ai_next) {
24        char host[NI_MAXHOST], service[NI_MAXSERV];
25        int rc = getnameinfo(item->ai_addr, item->ai_addrlen,
26            host, sizeof(host), service, sizeof(service),
27            NI_NUMERICHOST | NI_NUMERICSERV);
28        if (rc == 0)
29            printf("family=%d %s:%s\n", item->ai_family, host, service);
30    }
31    freeaddrinfo(results);
32    return 0;
33}

코드 조각별 설명

실제 코드 12행memset(&hints, 0

addrinfo의 사용하지 않는 field와 padding을 0으로 시작해 명시한 조건만 resolver에 전달한다.

실제 코드 13행AF_UNSPEC

IPv4/IPv6 모두 허용한다. AI_ADDRCONFIG 같은 flag는 host interface 구성에 따라 결과를 줄일 수 있어 요구사항에 맞춰 선택한다.

실제 코드 18행int error = getaddrinfo

반환값은 errno가 아니라 EAI_* 코드다. gai_strerror로 해석하고 EAI_SYSTEM일 때만 errno가 추가 의미를 가진다.

실제 코드 23행item = item->ai_next

연결 가능한 candidate list 전체를 순회한다. ai_addr와 ai_addrlen을 해당 family socket connect에 그대로 사용한다.

실제 코드 27행NI_NUMERICHOST | NI_NUMERICSERV

출력 단계에서 reverse DNS와 service lookup을 다시 하지 않고 numeric address/port만 format한다.

세부 동작

01

NSS lookup은 blocking 작업일 수 있다

getaddrinfo 호출 thread는 resolver timeout, NSS module, network 응답을 기다릴 수 있다. event loop thread에서 직접 호출하면 모든 connection 처리가 멈춘다.

전용 resolver pool, getaddrinfo_a 같은 비동기 확장, application DNS client를 latency 요구에 맞춰 선택한다.

02

AI_PASSIVE와 wildcard address

server bind용 NULL node + AI_PASSIVE는 wildcard address를 만든다. AI_PASSIVE가 없으면 loopback address가 나올 수 있다. client와 server hint를 같은 helper로 섞지 않는다.

IPv6 wildcard socket의 v4-mapped 동작은 IPV6_V6ONLY 설정과 OS 정책에 따라 확인한다.

03

cache TTL과 connection lifetime은 다르다

DNS record TTL이 끝나도 이미 established된 TCP connection이 자동으로 새 address로 이동하지 않는다. resolver cache, connection pool, retry 정책의 수명을 각각 둔다.

negative cache와 deployment address rotation 때 stale pool을 어떻게 drain할지 정한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
addrinfo listgetaddrinfo가 할당하고 freeaddrinfo에서 전체 해제한다family, socktype, protocol, sockaddr
NSS query stateresolver 호출 동안 source별로 생기고 결과/오류 뒤 정리된다timeout, search domain, cache
connection candidateaddrinfo entry마다 socket/deadline을 만들고 성공 또는 실패에서 닫는다address, attempt time, error

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
EAI_AGAIN일시 resolver 실패/timeoutNSS source와 retry budget
첫 address에서 오래 멈춤candidate 직렬 connectfamily별 attempt timeline
event loop stallblocking getaddrinfo를 loop thread에서 호출thread stack과 resolver latency

직접 확인

  1. localhost와 실제 hostname에서 /etc/hosts, nsswitch, DNS syscall 차이를 strace로 확인한다.
  2. AF_INET/AF_INET6/AF_UNSPEC hint 결과를 비교하고 각 candidate connect 오류를 기록한다.
  3. resolver worker thread와 result eventfd를 만들어 main epoll loop를 block하지 않게 한다.
실행./resolve localhost 80
추적strace -f -e trace=openat,read,connect,sendto,recvfrom ./resolve localhost 80

원문