
Rex는 eBPF 대신 Rust 커널 확장 프로그램을 로드하고 실행할 수 있게 해주는 안전하고 사용 가능한 커널 확장 프레임워크입니다.
____ _______ __ _____ _ _
| _ \| ____\ \/ / | ____|_ _| |_ ___ _ __ ___(_) ___ _ __ ___
| |_) | _| \ / | _| \ \/ / __/ _ \ '_ \/ __| |/ _ \| '_ \/ __|
| _ <| |___ / \ | |___ > <| || __/ | | \__ \ | (_) | | | \__ \
|_| \_\_____/_/\_\ |_____/_/\_\\__\___|_| |_|___/_|\___/|_| |_|___/
Rex는 eBPF 대신 Rust 커널 확장 프로그램을 로드하고 실행할 수 있는 안전하고 사용 가능한 커널 확장 프레임워크입니다. Aya와 같은 eBPF 기반 프레임워크와 달리, Rex 프로그램은 커널 내 검증기를 거치지 않습니다. 대신, 프로그램은 Rust의 안전한 부분집합으로 구현되며, Rust 컴파일러가 필요한 안전성 검사를 수행하고 기본 코드를 직접 생성합니다. 이 접근 방식은 지나치게 제한적인 검증 요구 사항(예: 프로그램 복잡성 제약)과 그로 인한 난해한 검증 오류를 피하면서, 동시에 기본 컴파일러 백엔드(즉, LLVM)에서 eBPF 백엔드 + 커널 내 JIT 방식보다 더 나은 최적화 기회를 제공할 수 있습니다.
Rex는 현재 다음 기능을 지원합니다:
kprobe, perf_event, tracepoint, xdp, tc.다음 예제는 선택한 시스템 콜에 연결되어 프로세스(해당 pid로 지정)의 시스템 콜에 오류(errno로 지정)를 주입하는 kprobe 프로그램을 구현합니다. 로더 프로그램을 포함한 전체 예제는 samples/error_injector에서 확인할 수 있습니다.
#![no_std]
#![no_main]
use rex::kprobe::kprobe;
use rex::map::RexHashMap;
use rex::pt_regs::PtRegs;
use rex::rex_kprobe;
use rex::rex_map;
use rex::Result;
#[allow(non_upper_case_globals)]
#[rex_map]
static pid_to_errno: RexHashMap<i32, u64> = RexHashMap::new(1, 0);
#[rex_kprobe]
pub fn err_injector(obj: &kprobe, ctx: &mut PtRegs) -> Result {
obj.bpf_get_current_task()
.map(|t| t.get_pid())
.and_then(|p| obj.bpf_map_lookup_elem(&pid_to_errno, &p).cloned())
.map(|e| obj.bpf_override_return(ctx, e))
.ok_or(0)
}
더 많은 샘플 프로그램은 samples에서 확인할 수 있습니다.
자세한 가이드는 여기에서 확인할 수 있습니다.
추가 설계 문서는 docs에서 확인할 수 있습니다.
기존 eBPF 확장은 안전성 보장을 위해 커널 내 eBPF 검증기에 의존합니다. 이는 안전한 프로그램이 검증기에 의해 거부되는 사용성 문제를 초래하며, 그 예로는 다음이 포함되지만 이에 국한되지는 않습니다:
Rex는 _안전한 Rust_의 안전성 보장을 직접 활용하여 이러한 문제를 해결하는 것을 목표로 합니다. 개발자는 몇 가지 제한 사항만으로 안전한 Rust로 작성할 수 있는 모든 방식으로 프로그램을 구현할 수 있으며, 더 이상 프로그램 복잡성, 코드 생성기, 또는 검증기를 만족시키기 위해 동일한 로직을 표현하는 (종종 직관에 반하는) 방법을 찾는 것에 대해 걱정할 필요가 없습니다.
우리는 BPF Memcached Cache (BMC)의 구현을 통해 이를 보여줍니다. BMC는 Memcached 가속을 위한 최신 확장 프로그램입니다. 복잡한 eBPF 프로그램으로서 BMC는 BPF 테일콜로 연결된 여러 구성 요소로 분할되고, 검증기를 만족시키기 위해 어색한 루프/분기 구현을 사용해야 했지만, Rex 구현에서는 이러한 것들이 전혀 필요하지 않습니다.
예를 들어, 패킷 페이로드에서 SET 명령을 검색하는 BPF-BMC의 캐시 무효화 로직 코드를 보여드립니다:
// Searches for SET command in payload
for (unsigned int off = 0;
off < BMC_MAX_PACKET_LENGTH && payload + off + 1 <= data_end;
off++) {
if (set_found == 0 && payload[off] == 's' &&
payload + off + 3 <= data_end && payload[off + 1] == 'e' &&
payload[off + 2] == 't') {
off += 3;
set_found = 1;
}
...
}
이 코드는 검증기를 통과하기 위해 루프에 추가 제약(off < BMC_MAX_PACKET_LENGTH)을 도입했을 뿐만 아니라, 패킷 끝(data_end)을 확인하는 반복적인 상용구 코드와 패킷에서 "set" 문자열을 일치시키는 번거로운 로직을 사용하고 있습니다.
Rex에서 안전한 Rust의 힘을 활용하면 이러한 부담이 전혀 필요하지 않습니다. Rex에는 복잡성 제한이 없으며 구현에 더 많은 자유를 제공합니다:
let set_iter = payload.windows(4).enumerate().filter_map(|(i, v)| {
if v == b"set " {
Some(i)
} else {
None
}
});
Rex로 작성된 BMC의 전체 구현은 samples/bmc에서 확인할 수 있습니다.
Rex는 GPLv2 라이선스에 따라 라이선스가 부여됩니다. 이 저장소의 서브모듈(Linux, Rust, LLVM)은 각각의 약관에 따라 라이선스가 부여됩니다. 자세한 내용은 해당 라이선스 파일을 참조하십시오. 또한 memcached 벤치마크는 MIT 라이선스에 따라 라이선스가 부여됩니다.