bpf_cmd:
BPF_MAP_CREATE: 创建一个新的BPF映射。
BPF_MAP_LOOKUP_ELEM: 在BPF映射中查找一个元素。
BPF_MAP_UPDATE_ELEM: 更新BPF映射中的一个元素。
BPF_MAP_DELETE_ELEM: 从BPF映射中删除一个元素。
BPF_MAP_GET_NEXT_KEY: 获取BPF映射中下一个键的迭代器。
BPF_PROG_LOAD: 加载一个新的BPF程序到内核中。
BPF_OBJ_PIN: 将BPF对象(如映射或程序)固定到文件系统中,以便后续检索。
BPF_OBJ_GET: 从文件系统中检索固定的BPF对象。
BPF_PROG_ATTACH: 将BPF程序附加到某个内核事件或对象上。
BPF_PROG_DETACH: 将BPF程序从之前附加的内核事件或对象上分离。
BPF_PROG_TEST_RUN/BPF_PROG_RUN: 测试或运行BPF程序。
BPF_PROG_GET_NEXT_ID: 获取下一个BPF程序ID的迭代器。
BPF_MAP_GET_NEXT_ID: 获取下一个BPF映射ID的迭代器。
BPF_PROG_GET_FD_BY_ID/BPF_MAP_GET_FD_BY_ID: 通过ID获取BPF程序或映射的文件描述符。
BPF_OBJ_GET_INFO_BY_FD: 通过文件描述符获取BPF对象的信息。
BPF_PROG_QUERY: 查询BPF程序的信息。
BPF_RAW_TRACEPOINT_OPEN: 打开一个原始跟踪点。
BPF_BTF_LOAD: 加载BPF类型信息(BTF)。
BPF_BTF_GET_FD_BY_ID: 通过ID获取BTF的文件描述符。
BPF_TASK_FD_QUERY: 查询与任务相关的文件描述符信息。
BPF_MAP_LOOKUP_AND_DELETE_ELEM: 查找并删除BPF映射中的一个元素。
BPF_MAP_FREEZE: 冻结BPF映射,防止进一步更新。
BPF_BTF_GET_NEXT_ID: 获取下一个BTF ID的迭代器。
BPF_MAP_LOOKUP_BATCH/BPF_MAP_LOOKUP_AND_DELETE_BATCH/BPF_MAP_UPDATE_BATCH/BPF_MAP_DELETE_BATCH: 批量操作BPF映射的元素。
BPF_LINK_CREATE/BPF_LINK_UPDATE/BPF_LINK_GET_FD_BY_ID/BPF_LINK_GET_NEXT_ID: 创建、更新、通过ID获取文件描述符、获取下一个链接ID的BPF链接操作。
BPF_ENABLE_STATS: 启用BPF统计信息。
BPF_ITER_CREATE: 创建BPF迭代器。
BPF_LINK_DETACH: 分离BPF链接。
BPF_PROG_BIND_MAP: 将BPF程序绑定到映射上。
bpf_map_type
BPF_MAP_TYPE_UNSPEC: 未指定的映射类型。
BPF_MAP_TYPE_HASH: 哈希表映射。
BPF_MAP_TYPE_ARRAY: 数组映射。
BPF_MAP_TYPE_PROG_ARRAY: 程序数组映射,用于存储BPF程序的数组。
BPF_MAP_TYPE_PERF_EVENT_ARRAY: 性能事件数组映射。
BPF_MAP_TYPE_PERCPU_HASH/BPF_MAP_TYPE_PERCPU_ARRAY: 每个CPU的哈希表/数组映射。
BPF_MAP_TYPE_STACK_TRACE: 堆栈跟踪映射。
BPF_MAP_TYPE_CGROUP_ARRAY: 控制组数组映射。
BPF_MAP_TYPE_LRU_HASH/BPF_MAP_TYPE_LRU_PERCPU_HASH: LRU(最近最少使用)哈希表映射,可选地每个CPU。
BPF_MAP_TYPE_LPM_TRIE: LPM(最长前缀匹配)树映射。
BPF_MAP_TYPE_ARRAY_OF_MAPS/BPF_MAP_TYPE_HASH_OF_MAPS: 映射的数组/哈希表映射。
BPF_MAP_TYPE_DEVMAP/BPF_MAP_TYPE_SOCKMAP/BPF_MAP_TYPE_CPUMAP: 设备/套接字/CPU映射,用于网络和设备编程。
BPF_MAP_TYPE_XSKMAP: XDP套接字映射。
BPF_MAP_TYPE_SOCKHASH: 套接字哈希映射。
BPF_MAP_TYPE_CGROUP_STORAGE/BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE: 控制组存储映射,可选地每个CPU。注意这些类型已弃用,建议使用BPF_MAP_TYPE_CGRP_STORAGE。
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY: 重用端口套接字数组映射。
BPF_MAP_TYPE_QUEUE/BPF_MAP_TYPE_STACK: 队列/堆栈映射。
BPF_MAP_TYPE_SK_STORAGE: 套接字存储映射。
BPF_MAP_TYPE_DEVMAP_HASH: 设备哈希映射。
BPF_MAP_TYPE_STRUCT_OPS: 结构体操作映射,用于修改内核函数的行为。
BPF_MAP_TYPE_RINGBUF: 环形缓冲区映射。
BPF_MAP_TYPE_INODE_STORAGE/BPF_MAP_TYPE_TASK_STORAGE: inode/任务存储映射。
BPF_MAP_TYPE_BLOOM_FILTER: 布隆过滤器映射。
BPF_MAP_TYPE_USER_RINGBUF: 用户空间环形缓冲区映射。
BPF_MAP_TYPE_CGRP_STORAGE: 控制组存储映射,支持更广泛的用例。
struct xdp_md {
__u32 data;
__u32 data_end;
__u32 data_meta;
/* Below access go through struct xdp_rxq_info */
这个成员表示接收数据包的网络接口的索引(ifindex)。它是通过 rxq->dev->ifindex 获得的,其中 rxq 是指向接收队列的指针,dev 是指向网络设备的指针。这个索引可以用来识别数据包是从哪个网络接口进入系统的。
__u32 ingress_ifindex; /* rxq->dev->ifindex */
这个成员表示接收数据包时所在的接收队列的索引。它是通过 rxq->queue_index 获得的,这个索引对于理解数据包在接收过程中的调度和负载均衡可能是有用的。
__u32 rx_queue_index; /* rxq->queue_index */
这个成员表示如果数据包被 XDP 程序修改并重新发送时,应该使用的发送网络接口的索引。然而,需要注意的是,在 XDP 程序的上下文中,egress_ifindex 的值可能并不总是有效或可用,因为它通常与数据包的发送路径相关,而 XDP 程序主要关注于数据包的接收和可能的修改。在某些情况下,这个成员可能被忽略或具有特定的默认值。
__u32 egress_ifindex; /* txq->dev->ifindex */
};
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP 程序决定不对数据包进行任何修改,并将其直接传递给网络栈的其余部分进行正常处理时,它会返回 XDP_PASS。这是 XDP 程序在不需要对数据包进行特殊处理时的默认行为。
XDP_PASS,
//如果 XDP 程序想要将数据包发送到另一个网络接口(即将其转发或发送到另一个地方),它会返回 XDP_TX 并可能伴随一个指向 xdp_tx_metadata 结构体的指针(这取决于具体的内核版本和 XDP 程序的实现)。这个操作允许数据包在内核内部被重新路由,而不需要将它们发送回用户空间进行处理。
XDP_TX,
XDP_REDIRECT,
};
如果是tcp 通信的话,XDP_DROP 了之后, 客户端会收到通知吗 ?
在TCP通信中,如果数据包在XDP层被XDP_DROP处理,客户端通常不会直接收到关于数据包被丢弃的通知。这是因为XDP(eXpress Data Path)是在网络数据包到达网络栈之前,即在TCP/IP协议栈的更低层次(如网卡驱动层)进行处理的。XDP的主要目的是在网络栈的早期阶段对数据包进行快速处理,以减少处理延迟和提高性能
过滤掉特定端口的数据包
- 例如丢弃所有目的端口为 80(HTTP)的数据包。
- 基于地理位置过滤, 获取 IP 地址的地理位置数据库:需要一个包含 IP 地址和对应地理位置的数据库,如 MaxMind 的 GeoIP 数据库。这个数据库可以提供 IP 地址所属的国家、地区、城市等信息。
加载地理位置数据库:在用户空间程序中加载地理位置数据库,并将其映射到内核空间的 eBPF 程序中。
eBPF 程序过滤逻辑:在 eBPF 程序中,通过查找地理位置数据库,判断数据包的源 IP 地址是否来自指定的国家或地区,并根据结果决定是否丢弃数据包。
//
// Created by putao on 2024/9/5.
//
// packet_filter.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <netinet/in.h>
SEC("xdp")
int packet_filter(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
struct tcphdr *tcp;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
if (ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end) {
return XDP_DROP;
}
if (tcp->dest == __constant_htons(80)) {
return XDP_DROP; // 丢弃目的端口为 80 的数据包
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
将所有数据包镜像到另一个网络接口
网络监控和分析:可以将数据包镜像到一个专用的监控接口或设备,用于实时分析网络流量,检测异常行为或入侵。
流量复制:在测试环境中,可以将生产环境的流量复制到测试环境,以便进行性能测试或故障排除。
数据包捕获:在网络故障排除或性能分析时,可以将数据包镜像到一个捕获设备或系统,用于详细分析。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
// 定义一个映射,用于存储目标接口索引
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32);
} ifindex_map SEC(".maps");
SEC("xdp")
int packet_mirror(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
__u32 key = 0;
__u32 *ifindex = bpf_map_lookup_elem(&ifindex_map, &key);
if (ifindex) {
// 将数据包镜像到指定的网络接口
bpf_clone_redirect(ctx, *ifindex, 0);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
将数据包负载均衡到不同的后端服务器。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <netinet/in.h>
// 定义一个映射,用于存储后端服务器的 IP 地址
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, __u32);
} backend_servers SEC(".maps");
SEC("xdp")
int packet_lb(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
struct tcphdr *tcp;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
if (ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end) {
return XDP_DROP;
}
__u32 src_ip = ip->saddr;
__u32 *backend_ip = bpf_map_lookup_elem(&backend_servers, &src_ip);
if (backend_ip) {
ip->daddr = *backend_ip;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
对特定 IP 地址的数据包进行限速。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} ip_rate_limit SEC(".maps");
SEC("xdp")
int packet_rate_limit(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
__u32 src_ip = ip->saddr;
__u64 *last_time = bpf_map_lookup_elem(&ip_rate_limit, &src_ip);
__u64 current_time = bpf_ktime_get_ns();
if (last_time) {
if (current_time - *last_time < 1000000000) { // 1 秒内限速
return XDP_DROP;
}
}
bpf_map_update_elem(&ip_rate_limit, &src_ip, ¤t_time, BPF_ANY);
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
DDoS 防护
分布式拒绝服务(DDoS)攻击是通过大量请求导致目标服务不可用的攻击方式。DDoS 防护可以通过速率限制、流量分析和过滤等方式来实现。基于 IP 地址的速率限制,同上。
深度包检测(DPI)
深度包检测(DPI)是一种网络流量分析技术,通过检查数据包的内容(包括应用层数据),可以识别特定的协议、应用或内容。DPI 可以用于内容过滤、入侵检测、带宽管理等。
基于url过滤
#include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/in.h> #define HTTP_PORT 80 #define MAX_URL_LEN 256 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 256); __type(key, char[MAX_URL_LEN]); __type(value, __u8); } blocked_urls SEC(".maps"); SEC("xdp") int http_filter(struct xdp_md *ctx) { void *data_end = (void *)(long)ctx->data_end; void *data = (void *)(long)ctx->data; struct ethhdr *eth = data; struct iphdr *ip; struct tcphdr *tcp; char *http_data; char url[MAX_URL_LEN] = {0}; if (data + sizeof(*eth) > data_end) { return XDP_DROP; } eth = data; if (eth->h_proto != __constant_htons(ETH_P_IP)) { return XDP_PASS; } ip = data + sizeof(*eth); if ((void *)ip + sizeof(*ip) > data_end) { return XDP_DROP; } if (ip->protocol != IPPROTO_TCP) { return XDP_PASS; } tcp = (void *)ip + sizeof(*ip); if ((void *)tcp + sizeof(*tcp) > data_end) { return XDP_DROP; } if (tcp->dest != __constant_htons(HTTP_PORT)) { return XDP_PASS; } http_data = (char *)tcp + sizeof(*tcp); if (http_data + 4 > (char *)data_end) { return XDP_DROP; } if (http_data[0] == 'G' && http_data[1] == 'E' && http_data[2] == 'T' && http_data[3] == ' ') { char *url_start = http_data + 4; char *url_end = url_start; while (url_end < (char *)data_end && *url_end != ' ' && *url_end != '\r' && *url_end != '\n') { url_end++; } if (url_end - url_start < MAX_URL_LEN) { __builtin_memcpy(url, url_start, url_end - url_start); url[url_end - url_start] = '\0'; } __u8 *blocked = bpf_map_lookup_elem(&blocked_urls, url); if (blocked) { return XDP_DROP; } } return XDP_PASS; } char _license[] SEC("license") = "GPL";
- 基于ua过滤
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#define HTTP_PORT 80
#define MAX_HEADER_LEN 256
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 256);
__type(key, char[MAX_HEADER_LEN]);
__type(value, __u8);
} blocked_user_agents SEC(".maps");
SEC("xdp")
int http_user_agent_filter(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *ip;
struct tcphdr *tcp;
char *http_data;
char user_agent[MAX_HEADER_LEN] = {0};
if (data + sizeof(*eth) > data_end) {
return XDP_DROP;
}
eth = data;
if (eth->h_proto != __constant_htons(ETH_P_IP)) {
return XDP_PASS;
}
ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end) {
return XDP_DROP;
}
if (ip->protocol != IPPROTO_TCP) {
return XDP_PASS;
}
tcp = (void *)ip + sizeof(*ip);
if ((void *)tcp + sizeof(*tcp) > data_end) {
return XDP_DROP;
}
if (tcp->dest != __constant_htons(HTTP_PORT)) {
return XDP_PASS;
}
http_data = (char *)tcp + sizeof(*tcp);
if (http_data + 4 > (char *)data_end) {
return XDP_DROP;
}
// 查找 User-Agent 头
char *header_start = http_data;
while (header_start < (char *)data_end) {
if (header_start + 10 < (char *)data_end &&
header_start[0] == 'U' && header_start[1] == 's' && header_start[2] == 'e' &&
header_start[3] == 'r' && header_start[4] == '-' && header_start[5] == 'A' &&
header_start[6] == 'g' && header_start[7] == 'e' && header_start[8] == 'n' &&
header_start[9] == 't' && header_start[10] == ':') {
char *ua_start = header_start + 12;
char *ua_end = ua_start;
while (ua_end < (char *)data_end && *ua_end != '\r' && *ua_end != '\n') {
ua_end++;
}
if (ua_end - ua_start < MAX_HEADER_LEN) {
__builtin_memcpy(user_agent, ua_start, ua_end - ua_start);
user_agent[ua_end - ua_start] = '\0';
}
break;
}
header_start++;
}
__u8 *blocked = bpf_map_lookup_elem(&blocked_user_agents, user_agent);
if (blocked) {
return XDP_DROP;
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
虽然SNI不是SSL/TLS协议中的必要部分,但在现代网络环境中,它对于支持多域名服务器、共享IP地址的虚拟主机、CDN和企业级应用等场景至关重要。
- 多域名服务器:当一台服务器配置了多个SSL证书,每个证书对应一个不同的域名时,SNI允许客户端在TLS握手过程中向服务器指明它希望连接的域名。这样,服务器就能根据SNI信息返回正确的证书,完成SSL/TLS握手。
- 共享IP地址的虚拟主机:在云环境和虚拟主机环境中,多个域名可能共享同一个IP地址。SNI使得这些域名能够在同一个IP地址上安全地提供HTTPS服务,而无需为每个域名分配一个独立的IP地址。
- 内容分发网络(CDN):CDN提供商经常需要为不同的客户提供HTTPS服务,而客户之间可能共享CDN的IP地址。SNI允许CDN根据客户端的请求动态地选择并返回正确的证书。
- 企业级应用:在大型企业中,可能存在多个内部或外部的应用和服务,它们共享同一个服务器或集群。SNI可以帮助这些应用和服务安全地提供HTTPS服务,同时保持网络的整洁和高效。
- 提升用户体验:使用SNI可以避免在不支持SNI的客户端上出现“证书不匹配”的错误,从而提升用户体验。这是因为没有SNI时,服务器可能无法确定客户端请求的域名,从而返回错误的证书。
SNI 过滤的意义主要体现在以下几个方面:
- 企业网络:企业可以通过 SNI 过滤来阻止员工访问某些社交媒体、视频流媒体或其他不相关的站点。
- 公共网络:公共 Wi-Fi 提供者可以通过 SNI 过滤来阻止访问某些不适当的内容。
- 在某些国家或地区,法律法规要求运营商或网络提供者阻止访问某些特定的网站或服务。通过 SNI 过滤,可以实现对这些网站的访问控制,确保合规性。
- 通过 SNI 过滤,网络管理员可以更好地管理网络资源,防止带宽被滥用。例如,可以阻止大文件下载或视频流媒体服务,以确保网络带宽的合理分配。
如果 SNI 被过滤,客户端通常无法与目标服务器建立 HTTPS 连接。为了更好地理解这个过程,我们需要了解 SSL/TLS 连接的建立过程。
以下是 SSL/TLS 连接建立的基本过程:
- 客户端发起连接(ClientHello)
客户端向服务器发送一个 ClientHello 消息,其中包括以下信息:
支持的协议版本(如 TLS 1.2 或 TLS 1.3)
支持的加密套件(Cipher Suites)
支持的压缩方法
随机数(用于生成会话密钥)
扩展字段(包括 SNI) - 服务器响应(ServerHello)
服务器接收到 ClientHello 消息后,发送 ServerHello 消息,其中包括:
协商的协议版本
选择的加密套件
选择的压缩方法
随机数
服务器证书(用于验证服务器身份)
服务器公钥(用于加密会话密钥)
其他扩展字段 - 服务器证书验证
客户端接收到 ServerHello 和服务器证书后,会验证服务器证书的有效性。这包括检查证书的签名、有效期和颁发机构等。 - 客户端密钥交换(Client Key Exchange)
客户端生成一个预主密钥(Pre-Master Secret),并使用服务器的公钥对其进行加密,然后发送给服务器。 - 生成会话密钥
客户端和服务器使用之前的随机数和预主密钥生成会话密钥(Session Key),用于加密后续的通信。 - 结束握手(Finished)
客户端和服务器分别发送 Finished 消息,表示握手过程结束。此时,客户端和服务器之间的通信将使用会话密钥进行加密。
+---------------------------------------------------------+
| Record Layer |
+----------------+----------------+-----------------------+
| Content Type | Version | Length |
| (1 byte) | (2 bytes) | (2 bytes) |
+----------------+----------------+-----------------------+
+---------------------------------------------------------+
| Handshake Layer |
+----------------+----------------+-----------------------+
| Message Type | Length | Version |
| (1 byte) | (3 bytes) | (2 bytes) |
+----------------+----------------+-----------------------+
| Random (32 bytes) |
+---------------------------------------------------------+
| Session ID Length | Session ID (可变长度) |
| (1 byte) | |
+-------------------+-------------------------------------+
| Cipher Suites Length | Cipher Suites (可变长度) |
| (2 bytes) | |
+----------------------+----------------------------------+
| Compression Methods Length | Compression Methods (可变长度) |
| (1 byte) | |
+---------------------------+-----------------------------+
| Extensions Length | Extensions (可变长度) |
| (2 bytes) | |
+---------------------------+-----------------------------+
+---------------------------------------------------------+
| Extensions |
+----------------+----------------+-----------------------+
| Extension Type | Extension Length | Extension Data |
| (2 bytes) | (2 bytes) | (可变长度) |
+----------------+------------------+---------------------+
+---------------------------------------------------------+
| SNI 扩展 |
+----------------+----------------+-----------------------+
| Extension Type | Extension Length | SNI List Length |
| (2 bytes) | (2 bytes) | (2 bytes) |
+----------------+------------------+---------------------+
| SNI Type | Host Name Length | Host Name (可变长度)|
| (1 byte) | (2 bytes) | |
+----------------+------------------+---------------------+
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。