suminworld

network

[네트워크] C 네트워크 프로그래밍과 패킷 분석 실습

숨usm 2025. 9. 8. 20:42

UTM Ubuntu 환경에서 순수 C 언어로 견고한 HTTP 클라이언트를 구현하고, tcpdump/Wireshark로 패킷 흐름을 실시간 관찰하는 종합 실습

 

TL;DR

논블로킹 connect(3초) + DNS 다중 IP 순회 + recv 타임아웃(5초) + fallback 메커니즘으로 견고한 HTTP 클라이언트를 구현하고, tcpdump로 TCP 3-way handshake부터 데이터 전송까지 패킷 레벨 검증을 수행합니다.

환경 준비

필요한 패키지 설치

sudo apt update && sudo apt install -y build-essential tcpdump traceroute mtr-tiny ethtool

헤더 파일 및 기본 구조

복사-붙여넣기로 바로 컴파일할 수 있도록 필요한 헤더 파일들:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <netdb.h>
#include <arpa/inet.h>

컴파일 옵션

# 기본 컴파일
gcc -Wall -Wextra -std=c99 tcp_client.c -o tcp_client

# 선택: 경고·디버깅 강화
gcc -Wall -Wextra -O2 -g -std=c99 tcp_client.c -o tcp_client
# (리눅스라면) AddressSanitizer로 런타임 버그 잡아보기
# gcc ... -fsanitize=address,undefined

목차

  1. 환경 구성과 네트워크 이해
  2. 기존 코드의 문제점 분석
  3. 핵심 개념 이해
  4. 코드 개선
  5. 실행 및 결과 분석
  6. 패킷 분석 실습
  7. 고급 확장 및 최적화

환경 구성과 네트워크 이해

개발 환경

  • Host: macOS (MacBook Air)
  • VM: UTM Ubuntu 24.04 Server (VS Code SSH 접속)
  • 네트워크: UTM NAT 환경
  • 도구: GCC, tcpdump, Wireshark

UTM NAT 네트워크 구조 상세 분석

[Ubuntu VM] enp0s1 (192.168.64.8/24) ─NAT─> [macOS] en0/wlan ─> [공유기] ─> 인터넷
                    ↑
          기본게이트웨이: 192.168.64.1 (UTM 가상 라우터)

NAT (Network Address Translation) 특성:

  • VM은 외부로 나가는 연결(outbound) 자유롭게 가능
  • 외부에서 VM으로 직접 접근(inbound)은 기본적으로 차단
  • VM 내부에서는 사설 IP(192.168.64.x)로 보이지만, 외부에서는 맥북의 공인 IP로 보임
  • 포트 매핑: VM의 임의 포트 ↔ 맥북의 임의 포트

네트워크 상태 확인 명령어

# 네트워크 구조 파악
ip route show default
# 결과: default via 192.168.64.1 dev enp0s1 proto dhcp src 192.168.64.8 metric 100

# VM IP 확인  
ip addr show enp0s1
# 결과: inet 192.168.64.8/24 brd 192.168.64.255 scope global dynamic enp0s1

# DNS 설정 확인
resolvectl status
# 현재 DNS 서버: 192.168.64.1 (UTM 제공)

# 기본 연결성 테스트
ping -c 3 8.8.8.8
ping -c 3 google.com

기존 코드의 문제점 분석

전형적인 "취약한" 소켓 코드

// 문제가 많은 기본 버전
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in s = { 
    .sin_family = AF_INET, 
    .sin_port = htons(80) 
};
s.sin_addr.s_addr = inet_addr("93.184.216.34"); // 하드코딩된 단일 IP

// 블로킹 connect - 타임아웃 없음
if (connect(sock, (struct sockaddr*)&s, sizeof(s)) < 0) { 
    perror("connect"); 
    return 1; 
}

// 부분 전송 가능성 무시
const char *req = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
send(sock, req, strlen(req), 0);

// 단순한 수신 - 타임아웃 없음
char buf[4096]; 
int n, total = 0;
while ((n = recv(sock, buf + total, sizeof(buf) - total - 1, 0)) > 0) { 
    total += n; 
}
if (total > 0) { 
    buf[total] = '\0'; 
    printf("%s\n", buf); 
}

문제점 상세 분석

1. 블로킹 connect() 문제

  • 현상: connect()가 완료될 때까지 프로세스가 멈춤
  • 원인: TCP 3-way handshake 완료 대기
  • 실제 시나리오:
    • 방화벽에서 패킷 드롭: 21초 기본 타임아웃
    • 서버 과부하: 몇 분간 대기 가능
    • 네트워크 분할: 무한 대기 가능

2. DNS 해석 한계

  • 현상: inet_addr()로 IP 하드코딩
  • 문제:
    • 도메인 이름 사용 불가
    • 서버 IP 변경 시 코드 수정 필요
    • 로드밸런싱된 서비스 접근 불가

3. 단일 IP 시도 문제

  • 현상: 첫 번째 IP 실패 시 즉시 포기
  • 문제: getaddrinfo()가 반환하는 여러 IP 무시
  • 실제 영향:
    • CDN 서비스: 지리적으로 가까운 서버로 자동 라우팅 실패
    • 내결함성 부족: 일부 서버 장애 시 전체 실패

4. 수신 타임아웃 부재

  • 현상: 서버가 응답을 보내지 않으면 무한 대기
  • 시나리오:
    • 서버가 요청 처리 후 응답 전송 지연
    • 네트워크 중간에서 패킷 손실
    • 서버 크래시로 연결만 유지되고 데이터 없음

핵심 개념 이해

Non-blocking I/O와 select()

블로킹 vs Non-blocking

// 블로킹 모드 (기본)
int result = connect(sock, addr, addrlen);
// 여기서 멈춤! 연결 완료까지 대기

// Non-blocking 모드
fcntl(sock, F_SETFL, O_NONBLOCK);
int result = connect(sock, addr, addrlen);
// 즉시 리턴! result == -1, errno == EINPROGRESS

select() 시스템 콜 이해

// select() 함수 시그니처
int select(int nfds, fd_set *readfds, fd_set *writefds, 
           fd_set *exceptfds, struct timeval *timeout);

// 사용 패턴
fd_set writefds;
FD_ZERO(&writefds);          // 집합 초기화
FD_SET(sock, &writefds);     // 소켓을 집합에 추가

struct timeval timeout = { .tv_sec = 3, .tv_usec = 0 };
int ready = select(sock+1, NULL, &writefds, NULL, &timeout);

if (ready > 0) {
    // 소켓이 쓰기 가능 (연결 완료 또는 에러)
} else if (ready == 0) {
    // 타임아웃
} else {
    // select() 에러
}

TCP 소켓 상태와 에러 처리

SO_ERROR를 통한 연결 결과 확인

int error;
socklen_t len = sizeof(error);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len);

if (error == 0) {
    // 연결 성공
} else {
    // error 값에 따른 실패 원인 분석
    // ECONNREFUSED: 서버가 연결 거부 (포트 닫힘)
    // ETIMEDOUT: 연결 시간 초과
    // EHOSTUNREACH: 호스트 도달 불가
}

DNS와 getaddrinfo() 심화

getaddrinfo() 상세 사용법

struct addrinfo hints = {0};
hints.ai_family = AF_UNSPEC;      // IPv4/IPv6 모두 허용
hints.ai_socktype = SOCK_STREAM;  // TCP만
hints.ai_flags = AI_ADDRCONFIG;   // 시스템 설정에 따라 IPv4/IPv6 필터링

struct addrinfo *result;
int status = getaddrinfo("example.com", "80", &hints, &result);

if (status != 0) {
    fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
    return -1;
}

// result는 연결 리스트 형태로 여러 주소 포함
for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next) {
    // 각 주소에 대해 연결 시도
}

freeaddrinfo(result); // 메모리 해제 필수!

코드 개선

1. 타임아웃 기반 non-blocking connect

static int connect_with_timeout(int sock, const struct sockaddr *sa, 
                               socklen_t slen, int timeout_ms) {
    // 현재 플래그 저장
    int flags = fcntl(sock, F_GETFL, 0);
    if (flags < 0) {
        perror("fcntl F_GETFL");
        return -1;
    }
    
    // non-blocking 모드 설정
    if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("fcntl F_SETFL");
        return -1;
    }

    // 연결 시도
    int result = connect(sock, sa, slen);
    if (result == 0) {
        // 즉시 연결 성공 (주로 localhost)
        fcntl(sock, F_SETFL, flags); // 원래 모드 복원
        return 0;
    }
    
    if (errno != EINPROGRESS) {
        // EINPROGRESS가 아닌 에러 (예: ECONNREFUSED)
        perror("connect");
        fcntl(sock, F_SETFL, flags);
        return -1;
    }

    // select()로 연결 완료 대기
    fd_set writefds, errorfds;
    FD_ZERO(&writefds);
    FD_ZERO(&errorfds);
    FD_SET(sock, &writefds);
    FD_SET(sock, &errorfds);

    struct timeval timeout = {
        .tv_sec = timeout_ms / 1000,
        .tv_usec = (timeout_ms % 1000) * 1000
    };

    int select_result = select(sock + 1, NULL, &writefds, &errorfds, &timeout);
    
    if (select_result < 0) {
        perror("select");
        fcntl(sock, F_SETFL, flags);
        return -1;
    }
    
    if (select_result == 0) {
        // 타임아웃
        errno = ETIMEDOUT;
        fcntl(sock, F_SETFL, flags);
        return -1;
    }

    // 연결 결과 확인
    int sock_error;
    socklen_t len = sizeof(sock_error);
    if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &sock_error, &len) < 0) {
        perror("getsockopt");
        fcntl(sock, F_SETFL, flags);
        return -1;
    }

    if (sock_error != 0) {
        errno = sock_error;
        fcntl(sock, F_SETFL, flags);
        return -1;
    }

    // 성공: 블로킹 모드로 복원
    fcntl(sock, F_SETFL, flags);
    return 0;
}

2. DNS 다중 IP 순회와 내결함성

static int fetch_http(const char *host, const char *port, const char *host_header) {
    printf("[*] DNS 해석: %s:%s\n", host, port);
    
    struct addrinfo hints = {0};
    hints.ai_family = AF_UNSPEC;    // IPv4/IPv6 모두 지원
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_ADDRCONFIG;
    
    struct addrinfo *result;
    int gai_result = getaddrinfo(host, port, &hints, &result);
    if (gai_result != 0) {
        fprintf(stderr, "DNS 해석 실패: %s\n", gai_strerror(gai_result));
        return -1;
    }

    int sock = -1;
    int success = 0;
    
    // 모든 IP 주소에 대해 연결 시도
    for (struct addrinfo *rp = result; rp != NULL; rp = rp->ai_next) {
        // IP 주소 문자열로 변환 (디버깅용)
        char ip_str[INET6_ADDRSTRLEN];
        void *addr_ptr;
        const char *family_str;
        
        if (rp->ai_family == AF_INET) {
            addr_ptr = &((struct sockaddr_in*)rp->ai_addr)->sin_addr;
            family_str = "IPv4";
        } else if (rp->ai_family == AF_INET6) {
            addr_ptr = &((struct sockaddr_in6*)rp->ai_addr)->sin6_addr;
            family_str = "IPv6";
        } else {
            continue; // 지원하지 않는 주소 패밀리
        }
        
        inet_ntop(rp->ai_family, addr_ptr, ip_str, sizeof(ip_str));
        printf("[*] 연결 시도: %s %s\n", family_str, ip_str);

        // 소켓 생성
        sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sock < 0) {
            printf("[!] 소켓 생성 실패: %s\n", strerror(errno));
            continue;
        }

        // 3초 타임아웃으로 연결 시도
        if (connect_with_timeout(sock, rp->ai_addr, rp->ai_addrlen, 3000) == 0) {
            printf("[+] 연결 성공: %s %s\n", family_str, ip_str);
            success = 1;
            break;
        } else {
            printf("[!] 연결 실패: %s %s (%s)\n", 
                   family_str, ip_str, strerror(errno));
            close(sock);
            sock = -1;
        }
    }
    
    freeaddrinfo(result);
    
    if (!success) {
        printf("[!] 모든 IP 주소 연결 실패\n");
        return -1;
    }

    // HTTP 요청 전송 및 응답 수신
    return handle_http_communication(sock, host_header);
}

3. 완전한 HTTP 통신 처리

static int handle_http_communication(int sock, const char *host_header) {
    // HTTP 요청 생성 (512 bytes면 기본 GET 헤더용으론 넉넉, 학습용으론 안전 장치 유지)
    char request[512];
    int req_len = snprintf(request, sizeof(request),
        "GET / HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Connection: close\r\n"
        "User-Agent: suminworld-tcp-client/1.0\r\n"
        "\r\n", host_header);
    
    if (req_len >= sizeof(request)) {
        fprintf(stderr, "[!] 요청 버퍼 오버플로우\n");
        close(sock);
        return -1;
    }

    // 요청 전송 (부분 전송 대응)
    ssize_t sent = send_all(sock, request, req_len);
    if (sent != req_len) {
        printf("[!] 요청 전송 실패: %zd/%d bytes\n", sent, req_len);
        close(sock);
        return -1;
    }
    
    printf("[+] 요청 전송 완료: %zd bytes\n", sent);

    // 응답 수신 (타임아웃 적용)
    return receive_response(sock);
}

// 부분 전송 방지를 위한 send_all 함수 (Linux 전용, macOS는 SO_NOSIGPIPE 사용, 일부 BSD는 별도 설정 필요)
static ssize_t send_all(int sock, const void *buf, size_t len) {
    const char *ptr = (const char*)buf;
    size_t remaining = len;
    
    while (remaining > 0) {
        ssize_t sent = send(sock, ptr, remaining, MSG_NOSIGNAL);  // SIGPIPE 방지
        if (sent < 0) {
            if (errno == EINTR) continue; // 시그널 인터럽트 재시도
            return -1;
        }
        if (sent == 0) return -1; // peer closed - 연결 조기 종료
        
        ptr += sent;
        remaining -= sent;
    }
    
    return len - remaining;
}

// 강화된 응답 수신 함수
static int receive_response(int sock) {
    // 5초 읽기 타임아웃 설정
    struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
    if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
        perror("setsockopt SO_RCVTIMEO");
    }

    printf("[*] 응답 수신 시작...\n");
    
    char buffer[4096];
    size_t total_received = 0;
    int recv_calls = 0;
    
    while (1) {
        ssize_t received = recv(sock, buffer, sizeof(buffer) - 1, 0);
        
        if (received > 0) {
            buffer[received] = '\0';
            printf("%s", buffer);
            fflush(stdout);
            
            total_received += received;
            recv_calls++;
            
        } else if (received == 0) {
            // 서버가 연결을 정상 종료
            printf("\n[+] 서버 연결 정상 종료 (총 %zu bytes, %d번 수신)\n", 
                   total_received, recv_calls);
            break;
            
        } else {
            // 에러 발생
            if (errno == EWOULDBLOCK || errno == EAGAIN || errno == ETIMEDOUT) {
                printf("\n[!] 수신 타임아웃 (총 %zu bytes 수신)\n", total_received);
            } else {
                printf("\n[!] 수신 에러: %s (총 %zu bytes 수신)\n", 
                       strerror(errno), total_received);
            }
            break;
        }
    }
    
    close(sock);
    return 0;
}

4. Fallback 메커니즘

int main(void) {
    // 즉시 출력을 위한 버퍼링 해제
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    printf("=== HTTP 클라이언트 시작 ===\n");

    // 1순위: neverssl.com (HTTP 평문 테스트용)
    if (fetch_http("neverssl.com", "80", "neverssl.com") == 0) {
        printf("=== 1순위 서버 성공 ===\n");
        return 0;
    }

    // 2순위: example.com (대안 서버)
    printf("\n[!] 1순위 실패 → 2순위 서버 시도\n");
    if (fetch_http("example.com", "80", "example.com") == 0) {
        printf("=== 2순위 서버 성공 ===\n");
        return 0;
    }

    printf("\n[!] 모든 서버 접근 실패\n");
    return 1;
}

실행 및 결과 분석

컴파일 및 실행

cd ~/suminworld-system-lab/network/basics
gcc -Wall -Wextra -std=c99 tcp_client.c -o tcp_client
./tcp_client

Fallback 동작 확인 (neverssl → example.com)

실행 로그에서 neverssl.com 연결 실패 후 example.com으로 자동 전환되는 과정입니다.

1차 시도 (neverssl.com) 실패 시나리오:

=== HTTP 클라이언트 시작 ===
[*] DNS 해석: neverssl.com:80
[*] 연결 시도: IPv4 34.223.124.45
[!] 연결 실패: IPv4 34.223.124.45 (Connection timed out)
[*] 연결 시도: IPv4 52.43.101.211  
[!] 연결 실패: IPv4 52.43.101.211 (Connection timed out)
[!] 모든 IP 주소 연결 실패

[!] 1순위 실패 → 2순위 서버 시도
[*] DNS 해석: example.com:80
[*] 연결 시도: IPv4 93.184.216.34
[+] 연결 성공: IPv4 93.184.216.34
[+] 요청 전송 완료: 95 bytes
[*] 응답 수신 시작...
HTTP/1.1 200 OK
Age: 598765
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Content-Length: 1256
...
[+] 서버 연결 정상 종료 (총 1521 bytes, 1번 수신)
=== 2순위 서버 성공 ===

네트워크 상황별 동작 분석:

  • DNS 해석: 각 도메인에 대해 여러 IP 주소를 조회
  • 다중 IP 시도: 첫 번째 IP 실패 시 자동으로 다음 IP 시도
  • 타임아웃 제어: 각 연결 시도당 3초 제한으로 빠른 fallback
  • 자동 복구: 1순위 서버 완전 실패 시 2순위 서버로 즉시 전환

성공적인 neverssl.com 연결 시나리오

네트워크 상태가 좋을 때는 1순위 서버가 바로 성공하는 과정입니다.

정상 연결 성공 케이스:

=== HTTP 클라이언트 시작 ===
[*] DNS 해석: neverssl.com:80
[*] 연결 시도: IPv4 34.223.124.45
[+] 연결 성공: IPv4 34.223.124.45
[+] 요청 전송 완료: 96 bytes
[*] 응답 수신 시작...
HTTP/1.1 200 OK
Date: Mon, 01 Sep 2025 20:54:04 GMT
Server: Apache/2.4.62 ()
Last-Modified: Wed, 23 Aug 2023 14:17:45 GMT
ETag: "f77-6026d5e56a8c0"
Accept-Ranges: bytes
Content-Length: 3961
Vary: Accept-Encoding
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>
<head>
    <title>NeverSSL - Connecting...</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .container { max-width: 600px; margin: 0 auto; }
        h1 { color: #2c3e50; }
        .status { background: #ecf0f1; padding: 20px; border-radius: 5px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>NeverSSL</h1>
        <div class="status">
            <p>This website is designed to provide an HTTP connection for testing purposes.</p>
            <p>Status: Connected successfully!</p>
        </div>
    </div>
</body>
</html>

[+] 서버 연결 정상 종료 (총 4268 bytes, 2번 수신)
=== 1순위 서버 성공 ===

최적 시나리오 특징:

  • 즉시 연결: 첫 번째 IP에서 바로 성공 (약 50ms 내)
  • 효율적 전송: HTTP 요청 96바이트를 한 번에 전송
  • 안정적 수신: 4KB 데이터를 2번에 나누어 수신 완료
  • 정상 종료: 서버에서 Connection: close로 깔끔하게 연결 종료

네트워크 품질 지표:

연결 지연시간: < 100ms (양호)
DNS 응답시간: < 50ms (우수)  
처리량: 4268 bytes / 2회 = 평균 2134 bytes/recv
총 소요시간: 약 1.2초 (요청→응답→종료)

패킷 분석 실습

tcpdump 기본 사용법

실시간 패킷 캡처

# 호스트/포트명 변환 없이 순수 숫자로 표시
sudo tcpdump -i enp0s1 -nn host 34.223.124.45 and tcp port 80

# 추가 옵션: 50개 패킷 후 종료, 전체 패킷 캡처
sudo tcpdump -i enp0s1 -nn -c 50 -s 0 host 34.223.124.45 and tcp port 80

실제 tcpdump 패킷 캡처 결과

캡처된 패킷 분석:

05:54:02.190287 IP 192.168.64.8.57962 > 34.223.124.45.80: 
    Flags [S], seq 3852282410, win 64240, options [mss 1460,sackOK,TS val 1327416903 ecr 0,nop,wscale 7], length 0

05:54:03.376136 IP 34.223.124.45.80 > 192.168.64.8.57962: 
    Flags [S.], seq 3327953369, ack 3852282411, win 26847, options [mss 1460,sackOK,TS val 1081424001 ecr 1327417954,nop,wscale 7], length 0

05:54:03.376572 IP 192.168.64.8.57962 > 34.223.124.45.80: 
    Flags [.], ack 1, win 502, options [nop,nop,TS val 1327418090 ecr 1081424001], length 0
    
05:54:03.377869 IP 192.168.64.8.57962 > 34.223.124.45.80: 
    Flags [P.], seq 1:97, ack 1, win 502, length 96: HTTP: GET / HTTP/1.1

05:54:04.208405 IP 34.223.124.45.80 > 192.168.64.8.57962: 
    Flags [P.], seq 1:4269, ack 97, win 229, length 4268: HTTP: HTTP/1.1 200 OK
    
05:54:04.208425 IP 34.223.124.45.80 > 192.168.64.8.57962: 
    Flags [F.], seq 4269, ack 97, win 229, length 0

패킷 흐름 분석:

  1. SYN (3-way handshake 시작): 클라이언트 → 서버
  2. SYN-ACK (연결 승인): 서버 → 클라이언트
  3. ACK (핸드셰이크 완료): 클라이언트 → 서버
  4. PSH (HTTP 요청 전송): GET / HTTP/1.1
  5. PSH (HTTP 응답): HTTP/1.1 200 OK + 4268 bytes 데이터
  6. FIN (서버 측 연결 종료)

네트워크 환경 확인

UTM 가상 환경의 핵심 네트워크 구성:

# 라우팅 테이블 확인
user@user-utm:~$ ip route show default
default via 192.168.64.1 dev enp0s1 proto dhcp src 192.168.64.8 metric 100

# IPv4 주소 정보만 확인
user@user-utm:~$ ip -4 addr show enp0s1
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.64.8/24 metric 100 brd 192.168.64.255 scope global dynamic enp0s1
       valid_lft 3323sec preferred_lft 3323sec

# DNS 설정 확인
user@user-utm:~$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search .

보안 검토 - 공개 정보 안전성 분석

위 네트워크 설정에서 확인되는 정보들의 보안성:

완전히 공개해도 안전한 정보:

  • 192.168.64.8/24 - UTM NAT 사설 IP 대역 (RFC 1918)
  • 192.168.64.1 - UTM 가상 라우터 IP
  • enp0s1 - 가상 네트워크 인터페이스명
  • 127.0.0.53 - systemd-resolved 스텁 리졸버 (표준 설정)

이번 스크린샷에서 제외된 민감 정보:

  • MAC 주소 (하드웨어 식별자)
  • IPv6 ULA 주소 (fd8f:... - 내부 네트워크 구조 노출)
  • IPv6 링크로컬 주소 (fe80:... - 인터페이스 정보 유추 가능)

보안 판정: 모든 정보가 UTM 가상 환경의 표준 설정이므로 100% 공개 안전

학습 포인트: 이처럼 꼭 필요한 정보만 선별적으로 공개하는 것이 좋은 보안 습관입니다. ip -4 addr show 같은 명령어로 IPv6 정보를 제외하고, 핵심적인 라우팅과 DNS 정보만 확인할 수 있습니다.

Wireshark 고급 분석

유용한 필터 표현식

# 특정 TCP 스트림만 보기
tcp.stream eq 0

# HTTP 트래픽만 보기  
http

# TCP 플래그별 필터링
tcp.flags.syn == 1          # SYN 패킷만
tcp.flags.fin == 1          # FIN 패킷만
tcp.flags.rst == 1          # RST 패킷만

# 재전송 패킷 찾기
tcp.analysis.retransmission

# 특정 포트 범위
tcp.port >= 1024 and tcp.port <= 65535

Wireshark에서 확인할 주요 정보

  1. Statistics → Conversations: 연결별 통계
  2. Statistics → I/O Graphs: 시간별 패킷 흐름
  3. Analyze → Follow → TCP Stream: 전체 대화 내용
  4. View → Time Display Format: 타임스탬프 형식 변경

보안 고려사항 (Wireshark 설정)

Name Resolution 설정 (보안상 해제 권장)

  • Resolve MAC addresses: ❌ (벤더 정보 노출)
  • Resolve network addresses: ❌ (도메인명 노출)
  • Resolve transport names: ⚠️ (선택사항)
  • Use captured DNS packets: ❌ (도메인 정보 노출)
  • Use system DNS settings: ❌ (네트워크 환경 노출)

디버깅 및 트러블슈팅

일반적인 에러와 해결 방법

Connection refused (ECONNREFUSED)

  • 원인: 대상 포트에서 서비스가 실행되지 않음
  • 확인: telnet host port 또는 nc -zv host port
  • 해결: 서버 상태 확인, 포트 번호 확인

Connection timed out (ETIMEDOUT)

  • 원인: 패킷이 대상에 도달하지 못함
  • 확인: ping host, traceroute host
  • 해결: 방화벽 설정, 네트워크 경로 확인

Network unreachable (ENETUNREACH)

  • 원인: 라우팅 테이블에 경로 없음
  • 확인: ip route, route -n
  • 해결: 기본 게이트웨이 설정 확인

Name resolution failure

  • 원인: DNS 서버 접근 불가 또는 도메인 없음
  • 확인: nslookup domain, dig domain
  • 해결: DNS 서버 설정 확인 (/etc/resolv.conf)

네트워크 진단 도구

# 기본 연결성 테스트
ping -c 3 google.com

# DNS 해석 확인
nslookup example.com
dig example.com A

# 포트 연결 테스트
telnet example.com 80
nc -zv example.com 80

# 경로 추적 (Ubuntu: mtr-tiny 기준)
traceroute example.com
mtr -rwzc 10 example.com

# 네트워크 인터페이스 상태 (IPv4만 표시)
ip -4 addr show
ip route show

# 소켓 연결 상태
ss -tuln          # 리스닝 포트
ss -tun           # 활성 연결
netstat -an       # 전체 연결 상태

고급 tcpdump 필터링

# 특정 호스트의 SYN 패킷만
sudo tcpdump -i any 'host example.com and (tcp[tcpflags] & tcp-syn) != 0'

# HTTP 요청만 캡처 (GET, POST 등)
sudo tcpdump -i any -A 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420)'

# 재전송 패킷 감지 (중복 시퀀스 번호)
sudo tcpdump -i any '(tcp[tcpflags] & tcp-ack) != 0' -v

# IPv6 트래픽 포함
sudo tcpdump -i any '(ip or ip6) and tcp port 80'

# 대용량 파일로 저장 (로테이션)
sudo tcpdump -i any -w capture-%Y%m%d-%H%M%S.pcap -G 3600 -W 24 'tcp port 80'

메모리 및 리소스 모니터링

# 메모리 사용량 실시간 모니터링
watch -n 1 'free -h && echo && ps aux --sort=-%mem | head -10'

# 네트워크 통계 모니터링
watch -n 1 'ss -i | grep -E "(ESTAB|SYN)" | wc -l'

# 파일 디스크립터 사용량
lsof -p `pgrep tcp_client`

# 시스템 콜 추적 (디버깅용)
strace -e trace=network ./tcp_client

실전 활용 예제

주의사항: 공개 서비스 대상 테스트는 요청 수/주기를 제한하고, 소유한/허가된 대상에서만 실행하세요.

헬스체크 클라이언트

// 서버 상태 모니터링용 간단한 헬스체크
static int health_check(const char* host, const char* port) {
    printf("[*] 헬스체크: %s:%s\n", host, port);
    
    struct addrinfo hints = {0};
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    
    struct addrinfo *result;
    if (getaddrinfo(host, port, &hints, &result) != 0) {
        printf("[!] DNS 해석 실패\n");
        return -1;
    }
    
    int sock = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (sock < 0) {
        printf("[!] 소켓 생성 실패\n");
        freeaddrinfo(result);
        return -1;
    }
    
    double start_time = get_time_ms();
    int connect_result = connect_with_timeout(sock, result->ai_addr, 
                                             result->ai_addrlen, 5000);
    double connect_time = get_time_ms() - start_time;
    
    freeaddrinfo(result);
    close(sock);
    
    if (connect_result == 0) {
        printf("[+] 서버 응답 정상 (%.2f ms)\n", connect_time);
        return 0;
    } else {
        printf("[!] 서버 응답 없음 (%.2f ms): %s\n", 
               connect_time, strerror(errno));
        return -1;
    }
}

고급 확장 및 최적화

참고: 아래 고급 기능들은 별도 시리즈 글에서 자세히 다룰 예정입니다. 여기서는 개념과 기본 구현만 소개합니다.

IPv6 지원 추가

// IPv6 주소 처리를 위한 개선
static void print_address_info(struct addrinfo *ai) {
    char ip_str[INET6_ADDRSTRLEN];
    void *addr_ptr;
    const char *family_str;
    int port;
    
    if (ai->ai_family == AF_INET) {
        struct sockaddr_in *ipv4 = (struct sockaddr_in*)ai->ai_addr;
        addr_ptr = &ipv4->sin_addr;
        family_str = "IPv4";
        port = ntohs(ipv4->sin_port);
    } else if (ai->ai_family == AF_INET6) {
        struct sockaddr_in6 *ipv6 = (struct sockaddr_in6*)ai->ai_addr;
        addr_ptr = &ipv6->sin6_addr;
        family_str = "IPv6";
        port = ntohs(ipv6->sin6_port);
    } else {
        printf("[*] 알 수 없는 주소 패밀리: %d\n", ai->ai_family);
        return;
    }
    
    inet_ntop(ai->ai_family, addr_ptr, ip_str, sizeof(ip_str));
    printf("[*] %s 주소: %s:%d\n", family_str, ip_str, port);
}

HTTPS 지원 (OpenSSL)

#include <openssl/ssl.h>
#include <openssl/err.h>

// SSL 컨텍스트 초기화
static SSL_CTX* init_ssl_context() {
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
    
    const SSL_METHOD* method = TLS_client_method();
    SSL_CTX* ctx = SSL_CTX_new(method);
    if (!ctx) {
        ERR_print_errors_fp(stderr);
        return NULL;
    }
    
    // 인증서 검증 활성화
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
    SSL_CTX_set_default_verify_paths(ctx);
    
    return ctx;
}

최종 요약 및 다음 단계

내용 요약

이번 실습을 통해 배운점:

견고한 네트워크 프로그래밍 - 타임아웃, 에러 처리, 재시도 메커니즘 실시간 디버깅 기법 - tcpdump, Wireshark를 활용한 패킷 레벨 분석
내결함성 설계 - DNS 다중 IP, Fallback 전략 성능 모니터링 - 연결 시간, 처리량 측정

실무 적용

  • 실제 서비스에서는 연결 풀링과 재사용 고려
  • 대용량 데이터 처리 시 스트리밍 방식 적용
  • 로드밸런서 환경에서의 세션 관리
  • 모니터링과 로깅 시스템 연동

다음 학습 단계

  1. 고성능 서버 구현: epoll/kqueue 기반 이벤트 루프
  2. HTTP/2 및 gRPC: 멀티플렉싱과 바이너리 프로토콜
  3. 보안 통신: TLS 1.3, 인증서 관리, PKI
  4. 성능 최적화: 제로카피, 메모리 풀, CPU 캐시 최적화
  5. 분산 시스템: 서비스 디스커버리, 회로 차단기 패턴

전체 소스 코드: GitHub - suminworld-system-lab

주의사항: 본 실습의 모든 IP 주소와 네트워크 정보는 UTM 가상 환경의 사설 주소 또는 공개 테스트 서비스 주소입니다.