2026年2月

简介

最近看到飞牛 NAS爆出的漏洞,路径遍历漏洞,NAS 只要放在 WAF 后面其实不会受到漏洞影响的。于是特来分享这篇 WAF 部署教程。

该文适合有一台小主机,性能还可以,有ipv4/6公网地址,并设置了DDNS。或者有公网 ip的用户

相信大家跟我一样有all in one小主机,家里也部署了几套应用(群晖 NAS飞牛 NASemby等),害怕哪天被黑客入侵了,丢失了数据,损失了时间。

网络拓扑

本次部署的 WAF 是使用的长亭家的雷池 WAF,官方推荐使用 docker 安装,所以想使用 WAF 先安装 docker,docker 安装教程请自行百度

下面是我家里的网络拓扑,如果有类似或者一样的可以抄作业了。我的all in one的配置是ESXI+OpenWrt+Windows10+Linux,底层使用的ESXI 系统作为虚拟机,大家也可以使用PVE,这个是不影响的,也不是本次的重点,重点是安装docker的那台Linux
image

部署长亭 WAF

WAF 是 Web Application Firewall 的缩写,也被称为 Web 应用防火墙。区别于传统防火墙,WAF 工作在应用层,对基于 HTTP/HTTPS 协议的 Web 系统有着更好的防护效果,使其免于受到黑客的攻击。
注:WAF 并不能对非 HTTP 协议的流量进行防护哦,例如 SSH、FTP 等

也就是说如果我们的部署了 web 服务就可以使用 WAF 来拦截非法请求,保护我们的系统,WAF 官网地址是 doge_flower

准备阶段

Linux 镜像,这里使用的是Debian 系统
爱折腾的你和你的小手
安装 Linux 系统步骤省略,如有不会的可以自行百度即可,配置这块最好是给 2C4G,40G

WAF 安装

安装命令,需使用 root 权限安装,这里我的 Linux 的 ip 为10.10.10.164,这台机器只是用户演示怎么安装流程,后续会用我已经使用的机器去配置如何设置 WAF。
bash -c "$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/manager.sh)"

image (1)
当看到这个的时候雷池就部署好了
image (3)
访问 https://10.10.10.164:9443 端口进行登录访问
image (2)
登录后的页面是这样的,后面我就用我现在使用的雷池,用于配置防护
image (5)

WAF 站点防护

在配置站点防护前,请大家阅读长亭 waf 的配置手册,以下是我的配置流程,如果你们的设置和我一样,可以进行抄作业,如果不一样,请大家结合 配置手册 ,进行配置。
在没有部署 waf 之前我们想要通过公网访问我们内网的应用需要做端口映射(转发),其网络拓扑是这样的

image (6)
需要在路由器上配置 10.10.10.198 的 5000 端口,进行端口映射(转发),才能访问到我们内网的群晖系统。
注:每个家用路由器配置页面都不一样,请根据自己的路由器型号自行配置,其原理都是一致的。

image (7)
现在我们给群晖配置 WAF,其工作流程就是先把流量引到 WAF 上检测,然后 WAF 再用 Nginx 将流量转发到群晖上。其网络拓扑如下:

image (8)

开始配置

1.了解工作流程后我们来配置 WAF ,首先上传我们的 SSL 证书,不上传证书也可以正常使用,证书上传不是必须。防护站点-证书管理-添加证书

image (9)

2.添加我们需要防护的站点,比如添加我们的群晖 NAS。防护站点-站点管理-添加站点

image (10)

上游服务器填写真实群晖 NAS 的访问地址,格式为http://ip+端口或者https://ip+端口
域名填写你的域名即可,这里的端口我们需要记录下来,因为群晖默认端口是 5000,这里我们也使用 5000
如果勾选 SSL 的话,就会出现中间下拉框选中证书,选择你上传后的证书即可。
这样我们就部署好了一个 web 应用的 waf 防护站点。接下来我们还需要设置端口映射(转发)

端口转发更换

设置端口转发,在上面我们说了,没有配置 waf 时,我们只需要直接填写 NAS 的地址+ip 就可以直接访问,现在我们部署了 waf,需要更改配置。
只需要更改内网地址为 WAF 的地址即可。其中内部端口要和 WAF 中配置的端口要一致,因为 WAF 上我们配置时使用的是 5000 端口,所以这里我们也是要设置 5000 端口,如果上面设置的是 6000 端口,这里内部端口就要设置为 6000。外部端口可以随便设置,这个端口就是你域名+这个端口,就可以访问到群晖 NAS。
注:每个家用路由器配置页面都不一样,请根据自己的路由器型号自行配置,其原理都是一致的。

image (11)

WAF 防护测试

现在我们已经配置好了 WAF,我们就去试试有没有效果,在设置之前,我们需要更改一些 WAF 的配置,推荐配置如下,如果不配置的话,WAF 会经常拦截我们正常的应用,导致无法使用。
防护配置-频率设置

image (12)

访问到这个页面就说明 WAF 已经部署成功,Ps.在部署的时候勾选了 SSL 证书,这里我们也是使用 https 访问的,因为 WAF 的作用类似 Nginx,我们把证书放在 Nginx 上,流量经过 Nginx 就会进行 SSL 证书校验,这样会让我们的 NAS 更加安全。

image (13)

查看 WAF 面板,可以看到流量请求。

image (14)

我们测试 WAF 的拦截效果,我们使用 https://xxxxxxx:5000/?id=1 and 1=1 #这个语句进行测试,看到下面这个页面说明 WAF 进行了拦截。

image (15)

在 WAF 面板上可以看到攻击状态,因为我在内网进行测试,如果所以攻击 ip 显示的是我网关的地址。

image (16)

这样我们的 WAF 部署就结束了,可以开心的玩耍了,再也不用担心应用被人干了还不知道了。

WAF 总结

这个 WAF 也不是那么完美,因为是免费的所以很多功能受限,比如日志功能等,但是他完全可以给我们的应用提供第一道防护,这次的飞牛 NAS 的 0Day 漏洞是完全可以防护的。

1346101770080608_.pic

在人工智能发展的早期阶段,AI 往往以“创新项目”“试点工程”或“专项研发”的形式存在于企业内部。它通常被视为一种附加能力,用于优化某个局部流程或解决特定问题。

进入 2026 年,这一形态正在系统性消失。

随着算力结构的持续优化、预训练模型泛化能力的显著提升,以及部署与治理体系的标准化成熟,AI 已完成从“技术插件”向“通用基础设施”的转变。它不再以独立项目的方式被单独管理,而是作为系统默认能力,嵌入到业务架构的底层逻辑之中。

这并不意味着 AI 的重要性下降,恰恰相反,这一变化标志着 AI 正式进入生产力稳定释放阶段。

一、从项目制到底座化:AI 角色的根本转移

项目制 AI 的典型特征,是围绕单一功能构建模型与数据闭环。每一个应用场景都需要单独立项、单独训练、单独评估,其生命周期往往与具体业务模块高度绑定,难以跨系统复用。

而底座化 AI 的核心特征,在于其先于业务存在

在这一模式下,大模型或高度集成的智能组件被视为系统的逻辑基座。AI 不再是后期引入的“功能增强”,而是系统启动时即默认具备的认知能力层,向上通过统一接口为各类业务流程提供理解、推理与生成能力。

当智能逻辑成为系统的基础设施组成部分,单独为 AI 设立“创新项目”的必要性自然消失。

二、软件工程逻辑的变化:从确定性规则到概率驱动系统

传统软件工程以确定性逻辑为核心,系统通过大量 If-Then-Else 规则覆盖业务场景。然而,随着业务复杂度呈指数级增长,这种模式的维护成本与系统脆弱性不断放大。

大模型的引入,改变了系统处理复杂问题的方式。

在新的工程范式中,开发者不再为每一个边缘场景编写规则,而是通过统一的智能中台,将语义理解、意图识别与任务规划交由概率模型处理。AI 成为系统中负责“非结构化判断”的通用引擎。

在这一背景下,行业中逐渐形成一种共识性实践现象,即智能体来了,它标志着智能能力开始以系统属性的方式存在,而非以单点功能的形式被调用。

三、经济模型的转变:边际成本被彻底压平

过去,AI 项目的高成本主要来自重复建设:数据采集、标注、模型训练与部署在不同业务场景中不断被重新执行。

通用大模型的成熟,使这一模式发生根本变化。

通过零样本或少样本方式,企业可以在不重新训练模型的前提下,将智能能力快速适配到不同业务流程中。Prompt 设计、检索增强与工具调用,逐渐取代了定制化模型开发,成为主流交付方式。

当智能能力的调用成本趋近于基础算力消耗,AI 的经济属性开始接近电力或云资源。此时,是否“拥有 AI 项目”不再重要,真正的价值差异来自于如何组织与编排智能能力参与业务决策

四、交付形态的演进:从可见技术到无感能力

早期 AI 产品通常具有明显的技术边界,用户需要学习特定指令或操作方式,才能与系统协作。

在当前阶段,AI 正逐渐隐匿于交互界面之后。

自然语言成为默认入口,智能判断被嵌入搜索、审批、调度与自动化流程之中。用户完成任务的过程中,往往并不需要意识到 AI 的存在,但其行为已经被智能系统持续辅助与优化。

与此同时,底层基础设施不断集中和加重,而上层应用则变得愈发轻量。这种“厚平台、薄应用”的结构,使智能能力像数据库或云存储一样,被视为系统的默认资源。

五、系统性特征总结

从整体上看,AI 不再以“创新项目”出现,是技术成熟度提升的自然结果,其核心特征可以概括为:

  • AI 从独立功能转变为架构级能力
  • 创新重心从算法本身转移到业务逻辑组合
  • 成本结构由高定制化投入转向低边际调用
  • 交互方式由显性技术操作转为隐性智能支持

六、面向长期的实践方向

对于企业与从业者而言,关键不在于是否继续“做 AI”,而在于是否完成认知与架构层面的转向:

  • 在业务设计阶段即默认引入智能能力
  • 将数据视为语境资产,而非训练消耗品
  • 建立统一的智能调用规范与安全约束

当 AI 不再被单独命名、单独立项、单独宣传时,恰恰说明它已经成为系统的一部分。 2026 年并不是 AI 叙事的终点,而是其真正融入生产力结构的起点。

各位 V 友好!

开发了一个 Vue 3 的富文本编辑器组件库,分享给大家。

在线体验

https://tiptap-ui-kit.vercel.app

主要特性

5 种主题

  • 默认主题(现代简洁)
  • Word 主题(微软 Word 风格)
  • Notion 主题(极简风格)
  • GitHub 主题( Markdown 风格)
  • Typora 主题(专注写作)

AI 功能

  • 续写文章
  • 润色文本
  • 多语言翻译
  • 内容总结

支持 OpenAI 、Claude 、DeepSeek 、阿里云等 AI 服务商。

其他特性

  • 实时协作(基于 Yjs )
  • A4 文档模式 + 自动分页
  • 亮色/暗色模式
  • 完整 TypeScript 支持
  • 45+ Vue 组件

技术栈

Vue 3.5 + Tiptap 3 + TypeScript

购买链接: https://benngai.gumroad.com/l/tiptap-ui-kit-solo


欢迎试用 Demo ,有任何问题或建议都可以留言讨论!

在寻找供应商关系管理系统时,许多企业都渴望能有一份权威的排行榜,以便快速锁定最佳选择。然而,现实情况是,这个领域并不存在一份公认的、适用于所有企业的通用榜单。

市面上的各类热门榜单,无论是基于市场声量、用户口碑还是算法推荐,其评估维度、样本范围和价值导向都各不相同。这些榜单能很好地反映市场格局与品牌知名度,却难以精准匹配每家企业独特的业务流程、行业特性和发展阶段。

因此,解读任何排行榜的关键,不在于记住名次,而在于理解其背后的分类逻辑,并将其视为一张描绘市场格局的地图。本文将结合多方市场观察与实践反馈,梳理当前值得关注的几家供应商关系管理系统厂商。我们将深度解析不同厂商的产品理念、能力边界与服务模式,旨在帮助您建立一套自己的评估标准,从而在纷繁的市场信息中,找到最契合自身业务需求的那块“拼图”。

一、正远科技SRM

作为一家数智化解决方案提供商,正远科技主营业务涵盖IT咨询与规划、流程咨询与规划、AI开发、管理软件及解决方案定制开发、BPM/SRM/RPA/LCDP/BI产品实施服务等领域,为用户提供管家式、个性化的解决方案及实施服务。立足智能化浪潮前沿,正远科技以客户价值创造为锚点,研发AI开发平台,为客户在Al时代的运营管理升级筑起新的基石。

正远SRM系统是一款以流程为驱动的企业级业务协同平台。该系统通过标准化服务接口和松耦合架构,实现了企业内部及与供应商之间业务流程的高效整合与灵活扩展,致力于优化供应商全生命周期管理,提升供应链透明度

和协同效率,助力企业实现采购管理的数智化转型升级。正远SRM系统采用双门户设计,分别面向企业内部用户和外部供应商,确保业务流、信息流和数据流的高效协同。

核心功能与好处:正远SRM系统的核心业务流程涵盖了从供应商准入到财务结结算的全链条数字化协同管理。其最大好处在于极强的业务灵活性,企业可以通过可视化配置快速适应组织变革或独特的采购政策,避免因系统僵化导致的二次开发或推倒重来。

竞对差异:相较于标准化、套装化的国际软件(如SAP),正远科技更擅长处理中国本土企业,特别是制造业中非标准、动态变化的复杂业务流程。相较于一些侧重轻量协同的工具型SaaS,它又能提供企业级的安全、集成和深度管控能力。

二、鲸采云SRM

鲸采云是近年来在SRM市场上表现突出的专业厂商,以其新一代灵活可配置的SRM系统为核心,致力于为企业提供全场景、全链路的数字化采购解决方案。它被视为 “全场景数字化标杆” 的代表,其市场地位建立在对企业多样化需求的深度适配和对效率提升的极致追求上,在多个行业标杆案例中取得了显著成效。

产品特色与服务

鲸采云SRM的核心竞争力在于高度的灵活性与深度的AI赋能。它旨在打破传统SRM系统功能固化的局面,让系统主动适配企业业务,而非相反。

核心功能与好处:产品提供供应商全生命周期管理、智慧寻源、采购商城、订单与财务协同等模块。其突出优势是内置了基于大语言模型的 “鲸采云AI” ,能实现智能填单、合同生成与审查、基准价推荐等功能,将数日工作压缩至几分钟。同时,其国际版全面支持多语言、多币种与多时区协同,适合有全球化业务的企业。

竞对差异:与许多传统SRM产品相比,鲸采云在人工智能与采购场景的深度融合上走在前列,不仅仅是流程自动化,更追求智能化决策。其开放的生态体系(内置超150种标准插件)也使其在与其他系统集成时更具优势,这是部分封闭或集成能力弱的系统所不具备的。

三、轻流

轻流是国内领先的无代码业务流程管理(BPM)平台提供商,以其灵活的可视化流程搭建能力而闻名。其市场定位是赋能业务人员快速构建各类管理系统,在SRM领域,它提供的是轻量、灵活、可快速定制的采购与供应商协同解决方案,尤其适合流程变化快、需要快速上线或对标准化套装软件感到束缚的中小企业和创新部门。

轻流的核心优势在于其强大的无代码流程引擎和高度可配置性,允许企业像搭积木一样,自定义搭建符合自身独特管理需求的SRM应用。

核心功能与好处:通过轻流,企业可以快速搭建供应商信息库、采购申请与审批流、询价比价流程、订单跟踪、库存管理等模块。其最大好处是极致的敏捷性:当采购政策或组织架构调整时,业务管理员可以自行调整流程和表单,无需依赖IT开发,实现“业务驱动IT”。

竞对差异:与正远科技、8Manage等专业的、功能预设完整的SRM系统相比,轻流更像一个 “SRM应用构建工具” 。它不提供开箱即用的、深度的战略寻源或复杂成本分析模块,但其在应对个性化、非标流程以及快速原型验证方面具有无可比拟的速度优势。对于流程尚不标准化或希望低成本试错的企业,它是理想起点。

四、8Manage SRM

8Manage SRM由高亚科技开发,是一家专注于提供端到端采购与供应商管理解决方案的专业厂商。在市场中,8Manage SRM以其模块化设计、功能全面和灵活部署的特点,服务于从成长型企业到大型集团的多样化客户,在制造、建筑等行业积累了众多知名客户案例。

8Manage SRM定位为一款综合性、一体化的采购管理平台,强调通过数据驱动决策和流程自动化来提升采购透明度与效率。

核心功能与好处:系统提供从采购需求、寻源招投标、供应商管理、合同管理到付款结算的完整闭环管理。其电子招投标和竞价模块支持灵活的规则配置,是其特色功能。系统支持多种部署方式(SaaS及私有化),并具备多语言支持能力,适应性较广。

竞对差异:与深度绑定在大型ERP生态内的SRM模块(如用友、金蝶面向大型企业的产品)相比,8Manage SRM作为独立专业产品,在功能深度与系统独立性之间取得了较好平衡,避免了企业被单一巨头生态锁定的风险。同时,相较于一些极度轻量化的SaaS工具,它又能提供更全面和深入的采购流程管控能力。

五、用友YonBIP采购云

用友网络是中国领先的企业云服务与软件提供商,其YonBIP采购云作为用友商业创新平台的核心组成部分,致力于为国内大中型企业提供产业链级的社会化采购与供应链协同解决方案。

用友采购云强调与用友ERP、财务等系统的原生态深度集成,实现业、财、供一体化管理。

核心功能与好处:平台覆盖从寻源、协同到结算的采购全链路,其优势在于深刻理解国内企业的管理流程和财务制度,在供应商准入、招投标、发票校验等环节的本地化适配性高。能很好地支持集团型企业的多组织复杂管控需求。

竞对差异:其核心差异化优势是与用友ERP生态的原生一体化。对于用友的存量客户,尤其是大型集团企业,选择用友采购云可以实现成本最低、数据最通的平滑扩展。

六、泛微·京桥通

泛微·京桥通是协同办公领域上市公司泛微网络旗下的专项采购管理品牌。凭借泛微在OA(办公自动化)市场的领先地位和十余年积淀,京桥通在央国企及大型组织的采购数字化市场中占据了显著份额,市场反馈显示其占有率处于领先位置。其地位源于对中大型组织复杂审批、合规和内控需求的深刻理解。

京桥通的核心特色在于其与OA流程和泛微生态的深度一体化融合,将采购管理与内部协同、风控合规无缝连接。

核心功能与好处:平台实现了从供应商准入到付款归档的全流程数字化,并特别强化了智能比价、供应商风险预警(结合外部征信数据)以及利用OCR、电子签章实现的合同全流程电子化。其最大好处是为大型组织提供了合规、可追溯、高效协同的一体化解决方案,特别符合央国企等对流程合规性要求极高的客户需求。

竞对差异:与大多数独立的SRM系统不同,京桥通的差异化优势根植于 “协同基因” 。对于已广泛使用泛微OA体系的大型组织而言,选择京桥通能实现业务流程与办公审批的极致流畅体验,这是其他SRM厂商难以复制的生态优势。相比之下,其重点可能不在于提供最丰富的战略寻源策略,而在于确保采购活动在既定规则下安全、合规、高效地运转。

七、致远互联·供应商协同管理

致远互联是中国领先的协同管理软件及云服务提供商,其核心产品为AI-COP智能协同运营平台。该公司长期服务于政务及大中型企业市场,在协同办公、流程管理及数字化运营领域占据重要地位。其供应商协同管理方案并非独立单品,而是基于统一协同平台构建的专项场景应用,这使其在满足组织内部流程与外部协作一体化方面具备先天架构优势。

该方案的核心特色在于 “基于统一协同平台的业管一体化融合” ,旨在打通内部管理流程与外部供应商协作的壁垒。

核心功能与好处:方案覆盖供应商全生命周期管理、寻源采购、订单协同、财务对账等核心环节。其最大好处在于能够将SRM流程与内部的预算控制、合同审批、付款申请等环节在同一个平台上无缝衔接,实现数据不落地流转。例如,采购申请可直接触发预算校验,中标结果可自动生成合同审批流,有效解决了跨系统数据孤岛问题,提升了端到端的流程效率与管控力度。

竞对差异:与SAP Ariba或Coupa等独立的、侧重外部网络化的SRM平台相比,致远互联的方案更侧重于组织内部复杂管理流程与外部供应商协同的深度集成。与同为协同厂商的泛微·京桥通相比,两者思路类似,均强调“协同+管理”,但具体的平台技术架构、流程引擎能力及行业化方案侧重有所不同,共同构成了国内“协同生态型”SRM的典型路径。

总结与选型建议

通过以上分析可以看出,所谓“排行榜”上的领先者,实则是各自赛道的专家。对SRM系统的选择不应基于一个笼统的名次,而应始于一次清晰的自我审视:

明确定位与核心诉求:首先问自己,企业是大型集团/跨国企业快速成长的中型企业,还是预算有限的初创/小微型企业?核心诉求是满足全球合规与复杂集成优化深度制造协同实现业财供一体化,还是仅仅快速解决供应商在线协同的燃眉之急?

识别关键差异化能力:接着,将您的核心诉求与厂商的差异化能力对齐。若追求业务的高度灵活与深度定制,正远科技的“双轮驱动”架构值得探究;若青睐AI深度赋能与全场景智能,可重点考察鲸采云;若急需低成本、快上线的轻量化工具,金蝶AI星辰是典型代表;若需要独立且全面的端到端管理,8Manage SRM是可靠选项;若处于强合规、重流程的泛微OA生态内,京桥通则是自然延伸。

超越榜单的实践验证:最后,请记住榜单仅是信息入口。建议基于以上分析,筛选出2-3家最契合的供应商,推动一场深入的 “概念验证(PoC)” 。围绕企业最核心、最复杂的1-2个采购场景进行实际搭建与测试,亲身感受系统的灵活性、易用性以及供应商的响应速度与服务深度。唯有通过实践验证的方案,才是对企业而言真正的“榜首”之选。

前言

大家好,我是拖更博主r0leG3n7。本文将简单介绍frp这款隧道代理工具的项目结构和代码运行流程以及如何通过对frp二次开发(后面简称"二开")来消除其静态特征和流量特征从而规避杀软以及EDR的检测。如有任何错误和不足欢迎各位师傅指正,转载请注明文章出处。

frp项目分析过程

致敬伟大的原项目:https://github.com/fatedier/frp,我选择的是较新版本0.65.0的frp。(我写这篇文章的时候frp刚刚更新到0.66.0,但应该不影响我当时二开的就是最新版的frp[狗头])。

先问大家一个问题,如果我需要对某个开源项目进行二开,我有必要把这个开源项目里所有的代码结构都分析得明明白白的嘛?那当然是没那个必要,对于这种大型的开源项目,我们需要很明确自己想要把这个项目改成什么样子,确定自己的需求,确定项目有哪些对于你来说是"缺陷"的地方,这样就不会在庞大的代码海洋里迷失自己。

需求分析

1、首先来到我们软件生命周期最重要的需求分析阶段,我的大致的需求就是要改frp的静态特征和流量特征来绕过EDR检测达到免杀的效果,确定大致的需求以后我需要知道frp有哪些特征。

原版的frp有如下几个典型特征:

1)frp的服务端和客户端启动时都会默认读取同目录名为frpc.ini或者frps.ini的配置文件。

2)frp的客户端与服务端发起TCP连接时会发送诸如版本号、架构、token、run id等信息进行登录认证。

3)frp的客户端与服务端在连接成功或者失败时都会在控制台输出一些debug信息或者提示信息。

4)frp的客户端与服务端在TCP连接建立后的第一个应用层数据包会发送一个自定义的字节,这个字节的值为0x17。

5)frp的客户端与服务端在TCP连接建立成功后,服务端可以通过对某些API接口发起get请求、post请求或者put请求去控制客户端,比如/api/reload、/api/stop等。

2、确定大致的需求并且定位"缺陷"以后,我要明确我的需求,明确我要把它改成什么样子。

我明确的需求:

1)对于frp的服务端和客户端在本地读取配置文件的行为,我可以把配置文件信息想象成shellcode,按照loader加载shellcode那样处理。我想到的是将配置文件硬编码在程序里面;或者将配置文件加密后通过命令行传入frp,frp客户端与服务端尝试建立连接时再进行解密;或者通过远程URL加载;还有最重要的是去除通过文件路径读取配置文件的功能。

2)对于frp的服务端和客户端建立连接失败时的输出的错误信息,我要进行删除或者修改;对于建立连接成功时发送的登录信息或者代理信息,我要进行TLS加密或者将默认变量名、键值对修改;对于TLS建立连接的默认自定义字节以及服务端控制客户端的api默认接口名也是一样地做修改处理。

项目分析

1、程序所需的依赖写在了项目的go.mod文件中,在GoLand的IDEA可以按"Alt键+回车键"自动下载对应的依赖。

2、项目的Makefile是编译命令文件,在这里可以找到服务端代码入口/cmd/frps以及客户端的代码入口/cmd/frpc。

3、我们可以直接定位到客户端/cmd/frpc/mian.go,编译时会自动搜索该目录下的mian.go文件作为编译的入口,重点关注sub.Execute()。

4、按alt跟进sub.Execute(),它的主要功能是rootCmd.Execute()这行 。

5、按alt跟进rootCmd.Execute(),rootCmd.Execute()会执行rootCmd中的RunE,RunE中包含两个关键的函数runMultipleClients()和runClient(),这两个函数主要的功能就是加载配置文件然后建立与服务端的链接。我们主要看runClient()函数,一般情况下一个frpc客户端只加载一个配置文件,所以不用怎么去考虑改runMultipleClients(),我二开的时候索性直接删掉了命令行配置文件路径的输入。runClient()传入一个名为cfgFile的全局变量,它是frp客户加载配置文件的路径。

6、纵观整个rootCmd.Execute()过程,我们都没有看到给cfgFile全局变量赋值的地方。但是我们知道原版的frpc客户端是从命令行输入配置文件路径的,我们可以从包的init()函数看到程序是怎么从命令行中获取用户输入的配置文件路径赋值cfgFile变量的。init()函数是 Go 语言中的一个特殊函数,通常用于资源、包和变量的初始化。它的特点是每个包的 init() 在程序运行期间只执行一次;init()无需手动调用,会在main()之前自动执行,导入包的 init() 先于当前包的 init() 执行。

7、回到rootCmd.Execute(),按alt跟进runClient()函数,我们来到了这次二开中最重要的函数config.LoadClientConfig(),它是我们修改配置文件传参关键。从返回值我们可以知道,它会返回配置文件的基本配置信息、代理配置信息、配置文件格式等。config.LoadClientConfig()的传入参数是配置文件路径,这个函数是需要完全改写的,我上面的需求已经说的很明确了,我会从硬编码、远程URL输入或者命令行输入去读取配置文件,不会有从文件路径读取配置文件的行为,减少文件落地。

8、虽然说要完全改写config.LoadClientConfig(),但是我们还是要按alt跟进看一下它的内部逻辑以便我们更精确无误地对它进行修改,config.LoadClientConfig()存在读取并转换配置文件的legacy.ParseClientConfig()方法。

9、按alt跟进legacy.ParseClientConfig(),legacy.ParseClientConfig()函数通过文件读取函数GetRenderedConfFromFile()以及传入的文件路径来读取配置文件信息并将其赋值content变量,然后将content的类型转化为字节数组后将其作为参数传给UnmarshalClientConfFromIni()方法,UnmarshalClientConfFromIni()将转换后的基础配置文件信息赋值给cfg。

10、同样地,legacy.ParseClientConfig()通过legacy.LoadAllProxyConfsFromIni(),将转换后的代理配置文件信息等赋值给变量proxyCfgs和visitorCfgs。这时候我们知道配置文件信息主要是靠UnmarshalClientConfFromIni()和LoadAllProxyConfsFromIni()两个函数进行转换的,到时候我们二开的时候就照着这两个函数简单修改一下就行了。

11、了解完它是怎么读取并转化配置文件信息后,我们再回到上面的runClient()函数,再大致了解一下它是怎么通过startService方法以及转化后的配置文件信息启动服务的,这里注意startService方法第五个参数cfgFile为配置文件路径,到时候服务端调用/api/reload接口重新加载配置文件时候会用到。因为我二开时将通过文件路径读取配置文件信息这个行为删除了,这个参数到时候会变成空值,这个参数置空以后服务端调用该接口可能会报错。

12、service.go的NewService创建服务对象方法。

13、service.go的Run运行服务对象方法。

frp项目二开过程

本节我将介绍如何对frp原项目进行二开改造隐藏其静态特征和流量特征,包括修改传参方式,修改frp默认输出,修改frp静态字符串,修改frp的TLS流量特征等。相信看过四大名著《三国演义》的都知道赵云在长坂坡七进七出,单骑救主的故事,我第一次了解到这个故事的时候我就觉得不可思议,真的有人能从这么多的魏军人马中带着个婴儿死里逃生吗?在二开了frp之后,我就悟到了。frp客户端就是赵云,配置文件就是阿斗,单骑救主护送阿斗回蜀就是frp客户端与服务端建立连接的通信过程。赵云之所以会被在茫茫人海中被魏军检测到,并不只是因为他喊了那句"我乃常山赵子龙",更多的是因为他有对阿斗进行明目张胆地"取餐"这个行为,不过好在他能及时调整,将阿斗硬编码到自己的怀里,才做到了七进七出。我觉得单骑救主这个故事可以有更多opsec的改进方案让他变得更加合理更加地叫人信服,至于怎么改,请看下面听我娓娓道来。

传参方式

传参方式的修改在上面需求的第一条已经提出来了,我的最终方案是去除通过文件路径读取配置文件的部分;如果frp收到命令行传入的加密配置文件,就解密该配置文件进行连接;如果读取不到命令行传入的加密配置文件,就读取硬编码的配置文件进行连接。

1、首先去除init()函数中接收对配置文件路径的输入,新增一个全局变量eStr,用于接收用户控制台输入的加密后的配置文件信息,使用示例"-e <加密的配置文件信息>"。

rootCmd.PersistentFlags().StringVarP()方法参数说明:

第一个参数为接收控制台输入的指针

第二个参数为参数名称

第三个参数为传入参数的简写,比如"-c ./frpc.ini"

第四个参数为参数的默认值(StringVarP就必须为字符串类型,BoolVarP就必须为布尔类型,以此类推)

第五个参数为参数介绍说明

2、修改rootCmd.Execute()逻辑,当eStr变量不为空(也就说收到来自用户在命令行输入的加密配置文件内容),就对传入的加密配置文件内容进行解密,将它解密后的明文传给一个自定义的cfgContent变量;如果eStr变量为空,就将硬编码的配置文件信息传给cfgContent变量。cfgContent变量最终会作为参数传给修改后的runClient()函数。

3、修改runClient()函数运行逻辑,之前runClient传入第一个参数是配置文件路径,我现在将这个参数改成配置文件内容,到时候硬编码的配置文件或者解密后的配置文件可以直接作为参数调用这个函数,修改的地方主要是config.LoadClientConfig()这个部分,将其修改为了一个新的函数config.LoadClientConfigFromContent(),用于接收传入的配置文件内容并将其转换。

4、config.LoadClientConfigFromContent()第一个传入参数为配置文件内容的字符串,返回值与之前一致。

func LoadClientConfigFromContent(content string, strict bool) (
    *v1.ClientCommonConfig,
    []v1.ProxyConfigurer,
    []v1.VisitorConfigurer,
    bool, error,
) {
    var (
        cliCfg         *v1.ClientCommonConfig
        proxyCfgs      = make([]v1.ProxyConfigurer, 0)
        visitorCfgs    = make([]v1.VisitorConfigurer, 0)
        isLegacyFormat bool
    )

    contentBytes := []byte(content)

    // Render template with values
    renderedContent, err := RenderWithTemplate(contentBytes, GetValues())
    if err != nil {
        return nil, nil, nil, false, fmt.Errorf("render template error: %v", err)
    }

    if DetectLegacyINIFormat(renderedContent) {
        // Parse legacy INI format
        legacyCommon, err := legacy.UnmarshalClientConfFromIni(renderedContent)
        if err != nil {
            return nil, nil, nil, true, err
        }

        // Parse all proxy and visitor configs from the same content
        legacyProxyCfgs, legacyVisitorCfgs, err := legacy.LoadAllProxyConfsFromIni(legacyCommon.User, renderedContent, legacyCommon.Start)
        if err != nil {
            return nil, nil, nil, true, err
        }

        cliCfg = legacy.Convert_ClientCommonConf_To_v1(&amp;legacyCommon)
        for _, c := range legacyProxyCfgs {
            proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
        }
        for _, c := range legacyVisitorCfgs {
            visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
        }
        isLegacyFormat = true
    } else {
        allCfg := v1.ClientConfig{}
        if err := LoadConfigure(renderedContent, &amp;allCfg, strict); err != nil {
            return nil, nil, nil, false, err
        }
        cliCfg = &amp;allCfg.ClientCommonConfig
        for _, c := range allCfg.Proxies {
            proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
        }
        for _, c := range allCfg.Visitors {
            visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
        }
    }

    if len(cliCfg.Start) &gt; 0 {
        startSet := sets.New(cliCfg.Start...)
        proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
            return startSet.Has(c.GetBaseConfig().Name)
        })
        visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
            return startSet.Has(c.GetBaseConfig().Name)
        })
    }

    proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
        enabled := c.GetBaseConfig().Enabled
        return enabled == nil || *enabled
    })
    visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
        enabled := c.GetBaseConfig().Enabled
        return enabled == nil || *enabled
    })

    if cliCfg != nil {
        if err := cliCfg.Complete(); err != nil {
            return nil, nil, nil, isLegacyFormat, err
        }
    }
    for _, c := range proxyCfgs {
        c.Complete(cliCfg.User)
    }
    for _, c := range visitorCfgs {
        c.Complete(cliCfg)
    }
    return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
}

5、将config.LoadClientConfigFromContent()返回的cfg, proxyCfgs, visitorCfgs作为参数传给startService(),传参方式这部分就修改完成了。注意startService()的第五个参数为配置文件路径,我修改frp的传参方式以后这个配置文件路径的值就不存在了,所以我把它的值置空了,这个参数在后面调用/api/reload重新加载配置文件时候会用到,如果仍需要调用这个api建议将其修改为一个系统默认路径,不然调用时可能会导致frp客户端异常。

6、运行效果就是先用加密程序将frpc配置文件加密后的字节数组以base64编码的字符串输出。

7、frpc客户端启动时如果接收到-e 传入的Base64编码的字符串,frpc客户端就会解密该字符串并转化配置文件;如果接收不到-e 传入的Base64编码的字符串,frpc客户端就会读取代码内硬编码的配置文件。从这里我们也可以看到frp客户端在连接到服务端时会输出一些诸如"client/service.go:331"的信息,这些都是要做处理的。

流量改造

上面讲frp特征的时候提到frp的客户端与服务端在TCP连接建立后的第一个应用层数据包会发送一个值为0x17的自定义字节,这个是frp在流量层面最显著的特征之一,就好像赵云的武器"龙胆亮银枪"以及坐骑"照夜玉狮子",使得赵云在茫茫魏军人马中被一眼认出。

我们先用wireshark看下frp原味的通信流量,下图是frp的TLS的握手流量,可以从"Client Hello"和"Server Hello"这几个关键字快速定位,点击"Transport Layer Security",我们可以看到这部分的第一个字节是0x16。

再看frp部分的应用流量,点击"Transport Layer Security",我们可以看到这部分的第一个字节是0x17。

再通过搜索0x17和0x16定位到项目中/pkg/util/net/tls.go和/server/server.go,我们看到自定义字节变量FRPTLSHeadByte的值为0x17,以及判断0x16和x017 switch逻辑,似乎就能跟上面wireshark看到的流量特征扯上一些联系?我之前听有些卖frp免杀课的人说修改了这个变量FRPTLSHeadByte(旧版的frp好像是另一个变量名)的值,就能消除上面wireshark看到的frp的0x17流量特征。

事实真的是这样的吗?其实他们只说对了一半,修改变量FRPTLSHeadByte确实能消除frp一部分的流量特征。但无论你怎么改FRPTLSHeadByte的值,如果像上面那样看frp的TLS的握手流量和frp的应用流量,无论你怎么改,看到的还是0x17。因为上面看到的那部分就是TLS的正常流量,TLS记录协议头固定为5字节,第一个字节0x17代表应用数据,如果是0x16代表TLS握手数据;第二第三个字节代表TLS版本;第四第五个字节代表数据长度。

他们错误地把FRP自定义字节理解为TLS记录协议头固定字节的第一个字节,因为FRP自定义字节默认值刚刚好就是0x17,和应用数据的TLS记录协议头第一个字节一样。但实际上FRP自定义字节是客户端与服务端在TCP连接建立后的第一个应用层数据包发送的第一个字节,为了方便大家理解,我把FRP自定义字节从默认值0x17改成了0x18,这个字节会在TLS握手之前发送,可以结合下图去理解。

在wireshark内,我们右键frp的tcp流量,选择"追踪流",再选择"TCPStream"

再选择显示为"Hex转储",我们就能看到frp自定义字节修改前的样子:

frp自定义字节修改后的样子:

但其实仅修改这一个字节还是不够,有部分厂商的流量设备已经能自动识别这类单字节修改后的流量,要想在流量层面隐藏得更好需要修改/pkg/util/net/tls.go的CheckAndEnableTLSServerConnWithTimeout()函数,在TLS握手之前多填充几个自定义字节。

在修改完自定义字节以后,需要frpc配置文件中[common]下面添加这一行,不然客户端连接到服务端会报错。

disable_custom_tls_first_byte = false

因为从frp的0.50.0版本开始,frp就将禁用发送自定义字节的默认值设置为true,如果使用了frp自定义字节就需要加上这一行

最后一定要记得服务端frps的配置文件加上强制TLS连接。

tls_only = True

客户端frpc的配置文件也加上使用TLS加密,不然前面做的一切都白费。

tls_enable = true

修改控制台输出

这一步主要是防止赵云有事没事就喊一句"我乃常山赵子龙"。其实就是简单改一些frp默认控制台输出,减少被检测识别的概率。

/client/server.go

/client/control.go

/pkg/auth/token.go

/cmd/frpc/sub/root.go

其他frp特征

默认字符串

/pkg/msg/msg.go中有frp客户端与服务端通信时的登录信息以及代理信息,在使用了TLS加密以后这个不修改其实也不会有很大影响,但是改了总比没改会好点。

默认盐值

client/service.go和server/service.go存在默认盐值crypto.DefaultSalt,frp身份认证过程不是直接交换密码或者token,而是进行带盐值的hash计算和比较,不修改默认盐值可能存在被爆破的风险。

配置文件验证

/cmd/frpc/sub/verify.go

这里也有一个包含校验读取配置文件方法的go文件,它也会有个读取本地文件的行为,这里可以直接删除掉/cmd/frpc/sub/verify.go,对编译和运行没影响,不删除反而在读取不到本地配置文件时会输出一些frp的特征,还能缩小编译后的文件体积。

版本信息

/pkg/util/version/version.go中存在frp的版本信息

api

/cmd/frpc/sub/admin.go

1、frp服务端提供了三个api接口控制frp客户端,这三个接口的功能分别是重新加载配置文件、查看代理状态、停止frp客户端运行。

2、重新加载配置文件接口会读取frp首次与客户端连接时输入的配置文件路径。

3、svr.configFilePath的来源于startService()方法传入的第五个参数,这部分要在runClient方法内修改。

/client/admin_api.go

混淆与编译

市面上常用的go语言工具混淆有三种,分别是:go-strip、cross-file-obfuscator和garble。附上项目地址:

https://github.com/boy-hack/go-strip

https://github.com/burrowers/garble

https://github.com/masterqiu01/cross-file-obfuscator

大家现在千万别用go-strip去混淆,go-strip前两年生存环境还可以,现在的杀软很容易识别到go-strip,并且只要是go-strip混淆就判定为恶意软件。我现在使用的最多的是garble,它能混淆一些类名以及字符串,并且能较好地压缩文件体积,garble需要go语言1.25.0以上的版本才能使用,下面简单介绍一下Kali怎么安装1.25.0以上的go语言以及garble。

go安装

1、下载解压go

wget https://mirrors.aliyun.com/golang/go1.25.4.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.25.4.linux-amd64.tar.gz
nano ~/.zshrc

2、配置环境变量,~/.zshrc末尾追加如下

export GOROOT=/usr/local/go
export GOPATH=$HOME/go  # 推荐设置工作目录
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

3、使配置生效

source ~/.zshrc

garble安装

export GOPROXY=https://goproxy.io
go install mvdan.cc/garble@latest

编译

1、在kali使用garble编译前建议先使用原生的go编译器编译一次,主要是让其下载对应的依赖

go build -o ./frpc.exe ./cmd/frpc

2、然后再用garble编译(在kali上编译需要指定目标架构,我这里编译的是windows的amd64)

export GOOS=windows
export GOARCH=amd64
garble  build -o ./frpc-1.exe ./cmd/frpc

3、garble压缩效果,原生的go编译器编译出来的大小为23Mb左右,garble编译出来的为18Mb左右

4、garble的混淆效果如下:

原生的go编译器

garble

5、如果kali不想指定目标架构,也可以使用如下命令跨平台编译

make -f Makefile.cross-compiles

免杀效果

客户端在卡巴斯基和核晶环境下通过frp代理做端口扫描能稳定运行

在没有签名没有做反沙箱状态下上传至VT,只有ESET-NOD32识别出来了是frp客户端

总结

frp在做完传参方式修改、流量改造以及garble混淆后基本就能bypass大部分杀软了。传参方式修改是必须做的,它不仅可以减少文件落地,更是一种针对frp特定的反沙箱操作,某些沙箱提供带有特定文件(比如frpc.ini和frps.ini)的环境来针对性地检测frp。在此基础再加上一些比如延迟执行等反沙箱操作就更好了,如果能把frp做成BOF插件从内存加载那就更opsec了。流量改造方面除了修改frp自定义字符,frp客户端与服务端连接时尽量以域名进行连接,不要直接暴露原生IP,服务端的域名要做cdn防护或者使用云平台PaaS进行转发,然后配置好自定义的TLS证书(最好TLS证书也像配置文件那样内嵌到程序里面,减少文件落地),这样能减少被溯源的概率。静态特征方面,除了garble混淆以外,如果在确定了是目标环境是什么杀软以后,可以考虑使用UPX对frp进行加壳,某些杀软对UPX并不敏感,加壳可以解决百分之90的静态特征问题,但是加壳以后UPX也成为了它的静态特征,不过UPX的静态特征也是可以处理的,其他静态特征处理可以翻看我公众号之前的文章,此处就不再赘述了。除了以上这些二开的点,我们还可以把frp做成系统服务进行权限维持、添加ICMP隧道类型等。

frp的优点就是稳定,但是缺点也很明显,当你需要搭建二级代理或者三级代理的时候,你就不得不把frp二级或三级代理的服务端也上传上去,这意味着你要上传很多个客户端以及服务端,而且对他们都要做免杀处理。隧道代理工具我目前用的比较多的除了frp还有Stowaway,项目地址:https://github.com/ph4ntonn/Stowaway,它也是一个很值得去二开的一个项目,Stowaway的优点是它既能做客户端也能做服务端,往往搭建二级代理或者三级代理上传一个可执行程序就足够了。

题⽬描述

给你⼀根⻓度为 n 的绳⼦,请把绳⼦剪成整数⻓的 m 段( m 、 n 都是整数, n > 1 并且 m >
1 , m <= n ),每段绳⼦的⻓度记为 k[1] ,..., k[m] 。请问 k[1] k[2] ... * k[m] 可能的最⼤乘积是多少?例如,当绳⼦的⻓度是 8 时,我们把它剪成⻓度分别为 2 、3 、3 的三段,此时得到的最⼤乘积是 18 。

由于答案过⼤,请对 998244353 取模。

思路解答

动态规划

自底向上计算最优解

public class Solution {
    private static final int MOD = 998244353;
    
    public int cutRope(int n) {
        if (n < 2) return 0;
        if (n == 2) return 1;
        if (n == 3) return 2;
        
        // dp[i]表示长度为i的绳子剪裁后的最大乘积
        long[] dp = new long[n + 1];
        
        // 基础情况:这些值不是乘积,而是长度本身(因为可以不剪)
        dp[0] = 0;
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        
        // 从长度为4开始计算
        for (int i = 4; i <= n; i++) {
            long max = 0;
            // 遍历所有可能的分割点,j <= i/2 避免重复计算
            for (int j = 1; j <= i / 2; j++) {
                // 比较各种分割方案的乘积
                long product = dp[j] * dp[i - j];
                if (product > max) {
                    max = product;
                }
            }
            dp[i] = max % MOD;
        }
        
        return (int) dp[n];
    }
}
  • 时间复杂度:O(n²),外层循环n-3次,内层循环i/2次
  • 空间复杂度:O(n),需要dp数组存储中间结果

优化动态规划

在上面版本上优化状态转移方程,提高代码效率,直接比较j*(i-j)j*dp[i-j]的最大值

dp[i] = max(max(j × (i-j), j × dp[i-j])) 其中 1 ≤ j < i

  • j × (i-j):剪一刀的情况
  • j × dp[i-j]:剪多刀的情况
public class Solution {
    private static final int MOD = 998244353;
    
    public int cutRope(int n) {
        if (n < 2) return 0;
        if (n == 2) return 1;
        if (n == 3) return 2;
        
        long[] dp = new long[n + 1];
        dp[1] = 1;
        
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                // 三种情况取最大值:不剪、剪一刀、剪多刀
                long temp = Math.max(j * (i - j), j * dp[i - j]);
                dp[i] = Math.max(dp[i], temp);
            }
            dp[i] %= MOD;
        }
        
        return (int) dp[n];
    }
}
  • 时间复杂度:O(n²),双重循环
  • 空间复杂度:O(n),dp数组空间

贪心算法(最优解)

我们仔细观察就会发现:要想乘积⽐较⼤,在没有1的前提下,优先使⽤3,如果出现1,那么优先使⽤2

⽐如:

2 = 1 + 1
3 = 1 + 2
4 = 2 + 2
5 = 2 + 3
6 = 3 + 3
7 = 3 + 2 + 2
8 = 3 + 3 + 2
9 = 3 + 3 + 3
10 = 3 + 3 + 2 + 2
11 = 3 + 3 + 3 + 2
12 = 3 + 3 + 3 + 3
public class Solution {
    public long cutRope(long number) {
        if (number == 2) return 1;
        if (number == 3) return 2;
        long res = 1;
        while (number > 4) {
            res *= 3;
            res = res % 998244353;
            number -= 3;
        }
        return res * number % 998244353;
    }
}

结果很不幸:运⾏超时:您的程序未能在规定时间内运⾏结束,请检查是否循环有错或算法复杂度过⼤。

于是我们需要想到其他的⽅式,如何快速计算 3 的 n 次⽅,这是我们需要解决的问题,因为在尽量凑 3的前提下,有以下三种情况:

  • 被 3 整除 等于 n :直接计算 3 的 n 次幂
  • 被 3 取余数为1,结果等于 n :直接计算 3 的 (n-1) 次幂,再乘以4,为什么呢?因为余数是1,我们避免有1,需要借出 3,和 1凑成为 4,4 分段之后的最⼤乘积也是 4(2 * 2)
  • 被 3 取余数为 2,结果等于 n:直接计算 3 的 n 次幂 ,再乘以2

也就是说,当n≥5时,优先剪出长度为3的段;剩余4时剪成2×2

为什么选择3?

  1. 数学证明:当n ≥ 5时,3(n-3) ≥ 2(n-2) > n
  2. 接近自然底数e:最优分段长度应接近e ≈ 2.718,3是最接近的整数
  3. 4的特殊处理:2×2 > 3×1,所以剩余4时剪成2×2而不是3×1

执行过程示例(n=10):

10 ÷ 3 = 3段...剩余1
调整:2段3 → 剩余4 → 剪成2×2
结果:3² × 2² = 9 × 4 = 36

在计算幂次⽅的时候,为了避免溢出,在每次相乘的时候,都需要除以998244353 ,为了计算快,每次以⾃身相乘的⽅式计算,代码如下:

public class Solution {
    private static final int MOD = 998244353;
    
    public int cutRope(int n) {
        // 特殊情况处理
        if (n <= 3) return n - 1;
        
        // 计算可以剪出多少段长度为3的绳子
        int countOf3 = n / 3;
        
        // 处理剩余部分:当剩余长度为1时,调整策略
        if (n - countOf3 * 3 == 1) {
            countOf3--; // 减少一段3,与剩余的1组成4
        }
        
        // 计算剩余部分能剪出多少段长度为2的绳子
        int countOf2 = (n - countOf3 * 3) / 2;
        
        // 计算结果:3的countOf3次方 × 2的countOf2次方
        long result = pow(3, countOf3) * pow(2, countOf2);
        return (int) (result % MOD);
    }
    
    /**
     * 快速幂算法计算a的b次方取模
     */
    private long pow(long a, long b) {
        long result = 1;
        while (b > 0) {
            if ((b & 1) == 1) {
                result = (result * a) % MOD;
            }
            a = (a * a) % MOD;
            b >>= 1;
        }
        return result;
    }
}
  • 时间复杂度:O(1),只有常数次操作
  • 空间复杂度:O(1),只使用固定变量

最近在官网充值了一些 Kimi 2.5 的 API ,感觉很好用,想订阅下会员,Kimi 对会员权益说明很模糊。之前按次数,被喷后,改成按额度,但是多少额度也不说清楚?

image-20260203005103844

只宣传限时扩容三倍。。。还有就是 Kimi Code 和 Kimi 会员有什么区别?

Kimi Code 显示 [购买以上任意套餐,还会获得其他 Kimi 会员权益]

image-20260203005001193

Kimi 会员界面显示 [ Kimi Code 2 倍额度]

image-20260203005207022

感情是在这打哑谜是吧,什么档次会员多少 token 是什么不能说的秘密吗? Kimi Code 送 Kimi 会员,Kimi 会员送 Kimi Code 在这套娃呢? Kimi Code 宣传限时扩容三倍,Kimi 会员宣传 Kimi Code 2 倍额度,真太乱了...

因自己有广告拦截和安全的需求自己建了 doh ,都是自己的服务器和可靠上游(国外是 controld+cf+google ,国内用的是阿里和腾讯的 doh),自建的都没有限速(部分 doh 有 qps 限速)。
自己手机是安卓系统,设置里面只有 dot 选项,没有 doh ,这就让我头疼了,于是让 codex 开发了这个 app ,已经经过测试,没有什么大问题。
当然也可以自定义其他的 doh (这是功能之一)
自建的 doh 国内的是上海的机(带宽很小),国外是 ovh ,当然也可能会增加更多机器(未来),还有一个 cf 反代的。
国内可直连 https://rvv.amd.pub/ix
套 cf cdn 的是 https://int.amd.pub/ix
位于上海的国内 doh: https://amd.pub/ix
app 可通过下面方式下载
https://rvv.amd.pub/mica.apk

https://int.amd.pub/mica.apk

https://drive.google.com/file/d/1sEWKBsVhRDxkA-osAUXML0IPeKZaQbd0/view?usp=sharing
app 截图: https://rvv.amd.pub/umr/Screenshot_20260203_063742.jpg
欢迎大家使用,有不足之处请多包涵!

吾身 (Diarum) - 取自"吾日三省吾身",一款零负担、快记录、怡复盘的日记应用,记录独一无二的人生。

零负担,软件使用非常简单,登陆后打开首页即跳转到今日日记。快记录,打开立刻开始记录,自动保存。怡复盘,可以愉快的完成复盘、总结分析。轻松实现现代化 AI 加持的“吾日三省吾身”。

配置 AI Key 之后自动触发日记向量化,后续可以跟 AI LLM 结合日记开展对话 。自然快速地完成:

  • 今日复盘
  • 周报生成
  • 年终总结
  • 等等

基于 PocketBase 和现代 Web 技术构建,简洁、优雅、可自托管。

  • 📝 富文本支持 - 使用富文本并兼容 Markdown 格式记录每日想法
  • 🖼️ 媒体上传 - 为日记条目添加图片和文件
  • 🔒 自托管 - 完全掌控你的个人数据
  • 🚀 易于部署 - 单一二进制文件,内嵌前端,随处部署
  • 💾 PocketBase 后端 - 可靠的数据库和内置管理面板

开发这款软件的初衷源自自己对日记的需求。现在市面上已经有很多优秀的日记和笔记软件。但都多少有点无法满足自己的需求。我期望的一个日记软件,是打开后立刻可以开始记录,不需要纠结文件名、标题、目录结构。最好是网页的,这样在各种设备都可以使用。我自己的设备涉及 MacBook 、HarmonyOS NEXT 、Android 、Arch Linux 、Windows 。只有网页应用能够很好的快速兼容这些平台。最好是可以很方便的自托管的,确保我自己对数据的掌控,且方便搬家。

于是就做了这样一款软件,英文名叫 Diarum ,中文名叫 “吾身”。使用 go+svelte 开发,轻快好用。花费了大量心思打磨移动端和桌面端的日记体验。现在我个人感觉使用体验已经比较丝滑,可以愉快的记录一天的各种事情。

在核心功能的基础上,集成了一个简单的 RAG 系统,配置好 AI KEY 和 MODEL 之后,会自动触发向量数据库的构建。这样一来跟内置的 AI 助手对话时,就可以将向量匹配到的日记放入上下文,方便的进行分析总结等。此外还提供了一个简单的 API 系统,可以方便的将日记数据对接到 n8n 这样的平台,实现自动化的周报、月报生成等灵活的工作流。

软件我自己已经使用了一段时间,感觉不错,记日记几乎是零心智负担。想到什么打开就可以立刻开始记录。迭代了几个版本,现在功能基本稳定可用了。分享出来,希望听听大家的看法。

软件已经开源,欢迎提 issue 和 PR 。提供一个 demo ,可以快速注册体验,期待大家使用。

事情要从一个本来很简单的念头说起。

我想做一个行情类应用。
目标朴素得不能再朴素了:

用户自己配个 API Key ,
就能看 A 股 / 港股 / 美股 / 汇率 / 指数 / 加密货币 的实时行情和历史曲线。

于是我开干了。


第一阶段:理想很丰满

应用很快写完了,逻辑清晰,代码优雅,README 看起来还行。
https://www.v2ex.com/t/1187033

设计初衷也非常“程序员式正义”:

  • 不收钱
  • 不代理数据
  • 不碰隐私
  • 用户自己申请 API Key ,爱用谁用谁

听起来是不是特别合理?
我当时也觉得自己是个天才。


第二阶段:现实一巴掌打醒我

应用一交到用户手里,问题就来了:

  • 「这个 API Key 去哪申请?」
  • 「这个平台要绑信用卡?」
  • 「免费额度不够用?」
  • 「这个接口不支持指数?」
  • 「那个接口不支持港股?」
  • 「我就想看个价格,怎么像在考证?还要梯子?」

总结一句话:为了用我的 App ,用户得先修完一门《全球金融数据 API 导论》。

本来是看行情,
结果变成了 配置行情

功能是有的,
体验极其反人类

我终于意识到一个残酷的事实:

用户根本不想折腾。


第三阶段:为了这碟醋,我决定包顿饺子

问题的本质其实很简单:

  • 应用不是核心
  • 数据整合和交付方式才是核心

既然如此,那干脆一步到位。

我干了件看起来有点“本末倒置”的事:

我去改了一个开源库

项目名叫 Aktools,原本就很强,
但我对它下手更狠了一点,加缓存,加数据源:

https://github.com/johnny-peters/aktools-change


第四阶段:饺子包完,醋也成了主角

我改造后的 Aktools ,目标只有一个:

用户什么都不用配置

只需要:

docker run / deploy aktools

然后就能直接用。

你能拿到什么?

  • ✅ A 股实时 & 历史行情
  • ✅ 港股实时 & 历史行情
  • ✅ 美股实时 & 历史行情
  • ✅ 汇率
  • ✅ 各类指数
  • ✅ 加密货币
  • ✅ K 线 / 曲线 / 时间序列

没有 API Key
没有平台选择
没有额度焦虑
没有配置地狱

一句话:

Aktools 部署完,数据就自己长出来了。


最终形态:饺子反而成了主菜

回头看这件事,特别有意思:

  • 我原本是想推广一个应用
  • 结果为了提升体验
  • 把底层工具链全重做了一遍
  • 最后发现——
    Aktools 才是本体

真正实现了:

“为了这碟醋,我包了顿饺子,
结果发现饺子比醋香多了。”


如果你是谁?

如果你是:

  • 普通用户

那你直接访问 https://price.btc-reborn.com/ ,可以订阅行情,配置持仓;
也可以在项目地址 https://github.com/johnny-peters/financial-dashboard 下载 exe,拥有一个无广告绿色的可开机启动的控件(仿 mac 做的)

如果你是:

  • 想做行情类应用的开发者
  • 想快速拿到金融数据的个人项目
  • 不想被 API Key 折磨的工程师
  • 想本地 / 私有化部署行情服务的人

那你可以直接从这里开始:

🔗 Aktools (改良版):
https://github.com/johnny-peters/aktools-change


最后一句真心话

技术人常犯的错是:

把“可配置性”当成“用户友好”。

但后来我才想明白:

真正好的体验,
是让用户连“配置”这个概念都不需要知道。

CloseTab - 智能标签页管理插件

Chrome Web Store

插件成功上架谷歌插件商店了,之前分享过一次,但是上次使用比较麻烦,现在可以直接从谷歌插件商店安装

简介

CloseTab 是一款强大的浏览器标签页管理插件,帮助你轻松管理大量标签页,提升浏览器性能和工作效率。支持一键保存、恢复、搜索和批量管理标签组。

📦 从 Chrome 应用商店安装

解决的问题

1. 标签页过多导致的性能问题

  • 浏览器打开几十上百个标签页,导致内存占用过高、系统卡顿
  • 通过一键保存并关闭标签页,释放系统资源,同时不丢失任何内容

2. 工作场景切换困难

  • 需要在不同项目、不同任务之间频繁切换
  • 每次切换都要重新打开一堆相关网页
  • 使用标签组功能,为不同场景保存专属的标签集合,一键恢复工作环境

3. 重要标签页意外丢失

  • 浏览器崩溃、误关闭窗口导致标签页丢失
  • 自动保存标签组,随时可以恢复,再也不用担心数据丢失

4. 标签页管理混乱

  • 标签页太多,找不到需要的页面
  • 提供搜索、排序、收藏等功能,快速定位目标标签页

5. 跨设备同步需求

  • 需要在不同设备间共享标签组
  • 支持导出/导入功能,轻松实现标签组的备份和迁移

核心功能

基础功能

  • 一键保存 - 保存当前窗口所有标签页为一个标签组
  • 一键恢复 - 在新窗口中恢复整个标签组
  • 单页打开 - 打开标签组中的单个标签页(自动检测重复)
  • 收藏标签组 - 标记重要的标签组,方便快速访问
  • 智能去重 - 打开标签页时自动检测是否已存在,避免重复

高级管理

  • 搜索功能 - 按标签组名称、标签页标题、URL 快速搜索
  • 多维排序 - 支持按时间、名称、标签页数量排序
  • 批量操作 - 批量选择、删除、合并标签组
  • 标签组合并 - 将多个标签组合并为一个
  • 导出/导入 - 支持追加导入和替换导入两种模式

个性化设置

  • 保存后行为 - 自定义保存后是否关闭标签页
  • 新窗口选项 - 选择打开空白窗口或管理页面
  • 收藏过滤 - 快速查看收藏的标签组

数据存储

  • 本地存储 - 使用 IndexedDB 本地存储,数据安全可靠
  • 无需登录 - 所有数据保存在本地,保护隐私
  • 持久化设置 - 用户偏好设置自动保存

安装教程

方式一:从应用商店安装(推荐)

Chrome / Edge 浏览器

直接访问 Chrome 应用商店 点击"添加至 Chrome"即可一键安装。

方式二:手动安装开发版

适用于想要体验最新功能或进行二次开发的用户。

下载地址: Gitee 仓库

  1. 访问 https://gitee.com/hongbin_chen/close-tab 下载 ZIP 压缩包
  2. 解压文件到任意目录
  3. 打开浏览器,进入扩展程序管理页面:
    • Chrome: chrome://extensions/
    • Edge: edge://extensions/
  4. 开启右上角的"开发者模式"
  5. 点击"加载已解压的扩展程序"
  6. 选择解压后的文件夹
  7. 安装完成,点击浏览器工具栏中的插件图标即可使用

使用说明

快速开始

  1. 保存标签组


    • 点击插件图标打开弹窗
    • 点击"关闭并保存"按钮
    • 当前窗口的所有标签页将被保存为一个标签组
  2. 恢复标签组


    • 在弹窗中查看已保存的标签组
    • 点击"恢复"按钮,在新窗口中打开所有标签页
    • 或点击单个标签页的"打开"图标,只打开该标签页
  3. 管理标签组


    • 点击"管理页面"进入完整管理界面
    • 使用搜索框查找特定标签组
    • 使用排序功能整理标签组
    • 点击标签组可选中,进行批量操作

高级技巧

  • 收藏重要标签组 - 点击星标图标收藏,在"收藏"标签页快速访问
  • 合并标签组 - 选中多个标签组,点击"合并"按钮
  • 导出备份 - 选中标签组,点击"导出"保存为 JSON 文件
  • 导入恢复 - 点击"导入"按钮,选择追加或替换模式导入数据

如果一个系统存在路径穿越漏洞,外部可能读取到正在使用的 swapfile 。
测试发现,虽然是概率性的,但 swapfile 中确实可能存在账号和应用的明文密码。似乎与近期是否有成功登录的行为有关。
进一步推论,如果攻击者采用一些手段推高内存占用,可能会导致更多信息外溢到 swapfile 从而被截获。

去年国庆左右在 88code 购买了半年的会员,当时 298 每天可以用 140 刀很划算,后面双 12 又搞活动,我又囤了好久,结果今年年初,因为 A 社加大封号力度,结果现在导致他们成本覆盖不了,结果一直停到今天,之前还以为可以恢复,结果一个月了都没消息。

本来之前去申请退款的,客服说我的套餐很划算,就没退,结果上周再去退,说一周退,到现在都没退会回来,问客服就是耐心等,然后据说客服工资都发不出来了,请问要咋维权呢

由于是使用机场自己的客户端,没有 tun 模式,网上搜了一下可以使用 Proxifier 解决 Antigravity 的登录问题,正好之前用 OBS 直播油管的时候安装过 Proxifier ,于是直接设置了一个规则,顺利登录 Antigravity 。

但是马上遇到一个棘手的问题,agent 加载不出来,也就是对话框和模型都加载不出来,我以为是跟 cursor 一样需要在设置里面配置 proxy ,结果设置了也不行,重启几次 IDE 都是加载不出来。

搜了一下公众号文章,都讲的不清不楚,Proxifier 规则里面要增加好几个 exe ,不止是 antigravity.exe ,直接复制别人公众号文章里面提供的文件名,行不通,于是在任务管理器里面,把 Antigravity 相关的所有 exe 都找到所在文件夹,通过手动添加进去,就搞定了。

需要添加的几个 exe 的文件夹路径(其中 XXX 需要换成你的用户名):

C:\Users\xxx\AppData\Local\Programs\Antigravity

C:\Users\xxx\AppData\Local\Programs\Antigravity\resources\app\extensions\antigravity

我添加进规则的几个 EXE:antigravity.exe; inno_updater.exe; language_server_windows_x64.exe; fd.exe
image

记得要通过 browser 这个按钮添加才有效。

附赠 Proxifier 注册码 5EZ8G-C3WL5-B56YG-SCXM9-6QZAP