네트워크 네임스페이스 심화
리눅스 네트워크 네임스페이스 내부 구조, clone/unshare/setns 시스템 콜, VRF 멀티테넌트 NGFW, veth/macvlan/bridge 간 패킷 전달, 컨테이너 네트워킹(Docker/K8s) 연관, netfilter 격리 종합 가이드. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.
핵심 요약
- 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 생성을 감사합니다.
단계별 이해
- 격리 범위 파악
어떤 네트워크 자원이 네임스페이스 경계로 분리되는지 표로 확인합니다. 인터페이스, 라우팅 테이블, conntrack, 소켓 등이 완전히 독립됩니다. - struct net 구조 이해
커널이 네임스페이스를struct net으로 표현하며, 내부에 라우팅 테이블, 인터페이스 리스트, Netfilter 훅 테이블이 포함됩니다. - API 실습
ip netns add,ip netns exec로 네임스페이스를 생성하고 진입하는 기본 패턴을 익힙니다. - veth 연결
두 네임스페이스를 veth 쌍으로 연결하고 ping으로 통신을 검증합니다. macvlan/ipvlan과의 차이를 비교합니다. - 컨테이너 연결 구조 분석
Docker/K8s가 내부적으로 netns + veth + bridge를 어떻게 조합하는지 추적합니다. - 진단 도구 활용
ip netns list,nsenter,lsns,bpftrace로 실행 중인 네임스페이스를 점검합니다. - 영속성 메커니즘 이해
ip netns add가 내부적으로unshare + mount --bind를 수행함을 이해하고, Named vs Anonymous netns의 차이를 파악합니다./proc/PID/ns/netinode 번호로 두 프로세스가 같은 netns인지 확인합니다. - K8s 네트워크 심화 분석
pause 컨테이너 netns 공유 구조, CNI 체인(main + meta 플러그인), Flannel/Calico/Cilium의 각기 다른 데이터 경로를 추적합니다.ip netns exec cni-* ip route로 Pod 내부 라우팅을 직접 확인합니다.
개요: 네트워크 격리의 핵심
리눅스 네트워크 네임스페이스(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에서 훅 테이블을 초기화합니다.
그림 1. 호스트 netns와 테넌트 netns 간의 격리 구조. 각 netns는 독립된 인터페이스, 라우팅 테이블, Netfilter 규칙, conntrack 테이블을 가집니다.
시스템 콜 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 명령 내부 동작
iproute2의 ip 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/ |
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 전용 체인만 출력됨
컨테이너 네트워킹 (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 | 메타 플러그인 | 다중 인터페이스 | 위임 | 위임 |
그림 2. Kubernetes CNI(flannel VXLAN) 패킷 흐름. 각 Pod는 독립 netns를 가지며, veth → cni0 브리지 → VXLAN 터널을 통해 노드 간 통신합니다.
네임스페이스 영속성과 마운트
네트워크 네임스페이스의 수명은 기본적으로 이를 참조하는 프로세스의 수명에 종속됩니다.
하지만 /var/run/netns/에 bind mount하여 프로세스 없이도 네임스페이스를 영속화할 수 있습니다.
이 메커니즘이 ip netns add 명령의 핵심입니다.
네임스페이스 수명주기: 프로세스 참조 vs 파일 참조
커널은 네트워크 네임스페이스의 참조를 두 가지 방식으로 유지합니다.
- 프로세스 참조: 프로세스의
nsproxy->net_ns포인터가struct net을 참조합니다. 프로세스가 종료되면 참조가 해제되고, 더 이상 참조하는 프로세스가 없으면put_net()→cleanup_net()으로 소멸됩니다. - 파일 참조(pin):
/proc/self/ns/net을 bind mount한 파일이 열려 있거나 마운트 포인트로 존재하는 동안 netns가 유지됩니다. 파일 디스크립터나 마운트가 netns의ns_common을 통해 참조를 유지합니다.
/* 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;
}
고급 정책 라우팅
각 네트워크 네임스페이스는 완전히 독립된 라우팅 테이블과 정책 규칙(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 # 이후
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
그림 3. pause 컨테이너 netns 구조. pause 컨테이너가 Pod netns의 소유자이며, 동일 Pod 내 모든 컨테이너가 netns를 공유합니다. Calico 환경에서 호스트의 cali* veth 인터페이스가 각 Pod netns와 연결됩니다.
CNI 체인 처리 흐름
Kubernetes CNI는 단일 플러그인이 아닌 체인(chaining) 방식으로 동작합니다. 메인 CNI가 veth/bridge를 생성하고, 메타 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 규칙을 가집니다.
그림 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);
}
}
'
관련 문서
네트워크 네임스페이스와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.