eBPF (bpf2go) - BPF_PROG_TYPE_SOCKET_FILTER 예제 - "cgroup_skb/egress", "cgroup_skb/egress"
지난 글의 BPF_PROG_TYPE_SOCKET_FILTER 예제에 이어,
eBPF (bpf2go) - BPF_PROG_TYPE_SOCKET_FILTER 예제 - SEC("socket")
; https://www.sysnet.pe.kr/2/0/14017
이번에는 동일하게 패킷 필터링 용도이긴 하지만 접점이 다른 BPF_PROG_TYPE_CGROUP_SKB 예제를 다뤄보겠습니다.
BPF_PROG_TYPE_CGROUP_SKB
; https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_CGROUP_SKB/
.\pkg\mod\github.com\cilium\ebpf@v0.16.0\examples\cgroup_skb
; https://github.com/cilium/ebpf/blob/main/examples/cgroup_skb/cgroup_skb.c
재미있게도 BPF_PROG_TYPE_SOCKET_FILTER와는 완전히 상반된 특징을 갖는데요, 1) 전역 소켓 모니터링이 가능하고, 2) in/out 패킷에 대해 다룰 수 있습니다. 예를 들어, 아래와 같이 eBPF 코드를 작성해 보면,
SEC("cgroup_skb/ingress")
int cgroup_ingress_packets(struct __sk_buff *skb) {
struct bpf_sock *bpf_sock = skb->sk;
bpf_printk("[ingress] bpf_sock = %p", bpf_sock);
return 1;
}
SEC("cgroup_skb/egress")
int cgroup_egress_packets(struct __sk_buff *skb) {
struct bpf_sock *bpf_sock = skb->sk;
bpf_printk("[egress] bpf_sock = %p", bpf_sock);
return 1;
}
이와 함께 (어차피 cgroup에 속한 유형이므로) BPF_PROG_TYPE_CGROUP_SOCK_ADDR에 속한 connect4도 함께 작성해,
#define SYS_REJECT 0
#define SYS_PROCEED 1
SEC("cgroup/connect4")
int socket_connect4(struct bpf_sock_addr *ctx)
{
struct bpf_sock* bpf_sk = ctx->sk;
bpf_printk("[socket_connect4] bpf_sk == %p", bpf_sk);
return SYS_PROCEED;
}
bpf2go로 로딩한 다음,
// ...[생략]...
func main() {
// ...[생략]...
// Get the first-mounted cgroupv2 path.
cgroupPath, err := detectCgroupPath() // 예를 들어, cgroupPath == "/sys/fs/cgroup"
if err != nil {
log.Fatal(err)
}
cgroupEgressFunc, err := linkAttachCgroup(cgroupPath, ebpf.AttachCGroupInetEgress, bpfObj.CgroupEgressPackets)
defer func(link link.Link) {
fmt.Printf("cgroupEgressFunc\n")
_ = link.Close()
}(cgroupEgressFunc)
cgroupIngressFunc, err := linkAttachCgroup(cgroupPath, ebpf.AttachCGroupInetIngress, bpfObj.CgroupIngressPackets)
defer func(link.Link) {
fmt.Printf("cgroupIngressFunc\n")
_ = link.Close()
}(cgroupIngressFunc)
cgroupSocketConnect4, err := linkAttachCgroup(cgroupPath, ebpf.AttachCGroupInet4Connect, bpfObj.SocketConnect4)
defer func(link link.Link) {
_ = link.Close()
}(cgroupSocketConnect4)
// ...[생략]...
}
func detectCgroupPath() (string, error) {
f, err := os.Open("/proc/mounts")
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
// example fields: cgroup2 /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime 0 0
fields := strings.Split(scanner.Text(), " ")
if len(fields) >= 3 && fields[2] == "cgroup2" {
return fields[1], nil
}
}
return "", errors.New("cgroup2 not mounted")
}
func linkAttachCgroup(cgroupPath string, attachType ebpf.AttachType, program *ebpf.Program) (link.Link, error) {
opt := link.CgroupOptions{ // https://github.com/cilium/ebpf/blob/main/examples/cgroup_skb/main.go
Path: cgroupPath,
Attach: attachType,
Program: program,
}
attached, err := link.AttachCgroup(opt)
if err != nil {
log.Printf("link.Tracepoint(%v): %v\n", attachType, err)
return nil, err
}
return attached, err
}
실행해 HTTP 호출을 해보면 이런 결과를 얻게 됩니다.
// 이 부분은 아마도 HTTP 요청 중 DNS lookup에 대한 요청으로 보이고,
bpf_trace_printk: [socket_connect4] bpf_sk == 00000000417b902c
bpf_trace_printk: [egress] bpf_sock = 00000000417b902c
bpf_trace_printk: [ingress] bpf_sock = 0000000063b0daeb
bpf_trace_printk: [egress] bpf_sock = 00000000417b902c
bpf_trace_printk: [ingress] bpf_sock = 0000000063b0daeb
// 여기서부터 HTTP 요청
bpf_trace_printk: [socket_connect4] bpf_sk == 000000001a9c864d
bpf_trace_printk: [egress] bpf_sock = 000000001a9c864d
bpf_trace_printk: [egress] bpf_sock = 000000001a9c864d
bpf_trace_printk: [egress] bpf_sock = 000000001a9c864d
간단하죠? ^^ 참고로 "cgroup_skb/ingress", "cgroup_skb/egress" 모두 1을 반환하면 PASS, 0을 반환하면 DROP 동작을 하므로 원한다면 간단한 방화벽 프로그램을 eBPF를 이용해 제작하는 것이 가능합니다.
하지만, 테스트 결과에서 한 가지 의문이 남는데요, 마지막 4개의 출력을 보면,
bpf_trace_printk: [socket_connect4] bpf_sk == 000000001a9c864d
bpf_trace_printk: [egress] bpf_sock = 000000001a9c864d
bpf_trace_printk: [egress] bpf_sock = 000000001a9c864d
bpf_trace_printk: [egress] bpf_sock = 000000001a9c864d
HTTP 호출이므로 당연히 응답이 왔을 것이고, 따라서 ingress 로그가 찍혀야 하는데 그 부분의 출력이 없습니다. 이것도 분명 무슨 이유가 있겠지만.. (리알못이라) 일단 여기까지만 실습한 걸로 만족하고 넘어가겠습니다. ^^;
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]