네트워크 네임스페이스 심화

리눅스 네트워크 네임스페이스 내부 구조, clone/unshare/setns 시스템 콜, VRF 멀티테넌트 NGFW, veth/macvlan/bridge 간 패킷 전달, 컨테이너 네트워킹(Docker/K8s) 연관, netfilter 격리 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: 네임스페이스, 네트워크 스택, 라우팅 문서를 먼저 읽으세요. 네트워크 네임스페이스는 리눅스 네임스페이스 중 가장 복잡한 격리 단위이며, 커널 네트워크 스택 전반에 대한 이해를 전제로 합니다.
일상 비유: 네트워크 네임스페이스는 같은 건물 안의 독립된 사무실과 같습니다. 각 사무실(네임스페이스)은 자체 전화번호(IP), 내선 시스템(라우팅), 보안 규칙(방화벽)을 가집니다. 사무실 간 통화는 복도(veth 쌍)를 통해 이루어지며, 한 사무실의 내선 규칙이 다른 사무실에 영향을 주지 않습니다.

핵심 요약

  • struct net — 네트워크 네임스페이스의 커널 표현, 모든 네트워크 자원을 포함합니다.
  • clone(CLONE_NEWNET) — 새 네트워크 네임스페이스를 생성하는 시스템 콜입니다.
  • veth 쌍 — 두 네임스페이스를 연결하는 가상 이더넷 케이블이며, 한쪽에서 전송하면 반대쪽에서 수신합니다.
  • 격리 범위 — 라우팅 테이블, 인터페이스, iptables/nftables, conntrack, 소켓, ARP 캐시가 독립됩니다.
  • VRF + netns — 멀티테넌트 NGFW 구성에서 테넌트별 완전 격리를 제공합니다.
  • Docker/K8s — 컨테이너마다 별도의 네트워크 네임스페이스를 생성하고 CNI 플러그인으로 연결합니다.
  • ip netns — /var/run/netns/ 하의 바인드 마운트로 네임스페이스를 영속화합니다.
  • Netfilter 격리 — 각 네임스페이스는 독립적인 iptables/nftables 체인을 가집니다.
  • 영속성(Persistence) — bind mount로 프로세스 없이도 netns를 유지하며, /proc/PID/ns/net 심볼릭 링크가 inode 번호로 netns를 식별합니다.
  • pause 컨테이너 — K8s Pod 내 모든 컨테이너가 pause 컨테이너의 netns를 공유하며, pause가 netns 수명을 관리합니다.
  • 보안 강화 — Seccomp으로 unshare/setns syscall을 제한하고, auditd+eBPF로 netns 생성을 감사합니다.
  • cleanup_net() — 참조 카운트가 0이 되면 워크큐에서 pernet_operations.exit()를 역순 호출하여 자원을 정리합니다.
  • sysctl per-netns — ip_forward, conntrack_max 등 대부분의 네트워크 sysctl이 netns별로 독립이며, 새 netns는 커널 기본값으로 초기화됩니다.
  • XDP/TC BPF — veth에서 XDP redirect, TC BPF 필터를 per-netns로 적용하여 컨테이너 네트워킹 성능을 10배 이상 향상시킵니다.
  • NETNS_ID — 커널 4.9+ 에서 정수 ID를 부여하여 cross-netns 디바이스 참조 및 netlink 기반 모니터링을 지원합니다.

단계별 이해

  1. 격리 범위 파악
    어떤 네트워크 자원이 네임스페이스 경계로 분리되는지 표로 확인합니다. 인터페이스, 라우팅 테이블, conntrack, 소켓 등이 완전히 독립됩니다.
  2. struct net 구조 이해
    커널이 네임스페이스를 struct net으로 표현하며, 내부에 라우팅 테이블, 인터페이스 리스트, Netfilter 훅 테이블이 포함됩니다.
  3. API 실습
    ip netns add, ip netns exec로 네임스페이스를 생성하고 진입하는 기본 패턴을 익힙니다.
  4. veth 연결
    두 네임스페이스를 veth 쌍으로 연결하고 ping으로 통신을 검증합니다. macvlan/ipvlan과의 차이를 비교합니다.
  5. 컨테이너 연결 구조 분석
    Docker/K8s가 내부적으로 netns + veth + bridge를 어떻게 조합하는지 추적합니다.
  6. 진단 도구 활용
    ip netns list, nsenter, lsns, bpftrace로 실행 중인 네임스페이스를 점검합니다.
  7. 영속성 메커니즘 이해
    ip netns add가 내부적으로 unshare + mount --bind를 수행함을 이해하고, Named vs Anonymous netns의 차이를 파악합니다. /proc/PID/ns/net inode 번호로 두 프로세스가 같은 netns인지 확인합니다.
  8. K8s 네트워크 심화 분석
    pause 컨테이너 netns 공유 구조, CNI 체인(main + meta 플러그인), Flannel/Calico/Cilium의 각기 다른 데이터 경로를 추적합니다. ip netns exec cni-* ip route로 Pod 내부 라우팅을 직접 확인합니다.
  9. sysctl/tc 격리 확인
    호스트에서 설정한 ip_forward, conntrack_max 등이 새 netns에 자동 적용되지 않음을 확인합니다. tc qdisc/filter도 디바이스(netns)에 종속됩니다.
  10. XDP/TC BPF 실습
    veth에 TC BPF 프로그램을 부착하여 per-netns 패킷 필터링과 bpf_redirect_peer() 고속 경로를 테스트합니다.
  11. 실습 랩 수행
    3-tier 마이크로서비스(frontend/backend/database)를 각 netns에 배치하고, nftables로 계층 간 접근 제어를 직접 구현합니다.

개요: 네트워크 격리의 핵심

리눅스 네트워크 네임스페이스(network namespace)는 커널 4.0 이전부터 제공된 격리 메커니즘으로, 하나의 호스트 위에서 완전히 독립된 네트워크 스택을 여러 개 운영할 수 있게 합니다. 각 네트워크 네임스페이스는 자체 네트워크 인터페이스, 라우팅 테이블, ARP 캐시, 소켓 테이블, Netfilter 규칙, conntrack 테이블을 가집니다.

컨테이너 기술(Docker, Kubernetes, LXC)이 급속히 확산되면서 네트워크 네임스페이스는 현대 클라우드 인프라의 핵심 격리 단위가 되었습니다. 또한 멀티테넌트 방화벽(NGFW), VPN 게이트웨이, 소프트웨어 정의 네트워킹(SDN)에서도 VRF(Virtual Routing and Forwarding)와 결합하여 강력한 격리를 제공합니다.

초기화 네임스페이스: 시스템 부팅 시 생성되는 최초의 네트워크 네임스페이스는 init_net이라 불리며, net/core/net_namespace.c에 정적으로 선언됩니다. 모든 후속 네임스페이스는 이 초기 네임스페이스로부터 파생됩니다.

네트워크 네임스페이스 격리 범위

자원 유형 격리 여부 커널 필드 비고
네트워크 인터페이스 완전 격리 net->dev (hlist) lo 인터페이스도 각 netns마다 별도 존재
라우팅 테이블 완전 격리 net->ipv4.fib_main FIB 트리 자체가 netns 내부에 위치
ARP / NDP 캐시 완전 격리 net->ipv4.arp_tbl neigh_table 구조체
소켓 테이블 완전 격리 net->ipv4.tcp_death_row bind/listen/connect 모두 netns 범위
iptables / nftables 완전 격리 net->nf 각 netns가 독립 체인/테이블 보유
conntrack 테이블 완전 격리 net->ct nf_conntrack_net 구조체
IPVS / LVS 완전 격리 net->ipvs netns_ipvs 구조체
sysctl 네트워크 파라미터 완전 격리 net->ipv4.sysctl_* ip_forward, rp_filter 등
물리 NIC 드라이버 이동 가능 dev->nd_net ip link set dev eth0 netns ns1
tc (트래픽 제어) 완전 격리 qdisc per-device 디바이스 이동 시 함께 이동

네임스페이스 내부 구조

커널은 각 네트워크 네임스페이스를 struct net 구조체로 표현합니다. 이 구조체는 include/net/net_namespace.h에 정의되어 있으며, 네트워크 스택 전체가 이 구조체를 통해 자신이 속한 네임스페이스를 참조합니다.

struct net 주요 필드 상세

필드 타입 역할
passive refcount_t 소멸 방지용 참조 카운트. 0이 되면 cleanup_net() 실행
dev_base_seq unsigned int 디바이스 변경 감지 시퀀스 번호 (netlink 동기화)
ifindex int 다음 할당할 인터페이스 인덱스 (netns 내 단조 증가)
dev_base_head struct list_head netns 내 모든 net_device 연결 리스트
dev_name_head struct hlist_head* 인터페이스 이름 → net_device 해시 테이블
dev_index_head struct hlist_head* ifindex → net_device 해시 테이블
ipv4 struct netns_ipv4 IPv4 서브시스템: fib_main, fib_local, arp_tbl, sysctl_* 포함
ipv6 struct netns_ipv6 IPv6 서브시스템: fib6_root, nd_tbl, ip6_null_entry 포함
nf struct netns_nf Netfilter 훅 등록 테이블 (hooks_ipv4[NF_INET_NUMHOOKS])
xt struct netns_xt xtables(iptables) 상태: 활성 테이블 목록
ct struct netns_ct conntrack 해시 테이블, GC 워커, max/count 제한
ipvs struct netns_ipvs* IPVS(LVS) 상태 (CONFIG_IP_VS 빌드 시)
user_ns struct user_namespace* 소유 User Namespace (CAP_NET_ADMIN 판단 기준)
ns struct ns_common /proc/[pid]/ns/net 노드 (inum: inode 번호)
rtnl struct sock* RTNL netlink 소켓 (ip 명령 통신)
rules_ops struct list_head FIB 정책 규칙 목록 (ip rule 항목)
ns_list struct list_head 전역 net_namespace_list 연결 (for_each_net 순회)
/* include/net/net_namespace.h (주요 필드 발췌) */
struct net {
    /* 참조 카운트 및 식별자 */
    refcount_t          passive;        /* 소멸 방지용 참조 카운트 */
    spinlock_t          rules_mod_lock;
    unsigned int        dev_unreg_count;
    unsigned int        dev_base_seq;   /* 디바이스 변경 시퀀스 번호 */
    int                 ifindex;        /* 다음 할당할 인터페이스 인덱스 */

    /* 디바이스 관련 */
    struct list_head    dev_base_head;  /* 모든 netdev 연결 리스트 */
    struct hlist_head  *dev_name_head;  /* 이름으로 조회하는 해시 테이블 */
    struct hlist_head  *dev_index_head; /* ifindex로 조회하는 해시 테이블 */

    /* IPv4 서브시스템 */
    struct netns_ipv4   ipv4;           /* fib_main, fib_local, arp_tbl 포함 */

    /* IPv6 서브시스템 */
    struct netns_ipv6   ipv6;           /* fib6_root, nd_tbl 포함 */

    /* Netfilter */
    struct netns_nf     nf;             /* 훅 등록 테이블 */
    struct netns_xt     xt;             /* xtables (iptables) 상태 */

    /* Connection tracking */
    struct netns_ct     ct;             /* conntrack 해시 테이블, GC */

    /* IPVS */
    struct netns_ipvs  *ipvs;           /* LVS 상태 (CONFIG_IP_VS) */

    /* 사용자 공간 식별자 */
    struct user_namespace *user_ns;     /* 소유 User Namespace */
    struct ns_common   ns;              /* /proc/[pid]/ns/net 노드 */

    /* 소켓 관련 */
    struct sock        *rtnl;           /* RTNL 소켓 (netlink) */
    struct sock        *genl_sock;      /* Generic Netlink 소켓 */

    struct list_head    rules_ops;      /* FIB 규칙 목록 */
    struct list_head    ns_list;        /* 전역 net_namespace_list 연결 */
};

get_net() / put_net() 참조 카운팅

/* include/net/net_namespace.h */

/* netns 참조 획득: refcount 증가 */
static inline struct net *get_net(struct net *net)
{
    refcount_inc(&net->ns.count);
    return net;
}

/* netns 참조 해제: refcount 감소, 0이면 cleanup 스케줄 */
static inline void put_net(struct net *net)
{
    if (refcount_dec_and_test(&net->ns.count))
        __put_net(net);   /* net/core/net_namespace.c: cleanup_net() 호출 */
}

/* 전역 netns 목록 순회 (RCU read lock 필요) */
/* for_each_net(net)은 net_namespace_list를 순회 */
#define for_each_net(VAR)                   \
    list_for_each_entry_rcu(VAR, &net_namespace_list, list)

/* 커널 내부에서 netns 순회 예시 */
struct net *net;
rcu_read_lock();
for_each_net(net) {
    /* 모든 활성 netns에 대해 처리 */
    printk("netns inum=%u\n", net->ns.inum);
}
rcu_read_unlock();

새 네임스페이스 생성: copy_net_ns()

프로세스가 clone(CLONE_NEWNET) 또는 unshare(CLONE_NEWNET)를 호출하면 커널은 copy_net_ns()를 통해 새 네트워크 네임스페이스를 생성합니다.

/* net/core/net_namespace.c */
struct net *copy_net_ns(unsigned long flags,
                        struct user_namespace *user_ns,
                        struct net *old_net)
{
    struct net *net;
    int rv;

    /* CLONE_NEWNET 플래그가 없으면 기존 netns 반환 */
    if (!(flags & CLONE_NEWNET))
        return get_net(old_net);

    net = net_alloc();               /* kmem_cache_alloc으로 struct net 할당 */
    if (!net)
        return ERR_PTR(-ENOMEM);

    get_user_ns(user_ns);
    net->user_ns = user_ns;

    rv = setup_net(net, user_ns);    /* 서브시스템별 초기화 콜백 호출 */
    if (rv < 0) {
        put_user_ns(user_ns);
        net_free(net);
        return ERR_PTR(rv);
    }

    /* 전역 목록에 등록 */
    down_write(&net_rwsem);
    list_add_tail_rcu(&net->list, &net_namespace_list);
    up_write(&net_rwsem);

    return net;
}

/* setup_net(): 각 pernet_operations의 init() 콜백을 순서대로 실행 */
static __net_init int setup_net(struct net *net, struct user_namespace *user_ns)
{
    const struct pernet_operations *ops, *saved_ops;
    int error = 0;

    atomic64_set(&net->net_cookie, atomic64_inc_return(&cookie_gen));
    net->user_ns = user_ns;
    idr_init(&net->netns_ids);

    list_for_each_entry(ops, &pernet_list, list) {
        if (ops->init) {
            error = ops->init(net);   /* IPv4, IPv6, Netfilter 등 초기화 */
            if (error < 0)
                goto out_undo;
        }
    }
    return 0;
}

setup_net()은 각 서브시스템이 등록한 pernet_operations 콜백의 init() 함수를 순서대로 호출합니다. 예를 들어 IPv4는 ipv4_net_ops.init에서 FIB 테이블을 초기화하고, Netfilter는 nf_net_ops.init에서 훅 테이블을 초기화합니다.

Host netns (init_net) eth0, lo (물리/가상 NIC) FIB 테이블 (fib_main, local) iptables / nftables 체인 conntrack (net->ct) ARP 캐시 / 소켓 테이블 veth0 (peer: veth1 in ns1) docker0 bridge veth netns: ns1 (컨테이너1) veth1, lo (격리된 인터페이스) 독립 FIB 테이블 독립 nftables / conntrack netns: ns2 (컨테이너2) veth3, lo (격리된 인터페이스) 독립 FIB 테이블 독립 nftables / conntrack 각 netns = struct net 인스턴스

그림 1. 호스트 netns와 테넌트 netns 간의 격리 구조. 각 netns는 독립된 인터페이스, 라우팅 테이블, Netfilter 규칙, conntrack 테이블을 가집니다.

cleanup_net(): 네임스페이스 소멸 경로

네트워크 네임스페이스의 참조 카운트가 0이 되면 __put_net()cleanup_net 워크큐에 작업을 등록합니다. 워크큐 스레드가 비동기로 모든 서브시스템의 pernet_operations.exit() 콜백을 역순으로 호출하여 네임스페이스 자원을 정리합니다.

/* net/core/net_namespace.c */

/* 참조 카운트 → 0: 소멸 스케줄 */
static void __put_net(struct net *net)
{
    /* cleanup_list에 추가 후 워크큐에 스케줄 */
    queue_work(netns_wq, &net_cleanup_work);
}

/* 워크큐 핸들러: 비동기 정리 */
static void cleanup_net(struct work_struct *work)
{
    struct net *net;
    struct list_head net_kill_list;

    /* 1. 전역 netns 목록에서 제거 */
    down_write(&net_rwsem);
    list_del_rcu(&net->list);
    up_write(&net_rwsem);

    /* 2. RCU grace period 대기 */
    synchronize_rcu();

    /* 3. pernet_operations.exit()를 등록 역순으로 호출 */
    list_for_each_entry_reverse(ops, &pernet_list, list) {
        if (ops->exit)
            ops->exit(net);
        /* IPv4: FIB 테이블 해제 */
        /* Netfilter: 훅/체인/테이블 해제 */
        /* conntrack: 해시 항목 정리 */
        /* lo 인터페이스: unregister */
    }

    /* 4. pernet_operations.exit_batch()도 호출 */
    list_for_each_entry_reverse(ops, &pernet_list, list) {
        if (ops->exit_batch)
            ops->exit_batch(&net_kill_list);
    }

    /* 5. struct net 메모리 해제 */
    net_free(net);  /* kmem_cache_free() */
}
네트워크 네임스페이스 수명주기 생성 clone(CLONE_NEWNET) unshare(CLONE_NEWNET) net_alloc() struct net 할당 kmem_cache setup_net() pernet_ops.init() 순회 IPv4/IPv6/NF/conntrack lo 인터페이스 생성 활성 (Active) net_namespace_list 등록 refcount > 0 프로세스/fd 참조 Pin (영속화) bind mount /var/run/netns/ refcount → 0 put_net() refcount_dec_and_test __put_net() netns_wq에 스케줄 cleanup_net() list_del_rcu() synchronize_rcu() pernet_ops.exit() 역순 net_free() kmem_cache_free pernet_operations.exit() 콜백 체인 (등록 역순) loopback_exit() conntrack_exit() nf_exit() fib_exit() dev_proc_net_exit() lo unregister → conntrack 해시 정리 → Netfilter 훅 해제 → FIB 트리 해제 → /proc/net 해제

그림 8. netns 수명주기. 생성(clone/unshare) → setup_net() 초기화 → 활성 사용 → 참조 카운트 0 → cleanup_net() 비동기 정리 → net_free() 메모리 해제.

cleanup_net 지연 문제: cleanup_net()은 비동기 워크큐(netns_wq)에서 실행됩니다. 대규모 netns 소멸 시(예: 수백 개 컨테이너 동시 종료) 워크큐가 병목이 되어 소멸이 지연될 수 있습니다. 커널 5.7+에서는 cleanup_net을 배치로 처리하여(net_kill_list) 성능이 개선되었습니다. dmesg | grep "net_namespace"로 정리 지연 로그를 확인할 수 있습니다.

시스템 콜 API (clone/unshare/setns)

네트워크 네임스페이스를 조작하는 세 가지 핵심 시스템 콜이 있습니다. 각각 생성, 분리, 진입의 역할을 담당합니다.

clone(CLONE_NEWNET)

/* 새 netns에서 자식 프로세스 시작 */
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static char child_stack[1 << 20];  /* 1 MiB 스택 */

static int child_fn(void *arg) {
    /* 이 함수는 새 network namespace에서 실행됨 */
    /* /proc/self/ns/net 은 부모와 다른 inode를 가짐 */
    system("ip link");       /* lo만 보임 */
    system("ip route");      /* 비어 있음 */
    return 0;
}

int main(void) {
    pid_t pid = clone(child_fn, child_stack + sizeof(child_stack),
                      CLONE_NEWNET | SIGCHLD, NULL);
    if (pid < 0) { perror("clone"); return 1; }
    waitpid(pid, NULL, 0);
    return 0;
}

unshare(CLONE_NEWNET)

/* 현재 프로세스를 새 netns로 분리 */
#define _GNU_SOURCE
#include <sched.h>

int main(void) {
    /* CAP_SYS_ADMIN 또는 User NS 내 CAP_NET_ADMIN 필요 */
    if (unshare(CLONE_NEWNET) != 0) {
        perror("unshare");
        return 1;
    }
    /* 이후 network 관련 시스템 콜은 새 netns에서 동작 */
    execv("/bin/bash", (char*[]){"/bin/bash", NULL});
    return 0;
}

/* 셸에서 동일 효과 */
/* $ unshare --net /bin/bash */

setns(fd, CLONE_NEWNET)

/* 기존 netns에 진입 */
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    /* argv[1]: /proc/[pid]/ns/net 또는 /var/run/netns/[name] */
    int fd = open(argv[1], O_RDONLY | O_CLOEXEC);
    if (fd < 0) { perror("open"); return 1; }

    /* CLONE_NEWNET: 이 fd가 network namespace임을 명시 */
    if (setns(fd, CLONE_NEWNET) != 0) {
        perror("setns");
        return 1;
    }
    close(fd);

    /* 이제 argv[1]이 가리키는 netns에서 실행 중 */
    execv("/bin/bash", (char*[]){"/bin/bash", NULL});
    return 0;
}

/* nsenter 유틸리티는 위 패턴을 구현한 것 */
/* $ nsenter --net=/var/run/netns/ns1 /bin/bash */

ip netns 명령 내부 동작

iproute2ip netns 명령은 다음과 같이 동작합니다.

# ip netns add ns1 내부 동작:
# 1. unshare(CLONE_NEWNET) → 새 netns 생성
# 2. /var/run/netns/ns1 파일 생성 (빈 파일)
# 3. /proc/self/ns/net 를 /var/run/netns/ns1 에 bind mount
#    → 프로세스가 종료되어도 netns 파일 디스크립터가 살아있음

# ip netns exec ns1 cmd 내부 동작:
# 1. open("/var/run/netns/ns1", O_RDONLY) → fd 획득
# 2. setns(fd, CLONE_NEWNET) → ns1으로 이동
# 3. execv(cmd, ...) → 명령 실행

# 실용 명령어 시퀀스
ip netns add ns1                         # netns 생성
ip netns add ns2                         # netns 생성
ip netns list                            # 목록 (inode 번호 포함)
ip netns identify $$                     # 현재 셸의 netns 이름
ip netns exec ns1 ip link                # ns1 내 인터페이스 조회
ip netns exec ns1 ip route               # ns1 내 라우팅 테이블 조회
ip netns exec ns1 bash                   # ns1 내 셸 진입

# netns 삭제 (bind mount 해제 → 참조 카운트 0이면 소멸)
ip netns delete ns1

veth 쌍으로 네임스페이스 연결

veth(virtual Ethernet) 쌍은 두 네임스페이스를 연결하는 가장 기본적인 수단입니다. 두 개의 가상 인터페이스가 내부적으로 직접 연결되어 있어, 한쪽으로 들어온 패킷은 즉시 반대쪽 인터페이스에서 수신됩니다.

veth 커널 구현

veth 드라이버는 drivers/net/veth.c에 구현되어 있습니다. veth_xmit()은 패킷을 peer 인터페이스의 수신 큐에 직접 전달합니다.

/* drivers/net/veth.c - 핵심 전송 함수 (간략화) */
static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
{
    struct veth_priv *priv = netdev_priv(dev);
    struct net_device *rcv = rcu_dereference(priv->peer); /* peer 인터페이스 */

    if (unlikely(!rcv)) {
        kfree_skb(skb);
        return NETDEV_TX_OK;
    }

    /* skb의 dev를 peer로 교체하여 수신 경로 진입 */
    skb->dev = rcv;
    /* XDP나 GRO 오프로드가 없으면 직접 netif_rx() 호출 */
    netif_rx(skb);
    return NETDEV_TX_OK;
}

veth 쌍 생성 및 네임스페이스 이동

# 두 netns 생성
ip netns add ns1
ip netns add ns2

# veth 쌍 생성 (둘 다 host netns에 먼저 생성됨)
ip link add veth0 type veth peer name veth1

# 각 인터페이스를 해당 netns로 이동
ip link set veth0 netns ns1
ip link set veth1 netns ns2

# 각 netns 내에서 IP 설정 및 활성화
ip netns exec ns1 ip addr add 10.0.1.1/24 dev veth0
ip netns exec ns1 ip link set veth0 up
ip netns exec ns1 ip link set lo up

ip netns exec ns2 ip addr add 10.0.1.2/24 dev veth1
ip netns exec ns2 ip link set veth1 up
ip netns exec ns2 ip link set lo up

# 통신 확인
ip netns exec ns1 ping -c 3 10.0.1.2

# 인터넷 연결을 위한 호스트 브리지 경유 설정
ip link add br0 type bridge
ip link add veth-host0 type veth peer name veth-ns0
ip link set veth-ns0 netns ns1
ip link set veth-host0 master br0
ip netns exec ns1 ip route add default via 10.0.2.1
# 호스트에서 NAT 활성화
iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -j MASQUERADE

veth 통계 및 진단

# ethtool로 veth 드라이버 통계 확인
ethtool -S veth0
# NIC statistics:
#      peer_ifindex: 5          ← peer 인터페이스 ifindex
#      rx_queue_index: 0
#      ...

# veth 쌍의 peer 확인 (ifindex로 역추적)
ip -d link show veth0 | grep peer_ifindex
# peer ifindex: 5
ip link show | grep "^5:"   # ifindex 5가 어느 인터페이스인지 확인

# 다른 netns의 peer 찾기
# peer ifindex 5가 ns1의 veth1이라면:
ip netns exec ns1 ip link show

# veth 처리량 실시간 확인
watch -n 1 "ip netns exec ns1 ip -s link show veth0"

ipvlan 예제

# ipvlan L3 모드: MAC 공유, IP 분리, 라우팅 기반
# 부모 인터페이스(eth0)에 ipvlan 서브인터페이스 생성
ip link add ipvlan0 link eth0 type ipvlan mode l3

# netns로 이동
ip link set ipvlan0 netns ns1
ip netns exec ns1 ip addr add 10.0.3.1/24 dev ipvlan0
ip netns exec ns1 ip link set ipvlan0 up

# 호스트에서 라우팅 설정 (L3 모드는 라우팅 필요)
ip route add 10.0.3.0/24 dev eth0

# ipvlan L2 모드: MAC 공유 + L2 브로드캐스트 도메인 공유
ip link add ipvlan1 link eth0 type ipvlan mode l2
ip link set ipvlan1 netns ns2

# 모드 비교:
# l2: 동일 브로드캐스트 도메인, MAC 공유, ARP 통과
# l3: 라우팅 기반, 다른 서브넷 가능, ARP 없음 (커널이 직접 전달)
# l3s: l3 + conntrack/방화벽 지원 (s=strict)

veth vs macvlan vs ipvlan 비교

항목 veth macvlan ipvlan (L2) ipvlan (L3)
MAC 주소 각자 독립 각자 독립 부모와 공유 부모와 공유
IP 주소 자유 자유 자유 자유
호스트 ↔ 컨테이너 통신 브리지 필요 bridge 모드만 비권장 가능
ARP 독립성 완전 독립 독립 공유 공유
promiscuous 모드 필요 불필요 필요 (일부) 불필요 불필요
주요 사용처 Docker, K8s VM-like 격리 고밀도 컨테이너 L3 라우팅 분리
커널 드라이버 drivers/net/veth.c drivers/net/macvlan.c drivers/net/ipvlan/ drivers/net/ipvlan/

macvlan 심화 실습

macvlan은 부모 인터페이스의 MAC 주소를 복제하지 않고 독립 MAC을 할당하여 가상 서브인터페이스를 만듭니다. veth처럼 브리지가 필요 없어 오버헤드가 적습니다. 4가지 모드(bridge/vepa/private/passthru)에 따라 트래픽 흐름이 달라집니다.

# macvlan bridge 모드: 같은 부모의 macvlan 간 직접 통신
ip link add macvlan0 link eth0 type macvlan mode bridge
ip link set macvlan0 netns ns1
ip netns exec ns1 ip addr add 192.168.1.100/24 dev macvlan0
ip netns exec ns1 ip link set macvlan0 up

ip link add macvlan1 link eth0 type macvlan mode bridge
ip link set macvlan1 netns ns2
ip netns exec ns2 ip addr add 192.168.1.101/24 dev macvlan1
ip netns exec ns2 ip link set macvlan1 up

# ns1 ↔ ns2 직접 통신 (bridge 모드: 부모 인터페이스 내부에서 포워딩)
ip netns exec ns1 ping -c 1 192.168.1.101

# macvlan vepa 모드: 모든 트래픽이 외부 스위치 경유
# → 외부 스위치가 hairpin/reflective relay 지원 필요
ip link add macvlan2 link eth0 type macvlan mode vepa

# macvlan private 모드: 같은 부모의 macvlan 간 통신 차단
# → 보안 격리가 필요한 환경에 적합
ip link add macvlan3 link eth0 type macvlan mode private

# macvlan passthru 모드: 부모 인터페이스 독점 사용
# → 1:1 매핑, promiscuous 모드 필요 없음
ip link add macvlan4 link eth0 type macvlan mode passthru

# 주의: macvlan과 부모 인터페이스(호스트)는 직접 통신 불가!
# 호스트 eth0 ↔ macvlan0 간 ping 실패
# 해결: 호스트에도 macvlan 인터페이스를 추가하거나 veth 사용
/* drivers/net/macvlan.c — macvlan 수신 경로 */
static rx_handler_result_t macvlan_handle_frame(
    struct sk_buff **pskb)
{
    /* 수신 패킷의 목적지 MAC으로 해당 macvlan 포트 조회 */
    struct macvlan_port *port = macvlan_port_get_rcu(skb->dev);
    struct macvlan_dev *vlan = macvlan_hash_lookup(port, eth->h_dest);

    if (!vlan)
        return RX_HANDLER_PASS;  /* 해당 MAC 없으면 부모에게 전달 */

    /* macvlan 인터페이스의 netns에서 수신 처리 */
    skb->dev = vlan->dev;
    /* → 이 macvlan이 속한 netns의 네트워크 스택으로 진입 */
    skb->pkt_type = PACKET_HOST;
    netif_rx(skb);
    return RX_HANDLER_CONSUMED;
}

macvlan 4가지 모드 비교

모드 같은 부모 macvlan 간 외부 네트워크 호스트 ↔ macvlan 사용 사례
bridge 직접 통신 가능 불가 컨테이너 간 L2 통신
vepa 외부 스위치 경유 가능 불가 외부 스위치 정책 적용
private 차단 가능 불가 보안 격리 (VM 유사)
passthru 해당 없음 (1:1) 가능 불가 SR-IOV VF 직접 사용

VRF + 네임스페이스 멀티테넌트 NGFW

엔터프라이즈 환경에서는 VRF(Virtual Routing and Forwarding)와 네트워크 네임스페이스를 결합하여 테넌트별 완전 격리된 차세대 방화벽(NGFW)을 구성합니다. 각 테넌트는 자체 netns, 자체 라우팅 테이블, 자체 방화벽 규칙을 가지며, 물리 NIC에서 VLAN 태그를 기반으로 트래픽이 분류됩니다.

NGFW 아키텍처

# 멀티테넌트 NGFW 구성 예시

# 1. VLAN 인터페이스 생성 (물리 NIC: eth0)
ip link add link eth0 name eth0.100 type vlan id 100  # 테넌트 A
ip link add link eth0 name eth0.200 type vlan id 200  # 테넌트 B

# 2. 테넌트별 netns 생성
ip netns add tenant-a
ip netns add tenant-b

# 3. VLAN 인터페이스를 테넌트 netns로 이동
ip link set eth0.100 netns tenant-a
ip link set eth0.200 netns tenant-b

# 4. 테넌트 A netns 내 VRF 설정
ip netns exec tenant-a ip link add vrf-a type vrf table 100
ip netns exec tenant-a ip link set vrf-a up
ip netns exec tenant-a ip link set eth0.100 master vrf-a
ip netns exec tenant-a ip addr add 192.168.100.1/24 dev eth0.100
ip netns exec tenant-a ip link set eth0.100 up

# 5. 테넌트 A 방화벽 규칙 (nftables)
ip netns exec tenant-a nft add table inet filter
ip netns exec tenant-a nft add chain inet filter forward \
    '{ type filter hook forward priority 0; policy drop; }'
ip netns exec tenant-a nft add rule inet filter forward \
    ct state established,related accept
ip netns exec tenant-a nft add rule inet filter forward \
    ip saddr 192.168.100.0/24 tcp dport 443 accept

# 6. VRF exec로 특정 VRF 컨텍스트에서 명령 실행
ip netns exec tenant-a ip vrf exec vrf-a ping 8.8.8.8

# 7. 테넌트 간 트래픽은 물리적으로 격리됨
# (다른 netns이므로 라우팅 테이블, conntrack, 방화벽이 완전히 분리)

NGFW 패킷 흐름

# 외부 → 테넌트 A 서버 패킷 흐름

외부 패킷 (VLAN 100 태그)
  ↓
eth0 (호스트 netns) - VLAN 디먹스
  ↓
eth0.100 (VLAN 서브인터페이스)
  ↓ ip link set eth0.100 netns tenant-a
tenant-a netns 수신
  ↓
VRF 마스터 디바이스 (vrf-a, table 100)
  ↓
nftables PREROUTING 훅 (tenant-a 전용)
  ↓
nftables FORWARD 훅 - 정책 검사
  ↓
FIB 조회 (tenant-a 전용 table 100)
  ↓
목적지 veth → 컨테이너 netns 전달

netns 간 패킷 전달 경로

두 네트워크 네임스페이스 사이의 패킷 전달은 반드시 가상 인터페이스(veth, macvlan 등)를 통해 이루어집니다. 커널의 라우팅 결정은 패킷이 속한 netns의 FIB 테이블을 기준으로 합니다.

/* 패킷이 netns A의 veth0에서 송신될 때 커널 경로 */

veth0->ndo_start_xmit()
  = veth_xmit()
    → rcu_dereference(priv->peer)  /* peer = netns B의 veth1 */
    → skb->dev = peer
    → netif_rx(skb)                /* netns B의 수신 큐에 진입 */

/* netns B에서 수신 */
net_rx_action()
  → ip_rcv()                       /* netns B의 IP 레이어 */ip_rcv_finish()
  → ip_route_input_noref()         /* netns B의 FIB 조회 */ip_forward() 또는 ip_local_deliver()

브리지를 통한 다중 netns 연결

# 호스트에 브리지를 두고 여러 netns의 veth를 연결
ip link add br0 type bridge
ip link set br0 up

for i in 1 2 3; do
    ip netns add ns${i}
    ip link add veth-h${i} type veth peer name veth-c${i}
    ip link set veth-h${i} master br0
    ip link set veth-h${i} up
    ip link set veth-c${i} netns ns${i}
    ip netns exec ns${i} ip addr add 10.10.${i}.1/24 dev veth-c${i}
    ip netns exec ns${i} ip link set veth-c${i} up
    ip netns exec ns${i} ip link set lo up
done

# 브리지 IP 설정 (게이트웨이 역할)
ip addr add 10.10.0.1/16 dev br0

# ns1 → ns2 통신: 브리지에서 L2 포워딩 (같은 서브넷이면 ARP 직접)
ip netns exec ns1 ping 10.10.2.1

# 서로 다른 서브넷이면 브리지의 IP를 게이트웨이로 사용
ip netns exec ns1 ip route add default via 10.10.1.254
# 브리지에서 IP 포워딩 활성화
sysctl -w net.ipv4.ip_forward=1

네임스페이스별 Netfilter 규칙 격리

각 네트워크 네임스페이스는 완전히 독립된 Netfilter 환경을 가집니다. 호스트의 iptables 규칙이 테넌트 netns에 영향을 주지 않으며, 반대도 마찬가지입니다. 이 격리는 struct net 내의 net->nf (struct netns_nf)와 net->xt (struct netns_xt)가 netns별로 독립적으로 관리되기 때문입니다.

nftables 네임스페이스별 적용

# 호스트 netns에서 nftables 규칙 설정
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; policy accept; }'
nft add rule inet filter input tcp dport 22 accept
nft add rule inet filter input drop

# ns1 netns에서 독립적인 nftables 규칙 설정
ip netns exec ns1 nft add table inet filter
ip netns exec ns1 nft add chain inet filter input \
    '{ type filter hook input priority 0; policy drop; }'
ip netns exec ns1 nft add rule inet filter input \
    ct state established,related accept
ip netns exec ns1 nft add rule inet filter input \
    ip saddr 10.0.1.0/24 tcp dport 80 accept

# 두 netns의 규칙은 완전히 독립
# ns1의 규칙 변경이 호스트 netns에 영향 없음
ip netns exec ns1 nft list ruleset

# conntrack도 각 netns별로 독립
ip netns exec ns1 conntrack -L   # ns1의 conntrack만 표시
conntrack -L                     # 호스트의 conntrack만 표시

Netfilter 훅 격리 원리

/* net/netfilter/core.c */
/* nf_register_net_hook()은 특정 netns에만 훅을 등록 */
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *reg)
{
    /* net->nf.hooks[pf][hooknum] 에 등록 */
    /* 다른 netns의 net->nf.hooks는 영향받지 않음 */
}

/* 패킷 처리 시 현재 dev의 netns를 사용 */
static inline int nf_hook(u_int8_t pf, unsigned int hook,
                          struct net *net, struct sock *sk,
                          struct sk_buff *skb, ...)
{
    /* net: 패킷이 속한 netns */
    /* 해당 netns의 훅만 실행됨 */
    hook_entries = rcu_dereference(net->nf.hooks_ipv4[hook]);
}

nftables 테이블 격리 실증

# 호스트 netns에 nftables 규칙 추가
nft add table inet host_filter
nft add chain inet host_filter input \
    '{ type filter hook input priority 0; policy accept; }'
nft add rule inet host_filter input tcp dport 22 accept
nft add rule inet host_filter input drop

# ns1에서는 호스트 규칙이 전혀 보이지 않음 (완전 격리)
ip netns exec ns1 nft list ruleset
# 빈 출력 (ns1에는 아직 규칙 없음)

# ns1 전용 규칙 생성
ip netns exec ns1 nft add table inet ns1_filter
ip netns exec ns1 nft add chain inet ns1_filter forward \
    '{ type filter hook forward priority 0; policy drop; }'
ip netns exec ns1 nft add rule inet ns1_filter forward \
    ct state established,related accept
ip netns exec ns1 nft add rule inet ns1_filter forward \
    ip saddr 10.0.1.0/24 tcp dport { 80, 443 } accept

# 호스트에서 ns1 규칙 확인 (불가 — 완전 격리)
nft list ruleset   # host_filter 만 표시됨

# per-netns iptables 체인 확인
ip netns exec ns1 iptables-save -t filter
# :INPUT ACCEPT [0:0]
# :FORWARD DROP [0:0]
# :OUTPUT ACCEPT [0:0]
# ns1 전용 체인만 출력됨

conntrack per-netns 심화

각 네트워크 네임스페이스는 완전히 독립된 conntrack(연결 추적) 테이블을 가집니다. struct netns_ct가 netns별로 독립적인 해시 테이블, GC 워커, 최대/현재 카운트를 관리하며, 한 netns의 conntrack이 다른 netns에 영향을 주지 않습니다.

/* include/net/netns/conntrack.h */
struct netns_ct {
    atomic_t        count;             /* 현재 conntrack 항목 수 */
    unsigned int    expect_count;      /* expectation 항목 수 */
    struct delayed_work  ecache_dwork;  /* 이벤트 캐시 워커 */
    bool            auto_assign_helper_warned;
    struct ct_pcpu __percpu *pcpu_lists; /* per-CPU unconfirmed 리스트 */
    struct ip_conntrack_stat __percpu *stat;  /* 통계 */
    struct nf_ct_event_notifier __rcu *nf_conntrack_event_cb;
    unsigned int    sysctl_log_invalid;
    int             sysctl_events;
    int             sysctl_acct;
    int             sysctl_tstamp;
};

/* 해시 테이블은 전역이지만 항목 조회 시 netns 필터링 */
/* nf_conntrack_find_get()에서 net 비교: */
if (!nf_ct_is_confirmed(ct))
    continue;
if (!net_eq(nf_ct_net(ct), net))
    continue;  /* 다른 netns의 항목은 무시 */
# per-netns conntrack 독립성 확인

# 호스트와 ns1의 conntrack 항목 완전 분리
conntrack -L 2>/dev/null | wc -l
# 150  (호스트의 conntrack 항목)
ip netns exec ns1 conntrack -L 2>/dev/null | wc -l
# 3    (ns1의 conntrack 항목 — 독립)

# per-netns conntrack 통계
ip netns exec ns1 conntrack -S
# cpu=0  found=12 invalid=2 insert=8 insert_failed=0
# delete=4 delete_list=0 search_restart=0

# per-netns conntrack_max 설정 (독립)
sysctl net.netfilter.nf_conntrack_max
# 262144  (호스트)
ip netns exec ns1 sysctl net.netfilter.nf_conntrack_max
# 65536   (ns1 — 기본값, 호스트와 다름)

# ns1의 conntrack_max를 별도 설정
ip netns exec ns1 sysctl -w net.netfilter.nf_conntrack_max=1024
# → ns1에서만 1024로 제한, 호스트에 영향 없음

# conntrack full 진단 (per-netns)
ip netns exec ns1 cat /proc/sys/net/netfilter/nf_conntrack_count
ip netns exec ns1 cat /proc/sys/net/netfilter/nf_conntrack_max
# count/max 비율이 높으면 conntrack full 위험

# conntrack 이벤트 모니터링 (per-netns)
ip netns exec ns1 conntrack -E
# [NEW] tcp  6 120 SYN_SENT src=10.0.1.2 dst=10.0.1.1 ...
# → ns1에서 발생하는 연결 추적 이벤트만 표시
주의: 커널의 conntrack 해시 테이블 자체는 전역(모든 netns 공유)이지만, 각 항목에 ct->ct_net으로 소속 netns가 기록되어 있어 조회 시 필터링됩니다. 대규모 멀티테넌트 환경(수천 netns)에서는 전역 해시 테이블의 버킷 수 (nf_conntrack_buckets)를 충분히 크게 설정해야 모든 netns의 conntrack 항목이 효율적으로 분산됩니다. nf_conntrack_buckets는 시스템 부팅 시에만 설정 가능하며(modprobe 파라미터), per-netns가 아닌 전역 설정입니다.

컨테이너 네트워킹 (Docker/K8s CNI)

컨테이너 런타임은 네트워크 네임스페이스를 기반으로 컨테이너별 격리된 네트워크를 제공합니다. Docker는 자체 libnetwork를 사용하고, Kubernetes는 CNI(Container Network Interface) 표준을 통해 다양한 네트워크 플러그인을 지원합니다.

Docker 네트워킹 구조

# Docker 컨테이너 시작 시 내부 동작

# 1. 새 netns 생성 (containerd/runc가 수행)
clone(CLONE_NEWNET | CLONE_NEWPID | ...)

# 2. veth 쌍 생성 및 연결
ip link add veth1a2b3c type veth peer name eth0
ip link set eth0 netns /proc/[container_pid]/ns/net

# 3. 호스트 쪽 veth를 docker0 브리지에 연결
ip link set veth1a2b3c master docker0
ip link set veth1a2b3c up

# 4. 컨테이너 netns 내 설정
ip netns exec [container_netns] ip addr add 172.17.0.2/16 dev eth0
ip netns exec [container_netns] ip link set eth0 up
ip netns exec [container_netns] ip route add default via 172.17.0.1

# 5. NAT 규칙 추가 (MASQUERADE)
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
iptables -A FORWARD -o docker0 -j DOCKER
iptables -A DOCKER -i docker0 -j RETURN

# 포트 포워딩 (docker run -p 8080:80)
iptables -t nat -A DOCKER -p tcp --dport 8080 \
    -j DNAT --to-destination 172.17.0.2:80

Kubernetes CNI 구조

Kubernetes의 CNI(Container Network Interface)는 플러그인 방식으로 네트워크를 설정합니다. kubelet이 Pod 생성 시 CNI 플러그인을 호출하며, 플러그인은 표준 ADD/DEL/CHECK 인터페이스를 구현합니다.

/* CNI 플러그인 호출 흐름 */

1. kubelet → container runtime (containerd)
   → containerd-shim → runc → CLONE_NEWNET (새 netns)

2. kubelet → CNI 플러그인 실행 (예: /opt/cni/bin/flannel)
   환경 변수:
     CNI_COMMAND=ADD
     CNI_CONTAINERID=abc123
     CNI_NETNS=/proc/[pid]/ns/net
     CNI_IFNAME=eth0

3. CNI 플러그인 내부 (ADD 커맨드):
   a) veth 쌍 생성
   b) 한쪽을 Pod netns(/proc/[pid]/ns/net)로 이동
   c) IP 주소 할당 (IPAM 플러그인 호출)
   d) 라우팅 설정
   e) 필요시 오버레이 터널 설정 (flannel: VXLAN)

4. 결과 JSON 반환:
   {
     "cniVersion": "0.4.0",
     "interfaces": [{"name": "eth0", "sandbox": "/proc/.../ns/net"}],
     "ips": [{"address": "10.244.1.5/24", "gateway": "10.244.1.1"}]
   }

주요 CNI 플러그인 비교

플러그인 방식 오버레이 NetworkPolicy eBPF
Flannel VXLAN/host-gw VXLAN UDP 8472 미지원 (별도 필요) 미사용
Calico BGP / IPIP IPIP 터널 지원 (Felix) 선택적
Cilium eBPF 직접 VXLAN/Geneve 지원 (eBPF) 완전 eBPF
Weave VXLAN VXLAN 지원 미사용
Multus 메타 플러그인 다중 인터페이스 위임 위임
K8s Node 1 Pod A (netns) eth0: 10.244.1.5/24 독립 라우팅/방화벽 APP (port 8080) Pod B (netns) eth0: 10.244.1.6/24 독립 라우팅/방화벽 APP (port 9090) veth veth cni0 브리지 (10.244.1.1/24) - Host netns iptables / eBPF (kube-proxy / Cilium) flannel.1 (VXLAN 터널 인터페이스) eth0 (물리 NIC) — 호스트 netns K8s Node 2 Pod C (netns) eth0: 10.244.2.5/24 독립 라우팅/방화벽 APP (port 7070) veth cni0 브리지 (10.244.2.1/24) - Host netns iptables / eBPF flannel.1 (VXLAN 터널 인터페이스) eth0 (물리 NIC) — 호스트 netns VXLAN UDP 8472

그림 2. Kubernetes CNI(flannel VXLAN) 패킷 흐름. 각 Pod는 독립 netns를 가지며, veth → cni0 브리지 → VXLAN 터널을 통해 노드 간 통신합니다.

네임스페이스 영속성과 마운트

네트워크 네임스페이스의 수명은 기본적으로 이를 참조하는 프로세스의 수명에 종속됩니다. 하지만 /var/run/netns/에 bind mount하여 프로세스 없이도 네임스페이스를 영속화할 수 있습니다. 이 메커니즘이 ip netns add 명령의 핵심입니다.

네임스페이스 수명주기: 프로세스 참조 vs 파일 참조

커널은 네트워크 네임스페이스의 참조를 두 가지 방식으로 유지합니다.

/* ip netns add ns1 의 내부 구현 (iproute2/ip/ipnetns.c 참고) */

/* 단계 1: 새 netns로 fork */
pid = fork();
if (pid == 0) {
    /* 자식: 새 netns 생성 */
    unshare(CLONE_NEWNET);

    /* 단계 2: /var/run/netns/ns1 파일 생성 (빈 파일) */
    fd = open("/var/run/netns/ns1", O_RDONLY|O_CREAT|O_EXCL, 0);
    close(fd);

    /* 단계 3: 현재 netns를 파일에 bind mount */
    /* /proc/self/ns/net (현재 netns) → /var/run/netns/ns1 */
    mount("/proc/self/ns/net", "/var/run/netns/ns1",
          "none", MS_BIND, NULL);

    /* 자식 프로세스 종료 후에도 bind mount로 netns 유지 */
    _exit(0);
}
waitpid(pid, NULL, 0);
/* 이후 /var/run/netns/ns1 파일이 존재하는 한 netns 살아있음 */

/proc/PID/ns/net 심볼릭 링크 구조

# /proc/PID/ns/net 의 실제 구조
ls -la /proc/1/ns/net
# lrwxrwxrwx ... /proc/1/ns/net -> net:[4026531840]
#                                    ↑    ↑
#                               type  inode (netns 고유 식별자)

# inode 번호로 두 프로세스가 같은 netns인지 확인
stat -L /proc/100/ns/net /proc/200/ns/net
# inode가 동일하면 같은 netns

# /proc/net/ 은 /proc/self/net 의 symlink
# → 현재 프로세스의 netns 뷰만 표시
ls -la /proc/net
# lrwxrwxrwx ... /proc/net -> self/net

# 커널 내부: ns_common.inum (inode 번호)가 netns 식별자
struct ns_common {
    atomic_long_t   stashed;   /* 스타시된 ns 파일 */
    const struct proc_ns_operations *ops;
    unsigned int    inum;      /* inode 번호 = netns 고유 ID */
};

nsenter 동작 원리

/* nsenter --net=/var/run/netns/ns1 /bin/bash 내부 동작 */

/* 단계 1: 파일 오픈 */
int fd = open("/var/run/netns/ns1", O_RDONLY | O_CLOEXEC);
/* /var/run/netns/ns1 은 bind mount된 netns 파일 */
/* → 커널이 이 fd를 통해 해당 struct net 을 식별 */

/* 단계 2: setns로 네임스페이스 전환 */
setns(fd, CLONE_NEWNET);
/* → 현재 프로세스의 task_struct->nsproxy->net_ns 교체 */

/* 단계 3: 명령 실행 */
execv("/bin/bash", args);

/* 결과: bash 는 ns1 의 네트워크 환경에서 실행됨 */

# 실용 명령어
nsenter --net=/var/run/netns/ns1 /bin/bash
nsenter --target $(docker inspect -f '{{.State.Pid}}' container1) \
    --net /bin/bash
nsenter -t 12345 --net --pid ip link

Named vs Anonymous 네임스페이스 비교

구분 Named (ip netns add) Anonymous (clone/unshare)
저장 위치 /var/run/netns/NAME /proc/PID/ns/net 만 존재
수명 관리 파일(bind mount) 참조 기반 프로세스 참조 기반
진입 방법 nsenter --net=/var/run/netns/NAME nsenter --target PID --net
목록 확인 ip netns list lsns -t net
Docker 컨테이너 기본적으로 Anonymous Anonymous (PID 기반)
삭제 방법 ip netns delete NAME 프로세스 종료 시 자동

C 코드: named netns 생성 및 소켓 바인드

/* named netns 생성 후 소켓 바인딩 예제 */
#define _GNU_SOURCE
#include <sched.h>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>

int create_named_netns(const char *name)
{
    char path[256];
    snprintf(path, sizeof(path), "/var/run/netns/%s", name);

    /* 새 netns 생성 (현재 프로세스를 새 netns로 이동) */
    if (unshare(CLONE_NEWNET) < 0) {
        perror("unshare"); return -1;
    }

    /* bind mount 포인트 파일 생성 */
    int fd = open(path, O_RDONLY | O_CREAT | O_EXCL, 0);
    if (fd < 0) { perror("open"); return -1; }
    close(fd);

    /* 현재 netns를 파일에 pin */
    if (mount("/proc/self/ns/net", path, "none", MS_BIND, NULL) < 0) {
        perror("mount bind"); unlink(path); return -1;
    }
    return 0;
}

int enter_netns(const char *name)
{
    char path[256];
    snprintf(path, sizeof(path), "/var/run/netns/%s", name);

    int fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd < 0) { perror("open"); return -1; }

    if (setns(fd, CLONE_NEWNET) < 0) {
        perror("setns"); close(fd); return -1;
    }
    close(fd);
    return 0;
}

int main(void)
{
    /* 프로세스를 fork하여 named netns 생성 */
    pid_t pid = fork();
    if (pid == 0) {
        create_named_netns("myns");
        _exit(0);
    }
    waitpid(pid, NULL, 0);

    /* 생성된 named netns에 진입하여 소켓 바인드 */
    enter_netns("myns");

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(8080),
        .sin_addr.s_addr = INADDR_ANY,
    };
    bind(sock, (struct sockaddr*)&addr, sizeof(addr));
    /* 이 소켓은 myns 네임스페이스에 바인딩됨 */
    /* → 호스트 netns의 8080과 충돌 없음 */
    printf("myns 내 8080 포트 바인딩 완료\n");
    close(sock);
    return 0;
}

NETNS_ID와 cross-netns 참조

커널 4.9+에서 도입된 NETNS_ID는 네트워크 네임스페이스에 정수 ID를 부여하여 다른 netns의 디바이스를 참조하거나 netlink 메시지에서 netns를 식별하는 데 사용됩니다. 컨테이너 오케스트레이션에서 cross-netns 모니터링의 핵심 메커니즘입니다.

# NETNS_ID 할당 (수동)
ip netns add ns1
ip netns set ns1 100         # ns1에 NETNS_ID 100 부여
ip netns list-id             # 할당된 ID 목록 확인
# nsid 100 (inode: 4026532008)

# NETNS_ID로 다른 netns의 디바이스 참조
# (cross-netns link 생성)
ip link add veth0 type veth peer name veth1 netns ns1
# → veth1이 ns1에 생성됨

# NETNS_ID를 사용한 link-netns 참조
ip link show veth0
# 5: veth0@if2: ... link-netnsid 100
# ↑ peer가 NETNS_ID 100(ns1)에 있음

# netlink 메시지에서 NETNSA_NSID 속성 사용
# → ip monitor, tc, devlink 등에서 cross-netns 이벤트 수신 가능
ip -n ns1 monitor link   # ns1의 링크 변경 모니터링

# NETNS_ID 자동 할당 (커널이 자동으로 증가하는 ID 부여)
ip link add veth2 type veth peer name veth3 netns ns1
ip link show veth2 | grep netnsid
# link-netnsid 100  (이미 할당된 ID 재사용)
/* net/core/net_namespace.c */
/* NETNS_ID는 per-netns IDR(Integer ID Radix tree)에 저장 */

/* netns_id 할당 */
int peernet2id_alloc(struct net *net, struct net *peer,
                     gfp_t gfp)
{
    int id;
    /* net의 IDR에서 peer에 대한 ID 검색 */
    id = idr_alloc(&net->netns_ids, peer, 0,
                   INT_MAX, gfp);
    /* id = cross-netns 참조 시 사용하는 정수 */
    return id;
}

/* NETNS_ID로 peer netns 조회 */
struct net *get_net_ns_by_id(const struct net *net, int id)
{
    struct net *peer = idr_find(&net->netns_ids, id);
    if (peer)
        get_net(peer);
    return peer;
}

AF_UNIX across netns 통신

Unix domain socket(AF_UNIX)은 네트워크 네임스페이스와 독립적입니다. 두 다른 netns의 프로세스가 같은 파일시스템의 소켓 파일을 통해 통신할 수 있으며, 이 특성은 컨테이너와 호스트 간 제어 채널, 로그 수집, 그리고 파일 디스크립터 전달(SCM_RIGHTS)에 활용됩니다.

# AF_UNIX 소켓은 네트워크 네임스페이스에 영향받지 않음
# → 서로 다른 netns 간 통신 가능 (파일시스템 접근만 있으면 됨)

# 호스트에서 Unix 소켓 서버 시작
socat UNIX-LISTEN:/tmp/cross-netns.sock,fork EXEC:date &

# ns1에서 호스트의 Unix 소켓에 접속
ip netns exec ns1 socat - UNIX-CONNECT:/tmp/cross-netns.sock
# → 정상 연결됨 (netns 무관)

# abstract Unix 소켓은 netns별 격리됨 (커널 5.0+)
# abstract 소켓: 이름이 \0으로 시작, 파일시스템에 존재하지 않음
# → 같은 netns에서만 접근 가능

# pathname 소켓 vs abstract 소켓 netns 격리 비교
# /tmp/sock.sock  → netns 무관 (파일시스템 기반)
# @abstract_name  → per-netns 격리 (커널 5.0+)
/* SCM_RIGHTS: netns 간 파일 디스크립터 전달 */
/* Unix 소켓으로 다른 netns의 소켓 FD를 전달하는 패턴 */

/* 호스트 프로세스: netns A의 TCP 소켓을 netns B로 전달 */
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(int))];
int fd_to_send = tcp_socket_in_netns_a;

msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type  = SCM_RIGHTS;     /* FD 전달 */
cmsg->cmsg_len   = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(cmsg) = fd_to_send;

sendmsg(unix_socket, &msg, 0);
/* → 수신 측(netns B 프로세스)은 이 FD를 recv하여
   netns A의 TCP 소켓을 직접 사용 가능 */
/* → 소켓이 바인딩된 netns(A)의 네트워크 스택에서 동작 */
활용 사례: Docker의 libnetwork와 containerd는 AF_UNIX 소켓을 통해 호스트와 컨테이너 간 제어 통신을 수행합니다. /var/run/docker.sock이 대표적인 예이며, 컨테이너 내에서 이 소켓에 접근하면 호스트 Docker 데몬을 제어할 수 있으므로 보안상 주의가 필요합니다. Kubernetes의 CRI 소켓 (/run/containerd/containerd.sock)도 동일한 원리입니다.

고급 정책 라우팅

각 네트워크 네임스페이스는 완전히 독립된 라우팅 테이블과 정책 규칙(ip rule)을 가집니다. 이를 활용하면 테넌트별 기본 게이트웨이 분리, 멀티 업링크 NGFW, VRF 조합 등 정교한 라우팅 정책을 구현할 수 있습니다.

per-netns 정책 라우팅

# netns별 독립 라우팅 테이블 확인
ip netns exec ns1 ip route show table all
# table local: 127.0.0.0/8 → lo
# table main:  10.0.1.0/24 → veth0

# 정책 라우팅 (ip rule) - ns1 내에서만 적용
ip netns exec ns1 ip rule add from 10.0.1.0/24 table 100 priority 100
ip netns exec ns1 ip rule add from 10.0.2.0/24 table 200 priority 200
ip netns exec ns1 ip rule show
# 0:    from all lookup local
# 100:  from 10.0.1.0/24 lookup 100
# 200:  from 10.0.2.0/24 lookup 200
# 32766: from all lookup main
# 32767: from all lookup default

# 각 테이블에 기본 경로 설정 (멀티 업링크)
ip netns exec ns1 ip route add default via 192.168.1.1 table 100  # ISP-A
ip netns exec ns1 ip route add default via 192.168.2.1 table 200  # ISP-B

# 출발지 기반 라우팅 검증
ip netns exec ns1 ip route get 8.8.8.8 from 10.0.1.5
# 8.8.8.8 from 10.0.1.5 via 192.168.1.1 dev veth0 table 100

VRF + netns 조합

VRF(Virtual Routing and Forwarding)는 단일 netns 내에서 라우팅 테이블을 분리하는 기법이고, netns는 더 강력한 완전 격리를 제공합니다. 두 기술을 조합하면 계층적 격리가 가능합니다.

비교 항목 VRF (단독) netns (단독) VRF + netns
라우팅 격리 테이블 분리 완전 격리 완전 격리 + 내부 분리
Netfilter 격리 미지원 (공유) 완전 격리 완전 격리
conntrack 격리 미지원 (공유) 완전 격리 완전 격리
sysctl 격리 미지원 완전 격리 완전 격리
소켓 충돌 동일 포트 사용 불가 동일 포트 가능 동일 포트 가능
관리 복잡성 단순 중간 복잡
주요 사용처 엔터프라이즈 라우터 컨테이너 멀티테넌트 NGFW
# VRF 디바이스 생성 (netns 내에서)
ip netns exec tenant-a ip link add vrf-red type vrf table 10
ip netns exec tenant-a ip link set vrf-red up

# VRF에 인터페이스 예속
ip netns exec tenant-a ip link set eth0.100 master vrf-red

# VRF 컨텍스트에서 라우팅 확인
ip netns exec tenant-a ip route show vrf vrf-red

# VRF 내에서 소켓 바인딩
ip netns exec tenant-a ip vrf exec vrf-red nc -l 8080

# BGP per-netns (FRRouting)
ip netns exec tenant-a vtysh -c "show bgp summary"
# → tenant-a netns 내 BGP 세션만 표시

MPLS per-netns 라우팅

# MPLS 활성화 (netns별 독립 설정)
ip netns exec ns1 sysctl -w net.mpls.platform_labels=1048575
ip netns exec ns1 modprobe mpls_router  # 필요시

# MPLS 입력 레이블 → 출력 설정
ip netns exec ns1 ip -f mpls route add 100 via inet 10.0.1.2
ip netns exec ns1 ip -f mpls route add 200 encap mpls 300 via inet 10.0.2.1

# MPLS 인터페이스 활성화 (netns별)
ip netns exec ns1 sysctl -w net.mpls.conf.veth0.input=1

# ns1의 MPLS 테이블 확인
ip netns exec ns1 ip -f mpls route show

# 호스트의 MPLS 테이블은 완전히 분리됨
ip -f mpls route show   # 다른 결과

5개 테넌트 NGFW 고급 예제

#!/bin/bash
# 5개 테넌트 NGFW: VRF + netns 혼합 구성

TENANTS=(A B C D E)
VLANS=(100 200 300 400 500)

for i in "${!TENANTS[@]}"; do
    TENANT=${TENANTS[$i]}
    VLAN=${VLANS[$i]}
    SUBNET="10.${i}.0"

    # 테넌트 netns 생성
    ip netns add tenant-${TENANT}

    # VLAN 인터페이스 생성 및 이동
    ip link add link eth0 name eth0.${VLAN} type vlan id ${VLAN}
    ip link set eth0.${VLAN} netns tenant-${TENANT}

    # VRF 디바이스 생성 (netns 내)
    ip netns exec tenant-${TENANT} \
        ip link add vrf-${TENANT} type vrf table $((i+10))
    ip netns exec tenant-${TENANT} ip link set vrf-${TENANT} up

    # VLAN 인터페이스를 VRF에 연결
    ip netns exec tenant-${TENANT} \
        ip link set eth0.${VLAN} master vrf-${TENANT}
    ip netns exec tenant-${TENANT} \
        ip addr add ${SUBNET}.1/24 dev eth0.${VLAN}
    ip netns exec tenant-${TENANT} ip link set eth0.${VLAN} up

    # 테넌트 전용 방화벽 (완전 격리)
    ip netns exec tenant-${TENANT} nft add table inet tenant_fw
    ip netns exec tenant-${TENANT} nft add chain inet tenant_fw forward \
        "{ type filter hook forward priority 0; policy drop; }"
    ip netns exec tenant-${TENANT} nft add rule inet tenant_fw forward \
        ct state established,related accept
    ip netns exec tenant-${TENANT} nft add rule inet tenant_fw forward \
        ip saddr ${SUBNET}.0/24 tcp dport { 80, 443, 8080 } accept

    echo "테넌트 ${TENANT} (VLAN ${VLAN}, ${SUBNET}.0/24) 설정 완료"
done

# src_valid_mark + netns 상호작용
# (멀티 업링크 환경에서 conntrack mark 기반 라우팅)
for TENANT in "${TENANTS[@]}"; do
    ip netns exec tenant-${TENANT} \
        sysctl -w net.ipv4.conf.all.src_valid_mark=1
done

네임스페이스 보안 강화

네트워크 네임스페이스는 강력한 격리를 제공하지만, 잘못 설정하면 컨테이너 탈출이나 권한 상승의 경로가 될 수 있습니다. User Namespace와의 조합, Seccomp 정책, 감사 설정을 통해 보안을 강화합니다.

User Namespace + Network Namespace 조합

/* User NS + Net NS 조합: 비특권 사용자의 netns 생성 */

/* CAP_NET_ADMIN 없이도 새 user_ns 내에서 net_ns 생성 가능 */
/* user_namespaces(7): 새 user_ns 내에서는 모든 capability 보유 */

/* 허용 조건 확인 */
cat /proc/sys/kernel/unprivileged_userns_clone
# 1 → 비특권 사용자도 user_ns + net_ns 생성 가능
# 0 → Debian 계열 기본값: 비활성화

/* 비특권 컨테이너 생성 예시 (podman rootless) */
unshare --user --map-root-user --net /bin/bash
# 새 user_ns (현재 사용자 → UID 0 매핑) + 새 net_ns

# Debian/Ubuntu에서 비특권 user_ns 비활성화 (보안 강화)
echo 0 > /proc/sys/kernel/unprivileged_userns_clone
# 또는 sysctl.conf에 추가:
# kernel.unprivileged_userns_clone = 0

컨테이너 탈출 공격 벡터

/* 벡터 1: /proc/PID/ns/net 경로 통한 호스트 netns 접근 */

/* 컨테이너 내부에서 PID 1 (init)의 netns 접근 시도 */
/* → 호스트 init의 netns = 호스트 네트워크 */
/* → 이 경로를 통해 setns()로 호스트 netns 진입 가능 */

/* 방어: /proc 마운트를 hidepid=2로 제한 */
mount -o remount,hidepid=2,gid=proc /proc
/* → 다른 사용자의 /proc/PID/ 접근 차단 */

/* 벡터 2: CAP_NET_ADMIN in netns */
/* netns 내에서 CAP_NET_ADMIN이 있으면 */
/* → ip link set dev eth0 netns $HOST_NETNS (netns 이동) */
/* → 호스트 인터페이스를 컨테이너 netns로 가져올 수 있음 */

/* 방어: seccomp으로 setns/unshare 제한 */
/* 방어: CAP_NET_ADMIN을 컨테이너에서 제거 */
/* docker run --cap-drop=NET_ADMIN ... */

/* 벡터 3: runc 취약점 (CVE-2019-5736) */
/* /proc/self/exe 덮어쓰기를 통한 호스트 코드 실행 */
/* → 현재 버전의 runc는 netns 전환 후 파일 접근 제한 */

Seccomp 정책: netns 관련 syscall 제한

/* Docker/containerd seccomp 정책 예시 (JSON) */
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["unshare"],
      "action": "SCMP_ACT_ALLOW",
      "args": [
        {
          "index": 0,
          "value": 0,
          "op": "SCMP_CMP_MASKED_EQ",
          "valueTwo": 1073741824  /* CLONE_NEWNET = 0x40000000: 차단 */
        }
      ]
      /* CLONE_NEWNET 플래그 포함 시 ERRNO 반환 */
    },
    {
      "names": ["setns"],
      "action": "SCMP_ACT_ALLOW",
      "args": [
        {
          "index": 1,
          "value": 1073741824,   /* CLONE_NEWNET */
          "op": "SCMP_CMP_NE"   /* CLONE_NEWNET 이 아닌 경우만 허용 */
        }
      ]
    }
  ]
}

SELinux/AppArmor + netns

# AppArmor 프로파일: unshare/setns 제한
# /etc/apparmor.d/docker_netns_restrict
profile docker_netns_restrict flags=(attach_disconnected) {
  # 네트워크 네임스페이스 생성 제한
  deny capability net_admin,
  deny capability sys_admin,

  # unshare 시스템 콜 제한 (AppArmor 3.x+)
  deny unshare network,
}

# SELinux: netns 관련 타입 확인
sesearch --allow -s container_t -p net_admin
# → container_t에 net_admin capability 부여 여부 확인

# SELinux boolean로 컨테이너 네트워크 제어
getsebool -a | grep container_net
setsebool container_manage_network_interface=0

네트워크 네임스페이스 감사 (eBPF)

/* bpftrace: 새 netns 생성 실시간 추적 */
bpftrace -e '
kprobe:copy_net_ns {
    $old = (struct net *)arg2;
    printf("[%s] PID=%d (%s) 새 netns 생성 (parent inum=%u)\n",
           strftime("%H:%M:%S", nsecs),
           pid, comm, $old->ns.inum);
}
kretprobe:copy_net_ns {
    $new = (struct net *)retval;
    if ($new > 0) {
        printf("  → 새 netns inum=%u\n", $new->ns.inum);
    }
}
'

/* bpftrace: setns 호출 모니터링 (컨테이너 탈출 탐지) */
bpftrace -e '
tracepoint:syscalls:sys_enter_setns {
    if (args->nstype & 0x40000000) {  /* CLONE_NEWNET */
        printf("[%s] setns(NEWNET) PID=%d (%s) uid=%d fd=%d\n",
               strftime("%H:%M:%S", nsecs),
               pid, comm, uid, args->fd);
        /* uid=0이면서 예상치 못한 프로세스면 경보 */
    }
}
'

/* auditd 규칙 추가 */
# /etc/audit/rules.d/netns.rules
-a always,exit -F arch=b64 -S clone \
    -F a0&0x40000000=0x40000000 -k netns_create
-a always,exit -F arch=b64 -S unshare \
    -F a0&0x40000000=0x40000000 -k netns_unshare
-a always,exit -F arch=b64 -S setns \
    -F a1=0x40000000 -k netns_enter

# 감사 로그 확인
ausearch -k netns_create -i | tail -20

대규모 netns 성능 최적화

클라우드 에지 환경이나 멀티테넌트 NGFW에서 수천 개의 네트워크 네임스페이스를 운영할 때 성능 특성과 최적화 전략을 이해하는 것이 중요합니다.

netns 생성 비용

/* clone(CLONE_NEWNET) 지연 시간 측정 */
#define _GNU_SOURCE
#include <sched.h>
#include <time.h>
#include <stdio.h>

#define N 1000

int null_fn(void *arg) { return 0; }
static char stacks[N][65536];

int main(void)
{
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);

    for (int i = 0; i < N; i++) {
        pid_t p = clone(null_fn, stacks[i] + 65536,
                        CLONE_NEWNET | SIGCHLD, NULL);
        /* 빠른 정리 (zombie 방지) */
        waitpid(p, NULL, 0);
    }

    clock_gettime(CLOCK_MONOTONIC, &t1);
    long ms = (t1.tv_sec - t0.tv_sec) * 1000 +
              (t1.tv_nsec - t0.tv_nsec) / 1000000;
    printf("%d netns 생성: %ld ms (평균 %.2f ms/netns)\n",
           N, ms, (double)ms / N);
    return 0;
}
/* 일반적인 결과:
   1000 netns 생성: ~800 ms (평균 ~0.8 ms/netns)
   → 대부분의 시간은 setup_net()의 pernet_operations 콜백 체인
   → lo 인터페이스 생성 및 초기화 포함 */

struct net 메모리 사용량

구성 요소 크기 (근사) 비고
struct net 자체 ~3-5 KB 커널 버전/빌드 옵션에 따라 변동
FIB 테이블 (초기) ~50-100 KB fib_main + fib_local trie
conntrack 해시 테이블 ~32-128 KB nf_conntrack_buckets 기본값 의존
proc_net 엔트리 ~10-20 KB /proc/net/* 등록
lo 인터페이스 ~4-8 KB struct net_device + 버퍼
총합 (기본) ~100-300 KB 설정에 따라 다름
# struct net 슬랩 캐시 확인
grep net_namespace /proc/slabinfo
# net_namespace  100  100  3072  10  8  ...
# → 현재 100개의 netns, 각 3072 B (슬랩 캐시 크기)

# 전체 netns 메모리 사용 추정
python3 -c "
import subprocess, re
out = subprocess.check_output(['cat', '/proc/slabinfo']).decode()
for line in out.splitlines():
    if 'net_namespace' in line:
        parts = line.split()
        count = int(parts[1])
        size = int(parts[3])
        print(f'netns 수: {count}, 슬랩 크기: {size} B')
        print(f'슬랩만: {count * size / 1024:.1f} KB')
        # 실제 총 메모리는 FIB, conntrack 등 포함하여 훨씬 큼
"

네임스페이스 풀링 패턴

#!/bin/bash
# 고밀도 멀티테넌트 환경: netns 풀 사전 생성

POOL_SIZE=50
POOL_DIR=/var/run/netns-pool

mkdir -p $POOL_DIR

# 풀 초기화: netns 사전 생성
echo "netns 풀 $POOL_SIZE 개 생성 중..."
for i in $(seq 1 $POOL_SIZE); do
    NAME="pool-${i}"
    if ! ip netns show | grep -q "^${NAME}$"; then
        ip netns add $NAME
        # 기본 설정 미리 적용
        ip netns exec $NAME ip link set lo up
        # veth 쌍 미리 생성 (재사용 시 빠른 할당)
        ip link add "h-${NAME}" type veth peer name "c-${NAME}"
        ip link set "c-${NAME}" netns $NAME
        ip netns exec $NAME ip link set "c-${NAME}" up
        echo "$NAME 준비 완료"
    fi
done

# 풀에서 netns 할당 (O(1) 조회)
allocate_netns() {
    local name=$(ip netns list | grep "^pool-" | head -1 | awk '{print $1}')
    if [ -z "$name" ]; then
        echo "풀 고갈 — 새 netns 생성 필요" >&2
        return 1
    fi
    # 이름 변경하여 할당 표시
    ip netns exec "$name" true  # 활성 확인
    echo "$name"
}

# 할당 예시
ALLOCATED=$(allocate_netns)
echo "할당된 netns: $ALLOCATED"

pernet_operations 최소화 전략

/* netns 생성 속도 개선: 불필요한 pernet_operations 비활성화 */

/* 예: conntrack을 사용하지 않는 환경 */
/* CONFIG_NF_CONNTRACK 미빌드 시 conntrack init 생략 */

/* 또는 모듈 언로드로 pernet_operations 제거 */
modprobe -r nf_conntrack

/* pernet_operations 수 확인 */
/* /proc/net/ptype 등으로 등록된 프로토콜 핸들러 확인 */

/* 성능 측정: pernet_operations 최소화 전후 비교 */
time for i in $(seq 1 100); do
    ip netns add perf-test-$i
done
# Before: 100 netns in ~3.2s
# After (conntrack 언로드): 100 netns in ~1.8s

for i in $(seq 1 100); do
    ip netns delete perf-test-$i
done

veth 성능 및 1000 netns 처리량

# 1000개 netns + veth 환경 성능 측정

# 환경 구성
for i in $(seq 1 1000); do
    ip netns add ns${i}
    ip link add vh${i} type veth peer name vc${i}
    ip link set vc${i} netns ns${i}
    ip addr add 10.$((i/256)).$((i%256)).1/30 dev vh${i}
    ip netns exec ns${i} ip addr add 10.$((i/256)).$((i%256)).2/30 dev vc${i}
    ip link set vh${i} up
    ip netns exec ns${i} ip link set vc${i} up
done 2>/dev/null

# 패킷 처리량 테스트 (1개 veth 쌍)
# veth GRO/GSO 오프로드 없이: ~5-8 Gbit/s (단일 코어)
# veth XDP redirect: ~20+ Gbit/s

# XDP per-netns 설정 시 고려사항:
# - XDP 프로그램은 veth의 어느 쪽(TX/RX) netns에 붙는지 중요
# - tc BPF: 더 세밀한 per-netns 정책 가능

# 메모리 사용 확인 (1000 netns)
grep MemAvailable /proc/meminfo  # 이전
# 1000 netns × ~200KB = ~200MB 추가 소모 예상
grep MemAvailable /proc/meminfo  # 이후

sysctl per-netns 격리 범위

리눅스 네트워크 관련 sysctl 파라미터 중 상당수가 네트워크 네임스페이스별로 독립적으로 관리됩니다. 하지만 일부 파라미터는 전역(global) 설정이며, netns 간에 공유됩니다. 이 구분을 정확히 이해하지 못하면 예상치 못한 동작이 발생합니다.

per-netns vs 전역 sysctl 구분

sysctl 파라미터 격리 수준 기본값 설명
net.ipv4.ip_forward per-netns 0 각 netns별 독립 포워딩 제어
net.ipv4.conf.*.rp_filter per-netns 2 (loose) Reverse Path Filtering
net.ipv4.conf.*.arp_ignore per-netns 0 ARP 응답 정책
net.ipv4.conf.*.accept_redirects per-netns 1 ICMP redirect 수신 허용
net.ipv4.tcp_syncookies per-netns 1 SYN flood 방어
net.ipv4.icmp_echo_ignore_all per-netns 0 ping 응답 제어
net.ipv4.ip_local_port_range per-netns 32768-60999 임시 포트 범위
net.ipv4.tcp_max_syn_backlog per-netns 128-4096 SYN 큐 크기
net.ipv4.neigh.*.gc_thresh* per-netns 128/512/1024 ARP 캐시 GC 임계값
net.netfilter.nf_conntrack_max per-netns 65536 연결 추적 최대 항목
net.ipv4.tcp_keepalive_time per-netns 7200 TCP keepalive 시작 시간(초)
net.ipv4.tcp_fin_timeout per-netns 60 FIN-WAIT-2 타임아웃
net.core.somaxconn per-netns 4096 listen() backlog 최대값
net.core.rmem_max per-netns 212992 수신 버퍼 최대값
net.core.wmem_max per-netns 212992 송신 버퍼 최대값
net.core.netdev_max_backlog per-netns 1000 인터페이스 수신 큐 크기
net.ipv6.conf.*.disable_ipv6 per-netns 0 IPv6 비활성화
net.mpls.platform_labels per-netns 0 MPLS 레이블 최대 수
net.core.bpf_jit_enable 전역 1 BPF JIT 컴파일러 (모든 netns 공유)
net.core.bpf_jit_harden 전역 0 BPF JIT 보안 강화 (전역)
kernel.unprivileged_userns_clone 전역 0-1 비특권 user_ns 생성 (전역)
net.ipv4.ip_unprivileged_port_start per-netns 1024 비특권 바인드 가능 최소 포트

커널 내부: per-netns sysctl 등록

/* net/ipv4/sysctl_net_ipv4.c */
/* per-netns sysctl은 netns_ipv4 구조체 필드를 직접 참조 */

static struct ctl_table ipv4_net_table[] = {
    {
        .procname   = "ip_forward",
        .data       = &init_net.ipv4.sysctl_ip_fwd_update_priority,
        /* ↑ 초기화 시에만 init_net 참조 */
        /* 실제 접근 시 현재 프로세스의 netns 값 사용 */
        .maxlen     = sizeof(u8),
        .mode       = 0644,
        .proc_handler = ipv4_sysctl_forward,
    },
    /* ... */
};

/* 새 netns 생성 시 sysctl 초기화 */
static int __net_init ipv4_sysctl_init_net(struct net *net)
{
    /* init_net의 값을 새 netns에 복사하지 않음 */
    /* → 모든 netns는 커널 기본값으로 초기화됨 */
    /* → 호스트에서 변경한 sysctl 값이 새 netns에 적용되지 않음! */
    net->ipv4.sysctl_ip_default_ttl = 64;
    net->ipv4.sysctl_ip_forward = 0;  /* 기본: 포워딩 꺼짐 */
    /* ... */
}
# per-netns sysctl 실습

# 호스트에서 설정
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.tcp_keepalive_time=600
sysctl -w net.core.somaxconn=65535

# 새 netns 생성
ip netns add sysctl-test

# 새 netns에서는 호스트 값이 아닌 커널 기본값!
ip netns exec sysctl-test sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0  (기본값, 호스트와 다름!)

ip netns exec sysctl-test sysctl net.ipv4.tcp_keepalive_time
# net.ipv4.tcp_keepalive_time = 7200  (기본값)

ip netns exec sysctl-test sysctl net.core.somaxconn
# net.core.somaxconn = 4096  (기본값)

# netns별 독립 설정 — 호스트에 영향 없음
ip netns exec sysctl-test sysctl -w net.ipv4.ip_forward=1
ip netns exec sysctl-test sysctl -w net.core.somaxconn=128

# 호스트 값 변경 없음 확인
sysctl net.core.somaxconn
# net.core.somaxconn = 65535  (호스트 값 유지)

# 전역 sysctl은 모든 netns에서 동일
sysctl net.core.bpf_jit_enable
# net.core.bpf_jit_enable = 1
ip netns exec sysctl-test sysctl net.core.bpf_jit_enable
# net.core.bpf_jit_enable = 1  (동일 — 전역 공유)

ip netns delete sysctl-test
주의: 컨테이너(Docker/K8s)에서 per-netns sysctl을 변경하려면 적절한 Capability가 필요합니다. Docker에서는 --sysctl net.ipv4.ip_forward=1 옵션으로 컨테이너 netns의 sysctl을 설정합니다. Kubernetes에서는 Pod spec의 securityContext.sysctls로 안전한(safe) sysctl만 변경할 수 있으며, net.ipv4.ip_unprivileged_port_start 등이 safe sysctl에 해당합니다. unsafe sysctl(net.ipv4.ip_forward 등)은 kubelet --allowed-unsafe-sysctls 플래그가 필요합니다.

XDP/TC BPF per-netns 패킷 처리

XDP(eXpress Data Path)와 TC(Traffic Control) BPF는 네트워크 네임스페이스와 밀접하게 관련됩니다. veth 쌍에서의 XDP redirect, TC BPF 필터의 per-netns 적용, 그리고 eBPF 맵의 netns 인식(awareness)을 이해하면 고성능 컨테이너 네트워킹의 핵심 원리를 파악할 수 있습니다.

XDP/TC BPF per-netns 패킷 경로 Pod netns Application (TCP/UDP) TC egress BPF (Pod측) eth0 (veth Pod쪽) — TX XDP (native/generic) veth peer에서 수신 시 실행 netns 경계 Host netns 기존 경로 (iptables) veth host쪽 — RX TC ingress BPF (host측) Netfilter/iptables hooks FIB lookup (Host) TC egress BPF (host측) eth0 물리 NIC → 외부 XDP redirect (fast path) veth host쪽 — XDP hook bpf_redirect_peer() netns 경계 직접 통과 iptables/conntrack 우회 FIB lookup 우회 → 최대 10x 성능 향상 eth0 물리 NIC → 외부 좌: 기존 경로 (TC BPF + iptables, 모든 후크 통과) | 우: XDP redirect (커널 스택 우회, Cilium/Calico eBPF 사용) bpf_redirect_peer(): veth peer의 TC ingress로 직접 전달 (5.10+) — netns 경계를 BPF 맵으로 직접 통과

그림 7. XDP/TC BPF per-netns 패킷 경로. 기존 iptables 경로(좌)와 XDP redirect 고속 경로(우)를 비교합니다. bpf_redirect_peer()는 커널 5.10+ 에서 지원됩니다.

veth에서의 XDP 동작

/* drivers/net/veth.c - XDP 지원 (커널 4.19+) */

/* veth XDP receive: peer의 XDP 프로그램을 수신측에서 실행 */
static int veth_xdp_rcv(struct veth_rq *rq,
                         int budget,
                         struct veth_xdp_tx_bq *bq)
{
    /* XDP 프로그램이 설정된 경우 netif_rx() 이전에 실행 */
    /* → 수신 측 netns의 XDP 프로그램이 판단 */

    act = bpf_prog_run_xdp(xdp_prog, &xdp);

    switch (act) {
    case XDP_PASS:
        /* 정상 수신 → 해당 netns의 네트워크 스택으로 진입 */
        break;
    case XDP_TX:
        /* 송신 측(peer)으로 반환 — 같은 veth 쌍 내 */
        break;
    case XDP_REDIRECT:
        /* bpf_redirect_peer(): 다른 netns의 veth로 직접 전달 */
        /* → 커널 스택 우회, 최고 성능 */
        break;
    case XDP_DROP:
        /* 드롭 — netns 진입 전에 차단 (최소 지연) */
        break;
    }
}

TC BPF per-netns 적용

# TC BPF는 인터페이스에 부착 → 인터페이스가 속한 netns에서 동작

# Pod netns의 eth0에 TC BPF 프로그램 부착 (Cilium 방식)
ip netns exec pod-ns tc qdisc add dev eth0 clsact

# ingress: Pod으로 들어오는 패킷 필터링
ip netns exec pod-ns tc filter add dev eth0 ingress \
    bpf da obj to-container.o sec tc

# egress: Pod에서 나가는 패킷 필터링
ip netns exec pod-ns tc filter add dev eth0 egress \
    bpf da obj from-container.o sec tc

# Host netns의 veth (cali*) 에도 TC BPF 가능
tc qdisc add dev caliXXXXXXXX clsact
tc filter add dev caliXXXXXXXX ingress \
    bpf da obj host-ingress.o sec tc

# 확인: TC BPF 프로그램 목록
ip netns exec pod-ns tc filter show dev eth0 ingress
tc filter show dev caliXXXXXXXX ingress

# bpf_redirect_peer() 사용 (커널 5.10+)
# → veth pair에서 peer의 TC ingress로 직접 전달
# → netfilter 우회, 최대 ~20 Gbit/s 달성

# veth XDP vs TC BPF 성능 비교 (iperf3, 단일 코어)
# 기본 veth (netif_rx):       ~5-8 Gbit/s
# TC BPF redirect:            ~12-15 Gbit/s
# XDP bpf_redirect_peer():    ~20-25 Gbit/s
# XDP native (물리 NIC):      ~40+ Gbit/s

eBPF 맵의 netns 인식

/* eBPF 프로그램에서 패킷의 netns 확인 */

/* bpf_get_netns_cookie(): 현재 패킷의 netns 쿠키 반환 */
/* → net->net_cookie (atomic64_t, 생성 시 할당) */
__u64 cookie = bpf_get_netns_cookie(skb);
/* cookie로 netns 구분 → per-netns 정책 적용 가능 */

/* Cilium의 per-netns 정책 적용 예시 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, __u64);      /* netns_cookie */
    __type(value, struct policy);
} netns_policy_map SEC(".maps");

SEC("tc")
int from_container(struct __sk_buff *skb)
{
    __u64 cookie = bpf_get_netns_cookie(skb);
    struct policy *pol = bpf_map_lookup_elem(&netns_policy_map, &cookie);
    if (!pol)
        return TC_ACT_SHOT;  /* 정책 없으면 드롭 */
    /* per-netns 정책에 따라 허용/거부 */
    return pol->allow ? TC_ACT_OK : TC_ACT_SHOT;
}

tc qdisc per-netns 격리

# tc(traffic control)의 qdisc/class/filter는 디바이스에 부착
# → 디바이스가 속한 netns에서만 유효

# ns1의 veth에 대역폭 제한 (TBF)
ip netns exec ns1 tc qdisc add dev eth0 root tbf \
    rate 100mbit burst 32kbit latency 10ms

# ns1의 veth에 HTB 계층적 대역폭 제어
ip netns exec ns1 tc qdisc add dev eth0 root handle 1: htb
ip netns exec ns1 tc class add dev eth0 parent 1: classid 1:1 \
    htb rate 200mbit ceil 500mbit
ip netns exec ns1 tc class add dev eth0 parent 1:1 classid 1:10 \
    htb rate 100mbit ceil 200mbit
ip netns exec ns1 tc filter add dev eth0 parent 1:0 protocol ip \
    u32 match ip dport 80 0xffff flowid 1:10

# ns1의 qdisc 확인 (호스트에서는 보이지 않음)
ip netns exec ns1 tc qdisc show dev eth0
ip netns exec ns1 tc class show dev eth0
ip netns exec ns1 tc filter show dev eth0

# 호스트의 qdisc (ns1과 완전 독립)
tc qdisc show dev eth0  # 호스트의 eth0에 대한 별도 qdisc

# netem: 패킷 지연/손실 시뮬레이션 (per-netns)
ip netns exec ns1 tc qdisc add dev eth0 root netem \
    delay 50ms 10ms loss 1%
# → ns1의 eth0에서만 50±10ms 지연, 1% 패킷 손실
# → 호스트의 eth0에는 영향 없음

Kubernetes 네트워크 심화

Kubernetes의 네트워크 격리는 pause 컨테이너, CNI 플러그인 체인, 그리고 다양한 CNI 구현체 (Flannel, Calico, Cilium)의 조합으로 이루어집니다. 각 구성 요소가 netns를 어떻게 활용하는지 심층적으로 분석합니다.

pause 컨테이너 (infra container) 역할

Kubernetes Pod 내의 모든 컨테이너는 하나의 네트워크 네임스페이스를 공유합니다. 이 공유 netns를 소유하고 관리하는 것이 pause 컨테이너(인프라 컨테이너)입니다.

/* pause 컨테이너 역할 */

/* 1. containerd + kubelet이 Pod 생성 시 가장 먼저 pause 컨테이너 시작 */
/* pause 이미지: registry.k8s.io/pause:3.9 */
/* pause 프로세스: pause() 시스템 콜만 수행 (신호 대기) */

/* 2. pause 컨테이너의 netns가 Pod의 netns로 사용됨 */
/* → pause PID의 /proc/[pause_pid]/ns/net 이 Pod netns */

/* 3. 이후 실제 컨테이너들은 pause의 netns를 공유 */
/* containerd 내부: */
ContainerSpec {
  Linux: {
    Namespaces: [
      {Type: "network", Path: "/proc/[pause_pid]/ns/net"}
      /* pause의 netns 경로를 직접 사용 */
    ]
  }
}

/* 4. pause가 종료되면 해당 netns의 PID 참조가 사라지므로 */
/* bind mount(/var/run/netns/cni-XXXX)로 유지됨 */

# 실제 확인
PAUSE_PID=$(docker inspect --format '{{.State.Pid}}' \
    $(docker ps | grep pause | awk '{print $1}'))
# Pod 내 다른 컨테이너들과 같은 netns인지 확인
stat /proc/$PAUSE_PID/ns/net
APP_PID=$(docker inspect --format '{{.State.Pid}}' app-container)
stat /proc/$APP_PID/ns/net
# inode 동일 → 같은 netns
K8s Node — pause 컨테이너와 netns 공유 구조 Pod A (netns A) pause 컨테이너 PID=1 (pause syscall) eth0: 10.244.1.5/24 lo: 127.0.0.1/8 App Container 1 nginx:8080 공유 netns A App Container 2 sidecar:9090 공유 netns A /var/run/netns/cni-XXXXXX (bind mount — netns A pin) veth 쌍 Pod B (netns B) pause 컨테이너 PID=1 (pause syscall) eth0: 10.244.1.6/24 lo: 127.0.0.1/8 App Container 3 app:7070 공유 netns B /var/run/netns/cni-YYYYYY (bind mount — netns B pin) veth 쌍 Host netns (Calico BGP 라우팅) caliXXXX1 (veth peer ← Pod A) caliXXXX2 (veth peer ← Pod B) BGP 라우팅 (Calico Felix) eth0 (물리 NIC) pause 컨테이너가 netns 수명을 관리. App 컨테이너들은 pause의 netns를 공유 (동일 eth0, lo, 포트 공간)

그림 3. pause 컨테이너 netns 구조. pause 컨테이너가 Pod netns의 소유자이며, 동일 Pod 내 모든 컨테이너가 netns를 공유합니다. Calico 환경에서 호스트의 cali* veth 인터페이스가 각 Pod netns와 연결됩니다.

CNI 체인 처리 흐름

Kubernetes CNI는 단일 플러그인이 아닌 체인(chaining) 방식으로 동작합니다. 메인 CNI가 veth/bridge를 생성하고, 메타 CNI가 대역폭 제한이나 포트 매핑을 추가합니다.

CNI 체인 처리 흐름 (ADD 커맨드) kubelet Pod 스케줄링 CRI containerd/runc pause 컨테이너 CLONE_NEWNET → 새 netns CNI ADD 호출 CNI_NETNS=/proc/PID/ns/net /etc/cni/net.d/10-flannel.conflist plugins: [flannel(main), portmap(meta), bandwidth(meta)] 체인 순서대로 각 플러그인 ADD 호출 Main CNI (flannel/bridge) 1. veth 쌍 생성 2. eth0 → Pod netns로 이동 3. IPAM → IP 할당 Meta CNI (portmap) hostPort → containerPort iptables DNAT 규칙 추가 (호스트 netns에 적용) Meta CNI (bandwidth) tc qdisc TBF 설정 ingress/egress 속도 제한 (Pod netns veth에 적용) CNI ADD 결과 JSON 반환 "interfaces": [{"name":"eth0","sandbox":"/proc/PID/ns/net"}], "ips": [{"address":"10.244.1.5/24"}] netns 파일: /var/run/netns/cni-XXXXXXXX (bind mount, CNI 관리)

그림 4. CNI 체인 처리 흐름. kubelet → CRI → pause 컨테이너 생성 → CNI ADD 호출 순서로 진행되며, 메인 CNI와 메타 CNI가 체인으로 연결되어 각자의 역할을 담당합니다.

# /etc/cni/net.d/10-flannel.conflist 예시
{
  "cniVersion": "0.3.1",
  "name": "cbr0",
  "plugins": [
    {
      "type": "flannel",      /* main CNI: VXLAN 오버레이 */
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",      /* meta CNI: hostPort 매핑 */
      "capabilities": {"portMappings": true}
    },
    {
      "type": "bandwidth",    /* meta CNI: 대역폭 제한 */
      "capabilities": {"bandwidth": true}
    }
  ]
}

Flannel netns 구현

# Flannel VXLAN 모드 패킷 경로 추적

# Pod1 (Node1, netns A) → Pod3 (Node2, netns C) 통신

# 단계 1: Pod1 netns에서 출발
ip netns exec cni-Pod1 ip route get 10.244.2.5
# 10.244.2.5 via 10.244.1.1 dev eth0
# → 기본 게이트웨이로 전달

# 단계 2: veth → cni0 브리지 (Host netns)
# Pod1 eth0 → veth peer (cali/veth on host) → cni0 bridge

# 단계 3: 호스트 라우팅 테이블 조회
ip route get 10.244.2.5
# 10.244.2.5 via 10.244.2.0 dev flannel.1
# → flannel.1 (VTEP) 인터페이스로 전달

# 단계 4: VXLAN 캡슐화
# flannel.1: VNI=1, 원본 L2 프레임을 UDP 8472로 캡슐화
# UDP 패킷 → Node2의 eth0으로 전송

# 단계 5: Node2에서 역캡슐화
# flannel.1 VTEP가 수신 → 원본 L2 프레임 추출
# → cni0 브리지 → Pod3 netns veth → Pod3

# flannel VTEP 정보 확인
bridge fdb show dev flannel.1
# aa:bb:cc:dd:ee:ff dst 192.168.1.2 self permanent
# → MAC aa:bb:cc:.. 는 Node2(192.168.1.2)의 VTEP로 전송

Calico netns 구현

# Calico: veth cali* + BGP 라우팅

# Pod netns에서의 인터페이스 확인
ip netns exec cni-PodA ip link show
# 2: eth0@if15: <BROADCAST,MULTICAST,UP> mtu 1500
#    ↑ if15가 호스트의 cali* 인터페이스 ifindex

# 호스트에서 cali* 인터페이스 확인
ip link show | grep cali
# 15: caliXXXXXXXXXXX@if2: ...
# → ifindex 2와 연결 (Pod의 eth0)

# Calico BGP 라우팅 테이블 (Felix가 관리)
ip route show
# 10.244.1.5 dev caliXXXXXXXXXXX scope link
# 10.244.2.0/24 via 192.168.1.2 dev eth0 proto bird
# → 다른 노드의 Pod 대역은 BGP(bird)로 학습

# 패킷 경로: Pod → cali* → 호스트 라우팅 → BGP → 다른 노드
# NAT 없음 (BGP로 실제 Pod IP 라우팅)

# Calico NetworkPolicy → iptables/eBPF 규칙
# Felix가 각 Pod의 veth에 tc BPF 프로그램 또는 iptables 체인 적용
iptables -L cali-fw-caliXXXXXXXXXXX -n
# Chain cali-fw-caliXXXXXXXXXXX (1 references)
# → Pod별 독립 방화벽 체인

Cilium netns 구현 (eBPF)

# Cilium: TC BPF at lxc (veth) 인터페이스

# Pod netns에서의 인터페이스 (lxcXXXX는 호스트 측 veth)
ip link show type veth | grep lxc
# 25: lxcXXXXXXXX@if2: ...

# TC BPF 프로그램 확인 (Cilium이 설치)
tc filter show dev lxcXXXXXXXX ingress
# filter protocol all pref 1 bpf
#   bpf from-container.o:[from-container] direct-action
tc filter show dev lxcXXXXXXXX egress
# filter protocol all pref 1 bpf
#   bpf to-container.o:[to-container] direct-action

# Cilium identity 기반 정책 (netns 대신 identity 사용)
cilium endpoint list
# ENDPOINT   POLICY    IDENTITY  LABELS
# 1234       Enabled   12345     k8s:app=frontend

# BPF 맵에서 정책 확인
cilium bpf policy get 1234

# eBPF 서비스 메시 (no sidecar)
# → Cilium이 각 Pod의 veth에 BPF 프로그램 삽입
# → kube-proxy 없이 BPF 맵으로 서비스 로드밸런싱
# → 모든 netfilter/iptables 우회

# hostNetwork: true 파드 보안 영향
# → 호스트 netns를 직접 사용 → 완전한 호스트 네트워크 접근
# → NetworkPolicy 적용 안 됨
# → 최소 권한 원칙 위반 가능성
kubectl get pod -o json | jq '.spec.hostNetwork'   # true이면 위험

멀티테넌트 NGFW 라우팅 구조

멀티테넌트 NGFW에서 각 테넌트가 독립된 netns와 VRF를 사용하는 구조를 시각적으로 확인합니다. veth 쌍으로 호스트와 테넌트가 연결되고, 각 테넌트는 완전히 독립된 nftables 규칙을 가집니다.

Host netns eth0 (외부 인터페이스, 203.0.113.1) veth-ha, veth-hb (테넌트 연결) 테넌트 A netns VRF-A (table 10) 10.1.0.0/24 내부망 nftables: 테넌트 A 전용 conntrack: 독립 테이블 veth-ca (peer: veth-ha on host) ip rule: from 10.1.0.0/24 table 10 default gw: 10.100.0.1 (ISP-A) 테넌트 B netns VRF-B (table 20) 10.2.0.0/24 내부망 nftables: 테넌트 B 전용 conntrack: 독립 테이블 veth-cb (peer: veth-hb on host) ip rule: from 10.2.0.0/24 table 20 default gw: 10.200.0.1 (ISP-B) veth 쌍 veth 쌍 완전 격리 (직접 통신 불가) 각 테넌트는 독립 netns로 완전 격리 (라우팅 테이블, conntrack, nftables, sysctl 모두 분리) 테넌트 간 트래픽: 호스트 netns 경유 (명시적 라우팅 설정 시에만 가능)

그림 5. 멀티테넌트 NGFW 라우팅 구조. 각 테넌트는 독립 netns를 가지며 veth 쌍으로 호스트와 연결됩니다. 테넌트 간 직접 통신은 차단되고, 모든 정책이 완전히 격리됩니다.

커널 소스 구조

네트워크 네임스페이스 관련 커널 소스는 여러 디렉터리에 분산되어 있습니다. 핵심 구조체와 초기화 코드, 그리고 각 서브시스템별 netns 지원 코드로 구성됩니다.

주요 소스 파일

파일 경로 역할
include/net/net_namespace.h struct net 정의, 헬퍼 매크로
net/core/net_namespace.c copy_net_ns(), setup_net(), cleanup_net(), init_net 선언
net/core/dev.c netdev의 netns 이동: dev_change_net_namespace()
drivers/net/veth.c veth 드라이버: veth_xmit(), veth_dev_init()
drivers/net/macvlan.c macvlan 드라이버
drivers/net/ipvlan/ipvlan_main.c ipvlan 드라이버
net/ipv4/fib_frontend.c FIB 테이블 초기화 (netns별)
net/netfilter/core.c nf_register_net_hook() - netns별 훅 등록
net/netfilter/nf_conntrack_core.c conntrack netns 초기화
fs/proc/namespaces.c /proc/[pid]/ns/net 구현
kernel/nsproxy.c nsproxy 구조체 관리, copy_namespaces()

pernet_operations: 서브시스템 등록

/* 새 netns 생성 시 자동 호출되는 서브시스템 초기화 패턴 */

/* net/ipv4/route.c */
static int __net_init ip_rt_net_init(struct net *net)
{
    /* 새 netns의 IPv4 라우팅 캐시 초기화 */
    net->ipv4.fib_main   = fib_trie_init();
    net->ipv4.fib_local  = fib_trie_init();
    return 0;
}

static void __net_exit ip_rt_net_exit(struct net *net)
{
    fib_free(net->ipv4.fib_main);
    fib_free(net->ipv4.fib_local);
}

static struct pernet_operations ipv4_rt_ops = {
    .init = ip_rt_net_init,
    .exit = ip_rt_net_exit,
};

/* 모듈 초기화 시 전역 목록에 등록 */
register_pernet_subsys(&ipv4_rt_ops);
/* → 이후 copy_net_ns() → setup_net() 호출 시 ip_rt_net_init() 자동 실행 */

인터페이스의 netns 이동

/* net/core/dev.c */
int dev_change_net_namespace(struct net_device *dev,
                             struct net *net, const char *pat)
{
    /* 현재 netns에서 제거 */
    unlist_netdevice(dev);

    /* dev의 참조 netns 변경 */
    dev_net_set(dev, net);

    /* 새 netns에서 인터페이스 이름 충돌 확인 및 등록 */
    err = dev_get_valid_name(net, dev, pat);
    list_netdevice(dev);

    /* netlink 이벤트 브로드캐스트 (각 netns에) */
    call_netdevice_notifiers(NETDEV_UNREGISTER, dev);  /* 이전 netns */
    call_netdevice_notifiers(NETDEV_REGISTER, dev);    /* 새 netns */

    return 0;
}

/* ip link set eth0 netns ns1 → rtnetlink이 위 함수 호출 */

진단 및 모니터링

운영 환경에서 네트워크 네임스페이스 문제를 진단하는 주요 도구와 방법을 설명합니다.

기본 진단 명령

# 현재 시스템의 모든 netns 목록 (inode 기반)
lsns -t net

# 특정 프로세스의 netns 확인
ls -la /proc/[pid]/ns/net

# 두 프로세스가 같은 netns인지 확인 (inode 비교)
stat /proc/[pid1]/ns/net /proc/[pid2]/ns/net

# ip netns 명령으로 이름 있는 netns 목록
ip netns list

# 특정 netns 내 전체 네트워크 상태 확인
ip netns exec ns1 ip -all link
ip netns exec ns1 ip -all addr
ip netns exec ns1 ip route show table all
ip netns exec ns1 ss -tulpn

# nsenter: 실행 중인 프로세스의 netns에 진입
nsenter --target [pid] --net /bin/bash

# nsenter: 이름 있는 netns에 진입
nsenter --net=/var/run/netns/ns1 /bin/bash

conntrack 및 Netfilter 진단

# 각 netns의 conntrack 테이블 확인
ip netns exec ns1 conntrack -L
ip netns exec ns1 conntrack -S  # 통계

# conntrack 테이블 크기 확인
ip netns exec ns1 cat /proc/sys/net/netfilter/nf_conntrack_count
ip netns exec ns1 cat /proc/sys/net/netfilter/nf_conntrack_max

# nftables 규칙 확인
ip netns exec ns1 nft list ruleset

# iptables 규칙 확인 (네임스페이스별)
ip netns exec ns1 iptables -t nat -nvL
ip netns exec ns1 iptables -t filter -nvL

bpftrace로 netns 생성 추적

# clone(CLONE_NEWNET) 호출 추적
bpftrace -e '
tracepoint:syscalls:sys_enter_clone {
    if (args->clone_flags & 0x40000000) {  /* CLONE_NEWNET */
        printf("PID %d (%s) 새 netns 생성\n", pid, comm);
    }
}
'

# unshare(CLONE_NEWNET) 추적
bpftrace -e '
tracepoint:syscalls:sys_enter_unshare {
    if (args->unshare_flags & 0x40000000) {
        printf("PID %d (%s) netns 분리\n", pid, comm);
    }
}
'

# setns(CLONE_NEWNET) 추적 - 컨테이너 진입 탐지
bpftrace -e '
tracepoint:syscalls:sys_enter_setns {
    if (args->nstype == 0x40000000) {
        printf("PID %d (%s) netns 전환, fd=%d\n", pid, comm, args->fd);
    }
}
'

# 네임스페이스 생성 시 copy_net_ns 커널 함수 추적
bpftrace -e '
kprobe:copy_net_ns {
    printf("copy_net_ns 호출: PID %d\n", pid);
}
kretprobe:copy_net_ns {
    printf("copy_net_ns 완료: 새 net* = %p\n", retval);
}
'

성능 모니터링

# netns별 인터페이스 통계
ip netns exec ns1 ip -s link

# veth 쌍 처리량 측정
ip netns exec ns1 iperf3 -s &
ip netns exec ns2 iperf3 -c 10.0.1.1 -t 10

# 소켓 통계
ip netns exec ns1 ss -s          # 소켓 요약
ip netns exec ns1 ss -tulpn      # 열린 포트
ip netns exec ns1 ss -i          # TCP 내부 정보

# netns당 메모리 사용량 확인 (struct net 크기)
# /proc/slabinfo 에서 net_namespace 슬랩 확인
grep net_namespace /proc/slabinfo

# 모든 netns의 인터페이스 한번에 조회
for ns in $(ip netns list | awk '{print $1}'); do
    echo "=== $ns ==="
    ip netns exec $ns ip link show
done

보안 감사

# 예상치 못한 netns 생성 감지 (auditd)
auditctl -a always,exit -F arch=b64 -S unshare \
    -F a0=0x40000000 -k netns_create

# 컨테이너 탈출 시도 감지 (다른 netns로의 setns)
auditctl -a always,exit -F arch=b64 -S setns \
    -F a1=0x40000000 -k netns_enter

# User namespace와 결합된 netns 생성 (권한 상승 위험)
bpftrace -e '
tracepoint:syscalls:sys_enter_unshare {
    $flags = args->unshare_flags;
    if (($flags & 0x40000000) && ($flags & 0x10000000)) {
        /* CLONE_NEWNET | CLONE_NEWUSER */
        printf("경고: User+Net NS 동시 생성 PID=%d (%s)\n", pid, comm);
    }
}
'

흔한 실수와 트러블슈팅

네트워크 네임스페이스 운영에서 빈번하게 발생하는 실수와 해결 방법을 정리합니다. 초기 설정부터 운영 환경 디버깅까지 실무에서 겪는 대표적인 문제를 다룹니다.

실수 1: lo 인터페이스 미활성화

# 새 netns 생성 후 lo가 DOWN 상태 (기본값)
ip netns add test
ip netns exec test ip link show lo
# 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
# ↑ state DOWN — localhost 통신 불가!

# 증상: 127.0.0.1 ping 실패, 로컬 서비스 접근 불가
ip netns exec test ping -c 1 127.0.0.1
# connect: Network is unreachable

# 해결: lo 반드시 활성화
ip netns exec test ip link set lo up

# 자동화 스크립트에 lo up 포함 필수:
create_netns() {
    ip netns add "$1"
    ip netns exec "$1" ip link set lo up  # 필수!
}

실수 2: veth 한쪽만 UP 설정

# veth 쌍의 양쪽 모두 UP이어야 통신 가능
ip link add veth-a type veth peer name veth-b
ip link set veth-b netns ns1

# 실수: host 쪽만 UP → ns1의 veth-b는 여전히 DOWN
ip link set veth-a up

# 올바른 설정: 양쪽 모두 UP + IP 설정
ip addr add 10.0.0.1/24 dev veth-a
ip link set veth-a up
ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth-b
ip netns exec ns1 ip link set veth-b up

# 진단: 양쪽 상태 확인
ip link show veth-a | grep state
ip netns exec ns1 ip link show veth-b | grep state

실수 3: 기본 라우팅 미설정

# 새 netns에는 라우팅 테이블이 비어있음
ip netns exec ns1 ip route show
# (연결된 서브넷 경로만 자동 생성, 기본 게이트웨이 없음)

# 증상: 같은 서브넷은 통신되지만 외부 통신 불가
ip netns exec ns1 ping 10.0.0.1    # 성공 (같은 서브넷)
ip netns exec ns1 ping 8.8.8.8     # 실패 (라우팅 없음)

# 해결: 기본 게이트웨이 + 호스트에서 ip_forward + NAT
ip netns exec ns1 ip route add default via 10.0.0.1
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -j MASQUERADE

실수 4: ip_forward per-netns 미설정

# ip_forward는 netns별로 독립 — 각 netns에서 개별 설정 필요!
# 호스트에서 ip_forward=1이어도 ns1에서는 기본 0

sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1  (호스트)

ip netns exec ns1 sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0  (ns1 — 기본값)

# ns1이 라우터/게이트웨이 역할을 하려면 반드시 설정
ip netns exec ns1 sysctl -w net.ipv4.ip_forward=1

# rp_filter도 per-netns — 비대칭 경로 시 패킷 드롭 원인
ip netns exec ns1 sysctl -w net.ipv4.conf.all.rp_filter=0

실수 5: ARP flux 문제 (다중 인터페이스)

# 하나의 netns에 여러 인터페이스 + 같은 서브넷일 때
# ARP 응답이 잘못된 인터페이스에서 발생하는 문제

# 해결: arp_ignore, arp_announce 설정
ip netns exec ns1 sysctl -w net.ipv4.conf.all.arp_ignore=1
# 1: 요청된 IP가 해당 인터페이스에 있을 때만 ARP 응답
ip netns exec ns1 sysctl -w net.ipv4.conf.all.arp_announce=2
# 2: 최적의 로컬 주소를 ARP 소스로 사용

실수 6: orphan netns (좀비 네임스페이스)

# Docker 컨테이너 비정상 종료 → netns가 남아있음
# → veth, conntrack, 메모리 누수

# 진단: 참조 없는 netns 찾기
lsns -t net -o NS,PID,COMMAND | sort -n

# named netns 중 프로세스 없는 항목 감지
for ns in $(ip netns list | awk '{print $1}'); do
    PIDS=$(ip netns pids "$ns" 2>/dev/null | wc -l)
    if [ "$PIDS" -eq 0 ]; then
        echo "경고: $ns 에 실행 중인 프로세스 없음 (orphan?)"
    fi
done

# 정리: 불필요한 netns 삭제
ip netns delete orphan-ns
# → umount + unlink → 참조 카운트 0이면 cleanup_net()

실수 7: Docker netns가 ip netns list에 안 보임

# Docker는 /var/run/netns/에 bind mount하지 않음 (Anonymous)
# → ip netns list로는 Docker 컨테이너 netns가 보이지 않음

# 해결 1: lsns로 모든 netns 확인
lsns -t net

# 해결 2: Docker PID에서 직접 netns 진입
PID=$(docker inspect -f '{{.State.Pid}}' my-container)
nsenter --target $PID --net ip addr

# 해결 3: Docker netns를 named netns로 등록 (디버깅용)
mkdir -p /var/run/netns
ln -s /proc/$PID/ns/net /var/run/netns/docker-my-container
ip netns exec docker-my-container ip route show
# 디버깅 후 심볼릭 링크 정리
rm /var/run/netns/docker-my-container

실수 8: 이동 불가 디바이스를 netns로 이동 시도

# 모든 디바이스가 netns 이동 가능한 것은 아님
# NETIF_F_NETNS_LOCAL 플래그가 설정된 디바이스는 이동 불가

# 이동 불가 디바이스 예시:
ip link set lo netns ns1
# RTNETLINK answers: Invalid argument (lo는 netns-local)

ip link set br0 netns ns1
# RTNETLINK answers: Invalid argument (bridge는 netns-local)

ip link set tunl0 netns ns1
# RTNETLINK answers: Invalid argument (tunl0는 netns-local)

# 이동 가능/불가 확인
ethtool -k eth0 | grep netns-local
# netns-local: off [fixed]  → 이동 가능
ethtool -k br0 | grep netns-local
# netns-local: on [fixed]   → 이동 불가

# 이동 가능 디바이스 목록:
# - 물리 NIC (eth0, ens3 등)
# - veth 쌍의 각 끝
# - macvlan / ipvlan 서브인터페이스
# - VLAN 서브인터페이스 (eth0.100)
# - tun/tap 디바이스
# - wireguard (wg0)

# 이동 불가 디바이스 (NETIF_F_NETNS_LOCAL):
# - lo (loopback) — 각 netns가 자체 lo를 가짐
# - bridge (br0) — netns 내에서 새로 생성해야 함
# - bonding (bond0)
# - vxlan, geneve, gre 터널 (일부)
# - dummy 인터페이스
/* include/linux/netdevice.h */
/* NETIF_F_NETNS_LOCAL 플래그: 이 디바이스는 netns 이동 불가 */
#define NETIF_F_NETNS_LOCAL   (1ULL << 38)

/* dev_change_net_namespace()에서 확인: */
if (dev->features & NETIF_F_NETNS_LOCAL)
    return -EINVAL;  /* 이동 거부 */

/* bridge 디바이스 등록 시: */
dev->features |= NETIF_F_NETNS_LOCAL;  /* netns 이동 차단 */

트러블슈팅 체크리스트

증상 확인 사항 해결 명령
localhost 접근 불가 lo 상태 확인 ip link set lo up
같은 서브넷 ping 실패 양쪽 veth UP + IP 설정 ip link show, ip addr show
외부 통신 불가 기본 라우트 + ip_forward + NAT ip route add default via ...
포워딩 안 됨 per-netns ip_forward sysctl -w net.ipv4.ip_forward=1
비대칭 경로 드롭 rp_filter 설정 sysctl -w net.ipv4.conf.all.rp_filter=0
conntrack full per-netns conntrack_max sysctl -w net.netfilter.nf_conntrack_max=...
veth peer 찾기 ifindex로 역추적 ethtool -S veth0 → peer_ifindex
netns 식별 inode 번호 비교 stat /proc/PID/ns/net
Docker netns 미노출 Anonymous netns lsns -t net 또는 nsenter
디바이스 이동 거부 NETIF_F_NETNS_LOCAL ethtool -k dev | grep netns-local

실습 랩: 3-tier 마이크로서비스 네트워크

프론트엔드, 백엔드, 데이터베이스를 각각 독립 netns에 배치하고, nftables로 계층 간 접근을 제어하는 실습입니다. 실제 컨테이너 없이 netns + veth + nftables만으로 구성합니다.

3-tier 마이크로서비스 netns 실습 구조 외부 클라이언트 (curl) Host netns — br-svc (10.100.0.1/24) NAT + ip_forward + DNAT :8080 → frontend veth veth veth ns-frontend eth0: 10.100.0.10/24 (:80) nftables: input 80 허용 → backend:3000 | DB 직접 차단 ns-backend eth0: 10.100.0.20/24 (:3000) nftables: 3000 (frontend만) → database:5432 | 외부 차단 ns-database eth0: 10.100.0.30/24 (:5432) nftables: 5432 (backend만) 외부+frontend 직접 차단 허용: 외부→frontend(:80) | frontend→backend(:3000) | backend→database(:5432) 금지: 외부→backend/database 직접 | frontend→database 직접

그림 6. 3-tier 마이크로서비스 실습 구조. 각 tier가 독립 netns를 가지며, nftables로 계층 간 접근 제어를 구현합니다.

랩 환경 구성 스크립트

#!/bin/bash
# 3-tier 마이크로서비스 netns 실습 — 전체 구성 스크립트
# root 권한 필요

set -e

# === 1. 정리 (기존 실습 환경 제거) ===
cleanup() {
    for ns in ns-frontend ns-backend ns-database; do
        ip netns delete $ns 2>/dev/null || true
    done
    ip link delete br-svc 2>/dev/null || true
}
trap cleanup EXIT

# === 2. 브리지 생성 (호스트 netns) ===
ip link add br-svc type bridge
ip addr add 10.100.0.1/24 dev br-svc
ip link set br-svc up
sysctl -w net.ipv4.ip_forward=1 >/dev/null

# === 3. 3개 netns + veth 생성 ===
declare -A NS_IPS=(
    [ns-frontend]="10.100.0.10/24"
    [ns-backend]="10.100.0.20/24"
    [ns-database]="10.100.0.30/24"
)

for ns in "${!NS_IPS[@]}"; do
    ip netns add $ns
    ip netns exec $ns ip link set lo up

    VETH_HOST="vh-${ns#ns-}"
    VETH_NS="vn-${ns#ns-}"
    ip link add $VETH_HOST type veth peer name $VETH_NS

    # 호스트: 브리지에 연결
    ip link set $VETH_HOST master br-svc
    ip link set $VETH_HOST up

    # netns: IP 설정
    ip link set $VETH_NS netns $ns
    ip netns exec $ns ip addr add ${NS_IPS[$ns]} dev $VETH_NS
    ip netns exec $ns ip link set $VETH_NS up
    ip netns exec $ns ip route add default via 10.100.0.1
done

# === 4. nftables 방화벽 정책 ===

# frontend: 80번 포트만 허용
ip netns exec ns-frontend nft add table inet filter
ip netns exec ns-frontend nft add chain inet filter input \
    '{ type filter hook input priority 0; policy drop; }'
ip netns exec ns-frontend nft add rule inet filter input \
    ct state established,related accept
ip netns exec ns-frontend nft add rule inet filter input iif lo accept
ip netns exec ns-frontend nft add rule inet filter input \
    tcp dport 80 accept
ip netns exec ns-frontend nft add rule inet filter input \
    icmp type echo-request accept

# backend: 3000번 포트, frontend(10.100.0.10)에서만 허용
ip netns exec ns-backend nft add table inet filter
ip netns exec ns-backend nft add chain inet filter input \
    '{ type filter hook input priority 0; policy drop; }'
ip netns exec ns-backend nft add rule inet filter input \
    ct state established,related accept
ip netns exec ns-backend nft add rule inet filter input iif lo accept
ip netns exec ns-backend nft add rule inet filter input \
    ip saddr 10.100.0.10 tcp dport 3000 accept
ip netns exec ns-backend nft add rule inet filter input \
    icmp type echo-request accept

# database: 5432번 포트, backend(10.100.0.20)에서만 허용
ip netns exec ns-database nft add table inet filter
ip netns exec ns-database nft add chain inet filter input \
    '{ type filter hook input priority 0; policy drop; }'
ip netns exec ns-database nft add rule inet filter input \
    ct state established,related accept
ip netns exec ns-database nft add rule inet filter input iif lo accept
ip netns exec ns-database nft add rule inet filter input \
    ip saddr 10.100.0.20 tcp dport 5432 accept

echo "=== 구성 완료 ==="
echo "frontend: 10.100.0.10 (:80)"
echo "backend:  10.100.0.20 (:3000)"
echo "database: 10.100.0.30 (:5432)"

검증 테스트

# === 5. 연결 테스트 ===

# 테스트 서버 시작 (각 netns에서)
ip netns exec ns-frontend python3 -m http.server 80 &
ip netns exec ns-backend python3 -m http.server 3000 &
ip netns exec ns-database python3 -m http.server 5432 &
sleep 1

echo "=== 허용된 경로 ==="
# 호스트 → frontend (허용)
curl -s -o /dev/null -w "호스트→frontend:80  %{http_code}\n" \
    http://10.100.0.10:80

# frontend → backend (허용)
ip netns exec ns-frontend curl -s -o /dev/null \
    -w "frontend→backend:3000  %{http_code}\n" \
    http://10.100.0.20:3000

# backend → database (허용)
ip netns exec ns-backend curl -s -o /dev/null \
    -w "backend→database:5432  %{http_code}\n" \
    http://10.100.0.30:5432

echo "=== 차단된 경로 ==="
# 호스트 → database 직접 (차단)
timeout 2 curl -s http://10.100.0.30:5432 || \
    echo "호스트→database:5432  BLOCKED"

# frontend → database 직접 (차단)
ip netns exec ns-frontend timeout 2 curl -s \
    http://10.100.0.30:5432 || \
    echo "frontend→database:5432  BLOCKED"

# 호스트 → backend 직접 (차단)
timeout 2 curl -s http://10.100.0.20:3000 || \
    echo "호스트→backend:3000  BLOCKED"

echo "=== 각 netns 방화벽 규칙 ==="
for ns in ns-frontend ns-backend ns-database; do
    echo "--- $ns ---"
    ip netns exec $ns nft list ruleset | grep -E "accept|drop|policy"
done

kill %1 %2 %3 2>/dev/null

실습 확장 과제

확장 과제:
  1. conntrack 관찰: 각 netns에서 conntrack -L로 연결 추적 테이블을 확인하고, frontend→backend 트래픽이 backend netns의 conntrack에만 나타나는지 검증하세요.
  2. tc 대역폭 제한: backend의 veth에 tc qdisc add dev vn-backend root tbf rate 1mbit burst 32kbit latency 400ms를 추가하고 iperf3로 속도 제한 효과를 측정하세요.
  3. 패킷 캡처: ip netns exec ns-backend tcpdump -i vn-backend -nn -c 20로 패킷을 캡처하고, 차단된 패킷의 타임아웃 패턴을 분석하세요.
  4. VRF 추가: backend netns 내에 VRF를 생성하고, 서비스와 관리 트래픽을 VRF 테이블로 분리해 보세요.
  5. NETNS_ID 설정: ip netns set ns-frontend 100으로 NETNS_ID를 부여하고, cross-netns 디바이스 참조를 테스트하세요.

네트워크 네임스페이스와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.