Netlink
Linux 커널의 Netlink 소켓(Socket)은 커널과 유저스페이스 간 양방향 IPC 메커니즘으로, 네트워크 구성(iproute2), 감사(audit), 디바이스 이벤트(udev) 등 현대 Linux 시스템의 핵심 통신 채널입니다. AF_NETLINK 소켓 프로토콜 패밀리, 메시지 구조(nlmsghdr/nlattr), rtnetlink, Generic Netlink, 커널/유저스페이스 API, multicast 이벤트, 에러 처리, 보안, 디버깅(Debugging)까지 종합적으로 분석합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- Netlink 소켓(AF_NETLINK) — 커널과 유저스페이스 간 양방향 통신을 제공하는 소켓 기반 IPC 메커니즘으로,
ioctl()의 한계를 극복합니다. - Generic Netlink(genetlink) — 새로운 Netlink 패밀리를 쉽게 등록할 수 있는 범용 프레임워크로, 프로토콜 번호 고갈 문제를 해결합니다.
- 메시지 구조(nlmsghdr + nlattr) — 고정 헤더(
nlmsghdr)와 가변 속성(nlattrTLV)으로 구성된 확장 가능한 메시지 포맷입니다. - Netlink 패밀리 —
NETLINK_ROUTE(라우팅),NETLINK_GENERIC(범용),NETLINK_KOBJECT_UEVENT(디바이스 이벤트) 등 용도별 프로토콜입니다. - 멀티캐스트 그룹(Multicast Group) — 커널 이벤트(링크 상태 변경, 주소 추가 등)를 구독한 유저스페이스 프로세스에 비동기로 통지합니다.
- rtnetlink — 라우팅 테이블, 네트워크 인터페이스, IP 주소 등 네트워크 설정을 관리하는 핵심 Netlink 패밀리입니다.
- nla_policy 속성 검증 — 커널이 유저스페이스 메시지의 속성 타입과 길이를 자동으로 검증하여 보안과 안정성을 보장합니다.
단계별 이해
- Netlink 소켓과 메시지 구조 이해 —
socket(AF_NETLINK, SOCK_RAW, protocol)으로 소켓을 생성하고,nlmsghdr헤더와nlattr속성의 TLV 포맷을 이해합니다.iproute2의ip명령이 내부적으로 rtnetlink 메시지를 어떻게 구성하는지strace로 관찰합니다. - rtnetlink으로 네트워크 설정 조작 —
NETLINK_ROUTE소켓으로 인터페이스 목록 조회(RTM_GETLINK), IP 주소 추가(RTM_NEWADDR), 라우팅 변경(RTM_NEWROUTE)을 직접 실습합니다.요청-응답 패턴과
NLM_F_DUMP덤프 메커니즘의 차이를 비교하고, 대량 데이터 전송 시 멀티파트 응답 처리를 구현합니다. - Generic Netlink 패밀리 등록 — 커널 모듈에서
genl_register_family()로 새 Generic Netlink 패밀리를 등록하고, 유저스페이스에서libnl로 통신하는 전체 경로를 구현합니다.명령(Command) 정의, 속성 정책(
nla_policy), 콜백 함수 등록 과정을 단계별로 작성합니다. - 멀티캐스트 이벤트 구독과 처리 —
bind()로 멀티캐스트 그룹에 가입하여 커널의 비동기 이벤트(링크 업/다운, 주소 변경)를 수신하는 모니터를 구현합니다.ip monitor명령의 동작 원리를 이해하고, 직접 C 코드로 동일한 이벤트 수신기를 작성합니다. - 보안·성능 튜닝과 디버깅 — Netlink 소켓의
NETLINK_CAP_ACK/NETLINK_EXT_ACK옵션으로 에러 처리를 개선하고, 소켓 버퍼 크기 튜닝으로 메시지 유실을 방지합니다.nlmon인터페이스와tcpdump로 Netlink 트래픽을 캡처하여 메시지 흐름을 디버깅합니다.
Netlink 개요
Netlink은 Linux 커널이 제공하는 소켓 기반 IPC 메커니즘으로, 커널 서브시스템과 유저스페이스 프로세스(Process) 간의 양방향 통신을 담당합니다. 전통적인 ioctl() 인터페이스의 한계를 극복하기 위해 설계되었습니다.
ioctl()은 동기식이며 구조체(Struct) 크기가 고정되어 확장이 어렵습니다. Netlink은 비동기 전달, 가변 길이 메시지, multicast 지원, 명확한 에러 보고(extack) 등을 제공하여 현대 커널 인터페이스의 표준으로 자리잡았습니다.
- 헤더 파일:
<linux/netlink.h>,<linux/rtnetlink.h>,<linux/genetlink.h> - 주요 소스:
net/netlink/af_netlink.c,net/core/rtnetlink.c,net/netlink/genetlink.c - 소켓 패밀리:
AF_NETLINK(PF_NETLINK)
Netlink의 핵심 특성
| 특성 | ioctl | Netlink |
|---|---|---|
| 통신 방향 | 유저 → 커널 (요청-응답) | 양방향 + 커널 → 유저 비동기 알림 |
| 메시지 크기 | 고정 구조체 | 가변 길이 (TLV 기반 nlattr) |
| 멀티캐스트 | 미지원 | 그룹 기반 multicast 지원 |
| 확장성 | 새 ioctl 번호 할당 필요 | 새 attribute 추가로 하위 호환 유지 |
| 에러 보고 | errno만 | NLMSG_ERROR + extack (상세 메시지) |
| 덤프(Dump) | 반복 호출 필요 | NLM_F_DUMP로 대량 데이터 스트리밍 |
Netlink 프로토콜 패밀리
Netlink 소켓 생성 시 socket(AF_NETLINK, SOCK_RAW|SOCK_DGRAM, protocol)의 세 번째 인자로 프로토콜 패밀리를 지정합니다. 각 패밀리는 특정 커널 서브시스템과 통신합니다.
| 프로토콜 상수 | 값 | 용도 | 주요 사용자 |
|---|---|---|---|
NETLINK_ROUTE | 0 | 라우팅, 링크, 주소, 이웃 관리 | iproute2, NetworkManager |
NETLINK_USERSOCK | 2 | 유저스페이스 간 netlink 통신 예약 | 사용자 정의 |
NETLINK_FIREWALL | 3 | (폐기됨) 방화벽(Firewall) 패킷 큐 | - |
NETLINK_SOCK_DIAG | 4 | 소켓 진단 (ss 명령) | iproute2 ss |
NETLINK_NFLOG | 5 | Netfilter 로그 | ulogd |
NETLINK_XFRM | 6 | IPsec 정책/SA 관리 | strongSwan, libreswan |
NETLINK_SELINUX | 7 | SELinux 이벤트 알림 | SELinux 데몬 |
NETLINK_ISCSI | 8 | iSCSI 전송 이벤트 | open-iscsi |
NETLINK_AUDIT | 9 | 감사 시스템 | auditd |
NETLINK_FIB_LOOKUP | 10 | FIB 룩업 요청 | ip rule |
NETLINK_CONNECTOR | 11 | 커넥터 (프로세스 이벤트 등) | proc connector |
NETLINK_NETFILTER | 12 | Netfilter 서브시스템 | nftables, conntrack |
NETLINK_KOBJECT_UEVENT | 15 | 디바이스 핫플러그(Hotplug) 이벤트 | udev, systemd |
NETLINK_GENERIC | 16 | 범용 Netlink (다중 패밀리) | nl80211, taskstats 등 |
NETLINK_CRYPTO | 21 | Crypto Framework (Crypto API) 설정 | crconf |
NETLINK_GENERIC(Generic Netlink)이 도입되어, 하나의 프로토콜 번호 위에 동적으로 패밀리를 등록할 수 있게 되었습니다.
메시지 구조
모든 Netlink 메시지는 nlmsghdr 헤더로 시작하며, 그 뒤에 프로토콜별 페이로드(Payload)와 TLV(Type-Length-Value) 형태의 nlattr 속성들이 따릅니다.
nlmsghdr 구조체
/* include/uapi/linux/netlink.h */
struct nlmsghdr {
__u32 nlmsg_len; /* 헤더 포함 전체 메시지 길이 */
__u16 nlmsg_type; /* 메시지 타입 (RTM_*, NLMSG_* 등) */
__u16 nlmsg_flags; /* 플래그 (NLM_F_REQUEST, NLM_F_DUMP 등) */
__u32 nlmsg_seq; /* 시퀀스 번호 (요청-응답 매칭) */
__u32 nlmsg_pid; /* 송신자 포트 ID (0 = 커널) */
};
/* sizeof(struct nlmsghdr) = 16 바이트 */
코드 설명
- nlmsg_len
nlmsghdr자체를 포함한 메시지 전체 길이(바이트)입니다. 수신 측은 이 값으로 메시지 경계를 판별하며,NLMSG_ALIGN()매크로로 4바이트 정렬된 다음 메시지 시작 위치를 계산합니다.include/uapi/linux/netlink.h에 정의되어 있습니다. - nlmsg_type메시지 종류를 식별합니다. 프로토콜별로 고유한 타입 번호를 사용하며, rtnetlink은
RTM_NEWLINK,RTM_GETROUTE등을, Generic Netlink는 동적 할당된 패밀리 ID를 사용합니다. 커널 내부 예약 타입(NLMSG_NOOP,NLMSG_ERROR,NLMSG_DONE)은 0x0~0x0F 범위입니다. - nlmsg_flags요청/응답 특성을 지정합니다. 유저스페이스 → 커널 요청에는 반드시
NLM_F_REQUEST를 설정해야 하며, 전체 테이블 덤프 시NLM_F_DUMP(=NLM_F_ROOT | NLM_F_MATCH)를 함께 설정합니다. - nlmsg_seq시퀀스 번호로 요청과 응답을 매칭합니다. 커널은 응답 메시지에 요청과 동일한 시퀀스 번호를 복사하므로, 유저스페이스가 비동기 요청 여러 개를 구분할 수 있습니다.
- nlmsg_pid송신자의 포트 ID입니다. 커널이 보내는 메시지는 0이고, 유저스페이스는 관례적으로
getpid()를 사용하지만bind()시 커널이 자동 할당한 값과 반드시 같지는 않습니다.net/netlink/af_netlink.c의netlink_autobind()에서 포트 ID를 할당합니다.
주요 nlmsg_flags
| 플래그 | 값 | 설명 |
|---|---|---|
NLM_F_REQUEST | 0x01 | 요청 메시지 (유저 → 커널) |
NLM_F_MULTI | 0x02 | 다중 파트 메시지 (NLMSG_DONE으로 종료) |
NLM_F_ACK | 0x04 | ACK 응답 요청 |
NLM_F_ECHO | 0x08 | 요청 메시지를 에코백 |
NLM_F_DUMP | 0x300 | 전체 테이블 덤프 요청 (ROOT|MATCH) |
NLM_F_ROOT | 0x100 | 루트부터 전체 반환 |
NLM_F_MATCH | 0x200 | 조건에 맞는 항목 반환 |
NLM_F_CREATE | 0x400 | 객체 생성 (없으면 생성) |
NLM_F_EXCL | 0x200 | 이미 존재하면 에러 |
NLM_F_REPLACE | 0x100 | 기존 객체 교체 |
NLM_F_APPEND | 0x800 | 끝에 추가 |
nlattr 속성 (TLV)
/* include/uapi/linux/netlink.h */
struct nlattr {
__u16 nla_len; /* 헤더 + 페이로드 길이 */
__u16 nla_type; /* 속성 타입 (상위 2비트: nested/byteorder 플래그) */
};
/* sizeof(struct nlattr) = 4 바이트 */
/* nla_type 상위 비트 플래그 */
#define NLA_F_NESTED (1 << 15) /* 중첩(nested) 속성 포함 */
#define NLA_F_NET_BYTEORDER (1 << 14) /* 네트워크 바이트 순서 */
코드 설명
- struct nlattrNetlink 속성의 TLV(Type-Length-Value) 헤더입니다. 모든 속성 데이터는 이 4바이트 헤더 뒤에 붙으며,
NLA_ALIGN(nla_len)으로 정렬된 크기가 실제 속성이 차지하는 공간입니다.include/uapi/linux/netlink.h에 정의되어 있습니다. - nla_len
nlattr헤더(4바이트)를 포함한 속성의 전체 길이입니다. 실제 페이로드 크기는nla_len - NLA_HDRLEN으로 계산하며, 커널의nla_data()헬퍼가 페이로드 시작 포인터를 반환합니다. - nla_type하위 14비트가 속성 번호이고, 상위 2비트는 플래그입니다. 비트 15(
NLA_F_NESTED)가 설정되면 페이로드가 하위nlattr들을 포함하는 중첩 구조이고, 비트 14(NLA_F_NET_BYTEORDER)가 설정되면 페이로드가 네트워크 바이트 순서(빅 엔디안)임을 나타냅니다. - NLA_F_NESTED커널 5.2 이후 strict 검증 모드에서는 이 플래그가 중첩 속성에 반드시 설정되어야 합니다.
nla_nest_start()가 자동으로 설정하므로 직접 조작할 필요는 없습니다.include/net/netlink.h의nla_nest_start_noflag()는 레거시 호환용으로 플래그를 생략합니다.
메시지 레이아웃과 정렬
/* 정렬 매크로 — include/uapi/linux/netlink.h */
#define NLMSG_ALIGNTO 4U
#define NLMSG_ALIGN(len) (((len) + NLMSG_ALIGNTO - 1) & ~(NLMSG_ALIGNTO - 1))
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* nlattr 접근 헬퍼 — include/net/netlink.h (커널) */
static inline void *nla_data(const struct nlattr *nla);
static inline int nla_len(const struct nlattr *nla);
static inline u32 nla_get_u32(const struct nlattr *nla);
static inline char *nla_get_string(const struct nlattr *nla);
static inline int nla_put_u32(struct sk_buff *skb, int attrtype, u32 value);
static inline int nla_put_string(struct sk_buff *skb, int attrtype, const char *str);
rtnetlink (라우팅 Netlink)
rtnetlink(NETLINK_ROUTE)은 가장 널리 사용되는 Netlink 프로토콜 패밀리로, 네트워크 인터페이스, IP 주소, 라우팅 테이블(Routing Table), ARP/이웃 테이블, 트래픽 제어(tc) 등을 관리합니다. iproute2 도구 모음(ip, tc, bridge 등)이 rtnetlink의 주요 유저스페이스 클라이언트입니다.
RTM_* 메시지 타입
| 메시지 그룹 | NEW | DEL | GET | 프로토콜 헤더 |
|---|---|---|---|---|
| 링크 | RTM_NEWLINK | RTM_DELLINK | RTM_GETLINK | struct ifinfomsg |
| 주소 | RTM_NEWADDR | RTM_DELADDR | RTM_GETADDR | struct ifaddrmsg |
| 라우트 | RTM_NEWROUTE | RTM_DELROUTE | RTM_GETROUTE | struct rtmsg |
| 이웃 | RTM_NEWNEIGH | RTM_DELNEIGH | RTM_GETNEIGH | struct ndmsg |
| 규칙 | RTM_NEWRULE | RTM_DELRULE | RTM_GETRULE | struct fib_rule_hdr |
| qdisc | RTM_NEWQDISC | RTM_DELQDISC | RTM_GETQDISC | struct tcmsg |
RTM_GETLINK 메시지 구성 예시
/* RTM_GETLINK 요청 메시지: 모든 네트워크 인터페이스 목록 요청 */
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifm;
} req;
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_type = RTM_GETLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; /* 전체 덤프 */
req.nlh.nlmsg_seq = 1;
req.ifm.ifi_family = AF_UNSPEC; /* 모든 주소 패밀리 */
send(fd, &req, req.nlh.nlmsg_len, 0);
/* 응답: 여러 RTM_NEWLINK 메시지 + NLMSG_DONE
* 각 메시지: nlmsghdr + ifinfomsg + nlattr 속성들
* IFLA_IFNAME → 인터페이스 이름 ("eth0")
* IFLA_MTU → MTU 값
* IFLA_ADDRESS → MAC 주소
* IFLA_STATS64 → 인터페이스 통계
* ...
*/
ifinfomsg 구조체
/* include/uapi/linux/if_link.h */
struct ifinfomsg {
unsigned char ifi_family; /* AF_UNSPEC */
unsigned char __ifi_pad;
unsigned short ifi_type; /* ARPHRD_ETHER 등 */
int ifi_index; /* 인터페이스 인덱스 */
unsigned int ifi_flags; /* IFF_UP, IFF_RUNNING 등 */
unsigned int ifi_change; /* 변경 마스크 */
};
/* IFLA_* 속성 타입 (주요 항목) */
enum {
IFLA_UNSPEC,
IFLA_ADDRESS, /* L2 주소 (MAC) */
IFLA_BROADCAST, /* L2 브로드캐스트 주소 */
IFLA_IFNAME, /* 인터페이스 이름 (문자열) */
IFLA_MTU, /* MTU (u32) */
IFLA_LINK, /* 링크 인덱스 */
IFLA_QDISC, /* qdisc 이름 */
IFLA_STATS, /* 인터페이스 통계 */
IFLA_MASTER, /* 마스터 인터페이스 인덱스 */
IFLA_OPERSTATE, /* 운용 상태 (IF_OPER_*) */
IFLA_LINKINFO, /* nested: 링크 타입 정보 (veth, bridge 등) */
IFLA_STATS64, /* 64비트 인터페이스 통계 */
/* ... 약 60개 이상의 속성 */
};
Generic Netlink (genetlink)
Generic Netlink은 NETLINK_GENERIC 프로토콜 위에 다중 패밀리를 동적으로 등록하는 프레임워크입니다. 새 커널 서브시스템이 Netlink 인터페이스를 추가할 때 프로토콜 번호를 소비하지 않으며, 자동으로 패밀리 ID를 할당받습니다.
Generic Netlink 메시지 구조
/* include/uapi/linux/genetlink.h */
struct genlmsghdr {
__u8 cmd; /* 명령 번호 (패밀리별 정의) */
__u8 version; /* 패밀리 프로토콜 버전 */
__u16 reserved; /* 예약 */
};
/* Generic Netlink 메시지 레이아웃:
* ┌───────────────────┐
* │ nlmsghdr (16B) │ nlmsg_type = 패밀리 ID (동적 할당)
* ├───────────────────┤
* │ genlmsghdr (4B) │ cmd = 명령 번호
* ├───────────────────┤
* │ nlattr 속성들 │
* └───────────────────┘
*/
커널 모듈(Kernel Module)에서 Generic Netlink 패밀리 등록
/* 커널 모듈: Generic Netlink 패밀리 등록 예제 */
#include <linux/module.h>
#include <net/genetlink.h>
/* 속성(attribute) 정의 */
enum my_genl_attrs {
MY_ATTR_UNSPEC,
MY_ATTR_MSG, /* NLA_STRING: 메시지 문자열 */
MY_ATTR_VALUE, /* NLA_U32: 정수 값 */
__MY_ATTR_MAX,
};
#define MY_ATTR_MAX (__MY_ATTR_MAX - 1)
/* 속성 검증 정책 */
static const struct nla_policy my_genl_policy[MY_ATTR_MAX + 1] = {
[MY_ATTR_MSG] = { .type = NLA_NUL_STRING, .len = 256 },
[MY_ATTR_VALUE] = { .type = NLA_U32 },
};
/* 명령(command) 정의 */
enum my_genl_cmds {
MY_CMD_UNSPEC,
MY_CMD_ECHO, /* 유저 → 커널 → 유저 에코 */
MY_CMD_NOTIFY, /* 커널 → 유저 알림 */
};
/* multicast 그룹 */
enum my_genl_groups {
MY_GENL_GRP_EVENTS,
};
static const struct genl_multicast_group my_genl_mcgrps[] = {
[MY_GENL_GRP_EVENTS] = { .name = "events" },
};
/* ECHO 명령 핸들러 */
static int my_genl_echo(struct sk_buff *skb,
struct genl_info *info)
{
struct sk_buff *reply;
void *hdr;
char *msg = "(none)";
u32 val = 0;
if (info->attrs[MY_ATTR_MSG])
msg = nla_data(info->attrs[MY_ATTR_MSG]);
if (info->attrs[MY_ATTR_VALUE])
val = nla_get_u32(info->attrs[MY_ATTR_VALUE]);
pr_info("genl echo: msg=%s val=%u\\n", msg, val);
/* 응답 메시지 구성 */
reply = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
if (!reply)
return -ENOMEM;
hdr = genlmsg_put_reply(reply, info,
&my_genl_family, 0, MY_CMD_ECHO);
if (!hdr) {
nlmsg_free(reply);
return -EMSGSIZE;
}
nla_put_string(reply, MY_ATTR_MSG, msg);
nla_put_u32(reply, MY_ATTR_VALUE, val + 1);
genlmsg_end(reply, hdr);
return genlmsg_reply(reply, info);
}
/* 명령 오퍼레이션 배열 */
static const struct genl_small_ops my_genl_ops[] = {
{
.cmd = MY_CMD_ECHO,
.doit = my_genl_echo,
.flags = GENL_CMD_CAP_DO,
},
};
/* 패밀리 정의 */
static struct genl_family my_genl_family = {
.name = "MY_GENL",
.version = 1,
.maxattr = MY_ATTR_MAX,
.policy = my_genl_policy,
.module = THIS_MODULE,
.small_ops = my_genl_ops,
.n_small_ops = ARRAY_SIZE(my_genl_ops),
.mcgrps = my_genl_mcgrps,
.n_mcgrps = ARRAY_SIZE(my_genl_mcgrps),
};
static int __init my_genl_init(void)
{
return genl_register_family(&my_genl_family);
}
static void __exit my_genl_exit(void)
{
genl_unregister_family(&my_genl_family);
}
module_init(my_genl_init);
module_exit(my_genl_exit);
MODULE_LICENSE("GPL");
코드 설명
- enum my_genl_attrs속성 열거형은
UNSPEC(0)부터 시작하고 마지막에__MY_ATTR_MAX를 두어 속성 개수를 자동 계산합니다. 이 패턴은 커널 전역에서 사용되며,MY_ATTR_MAX는nla_policy배열 크기와genl_family.maxattr에 전달됩니다. - my_genl_policy[]
nla_policy배열로 각 속성의 타입과 제약을 선언합니다. 커널이nlmsg_parse()또는 Generic Netlink 프레임워크 내부에서 이 정책에 따라 속성을 자동 검증하므로, 핸들러에서 별도 타입 체크가 불필요합니다.include/net/netlink.h에 정의된struct nla_policy를 사용합니다. - genl_multicast_group멀티캐스트 그룹 이름을 문자열로 등록합니다. 유저스페이스는
genl_ctrl_resolve_grp()로 이름을 그룹 ID로 변환한 뒤nl_socket_add_membership()으로 구독합니다. 그룹 ID는 커널이 패밀리 등록 시 동적으로 할당합니다. - my_genl_echo()
doit콜백으로, 단일 요청-응답을 처리합니다.info->attrs[]에 검증 완료된 속성이 배열로 전달되므로 NULL 체크만 하면 됩니다. 응답은genlmsg_new()로 SKB를 할당하고,genlmsg_put_reply()로 헤더를 작성한 뒤,nla_put_*()로 속성을 추가합니다. - genl_family 구조체패밀리의 모든 메타데이터를 한 곳에 모읍니다.
small_ops는genl_ops보다 메모리 효율적인 경량 버전이며,include/net/genetlink.h에 정의되어 있습니다.genl_register_family()호출 시net/netlink/genetlink.c에서 패밀리 ID를 동적 할당하고nlctrl컨트롤러에 등록합니다. - genl_register_family()모듈 초기화 시 호출하여 패밀리를 커널에 등록합니다. 내부적으로
idr_alloc()를 통해 고유 패밀리 ID(0x10 이상)를 할당하고, ops와 멀티캐스트 그룹을 연결합니다. 모듈 언로드 시genl_unregister_family()로 정리합니다.
커널 측 Netlink API
커널 모듈이 Netlink 소켓을 생성하고 메시지를 송수신하는 핵심 API를 살펴봅니다.
netlink_kernel_create()
/* net/netlink/af_netlink.c */
struct sock *netlink_kernel_create(
struct net *net, /* 네트워크 네임스페이스 */
int unit, /* 프로토콜 번호 (NETLINK_*) */
struct netlink_kernel_cfg *cfg /* 설정 */
);
struct netlink_kernel_cfg {
unsigned int groups; /* multicast 그룹 수 */
unsigned int flags; /* NL_CFG_F_NONROOT_RECV 등 */
void (*input)(struct sk_buff *skb); /* 수신 콜백 */
struct mutex *cb_mutex;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
};
/* 커널 측 Netlink 소켓 생성 예시 */
static struct sock *nl_sk;
static void my_nl_recv_msg(struct sk_buff *skb)
{
struct nlmsghdr *nlh = nlmsg_hdr(skb);
char *payload = (char *)nlmsg_data(nlh);
pid_t pid = nlh->nlmsg_pid;
pr_info("Received from pid %d: %s\\n", pid, payload);
/* 유니캐스트 응답 전송 */
struct sk_buff *skb_out;
int msg_size = strlen(payload);
skb_out = nlmsg_new(msg_size, GFP_KERNEL);
nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
strncpy(nlmsg_data(nlh), payload, msg_size);
netlink_unicast(nl_sk, skb_out, pid, MSG_DONTWAIT);
}
static int __init my_nl_init(void)
{
struct netlink_kernel_cfg cfg = {
.input = my_nl_recv_msg,
};
nl_sk = netlink_kernel_create(&init_net, NETLINK_USERSOCK, &cfg);
if (!nl_sk) {
pr_err("Failed to create netlink socket\\n");
return -ENOMEM;
}
return 0;
}
static void __exit my_nl_exit(void)
{
netlink_kernel_release(nl_sk);
}
코드 설명
- netlink_kernel_create()커널 측 Netlink 소켓을 생성하는 핵심 함수입니다.
net/netlink/af_netlink.c에 구현되어 있으며, 내부적으로__netlink_kernel_create()를 호출하여 소켓을 할당하고 프로토콜 테이블에 등록합니다. 반환된struct sock *은 이후netlink_unicast(),netlink_broadcast()등에서 사용됩니다. - netlink_kernel_cfg소켓 설정 구조체입니다.
input콜백은 유저스페이스로부터 메시지가 도착할 때 호출되며,groups는 지원할 멀티캐스트 그룹 수를 지정합니다.bind/unbind콜백은 유저스페이스가 멀티캐스트 그룹에 가입/탈퇴할 때 커널에 알림을 제공합니다. - my_nl_recv_msg()
input콜백 구현 예시입니다.nlmsg_hdr()로 SKB에서nlmsghdr포인터를 추출하고,nlmsg_data()로 페이로드에 접근합니다. 이 헬퍼들은include/net/netlink.h에 인라인 함수로 정의되어 있습니다. - nlmsg_new() / nlmsg_put()응답 메시지를 구성하는 패턴입니다.
nlmsg_new()로 지정 크기의 SKB를 할당하고,nlmsg_put()으로nlmsghdr를 작성합니다.nlmsg_put()의 인자 순서는 (skb, portid, seq, type, payload_len, flags)이며, 공간 부족 시 NULL을 반환합니다. - netlink_unicast()특정 유저스페이스 소켓으로 메시지를 전송합니다.
pid인자는 대상의 포트 ID이며,MSG_DONTWAIT플래그로 비차단 전송을 수행합니다. SKB의 소유권이 이 함수로 이전되므로, 호출 후nlmsg_free()를 별도로 호출하면 안 됩니다. - netlink_kernel_release()모듈 언로드 시 커널 Netlink 소켓을 해제합니다. 내부적으로
sock_release()를 호출하여 소켓과 연관된 모든 자원을 정리합니다.
커널 송신 함수
| 함수 | 용도 |
|---|---|
netlink_unicast(sk, skb, portid, flags) | 특정 유저스페이스 소켓으로 유니캐스트 전송 |
netlink_broadcast(sk, skb, portid, group, flags) | multicast 그룹에 브로드캐스트 |
nlmsg_unicast(sk, skb, portid) | netlink_unicast 래퍼 (MSG_DONTWAIT) |
nlmsg_multicast(sk, skb, portid, group, flags) | netlink_broadcast 래퍼 |
genlmsg_reply(skb, info) | Generic Netlink 응답 (info에서 portid 추출) |
genlmsg_multicast(family, skb, portid, group, flags) | Generic Netlink multicast |
커널에서 메시지 빌드 패턴
/* Generic Netlink multicast 알림 전송 패턴 */
static int my_send_event(const char *msg, u32 val)
{
struct sk_buff *skb;
void *hdr;
skb = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
if (!skb)
return -ENOMEM;
hdr = genlmsg_put(skb, 0, 0, &my_genl_family,
0, MY_CMD_NOTIFY);
if (!hdr) {
nlmsg_free(skb);
return -EMSGSIZE;
}
nla_put_string(skb, MY_ATTR_MSG, msg);
nla_put_u32(skb, MY_ATTR_VALUE, val);
genlmsg_end(skb, hdr);
/* MY_GENL_GRP_EVENTS 그룹으로 multicast */
return genlmsg_multicast(&my_genl_family, skb, 0,
MY_GENL_GRP_EVENTS, GFP_KERNEL);
}
코드 설명
- genlmsg_new()Generic Netlink 메시지용 SKB를 할당합니다.
NLMSG_GOODSIZE는 페이지 크기에서 SKB 오버헤드를 뺀 최적 크기(보통 ~3,800바이트)이며,include/net/netlink.h에 정의되어 있습니다. 두 번째 인자는 GFP 할당 플래그로, 인터럽트 컨텍스트에서는GFP_ATOMIC을, 프로세스 컨텍스트에서는GFP_KERNEL을 사용합니다. - genlmsg_put()
nlmsghdr와genlmsghdr를 한번에 작성합니다. portid=0은 커널 발신을 의미하고,MY_CMD_NOTIFY는genlmsghdr.cmd에 설정됩니다. 반환값은 Generic Netlink 헤더 시작 포인터로, 이후genlmsg_end()에 전달하여nlmsg_len을 확정합니다. - nla_put_string() / nla_put_u32()속성을 SKB에 추가하는 헬퍼 함수입니다. 내부적으로
nlattr헤더를 작성하고 페이로드를 복사합니다. 공간 부족 시-EMSGSIZE를 반환하므로, 실전 코드에서는 반환값을 반드시 체크해야 합니다.include/net/netlink.h에 인라인으로 구현되어 있습니다. - genlmsg_multicast()지정한 멀티캐스트 그룹의 모든 구독자에게 메시지를 전송합니다. 내부적으로
nlmsg_multicast()→netlink_broadcast()를 호출하며,net/netlink/genetlink.c에 구현되어 있습니다. 구독자가 없으면-ESRCH를 반환하는데, 이는 정상적인 상황이므로 무시해도 됩니다.
유저스페이스 Netlink API
유저스페이스에서 Netlink 소켓을 직접 사용하거나, libnl 라이브러리를 활용할 수 있습니다.
Raw 소켓 API (직접 사용)
/* 유저스페이스: raw Netlink 소켓으로 인터페이스 목록 조회 */
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
int main(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) {
perror("socket");
return 1;
}
/* 소켓 바인드 */
struct sockaddr_nl sa = {
.nl_family = AF_NETLINK,
.nl_pid = getpid(), /* 고유 포트 ID */
.nl_groups = 0, /* multicast 없음 */
};
bind(fd, (struct sockaddr *)&sa, sizeof(sa));
/* RTM_GETLINK 요청 전송 */
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifm;
} req = {
.nlh = {
.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.nlmsg_type = RTM_GETLINK,
.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
.nlmsg_seq = 1,
},
.ifm.ifi_family = AF_UNSPEC,
};
send(fd, &req, req.nlh.nlmsg_len, 0);
/* 응답 수신 및 파싱 */
char buf[8192];
int done = 0;
while (!done) {
int len = recv(fd, buf, sizeof(buf), 0);
struct nlmsghdr *nlh;
for (nlh = (struct nlmsghdr *)buf;
NLMSG_OK(nlh, len);
nlh = NLMSG_NEXT(nlh, len))
{
if (nlh->nlmsg_type == NLMSG_DONE) {
done = 1;
break;
}
if (nlh->nlmsg_type == NLMSG_ERROR) {
struct nlmsgerr *err = NLMSG_DATA(nlh);
fprintf(stderr, "Error: %d\\n", err->error);
done = 1;
break;
}
/* RTM_NEWLINK 메시지 파싱 */
struct ifinfomsg *ifi = NLMSG_DATA(nlh);
struct rtattr *rta;
int rta_len = nlh->nlmsg_len
- NLMSG_LENGTH(sizeof(*ifi));
for (rta = IFLA_RTA(ifi);
RTA_OK(rta, rta_len);
rta = RTA_NEXT(rta, rta_len))
{
if (rta->rta_type == IFLA_IFNAME) {
printf("if%d: %s (flags=0x%x)\\n",
ifi->ifi_index,
(char *)RTA_DATA(rta),
ifi->ifi_flags);
}
}
}
}
close(fd);
return 0;
}
libnl 라이브러리 사용
/* libnl3를 사용한 인터페이스 목록 조회
* 컴파일: gcc -o libnl_demo libnl_demo.c $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0) */
#include <netlink/netlink.h>
#include <netlink/route/link.h>
#include <netlink/cache.h>
int main(void)
{
struct nl_sock *sk = nl_socket_alloc();
nl_connect(sk, NETLINK_ROUTE);
struct nl_cache *cache;
rtnl_link_alloc_cache(sk, AF_UNSPEC, &cache);
struct rtnl_link *link;
link = (struct rtnl_link *)nl_cache_get_first(cache);
while (link) {
printf("%d: %s mtu=%d\\n",
rtnl_link_get_ifindex(link),
rtnl_link_get_name(link),
rtnl_link_get_mtu(link));
link = (struct rtnl_link *)nl_cache_get_next((struct nl_object *)link);
}
nl_cache_free(cache);
nl_socket_free(sk);
return 0;
}
libnl-genl을 사용한 Generic Netlink 통신
/* libnl-genl로 Generic Netlink 패밀리와 통신
* 컴파일: gcc -o genl_demo genl_demo.c $(pkg-config --cflags --libs libnl-3.0 libnl-genl-3.0) */
#include <netlink/netlink.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/ctrl.h>
static int recv_cb(struct nl_msg *msg, void *arg)
{
struct nlattr *attrs[MY_ATTR_MAX + 1];
struct genlmsghdr *ghdr = nlmsg_data(nlmsg_hdr(msg));
nla_parse(attrs, MY_ATTR_MAX,
genlmsg_attrdata(ghdr, 0),
genlmsg_attrlen(ghdr, 0),
NULL);
if (attrs[MY_ATTR_MSG])
printf("Reply msg: %s\\n", nla_get_string(attrs[MY_ATTR_MSG]));
if (attrs[MY_ATTR_VALUE])
printf("Reply val: %u\\n", nla_get_u32(attrs[MY_ATTR_VALUE]));
return NL_OK;
}
int main(void)
{
struct nl_sock *sk = nl_socket_alloc();
genl_connect(sk);
/* 패밀리 ID 조회 (이름 → ID 변환) */
int family_id = genl_ctrl_resolve(sk, "MY_GENL");
if (family_id < 0) {
fprintf(stderr, "Family not found\\n");
return 1;
}
/* 메시지 구성 및 전송 */
struct nl_msg *msg = nlmsg_alloc();
genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ,
family_id, 0, 0, MY_CMD_ECHO, 1);
nla_put_string(msg, MY_ATTR_MSG, "Hello Netlink");
nla_put_u32(msg, MY_ATTR_VALUE, 42);
nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, recv_cb, NULL);
nl_send_auto(sk, msg);
nl_recvmsgs_default(sk);
nlmsg_free(msg);
nl_socket_free(sk);
return 0;
}
Netlink 이벤트 (Multicast)
Netlink의 가장 강력한 기능 중 하나는 커널이 유저스페이스로 비동기 이벤트를 브로드캐스트하는 능력입니다. 유저스페이스 프로세스는 관심 있는 multicast 그룹에 가입하여, 해당 그룹의 이벤트를 수신합니다.
rtnetlink multicast 그룹
| 그룹 | 상수 | 이벤트 내용 |
|---|---|---|
| LINK | RTNLGRP_LINK | 인터페이스 상태 변경 (UP/DOWN, MTU 변경 등) |
| IPv4 주소 | RTNLGRP_IPV4_IFADDR | IPv4 주소 추가/삭제 |
| IPv6 주소 | RTNLGRP_IPV6_IFADDR | IPv6 주소 추가/삭제 |
| IPv4 라우트 | RTNLGRP_IPV4_ROUTE | IPv4 라우팅 테이블 변경 |
| IPv6 라우트 | RTNLGRP_IPV6_ROUTE | IPv6 라우팅 테이블 변경 |
| 이웃 | RTNLGRP_NEIGH | ARP/NDP 이웃 엔트리 변경 |
Multicast 그룹 구독
/* 유저스페이스: 네트워크 이벤트 수신 (ip monitor 동작 원리) */
#include <stdio.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
int main(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
struct sockaddr_nl sa = {
.nl_family = AF_NETLINK,
.nl_pid = getpid(),
.nl_groups = RTMGRP_LINK /* 링크 이벤트 */
| RTMGRP_IPV4_IFADDR /* IPv4 주소 이벤트 */
| RTMGRP_IPV4_ROUTE, /* IPv4 라우팅 이벤트 */
};
bind(fd, (struct sockaddr *)&sa, sizeof(sa));
/* 또는 setsockopt로 개별 그룹 추가 */
int group = RTNLGRP_IPV6_IFADDR;
setsockopt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
&group, sizeof(group));
printf("Monitoring network events...\\n");
char buf[8192];
while (1) {
int len = recv(fd, buf, sizeof(buf), 0);
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
switch (nlh->nlmsg_type) {
case RTM_NEWLINK:
case RTM_DELLINK:
printf("[LINK] type=%d\\n", nlh->nlmsg_type);
break;
case RTM_NEWADDR:
case RTM_DELADDR:
printf("[ADDR] type=%d\\n", nlh->nlmsg_type);
break;
case RTM_NEWROUTE:
case RTM_DELROUTE:
printf("[ROUTE] type=%d\\n", nlh->nlmsg_type);
break;
}
}
}
}
Netlink과 iproute2
iproute2는 Netlink 소켓의 가장 대표적인 유저스페이스 클라이언트입니다. ip, tc, bridge, ss 등의 명령이 내부적으로 rtnetlink 메시지를 교환합니다.
| iproute2 명령 | Netlink 메시지 | 설명 |
|---|---|---|
ip link show | RTM_GETLINK + NLM_F_DUMP | 모든 인터페이스 조회 |
ip link set eth0 up | RTM_NEWLINK (IFF_UP 플래그) | 인터페이스 활성화 |
ip addr add 10.0.0.1/24 dev eth0 | RTM_NEWADDR | IP 주소 추가 |
ip route add 192.168.0.0/24 via 10.0.0.1 | RTM_NEWROUTE | 라우트 추가 |
ip neigh show | RTM_GETNEIGH + NLM_F_DUMP | ARP/NDP 테이블 조회 |
ip monitor | multicast 그룹 구독 | 실시간(Real-time) 이벤트 수신 |
ss -t | SOCK_DIAG_BY_FAMILY (NETLINK_SOCK_DIAG) | TCP 소켓 목록 |
# strace로 ip 명령의 Netlink 통신 확인
strace -e trace=network ip link show 2>&1 | grep -E 'socket|sendmsg|recvmsg'
# socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) = 3
# sendmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=0},
# msg_iov=[{iov_base={{len=32, type=RTM_GETLINK, flags=NLM_F_REQUEST|NLM_F_DUMP, ...}}}]}) = 32
# recvmsg(3, ...) = 3456
# ip monitor 이벤트 모니터링 예시
ip monitor link addr route
# [LINK]Deleted 3: veth0@if4: <BROADCAST,MULTICAST> mtu 1500 ...
# [ADDR]2: eth0 inet 10.0.0.5/24 scope global eth0
# [ROUTE]10.0.1.0/24 via 10.0.0.1 dev eth0
에러 처리
Netlink은 체계적인 에러 보고 메커니즘을 제공합니다. 요청에 NLM_F_ACK 플래그를 설정하면 커널이 NLMSG_ERROR 메시지로 응답하며, 커널 4.12 이후 extack(extended error reporting)으로 더 상세한 에러 정보를 전달합니다.
NLMSG_ERROR 구조
/* include/uapi/linux/netlink.h */
struct nlmsgerr {
int error; /* 음수 errno (0 = ACK 성공) */
struct nlmsghdr msg; /* 원본 요청 헤더 (+ 일부 페이로드) */
/*
* 뒤에 nlattr 속성이 올 수 있음 (extack):
* NLMSGERR_ATTR_MSG → 에러 메시지 문자열
* NLMSGERR_ATTR_OFFS → 문제가 된 속성 오프셋
* NLMSGERR_ATTR_COOKIE → 커널이 전달하는 쿠키
* NLMSGERR_ATTR_POLICY → 속성 검증 정책 위반 정보
*/
};
/* extack을 활성화하려면: */
int one = 1;
setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one));
extack 에러 메시지 확인
# extack 에러 메시지 예시 (iproute2는 자동으로 extack 파싱)
ip route add 10.0.0.0/8 via 999.999.999.999
# Error: inet address is expected rather than "999.999.999.999".
ip link set nonexistent up
# Error: Cannot find device "nonexistent"
# extack 상세 정보가 포함된 에러
ip route add 10.0.0.0/24 via 192.168.1.1 dev eth0 mtu 999999
# Error: Invalid MTU value.
NL_SET_ERR_MSG(extack, "message") 또는 NL_SET_ERR_MSG_ATTR(extack, attr, "message") 매크로(Macro)로 상세 에러 메시지를 설정할 수 있습니다. 이는 유저스페이스 도구의 디버깅을 크게 개선합니다.
/* 커널 측: extack 에러 메시지 설정 */
static int my_newroute(struct sk_buff *skb,
struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
struct nlattr *tb[RTA_MAX + 1];
int err;
err = nlmsg_parse(nlh, sizeof(struct rtmsg),
tb, RTA_MAX, rtm_policy, extack);
if (err < 0)
return err; /* nlmsg_parse가 extack에 에러 설정 */
if (!tb[RTA_GATEWAY]) {
NL_SET_ERR_MSG(extack, "Gateway address is required");
return -EINVAL;
}
if (tb[RTA_METRICS]) {
u32 mtu = nla_get_u32(tb[RTA_METRICS]);
if (mtu > 65535) {
NL_SET_ERR_MSG_ATTR(extack, tb[RTA_METRICS],
"Invalid MTU value");
return -EINVAL;
}
}
/* ... */
return 0;
}
Netlink 보안
Netlink 소켓은 커널 인터페이스에 직접 접근하므로 보안이 중요합니다. 커널은 여러 계층의 접근 제어(Access Control)를 적용합니다.
Capability 기반 접근 제어
/* 커널: Netlink 권한 검사 함수 */
/* 네트워크 네임스페이스 인식 capability 검사 */
bool netlink_ns_capable(
const struct sk_buff *skb,
struct user_namespace *user_ns,
int cap);
/* 일반적인 네트워크 관리 권한 검사 */
bool netlink_capable(
const struct sk_buff *skb,
int cap);
/* 사용 예시: rtnetlink에서 라우트 수정 시 */
if (!netlink_ns_capable(skb, net->user_ns, CAP_NET_ADMIN)) {
NL_SET_ERR_MSG(extack, "Permission denied");
return -EPERM;
}
/* 주요 capability:
* CAP_NET_ADMIN — 네트워크 구성 변경 (라우트, 인터페이스, 방화벽)
* CAP_NET_RAW — raw 소켓 생성
* CAP_SYS_ADMIN — 커널 모듈, 시스템 설정
* CAP_AUDIT_WRITE — 감사 로그 기록 (NETLINK_AUDIT)
*/
네트워크 네임스페이스(Namespace) 격리(Isolation)
netlink_kernel_create()의 첫 인자 struct net *가 네임스페이스를 결정합니다.
/* 커널: 네트워크 네임스페이스별 Netlink 소켓 */
struct sock *netlink_kernel_create(
struct net *net, /* 네임스페이스 지정 */
int unit,
struct netlink_kernel_cfg *cfg);
/* rtnetlink은 per-netns로 초기화됨 */
/* net/core/rtnetlink.c */
static int __net_init rtnetlink_net_init(struct net *net)
{
struct sock *sk;
struct netlink_kernel_cfg cfg = {
.groups = RTNLGRP_MAX,
.input = rtnetlink_rcv,
.flags = NL_CFG_F_NONROOT_RECV,
};
sk = netlink_kernel_create(net, NETLINK_ROUTE, &cfg);
net->rtnl = sk;
return 0;
}
Strict 검증 모드
/* Netlink strict 검증 (커널 5.2+)
* 엄격한 메시지 파싱으로 보안 강화 */
/* 유저스페이스에서 strict 모드 활성화 */
int one = 1;
setsockopt(fd, SOL_NETLINK, NETLINK_GET_STRICT_CHK,
&one, sizeof(one));
/* strict 모드에서 추가 검증:
* - 알려지지 않은 속성(nlattr) 거부
* - 메시지 끝에 여분의 바이트 거부
* - GET 요청에서 필터링 속성 검증
* - 중첩 속성(nested) 구조 엄격 검사
*/
Netlink 디버깅
Netlink 통신 문제를 진단하는 다양한 도구와 기법을 살펴봅니다.
nlmon (Netlink 모니터 인터페이스)
# nlmon: Netlink 트래픽을 캡처할 수 있는 가상 인터페이스
# 커널 모듈 로드
modprobe nlmon
# nlmon 인터페이스 생성
ip link add nlmon0 type nlmon
ip link set nlmon0 up
# tcpdump/wireshark로 Netlink 메시지 캡처
tcpdump -i nlmon0 -w netlink.pcap
# 다른 터미널에서 ip 명령 실행 → 패킷 캡처됨
# Wireshark에서 netlink.pcap 열면 모든 Netlink 메시지를
# 프로토콜별로 파싱하여 표시
# 정리
ip link del nlmon0
iproute2 monitor
# 모든 rtnetlink 이벤트 모니터링
ip monitor all
# 특정 이벤트만 모니터링
ip monitor link # 링크 상태 변경
ip monitor address # IP 주소 변경
ip monitor route # 라우팅 테이블 변경
ip monitor neigh # ARP/NDP 변경
# 타임스탬프 포함
ip -ts monitor link
# JSON 출력 (파싱용)
ip -j monitor link
strace를 활용한 Netlink 추적
# strace로 ip 명령의 Netlink 시스콜 추적
strace -e trace=socket,bind,sendmsg,recvmsg -s 256 ip link show
# Netlink 소켓 파일 디스크립터만 필터
strace -e trace=network -y ip route show
# sendmsg 페이로드 상세 출력
strace -e trace=sendmsg -x -s 1024 ip addr add 10.0.0.1/24 dev lo
bpftrace를 활용한 커널 측 추적
# netlink_sendmsg 추적
bpftrace -e 'kprobe:netlink_sendmsg {
printf("pid=%d comm=%s protocol=%d\n",
pid, comm, ((struct sock *)arg0)->sk_protocol);
}'
# rtnetlink 메시지 수신 추적
bpftrace -e 'kprobe:rtnetlink_rcv {
printf("rtnetlink_rcv: skb=%p len=%d\n",
arg0, ((struct sk_buff *)arg0)->len);
}'
# netlink_broadcast 추적 (커널 → 유저 이벤트)
bpftrace -e 'kprobe:netlink_broadcast_filtered {
printf("broadcast: group=%d protocol=%d\n",
arg3, ((struct sock *)arg0)->sk_protocol);
}'
주요 /proc 파일
| 경로 | 설명 |
|---|---|
/proc/net/netlink | 열린 Netlink 소켓 목록 (프로토콜, pid, 그룹 등) |
/proc/net/protocols | 프로토콜 통계 (netlink 항목 포함) |
/sys/kernel/debug/tracing/events/netlink/ | ftrace Netlink 이벤트 트레이스포인트 |
# 열린 Netlink 소켓 확인
cat /proc/net/netlink
# sk Eth Pid Groups Rmem Wmem Dump Locks Drops Inode
# ffff... 0 0 00000000 0 0 0 2 0 7
# ffff... 0 1234 00000111 0 0 0 2 0 23456
# ↑ ↑
# Eth=protocol(0=ROUTE) Groups=subscribed multicast groups (bitmask)
# Generic Netlink 등록된 패밀리 확인
genl-ctrl-list
# 또는
python3 -c "
import socket, struct
# NETLINK_GENERIC(16) CTRL 패밀리로 등록된 모든 패밀리 조회
"
커널 내부 데이터 경로
유저스페이스에서 sendmsg()로 전송한 Netlink 메시지가 커널 서브시스템에 도달하기까지의 내부 경로를 이해하면, 디버깅과 성능 분석에 큰 도움이 됩니다.
netlink_sock 구조체
각 Netlink 소켓은 netlink_sock 구조체로 표현됩니다. 이 구조체는 범용 sock 구조체를 확장하여 Netlink 고유 필드를 추가합니다.
/* net/netlink/af_netlink.h */
struct netlink_sock {
struct sock sk; /* 범용 소켓 구조체 (반드시 첫 필드) */
u32 portid; /* 소켓 포트 ID (유저: pid 기반, 커널: 0) */
u32 dst_portid; /* 대상 포트 ID */
u32 dst_group; /* 대상 multicast 그룹 */
u32 flags; /* NETLINK_F_* 플래그 */
u32 subscriptions;/* 구독 중인 그룹 수 */
u32 ngroups; /* 할당된 그룹 슬롯 수 */
unsigned long *groups; /* multicast 그룹 비트맵 */
unsigned long state; /* NETLINK_S_* 상태 플래그 */
size_t max_recvmsg_len; /* 최대 수신 메시지 크기 */
wait_queue_head_t wait; /* 대기 큐 */
struct netlink_callback cb; /* dump 콜백 상태 */
void (*netlink_rcv)(struct sk_buff *skb);
int (*netlink_bind)(struct net *net, int group);
void (*netlink_unbind)(struct net *net, int group);
struct module *module; /* 소유 모듈 (커널 소켓) */
};
Netlink 소켓 해시 테이블(Hash Table)
커널은 프로토콜별로 netlink_table 배열을 유지하여 소켓을 관리합니다. portid를 키로 사용하는 해시 테이블로 대상 소켓을 빠르게 탐색합니다.
/* net/netlink/af_netlink.c */
struct netlink_table {
struct rhashtable hash; /* portid 기반 해시 테이블 */
struct hlist_head mc_list; /* multicast 구독자 리스트 */
struct listeners __rcu *listeners; /* 그룹별 리스너 비트맵 */
unsigned int flags;
unsigned int groups; /* 지원하는 multicast 그룹 수 */
struct mutex *cb_mutex;
struct module *module;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
int registered; /* 등록 여부 */
};
/* 프로토콜별 테이블 (MAX_LINKS = 32) */
static struct netlink_table *nl_table;
/* portid로 소켓 탐색 */
static struct sock *netlink_lookup(
struct net *net,
int protocol,
u32 portid); /* rhashtable_lookup_fast() 사용 */
rtnetlink 메시지 디스패치(Dispatch)
rtnetlink_rcv()가 호출되면, 메시지 타입에 따라 등록된 doit/dumpit 핸들러(Handler)로 분기합니다. rtnl_register()로 등록된 핸들러 테이블을 참조합니다.
/* net/core/rtnetlink.c — 핸들러 등록 */
void rtnl_register(
int protocol, /* PF_UNSPEC 또는 특정 프로토콜 */
int msgtype, /* RTM_NEWLINK, RTM_GETROUTE 등 */
rtnl_doit_func doit, /* 단일 요청 처리 (NEW/DEL/SET) */
rtnl_dumpit_func dumpit, /* 덤프 요청 처리 (GET + NLM_F_DUMP) */
unsigned int flags); /* RTNL_FLAG_DOIT_UNLOCKED 등 */
/* 예: 라우팅 서브시스템의 핸들러 등록 */
rtnl_register(PF_UNSPEC, RTM_GETROUTE, inet_rtm_getroute,
inet_dump_fib, 0);
rtnl_register(PF_UNSPEC, RTM_NEWROUTE, inet_rtm_newroute,
NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELROUTE, inet_rtm_delroute,
NULL, 0);
/* 디스패치 흐름:
* rtnetlink_rcv()
* → netlink_rcv_skb()
* → rtnetlink_rcv_msg()
* → if (NLM_F_DUMP) → rtnl_dumpit() (비동기 덤프)
* → else → rtnl_doit() (동기 처리)
*/
rtnl_lock() (전역 뮤텍스(Mutex))을 보유한 상태에서 실행됩니다. 이는 네트워크 구성 변경의 직렬화(Serialization)를 보장하지만, 동시성 병목(Bottleneck)이 될 수 있습니다. 커널 5.x 이후 RTNL_FLAG_DOIT_UNLOCKED 플래그로 일부 핸들러를 RTNL 락 없이 실행할 수 있습니다.
NLM_F_DUMP 덤프 메커니즘
대량의 데이터(라우팅 테이블 전체, 수천 개 인터페이스 등)를 조회할 때는 NLM_F_DUMP 플래그를 사용합니다. 커널은 데이터를 여러 메시지로 분할하여 스트리밍 방식으로 전달합니다.
/* 커널 측 dumpit 콜백 구조 */
struct netlink_callback {
struct sk_buff *skb; /* 응답을 채울 sk_buff */
const struct nlmsghdr *nlh; /* 원본 요청 헤더 */
int (*dump)(struct sk_buff *skb,
struct netlink_callback *cb);
int (*done)(struct netlink_callback *cb);
void *data;
u16 family;
u16 answer_flags;
u32 min_dump_alloc; /* 최소 skb 할당 크기 */
unsigned int prev_seq;
unsigned int seq;
long args[6]; /* 반복 상태 저장용 */
};
/* dumpit 구현 예시: 라우팅 테이블 덤프 */
static int inet_dump_fib(struct sk_buff *skb,
struct netlink_callback *cb)
{
/* cb->args[0] = 현재 테이블 ID
* cb->args[1] = 현재 버킷 인덱스
* cb->args[2] = 현재 항목 오프셋
*
* skb에 항목을 추가하다가 공간이 부족하면 반환
* → 커널이 skb를 유저에게 전달 후 다시 호출
* → cb->args[]로 이전 위치에서 재개
*/
int tb_id = cb->args[0];
/* ... 라우팅 엔트리 반복 ... */
for (; tb_id < RT_TABLE_MAX; tb_id++) {
/* nlmsg_put()으로 skb에 메시지 추가 */
if (nlmsg_put(skb, ...) == NULL) {
cb->args[0] = tb_id; /* 위치 저장 */
return skb->len; /* 더 있음 */
}
}
return 0; /* 완료 → NLMSG_DONE 전송 */
}
코드 설명
- netlink_callback덤프 작업의 전체 상태를 관리하는 구조체입니다.
net/netlink/af_netlink.c의netlink_dump()함수가 이 구조체를 생성하고,dump콜백을 반복 호출합니다.args[6]배열은 콜백 간에 반복 상태(현재 위치, 인덱스 등)를 저장하는 범용 저장소입니다. - dumpit 콜백 패턴커널의 덤프는 반복(iterative) 방식으로 동작합니다. 콜백이 호출될 때마다
skb에 가능한 많은 항목을 채우고, 공간이 부족하면 현재 위치를cb->args[]에 저장한 뒤skb->len을 반환합니다. 커널은 이 SKB를 유저스페이스로 전달한 후 다시 콜백을 호출하여 남은 데이터를 계속 전송합니다. - nlmsg_put() == NULL
nlmsg_put()이 NULL을 반환하면 SKB에 더 이상 공간이 없다는 뜻입니다. 이 시점에서 현재 반복 위치를cb->args[]에 저장하고skb->len(현재까지 채운 바이트 수)을 반환하면, 커널이 이 부분을 먼저 유저스페이스로 전달합니다. - return 00을 반환하면 모든 데이터 전송이 완료되었음을 의미합니다. 커널은
NLMSG_DONE메시지를 유저스페이스에 전송하여 덤프 종료를 알립니다. 유저스페이스의NLM_F_MULTI메시지 수신 루프는 이NLMSG_DONE을 받으면 종료됩니다.
NLM_F_DUMP_INTR 플래그가 설정되어 유저스페이스에 알립니다. 유저스페이스는 이 플래그를 확인하고 필요시 덤프를 재시도해야 합니다.
nla_policy 속성 검증
Netlink 메시지의 nlattr 속성은 nla_policy 배열을 통해 자동 검증됩니다. 이는 커널 보안의 핵심으로, 유저스페이스에서 전달된 데이터의 타입과 범위를 커널이 파싱 전에 검증합니다.
/* include/net/netlink.h */
struct nla_policy {
u8 type; /* NLA_* 타입 */
u8 validation_type; /* NLA_VALIDATE_* */
u16 len; /* 최대/고정 길이 */
union {
const u32 bitfield32_valid; /* NLA_BITFIELD32 유효 마스크 */
const u32 mask;
const char *reject_message;
const struct nla_policy *nested_policy;
struct {
s16 min, max; /* 범위 검증 */
};
};
};
| NLA 타입 | 크기 | 설명 | len 필드 의미 |
|---|---|---|---|
NLA_U8 | 1 | 8비트 부호 없는 정수 | 무시 |
NLA_U16 | 2 | 16비트 부호 없는 정수 | 무시 |
NLA_U32 | 4 | 32비트 부호 없는 정수 | 무시 |
NLA_U64 | 8 | 64비트 부호 없는 정수 | 무시 |
NLA_S32 | 4 | 32비트 부호 있는 정수 | 무시 |
NLA_STRING | 가변 | 문자열 (NUL 포함 안 함) | 최대 길이 |
NLA_NUL_STRING | 가변 | NUL 종료 문자열 | 최대 길이 |
NLA_BINARY | 가변 | 바이너리 데이터 | 최대 길이 |
NLA_FLAG | 0 | 불리언 플래그 (존재 = true) | 무시 |
NLA_NESTED | 가변 | 중첩 속성 컨테이너(Container) | 무시 (nested_policy 사용) |
NLA_NESTED_ARRAY | 가변 | 중첩 속성 배열 | 무시 |
NLA_BITFIELD32 | 8 | 32비트 비트필드 + 마스크 | 무시 (bitfield32_valid 사용) |
NLA_REJECT | - | 속성 거부 (오류 메시지 반환) | 무시 (reject_message 사용) |
/* 속성 검증 정책 실전 예시 — 범위 검증, 중첩, 문자열 */
static const struct nla_policy my_policy[MY_ATTR_MAX + 1] = {
[MY_ATTR_NAME] = {
.type = NLA_NUL_STRING,
.len = IFNAMSIZ - 1, /* 최대 15바이트 + NUL */
},
[MY_ATTR_MTU] = {
.type = NLA_U32,
.validation_type = NLA_VALIDATE_RANGE,
.min = 68, /* IPv4 최소 MTU */
.max = 65535, /* 최대 MTU */
},
[MY_ATTR_FLAGS] = {
.type = NLA_BITFIELD32,
.bitfield32_valid = MY_FLAG_A | MY_FLAG_B | MY_FLAG_C,
},
[MY_ATTR_CONFIG] = {
.type = NLA_NESTED,
.nested_policy = my_config_policy, /* 하위 속성 정책 */
.len = MY_CONFIG_ATTR_MAX,
},
[MY_ATTR_DEPRECATED] = {
.type = NLA_REJECT,
.reject_message = "This attribute is no longer supported",
},
};
코드 설명
- NLA_NUL_STRING + .len
MY_ATTR_NAME은 NUL 종료 문자열로,.len은 NUL을 제외한 최대 길이입니다.IFNAMSIZ - 1(15)을 지정하여 인터페이스 이름 길이 제한을 적용합니다. 커널의nla_validate_range_unsigned()가include/net/netlink.h의 정책에 따라 검증합니다. - NLA_VALIDATE_RANGE
MY_ATTR_MTU에 범위 검증을 적용합니다..min = 68(IPv4 최소 MTU),.max = 65535로 설정하여, 이 범위를 벗어나는 값은 커널이-ERANGE와 함께 extack 에러 메시지를 자동 반환합니다. 커널 4.20+에서 도입되어 핸들러 코드의 수동 검증을 대체합니다. - NLA_BITFIELD32
MY_ATTR_FLAGS는 32비트 비트필드로,.bitfield32_valid에 허용되는 비트 마스크를 지정합니다. 유저스페이스가 유효하지 않은 비트를 설정하면 커널이 자동으로 거부합니다. 페이로드는struct nla_bitfield32(value + selector 각 4바이트)로 총 8바이트입니다. - NLA_NESTED + nested_policy
MY_ATTR_CONFIG는 중첩 속성으로,.nested_policy로 하위 속성의 검증 정책을 재귀적으로 지정합니다. 커널 5.2+ strict 검증 모드에서는 중첩 정책이 있으면 하위 속성까지 자동으로 검증합니다. - NLA_REJECT더 이상 지원하지 않는 속성을 명시적으로 거부합니다. 유저스페이스가 이 속성을 전송하면
.reject_message문자열이 extack을 통해 반환되어, 사용자에게 폐기 사유를 알립니다. 하위 호환성을 유지하면서 점진적으로 속성을 제거할 때 유용합니다.
소켓 버퍼 튜닝
고부하 환경에서 Netlink 메시지 손실(ENOBUFS)을 방지하려면 소켓 버퍼 크기를 적절히 조정해야 합니다. 특히 multicast 이벤트를 대량으로 수신하는 모니터링 프로세스에서 중요합니다.
관련 sysctl 파라미터
| sysctl 파라미터 | 기본값 | 설명 |
|---|---|---|
net.core.rmem_default | 212992 | 소켓 수신 버퍼 기본 크기 (바이트) |
net.core.rmem_max | 212992 | 소켓 수신 버퍼 최대 크기 (setsockopt 상한) |
net.core.wmem_default | 212992 | 소켓 송신 버퍼 기본 크기 |
net.core.wmem_max | 212992 | 소켓 송신 버퍼 최대 크기 |
net.core.optmem_max | 20480 | 소켓 옵션 메모리 최대 크기 |
유저스페이스 소켓 버퍼 설정
/* 소켓 수신 버퍼 크기 확대 */
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
/* 수신 버퍼 크기를 8MB로 설정 */
int rcvbuf = 8 * 1024 * 1024;
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
/* 실제 할당: min(rcvbuf * 2, rmem_max)
* rmem_max보다 큰 값은 무시됨 */
/* CAP_NET_ADMIN 있으면 rmem_max 무시 가능 */
setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf));
/* ENOBUFS 무시 (메시지 손실 허용, 드롭 대신 계속 수신) */
int one = 1;
setsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &one, sizeof(one));
# 운영 환경: Netlink 버퍼 관련 sysctl 튜닝
# 대량 라우팅 이벤트 수신 시 (BGP fullview 등)
sysctl -w net.core.rmem_max=16777216 # 16MB
sysctl -w net.core.rmem_default=8388608 # 8MB
# 현재 Netlink 소켓별 버퍼 사용량 확인
cat /proc/net/netlink
# Rmem 컬럼: 현재 수신 버퍼 사용량 (바이트)
# Netlink 드롭 통계 확인
cat /proc/net/netlink | awk 'NR>1 && $9>0 {print "protocol="$2, "pid="$3, "drops="$9}'
ENOBUFS를 반환합니다. NetworkManager, systemd-networkd 같은 데몬이 이벤트를 놓치면 인터페이스 상태 불일치가 발생할 수 있습니다. 운영 환경에서는 반드시 rmem_max를 충분히 확보하세요.
성능 고려사항과 일반적인 실수
RTNL 락 병목
네트워크 구성 변경(인터페이스 추가/삭제, 주소/라우트 변경)은 전역 rtnl_mutex로 직렬화됩니다. 수천 개의 VLAN/VxLAN 인터페이스를 동시에 설정하거나, 대규모 라우팅 테이블을 갱신할 때 병목이 됩니다.
| 문제 | 증상 | 해결 방안 |
|---|---|---|
| RTNL 락 경합(Contention) | ip 명령 응답 지연, D 상태 프로세스 |
배치 처리 (ip -batch), 커널 업그레이드 (RTNL 락 세분화) |
| 덤프 중 변경 | NLM_F_DUMP_INTR, 불완전한 결과 |
덤프 재시도 로직 구현, 짧은 덤프 구간 유지 |
| multicast 폭풍 | 모든 구독자에게 복사 → CPU 부하, ENOBUFS |
필요한 그룹만 구독, 버퍼 크기 확대 |
| 메시지 크기 초과 | EMSGSIZE 에러, 불완전한 응답 |
min_dump_alloc 확대, 속성 분할 |
| 시퀀스 번호 불일치 | 응답 매칭 실패, 잘못된 파싱 | 고유한 시퀀스 번호 생성, 응답 필터링 구현 |
배치 처리 (ip -batch)
# 비효율적: 각 명령마다 소켓 생성/해제 + RTNL 락 획득/해제
for i in $(seq 1 1000); do
ip addr add 10.0.$((i/256)).$((i%256))/32 dev dummy0
done
# → 약 15초 (RTNL 락 1000회 획득)
# 효율적: 배치 모드 (단일 소켓, RTNL 락 최소화)
for i in $(seq 1 1000); do
echo "addr add 10.0.$((i/256)).$((i%256))/32 dev dummy0"
done | ip -batch -
# → 약 0.5초
# pyroute2 (Python) — 배치 + NLM_F_ACK 활용
# from pyroute2 import IPRoute
# ipr = IPRoute()
# for i in range(1000):
# ipr.addr('add', index=ipr.link_lookup(ifname='dummy0')[0],
# address=f'10.0.{i//256}.{i%256}', prefixlen=32)
일반적인 구현 실수
- 정렬 무시 —
NLMSG_ALIGN(),NLA_ALIGN()없이 포인터 연산하면 SIGBUS(일부 아키텍처) 또는 파싱 오류 발생 - NLMSG_DONE 미확인 — 덤프 응답에서 NLMSG_DONE을 확인하지 않으면 불완전한 데이터를 최종 결과로 사용
- 수신 버퍼 크기 부족 — 기본
recv()버퍼가 작으면 메시지가 잘려서MSG_TRUNC발생 - portid 충돌 — 여러 소켓에서 같은
nl_pid를 사용하면EADDRINUSE; 0으로 설정하면 커널이 자동 할당 - nla_policy 미설정 — 속성 검증 정책 없이 파싱하면 임의 데이터가 커널에 전달되어 보안 취약점(Vulnerability) 발생
Netlink 기반 커널 서브시스템
현대 Linux 커널의 수십 개 서브시스템이 Netlink를 통해 유저스페이스와 통신합니다. 각 서브시스템의 Netlink 프로토콜과 관련 도구를 정리합니다.
| 서브시스템 | 프로토콜/패밀리 | 유저 도구 | 상세 페이지(Page) |
|---|---|---|---|
| 라우팅 | NETLINK_ROUTE (rtnetlink) | iproute2 (ip) | 라우팅 |
| TC (트래픽 제어) | NETLINK_ROUTE (tc 메시지) | tc | TC |
| Netfilter | NETLINK_NETFILTER (nfnetlink) | nftables, conntrack | Netfilter |
| ethtool | Generic Netlink (ETHTOOL) | ethtool | ethtool |
| devlink | Generic Netlink (devlink) | devlink | devlink |
| Wireless | Generic Netlink (nl80211) | iw, wpa_supplicant | Wireless |
| WireGuard | Generic Netlink (wireguard) | wg | WireGuard |
| Open vSwitch | Generic Netlink (ovs_*) | ovs-vsctl | OVS |
| Bridge | NETLINK_ROUTE (bridge 메시지) | bridge | Bridge |
| IPsec | NETLINK_XFRM | ip xfrm, strongSwan | IPsec & xfrm |
| 감사 (Audit) | NETLINK_AUDIT | auditd, auditctl | - |
| udev | NETLINK_KOBJECT_UEVENT | udevadm | 디바이스 드라이버 |
| L2TP | Generic Netlink (l2tp) | ip l2tp | L2TP |
Netlink 소켓 옵션 (SOL_NETLINK)
setsockopt(fd, SOL_NETLINK, ...)으로 설정하는 Netlink 전용 소켓 옵션을 정리합니다.
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
NETLINK_ADD_MEMBERSHIP | int (그룹 번호) | - | multicast 그룹 구독 추가 |
NETLINK_DROP_MEMBERSHIP | int (그룹 번호) | - | multicast 그룹 구독 해제 |
NETLINK_PKTINFO | int (0/1) | 0 | recvmsg()에 nl_pktinfo cmsg 포함 |
NETLINK_BROADCAST_ERROR | int (0/1) | 0 | broadcast 전달 실패 시 에러 보고 |
NETLINK_NO_ENOBUFS | int (0/1) | 0 | 버퍼 오버런 시 ENOBUFS 대신 자동 드롭 |
NETLINK_LISTEN_ALL_NSID | int (0/1) | 0 | 다른 네임스페이스의 이벤트도 수신 |
NETLINK_CAP_ACK | int (0/1) | 0 | ACK 메시지에서 원본 페이로드 생략 (오버헤드(Overhead) 감소) |
NETLINK_EXT_ACK | int (0/1) | 0 | 확장 에러 보고 (extack) 활성화 |
NETLINK_GET_STRICT_CHK | int (0/1) | 0 | 엄격한 메시지 검증 모드 |
nlmsghdr / nlattr
Netlink 메시지의 기반이 되는 nlmsghdr와 nlattr의 내부 동작을 상세히 분석합니다. 바이트 정렬, 중첩 속성 처리, 매크로 기반 반복자 패턴은 올바른 Netlink 구현의 핵심입니다.
nlmsghdr 필드 상세
nlmsg_len 필드는 메시지 전체 길이를 나타내며, 반드시 NLMSG_ALIGN()로 정렬된 값이어야 합니다. 유저스페이스에서 메시지를 순회할 때 NLMSG_OK()와 NLMSG_NEXT() 매크로가 이 길이 필드를 기반으로 경계를 계산합니다.
/* nlmsghdr 필드별 상세 설명 */
/* nlmsg_len: 전체 메시지 길이 (헤더 16바이트 포함)
* - NLMSG_LENGTH(payload_len)으로 계산
* - 메시지 간 경계 계산의 기준
* - 수신 측에서 NLMSG_OK(nlh, remaining)으로 유효성 검증 */
/* nlmsg_type: 메시지 유형
* - 0x0000 ~ 0x000F: 예약 (NLMSG_NOOP=1, NLMSG_ERROR=2, NLMSG_DONE=3)
* - 0x0010 ~ 0x003F: rtnetlink (RTM_NEWLINK=16, RTM_DELLINK=17, ...)
* - genetlink: 동적 할당된 패밀리 ID (>= GENL_MIN_ID=0x10) */
/* nlmsg_flags: 요청/응답 플래그 조합
* GET 요청: NLM_F_REQUEST | NLM_F_DUMP (전체) 또는 NLM_F_REQUEST (단일)
* NEW 요청: NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL (생성만)
* NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE (생성 또는 교체)
* DEL 요청: NLM_F_REQUEST
* 항상 포함: NLM_F_ACK (에러/성공 응답 요청) */
/* nlmsg_seq: 시퀀스 번호
* - 유저스페이스가 요청에 설정
* - 커널 응답에 동일한 seq 반환 → 요청-응답 매칭
* - multicast 이벤트에서는 0 (커널 발신) */
/* nlmsg_pid: 발신자 포트 ID
* - 커널: 0
* - 유저스페이스: bind() 시 지정하거나 커널 자동 할당
* - 주의: 프로세스 PID와 반드시 같지 않음 (관례적으로 getpid() 사용) */
nla_nest: 중첩 속성 처리
Netlink 속성은 트리 구조로 중첩될 수 있습니다. nla_nest_start()와 nla_nest_end()로 중첩 컨테이너를 생성하며, 커널은 NLA_F_NESTED 플래그로 중첩 여부를 표시합니다.
/* 커널: 중첩 속성 작성 패턴 */
struct sk_buff *skb = nlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
struct nlattr *nest;
/* 중첩 속성 시작 — NLA_F_NESTED 자동 설정 */
nest = nla_nest_start(skb, IFLA_LINKINFO);
if (!nest)
goto nla_put_failure;
/* 중첩 내부에 하위 속성 추가 */
nla_put_string(skb, IFLA_INFO_KIND, "veth");
/* 2단계 중첩: IFLA_INFO_DATA 내부에 veth 파라미터 */
struct nlattr *data = nla_nest_start(skb, IFLA_INFO_DATA);
nla_put_u32(skb, VETH_INFO_PEER, peer_ifindex);
nla_nest_end(skb, data);
/* 외부 중첩 종료 — nla_len 자동 계산 */
nla_nest_end(skb, nest);
/* 중첩 취소 (에러 시): nla_nest_cancel(skb, nest); */
/* 유저스페이스 (libnl): 중첩 속성 파싱 */
struct nlattr *linkinfo[IFLA_INFO_MAX + 1];
struct nlattr *attr = attrs[IFLA_LINKINFO];
if (attr) {
/* 중첩 속성 내부를 파싱 */
nla_parse_nested(linkinfo, IFLA_INFO_MAX, attr, info_policy);
if (linkinfo[IFLA_INFO_KIND])
printf("kind: %s\n", nla_get_string(linkinfo[IFLA_INFO_KIND]));
}
/* nla_for_each_nested: 중첩 속성 순회 매크로 */
struct nlattr *cur;
int rem;
nla_for_each_nested(cur, attr, rem) {
printf(" sub-attr type=%d len=%d\n",
nla_type(cur), nla_len(cur));
}
바이트 정렬 함정
- nla_len vs NLA_ALIGN(nla_len):
nla_len은 실제 데이터 크기를 나타내지만, 다음 속성까지의 오프셋(Offset)은NLA_ALIGN(nla_len)입니다. 패딩(Padding) 바이트를 데이터로 읽으면 파싱이 깨집니다. - nlmsg_len 계산 오류:
nlmsg_len에 패딩을 잘못 포함하거나 누락하면,NLMSG_NEXT()가 잘못된 위치를 가리킵니다. - 직접 포인터 연산:
nlh + 1대신 반드시NLMSG_DATA(nlh)매크로를 사용해야 합니다. 구조체 크기와 정렬 크기가 다를 수 있습니다.
/* 올바른 메시지 순회 패턴 */
struct nlmsghdr *nlh;
int remaining = recv_len;
/* NLMSG_OK: nlmsg_len >= 16 && nlmsg_len <= remaining 검증 */
for (nlh = (struct nlmsghdr *)buf;
NLMSG_OK(nlh, remaining);
nlh = NLMSG_NEXT(nlh, remaining))
{
/* NLMSG_DATA: nlh + NLMSG_HDRLEN (정렬된 헤더 크기) */
void *data = NLMSG_DATA(nlh);
/* NLMSG_PAYLOAD: nlmsg_len - NLMSG_HDRLEN */
int payload_len = NLMSG_PAYLOAD(nlh, 0);
/* 속성 반복 — 프로토콜 헤더 크기를 빼고 시작 */
struct rtattr *rta;
int rta_len = NLMSG_PAYLOAD(nlh, sizeof(struct ifinfomsg));
for (rta = (struct rtattr *)((char *)data + NLMSG_ALIGN(sizeof(struct ifinfomsg)));
RTA_OK(rta, rta_len);
rta = RTA_NEXT(rta, rta_len))
{
/* rta->rta_type, RTA_DATA(rta), RTA_PAYLOAD(rta) 사용 */
}
}
Generic Netlink
Generic Netlink 프레임워크의 내부 디스패치 메커니즘, genl_family 등록 과정, 정책 검증 흐름, 그리고 멀티캐스트 그룹 관리를 상세히 분석합니다.
genl_ops 상세
/* include/net/genetlink.h — ops 구조체 상세 */
struct genl_ops {
int (*doit)(struct sk_buff *skb,
struct genl_info *info); /* 단일 요청 처리 */
int (*start)(struct netlink_callback *cb); /* 덤프 시작 (초기화) */
int (*dumpit)(struct sk_buff *skb,
struct netlink_callback *cb); /* 덤프 반복 */
int (*done)(struct netlink_callback *cb); /* 덤프 완료 (정리) */
const struct nla_policy *policy; /* 명령별 정책 (family 정책 오버라이드) */
unsigned int maxattr; /* 명령별 최대 속성 번호 */
u8 cmd; /* 명령 번호 */
u8 internal_flags; /* 내부 플래그 */
u8 flags; /* GENL_* 접근 제어 플래그 */
u8 validate; /* GENL_DONT_VALIDATE_* */
};
/* genl_info: doit 핸들러에 전달되는 요청 정보 */
struct genl_info {
u32 snd_seq; /* 송신 시퀀스 번호 */
u32 snd_portid; /* 송신자 포트 ID */
const struct genl_family *family;
const struct nlmsghdr *nlhdr;
struct genlmsghdr *genlhdr;
struct nlattr **attrs; /* 검증 완료된 속성 배열 */
struct netlink_ext_ack *extack; /* extack 포인터 */
struct net *_net; /* 네트워크 네임스페이스 */
void *user_ptr[2]; /* pre/post_doit 전달용 */
};
/* genl_ops.flags 접근 제어 상수 */
#define GENL_ADMIN_PERM 0x01 /* CAP_NET_ADMIN 필요 */
#define GENL_CMD_CAP_DO 0x02 /* doit 지원 */
#define GENL_CMD_CAP_DUMP 0x04 /* dumpit 지원 */
#define GENL_UNS_ADMIN_PERM 0x10 /* 네임스페이스별 CAP_NET_ADMIN */
코드 설명
- struct genl_opsGeneric Netlink 명령별 동작을 정의하는 구조체입니다.
doit은 단일 요청-응답 처리,dumpit은 다중 항목 덤프,start/done은 덤프의 초기화/정리를 담당합니다. 명령별로policy를 오버라이드할 수 있어, 같은 패밀리 내에서도 명령마다 다른 속성 검증이 가능합니다.include/net/genetlink.h에 정의되어 있습니다. - struct genl_info
doit핸들러에 전달되는 요청 컨텍스트입니다.attrs[]는nla_policy에 따라 파싱 및 검증이 완료된 속성 포인터 배열이므로, 핸들러에서는 NULL 체크만 하면 안전하게 접근할 수 있습니다.extack을 통해 상세한 에러 메시지를 유저스페이스에 전달할 수 있습니다. - user_ptr[2]
genl_family의pre_doit훅에서 리소스를 조회하여user_ptr[]에 저장하면,doit핸들러에서 바로 사용할 수 있습니다.post_doit훅에서 리소스를 해제하는 패턴으로, 핸들러 코드를 간결하게 유지합니다. devlink, nl80211 등 대규모 패밀리에서 널리 활용됩니다. - flags 상수
GENL_ADMIN_PERM은CAP_NET_ADMIN권한을 요구하며,GENL_UNS_ADMIN_PERM은 네트워크 네임스페이스별로 권한을 체크합니다.GENL_CMD_CAP_DO와GENL_CMD_CAP_DUMP는 해당 명령이doit또는dumpit을 지원함을 선언합니다.net/netlink/genetlink.c의genl_rcv_msg()에서 이 플래그를 확인합니다.
컨트롤러 패밀리 (nlctrl)
Generic Netlink에는 항상 nlctrl 컨트롤러 패밀리(ID=0x10)가 존재합니다. 유저스페이스는 이를 통해 등록된 패밀리 목록을 조회하고, 패밀리 이름을 ID로 변환합니다.
# 등록된 모든 Generic Netlink 패밀리 목록
genl ctrl list
# Name: nlctrl
# ID: 0x10 Version: 2 header size: 0 max attribs: 10
# Name: nl80211
# ID: 0x1c Version: 1 header size: 0 max attribs: 321
# Name: devlink
# ID: 0x14 Version: 1 header size: 0 max attribs: 216
# ...
# 특정 패밀리 정보 조회
genl ctrl list name nl80211
# 멀티캐스트 그룹 포함:
# mcast group: scan ID: 0x07
# mcast group: regulatory ID: 0x08
# mcast group: mlme ID: 0x09
# pyroute2로 패밀리 조회
python3 -c "
from pyroute2.netlink.generic import GenericNetlinkSocket
gs = GenericNetlinkSocket()
for f in gs.get_family_list():
print(f' {f}')
"
Generic Netlink 멀티캐스트 그룹 상세
/* 커널: 멀티캐스트 이벤트 전송 패턴 */
static void my_notify_event(struct net *net, u8 cmd,
const char *data, u32 val)
{
struct sk_buff *skb;
void *hdr;
skb = genlmsg_new(NLMSG_GOODSIZE, GFP_ATOMIC);
if (!skb)
return;
hdr = genlmsg_put(skb, 0, 0, &my_genl_family, 0, cmd);
if (!hdr)
goto free;
if (nla_put_string(skb, MY_ATTR_MSG, data) ||
nla_put_u32(skb, MY_ATTR_VALUE, val))
goto cancel;
genlmsg_end(skb, hdr);
/* 특정 네임스페이스의 그룹 구독자에게만 전달 */
genlmsg_multicast_netns(&my_genl_family, net, skb,
0, MY_GENL_GRP_EVENTS, GFP_ATOMIC);
return;
cancel:
genlmsg_cancel(skb, hdr);
free:
nlmsg_free(skb);
}
/* 유저스페이스: 멀티캐스트 그룹 구독 (libnl-genl) */
/* int group = genl_ctrl_resolve_grp(sk, "MY_GENL", "events");
* nl_socket_add_membership(sk, group);
* nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM, event_cb, NULL);
* while (1) nl_recvmsgs_default(sk); // 이벤트 대기 루프
*/
rtnetlink
rtnetlink의 핵심인 링크 관리(rtnl_link_ops)와 인터페이스 생성/삭제 경로를 심층 분석합니다. 가상 인터페이스(veth, bridge, vxlan 등)의 생성이 커널 내부에서 어떻게 처리되는지 이해하면, 네트워크 구성 자동화와 디버깅에 크게 도움됩니다.
rtnl_link_ops 구조체
/* include/net/rtnetlink.h — 링크 타입 등록 */
struct rtnl_link_ops {
struct list_head list;
const char *kind; /* "veth", "bridge", "vxlan" 등 */
size_t priv_size; /* private 영역 크기 */
struct net_device *(*alloc)(struct nlattr *tb[],
const char *ifname,
unsigned char name_assign_type,
unsigned int num_tx_queues,
unsigned int num_rx_queues);
void (*setup)(struct net_device *dev);
int (*validate)(struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);
int (*newlink)(struct net *src_net,
struct net_device *dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);
int (*changelink)(struct net_device *dev,
struct nlattr *tb[],
struct nlattr *data[],
struct netlink_ext_ack *extack);
void (*dellink)(struct net_device *dev,
struct list_head *head);
size_t (*get_size)(const struct net_device *dev);
int (*fill_info)(struct sk_buff *skb,
const struct net_device *dev);
unsigned int maxtype; /* IFLA_INFO_DATA 최대 속성 */
const struct nla_policy *policy; /* IFLA_INFO_DATA 검증 정책 */
unsigned int slave_maxtype;
const struct nla_policy *slave_policy;
};
/* 링크 타입 등록 예시 (veth) */
static struct rtnl_link_ops veth_link_ops = {
.kind = "veth",
.priv_size = sizeof(struct veth_priv),
.setup = veth_setup,
.validate = veth_validate,
.newlink = veth_newlink,
.dellink = veth_dellink,
.policy = veth_policy,
.maxtype = VETH_INFO_MAX,
.get_size = veth_get_size,
.fill_info = veth_fill_info,
};
/* 모듈 초기화 시 */
rtnl_link_register(&veth_link_ops);
인터페이스 생성/삭제 유저스페이스 예시
/* 유저스페이스: veth 쌍 생성 (raw Netlink) */
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifi;
char buf[1024];
} req;
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_type = RTM_NEWLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK;
req.nlh.nlmsg_seq = ++seq;
req.ifi.ifi_family = AF_UNSPEC;
/* 인터페이스 이름 */
addattr_l(&req.nlh, sizeof(req), IFLA_IFNAME, "veth0", 6);
/* IFLA_LINKINFO: 중첩 속성으로 링크 타입 지정 */
struct rtattr *linkinfo = addattr_nest(&req.nlh, sizeof(req), IFLA_LINKINFO);
addattr_l(&req.nlh, sizeof(req), IFLA_INFO_KIND, "veth", 5);
/* IFLA_INFO_DATA: veth 파라미터 (peer 정보) */
struct rtattr *data = addattr_nest(&req.nlh, sizeof(req), IFLA_INFO_DATA);
struct rtattr *peer = addattr_nest(&req.nlh, sizeof(req), VETH_INFO_PEER);
/* peer의 ifinfomsg + IFLA_IFNAME */
struct ifinfomsg peer_ifi = { .ifi_family = AF_UNSPEC };
addattr_l(&req.nlh, sizeof(req), 0, &peer_ifi, sizeof(peer_ifi));
addattr_l(&req.nlh, sizeof(req), IFLA_IFNAME, "veth1", 6);
addattr_nest_end(&req.nlh, peer);
addattr_nest_end(&req.nlh, data);
addattr_nest_end(&req.nlh, linkinfo);
req.nlh.nlmsg_len = NLMSG_ALIGN(req.nlh.nlmsg_len);
send(fd, &req, req.nlh.nlmsg_len, 0);
/* ACK 수신 → error=0이면 성공 */
Netlink 덤프 프로토콜
NLM_F_DUMP 프로토콜의 분할 메시지, 재시작(Reboot) 감지, 인터럽트(Interrupt) 처리를 상세히 다룹니다.
덤프 재시작과 일관성
/* 커널: 덤프 일관성 검사 패턴
* 테이블의 generation counter를 활용 */
static int my_dumpit(struct sk_buff *skb,
struct netlink_callback *cb)
{
struct net *net = sock_net(skb->sk);
int idx = cb->args[0];
int s_idx = idx;
/* 일관성 검사: 이전 호출 이후 테이블이 변경되었는가? */
nl_dump_check_consistent(cb, nlmsg_hdr(skb),
my_table_generation(net));
/* 변경 시 NLM_F_DUMP_INTR 자동 설정 */
/* 항목 반복 */
for (; idx < my_table_size(net); idx++) {
if (my_fill_entry(skb, cb, idx) < 0) {
/* skb 공간 부족 → 현재 위치 저장 후 반환 */
cb->args[0] = idx;
goto done;
}
}
cb->args[0] = idx;
done:
return skb->len; /* 0이면 완료, >0이면 계속 */
}
/* 유저스페이스: NLM_F_DUMP_INTR 처리 */
int dump_interrupted = 0;
while (!done) {
int len = recv(fd, buf, sizeof(buf), 0);
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
if (nlh->nlmsg_type == NLMSG_DONE) {
/* DONE 메시지의 페이로드에 에러 코드 포함 가능 */
int *errp = NLMSG_DATA(nlh);
if (*errp < 0)
fprintf(stderr, "Dump error: %d\n", *errp);
done = 1;
break;
}
if (nlh->nlmsg_flags & NLM_F_DUMP_INTR) {
dump_interrupted = 1;
/* 덤프 결과가 불완전할 수 있음 */
}
/* 메시지 처리 ... */
}
}
if (dump_interrupted) {
fprintf(stderr, "Dump interrupted, retrying...\n");
/* 덤프 재시도 */
}
유저스페이스 라이브러리 비교
Netlink 통신을 위한 유저스페이스 라이브러리는 복잡도와 추상화 수준에 따라 선택할 수 있습니다. 각 라이브러리의 특성과 적합한 사용 시나리오를 비교합니다.
| 라이브러리 | 언어 | 크기 | 장점 | 단점 | 적합한 사용 |
|---|---|---|---|---|---|
| Raw Socket | C | 0 (표준) | 의존성 없음, 완전한 제어, 최소 오버헤드 | 모든 파싱/정렬/에러 처리를 직접 구현 | 임베디드, 단순 모니터링, BusyBox |
| libmnl | C | ~4K LOC | 경량, 콜백(Callback) 기반 파서, 학습 용이 | 고수준 추상화 없음 (cache, 자동 매칭 없음) | nftables, conntrack-tools, ethtool |
| libnl-3 | C | ~60K LOC | 객체 캐시(Cache), 자동 덤프/매칭, route/genl/nf 모듈 | API 복잡, 메모리 사용 높음, 버전 호환 주의 | NetworkManager, hostapd, wpa_supplicant |
| pyroute2 | Python | ~30K LOC | 고수준 Python API, IPRoute/NetNS, 프로토타이핑 용이 | 성능 (Python 오버헤드), 비표준 에러 처리 | 자동화 스크립트, 테스트, 네트워크 오케스트레이션 |
libmnl 사용 예시
/* libmnl: 최소 헬퍼를 사용한 인터페이스 목록 조회
* 컴파일: gcc -o mnl_demo mnl_demo.c -lmnl */
#include <libmnl/libmnl.h>
#include <linux/rtnetlink.h>
static int data_attr_cb(const struct nlattr *attr, void *data)
{
const struct nlattr **tb = data;
int type = mnl_attr_get_type(attr);
if (mnl_attr_type_valid(attr, IFLA_MAX) < 0)
return MNL_CB_OK;
tb[type] = attr;
return MNL_CB_OK;
}
static int data_cb(const struct nlmsghdr *nlh, void *data)
{
struct nlattr *tb[IFLA_MAX + 1] = {};
struct ifinfomsg *ifm = mnl_nlmsg_get_payload(nlh);
mnl_attr_parse(nlh, sizeof(*ifm), data_attr_cb, tb);
if (tb[IFLA_IFNAME])
printf("idx=%d name=%s\n", ifm->ifi_index,
mnl_attr_get_str(tb[IFLA_IFNAME]));
return MNL_CB_OK;
}
int main(void)
{
struct mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE);
mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID);
char buf[MNL_SOCKET_BUFFER_SIZE];
struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
nlh->nlmsg_type = RTM_GETLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
nlh->nlmsg_seq = time(NULL);
struct ifinfomsg *ifm = mnl_nlmsg_put_extra_header(nlh, sizeof(*ifm));
ifm->ifi_family = AF_UNSPEC;
mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);
int ret;
while ((ret = mnl_socket_recvfrom(nl, buf, sizeof(buf))) > 0) {
ret = mnl_cb_run(buf, ret, nlh->nlmsg_seq,
mnl_socket_get_portid(nl), data_cb, NULL);
if (ret <= MNL_CB_STOP)
break;
}
mnl_socket_close(nl);
return 0;
}
pyroute2 사용 예시
# pyroute2: Python 고수준 API 활용
from pyroute2 import IPRoute, NetNS
# 인터페이스 목록 조회
with IPRoute() as ipr:
for link in ipr.get_links():
attrs = dict(link['attrs'])
print(f" {link['index']}: {attrs.get('IFLA_IFNAME')} "
f"mtu={attrs.get('IFLA_MTU')} "
f"state={attrs.get('IFLA_OPERSTATE')}")
# veth 쌍 생성
with IPRoute() as ipr:
ipr.link('add', ifname='veth0', kind='veth',
peer={'ifname': 'veth1'})
# 라우팅 이벤트 모니터링 (ip monitor 등가)
from pyroute2 import IPRoute
ipr = IPRoute()
ipr.bind(groups=0xFFFFFF) # 모든 그룹 구독
while True:
msgs = ipr.get()
for msg in msgs:
print(f" event={msg['event']} "
f"attrs={dict(msg.get('attrs', []))}")
# 네트워크 네임스페이스에서 작업
with NetNS('test_ns') as ns:
ns.link('set', index=ns.link_lookup(ifname='lo')[0], state='up')
ns.addr('add', index=ns.link_lookup(ifname='lo')[0],
address='10.0.0.1', prefixlen=32)
Netlink 보안
Netlink 보안의 세 가지 핵심 축인 NETLINK_CAP_ACK, LSM 훅, 그리고 네임스페이스 격리의 상세 메커니즘을 분석합니다.
NETLINK_CAP_ACK과 정보 노출 방지
/* NETLINK_CAP_ACK: ACK 메시지에서 원본 요청 페이로드 제거
*
* 기본 동작: NLMSG_ERROR 응답에 원본 요청 헤더 + 페이로드 포함
* → 정보 노출 위험 + 불필요한 대역폭 소비
*
* CAP_ACK 활성화: 원본 요청의 nlmsghdr만 포함 (16바이트)
* → 오버헤드 감소, 보안 개선
*/
int one = 1;
setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &one, sizeof(one));
/* 효과:
* Before: nlmsgerr { error, nlmsghdr + 원본 페이로드 전체 }
* After: nlmsgerr { error, nlmsghdr만 (16B) }
*
* 대량 배치 작업 시 ACK 크기 대폭 감소
* iproute2는 기본적으로 CAP_ACK 활성화
*/
LSM (Linux Security Module) 훅
SELinux, AppArmor 등의 LSM은 Netlink 소켓 생성과 메시지 전송 시 보안 정책을 적용합니다.
/* 커널: Netlink 관련 LSM 훅 */
/* 1. 소켓 생성 시 */
/* security_socket_create(family=AF_NETLINK, type, protocol, kern) */
/* 2. 메시지 송신 시 */
/* security_netlink_send(sk, skb) */
/* → SELinux: selinux_netlink_send()
* - nlmsg_type에 따라 권한 검사
* - RTM_NEWROUTE → NETLINK_ROUTE_SOCKET__NLMSG_WRITE
* - RTM_GETROUTE → NETLINK_ROUTE_SOCKET__NLMSG_READ
*/
/* SELinux 정책 예시:
* allow NetworkManager_t self:netlink_route_socket {
* create bind getattr read write nlmsg_read nlmsg_write
* };
*
* audit 로그:
* avc: denied { nlmsg_write } for pid=1234 comm="myapp"
* scontext=user_t tcontext=user_t tclass=netlink_route_socket
*/
/* 3. bind 시 multicast 그룹 접근 제어 */
/* NL_CFG_F_NONROOT_RECV: 비 root 프로세스도 multicast 수신 허용
* NL_CFG_F_NONROOT_SEND: 비 root 프로세스도 커널에 전송 허용
*
* rtnetlink: NL_CFG_F_NONROOT_RECV 설정됨
* → 일반 사용자도 ip monitor 가능
* → 하지만 ip link set 같은 변경은 CAP_NET_ADMIN 필요
*/
네임스페이스 격리 상세
# 네트워크 네임스페이스와 Netlink 격리 확인
# 호스트에서 Netlink 소켓 확인
cat /proc/net/netlink | head -5
# sk Eth Pid Groups ...
# 네임스페이스 생성 후 격리 확인
ip netns add test_ns
ip netns exec test_ns cat /proc/net/netlink
# → 독립적인 Netlink 소켓 목록 (호스트와 완전히 분리)
# 네임스페이스 간 Netlink 이벤트 격리 확인
# 터미널 1: 호스트에서 모니터링
ip monitor link
# 터미널 2: 네임스페이스에서 인터페이스 변경
ip netns exec test_ns ip link set lo up
# → 터미널 1에 이벤트 표시 안 됨 (격리 확인)
# NETLINK_LISTEN_ALL_NSID: 다른 네임스페이스 이벤트 수신
# (CAP_NET_ADMIN + setsockopt 필요)
# ip monitor link nsid-all
ip netns del test_ns
성능 최적화
대규모 Netlink 메시지 처리를 위한 고급 최적화 기법을 다룹니다. recvmmsg(), 배치 메시지, 페이지 크기 버퍼, strict checking 등을 활용하면 Netlink 통신 성능을 크게 개선할 수 있습니다.
recvmmsg() 배치 수신
/* recvmmsg(): 단일 시스콜로 여러 메시지 수신
* 시스콜 오버헤드를 줄여 대량 이벤트 수신 성능 개선 */
#define VLEN 32
struct mmsghdr msgs[VLEN];
struct iovec iovecs[VLEN];
char bufs[VLEN][16384];
for (int i = 0; i < VLEN; i++) {
iovecs[i].iov_base = bufs[i];
iovecs[i].iov_len = sizeof(bufs[i]);
msgs[i].msg_hdr.msg_iov = &iovecs[i];
msgs[i].msg_hdr.msg_iovlen = 1;
}
/* 최대 VLEN개 메시지를 한 번에 수신 */
int nmsgs = recvmmsg(fd, msgs, VLEN, 0, NULL);
for (int i = 0; i < nmsgs; i++) {
int len = msgs[i].msg_len;
struct nlmsghdr *nlh = (struct nlmsghdr *)bufs[i];
for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
/* 메시지 처리 */
}
}
페이지 크기 버퍼와 MSG_TRUNC 감지
/* 최적 수신 버퍼 크기 결정 */
/* 1. 시스템 페이지 크기 활용 (TLB 효율) */
long page_size = sysconf(_SC_PAGESIZE); /* 보통 4096 */
int buf_size = page_size * 4; /* 16KB — 대부분의 덤프 응답 수용 */
/* 2. MSG_TRUNC 감지로 잘린 메시지 처리 */
struct msghdr msg = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
int len = recvmsg(fd, &msg, 0);
if (msg.msg_flags & MSG_TRUNC) {
/* 메시지가 버퍼보다 큼 → 버퍼 확대 후 재시도 */
fprintf(stderr, "Message truncated, need larger buffer\n");
/* MSG_PEEK | MSG_TRUNC로 필요한 크기 확인 가능 */
len = recvmsg(fd, &msg, MSG_PEEK | MSG_TRUNC);
/* len이 실제 메시지 크기 */
}
/* 3. Netlink 소켓용 권장 버퍼 크기 */
#define NL_RECV_BUF (32768) /* 일반 요청/응답 */
#define NL_DUMP_BUF (65536) /* 대량 덤프 수신 */
#define NL_MONITOR_BUF (131072) /* 이벤트 모니터링 (폭주 대비) */
배치 메시지 전송
/* 여러 Netlink 메시지를 단일 sendmsg()로 전송
* RTNL 락 획득 횟수 최소화 → 대규모 설정 시 필수 */
char buf[65536];
int offset = 0;
/* 메시지 1: 인터페이스 UP */
struct nlmsghdr *nlh1 = (struct nlmsghdr *)(buf + offset);
nlh1->nlmsg_type = RTM_NEWLINK;
nlh1->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh1->nlmsg_seq = seq++;
/* ... ifinfomsg + IFLA_* 속성 채움 ... */
nlh1->nlmsg_len = NLMSG_ALIGN(nlh1->nlmsg_len);
offset += nlh1->nlmsg_len;
/* 메시지 2: IP 주소 추가 */
struct nlmsghdr *nlh2 = (struct nlmsghdr *)(buf + offset);
nlh2->nlmsg_type = RTM_NEWADDR;
nlh2->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK;
nlh2->nlmsg_seq = seq++;
/* ... ifaddrmsg + IFA_* 속성 채움 ... */
nlh2->nlmsg_len = NLMSG_ALIGN(nlh2->nlmsg_len);
offset += nlh2->nlmsg_len;
/* 단일 sendmsg()로 전송 — 커널이 순차 처리 */
send(fd, buf, offset, 0);
/* 각 메시지에 대한 ACK를 순서대로 수신 */
for (int i = 0; i < 2; i++) {
char reply[4096];
recv(fd, reply, sizeof(reply), 0);
/* NLMSG_ERROR 파싱 → error 확인 */
}
ethtool Netlink
전통적으로 ioctl(SIOCETHTOOL)에 의존하던 ethtool 인터페이스는 커널 5.6부터 Generic Netlink 기반으로 전환되었습니다. 이 전환은 Netlink의 확장성과 이벤트 모델을 활용하여, ethtool의 오랜 한계를 극복합니다.
ethtool Netlink 명령과 사용
# ethtool Netlink (커널 5.6+) 확인
genl ctrl list name ethtool
# Name: ethtool
# ID: 0x15 Version: 1
# mcast group: monitor ID: 0x07
# ethtool은 Netlink 사용 가능 시 자동 전환
# --debug=0x08 플래그로 ioctl/Netlink 경로 확인
ethtool --debug 0x08 eth0 2>&1 | head
# netlink: using genetlink
# ethtool Netlink 이벤트 모니터링
ethtool --monitor eth0
# (다른 터미널에서 ethtool -s eth0 speed 100)
# → 설정 변경 이벤트 실시간 수신
# JSON 출력 (Netlink 경로만 지원)
ethtool --json -S eth0
/* 커널: ethtool Netlink 핸들러 등록 구조
* net/ethtool/netlink.c */
static const struct genl_ops ethtool_genl_ops[] = {
{
.cmd = ETHTOOL_MSG_STRSET_GET,
.doit = ethnl_default_doit,
.start = ethnl_default_start,
.dumpit = ethnl_default_dumpit,
.done = ethnl_default_done,
.policy = ethnl_strset_get_policy,
},
{
.cmd = ETHTOOL_MSG_LINKINFO_GET,
.doit = ethnl_default_doit,
.dumpit = ethnl_default_dumpit,
.policy = ethnl_linkinfo_get_policy,
},
{
.cmd = ETHTOOL_MSG_LINKINFO_SET,
.doit = ethnl_default_set_doit,
.flags = GENL_UNS_ADMIN_PERM,
.policy = ethnl_linkinfo_set_policy,
},
/* ... LINKMODES, LINKSTATE, DEBUG, WOL, FEATURES,
* PRIVFLAGS, RINGS, CHANNELS, COALESCE, PAUSE,
* EEE, TSINFO, CABLE_TEST, TUNNEL_INFO, FEC,
* MODULE_EEPROM, STATS, PHC_VCLOCKS, ... */
};
/* ethtool 멀티캐스트 그룹: 설정 변경 알림 */
static const struct genl_multicast_group ethtool_nl_mcgrps[] = {
[ETHNL_MCGRP_MONITOR] = { .name = "monitor" },
};
Netlink 에러 처리
확장 ACK(NETLINK_EXT_ACK)의 내부 구조, 에러 오프셋, 정책 위반 보고를 상세히 다룹니다.
extack 내부 구조
/* include/linux/netlink.h — 확장 에러 보고 구조체 */
struct netlink_ext_ack {
const char *_msg; /* 에러 메시지 문자열 */
const struct nlattr *bad_attr; /* 문제 속성 포인터 */
const struct nla_policy *policy; /* 위반된 정책 */
const struct nlattr *miss_nest; /* 누락 속성의 부모 */
u16 miss_type; /* 누락된 속성 타입 */
u8 cookie[NETLINK_MAX_COOKIE_LEN];
u8 cookie_len;
};
/* NLMSG_ERROR 응답에 포함되는 extack 속성들 */
enum nlmsgerr_attrs {
NLMSGERR_ATTR_UNUSED,
NLMSGERR_ATTR_MSG, /* NLA_STRING: 에러 메시지 */
NLMSGERR_ATTR_OFFS, /* NLA_U32: 문제 속성의 오프셋 (바이트) */
NLMSGERR_ATTR_COOKIE, /* NLA_BINARY: 커널 전달 쿠키 */
NLMSGERR_ATTR_POLICY, /* NLA_NESTED: 위반된 정책 정보 */
NLMSGERR_ATTR_MISS_TYPE, /* NLA_U32: 누락 속성 타입 (5.12+) */
NLMSGERR_ATTR_MISS_NEST, /* NLA_U32: 누락 속성 부모 오프셋 (5.12+) */
};
/* 커널: extack 매크로 사용 패턴 */
/* 일반 에러 메시지 */
NL_SET_ERR_MSG(extack, "MTU must be between 68 and 65535");
/* 특정 속성에 대한 에러 (오프셋 자동 계산) */
NL_SET_ERR_MSG_ATTR(extack, tb[MY_ATTR],
"Invalid value for MY_ATTR");
/* printf 스타일 포맷 (5.x+) */
NL_SET_ERR_MSG_FMT(extack,
"Invalid MTU %u (must be %u-%u)", mtu, min_mtu, max_mtu);
/* 누락 속성 보고 (5.12+) */
NL_SET_ERR_MSG_ATTR(extack, tb[PARENT_ATTR],
"Required attribute CHILD_ATTR is missing");
extack->miss_nest = tb[PARENT_ATTR];
extack->miss_type = CHILD_ATTR;
유저스페이스 extack 파싱
/* 유저스페이스: NLMSG_ERROR + extack 파싱 */
static void parse_nlmsg_error(struct nlmsghdr *nlh)
{
struct nlmsgerr *err = NLMSG_DATA(nlh);
int hlen = sizeof(*err);
/* NETLINK_CAP_ACK 시 원본 헤더만 포함 */
if (!(nlh->nlmsg_flags & NLM_F_ACK_TLVS))
hlen += err->msg.nlmsg_len - sizeof(struct nlmsghdr);
printf("Error: %d (%s)\n", err->error,
strerror(-err->error));
/* extack 속성 파싱 */
if (nlh->nlmsg_len > NLMSG_LENGTH(hlen)) {
struct nlattr *attr;
int alen = nlh->nlmsg_len - NLMSG_ALIGN(NLMSG_LENGTH(hlen));
for (attr = (struct nlattr *)((char *)err + NLMSG_ALIGN(hlen));
NLA_OK(attr, alen);
attr = NLA_NEXT(attr, alen))
{
switch (nla_type(attr)) {
case NLMSGERR_ATTR_MSG:
printf(" extack msg: %s\n",
(char *)nla_data(attr));
break;
case NLMSGERR_ATTR_OFFS:
printf(" bad attr offset: %u\n",
*(__u32 *)nla_data(attr));
break;
}
}
}
}
Netlink 디버깅
nlmon 인터페이스와 Wireshark를 활용한 Netlink 패킷 분석, ftrace 이벤트, 그리고 실전 장애 진단 절차를 다룹니다.
Wireshark Netlink 분석
# 1. nlmon 인터페이스로 Netlink 트래픽 캡처
modprobe nlmon
ip link add nlmon0 type nlmon
ip link set nlmon0 up
# 2. tcpdump로 pcap 파일 생성
tcpdump -i nlmon0 -w /tmp/netlink-trace.pcap &
TCPDUMP_PID=$!
# 3. 트래픽 발생 (예: 인터페이스 조회)
ip link show
ip route show
ip addr add 10.99.0.1/24 dev lo
ip addr del 10.99.0.1/24 dev lo
# 4. 캡처 종료
kill $TCPDUMP_PID
ip link del nlmon0
# 5. Wireshark에서 분석 (tshark CLI 예시)
tshark -r /tmp/netlink-trace.pcap -V -T text | head -80
# Netlink route (rtnetlink) 프로토콜 디코딩:
# Message type: Get Link (18)
# Flags: 0x0301, Request, Root, Match
# Sequence number: 1234
# Port ID: 5678
# Address family: AF_UNSPEC (0)
# Interface index: 0
# Attributes:
# IFLA_IFNAME: eth0
# IFLA_MTU: 1500
# ...
# Wireshark 필터 예시
# netlink-route — rtnetlink 메시지만
# genl — Generic Netlink만
# netlink.type == 2 — NLMSG_ERROR만
# netlink.nlmsg_flags.dump == 1 — 덤프 요청만
ftrace Netlink 트레이스포인트
# Netlink 관련 ftrace 트레이스포인트 확인
ls /sys/kernel/debug/tracing/events/netlink/
# netlink_extack/
# netlink_extack 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/netlink/netlink_extack/enable
cat /sys/kernel/debug/tracing/trace_pipe &
# 잘못된 명령으로 extack 트리거
ip link set nonexistent up 2>/dev/null
# trace: netlink_extack: msg="Cannot find device \"nonexistent\""
# 정리
echo 0 > /sys/kernel/debug/tracing/events/netlink/netlink_extack/enable
# kprobe로 상세 추적 (bpftrace)
bpftrace -e '
kprobe:netlink_ack {
$nlh = (struct nlmsghdr *)arg1;
$err = arg2;
printf("netlink_ack: type=%d seq=%d err=%d pid=%d\n",
$nlh->nlmsg_type, $nlh->nlmsg_seq, $err, pid);
}
'
장애 진단 체크리스트
| 증상 | 원인 | 진단 명령 | 해결 |
|---|---|---|---|
ENOBUFS 반복 |
수신 버퍼 오버플로(Buffer Overflow) | cat /proc/net/netlink (Drops 컬럼) |
rmem_max 증가, NETLINK_NO_ENOBUFS |
EBUSY / RTNL 지연 |
RTNL 락 경합 | cat /proc/locks | grep rtnl |
ip -batch, 커널 업그레이드 |
EADDRINUSE |
portid 충돌 | cat /proc/net/netlink (Pid 컬럼) |
nl_pid=0 (자동 할당) |
EINVAL + extack |
속성 검증 실패 | ip -d 또는 NETLINK_EXT_ACK 활성화 |
extack 메시지로 원인 파악 |
| NLM_F_DUMP_INTR | 덤프 중 테이블 변경 | strace로 응답 플래그 확인 | 덤프 재시도 로직 구현 |
| 메시지 잘림 (MSG_TRUNC) | 수신 버퍼 크기 부족 | strace recvmsg 플래그 확인 | 버퍼 크기 32KB 이상으로 확대 |
| 응답 없음 / 타임아웃 | seq 불일치 또는 소켓 닫힘 | nlmon + Wireshark로 양방향 확인 | seq 매칭, 소켓 상태 확인 |
커널-유저스페이스 메시지 왕복
Netlink 메시지의 전체 라이프사이클을 유저스페이스 sendmsg()부터 커널 처리, 응답 생성, recvmsg()까지 추적합니다.
참고 사항
- 커널 소스:
net/netlink/af_netlink.c(핵심),net/core/rtnetlink.c(rtnetlink),net/netlink/genetlink.c(Generic Netlink) - 헤더 파일:
include/uapi/linux/netlink.h,include/uapi/linux/rtnetlink.h,include/uapi/linux/genetlink.h,include/net/netlink.h(커널 API) - RFC 3549: Linux Netlink as an IP Services Protocol
- libnl 문서: libnl-suite (libnl-3, libnl-route-3, libnl-genl-3)
- 관련 페이지: 네트워크 스택 · sk_buff 자료구조 · Netfilter · 라우팅 · 네임스페이스
- 커널 모듈 기초: 커널 모듈 페이지 참조
- 커널 공식 문서: Netlink 사용자 공간 API
- 커널 공식 문서: Generic Netlink
- man 페이지: netlink(7) — Netlink 소켓 프로토콜 설명
- man 페이지: rtnetlink(7) — 라우팅 Netlink 메시지 형식
- man 페이지: libnetlink(3) — Netlink 유틸리티 라이브러리
- RFC 3549: Linux Netlink as an IP Services Protocol
- LWN.net: Generic Netlink howto (2006)
- 커널 소스: net/netlink 디렉터리
관련 문서
Netlink와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.