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
목차
환경 구성과 네트워크 이해
개발 환경
- 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
패킷 흐름 분석:
- SYN (3-way handshake 시작): 클라이언트 → 서버
- SYN-ACK (연결 승인): 서버 → 클라이언트
- ACK (핸드셰이크 완료): 클라이언트 → 서버
- PSH (HTTP 요청 전송): GET / HTTP/1.1
- PSH (HTTP 응답): HTTP/1.1 200 OK + 4268 bytes 데이터
- 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에서 확인할 주요 정보
- Statistics → Conversations: 연결별 통계
- Statistics → I/O Graphs: 시간별 패킷 흐름
- Analyze → Follow → TCP Stream: 전체 대화 내용
- 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 전략 성능 모니터링 - 연결 시간, 처리량 측정
실무 적용
- 실제 서비스에서는 연결 풀링과 재사용 고려
- 대용량 데이터 처리 시 스트리밍 방식 적용
- 로드밸런서 환경에서의 세션 관리
- 모니터링과 로깅 시스템 연동
다음 학습 단계
- 고성능 서버 구현: epoll/kqueue 기반 이벤트 루프
- HTTP/2 및 gRPC: 멀티플렉싱과 바이너리 프로토콜
- 보안 통신: TLS 1.3, 인증서 관리, PKI
- 성능 최적화: 제로카피, 메모리 풀, CPU 캐시 최적화
- 분산 시스템: 서비스 디스커버리, 회로 차단기 패턴
전체 소스 코드: GitHub - suminworld-system-lab
주의사항: 본 실습의 모든 IP 주소와 네트워크 정보는 UTM 가상 환경의 사설 주소 또는 공개 테스트 서비스 주소입니다.
'network' 카테고리의 다른 글
| ALFA AWUS036ACM(MT7612U) 모니터모드 캡처 설정 (0) | 2025.10.01 |
|---|---|
| [네트워크] UTM VM에서 USB 무선랜카드 연결하는 법 (macOS) (0) | 2025.09.08 |
| [네트워크] C 소켓 프로그래밍 Echo 서버 구현 - 기본부터 멀티클라이언트까지 (0) | 2025.09.08 |