最近在部署Arkime，可给折磨坏了，终于试了各种错，找出了一个比较优雅的部署方案。Arkime简介Arkime（原Moloch）是一套面向大规模网络流量分析与取证场景设计的开源网络流量捕获平台，其核心目标是在保证全流量留存的前提下，实现PB级网络数据的高效索引、检索与关联分析。Arkime采用“全包捕获（Full Packet Capture）+元数据索引”的架构设计，通过高速抓包引擎持续采集网络数据包，并将原始PCAP文件存储至本地磁盘，同时提取会话元数据（Session Metadata）写入OpenSearch或Elasticsearch索引，从而兼顾长期流量存储能力与快速查询性能。在架构层面，Arkime主要由Capture、Viewer和OpenSearch三部分组成。其中Capture组件负责网络数据包采集、协议解析和会话重组；Viewer组件提供Web管理界面及REST API接口；OpenSearch负责存储和检索流量元数据索引。Arkime支持对HTTP、DNS、TLS、SMTP、SSH等多种常见协议进行深度解析，并能够基于IP地址、端口、域名、URL、证书指纹、用户代理等字段进行复杂查询和关联分析。与传统网络分析工具相比，Arkime最大的特点在于其将原始流量与索引数据进行解耦存储。用户在检索时首先通过OpenSearch快速定位目标会话，再根据索引记录回溯对应的PCAP数据，从而避免直接扫描海量数据包带来的性能瓶颈。这种设计使Arkime能够在数十TB甚至PB级流量环境下仍保持较高的查询效率。在实际应用中，Arkime广泛部署于安全运营中心（SOC）、网络取证平台以及威胁狩猎环境中，常与Zeek、Suricata等安全分析工具协同工作。其中Arkime负责流量采集与回溯取证，Zeek负责生成高层协议日志和行为分析结果，Suricata负责实时威胁检测与告警，三者共同构建覆盖“检测—分析—溯源”全流程的网络安全监测体系。这里，我们选择用Docker方式部署，因为Docker环境隔离做的很漂亮，而且不用考虑乱七八糟的依赖，这里我们一共用到两个Docker镜像，一个是Arkime的，另一个是Opensearch的。创建目录首先创建 Arkime 所需的数据目录，用于后续 Docker 容器挂载：sudo mkdir -p /opt/arkime/{raw,etc,opensearch}目录说明如下：目录作用/opt/arkime/raw存储 Arkime 抓取的原始 PCAP 数据/opt/arkime/etc存储 Arkime 配置文件、证书等运行配置/opt/arkime/opensearch存储 OpenSearch 数据库持久化数据通过将数据挂载到宿主机目录，即使容器重建或升级，抓包数据和数据库数据也不会丢失。编写 Docker Compose 文件Arkime 采用分组件架构运行，核心组件包括：Capture：负责网络流量采集与 PCAP 存储；Viewer：提供 Web 界面以及数据查询功能；OpenSearch：负责索引存储和会话检索。下面给出一个最小可运行版本：services: opensearch: image: opensearchproject/opensearch:2.19.1 container_name: opensearch environment: - discovery.type=single-node - OPENSEARCH_INITIAL_ADMIN_PASSWORD=ArkimeOpenSearch123! - OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g volumes: - /opt/arkime/opensearch:/usr/share/opensearch/data ports: - "9200:9200" restart: unless-stopped capture: image: ghcr.io/arkime/arkime/arkime:v6-latest container_name: arkime-capture network_mode: host cap_add: - NET_ADMIN - NET_RAW - SYS_NICE command: > /opt/arkime/bin/docker.sh capture volumes: - /opt/arkime/raw:/opt/arkime/raw - /opt/arkime/etc:/opt/arkime/etc restart: unless-stopped depends_on: - opensearch viewer: image: ghcr.io/arkime/arkime/arkime:v6-latest container_name: arkime-viewer network_mode: host command: > /opt/arkime/bin/docker.sh viewer volumes: - /opt/arkime/raw:/opt/arkime/raw - /opt/arkime/etc:/opt/arkime/etc restart: unless-stopped depends_on: - opensearchOpenSearch 配置说明discovery.type=single-node表示以单节点模式运行 OpenSearch。对于实验环境或中小规模部署场景，无需配置集群即可正常工作。OPENSEARCH_INITIAL_ADMIN_PASSWORD设置 OpenSearch 内置 admin 用户密码，后续 Arkime 连接数据库时会使用该账号。OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g设置 JVM 堆内存大小：-Xms2g：初始堆内存 2GB-Xmx2g：最大堆内存 2GB生产环境通常建议分配主机内存的 50% 左右给 OpenSearch。Capture 配置说明network_mode: hostArkime 需要直接访问宿主机网卡进行流量捕获，因此推荐使用 Host 网络模式。cap_add: - NET_ADMIN - NET_RAW - SYS_NICE授予容器抓包所需权限：权限作用NET_ADMIN修改网卡参数NET_RAW创建原始套接字进行抓包SYS_NICE调整线程优先级，提高抓包性能数据持久化volumes:将容器内部目录映射到宿主机：/opt/arkime/raw用于保存抓取的 PCAP 文件。/opt/arkime/etc用于保存 Arkime 配置文件和证书。初始化数据库首先启动 OpenSearch：docker compose up -d opensearch确认 9200 端口可以正常访问后，初始化 Arkime 数据库结构：docker run --rm \ --network host \ -v /opt/arkime/etc:/opt/arkime/etc \ ghcr.io/arkime/arkime/arkime:v6-latest \ /opt/arkime/db/db.pl \ --insecure \ 'https://admin:ArkimeOpenSearch123!@127.0.0.1:9200' \ init参数说明：--insecureOpenSearch 默认启用 HTTPS，并使用自签名证书。初始化阶段 Arkime 尚未导入根证书，因此需要通过该参数跳过证书校验，否则会出现 TLS 验证失败的问题。init创建 Arkime 所需的索引、模板以及数据库结构。初始化 Web 用户创建 Arkime Web 管理员账号：docker run --rm \ --network host \ -v /opt/arkime/etc:/opt/arkime/etc \ ghcr.io/arkime/arkime/arkime:v6-latest \ /opt/arkime/bin/arkime_add_user.sh \ admin \ "Admin User" \ Password123! \ --admin参数含义：admin：登录用户名。Admin User：显示名称。Password123!：登录密码。--admin：授予管理员权限。导出 OpenSearch 根证书OpenSearch 默认启用了 TLS 加密通信。Arkime 在运行阶段需要验证 OpenSearch 证书，因此需要将 OpenSearch 容器中的根证书导出到本地：docker cp opensearch:/usr/share/opensearch/config/root-ca.pem \ /opt/arkime/etc/opensearch-ca.pem执行完成后，本地目录结构如下：/opt/arkime/etc/ ├── config.ini └── opensearch-ca.pem如果缺少该证书，Arkime 在启动时通常会出现证书校验失败，从而无法连接 OpenSearch。配置 config.ini编辑配置文件：vim /opt/arkime/etc/config.ini内容如下：[default] elasticsearch=https://admin:ArkimeOpenSearch123!@127.0.0.1:9200 caTrustFile=/opt/arkime/etc/opensearch-ca.pem interface=ens18 pcapDir=/opt/arkime/raw snapLen=32768参数说明：elasticsearch：指定 OpenSearch 连接地址。caTrustFile：指定 OpenSearch 根证书文件，用于 TLS 证书验证。interface：指定抓包网卡。可通过以下命令查看网卡名称：ifconfigpcapDir：指定 PCAP 文件存储目录。snapLen：指定单个数据包最大捕获长度。常见取值：值说明1514仅保存标准以太网帧9000支持 Jumbo Frame32768推荐值，适用于绝大多数场景注意，这里snapLen=0会报错。启动 Arkime完成上述配置后，启动所有组件：docker compose up -d查看容器状态：docker ps正常情况下可以看到：OpenSearchArkime CaptureArkime Viewer三个容器均处于运行状态。至此，一个基于 Docker 的 Arkime 单节点部署已经完成，可以通过 Web 界面对网络流量进行检索、分析和管理。需要说明的是，本文仅提供一个最小可运行环境，实际生产部署时还需要根据硬件配置、流量规模以及存储需求，对 OpenSearch、Arkime Capture 线程数、PCAP 保留策略以及索引生命周期等参数进行进一步优化。
本篇文章主要梳理一下现在主流的Agent框架如何做文件系统管理的，如会话、记忆如何存放，如何做项目隔离等等。引言随启随用的成熟Agent框架大大简化了我们的工作流，但其目录结构和文件组织形式却常被忽略，所以本文将针对Claude Code、Codex和Copilot三大主流Vibe Coding Agent框架的文件系统进行简要介绍。Claude CodeClaude Code的目录分为两种，即全局~/.claude/和项目级.claude/，且项目级配置的优先级高于全局配置。首先，我们以全局文件目录入手，展示Claude Code的文件目录是如何组织的~ # 用户目录 ├── .claude.json # 自动生成的本地应用状态文件，用于保存登录会话、MCP配置等运行时信息 └── .claude/ # 全局个人配置，跨所有项目生效 ├── CLAUDE.md # 全局个人记忆，每次会话与项目CLAUDE.md同时加载冲突时项目级优先 ├── settings.json # 全局默认设置包括权限/hooks/模型/env，会被项目级settings.json覆盖 ├── keybindings.json # 自定义快捷键映射，启动时加载，文件变更后热重载 ├── stats-cache.json # /usage展示的token与费用累计统计缓存，永久保留，不自动清理 ├── remote-settings.json # 企业managed settings本地缓存；每次启动刷新，永久保留 ├── history.jsonl # 所有会话中输入过的prompt历史，支持上箭头跨会话回调，永久保留 ├── .last-cleanup # 记录上次自动清理的时间戳，启动时用于判断是否触发清理任务 ├── .last-update-result.json # 记录上次自动更新的结果即成功/失败/版本号，用于显示更新提示 ├── settings.json.ds # settings.json的结构说明文件文件，用于IDE做自动补全和校验 ├── themes/ # 自定义颜色主题 │ └── <name>.json # 通过/theme创建或手写，/theme <name>切换 ├── rules/ # 全局规则 │ └── *.md # 可以随意命名，无paths参数每次启动时加载，有paths匹配文件进入上下文时加载 ├── skills/ # 全局个人技能，任意项目中用/<name>调用，或由Claude自动触发 │ └── <name>/ # name为自定义字段 │ ├── SKILL.md # description决定Claude是否自动触发，支持$ARGUMENTS │ └── [辅助文件] # 与SKILL.md捆绑的参考文档、模板、脚本等，Claude可按需读取 ├── commands/ # 单文件命令，旧机制，现已与skills合并，并建议新工作流改用skills │ └── <name>.md # /<name>调用；同名时skill优先于command ├── agents/ # 用于定义具有独立上下文与工具权限的子Agent │ └── <name>.md # @<name>手动调用或由Claude自动委派 ├── workflows/ # 由Claude生成或用户定义的可执行工作流脚本，用于把多步骤任务自动化成一个命令 │ └── <name>.js # 启动时加载为/<name>命令，会被同名项目级workflow覆盖 ├── agent-memory/ # memory:user子代理的跨项目持久记忆 │ └── <name>/ # name与agents中的保持一致 │ └── MEMORY.md # 子Agent自动维护，启动时注入system prompt，注入前200行或25KB ├── output-styles/ # 用于定义回答的语气、结构和表达方式，会话启动时注入system prompt │ └── *.md # 通过settings.json中的outputStyle字段设置启用 ├── plugins/ # 插件市场与已安装插件数据，由claude plugin命令管理，不建议手动编辑 │ ├── config.json # 全局插件配置 │ ├── installed_plugins.json # 已安装插件列表 │ ├── known_marketplaces.json # 插件市场源配置 │ └── marketplaces/ # 已克隆的插件市场内容与插件文件 ├── ide/ # IDE集成相关的锁文件与通信文件，供IDE扩展与CLI进程协调使用 ├── sessions/ # Desktop应用的会话窗口状态，用于应用重启后恢复多会话面板，CLI不使用此目录 ├── cache/ # 通用运行时缓存，存放插件、MCP或其他功能的临时计算结果 ├── daemon/ # 后台守护进程的PID与socket文件，用于CLI与IDE插件之间的进程管理与通信协调 ├── telemetry/ # 遥测数据缓存，用于记录匿名使用统计与性能指标，用于产品优化，可通过配置关闭 ├── statsig/ # 用于产品功能灰度和实验控制的配置缓存，属于产品优化的“开关层”。 ├── projects/ # 项目级持久数据存储，包含会话记录与衍生记忆 │ └── <project-path>/ # 以代码库绝对路径命名 │ ├── <session>.jsonl # 完整某次会话记录，默认保留30天，通过cleanupPeriodDays修改 │ ├── <session>/ # 单次会话附属数据目录 │ │ ├── subagents/ # 子代理会话记录，随父session一起清理 │ │ └── tool-results/ # 大型工具输出溢出文件 │ └── memory/ # Claude自动积累维护的项目知识，永久保留 │ ├── MEMORY.md # 记忆索引，每次启动加载前200行或25KB，由Claude自动维护 │ └── <topic>.md # 按主题拆分的记忆文件，如debugging.md，相关任务时按需加载 ├── file-history/ # Claude Code修改文件前做的的快照，用于后续/undo回滚 │ └── <session>/ # 按会话存储的文件原始状态，默认保留30天 ├── plans/ # 在Plan Mode下生成的任务计划草稿和执行蓝图的存储目录，默认保留30天 ├── paste-cache/ # 粘贴内容临时缓存，避免直接塞进对话导致上下文污染或超长问题，默认保留30天 ├── image-cache/ # 粘贴/拖入的图像文件缓存，默认保留30天 ├── session-env/ # 每会话环境元数据，用于调试与恢复运行上下文，默认保留30天 ├── tasks/ # 在执行复杂任务时自动生成的结构化任务拆解列表，默认保留30天 ├── shell-snapshots/ # 在执行Bash工具时，对运行环境做的快照，用于异常恢复，正常退出时删除 ├── debug/ # --debug或/debug启动时生成的调试日志，默认保留30天 └── backups/ # ~/.claude.json配置迁移前的时间戳备份，默认保留30天 Claude Code的全局目录用于定义跨项目的个人默认行为和工具能力，而项目级目录用于存储单个代码库的专属规则、记忆与执行上下文，项目级目录如下：MyProject/ # 示例项目根目录 ├── CLAUDE.md # 项目总纲领，包含项目说明书、行为约束、工作规范等 ├── CLAUDE.local.md # 本地个人偏好，不会被提交至Git ├── .mcp.json # 项目专用MCP服务器配置 ├── .worktreeinclude # 指定git worktree新建工作树时需一并复制的文件列表 └── .claude/ # 项目级专属配置目录 ├── settings.json # 项目共享设置 ├── settings.local.json # 仅本机生效的项目设置覆盖，不会被提交至Git ├── rules/ # 项目专用规则库，功能与全局目录中的类似 ├── skills/ # 项目专用技能库，功能与全局目录中的类似 ├── agents/ # 项目专用自定义子Agent，功能与全局目录中的类似 ├── workflows/ # 项目专用工作流脚本，功能与全局目录中的类似 ├── agent-memory/ # 项目级专用子代理记忆库，功能与全局目录中的类似 └── output-styles/ # 项目级输出格式约束，功能与全局目录中的类似 能看到，项目级的目录和全局目录有很多配置是重复的，所以如果在项目级目录中配置，则会覆盖全局目录里面的配置。此外，上述结构可能并不完整。Codex相较于Claude Code可以随意配置大模型API的灵活性，Codex更像是一个GPT专用产品，一般都是充值GPT Plus或者Pro使用，当然也有一些手段去强制切换Codex的大模型API，不过需要一些中间件。Codex的文件目录如下所示：~/.codex/ ├── config.toml # CLI全局配置控制模型工具权限与默认行为 ├── auth.json # 存储登录认证信息与访问凭证 ├── installation_id # 当前安装实例唯一标识 ├── version.json # 当前CLI版本信息 ├── cap_sid # 当前能力会话标识用于功能协商 ├── history.jsonl # 用户输入历史索引 ├── models_cache.json # 模型能力与路由缓存 ├── personality_migration # 个性化配置迁移标记 ├── sessions/ # 会话事件流按日期存储 │ └── YYYY/ │ └── MM/ │ └── DD/ │ └── rollout-<timestamp>-<uuid>.jsonl # 完整会话日志 ├── skills/ # Skills能力包与自定义技能 ├── cache/ # 网络请求与资源缓存 ├── tmp/ # 临时运行文件 ├── .tmp/ # 系统内部临时目录 ├── .sandbox # sandbox配置 ├── .sandbox-bin # sandbox执行入口 ├── .sandbox-secrets # sandbox隔离密钥 ├── sandbox-YYYY-MM-DD.log # sandbox运行日志 ├── goals_<n>.sqlite # Agent任务规划数据库 ├── goals_<n>.sqlite-shm # SQLite共享内存 ├── goals_<n>.sqlite-wal # SQLite预写日志 ├── memories_<n>.sqlite # 长期记忆数据库 ├── state_<n>.sqlite # Agent运行状态数据库 ├── state_<n>.sqlite-shm # SQLite共享内存 ├── state_<n>.sqlite-wal # SQLite预写日志 ├── logs_<n>.sqlite # 结构化日志数据库 ├── logs_<n>.sqlite-shm # SQLite共享内存 └── logs_<n>.sqlite-wal # SQLite预写日志 可以明显感觉到，其实Codex不像Claude Code一样，按project划分聊天记录，而是按照session直接划分，且多了sandbox相关文件。CopilotCopilot其实还可以细分一下，即Copilot CLI和VSCode Copilot。不过大多数情况下，VSCode Copilot的使用比较多一些。Copilot CLI和VSCode Copilot的区别不在于能力强弱或能不能做某些事情，而在于交互方式：它们本质上共享同一套模型能力，VSCode Copilot更偏向IDE内的实时编码辅助，即补全、理解、人工逐步确认，而Copilot CLI更偏向终端中的任务驱动式交互，即用自然语言生成并组织命令流程。因此差异主要是使用场景和操作形态，而不是底层能力本身。这里我们先简单介绍一下Copilot CLI的目录文件组织形式，然后主要介绍VSCode Copilot。如下是Copilot CLI的目录文件组织形式：~/.copilot/ # Copilot CLI用户主目录，存放所有配置状态会话与插件数据 ├── settings.json # 用户主配置文件，用于模型界面行为和功能偏好设置 ├── mcp-config.json # MCP服务器配置文件，用于所有会话共享工具连接配置 ├── lsp-config.json # 语言服务器配置文件，用于代码智能提示与补全能力 ├── instructions/ # 全局指令目录，用于定义所有会话通用行为规则 │ └── *.instructions.md # 全局指令文件，用于自动加载的长期行为约束 ├── agents/ # 全局自定义Agent目录，用于定义可复用任务执行智能体 │ └── *.agent.md # Agent定义文件，用于描述任务规划与执行逻辑 ├── skills/ # Skills能力模块目录，用于封装可复用任务能力 │ └── */SKILL.md # Skill定义文件，用于提供结构化任务能力 ├── copilot-instructions.md # 全局默认指令文件，用于所有会话基础行为约束 ├── extensions/ # CLI扩展目录，用于安装和管理功能扩展 ├── hooks/ # 钩子目录，用于在特定生命周期触发自定义脚本 ├── permissions-config.json # 权限配置文件，用于记录目录与工具访问授权 ├── config.json # 内部状态文件，用于保存运行状态与实验配置 ├── session-state/ # 会话状态目录，用于保存每次对话运行过程数据 │ └── <session-id>/ # 单个会话目录，用于存储一次完整交互状态 │ ├── events.jsonl # 会话事件日志文件，用于记录完整交互过程 │ ├── plans/ # 计划目录，用于存放任务执行计划数据 │ └── checkpoints/ # 检查点目录，用于保存阶段性执行状态 ├── command-history-state/ # 命令历史目录，用于记录终端输入历史 ├── session-store.db # 会话索引数据库，用于跨会话检索与恢复 ├── logs/ # 日志目录，用于存放运行调试与错误日志 │ └── *.log # 日志文件，用于记录运行过程与错误信息 ├── installed-plugins/ # 插件安装目录，用于存放来自市场或Git的插件 │ ├── <marketplace>/ # 市场插件目录，用于官方插件来源 │ └── _direct/ # 直接安装插件目录，用于Git或URL安装插件 ├── plugin-data/ # 插件数据目录，用于存储插件运行时持久化数据 ├── ide/ # IDE通信目录，用于VSCode等编辑器连接状态管理 ├── mcp-secrets/ # MCP密钥目录，用于存储认证令牌与备用凭证 相比之下，VSCode Copilot的文件目录组织形式就要复杂很多了，它相当于一个扩展，寄生于VSCode的目录里面。Linux下VSCode的用户配置根目录为~/.config/Code/User/，Windows下为%APPDATA%\Code\User\，我们这里以Windows为例。首先，我们先看一下VSCode的用户目录，当然不同版本之间可能存在差异：%APPDATA%\Code\User\ # VSCode用户配置根目录 ├── settings.json # 用户全局配置文件用于编辑器行为扩展配置 ├── keybindings.json # 自定义快捷键绑定配置文件 ├── tasks.json # 任务运行系统配置文件用于脚本执行 ├── launch.json # 调试配置文件用于debug运行参数 ├── snippets/ # 代码片段目录用于全局代码模板 │ └── *.json # 各语言代码片段定义文件 ├── profiles/ # VSCode多配置环境用于隔离开发环境 │ └── <profile-id>/ # 单个profile隔离配置目录 ├── globalStorage/ # 扩展全局存储用于跨workspace持久化数据 │ └── github.copilot-chat/ # Copilot Chat扩展全局状态存储 │ ├── chatConfig.json # Chat功能配置缓存 │ ├── models.json # 模型路由配置与可用模型列表 │ ├── state.json # 扩展运行状态缓存 │ └── telemetry/ # 遥测与使用数据记录 ├── workspaceStorage/ # 工作区级扩展状态核心存储 │ └── <workspace-hash>/ # 每个项目唯一hash目录 │ ├── state.vscdb # SQLite扩展状态数据库核心文件 │ ├── state.vscdb-wal # SQLite写前日志文件 │ ├── state.vscdb-shm # SQLite共享内存文件 │ ├── workspace.json # 当前workspace元信息用于记录项目路径和结构 │ ├── chatSessions/ # Copilot Chat会话记录目录 │ │ └── *.jsonl # 按事件流记录的对话日志 │ ├── chatEditingSessions/ # Copilot Agent编辑会话记录 │ │ └── *.json # AI多文件修改操作记录 │ ├── GitHub.copilot-chat/ # Copilot工作区索引与缓存目录 │ ├── extensionStorage/ # 其他扩展工作区数据存储 │ ├── history/ # 工作区文件操作历史记录 │ └── backups/ # 自动保存与崩溃恢复文件 ├── sync/ # 设置同步本地缓存用于账号同步状态 └── History/ # 恢复上次编辑状态的轻量行为缓存 可以看到，当VSCode打开某个工作区时，会在workspaceStorage目录下生成一个项目目录，然后所有的Copilot对话日志都会放到这个项目目录里面。此外，VSCode还有远程目录，结构如下：~/.vscode-server/data/User/ # 远程服务器上的目录 ├── workspaceStorage/ # 与本地一致的工作区存储 │ └── <workspace-hash>/ # 项目目录 当用VSCode连接远程服务器目录时，会在本地和远程服务器中，各自生成一个具有相同哈希名字的项目目录。当使用Copilot在远程服务器上对话时，包含local和copilot cli模式，会将对话记录存储在本地的workspaceStorage，而非远端。VSCode Copilot还有项目级目录，结构如下：MyProject/.github/copilot/ # Copilot项目级配置目录 ├── settings.json # 项目级Copilot行为配置 ├── instructions.md # 项目级AI行为指令 └── prompts/ # 预设Prompt 这个项目目录的作用其实和Claude Code的项目目录差不多，都是写给这个项目的一些专属配置。此外，VSCode Copilot自带Claude Code和Codex的SDK，即使本地没有安装也可以直接使用Claude Code，不过不能切换大模型API，且消耗的是Copilot的额度，最终会将聊天记录保存到~/.claude/projects中。如果通过远程连接，则会创建至远程服务器的.claude文件夹中，如果远程主机上原来没有Claude Code，则会自动创建一个.claude文件夹，用于存储数据。如果远程服务器上装了Claude Code，那么VSCode能够自动识别Cluade Code在当前项目文件夹中的聊天记录，即VSCode Copilot中的Claude Code SDK和自行安装的Claude Code其实是共用~.claude文件夹的。值得指出的是，通过claude --resume命令一般看不全所有对话，但是VSCode却能全部列出来。这里还要分享一个小技巧，如果想在远程服务器上不安装Claude Code的情况下，使用本地VSCode中的Claude Code，且想自己配置大模型API，则需要安装Claude Code For Vscode插件，并在VSCode中的setting->extension->Claude Code中设置setting，具体修改内容可参考如下：{ "files.autoSave": "afterDelay", "workbench.startupEditor": "none", "workbench.colorTheme": "Dark Modern", "claudeCode.preferredLocation": "panel", "claudeCode.environmentVariables": [ { "name": "ANTHROPIC_BASE_URL", "value": "https://xxxx" }, { "name": "ANTHROPIC_AUTH_TOKEN", "value": "xxxxx" }, { "name": "ANTHROPIC_MODEL", "value": "mimo-v2.5" }, { "name": "ANTHROPIC_DEFAULT_SONNET_MODEL","value": "mimo-v2.5"}, { "name": "ANTHROPIC_DEFAULT_OPUS_MODEL", "value": "mimo-v2.5-pro" }, { "name": "ANTHROPIC_DEFAULT_HAIKU_MODEL", "value": "mimo-v2.5" } ] } 这个插件相当于重新安装了一份Claude Code，而没有使用VSCode Copilot中的，不过最终会被VSCode Copilot识别，并自动添加至VSCode Copilot扩展界面中，如下所示：实际上，这两种方式使用的Claude和Codex功能上并没有区别，只不过一个基于Copilot的额度，一个可以自定义模型API。一些小tricks一般涉及到远程开发，通常会用vscode连接许多服务器，然而，在所有服务器上面都安装codex或claude code很不优雅，于是有个比较优雅的解决方案是，直接在vscode中安装codex和claude code扩展，这样便可以免去在服务端中再安装agent cli，而且，claude code的扩展可以直接在vscode中配置第三方api。如果想用codex和claude code官方的登录方式，而不是用api，则需要开启本地机器的代理，于是又涉及到配置本地系统代理，有点不优雅，所以，这种情况下，就必须在服务端中安装codex和claude code的客户端了，并在服务端中配置好系统代理，这样当vscode打开codex或claude code时，会惊奇的发现，它居然直接使用了远端的cli，于是便不再需要配置本地的系统代理，如果使用本地的codex，则需要在.codex中添加.env文件，配置代理，不然websocket代理会不正常，详见。相较于codex、claude code等Agent的CLI，用vscode的扩展会更爽，因为不仅有图形化界面，可以直观看见代码修改的前后对比，还有针对ide的优化，比如鼠标选中的内容或者打开的文档可以直接加载到当前会话上下文中，比CLI爽多了。体验了claude code和codex一段时间，发现一个主要的区别是，claude code默认情况下每次写文件/执行命令都会要求用户点击同意才会继续，而codex却只有执行命令才会让用户审查是否同意，这一点很烦啊，如果事先没有git备份，codex一乱改直接炸了，此外，总而言之，claude code高兼容性，可以随意配置第三方API，安全意识较高，codex深度绑定GPT生态，且倾向于一键化任务，减少人工干预。References[1] https://code.claude.com/docs/en/overview[2] https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
本篇博文主要梳理Web网站框架和技术栈，旨在更深刻理解Web工作原理，助于渗透测试。其实网站种类很多，架构也五花八门，这里从大类角度，将网站分为静态网站和动态网站。静态网站静态网站是最简单的一类网站。其核心特点是：Web服务器直接返回已经存在的HTML文件，而不会在访问时动态生成页面内容。访问流程如下：浏览器->Web服务器->HTML文件 例如客户端直接访问https://example.com/index.html，服务器直接读取网站目录下的index.html文件并返回给浏览器客户端。这里有个常见的误区，即只有动态网站才会使用Web服务器。实际上，无论是静态网站还是动态网站，都离不开Web服务器。Web服务器（Web Server）本质上是一种能够接收HTTP请求并返回HTTP响应的软件。例如，当用户在浏览器中访问某个URL，浏览器会向服务器发送HTTP请求，Web服务器接收到请求后，会根据配置找到对应的资源并返回给浏览器。常见的Web服务器包括Nginx、Apache、Caddy和IIS等，常用于本地测试的Python http.server模块也可看作Web服务器。此外，还有一种比较特殊的、经常用于搭建个人博客或项目文档的网站部署方式，即静态网站托管服务（Static Website Hosting Service）。这类服务通常由平台提供现成的服务器、HTTPS证书、CDN加速以及自动部署能力，用户只需上传或同步静态网站文件即可完成发布，无需自行购买服务器或配置Web服务器。常见的静态网站托管服务包括GitHub Pages、Cloudflare Pages、Netlify和Vercel等。由于这类服务通常仅负责托管HTML、CSS、JavaScript、图片等静态资源，无法直接运行PHP、Java、Python等服务端程序，因此本质上仍属于静态网站的范畴。现代开发中，静态网站早已脱离了“纯手写HTML”的原始阶段。开发者通常使用静态网站生成器（Static Site Generator，SSG），常见的如Hexo、Hugo、VitePress）来自动生成页面。此外，借助JAMstack架构，静态网站可以通过JavaScript在客户端调用第三方API或Serverless函数。这意味着，即使网站本质上是静态托管的，依然可以实现动态评论、在线搜索、用户登录等丰富的交互功能。HTML、CSS和JavaScript是构建网页最基础的前端技术，也是绝大多数网站前端部分的核心组成。静态网站具有部署简单、访问速度快、服务器资源消耗低以及安全风险较少等显著优点。然而，其缺点也同样明显：由于页面内容固定，导致用户的交互能力有限，一旦数据需要更新，就必须重新生成整个页面。动态网站动态网站主要是指页面内容并非预先生成并存储为固定的HTML文件，而是在用户访问时由服务端程序或客户端脚本根据请求动态生成。与静态网站相比，动态网站能够根据用户身份、数据库内容以及业务逻辑实时返回不同的页面和数据。通信流程如下所示：浏览器->Web服务器->应用服务器->数据库->HTML代码 传统一体化传统一体化是早期Web开发最常见的模式。在这种架构下，前端页面和后端业务逻辑运行在同一个应用系统中，由后端程序直接生成HTML页面并返回给浏览器。一个较为完整的动态网站架构，通常包含Web服务器、应用服务器和数据库等组件。其中，应用服务器（Application Server）负责处理业务逻辑、访问数据库并动态生成HTML代码返回给Web服务器，最终由Web服务器返回给客户端。在实际部署中，并非所有动态网站都会包含上述全部组件。例如一些小型网站可以直接由Tomcat、Spring Boot或Node.js运行时对外提供Web服务，而无需额外部署Nginx或Apache等独立Web服务器。这是因为许多现代应用服务器或运行环境本身已经具备处理HTTP请求和返回HTTP响应的能力；对于数据量较小或内容固定的场景，网站甚至可以不接入数据库。此外，在LAMP（Linux+Apache+MySQL+PHP）和LNMP（Linux+Nginx+MySQL+PHP）等经典PHP架构中，通常不存在类似Tomcat、WebLogic等独立的应用服务器。Web服务器会根据配置规则匹配请求，通常通过请求文件的后缀名等，将PHP请求转发给PHP运行环境，如mod_php或PHP-FPM执行，PHP解释器负责处理业务逻辑、访问数据库并生成动态页面。从功能上看，PHP运行环境承担了应用服务器的职责，因此也可以视为应用服务器层的一种实现形式。当前互联网网站最常见的服务端技术生态主要包括PHP、Java、Python、.NET以及Node.js，它们覆盖了绝大多数企业网站、管理系统和互联网应用。除此之外，Go、Ruby、Rust等语言也被广泛应用于部分特定场景。不同开发语言可以使用相同的Web服务器，但应用服务器通常与开发语言密切相关。为了简化页面渲染、路由管理、数据库访问等开发工作，各种语言生态逐渐出现了大量Web框架，用于快速构建动态网站。下表列出了常见的传统一体化网站开发技术栈，具体如下表所示：开发语言应用服务器Web框架PHPPHP-FPM、mod_phpThinkPHP、LaravelJavaTomcat、Jetty、Undertow、WebLogicSpring Boot、Spring MVC、JSP/ServletPythonGunicorn、uWSGIDjango、FlaskC#KestrelASP.NET Core、ASP.NET MVCJavaScriptNode.jsExpress、Next.jsGonet/http packageBeegoRubyPuma、PassengerRuby on Rails、Sinatra传统一体化架构的优点是开发简单、部署方便，前后端代码集中在同一个项目中，适合中小型网站快速开发。然而随着业务规模增长，前端与后端代码耦合较高，团队协作和系统扩展会逐渐变得困难。因此，现代互联网应用更多采用前后端分离架构。前后端分离前后端分离是目前互联网应用最主流的Web开发架构。与传统一体化模式中由后端直接生成HTML页面不同，前后端分离将页面展示与业务逻辑彻底拆分：前端负责页面渲染与用户交互，后端则专注于业务处理和数据提供，两者通过API接口进行通信。在这种架构下，浏览器首次访问时，Web服务器会返回前端页面资源（HTML、CSS、JavaScript）。随后，JavaScript代码在浏览器中运行，通过HTTP接口向后端请求数据。后端通常以JSON格式返回数据，前端再根据返回结果动态更新页面内容。与传统一体化架构相比，前后端分离架构中的应用服务器不再负责生成HTML页面，而是通过API接口向浏览器返回JSON等结构化数据。浏览器加载的前端页面（通常由Vue、React等框架开发）负责解析数据并完成页面渲染。在实际部署中，前端项目通常会被编译为HTML、CSS和JavaScript等静态文件，并部署在Nginx、Apache、CDN或静态网站托管平台上；后端服务则独立部署在应用服务器、容器平台或云服务中，通过API接口向前端提供数据。常见的技术栈如下表所示：层级常见技术前端开发框架Vue、React、Angular前端构建工具Vite、Webpack后端API框架Spring Boot、Django、Flask、Express、Gin数据交换格式JSON接口规范REST API、GraphQL各层次的作用如下：前端开发框架：用于构建用户界面和实现页面交互逻辑，负责将数据渲染为用户可见的网页内容。前端构建工具：用于将开发阶段的源码进行编译、打包和优化，生成可直接部署到Web服务器的静态文件。后端API框架：用于实现业务逻辑、处理用户请求、访问数据库，并通过接口向前端提供数据服务。数据交换格式：用于前后端之间传输数据，其中JSON因结构清晰、跨语言支持良好而成为最常用格式。接口规范：用于定义前后端之间的数据交互方式和接口设计规则，保证不同系统能够统一通信。前后端分离架构的优点是职责清晰、便于团队协作、前后端可以独立开发和部署，同时能够支持Web、移动端、小程序等多个客户端共享同一套后端接口，因此非常适合中大型互联网应用。然而，该架构也会增加系统复杂度，需要额外处理接口认证、跨域访问、前后端版本兼容等问题，对开发和运维能力提出了更高要求。References[1] https://aws.amazon.com/cn/compare/the-difference-between-web-server-and-application-server/
这篇文章讲一下申请SSL证书的几种流程，包括Web、ACME等方式。SSL证书签发机制CA（Certificate Authority，证书颁发机构）是互联网中的“可信第三方”，负责验证域名所有权并签发SSL/TLS证书。CA的核心职责不是为域名生成密钥对，而是对用户生成的公钥进行签名，证明这个公钥确实属于某个身份，如域名、企业等。浏览器之所以信任某个网站的证书，不是因为信任网站本身，而是因为该证书最终能够追溯到浏览器或操作系统内置的受信任根CA。像Let's Encrypt、DigiCert、Sectigo等都是被主流浏览器信任的公开CA，而腾讯云、阿里云等证书申请平台通常只是代办或管理平台，真正签发证书的仍然是这些CA。判断一个CA是否可信，最简单的方法就是看它的根证书是否被Chrome、Firefox、Windows、macOS等主流系统和浏览器默认信任。此外，如果一个恶意SSL证书被浏览器/系统信任，那么这个恶意证书所有者便可以伪装服务端，对客户端发起中间人攻击。Web端申请这是最传统的SSL证书申请方式，流程如下：登录证书申请网站——>填写申请域名信息——>完成域名验证——>下载证书——>部署证书 常见的证书申请平台包括ZeroSSL、腾讯云SSL证书服务、阿里云SSL证书服务等。其中，DNS TXT记录验证是目前最常见的方式之一，通过在域名的DNS配置中添加指定的TXT记录，证书颁发机构（Certificate Authority，CA）即可验证申请者对域名的控制权。ACME端申请ACME 全称：Automated Certificate Management Environment，中文一般翻译为自动化证书管理环境，它本身是一种协议，由IETF（Internet Engineering Task Force）制定。除了ACME，IETF还制定了HTTP、DNS、TCP/IP等等众多协议，包括先前博文提到的RFC文档也是这个组织发布的。相较于繁琐的Web控制台申请方式，ACME是目前最主流的SSL证书申请方式，其大致流程如下：安装ACME客户端——>向CA发起证书申请——>CA下发Challenge——>客户端自动完成域名验证——>CA签发证书——>客户端自动下载证书——>部署证书 在RFC 8737中，ACME的Challenge一共就3种，如下表所示：Challenge验证方式端口需求支持泛域名HTTP-01Web 服务80否DNS-01DNS 记录无是TLS-ALPN-01TLS 服务443否下面详细介绍3种Challenge的认证机制HTTP-01在ACME协议中，HTTP-01 Challenge是最常见的域名所有权验证方式。其核心思想是：通过验证申请者是否能够控制目标域名对应的Web服务，来证明其拥有该域名的管理权限。当客户端向 CA 申请证书时，CA 会生成一个随机的 Challenge Token，并要求客户端将该 Token 按照规定格式放置在 Web 服务器的指定路径下：http://<domain>/.well-known/acme-challenge/<token> 随后，CA会主动向该URL发起HTTP请求。如果能够成功获取到预期内容，则认为申请者拥有该域名的控制权，验证通过后即可签发证书。这个临时的Web服务，通常由ACME客户端自动化轻量化提供，所以，通过这种方式签发SSL证书时，一定要确保主机80端口没有被占用。DNS-01DNS-01是ACME协议中基于DNS记录的域名验证方式。与HTTP-01不同，DNS-01不依赖Web服务，而是通过验证指定DNS TXT记录的内容来确认申请者拥有目标域名的控制权。由于域名解析记录通常只有域名管理者才能修改，因此能够成功创建指定TXT记录即可证明对该域名拥有管理权限。当客户端向 CA 申请证书时，CA 会生成一个随机 Challenge Token，并要求客户端在目标域名下创建一条特定的 TXT 记录：_acme-challenge.<domain> 客户端将计算得到的 Challenge Value 写入该 TXT 记录后，CA 会主动查询 DNS 服务器。如果查询结果与预期内容一致，则验证通过，CA 随后签发证书。通过ACME客户端申领时，由于要配置DNS解析，所以要提供DNS服务商的API凭证，以确保ACME客户端能够自动化配置DNS解析。这种方式其实和Web端申领SSL证书的原理一样。TLS-ALPN-01与HTTP-01通过HTTP文件验证不同，TLS-ALPN-01通过TLS握手过程中的ALPN（Application-Layer Protocol Negotiation）扩展完成域名所有权验证。该方式要求申请者能够控制目标域名对应的 HTTPS 服务，因此常用于无法开放80端口但能够开放443端口的场景。当客户端申请证书时，CA会生成一个Challenge Token，并要求客户端在443端口提供一个特殊的临时证书。随后，CA会与目标域名建立TLS连接，并在TLS握手阶段通过ACME专用的 ALPN 标识，acme-tls/1请求验证。如果服务器返回的临时证书中包含正确的Challenge信息，则验证通过，CA随后签发正式证书。自签证书除了上述两种证书申请模式，还有一种自签证书。自签名证书是由证书持有者自行签发的数字证书，其签发者（Issuer）和主体（Subject）为同一实体。与由公开证书颁发机构（CA）签发的证书不同，自签名证书不需要经过第三方机构认证，因此浏览器和操作系统默认不会信任该证书。使用OpenSSL可快速生成自签名证书：openssl req \ -x509 \ -newkey rsa:2048 \ -nodes \ -keyout server.key \ -out server.crt \ -days 365 生成文件：server.key 私钥server.crt 自签名证书随后即可配置到Nginx、Apache或其他TLS服务中使用。此外，自签证书可以随时将公钥发送给CA机构进行签名，签名后即可受浏览器或操作系统信任。ACME客户端介绍常见 ACME 客户端ACME客户端负责与CA通信、完成Challenge验证、申请证书以及自动续期。当前主流的 ACME 客户端如下：客户端平台开发语言特点CertbotLinux/UnixPython官方推荐，功能完善acme.sh跨平台Shell轻量级，DNS API 支持丰富lego跨平台Go单文件程序，易于集成win-acmeWindowsC#适用于 IIS 和 Windows Servercert-managerKubernetesGoKubernetes 原生证书管理比如，使用acme.sh，可通过以下命令迅速签发一个SSL证书~/.acme.sh/acme.sh --issue --standalone --domain example.com 所以，由于ACME协议的便捷性，已经成为SSL证书申请的主流方式。CA证书与SSL证书SSL证书又称服务器证书，是颁发给特定服务器或域名的数字证书，其主要作用是证明服务器身份并建立加密通信通道。当用户访问HTTPS网站时，服务器会向浏览器发送自己的SSL证书，其中包含网站域名、公钥、有效期以及证书签名等信息。浏览器通过验证证书的合法性，确认当前访问的服务器确实属于目标网站，而不是攻击者伪造的服务器。SSL证书通常仅用于自身身份认证，不具备签发其他证书的能力，其证书属性中一般包含“CA: FALSE”，表示该证书不能作为证书颁发机构使用。CA（Certificate Authority）即证书颁发机构，负责签发和管理数字证书。CA证书本质上也是一种数字证书，但与普通SSL证书不同的是，它拥有签发下级证书的权限。常见的公共CA机构包括DigiCert、GlobalSign、Sectigo以及Let's Encrypt等，它们通过自己的CA私钥为网站签发SSL证书。CA证书通常包含“CA: TRUE”属性，表示其具备签发其他证书的能力。根据层级不同，CA证书又可以分为根CA证书和中间CA证书，共同构成互联网证书信任体系的基础。CA证书与SSL证书并不是相互独立的，而是共同组成了一条完整的证书信任链。典型的HTTPS证书链由网站SSL证书、中间CA证书以及根CA证书构成。当浏览器收到网站证书后，会逐级验证证书是否有效、是否由可信CA签发，以及最终是否能够追溯到系统预置的根CA。只有整个信任链验证通过后，浏览器才会认为该网站是可信的。因此可以简单理解为，SSL证书负责证明“我是谁”，而CA证书负责证明“我有资格证明别人是谁”。典型的HTTPS证书链如下：网站SSL证书->中间CA证书->根CA证书 网站证书是否有效；网站证书是否由可信CA签发；CA证书是否最终链向系统信任的根CA。CA证书的自签方式与SSL证书大差不差，主要在属性区别即CA:TRUE信任一个SSL证书，通常只意味着信任某一个具体服务器。例如信任邮件服务器mail.example.com的证书，其影响范围一般仅限于该服务器本身。而信任一个CA证书，则意味着信任该CA未来签发的所有证书。理论上，只要拥有对应CA的私钥，就可以为任意域名签发证书，因此系统对根CA证书的管理极为严格。一旦根CA私钥泄露，攻击者便可能伪造大量网站证书，从而威胁整个证书信任体系的安全。因此，从权限和影响范围来看，信任CA证书的风险远高于信任单个SSL证书。Burp Suite等抓包工具能够解密HTTPS流量，其核心原理正是利用CA证书。用户在设备中安装并信任Burp生成的根CA后，Burp便可以在访问网站时动态生成对应域名的SSL证书，并使用自己的CA私钥进行签名。由于客户端已经信任Burp CA，因此会认为这些证书合法，从而允许建立TLS连接。Burp借此同时与客户端和目标服务器建立两条独立的加密通道，实现HTTPS流量的解密、分析与修改。如果Burp使用的是普通SSL证书而不是CA证书，则无法为任意网站动态签发证书，也就无法实现HTTPS抓包功能。
好久没有更新博客了，最近在忙很多事情，今天突来的兴致，把之前梳理的一些文档整理成博客，今天先搞一个ModSecurity+Nginx搭建流程。补充OpenResty由于原版的Nginx不能对请求进行精细化控制，所以出现了OpenResty，即Nginx+Lua。OpenResty可以通过Lua语言，写相应的脚本，从而实现将Nginx接受的HTTP请求，原封不动的转为文本，然后发送给WAF的API。OpenResty安装ModSecurity与Nginx几乎一模一样，其版本与Nginx版本对应。make时要用gmakewget https://openresty.org/download/openresty-1.25.3.2.tar.gz # 编译用gmake gamke gamke install 1. 编译安装ModSecurity参考ModSecurity官方文档，找到指定系统的编译安装指令。Ubuntu 22.04参考22.10，下述补充了OpenResty编译需要的库。sudo apt-get install git g++ apt-utils autoconf automake build-essential libcurl4-openssl-dev libgeoip-dev liblmdb-dev libpcre2-dev libtool libxml2-dev libyajl-dev pkgconf zlib1g-dev wget libssl-dev libpcre3 libpcre3-dev curl git clone https://github.com/owasp-modsecurity/ModSecurity cd ModSecurity/ git submodule init git submodule update sh build.sh ./configure --with-pcre2 make make install ./configure能正常输出依赖结果即可2. 下载ModSecurity-nginx连接器git clone https://github.com/owasp-modsecurity/ModSecurity-nginx 只需下载，无需做任何操作。3. 编译安装Nginx注：由于WAF需要，应该将Nginx替换为OpenResty。使用ModSecurity必须用源码方式安装Nginx，注意不同ModSecurity版本对Nginx的要求wget https://nginx.org/download/nginx-1.26.1.tar.gz tar -zxvf nginx-1.26.1.tar.gz cd nginx-1.26.1 将下载好的ModSecurity-nginx第三方模块静态编译进Nginx，同时开启NginxSSL等模块，这些模块在APT方式安装的二进制Nginx里面都已经被编译进去了，自己编译Nginx时，需要手动指定。./configure --add-module=/path/to/ModSecurity-nginx --with-http_ssl_module make make install 如果报错没有发现OpenSSL库，则需安装，Ubuntu安装命令如下：apt install libssl-dev 安装完成后，找到configure中显示的nginx安装目录，执行下述命令查看当前Nginx配置cd /usr/local/nginx ./sbin/nginx -V 回显如下表示正常：root@c4dcf83416c9:/usr/local/nginx/sbin# ./nginx -V nginx version: nginx/1.26.1 built by gcc 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04.2) built with OpenSSL 3.0.2 15 Mar 2022 TLS SNI support enabled configure arguments: --add-module=/root/ModSecurity-nginx --with-http_ssl_module 4. 下载ModSecurity配置文件ModSecurity是根据某些规则运作的，所以要下载相应的规则配置文件。git clone https://github.com/coreruleset/coreruleset.git 将最开始下载的ModSecurity的源码中的modsecurity.conf-recommended文件重命名为modsecurity.conf，将此文件同unicode.mapping一起移动到coreruleset文件夹中将下载的规则文件coreruleset下的crs-setup.conf.example重命名为crs-setup.conf。并将下载的规则导入modsecurity.conf，示例如下：# 并修改SecRuleEngine，使其从DetectionOnly变为On。 SecRuleEngine On Include /path/to/coreruleset/crs-setup.conf Include /path/to/coreruleset/rules/*.conf 5. 在Nginx中配置ModSecurity切到指定Nginx配置目录，修改nginx.conf文件，开启ModSecurity，并指定ModSecurity配置路径，详细查看ModSecurity-nginx的配置介绍。可以将其配置进http{}、server{}和location{}。示例如下：# 所有server的所有location共用一套规则 http { modsecurity on; modsecurity_rules_file /path/to/modsecurity.conf; 6. 运行Nginx通过nginx -t测试配置文件可用性，通过后启动nginx，测试waf是否生效curl -v 'https://your-domain.com/?test= ' 返回403表示生效。
最近在研究HTTP 2相关的协议，且大多数CDN都是默认HTTP 2，所以记录一下HTTP 2的详细通信流程。1、HTTP 2概述据Cloudflare统计，HTTP 2网站已经占到所有网站的50%，HTTP 3则占到了20.5%。所以HTTP 2在如今的网络环境中越来越重要。HTTP 2的RFC文档在此。HTTP 2通信常被分为两类，即h2c和h2。h2c：是基于HTTP 1.1 Upgrade试探升级HTTP 2，或在C/S框架中客户端已知服务端支持HTTP 2，直接发送HTTP 2数据包，不论哪一种，其通信过程是都是明文的，这是极其不安全的，主流浏览器都不支持，也很少有人使用，所以这里我们不做讨论。h2：是被大多数人推广使用的，即在TLS的ClientHello包中的ALPN(Application Layer Protocol Negotiation)扩展中就声明支持h2，若服务端支持h2，则会选中该扩展，发回客户端。随后TLS连接建立完之后，就可以直接用HTTP 2通信了。2、帧结构HTTP 2的帧（Frame）结构由固定9字节帧头（Frame Header）和可变长度帧负载（Frame Payload）组成，定义于RFC 7540。字段长度说明Length24bitPayload长度Type8bit帧类型Flags8bit与帧类型相关的标志位R1bit保留位，必须为0Stream Identifier31bit流ID，0表示连接级帧Length：默认大小区间为为0~16,384(2^14)，一旦超过默认最大值2^14(16384)，发送方将不再允许发送，除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE（一般此值区间为2^14 ~ 2^24）值的通知Type：帧类型，共10种，如下表所示Flags：与帧类型相关的标志位，如END_STREAM表示流的一端关闭、END_HEADERS表示Header块结束、ACK表示确认SETTINGS或PING帧R：保留位，必须为0Stream Identifier：帧所属的流ID，0表示连接帧，如SETTINGS、PING、GOAWAY，奇数表示由客户端创建的流，偶数表示由服务端建立的流Type帧类型简介0x0DATA承载请求或响应的实体数据，受流量控制机制约束0x1HEADERS传输请求或响应的首部块，使用HPACK压缩0x2PRIORITY指定流的优先级和依赖关系0x3RST_STREAM异常终止一个已存在的流0x4SETTINGS交换连接级参数配置，仅使用流ID 00x5PUSH_PROMISE服务器推送时，声明即将发送的请求0x6PING连接保活、RTT测量，携带8字节不透明数据0x7GOAWAY通知对端即将关闭连接并指明最后可处理流0x8WINDOW_UPDATE流量控制，更新连接或流的发送窗口0x9CONTINUATION继续传输未在HEADERS/PUSH_PROMISE中完成的首部块HTTP 2本质上没有改变HTTP语义，只是将HTTP的Header和Body部分重新用帧结构封装了一下。HEADERS采用了HPCAK压缩算法，且仅有其使用。3、通信流程HTTP 2整体上还是遵循客户端请求-服务端响应的通信结构，只不过会多一些控制帧。首先，客户端先发送Magic序列，这是一个固定的24字节序列，表明其要用HTTP 2协议通信，然后紧接着必须发送SETTING帧，之后还可能衔接WINDOW_UPDATE帧，随后则是HEADERS和DATA，且HEADERS必须先于DATA。服务端在TLS握手结束后，立即同步发送SETTING帧。
很多本地代理都为Socks5或者HTTP代理，本篇将从理论与实践角度，解释二者原理。本篇是代理技术鉴赏的一篇分支，更加细致的介绍了Socks5和HTTP代理。Socks5代理于RFC 1928文档中定义，其是Socks4的升级版，在其基础上添加了认证机制、UDP代理和IPv6支持，Socks4已被Socks5完全替代。Socks5代理TCP时，分为认证、请求和通信3个阶段。1、Socks5代理TCP1.1 TCP认证首先，客户端会与Socks5代理建立TCP三次握手，之后发送认证请求，格式如下：VERNMETHODSMETHODS111 to 255VER：表示socks协议版本，值为0x05NMETHODS：表示支持多少种验证方式，也就是后面METHODS字段的长度METHODS：表示支持的验证方式，最多可提供255种，最少提供1种验证方式。0x00 无须验证 0x01 GSSAPI 0x02 用户名/密码 0x03~0x7F IANA分配 0x80~0xFE 自定义验证方式 0xFF 没有可验证的方式（这种情况下socks服务器必须关闭连接）流量示例：socks5服务端收到消息后，选择一种认证方式并发送给客户端，格式如下：VERMETHOD11流量示例：然后进入选定的认证方法认证阶段，这里以用户名/密码认证方式为例，给出流量示例：这里面用户名为bigbyto，密码为123456，然后服务端回复。1.2 TCP请求客户端认证后，就可以开始发送连接请求了，格式如下：VERCMDRSVATYPDST.ADDRDST.PORT110x001Variable2VER：表示socks协议版本，值为0x05CMD：请求命令类型，表示客户端希望代理执行的操作0x01 CONNECT（TCP连接请求） 0x02 BIND（绑定端口，用于服务器反向连接） 0x03 UDP ASSOCIATE（UDP中继，用于UDP转发）RSV：保留字段，必须填0x00。 ATYP：目标地址类型0x01 IPv4地址（4字节） 0x03 域名（第一个字节为域名长度） 0x04 IPv6地址（16字节）DST.ADDR：目标主机地址，格式取决于ATYPDST.PORT：目标主机端口大多数情况下，ATYP为域名，即客户端将域名直接传输给Socks5服务端，由服务端做DNS解析，由于Socks5服务端通常也在本地，故由后续的代理软件决定，如全局模式，就是在远端节点做DNS解析。这种情况下，客户端并不知道域名具体的IP，只知道已经通过代理转发给目的地址。流量示例：客户端请求访问a.baidu.com的80端口。socks5服务端收到访问请求后，回复客户端，格式如下：VERREPRSVATYPBND.ADDRBND.PORT110x001Variable2VER：协议版本，固定值为0x05。REP：回复字段，表示服务器执行请求的结果：0x00 成功 0x01 通用SOCKS服务器故障 0x02 连接不被规则集允许 0x03 网络不可达 0x04 主机不可达 0x05 连接被拒绝 0x06 TTL过期 0x07 不支持的命令 0x08 不支持的地址类型 0x09~0xFF 未分配RSV：保留字段，固定为0x00。ATYP：后续地址的类型：0x01 IPv4地址 0x03 域名 0x04 IPv6地址BND.ADDR：服务器绑定的地址。BND.PORT：服务器绑定的端口号。在最常见的CONNECT情况下，BND.ADDR和BND.PORT通常为0.0.0.0:0，只表示“连接成功”，具体绑定信息无意义。而BIND和UDP ASSOCIATE情况下，表示实际地址和端口，用于后续数据通道建立。流量示例：1.3 TCP通信连接建立后，客户端就可以和正常访问目标服务一样通信了，此时socks5代理仅会改变传输层以下的数据，而应用层的数据将原封不动的转发给目标服务器。也就是说，现在就可以把socks5代理服务器，当作目标服务器通信了。流量示例：2、Socks5代理UDP2.1 UDP认证Socks5代理UDP时，认证阶段与代理TCP时一样，且认证过程用的TCP连接。2.2 UDP请求认证通过后，客户端发送UDP代理请求，格式如下：VERCMDRSVATYPDST.ADDRDST.PORT110x001Variable2这个请求格式与Socks5代理TCP时相同，不过这里的CMD值是0x03，也就是UDP类型，说明本次请求是UDP请求，且目标地址和端口可以全0表示“尚不确定”。Socks5代理收到客户端发来的请求后，创建一个UDP监听端口，并将这个监听信息发回客户端，格式如下：VERREPRSVATYPBND.ADDRBND.PORT110x001Variable2这个回复格式依旧与Socks5代理TCP时相同，其中，BND.ADDR/BND.PORT的值为创建的UDP连接关联的地址和端口，客户端会直接将UDP数据发送到这个UDP连接中，所以上一个步骤接收的DST.ADDR/DST.PORT并没有太大意义。且在UDP通信阶段，请求阶段的TCP连接不能断开，否则UDP代理将结束。2.3 UDP通信客户端收来Socks5代理服务器发来的UDP端口后，就可以将本地的UDP数据发送到指定端口，格式如下：RSVFRAGATYPDST.ADDRDST.PORTDATA211Variable2VariableRSV：保留值，始终为 0x0000FRAG：分包序号，可能有以下值：​ 0x00 这是一个不需要分包的 UDP 报文0x01~0x7F 分包序号 0x80~0xFF 最高位是1，表示这个是最后一个分包ATYP：地址类型，可能有以下值：​ 0x01 IPV4 地址（占 4 字节）0x03 域名（第一字节为域名长度，后面为域名字符） 0x04 IPV6 地址（占 16 字节）DST.ADDR：目标地址DST.PORT：目标端口DATA：UDP数据相较于TCP代理，UDP代理在本地新开了一个UDP端口，但仍然与Socks5的TCP端口保持连接。但是，实际上很多Socks5代理工具并没有实现UDP代理功能[1]。目前代理UDP的方法还是启用TUN模式。3、HTTP代理HTTP分为正向代理和反向代理。正向代理为客户端服务，HTTP正向代理可以代理HTTP、HTTPS以及任何基于TCP的流量，而反向代理是针对服务端来说的，如Nginx的反向代理。HTTP正向代理：HTTP 1.1中，新增了CONNECT方法，使得HTTP能够正向代理任何基于TCP的流量，如可利用HTTP代理SSH流量，当客户端与代理服务端CONNECT协商后，这条通道就回退成了一条纯TCP通道，不再含有HTTP语义。HTTP反向代理：HTTP反向代理中，增加了WebSocket机制，使其能够基于HTTP语义升级WebSocket协议后代理任意TCP流量，不过客户端与Web服务器通信间还是会有WebSocket语义格式，而Web服务器反向代理时，会去除WebSocket语义，只留下Payload部分。科学上网工具常用WebSocket协议，因为Nginx等Web服务器的HTTP模块，可以实现即打开WebSocket反向代理，又启动Web服务，实现伪站防主动探测。另外，Nginx中虽然有stream模块，可以反向代理任意TCP流量，但其不能在同一端口打开Web服务，也就不抗主动探测攻击。HTTP代理在代理HTTP流量时，代理理解并转发HTTP请求内容，用GET/POST等普通方法。流量示例如下：HTTP在代理HTTPS、SSH等TCP流量时，会调用CONNECT方法，为客户端打开到目标主机的原始TCP连接并建立隧道。HTTP代理SSH流量示例如下：一些杂谈由于Socks5和HTTP的服务端都没有带加密功能，在互联网上明文传输数据，HTTP中的认证也仅是做了Base64编码，所以极其不安全，如今已经将二者的应用场景从远端移到了本地。浏览器能创建的UDP连接只有QUIC(HTTP 3.0)和WebRTC，其中WebRTC是正统的UDP。不过这两者都不会走浏览器或系统代理，只有在开启了TUN模式时，才可能会走，具体还要看代理节点对UDP的支持程度。WebRTC使用独立的网络栈，直接创建系统级UDP socket，不经过浏览器的代理模块，所以不会走浏览器或ZeroOmega设置的Socks5代理。WebRTC Control等防止WebRTC泄露IP的扩展工具，都是通过禁用STUN协议来的。DNS不算浏览器产生的，它是由浏览器调用系统API产生的。如果浏览器安装了代理插件，则会直接忽视系统代理，例如代理插件设置了直连，系统设置了代理，那么浏览器还是会走直连。References[1] https://www.txthinking.com/talks/articles/socks5-and-http-proxy.article
NAT穿透也常被称为UDP打洞，用于让处在不同内网、被NAT隔离的设备能直接建立点对点（Peer to Peer，P2P）连接。1、NAT介绍NAT全称Network Address Translation，即网络地址转换协议。起初网络设计者并没有想到互联网能够如此发达，以至于2^32个IPv4地址不够用。针对该问题，设计者给出了两种解决方案，一种就是推行IPv6，但这需要相应的硬件设备支持，且需要一定过渡时间；另一种方案便是使用NAT协议，将IP分为公网IP和内网IP。根据RFC 1918规定，将内网IP划分为3类。类别CIDR地址范围数量A10.0.0.0/810.0.0.0 – 10.255.255.25516,777,216B172.16.0.0/12172.16.0.0 – 172.31.255.2551,048,576C192.168.0.0/16192.168.0.0 – 192.168.255.25565,536在局域网中，设备分配内网IP，与外界进行通信时，ISP具有公网IP的路由器会维护一张NAT映射表，最初的NAT只改变IP，而Port不变，但随着发展，出现了PAT（Port Address Translation），即在NAT的基础上添加了端口映射，最终将内网IP:Port映射到路由器公网IP上的某个端口，然后通过这个公网端口与外界进行通信。PAT被视为NAT Overload，通常情况下不会刻意区分这两者，统一称为NAT，具体通信流程如下[1]。2、NAT穿透与STUNRFC 3489将NAT分为4类[2]，并定义了早期STUN（Simple Traversal of UDP Through NATs）协议，旨在帮助UDP应用穿越NAT，但后续由于实际网络中NAT行为复杂多变、检测结果极易错误。于是便推出了RFC 5389。NAT类型特点Full Cone内网IP:Port映射到固定公网IP:Port，任何外部IP都可回包Restricted Cone内网IP:Port映射固定，只有之前通信过的外部IP可以回包Port Restricted Cone内网IP:Port映射固定，只有之前通信过的外部IP:Port可以回包Symmetric内网IP:Port对每个目标IP:Port映射不同公网端口端口限制型与对称型的区别在于，端口限制锥型NAT是所有请求映射到相同的公网IP地址和端口，即改变目的IP:Port时，映射不变，而对称NAT是不同的请求有不同的映射，主要区别是内网源端口映射到公网端口是否随目标地址变化。判断NAT类型，只需要判断最外层NAT就行。且由于UDP无状态，没有繁杂的ESTABLISHED、LISTENING等状态，UDP端口只要映射出去就能用。RFC 5389彻底重定义并取代RFC 3489，将STUN定义为Session Traversal Utilities for NAT。它移除所有NAT类型分类逻辑，明确建议应用程序放弃诊断NAT，转而使用ICE协议框架通过实际连通性检查建立连接，从而简化协议、提升可靠性，成为现代WebRTC等P2P系统的基石。但如今涉及到NAT穿透，NAT的四种类型还是会有理论指导作用。ICE（Interactive Connectivity Establishment，交互式连接建立）是一套“自动试错找路”的协议框架，依赖STUN/TURN，最终让NAT后的两台设备像没NAT一样直连。RFC 3489的STUN“想太多”，试图诊断NAT类型，结果复杂、错误。RFC 5389的STUN“回归初心”，只干一件事，安全地告诉你公网映射，然后让ICE去试错选路。后来，又推出了RFC 5780重新定义了NAT类型，这次更加详细彻底，如下图所示，绿色为 Mapping Behavior，蓝色为 Filtering Behavior。3、NAT穿透与FRP这里需要区别FRP和NAT打洞的区别，FRP内网穿透是将内网服务映射到公网，而打洞仅仅是想建立两台设备的连接，两种均适用于TCP和UDP，但前者以TCP为主，后者以UDP为主[3]。因为后者即使在Full Cone的情况下，正常情况下，将内网TCP端口映射到公网，其TCP连接状态是ESTABLISHED，也就是以连接发送者的身份出现的，与用FRP穿透出去的LISTENING端口不同，其是以服务提供者的身份出现的。因为TCP连接是有状态的，所以若想将已经ESTABLISHED的端口转为LISTENING状态，则要显示的开启端口复用，否则则会报错端口占用。针对TCP打洞，有两种方案，第一，协调双方同时交叉发送SYN包，如下图，像极了TCP的彩蛋，理论成立，但现实很难做到。第二种就是通过SO_REUSEADDR实现TCP端口复用，让本地与外部ESTABLISHED的端口同时LISTENING，这样只需要保持ESTABLISHED的连接存活即可，NATMap工具穿透TCP就是利用的这一机制。但标准TCP栈不允许一个端口既LISTENING又ESTABLISHED。References[1] https://www.networkacademy.io/ccna/network-services/nat-overload-pat[2] https://info.support.huawei.com/info-finder/encyclopedia/zh/NAT.html[3] https://blog.csdn.net/dxpqxb/article/details/74942302[4] https://halc.top/p/bb3a9deb[5] https://netmanias.com/en/post/techdocs/6065/nat-network-protocol/stun-rfc-3489-vs-stun-rfc-5389-5780
最近在搞WebSocket、QUIC等协议，发现都和HTTP协议有关，遂整理一篇。1、概述HTTP至今已经5个版本了，详细信息如下表所示。版本正式发布年份最新RFC文档HTTP 0.91991无正式文档HTTP 1.01996RFC 1945HTTP 1.11997RFC 7230–7235HTTP 2.02015RFC 9113HTTP 3.02022RFC 9114不同HTTP版本占比可见Cloudflare调研文档。2、HTTP 0.9这是最早的HTTP版本，只提供了简单的GET方法，请求纯文本，且只有请求行（Request-Line），无请求头（Header），请求体（Message-Body）。如GET /index.html响应也没有状态行（Status-Line），响应头（Header），只有响应体（Message-Body），如 Hello World 3、HTTP 1.0相较于HTTP 0.9，该版本的HTTP添加了请求头，请求体，状态行，响应头。且添加了POST、HEAD等方法。请求示例如下：#请求行 GET /index.html HTTP/1.0 #请求头 Host: www.example.com User-Agent: MyBrowser/1.0 Accept: text/html #请求体 #一般在POST方法中存在。响应示例如下：#状态行 HTTP/1.0 200 OK #响应头 Date: Tue, 28 Oct 2025 10:00:00 GMT Server: ExampleServer/1.0 Content-Type: text/html Content-Length: 44 Connection: close #响应体 Hello World 4、HTTP 1.1HTTP 1.1 是在1.0的基础上增加了连接复用、请求方法、缓存控制等内容。在1.0版本时，客户端请求并受到服务端响应后，会断开TCP连接。而1.1版本添加了Connection请求头，默认为keep-alive，即会复用TCP连接，直到超时或关闭连接。一个网站最多会同时建立6个TCP连接，当然这是浏览器决定的。这也是常说的短连接（Short-lived Connection）和长连接（Persistent Connection）[1]。具体差异如下图所示。这里的流水线（Pipelining）传输方式最终并没有流行开来，而是被HTTP 2.0中的多路复用（Multiplexing）所替代，因为Pipelining还是串行传输，前者会影响后者，而多路复用则是任意顺序传输，且前者的使用受多种因素影响，如请求方法等。这里能明显看到，HTTP 1.1存在应用层的队头阻塞（Head-of-line blocking），即前一个HTTP Response没到达，客户端是不会发送下一个Request的。此外，知名的WebSocket协议就是在HTTP 1.1的基础上扩展而来的。5、HTTP 2.0HTTP 2.0在1.1的基础上，在保持HTTP语义不变的前提下，通过二进制分帧、多路复用、头部压缩和服务器推送，大幅提升性能和传输效率，直观展示请看[2]。Burpsuite也支持抓HTTP 2.0的包，现浏览器也大多支持了HTTP 2.0，具体也体现在JA4指纹中。HTTP 2.0与1.1的流水线传输方式类似，只不过其是乱序传输的，例如在HTTP 1.1中，客户端发送1.png, 2.png, 3.png三个图片的Request，其是顺序进行的，当然，浏览器可以开3个TCP连接，分别请求这3个图片，从应用层整体来看，也勉强能称得上乱序，但在单个TCP连接中，依旧是顺序的，且这个并行乱序是浏览器带来的，不是协议本身的特性。而HTTP 2.0中，其只建立1个TCP连接，3个图片的request被封装成多个Frame，每个Frame平等，随意混合，不存在文件先后顺序。每个文件都看作一个Stream，Stream由多个Frame组成，最后服务端通过Frame上带的Stream ID重组收到的Frame，最终获取完整Request，随后返回Response过程同理。具体关系如下图：由此，能看到HTTP 2.0解决了应用层上的队头堵塞，但传输层，也就是TCP层仍然存在队头阻塞，即TCP连接中，前一个字节没到达，就会阻塞后一个字节的传输。虽然在HTTP 2.0中，支持了服务器推送（Server Push），但其本质语义上还是request-response的形式，只不过当客户端请求index.html时，服务端能够顺带将相关的css和js文件一同推送给客户端，但由于一些兼容性问题，主流浏览器已经不再支持，基本处于弃用状态。而HTTP 1.1中的WebSocket协议，真正实现了应用层上的全双工通信，即使客户端不发送任何消息，服务端也能主动给客户端发送消息。此外，APNs和FCM等消息推送（Push Notification）协议，开发者与推送服务器的通信协议，也都从HTTP 1.1迁移到了HTTP 2.0，但推送服务器与设备之间的通信均为私有协议，其通信流程图如下。6、HTTP 3.0HTTP 3.0是由QUIC协议催生的，因为QUIC协议是一个四不像的协议，属于传输层和应用层各占一部分，对标的是更好的TCP，于是也就不能适用于HTTP 2.0，所以才推出了HTTP 3.0，详见RFC 8999-9002，其网络层次如下图。HTTP 3.0有着0/1 RTT快速连接，默认启用TLS 1.3加密等特点，目前主流浏览器均已支持HTTP 3.0，可在该网站进行测试（建议取消代理，清除浏览器缓存或无痕模式下测试），但是如今主流的抓包均不支持HTTP 3.0，此外大多数代理工具也会选择禁用QUIC提速（大多数会直接禁用UDP的443端口）。有关HTTP版本使用情况，请见CloudFlare的调研报告[3]。HTTP 3.0同HTTP 2.0一样，都是将文件看作Stream然后分成Frame传输，但HTTP 3.0彻底解决了队头阻塞问题，传输层由TCP变为UDP。具体差异如下表。版本传输方式乱序传输队头阻塞连接个数HTTP 1.1串行不支持应用层堵塞6HTTP 2.0多路复用支持传输层堵塞1HTTP 3.0多路复用支持无堵塞1References[1] https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Connection_management_in_HTTP_1.x[2] https://www.tunetheweb.com/performance-test-360/[3] https://blog.cloudflare.com/zh-cn/http3-usage-one-year-on/
最近在看一些协议的源码，其大都用Go语言实现，遂了解一下Go语言的基本数据结构。编程思想有很多，例如面向过程的C、面向对象的C++和JAVA等，而Go相较于C多了接口、相较于JAVA少了类，好像一种半面向对象的编程方法。1、结构体Go语言中数组可以存储同一类型的数据，但在结构体中可以为不同项定义不同的数据类型。结构体（struct）是由一系列具有相同类型或不同类型的数据构成的数据集合。下面是一个具体示例：package main import ( "fmt" ) //定义一个结构体 type Student struct { //定义结构体的属性 Name string Age int } func main() { //实例化一个结构体 var a Student //为实例a的属性赋值 a.Name="Alice" a.Age=22 fmt.Println(a) //实例化b并赋值 b:=Student{"Bob",24} fmt.Println(b) }结构体是由变量组成的，其中并不包含方法，即纯粹的数据属性。2、接口Go语言中的接口（interface）用于定义方法的集合，它通过描述类型（type，大多是struct，也可是其他，这里只讨论struct）必须实现的方法，规定了类型的行为契约。Go的接口与JAVA最大的区别在于，Go是由结构体隐式的实现某个接口，如果一个结构体满足了某个接口定义的所有方法，那么这个结构体就实现了该接口。而JAVA中则是由类显式的实现某个接口，并实现该接口的所有功能。下面是一个示例： package main import ( "fmt" ) //定义一个结构体 type Student struct { //定义结构体的属性 Name string Age int } //声明一个接口 type Action interface { //声明接口的方法 Introduction() string ChangeInfo(name string,age int) } //定义结构体的方法，以实现接口 //func (方法接收者) 方法名(参数) 返回值 func (s Student) Introduction() string { mystr:=fmt.Sprintf("My name is %s, my age is %d",s.Name,s.Age) return mystr } //这里传递指针，否则不会改变值 func (s *Student) ChangeInfo(name string,age int){ s.Name=name s.Age=age } //定义另一个结构体 type Teacher struct { Name string Age int Object string } //定义Teacher结构体的方法，以满足接口 func (t Teacher) Introduction() string { mystr:=fmt.Sprintf("My name is %s, my age is %d, my object is %s",t.Name,t.Age,t.Object) return mystr } func (t Teacher) ChangeInfo(name string,age int) { //不用该函数，但必须定义，否则无法满足接口需求 } func main() { //实例化一个结构体 var a Student //为实例a的属性赋值 a.Name="Alice" a.Age=22 fmt.Println(a) //实例化b并赋值 b:=Student{"Bob",24} fmt.Println(b) //声明接口 var i Action //由于定义了指针类型的方法，所以接口赋值时要用指针 i=&a fmt.Println(i.Introduction()) //调用方法修改结构体属性值 i.ChangeInfo("New Bob",22) //实例化一个教师结构体 c:=Teacher{"Mike",50,"English"} //赋值给接口 i=c fmt.Println(i.Introduction()) } 也就是说，接口可以对应拥有不同属性，相同方法的结构体。