네트워크 드라이버 구현 가이드 (net_device)

구조·콜백(Callback) 레퍼런스에서 다루는 net_device/net_device_ops 계약을 바탕으로, 드라이버를 실제로 구현할 때 필요한 실무 절차를 정리합니다. 최소 골격부터 ethtool 통계, 물리 링크 연동(PHY/phylink), RTNL 제어 경로, 동시성(Concurrency) 모델, 보안 하드닝(Hardening), 검증·리뷰 체크리스트까지 다룹니다.

전제 조건: 네트워크 디바이스 드라이버 (net_device) 문서를 먼저 읽으세요. 구현 실습은 구조·콜백 계약을 알고 있다는 전제에서 출발하므로, 할당/등록/해제와 net_device_ops 진입점(Entry Point)을 먼저 잡아야 합니다.
일상 비유: 이 주제는 점포 개업 절차와 비슷합니다. 개업 전 사전 준비(할당/등록), 영업 중 안내 데스크(ethtool/통계), 물리 시설 계약(PHY/phylink)을 각각 확정해야 안정적으로 운영됩니다.

핵심 요약

  • 구현 골격alloc_netdev()register_netdev() 순서를 기준으로 계획합니다.
  • net_device_ops — open/stop/xmit 콜백 계약을 최소 기능부터 채웁니다.
  • ethtool/통계 — 운영자가 볼 수 있는 지표(Statistic)를 초기부터 연결합니다.
  • PHY/phylink — 링크 상태 변화를 제어 경로와 분리합니다.
  • 검증 체크리스트 — 릴리스 전 회귀(Regression) 검증 항목을 고정합니다.

단계별 이해

  1. 최소 골격 작성
    등록/해제만 동작하는 드라이버를 먼저 만듭니다.
  2. 데이터 경로 연결
    ndo_start_xmit()와 TX 완료 처리 흐름을 확정합니다.
  3. 운영 인터페이스 추가
    ethtool, 통계, 링크 상태를 차례로 붙입니다.
  4. 동시성·보안 강화
    RTNL과 잠금(Lock) 모델, 입력 검증을 적용합니다.
  5. 검증 매트릭스 통과
    체크리스트 기반 회귀 테스트를 통과시킵니다.
대상 독자: 커널 네트워크 드라이버를 처음부터 구현하거나 유지보수하는 3년차 이상 개발자를 대상으로 합니다. 네트워크 디바이스 드라이버 (net_device)의 구조·콜백 설명을 먼저 읽을 것을 권장합니다.

구현 가이드: 최소 골격부터 확장까지

  1. 1단계: 최소 송수신 경로ndo_open/stop/start_xmit, 단일 NAPI queue, 기본 IRQ 동작
  2. 2단계: 안정성 확보 — 에러 경로 정리, queue stop/wake 일관성, teardown 순서 검증
  3. 3단계: 운영성 확보ethtool_ops, 통계, self-test, 링 파라미터 조정
  4. 4단계: 성능 확장 — 멀티큐 RSS, XDP/AF_XDP, page_pool, BQL, NUMA affinity
  5. 5단계: 가상 netdev 통합 — TUN/TAP, veth, virtio-net과 공통 코어 재사용 전략 수립
Now I have a good understanding of the style. Let me produce the four sections.

통계와 ethtool 연동

운영 환경에서는 “성능이 안 나옵니다”보다 “왜 안 나오는가”를 보여주는 통계가 더 중요합니다. ethtool -S로 확인 가능한 드라이버 통계를 설계하면 장애 분석 시간이 크게 줄어듭니다.

/* 개념 예시: ethtool 통계 구조와 per-CPU 집계 */
struct my_pcpu_stats {
    u64 rx_packets;
    u64 rx_bytes;
    u64 tx_packets;
    u64 tx_bytes;
    struct u64_stats_sync syncp;
};

static void my_ndo_get_stats64(struct net_device *ndev,
                               struct rtnl_link_stats64 *stats)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        struct my_pcpu_stats *pcpu = per_cpu_ptr(my_stats, cpu);
        u64 rx_pkts, rx_bytes, tx_pkts, tx_bytes;
        unsigned int start;

        do {
            start = u64_stats_fetch_begin(&pcpu->syncp);
            rx_pkts = pcpu->rx_packets;
            rx_bytes = pcpu->rx_bytes;
            tx_pkts = pcpu->tx_packets;
            tx_bytes = pcpu->tx_bytes;
        } while (u64_stats_fetch_retry(&pcpu->syncp, start));

        stats->rx_packets += rx_pkts;
        stats->rx_bytes += rx_bytes;
        stats->tx_packets += tx_pkts;
        stats->tx_bytes += tx_bytes;
    }
}
진단 명령확인 포인트
ethtool -i eth0드라이버/펌웨어(Firmware) 버전
ethtool -k eth0TSO/GRO/checksum offload 상태
ethtool -S eth0링 드롭, 에러, 큐별 카운터
ethtool -l eth0채널(RX/TX queue) 구성
ip -s link show dev eth0커널 링크 통계의 상위 뷰

현대 NIC/MAC 드라이버는 PHY 연결을 phylink로 통합하는 추세입니다. SFP, fixed-link, in-band status를 동시에 다뤄야 하는 경우 phylink가 사실상 표준입니다.

/* 개념 예시: phylink 초기화와 플랫폼별 연결 분기 */
static const struct phylink_mac_ops my_phylink_ops = {
    .mac_config = my_mac_config,
    .mac_link_up = my_mac_link_up,
    .mac_link_down = my_mac_link_down,
};

static int my_phylink_init(struct my_priv *priv)
{
    struct phylink_config *cfg = &priv->phylink_config;
    struct fwnode_handle *fwnode = dev_fwnode(priv->dev);
    phy_interface_t iface = priv->phy_mode; /* DT/ACPI 설정에서 파생 */

    priv->phylink = phylink_create(cfg, fwnode, iface,
                                 &my_phylink_ops);
    if (IS_ERR(priv->phylink))
        return PTR_ERR(priv->phylink);

    /* 펌웨어 타입(OF/fwnode)에 맞는 connect 경로를 선택 */
    if (is_of_node(fwnode))
        return phylink_of_phy_connect(priv->phylink, to_of_node(fwnode), 0);
    return phylink_fwnode_phy_connect(priv->phylink, fwnode, 0);
}

phylib 상태 머신 내부 분석

커널의 PHY 추상화 계층(phylib)은 drivers/net/phy/phy.c에 구현된 상태 머신(State Machine)을 중심으로 동작합니다. phy_state_machine() 함수가 delayed_work로 주기적으로 호출되며, PHY의 링크 상태를 감시하고 MAC 드라이버에 변화를 통지합니다.

PHY 상태 머신은 다음 6개 상태를 순환합니다.

상태의미진입 조건
PHY_DOWN0PHY가 초기화되지 않았거나 중지된 상태드라이버 로드, phy_stop() 호출
PHY_READY1PHY가 초기화되었으나 아직 시작하지 않은 상태phy_init_hw() 완료
PHY_UP2PHY가 시작되었으나 링크가 아직 성립하지 않은 상태phy_start() 호출
PHY_RUNNING3링크가 활성화되어 데이터 전송이 가능한 상태Auto-Negotiation 완료, 링크 업
PHY_NOLINK4PHY가 동작 중이나 링크가 끊어진 상태케이블 분리, 원격 장애
PHY_HALTED5PHY가 명시적으로 정지된 상태phy_stop() 호출

커널 소스 drivers/net/phy/phy.cphy_state_machine() 핵심 로직을 분석하면 다음과 같습니다.

/* drivers/net/phy/phy.c - phy_state_machine() 핵심 흐름 분석 */
void phy_state_machine(struct work_struct *work)
{
    struct phy_device *phydev =
        container_of(to_delayed_work(work),
                     struct phy_device, state_queue);
    bool needs_aneg = false, do_suspend = false;
    enum phy_state old_state;
    int err = 0;

    mutex_lock(&phydev->lock);
    old_state = phydev->state;

    switch (phydev->state) {
    case PHY_DOWN:
    case PHY_READY:
        break; /* 대기 상태 — 외부 이벤트 대기 */
    case PHY_UP:
        needs_aneg = true; /* Auto-Negotiation 시작 */
        break;
    case PHY_NOLINK:
    case PHY_RUNNING:
        err = phy_check_link_status(phydev); /* MDIO 레지스터 읽기 */
        break;
    case PHY_HALTED:
        if (phydev->link) {
            phydev->link = 0;
            phy_link_down(phydev);
        }
        do_suspend = true;
        break;
    }

    mutex_unlock(&phydev->lock);

    if (needs_aneg)
        err = phy_start_aneg(phydev);

    /* 상태 변경이 있으면 콜백 호출 */
    if (old_state != phydev->state) {
        phydev_dbg(phydev, "PHY state change %s -> %s\n",
                   phy_state_to_str(old_state),
                   phy_state_to_str(phydev->state));
        if (phydev->drv && phydev->drv->link_change_notify)
            phydev->drv->link_change_notify(phydev);
    }

    /* 정지 상태가 아니면 작업 큐 재스케줄 */
    if (!do_suspend && phy_polling_mode(phydev))
        phy_queue_state_machine(phydev, PHY_STATE_TIME);
}

phy_check_link_status()는 MDIO 레지스터(Register)를 읽어 실제 링크 상태를 확인하고, phy_link_up() 또는 phy_link_down()을 호출하여 adjust_link 콜백을 트리거합니다. 상태 전이 흐름을 다이어그램으로 표현하면 다음과 같습니다.

PHY_DOWN PHY_READY PHY_UP PHY_RUNNING PHY_NOLINK PHY_HALTED phy_init_hw() phy_start() AN 완료, link up AN 완료, no link link down link up phy_stop() phy_stop() phy_start() 재시작 상태 전이 트리거 요약 • phy_init_hw(): PHY_DOWN → PHY_READY (H/W 초기화 완료) • phy_start(): PHY_READY/HALTED → PHY_UP (Auto-Negotiation 시작) • phy_check_link_status(): PHY_UP/NOLINK → PHY_RUNNING, 또는 RUNNING → NOLINK • phy_stop(): 모든 활성 상태 → PHY_HALTED (명시적 정지) • 상태 머신 주기: PHY_STATE_TIME (기본 1초) 간격으로 delayed_work 실행

phylinkphylink_mac_ops 구조체(Struct)를 통해 MAC 드라이버에 링크 설정 변경을 통지합니다. drivers/net/phy/phylink.cphylink_resolve()가 PHY/in-band 상태를 종합하여 MAC 콜백을 호출하는 중앙 분기점입니다.

콜백호출 시점드라이버 책임
mac_config링크 파라미터 변경 시speed, duplex, pause 프레임 등 MAC 하드웨어 레지스터 설정
mac_link_up링크 성립 확인 후TX 활성화, 통계 카운터 시작, netif_carrier_on() 연계
mac_link_down링크 끊김 감지 시TX 비활성화, 큐 정지, DMA 드레인(Drain)
mac_preparemac_config 직전MAC 정지, 클럭 재설정 등 사전 준비
mac_finishmac_config 직후MAC 재시작(Reboot), PCS 잠금 확인 등 사후 정리
mac_select_pcs인터페이스 모드 결정 시사용할 PCS(Physical Coding Sublayer) 인스턴스 반환

커널 소스 phylink_resolve()의 핵심 흐름을 분석하면, PHY 상태와 in-band 상태를 종합하여 최종 링크 상태를 결정하는 과정을 확인할 수 있습니다.

/* drivers/net/phy/phylink.c - phylink_resolve() 핵심 흐름 분석 */
static void phylink_resolve(struct work_struct *w)
{
    struct phylink *pl = container_of(w, struct phylink, resolve);
    struct phylink_link_state link_state;
    bool cur_link_is_up;

    mutex_lock(&pl->state_mutex);
    cur_link_is_up = pl->old_link_state;

    /* 1단계: PCS/PHY 상태를 수집하여 link_state 결정 */
    phylink_resolve_an_pause(&link_state);

    if (pl->phydev)
        phylink_get_phy_state(pl, &link_state);

    if (pl->pcs)
        phylink_pcs_get_state(pl, &link_state);

    /* 2단계: 링크 상태가 변경된 경우에만 MAC 콜백 호출 */
    if (link_state.link != cur_link_is_up) {
        if (!link_state.link) {
            /* 링크 다운: mac_link_down 호출 */
            pl->mac_ops->mac_link_down(pl->config,
                pl->cur_link_an_mode, pl->cur_interface);
        } else {
            /* 링크 업: mac_prepare - mac_config - mac_finish - mac_link_up */
            if (pl->mac_ops->mac_prepare)
                pl->mac_ops->mac_prepare(pl->config,
                    pl->cur_link_an_mode, pl->cur_interface);

            pl->mac_ops->mac_config(pl->config,
                pl->cur_link_an_mode, &link_state);

            if (pl->mac_ops->mac_finish)
                pl->mac_ops->mac_finish(pl->config,
                    pl->cur_link_an_mode, pl->cur_interface);

            pl->mac_ops->mac_link_up(pl->config, pl->phydev,
                pl->cur_link_an_mode, pl->cur_interface,
                link_state.speed, link_state.duplex,
                !!(link_state.pause & MLO_PAUSE_TX),
                !!(link_state.pause & MLO_PAUSE_RX));
        }
        pl->old_link_state = link_state.link;
    }
    mutex_unlock(&pl->state_mutex);
}

실제 MAC 드라이버에서 phylink_mac_ops를 구현하는 전체 패턴은 다음과 같습니다.

/* phylink_mac_ops 전체 구현 예시 */
static void my_mac_config(struct phylink_config *config,
                          unsigned int mode,
                          const struct phylink_link_state *state)
{
    struct my_priv *priv = container_of(config, struct my_priv,
                                         phylink_config);
    u32 mac_ctrl = my_read_reg(priv, MAC_CTRL);

    /* speed 설정 */
    mac_ctrl &= ~MAC_SPEED_MASK;
    switch (state->speed) {
    case SPEED_10000: mac_ctrl |= MAC_SPEED_10G; break;
    case SPEED_1000:  mac_ctrl |= MAC_SPEED_1G;  break;
    case SPEED_100:   mac_ctrl |= MAC_SPEED_100; break;
    }

    /* duplex 설정 */
    if (state->duplex == DUPLEX_FULL)
        mac_ctrl |= MAC_FULL_DUPLEX;
    else
        mac_ctrl &= ~MAC_FULL_DUPLEX;

    /* pause 프레임 설정 */
    mac_ctrl &= ~(MAC_TX_PAUSE | MAC_RX_PAUSE);
    if (state->pause & MLO_PAUSE_TX)
        mac_ctrl |= MAC_TX_PAUSE;
    if (state->pause & MLO_PAUSE_RX)
        mac_ctrl |= MAC_RX_PAUSE;

    my_write_reg(priv, MAC_CTRL, mac_ctrl);
}

static void my_mac_link_up(struct phylink_config *config,
                           struct phy_device *phy,
                           unsigned int mode, phy_interface_t interface,
                           int speed, int duplex,
                           bool tx_pause, bool rx_pause)
{
    struct my_priv *priv = container_of(config, struct my_priv,
                                         phylink_config);

    /* MAC TX 활성화 */
    my_set_bits(priv, MAC_CTRL, MAC_TX_EN | MAC_RX_EN);

    /* 커널 네트워크 스택에 캐리어 상태 통지 */
    netif_carrier_on(priv->ndev);
    netif_tx_wake_all_queues(priv->ndev);
}

static void my_mac_link_down(struct phylink_config *config,
                             unsigned int mode,
                             phy_interface_t interface)
{
    struct my_priv *priv = container_of(config, struct my_priv,
                                         phylink_config);

    /* TX 큐 정지 후 MAC 비활성화 */
    netif_tx_stop_all_queues(priv->ndev);
    netif_carrier_off(priv->ndev);
    my_clear_bits(priv, MAC_CTRL, MAC_TX_EN);

    /* DMA 진행 중인 프레임 완료 대기 */
    my_drain_tx_dma(priv);
}

static const struct phylink_mac_ops my_phylink_mac_ops = {
    .mac_config    = my_mac_config,
    .mac_link_up   = my_mac_link_up,
    .mac_link_down = my_mac_link_down,
};

SFP 케이지와 sfp_bus 통합

SFP(Small Form-factor Pluggable) 모듈을 지원하는 드라이버는 sfp_bus 프레임워크를 통해 모듈 삽입/제거 이벤트를 처리합니다. SFP 모듈 내부에 PHY가 포함된 경우(예: 1000BASE-T SFP) 자동으로 PHY 탐색(Discovery)과 연결이 수행됩니다.

SFP 통합의 핵심 구조는 sfp_upstream_ops 콜백입니다. 드라이버는 이 콜백을 구현하여 모듈 이벤트에 대응합니다.

/* sfp_upstream_ops를 통한 SFP 케이지 통합 */
static int my_sfp_module_insert(void *priv,
                                const struct sfp_eeprom_id *id)
{
    struct my_priv *p = priv;

    /* SFP EEPROM에서 모듈 타입/속도 확인 */
    dev_info(p->dev, "SFP module inserted: %s\n",
             id->base.vendor_name);

    /* phylink에 SFP 모듈 정보 전달 */
    return phylink_sfp_module_insert(p->phylink, id);
}

static void my_sfp_module_remove(void *priv)
{
    struct my_priv *p = priv;

    phylink_sfp_module_remove(p->phylink);
}

static void my_sfp_link_down(void *priv)
{
    struct my_priv *p = priv;

    phylink_sfp_link_down(p->phylink);
}

static void my_sfp_link_up(void *priv)
{
    struct my_priv *p = priv;

    phylink_sfp_link_up(p->phylink);
}

static const struct sfp_upstream_ops my_sfp_ops = {
    .module_insert  = my_sfp_module_insert,
    .module_remove  = my_sfp_module_remove,
    .link_down      = my_sfp_link_down,
    .link_up        = my_sfp_link_up,
    .attach         = phy_sfp_attach,
    .detach         = phy_sfp_detach,
    .connect_phy    = phy_sfp_connect_phy,
    .disconnect_phy = phy_sfp_disconnect_phy,
};

/* 프로브 시점에서 SFP 버스 등록 */
static int my_probe_sfp(struct my_priv *priv)
{
    struct sfp_bus *sfp_bus;

    /* phylink_config에 SFP 지원 플래그 설정 */
    priv->phylink_config.type = PHYLINK_NETDEV;
    __set_bit(PHY_INTERFACE_MODE_SGMII,
              priv->phylink_config.supported_interfaces);
    __set_bit(PHY_INTERFACE_MODE_1000BASEX,
              priv->phylink_config.supported_interfaces);

    /* SFP 버스를 fwnode에서 검색하여 등록 */
    sfp_bus = sfp_bus_find_fwnode(dev_fwnode(priv->dev));
    if (sfp_bus)
        sfp_bus_add_upstream(sfp_bus, priv, &my_sfp_ops);

    return 0;
}

phylink은 PHY 직접 연결 외에도 두 가지 대안적 링크 모드를 지원합니다. SGMII/1000BASE-X에서 사용하는 인밴드 Auto-Negotiation(In-band AN)과, 링크 파라미터가 고정된 고정 링크(fixed-link) 모드입니다.

모드phylink_config.type링크 감지 방식사용 시나리오
PHY 직접 연결PHYLINK_NETDEVPHY 상태 머신 (MDIO 폴링(Polling)/인터럽트(Interrupt))일반적인 구리 또는 SFP PHY
In-band ANPHYLINK_NETDEVPCS 레지스터 기반 인밴드 신호SGMII, 1000BASE-X, USXGMII SFP
Fixed-linkPHYLINK_NETDEVDevice Tree/ACPI에서 고정 파라미터스위치 백본, MAC-to-MAC 직결
MAC 전용PHYLINK_DEVMAC 자체 링크 감지가상 디바이스, DSA 마스터

in-band AN은 SGMII에서 PHY가 MAC에게 링크 파라미터를 전달하는 방식입니다. PCS(Physical Coding Sublayer)가 SGMII 제어 워드를 파싱하여 speed/duplex를 추출합니다.

/* In-band AN 지원을 위한 PCS 구현 예시 */
static void my_pcs_get_state(struct phylink_pcs *pcs,
                             struct phylink_link_state *state)
{
    struct my_pcs *mpcs = container_of(pcs, struct my_pcs, pcs);
    u32 status = my_pcs_read(mpcs, PCS_STATUS_REG);

    state->link = !!(status & PCS_LINK_UP);
    state->an_complete = !!(status & PCS_AN_COMPLETE);

    if (state->link) {
        /* SGMII 제어 워드에서 speed/duplex 추출 */
        u32 lpa = my_pcs_read(mpcs, PCS_LP_ABILITY_REG);

        switch ((lpa >> 10) & 0x3) {
        case 0: state->speed = SPEED_10;   break;
        case 1: state->speed = SPEED_100;  break;
        case 2: state->speed = SPEED_1000; break;
        }
        state->duplex = (lpa & PCS_SGMII_DUPLEX) ?
                        DUPLEX_FULL : DUPLEX_HALF;
    }
}

static int my_pcs_config(struct phylink_pcs *pcs,
                         unsigned int neg_mode,
                         phy_interface_t interface,
                         const unsigned long *advertising,
                         bool permit_pause_to_mac)
{
    struct my_pcs *mpcs = container_of(pcs, struct my_pcs, pcs);

    if (neg_mode == PHYLINK_PCS_NEG_INBAND_ENABLED) {
        /* In-band AN 활성화 */
        my_pcs_write(mpcs, PCS_CTRL_REG,
                     PCS_AN_ENABLE | PCS_AN_RESTART);
    } else {
        /* Forced 모드: AN 비활성화 */
        my_pcs_write(mpcs, PCS_CTRL_REG, 0);
    }

    return 0;
}

static const struct phylink_pcs_ops my_pcs_ops = {
    .pcs_get_state = my_pcs_get_state,
    .pcs_config    = my_pcs_config,
};

Fixed-link은 Device Tree에서 다음과 같이 설정됩니다.

/* Device Tree fixed-link 예시:
   &ethernet {
       fixed-link {
           speed = <1000>;
           full-duplex;
       };
   };
*/

/* 드라이버에서 fixed-link 처리 -- phylink이 자동으로 감지 */
static int my_setup_phylink(struct my_priv *priv)
{
    struct fwnode_handle *fwnode = dev_fwnode(priv->dev);

    priv->phylink_config.type = PHYLINK_NETDEV;
    priv->phylink_config.mac_capabilities =
        MAC_SYM_PAUSE | MAC_10 | MAC_100 | MAC_1000FD;

    /* phylink_create()는 fwnode에서 fixed-link 노드를 자동 탐색 */
    priv->phylink = phylink_create(&priv->phylink_config,
                                   fwnode, PHY_INTERFACE_MODE_SGMII,
                                   &my_phylink_mac_ops);

    /* fixed-link인 경우 phylink_of_phy_connect()는 내부적으로
     * phy_connect_direct()를 건너뛰고 고정 상태를 사용합니다 */
    return phylink_of_phy_connect(priv->phylink,
                                  to_of_node(fwnode), 0);
}

PHY 인터럽트 vs 폴링 모드

PHY 상태 변화를 감지하는 방식은 크게 인터럽트 구동(Interrupt-driven)과 타이머(Timer) 구동(Timer-driven, 폴링) 두 가지입니다. 인터럽트 방식이 CPU 효율이 높지만, 모든 PHY/보드 조합에서 인터럽트가 올바르게 배선되어 있지는 않습니다.

항목인터럽트 모드폴링 모드
반응 시간수 마이크로초 (즉시)최대 PHY_STATE_TIME(1초) 지연(Latency)
CPU 부하이벤트 발생 시만 처리주기적 MDIO 읽기 (1초마다)
구현 요구사항PHY IRQ 핀 배선, config_intr/handle_interrupt 구현추가 구현 없음 (기본 동작)
안정성IRQ 라인 노이즈에 민감할 수 있음매우 안정적
적합 환경고성능 서버, 빠른 failover 필요 환경임베디드, IRQ 미배선 보드
커널 함수phy_interrupt()phy_trigger_machine()phy_state_machine() delayed_work

인터럽트 모드에서 PHY 드라이버는 config_intrhandle_interrupt 콜백을 구현해야 합니다. 커널의 phy_interrupt() 핸들러(Handler)가 이 콜백들을 조율합니다.

/* drivers/net/phy/phy.c - phy_interrupt() 핵심 분석 */
static irqreturn_t phy_interrupt(int irq, void *phy_dat)
{
    struct phy_device *phydev = phy_dat;
    struct phy_driver *drv = phydev->drv;
    irqreturn_t ret;

    /* PHY 드라이버의 인터럽트 핸들러 호출 */
    ret = drv->handle_interrupt(phydev);

    if (ret == IRQ_HANDLED) {
        /* 인터럽트가 처리되면 상태 머신을 즉시 트리거 */
        phy_trigger_machine(phydev);
    }

    return ret;
}

/* PHY 드라이버의 인터럽트 콜백 구현 예시 (Realtek/Broadcom 패턴) */
static int my_phy_config_intr(struct phy_device *phydev)
{
    u16 val;

    if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
        /* 링크 상태 변경 인터럽트 활성화 */
        val = phy_read(phydev, MY_PHY_INTR_MASK);
        val |= MY_PHY_INTR_LINK_CHANGE;
        phy_write(phydev, MY_PHY_INTR_MASK, val);
    } else {
        /* 모든 인터럽트 비활성화 */
        phy_write(phydev, MY_PHY_INTR_MASK, 0);
    }

    return 0;
}

static irqreturn_t my_phy_handle_interrupt(struct phy_device *phydev)
{
    u16 status;

    /* 인터럽트 상태 읽기 (읽으면 자동 클리어) */
    status = phy_read(phydev, MY_PHY_INTR_STATUS);
    if (!(status & MY_PHY_INTR_LINK_CHANGE))
        return IRQ_NONE; /* 이 PHY의 인터럽트가 아님 */

    /* phylib에 인터럽트 발생 알림 */
    phy_trigger_machine(phydev);

    return IRQ_HANDLED;
}

/* 폴링 모드 전환: IRQ를 PHY_POLL로 설정하면 자동 폴링 */
static struct phy_driver my_phy_driver[] = {{
    .phy_id           = MY_PHY_ID,
    .phy_id_mask      = 0xfffffff0,
    .name             = "My PHY",
    .config_intr      = my_phy_config_intr,
    .handle_interrupt = my_phy_handle_interrupt,
    /* .irq = PHY_POLL 이면 폴링 모드로 동작 */
}};

phy_polling_mode()는 PHY의 IRQ가 PHY_POLL로 설정되었는지 확인합니다. 폴링 모드에서는 phy_state_machine()PHY_STATE_TIME(기본 1초) 간격으로 MDIO 레지스터를 읽어 링크 상태를 확인합니다. 인터럽트 모드에서는 phy_trigger_machine()이 상태 머신을 즉시 실행하므로 링크 변화에 대한 반응이 훨씬 빠릅니다.

제어 경로: RTNL, RTNETLINK, feature 토글

데이터 경로가 빠르더라도 제어 경로가 불안정하면 운영 장애가 반복됩니다. MTU 변경, queue 개수 변경, 링크 down/up, offload 토글은 모두 RTNL 보호 하에서 일관되게 처리해야 합니다.

/* 개념 예시: MTU 변경 시 stop/open 오류 경로 보강 */
static int my_ndo_change_mtu(struct net_device *ndev, int new_mtu)
{
    int ret;
    int old_mtu = ndev->mtu;

    if (new_mtu < 68 || new_mtu > 9700)
        return -EINVAL;

    if (netif_running(ndev)) {
        ret = my_ndo_stop(ndev);
        if (ret)
            return ret;

        ndev->mtu = new_mtu;
        ret = my_ndo_open(ndev);
        if (ret) {
            ndev->mtu = old_mtu;
            return ret;
        }
        return 0;
    }

    ndev->mtu = new_mtu;
    return 0;
}

static int my_ndo_set_features(struct net_device *ndev, netdev_features_t features)
{
    netdev_features_t changed = ndev->features ^ features;

    if (changed & NETIF_F_TSO)
        my_hw_toggle_tso(ndev, !!(features & NETIF_F_TSO));
    if (changed & NETIF_F_GRO)
        my_hw_toggle_gro(ndev, !!(features & NETIF_F_GRO));

    return 0;
}

RTNL 잠금 계층과 보호 영역

RTNL(Routing Netlink) 잠금은 리눅스 네트워크 스택(Network Stack)에서 가장 광범위한 뮤텍스(Mutex)입니다. net_device의 등록/해제, 주소 변경, 링크 상태 변경, offload 토글 등 거의 모든 제어 경로 작업이 RTNL 보호 하에서 수행됩니다. 커널 6.13부터는 per-namespace RTNL이 도입되어 네트워크 네임스페이스(Network Namespace) 간 병렬성이 개선되었습니다.

/* net/core/rtnetlink.c — RTNL 락 구현 */
static DEFINE_MUTEX(rtnl_mutex);

void rtnl_lock(void)
{
    mutex_lock(&rtnl_mutex);
}

void rtnl_unlock(void)
{
    /* 언락 전에 대기 중인 netdev 해제 요청 처리 */
    netdev_run_todo();
    mutex_unlock(&rtnl_mutex);
}

int rtnl_trylock(void)
{
    return mutex_trylock(&rtnl_mutex);
}

/* per-namespace RTNL (v6.13+) — net->rtnl_lock */
void rtnl_net_lock(struct net *net)
{
    rtnl_lock();             /* 전역 RTNL 먼저 */
    mutex_lock(&net->rtnl_lock); /* 이후 ns RTNL */
}
작업RTNL 필요 여부비고
register_netdevice()필수등록 과정 전체 RTNL 보호
dev_change_mtu()필수ndo_change_mtu 호출 전 RTNL 확인
dev_set_mac_address()필수주소 변경 → 알림 체인(Notifier Chain) 전파
ndo_open() / ndo_stop()필수링크 상태 변경 보호
ndo_set_features()필수offload 토글
ndo_start_xmit()불필요데이터 경로 — NAPI/softirq 컨텍스트
napi_poll()불필요데이터 경로 — softirq 컨텍스트
ethtool_get_stats()선택적일부 드라이버에서 RTNL 생략
devlink_health_report()불필요자체 뮤텍스 사용
데드락 주의: RTNL을 잡은 상태에서 flush_workqueue()를 호출하면 안 됩니다. workqueue에 대기 중인 작업이 RTNL을 요청할 수 있기 때문입니다. 대신 rtnl_unlock()flush_workqueue()rtnl_lock() 패턴을 사용합니다.

RTNETLINK는 사용자 공간(User Space)에서 커널 네트워크 설정을 변경하는 주요 인터페이스입니다. ip link add, ip link set 같은 명령은 모두 RTM_NEWLINK, RTM_SETLINK 등의 RTNETLINK 메시지로 변환됩니다.

/* 커스텀 netdev를 위한 rtnl_link_ops 등록 예시 */
static int my_virt_newlink(struct net *src_net,
                           struct net_device *dev,
                           struct nlattr *tb[],
                           struct nlattr *data[],
                           struct netlink_ext_ack *extack)
{
    struct my_virt_priv *priv = netdev_priv(dev);
    int err;

    /* IFLA_MY_MODE 등 드라이버 전용 속성 파싱 */
    if (data && data[IFLA_MY_MODE])
        priv->mode = nla_get_u32(data[IFLA_MY_MODE]);

    err = register_netdevice(dev);
    if (err)
        return err;

    netif_carrier_off(dev);
    return 0;
}

static void my_virt_dellink(struct net_device *dev,
                            struct list_head *head)
{
    unregister_netdevice_queue(dev, head);
}

static const struct nla_policy my_virt_policy[IFLA_MY_MAX + 1] = {
    [IFLA_MY_MODE] = { .type = NLA_U32 },
    [IFLA_MY_FLAGS] = { .type = NLA_U32 },
};

static struct rtnl_link_ops my_virt_link_ops = {
    .kind         = "my_virt",
    .priv_size    = sizeof(struct my_virt_priv),
    .setup        = my_virt_setup,
    .newlink      = my_virt_newlink,
    .dellink      = my_virt_dellink,
    .policy       = my_virt_policy,
    .maxtype      = IFLA_MY_MAX,
};

/* 모듈 초기화 시 등록 */
static int __init my_virt_init(void)
{
    return rtnl_link_register(&my_virt_link_ops);
}

RTNETLINK 메시지 처리 흐름은 다음과 같습니다. 사용자가 ip link add 명령을 실행하면, RTM_NEWLINK 메시지가 소켓(Socket)을 통해 커널에 도달합니다. rtnetlink_rcv_msg()가 메시지를 디스패치(Dispatch)하고, rtnl_newlink()rtnl_link_ops를 찾아 newlink 콜백을 호출합니다.

커널 5.6부터 ethtool은 기존 ioctl 인터페이스를 대체하는 Netlink 기반 인터페이스를 제공합니다. ethtool-netlink(이하 ethnl)은 확장성이 뛰어나고, 변경 알림(Notification)을 지원하며, 구조화된 데이터 전달이 가능합니다.

ethnl 커맨드 패밀리대응 레거시 ioctl설명
ETHTOOL_MSG_LINKINFO_GET/SETETHTOOL_GSET/SSET링크 속도, 듀플렉스(Duplex), autoneg
ETHTOOL_MSG_RINGS_GET/SETETHTOOL_GRINGPARAMRX/TX 링 크기
ETHTOOL_MSG_CHANNELS_GET/SETETHTOOL_GCHANNELS큐/채널 수
ETHTOOL_MSG_COALESCE_GET/SETETHTOOL_GCOALESCE인터럽트 코얼레싱(Coalescing)
ETHTOOL_MSG_STATS_GETETHTOOL_GSTATS드라이버 통계
ETHTOOL_MSG_PAUSE_GET/SETETHTOOL_GPAUSEPARAM일시 정지 프레임(Pause Frame) 설정
ETHTOOL_MSG_FEC_GET/SETETHTOOL_GFECPARAMFEC(Forward Error Correction) 설정
/* ethtool-netlink 코얼레싱 ops 구현 예시 */
static int my_get_coalesce(struct net_device *ndev,
                           struct ethtool_coalesce *ec,
                           struct kernel_ethtool_coalesce *kec,
                           struct netlink_ext_ack *extack)
{
    struct my_priv *priv = netdev_priv(ndev);

    ec->rx_coalesce_usecs = priv->rx_usecs;
    ec->rx_max_coalesced_frames = priv->rx_frames;
    ec->tx_coalesce_usecs = priv->tx_usecs;
    ec->tx_max_coalesced_frames = priv->tx_frames;
    ec->use_adaptive_rx_coalesce = priv->adaptive_rx;
    ec->use_adaptive_tx_coalesce = priv->adaptive_tx;

    return 0;
}

static int my_set_coalesce(struct net_device *ndev,
                           struct ethtool_coalesce *ec,
                           struct kernel_ethtool_coalesce *kec,
                           struct netlink_ext_ack *extack)
{
    struct my_priv *priv = netdev_priv(ndev);

    if (ec->rx_coalesce_usecs > MY_MAX_COAL_USECS) {
        NL_SET_ERR_MSG_MOD(extack,
            "rx-usecs exceeds maximum");
        return -ERANGE;
    }

    priv->rx_usecs = ec->rx_coalesce_usecs;
    priv->rx_frames = ec->rx_max_coalesced_frames;
    priv->tx_usecs = ec->tx_coalesce_usecs;
    priv->tx_frames = ec->tx_max_coalesced_frames;
    priv->adaptive_rx = ec->use_adaptive_rx_coalesce;
    priv->adaptive_tx = ec->use_adaptive_tx_coalesce;

    /* 하드웨어에 새 코얼레싱 값 적용 */
    my_hw_set_coalesce(priv);

    return 0;
}

static const struct ethtool_ops my_ethtool_ops = {
    .supported_coalesce_params = ETHTOOL_COALESCE_USECS |
                                  ETHTOOL_COALESCE_MAX_FRAMES |
                                  ETHTOOL_COALESCE_USE_ADAPTIVE,
    .get_coalesce   = my_get_coalesce,
    .set_coalesce   = my_set_coalesce,
    /* ... 기타 ops ... */
};
알림 메커니즘: ethtool-netlink은 ETHTOOL_MSG_*_NTF 형태의 알림 메시지를 멀티캐스트(Multicast) 그룹(ETHNL_MCGRP_MONITOR)으로 전송합니다. ethtool --monitor 명령으로 실시간(Real-time) 변경 사항을 관찰할 수 있습니다.

채널/큐 동적 재구성

ethtool -L 명령으로 채널(Channel) 수를 동적으로 변경할 때, 드라이버는 stop → 재구성 → restart 패턴을 따라야 합니다. 이 과정에서 패킷(Packet) 손실을 최소화하고, 경쟁 상태(Race Condition)를 방지하는 것이 핵심입니다.

/* 안전한 채널 재구성 구현 */
static int my_set_channels(struct net_device *ndev,
                           struct ethtool_channels *ch)
{
    struct my_priv *priv = netdev_priv(ndev);
    unsigned int new_rx = ch->combined_count ?: ch->rx_count;
    unsigned int new_tx = ch->combined_count ?: ch->tx_count;
    int err;

    /* 검증: 하드웨어 최대값 초과 방지 */
    if (new_rx > priv->max_rx_queues ||
        new_tx > priv->max_tx_queues)
        return -EINVAL;

    /* XDP가 활성화된 경우 TX 큐 수 제한 확인 */
    if (priv->xdp_prog && new_tx < new_rx)
        return -EINVAL;

    /* 인터페이스가 UP 상태인 경우만 재시작 필요 */
    if (netif_running(ndev)) {
        /* 1단계: 데이터 경로 중단 */
        netif_tx_disable(ndev);
        my_napi_disable_all(priv);
        my_free_irqs(priv);
        my_free_rings(priv);

        /* 2단계: 새 큐 수 적용 */
        priv->num_rx_queues = new_rx;
        priv->num_tx_queues = new_tx;

        /* 3단계: 새 링/NAPI/IRQ 할당 */
        err = my_alloc_rings(priv);
        if (err)
            goto err_rollback;

        err = my_request_irqs(priv);
        if (err)
            goto err_free_rings;

        /* 4단계: 데이터 경로 재시작 */
        my_napi_enable_all(priv);
        netif_tx_start_all_queues(ndev);

        /* netdev 큐 수 갱신 (XPS/RPS 재설정) */
        netif_set_real_num_rx_queues(ndev, new_rx);
        netif_set_real_num_tx_queues(ndev, new_tx);
    } else {
        priv->num_rx_queues = new_rx;
        priv->num_tx_queues = new_tx;
    }

    return 0;

err_free_rings:
    my_free_rings(priv);
err_rollback:
    /* 롤백: 이전 큐 수로 복원 시도 */
    priv->num_rx_queues = priv->prev_rx_queues;
    priv->num_tx_queues = priv->prev_tx_queues;
    my_alloc_rings(priv);
    my_request_irqs(priv);
    my_napi_enable_all(priv);
    netif_tx_start_all_queues(ndev);
    return err;
}
경쟁 상태 방지: set_channels는 RTNL 보호 하에서 호출되므로 동시에 두 번 실행될 수 없습니다. 그러나 ndo_start_xmit()과의 경쟁은 netif_tx_disable()으로 방지해야 합니다. NAPI 폴링과의 경쟁은 napi_disable()이 보장합니다. 반드시 IRQ 해제 → NAPI disable → 큐 해제 순서를 지켜야 합니다.

보안 하드닝: 입력 검증과 경계 조건

네트워크 드라이버 취약점(Vulnerability)은 원격 트리거 가능성이 있습니다. 길이 검증, ring index 범위 확인, DMA 주소 검증은 성능 최적화보다 우선되어야 합니다.

취약 패턴점검 포인트방어 전략
RX length 신뢰HW가 넘긴 length를 그대로 사용최소/최대 길이, headroom 검증
ring index overflowproducer/consumer wrap 처리 누락mask 기반 인덱싱 + assert
UAF on resetreset 중 skb/page 소유권 경합(Contention)state machine + refcount 엄격화
ioctl/netlink 입력 검증 부족사용자 파라미터 경계값 누락range check, capability check

RX 패킷 길이 검증 패턴

네트워크 드라이버에서 가장 흔한 취약점은 하드웨어가 보고한 패킷 길이를 무조건 신뢰하는 것입니다. NIC 펌웨어 버그, DMA 오류, 악의적인 패킷 조작 등으로 인해 실제 데이터 크기와 보고된 길이가 다를 수 있으며, 이를 검증하지 않으면 버퍼(Buffer) 오버리드(Buffer Overread) 또는 힙 오버플로우(Heap Overflow)가 발생합니다.

실제 CVE 사례를 통해 위험성을 확인할 수 있습니다.

/* 안전한 RX 패킷 길이 검증 패턴 */

/* 1단계: 하드웨어 보고 길이의 기본 범위 검증 */
static bool my_validate_rx_length(struct my_rx_desc *desc,
                                    struct net_device *ndev)
{
    u32 len = le32_to_cpu(desc->length);

    /* 최소 길이: 이더넷 헤더(14) + 최소 페이로드 */
    if (unlikely(len < ETH_HLEN)) {
        ndev->stats.rx_length_errors++;
        return false;
    }

    /* 최대 길이: MTU + 헤더 + VLAN 태그 + FCS */
    if (unlikely(len > ndev->mtu + ETH_HLEN + VLAN_HLEN + ETH_FCS_LEN)) {
        ndev->stats.rx_length_errors++;
        return false;
    }

    /* DMA 버퍼 크기를 초과하지 않는지 확인 */
    if (unlikely(len > MY_RX_BUF_SIZE)) {
        netdev_warn_once(ndev,
            "RX length %u exceeds buffer size %u\n",
            len, MY_RX_BUF_SIZE);
        ndev->stats.rx_length_errors++;
        return false;
    }

    return true;
}

/* 2단계: 헤더 파싱 시 경계 검사 */
static int my_parse_rx_headers(struct sk_buff *skb)
{
    struct ethhdr *eth;
    struct iphdr *iph;

    /* pskb_may_pull()로 최소 헤더 크기만큼 linear 보장 */
    if (!pskb_may_pull(skb, ETH_HLEN))
        return -EINVAL;

    eth = eth_hdr(skb);

    if (eth->h_proto == htons(ETH_P_IP)) {
        /* IP 헤더 접근 전 추가 pull 필요 */
        if (!pskb_may_pull(skb, ETH_HLEN + sizeof(*iph)))
            return -EINVAL;

        iph = ip_hdr(skb);

        /* IP 헤더 길이 검증 (IHL 최소 5) */
        if (iph->ihl < 5)
            return -EINVAL;

        /* IP 총 길이와 SKB 길이 일관성 확인 */
        if (ntohs(iph->tot_len) > skb->len - ETH_HLEN)
            return -EINVAL;
    }

    return 0;
}

/* 3단계: DMA coherent 버퍼 접근 안전 패턴 */
static void my_safe_dma_read(struct my_ring *ring, u32 idx)
{
    struct my_rx_desc *desc;

    /* DMA 동기화: CPU가 최신 데이터를 보도록 보장 */
    dma_rmb();

    desc = &ring->desc[idx];

    /* volatile 읽기 또는 READ_ONCE로 컴파일러 최적화 방지 */
    u32 status = READ_ONCE(desc->status);
    u32 length = READ_ONCE(desc->length);

    /* 동일 descriptor를 두 번 읽으면 값이 바뀔 수 있음
     * (TOCTOU 방지) - 한 번 읽은 값을 로컬 변수에 저장 */
}

Ring Buffer 오버플로우 방지

링 버퍼(Ring Buffer)의 프로듀서(Producer)/컨슈머(Consumer) 인덱스 관리에서 정수 오버플로(Integer Overflow)우(Integer Overflow)나 래핑(Wrapping) 오류가 발생하면 임의 메모리 접근이 가능해집니다. 안전한 인덱스 관리 패턴을 일관되게 적용해야 합니다.

링 버퍼 인덱스 관리의 핵심 원칙은 다음과 같습니다.

/* 안전한 Ring Buffer 인덱스 관리 패턴 */

#define MY_RING_SIZE      1024   /* 반드시 2의 거듭제곱 */
#define MY_RING_MASK      (MY_RING_SIZE - 1)

struct my_ring {
    struct my_desc  *desc;       /* DMA coherent 디스크립터 배열 */
    u32             prod;       /* 프로듀서 인덱스 (다음 쓸 위치) */
    u32             cons;       /* 컨슈머 인덱스 (다음 읽을 위치) */
    u32             size;       /* 링 크기 */
    u32             mask;       /* size - 1 */
};

/* 안전한 인덱스 래핑 - 항상 마스크 사용 */
static inline u32 ring_next(struct my_ring *ring, u32 idx)
{
    return (idx + 1) & ring->mask;
}

/* 사용 가능한 슬롯 수 계산 (오버플로우 안전) */
static inline u32 ring_space(struct my_ring *ring)
{
    /* u32 래핑을 활용: prod - cons는 항상 올바른 값 */
    return ring->size - (ring->prod - ring->cons) - 1;
}

/* 처리 대기 중인 디스크립터 수 */
static inline u32 ring_pending(struct my_ring *ring)
{
    return ring->prod - ring->cons;
}

/* TX: 안전한 디스크립터 제출 */
static netdev_tx_t my_safe_xmit(struct sk_buff *skb,
                                struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);
    struct my_ring *tx = &priv->tx_ring;
    u32 idx;

    /* 공간 확인 - 경쟁 조건 방지를 위해 READ_ONCE 사용 */
    if (unlikely(ring_space(tx) == 0)) {
        netif_stop_queue(ndev);
        /* 재확인: stop과 check 사이에 completion이 올 수 있음 */
        smp_mb();
        if (ring_space(tx) > 0)
            netif_wake_queue(ndev);
        else
            return NETDEV_TX_BUSY;
    }

    /* 마스크 기반 인덱싱으로 범위 초과 불가 */
    idx = tx->prod & tx->mask;

    /* WARN_ON으로 디버그 빌드에서 invariant 검증 */
    WARN_ON_ONCE(idx >= tx->size);

    /* 디스크립터 소유권 확인 */
    if (unlikely(READ_ONCE(tx->desc[idx].flags) & DESC_HW_OWNED)) {
        netdev_err(ndev, "TX desc %u still owned by HW\n", idx);
        return NETDEV_TX_BUSY;
    }

    my_fill_tx_desc(tx, idx, skb);
    tx->prod++;  /* u32 자연 래핑에 의존 */

    return NETDEV_TX_OK;
}

네트워크 드라이버의 IOCTL 및 Netlink 핸들러는 사용자 공간(Userspace)에서 전달되는 데이터를 처리합니다. 모든 사용자 입력은 신뢰할 수 없으며(Untrusted), 매개변수 범위 검증, 권한 확인(Capability Check), 경계 조건 처리를 철저히 수행해야 합니다.

/* 안전한 ethtool IOCTL 핸들러 패턴 */

/* 링 크기 설정 - 범위 검증 필수 */
static int my_set_ringparam(struct net_device *ndev,
                            struct ethtool_ringparam *ring,
                            struct kernel_ethtool_ringparam *kernel_ring,
                            struct netlink_ext_ack *extack)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 최소/최대 범위 검증 */
    if (ring->rx_pending < MY_MIN_RING_SIZE ||
        ring->rx_pending > MY_MAX_RING_SIZE) {
        NL_SET_ERR_MSG_MOD(extack,
            "RX ring size out of range");
        return -EINVAL;
    }

    /* 2의 거듭제곱 정렬 검증 */
    if (!is_power_of_2(ring->rx_pending)) {
        NL_SET_ERR_MSG_MOD(extack,
            "RX ring size must be power of 2");
        return -EINVAL;
    }

    /* TX 링 크기도 동일하게 검증 */
    if (ring->tx_pending < MY_MIN_RING_SIZE ||
        ring->tx_pending > MY_MAX_RING_SIZE ||
        !is_power_of_2(ring->tx_pending)) {
        NL_SET_ERR_MSG_MOD(extack,
            "TX ring size invalid");
        return -EINVAL;
    }

    /* 설정 적용 (락 보호 하에) */
    mutex_lock(&priv->conf_lock);
    priv->rx_ring_size = ring->rx_pending;
    priv->tx_ring_size = ring->tx_pending;
    mutex_unlock(&priv->conf_lock);

    /* 런타임 적용은 인터페이스 재시작 필요 */
    if (netif_running(ndev))
        return my_restart_dev(priv);

    return 0;
}

/* Private flags 설정 - 비트 범위 검증 */
static int my_set_priv_flags(struct net_device *ndev, u32 flags)
{
    struct my_priv *priv = netdev_priv(ndev);
    u32 changed;

    /* 알 수 없는 플래그 비트가 설정되어 있으면 거부 */
    if (flags & ~MY_KNOWN_PRIV_FLAGS) {
        netdev_warn(ndev, "unknown priv flags: 0x%x\n",
                    flags & ~MY_KNOWN_PRIV_FLAGS);
        return -EINVAL;
    }

    changed = priv->priv_flags ^ flags;
    priv->priv_flags = flags;

    /* 변경된 플래그에 따라 필요한 재구성 수행 */
    if (changed & MY_PRIV_FLAG_NAPI_BUSY_POLL)
        my_reconfigure_napi(priv);

    return 0;
}

퍼저(Fuzzer) 기반 보안 테스트

네트워크 드라이버는 외부에서 들어오는 패킷을 처리하므로 공격 표면(Attack Surface)이 넓습니다. 퍼징(Fuzzing)은 무작위 또는 반무작위 입력을 자동 생성하여 예상치 못한 코드 경로와 크래시를 발견하는 효과적인 보안 테스트 방법입니다.

주요 퍼징 도구와 적용 방법은 다음과 같습니다.

도구퍼징 대상커버리지 유형적용 난이도
syzkaller시스콜(Syscall) 인터페이스, IOCTL커버리지 기반 (KCOV)중간
Scapy프로토콜 파싱, 패킷 처리 경로생성 기반 (수동 규칙)낮음
AFL/libFuzzer사용자 공간 유틸리티커버리지 기반낮음
kcov + custom특정 드라이버 경로커버리지 가이드높음
# syzkaller 설정 예시: 네트워크 드라이버 대상 퍼징

# 1. 커널 빌드 옵션 (필수)
# CONFIG_KCOV=y
# CONFIG_KASAN=y (메모리 오류 감지)
# CONFIG_UBSAN=y (정의되지 않은 동작 감지)
# CONFIG_LOCKDEP=y (락 오류 감지)
# CONFIG_DEBUG_KMEMLEAK=y

# 2. syzkaller 설정 파일 (syz-manager.cfg)
cat <<EOF > syz-manager.cfg
{
    "target": "linux/amd64",
    "http": "127.0.0.1:56741",
    "workdir": "/tmp/syzkaller-workdir",
    "kernel_obj": "/path/to/linux/build",
    "image": "/path/to/stretch.img",
    "sshkey": "/path/to/stretch.id_rsa",
    "syzkaller": "/path/to/syzkaller",
    "procs": 8,
    "type": "qemu",
    "vm": {
        "count": 4,
        "kernel": "/path/to/linux/build/arch/x86/boot/bzImage",
        "cpu": 2,
        "mem": 2048
    },
    "enable_syscalls": [
        "setsockopt", "getsockopt",
        "ioctl\$SIOCETHTOOL",
        "ioctl\$SIOCDEVPRIVATE",
        "sendmsg", "recvmsg",
        "syz_emit_ethernet"
    ]
}
EOF

# 3. 실행
syz-manager -config=syz-manager.cfg
# Scapy를 사용한 패킷 퍼징 예시
python3 <<'PYEOF'
from scapy.all import *
import random

# 대상 인터페이스
iface = "eth0"

# 기본 이더넷 프레임에 무작위 페이로드
for i in range(10000):
    # 무작위 EtherType
    etype = random.randint(0, 0xFFFF)

    # 무작위 길이 페이로드 (0 ~ 9000 바이트)
    payload_len = random.randint(0, 9000)
    payload = bytes(random.getrandbits(8) for _ in range(payload_len))

    pkt = Ether(type=etype) / Raw(load=payload)
    sendp(pkt, iface=iface, verbose=False)

    # 비정상 IP 헤더 퍼징
    if i % 10 == 0:
        ip_pkt = Ether() / IP(
            ihl=random.randint(0, 15),
            tot_len=random.randint(0, 65535),
            frag=random.randint(0, 8191),
            proto=random.randint(0, 255)
        ) / Raw(load=payload[:100])
        sendp(ip_pkt, iface=iface, verbose=False)

print("퍼징 완료: 10000 패킷 전송")
PYEOF

동기화, RTNL, 메모리 모델

netdev 코드의 동기화는 단일 잠금으로 끝나지 않습니다. 설정 경로는 RTNL, 데이터 경로는 per-queue spinlock/NAPI, 통계 경로는 u64_stats_sync를 조합합니다.

RTNL per-namespace 잠금 (v6.13+): 커널 6.13에서 rtnl_lockper-network namespace 단위로 세분화되었습니다. 기존에는 전체 네트워크 네임스페이스(Namespace)가 단일 글로벌 RTNL 뮤텍스를 공유하여, 컨테이너(Container) 수천 개를 운영하는 환경에서 제어 경로 병목(Bottleneck)이 발생했습니다. per-namespace RTNL 잠금을 통해 서로 다른 네임스페이스의 링크 설정 작업이 병렬로 수행되어 경합이 크게 감소합니다.

RTNL 잠금 획득 패턴과 데드락(Deadlock) 방지

RTNL(Route Netlink) 뮤텍스는 네트워크 서브시스템의 최상위 잠금으로, 장치 등록/해제, 주소 설정, 링크 상태 변경 등 모든 제어 경로를 직렬화(Serialization)합니다. 올바른 RTNL 사용 패턴을 이해하지 못하면 데드락이 쉽게 발생합니다.

/* net/core/rtnetlink.c - RTNL 락 API */
void rtnl_lock(void)
{
    mutex_lock(&rtnl_mutex);
}

int rtnl_trylock(void)
{
    return mutex_trylock(&rtnl_mutex);
}

void rtnl_unlock(void)
{
    /* 지연된 netlink 알림을 unlock 시점에 배치 전송 */
    netdev_run_todo();
    mutex_unlock(&rtnl_mutex);
}

/* RTNL 보유 검증 매크로 — 디버깅에 필수 */
#define ASSERT_RTNL() \
    WARN_ON_ONCE(!rtnl_is_locked())

ASSERT_RTNL()은 RTNL 보유를 런타임에 검증하며, netdev 코드에서 광범위하게 사용됩니다. 이 매크로(Macro)가 경고를 출력하면 제어 경로에서 RTNL 없이 보호되지 않은 접근이 일어난 것이므로 반드시 수정해야 합니다.

RTNL 관련 데드락은 주로 다음 시나리오에서 발생합니다.

/* 올바른 패턴: worker 컨텍스트에서 안전한 RTNL 획득 */
static void my_reset_work_handler(struct work_struct *work)
{
    struct my_priv *priv = container_of(work, struct my_priv, reset_work);
    struct net_device *ndev = priv->netdev;

    /* rtnl_lock을 먼저 획득 — 락 순서: RTNL → device lock */
    rtnl_lock();

    /* 장치가 이미 해제 중인지 확인 */
    if (!netif_running(ndev)) {
        rtnl_unlock();
        return;
    }

    /* 안전하게 장치 리셋 수행 */
    my_close_locked(ndev);    /* RTNL 보유 상태에서 호출 */
    my_open_locked(ndev);     /* RTNL 보유 상태에서 호출 */

    rtnl_unlock();
}

/* 위험한 패턴 — ndo_stop에서 reset_work와 데드락 가능 */
static int my_ndo_stop_BAD(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* !! RTNL 이미 보유 중 (ndo_stop 호출자가 획득) */
    /* !! reset_work가 rtnl_lock()을 시도하면 데드락 !! */
    cancel_work_sync(&priv->reset_work);  /* 위험! */

    return 0;
}

/* 안전한 패턴 — 플래그 기반 취소 */
static int my_ndo_stop_SAFE(struct net_device *ndev)
{
    struct my_priv *priv = netdev_priv(ndev);

    /* 플래그로 reset work에게 중단 신호 전달 */
    set_bit(MY_STATE_CLOSING, &priv->state);

    /* RTNL을 잠시 놓고 work 완료 대기 */
    rtnl_unlock();
    cancel_work_sync(&priv->reset_work);
    rtnl_lock();

    /* 재진입 후 상태 재확인 필수 */
    if (!netif_running(ndev))
        return 0;

    my_hw_shutdown(priv);
    return 0;
}
잠금 순서 규칙: 네트워크 서브시스템의 잠금 획득 순서는 반드시 RTNL → 장치 spinlock → NAPI 순서를 따라야 합니다. 역순 획득은 데드락을 유발합니다. lockdep이 활성화된 커널에서 이 순서 위반을 자동으로 감지합니다.

per-CPU 통계의 u64_stats_sync 메커니즘

u64_stats_sync는 32비트 시스템에서 64비트 통계 카운터를 원자적(Atomic)으로 읽기 위한 경량 동기화 메커니즘입니다. 64비트 시스템에서는 단일 명령어로 64비트 값을 읽을 수 있지만, 32비트 시스템에서는 상위/하위 32비트를 별도로 읽어야 하므로 중간에 쓰기가 끼어들면 잘못된 값(torn read)을 읽을 수 있습니다.

/* include/linux/u64_stats_sync.h - 핵심 구조와 API */
struct u64_stats_sync {
#if BITS_PER_LONG == 32
    seqcount_t seq;  /* 32비트에서만 시퀀스 카운터 사용 */
#endif
};

/* 쓰기 측: 통계 업데이트 보호 */
static inline void u64_stats_update_begin(struct u64_stats_sync *syncp)
{
#if BITS_PER_LONG == 32
    write_seqcount_begin(&syncp->seq);
#endif
}

static inline void u64_stats_update_end(struct u64_stats_sync *syncp)
{
#if BITS_PER_LONG == 32
    write_seqcount_end(&syncp->seq);
#endif
}

/* 읽기 측: 일관된 64비트 값 읽기 */
static inline unsigned int
u64_stats_fetch_begin(const struct u64_stats_sync *syncp)
{
#if BITS_PER_LONG == 32
    return read_seqcount_begin(&syncp->seq);
#else
    return 0;  /* 64비트에서는 no-op */
#endif
}

static inline bool
u64_stats_fetch_retry(const struct u64_stats_sync *syncp,
                      unsigned int start)
{
#if BITS_PER_LONG == 32
    return read_seqcount_retry(&syncp->seq, start);
#else
    return false;  /* 64비트에서는 항상 성공 */
#endif
}

u64_stats_syncseqlock의 차이점을 이해하는 것이 중요합니다. seqlock은 읽기-쓰기 모두에 대한 범용 동기화인 반면, u64_stats_sync는 쓰기 측이 선점(Preemption) 금지(preempt_disable) 컨텍스트에서만 동작한다고 가정합니다. NAPI poll이나 softirq 컨텍스트에서 통계를 업데이트하므로 이 가정이 자연스럽게 충족됩니다.

/* 완전한 per-CPU 네트워크 통계 구현 예시 */
struct my_pcpu_stats {
    struct u64_stats_sync syncp;  /* 동기화 프리미티브 */
    u64 rx_packets;
    u64 rx_bytes;
    u64 tx_packets;
    u64 tx_bytes;
    u64 rx_errors;
    u64 tx_dropped;
};

/* 데이터 경로 (NAPI poll 컨텍스트): 통계 업데이트 */
static void my_rx_update_stats(struct my_priv *priv,
                                unsigned int len)
{
    struct my_pcpu_stats *stats = this_cpu_ptr(priv->pcpu_stats);

    u64_stats_update_begin(&stats->syncp);
    stats->rx_packets++;
    stats->rx_bytes += len;
    u64_stats_update_end(&stats->syncp);
}

/* ndo_get_stats64: 모든 CPU의 통계를 안전하게 합산 */
static void my_get_stats64(struct net_device *ndev,
                            struct rtnl_link_stats64 *s)
{
    struct my_priv *priv = netdev_priv(ndev);
    int cpu;

    for_each_possible_cpu(cpu) {
        struct my_pcpu_stats *stats;
        u64 rx_packets, rx_bytes, tx_packets, tx_bytes;
        unsigned int start;

        stats = per_cpu_ptr(priv->pcpu_stats, cpu);

        do {
            start = u64_stats_fetch_begin(&stats->syncp);
            rx_packets = stats->rx_packets;
            rx_bytes   = stats->rx_bytes;
            tx_packets = stats->tx_packets;
            tx_bytes   = stats->tx_bytes;
        } while (u64_stats_fetch_retry(&stats->syncp, start));

        s->rx_packets += rx_packets;
        s->rx_bytes   += rx_bytes;
        s->tx_packets += tx_packets;
        s->tx_bytes   += tx_bytes;
    }
}
u64_stats_sync: torn read 방지 메커니즘 (32비트 시스템) Writer (NAPI poll) seq=0 B seq=1 (홀수) 상위 32비트 쓰기 하위 32비트 쓰기 E seq=2 (짝수) Reader 1 (torn read 감지) B start=0 상위 읽기 하위 읽기 R seq=2 != start=0 → 재시도! Reader 2 (정상 읽기) B start=2 상위 읽기 하위 읽기 R seq=2 == start=2 → 성공 B = fetch_begin / update_begin E = update_end R = fetch_retry (seq 비교) 64비트 시스템에서는 모든 동기화가 no-op으로 컴파일됩니다 (단일 명령어 원자적 읽기)
u64_stats_sync는 시퀀스 카운터를 사용하여 32비트 시스템에서 64비트 값의 torn read를 감지하고 재시도합니다.

RCU를 활용한 XDP 프로그램 교체

XDP 프로그램은 net_device에 RCU 포인터(RCU Pointer)로 저장됩니다. 이를 통해 데이터 경로에서 잠금 없이 XDP 프로그램을 참조하면서, 제어 경로에서는 안전하게 프로그램을 교체할 수 있습니다.

/* net/core/dev.c - dev_xdp_install() RCU 기반 XDP 프로그램 교체 */
static int dev_xdp_install(struct net_device *dev,
                            bpf_op_t bpf_op,
                            struct netlink_ext_ack *extack,
                            u32 flags,
                            struct bpf_prog *prog)
{
    struct bpf_prog *old_prog;
    struct netdev_bpf xdp;
    int err;

    /* RTNL 보유 상태에서 호출됨 → 제어 경로 직렬화 보장 */
    ASSERT_RTNL();

    memset(&xdp, 0, sizeof(xdp));
    xdp.command = XDP_SETUP_PROG;
    xdp.prog = prog;
    xdp.extack = extack;
    xdp.flags = flags;

    /* 드라이버 콜백을 통해 HW/SW XDP 프로그램 설치 */
    err = bpf_op(dev, &xdp);
    if (err)
        return err;

    /* 핵심: RCU를 통해 새 프로그램 포인터 발행 */
    old_prog = rcu_replace_pointer(
        dev->xdp_prog, prog,
        lockdep_is_held(&rtnl_mutex));

    if (old_prog) {
        /* grace period 이후에 이전 프로그램 해제 */
        bpf_prog_put(old_prog);
    }

    return 0;
}

/* 데이터 경로: RCU read-side에서 XDP 프로그램 참조 */
static u32 my_run_xdp(struct my_rx_ring *ring,
                       struct xdp_buff *xdp)
{
    struct bpf_prog *prog;
    u32 act = XDP_PASS;

    /* RCU read-side: 락 없이 프로그램 포인터 읽기 */
    prog = rcu_dereference(ring->netdev->xdp_prog);
    if (!prog)
        return act;

    /* XDP 프로그램 실행 — NAPI 컨텍스트 = RCU read-side */
    act = bpf_prog_run_xdp(prog, xdp);

    switch (act) {
    case XDP_PASS:
    case XDP_TX:
    case XDP_REDIRECT:
        break;
    default:
        bpf_warn_invalid_xdp_action(ring->netdev, prog, act);
        /* fallthrough */
    case XDP_ABORTED:
    case XDP_DROP:
        act = XDP_DROP;
        break;
    }

    return act;
}

RCU 기반 교체의 핵심은 rcu_replace_pointer()가 새 포인터를 발행(publish)한 후, 기존 데이터 경로에서 이전 프로그램을 사용 중인 모든 CPU가 RCU grace period를 통과한 뒤에야 이전 프로그램이 해제되는 점입니다. 이로써 데이터 경로는 잠금 없이도 항상 유효한 프로그램 포인터를 참조할 수 있습니다.

데이터 경로 동시성 모델

네트워크 드라이버의 데이터 경로는 TX(송신)와 RX(수신)에서 서로 다른 동시성 모델을 사용합니다. 이 차이를 이해하는 것이 드라이버 개발의 핵심입니다.

TX 경로: qdisc 잠금이 큐별로 직렬화를 제공합니다. 멀티큐 드라이버에서 각 TX 큐는 독립적인 qdisc 잠금을 가지므로, 서로 다른 큐에 대한 ndo_start_xmit() 호출은 병렬로 실행됩니다. 동일 큐에 대해서는 qdisc 잠금이 직렬화를 보장합니다.

RX 경로: NAPI poll은 동일 NAPI 인스턴스에 대해 직렬화가 보장됩니다. NAPI_STATE_SCHED 비트가 설정된 동안에는 해당 NAPI의 poll 함수가 다른 CPU에서 동시에 호출되지 않습니다. 단, 서로 다른 NAPI 인스턴스(멀티큐)는 병렬로 실행됩니다.

완료 인터럽트(Completion Interrupt)와 NAPI 컨텍스트의 상호작용: TX 완료 처리가 RX NAPI poll 안에서 이루어지는 구조(shared NAPI)에서는 TX 완료와 RX 처리가 동일 NAPI 컨텍스트에서 직렬화됩니다. 반면 별도의 TX 완료 인터럽트를 사용하는 경우, TX 완료 경로와 TX 전송 경로 사이의 동기화를 드라이버가 직접 관리해야 합니다.

net_device 락 계층 구조와 적용 범위 RTNL Mutex (제어 경로 전체) register/unregister, open/close, 주소 변경, MTU 변경, ethtool 설정 TX qdisc Lock (큐별) ndo_start_xmit 직렬화, netif_tx_lock NAPI State Bit (인스턴스별) NAPI_STATE_SCHED로 poll 직렬화 TX Queue 0 qdisc_lock(q) TX Queue 1 qdisc_lock(q) NAPI 0 (RX Q0) napi_poll() NAPI 1 (RX Q1) napi_poll() 병렬 실행 가능 병렬 실행 가능 RCU Read-Side (락 없는 읽기) XDP prog 참조, rx_handler, filter table, neigh entry, route lookup u64_stats_sync (통계 경로) per-CPU 카운터 읽기/쓰기 — 32비트 시스템에서만 실제 동기화 DMA/Memory Barriers: dma_wmb(), smp_wmb() — descriptor ring 순서 보장
네트워크 드라이버는 경로별로 다른 동기화 메커니즘을 조합합니다. RTNL이 최상위, RCU/u64_stats가 최하위 오버헤드(Overhead)입니다.
잠금 유형보호 대상컨텍스트범위오버헤드
rtnl_mutex장치 설정, 등록/해제프로세스(Process)전역 (per-ns v6.13+)높음 (sleep 가능)
netif_tx_lockTX 큐별 전송BHper-queue중간
NAPI_STATE_SCHEDNAPI poll 직렬화softirq/kthreadper-NAPI낮음 (비트 연산)
RCUXDP prog, rx_handler모든 컨텍스트읽기 경로 전역매우 낮음
u64_stats_sync64비트 통계 카운터NAPI/BH (쓰기)per-CPU0 (64비트) / 낮음 (32비트)
dma_wmb()DMA descriptor 순서모든 컨텍스트CPU-장치 간아키텍처 의존

메모리 순서와 DMA 배리어(Barrier)

네트워크 드라이버에서 DMA 배리어는 CPU가 작성한 디스크립터(Descriptor)가 장치에 올바른 순서로 보이도록 보장합니다. 배리어 유형을 잘못 선택하면 패킷 손실이나 DMA 오류가 발생합니다.

배리어용도x86ARM64사용 시점
dma_wmb()DMA 쓰기 순서 보장(Ordering)컴파일러 배리어dmb(oshst)디스크립터 필드 쓰기 후, ownership 비트 설정 전
dma_rmb()DMA 읽기 순서 보장컴파일러 배리어dmb(oshld)ownership 비트 확인 후, 데이터 읽기 전
wmb()모든 쓰기 순서 보장sfencedsb(st)MMIO doorbell 쓰기 전
smp_wmb()SMP 쓰기 순서 보장컴파일러 배리어dmb(ishst)CPU 간 공유 데이터 순서 보장
readl()/writel()MMIO 접근암묵적 순서 보장암묵적 순서 보장레지스터 doorbell, CSR 접근
/* 올바른 TX 디스크립터 쓰기 순서 예시 */
static netdev_tx_t my_start_xmit(struct sk_buff *skb,
                                   struct net_device *ndev)
{
    struct my_tx_desc *desc = my_get_next_tx_desc(ring);

    /* 1단계: 디스크립터 필드 채우기 */
    desc->buf_addr = dma_map_single(dev, skb->data,
                                      skb->len, DMA_TO_DEVICE);
    desc->len = cpu_to_le16(skb->len);
    desc->vlan_tag = cpu_to_le16(skb_vlan_tag_get(skb));

    /* 2단계: dma_wmb() — 위의 필드가 장치에 먼저 보이도록 보장 */
    dma_wmb();

    /* 3단계: ownership 비트 설정 — 장치가 이 디스크립터를 처리 시작 */
    desc->cmd_type_len = cpu_to_le32(MY_TXD_CMD_EOP | MY_TXD_CMD_RS |
                                       skb->len);

    /* 4단계: wmb() — MMIO 쓰기 전에 모든 메모리 쓰기 완료 보장 */
    wmb();

    /* 5단계: doorbell — 장치에 새 디스크립터 알림 */
    writel(ring->next_to_use, ring->tail_reg);

    return NETDEV_TX_OK;
}

/* 올바른 RX 디스크립터 읽기 순서 예시 */
static int my_clean_rx(struct my_rx_ring *ring, int budget)
{
    while (work_done < budget) {
        struct my_rx_desc *desc = &ring->desc[ring->next_to_clean];

        /* 1단계: ownership/status 비트 확인 */
        u32 status = le32_to_cpu(desc->status);
        if (!(status & MY_RXD_STAT_DD))
            break;  /* 장치가 아직 소유 중 */

        /* 2단계: dma_rmb() — status 읽기 후, 나머지 필드 읽기 전 */
        dma_rmb();

        /* 3단계: 이제 길이, 체크섬 등 안전하게 읽기 가능 */
        len = le16_to_cpu(desc->length);
        vlan = le16_to_cpu(desc->vlan_tag);

        my_process_rx_packet(ring, desc, len);
        work_done++;
    }
    return work_done;
}
x86 vs ARM64 배리어 차이: x86은 강한 메모리 순서(TSO, Total Store Order) 모델이므로 dma_wmb()smp_wmb()가 단순 컴파일러 배리어로 충분합니다. 반면 ARM64는 약한 메모리 순서 모델이므로 실제 하드웨어 배리어 명령어(dmb)가 필요합니다. 드라이버 코드에서는 항상 커널 배리어 API를 사용하여 아키텍처별 차이를 추상화해야 합니다.

검증 매트릭스: 릴리스 전 체크 항목

영역필수 검증합격 기준 예시
기능up/down, MTU, VLAN, bridge, bond10k회 반복 시 누수/lockup 없음
성능단일/다중 스트림, 작은 패킷/점보 프레임목표 PPS/Gbps 달성, drop rate 임계 이내
안정성link flap, reset storm, hotplug, suspend/resume자동 복구 성공, 수동 재로드 불필요
가시성ethtool/stat/devlink health dump장애 원인 식별 가능한 텔레메트리 제공
보안XDP/tc rule 경계값, malformed packetcrash 없이 drop/에러 처리

Bring-up 30분 체크리스트

  1. 장치 인식 확인
    lspci -nn, dmesg | grep -i <driver>로 probe 성공 여부 확인
  2. 링크 기본 동작
    ip link set dev eth0 up, ethtool eth0로 speed/duplex/link 검증
  3. 기본 송수신
    ping, 단일 iperf3로 RX/TX 모두 정상 동작 확인
  4. 오프로드/큐 설정
    ethtool -k/-l/-x 확인 후 장비 정책에 맞게 조정
  5. 에러 카운터 스냅샷
    ethtool -S eth0 초기값 저장, 10분 부하 후 증분 비교
  6. 리셋 복구
    의도적 link flap 또는 함수 리셋 후 자동 복구 성공 여부 확인
  7. 관측/알람 연계
    링크/에러/reset 지표가 모니터링 시스템에 수집되는지 검증

코드 리뷰 체크리스트 (net_device 전용)