Netlink 심화

Linux 커널의 Netlink 소켓은 커널과 유저스페이스 간 양방향 IPC 메커니즘으로, 네트워크 구성(iproute2), 감사(audit), 디바이스 이벤트(udev) 등 현대 Linux 시스템의 핵심 통신 채널입니다. AF_NETLINK 소켓 프로토콜 패밀리, 메시지 구조(nlmsghdr/nlattr), rtnetlink, Generic Netlink, 커널/유저스페이스 API, multicast 이벤트, 에러 처리, 보안, 디버깅까지 종합적으로 분석합니다.

관련 표준 및 참고: RFC 3549 (Linux Netlink as an IP Services Protocol) — Netlink은 Linux 고유의 프로토콜이지만, IETF에서 RFC로 문서화되었습니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

Netlink 개요

Netlink은 Linux 커널이 제공하는 소켓 기반 IPC 메커니즘으로, 커널 서브시스템과 유저스페이스 프로세스 간의 양방향 통신을 담당합니다. 전통적인 ioctl() 인터페이스의 한계를 극복하기 위해 설계되었습니다.

왜 ioctl 대신 Netlink인가?

ioctl()은 동기식이며 구조체 크기가 고정되어 확장이 어렵습니다. Netlink은 비동기 전달, 가변 길이 메시지, multicast 지원, 명확한 에러 보고(extack) 등을 제공하여 현대 커널 인터페이스의 표준으로 자리잡았습니다.

Netlink의 핵심 특성

특성ioctlNetlink
통신 방향유저 → 커널 (요청-응답)양방향 + 커널 → 유저 비동기 알림
메시지 크기고정 구조체가변 길이 (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_ROUTE0라우팅, 링크, 주소, 이웃 관리iproute2, NetworkManager
NETLINK_USERSOCK2유저스페이스 간 netlink 통신 예약사용자 정의
NETLINK_FIREWALL3(폐기됨) 방화벽 패킷 큐-
NETLINK_SOCK_DIAG4소켓 진단 (ss 명령)iproute2 ss
NETLINK_NFLOG5Netfilter 로그ulogd
NETLINK_XFRM6IPsec 정책/SA 관리strongSwan, libreswan
NETLINK_SELINUX7SELinux 이벤트 알림SELinux 데몬
NETLINK_ISCSI8iSCSI 전송 이벤트open-iscsi
NETLINK_AUDIT9감사 시스템auditd
NETLINK_FIB_LOOKUP10FIB 룩업 요청ip rule
NETLINK_CONNECTOR11커넥터 (프로세스 이벤트 등)proc connector
NETLINK_NETFILTER12Netfilter 서브시스템nftables, conntrack
NETLINK_KOBJECT_UEVENT15디바이스 핫플러그 이벤트udev, systemd
NETLINK_GENERIC16범용 Netlink (다중 패밀리)nl80211, taskstats 등
NETLINK_CRYPTO21Crypto API 설정crconf
프로토콜 번호 고갈 문제: 초기 Netlink은 서브시스템마다 고유 프로토콜 번호를 할당했으나, 번호가 부족해졌습니다. 이 문제를 해결하기 위해 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_REQUEST0x01요청 메시지 (유저 → 커널)
NLM_F_MULTI0x02다중 파트 메시지 (NLMSG_DONE으로 종료)
NLM_F_ACK0x04ACK 응답 요청
NLM_F_ECHO0x08요청 메시지를 에코백
NLM_F_DUMP0x300전체 테이블 덤프 요청 (ROOT|MATCH)
NLM_F_ROOT0x100루트부터 전체 반환
NLM_F_MATCH0x200조건에 맞는 항목 반환
NLM_F_CREATE0x400객체 생성 (없으면 생성)
NLM_F_EXCL0x200이미 존재하면 에러
NLM_F_REPLACE0x100기존 객체 교체
NLM_F_APPEND0x800끝에 추가

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_ROUTE)은 가장 널리 사용되는 Netlink 프로토콜 패밀리로, 네트워크 인터페이스, IP 주소, 라우팅 테이블, ARP/이웃 테이블, 트래픽 제어(tc) 등을 관리합니다. iproute2 도구 모음(ip, tc, bridge 등)이 rtnetlink의 주요 유저스페이스 클라이언트입니다.

RTM_* 메시지 타입

메시지 그룹NEWDELGET프로토콜 헤더
링크RTM_NEWLINKRTM_DELLINKRTM_GETLINKstruct ifinfomsg
주소RTM_NEWADDRRTM_DELADDRRTM_GETADDRstruct ifaddrmsg
라우트RTM_NEWROUTERTM_DELROUTERTM_GETROUTEstruct rtmsg
이웃RTM_NEWNEIGHRTM_DELNEIGHRTM_GETNEIGHstruct ndmsg
규칙RTM_NEWRULERTM_DELRULERTM_GETRULEstruct fib_rule_hdr
qdiscRTM_NEWQDISCRTM_DELQDISCRTM_GETQDISCstruct tcmsg
/* 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은 NETLINK_GENERIC 프로토콜 위에 다중 패밀리를 동적으로 등록하는 프레임워크입니다. 새 커널 서브시스템이 Netlink 인터페이스를 추가할 때 프로토콜 번호를 소비하지 않으며, 자동으로 패밀리 ID를 할당받습니다.

Generic Netlink 사용 예: nl80211 (무선 LAN 설정), taskstats (프로세스 통계), devlink (네트워크 디바이스 관리), thermal (열 관리), OVS (Open vSwitch), WireGuard 등 수십 개의 커널 서브시스템이 Generic Netlink을 사용합니다.

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를 살펴봅니다.

/* 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 그룹에 가입하여, 해당 그룹의 이벤트를 수신합니다.

그룹상수이벤트 내용
LINKRTNLGRP_LINK인터페이스 상태 변경 (UP/DOWN, MTU 변경 등)
IPv4 주소RTNLGRP_IPV4_IFADDRIPv4 주소 추가/삭제
IPv6 주소RTNLGRP_IPV6_IFADDRIPv6 주소 추가/삭제
IPv4 라우트RTNLGRP_IPV4_ROUTEIPv4 라우팅 테이블 변경
IPv6 라우트RTNLGRP_IPV6_ROUTEIPv6 라우팅 테이블 변경
이웃RTNLGRP_NEIGHARP/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 showRTM_GETLINK + NLM_F_DUMP모든 인터페이스 조회
ip link set eth0 upRTM_NEWLINK (IFF_UP 플래그)인터페이스 활성화
ip addr add 10.0.0.1/24 dev eth0RTM_NEWADDRIP 주소 추가
ip route add 192.168.0.0/24 via 10.0.0.1RTM_NEWROUTE라우트 추가
ip neigh showRTM_GETNEIGH + NLM_F_DUMPARP/NDP 테이블 조회
ip monitormulticast 그룹 구독실시간 이벤트 수신
ss -tSOCK_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.
extack 활용: 커널 모듈에서 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: 각 네트워크 네임스페이스는 독립적인 Netlink 소켓 공간을 가집니다. 한 네임스페이스의 프로세스는 다른 네임스페이스의 Netlink 메시지를 수신할 수 없습니다. 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 · 라우팅 · 네임스페이스
  • 커널 모듈 기초: 커널 모듈 페이지 참조