학사 나부랭이
Network - TCP/UDP 본문
이전에 본 것처럼 패킷은 전송 도중 유실되거나 오차가 있을 수 있는데, 이는 당연히 네트워크의 신뢰성에 문제가 생길 수 있죠.
여기서, TCP는 이러한 패킷 전달에서 신뢰성을 보장하기 위해 고안된 프로토콜이에요. 통신 전에 항상 세션을 성립시키며, 패킷을 전송한 후, 패킷을 받은 측에서 패킷이 잘 도착했다고 알리죠. 그러니 속도가 상대적으로 느려요.
그에 비해 라이브 방송, 게임과 같은 약간 내용이 깨지는 정도는 감수하더라도 실시간 통신이 중요한 때가 있겠죠? 이렇게 속도가 정확도보다 중요한 경우 UDP를 사용해요.
TCP | UDP |
연결형 서비스 | 비연결형 서비스 |
高 신뢰성 | 低 신뢰성 |
低 속도 | 高 속도 |
수신 여부 확인 | 수신 여부 확인 X |
전송 순서 보장 | 전송 순서 보장 X |
TCP 헤더
위는 TCP 헤더 구조의 도식인데, IP 헤더와 마찬가지로 20bytes가 기본 길이에요.
Source port: 출발지 포트 번호예요. 0~655335의 값을 가질 수 있죠.
Destination port: 도착지 포트 번호예요. 마찬가지로 0~65535의 값을 가질 수 있어요.
Sequence number: TCP 패킷의 순서를 나타내는 숫자예요. Identifiable 하며, 패킷이 생성될 때, 무작위 값으로 초기화되죠.
Acknowledgment number: 다음에 받을 Sequence number를 상대측에게 알려줘요.
Offset: IP 헤더의 IHL처럼 TCP 헤더의 길이를 나타내요. 이게 없으면 보통 20bytes = 5words이죠.
Reserved: 예약된 항목이에요.
TCP flags: TCP 패킷의 역할이 정해지는 필드예요. CEUAPRSF로 앞 글자로 각 필드를 알아야 해요.
- CWR, ECE: TCP 혼잡 제어와 관련된 필드예요.
- URF(URgent): Urgent pointer 필드의 값이 유효한 지 나타내요.
- ACK(ACKnowledge): TCP 3-way handshake 시 사용되는 플래그예요.
- PSH(PuSH): 수신 애플리케이션에 버퍼링 된 데이터를 푸시해 줄지 나타내요.
- RST(ReSeT): TCP 연결을 강제 종료해요.
- SYN(SYNchronize): 동기화 시퀀스 번호, 최초 TCP 3-way handshake 시 사용되어요.
- FIN(FINish): TCP 연결 종료 시 4-way handshake 시 사용되어요. 더는 데이터가 없음을 알려주죠.
Window: 수신 윈도우 크기, 패킷 내용의 사이즈를 나타내요.
Checksum: 헤더 내용의 오류 검출을 위해 존재해요.
Urgent pointer: URG 플래그가 설정되면 시퀀스 번호에서 오프셋을 나타내요.
TCP options: 초기 연결 시 Maximum Segment Size 값의 설정, Window-scaling, Timestamp 설정 등에 대한 추가 옵션을 설정할 수 있어요.
TCP 3-way handshake
TCP/IP 프로토콜을 이용해 통신하는 애플리케이션이 데이터를 전송하기 전에 정확한 전송을 보장하기 위해 상대방과 사전에 세션을 수립시키는 과정을 의미해요.
① 먼저 클라이언트가 서버에 접속을 요청하는 SYN 패킷을 보내고, SYN_SENT 상태가 되어요.
② 서버는 처음에 포트를 열고 LISTENING 상태로 클라이언트의 요청을 기다려요. 클라이언트의 SYN 요청을 받으면 SYN, ACK 플래그가 설정된 패킷으로 응답하고 SYN_RECEIVED 상태로 변경되어요.
③ 클라이언트가 서버에서 SYN. ACK 패킷을 받으면 ACK 패킷으로 응답해 데이터 송수신이 가능한 상태 즉, 세션이 맺어진 ESTABLISHED 상태가 되죠.
TCP 4-way handshake
위의 3-way handshke가 TCP의 연결을 초기화할 때 수행된다면, 4-way handshake는 세션을 종료할 때 수행되는 절차예요.
① 클라이언트가 연결을 종료하겠다는 FIN, ACK 플래그를 전송해요.
② 서버는 ACK 응답을 보내 애플리케이션 종료 과정을 수행해요.
#1. 애플리케이션이 종료할 준비가 되면, FIN, ACK 패킷을 전송해요.
#2. 마지막으로 ACK 패킷을 보내고 TIME-WAIT 상태로 대기해요. 이는 Maximum Segment Lifetime * 2, 즉 약 120초 정도 기다린 후 완전히 종료해요. 이 때 기다리는 이유는 마지막 클라이언트의 ACK 패킷이 유실되었을 때 서버가 다시 보내는 FIN, ACK 패킷에 대응하지 못하기 때문이죠. 그러니 완전히 종료하기까지 일정 시간 기다려요.
UDP 헤더
UDP는 TCP와 달리 8bytes의 기본 헤더로 이루어져 있어요.
Source port: 출발지 포트 번호예요. 0~65535의 값을 가질 수 있어요.
Destination port: 도착지 포트 번호예요. 마찬가지로 0~65535의 값을 가질 수 있죠.
Length: 패킷의 길이예요. UDP 헤더의 시작부터 상위 계층까지의 모든 길이를 포함해요.
Checksum: 헤더 내용의 오류 검출을 위해 존재해요.
from socket import *
import os
import struct
def parseIPHeader(payload): # more accurate then before
pre_ip_headers = struct.unpack("!2B3H2BH4s4s", payload[:20])
ihl = (pre_ip_headers[0] & 0x0F) * 4 # IP Header Length, 4bytes = 1word
ip_payloads = payload[ihl:]
return pre_ip_headers, ip_payloads
def parseICMPHeader(icmp_data):
icmp_headers = struct.unpack("!2B3H", icmp_data[:8])
icmp_payloads = icmp_data[8:]
return icmp_headers, icmp_payloads
def parseTCPHeader(payload): # Calculate TCP Header's Length
pre_tcp_headers = struct.unpack("!2H2L2B3H", payload[:20])
tcp_header_length = (pre_tcp_headers[4] >> 4) * 4
return pre_tcp_headers, payload[tcp_header_length:]
def parseUDPHeader(payload):
pre_udp_headers = struct.unpack("!4H", payload[:8])
return pre_udp_headers, payload[8:]
def tcpFlags(int_num):
return str(bin(int_num))[2:].zfill(8) # return tcp flag as binary numbers
def parsing(host): # creating and binding raw socket
if os.name == "nt":
sock_protocol = IPPROTO_IP
else:
sock_protocol = IPPROTO_ICMP
sock = socket(AF_INET, SOCK_RAW, sock_protocol)
sock.bind((host, 0))
sock.setsockopt(IPPROTO_IP, IP_HDRINCL, 1) # socket option
if os.name == "nt": # turn on the promiscuous mode
sock.ioctl(SIO_RCVALL, RCVALL_ON)
packet_number = 0
try:
while True:
packet_number += 1
data = sock.recvfrom(65535)
ip_headers, ip_payloads = parseIPHeader(data[0])
ip_source_address = inet_ntoa(ip_headers[8])
ip_destination_address = inet_ntoa(ip_headers[9])
if ip_headers[6] == 6: # only TCP
print(
f"{packet_number}th packet\n[TCP] {ip_source_address} -> {ip_destination_address}"
)
tcp_headers, tcp_payloads = parseTCPHeader(ip_payloads)
print("Source port: ", tcp_headers[0])
print("Destination port: ", tcp_headers[1])
print("Sequence number: ", tcp_headers[2])
print("Acknowledgment number: ", tcp_headers[3])
print("Offset(Length): ", tcp_headers[4] >> 4)
print("TCP flags:")
print(" CEUAPRSF")
print(" ", tcpFlags(tcp_headers[5]))
print("Window size: ", tcp_headers[6])
print("Checksum: ", tcp_headers[7])
print("Urgent pointer: ", tcp_headers[8])
print("TCP payloads: ")
print(tcp_payloads.decode("utf-8", "ignore"))
elif ip_headers[6] == 17: # only UDP
print(
f"{packet_number}th packet\n[UDP] {ip_source_address} -> {ip_destination_address}"
)
udp_headers, udp_payloads = parseUDPHeader(ip_payloads)
print("Source port: ", udp_headers[0])
print("Destination port: ", udp_headers[1])
print("Length: ", udp_headers[2])
print("Checksum: ", udp_headers[3])
print("UDP payloads: ", udp_payloads.decode("utf-8", "ignore"))
print("=" * 30)
except KeyboardInterrupt: # input Ctrl+C
if os.name == "nt":
sock.ioctl(SIO_RCVALL, RCVALL_OFF)
def main():
host = "192.168.0.9"
print(f"Start sniffing at {host}")
parsing(host)
if __name__ == "__main__":
main()
위 그림은 IP 헤더의 첫 1byte이며, 왼쪽 4bits는 IP Version, 오른쪽 4bits는 IP Header Length라고 이전에 설명했죠? 코드의 AND 연산(0x0F = 1111)을 통해 IHL에 해당하는 비트를 구할 수 있답니다.
'自習 > Network' 카테고리의 다른 글
Network - HTTP (0) | 2021.07.30 |
---|---|
Network - Web (0) | 2021.07.30 |
Network - Ping sweep scaner (0) | 2021.07.12 |
Network - Internet Control Message Protocol (0) | 2021.07.01 |
Network - Internet Protocol (0) | 2021.07.01 |