Rust 및 BPF 기술을 기반으로 하는 네트워크 TCP 워터마크

 네트워크 워터마크는 Anti-DDoS 공격 및 네트워크 트래픽과 같은 보안 분야에서 사용됩니다.메시지에 특성 정보를 추가하는 것이 원칙입니다.TCP 옵션을 기반으로 추가된 필드는 방화벽 및 기타 수정에 의해 제거되지 않습니다.이 기사에서는 BPF 및 Rust를 사용합니다. 효율적인 구현 특정 설명을 달성하기 위해. 1  소스코드는 [] 참조  지적재산권 보호를 위한 이미지 워터마크는 여기에 기재되어 있지 않습니다.

  • BPF 드라이버는 c를 사용하여 개발되었습니다.
  • Userland 구성 및 로더는 Rust 및 libbpf-rs를 사용하여 개발 속도를 높입니다.

종속성 캡슐화

libbpf-rs 개발을 기반으로 libbpf-rs는 드라이버 개체 및 해당 정적 프로그램, 통신 및 마운트된 프로그램의 리소스 추상화를 포함하여 개발을 위한 API 추상화를 제공합니다.

동시에 libbpf-sys는 안전하지 않은 libbpf, libelf 및 zlib를 캡슐화하며 핵심은 커널의 일부인 libbpf를 기반으로 개발된 정적 링크 라이브러리입니다. elf 파일에서 사용하는 동적으로 연결된 libelf 및 zlib 드라이버를 로드하고 구문 분석하는 자세한 API

프로젝트 디렉토리는 libbpf-cargo 스캐폴딩을 기반으로 생성되며 빌드 명령은 gen 및 make를 호출하여 완료합니다.

프로젝트 디렉토리 구조

libbpf-cargo에 의해 자동으로 생성된 스켈레톤 디렉토리용 코드

netoken\
  src\
    bpf\                  //驱动
      .output\            //脚手架自动生成
        netoken.skel.rs  //=>libbpf_rs
      netoken.c              ^
      vmlinux.h               |
    main.rs  //=>libbpf_rs    |
build.rs     //=>libbpf_cargo-+
Cargo.toml
libbpf-rs\
  .git
  libbpf-rs\
  libbpf-cargo\

libbpf-xxx의 특정 위치는 Cargo.toml에서 구성할 수 있습니다.

[dependencies]
libbpf-rs = { path = "../libbpf-rs/libbpf-rs" }
[build-dependencies]
libbpf-cargo = { path = "../libbpf-rs/libbpf-cargo" }

스켈레톤 코드 생성 프로세스

  1. 사용자 프로젝트 build.rs->libbpf-cargo.rs SkeletonBuilder()  .bpf.c => .output/ .skel.rs
  2. 사용자 프로젝트에서 생성된 *.skel.rs 프로세스:
    1. 객체: 데이터 [u8]
    2. SkelBuilder()->Open Skel()->*Skel(), progs/maps/links
    3. build_skel_config()->ObjectSkeletonConfigBuilder::new(DATA)
    4. builder.name( ).map("").prog("handle_ ").build()
  3. libbpf-rs는 사용자를 위한 공통 도구를 제공합니다.
    1. 물체/골격
    2. 프로그램/지도/링크
    3. iter/인쇄/쿼리/유틸
    4. 성능/링버프
  4. libbpf-cargo는 cargo build, gen/make를 제공할 때 자동으로 skel을 생성합니다.
  5. *.skel.rs 프로세스 실행
    1. TcSkelBuilder.open()->OpenTcSkel(obj, 구성)
    2. OpenTcSkel.load() >> bpf_object__load_skeleton(config)
    3. OpenTcSkel.load()->TcSkel(obj,config, Tclinks())
    4. OpenTcSkel.progs()->OpenTcProgs(obj)
    5. OpenTcSkel.maps()->OpenTcMaps()
    6. OpenTcSkel.data()->ffi::c_void()
    7. TcSkel.attach() >> bpf_object__attach_skeleton(config)
    8. TcSkel.links = TcLinks(handle_tc)
  6. libbpf.h typedef 구조체 bpf_object_skeleton bos
    • libbpf는 커널 bpf_xxx를 3개의 구조체와 4단계 API로 캡슐화하여 커널의 많은 세부 정보를 숨깁니다.
    • 오류/인쇄/open_opts/
    • bpf_object_open_[버퍼/메모리/xattr]xxx
    • bpf_object__load/__next/__set/핀/핀 해제
    • bpf_[프로그램/맵/링크]__set/load/fd/xxx;__attach_xxx
    • bpf_map__[세트/얻기/찾기_맵]
    • bpf_perf/kprob/uprobe/tracepoint/link_xdp/tc_hook

사용자 모드 로딩 프로세스

  1. 명령줄 매개변수에서 네트워크 카드 이름을 구문 분석하고 시스템에서 네트워크 카드 번호를 가져옵니다.
  2. 생성된 skelBuilder를 사용하여 드라이버 개체의 메모리 파일 설명자 fd를 점진적으로 가져옵니다.
  3. 생성된 hookBuilder를 사용하여 네트워크 항목에서 자신을 생성 및 연결하고 egress/ingress를 종료하십시오. 이 프로그램은 이그레스만 사용합니다.
  4. egress가 커널에 로드되고 실행을 시작합니다.
  5. tc의 출구 정보 조회

fn main() -> Result<()> {
    // 命令行参数解析
    let opts = Command::parse();

    // 去掉系统的内存限制
    bump_memlock_rlimit()?;

    // 从Skel里获取信息,从opts里获取ifindex
    let builder = netokenSkelBuilder::default();
    let open = builder.open()?;
    let skel = open.load()?;
    let fd = skel.progs().handle_tc().fd();
    let ifidx = nix::net::if_::if_nametoindex(opts.iface.as_str())? as i32;

    let mut tc_builder = TcHookBuilder::new();
    tc_builder
        .fd(fd)
        .ifindex(ifidx)
        .replace(true)
        .handle(1)
        .priority(1);

    // 挂载驱动到TC的egress接口上
    let mut egress = tc_builder.hook(TC_EGRESS);

    // 执行attach,驱动开始工作
    if opts.attach {
        if let Err(e) = egress.attach() {
            bail!("failed to attach egress hook {}", e);
        }
    }
    
    // 执行destory,销毁驱动
    if opts.destroy {
        if let Err(e) = egress.detach() {
            println!("failed to detach egress hook {}", e);
        }

        if let Err(e) = egress.destroy() {
            println!("failed to destroy {}", e);
        }
    }

    // 执行query,查询执行驱动的id
    match egress.query() {
        Err(e) => println!("failed to find egress hook: {}", e),
        Ok(prog_id) => println!("found egress hook prog_id: {}", prog_id),
    }

    // 执行监听perf,收到数据后进入handle_event,收到错误丢包进入handle_lost_events
    let perf = PerfBufferBuilder::new(skel.maps_mut().events())
        .sample_cb(handle_event)
        .lost_cb(handle_lost_events)
        .build()?;

    // 100ms一次的轮询驱动,有事件后进入上面的event
    loop {
        perf.poll(Duration::from_millis(100))?;
    }

    Ok(())
}

tc 도구 기반 구현과 유사

$ tc qdisc add dev xxx 
$ tc filter [add|change|replace] dev xxx 
$ tc qdisc show dev xxx

드라이브 프로세스

  1. 입력 파라미터 struct __sk_buff가 tcp 메시지인지 확인하고, 2계층부터 4계층까지 차례로 분석 및 판단
  2. tcp 패킷이 핸드셰이크 syn 패킷인지 확인합니다. syn 패키지는 두 당사자 간의 협상 기능에 대한 옵션을 제공합니다.
  3. 다른 정책에서 토큰을 얻으려면 정책 맵을 읽으십시오.
  4. syn 패키지에 토큰 옵션 추가
// 驱动入口,数据已由内核组装为__sk_buff
SEC("tc")
int handle_tc(struct __sk_buff* ctx) {
    struct pkthdr pkt;

    RET_IF(pkt_check(ctx, &pkt) != RET_OK);
    RET_IF(pkt.tcp->syn != 1 || pkt.tcp->ack != 0);
    update_token_by_policy();
    RET_IF(extend_options_token(ctx, &pkt, epp_token) != RET_OK);

    return TC_ACT_OK;
}

// 检查是否tcp包
BPF_INLNE int pkt_check(struct __sk_buff* ctx, struct pkthdr* pkt) {
    pkt->data = (void*)(long)ctx->data;
    pkt->data_end = (void*)(long)ctx->data_end;
    pkt->eth = pkt->data;
    pkt->ipv4 = pkt->data + sizeof(struct ethhdr);

    RET_ERR_IF(pkt->eth + 1 > (struct ethhdr*)(pkt->data_end));
    RET_ERR_IF(pkt->eth->h_proto != bpf_constant_htons(ETH_P_IP));
    RET_ERR_IF(pkt->ipv4 + 1 > (struct iphdr*)(pkt->data_end));
    RET_ERR_IF(pkt->ipv4->protocol != IPPROTO_TCP);
    pkt->tcp = pkt->data + sizeof(struct ethhdr) + (pkt->ipv4->ihl * 4);
    RET_ERR_IF(pkt->tcp + 1 > (struct tcphdr*)(pkt->data_end));

    return RET_OK;
}

// 增加tcp的options的token option,对网卡以支持的offload计算checksum的,注释掉加快执行
BPF_INLNE int extend_options_token(struct __sk_buff* ctx, struct pkthdr* pkt, u64 token) {
    u32 data_end = ctx->len; // 非线性包总长
    u16 sz = sizeof(token);
    pkt->ipv4->tot_len = bpf_htons(pkt->ipv4->ihl * 4 + pkt->tcp->doff * 4 + sz);
    pkt->tcp->doff = pkt->tcp->doff + sz / 4;

    RET_IF(bpf_skb_change_tail(ctx, ctx->len + sz, 0));
    RET_IF(bpf_skb_store_bytes(ctx, data_end, &token, sizeof(token), 0));

    RET_IF(bpf_l3_csum_replace(ctx, IP_CSUM_OFFSET, 0, bpf_constant_htons(sz), 0));
    // RET_IF(bpf_l4_csum_replace(ctx, TCP_CSUM_OFFSET, 0, sz / 4, BPF_F_PSEUDO_HDR | sizeof(u8)))

    u16 csum = bpf_csum_diff(0, 0, (u32*)&token, sizeof(token), 0); // 2 tcp pseudo
    // RET_IF(bpf_l4_csum_replace(ctx, TCP_CSUM_OFFSET, 0, csum, 0));

    update_metrics();

    return RET_OK;
}

확장된 xdp

libbpf-rs는 attach_xdp 인터페이스만 제공하며 다른 xdp는 libbpf 프로젝트에서 독립적으로 구현됩니다.

af_xdp의 고급 기능이 필요한 경우  libxdp-rs를 사용해 볼 수 있습니다.  이는 Tencent 직원이 개발하고 주로 Rust 바인딩의 xdp-tools 기능을 사용하며 그 중 독자적으로 개발한 libxdp가 있습니다.

aya와 같은 다른 Rust bpf 프로젝트에 대한 간략한 소개

libpf-rs의 복잡성이 낮고 개발 시작점도 낮으며 안전하지 않은 코드가 거의 없습니다. Aya와 redbpf는 차원이 높고 능력과 야망이 뛰어나므로 당연히 난이도가 더 큽니다.

먼저 녹을 사용하여 드라이버를 작성하고 표준이 없으며 더 안전하지 않고 MaybeUninit, 내 마음이 두렵습니다.

또한 성공적인 개발 사례가 많지 않아 사업 복잡도가 높다면 고려할 수 있다. 하지만 걱정하지 마세요. 커널 5.20이 Rust에 합류한 후에 시작할 수 있습니다.

드라이버는 녹의 표현력을 반영할 수 있습니다.

#![no_std] // 
#![no_main] // 

use aya_bpf::{ macros::xdp, bindings::xdp_action, programs::XdpContext,
    maps::{HashMap, PerfEventArray}, };
use aya_log_ebpf::info;
use myapp_common::PacketLog;

#[map(name = "EVENTS")]  // map macro
static mut EVENTS: PerfEventArray<ip_src> =
    PerfEventArray::<ip_src>::with_max_entries(1024, 0);

#[xdp(name="myapp")] // hook点用macro实现,很rust
pub fn myapp(ctx: XdpContext) -> u32 {
    // match匹配xdp_ctx也很直观
    match unsafe { try_myapp(ctx) } {
        Ok(ret) => ret,
        Err(_) => xdp_action::XDP_ABORTED,
    }
}

unsafe fn try_myapp(ctx: XdpContext) -> Result<u32, u32> {
    // 方便的printk宏
    info!(&ctx, "received a packet");

    unsafe {
        EVENTS.output(&ctx, &ip_src, 0); // 隐去ip_src的解析过程...
    }
    Ok(xdp_action::XDP_PASS)
}

#[panic_handler] // hook在异常上
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

사용자 상태

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let mut bpf = Bpf::load(include_bytes_aligned!(
        "../../target/bpfel-unknown-none/release/myapp"
    ))?;
    let program: &mut Xdp = bpf.program_mut("xdp").unwrap().try_into()?;
    program.load()?;
    program.attach(&opt.iface, XdpFlags::default())
        .context("failed to attach the XDP program with default flags")?;

    ...

    let mut perf_array = AsyncPerfEventArray::try_from(bpf.map_mut("EVENTS")?)?;

    // 下面还可以封装简化下
    for cpu_id in online_cpus()? { // iter vec<u32> cpus
        let mut buf = perf_array.open(cpu_id, None)?;

        task::spawn(async move {
            let mut buffers = (0..10)
                .map(|_| BytesMut::with_capacity(1024))
                .collect::<Vec<_>>();

            loop {
                let events = buf.read_events(&mut buffers).await.unwrap();
                for i in 0..events.read {
                    let buf = &mut buffers[i];
                    let ptr = buf.as_ptr() as *const ip_src;
                    let data = unsafe { ptr.read_unaligned() };
                    let src_addr = net::Ipv4Addr::from(data.ip_src);
                    println!("LOG: SRC {}, ACTION {}", src_addr, data.action);
                }
            }
        });
    }
    signal::ctrl_c().await.expect("failed to listen for event");
    Ok::<_, anyhow::Error>(())
}

위는 작은 부분에 불과하며 더 많은 지원이 있습니다.

  • 프로브
  • 추적점
  • 소켓 프로그램
  • 분류자
  • 씨그룹
  • PDP
  • LSM

다음은 API 참조로 사용됩니다.

libbpf-rs의 API

    struct bpf_map_skeleton { *name, **map }
    struct bpf_prog_skeleton { *name, **prog }
    struct bpf_object_skeleton { **obj, *maps(skel), *progs(skel) }
   
    int bpf_object__open_skeleton(bos *s, *opts);
    int bpf_object__load_skeleton(bos *s);
    int bpf_object__attach_skeleton(bos *s);
    void bpf_object__detach_skeleton(bos *s);
    void bpf_object__destroy_skeleton(bos *s);

build.rs는 자동으로 코드 템플릿 tc.skel.rs를 생성합니다.

마지막으로 데이터 맵의 progs, 맵, 필드를 포함하여 TcSkel 사용자에게 반환되었습니다. bpf.c의 맵에 따라 Generate call->libbpf-cargo::lib.rs::SkeletonBuilder().build_generate(&skel)

pub struct 
    TcSkelBuilder.ObjectBuilder,
    OpenTcSkel.OpenObject,
    TcSkel.Object,

    OpenTcProgs.OpenObject,
    TcProgs.Object,

    OpenTcMaps.OpenObject,
    TcMaps.Object,

    TcLinks.Option<Link>,

TcSkelBuilder.open()->OpenTcSkel(obj, config)
OpenTcSkel.load() >> bpf_object__load_skeleton(config)
OpenTcSkel.load()->TcSkel(obj,config, Tclinks())
    OpenTcSkel.progs()->OpenTcProgs(obj)
    OpenTcSkel.maps()->OpenTcMaps()
    OpenTcSkel.data()->ffi::c_void()

TcSkel.attach() >> bpf_object__attach_skeleton(config)
TcSkel.links = TcLinks(handle_tc)

TcProgs.handle_tc
TcMaps.[ports、data、rodata]

Rust-bindgen에 의해 자동으로 생성된 bindings.rs

10개의 헤더 파일에서 build.rs 규칙을 사용하여 process::Command::new("make") bpf.h libbpf.h btf.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h를 생성합니다. libbpf_common.h

libbpf-rs 함수

다양한 도구 제공

tc.rs

TcHookBuilder-> TcHook
    tc_builder
        .fd(fd)
        .ifindex(ifidx)
        .replace(true)
        .handle(1)
        .priority(1);

TcHook
    tc_hook

skeleton.rs

SkelConfig는 맵과 prog를 캡슐화합니다.

libbpf_sys::bpf_object_skeleton의 래퍼

수명 주기 obj 및 메모리 보유를 위한 _data/_string_pool

메모리 파괴 드롭을 위한 프로그램/맵의 레이아웃

ObjectSkeletonConfigBuilder.build()->ObjectSkeletonConfig()
    libbpf_sys::bpf_object_skeleton()
    .build_maps(s, string_pool)->maps_layout
    .build_progs(s, string_pool)->progs_layout

// libbpf_sys::bpf_object_skeleton  wrap
/// * ensure lifetimes are valid for dependencies (pointers, data buffer)
/// * free any allocated memory on drop
pub struct ObjectSkeletonConfig<'a> {
    inner: bpf_object_skeleton,
    obj: Box<*mut bpf_object>,
    maps: Vec<MapSkelConfig>,
    progs: Vec<ProgSkelConfig>,
    /// Layout necessary to `dealloc` memory
    maps_layout: Option<Layout>,
    /// Same as above
    progs_layout: Option<Layout>,
    /// Hold this reference so that compiler guarantees buffer lives as long as us
    _data: &'a [u8],
    /// Hold strings alive so pointers to them stay valid
    _string_pool: Vec<CString>,
}

query.rs

for prog in ProgInfoIter::default() {
     println!("{}", prog.name);
[Program/Map/Btf/Link] Info

program.rs

OpenProgram.[set_[map_ifindex/fd/...]]
Program.[name/sectyion/fd/key_size/value_size/lookup/delte/update/pin/unpin]]

maps.rs

OpenMap.[set_[prog_type/attach_type/ifindex/flags]]
Map.[name/sectyion/fd/pin/unpin/attach[cgroup/perf/uprobe/trace/xdp]]
Link.[open/update_prog/dsconnet/pin/fd/detach]
bpf_link_type.[xdp/perf_event/cgroup/raw/trace]

object.rs

OpenObject: open but not load [bpf_object/maps/progs/name/map/prog/load] Populate obj.maps/obj.progs
Object: open and loaded object
ObjectBuilder.[name/debug/opts/open_file/open_mem]->OpenObject::new()

ringbuf.rs

RingBuffer.[ring_buffer/poll/consume]
RingBufferBuilder.[RingBufferCallback/add/build]->RingBuffer()

util.rs

str_to_cstring/path_to_cstring/c_ptr_to_string
roundup/num_possible_cpus
parse_ret/parse_ret_i32/parse_ret_usize

libbpf-화물 함수

main.rs

clap Command:[Build/Gen/Make]는 다음 3개 파일에 해당하며, main은 api입니다.

lib.rs

사용자 프로젝트 build.rs에 대한 자동 빌드 및 생성 제공

    SkeletonBuilder::new().source(SRC).build_and_generate(&skel)
    build()->build::build_single()
    generate()->gen::gen_single()

make.rs

  • 배치 빌드 및 생성
  • 빌드::빌드()
  • 세대::세대()
  • 마지막으로 Command::new("cargo").arg("build")

build.rs

사용자 프로젝트에 대한 build_single()->compile_one()->Command

build() 用于cargo ->compile()->compile_one()
    extract_libbpf_headers_to_disk()
    check_progs/check_clang()/

gen.rs

    gen->gen_single->gen_skel(debug, name, obj_file, output, rustfmt_path)->
    gen_skel_contents()
        open_bpf_object()
        gen_skel_c_skel_constructor()->libbpf_rs::skeleton::**ObjectSkeletonConfigBuilder**::new(DATA); # skeleton.rs
        map/prog/datasec
            gen_skel_xxx_defs()?; gen_skel_xxx_getter()?; gen_skel_link_getter()
            gen_skel_attach()->libbpf_sys::bpf_object__attach_skeleton(

metadata.rs

cargo时的 to_compile
get()->target_dir, metadata.target_directory.into_std_path_buf()
    轮询所有package后,if id == &package.id 
    get_package() 

1

netoken


추천

출처blog.csdn.net/zmule/article/details/126549532