深入理解netfilter和iptables

目录

Netfilter的设计与实现

内核数据包处理流

netfilter钩子

钩子触发点

NF_HOOK宏与Netfilter裁定

回调函数与优先级

iptables

内核空间模块

xt_table的初始化

ipt_do_table()

 复杂度与更新延时

用户态的表,链与规则

conntrack


Netfilter(结合iptables)使应用程序能注册处理规则来使内核协议栈处理数据包,使能高效的网络转发和过滤。很多常用的主机端防火墙比如K8S的转发服务就是用iptables实现的。

很多关于netfilter的介绍性文章只是概括了主要部分,对于基础的内核实现方面涉及的不多。这篇文章就是主要针对Linux 2.6版本的内核代码,也许会和最新的5.x版本有很大的不同,但基本的设计思想不会改变并且不会影响对原理的理解。

这篇文章假设读者对TCP/IP有基本的理解。

Netfilter的设计与实现

netfilter是实现在Linux内核里的一种网络数据包处理框架。为了彻底地理解netfilter是如何工作的,我们首先要了解Linux内核里数据包处理流程。

内核数据包处理流

在内核里,数据包的处理流程可根据TCP/IP模型大体分为多层,例如内核代码链被调用来处理网路数据包,下面以IPv4的TCP数据包举例。

1、在设备物理层上,网卡通过DMA将接收到的网络数据包写入RX Ring缓存区。在一系列的中断和调度后,操作系统内核调用__skb_dequeue将数据包添加到响应设备的处理队列里,并将数据包转换成sk_buffer类型。这样数据包就能用sk_buffer表示了,最终调用netif_receive_skb功能来根据协议类型解析数据包并跳转到对应的数据包处理函数。流程以下图展示:

 2、假设收到的数据包是网络层数据包,那对应的响应处理函数就是ip_rcv,也就是数据包处理进入网络层。ipv_rcv会检查IP报文头并丢弃错误包,或必要时组合分片的报文。随后ip_rcv_finish函数被执行来路由报文(决定该报文是本地接收还是转发给其他主机)。假设数据包的目的地址是本机,那么下步dst_input函数会调用ip_local_deliver函数。ip_local_deliver函数会根据IP头里的协议号决定数据包的协议类型,并最终调用对应类型的数据包处理函数。在这个例子中,对于TCP协议tcp_v4_rcv函数会被调用,这样数据包处理就进入了传输层。

3、tcp_v4_rcv函数会读取数据包的TCP头,并校验checksum值,然后再报文对应的TCP控制字段填充一些必要的状态值,包括TCP序列号和SACK值。下步调用__tcp_v4_lookup函数查询报文对应的套接字,如果找不到对应的套接字或者套接字的状态是TCP_TIME_WAIT,数据包就被丢弃。如果套接字可用,会调用tcp_prequeue函数让数据包进入prequeue队列,以便于用户态的用户程序能处理。传输层的处理流程超出了本书的范围,实际是很复杂的。

netfilter钩子

让我们继续,netfilter的主要部分就是netfilter钩子。

钩子触发点

针对不同的协议(IPv4,IPv6,或ARP等),Linux内核网络协议栈会沿着协议栈的处理路径上在预定义的节点触发对应的钩子函数。以IPv4协议举例,不同协议的处理数据流里的触发点位置和对应的钩子名字入下图所示:

这些所谓的钩子在代码中实际上是枚举类型的

enumnf_inet_hooks {
    NF_INET_PRE_ROUTING,
    NF_INET_LOCAL_IN,
    NF_INET_FORWARD,
    NF_INET_LOCAL_OUT,
    NF_INET_POST_ROUTING,
    NF_INET_NUMHOOKS
};

每个钩子都放在了内核协议栈的特定触发点上,以下图IPv4举例。

 上图中钩子的详细描述如下:

NF_INET_PRE_ROUTING:这个钩子在IPv4栈的ip_rcv函数里执行或在IPv6栈的ipv6_rcv函数里执行。对于所有进来的数据包来说,是第一个触发的钩子节点(事实上,新版本的Linux添加了INGRSS作为最早的钩子触发节点),在路由判断前执行。

NF_INET_LOCAL_IN: 这个钩子在IPv4栈的ip_local_deliver函数里执行或在IPv6栈的ip6_input函数里执行。在路由判断后,所有目的地址是本机的进来的数据包都会触发该钩子函数。

NF_INET_FORWARD:这个钩子是在IPv4栈的ip_forward函数内执行或在IPv6栈的ip6_forward函数内执行。在路由判断后,所有非本机接收的进来数据包都会触发该钩子函数。

NF_INET_LOCAL_OUT:这个钩子在IPv4栈的__ip_local_out函数内执行或在IPv6栈的__ipv6_local_out函数内执行。所有本地生成的准备发出去的数据包在进入网络协议栈后都会触发该钩子函数。

NF_INET_POST_ROUTING:这个钩子在IPv4栈的ip_output函数内执行或在IPv6栈的ip6_finish_output2函数内执行。所有本地生成的准备发出去的或者需要转发的数据包在路由判断后触发该钩子函数。

NF_HOOK宏与Netfilter裁定

宏NF_HOOK被所有的钩子触发节点调用。代码展示如下:

static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct sk_buff *skb,
    struct net_device *in, struct net_device *out,
    int(*okfn)(struct sk_buff *))
{
    return NF_HOOK_THRESH(pf, hook, skb, in, out, okfn, INT_MIN);
}

NF_HOOK参数介绍如下:

  • pf: 数据包的协议族, 比如IPv4为NEPROTO_IPv4
  • hook: 上面展示过的netfilter钩子枚举类型,比如NF_INET_PRE_ROUTING 或 NF_INET_LOCAL_OUT
  • skb: 正在处理的数据包SKB结构体
  • in: 数据包的输入网络设备
  • out: 数据包的输出网络设备
  • okfn: 钩子将结束时会被调用的函数指针,通常为数据包处理流程里下一个处理函数。

NF_HOOK的返回值是如下所示并解释特定含义

  1. NF_ACCEPT: 通常继续在处理路径上(事实上是 NF-HOOK传进来的最后一个参数okfn会被执行)。
  2. NF_DROP: 丢弃数据包并终止处理。
  3. NF_STOLEN: 数据包被转发并终止处理。
  4. NF_QUEUE: 将数据包入队列给应用程序处理。
  5. NF_REPEAT: 重新进行本钩子函数处理。

回到源代码,IPv4内核网络协议栈会在代码模块内调用NF_HOOK()。

比如net/ipv4/ip_forward.c源代码内的数据包转发为例,NF_HOOK宏会在ip_forward函数的最后被调用,NF_INET_FOEWARD钩子作为NF_HOOK的输入参数,ip_forward_finish作为okfn参数输入做为下一步处理函数。

int ip_forward(struct sk_buff *skb)
{
.....
if (rt->rt_flags&RTCF_DOREDIRECT && !opt->srr && !skb_sec_path(skb))
        ip_rt_send_redirect(skb);

    skb->priority = rt_tos2priority(iph->tos);

    return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD, skb, skb->dev,
          rt->dst.dev, ip_forward_finish);
.....
}

回调函数与优先级

netfilter的另一个组件是钩子回调函数。

内核网络协议栈使用钩子来代表指定的触发节点并用钩子(枚举值)作为索引值(NF_INET_FORWARD)来调用回调函数响应钩子节点。

内核里的其他模块可以通过netfilter提供的API向指定的钩子上注册回调函数。同一个钩子可以注册多个回调函数,并且在注册的时候能指定优先级参数priority来制定执行时按优先级来调用。

为一个钩子注册回调函数,第一步是定义nf_hook_ops结构体,示例如下:

struct nf_hook_ops {
    struct list_head list;

    /* User fills in from here down. */
    nf_hookfn *hook;
    struct module *owner;
    u_int8_t pf;
    unsigned int hooknum;
    /* Hooks are ordered in ascending priority. */
  int priority;
};

 结构体定义里有是三种重要的成员解释如下:

  • hook: 被注册的回调函数,函数的参数跟NF_HOOK参数类似,能通过okfn参数与其他函数串联在一起。
  • hooknum: 被注册的钩子的枚举值
  • priority: 回调函数的优先级,最小值优先。

定义了上述结构体后,一个或者多个回调函数用int nf_register_hook(struct nf_hook_ops *reg);函数或者int nf_register_hooks(struct nf_hook_ops *reg, unsigned int n);来注册。同一个netfiler下的所有nf_hook_ops都是注册的时候按优先级顺序形成链表,所以注册程序会根据优先级在链表中找到nf_hook_ops的合适位置并做链表插入动作。

当NF_HOOK宏被执行来触发指定的钩子时,nf_iterate函数会被调用来遍历链表里的nf_hook_ops以响应该钩子,且每个带hookfn成员的nf_hook_ops都会按序执行。如下图所示:

这种回调函数按链工作的方式也是导致netfilter钩子被称为chain的原因,这种关系尤其反应在iptables的介绍章节。

每个调用函数都必须返回netfilter裁定值;如果裁定值是NF_ACCEPT,nf_iterate会继续调用下一个nf_hook_ops的回调函数知道所有的回调函数被调用完且都是NF_ACCEPT返回值。如果裁定值是NF_DROP,它机会打破传输直接返回NF_DROP;如果裁定值是NF_REPEAT,回调函数会被重新执行。nf_iterate的返回值会被NF_HOOK当做返回值使用,网络协议栈会根据该返回值决定是否继续处理。具体流程如下图所示:

netfiler钩子的回调函数机制有以下特点:

  • 回调函数会依优先级执行,且只有前面的回调函数返回NF_ACCEPT,下一步回调函数才会被执行。
  • 如何一个回调函数可以中断该钩子的回调函数执行链且请求整个网络协议栈中止函数处理。

iptables

基于内核netfilter提供的钩子回调函数机制,netfilter的作者Rusty Russell也开发了iptables来管理用户规则应用到用户态的数据包。

iptbles主要分成以下两个部分:

  • 用户态的ipatbels命令给用户提供了通向内核iptables模块的管理接口。
  • 内核空间的ipatbles模块在内存空间上分配了规则表,能创建或注册规则表。

内核空间模块

xt_table的初始化

在内核网络协议栈,ipatbles通过xt_table结构体有条不紊地管理了大量的数据包处理规则。一个xt_table对应一个规则表,即用户空间下发的规则表。

  • 作用于不同的netfilter钩子
  • 在同一个钩子上不同的规则表有不同的优先级

基于规则的最终目的,ipatbels会默认初始化四个不同的规则表,名字为raw,filter,nat和mangle。下面示例了xt_table的初始化和调用过程。

过滤表的定义如下:

#define FILTER_VALID_HOOKS ((1 << NF_INET_LOCAL_IN) | \
                            (1 << NF_INET_FORWARD) | \ 
                            (1 << NF_INET_LOCAL_OUT))

static const struct xt_table packet_filter = { 
        .name = "filter", 
        .valid_hooks = FILTER_VALID_HOOKS, 
        .me = THIS_MODULE, 
        .af = NFPROTO_IPV4, 
        .priority = NF_IP_PRI_FILTER, 
};

/* net/ipv4/netfilter/iptable_filter.c */

iptable_filter.c里的初始化函数iptable_filter_init函数会调用xt_hook_link为表packet_filter(struct xt_table类型)绑定必要的钩子函数iptable_filter_init。

struct nf_hook_ops *xt_hook_link(const struct xt_table *table, nf_hookfn *fn)
{
        unsigned int hook_mask = table->valid_hooks;
        uint8_t i, num_hooks = hweight32(hook_mask);
        uint8_t hooknum;
        struct nf_hook_ops *ops;
        int ret;

        ops = kmalloc(sizeof(*ops) * num_hooks, GFP_KERNEL);
        if (ops == NULL)
                return ERR_PTR(-ENOMEM);

        for (i = 0, hooknum = 0; i < num_hooks && hook_mask != 0;
             hook_mask >>= 1, ++hooknum) {
                if (!(hook_mask & 1))
                        continue;
                ops[i].hook     = fn;
                ops[i].owner    = table->me;
                ops[i].pf       = table->af;
                ops[i].hooknum  = hooknum;
                ops[i].priority = table->priority;
                ++i;
        }

        ret = nf_register_hooks(ops, num_hooks);
        if (ret < 0) {
                kfree(ops);
                return ERR_PTR(ret);
        }

        return ops;
}
EXPORT_SYMBOL_GPL(xt_hook_link);

static int __init iptable_filter_init(void)
{
        int ret;

        ret = register_pernet_subsys(&iptable_filter_net_ops);
        if (ret < 0)
                return ret;

        /* Register hooks */
        filter_ops = xt_hook_link(&packet_filter, iptable_filter_hook);
        if (IS_ERR(filter_ops)) {
                ret = PTR_ERR(filter_ops);
                unregister_pernet_subsys(&iptable_filter_net_ops);
        }

        return ret;
}

执行如下的初始化过程:

  1. 为xt_table作用的每个钩子遍历.valid_hooks属性,比如filter有3个钩子NF_INET_LOCAL_INNF_INET_FORWARDNF_INET_LOCAL_OUT。
  2. 为每个钩子,按xt_table的优先级属性注册回调函数到钩子。

不同表的优先级值枚举如下所示:

enum nf_ip_hook_priorities {
    NF_IP_PRI_RAW = -300,
    NF_IP_PRI_MANGLE = -150,
    NF_IP_PRI_NAT_DST = -100,
    NF_IP_PRI_FILTER = 0,
    NF_IP_PRI_SECURITY = 50,
    NF_IP_PRI_NAT_SRC = 100,
};

当一个数据包达到钩子触发节点,所有被不同的规则表注册到该钩子的回调函数都会被有序执行,根据以上的优先级顺序执行。

ipt_do_table()

注册到filter表的钩子回调函数iptable_filter_hook会执行公共的规则检查函数ipt_do_table。ipt_do_table函数接收skb,hook和xt_table作为参数,并执行由后两个参数针对数据包选定的规则集,返回netfilter裁定值作为回调函数的返回值。

在深入规则执行前,有必要理解下内存里规则集是如何表示的。每个规则都包含三个部分

  1. ipt_entry结构体的next_offset成员指向下一个ipt_entry的内存偏移地址处。
  2. 0个或多个ipt_entry_match结构体,每个都可以动态增添额外数据。
  3. 一个ipt_entry_target结构体,每个都可以动态增添额外数据。

ipt_entry结构体定义如下:

struct ipt_entry {
    struct ipt_ip ip;
    unsigned int nfcache;

    /* ipt_entry + matches 在内存中的大小*/
    u_int16_t target_offset;
    /* ipt_entry + matches + target 在内存中的大小 */
    u_int16_t next_offset;

    /* 跳转后指向前一规则 */
    unsigned int comefrom;
    /* 数据包计数器 */
    struct xt_counters counters;
    /* 长度为0数组的特殊用法,作为 match 的内存地址 */
    unsigned char elems[0];
};

 ipt_do_table first jumps to the corresponding ruleset memory area based on the hook type and the xt_table.private.entries property, and performs the following procedure.

  1. first check whether the IP initial of the packet matches the .ipt_ip attribute of the first rule ipt_entry, if it does not match according to the next_offset attribute jump to the next rule.
  2. If the IP initial matches, all ipt_entry_match objects defined by the rule are checked in turn, and the match function associated with the object is called, with the result of returning to the callback function (and whether to drop the packet), jumping to the next rule, or continuing the check, depending on the return value of the call.
  3. ipt_entry_target is read after all checks are passed, and according to its properties returns the netfilter vector to the callback function, continues to the next rule or jumps to another rule at the specified memory address. non-standard ipt_entry_target will also call the bound function, but can only return the vector value and cannot jump to another rule.

 复杂度与更新延时

The above data structure and execution provides iptables with the power to scale, allowing us to flexibly customize the matching criteria for each rule and perform different behaviors based on the results, and even stack-hop between additional rulesets.

Because each rule is of varying length, has a complex internal structure, and the same ruleset is located in contiguous memory space, iptables uses full replacement to update rules, which allows us to add/remove rules from user space with atomic operations, but non-incremental rule updates can cause serious performance problems when the rules are orders of magnitude larger: if a large If you use the iptables approach to implementing services in a large Kubernetes cluster, when the number of services is large, updating even one service will modify the iptables rule table as a whole. The full commit process is protected by kernel lock, so there is a significant update latency.

用户态的表,链与规则

用户态的iptables命令行工具可以从给定的表里读出数据显示在终端,添加新的规则(实际是替换整个规则表内容)等。

iptables主要操作如下对象:

  • table: corresponds to the xt_table structure in kernel space. All operations of iptable are performed on the specified table, which defaults to filter.
  • chain: The set of rules that the specified table calls through a specific netfilter hook, and you can also customize the rule set and jump from the hook rule set.
  • rule: corresponds to ipt_entryipt_entry_match and ipt_entry_target above, defining the rules for matching packets and the behavior to be performed after the match.
  • match: A highly scalable custom match rule.
  • target: A highly scalable custom post-match behavior.

代码调用过程描述如下,链和规则按如下图表执行:

 iptables的一些特殊用法和命令,你可以参考如下的链接:Common Firewall Rules and Commands | DigitalOcean.

conntrack

It is not enough to filter packets by the first part of the layer 3 or 4 information, sometimes it is necessary to further consider the state of the connection. netfilter performs connection tracking with another built-in module conntrack to provide more advanced network filtering functions such as filtering by connection, address translation (NAT), etc. Because of the need to determine the connection status, conntrack has a separate implementation for protocol characteristics, while the overall mechanism is the same.

I was going to continue with conntrack and NAT, but I decided against it because of its length. I will write another article explaining the principles, applications and Linux kernel implementation of conntrack when I have time.

翻译原文如下:

In-depth understanding of netfilter and iptables - SoByte

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

100种思维模型之安全边际思维模型-92

安全边际&#xff0c; 简而言之即距离某一件糟糕的事件发生&#xff0c;还有多大的空间&#xff0c;安全边际越高&#xff0c;我们就越安全&#xff01; 安全边际思维模型一个 让生活变得更从容 的 思维模型。 01、何谓安全边际思维模型 一、安全边际思维 安全边际 源于…

ACL 2023 | 持续进化中的语言基础模型

尽管如今的 AI 模型已经具备了理解自然语言的能力&#xff0c;但科研人员并没有停止对模型的不断改善和理论探索。自然语言处理&#xff08;NLP&#xff09;领域的技术始终在快速变化和发展当中&#xff0c;酝酿着新的潮流和突破。 NLP 领域的顶级学术会议国际计算语言学年会 …

声网 Agora音视频uniapp插件跑通详解

一、前言 在使用声网SDK做音视频会议开发时, 通过声网官方论坛 了解到,声网是提供uniapp插件的,只是在官方文档中不是很容易找到。 插件地址如下: Agora音视频插件 Agora音视频插件(JS) 本文讲解如何跑通演示示例 二、跑通Demo 2.1 环境安装: 参考: 2. 通过vue-…

vue3+element+sortablejs实现table表格 行列动态拖拽

vue3elementsortablejs实现table动态拖拽 1.第一步我们要安装sortablejs依赖2.在我们需要的组件中引入3.完整代码4.效果 1.第一步我们要安装sortablejs依赖 去博客设置页面&#xff0c;选择一款你喜欢的代码片高亮样式&#xff0c;下面展示同样高亮的 代码片. npm install so…

巩固一下NodeJs

1、初始化(确保当前电脑有node环境) npm init 2、安装express npm i expressnpm i ws文件结构 3、编写相关代码启动node服务(server.js) //导入下列模块&#xff0c;express搭建服务器&#xff0c;fs用来操作文件、ws用来实现webscoket const express require("expr…

Android 使用webView打开网页可以实现自动播放音频

使用webview 自动播放音视频&#xff0c;场景如&#xff0c;流媒体自动部分&#xff0c;音视频通话等。会出现如下问题&#xff1a; 解决方案如下&#xff1a; 配置webview 如下&#xff0c;这样可以自动播放音频。 webView.getSettings().setMediaPlaybackRequiresUserGestur…

原生JS实现图片裁剪功能

功能介绍&#xff1a;图片通过原生input上传&#xff0c;使用canvas进行图片裁剪。 裁剪框限制不允许超出图片范围&#xff0c;图片限制了最大宽高&#xff08;自行修改要的尺寸&#xff09;&#xff0c;点击确认获取新的base64图片数据 注&#xff1a;fixed布局不适用该方案&…

在vue中点击弹框给弹框中的表格绑值

场景描述&#xff1a;如下图所示&#xff0c;我们需要点击 ‘账单生成’ 按钮&#xff0c;然后里边要展示一个下图这样的表格。 最主要的是如何展示表格中的内容&#xff0c;一起看看吧&#xff01; <template><!-- 水费 欠费--><el-dialog title"水费欠费…

短视频seo矩阵源码开发与实践分享

在短视频矩阵系统源码开发中&#xff0c;需要注意以下几个细节&#xff1a; 1. 确定系统的功能需求&#xff1a;在开发短视频矩阵系统源码时&#xff0c;必须先明确系统的功能需求&#xff0c;包括用户的基本操作、系统数据的生成和处理等。 2. 定义数据库结构&#xff1a;短…

零售数字化转型如何破局?这篇文章全说清了!

“数字化转型”&#xff0c;一个老生常谈的话题。自19世纪互联网崭露头角&#xff0c;亚马逊和eBay等电商平台崛起&#xff0c;引领电子商务的发展。传统零售业开始意识到在线渠道的重要性&#xff0c;并纷纷推出自己的电子商务网站&#xff0c;从自此进入数字化转型的赛道当中…

利用电价运行策略研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

STM32单片机语音识别台灯控制系统人检测亮度调节

实践制作DIY- GC00156-语音识别台灯控制系统 一、功能说明&#xff1a; 基于STM32单片机设计-语音识别台灯控制系统 二、功能说明&#xff1a; 电路&#xff1a;STM32F103C系列最小系统串口语音识别模块LED灯板1个红外传感器 1.任何时候没有人则关闭灯。有人可以自动打开灯。…

激斗云计算:互联网大厂打响新一轮排位战

大模型如同一辆时代列车&#xff0c;所有科技大厂都想上车。 自去年底ChatGPT一炮而红&#xff0c;国内外数十家科技大厂、创业公司、机构相继下场&#xff0c;一时间掀起大模型的热浪。 《中国人工智能大模型地图研究报告》显示&#xff0c;截至今年5月28日&#xff0c;中国…

contentEditable属性

我们最常用的输入文本内容便是input与textarea&#xff0c;但是有一个属性&#xff0c;可以让我们在很多标签中&#xff0c;如div,table,p,span,body等&#xff0c;可以像input输入框一样&#xff0c;实现文本编辑&#xff0c;这便是contentEditable属性 之前有用到这个属性是在…

快速搭建一个美观且易用的 Django 管理后台 —— django-xadmin

Django-xadmin&#xff08;也称为Xadmin&#xff09;是一个第三方的 Django 应用程序&#xff0c;它提供了一系列工具和模板来快速开发基于 Django 的后台管理界面。使用 Django-xadmin 可以用很少的代码就创建出一个强大的、具备实时查看数据、增、删、改等基本操作的 Django …

LiveNVR监控流媒体Onvif/RTSP功能-安全控制HTTP接口鉴权开启禁止游客访问开启后401 Unauthorized如何播放调用接口

LiveNVR安全控制HTTP接口鉴权开启禁止游客访问开启后401 Unauthorized如何播放调用接口&#xff1f; 1、安全控制1.1、接口鉴权1.2、禁止游客访问 2、401 Unauthorized2.1、携带token调用接口2.1.1、获取鉴权token2.1.2、调用其它接口2.1.2.1、携带 CookieToken2.1.2.2、携带 U…

C人脸识别

1、原始图片&#xff1a; 2、灰度化下&#xff1a; 3、均值滤波&#xff1a; 4、 二值图加边缘检测 5、生成积分图 6、把待检测的人脸区域划分为25个&#xff0c;因为是一个数组&#xff0c;这样分别统计每个区域的像素个数&#xff1a; x0: 60, y0: 100, x1: 157, y1: 200 …

介绍AI绘画课,让智能工具助力创作 释放无限想象力 助你成为绘画大师

演示地址&#xff1a; www.runruncode.com/portal/article/index/id/19458/cid/81.html 画画是一项有趣的活动&#xff0c;它让人充满无限可能。对许多人来说&#xff0c;画画既是一种放松的方式&#xff0c;也是一种与创意、文化和艺术联系的途径。如果你是一个初学者&#x…

云原生——Docker容器化实战

❄️作者介绍&#xff1a;奇妙的大歪❄️ &#x1f380;个人名言&#xff1a;但行前路&#xff0c;不负韶华&#xff01;&#x1f380; &#x1f43d;个人简介&#xff1a;云计算网络运维专业人员&#x1f43d; 前言 "Docker"一词指代了多个概念&#xff0c;包括开源…

uniapp实现聊天消息触,vue3和vue2实现聊天消息触底 scrollTop ,scrollHeight Pc端H5端都适用

uniapp触底SDN链接如下(本人的另一篇博客) uniapp聊天时时触底链接 Pc端 模拟手机端H5 vue3写法 <template><div><!-- 聊天窗体 --><div class"test" id"gundong"><div class"text" v-for"p in chat"&…