#tcp-udp #ip #udp #tcp #networking

l3l4kit

将l3数据包转换为l4数据以及相反的库

1个不稳定版本

0.1.0 2023年2月8日

#58 in #tcp-udp

MIT 协议

55KB
1K SLoC

数据包入,数据出和数据入,数据包出

基本操作

标题概括了这个库的功能。给它l3 IP数据包作为输入,它会输出l4数据。同样,给它l4数据作为输入,它会输出l3 IP数据包。输入的数据包通过调用l3_rx() API提供,输出的数据通过回调l4_rx()提供。需要传输的l4数据通过API l4_tx()提供,然后数据会被打包,并通过l3_tx()回调输出。

数据包可以是任何源或目的IP,无需任何路由表或其他类似的东西。数据包从哪里来,如何出去,这不是这个库的关心点。

为什么l4_rx()和l3_tx()需要回调?

如果l4_tx()数据可以作为IP数据包传输,我们要求调用者提供一个“接口缓冲区”,数据包直接写入其中。这避免了库将数据包复制到某个中间缓冲区,然后调用者再将数据包复制到接口缓冲区 - 例如,如果接口驱动程序是dpdk,l3_tx_buffer() API可以直接提供dpdk缓冲区对象。无论提供的对象类型如何,都需要可以作为一个连续切片来寻址。因此,在l4_tx()内直接调用l3_tx()允许将IP数据包直接写入接口缓冲区,而不进行任何中间复制等操作。

如果l3_rx()接收到的数据包最终在TCP/UDP缓冲区中包含L4数据,理论上我们可以提供API来查看这些缓冲区,以便调用者可以获取数据而不进行任何中间复制。但是smoltcp不允许直接查看缓冲区,它只允许提供回调,其中提供缓冲区切片,因此我们也有l4_rx()作为回调。

维护

TCP套接字显然需要在将来安排任务,如重传等。因此,调用者必须安排某种方式在未来某个由该API返回值指示的点调用l4_poll() API。

伪代码

下面我们提供了一个伪代码概述,具体示例中有一个简单的echo服务器 - https://github.com/gopakumarce/l3l4kit/blob/main/examples/echo_server.rs 这里有一个稍微复杂一点的示例,使用代理服务器 - https://github.com/gopakumarce/l3l4kit/blob/main/examples/proxy_server.rs

struct MyPacketBuf{}
struct MyFlowInfo{}
struct MyCallbacks {my_flows: HashMap<Flow, MyFlowInfo>}

impl MyCallBacks {
    fn add_flow_to_my_flows(&self, flow: Flow) {
        if !self.my_flows.contains_key(&flow) {
            self.my_flows.insert(flow, MyFlowInfo{})
        }
    }
    
    fn process_data(&self, flow: Flow, data: Option<&[u8]>) {
        if let Some(data) = data {
            // process the data
        } else {
            // flow is not going to receive any more data, maybe take 
            // the flow out of the my_flows hashtable ?
        }
    }
}

impl Callbacks<MyPacketBuf> for MyCallbacks {
    fn l3_tx(&self, pkt: MyPacketBuf) {
        // Do whatever you have to do to transmit the pkt
    }

    fn l3_tx_buffer() -> Option<MyPacketBuf> {
        // Return a new packet buffer or None if exhausted
    }

    fn l3_tx_buffer_mut<'a>(&self, pkt: &'a mut MyPacketBuf) -> &'a mut [u8] {
        // Return a contigious slice version of the pkt buffer
    }

    fn l4_rx(&self, flow: Flow, data: Opion<&[u8]>) {
        // You got data for a flow. The flow can be something you have seen
        // before or a new flow. You can maintain your own data structures to
        // store the flow and do whatever you want with it and with the data
        // received.
        add_flow_to_my_flows(flow);
        process_data(flow, data);
    }
}

main() {
    let callback = MyCallbacks::default();
    let l3l4 = L4L4Build::default().<options you want>.finalize();
    let next_house_keeping_time = l3l4.l4_poll();

    loop {
        // We might either get an Rx L3 packet or our application might have generated 
        // some L4 data to be transmitted, and/or we might have periodic house-keeping
        // work to do. How these three activities are interleaved / scheduled is upto
        // the caller, one way which we use in the examples/echo_server.rs is by
        // using async/await, but that absolutely need not be how its done
        let (pkt, time_for_house_keeping, l4_to_send) = get_l3_rx_or_l4_tx_with_timeout(next_house_keeping_time);
        if time_for_house_keeping {
            next_house_keeping_time = l3l4.l4_poll();
        }

        // This can trigger  MyCallbacks::l4_rx()
        flow = l3l4.l3_rx(&callback, pkt);

        // The l3 Rx packet above might have an ACK for this flow and hence we might 
        // have room to transmit. So try to send out data in pending queue
        if pending_l4_send = has_previous_pending_l4_tx(flow) {
            // This can trigger MyCallbacks::l3_tx()
            l3l4.l4_tx(&callback, pending_l4_send.flow, pending_l4_send.buffer);
        }

        // This can trigger MyCallbacks::l3_tx()
        // If the l4_to_send data is not fully transmitted, queue it to the pending queue
        l3l4.l4_tx(&callback, l4_to_send.flow, l4_to_send.buffer);
    }
}

依赖关系

~3.5MB
~75K SLoC