TCP 소켓 통신 문제

익명 사용자의 이미지

간단히 설명하자면 클라이언트가 서버에 메시지를 보내고 서버가 똑같이 응답해주는 일종의 에코 서버입니다. 그런데 외부에서는 접속이 잘만 되는데 같은 컴퓨터에서 서버와 클라이언트를 동시에 구동하면 접속이 거부됩니다. 아래는 전체 소스코드 내용입니다.

서버 소스 코드:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main() {
	int serd, clid;
	char buffer[BUFSIZ];
	struct sockaddr_in socketaddr;
	struct sockaddr socbuf;
	socklen_t soclen = sizeof(struct sockaddr_in);
	ssize_t sockrwlen;
 
	memset((char*)&socketaddr, 0, sizeof(struct sockaddr_in));
	memset(&socketaddr.sin_zero, 0, 8);
	socketaddr.sin_family = AF_INET;
	socketaddr.sin_port = htons(19780);
	socketaddr.sin_addr.s_addr = inet_addr("192.168.219.102");
	serd = socket(AF_INET, SOCK_STREAM, 0);
	if (serd < 0) {
		perror("socket");
		return -1;
	}
	bind(serd, (struct sockaddr *)&socketaddr, sizeof(struct sockaddr));
 
	listen(serd, 1);
	clid = accept(serd, &socbuf, &soclen);
	if (clid < 0) {
		perror("accept");
		return -1;
	}
	printf("Connected.\n");
 
	while ((sockrwlen = recv(clid, buffer, BUFSIZ, 0)) >= 0) {
		printf("Received : '%s'\nSize : %ld\n", buffer, sockrwlen);
		sockrwlen = send(clid, buffer, sockrwlen, 0);
		printf("Sent. Size : %ld\n", sockrwlen);
		if (strcmp(buffer, "/q") == 0) break;
	}
	close(clid);
	close(serd);
	printf("Disconnected.\n");
	return 0;
}

클라이언트 소스 코드:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main() {
	int clid;
	char buffer[BUFSIZ];
	struct sockaddr_in socketaddr;
	struct sockaddr socbuf;
	ssize_t sockrwlen;
 
	memset((char*)&socketaddr, 0, sizeof(struct sockaddr_in));
	socketaddr.sin_family = AF_INET;
	socketaddr.sin_port = htons(19781);
	socketaddr.sin_addr.s_addr = inet_addr(some_thing);
	clid = socket(AF_INET, SOCK_STREAM, 0);
	if (clid < 0) {
		perror("socket");
		return -1;
	}
	printf("Connecting...\n");
	if (connect(clid, (struct sockaddr *)&socketaddr, sizeof(struct sockaddr_in))) {
		perror("connect");
		return -1;
	}
	printf("Connected.\n");
 
	while (1) {
		printf("Input a message send to server : ");
		fgets(buffer, BUFSIZ, stdin);
		buffer[strlen(buffer)-1] = '\0';
		sockrwlen = send(clid, buffer, strlen(buffer)+1, 0);
		sockrwlen = recv(clid, buffer, BUFSIZ, 0);
		printf("from server : %s\n", buffer);
		if (strcmp(buffer, "/q") == 0) break;
	}
 
	if (sockrwlen < 0) {
		perror("recv");
		return -1;
	}
 
	close(clid);
	printf("Disconnected.\n");
	return 0;
}

클라이언트 소스에서 some_thing은 공인 IP입니다. 개인정보 문제상 가렸습니다.

공유기에서 포트포워딩으로 19781-19789는 내부 IP 192.168.219.102:19780으로 연결시켰고 사용하고 있는 19780과 19781 포트는 방화벽에서 허용이 되어있습니다.

즉 외부에서는 접속이 되지만 같은 컴퓨터 내에서는 접속이 거부되거나 연결이 도저히 이어지질 않습니다. 예외적으로 외부에서도 접속이 안될때는 방화벽을 재설정했을때만 그렇고 나머지는 모든 상황에서 외부 연결만 정상적으로 됩니다.

도대체 무엇이 문제일까요?

chanik의 이미지

공인IP를 xxx.xxx.xxx.xxx 라고 할 때, 클라이언트에서는 아래와 같은 발신지/목적지 주소를 달고 SYN 패킷이 전송되고 소켓은 SYN-SENT 상태로 들어갈겁니다.

  Proto  Local Address          Foreign Address        State
  TCP    192.168.219.102:nnnnn  xxx.xxx.xxx.xxx:19781  SYN-SENT

서버에서는 공유기를 거쳐 (발신:192.168.219.102:nnnnn, 수신:192.168.219.102:19780) 식으로 목적지가 변조된 SYN 패킷을 받게 될겁니다. 그리고 응답으로 SYN+ACK 패킷을 보내야 하는데, 서버가 받은 SYN 패킷의 발신지/수신지 IP가 동일하므로 패킷이 랜카드를 통해 공유기로 보내지는 것이 아니고 커널 내에서 바로 처리되겠죠.

하지만 (발신:192.168.219.102:19780, 수신:192.168.219.102:nnnnn)인 SYN+ACK 패킷에 매칭되는 소켓이 없으므로 접속 체결에 문제가 생길겁니다. 클라이언트 소켓은 xxx.xxx.xxx.xxx:19781 로부터 오는 SYN+ACK를 기다리는 것이지 192.168.219.102:19780 으로부터의 응답을 기다리는 것이 아니니까요.

PC에서 wireshark으로 "tcp.port == 19780 or tcp.port == 19781" 정도의 필터를 걸어서 캡쳐하면서 실험해보시면 패킷들의 모습이 보일 것입니다. 클라이언트는 몇 번에 걸쳐 SYN 패킷을 재전송하지만 서버로부터의 SYN+ACK는 받지 못하므로 결국 접속실패에 이르는 거죠.

같은 PC에서뿐 아니라 같은 서브넷상의 다른 PC로부터도 xxx.xxx.xxx.xxx:19781 식으로 공인IP를 목적지로 하는 접속에는 문제가 생길겁니다. 캡처하면서 관찰하시면 무슨 일이 벌어지는지 확인 가능합니다.

같은 PC 또는 같은 서브넷상에서 접속할 때는 목적지 주소를 공인IP 말고 192.168.219.102:19780 로 바로 주시면 되겠습니다.

chanik의 이미지

위 답변은 제가 쓰는 환경에서 간단한 테스트를 해본 결과를 적은 것입니다. 시중에서 파는 공유기를 쓴 것이 아니라 리눅스PC를 공유기처럼 사용하는 상황에서 해 본 것이고요.

그런데, ipTIME 공유기에서 해보니 같은 PC 상에서도 포트포워딩을 통한 서버/클라이언트 접속이 잘 되네요.

잘 정돈해서 설명할 수 있을지 모르겠는데, 일단 PC의 클라이언트에서 xxx.xxx.xxx.xxx:19781 으로 접속 시도하면 SYN 패킷이 아래와 같은 주소를 달고 공유기로 발송됩니다.

SYN: PC -> 공유기 (출발지:192.168.219.102:nnnnn, 목적지:xxx.xxx.xxx.xxx:19781)

공유기에서는 DNAT가 이뤄져 아래와 같이 목적지 주소/포트가 변경되어 PC의 서버로 전달되죠.

SYN: 공유기 -> PC (출발지:192.168.219.102:nnnnn, 목적지:192.168.219.102:19780)

제가 쓰는 리눅스 공유기는 이렇게 동작합니다. 정확히는, 그렇게 동작하도록 제가 설정해둔 셈이죠. 이렇게 되면 이전 글에 적은대로 소켓 접속이 완성되지 못하고 실패로 끝납니다. 그런데, ipTIME 공유기에서 테스트해보니 DNAT만 이뤄지는 것이 아니고 SNAT까지 이뤄져 아래와 같이 되더군요 (공유기IP: 192.168.219.1로 가정).

SYN: 공유기 -> PC (출발지:192.168.219.1:nnnnn, 목적지:192.168.219.102:19780)

이렇게 되면, 서버소켓의 SYN+ACK 응답은 일단 공유기로 향하게 되고, 아래와 같이 공유기에서 SNAT/DNAT를 적절히 거쳐서 다시 PC의 클라이언트 소켓으로 제대로 전달되게 됩니다.

SYN+ACK: PC -> 공유기 (출발지:192.168.219.102:19780, 목적지:192.168.219.1:nnnnn)
SYN+ACK: 공유기 -> PC (출발지:xxx.xxx.xxx.xxx:19781, 목적지:192.168.219.102:nnnnn)

netstat 해보면 아래와 같은 모습을 보이게 될 것이고요.

  Proto  Local Address          Foreign Address        State
  TCP    192.168.219.102:19780  0.0.0.0:0              LISTENING
  TCP    192.168.219.102:nnnnn  xxx.xxx.xxx.xxx:19781  ESTABLISHED
  TCP    192.168.219.102:19780  192.168.219.1:nnnnn    ESTABLISHED

첫 번째 줄은 리스닝 소켓이고
두 번째 줄은 클라이언트쪽 소켓, 세 번째 줄이 서버쪽 소켓입니다.

서버와 클라이언트는 한 PC에서 동작하면서도 서로 상대방의 주소를 착각하여 다른 컴에 있는 것으로 알고 있고, 공유기에서 SNAT/DNAT를 통해 이 착각을 현실로 만들어주는 셈입니다. 저도 전에 비슷한 현상을 겪고 의문을 느끼다 대충 넘어간 적이 있어 질문을 보고 호기심이 생겨 테스트해봤습니다.

지금 쓰시는 공유기에서 DNAT만 이뤄지고 있는지, SNAT까지 이뤄지고 있는지 확인해보시면 원인이 나올 것이라 생각하고, 확인은 Wireshark 등으로 패킷캡처를 통해 할 수 있을 겁니다.

익명 사용자의 이미지

같은 컴퓨터에서 서버/클라이언트를 동시에 돌리면 그럴수 밖에 없겠네요
서버는 19780포트로 listen되어 있고
클라이언트는 19781 포트로 접근하니까요.

참고로 port forwarding은 l3 구간입니다.

같은 서브넷 네트워크 l2에선 적용이 안되요

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.