1.1. 개요
[PNG 그림 (982 Bytes)]
Netlink 는 Kernel과 User space간 원활한 정보교류를 위한 비교적 유연한 통신 방식입니다.
본 내용을 이해하기 위해서 다음의 Linux kernel header 를 먼저 열어보시고 함께 보시면 좋습니다.예제소스
본 내용을 이해하기 위해서 다음의 Linux kernel header 를 먼저 열어보시고 함께 보시면 좋습니다.
- "<linux/netlink.h>"
- More
- "<linux/genetlink.h>"
- More
- "<linux/rtnetlink.h>"
- More
- "<linux/if_addr.h>"
- "<linux/if_link.h>"
- "<linux/netconf.h>"
- "<linux/neighbour.h>"
- "<linux/xfrm.h>" : (Transform) IPSec VPN에 관련한 Netlink 정의
- IPSsec 관련 모니터링 예제소스 : netlink_ipsec_monitor-source.tar.gz (8.69 KB)
- Hotplug(udev) 장치 Event 예제소스 : mzudev-source.tar.gz (4.94 KB)
- Network interface 및 routing 정보 질의 예제소스 : netlink_rtnetlink_monitor-source-20180628.tar.gz (11.5 KB)
- 초단간 Process 별 I/O(Read/Write) 누적량 확인 예제소스 : netlink_iotop-source-~20190319.tar.gz (6.12 KB)
- => 보다 정확한 Netlink message 구현은 iproute2 소스 및 iotop 소스를 참고하실 것을 추천합니다.
1.2. netlink message format
기본적으로 Netlink message는 하나 이상을 연속으로 붙여 하나의 요청(Request) 또는 응답(Response) 단위로 묶어 전송구현합니다.
하나의 Netlink message의 Header와 Payload는 각각 정렬(Align)된 형태를 맞추기 위해서 Padding 을 포함할 수 있습니다. (정렬의 크기는 "<linux/netlink.h>" kernel header에 정의된 NLMSG_ALIGNTO 정의를 기준으로 하며 NLMSG_ALIGN macro 함수를 사용하여 계산할 수 있습니다.)
+----------------------+----------------------+----------------------+----------------------+ ~ ~ ~ ~ ~ +----------------------+ | Netlink messaeg #1 | Netlink messaeg #2 | Netlink messaeg #3 | Netlink messaeg #4 | ... | Netlink messaeg #n | | (Header+Payload+Pad) | (Header+Payload+Pad) | (Header+Payload+Pad) | (Header+Payload+Pad) | | (Header+Payload+Pad) | +----------------------+----------------------+----------------------+----------------------+ ~ ~ ~ ~ ~ +----------------------+ <--------------------------------------------------- Request OR Response packet ----------------------------------------------->
- Netlink message format
Netlink message format 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Description 32bit Length (Header를 포함한 message 크기) ↑↓16 bytes NLMSG_HDRLEN
(struct nlmsghdr)↑↓Netlink message
(nlmsghdr.nlmsg_len)16-bit Type (Message content) 16-bit Flags (Additional flags) 32bit Sequence Number 32bit Process ID (PID, Sending process port ID) Payload (Variable data)
data_ptr = NLMSG_DATA(nlmsghdr)↑↓(nlmsghdr.nlmsg_len - NLMSG_HDRLEN)
- Netlink message overview
<----- NLMSG_HDRLEN ------> <-------- Payload-Len --------> +---------------------+- - -+- - - - - - - - - - - - - - - -+ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ | Header | Pad | Payload | ... (Next netlink message) ... | (struct nlmsghdr) | ing | Specific data + [attribute..] | +---------------------+- - -+- - - - - - - - - - - - - - - -+ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ↑ nlmsghdr ↑ NLMSG_DATA(&nlmsghdr) ↑ NLMSG_NEXT(&nlmsghdr) <------------------ nlmsghdr->nlmsg_len ------------------> <------------------ NLMSG_LENGTH(Payload-Len) ------------> Payload의 선두부분은 nlmsg_type에 따른 고유 구조체(Specific data) 형식이 올 수 있으며 NLMSG_ALIGN(sizeof(Specific data)) 정렬 후 그 다음에 attribute로 구성되는게 일반적입니다.
- Netlink attribute overview (Netlink message payload 내에서 Specific data 뒷부분에 선택적으로 추가됨)
<------- NLA_HDRLEN ------> <-- NLA_ALIGN(payload)--> +---------------------+- - -+- - - - - - - - - -+- - -+ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ | Header | Pad | Payload | Pad | ... (Next attribute) ... | (struct nlattr) | ing | | ing | +---------------------+- - -+- - - - - - - - - -+- - -+ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ↑ nlattr <-------------- nlattr->nla_len --------------> nla_type (16 bits) +---+---+-------------------------------+ | N | O | Attribute Type | +---+---+-------------------------------+ N := Carries nested attributes O := Payload stored in network byte order Note: The N and O flag are mutually exclusive.
- netlink header struct
struct nlmsghdr { uint32_t nlmsg_len; /* Header를 포함한 Netlink message 크기 */ uint16_t nlmsg_type; /* Message content */ uint16_t nlmsg_flags; /* Additional flags */ uint32_t nlmsg_seq; /* Sequence number */ uint32_t nlmsg_pid; /* Sending process port ID */ };
- netlink attribute header struct
struct nlattr { uint16_t nla_len; /* Header를 포함한 attribute 크기 */ uint16_t nla_type; /* Attribute type */ };
1.3. Generic Netlink message Overview
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Netlink message header (nlmsghdr) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Generic Netlink message header (genlmsghdr) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Optional user specific message header | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Optional Generic Netlink message payload | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"iotop" 명령어가 task 상태에 대한 모니터링을 위한 구현을 위해서 Generic Netlink를 사용하는 대표적 사용예입니다.
1.4. 구현사항에 대한 간략한 골격구조 설명
- Netlink socket을 XFRM쪽으로 open
s_socket = socket(PF_NETLINK, SOCK_RAW, NETLINK_XFRM /* 6 - ipsec */);
- XFRM모듈쪽의 어떤 Group 과 통신할지를 binding
__u32 s_nl_groups; struct sockaddr_nl s_sockaddr_nl; s_nl_groups |= XFRMNLGRP_ACQUIRE; s_nl_groups |= XFRMNLGRP_EXPIRE; s_nl_groups |= XFRMNLGRP_SA; s_nl_groups |= XFRMNLGRP_POLICY; s_nl_groups |= XFRMNLGRP_AEVENTS; s_nl_groups |= XFRMNLGRP_REPORT; s_nl_groups |= XFRMNLGRP_MIGRATE; s_nl_groups |= XFRMNLGRP_MAPPING; s_sockaddr_nl.nl_family = AF_NETLINK; s_sockaddr_nl.nl_pad = (unsigned short)0u; s_sockaddr_nl.nl_pid = (pid_t)0; s_sockaddr_nl.nl_groups = s_nl_groups; /* Multicast groups mask */ bind(s_socket, (const struct sockaddr *)(&s_sockaddr_nl), (socklen_t)sizeof(s_sockaddr_nl));
- netlink socket으로부터 Netlink protocol RAW 수신
socklen_t s_socklen; s_socklen = (socklen_t)sizeof(s_sockaddr_nl); s_recv_bytes = recvfrom( s_socket, s_buffer, s_buffer_size, MSG_NOSIGNAL, (struct sockaddr *)(&s_sockaddr_nl), (socklen_t *)(&s_socklen) );
- 수신된 Netlink protocol RAW data에서 Netlink header 를 통해서 각 요소별 분리
size_t s_msg_size; struct nlmsghdr *s_nlmsghdr; size_t s_payload_size; void *s_payload; s_msg_size = (size_t)s_recv_bytes; for(s_nlmsghdr = (struct nlmsghdr *)s_buffer;(s_is_break == 0) && NLMSG_OK(s_nlmsghdr, s_msg_size);s_nlmsghdr = NLMSG_NEXT(s_nlmsghdr, s_msg_size)) { /* Netlink 수신패킷 하나에 여러개의 Netlink header가 탑재될 수 있는데 이를 각 Header 단위로 분리하는 Loop */ s_payload_size = (size_t)NLMSG_PAYLOAD(s_nlmsghdr, 0); /* Header 내의 실제 Data 크기 */ s_payload = NLMSG_DATA(s_nlmsghdr); /* Header 내의 실제 Data 위치 포인터 */ switch(s_nlmsghdr->nlmsg_type) { /* 각 메세지의 종류별로 다른 파싱구조를 가지고 있으므로 커널을 참조하여 해당 부분을 파싱해야 합니다. */ ..... } }
1.5. 실제 VPN 장비에서 VPN연결과정에서 본 예제프로그램으로 Netlink 통신을 수신하여 파싱된 내용을 모니터링한 콘솔내용
- 내용을 보시려면 여기를 클릭해주세요.
1.6. Netlink 를 가공하기 위한 함수 예시
- Netlink를 다루기 위한 기본 macro 를 포함하기 위한 include
#include <linux/netlink.h> #include <linux/genetlink.h>
- Netlink message (NLMSG) build 함수
size_t hwport_generate_netlink_message(void *s_buffer, size_t s_buffer_size, unsigned int s_nlmsg_type, unsigned int s_nlmsg_flags, unsigned int s_nlmsg_seq, unsigned int s_nlmsg_pid, const void *s_payload, size_t s_payload_size) { int s_aligned_payload_size; struct nlmsghdr *s_nlmsghdr; void *s_payload_ptr; s_aligned_payload_size = (int)NLMSG_ALIGN((uint32_t)s_payload_size); if (s_buffer_size < ((size_t)NLMSG_LENGTH(s_aligned_payload_size))) { return((size_t)0u); } s_nlmsghdr = (struct nlmsghdr *)memset(s_buffer, 0, sizeof(struct nlmsghdr)); s_nlmsghdr->nlmsg_len = (uint32_t)NLMSG_LENGTH(s_aligned_payload_size); s_nlmsghdr->nlmsg_type = (uint16_t)s_nlmsg_type; s_nlmsghdr->nlmsg_flags = (uint16_t)s_nlmsg_flags; s_nlmsghdr->nlmsg_seq = (uint32_t)s_nlmsg_seq; s_nlmsghdr->nlmsg_pid = (uint32_t)s_nlmsg_pid; s_payload_ptr = (void *)NLMSG_DATA(s_nlmsghdr); if (s_payload_size > ((size_t)0u)) { if (s_payload == ((const void *)(NULL))) { (void)memset(s_payload_ptr, 0, s_payload_size); } else { (void)memcpy(s_payload_ptr, s_payload, s_payload_size); } } return((size_t)s_nlmsghdr->nlmsg_len); }
- Netlink attribute (NLA) build append 함수
size_t hwport_append_netlink_attr(void *s_buffer, size_t s_buffer_size, unsigned int s_nla_type, const void *s_attr1, size_t s_attr1_size, const void *s_attr2, size_t s_attr2_size) { struct nlmsghdr *s_nlmsghdr; struct nlattr *s_nlattr; size_t s_attr_size; void *s_attr_ptr; s_attr_size = s_attr1_size + s_attr2_size; s_nlmsghdr = (struct nlmsghdr *)s_buffer; if (s_buffer_size < ((size_t)(s_nlmsghdr->nlmsg_len + NLA_HDRLEN + NLA_ALIGN(s_attr_size)))) { return((size_t)0u); } s_nlattr = (struct nlattr *)(((uint8_t *)s_buffer) + s_nlmsghdr->nlmsg_len); s_nlattr->nla_len = (uint16_t)NLA_HDRLEN + s_attr_size; s_nlattr->nla_type = (uint16_t)s_nla_type; s_attr_ptr = (void *)(((uint8_t *)s_nlattr) + NLA_HDRLEN); if(s_attr1_size > ((size_t)0u)) { if(s_attr1 == ((const void *)0)) { (void)memset((void *)s_attr_ptr, 0, s_attr1_size); } else if(((const void *)s_attr_ptr) != s_attr1) { (void)memcpy((void *)s_attr_ptr, s_attr1, s_attr1_size); } s_attr_ptr = (void *)(((uint8_t *)s_attr_ptr) + s_attr1_size); } if(s_attr2_size > ((size_t)0u)) { if(s_attr2 == ((const void *)0)) { (void)memset((void *)s_attr_ptr, 0, s_attr2_size); } else if(((const void *)s_attr_ptr) != s_attr2) { (void)memcpy((void *)s_attr_ptr, s_attr2, s_attr2_size); } } s_nlmsghdr->nlmsg_len += (uint32_t)NLA_ALIGN(s_nlattr->nla_len); return((size_t)s_nlmsghdr->nlmsg_len); }
- Netlink message request 함수
ssize_t hwport_request_generic_netlink(int s_socket, const void *s_data, size_t s_size, const struct sockaddr *s_sockaddr, socklen_t s_socklen) { size_t s_sent_size; size_t s_want_size; ssize_t s_send_bytes; const uint8_t *s_uint8_ptr; struct sockaddr_nl s_sockaddr_nl; if (s_socket == (-1)) { errno = EINVAL; return((ssize_t)(-1)); } if (s_sockaddr == ((struct sockaddr *)(NULL))) { s_socklen = (socklen_t)sizeof(s_sockaddr_nl); s_sockaddr = (const struct sockaddr *)memset((void *)(&s_sockaddr_nl), 0, sizeof(s_sockaddr_nl)); s_sockaddr_nl.nl_family = AF_NETLINK; s_sockaddr_nl.nl_pid = 0; /* port ID */ s_sockaddr_nl.nl_groups = 0; /* multicast groups mask */ } s_sent_size = (size_t)0u; s_uint8_ptr = (const uint8_t *)s_data; while (s_sent_size < s_size) { s_want_size = s_size - s_sent_size; s_send_bytes = sendto( s_socket, (const void *)(&s_uint8_ptr[s_sent_size]), s_want_size, MSG_NOSIGNAL, s_sockaddr, s_socklen ); if (s_send_bytes > ((ssize_t)0)) { s_sent_size += (size_t)s_send_bytes; } else if((s_send_bytes == ((ssize_t)(-1))) && ((errno == EINTR) || (errno == EAGAIN))) { continue; } else { /* error */ return(s_send_bytes); } } return((ssize_t)s_sent_size); }
- Netlink message response 함수
ssize_t hwport_response_generic_netlink(int s_socket, void *s_data, size_t s_size, struct sockaddr *s_sockaddr, socklen_t *s_socklen_ptr) { struct sockaddr_storage s_sockaddr_storage; socklen_t s_socklen; if (s_socket == (-1)) { errno = EINVAL; return((ssize_t)(-1)); } if (s_sockaddr == ((struct sockaddr *)(NULL))) { s_sockaddr = (struct sockaddr *)memset((void *)(&s_sockaddr_storage), 0, sizeof(s_sockaddr_storage)); } if (s_socklen_ptr == ((socklen_t *)(NULL))) { s_socklen = (socklen_t)sizeof(struct sockaddr_nl); s_socklen_ptr = (socklen_t *)(&s_socklen); } return(recvfrom(s_socket, s_data, s_size, MSG_NOSIGNAL, s_sockaddr, s_socklen_ptr)); }
- Generic Netlink message 중에서 Family Name으로 요청하여 Family ID를 얻는 함수
int hwport_get_family_id_by_name(int s_socket, const char *s_family_name) { int s_family_id; size_t s_family_name_size; uint8_t s_buffer[ 4 << 10 ]; size_t s_message_size; size_t s_offset; int s_is_break; struct nlmsghdr *s_nlmsghdr; struct genlmsghdr *s_genlmsghdr; struct nlattr *s_nlattr; ssize_t s_send_bytes; ssize_t s_recv_bytes; if (s_socket == (-1)) { errno = EINVAL; return(-1); } if (s_family_name == ((const char *)(NULL))) { errno = EINVAL; return(-1); } s_family_name_size = strlen(s_family_name); s_nlmsghdr = (struct nlmsghdr *)(&s_buffer[0]); s_message_size = hwport_generate_netlink_message( (void *)s_nlmsghdr, sizeof(s_buffer), (unsigned int)GENL_ID_CTRL, /* nlmsg_type */ (unsigned int)NLM_F_REQUEST /* | NLM_F_ACK */, /* nlmsg_flags */ 0u, /* nlmsg_seq */ (unsigned int)getpid(), /* nlmsg_pid */ (const void *)(NULL), /* payload */ sizeof(struct genlmsghdr) ); if (s_message_size <= ((size_t)0u)) { errno = ENOMEM; return(-1); } s_genlmsghdr = (struct genlmsghdr *)memset((void *)NLMSG_DATA(s_nlmsghdr), 0, sizeof(struct genlmsghdr)); s_genlmsghdr->cmd = CTRL_CMD_GETFAMILY; /* SeeAlso : CTRL_CMD_XXXX in "include/uapi/linux/genetlink.h" */ s_genlmsghdr->version = 1 /* 해당 message 종류에 따른 VERSION을 넣어야 함 */; s_message_size = hwport_append_netlink_attr( (void *)s_nlmsghdr, sizeof(s_buffer), (unsigned int)CTRL_ATTR_FAMILY_NAME, /* nla_type */ (const void *)s_family_name, /* attr1 */ s_family_name_size + ((size_t)1u), /* attr1_size (family name 은 문자열 뒤의 nul terminate까지 포함한 길이여야 함) */ (const void *)(NULL), /* attr2 */ (size_t)0u /* attr2_size */ ); if (s_message_size <= ((size_t)0u)) { errno = ENOMEM; return(-1); } s_send_bytes = hwport_request_generic_netlink( s_socket, (const void *)s_nlmsghdr, s_message_size, (const struct sockaddr *)(NULL), (socklen_t)0 ); if (s_send_bytes <= ((ssize_t)0)) { return(-1); } s_recv_bytes = hwport_response_generic_netlink( s_socket, (void *)(&s_buffer[0]), sizeof(s_buffer), (struct sockaddr *)(NULL), (socklen_t *)(NULL) ); if (s_recv_bytes <= ((ssize_t)0)) { return(-1); } s_family_id = (-1); s_is_break = 0; s_message_size = (size_t)s_recv_bytes; for (s_nlmsghdr = (struct nlmsghdr *)(&s_buffer[0]);(s_is_break == 0) && NLMSG_OK(s_nlmsghdr, s_message_size);s_nlmsghdr = NLMSG_NEXT(s_nlmsghdr, s_message_size)) { /* payload_size = (size_t)NLMSG_PAYLOAD(s_nlmsghdr, 0); */ /* payload = NLMSG_DATA(s_nlmsghdr); */ switch(s_nlmsghdr->nlmsg_type) { case NLMSG_NOOP: break; case NLMSG_ERROR: s_is_break = 1; break; case NLMSG_DONE: s_is_break = 1; break; case NLMSG_OVERRUN: s_is_break = 1; break; case GENL_ID_CTRL: /* NLMSG_MIN_TYPE */ s_genlmsghdr = (struct genlmsghdr *)NLMSG_DATA(s_nlmsghdr); for (s_offset = (size_t)(NLMSG_HDRLEN + NLMSG_ALIGN(sizeof(struct genlmsghdr)));s_offset < s_nlmsghdr->nlmsg_len;) { if ((s_offset + ((size_t)NLA_HDRLEN)) > ((size_t)s_nlmsghdr->nlmsg_len)) { /* attibute header 만큼이 남지 않았음. */ break; } s_nlattr = (struct nlattr *)(((uint8_t *)s_nlmsghdr) + s_offset); if (s_nlattr->nla_len < ((size_t)NLA_HDRLEN)) { /* attribute length 가 최소 크기를 만족하지 못함 */ break; } if ((s_offset + ((size_t)NLA_ALIGN(s_nlattr->nla_len))) > ((size_t)s_nlmsghdr->nlmsg_len)) { /* attribute 공간이 nlmsg를 넘어섬 */ break; } switch(s_nlattr->nla_type) { /* SeeAlso : CTRL_ATTR_XXXX in "include/uapi/linux/genetlink.h" */ case CTRL_ATTR_UNSPEC: /* 0 */ break; case CTRL_ATTR_FAMILY_ID: /* 1 */ s_family_id = (int)(*((uint16_t *)(((uint8_t *)s_nlattr) + NLA_HDRLEN))); #if 1L if (s_family_id != (-1)) { /* family id 를 인지했으므로 더이상의 attr은 볼 필요가 없음 */ s_is_break = 1; break; } #endif break; case CTRL_ATTR_FAMILY_NAME: /* 2 */ break; case CTRL_ATTR_VERSION: /* 3 */ break; case CTRL_ATTR_HDRSIZE: /* 4 */ break; case CTRL_ATTR_MAXATTR: /* 5 */ break; case CTRL_ATTR_OPS: /* 6 */ break; case CTRL_ATTR_MCAST_GROUPS: /* 7 */ break; default: /* 8 >= */ break; } s_offset += (size_t)NLA_ALIGN(s_nlattr->nla_len); } break; default: break; } } return(s_family_id); }
- Generic Netlink socket open 함수
int hwport_open_generic_netlink(uint32_t s_port_id, uint32_t s_groups_mask) { int s_socket; struct sockaddr_nl s_sockaddr_nl; s_socket = socket(PF_NETLINK, SOCK_RAW, NETLINK_GENERIC); if (s_socket == (-1)) { return(-1); } (void)memset((void *)(&s_sockaddr_nl), 0, sizeof(s_sockaddr_nl)); s_sockaddr_nl.nl_family = AF_NETLINK; #if 0L s_sockaddr_nl.nl_pad = 0; #endif s_sockaddr_nl.nl_pid = s_port_id; /* port ID */ s_sockaddr_nl.nl_groups = s_groups_mask; /* multicast groups mask */ if (bind(s_socket, (struct sockaddr *)(&s_sockaddr_nl), (socklen_t)sizeof(s_sockaddr_nl)) == (-1)) { int s_check; int s_save_errno; s_save_errno = errno; do { s_check = close(s_socket); }while((s_check == (-1)) && (errno == EINTR)); errno = s_save_errno; return(-1); } return(s_socket); }
- Generic Netlink socket close 함수
int hwport_close_generic_netlink(int s_socket) { int s_check; if (s_socket == (-1)) { errno = EINVAL; return(-1); } do { s_check = close(s_socket); }while((s_check == (-1)) && (errno == EINTR)); return(s_check); }
1.7. 참고자료
- https://www.rfc-editor.org/rfc/rfc3549
- https://github.com/shemminger/iproute2 <= Netlink 구현은 이 소스가 이해하는데 많은 도움이 됩니다. (추천)
- http://www.linuxfoundation.org/collaborate/workgroups/networking/generic_netlink_howto
- https://people.redhat.com/nhorman/papers/netlink.pdf
- http://inai.de/documents/Netlink_Protocol.pdf
- https://nscpolteksby.ac.id/ebook/files/Ebook/Computer%20Engineering/Linux%20Kernel%20Networking%20-%20Implementation%20(2014)/chapter%202%20Netlink%20Sockets.pdf
- https://people.netfilter.org/pablo/netlink/netlink-libmnl-manual.pdf
- http://haifux.org/lectures/219/netLec6.pdf
- https://medium.com/@mdlayher/linux-netlink-and-go-part-1-netlink-4781aaeeaca8
- https://stackoverrun.com/ko/q/7837552
- https://www.infradead.org/~tgr/libnl/doc/core.html
- https://www.ctolib.com/topics-134609.html
- http://linux-development-for-fresher.blogspot.com/2012/05/understanding-netlink-socket.html
- https://gist.github.com/arunk-s/c897bb9d75a6c98733d6