默安逐日实验室:XDP的应用实践

news2024/11/27 16:26:35

1. 网络数据包是如何进入进计算机的

众所周知,网络数据包通常需要在TCP/IP协议栈中进行处理,但网络数据包并不直接进入TCP/IP协议栈;相反,他们直接进入网络接口。因此,在数据包进入 TCP/IP 堆栈之前,它们已经到达计算机内部。到目前为止,大多数应用程序都是在 TCP/IP 堆栈之后处理的。

image-20240118150012600

2. 什么是XDP

XDP,全称eXpress Data Path,是Linux内核提供的一个高可用性和可编程性的网络数据包处理框架,XDP允许在网络数据包到达Linux网络栈之前,在网络驱动程序级别对数据包进行处理。 XDP 使内核有能力在数据包到达网络层时快速处理数据包,它有高性能、高灵活度、低开销等优点。

3. 开始一个简单的XDP项目

挂载XDP程序请谨慎,一条错误的xdp规则是极有可能导致服务器失联的!

以下是一个很简单的XDP示例程序

#include <linux/bpf.h>

SEC("xdp")
//XDP 处理逻辑
int xdp_main(struct xdp_md *ctx)
{
    static int count = 1;
    count++;
    if (count%2)
    {
        return XDP_DROP;
    }
    else
    {
        return XDP_PASS;
    }
}

char __license[] __section("license") = "GPL";

工作时,当数据包进入接口时,计数将加1。当计数为偶数时,数据包被丢弃。

我这里采用了clang来编译XDP程序,不过clang有一定的版本限制,需要10.0以上,同样对于内核版本来说xdp需要4.15以上。

编译命令

clang -O2 -g -Wall -target bpf -c main.c -o xdp.o

当命令执行时,它会生成一个名为 xdp.o 的文件。

image-20240119104600652

当所有准备工作完成后,在网络接口上挂载xdp.o。

ip link set dev ens33 xdp obj xdp.o

如果您不需要此xdp程序,可以将其卸载。

ip link set dev ens33 xdp off

当这个XDP程序处于运行状态时,如果去ping该主机,每两组数据包中,就会有一组无响应,就像如下这样。image-20240119111654161

4. 逐步升级XDP应用功能

4.1 Level 1 使用XDP记录源地址IP

由于XDP是内核应用,而将源地址IP记录到本地又是一个用户态行为,那么我们就需要设法让内核态跟用户态进行交互,在这里我们使用了自带的bpf系统当中的MAP来进行数据交换。Map的本质是结构,它允许用户在内核空间和用户空间之间存储和共享数据。

4.1.1 XDP
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

#define MAX_ENTRIES 1024

// 定义一个xdp_map结构,其中要包含map的类型、kv的数据类型以及map的最大条数
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u32);
    __uint(max_entries, MAX_ENTRIES);
} xdp_map SEC(".maps");

struct ip_event {
    __u32 src_ip;
};

SEC("xdp")
int xdp_main(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;
    // 对一些畸形包以及非ipv4的包放行
    if (eth + 1 > data_end) {
        return XDP_PASS;
    }
    if (eth->h_proto != __constant_htons(ETH_P_IP)) {
        return XDP_PASS;
    }

    ip = data + sizeof(*eth);
    if (ip + 1 > data_end) {
        return XDP_PASS;
    }
    struct ip_event evt = {
        .src_ip = ip->saddr,
    };
    __u32 key = 0; 
    __u32 value = evt.src_ip;
    bpf_map_update_elem(&xdp_map, &key, &value, BPF_ANY);
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

在上述代码当中使用了bpf_map_update_elem接口,这个接口是bpf系统提供用来更新映射Map的。

4.1.2 用户态
package main

import (
	"fmt"
	"log"
	"net"
	"time"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/rlimit"
)

var (
	XDP_PATH = "xdp.o"
	IF       = "ens33"
	XDP_Func = "xdp_main"
	XDP_Map  = "xdp_map"
)

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatalf("关闭内存锁失败: %v", err)
	}
	// 加载xdp程序规则
	spec, err := ebpf.LoadCollectionSpec(XDP_PATH)
	if err != nil {
		log.Fatalf("XDP 程序加载失败: %v", err)
	}
	coll, err := ebpf.NewCollection(spec)
	if err != nil {
		log.Fatalf("创建新的程序集失败: %v", err)
	}
	defer coll.Close()
	// 在当前的程序集当中寻找名为 xdp_map 的bpfmap对象
	cmdMap := coll.DetachMap(XDP_Map)
	if cmdMap == nil {
		log.Fatalf("在%s当中未找到%s对象", XDP_PATH, XDP_Map)
	}
	// 将网卡名称转换为网卡index
	ifIndex, err := getInterfaceIndex(IF)
	if err != nil {
		log.Fatalf("获取%s索引失败: %v", IF, err)
	}
	// 寻找
	prog := coll.Programs[XDP_Func]
	if prog == nil {
		log.Fatalf("未找到%s方法",XDP_Func)
	}
	link, err := link.AttachXDP(link.XDPOptions{
		Program:   prog,
		Interface: ifIndex,
	})
	defer link.Close()
	// 读取map当中内容
	var key uint32 = 0
	value := make([]byte, 512)

	for {
		err = cmdMap.Lookup(&key, &value)
		if err != nil {
			log.Printf("查询map失败: %v", err)
			time.Sleep(1 * time.Second)
			continue
		}
		fmt.Println(value)
		time.Sleep(1 * time.Second)
	}
}
func getInterfaceIndex(name string) (int, error) {
	ifIndex, err := net.InterfaceByName(name)
	if err != nil {
		return 0, err
	}
	return ifIndex.Index, nil
}

在一通折腾下,达成效果如下,这同时也意味着,成功的将网卡收到的数据经过xdp程序传递到了用户态。

image-20240623174709784

当然对于想要进行使用某一个特定源ip的访问来触发某个事件的场景,也可以将打印的逻辑改掉,改成自己想要的样子。

4.2 Level 2 使用XDP做一个简易的防火墙

第二阶段开始尝试着使用XDP的行为指令来处理数据包,XDP 定义了数据包的五种处理行为

enum xdp_action {
	XDP_ABORTED = 0, // 将数据包丢弃,并抛出异常
	XDP_DROP, // 丢弃数据包
	XDP_PASS, // 放行至内核协议栈
	XDP_TX, // 从收到的这张往卡上再将包发出
	XDP_REDIRECT, // 从其他网卡将包发出,
};
4.2.1 XDP程序

在这个xdp程序当中,使用了bpf_map_lookup_elem来获取被禁止的ip,如果被禁止的ip存在在map当中,那么该ip就会被XDP_DROP行为丢掉

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

#define MAX_ENTRIES 1024

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u32);
    __uint(max_entries, MAX_ENTRIES);
} xdp_map SEC(".maps");

SEC("xdp")
int xdp_main(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 (eth + 1 > data_end) {
        return XDP_PASS;
    }

    if (eth->h_proto != __constant_htons(ETH_P_IP)) {
        return XDP_PASS;
    }

    ip = data + sizeof(*eth);
    if (ip + 1 > data_end) {
        return XDP_PASS;
    }

    __u32 src_ip = ip->saddr;
    __u32 *value;

    // 检查源 IP 是否在 BPF map 中
    value = bpf_map_lookup_elem(&xdp_map, &src_ip);
    if (value) {
        // 如果存在,则丢弃包
        return XDP_DROP;
    }

    // 否则放行包
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";
4.2.2 用户态程序

在研究过程中,使用golang来写用户态程序会在将ip的uint32作为key出现问题,可能是遇到了C跟go之间的奇妙羁绊了,所以无奈使用c来写这个用户态程序,值得注意的是在C当中挂载函数bpf_set_link_xdp_fd在较高内核版本当中变为了bpf_xdp_attach

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>

static int xdp_map_fd;
static int ifindex;

// cleanup用来清除xdp程序,若不写善后程序则在用户态程序关闭之后xdp程序还挂载在网卡上
static void cleanup(int sig) {
    if (ifindex) {
        bpf_set_link_xdp_fd(ifindex, -1, 0);
    }
    exit(0);
}

int main(int argc, char **argv) {
    struct bpf_object *obj;
    int prog_fd;
    __u32 key, value;


    signal(SIGINT, cleanup);
    signal(SIGTERM, cleanup);

    obj = bpf_object__open_file(argv[1], NULL);
    if (libbpf_get_error(obj)) {
        fprintf(stderr, "XDP 程序打开失败\n");
        return 1;
    }

    if (bpf_object__load(obj)) {
        fprintf(stderr, "XDP 程序加载失败\n");
        return 1;
    }

    prog_fd = bpf_program__fd(bpf_object__find_program_by_title(obj, "xdp"));
    if (prog_fd < 0) {
        fprintf(stderr, "未查询到xdp程序的fd\n");
        return 1;
    }

    xdp_map_fd = bpf_object__find_map_fd_by_name(obj, "xdp_map");
    if (xdp_ctrl_map_fd < 0) {
        fprintf(stderr, "未查询到map的fd\n");
        return 1;
    }

    ifindex = if_nametoindex(argv[2]);
    if (ifindex == 0) {
        return 1;
    }

    if (bpf_set_link_xdp_fd(ifindex, prog_fd, 0) < 0) {
        return 1;
    }

    char command[256];
    char ip_str[INET_ADDRSTRLEN];
    char action[10];

    while (1) {
        printf("Enter command (e.g., '192.168.11.102 block' or '10.102.11.192 accept'): ");
        fgets(command, sizeof(command), stdin);

        if (sscanf(command, "%s %s", ip_str, action) != 2) {
            fprintf(stderr, "非法输入\n");
            continue;
        }

        key = inet_addr(ip_str);

        if (strcmp(action, "block") == 0) {
            value = 1;
            if (bpf_map_update_elem(xdp_ctrl_map_fd, &key, &value, BPF_ANY) != 0) {
                perror("数据压入失败");
            } else {
                printf("Blocked IP address: %s\n", ip_str);
            }
        } else if (strcmp(action, "accept") == 0) {
            if (bpf_map_delete_elem(xdp_map_fd, &key) != 0) {
                perror("数据删除失败");
            } else {
                printf("Accepted IP address: %s\n", ip_str);
            }
        } else {
            fprintf(stderr, "未知操作: %s\n", action);
        }
    }

    return 0;
}

// 编译命令gcc -o user user.c -l:libbpf.a -lelf -lz

下面则是正常情况下golang对map进行更新的操作

// 使用golang对map进行更新
xdpMap := coll.Maps[XDP_Map]
xdpMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), ebpf.UpdateAny)

在这个测试当中,我对11.1这个ip先进行了封禁,又开了放行,这样用户态和xdp程序联动的防火墙就做好了

image-20240620153349576

image-20240620153333031

4.3 Level 3 使用 XDP 拦截特定的请求包,并发送给用户态

第三阶段就是设法将XDP的数据包传递给用户态程序,并让用户态根据收到的数据进行一些操作。这里参考leveryd师傅的项目,使用连接起来最简单的udp进行信息的传递,使用map将获取到的信息储存在value当中。

4.3.1 XDP
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>

#define SIZE1 200
#define SIZE2 180

typedef char PAYLOAD[SIZE1];

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, PAYLOAD);
    __uint(max_entries, 1);
} xdp_map SEC(".maps");

SEC("xdp")
int xdp_main(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    char match_pattern[] = "xdp";
    unsigned int payload_size, i;
    struct ethhdr *eth = data;
    unsigned char *payload;
    struct udphdr *udp;
    struct iphdr *ip;

    __u32 key = 0;
    PAYLOAD value;

    if ((void *)eth + sizeof(*eth) > data_end) {
        return XDP_PASS;
    }

    ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end) {
        return XDP_PASS;
    }

    if (ip->protocol != IPPROTO_UDP){
        return XDP_PASS;
    }

    udp = (void *)ip + sizeof(*ip);
    if ((void *)udp + sizeof(*udp) > data_end){
        return XDP_PASS;
    }

    payload_size = ntohs(udp->len) - sizeof(*udp);
    if (payload_size != SIZE1) {
        return XDP_PASS;
    }

    payload = (unsigned char *)udp + sizeof(*udp);
    if ((void *)payload + payload_size > data_end) {
        return XDP_PASS;
    }

    for (i = 0; i < payload_size && payload_size <= SIZE1; i++){
        if (i == sizeof(match_pattern) - 1) {
            break;
        }
        if (payload[i] != match_pattern[i]){
            return XDP_PASS;
        }
    }
        bpf_map_update_elem(&xdp_map, &key, (char *)payload, BPF_ANY);
    return XDP_DROP;
}

char _license[] SEC("license") = "GPL";
4.3.2 用户态
package main

import (
	"fmt"
	"log"
	"net"
	"time"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/rlimit"
)

const PayloadSize = 200

var (	XDP_PATH = "xdp.o"
	IF       = "ens33"
	XDP_Func = "xdp_main"
	XDP_Map  = "xdp_map") 

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatalf("移除内存锁失败: %v", err)
	}
	spec, err := ebpf.LoadCollectionSpec(XDP_PATH)
	if err != nil {
		log.Fatalf("加载 XDP 程序失败: %v", err)
	}
	
	coll, err := ebpf.NewCollection(spec)
	if err != nil {
		log.Fatalf("创建新的集合失败: %v", err)
	}
	defer coll.Close()
	xdpMap := coll.DetachMap(XDP_Map)

	if xdpMap == nil {
		log.Fatalf("在 XDP 程序中找不到 map")
	}
	fmt.Println(xdpMap.FD())
	
	ifIndex, err := getInterfaceIndex(IF)
	if err != nil {
		log.Fatalf("获取接口索引失败: %v", err)
	}
	prog := coll.Programs[XDP_Func]
	if prog == nil {
		log.Fatalf("找不到 % 程序")
	}
	link.AttachXDP(link.XDPOptions{
		Program:   prog,
		Interface: ifIndex,
	})
	
	var key uint32 = 0
	value := make([]byte, 180)
	for {
		value, err = xdpMap.LookupBytes(&key)
		fmt.Println(xdpMap.String())
		if err != nil {
			log.Printf("查找 map 失败: %v", err)
			time.Sleep(1 * time.Second)
			continue
		}
		fmt.Println(value)
		if value[0] != '\x00' {
			fmt.Printf("接收到的值: %s\n", string(value))
			// 重置 map 中的值
			err = xdpMap.Update(key, make([]byte, PayloadSize), ebpf.UpdateAny)
			if err != nil {
				log.Printf("更新 map 失败: %v", err)
			}
		}
		time.Sleep(1 * time.Second)
	}
}
func getInterfaceIndex(name string) (int, error) {
	ifIndex, err := net.InterfaceByName(name)
	if err != nil {
		return 0, err
	}
	return ifIndex.Index, nil
}

由于xdp是在二层处理数据,而端口是四层才有的概念,所以对发往任意端口的包都会被接收(前提是不超过65535),因为记录包内容以后采取的是XDP_DROP行为,所以在流量上看并不能发现这个包未到达,

image-20240624111925272

image-20240624021100965

4.4 Level 4 使用XDP将处理过的请求包发回

4.4.1 XDP_TX

在这个demo当中,我们尝试了对udp包的源地址目的地址进行了对调,并通过xdp挂载的网卡发出,其中用到了XDP_TX行为,XDP_TX默认会将包原封不动的从自己的网卡丢出。

 if (ip->protocol != IPPROTO_UDP)
        return XDP_PASS;

    struct udphdr *udp = (void *)(ip + 1);
    if ((void *)(udp + 1) > data_end)
        return XDP_PASS;
    // 对换mac
    unsigned char tmp_mac[ETH_ALEN];
    __builtin_memcpy(tmp_mac, eth->h_source, ETH_ALEN);
    __builtin_memcpy(eth->h_source, eth->h_dest, ETH_ALEN);
    __builtin_memcpy(eth->h_dest, tmp_mac, ETH_ALEN);
    
    // 对换地址
    __u32 tmp_ip = ip->saddr;
    ip->saddr = ip->daddr;
    ip->daddr = tmp_ip;

    // 对换端口
    __u16 tmp_port = udp->source;
    udp->source = udp->dest;
    udp->dest = tmp_port;
    

    return XDP_TX;

下面是使用起来的效果

image-20240623184432678

4.4.2 修改发出的包

那么利用level2当中的向手段与XDP_TX行为结合,就可以修改包内数据并发出了,以下是实现所用到的三个代码块。

// 定义一个map
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __uint(value_size, 128);
    __uint(max_entries, 1);
} xdp_map SEC(".maps");

// 从map当中获取收到的包
__u32 key = 0;
char *payload = bpf_map_lookup_elem(&xdp_map, &key);  

//将数据包进行修改
char *udp_payload = (char *)(udp + 1);
    if ((void *)udp_payload + 128 > data_end)  
        return XDP_PASS;
 __builtin_memcpy(udp_payload, payload, 128);

在之前golang加载器的基础上加入向map当中压数据的操作,新的加载器就做好了

// golang loader
	key := uint32(0)
	payload := [128]byte{69, 120, 112, 101, 108, 108, 105, 97, 114, 109, 117, 115}
	err = xdpMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&payload), ebpf.UpdateAny)
	if err != nil {
		log.Fatalf("写入map失败: %v", err)
	}

用起来的效果是这个样子的

image-20240623193707516

利用xdp将发包收包的功能进行结合,就可以达到不通过协议栈来进行数据传递了,需要注意的是,如过想要使发回的包正常进入协议栈,还需要对增加ip包与udp包的校验和重新计算,只有校验和正确的包才能进入到传输层端口上。

5. 恶意XDP如何处置

当一台主机发生一些邪门的行为的时候,尤其是网络行为,有几率是被下了恶意的XDP程序,恶意的xdp可以使用bpftool工具进行排查

bpftool prog

image-20240623204808594

prog参数会将所有的prog以及挂载时间都列出来,根据prog可以寻找可疑的xdp运行程序,再使用ip命令可以找出是哪张网卡有xdp的挂载

ip link show

image-20240623205223795

如果发现了恶意程序再使用ip命令进行卸载

ip link set dev <interface> xdp off

参考文献

https://developers.redhat.com/blog/2021/04/01/get-started-with-xdp

https://www.cnblogs.com/bakari/p/10966303.html

https://rexrock.github.io/post/xdp1/

https://www.leveryd.top/2022-08-14-%E8%81%8A%E4%B8%80%E8%81%8A%E5%9F%BA%E4%BA%8E%22ebpf%20xdp%22%E7%9A%84rootkit/

https://developers.redhat.com/blog/2021/04/01/get-started-with-xdp

https://github.com/xdp-project/xdp-tutorial

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1897260.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算智能】遗传算法(二):基本遗传算法在优化问题中的应用【实验】

前言 本系列文章架构概览&#xff1a; 本文将介绍基本遗传算法在解决优化问题中的应用,通过实验展示其基本原理和实现过程&#xff1a;选取一个简单的二次函数作为优化目标&#xff0c;并利用基本遗传算法寻找其在指定范围内的最大值。 2. 基本遗传算法&#xff08;SGA&#x…

柯桥小语种学校成人生活口语学习|西班牙语中H为什么不发音…

01 H en el alfabeto espaol 西语字母表中的h 字母H是唯一一个在标准西班牙语中不再代表任何音素的字母。尽管在它单独出现时被叫做HACHE&#xff0c;但在大多数单词拼写中&#xff0c;它只是一个没有声音对应关系的字母&#xff0c;因此RAE称其为“无声的H”&#xff08;hac…

如何正确安装与维护电化学一氧化碳传感器?专业指南

电化学一氧化碳传感器是一种用于检测空气中一氧化碳&#xff08;CO&#xff09;浓度的设备&#xff0c;它基于电化学原理运作。这类传感器的核心组成部分包括电极、电解质和透气膜&#xff0c;它们共同构成一个微型的电化学电池。 电化学一氧化碳传感器的特点包括&#xff1a;…

滑动窗口练习3-最大连续1的个数(三)

题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 给定一个二进制数组 nums 和一个整数 k&#xff0c;如果可以翻转最多 k 个 0 &#xff0c;则返回 数组中连续 1 的最大个数 。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1,0,0,0,1…

平面设计考试题

考试题 缺省页作用&#xff1a;缓减缺省页带来的负面情绪&#xff0c;增加s用户与产品的粘度&#xff0c;提升产品的用户体验 网站基本类型 c端b端 c端 面向用户和消费者的 门户站 产品网站 企业网站 电商网站 专题页面 游戏网站 视频网站 h5移动端 四大门户网站:新浪&…

【Android源码】Gerrit上传Android源码

关于Gerrit的安装参考下面链接 【Android源码】Gerrit安装 要实现上传Android源码&#xff0c;需要经历以下几步&#xff1a; 下载Android代码创建源码仓库创建manifests仓库上传源码其他电脑下载源码 要证明Gerrit中的源码真实可用&#xff0c;肯定是以其他人能真正共享到代…

idea中maven全局配置

配置了就不需要每次创建项目都来设置maven仓库了。 1.先把项目全关了 2. 进入全局设置 3.设置maven的仓库就可以了

绝地求生PUBG都准备了但是一直不匹配怎么办

绝地求生PUBG&#xff0c;作为一款备受玩家喜爱的射击游戏&#xff0c;以其真实的战斗体验和策略性决策而著称。玩家们在游戏中需要熟悉地图、选择适合的装备和战斗方式&#xff0c;并与队友进行默契的团队合作&#xff0c;才能在战场上取得优势。最近很多玩家都开始下载游玩绝…

RedHat9 | kickstart无人值守批量安装

一、知识补充 kickstart Kickstart是一种用于Linux系统安装的自动化工具&#xff0c;它通过一个名为ks.cfg的配置文件来定义Linux安装过程中的各种参数和设置。 kickstart的工作原理 Kickstart的工作原理是通过记录典型的安装过程中所需人工干预填写的各种参数&#xff0c;…

昇思25天学习打卡营第17天|ChatGLM-6B聊天demo

一、简介&#xff1a; 本次实验&#xff0c;基于MindNLP和ChatGLM6B模型搭建一个小的聊天应用&#xff0c;ChatGLM6B 是基于 GLM-4 模型开发的开源对话机器人&#xff0c;拥有 62 亿个参数&#xff0c;能够进行自然流畅的语言交流。在对话中&#xff0c;ChatGLM6B 可以胜任文案…

实现原理:远程过程调用(RPC)

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

破解在制品管理不透明难题

在快节奏的现代工业浪潮中&#xff0c;每一个细微的管理环节都直接关系到企业的竞争力与盈利能力。在车间生产中&#xff0c;在制品管理流程不透明是一个常见问题&#xff0c;它可能导致生产效率低下、成本增加、库存积压以及沟通障碍等负面影响。 在制品管理流程不透明&#x…

人员定位技术的行业应用,你有没有了解过?

在之前的文章中&#xff0c;新锐科创为大家讲述了&#xff0c;将人员定位技术实际应用于工厂人员定位的效果&#xff0c;大家感觉是不是有点新颖&#xff0c;那么&#xff0c;你又是否清楚人员定位技术的行业应用呢&#xff1f;如果你从来都没有了解过&#xff0c;建议看看本篇…

【EFK】efk 8收集docker容器日志测试

前言 目前&#xff0c;efk 全家桶已经更新到版本8 了&#xff0c;本章节我们使用8版本的elk搭建日志收集系统&#xff0c;了解它的配置运行过程&#xff0c;方便以后在更复杂的环境中更好的使用。 版本默认就是8最新的&#xff0c;也可以自己指定其他8的版本 elasticsearch: …

php简单商城小程序系统源码

&#x1f6cd;️【简单商城小程序】&#x1f6cd;️ &#x1f680;一键开启&#xff0c;商城搭建新体验&#x1f680; 你还在为繁琐的商城搭建流程头疼吗&#xff1f;现在&#xff0c;有了简单商城系统小程序&#xff0c;一切变得轻松又快捷&#xff01;无需复杂的编程知识&a…

Stable Diffusion美得令人心动亚洲女性真人模型、提示词分享!

前言 提示词1: 电影灯光 Prompt&#xff1a; xxmixgirl, a mysterious woman, fog, movie lights, 【colors】 theme, smiling 提示词&#xff1a;神秘女子&#xff0c;迷雾&#xff0c;电影灯光&#xff0c;【颜色】主题&#xff0c;微笑 颜色&#xff1a;可以指定一种或多种…

晨持绪电商:大学毕业生投资抖音网店怎么样

在这个数字化飞速发展的时代&#xff0c;传统的职业路径已不再是唯一的选择。对于充满激情和创意的大学毕业生来说&#xff0c;投资抖音网店或许是一个颇具前景的选择。 抖音作为一个流量巨大的社交媒体平台&#xff0c;为年轻人提供了一个展示自我、推广产品的绝佳舞台。与传统…

Kotlin/Android中执行网络请求

方式一&#xff1a;使用okhttp3 okhttp官网 okhttp3 github地址 打开build.gradle.kts文件加入依赖 dependencies {implementation("com.squareup.okhttp3:okhttp:4.9.0") }在IDEA的Gradle面板点击reload按钮便会自动下载jar 使用网络请求时需要把网络的权限打开&a…

Web缓存—Nginx和CDN应用

目录 一、代理的工作机制 二、概念 三、作用 四、常用的代理服务器 二.Nginx缓存代理服务器部署 1.在三台服务器上部署nginx 此处yum安装 2.准备测试界面 三、CDN概念及作用 1.CDN的工作过程 一、代理的工作机制 &#xff08;1&#xff09;代替客户机向网站请求数据…

echarts横向立体3D柱状图

实现原理&#xff1a;series中包含两个普通的柱状图bar&#xff0c;其宽度各占一半且设置间距barGap为0&#xff0c;再添加一个象形柱状图pictorialBar&#xff0c;symbol设为菱形diamond&#xff0c;调整其位置大小层级等数据以达到覆盖在柱状图顶部的立体效果。 运行效果&am…