BPF
- BPF(Berkeley Packet Filter): Kernel space 에서 packet 을 효율적으로 필터링하기 위한사한 뒤 필터링하면 성능적으로 비효율적이다.
- 패킷은 매우 빠른 속도로 들어오고, 대부분의 경우 사용자가 보고 싶은 패킷은 전체 패킷 중 일부이다.
- 그래서 BPF 는 User space 로 패킷을 넘기기 전에 Kernel space 에서 먼저 필터링을 수행한다.
- BPF 는 작은 필터 프로그램을 커널 안에서 실행하고, 그 결과에 따라 해당 패킷을 User space 로 전달할지 결정한다.
- 현재는 기존 BPF 를 eBPF 와 구분하기 위해 Classic BPF 또는 cBPF 라고 부르기도 한다.
BPF 가 필요한 이유
패킷 캡쳐 도구는 네트워크 인터페이스로 들어오는 패킷을 확인해야 한다.
예를 들어 사용자가 다음과 같은 명령어를 실행했다고 하면
tcpdump port 80
사용자는 모든 패킷을 보고 싶은 것이 아니라 80번 포트와 관련된 패킷만 보고 싶은 것이다.
가장 단순한 방식은 Kernel 이 모든 패킷을 User space 로 복사하고, tcpdump 가 User space 에서 직접 조건을 검사하는 방식이다.
하지만 이 방식은 비효율적이다.
- 필요 없는 패킷까지 User space 로 복사해야 한다.
- Kernel space 와 User space 사이의 데이터 복사 비용이 발생한다.
- 패킷 수가 많아지면 캡쳐 도구의 부하가 커진다.
- 불필요한 context switch 와 메모리 사용량이 증가할 수 있다.
BPF 는 이런 문제를 해결하기 위해 패킷 필터링을 Kernel space 에서 먼저 수행한다.
즉, BPF 의 핵심 목적은 필요한 패킷만 User space 로 전달해서 패킷 캡쳐 성능을 높이는 것이다.
BPF 의 기본 구조
BPF 는 단순히 필터 문법만 의미하는 것이 아니라, 패킷 필터를 실행하기 위한 구조 전체를 의미한다.
BPF 는 대략 다음과 같은 흐름으로 동작한다.
- User space 프로그램이 필터 조건을 작성한다.
- 필터 조건이 BPF instruction 으로 변환된다.
- 변환된 BPF program 이 Kernel 로 전달된다.
- Kernel 은 패킷이 들어올 때마다 BPF program 을 실행한다.
- BPF program 의 반환값에 따라 패킷을 버리거나 User space 로 전달한다.
예를 들어 tcpdump 에서 다음과 같은 필터를 사용한다고 하면
tcpdump host 192.168.0.10
이 필터 조건은 내부적으로 BPF instruction 으로 변환된다.
BPF program 은 패킷의 Ethernet header, IP header, TCP header 등을 읽으면서 조건에 맞는지 검사한다.
조건에 맞으면 패킷을 User space 로 전달하고, 조건에 맞지 않으면 User space 로 전달하지 않는다.
User space 와 Kernel space
BPF 를 이해할 때 중요한 개념은 User space 와 Kernel space 이다.
- User space: 일반 프로그램이 실행되는 영역
- Kernel space: 운영체제 커널이 실행되는 영역
tcpdump, Wireshark, libpcap 같은 프로그램은 User space 에서 실행된다.
반면 네트워크 인터페이스로 들어오는 패킷은 먼저 Kernel 에 의해 처리된다.
따라서 패킷 캡쳐 도구가 패킷을 보기 위해서는 Kernel space 에 있는 패킷 데이터가 User space 로 전달되어야 한다.
이때 모든 패킷을 그대로 User space 로 넘기면 비용이 크다.
그래서 BPF 는 Kernel space 에서 먼저 필터를 실행하고, 필요한 패킷만 User space 로 전달한다.
즉, BPF 는 User space 의 패킷 캡쳐 프로그램과 Kernel 의 네트워크 처리 경로 사이에서 동작하는 필터링 계층이라고 볼 수 있다.
BPF Program
BPF Program 은 BPF instruction 으로 구성된 작은 프로그램이다.
일반적인 C 프로그램처럼 복잡한 로직을 수행하는 것이 아니라, 패킷의 특정 offset 을 읽고 조건을 비교한 뒤, 최종적으로 값을 반환하는 단순한 구조를 가진다.
BPF program 은 보통 다음과 같은 작업을 수행한다.
- 패킷의 특정 위치에서 값을 읽는다.
- 읽은 값이 특정 값과 같은지 비교한다.
- 조건이 맞으면 다음 instruction 으로 이동한다.
- 조건이 맞지 않으면 다른 instruction 으로 분기한다.
- 최종적으로 패킷을 받을지 버릴지 결정한다.
예를 들어 IP 패킷인지 확인하려면 Ethernet header 의 type 필드를 확인해야 한다.
Ethernet frame 에서 EtherType 은 보통 offset 12 위치에 있다.
IPv4 의 EtherType 값은 0x0800 이다.
BPF program 은 대략 다음과 같은 방식으로 동작할 수 있다.
패킷의 12번째 offset 에서 2바이트를 읽는다.
읽은 값이 0x0800 인지 비교한다.
맞으면 패킷을 통과시킨다.
아니면 패킷을 버린다.
이를 BPF 의 관점에서 보면 패킷 데이터를 직접 해석하는 작은 VM program 이라고 볼 수 있다.
BPF Virtual Machine
BPF 는 내부적으로 작은 가상 머신 구조를 가진다.
BPF program 은 실제 CPU instruction 이 아니라 BPF instruction 으로 구성된다.
Kernel 은 이 BPF instruction 을 해석해서 실행한다.
전통적인 BPF VM 은 매우 단순한 구조를 가진다.
대표적으로 다음과 같은 요소가 있다.
- Accumulator Register
- Index Register
- Scratch Memory
- Packet Data
- BPF Instruction
BPF 는 복잡한 범용 VM 이 아니라 패킷 필터링에 필요한 최소한의 연산만 수행하도록 설계되었다.
그래서 명령어 구조도 단순하고, 실행 흐름도 제한적이다.
이런 구조 덕분에 Kernel 안에서 비교적 안전하고 빠르게 실행될 수 있다.
Accumulator Register 와 Index Register
Classic BPF 에서는 주로 두 개의 Register 를 사용한다.
- A: Accumulator Register
- X: Index Register
A Register 는 연산의 중심이 되는 Register 이다.
패킷에서 값을 읽거나, 상수와 비교하거나, 산술 연산을 수행할 때 주로 A Register 가 사용된다.
X Register 는 보조적인 Index Register 이다.
패킷의 가변적인 위치를 계산하거나, 특정 offset 접근에 사용될 수 있다.
예를 들어 IP header 의 길이는 고정되어 있지 않을 수 있다.
IP header length 값을 읽어서 TCP header 의 시작 위치를 계산해야 할 때 X Register 같은 보조 Register 가 사용될 수 있다.
즉, BPF VM 은 일반적인 CPU 처럼 많은 Register 를 가진 구조가 아니라, 패킷 필터링에 필요한 최소한의 Register 만 가진 단순한 구조이다.
Scratch Memory
BPF 는 작은 Scratch Memory 를 사용할 수 있다.
Scratch Memory 는 BPF program 이 실행되는 동안 임시 값을 저장하기 위한 공간이다.
일반적인 프로그램의 지역 변수와 비슷하게 볼 수 있지만, 크기와 사용 방식이 제한적이다.
BPF program 은 패킷에서 읽은 값이나 중간 계산 결과를 Scratch Memory 에 저장하고, 이후 instruction 에서 다시 사용할 수 있다.
하지만 BPF 는 패킷 필터링을 위한 제한적인 환경이기 때문에 임의의 메모리 접근을 자유롭게 할 수 있는 구조는 아니다.
이런 제한은 BPF program 이 Kernel 안에서 실행되기 때문에 필요하다.
만약 BPF program 이 커널 메모리를 임의로 읽거나 쓸 수 있다면, 커널 안정성과 보안에 문제가 생길 수 있다.
BPF Instruction
BPF Instruction 은 BPF VM 이 실행하는 명령어이다.
BPF instruction 은 보통 다음과 같은 종류로 나눌 수 있다.
- Load instruction
- Store instruction
- ALU instruction
- Jump instruction
- Return instruction
Load instruction 은 packet data 나 memory 에서 값을 읽어오는 명령어이다.
예를 들어 패킷의 특정 offset 에서 1바이트, 2바이트, 4바이트 값을 읽을 수 있다.
Store instruction 은 값을 Scratch Memory 에 저장하는 명령어이다.
ALU instruction 은 덧셈, 뺄셈, AND, OR 같은 연산을 수행하는 명령어이다.
Jump instruction 은 조건에 따라 실행 흐름을 바꾸는 명령어이다.
Return instruction 은 BPF program 의 최종 결과를 반환하는 명령어이다.
BPF program 의 반환값은 패킷을 얼마나 받을지 결정하는 데 사용된다.
BPF Return Value
BPF program 은 마지막에 값을 반환한다.
이 반환값은 패킷을 User space 로 전달할지 결정하는 기준이 된다.
일반적으로 반환값이 0 이면 패킷을 버린다.
반환값이 0 보다 크면 해당 크기만큼 패킷을 전달한다.
예를 들어 BPF program 이 다음과 같은 의미의 값을 반환한다고 하면
return 0
이 패킷은 User space 로 전달되지 않는다.
반대로 다음과 같은 값을 반환한다고 하면
return 262144
패킷을 통과시키겠다는 의미로 볼 수 있다.
즉, BPF 에서 Return value 는 단순한 함수 반환값이 아니라 패킷 필터링 결과를 의미한다.
- 0: 패킷 버림
- 0 보다 큰 값: 해당 크기만큼 패킷 전달
이 구조 때문에 BPF program 은 마지막에 반드시 패킷을 받을지 말지 결정해야 한다.
BPF 와 tcpdump
BPF 를 가장 쉽게 볼 수 있는 도구는 tcpdump 이다.
tcpdump 는 사용자가 입력한 필터 표현식을 내부적으로 BPF instruction 으로 변환해서 사용한다.
예를 들어 다음과 같은 명령어가 있다고 하면
tcpdump tcp port 80
tcp port 80 이라는 표현식은 사람이 읽기 쉬운 필터 문법이다.
하지만 Kernel 은 이 문자열을 그대로 실행할 수 없다.
그래서 tcpdump 또는 libpcap 은 이 필터 표현식을 BPF bytecode 로 컴파일한다.
그 후 생성된 BPF program 을 Kernel 에 전달한다.
패킷이 들어오면 Kernel 은 해당 BPF program 을 실행해서 이 패킷이 tcp port 80 조건에 맞는지 검사한다.
조건에 맞으면 tcpdump 로 패킷을 전달하고, 조건에 맞지 않으면 전달하지 않는다.
즉, tcpdump 의 필터 문법은 내부적으로 BPF program 으로 변환되어 실행된다.
libpcap 과 BPF
libpcap 은 패킷 캡쳐를 위한 라이브러리이다.
tcpdump 나 여러 네트워크 분석 도구는 libpcap 을 사용해서 패킷을 캡쳐할 수 있다.
libpcap 은 사용자가 작성한 필터 표현식을 BPF instruction 으로 컴파일하는 역할도 수행한다.
예를 들어 사용자가 다음과 같은 필터를 작성했다고 하면
src host 10.0.0.1 and tcp port 443
libpcap 은 이 표현식을 분석해서 BPF instruction 으로 변환한다.
그 후 Kernel 에 해당 BPF program 을 붙여서 패킷 필터링을 수행하게 한다.
즉, BPF 를 직접 작성하지 않아도 tcpdump 나 libpcap 을 사용하면 내부적으로 BPF 를 사용하게 된다.
BPF Filter 의 예시
예를 들어 IPv4 패킷만 받고 싶은 상황을 생각해볼 수 있다.
Ethernet header 에서 EtherType 값이 0x0800 이면 IPv4 패킷이다.
이를 사람이 이해하기 쉬운 조건으로 표현하면 다음과 같다.
ether proto 0x0800
이 조건은 내부적으로 다음과 같은 흐름의 BPF program 으로 변환될 수 있다.
1. packet[12:2] 값을 읽는다.
2. 읽은 값이 0x0800 인지 비교한다.
3. 같으면 패킷을 통과시킨다.
4. 다르면 패킷을 버린다.
BPF instruction 관점에서는 대략 다음과 같은 형태로 볼 수 있다.
ldh [12]
jeq #0x0800, pass, drop
pass:
ret #262144
drop:
ret #0
여기서 ldh [12] 는 packet 의 12번째 offset 에서 half-word, 즉 2바이트를 읽는다는 의미이다.
jeq #0x0800 은 읽은 값이 0x0800 과 같은지 비교하는 조건 분기이다.
조건이 맞으면 ret #262144 로 패킷을 통과시키고, 조건이 맞지 않으면 ret #0 으로 패킷을 버린다.
이처럼 BPF 는 패킷의 특정 위치를 읽고 조건을 비교하는 방식으로 동작한다.
BPF 와 Packet Offset
BPF 는 패킷을 구조체처럼 직접 파싱하는 것이 아니라, packet data 의 offset 을 기준으로 값을 읽는다.
예를 들어 Ethernet frame 은 보통 다음과 같은 구조를 가진다.
Destination MAC: 6 bytes
Source MAC: 6 bytes
EtherType: 2 bytes
Payload: variable
EtherType 은 Ethernet header 의 12번째 offset 에 위치한다.
그래서 BPF program 은 IPv4 패킷인지 확인하기 위해 packet offset 12 에서 2바이트를 읽는다.
TCP port 를 확인하려면 Ethernet header, IP header 를 지나 TCP header 의 위치까지 계산해야 한다.
이때 IP header 의 길이는 옵션 여부에 따라 달라질 수 있다.
그래서 단순히 고정 offset 만 사용하는 것이 아니라, header length 값을 읽어서 다음 header 의 위치를 계산해야 하는 경우도 있다.
즉, BPF filter 를 정확하게 이해하려면 네트워크 프로토콜의 header 구조와 offset 계산을 함께 이해해야 한다.
BPF 의 실행 위치
BPF 는 패킷이 User space 로 전달되기 전에 Kernel space 에서 실행된다.
패킷이 네트워크 인터페이스로 들어오면 Kernel 은 해당 패킷을 처리하는 과정에서 socket 에 붙어있는 BPF filter 를 실행한다.
BPF filter 의 결과에 따라 해당 패킷이 socket 으로 전달될지 결정된다.
이 구조를 단순하게 나타내면 다음과 같다.
Network Interface
↓
Kernel Network Stack
↓
BPF Filter
↓
User space Program
BPF Filter 에서 조건에 맞지 않는 패킷은 User space Program 까지 올라가지 않는다.
따라서 User space 프로그램은 필요한 패킷만 받게 된다.
이것이 BPF 가 패킷 캡쳐 성능을 높일 수 있는 핵심 이유이다.
BPF Interpreter 와 JIT
BPF program 은 BPF instruction 으로 구성된다.
Kernel 은 이 BPF instruction 을 해석해서 실행할 수 있다.
이처럼 instruction 을 하나씩 읽어서 실행하는 방식을 Interpreter 방식이라고 볼 수 있다.
하지만 Interpreter 방식은 실제 CPU instruction 으로 바로 실행하는 것보다 느릴 수 있다.
그래서 일부 환경에서는 BPF instruction 을 실제 machine code 로 변환해서 실행할 수 있다.
이를 JIT(Just-In-Time) Compilation 이라고 한다.
JIT 를 사용하면 BPF instruction 을 매번 해석하지 않고, CPU 가 실행할 수 있는 native instruction 으로 변환해서 실행할 수 있다.
즉, BPF 는 Interpreter 방식으로 실행될 수도 있고, JIT 를 통해 더 빠르게 실행될 수도 있다.
BPF 의 안전성
BPF program 은 Kernel 안에서 실행되기 때문에 안전성이 중요하다.
만약 BPF program 이 무한 루프를 돌거나, 커널 메모리에 잘못 접근하거나, 임의의 위치로 분기할 수 있다면 Kernel 전체의 안정성에 영향을 줄 수 있다.
그래서 BPF 는 일반적인 프로그램보다 많은 제한을 가진다.
전통적인 BPF 는 다음과 같은 특성을 가진다.
- 제한된 instruction set 을 사용한다.
- 제한된 Register 와 Memory 만 사용한다.
- 패킷 데이터 접근이 제한된다.
- 실행 흐름이 비교적 단순하다.
- 필터링 목적에 맞는 작은 프로그램을 실행한다.
이런 제한 때문에 BPF 는 범용 프로그램 실행 환경이라기보다는 패킷 필터링을 위한 제한된 VM 에 가깝다.
BPF 와 Packet Capture
BPF 는 패킷 캡쳐에서 중요한 역할을 한다.
패킷 캡쳐 도구는 보통 모든 트래픽을 다 저장하거나 출력하지 않는다.
분석 목적에 맞는 패킷만 골라서 확인하는 경우가 많다.
예를 들어 다음과 같은 필터를 사용할 수 있다.
tcpdump host 10.0.0.1
tcpdump port 53
tcpdump tcp
tcpdump udp and port 123
이런 필터들은 사람이 보기에는 간단한 문자열이지만, 내부적으로는 BPF program 으로 변환된다.
그리고 Kernel 은 각 패킷에 대해 BPF program 을 실행해서 조건에 맞는 패킷만 User space 로 전달한다.
즉, BPF 는 패킷 캡쳐 도구의 필터링 성능을 담당하는 핵심 기반 기술이라고 볼 수 있다.
BPF 와 Wireshark
Wireshark 도 패킷 캡쳐 과정에서 BPF filter 를 사용할 수 있다.
Wireshark 에는 크게 Capture Filter 와 Display Filter 가 있다.
- Capture Filter: 패킷을 캡쳐할 때 적용되는 필터
- Display Filter: 이미 캡쳐된 패킷을 화면에 보여줄 때 적용되는 필터
BPF 와 직접적으로 관련 있는 것은 Capture Filter 이다.
Capture Filter 는 캡쳐 시점에 어떤 패킷을 가져올지 결정한다.
즉, 조건에 맞지 않는 패킷은 애초에 캡쳐되지 않는다.
반면 Display Filter 는 이미 캡쳐된 패킷 중에서 화면에 표시할 패킷만 고르는 것이다.
예를 들어 Wireshark 에서 Capture Filter 로 다음과 같이 설정하면
tcp port 80
80번 포트와 관련된 TCP 패킷만 캡쳐된다.
하지만 Display Filter 로 설정하면 이미 캡쳐된 패킷 중에서 80번 포트와 관련된 패킷만 화면에 보여준다.
즉, BPF 는 패킷을 캡쳐하기 전에 걸러내는 Capture Filter 쪽과 더 관련이 깊다.
BPF 와 seccomp
BPF 는 네트워크 패킷 필터링을 위해 만들어졌지만, 이후에는 다른 용도로도 사용되었다.
대표적인 예시가 seccomp 이다.
seccomp 는 Linux 에서 프로세스가 사용할 수 있는 system call 을 제한하는 보안 기능이다.
seccomp filter 는 BPF 형식의 필터를 사용해서 system call 을 허용하거나 차단할 수 있다.
예를 들어 어떤 프로그램이 read, write, exit 같은 system call 만 사용할 수 있게 제한하고, 나머지 system call 은 차단하도록 만들 수 있다.
이때 BPF 는 packet 이 아니라 system call 정보를 대상으로 필터링을 수행한다.
즉, BPF 는 처음에는 packet filtering 을 위해 만들어졌지만, 제한된 VM 에서 조건을 검사하고 결과를 반환하는 구조 때문에 system call filtering 같은 보안 기능에도 사용될 수 있었다.
정리
BPF 는 Berkeley Packet Filter 의 약자로, Kernel space 에서 packet 을 효율적으로 필터링하기 위한 기술이다.
패킷 캡쳐 도구가 모든 패킷을 User space 로 가져온 뒤 필터링하면 불필요한 복사 비용과 처리 비용이 발생한다.
BPF 는 User space 로 패킷을 넘기기 전에 Kernel space 에서 먼저 필터링을 수행해서 필요한 패킷만 전달한다.
'DevSec' 카테고리의 다른 글
| eBPF for Windows (0) | 2026.06.11 |
|---|---|
| eBPF (0) | 2026.06.10 |
| Mutual Exclusion (0) | 2026.06.06 |
| Critical Section (0) | 2026.06.06 |
| Busy Waiting (0) | 2026.06.05 |