chore: push all design docs, V2 plan specs, and current working state

Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2,
AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code
changes across packages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-12 17:12:29 +08:00
parent 8f55c962bb
commit ae44be31d5
364 changed files with 46779 additions and 2812 deletions

View File

@@ -0,0 +1,44 @@
{
"name": "airndb",
"version": "0.1.2",
"description": "Network-debug workflow for bounded local or remote tcpdump or WinDump packet capture, first-run tcpdump or WinDump detection, official WinDump auto-download on Windows, remote tcpdump auto-configuration over SSH, pcap analysis, BPF filters, and AI-readable network debugging evidence.",
"author": {
"name": "14816",
"email": "noreply@example.com",
"url": "https://airlongdian.fun/plugins/airndb"
},
"homepage": "https://airlongdian.fun/plugins/airndb",
"repository": "https://airlongdian.fun/plugins/airndb",
"license": "MIT",
"keywords": [
"airndb",
"network-debug",
"tcpdump",
"windump",
"pcap",
"bpf",
"packet-capture"
],
"skills": "./skills/",
"interface": {
"displayName": "AirNDB",
"shortDescription": "Local and remote packet capture workflow with WinDump setup",
"longDescription": "AirNDB helps Codex detect tcpdump or WinDump on first startup, automatically download official WinDump.exe on Windows when no capture tool is available, configure project-local tool.env, use a remote device helper over SSH for remote packet capture and auto-configure missing remote tcpdump when possible, collect bounded packet captures, choose safe BPF filters, save timestamped pcap artifacts, read packet summaries, and record network debugging evidence in AGENTS.md, ADR, C4 module, and network debug logs.",
"developerName": "14816",
"category": "Productivity",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://airlongdian.fun/plugins/airndb",
"privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",
"termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",
"defaultPrompt": [
"Use AirNDB to investigate a network issue and collect bounded packet capture evidence.",
"Use AirNDB to detect or configure tcpdump or WinDump before starting a local or remote capture.",
"Use AirNDB to provide packet-level evidence back to AirDbg or AirDo for debugging or validation."
],
"brandColor": "#0891B2",
"screenshots": []
}
}

View File

@@ -0,0 +1,109 @@
---
description: Enter, exit, inspect, capture, remote-capture, or read AirNDB tcpdump/WinDump network debug mode
argument-hint: [enter|setup|exit|status|interfaces|command|capture|read|remote-setup|remote-status|remote-interfaces|remote-command|remote-capture]
allowed-tools: [Read, Glob, Grep, Bash, Write, Edit]
---
# /airndb
控制当前工作区的 AirNDB 网络抓包调试模式。
用户传入参数:`$ARGUMENTS`
- `enter` 或空参数:进入 AirNDB 模式并初始化网络调试上下文。
- `status`:检查 AirNDB 状态和关键文件是否存在。
- `setup`执行首启工具自检Windows 下缺少 tcpdump/WinDump 时自动下载官方 `WinDump.exe` 并写入 `AirPlan/state/airndb/tool.env`
- `interfaces`:调用 tcpdump/WinDump 列出可抓包接口。
- `command`:生成安全、有界的 tcpdump/WinDump 抓包命令,不实际执行。
- `capture`:执行短时有界抓包,写入 timestamped `.pcap` 和 JSON 报告。
- `read`:读取已有 `.pcap`,生成文本摘要和 JSON 报告。
- `remote-setup`:检查远程设备 SSH 和抓包工具;缺失时自动尝试配置远程 `tcpdump`
- `remote-status`:只检查远程设备配置和工具状态,不自动安装。
- `remote-interfaces`:通过 SSH 调用远端抓包工具列出接口。
- `remote-command`:生成安全、有界的远程抓包命令,不实际执行。
- `remote-capture`:通过 SSH 执行短时有界远程抓包,并把 pcap/JSON 报告拉回当前项目。
- `exit`:退出 AirNDB 模式。
## 执行步骤
1. 解析 `$ARGUMENTS`,默认动作为 `enter`
2. 在当前项目根目录运行;`enter` / `setup` 会检测 `tcpdump` / `WinDump.exe`Windows 缺失时自动从 WinDump 官方下载页获取 `WinDump.exe`,校验 SHA1 后配置 `AirPlan/state/airndb/tool.env`
```bash
python "$HOME/plugins/airndb/scripts/airndb_mode.py" --mode <enter|setup|exit|status> --project .
```
如果 `python` 不存在,尝试 `py``python3` 或用户提供的 Python 绝对路径。
3. 如果 `$ARGUMENTS` 包含 `remote``ssh``远程`或用户已经说明目标流量在远程设备、测试机、服务器、VM、容器宿主机、SSH 主机上,优先运行远程设备 helper
```bash
python "$HOME/plugins/airndb/scripts/airndb_remote_device.py" --project . --action <setup|status|interfaces|command|capture>
```
首次缺少远程配置时helper 会生成 `AirPlan/state/airndb/remote-device.env.example` 并提示设置 `AIRNDB_REMOTE_SSH_TARGET`。有 SSH 目标后helper 会探测远端 `tcpdump` / `dumpcap`;缺失时自动尝试安装 `tcpdump`,只使用非交互式 `sudo -n`,无法自动配置时停止并提示用户。
4. `interfaces` 且目标是本机时运行:
```bash
python "$HOME/plugins/airndb/scripts/airndb_capture.py" --project . --action interfaces
```
5. `command` 且目标是本机时先收集接口、host/port/protocol/filter、包数或超时再运行
```bash
python "$HOME/plugins/airndb/scripts/airndb_capture.py" --project . --action command --iface <iface> --filter "<bpf>" --count 200
```
6. `capture` 且目标是本机时必须使用有界抓包:
```bash
python "$HOME/plugins/airndb/scripts/airndb_capture.py" --project . --action capture --iface <iface> --filter "<bpf>" --count 200 --timeout 30
```
7. `read` 时读取已有 pcap
```bash
python "$HOME/plugins/airndb/scripts/airndb_capture.py" --project . --action read --read-file docs/network/airndb-captures/<file>.pcap --filter "<bpf>"
```
8. 每次抓包或读取后维护:
- `AirPlan/docs/network/airndb-log.md`
- `docs/architecture/adr/`
- `docs/architecture/c4/module.md`
- `AGENTS.md`
## 安全边界
- 只抓取用户授权的本机、项目、测试环境或明确允许的网络流量。
- 默认禁止无界抓包;必须使用 `--count``--timeout` 或轮转策略。
- 默认使用 `-nn``-s 0`,避免 DNS/service-name 解析并保留完整包。
- 自动获取仅下载 `WinDump.exe` 并配置工具路径,不静默安装 WinPcap/Npcap 抓包驱动;接口列举失败时提示用户安装驱动或以管理员权限重试。
- pcap 可能包含凭据、cookie、token、内网地址或个人信息对外分享前必须提醒脱敏。
## 输出文案
进入模式:
```text
AirNDB 模式已开启:已初始化或检查 AGENTS.md、ADR、C4 module 和 network debug log并完成 tcpdump/WinDump 工具自检。请说明网络问题、目标主机/端口/协议、抓包接口和允许的抓包窗口。
```
抓包完成:
```text
AirNDB 抓包完成pcap 和 JSON 报告已写入 docs/network/airndb-captures/,请结合 airndb-log.md 继续分析。
```
remote-capture 完成:
```text
AirNDB 远程抓包完成:已通过 SSH 执行有界抓包pcap 和 JSON 报告已写入 docs/network/airndb-captures/,请结合 airndb-log.md 继续分析。
```
状态检查:
```text
AirNDB 状态:<enabled|disabled>
关键文件:逐项列出 ok/missing。
```

View File

@@ -0,0 +1,441 @@
#!/usr/bin/env python3
"""Run safe bounded tcpdump/WinDump capture helpers for AirNDB."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import platform
import shlex
import shutil
import subprocess
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence
DEFAULT_COUNT = 200
DEFAULT_TIMEOUT = 30
MAX_COUNT = 500000
WINDUMP_DOWNLOAD_PAGE = "https://www.winpcap.org/windump/install/"
WINDUMP_DOWNLOAD_URL = "https://www.winpcap.org/windump/install/bin/windump_3_9_5/WinDump.exe"
WINDUMP_SHA1 = "d59bc54721951dec855cbb4bbc000f9a71ea4d95"
def now_stamp() -> str:
return datetime.now().strftime("%Y%m%d-%H%M%S")
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def is_windows() -> bool:
return platform.system().lower().startswith("win")
def parse_env_file(path: Path) -> Dict[str, str]:
if not path.exists():
return {}
values: Dict[str, str] = {}
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"')
return values
def project_tool_env(project_root: Optional[Path]) -> Dict[str, str]:
if not project_root:
return {}
return parse_env_file(project_root / "AirPlan" / "state" / "airndb" / "tool.env")
def tool_dir(project_root: Optional[Path] = None) -> Path:
configured = os.environ.get("AIRNDB_TOOL_DIR", "").strip()
if configured:
return Path(configured).expanduser().resolve()
if project_root:
return (project_root / "AirPlan" / "state" / "airndb" / "tools").resolve()
return Path.home() / ".airndb" / "tools"
def detect_tool(explicit: str = "", project_root: Optional[Path] = None) -> Optional[str]:
candidates: List[str] = []
if explicit:
candidates.append(explicit)
project_env = project_tool_env(project_root)
project_tool = project_env.get("AIRNDB_TCPDUMP", "")
if project_tool:
candidates.append(project_tool)
env_tool = os.environ.get("AIRNDB_TCPDUMP", "")
if env_tool:
candidates.append(env_tool)
bundled = tool_dir(project_root) / "WinDump.exe"
if bundled.exists():
candidates.append(str(bundled))
candidates.extend(["windump", "WinDump.exe", "tcpdump"] if is_windows() else ["tcpdump", "windump", "WinDump.exe"])
for candidate in candidates:
if Path(candidate).exists():
return str(Path(candidate).resolve())
found = shutil.which(candidate)
if found:
return found
return None
def sha1_file(path: Path) -> str:
digest = hashlib.sha1()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def write_project_tool_env(project_root: Path, tool_path: Path) -> Path:
airndb_dir = project_root / "AirPlan" / "state" / "airndb"
airndb_dir.mkdir(parents=True, exist_ok=True)
env_path = airndb_dir / "tool.env"
env_path.write_text(f"AIRNDB_TCPDUMP={tool_path}\n", encoding="utf-8", newline="\n")
gitignore = airndb_dir / ".gitignore"
existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
if "tool.env" not in existing.splitlines():
gitignore.write_text((existing.rstrip() + "\ntool.env\n").lstrip(), encoding="utf-8", newline="\n")
return env_path
def download_windump(target: Path, timeout: int = 60) -> Dict[str, Any]:
target.parent.mkdir(parents=True, exist_ok=True)
temp = target.with_suffix(".download")
try:
with urllib.request.urlopen(WINDUMP_DOWNLOAD_URL, timeout=timeout) as response:
data = response.read()
temp.write_bytes(data)
actual_sha1 = sha1_file(temp)
if actual_sha1.lower() != WINDUMP_SHA1.lower():
temp.unlink(missing_ok=True)
return {
"status": "failed",
"reason": "sha1_mismatch",
"expectedSha1": WINDUMP_SHA1,
"actualSha1": actual_sha1,
"downloadPage": WINDUMP_DOWNLOAD_PAGE,
"downloadUrl": WINDUMP_DOWNLOAD_URL,
}
if target.exists():
target.unlink()
temp.replace(target)
return {
"status": "downloaded",
"path": str(target),
"sha1": actual_sha1,
"downloadPage": WINDUMP_DOWNLOAD_PAGE,
"downloadUrl": WINDUMP_DOWNLOAD_URL,
}
except (urllib.error.URLError, TimeoutError, OSError) as exc:
temp.unlink(missing_ok=True)
return {
"status": "failed",
"reason": type(exc).__name__,
"message": str(exc),
"downloadPage": WINDUMP_DOWNLOAD_PAGE,
"downloadUrl": WINDUMP_DOWNLOAD_URL,
}
def ensure_capture_tool(project_root: Path, explicit: str = "", auto_install: bool = False) -> Dict[str, Any]:
existing = detect_tool(explicit, project_root)
if existing:
env_path = write_project_tool_env(project_root, Path(existing)) if Path(existing).exists() else None
return {
"status": "ok",
"tool": existing,
"source": "existing",
"projectEnv": str(env_path) if env_path else "",
}
if not is_windows():
return {
"status": "missing",
"tool": "",
"reason": "tcpdump_or_windump_not_found",
"hint": "Install tcpdump or pass --tool <path>.",
}
if not auto_install:
return {
"status": "missing",
"tool": "",
"reason": "tcpdump_or_windump_not_found",
"hint": "Run /airndb enter or pass --tool <path>.",
"downloadPage": WINDUMP_DOWNLOAD_PAGE,
}
target = tool_dir(project_root) / "WinDump.exe"
download = download_windump(target)
if download.get("status") != "downloaded":
return {
"status": "missing",
"tool": "",
"reason": "windump_download_failed",
"download": download,
"hint": "Install tcpdump/WinDump manually or set AIRNDB_TCPDUMP.",
}
env_path = write_project_tool_env(project_root, target)
return {
"status": "ok",
"tool": str(target),
"source": "downloaded",
"projectEnv": str(env_path),
"download": download,
"driverHint": "WinDump still requires a packet capture driver such as Npcap or WinPcap; install one manually if interface listing fails.",
}
def split_filter(filter_expr: str) -> List[str]:
if not filter_expr.strip():
return []
try:
return shlex.split(filter_expr, posix=not is_windows())
except ValueError:
return filter_expr.split()
def display_command(command: Sequence[str]) -> str:
if is_windows():
return subprocess.list2cmdline(list(command))
return shlex.join(list(command))
def tail(text: str, limit: int = 8000) -> str:
if len(text) <= limit:
return text
return text[-limit:]
def bounded_count(value: int) -> int:
if value < 1:
raise SystemExit("--count must be >= 1")
if value > MAX_COUNT:
raise SystemExit(f"--count must be <= {MAX_COUNT}")
return value
def capture_dir(project_root: Path, output_dir: str = "") -> Path:
path = Path(output_dir).expanduser().resolve() if output_dir else project_root / "AirPlan" / "docs" / "network" / "airndb-captures"
path.mkdir(parents=True, exist_ok=True)
return path
def base_capture_command(tool: str, iface: str, output: Path, count: int, filter_expr: str) -> List[str]:
if not iface:
raise SystemExit("--iface is required for capture or command")
command = [tool, "-i", iface, "-nn", "-s", "0", "-w", str(output), "-c", str(bounded_count(count))]
command.extend(split_filter(filter_expr))
return command
def read_command(tool: str, read_file: Path, filter_expr: str) -> List[str]:
if not read_file.exists():
raise SystemExit(f"read file not found: {read_file}")
command = [tool, "-nn", "-tttt", "-r", str(read_file)]
command.extend(split_filter(filter_expr))
return command
def run_command(command: Sequence[str], timeout: int) -> Dict[str, Any]:
started = now_iso()
try:
completed = subprocess.run(
list(command),
check=False,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout,
)
return {
"startedAt": started,
"finishedAt": now_iso(),
"timedOut": False,
"returnCode": completed.returncode,
"stdout": tail(completed.stdout),
"stderr": tail(completed.stderr),
}
except subprocess.TimeoutExpired as exc:
return {
"startedAt": started,
"finishedAt": now_iso(),
"timedOut": True,
"returnCode": None,
"stdout": tail((exc.stdout or "") if isinstance(exc.stdout, str) else ""),
"stderr": tail((exc.stderr or "") if isinstance(exc.stderr, str) else ""),
}
def write_report(output_dir: Path, action: str, payload: Dict[str, Any]) -> Path:
report_path = output_dir / f"{now_stamp()}-airndb-{action}.json"
report_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return report_path
def append_log(project_root: Path, payload: Dict[str, Any]) -> None:
log_path = project_root / "AirPlan" / "docs" / "network" / "airndb-log.md"
log_path.parent.mkdir(parents=True, exist_ok=True)
lines = [
"",
f"## {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: AirNDB {payload['action']}",
"",
f"- Tool: `{payload.get('tool', 'missing')}`",
f"- Interface: `{payload.get('interface', '')}`",
f"- Filter: `{payload.get('filter', '')}`",
f"- Command: `{payload.get('command', '')}`",
f"- Artifacts: `{payload.get('artifact', '')}`",
f"- Report: `{payload.get('report', '')}`",
f"- Result: `{payload.get('result', '')}`",
"- AirDbg handoff: TODO",
"- Residual risk: pcap files may contain sensitive packet contents; review before sharing.",
]
with log_path.open("a", encoding="utf-8", newline="\n") as handle:
handle.write("\n".join(lines) + "\n")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="AirNDB tcpdump/WinDump helper.")
parser.add_argument("--project", default=".")
parser.add_argument("--action", choices=["interfaces", "command", "capture", "read"], default="interfaces")
parser.add_argument("--tool", default="", help="Explicit tcpdump/WinDump path.")
parser.add_argument("--iface", default="", help="Interface name or WinDump interface index.")
parser.add_argument("--filter", default="", help="BPF filter expression.")
parser.add_argument("--count", type=int, default=DEFAULT_COUNT)
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
parser.add_argument("--output-dir", default="")
parser.add_argument("--output", default="")
parser.add_argument("--read-file", default="")
return parser.parse_args()
def main() -> None:
args = parse_args()
project_root = Path(args.project).expanduser().resolve()
output_dir = capture_dir(project_root, args.output_dir)
tool = detect_tool(args.tool, project_root)
if not tool:
print("airndb_status=blocked")
print("reason=tcpdump_or_windump_not_found")
print("hint=Install tcpdump on Unix-like systems or WinDump/Npcap on Windows, or pass --tool <path>.")
raise SystemExit(2)
if args.action == "interfaces":
command = [tool, "-D"]
result = run_command(command, max(5, args.timeout))
payload = {
"action": "interfaces",
"tool": tool,
"command": display_command(command),
"result": result,
}
report = write_report(output_dir, "interfaces", payload)
print("airndb_status=ok" if result["returnCode"] == 0 else "airndb_status=failed")
print(f"tool={tool}")
print(f"command={display_command(command)}")
print(f"report={report}")
print(result.get("stdout", ""))
print(result.get("stderr", ""))
append_log(project_root, {
"action": "interfaces",
"tool": tool,
"interface": "",
"filter": "",
"command": display_command(command),
"artifact": "",
"report": str(report),
"result": "ok" if result["returnCode"] == 0 else "failed",
})
return
if args.action == "command":
output = Path(args.output).expanduser().resolve() if args.output else output_dir / f"{now_stamp()}-airndb.pcap"
command = base_capture_command(tool, args.iface, output, args.count, args.filter)
print("airndb_status=ok")
print(f"capture_command={display_command(command)}")
print(f"output={output}")
return
if args.action == "capture":
output = Path(args.output).expanduser().resolve() if args.output else output_dir / f"{now_stamp()}-airndb.pcap"
command = base_capture_command(tool, args.iface, output, args.count, args.filter)
result = run_command(command, max(1, args.timeout))
payload = {
"action": "capture",
"tool": tool,
"interface": args.iface,
"filter": args.filter,
"count": bounded_count(args.count),
"timeoutSeconds": args.timeout,
"pcap": str(output),
"command": display_command(command),
"result": result,
}
report = write_report(output_dir, "capture", payload)
status = "timeout" if result["timedOut"] else ("ok" if result["returnCode"] == 0 else "failed")
print(f"airndb_status={status}")
print(f"pcap={output}")
print(f"report={report}")
print(f"command={display_command(command)}")
append_log(project_root, {
"action": "capture",
"tool": tool,
"interface": args.iface,
"filter": args.filter,
"command": display_command(command),
"artifact": str(output),
"report": str(report),
"result": status,
})
return
if args.action == "read":
if not args.read_file:
raise SystemExit("--read-file is required for read")
read_file = Path(args.read_file).expanduser()
if not read_file.is_absolute():
read_file = project_root / read_file
read_file = read_file.resolve()
command = read_command(tool, read_file, args.filter)
result = run_command(command, max(1, args.timeout))
summary_path = output_dir / f"{now_stamp()}-airndb-read.txt"
summary_path.write_text((result.get("stdout") or "") + (result.get("stderr") or ""), encoding="utf-8", newline="\n")
payload = {
"action": "read",
"tool": tool,
"readFile": str(read_file),
"filter": args.filter,
"summary": str(summary_path),
"command": display_command(command),
"result": result,
}
report = write_report(output_dir, "read", payload)
status = "ok" if result["returnCode"] == 0 else "failed"
print(f"airndb_status={status}")
print(f"summary={summary_path}")
print(f"report={report}")
print(f"command={display_command(command)}")
append_log(project_root, {
"action": "read",
"tool": tool,
"interface": "",
"filter": args.filter,
"command": display_command(command),
"artifact": str(summary_path),
"report": str(report),
"result": status,
})
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""Bootstrap AirNDB network debugging context files."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, Tuple
from airndb_capture import ensure_capture_tool
MARKER_BEGIN = "<!-- AIRNDB:BEGIN -->"
MARKER_END = "<!-- AIRNDB:END -->"
def now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def airndb_agents_block() -> str:
return f"""{MARKER_BEGIN}
## AirNDB Network Debug Workflow
1. Use AirNDB for `/airndb` sessions that need tcpdump/WinDump packet capture, pcap reading, BPF filters, or network evidence.
2. On first entry, detect tcpdump/WinDump; on Windows, if none is available, download official `WinDump.exe` from https://www.winpcap.org/windump/install/, verify SHA1, and write `AirPlan/state/airndb/tool.env`.
3. Only capture authorized traffic; prefer short bounded captures with `-nn`, `-s 0`, `-c <count>`, and narrow BPF filters.
4. Before capture, load:
- `AirPlan/AGENTS.md`
- `AirPlan/docs/architecture/adr/`
- `AirPlan/docs/architecture/c4/module.md`
- `AirPlan/docs/network/airndb-log.md`
5. Store pcap, text summaries, and JSON reports under `AirPlan/docs/network/airndb-captures/`.
6. Record exact command, interface, filter, capture window, artifact paths, key observations, and residual risk in `AirPlan/docs/network/airndb-log.md`.
7. AirDbg may call AirNDB when debugging needs DNS/TCP/UDP/TLS/HTTP, ports, proxy, firewall, packet-loss, retransmit, reset, or pcap evidence.
8. Update ADR/C4 when network boundaries, capture tooling, observability, ports, protocols, DNS, proxy, TLS, or runtime topology become durable architecture context.
{MARKER_END}
"""
def c4_module_template() -> str:
return """# C4 Module
## System Context
- TODO: Describe the system, users, and important external systems.
## Containers
- TODO: Describe runtime/deployable units and network boundaries.
## Modules
| Module | Responsibility | Network Interfaces | Dependencies | Data Ownership | Network Debug Notes |
| --- | --- | --- | --- | --- | --- |
| TODO | TODO | TODO | TODO | TODO | TODO |
## Network / Observability Boundaries
- tcpdump/WinDump capture points, interfaces, container/WSL/VM/host boundaries: TODO
- Ports, protocols, DNS, proxy, TLS, firewall, NAT, or gateway notes: TODO
## Change Log
- TODO: Record network-boundary or observability changes discovered by AirNDB.
"""
def adr_template() -> str:
return """# ADR-0001: AirNDB Packet Capture Governance
- Status: Accepted
- Date: TODO
## Context
Network debugging needs durable, AI-readable packet capture context and bounded evidence collection.
## Decision
Use AirNDB to build safe tcpdump/WinDump commands, capture or read pcap artifacts, and maintain `AirPlan/docs/network/airndb-log.md`, C4 module docs, and ADR records when network boundaries or diagnostics change.
## Consequences
- Packet evidence can be reused by future AirDbg or AirNDB sessions.
- Captures must stay bounded and authorized.
- pcap artifacts may contain sensitive data and need careful handling.
## Alternatives
- Chat-only packet notes: rejected because commands, filters, and pcap paths are easy to lose.
"""
def network_log_template() -> str:
return """# AirNDB Network Debug Log
Append entries for AirNDB packet capture sessions.
## Entry Template
### YYYY-MM-DD: short network issue title
- Scope / authorization: TODO
- Symptom: TODO
- Interface: TODO
- Filter: TODO
- Capture window: TODO
- Command: TODO
- Artifacts: TODO
- Key observations: TODO
- AirDbg handoff: TODO
- ADR/C4 updates: TODO
- Residual risk: TODO
"""
def captures_gitignore_template() -> str:
return """*.pcap
*.pcapng
*.cap
*.txt
*.json
!.gitignore
"""
def airndb_gitignore_template() -> str:
return """tool.env
"""
def write_if_missing(path: Path, content: str) -> bool:
if path.exists():
return False
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8", newline="\n")
return True
def upsert_agents_md(path: Path) -> str:
block = airndb_agents_block().rstrip() + "\n"
if path.exists():
original = path.read_text(encoding="utf-8")
existed = True
else:
original = "# AGENTS.md\n\n"
existed = False
begin = original.find(MARKER_BEGIN)
end = original.find(MARKER_END)
if begin >= 0 and end > begin:
end += len(MARKER_END)
updated = original[:begin].rstrip() + "\n\n" + block + original[end:].lstrip()
status = "updated"
else:
updated = original.rstrip() + "\n\n" + block
status = "updated" if existed else "created"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(updated, encoding="utf-8", newline="\n")
return status
def artifact_map(project_root: Path) -> Dict[str, Path]:
return {
"AGENTS.md": project_root / "AirPlan" / "AGENTS.md",
"c4_module": project_root / "AirPlan" / "docs" / "architecture" / "c4" / "module.md",
"adr_dir": project_root / "AirPlan" / "docs" / "architecture" / "adr",
"adr_0001": project_root / "AirPlan" / "docs" / "architecture" / "adr" / "ADR-0001-airndb-packet-capture-governance.md",
"network_log": project_root / "AirPlan" / "docs" / "network" / "airndb-log.md",
"captures_dir": project_root / "AirPlan" / "docs" / "network" / "airndb-captures",
"captures_gitignore": project_root / "AirPlan" / "docs" / "network" / "airndb-captures" / ".gitignore",
"tool_env": project_root / "AirPlan" / "state" / "airndb" / "tool.env",
"airndb_gitignore": project_root / "AirPlan" / "state" / "airndb" / ".gitignore",
"state": project_root / "AirPlan" / "state" / "airndb" / "state.json",
}
def write_state(path: Path, enabled: bool, project_root: Path) -> None:
artifacts = artifact_map(project_root)
health = {
name: artifacts[name].exists()
for name in [
"AGENTS.md",
"c4_module",
"adr_dir",
"adr_0001",
"network_log",
"captures_dir",
"captures_gitignore",
"tool_env",
"airndb_gitignore",
]
}
payload = {
"enabled": enabled,
"updatedAt": now_iso(),
"projectRoot": str(project_root),
"artifactHealth": health,
"captureTool": ensure_capture_tool(project_root, auto_install=False),
}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
def enter_mode(project_root: Path) -> Tuple[str, Dict[str, str]]:
artifacts = artifact_map(project_root)
results: Dict[str, str] = {}
results["AGENTS.md"] = upsert_agents_md(artifacts["AGENTS.md"])
results["c4_module"] = "created" if write_if_missing(artifacts["c4_module"], c4_module_template()) else "exists"
artifacts["adr_dir"].mkdir(parents=True, exist_ok=True)
results["adr_dir"] = "exists"
results["adr_0001"] = "created" if write_if_missing(artifacts["adr_0001"], adr_template()) else "exists"
results["network_log"] = "created" if write_if_missing(artifacts["network_log"], network_log_template()) else "exists"
artifacts["captures_dir"].mkdir(parents=True, exist_ok=True)
results["captures_dir"] = "exists"
results["captures_gitignore"] = "created" if write_if_missing(artifacts["captures_gitignore"], captures_gitignore_template()) else "exists"
results["airndb_gitignore"] = "created" if write_if_missing(artifacts["airndb_gitignore"], airndb_gitignore_template()) else "exists"
tool_status = ensure_capture_tool(project_root, auto_install=True)
results["capture_tool"] = tool_status.get("status", "unknown")
results["capture_tool_path"] = tool_status.get("tool", "")
results["capture_tool_source"] = tool_status.get("source", "")
results["capture_tool_env"] = tool_status.get("projectEnv", "")
if tool_status.get("driverHint"):
results["capture_driver_hint"] = tool_status["driverHint"]
write_state(artifacts["state"], True, project_root)
return "enabled", results
def exit_mode(project_root: Path) -> Tuple[str, Dict[str, str]]:
artifacts = artifact_map(project_root)
write_state(artifacts["state"], False, project_root)
return "disabled", {}
def status_mode(project_root: Path) -> Tuple[str, Dict[str, str]]:
artifacts = artifact_map(project_root)
state_file = artifacts["state"]
enabled = False
if state_file.exists():
try:
payload = json.loads(state_file.read_text(encoding="utf-8"))
enabled = bool(payload.get("enabled"))
except json.JSONDecodeError:
enabled = False
results = {
name: ("ok" if path.exists() else "missing")
for name, path in artifacts.items()
if name != "state"
}
return ("enabled" if enabled else "disabled"), results
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Manage AirNDB network debug artifacts.")
parser.add_argument("--mode", choices=["enter", "setup", "exit", "status"], default="enter")
parser.add_argument("--project", default=".")
return parser.parse_args()
def main() -> None:
args = parse_args()
project_root = Path(args.project).expanduser().resolve()
if args.mode in {"enter", "setup"}:
mode_state, result = enter_mode(project_root)
elif args.mode == "exit":
mode_state, result = exit_mode(project_root)
else:
mode_state, result = status_mode(project_root)
print(f"airndb_mode={mode_state}")
print(f"project_root={project_root}")
for key, value in result.items():
print(f"{key}={value}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""AirNDB remote packet capture helper over SSH."""
from __future__ import annotations
import argparse, base64, json, os, shlex, shutil, subprocess
from datetime import datetime, timezone
from pathlib import Path
MAX_COUNT = 500000
def stamp(): return datetime.now().strftime('%Y%m%d-%H%M%S')
def iso(): return datetime.now(timezone.utc).isoformat()
def q(s): return shlex.quote(str(s))
def split(s): return shlex.split(s, posix=True) if s.strip() else []
def read_env(path):
vals = {}
if path.exists():
for raw in path.read_text(encoding='utf-8-sig').splitlines():
line = raw.strip()
if line and not line.startswith('#') and '=' in line:
k, v = line.split('=', 1); vals[k.strip()] = v.strip().strip('"').strip("'")
for k in ['AIRNDB_REMOTE_SSH_TARGET','AIRNDB_REMOTE_SSH_PORT','AIRNDB_REMOTE_SSH_OPTIONS','AIRNDB_REMOTE_WORKDIR','AIRNDB_REMOTE_TCPDUMP','AIRNDB_REMOTE_CAPTURE_PREFIX']:
if os.environ.get(k): vals[k] = os.environ[k]
vals.setdefault('AIRNDB_REMOTE_SSH_PORT','22'); vals.setdefault('AIRNDB_REMOTE_TCPDUMP','auto'); vals.setdefault('AIRNDB_REMOTE_CAPTURE_PREFIX','sudo -n')
return vals
def write_env(path, updates):
path.parent.mkdir(parents=True, exist_ok=True); old = path.read_text(encoding='utf-8') if path.exists() else ''; keys = set(updates); lines = []
for raw in old.splitlines():
key = raw.split('=',1)[0].strip() if '=' in raw and not raw.strip().startswith('#') else None
if key not in keys: lines.append(raw)
if lines and lines[-1].strip(): lines.append('')
for k, v in updates.items():
if v: lines.append(f'{k}={v}')
path.write_text('\n'.join(lines).rstrip()+'\n', encoding='utf-8', newline='\n')
def ensure_files(project):
d = project/'AirPlan'/'state'/'airndb'; d.mkdir(parents=True, exist_ok=True); ex = d/'remote-device.env.example'
if not ex.exists():
ex.write_text('# AirNDB remote packet-capture device configuration\nAIRNDB_REMOTE_SSH_TARGET=user@host\nAIRNDB_REMOTE_SSH_PORT=22\nAIRNDB_REMOTE_SSH_OPTIONS=\nAIRNDB_REMOTE_WORKDIR=\nAIRNDB_REMOTE_TCPDUMP=auto\nAIRNDB_REMOTE_CAPTURE_PREFIX=sudo -n\n', encoding='utf-8', newline='\n')
gi = d/'.gitignore'; old = gi.read_text(encoding='utf-8') if gi.exists() else ''
for item in ['remote-device.env','tool.env']:
if item not in old.splitlines(): old = (old.rstrip()+f'\n{item}\n').lstrip()
gi.write_text(old, encoding='utf-8', newline='\n')
def ssh_args(env, cmd):
target = env.get('AIRNDB_REMOTE_SSH_TARGET','')
if not target: raise SystemExit('AIRNDB_REMOTE_SSH_TARGET is required')
args = [shutil.which('ssh') or 'ssh']; port = env.get('AIRNDB_REMOTE_SSH_PORT','22')
if port: args += ['-p', port]
args += split(env.get('AIRNDB_REMOTE_SSH_OPTIONS','')); return args + [target, cmd]
def run(env, cmd, timeout=60):
return subprocess.run(ssh_args(env, cmd), capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=timeout)
def remote_home(env):
r = run(env, 'printf %s "$HOME"', 15); return r.stdout.strip() if r.returncode == 0 else ''
def workdir(env):
if env.get('AIRNDB_REMOTE_WORKDIR'): return env['AIRNDB_REMOTE_WORKDIR']
home = remote_home(env); return (home.rstrip('/')+'/.airndb') if home else '.airndb'
def detect_tool(env):
tool = env.get('AIRNDB_REMOTE_TCPDUMP','auto').strip()
if tool and tool != 'auto': return tool
r = run(env, 'for t in tcpdump dumpcap windump WinDump.exe; do command -v "$t" >/dev/null 2>&1 && { command -v "$t"; exit 0; }; done', 15)
return r.stdout.strip().splitlines()[-1] if r.returncode == 0 and r.stdout.strip() else ''
def install_tool(env):
script = """set -e
if command -v tcpdump >/dev/null 2>&1; then exit 0; fi
if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then SUDO=""; else SUDO="sudo -n"; fi
if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update && $SUDO apt-get install -y tcpdump;
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y tcpdump;
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y tcpdump;
elif command -v apk >/dev/null 2>&1; then $SUDO apk add tcpdump;
elif command -v pacman >/dev/null 2>&1; then $SUDO pacman -Sy --noconfirm tcpdump;
else exit 42; fi"""
return run(env, script, 300)
def setup(project, env, auto=True):
if not env.get('AIRNDB_REMOTE_SSH_TARGET'):
return {'status':'blocked','reason':'missing_remote_target','example':str(project/'AirPlan'/'state'/'airndb'/'remote-device.env.example')}
if not shutil.which('ssh'): return {'status':'blocked','reason':'ssh_not_found'}
probe = run(env, 'printf ok', 20)
if probe.returncode: return {'status':'blocked','reason':'ssh_probe_failed','stderr':probe.stderr.strip()}
wd = workdir(env); mk = run(env, f'mkdir -p {q(wd)}', 20)
if mk.returncode: return {'status':'blocked','reason':'remote_workdir_failed','stderr':mk.stderr.strip()}
tool = detect_tool(env); install = 'skipped'
if not tool and auto:
inst = install_tool(env); install = 'ok' if inst.returncode == 0 else f'failed:{inst.returncode}'; tool = detect_tool(env)
upd = {'AIRNDB_REMOTE_WORKDIR':wd}
if tool: upd['AIRNDB_REMOTE_TCPDUMP'] = tool
write_env(project/'AirPlan'/'state'/'airndb'/'remote-device.env', upd)
return {'status':'ok' if tool else 'blocked','target':env.get('AIRNDB_REMOTE_SSH_TARGET',''),'workdir':wd,'tool':tool,'capturePrefix':env.get('AIRNDB_REMOTE_CAPTURE_PREFIX',''),'autoConfigure':install,'hint':'' if tool else 'Install tcpdump/dumpcap or set AIRNDB_REMOTE_TCPDUMP.'}
def bounded(n):
if n < 1 or n > MAX_COUNT: raise SystemExit(f'--count must be 1..{MAX_COUNT}')
return n
def filt(expr):
if not expr.strip(): return []
try: return split(expr)
except ValueError: return expr.split()
def cap_cmd(env, tool, iface, out, count, filter_expr):
if not iface: raise SystemExit('--iface is required')
parts = split(env.get('AIRNDB_REMOTE_CAPTURE_PREFIX','')) + [tool,'-i',iface,'-nn','-s','0','-w',out,'-c',str(bounded(count))] + filt(filter_expr)
return shlex.join(parts)
def outdir(project, configured):
p = Path(configured).expanduser().resolve() if configured else project/'AirPlan'/'docs'/'network'/'airndb-captures'; p.mkdir(parents=True, exist_ok=True); return p
def emit(prefix, data):
print(f'{prefix}_status={data.get("status","unknown")}')
for k, v in data.items():
if k != 'status': print(f'{k}={json.dumps(v, ensure_ascii=False) if isinstance(v,(dict,list)) else v}')
def log(project, data):
lp = project/'AirPlan'/'docs'/'network'/'airndb-log.md'; lp.parent.mkdir(parents=True, exist_ok=True)
lp.open('a', encoding='utf-8', newline='\n').write(f"\n## {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: AirNDB remote {data.get('action','capture')}\n\n- Remote target: `{data.get('target','')}`\n- Tool: `{data.get('tool','')}`\n- Interface: `{data.get('interface','')}`\n- Filter: `{data.get('filter','')}`\n- Command: `{data.get('command','')}`\n- Artifacts: `{data.get('artifact','')}`\n- Report: `{data.get('report','')}`\n- Result: `{data.get('status','')}`\n- AirDbg handoff: TODO\n- Residual risk: remote pcap files may contain sensitive packet contents.\n")
def main():
ap = argparse.ArgumentParser(); ap.add_argument('--project', default='.'); ap.add_argument('--action', choices=['setup','status','interfaces','command','capture'], default='setup'); ap.add_argument('--iface', default=''); ap.add_argument('--filter', default=''); ap.add_argument('--count', type=int, default=200); ap.add_argument('--timeout', type=int, default=30); ap.add_argument('--output-dir', default=''); ap.add_argument('--output', default=''); ap.add_argument('--no-auto-configure', action='store_true'); a = ap.parse_args()
project = Path(a.project).expanduser().resolve(); ensure_files(project); env = read_env(project/'AirPlan'/'state'/'airndb'/'remote-device.env')
info = setup(project, env, auto=(a.action!='status' and not a.no_auto_configure))
if a.action in ['setup','status'] or info.get('status') != 'ok': emit('airndb_remote', info); raise SystemExit(0 if info.get('status')=='ok' else 2)
od = outdir(project, a.output_dir); tool = info['tool']
if a.action == 'interfaces':
icmd = shlex.join(split(env.get('AIRNDB_REMOTE_CAPTURE_PREFIX','')) + [tool,'-D']); r = run(env, icmd, max(5,a.timeout)); data = {'action':'interfaces','status':'ok' if r.returncode==0 else 'failed','target':info['target'],'tool':tool,'command':shlex.join(ssh_args(env, icmd)),'stdout':r.stdout.strip(),'stderr':r.stderr.strip()}; report = od/(stamp()+'-airndb-remote-interfaces.json'); report.write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n', encoding='utf-8'); data['report']=str(report); log(project,data); emit('airndb_remote',data); print(r.stdout); print(r.stderr); raise SystemExit(0 if data['status']=='ok' else 1)
remote = a.output or info['workdir'].rstrip('/') + '/' + stamp() + '-airndb-remote.pcap'; cmd = cap_cmd(env, tool, a.iface, remote, a.count, a.filter)
if a.action == 'command': emit('airndb_remote', {'status':'ok','target':info['target'],'remotePcap':remote,'command':shlex.join(ssh_args(env, cmd))}); return
r = run(env, cmd, max(1,a.timeout)); data = {'action':'capture','status':'ok' if r.returncode==0 else 'failed','target':info['target'],'tool':tool,'interface':a.iface,'filter':a.filter,'count':bounded(a.count),'timeoutSeconds':a.timeout,'remotePcap':remote,'command':shlex.join(ssh_args(env, cmd)),'stdout':r.stdout.strip(),'stderr':r.stderr.strip(),'capturedAt':iso()}
if r.returncode == 0:
b64 = run(env, f'base64 < {q(remote)}', a.timeout); local = od/(stamp()+'-airndb-remote.pcap'); local.write_bytes(base64.b64decode(''.join(b64.stdout.split()))); data['artifact'] = str(local)
report = od/(stamp()+'-airndb-remote-capture.json'); report.write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n', encoding='utf-8'); data['report'] = str(report); log(project, data); emit('airndb_remote', data); raise SystemExit(0 if data['status']=='ok' else 1)
if __name__ == '__main__': main()

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Install the AirNDB plugin into the current user's home-local plugin directory."""
from __future__ import annotations
import argparse
import json
import shutil
from pathlib import Path
from typing import Any, Dict
PLUGIN_NAME = "airndb"
def copy_plugin(source: Path, target: Path) -> None:
if source.resolve() == target.resolve():
return
if target.exists():
shutil.rmtree(target)
ignore = shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store", ".git")
shutil.copytree(source, target, ignore=ignore)
def marketplace_payload() -> Dict[str, Any]:
return {
"name": "local-airarc",
"interface": {"displayName": "Local AirArc Plugins"},
"plugins": [],
}
def update_marketplace(path: Path) -> None:
if path.exists():
payload = json.loads(path.read_text(encoding="utf-8"))
else:
payload = marketplace_payload()
payload.setdefault("name", "local-airarc")
payload.setdefault("interface", {}).setdefault("displayName", "Local AirArc Plugins")
plugins = payload.setdefault("plugins", [])
entry = {
"name": PLUGIN_NAME,
"source": {
"source": "local",
"path": f"./plugins/{PLUGIN_NAME}",
},
"policy": {
"installation": "INSTALLED_BY_DEFAULT",
"authentication": "ON_INSTALL",
},
"category": "Productivity",
}
for index, existing in enumerate(plugins):
if existing.get("name") == PLUGIN_NAME:
plugins[index] = entry
break
else:
plugins.append(entry)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Install AirNDB as a home-local Codex plugin.")
parser.add_argument("--source", default=str(Path(__file__).resolve().parents[1]))
parser.add_argument("--home", default=str(Path.home()))
return parser.parse_args()
def main() -> None:
args = parse_args()
source = Path(args.source).expanduser().resolve()
home = Path(args.home).expanduser().resolve()
target = home / "plugins" / PLUGIN_NAME
marketplace = home / ".agents" / "plugins" / "marketplace.json"
copy_plugin(source, target)
update_marketplace(marketplace)
print(f"installed_plugin={target}")
print(f"marketplace={marketplace}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,214 @@
---
name: airndb
description: Network-debug packet capture workflow. Use when the user invokes /airndb or asks to debug networking, packet loss, DNS, TCP, UDP, TLS handshakes, HTTP connectivity, ports, retransmits, resets, latency, firewall, proxy, service reachability, pcap files, tcpdump, WinDump, remote packet capture over SSH, or BPF filters. Load or initialize AirPlan/AGENTS.md, AirPlan/docs/architecture/adr/ decision records, AirPlan/docs/architecture/c4/module.md, AirPlan/docs/network/airndb-log.md, and AirPlan/docs/network/airndb-captures/; on first startup detect tcpdump/WinDump and on Windows auto-download official WinDump.exe when no capture tool is available; when remote debugging, call the AirNDB remote device helper and auto-configure remote tcpdump/dumpcap when missing; build safe bounded tcpdump/WinDump commands; capture or read pcap artifacts; summarize packet evidence; and maintain AirPlan/AGENTS.md, ADR, C4 module docs, and network debug logs when capture tooling, network boundaries, or debugging decisions change.
---
# AirNDB
## 核心约束
- 全程使用中文与用户交流命令、接口名、BPF、日志、路径和协议名保持原文。
- `/airndb` 专用于网络抓包、pcap 分析和网络层调试证据收集。
- 只抓取用户授权的本机、项目、测试环境或明确允许的网络流量。
- 默认不做无界抓包;必须使用包数、超时、时长或轮转上限。
- 默认先列接口,再确认接口、目标 host/port/protocol/filter、抓包窗口和输出路径。
- 初次启动必须检测 `tcpdump` / `windump` / `WinDump.exe` 是否可用Windows 下如果不可用,自动从 WinDump 官方下载页获取 `WinDump.exe`,校验 SHA1 后写入 `AirPlan/state/airndb/tool.env`
- 远程设备、测试机、VM 或 SSH 主机上的网络调试,先调用 `../../scripts/airndb_remote_device.py`;缺少远程 `tcpdump` / `dumpcap` 时允许脚本自动配置,无法无密码 `sudo` 或无包管理器时停止并提示用户。
- 自动获取只下载 WinDump 用户态程序,不静默安装 WinPcap/Npcap 抓包驱动;如果接口列举失败,提示用户安装 Npcap 或 WinPcap 并用管理员权限重试。
- 默认使用 `-nn` 避免 DNS/service-name 解析,使用 `-s 0` 写入完整 pcap。
- pcap 可能包含凭据、cookie、token、payload、内网地址、主机名或个人信息对外分享前必须提醒脱敏。
- 网络证据要写入 `AirPlan/docs/network/airndb-log.md`pcap/摘要/JSON 报告写入 `AirPlan/docs/network/airndb-captures/`
- 根据项目变化维护 `AirPlan/AGENTS.md`、ADR 和 C4 module。
- 如果需要 WinDump/tcpdump 选项和 BPF 简表,读取 [references/windump-tcpdump-notes.md](references/windump-tcpdump-notes.md)。
## 启动与初始化
进入 `/airndb` 时运行:
```bash
python ../../scripts/airndb_mode.py --mode enter --project .
```
如果当前环境没有 `python`,尝试 `py``python3` 或用户提供的 Python 绝对路径。脚本不可用时,手动确保以下结构存在:
- `AirPlan/AGENTS.md`
- `AirPlan/docs/architecture/adr/`
- `AirPlan/docs/architecture/c4/module.md`
- `AirPlan/docs/network/airndb-log.md`
- `AirPlan/docs/network/airndb-captures/`
- `AirPlan/state/airndb/state.json`
初始化后读取已有内容作为上下文。不要覆盖用户已有正文;只补齐缺失结构或更新 AirNDB 标记块。
## 首次工具配置
`airndb_mode.py --mode enter` 会执行工具自检:
1. 查找显式配置、项目 `AirPlan/state/airndb/tool.env`、环境变量 `AIRNDB_TCPDUMP`、项目 `AirPlan/state/airndb/tools/WinDump.exe`、PATH 中的 `windump` / `WinDump.exe` / `tcpdump`
2. 如果找到可用工具,写入或刷新 `AirPlan/state/airndb/tool.env`,后续 `airndb_capture.py` 自动读取。
3. 如果 Windows 上找不到工具,自动从 WinDump 官方下载地址获取 `WinDump.exe`,校验 SHA1 `d59bc54721951dec855cbb4bbc000f9a71ea4d95`,保存到 `AirPlan/state/airndb/tools/WinDump.exe`,然后写入 `AirPlan/state/airndb/tool.env`
4. 如果下载失败或校验失败,停止并提示用户手动安装 `tcpdump` / `WinDump.exe` 或设置 `AIRNDB_TCPDUMP`
`AirPlan/state/airndb/tool.env` 是本机路径配置,由 `AirPlan/state/airndb/.gitignore` 忽略,不应提交。
注意WinDump 仍需要抓包驱动。官方 WinDump 安装页要求先安装 WinPcap 3.1 或更新版本WinPcap 主页提示项目已停止维护并建议 Windows 10 用户使用 Npcap。AirNDB 不静默安装驱动,只负责检测、下载 WinDump.exe 和配置本机路径。
## 远程设备工具配置
当用户说明目标流量发生在远程设备、测试机、服务器、VM、容器宿主机、SSH 主机,或本机抓包看不到目标流量时,不要先使用本机 `airndb_capture.py`。先运行远程设备 helper
```bash
python ../../scripts/airndb_remote_device.py --project . --action setup
```
如果当前环境没有 `python`,尝试 `py``python3` 或用户提供的 Python 绝对路径。首次运行会生成 `AirPlan/state/airndb/remote-device.env.example`;把连接信息写入 `AirPlan/state/airndb/remote-device.env` 或当前环境变量:
- `AIRNDB_REMOTE_SSH_TARGET=user@host`
- `AIRNDB_REMOTE_SSH_PORT=22`
- `AIRNDB_REMOTE_SSH_OPTIONS=`
- `AIRNDB_REMOTE_WORKDIR=`
- `AIRNDB_REMOTE_TCPDUMP=auto`
- `AIRNDB_REMOTE_CAPTURE_PREFIX=sudo -n`
远程 helper 行为:
- 检查本机 `ssh`、远程连通性和远程工作目录。
- 探测 `tcpdump``dumpcap``windump``WinDump.exe`
- 工具缺失时自动尝试用远端包管理器安装 `tcpdump`,只使用非交互式 `sudo -n`;需要密码、管理员确认或无支持的包管理器时停止并提示用户。
- 将可复用配置写入 `AirPlan/state/airndb/remote-device.env`,该文件由 `AirPlan/state/airndb/.gitignore` 忽略。
- 抓包产物拉回 `AirPlan/docs/network/airndb-captures/`,并追加 `AirPlan/docs/network/airndb-log.md`
常用远程命令:
```bash
python ../../scripts/airndb_remote_device.py --project . --action interfaces
python ../../scripts/airndb_remote_device.py --project . --action command --iface <iface> --filter "<bpf>" --count 200
python ../../scripts/airndb_remote_device.py --project . --action capture --iface <iface> --filter "<bpf>" --count 200 --timeout 30
```
远程抓包仍必须有明确授权、接口、BPF、包数或超时上限。`AIRNDB_REMOTE_CAPTURE_PREFIX` 默认是 `sudo -n`;如果远端已配置免 sudo 的 capture capability可改为空或指定更合适的前缀。
## 工作流
1. 明确网络问题:
- 现象连不上、超时、重置、DNS 异常、TLS 握手失败、丢包、延迟、端口不可达、代理/防火墙疑似问题。
- 目标:源/目的 host、端口、协议、服务名、容器/VM/WSL/宿主机边界。
- 抓包窗口:包数、超时、复现步骤和是否允许保存 payload。
2. 发现接口:
- 本机调试运行 `airndb_capture.py --action interfaces`
- 远程调试运行 `airndb_remote_device.py --action interfaces`
- Windows 优先使用 `windump -D``WinDump.exe -D`Linux/macOS 优先 `tcpdump -D`
3. 设计过滤器:
- 使用最窄可行 BPF`host``src host``dst host``port``tcp``udp``icmp``net`
- 不确定时先短时宽过滤,再根据结果收窄。
4. 执行有界抓包:
- 使用 `airndb_capture.py --action capture --iface <iface> --filter "<bpf>" --count <n> --timeout <seconds>`
- 产物写入 `AirPlan/docs/network/airndb-captures/`
5. 读取和分析:
- 使用 `airndb_capture.py --action read --read-file <pcap> --filter "<bpf>"` 生成文本摘要。
- 结合时间线、TCP flags、重传、RST、DNS 响应、ICMP、TLS ClientHello/ServerHello 迹象判断网络层事实。
6. 记录证据:
- exact command
- interface
- BPF filter
- packet count or timeout
- pcap path
- summary/report path
- 观察结论、限制和剩余风险
## 与 AirDbg 协作
- AirNDB 负责抓包、pcap 摘要、网络层证据和过滤器。
- AirDbg 负责代码层根因分析、修复和验证收尾。
- AirDbg 调试中遇到 DNS、TCP、UDP、TLS、HTTP 连接、端口、代理、防火墙、丢包、重传或 pcap 证据需求时,可以调用 AirNDB。
- AirNDB 收集到的证据必须能被 AirDbg 直接引用命令、pcap 路径、摘要、关键包、时间线和结论要写清楚。
## 常用命令
列接口:
```bash
python ../../scripts/airndb_capture.py --project . --action interfaces
```
检查或初始化工具路径:
```bash
python ../../scripts/airndb_mode.py --project . --mode enter
```
只生成命令:
```bash
python ../../scripts/airndb_capture.py --project . --action command --iface 1 --filter "tcp and port 443" --count 200
```
短时抓包:
```bash
python ../../scripts/airndb_capture.py --project . --action capture --iface 1 --filter "tcp and port 443" --count 200 --timeout 30
```
读取 pcap
```bash
python ../../scripts/airndb_capture.py --project . --action read --read-file AirPlan/docs/network/airndb-captures/example.pcap
```
## AGENTS.md 维护
在以下情况更新 `AGENTS.md`
- 发现稳定可复用的 tcpdump/WinDump 命令、接口选择规则、BPF 过滤器或 pcap 读取方式。
- 发现影响后续 AI 调试的网络边界容器、WSL、VM、代理、防火墙、VPN、DNS、TLS、NAT、端口映射。
- 发现抓包权限、驱动、管理员权限或平台差异。
- 发现本机 tcpdump/WinDump 路径或 `AirPlan/state/airndb/tool.env` 配置方式。
- 发现远程设备 SSH 入口、远程抓包工具、`AIRNDB_REMOTE_*` 配置方式或远端抓包权限限制。
## ADR 维护
目录:`docs/architecture/adr/`
需要 ADR 的情况:
- 长期采用 tcpdump/WinDump 作为项目网络诊断方式。
- 抓包流程改变了测试边界、网络观测边界、运行权限、数据留存或安全策略。
- 发现需要保留的网络架构决策例如代理、DNS、TLS、端口、服务发现或跨容器/宿主机边界。
ADR 保持简洁Context、Decision、Consequences、Alternatives。
## C4 Module 维护
文件:`docs/architecture/c4/module.md`
当网络调试发现或改变以下内容时,必须更新:
- 模块间网络依赖。
- 服务端口、协议、DNS、代理、TLS、队列、网关、容器/宿主机/WSL/VM 边界。
- 抓包或观测基础设施成为长期模块或运行边界。
- 网络错误处理、重试、超时、连接池或安全边界。
## network log 维护
文件:`AirPlan/docs/network/airndb-log.md`
每次 AirNDB 会话至少追加:
- 问题摘要。
- 授权范围和目标流量。
- 接口、BPF、抓包窗口。
- 是否使用远程设备 helper 以及远程目标、抓包工具和权限限制。
- pcap/summary/report 路径。
- 关键包或时间线观察。
- 结论、限制和给 AirDbg 的线索。
- ADR/C4/AGENTS 更新。
## 完成输出
本轮网络调试结束时,用中文简洁汇报:
- 使用了哪个接口和过滤器。
- 抓包是否成功,证据在哪里。
- 关键观察和网络层结论。
- 更新了哪些 `AGENTS.md` / ADR / C4 / network log。
- 是否需要切给 AirDbg 做代码层修复。

View File

@@ -0,0 +1,3 @@
name: airndb
short_description: Local and remote packet capture workflow with tcpdump and WinDump
default_prompt: "使用 AirNDB 做安全有界抓包;远程设备优先调用 remote device helper 并自动配置 tcpdump。"

View File

@@ -0,0 +1,83 @@
# WinDump / Tcpdump Notes
Source: https://www.winpcap.org/windump/docs/manual.htm
## AirNDB Summary
- WinDump follows tcpdump-style packet capture usage on Windows.
- `-D` lists available capture interfaces.
- `-i <interface>` selects the capture interface. On Windows this is often the interface number from `-D`.
- `-c <count>` stops after a bounded number of packets.
- `-w <file>` writes raw packets to a pcap file.
- `-r <file>` reads packets back from a pcap file.
- `-n` avoids host name resolution; `-nn` also avoids service name resolution.
- `-s <snaplen>` controls packet snapshot length. AirNDB uses `-s 0` for pcap captures so packets are not truncated.
- Filter expressions use BPF primitives such as `host`, `net`, `port`, `src`, `dst`, `tcp`, `udp`, `icmp`, `arp`, `and`, `or`, and `not`.
## Windows Notes
- Prefer `WinDump.exe` or `windump` when `tcpdump` is unavailable on Windows.
- WinDump normally requires a packet capture driver such as WinPcap/Npcap and may require an elevated terminal.
- Interface names can be long adapter paths; the numeric index from `windump -D` is usually easier to use.
- Store pcap artifacts in a project-local ignored directory such as `docs/network/airndb-captures/`.
## AirNDB Auto Setup
- On `/airndb enter`, AirNDB checks for `tcpdump`, `windump`, or `WinDump.exe`.
- If no capture tool is available on Windows, AirNDB downloads the official `WinDump.exe` linked from the WinDump install page:
```text
https://www.winpcap.org/windump/install/bin/windump_3_9_5/WinDump.exe
```
- AirNDB verifies SHA1 before using the file:
```text
d59bc54721951dec855cbb4bbc000f9a71ea4d95
```
- AirNDB stores the binary at `AirPlan/state/airndb/tools/WinDump.exe` and writes `AirPlan/state/airndb/tool.env`:
```text
AIRNDB_TCPDUMP=<absolute path to WinDump.exe>
```
- AirNDB does not silently install WinPcap/Npcap drivers. If `WinDump.exe -D` fails after download, tell the user to install Npcap or WinPcap and retry from an elevated terminal.
## Safe Defaults
- Start with interface discovery before capture:
```bash
windump -D
tcpdump -D
```
- Prefer short, bounded capture:
```bash
tcpdump -i <iface> -nn -s 0 -w <file>.pcap -c 200 '<bpf>'
```
- Read back a pcap summary:
```bash
tcpdump -nn -r <file>.pcap '<bpf>'
```
## BPF Examples
```text
host 192.0.2.10
tcp and port 443
udp and port 53
src host 192.0.2.10 and dst port 443
net 10.0.0.0/8 and not port 22
icmp or icmp6
```
## Evidence Rules
- Record exact command, interface, filter, packet count, capture window, pcap path, and summary path.
- Keep pcap files private unless reviewed; they can contain tokens, cookies, payload, internal hostnames, and addresses.
- If application payload is encrypted, use packet timing, DNS, TCP/TLS handshakes, retransmissions, resets, or connection failures as evidence instead of expecting plaintext.