strparser
参考:https://lwn.net/Articles/731133/
参考:https://www.cnblogs.com/codestack/p/13947183.html
参考:https://www.cnblogs.com/codestack/p/12723229.html
BPF_PROG_TYPE_SK_SKB
使用场景
场景一:修改 skb/socket 信息,socket 重定向
这个功能依赖 sockmap,后者是一种特殊类型的 BPF map,其中存储的是 socket 引用(references)。
典型流程:
- 创建 sockmap
- 拦截 socket 操作,将 socket 信息存入 sockmap
- 拦截 socket sendmsg/recvmsg 等系统调用,从 msg 中提取信息(IP、port 等),然后 在 sockmap 中查找对端 socket,然后重定向过去。
根据提取到的 socket 信息判断接下来应该做什么的过程称为 verdict(判决)。 verdict 类型可以是:
__SK_DROP
__SK_PASS
__SK_REDIRECT
场景二:动态解析消息流(stream parsing)
这种程序的一个应用是 strparser framework。
它与上层应用配合,在内核中提供应用层消息解析的支持(provide kernel support for application layer messages)。两个使用了 strparser 框架的例子:TLS 和 KCM( Kernel Connection Multiplexor)。
strparser是 Linux 内核在 4.9 版本引入的 feature (https://lwn.net/Articles/703116/)。它允许用户在内核层面拦截送往 TCP 套接字的报文并做自定义的处理。处理的地方可以是内核模块,也可以是 eBPF 程序。
- 内核模块处理截获报文的例子:KTLS(https://github.com/ktls/af_ktls)
KTLS 这个 feature 已经进入内核代码主线了,它的设计思想是让 TLS 需要的加解密操作就在内核层面就完成,而不必拷贝之后在用户态做,根据论文 https://netdevconf.info/1.2/papers/ktls.pdf的分析结果,这样做可以减少 不少 的传输时延。
- eBPF程序处理截获报文的例子:psock 使用 strpaser,将数据包的控制权转移到 eBPF 处理程序,用户可以在 eBPF 程序里完成网络报文的重定向
strparser 的工作原理
核心数据结构
struct strparser 是 strparser 框架的核心数据结构,它绑定(attach)一个 TCP sock 结构 sk 和一组回调函数 cb。
/* Structure for an attached lower socket */
struct strparser {
struct sock *sk;
u32 stopped : 1;
u32 paused : 1;
u32 aborted : 1;
u32 interrupted : 1;
u32 unrecov_intr : 1;
struct sk_buff **skb_nextp;
struct sk_buff *skb_head;
unsigned int need_bytes;
struct delayed_work msg_timer_work;
struct work_struct work;
struct strp_stats stats;
struct strp_callbacks cb;
};
//struct strparser 是 strparser 框架的核心数据结构,它绑定(attach)一个 TCP sock 结构 sk 和一组回调函数 cb。
- strp_init() 完成 struct strparser 的初始化,它的参数就是要绑定的 TCP 连接和使用者设置的回调函数
int strp_init(struct strparser *strp, struct sock *sk,
const struct strp_callbacks *cb)
{
if (!cb || !cb->rcv_msg || !cb->parse_msg)
return -EINVAL;
/* The sk (sock) arg determines the mode of the stream parser.
*
* If the sock is set then the strparser is in receive callback mode.
* The upper layer calls strp_data_ready to kick receive processing
* and strparser calls the read_sock function on the socket to
* get packets.
*
* If the sock is not set then the strparser is in general mode.
* The upper layer calls strp_process for each skb to be parsed.
*/
if (!sk) {
if (!cb->lock || !cb->unlock)
return -EINVAL;
}
memset(strp, 0, sizeof(*strp));
strp->sk = sk;
strp->cb.lock = cb->lock ? : strp_sock_lock;
strp->cb.unlock = cb->unlock ? : strp_sock_unlock;
strp->cb.rcv_msg = cb->rcv_msg;
strp->cb.parse_msg = cb->parse_msg;
strp->cb.read_sock_done = cb->read_sock_done ? : default_read_sock_done;
strp->cb.abort_parser = cb->abort_parser ? : strp_abort_strp;
INIT_DELAYED_WORK(&strp->msg_timer_work, strp_msg_timeout);
INIT_WORK(&strp->work, strp_work);
return 0;
}
strp_callbacks回调函数一共有以下六个:
/* Callbacks are called with lock held for the attached socket */
struct strp_callbacks {
int (*parse_msg)(struct strparser *strp, struct sk_buff *skb);
void (*rcv_msg)(struct strparser *strp, struct sk_buff *skb);
int (*read_sock_done)(struct strparser *strp, int err);
void (*abort_parser)(struct strparser *strp, int err);
void (*lock)(struct strparser *strp);
void (*unlock)(struct strparser *strp);
};
parse_msg() 在 strpaser 收到报文时被框架调用。它用于从报文中提取下一个应用层消息(message)的长度。一个 TCP 报文里可能不止一个应用层消息,而 parse_msg() 就是提供给使用者去识别各个消息的手段。
strpaser 截获报文
正常情况下,内核 TCP 层处理报文后,会调用 sock->sk_data_ready(sk) , 它的默认动作是 wake up 一个用户态进程.
void tcp_data_ready(struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
--------------------
sk->sk_data_ready(sk);
}
期望报文能进入 strpaser ,但报文显然不会平白无故地地进入 strpaser ,因此,我们需要在报文的上送路径上动一些手脚:替换掉 sk->sk_data_ready 函数
KTLS 的例子中,在做好备份后, tls_data_ready() 替换被赋值到 sk->sk_data_ready
static int tls_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len){
// code omitted
tsk->saved_sk_data_ready = tsk->socket->sk->sk_data_ready;
tsk->saved_sk_write_space = tsk->socket->sk->sk_write_space;sk_write_space
tsk->socket->sk->sk_data_ready = tls_data_ready;
tsk->socket->sk->sk_write_space = tls_write_space;
tsk->socket->sk->sk_user_data = tsk;
// code omitted
}
同样地,在 psock 的例子中, sk_psock_strp_data_ready() 被赋值到 sk->sk_data_ready
void sk_psock_start_strp(struct sock *sk, struct sk_psock *psock)
{
struct sk_psock_parser *parser = &psock->parser;
if (parser->enabled)
return;
parser->saved_data_ready = sk->sk_data_ready;
sk->sk_data_ready = sk_psock_strp_data_ready;
sk->sk_write_space = sk_psock_write_space;
parser->enabled = true;
}
对 KTLS
static void tls_data_ready(struct sock *sk)
{
struct tls_context *tls_ctx = tls_get_ctx(sk);
struct tls_sw_context_rx *ctx = tls_sw_ctx_rx(tls_ctx);
strp_data_ready(&ctx->strp);
}
对 psock
/* Called with socket lock held. */
static void sk_psock_strp_data_ready(struct sock *sk)
{
struct sk_psock *psock;
rcu_read_lock();
psock = sk_psock(sk);
if (likely(psock)) {
if (tls_sw_has_ctx_rx(sk)) {
psock->parser.saved_data_ready(sk);
} else {
write_lock_bh(&sk->sk_callback_lock);
strp_data_ready(&psock->parser.strp);
write_unlock_bh(&sk->sk_callback_lock);
}
}
rcu_read_unlock();
}
strpaser 处理报文
strpaser 框架拿到报文之后,通常会依次调用用户设置的 parse_msg 和 rcv_msg 回调函数,用户在回调函数里用来决定报文应该何去何从
strp_data_ready
|- strp_read_sock
|- tcp_read_sock
|- strp_recv
|- __strp_recv
|- strp->cb.parse_msg(strp, head)
...
|- strp->cb.rcv_msg(strp, head);
对于 psock, 则是运行 eBPF 程序,得到动作(verdict)。
static void sk_psock_strp_read(struct strparser *strp, struct sk_buff *skb)
{
struct sk_psock *psock = sk_psock_from_strp(strp);
struct bpf_prog *prog;
int ret = __SK_DROP;
rcu_read_lock();
prog = READ_ONCE(psock->progs.skb_verdict);
if (likely(prog)) {
skb_orphan(skb);
tcp_skb_bpf_redirect_clear(skb);
ret = sk_psock_bpf_run(psock, prog, skb); // if we rdir , return SK_PASS
ret = sk_psock_map_verd(ret, tcp_skb_bpf_redirect_fetch(skb));
}
rcu_read_unlock();
sk_psock_verdict_apply(psock, skb, ret);
}
对于Psock的初始化:
ret = sk_psock_init_strp(sk, psock);
psock_set_prog(&psock->progs.skb_verdict, skb_verdict);
psock_set_prog(&psock->progs.skb_parser, skb_parser);
sk_psock_start_strp(sk, psock);
int strp_init(struct strparser *strp, struct sock *sk,
const struct strp_callbacks *cb)
{
if (!cb || !cb->rcv_msg || !cb->parse_msg)
return -EINVAL;
/* The sk (sock) arg determines the mode of the stream parser.
*
* If the sock is set then the strparser is in receive callback mode.
* The upper layer calls strp_data_ready to kick receive processing
* and strparser calls the read_sock function on the socket to
* get packets.
*
* If the sock is not set then the strparser is in general mode.
* The upper layer calls strp_process for each skb to be parsed.
*/
if (!sk) {
if (!cb->lock || !cb->unlock)
return -EINVAL;
}
memset(strp, 0, sizeof(*strp));
strp->sk = sk;
strp->cb.lock = cb->lock ? : strp_sock_lock;
strp->cb.unlock = cb->unlock ? : strp_sock_unlock;
strp->cb.rcv_msg = cb->rcv_msg;
strp->cb.parse_msg = cb->parse_msg;
strp->cb.read_sock_done = cb->read_sock_done ? : default_read_sock_done;
strp->cb.abort_parser = cb->abort_parser ? : strp_abort_strp;
INIT_DELAYED_WORK(&strp->msg_timer_work, strp_msg_timeout);
INIT_WORK(&strp->work, strp_work);
return 0;
}
int sk_psock_init_strp(struct sock *sk, struct sk_psock *psock)
{
static const struct strp_callbacks cb = {
.rcv_msg = sk_psock_strp_read,
.read_sock_done = sk_psock_strp_read_done,
.parse_msg = sk_psock_strp_parse,
};
psock->parser.enabled = false;
return strp_init(&psock->parser.strp, sk, &cb);
}
void sk_psock_start_strp(struct sock *sk, struct sk_psock *psock)
{
struct sk_psock_parser *parser = &psock->parser;
if (parser->enabled)
return;
parser->saved_data_ready = sk->sk_data_ready;
sk->sk_data_ready = sk_psock_strp_data_ready;
sk->sk_write_space = sk_psock_write_space;
parser->enabled = true;
}
参考:https://docs.kernel.org/networking/tls-offload.html
参考:https://www.kernel.org/doc/Documentation/networking/strparser.txt
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
2023-04-03 freeradius 的bug处理