Jinmiao’s Blog

基于 Claude Code 的 GitLab CI 代码评审

前言 本文介绍如何在自建 GitLab 中实现基于 Claude Code 的自动代码评审,而无需依赖 GitLab 企业版的 AI 能力(GitLab Duo)。 只需: 一个 Claude Code Max 订阅 一个配置了代理的 GitLab Runner 就能为团队带来 AI 代码评审的能力,生成 Markdown 和 PDF 格式的评审报告。 前置准备 个人建议 不要在 CI 中使用个人订阅,应使用 Anthropic 的 API 服务而不是个人订阅,个人订阅会因为违法使用条款中的多人共享规则而被封禁,这是我个人被封禁 2 个帐号后的经验之谈。 如果不方便接入 Anthropic API,可以考虑接入 OpenRouter 等第三方 API 服务商。 另外,阿里云的 Qwen3 也是一个可行的方案,但实测下来报告质量不如 Opus 4.5,表达不够简洁,内容相对价值下降。 订阅 Claude Code Max 为了保证代码评审报告的质量,我们选择了支持 Opus 模型的 Max 套餐。 对于在中国的用户,可通过 iOS 应用内购买完成订阅: 通过 giffgaff 获取英国手机号,使用 Google 账号登录 claude.ai 完成注册 准备美区 Apple ID,在 Apple 官网购买礼品卡并充值 在 iPhone 上下载 Claude App,使用应用内购买完成订阅 订阅方式 Max 套餐(5x) Max 套餐(20x) 官网直接订阅(需美国信用卡) $100/月 $200/月 iOS 应用内购买 $125/月 $250/月 网络环境要求 Claude Code 使用 OAuth Token 认证,需要访问 Anthropic 服务。如果 GitLab Runner 部署在中国大陆,需要配置透明代理,使流量通过美国等地区的代理服务器进行转发,也可直接使用美国地区的服务器作为 Runner。 ...

2025年12月20日 · 罗锦淼

裸金属服务器网络配置

TLDR 裸金属服务器 链路聚合 配置 裸金属服务器 链路聚合+VLAN 配置 裸金属服务器 链路聚合+VLAN+Bridge 配置 裸金属服务器 衍生实践推荐 前言 我已很久没写博客。 迟迟不想动笔,原因有二。 其一,技术类的很多问题 OpenAI 的 ChatGPT 都有完美的回答,我再重复略显多余。 其二,疲惫让我无法静心思考,我不知道我应分享什么,因此也找不到动笔的动力。 坐在书店里,避开了回乡的人来人往,咖啡、音乐、落日(即将到来)。麻木之后重新感受到的片刻美好,让我重拾了动力。 我想就记录一点工作遇到的问题吧,或许有人也会遇到呢也说不定。 内网带宽瓶颈 公司有基于开源文生文(图)模型的服务,这些服务的模型数据少的有 60G、多的有 100+G。 模型存储在另外一个服务器上,通过 NFS 同时为内网多台 GPU 服务器提供模型数据。如果按千兆交换机的速率计算, 100G 的数据需要 13.5 分钟才能完成一次完整的模型数据读取,10 个 GPU 服务全部启动完毕,需 135 分钟(2个多小时)。 为了避免 NFS 服务瓶颈导致的启动慢问题,可使用链路聚合(LACP)来解决。链路聚合可将两个网卡合并成一个,从而获得两个累加后的带宽。比如 NFS 服务器上有两个千兆的网口,通过配置链路聚合, 可让该服务器具备两千兆的内网带宽,在多服务同时启动时实现总启动时间减少一半的效果(这里只是举例说明,裸金属服务器实际上是单口 25G)。 下面给出具体的 systemd networkd 配置,假设两个物理网口是 eth0、eth1。 # /etc/systemd/network/1-eth0.network [Match] Name=eth0 [Link] MTUBytes=9000 [Network] Bond=bond0 # /etc/systemd/network/1-eth1.network [Match] Name=eth1 [Link] MTUBytes=9000 [Network] Bond=bond0 # /etc/systemd/network/5-bond0.netdev [NetDev] Name=bond0 Kind=bond [Bond] Mode=802.3ad TransmitHashPolicy=layer3+4 MIIMonitorSec=0.1s LACPTransmitRate=fast UpDelaySec=0.2s DownDelaySec=0.2s # /etc/systemd/network/5-bond0.network [Match] Name=bond0 [Link] MTUBytes=9000 [DHCPv4] UseDNS=false [DHCPv6] UseDNS=false [IPv6AcceptRA] UseDNS=false [Network] BindCarrier=eth0 eth1 Gateway=172.16.160.254 Domains=jinmiaoluo.com. DNS=223.5.5.5#dns.alidns.com DNS=223.6.6.6#dns.alidns.com DNSOverTLS=true [Address] Address=172.16.160.12/24 经过上面的配置,这台服务器就具备了两倍的内网并发带宽了。 ...

2025年1月30日 · 罗锦淼

WireGuard:C 实现

内核模块逻辑 指定模块初始化的入口函数 wg_device_init 注册虚拟网络设备 rtnl_link_register(&link_ops) net_device 结构体的初始化操作 wg_setup 指定 ip link add 命令时对应的初始化操作(是主要的功能逻辑所在) wg_newlink 指定虚拟网络设备启动时的操作 wg_open 指定虚拟网络设备停止时的操作 wg_stop 指定虚拟网络设备开始发送第一个封包的操作 wg_xmit wireguard 初始化的操作是: 通过 ip link add dev wg0 type wireguard 添加虚拟网络设备 通过 ip addr add 10.0.0.4/24 dev wg0 配置虚拟网络设备地址 通过 wg set wg0 添加 wireguard 配置 通过 ip link set wg0 up 启动虚拟网络设备 因此我们应先看 wg_newlink 的逻辑,再看 wg_open 的逻辑

2024年10月27日 · 罗锦淼

WireGuard:Golang 实现

前言 本文基于 WireGuard Golang 实现,平台是 Linux,Git 版本是:12269c2 内容 关于收发包 通过创建 TUN 设备,跟系统的网络栈交互。 数据抽象的介绍 关于限速器 WireGuard 在最终进行数据的加密、解密之前,需要通过加密协议 Noise Protocol 基于加密算法协商出对称加密密钥(临时的),这个协商的过程(握手)会涉及到加/解密,哈希运算。为了避免针对响应方 CPU 负载的 DoS 攻击,响应方会根据实时负载情况对握手的请求进行限速,根据请求方的来源 IP 实例化对应的令牌桶(每个来源 IP 对应一个令牌桶),基于令牌桶对握手包进行频率限制。默认的限制策略是每秒钟 20 个握手包,每个时刻(即每纳秒)最多允许 5 个握手包的突发流量。 关于索引表 每个数据包的包头第 4 到第 8 字节会存储一个 32 bits 的无符号整数作为索引,通过索引和索引表(map[uint32]IndexTableEntry),可以查到对应的 Peer、Handshake、Keypair。 索引值本身是个随机值,在: func (table *IndexTable) NewIndexForHandshake(peer *Peer, handshake *Handshake) (uint32, error) 函数中被创建,使用该索引值时使用了两阶段锁(索引表首先基于读锁检查索引是否已被占用,如果该索引未被使用,再基于写锁创建对应的索引条目)。 索引表的作用是? 索引表会在两种场合被使用。 第一种是握手时,WireGuard 初始方创建握手包和消费响应包,完成 Triple-way DH 流程。初始方创建、发送握手包,然后等待、消费响应包,中间有个发送并等待响应的中间状态,这个中间状态的值被存储在 Handshake 实例中,Handshake 作为索引表的值(值是一个结构体,其中一个字段),通过索引和该握手包绑定在一起。当开始消费响应包时,通过响应包头部的接收方 ID(响应方将初始握手包中的发送方 ID 作为接收方 ID 返回),查询索引表从而获得创建握手包时的 Handshake 实例,从而完成整个 Triple-way DH 流程。 第二种是接收到数据包时。响应方在消费握手包并发送响应后,会在本地创建 Peer、Keypair,并更新索引表。接收到的数据包头部的索引值,允许 WireGuard 通过索引表找到握手时创建的 Keypair 和 Peer。Keypair 用于确认该数据包是否有对应的密钥对,加密密钥是否已经过期。Peer 用于定位数据包的队列。数据包基于 Keypair 进行认证和解密,解密后的数据包将添加到 Peer 的队列内等待消费。 ...

2024年2月29日 · 罗锦淼

WireGuard:远程办公网络

前言 本文是对上一篇文章:WireGuard:局域网跨城通信 的补充。将围绕远程办公的开发者,即不在办公网内的开发者,如何基于 WireGuard® 跨城访问办公网中的服务。 场景 团队提倡远程办公,开发者位于全国各地。公司在某个城市设有办公地点,办公地点的局域网内自建了虚拟化,并部署了开发相关的一系列服务(GitLab,CI,K8s)。 不同城市的开发者需访问办公地点局域网中的多个服务。 不同城市的开发者之间需互相访问。 挑战 开发者如何稳定的跨公网访问局域网内所有设备(如同在办公地点的局域网内)。 局域网内的 WireGuard 节点位于运营商 NAT 设备之后,不支持 NAT 穿透且无公网 IP。 局域网内只运行一个 WireGuard 节点(而不是将所有服务加入 WireGuard 网络)。 如何解决局域网网关不支持静态路由规则的问题。 如何避免路由冲突。 架构 思路 假设有两个局域网 LAN1,LAN2 分别是公司在深圳(LAN1)和北京(LAN2)的办公网。 有三台服务器,z(IP 前的字母)是公网服务器(有公网 IP),a 和 c 是内网服务器(无公网 IP)。公网服务器(z)和内网服务器(a 和 c)组建了一个 WireGuard 网络,且三台服务器开启封包转发(IP Forward)。 e 和 f 是两个远程办公的开发者,其中 e 在广州,f 在上海,每个开发者是一个 WireGuard Client,通过各自的 WireGuard 客户端配置加入 WireGuard 网络。 开发者访问服务 假设广州的开发者 e(10.70.0.4)要访问北京办公网(LAN2)的中的设备 d(192.168.200.244)。 e 通过 WireGuard 客户端接入 WireGuard 网络。客户端与 WireGuard Server 建立连接,并向系统路由表写入相关路由规则(192.168.200.0/24 由 wg0 负责接收)。 ...

2024年1月21日 · 罗锦淼

WireGuard:局域网跨城通信

前言 物理专线可以实现多个局域网之间的设备互通(像在同一个局域网内),但由于复杂的流程(运营商审批与实施),对于个人开发者和中小型团队并不友好。 本文将介绍如何基于 WireGuard® 的 “云专线”,以较低的成本(无需审批,每年只需 60 元1)实现多个局域网之间设备互通。 挑战 局域网层面的流量转发。即局域网 A 中任意设备可以访问局域网 B 中任意设备(如同在同一个局域网内)。 局域网内的 WireGuard 节点位于运营商 NAT 设备之后,不支持 NAT 穿透且无公网 IP。 每个局域网内只运行一个 WireGuard 节点。 场景 公司有多个办公地点(分布在不同城市),每个办公地点一个局域网,局域网内自建了私有服务(K8s,GitLab,CI)。 不同办公地点的开发者之间需互相访问。 不同办公地点的私有服务之间需互相访问。 A 办公地点的开发者需要访问 B 办公地点的私有服务。 A 办公地点的开发者需要接收来自 B 办公地点私有服务的回调。 架构 思路 假设有两个局域网 LAN1,LAN2,对应的网段是 192.168.201.0/24,192.168.200.0/24。其中 z(IP 前的字母)是公网服务器(有公网 IP),a 和 c 是内网服务器(无公网 IP)。 公网服务器(z)和内网服务器(a 和 c)组建了一个 WireGuard 网络,且三台服务器开启封包转发(IP Forward)。 LAN1 的设备 b(192.168.201.55)请求 LAN2 的设备 d(192.168.200.244),比如在 b 上执行:ping 192.168.200.244,b 和 d 是局域网内的普通设备,而不是 WireGuard 节点。 设备 b 通过默认网关(192.168.201.1)确认数据包的下一跳,默认网关中配置了静态路由规则(指定 192.168.200.0/24 的下一跳是 a),因此默认网关将数据包发给 a。 ...

2024年1月21日 · 罗锦淼

WireGuard:调试

前言 WireGuard® 是 Linux 的内核模块之一(主流发行版都已内置)。 最近会通过调试的方式来理解 WireGuard 的代码,本文是对这个过程(调试 WireGuard 内核模块源码)的记录。 挑战 内核(含调试信息)如何构建 QEMU 如何启动调试环境 GDB 如何进行远程调试 VSCode 如何整合图形化调试流程 内容 思路 基于最新的 WireGuard 内核模块代码构建出含有 DEBUG symbol 的 Linux 内核 通过 QEMU 和内核(上一步构建出来的)启动虚拟机 通过 GDB 进行远程调试 准备 本地开发环境需要满足的条件: 安装了构建内核所需依赖包1 安装了虚拟化所需 QEMU 包 安装了调试所需 GDB 包 当前开发环境支持虚拟化(如果是虚拟机,需确保当前环境支持虚拟化嵌套2) 操作 克隆 WireGuard 内核模块源码到本地(实际上就是含有最新 WireGuard 修改的内核源码): git clone git://git.zx2c4.com/wireguard-linux 构建内核(需要等一会,我本地构建一次需 3 分钟)命令如下: cd wireguard-linux # 为了确保后面的操作体验一致,这里我默认所有操作基于:fa41884c1c6d commit git checkout fa41884c1c6d DEBUG_KERNEL=yes ARCH=x86_64 make -C tools/testing/selftests/wireguard/qemu build -j$(nproc) 如何你对手动构建内核感兴趣,可以看看 Arch Linux Wiki。 源码仓库中的 Makefile 封装了构建内核所需的所有操作, 上面的命令直接调用仓库中的 Makefile 来构建含有调试信息的内核。 ...

2024年1月11日 · 罗锦淼

WireGuard:抓包和实时解密

前言 最近在读 WireGuard® 的源码和论文时使用了 Wireshark 进行抓包分析,本文是对这个过程(如何通过 Wireshark 实时抓取 WireGuard 加密包并解密1)的一点记录。 内容 思路 在虚拟化准备两台虚拟机用以构建一个 WireGuard VPN 测试网络,其中虚拟机 A 作为请求发起方,虚拟机 B 作为响应方。 在本地 Windows 笔记本上安装 Wireshark(包含 sshdump 功能)通过 SSH 的形式连接虚拟机 A,实现 Wireshark 远程实时抓包。 挑战 前向保密性2对流量分析的挑战。WireGuard 为了保证前向保密性,每隔 120s 会更换一个临时密钥(一次性的)来加密数据包。因此,如果要获取实时的明文数据,Wireshark 需要动态的获取临时密钥,并基于临时密钥实时解密。 需要更换内核。WireGuard 是 Linux 内核模块,笔者的测试环境需要重新编译并替换内核才能基于 Kprobe 的方式动态获取临时密钥。 具体操作 编译并更换内核 我是在虚拟机 A 上抓包,所以需要编译和替换虚拟机 A 的内核,操作见:Arch Linux Wiki 我使用的内核是这个: # 切到 stable 分支,我实践的时候,内核版本是 6.5,commit:fa41884c1c6d git clone git://git.zx2c4.com/wireguard-linux 按照 Arch Linux Wiki 操作,大概有这些步骤: 准备内核配置 zcat /proc/config.gz > .config 开启 WireGuard Debug make nconfig 构建内核 make -j 构建内核模块 make modules 安装内核模块 make modules_install 构建引导镜像 make bzImage 安装引导镜像 cp -v arch/x86/boot/bzImage /boot/vmlinuz-linux-wg 准备引导配置 cp /etc/mkinitcpio.d/linux.preset /etc/mkinitcpio.d/linux-wg.preset && vim /etc/mkinitcpio.d/linux-wg.preset 更新引导配置 mkinitcpio -p linux-wg && grub-mkconfig -o /boot/grub/grub.cfg 一切顺利你会得到一个这样的内核: ...

2023年11月29日 · 罗锦淼

远程剪切板

前言 我本地是一台 Windows 11 的 ThinkPad T480,开发环境运行在一台 Linux 服务器上,基于 SSH 的方式进行访问和控制。我希望在 Linux 服务器的 Vim 中,通过 y 复制代码到本地 Windows 11 的剪切板内,通过 p 将本地 Windows 11 剪切板中最新的内容黏贴到服务器的 vim 内。本文将提供一种低成本的解决方案。 思路 Windows 的 WSL 支持直接运行 Linux X11 GUI 程序,并且在 Linux X11 GUI 中,是可以直接访问 Windows 本地的剪切板的。这意味着在 WSL 内,我们可以通过 SSH 的 X11Forward 将远端的 X11 转发到 WSL 内,这样 Linux 服务器上的 X11 GUI 程序就可以经 WSL 中转,访问 Windows 11 本地的剪切板了。 因此,我们只需: 基于 WSL 来登录 Linux 服务器 开启 SSH 的 X11Forward 将 Linux 服务器的 Vim 包用 GVim 包替换 即可。 ...

2023年11月28日 · 罗锦淼

Miniflux:开源 SaaS 项目源码笔记

前言 我从 9 月的 27 号开始阅读 Miniflux 项目的代码,阅读成熟的开源项目就像在阅读一本适合自己的书,节奏虽慢但充实。因为快接近阅读这个项目代码的尾声,故整理这篇博文作为过程的记录和分享。 开始之前简单介绍一下 Miniflux。Miniflux 是一个开源的 RSS 订阅服务实现,类似 Tiny Tiny RSS,使用过 RSS 订阅服务的朋友应该不陌生,部署 Miniflux 服务相当于拥有了自己的 Feedly 服务。该项目是基于 Go 和 Vanilla JS (没有使用任何框架的原生 JavaScript API) 的 SaaS APP 实现,提倡尽可能少的外部依赖,简单可维护易扩展,所以这个项目会有很多基础功能的实现(这些细节在参与 Web 框架本身的开发时才会涉及,平时基于 Web 框架的项目开发,比如 Django/Rails 会自带这些功能,直接用就可以)。 本文会通过少量的代码,记录阅读过程中发现的有趣的点,如果你对这些点感兴趣,再去阅读代码即可。我更希望这篇博客像在介绍一本书,让你知道这本书哪里有趣,然后你再自己去判断适不适合你,或者有哪些点适合你。读书不一定要整本通读,发现感兴趣的点,只读感兴趣的部分,也是一种不错的选择。 软件工程应该是件有趣的事,或者说,作为从业者我们应该去发现那些有趣的点,穿透高度抽象带来的认知迷雾。 内容 不使用 ORM 如何实现数据库抽象 如果你想在项目内使用 Golang 标准库的 database/sql 来封装 SQL 操作,可以参考。 Miniflux 中的数据库表的管理,代码分成两类,第一类是对这个表的 SQL 操作的封装,在 storage 包内,不同的表对应不同的 storage 文件夹内的文件,比如 users 表对应 user.go 文件,user.go 中一个操作: // SetLastLogin updates the last login date of a user. func (s *Storage) SetLastLogin(userID int64) error { query := `UPDATE users SET last_login_at=now() WHERE id=$1` _, err := s.db.Exec(query, userID) if err != nil { return fmt.Errorf(`store: unable to update last login date: %v`, err) } return nil } 第二类是对表结构和相关的改动请求的抽象,在 model 包内,不同的表对应不同的 model 包内的文件,比如 users 表对应 user.go 文件,user.go 中的两个抽象: ...

2023年10月22日 · 罗锦淼