프롤로그
IP로 너무 날먹했나...
이게 지피티랑 코파일럿 자동 코딩과 멘토님 코드가 없었으면 나는 과연 얼마나 빠르고 효율적으로 코드를 짤 수 있었을까
이번에는 TCP랑 UDP각각 만들어볼 거다.
저번에 IP헤더에서 protocol에 따라서 다음이 어떤 게 올지 결정이 되는데
tcp랑 udp기준으로 일단 만들어보자.
TCP
역시 정보통신기술용어 설명 사이트 참고해서 적겠다.
http://www.ktword.co.kr/test/view/view.php?m_temp1=1889
TCP Header
TCP Header Transmission Control Protocol Header TCP 헤더(2023-04-30)초기 순서번호, ISN, Window Size , 윈도우 크기 , Windowsize, Acknowledgement Number, 확인응답 번호 1. TCP 세그먼트 내 헤더 구성 ※ [참고] TCP 헤더 크
www.ktword.co.kr

IP 헤더크기가 IP헤더에서 나왔고, 거기서 IP헤더 시작점에서부터 크기만큼 뒤로 가면 TCP 헤더가 나온다.
TCP 헤더도 똑같이 옵션이 없다면 20바이트, 있다면 최대 60바이트까지 존재가능하다.
다시 표로 하나하나 정리해보겠다.
| 필드명 | 비트 수 | 의미 및 설명 |
| source port | 16 | 출발지 포트 번호 |
| destination port | 16 | 도착지 포트 번호 |
| sequence number | 32 | 바이트 단위로 구분되어 순서화 되는 번호 이를 통해서 TCP에서는 신뢰성 및 흐름 제어 기능을 제공한다. 임의 값으로 시작하고, 송신 데이터에 순서화된 일련번호를 붙일 수 있다. |
| acknowledgement number | 32 | 확인 응답 번호이다. 수신하기를 기대하는 다음 바이트 번호이다. 마지막 수신 성공 번호 + 1 |
| hlen | 4 | 헤더 길이 필드. TCP 헤더 길이를 4바이트 단위로 표시한다. |
| reserved | 3 | 미래 확장용. 항상 0 |
| flags | 9 | 9개의 플래그들이 있다. TCP세그먼트 전달과 회선 및 데이터 관리 제어 기능을 한다. 옛날 자료들은 플래그가 6개만 있는 것도 있는데, 최신 자료는 RFC793 + RFC3168에 의하여 플래그 9개 ,reserved 3bit이다. |
| window size | 16 | 흐름 제어를 위해 사용하는 16비트 필드. |
| checksum | 16 | 검사합 |
| urgent pointer | 16 | TCP 세그먼트에 포함된 긴급 데이터의 마지막 바이트에 대한 일련번호 |
여기서
flags 9비트가 좀 중요한데
| Flags | 의미 | 사용 시나리오 |
| SYN | 연결 시작 요청 | 3-way handshake 시작 |
| ACK | 직전 데이터 확인 응답 | 대부분 TCP 패킷에 포함됨 |
| FIN | 연결 종료 요청 | 정상적 연결 종료 |
| RST | 연결 강제 종료 | 비정상 세션 초기화 |
| PSH | 즉시 상위로 전달 | Interactive 통신 (SSH 등) |
| URG | 긴급 데이터 존재 | Urgent Pointer 사용 |
| ECE / CWR | 혼잡 제어 관련 | ECN 활성 상태일 때 |
| NS | ECN 확장용 | 거의 안 쓰임 |
그래서 TCP가 신뢰성을 제공하는 이유는
| 기능 | 관련 필드 |
| 데이터 순서 보장 | Sequence Number |
| 재전송 / 누락 감지 | ACK + Sequence |
| 흐름 제어 | Window Size |
| 혼잡 제어 | CWR/ECE |
| 오류 검출 | Checksum |
때문이다.
그렇게 신뢰성 신뢰성 하는 이유가 이것때문이고, 시퀀스 넘버랑 애크놀로지 넘버가 굉장히 중요하다.
Sequence Number와 Acknowledge Number
방금 위에서 TCP가 신뢰성이 높다고 한 이유는? 데이터 순서 보장을 하고, 재전송 / 누락감지를 한다고 방금 위에서 언급하였다. TCP는 데이터를 전송하기 전에 상대방과 연결을 먼저 진행 한 후에 데이터를 전송하게 되는데, 상대방과 처음 연결을 맺는 과정을 3way- handshake라고 하고, 반대로 통신 종료할 때 연결을 끊는 과정을 4way-handshake라고 한다.
이 과정에서 Sequence Number(Seq)와 Acknowledge Number (Ack), 그래서 플래그 사용되게 되는데, 이 부분이 굉장히 중요하다.
일단 이것부터 기억하자. 나중가면 헷갈릴 수 있다.
Sequence Number는 '나 여기서부터 데이터 보낼게~' 라고 이번에 보내는 데이터가 몇 번째 바이트인지 표시하는 번호이다.
Acknowledge Number는 '나 여기까진 잘 받았고, 그 다음 번호인 여기서부터 보내주면 돼~' 라고, 다음에 받고 싶은 바이트 번호이다.
러브라이브로 예시를 들어보겠다.

검정이가 흰색한테 러브라이브 뮤즈의 멤버를 얘기하려고 한다.
검정이가 정말 힘들게 뮤즈의 멤버 9명을 호노카 1번부터 니코 9번까지 싹 말했다.
근데 흰색이 기억력이 좋지 않아서 우미까지는 제대로 들었으니까 마키부터 설명을 해달라고 하는 상황이다.
여기서 검정이가 처음 말하는 순서인 호노카가 바로 시퀀스 넘버, 즉 여기서부터 데이터 보낼게~에 해당하기 때문에 호노카가 시퀀스 넘버(seq)에 해당하고, 흰색이가 다음 마키부터 보내달라고 한 부분이 애크 넘버(ack)가 되겠다.
그래서 이렇게 시퀀스 넘버와 애크넘버를 통해서 데이터가 순서대로 도착했는지 확인할 수가 있고, 손실되면 재전송하여 상대가 잘 받았는지 ACK로 확인하기 때문에 신뢰성이 높다.
근데 처음 연결할 때 이 seq 와 ack가 쓰인다고 했다. 근데 둘은 이미 데이터가 주고받아진 다음에, 다음 어디서부터 보내 라고 얘기하는 건데..어떻게 쓰일까?
3way-handshake
TCP의 3way handshake를 조금 더 본질적으로 보면, 처음 연결할 때는 A와 B가 서로 자기 시퀀스 넘버를 서로 교환하는 과정이라고 생각하면 된다.
이게 무슨 말이냐면, A가 B로 보내는 데이터와 B가 A로 보내는 데이터가 서로 독립된 스트림이기에 서로의 데이터가 다르기 때문이다. 위쪽에서 그림은 검정이 보낸 게 SEQ, 흰색이 보낸 게 ACK로 일방적인 느낌이 강하지만
원래는 흰색이 보낸 SEQ와 검정이 보낸 ACK가 동시에 이루어진다.
그 과정이 3way-handshake , 즉 처음 연결시도할 때도 똑같이 이루어진다.
처음 연결할 때 A의 Seq를 B에게 알려주고, B의 Seq도 A에게 알려주면서 서로 Ack와 Seq가 변동이 생길 것이고, 이 과정이 합쳐져서 3-way handshake가 이루어지는 거다.
여기는 러브라이브로 예시들면 좀 설명이 이상해져서 정상적인 그림으로 표현하겠다.

처음 연결 시도하려는 친구는 Flag에서 SYN 1을 키면서 첫 seq 를 주면서 연결 요청을 한다
그거를 받은 B는 ack번호, 너가 준 거 다음 번호부터 주면 된다고 해주면서, ACK Flag를 킨다. 그리고 똑같이 Seq넘버를 주기 위해서 SYN 플래그를 켜서 보낸다.
핸드셰이크 초기 두 단계(SYN → SYN+ACK)에서는 실제 payload 데이터가 오가지 않으므로 각 메시지마다 시퀀스 번호가 한 번만 소비(= +1 증가) 된다.
(내용을 보내지 않았으니 “1 byte만 사용했다”는 것은 `SYN 패킷이 1 byte 취급되는 것이 아니라, 제어용 플래그 패킷도 Seq 공간을 1 소비한다는 TCP의 룰 때문이다.)
그래서 최종적으로 B로부터 다시 A가 B의 Seq 번호를 받으면? Ack로 되돌려주고 본인의 다음 시퀀스 넘버인 101번쨰에서 보내준다.
최종적으로 보면 A는 100에서 시작하고 B는 407에서 시작했다.
그리고 3way handshake가 끝나고 나니 번호는
101 / 408로 각각 +1이 되었는데, 이러면 정상적인 연결이 끝났다고 판단하고 통신을 시작한다.
다른 책이나 블로그에서는 0번으로 표기하는데, 그건 상대적인 시퀀스 넘버라고 흔히 알아보기 쉽게 , 보기 편하라고 하는 거다. 원래라면 랜덤인 값이 시퀀스 넘버로 선택된다.
정리를 하면 SYN/ACK 교환은 데이터를 주고 받는 과정이 아니라 서로의 초기 시퀀스 번호를 교환하며 신뢰 기반 스트림을 열기 위한 준비과정이다.
이게 왜 중요하냐면, 나중에 VPN 만들 때, 우리의 브로커 서버와 통신할 떄 이 과정이 들어가기 때문이다.
TCP 헤더 코드 작성
애매하게 1바이트 씩, 4바이트 먹는 건 그냥 한 번에 처리했다.
struct TcpHeader
{
private:
uint16_t src_port;
uint16_t dst_port;
uint32_t seq_num;
uint32_t ack_num;
uint16_t hlen_flags;
uint16_t window_size;
uint16_t checksum;
uint16_t urgent_pointer;
그리고, 이거와 관련된 함수들도 생각나는 대로만들었다.
그래서 최종적으로
#include <stdint.h>
#include <arpa/inet.h>
struct TcpHeader
{
private:
uint16_t src_port;
uint16_t dst_port;
uint32_t seq_num;
uint32_t ack_num;
uint16_t hlen_flags;
uint16_t window_size;
uint16_t checksum;
uint16_t urgent_pointer;
public:
uint16_t sPort() const { return ntohs(src_port); }
uint16_t dPort() const { return ntohs(dst_port); }
uint32_t seqNum() const { return ntohl(seq_num); }
uint32_t ackNum() const { return ntohl(ack_num); }
uint8_t headerLength() const { return (ntohs(hlen_flags) >> 12) * 4; }
bool flagACK() const { return (ntohs(hlen_flags) & 0x10) != 0; }
bool flagSYN() const { return (ntohs(hlen_flags) & 0x02) != 0; }
bool flagFIN() const { return (ntohs(hlen_flags) & 0x01) != 0; }
bool flagRST() const { return (ntohs(hlen_flags) & 0x04) != 0; }
};
이 되었다.
나중에 잠수함패치로 필요한 건 그때그때 만들 거 같다.
TCP 패킷 테스트
일단은 임시적으로 . 여기서 이따가 enum으로 처리할 거긴 한데 되는지 부터 보자.
#include "pch.h"
#include "handler.h"
#include "ethernet.h"
#include "ipheader.h"
#include "tcp.h"
void packet_handler(u_char* user, const struct pcap_pkthdr* h, const u_char* bytes){
std::cout << "Packet captured: " << h->len << " bytes" << std::endl;
EthernetHeader* ethernet = (EthernetHeader*)bytes;
// std::cout << "Destination MAC: " << std::string(ethernet->dmac()) << std::endl;
// std::cout << "Source MAC: " << std::string(ethernet->smac()) << std::endl;
// printf("Ethernet Type: 0x%04x\n", ethernet->type());
if (ethernet->type() == EtherType::ipv4)
{
std::cout << "This is an IPv4 packet." << std::endl;
IpHeader *ip_header = (IpHeader*)(bytes + sizeof(EthernetHeader));
std::cout << "Source IP: " << std::string(ip_header->sip()) << std::endl;
std::cout << "Destination IP: " << std::string(ip_header->dip()) << std::endl;
std::cout << "IP Version: " << static_cast<int>(ip_header->version()) << std::endl;
std::cout << "IP Header Length: " << static_cast<int>(ip_header->headerLength()) << " bytes" << std::endl;
std::cout << "Protocol: " << static_cast<int>(ip_header->protocolType()) << std::endl;
if(ip_header->protocolType() == 6) // TCP Protocol
{
printf("\nThis is a TCP packet.\n");
TcpHeader* tcp_header = (TcpHeader*)(bytes + sizeof(EthernetHeader) + ip_header->headerLength());
std::cout << "Source Port: " << tcp_header->sPort() << std::endl;
std::cout << "Destination Port: " << tcp_header->dPort() << std::endl;
std::cout << "Sequence Number: " << tcp_header->seqNum() << std::endl;
std::cout << "Acknowledgment Number: " << tcp_header->ackNum() << std::endl;
std::cout << "TCP Header Length: " << static_cast<int>(tcp_header->headerLength()) << " bytes" << std::endl;
std::cout << "Flags: "
<< (tcp_header->flagSYN() ? "SYN " : "")
<< (tcp_header->flagACK() ? "ACK " : "")
<< (tcp_header->flagFIN() ? "FIN " : "")
<< (tcp_header->flagRST() ? "RST " : "")
<< std::endl;
}
}
else
{
std::cout << "This is not an IPv4 packet." << std::endl;
}
printf("\n");
}
이렇게 설정하고
makeFile을
TARGET=vpn
CXXFLAGS=-g -Wall
all: $(TARGET)
$(TARGET) : main.cpp handler.cpp ethernet.cpp mac.cpp ip.cpp ipheader.cpp tcp.cpp
$(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@ -lpcap
clean:
rm -f $(TARGET)
rm -f *.o
rm -rf *.dSYM
이렇게 설정하고 make후에 실행하고 와이어샤크랑 비교해보면서 확인해보자.

└─$ sudo ./vpn eth0 1.1.1.1
Packet captured: 102 bytes
This is an IPv4 packet.
Source IP: 172.30.1.100
Destination IP: 172.30.1.87
IP Version: 4
IP Header Length: 20 bytes
Protocol: 6
This is a TCP packet.
Source Port: 51748
Destination Port: 22
Sequence Number: 3098214425
Acknowledgment Number: 2075427602
TCP Header Length: 32 bytes
Flags: ACK
잘 나오는 것을 확인하였다.
UDP
TCP랑 UDP의 점이 있다면, TCP는 연결을 먼저 진행한 후에 신뢰있게 서로 데이터를 주고받는다면, UDP는 비연결 지향 프로토콜(Connectionless) 로, 연결 협상 없이 일단 데이터부터 보내고 보는 거다. 그래서 딱 포트번호랑 무결성을 위한 체크섬, 길이 정도만 필요하고, 핸드셰이킹 대화가 필요없으니까 비신뢰성에 노출된다.
그러면 이걸 왜 쓰냐? 할 순 있는데 쓸데없는 연결 신뢰과정이 없어지니까 굉장히 빨라서 DNS 쿼리 등 빠른 답변을 얻어야할 때 주로 쓰이고 있다.

진짜 단순하다.
헤더만 보면 32비트(4바이트) * 2 해서 총 8바이트 정도의 크기로 매우 작다.
| 필드명 | 비트 수 | 의미 및 설명 |
| Source Port | 16 | 발신지 포트 |
| Destination Port | 16 | 수신지 포트 |
| Length | 16 | 최소값 8바이트(헤더만 포함될 떄) |
| CheckSum | 16 | 에러 검출용 |
UDP 헤더 코드 작성
그래서 이제 UDP 헤더 코드를 작성해보자.
#include <stdint.h>
#include <arpa/inet.h>
struct UdpHeader
{
private:
uint16_t src_port;
uint16_t dst_port;
uint16_t header_length;
uint16_t checksum;
public:
uint16_t sPort() const { return ntohs(src_port); }
uint16_t dPort() const { return ntohs(dst_port); }
uint16_t length() const { return ntohs(header_length); }
};
그리고 Ipheader에
enum IpProtocolType : uint8_t
{
icmp = 1,
tcp = 6,
udp = 17
};
프로토콜 타입을 추가해준다.
UDP 패킷 테스트
packet_handler 코드를
#include "pch.h"
#include "handler.h"
#include "ethernet.h"
#include "ipheader.h"
#include "tcp.h"
#include "udp.h"
void packet_handler(u_char* user, const struct pcap_pkthdr* h, const u_char* bytes){
std::cout << "Packet captured: " << h->len << " bytes" << std::endl;
EthernetHeader* ethernet = (EthernetHeader*)bytes;
// std::cout << "Destination MAC: " << std::string(ethernet->dmac()) << std::endl;
// std::cout << "Source MAC: " << std::string(ethernet->smac()) << std::endl;
// printf("Ethernet Type: 0x%04x\n", ethernet->type());
if (ethernet->type() == EtherType::ipv4)
{
std::cout << "This is an IPv4 packet." << std::endl;
IpHeader *ip_header = (IpHeader*)(bytes + sizeof(EthernetHeader));
std::cout << "Source IP: " << std::string(ip_header->sip()) << std::endl;
std::cout << "Destination IP: " << std::string(ip_header->dip()) << std::endl;
std::cout << "IP Version: " << static_cast<int>(ip_header->version()) << std::endl;
std::cout << "IP Header Length: " << static_cast<int>(ip_header->headerLength()) << " bytes" << std::endl;
std::cout << "Protocol: " << static_cast<int>(ip_header->protocolType()) << std::endl;
if(ip_header->protocolType() == IpProtocolType::tcp) // TCP Protocol
{
printf("\nThis is a TCP packet.\n");
TcpHeader* tcp_header = (TcpHeader*)(bytes + sizeof(EthernetHeader) + ip_header->headerLength());
std::cout << "Source Port: " << tcp_header->sPort() << std::endl;
std::cout << "Destination Port: " << tcp_header->dPort() << std::endl;
std::cout << "Sequence Number: " << tcp_header->seqNum() << std::endl;
std::cout << "Acknowledgment Number: " << tcp_header->ackNum() << std::endl;
std::cout << "TCP Header Length: " << static_cast<int>(tcp_header->headerLength()) << " bytes" << std::endl;
std::cout << "Flags: "
<< (tcp_header->flagSYN() ? "SYN " : "")
<< (tcp_header->flagACK() ? "ACK " : "")
<< (tcp_header->flagFIN() ? "FIN " : "")
<< (tcp_header->flagRST() ? "RST " : "")
<< std::endl;
}
else if(ip_header->protocolType() == IpProtocolType::udp) // UDP Protocol
{
printf("\nThis is a UDP packet.\n");
UdpHeader* udp_header = (UdpHeader*)(bytes + sizeof(EthernetHeader) + ip_header->headerLength());
std::cout << "Source Port: " << udp_header->sPort() << std::endl;
std::cout << "Destination Port: " << udp_header->dPort() << std::endl;
std::cout << "UDP Header Length: " << static_cast<int>(udp_header->length()) << " bytes" << std::endl;
}
}
else
{
std::cout << "This is not an IPv4 packet." << std::endl;
}
printf("\n");
}
이렇게 수정해주고 make후에 테스트를 해보자.

This is a UDP packet.
Source Port: 55087
Destination Port: 443
UDP Header Length: 42 bytes
와이어샤크값과 값이 같은 것을 확인할 수 있다.
에필로그
다음을 netfilter로 할지....TUN으로 구현을 해볼지 고민이다.
참고자료
http://www.ktword.co.kr/test/view/view.php?m_temp1=1889
TCP Header
TCP Header Transmission Control Protocol Header TCP 헤더(2023-04-30)초기 순서번호, ISN, Window Size , 윈도우 크기 , Windowsize, Acknowledgement Number, 확인응답 번호 1. TCP 세그먼트 내 헤더 구성 ※ [참고] TCP 헤더 크
www.ktword.co.kr
UDP
U ser D atagram P rotocol, UDP 컴퓨터가 다른 컴퓨터와 데이터 통신을 하기 위한 규약(프
namu.wiki
http://www.ktword.co.kr/test/view/view.php?no=323
UDP
UDP User Datagram Protocol (2023-08-27)UDP Header, UDP 헤더 1. UDP (User Datagram Protocol) ㅇ TCP/IP 프로토콜 群 중 트랜스포트 계층의 통신 프로토콜의 하나 (TCP에 대비됨) - 신뢰성이 낮은 프로토콜로써, 완전
www.ktword.co.kr
'네트워크' 카테고리의 다른 글
| [VPN-6] 네트워크 인터페이스와 TUN / IP 라우팅 프로그래밍 (2/2) (0) | 2025.12.31 |
|---|---|
| [VPN-5] 네트워크 인터페이스와 TUN / IP 라우팅 프로그래밍 (1/2) (0) | 2025.12.30 |
| [VPN-3] pcap를 이용한 ip 패킷 프로그래밍 (0) | 2025.12.26 |
| [VPN-2] pcap을 이용한 ethernet과 MAC 패킷 프로그래밍 (2) | 2025.12.26 |
| [VPN-1] VPN과 프록시 (2) | 2025.12.25 |