Netlink 심화
Linux 커널의 Netlink 소켓은 커널과 유저스페이스 간 양방향 IPC 메커니즘으로, 네트워크 구성(iproute2), 감사(audit), 디바이스 이벤트(udev) 등 현대 Linux 시스템의 핵심 통신 채널입니다. AF_NETLINK 소켓 프로토콜 패밀리, 메시지 구조(nlmsghdr/nlattr), rtnetlink, Generic Netlink, 커널/유저스페이스 API, multicast 이벤트, 에러 처리, 보안, 디버깅까지 종합적으로 분석합니다.
Netlink 개요
Netlink은 Linux 커널이 제공하는 소켓 기반 IPC 메커니즘으로, 커널 서브시스템과 유저스페이스 프로세스 간의 양방향 통신을 담당합니다. 전통적인 ioctl() 인터페이스의 한계를 극복하기 위해 설계되었습니다.
ioctl()은 동기식이며 구조체 크기가 고정되어 확장이 어렵습니다. 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 통신 구조 개요
유저스페이스 커널
┌──────────────┐ ┌──────────────────┐
│ iproute2 │ │ rtnetlink │
│ NetworkMgr │◄────────────►│ (NETLINK_ROUTE) │
│ systemd │ AF_NETLINK │ │
│ libnl │ 소켓 │ genetlink │
│ 사용자 앱 │ │ (NETLINK_GENERIC)│
└──────────────┘ └──────────────────┘
│ │
│ sendmsg() / recvmsg() │ netlink_unicast()
│ bind() / multicast group │ netlink_broadcast()
└──────────────────────────────┘
Netlink 프로토콜 패밀리
Netlink 소켓 생성 시 socket(AF_NETLINK, SOCK_RAW|SOCK_DGRAM, protocol)의 세 번째 인자로 프로토콜 패밀리를 지정합니다. 각 패밀리는 특정 커널 서브시스템과 통신합니다.
| 프로토콜 상수 | 값 | 용도 | 주요 사용자 |
|---|---|---|---|
NETLINK_ROUTE | 0 | 라우팅, 링크, 주소, 이웃 관리 | iproute2, NetworkManager |
NETLINK_USERSOCK | 2 | 유저스페이스 간 netlink 통신 예약 | 사용자 정의 |
NETLINK_FIREWALL | 3 | (폐기됨) 방화벽 패킷 큐 | - |
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 | 디바이스 핫플러그 이벤트 | udev, systemd |
NETLINK_GENERIC | 16 | 범용 Netlink (다중 패밀리) | nl80211, taskstats 등 |
NETLINK_CRYPTO | 21 | Crypto API 설정 | crconf |
NETLINK_GENERIC(Generic Netlink)이 도입되어, 하나의 프로토콜 번호 위에 동적으로 패밀리를 등록할 수 있게 되었습니다.
메시지 구조
모든 Netlink 메시지는 nlmsghdr 헤더로 시작하며, 그 뒤에 프로토콜별 페이로드와 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_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) /* 네트워크 바이트 순서 */
메시지 레이아웃과 정렬
Netlink 메시지 레이아웃 (4바이트 정렬)
┌─────────────────────────────────────────────────┐
│ nlmsghdr (16 bytes) │
│ nlmsg_len | nlmsg_type | nlmsg_flags │
│ nlmsg_seq | nlmsg_pid │
├─────────────────────────────────────────────────┤
│ 프로토콜별 헤더 (예: ifinfomsg, rtmsg 등) │
│ + NLMSG_ALIGN 패딩 │
├─────────────────────────────────────────────────┤
│ nlattr #1 (nla_len | nla_type | payload + pad) │
├─────────────────────────────────────────────────┤
│ nlattr #2 (nla_len | nla_type | payload + pad) │
├─────────────────────────────────────────────────┤
│ nlattr #3 (nested) │
│ ├─ nlattr #3.1 (nla_len | nla_type | payload) │
│ └─ nlattr #3.2 (nla_len | nla_type | payload) │
├─────────────────────────────────────────────────┤
│ ... │
└─────────────────────────────────────────────────┘
정렬 매크로:
NLMSG_ALIGN(len) → 4바이트 정렬
NLA_ALIGN(len) → 4바이트 정렬
NLMSG_HDRLEN → NLMSG_ALIGN(sizeof(struct nlmsghdr)) = 16
NLMSG_LENGTH(len) → NLMSG_HDRLEN + (len)
NLMSG_SPACE(len) → NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 정렬 매크로 — 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 주소, 라우팅 테이블, 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 속성들 │
* └───────────────────┘
*/
커널 모듈에서 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");
커널 측 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_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);
}
유저스페이스 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 그룹 구독 | 실시간 이벤트 수신 |
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") 매크로로 상세 에러 메시지를 설정할 수 있습니다. 이는 유저스페이스 도구의 디버깅을 크게 개선합니다.
/* 커널 측: 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 소켓은 커널 인터페이스에 직접 접근하므로 보안이 중요합니다. 커널은 여러 계층의 접근 제어를 적용합니다.
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)
*/
네트워크 네임스페이스 격리
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 패밀리로 등록된 모든 패밀리 조회
"
참고 사항
- 커널 소스:
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 · 라우팅 · 네임스페이스
- 커널 모듈 기초: 커널 모듈 페이지 참조