Linux Kernel IPv6

24 minute read

해당 포스트에서는 리눅스 커널의 IPv6 구현에 대해 설명합니다.

IPv6

IPv6는 IPv4 프로토콜의 주소가 32비트라는 제한된 주소 공간의 한계점으로 인해 지속적인 인터넷 발전에 문제가 예상되어, 이에 대한 대안으로 제안된 네트워크 계층 프로토콜이다.
IPv6의 커널 구현을 살펴보면 IPv4와 많은 유사점을 찾을 수 있다. 따라서, IPv4 포스트에서 설명한 내용을 기반으로 변경된 부분들을 보이고 새로운 기능을 설명한다.

IPv6 주소

IPv6 주소는 세 가지 유형이 있다.

  • 유니캐스트 : 인터페이스를 식별하는 주소이다. 유니캐스트 주소로 전송한 패킷은 해당 주소로 식별되는 인터페이스에 전달된다.
  • 애니캐스트 : 인터페이스 그룹에 할당되는 주소이다. 애니캐스트 주소로 전송한 패킷은 그룹 내의 인터페이스 중 어느 하나에 전달된다. (라우팅 프로토콜에 따라 가장 가까운 인터페이스에 전달된다.)
  • 멀티캐스트 : 인터페이스 그룹에 할당되는 주소이다. 멀티캐스트 주소로 전송한 패킷은 그룹 내의 모든 인터페이스에 전달된다.

IPv6 주소는 16비트씩 8블록으로 구성돼 있으며, 총 128비트다. IPv6 주소는 xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx (x는 16진수)로 표현한다.
IPv6 주소 중 “::” 는 모두 0(zero)인 주소를 줄여서 표현한 것이다.
IPv6에는 주소 접두사(prefix)가 사용된다. IPv4의 서브넷 마스크와 같은 방식으로 Address/prefix-length 표기법으로 나타낸다.

IPv6 주소는 시작하는 비트에 따라 주소 유형이 달라진다.

  • 0000 001 (0[2or3]) : NSAP Address
  • 001 ([2or3]): Global Unicast Address
    • 전역으로 사용할 수 있는 유니캐스트 주소이다. 공인 IPv4 주소와 유사한 개념이다.
  • 1111 1110 10 (FE[8-B]) : Link-Local Unicast Address
    • 물리적으로 연결된 링크에서 사용할 수 있는 유니캐스트 주소이다. 다른 링크의 주소와 해당 주소로 통신이 불가능하다.
  • 1111 1110 11 (FE[C-F]) : Site-Local Unicast Address
    • 논리적으로 연결된 사이트에서 사용할 수 있는 유니캐스트 주소이다. RFC 3879에서 사용하지 않기로 지원이 중단되었다.
  • 1111 1111 (FF) : Multicast Address

멀티캐스트 주소는 멀티캐스트 그룹을 정의하는 수단을 제공하며, 노드는 하나 이상의 멀티캐스트 그룹에 속할 수 있다.
멀티캐스트 주소는 FF로 시작한다. 그 다음은 플래그를 나타내는 4비트와 범위를 나타내는 4비트가 따른다. 남은 112 비트는 그룹 ID이다.
플래그 필드의 4비트는 다음과 같은 의미가 있다.

  • 비트 0 : 예약된 필드
  • 비트 1 (R Flag) : 값이 1인 경우 주소에 Rendezvous Point가 삽입됐음을 가리킨다.
  • 비트 2 (P Flag) : 값이 1인 경우 멀티캐스트 주소가 네트워크 접두사를 기반으로 할당됐음을 가리킨다.
  • 비트 3 (T Flag) : 값이 0인 경우 영구적으로 할당된 (Well-known) 멀티캐스트 주소를 가리키며, IANA에서 할당한다. 값이 1인 경우 비영구적으로 할당된 (Tentative) 멀티캐스트 주소를 가리킨다.

멀티캐스트 주소에는 다음과 같이 특수한 Well-Known 타입의 주소가 있다.

  • FF0x::1 : All-Nodes 멀티캐스트 주소이다. 해당 주소 범위에 포함된 모든 노드가 패킷을 수신한다. (해당 주소를 사용하면 브로드캐스트와 같은 효과를 가진다.)
    • x에는 1(Node-Local Scope)와 2(Link-Local Scope) 값을 가질 수 있다.
  • FF0x::2 : All-Routers 멀티캐스트 주소이다. 해당 주소 범위에 포함된 모든 라우터가 패킷을 수신한다.
    • x에는 1(Node-Local Scope)와 2(Link-Local Scope)와 5(Site-Local Scope) 값을 가질 수 있다.

IPv6에도 IPv4와 마찬가지로 특수한 주소가 있다. (예를 들어, 192.168.x.x나 127.0.0.1와 같은 주소이다)
다음은 특수한 IPv6 주소와 각 주소의 사용법이다.

  • 각 인터페이스마다 최소한 하나의 Link-Local 유니캐스트 주소가 있어야 한다. 라우터는 Link-Local 출발지 또는 목적지 주소를 가진 어떤 패킷도 포워딩해서는 안 된다. Link-Local 주소는 fe80::/64 접두사로 할당된다.
  • 전역 유니캐스트 주소의 일반적인 형식은 다음과 같다. 첫 n비트는 전역 라우팅 접두사이며, 다음 m비트는 서브넷 ID, 나머지 128 - n - m 비트는 인터페이스 ID이다.
    • 전역 라우팅 접두사 : Site에 할당된 값으로, 네트워크 ID 또는 주소 접두사를 의미한다.
    • 서브넷 ID : Site 내의 서브넷 식별자이다.
    • 인터페이스 ID : 서브넷 내에서 유일한 인터페이스 ID로, 호스트 주소를 의미한다.
  • IPv6의 루프백 주소는 ::1 이다.
  • 모두 0인 주소 ::는 미지정 주소(unspecified address)라고 한다. 이 주소는 중복 주소 탐지에 사용된다. 인터페이스에 해당 주소를 할당할 수 없으며, 목적지 주소로 지정할 수도 없다.
  • IPv4 매핑 IPv6 주소(IPv4-Mapped IPv6 Address)는 80비트의 0으로 시작한다. 다음 16비트는 1이고, 남은 32비트는 IPv4 주소이다. 예를 들어, IPv4 주소 192.0.2.128를 ::ffff:192.0.2.128로 매핑할 수 있다.
  • IPv4 호환 IPv6 주소(IPv4-Compatible IPv6 Address)는 96비트의 0로 시작하고, 남은 32비트를 IPv4 주소로 할당한다. 예를 들어, IPv4 주소 192.0.2.128를 ::192.0.2.128로 호환할 수 있다.
    • 호환 주소는 실제로 IPv6 주소를 사용하는 듀얼 스택 디바이스에서만 사용할 수 있는 주소이다.

IPv6 주소는 리눅스에서 in6_addr 구조체로 표현한다. 해당 구조체의 정의는 다음과 같다.

#if __UAPI_DEF_IN6_ADDR
struct in6_addr {
	union {
		__u8		u6_addr8[16];
#if __UAPI_DEF_IN6_ADDR_ALT
		__be16		u6_addr16[8];
		__be32		u6_addr32[4];
#endif
	} in6_u;
#define s6_addr			in6_u.u6_addr8
#if __UAPI_DEF_IN6_ADDR_ALT
#define s6_addr16		in6_u.u6_addr16
#define s6_addr32		in6_u.u6_addr32
#endif
};
#endif /* __UAPI_DEF_IN6_ADDR */

IPv6에서 노드는 노드 인터페이스에 설정된 모든 유니캐스트 및 애니캐스트 주소에 대하여 Solicited-Node 멀티캐스트 주소를 계산하고 참가해야 한다.
Solicited-Node 멀티캐스트 주소는 노드의 유니캐스트와 애니캐스트 주소를 토대로 계산된다.
접두사 ff02:0:0:0:0:1:ff00::/104에 유니캐스트 또는 애니캐스트 주소 하위 24비트를 덧붙여서 생성한다.
addrconf_addr_solict_mult() 함수가 이 Solicited-Node 주소를 계산한다.
addrconf_join_solict 함수는 의뢰한 주소 멀티캐스트 그룹에 참가한다.

IPv6 헤더

옵션에 따라 20 바이트부터 60 바이트였던 IPv4의 가변 길이 헤더와 달리, IPv6는 40 바이트의 고정 길이를 가진다.
IPv6에는 옵션 대신 확장 헤더 메커니즘이 있으며, 확장 헤더는 이 후에 설명한다.
IPv6 헤더의 일반적인 포맷은 다음과 같다.
ipv6_header

커널에서 구현한 IPv6 헤더 구조체는 다음과 같다.

struct ipv6hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8			priority:4,
				version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
	__u8			version:4,
				priority:4;
#else
#error	"Please fix <asm/byteorder.h>"
#endif
	__u8			flow_lbl[3];

	__be16			payload_len;
	__u8			nexthdr;
	__u8			hop_limit;

	struct	in6_addr	saddr;
	struct	in6_addr	daddr;
};
  • version : IP 버전을 의미하며, IPv6 이므로 6이다.
  • priority : 트래픽 등급 또는 패킷의 우선 순위를 의미한다.
  • flow_lbl : flow label 필드는 특정 flow의 패킷 순서를 표시하는 방법을 제공한다.
  • payload_len : 헤더를 제외한 패킷의 크기를 나타낸다.
  • nexthdr : 다음 확장 헤더 타입이다. 확장 헤더가 없을 경우 IPPROTO_UDP(17)나 IPRPTO_TCP(6)같은 상위 계층 프로토콜 번호가 된다.
  • hop-limit : IPv4의 TTL 필드와 같이, 포워딩 장치에서 1 감소하는 홉 제한 값이다.
  • saddr : 128비트 IPv6 출발지 주소
  • daddr : 128비트 IPv6 목적지 주소

RFC와 달리 트래픽 등급(priority)이 4비트임에 유의한다.
또한, IPv4 헤더와 달리 IPv6 헤더에는 체크섬 필드가 없다. 체크섬은 2계층과 4계층에서 보장되는 것으로 가정한다.

확장 헤더

IPv6 패킷은 확장 헤더를 0개 이상 포함할 수 있다. 이러한 확장 헤더는 패킷의 IPv6 헤더와 상위 계층 헤더 사이에 위치할 수 있다.
IPv6 헤더의 nexthdr 필드는 바로 다음 확장 헤더의 타입이다. 할당될 수 있는 타입은 다음과 같다.

#define NEXTHDR_HOP		0	/* Hop-by-hop option header. */
#define NEXTHDR_IPV4		4	/* IPv4 in IPv6 */
#define NEXTHDR_TCP		6	/* TCP segment. */
#define NEXTHDR_UDP		17	/* UDP message. */
#define NEXTHDR_IPV6		41	/* IPv6 in IPv6 */
#define NEXTHDR_ROUTING		43	/* Routing header. */
#define NEXTHDR_FRAGMENT	44	/* Fragmentation/reassembly header. */
#define NEXTHDR_GRE		47	/* GRE header. */
#define NEXTHDR_ESP		50	/* Encapsulating security payload. */
#define NEXTHDR_AUTH		51	/* Authentication header. */
#define NEXTHDR_ICMP		58	/* ICMP for IPv6. */
#define NEXTHDR_NONE		59	/* No next header */
#define NEXTHDR_DEST		60	/* Destination options header. */
#define NEXTHDR_SCTP		132	/* SCTP message. */
#define NEXTHDR_MOBILITY	135	/* Mobility header. */

#define NEXTHDR_MAX		255

다음 그림은 확장 헤더 체인의 예시를 나타낸다.
ipv6_nxthdr

확장 헤더는 홉별 옵션 헤더를 제외하고 패킷이 최종 목적지에 도착할 때까지 경로에 있는 어떠한 노드에서도 처리되지 않는다.
또한, 확장 헤더는 반드시 패킷에 나타난 순서로 처리돼야 한다.
각 확장 헤더는 목적지 옵션 헤더를 제외하고 한 번만 발생한다.
홉별 옵션 헤더는 IPv6 기본 헤더 바로 다음에 나타나야 하며, 그 외 확장 헤더는 어떤 순서로도 나타날 수 있다.
패킷이 처리되는 동안 알 수 없는 다음 헤더 번호를 만나면 icmpv6_param_prob() 함수를 호출해 ICMPV6_UNK_NEXTHDR 메시지를 회신한다.
각 확장 헤더는 반드시 8바이트 경계로 정렬돼야 한다. 가변 길이 확장 헤더의 경우 패딩을 사용하여 정렬한다.

프로토콜 핸들러는 inet6_add_protocol() 함수를 통해 각 확장 헤더에 대해 등록한다.
확장 헤더 중 ‘홉 별 옵션 헤더’는 프로토콜 핸들러에 등록되지 않는데, ip6_rcv_core() 함수에서 nexthdr 값이 NEXTHDR_HOP 인 경우 ipv6_parse_hopopts() 함수를 호출하여 특별히 처리하기 때문이다.
확장 헤더에 대한 프로토콜 핸들러 등록 예시는 다음과 같다. (단편화 확장 헤더 등록)

static const struct inet6_protocol frag_protocol = {
	.handler	=	ipv6_frag_rcv,
	.flags		=	INET6_PROTO_NOPOLICY,
};

int __init ipv6_frag_init(void)
{
	...
	ret = inet6_add_protocol(&frag_protocol, IPPROTO_FRAGMENT);
	if (ret)
		goto err_protocol;
	...
}

다음은 IPv6 확장 헤더에 대한 간단한 설명이다. 자세한 내용은 tcp/ip guide의 IPv6 챕터를 참고한다.

  • 홉 별(Hop-by-Hop) 옵션 헤더 : 경로 상에 있는 모든 노드에서 처리되어야 할 옵션에 대한 헤더이다. 해당 헤더가 존재하는 경우, 항상 IPv6 기본 헤더 바로 뒤에 위치하며, ipv6_parse_hopopts() 함수를 통해 파싱된다.
  • 라우팅 옵션 헤더 : 최종 목적지로 향하는 패킷의 경로를 따라 방문하는 하나 이상의 라우터를 지정할 수 있는 기능을 제공한다.
  • 단편화 옵션 헤더 : IPv6에서 단편화는 패킷을 전송하는 호스트에서만 발생할 수 있다. (중간 라우터에서는 단편화가 발생하지 않는다.) 단편화는 ip6_finish_output() 함수에서 ip6_fragment() 함수를 호출하여 처리한다.
  • 인증 헤더 : 데이터 인증, 데이터 무결성, Replay 공격 보호 등을 제공한다.
  • ESP 헤더 : Encapsulating Security Payload 프로토콜은 암호화 및 캡슐화 등의 보안 관련 기능을 제공한다.
  • 목적지 옵션 헤더 : 목적지 옵션 헤더는 패킷의 라우팅 옵션 헤더 앞과 뒤에 두 번 나타날 수 있다. 라우팅 옵션 헤더 앞에 있으면 라우터 옵션 헤더에 지정된 라우터에서 처리할 정보를 포함한다. 라우터 옵션 헤더 다음에 있으면 최종 목적지에서 처리할 정보를 포함한다.

IPv6 초기화

IPv6 초기화는 다음과 같이 부팅 시에 inet6_init() 함수에서 진행한다.

static int __init inet6_init(void)
{
	struct list_head *r;
	int err = 0;

	...
	err = ip6_mr_init();
	if (err)
		goto ipmr_fail;
	err = icmpv6_init();
	if (err)
		goto icmp_fail;
	err = ndisc_init();
	if (err)
		goto ndisc_fail;
	err = igmp6_init();
	if (err)
		goto igmp_fail;

	err = ipv6_netfilter_init();
	if (err)
		goto netfilter_fail;
	...
	err = ip6_route_init();
	if (err)
		goto ip6_route_fail;
	err = ndisc_late_init();
	if (err)
		goto ndisc_late_fail;
	err = ip6_flowlabel_init();
	if (err)
		goto ip6_flowlabel_fail;
	err = ipv6_anycast_init();
	if (err)
		goto ipv6_anycast_fail;
	err = addrconf_init();
	if (err)
		goto addrconf_fail;

	/* Init v6 extension headers. */
	err = ipv6_exthdrs_init();
	if (err)
		goto ipv6_exthdrs_fail;
	err = ipv6_frag_init();
	if (err)
		goto ipv6_frag_fail;
	...
	err = ipv6_packet_init();
	if (err)
		goto ipv6_packet_fail;
	...
}

static struct packet_type ipv6_packet_type __read_mostly = {
	.type = cpu_to_be16(ETH_P_IPV6),
	.func = ipv6_rcv,
	.list_func = ipv6_list_rcv,
};

static int __init ipv6_packet_init(void)
{
	dev_add_pack(&ipv6_packet_type);
	return 0;
}

위와 같이 IPv6 서브시스템 (이웃 탐색, 멀티캐스트 라우팅, 라우팅 서브시스템 등) 초기화를 진행하고, ipv6_packet_init() 함수로 프로토콜 핸들러를 등록한다.
그 결과 ethertype이 ETH_P_IPV6(0x86DD)인 패킷은 ipv6_rcv() 함수를 통해 처리될 것이다.

Autoconfiguration

자동설정은 호스트가 자신의 각 인터페이스에 대해 유일한 주소를 얻거나 생성하는 메커니즘이다.
IPv6 자동설정은 부팅 시 시작된다. 노드는 자신의 인터페이스에 대한 Link-Local 주소를 생성한다.
해당 주소는 임시(tentative) 주소로 간주되고, 이 때 노드는 이웃 탐색 메시지로만 통신할 수 있다.
노드는 해당 주소가 DAD (중복 주소 탐지) 메커니즘을 통해 링크 상의 다른 노드에서 이미 사용되고 있지 않다는 것을 확인해야 한다.
해당 주소가 유일하지 않다면 자동설정 과정은 중단되고 수동 설정이 필요할 것이다.
주소가 유일하다면 다음 단계로 All-Routers 멀티캐스트 그룹 주소(FF02::2)에 하나 이상의 라우터 의뢰(Router Solicitation)를 전송한다.
위 과정은 addrconf_dad_completed() 함수에서 ndisc_send_rs() 함수를 호출하여 수행한다.
라우터에서는 All-Nodes 멀티캐스트 그룹 주소(FF02::1)에 라우터 알림(Router Advertisement) 메시지로 응답한다.

호스트가 라우터 알림 메시지를 수신하면 호스트는 자신의 주소와 여러 매개변수를 자동으로 설정할 수 있으며, 기본 라우터를 선택할 수도 있다.
또한, 호스트에 자동으로 설정되는 주소에 선호 수명(preferred lifetime)과 유효 수명(valid lifetime)도 설정할 수 있다.
선호 수명 시간이 끝나면 해당 주소는 통신을 멈추고, 유효 수명 시간이 끝나면 주소가 제거된다.
해당 내용은 inet6_ifaddr 구조체의 prefered_lft 필드와 valid_lft 필드로 표현된다.

IPv6 패킷 수신

IPv6 패킷 수신 Flow를 간단하게 도식화하면 다음과 같다.
ipv6_rcv 전체적인 흐름은 IPv4의 패킷 수신과 유사하다. 목적지가 로컬 호스트라면 ip6_input() 함수에 도착할 것이고, 포워딩 될 패킷이면 ip6_forward() 함수로 처리될 것이다.
위 다이어그램에서 listified packet은 ipv6_list_rcv()와 ip6_list_rcv_finish() 함수로 처리될 것이다.
주요 온전성 검사는 ipv6_rcv() 함수나 ipv6_list_rcv() 함수에서 호출하는 ip6_rcv_core() 함수에서 수행한다.
해당 함수에서 패킷 타입이 PACKET_OTHERHOST이거나, IPv6 헤더의 버전이 6이 아니거나, 목적지 주소가 루프백이거나, 출발지 주소가 멀티캐스트 주소이거나 등의 경우엔 패킷을 드랍한다. 이 후 헤더의 nexthdr 값이 NEXTHDR_HOP이라면 (즉, Hop-by-Hop 옵션 헤더를 가지고 있다면) 다음과 같이 해당 확장 헤더를 처리한다.

	if (hdr->nexthdr == NEXTHDR_HOP) {
		if (ipv6_parse_hopopts(skb) < 0) {
			__IP6_INC_STATS(net, idev, IPSTATS_MIB_INHDRERRORS);
			rcu_read_unlock();
			return NULL;
		}
	}

이 후 ip6_rcv_finish_core() 함수에서는 ip6_route_input() 함수를 호출하여 SKB에 dst 객체가 없는 경우 라우팅 서브시스템 탐색을 수행한다.
ip6_route_input() 함수에서는 ip6_route_input_lookup() 함수를 호출하고, 해당 함수에서 fib6_rule_lookup() 함수를 호출하여 라우팅 테이블을 탐색한다.
ip6_rcv_finish_core() 함수에서 라우팅 서브시스템 탐색이 끝나면, 해당 함수를 호출했던 ip6_rcv_finish() 함수나 ip6_list_rcv_finish() 함수로 돌아와 dst_input() 함수를 호출한다. 해당 함수에서는 라우팅 서브시스템을 탐색하며 연관된 dst 객체의 input 콜백 함수를 호출한다.
IPv6 라우팅 서브시스템을 탐색하고 나면 목적지 캐시 dst의 input 콜백이 다음과 같이 설정된다.

  • 패킷이 로컬 머신을 목적지로 하면 ip6_input()
  • 패킷이 포워딩돼야 하면 ip6_forward()
  • 패킷이 멀티캐스트 주소를 목적지로 하면 ip6_mc_input()
  • 패킷이 폐기돼야 하면 ip6_pkt_discard()

위 내용은 ip6_rt_init_dst() 함수에서 확인할 수 있다.

로컬 전달

로컬 전달을 위한 ip6_input() 함수는 넷필터 콜백 이후 ip6_input_finish() 함수를 호출하도록 등록한다.
ip6_input_finish() 함수는 결국 ip6_protocol_deliver_rcu() 함수를 호출하여 로컬 전달에 관한 내용을 처리한다.
ip6_protocol_deliver_rcu() 함수의 주요 내용은 다음과 같다.

  1. RAW 소켓이라면 RAW 소켓 핸들링 루틴으로 처리한다.
  2. ipprot = rcu_dereference(inet6_protos[nexthdr]) 라인과 이 후로 확장 헤더를 파싱한다.
  3. !ipv6_is_mld(skb, nexthdr, skb_network_header_len(skb) 라인에서 필터링 될 멀티캐스트 패킷 중 MLD 패킷이 아니라면 drop한다.
  4. IPSec 정책 검사를 수행한다.
  5. ret = INDIRECT_CALL_2(ipprot->handler, tcp_v6_rcv, udpv6_rcv, skb); 라인으로 다음 확장 헤더 혹은 다음 계층에 대한 핸들러를 호출한다.
  6. ret 값이 0보다 크다면 (확장 헤더라면) 다음 확장 헤더 파싱을 위해 resubmit 레이블로 이동하고, 0이라면 통계를 업데이트 후 종료한다.

    포워딩

포워딩 핸들러 ip6_forward() 함수의 주요 내용은 다음과 같다.

  1. pkt_type이 PACKET_HOST가 아니거나, LRO 패킷이거나, IPSec 정책에 맞지 않거나 등의 경우 패킷은 drop된다.
  2. hop_limit 값을 검사한다. (기존에 1 이하면 ICMPv6 오류 메시지 회신)
  3. IPSec 관련 보안 검사를 수행한다.
  4. MTU 검사를 수행한다. IPv6에서의 최소 MTU는 1280 바이트이다. MTU보다 패킷이 크다면 ICMPv6 오류 메시지를 회신한다. (포워딩 과정에서 단편화를 수행하지 않고, 바로 ICMPv6 메시지를 회신한다.)
  5. ho_limit 값을 감소하고 넷필터 훅에 ip6_forward_finish() 함수를 등록하여 호출한다.

ip6_forward_finish() 함수는 다음과 같이 tstamp를 0으로 설정하고 dst_output() 함수를 호출한다.

static inline int ip6_forward_finish(struct net *net, struct sock *sk,
				     struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb);

	__IP6_INC_STATS(net, ip6_dst_idev(dst), IPSTATS_MIB_OUTFORWDATAGRAMS);
	__IP6_ADD_STATS(net, ip6_dst_idev(dst), IPSTATS_MIB_OUTOCTETS, skb->len);

#ifdef CONFIG_NET_SWITCHDEV
	if (skb->offload_l3_fwd_mark) {
		consume_skb(skb);
		return 0;
	}
#endif

	skb->tstamp = 0;
	return dst_output(net, sk, skb);
}

멀티캐스트 패킷 수신

앞서 ip6_rcv_finish_core() 함수에서 ip6_route_input() 함수를 호출하여 라우팅 서브시스템의 탐색을 수행한다고 하였다.
해당 함수에서 멀티캐스트 패킷을 수신하는 경우에 input 콜백이 ip6_mc_input() 함수로 설정된다.
ip6_mc_input() 함수의 주요 내용은 다음과 같다.

  1. deliver = ipv6_chk_mcast_addr(dev, &hdr->daddr, NULL); 라인으로 지정한 멀티캐스트 목적지 주소에 인터페이스가 포함되어 있는지 검사한다.
  2. CONFIG_IPV6_MROUTE 매크로로 묶인 부분에서 멀티캐스트 라우터 관련 처리를 수행한다.
    1. MLD 체크를 수행한다.
    2. 패킷을 복사한다.
    3. ip6_mr_input(skb2) 라인으로 forward - split을 수행한다.
  3. ip6_input() 함수를 호출하여 멀티캐스트 출발지 필터링(MSF; Multicast Source Filtering)을 수행하고, 로컬에 전달한다.

멀티캐스트 리스너 탐색 (MLD)

MLD 프로토콜은 멀티캐스트 호스트와 라우터 사이의 그룹 정보를 교환하는 데 사용한다.
MLD는 IGMP에서 파생되었지만, ICMPv6 프로토콜을 사용한다.
IPv6에서는 MLDv2를 사용하는데, 이는 MLDv1의 모든 출발지 멀티캐스트(ASM; Any-Source Multicast) 모델에 출발지 지정 멀티캐스트(SSM; Source Specific Multicast)에 대한 지원 기능을 추가하였다.
SSM은 노드가 특정 유니캐스트 출발지 주소로부터 전송된 패킷에 대한 리스닝을 포함하거나 배제하는 기능인데, 이를 출발지 필터링이라고 한다.
MLDv2 프로토콜은 멀티캐스트 리스너 보고(Report)와 멀티캐스트 리스너 질의(Query)를 기반으로 한다.
MLDv2 라우터는 노드의 멀티캐스트 그룹 상태를 파악하기 위해 주기적으로 멀티캐스트 리스너 질의를 보낸다.
여러 MLDv2 라우터가 동일 링크 상에 있다면 그 중 하나만 질의자(Querier)로 선택되고, 다른 라우터는 비질의자 상태로 설정된다.
노드는 이 질의에 멀티캐스트 리스너 보고로 응답하며, 이 때 노드가 속한 멀티캐스트 그룹에 관한 정보를 제공한다.
리스너가 일부 멀티캐스트 그룹에 대한 리스닝을 멈추고 싶으면 질의자에게 이에 관해 알리고, 질의자는 해당 리스너의 멀티캐스트 주소 리스너 상태를 삭제하기 전에 해당 멀티캐스트 그룹 주소의 다른 리스너에게 질의해야 한다.
MLDv2 라우터는 멀티캐스트 라우팅 프로토콜에 리스너에 관한 상태 정보를 제공할 수 있다.

멀티캐스트 그룹 참가와 탈퇴

멀티캐스트 그룹에 참가하거나 탈퇴하는 방법은 다음과 같은 두 가지가 있다.

  1. 커널 내에서 ipv6_dev_mc_inc() / ipv6_dev_mc_dec() 함수 호출
  2. 유저 스페이스에서 IPV6_JOIN_GROUP / IPV6_DROP_MEMBERSHIP 소켓 옵션으로 setsockopt() 함수를 호출

유저 스페이스에서 setsockopt() 함수를 호출하는 경우 결국 그에 대응하는 ipv6_dev_mc_inc() / ipv6_dev_mc_dec() 함수를 호출한다.
이 함수는 다음과 같이 매개변수로 네트워크 장치 객체와 멀티캐스트 그룹 주소를 받는다.

int ipv6_dev_mc_inc(struct net_device *dev, const struct in6_addr *addr);
int ipv6_dev_mc_dec(struct net_device *dev, const struct in6_addr *addr);

예를 들어, 네트워크 장치를 등록할 때 ipv6_add_dev() 함수를 호출한다.
각 인터페이스는 Interface-Local(Node-Local) All-Nodes 멀티캐스트 그룹(FF01::1)과 Link-Local All-Nodes 멀티캐스트 그룹(FF02::1)에 참가해야 한다.

	/* Join interface-local all-node multicast group */
	ipv6_dev_mc_inc(dev, &in6addr_interfacelocal_allnodes);

	/* Join all-node multicast group */
	ipv6_dev_mc_inc(dev, &in6addr_linklocal_allnodes);

멀티캐스트 리스너 보고 (Report)

멀티캐스트 리스너 보고는 다음과 같이 mld2_report 구조체로 표현한다.

struct mld2_grec {
	__u8		grec_type;
	__u8		grec_auxwords;
	__be16		grec_nsrcs;
	struct in6_addr	grec_mca;
	struct in6_addr	grec_src[];
};

struct mld2_report {
	struct icmp6hdr		mld2r_hdr;
	struct mld2_grec	mld2r_grec[];
};

mld2r_grec[] 멤버가 MLDv2 그룹 레코드(멀티캐스트 주소 레코드)를 나타낸다.
mld2_grec 구조체 멤버에 대한 설명은 다음과 같다.

  • grec_type : 멀티캐스트 주소 기록의 유형
  • grec_auxwords : 보조 데이터의 길이
  • grec_nsrcs : 출발지 주소의 개수
  • grec_mca : Record와 관련된 멀티캐스트 주소
  • grec_src[] : 유니캐스트 출발지 주소 (필터링하려는 주소)

멀티캐스트 출발지 필터링 (MSF)

커널은 멀티캐스트 출발지 필터링을 통해 예상되는 하나 이상의 출발지 멀티캐스트 통신을 폐기한다.
호스트는 유저 스페이스에서 IPv6 소켓을 열어 group_source_req 객체를 생성하고, 요청에 다음과 같은 세 매개변수를 설정하여 필터링을 통해 멀티캐스트 그룹에 참여할 수 있다. (해당 방법으로 MSF를 이용할 수 있다.)

  • gsr_group : 호스트가 가입하려는 멀티캐스트 그룹 주소
  • gsr_source : 허용하려는 멀티캐스트 그룹 출발지 주소
  • ipv6_mr_interface : 설정하려는 네트워크 인터페이스의 ifindex

이 후 MCAST_JOIN_SOURCE_GROUP 소켓 옵션으로 setsockopt() 함수를 호출해야 한다.
이에 대한 예제는 다음과 같다.

int sockd;
struct group_source_req mreq;
struct addrinfo *result1, *result2;

// 참여하길 바라는 IPv6 멀티캐스트 그룹 주소를 넣음
memcpy(&(mreq.gsr_group), results1->ai_addr, sizeof(struct sockaddr_in6));
// 허용하길 바라는 IPv6 멀티캐스트 그룹 주소를 넣음
memcpy(&(mreq.gsr_source), results2->ai_addr, sizeof(struct sockaddr_in6));
// 설정하려는 네트워크 인터페이스의 ifindex
mreq.gsr_interface = 3;

sockd = socket(AF_INET6, SOCK_DGRAM, 0);
setsockopt(sockd, IPPROTO_IPV6, MCAST_JOIN_SOURCE_GROUP, &mreq, sizeof(mreq));

이 요청은 커널에서 ipv6_sock_mc_join() 함수와 ip6_mc_source() 함수로 처리된다.
이 후 그룹을 떠나려면 MCAST_LEAVE_SOURCE_GROUP 소켓 옵션으로 setsockopt() 함수를 호출하거나 열었던 소켓을 닫아야 한다.

IPv6 패킷 송신

IPv6의 Tx 경로는 IPv4의 Tx와 유사하다.
4계층인 전송 계층에서 3계층 네트워크 계층으로 패킷을 전송하기 위한 두 가지 주요 함수가 있다.

  1. ip6_xmit()
  2. ip6_append_data()

ip6_xmit() 함수는 TCP, SCTP, DCCP와 같은 프로토콜에서 사용한다.
IPv4에서 SYNACK 메시지 처리를 위한 함수가 따로 존재한 것과 달리, IPv6에서는 SYNACK 메시지 또한 ip6_xmit() 함수로 처리한다.
ip6_append_data() 함수는 UDP, ICMP, RAW 소켓에 사용한다.

로컬 호스트에서 생성된 패킷은 ip6_local_out() 함수를 통해 전송된다. 해당 함수의 정의는 다음과 같다.

int ip6_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	int err;

	err = __ip6_local_out(net, sk, skb);
	if (likely(err == 1))
		err = dst_output(net, sk, skb);

	return err;
}

dst_output() 함수는 ip6_output() 함수를 호출하고, 해당 함수는 ip6_finish_output() 함수를 호출한다.
ip6_finish_output() 함수는 내부에서 __ip6_finish_output() 함수를 호출한다.
이 함수는 필요하다면 ip6_fragment() 함수를 호출하여 단편화를 진행하고, ip6_finish_output2() 함수를 호출하여 패킷을 전송한다.

static int __ip6_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	unsigned int mtu;

#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
	/* Policy lookup after SNAT yielded a new policy */
	if (skb_dst(skb)->xfrm) {
		IPCB(skb)->flags |= IPSKB_REROUTED;
		return dst_output(net, sk, skb);
	}
#endif

	mtu = ip6_skb_dst_mtu(skb);
	if (skb_is_gso(skb) && !skb_gso_validate_network_len(skb, mtu))
		return ip6_finish_output_gso_slowpath_drop(net, sk, skb, mtu);

	if ((skb->len > mtu && !skb_is_gso(skb)) ||
	    dst_allfrag(skb_dst(skb)) ||
	    (IP6CB(skb)->frag_max_size && skb->len > IP6CB(skb)->frag_max_size))
		return ip6_fragment(net, sk, skb, ip6_finish_output2);
	else
		return ip6_finish_output2(net, sk, skb);
}

예제

AF_INET6(IPv6)로 통신하는 유저 스페이스 서버/클라이언트 예제를 만들고, 통신 중에 호출되는 함수들에 대해 dump_stack()으로 콜스택을 덤프한다.

inet6_tcp_serv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <errno.h>

#define LISTEN_BACKLOG	512
#define MAX_POOL	5

int fd_listener;
void start_child(int fd, int idx);

int main(int argc, char *argv[])
{
	int i;
	char *port;
	socklen_t len_saddr;
	pid_t pid;

	if (argc > 2) {
		printf("%s [port number]\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	if (argc == 2)
		port = strdup(argv[1]);
	else
		port = strdup("0");

	struct addrinfo ai, *ai_ret;
	int rc_gai;

	memset(&ai, 0, sizeof(ai));
	ai.ai_family = AF_INET6;
	ai.ai_socktype = SOCK_STREAM;
	ai.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;

	if ((rc_gai = getaddrinfo(NULL, port, &ai, &ai_ret)) != 0) {
		printf("Fail: getaddrinfo():%s\n", gai_strerror(rc_gai));
		exit(EXIT_FAILURE);
	}

	if ((fd_listener = socket(ai_ret->ai_family, ai_ret->ai_socktype,
					ai_ret->ai_protocol)) == -1) {
		printf("Fail: socket()");
		exit(EXIT_FAILURE);
	}

	if (bind(fd_listener, ai_ret->ai_addr, ai_ret->ai_addrlen) == -1) {
		printf("Fail: bind()");
		exit(EXIT_FAILURE);
	}

	if (!strncmp(port, "0", strlen(port))) {
		struct sockaddr_storage saddr_s;
		len_saddr = sizeof(saddr_s);
		getsockname(fd_listener, (struct sockaddr *)&saddr_s, &len_saddr);
		if (saddr_s.ss_family == AF_INET) {
			printf("IPv4 Port : #%d\n", ntohs(((struct sockaddr_in *)&saddr_s)->sin_port));
		} else if (saddr_s.ss_family == AF_INET6) {
			printf("IPv6 Port : #%d\n", ntohs(((struct sockaddr_in6 *)&saddr_s)->sin6_port));
		} else {
			printf("ss_family = %d\n", saddr_s.ss_family);
		}
	}

	listen(fd_listener, LISTEN_BACKLOG);
	for (i = 0; i < MAX_POOL; i++) {
		switch (pid = fork()) {
			case 0:
				start_child(fd_listener, i);
				exit(EXIT_SUCCESS);
			case -1:
				printf("Fail: fork()\n");
				break;
			default:
				printf("Making child process No.%d\n", i);
				break;
		}
	}

	for (;;)
		pause();
	return 0;
}

void start_child(int sfd, int idx)
{
	int cfd, ret_len, rc_gai;
	socklen_t len_saddr;
	char buf[64], addrstr[INET6_ADDRSTRLEN], portstr[8];
	struct sockaddr_storage saddr_c;

	for (;;) {
		len_saddr = sizeof(saddr_c);
		if ((cfd = accept(sfd, (struct sockaddr *)&saddr_c, &len_saddr)) == -1) {
			printf("Fail: accept()");
			close(cfd);
			continue;
		}

		rc_gai = getnameinfo((struct sockaddr *)&saddr_c, len_saddr, addrstr, sizeof(addrstr),
				portstr, sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV);
		if (rc_gai) {
			printf("Fail: getnameinfo(): %s\n", gai_strerror(rc_gai));
			exit(EXIT_FAILURE);
		}

		if (saddr_c.ss_family == AF_INET) {
			printf("[Child:%d] accept IPv4 (ip:port) (%s:%s)\n", idx, addrstr, portstr);
		} else if (saddr_c.ss_family == AF_INET6) {
			printf("[Child:%d] accept IPv6 (ip:port,scope) (%s:%s,%d)\n", idx, addrstr, portstr,
					((struct sockaddr_in6 *)&saddr_c)->sin6_scope_id);
		}

		for (;;) {
			memset(buf, 0, sizeof(buf));
			ret_len = recv(cfd, buf, sizeof(buf), 0);
			if (ret_len == -1) {
				if (errno == EINTR)
					continue;
				printf("[Child:%d] Fail: recv(): %s\n", idx, strerror(errno));
				break;
			}

			if (ret_len == 0) {
				printf("[Child:%d] Session closed\n", idx);
				close(cfd);
				break;
			}

			buf[ret_len] = '\0';

			printf("[Child:%d] RECV(%d)[%.*s]\n", idx, ret_len, ret_len, buf);
			if (send(cfd, buf, ret_len, 0) == -1) {
				printf("[Child:%d] Fail: send() to socket(%d)\n", idx, cfd);
				close(cfd);
			}
		}
	}
}

inet_tcp_clnt.c

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

#define	str_ord		"pr0gr4m"
#define str_oob		"OOB"

int main(int argc, char *argv[])
{
	int fd, rc_gai;
	struct addrinfo ai, *ai_ret;

	if (argc != 3) {
		printf("%s <hostname> <port> \n", argv[0]);
		exit(EXIT_FAILURE);
	}

	memset(&ai, 0, sizeof(ai));
	ai.ai_family = AF_INET6;
	ai.ai_socktype = SOCK_STREAM;
	ai.ai_flags = AI_ADDRCONFIG;

	if ((rc_gai = getaddrinfo(argv[1], argv[2], &ai, &ai_ret)) != 0) {
		printf("Fail: getaddrinfo():%s\n", gai_strerror(rc_gai));
		exit(EXIT_FAILURE);
	}

	if ((fd = socket(ai_ret->ai_family, ai_ret->ai_socktype,
					ai_ret->ai_protocol)) == -1) {
		printf("Fail: socket()\n");
		exit(EXIT_FAILURE);
	}

	if (fcntl(fd, F_SETFL, O_NONBLOCK | fcntl(fd, F_GETFL)) == -1) {
		printf("Fail: fcntl()\n");
		exit(EXIT_FAILURE);
	}

	(void)connect(fd, ai_ret->ai_addr, ai_ret->ai_addrlen);
	if (errno != EINPROGRESS) {
		printf("Fail: connect()\n");
		exit(EXIT_FAILURE);
	}

	fd_set fdset_w;
	FD_ZERO(&fdset_w);
	FD_SET(fd, &fdset_w);

	if (select(fd + 1, NULL, &fdset_w, NULL, NULL) == -1) {
		printf("Fail: select()\n");
		exit(EXIT_FAILURE);
	}

	int sockopt;
	socklen_t len_sockopt = sizeof(sockopt);

	if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &sockopt, &len_sockopt) == -1) {
		printf("Fail: getsockopt()\n");
		exit(EXIT_FAILURE);
	}

	if (sockopt) {
		printf("SO_ERROR: %s(%d)\n", strerror(sockopt), sockopt);
	}

	printf("[nonblocking] connection established\n");
	printf("[Client] : 1:ordinary 2:OOB 0:Exit\n");

	int rc_getline, rc_send, flag_send;
	char *p_sbuf, *p_buf = NULL;
	size_t len_buf;

	for (;;) {
		if ((rc_getline = getline(&p_buf, &len_buf, stdin)) == -1) {
			return EXIT_FAILURE;
		}

		switch (atoi(p_buf)) {
			case 0:
				exit(EXIT_SUCCESS);
				
			case 1:
				p_sbuf = str_ord;
				flag_send = 0;
				printf(">> will send ordinary msg: data = [%s]\n", p_sbuf);
				break;

			case 2:
				p_sbuf = str_oob;
				flag_send = MSG_OOB;
				printf(">> will send OOB msg: data = [%s]\n", p_sbuf);
				break;

			default:
				printf(">> Error : (%s)\n", p_sbuf);
				continue;
		}

		free(p_buf);
		p_buf = NULL;

		if ((rc_send = send(fd, p_sbuf, strlen(p_sbuf), flag_send)) == -1) {
			printf("Fail: send()\n");
		}
	}

	return 0;
}

Makefile


include Makefile.inc

EXE = inet6_tcp_serv inet6_tcp_clnt

all : ${EXE}

clean :
	${RM} *.o ${EXE}

showall:
	@echo ${EXE}

Makefile.inc


LINUX_LIBRT = -lrt
LINUX_LIBDL = -ldl
LINUX_LIBACL = -lacl
LINUX_LIBCRYPT = -lcrypt
LINUX_LIBCAP = -lcap

CC = gcc

COMMON_FLAGS = -std=c99 \
	       -D_XOPEN_SOURCE=700 \
	       -D_DEFAULT_SOURCE \
	       -g \
	       -pedantic \
	       -Wall \
	       -W

CFLAGS = ${COMMON_FLAGS}

RM = rm -f

linux/net/ipv6/ip6_input.c

헤더 파일을 삽입한다.

#include "ip_km.h"

ip6_rcv_core() 함수와 ip6_input_finish() 함수에 다음 라인을 추가한다.

static struct sk_buff *ip6_rcv_core(struct sk_buff *skb, struct net_device *dev,
				    struct net *net)
{
	const struct ipv6hdr *hdr;
	u32 pkt_len;
	struct inet6_dev *idev;

	// 추가된 라인
	if (km_debug_state == KM_DEBUG_IP6_RCV)	// 154라인
		dump_stack();

	if (skb->pkt_type == PACKET_OTHERHOST) {
		kfree_skb(skb);
		return NULL;
	}
	...
}

static int ip6_input_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	rcu_read_lock();
	// 추가된 라인
	if (km_debug_state == KM_DEBUG_IP6_RCV)	// 470 라인 부근
		dump_stack();
	ip6_protocol_deliver_rcu(net, skb, 0, false);
	rcu_read_unlock();

	return 0;
}

linux/net/ipv6/ip6_output.c

헤더 파일을 삽입한다.

#include "ip_km.h"

ip6_finish_output2() 함수에 다음 라인을 추가한다.

static int ip6_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb);
	struct net_device *dev = dst->dev;
	const struct in6_addr *nexthop;
	struct neighbour *neigh;
	int ret;

	if (km_debug_state == KM_DEBUG_IP6_RCV)
		dump_stack();
	...
}

빌드 및 결과

서버측에서 다음과 같이 실행한다.

$ make
$ ./inet6_tcp_serv

클라이언트측에서 다음과 같이 실행한다.

$ make
$ ./inet6_tcp_clnt <server ip6 address> <port>

통신 과정에서 생긴 dmesg 내용은 다음과 같다.

[ 1898.620998] CPU: 3 PID: 0 Comm: swapper/3 Not tainted 5.9.0+ #2
[ 1898.621001] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 02/27/2020
[ 1898.621001] Call Trace:
[ 1898.621003]  <IRQ>
[ 1898.621010]  dump_stack+0x74/0x9a
[ 1898.621013]  ip6_rcv_core.isra.0.cold+0x5/0xa
[ 1898.621014]  ipv6_list_rcv+0xb1/0x140
[ 1898.621017]  __netif_receive_skb_list_core+0x1a4/0x250
[ 1898.621019]  netif_receive_skb_list_internal+0x1a1/0x2b0
[ 1898.621024]  ? e1000_clean_rx_irq+0x366/0x510 [e1000]
[ 1898.621026]  gro_normal_list.part.0+0x1e/0x40
[ 1898.621027]  napi_complete_done+0x71/0x180
[ 1898.621029]  e1000_clean+0x28a/0x5e0 [e1000]
[ 1898.621031]  net_rx_action+0x142/0x390
[ 1898.621033]  __do_softirq+0xe1/0x2da
[ 1898.621035]  asm_call_irq_on_stack+0x12/0x20
[ 1898.621035]  </IRQ>
[ 1898.621038]  do_softirq_own_stack+0x3d/0x50
[ 1898.621041]  irq_exit_rcu+0xa4/0xb0
[ 1898.621042]  common_interrupt+0x7d/0x150
[ 1898.621043]  asm_common_interrupt+0x1e/0x40
[ 1898.621045] RIP: 0010:native_safe_halt+0xe/0x10
[ 1898.621046] Code: 7b ff ff ff eb bd cc cc cc cc cc cc e9 07 00 00 00 0f 00 2d 36 40 48 00 f4 c3 66 90 e9 07 00 00 00 0f 00 2d 26 40 48 00 fb f4 <c3> cc 0f 1f 44 00 00 55 48 89 e5 41 54 53 65 44 8b 25 2c 8d 08 4d
[ 1898.621047] RSP: 0018:ffffa391c00b7e08 EFLAGS: 00000246
[ 1898.621048] RAX: 0000000000004000 RBX: ffff8bba46caac00 RCX: ffff8bbab5ec0000
[ 1898.621049] RDX: 0000000000000001 RSI: ffffffffb3b84f40 RDI: ffff8bbab34d3464
[ 1898.621049] RBP: ffffa391c00b7e10 R08: ffff8bbab34d3400 R09: 0000000000000000
[ 1898.621049] R10: 000000000000000f R11: ffff8bbab5eeb324 R12: 0000000000000001
[ 1898.621050] R13: ffff8bbab34d3464 R14: ffffffffb3b84fc0 R15: 0000000000000001
[ 1898.621052]  ? acpi_idle_do_entry+0x4d/0x60
[ 1898.621054]  acpi_idle_enter+0x5b/0xd0
[ 1898.621056]  cpuidle_enter_state+0x8e/0x3a0
[ 1898.621057]  cpuidle_enter+0x2e/0x40
[ 1898.621059]  call_cpuidle+0x23/0x40
[ 1898.621060]  do_idle+0x1df/0x260
[ 1898.621061]  cpu_startup_entry+0x20/0x30
[ 1898.621063]  start_secondary+0x111/0x150
[ 1898.621064]  secondary_startup_64+0xb6/0xc0
[ 1898.621104] CPU: 3 PID: 0 Comm: swapper/3 Not tainted 5.9.0+ #2
[ 1898.621107] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 02/27/2020
[ 1898.621108] Call Trace:
[ 1898.621108]  <IRQ>
[ 1898.621110]  dump_stack+0x74/0x9a
[ 1898.621111]  ip6_input_finish.cold+0xd/0x21
[ 1898.621112]  ip6_input+0xa2/0xb0
[ 1898.621114]  ip6_sublist_rcv_finish+0x3d/0x50
[ 1898.621115]  ip6_sublist_rcv+0x1f9/0x280
[ 1898.621116]  ipv6_list_rcv+0x11b/0x140
[ 1898.621118]  __netif_receive_skb_list_core+0x1a4/0x250
[ 1898.621119]  netif_receive_skb_list_internal+0x1a1/0x2b0
[ 1898.621122]  ? e1000_clean_rx_irq+0x366/0x510 [e1000]
[ 1898.621123]  gro_normal_list.part.0+0x1e/0x40
[ 1898.621124]  napi_complete_done+0x71/0x180
[ 1898.621126]  e1000_clean+0x28a/0x5e0 [e1000]
[ 1898.621127]  net_rx_action+0x142/0x390
[ 1898.621128]  __do_softirq+0xe1/0x2da
[ 1898.621129]  asm_call_irq_on_stack+0x12/0x20
[ 1898.621130]  </IRQ>
[ 1898.621130]  do_softirq_own_stack+0x3d/0x50
[ 1898.621132]  irq_exit_rcu+0xa4/0xb0
[ 1898.621132]  common_interrupt+0x7d/0x150
[ 1898.621133]  asm_common_interrupt+0x1e/0x40
[ 1898.621134] RIP: 0010:native_safe_halt+0xe/0x10
[ 1898.621135] Code: 7b ff ff ff eb bd cc cc cc cc cc cc e9 07 00 00 00 0f 00 2d 36 40 48 00 f4 c3 66 90 e9 07 00 00 00 0f 00 2d 26 40 48 00 fb f4 <c3> cc 0f 1f 44 00 00 55 48 89 e5 41 54 53 65 44 8b 25 2c 8d 08 4d
[ 1898.621135] RSP: 0018:ffffa391c00b7e08 EFLAGS: 00000246
[ 1898.621136] RAX: 0000000000004000 RBX: ffff8bba46caac00 RCX: ffff8bbab5ec0000
[ 1898.621137] RDX: 0000000000000001 RSI: ffffffffb3b84f40 RDI: ffff8bbab34d3464
[ 1898.621137] RBP: ffffa391c00b7e10 R08: ffff8bbab34d3400 R09: 0000000000000000
[ 1898.621137] R10: 000000000000000f R11: ffff8bbab5eeb324 R12: 0000000000000001
[ 1898.621152] R13: ffff8bbab34d3464 R14: ffffffffb3b84fc0 R15: 0000000000000001
[ 1898.621155]  ? acpi_idle_do_entry+0x4d/0x60
[ 1898.621156]  acpi_idle_enter+0x5b/0xd0
[ 1898.621157]  cpuidle_enter_state+0x8e/0x3a0
[ 1898.621158]  cpuidle_enter+0x2e/0x40
[ 1898.621159]  call_cpuidle+0x23/0x40
[ 1898.621160]  do_idle+0x1df/0x260
[ 1898.621161]  cpu_startup_entry+0x20/0x30
[ 1898.621163]  start_secondary+0x111/0x150
[ 1898.621163]  secondary_startup_64+0xb6/0xc0
[ 1898.621249] CPU: 3 PID: 1949 Comm: inet6_tcp_serv Not tainted 5.9.0+ #2
[ 1898.621249] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 02/27/2020
[ 1898.621251] Call Trace:
[ 1898.621253]  dump_stack+0x74/0x9a
[ 1898.621255]  ip6_finish_output2.cold+0x5/0x18
[ 1898.621257]  __ip6_finish_output+0xe7/0x110
[ 1898.621258]  ip6_finish_output+0x2d/0xb0
[ 1898.621259]  ip6_output+0x77/0x120
[ 1898.621260]  ? __ip6_finish_output+0x110/0x110
[ 1898.621262]  ip6_xmit+0x2b5/0x5d0
[ 1898.621264]  ? page_counter_cancel+0x23/0x30
[ 1898.621265]  ? page_counter_uncharge+0x22/0x40
[ 1898.621266]  ? __sk_dst_check+0x35/0x70
[ 1898.621268]  ? inet6_csk_route_socket+0x11d/0x1e0
[ 1898.621269]  ? sched_clock+0x9/0x10
[ 1898.621270]  inet6_csk_xmit+0xa7/0xf0
[ 1898.621272]  __tcp_transmit_skb+0x586/0xc40
[ 1898.621273]  tcp_write_xmit+0x3c2/0x11e0
[ 1898.621275]  ? __alloc_skb+0x98/0x1d0
[ 1898.621277]  __tcp_push_pending_frames+0x37/0x100
[ 1898.621277]  tcp_push+0xfc/0x100
[ 1898.621278]  tcp_sendmsg_locked+0xcdc/0xe10
[ 1898.621279]  ? try_to_wake_up+0x4f0/0x530
[ 1898.621281]  tcp_sendmsg+0x2d/0x50
[ 1898.621282]  inet6_sendmsg+0x43/0x70
[ 1898.621283]  sock_sendmsg+0x48/0x70
[ 1898.621284]  __sys_sendto+0x113/0x190
[ 1898.621286]  ? vfs_write+0x15a/0x200
[ 1898.621287]  ? vfs_write+0x15a/0x200
[ 1898.621288]  ? exit_to_user_mode_prepare+0x3d/0x1b0
[ 1898.621289]  __x64_sys_sendto+0x29/0x30
[ 1898.621290]  do_syscall_64+0x38/0x90
[ 1898.621291]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[ 1898.621292] RIP: 0033:0x7f15c1c7e690
[ 1898.621293] Code: ff eb bc 0f 1f 80 00 00 00 00 f3 0f 1e fa 41 89 ca 64 8b 04 25 18 00 00 00 85 c0 75 1d 45 31 c9 45 31 c0 b8 2c 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 68 c3 0f 1f 80 00 00 00 00 55 48 83 ec 20 48
[ 1898.621295] RSP: 002b:00007ffd3a22c288 EFLAGS: 00000246 ORIG_RAX: 000000000000002c
[ 1898.621296] RAX: ffffffffffffffda RBX: 000055f040732a10 RCX: 00007f15c1c7e690
[ 1898.621296] RDX: 0000000000000007 RSI: 00007ffd3a22c370 RDI: 0000000000000004
[ 1898.621296] RBP: 00007ffd3a22c3c0 R08: 0000000000000000 R09: 0000000000000000
[ 1898.621297] R10: 0000000000000000 R11: 0000000000000246 R12: 000055f040732320
[ 1898.621297] R13: 00007ffd3a22c5b0 R14: 0000000000000000 R15: 0000000000000000
[ 1898.621508] CPU: 3 PID: 779 Comm: rs:main Q:Reg Not tainted 5.9.0+ #2
[ 1898.621509] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 02/27/2020
[ 1898.621509] Call Trace:
[ 1898.621510]  <IRQ>
[ 1898.621512]  dump_stack+0x74/0x9a
[ 1898.621514]  ip6_rcv_core.isra.0.cold+0x5/0xa
[ 1898.621515]  ipv6_list_rcv+0xb1/0x140
[ 1898.621516]  __netif_receive_skb_list_core+0x1a4/0x250
[ 1898.621518]  netif_receive_skb_list_internal+0x1a1/0x2b0
[ 1898.621520]  ? e1000_clean_rx_irq+0x366/0x510 [e1000]
[ 1898.621522]  gro_normal_list.part.0+0x1e/0x40
[ 1898.621522]  napi_complete_done+0x71/0x180
[ 1898.621525]  e1000_clean+0x28a/0x5e0 [e1000]
[ 1898.621527]  ? check_preempt_wakeup+0xfd/0x210
[ 1898.621528]  net_rx_action+0x142/0x390
[ 1898.621530]  __do_softirq+0xe1/0x2da
[ 1898.621531]  asm_call_irq_on_stack+0x12/0x20
[ 1898.621531]  </IRQ>
[ 1898.621532]  do_softirq_own_stack+0x3d/0x50
[ 1898.621533]  irq_exit_rcu+0xa4/0xb0
[ 1898.621534]  common_interrupt+0x7d/0x150
[ 1898.621536]  ? asm_common_interrupt+0x8/0x40
[ 1898.621536]  asm_common_interrupt+0x1e/0x40
[ 1898.621537] RIP: 0033:0x7f390d5fe37c
[ 1898.621538] Code: 24 68 e8 97 38 00 00 e8 82 3c 00 00 89 de 45 31 d2 31 d2 41 89 c0 40 80 f6 80 4c 89 ff b8 ca 00 00 00 0f 05 48 3d 00 f0 ff ff <0f> 87 26 01 00 00 44 89 c7 e8 b6 3c 00 00 31 f6 4c 89 f7 e8 8c 38
[ 1898.621538] RSP: 002b:00007f3907ffeab0 EFLAGS: 00000207
[ 1898.621539] RAX: 0000000000000000 RBX: 0000000000000000 RCX: 00007f390d5fe376
[ 1898.621539] RDX: 0000000000000000 RSI: 0000000000000080 RDI: 00005556b6c2f994
[ 1898.621541] RBP: 00005556b6c2f968 R08: 0000000000000001 R09: 0000000000000004
[ 1898.621541] R10: 0000000000000000 R11: 0000000000000282 R12: 00005556b6c2f98c
[ 1898.621541] R13: 00005556b6c2fe50 R14: 00007f3907ffeaf0 R15: 00005556b6c2f994
[ 1898.621544] CPU: 3 PID: 779 Comm: rs:main Q:Reg Not tainted 5.9.0+ #2
[ 1898.621544] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 02/27/2020
[ 1898.621544] Call Trace:
[ 1898.621544]  <IRQ>
[ 1898.621546]  dump_stack+0x74/0x9a
[ 1898.621547]  ip6_input_finish.cold+0xd/0x21
[ 1898.621548]  ip6_input+0xa2/0xb0
[ 1898.621549]  ip6_sublist_rcv_finish+0x3d/0x50
[ 1898.621551]  ip6_sublist_rcv+0x1f9/0x280
[ 1898.621553]  ipv6_list_rcv+0x11b/0x140
[ 1898.621554]  __netif_receive_skb_list_core+0x1a4/0x250
[ 1898.621555]  netif_receive_skb_list_internal+0x1a1/0x2b0
[ 1898.621558]  ? e1000_clean_rx_irq+0x366/0x510 [e1000]
[ 1898.621559]  gro_normal_list.part.0+0x1e/0x40
[ 1898.621560]  napi_complete_done+0x71/0x180
[ 1898.621562]  e1000_clean+0x28a/0x5e0 [e1000]
[ 1898.621564]  ? check_preempt_wakeup+0xfd/0x210
[ 1898.621565]  net_rx_action+0x142/0x390
[ 1898.621567]  __do_softirq+0xe1/0x2da
[ 1898.621568]  asm_call_irq_on_stack+0x12/0x20
[ 1898.621568]  </IRQ>
[ 1898.621569]  do_softirq_own_stack+0x3d/0x50
[ 1898.621570]  irq_exit_rcu+0xa4/0xb0
[ 1898.621571]  common_interrupt+0x7d/0x150
[ 1898.621572]  ? asm_common_interrupt+0x8/0x40
[ 1898.621573]  asm_common_interrupt+0x1e/0x40
[ 1898.621573] RIP: 0033:0x7f390d5fe37c
[ 1898.621574] Code: 24 68 e8 97 38 00 00 e8 82 3c 00 00 89 de 45 31 d2 31 d2 41 89 c0 40 80 f6 80 4c 89 ff b8 ca 00 00 00 0f 05 48 3d 00 f0 ff ff <0f> 87 26 01 00 00 44 89 c7 e8 b6 3c 00 00 31 f6 4c 89 f7 e8 8c 38
[ 1898.621574] RSP: 002b:00007f3907ffeab0 EFLAGS: 00000207
[ 1898.621575] RAX: 0000000000000000 RBX: 0000000000000000 RCX: 00007f390d5fe376
[ 1898.621576] RDX: 0000000000000000 RSI: 0000000000000080 RDI: 00005556b6c2f994
[ 1898.621576] RBP: 00005556b6c2f968 R08: 0000000000000001 R09: 0000000000000004
[ 1898.621577] R10: 0000000000000000 R11: 0000000000000282 R12: 00005556b6c2f98c
[ 1898.621577] R13: 00005556b6c2fe50 R14: 00007f3907ffeaf0 R15: 00005556b6c2f994

dump_stack()의 결과로 Rx/Tx 경로의 함수 콜 스택을 볼 수 있다.