From 7507507fd9300812defda7eeaf09329c93548195 Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:06:41 +0800 Subject: [PATCH] fix(forward): show all users by default in non-compact mode Restore 2.1.8-beta9 admin default filtering when compact mode is off; keep compact mode focused on self. --- .agents/skills/security-scan/SKILL.md | 165 ++++++++++++++++++ .claude/skills/security-scan | 1 + .github/ISSUES/forward-show-tunnel-ratio.md | 62 +++++++ plans/040-gost-udp-vs-realm-analysis.md | 178 ++++++++++++++++++++ plans/041-node-info-popover.md | 18 ++ skills-lock.json | 10 ++ vite-frontend/src/pages/forward.tsx | 106 ++++++++---- 7 files changed, 507 insertions(+), 33 deletions(-) create mode 100644 .agents/skills/security-scan/SKILL.md create mode 120000 .claude/skills/security-scan create mode 100644 .github/ISSUES/forward-show-tunnel-ratio.md create mode 100644 plans/040-gost-udp-vs-realm-analysis.md create mode 100644 plans/041-node-info-popover.md create mode 100644 skills-lock.json diff --git a/.agents/skills/security-scan/SKILL.md b/.agents/skills/security-scan/SKILL.md new file mode 100644 index 0000000..dba372e --- /dev/null +++ b/.agents/skills/security-scan/SKILL.md @@ -0,0 +1,165 @@ +--- +name: security-scan +description: Scan your Claude Code configuration (.claude/ directory) for security vulnerabilities, misconfigurations, and injection risks using AgentShield. Checks CLAUDE.md, settings.json, MCP servers, hooks, and agent definitions. +origin: ECC +--- + +# Security Scan Skill + +Audit your Claude Code configuration for security issues using [AgentShield](https://github.com/affaan-m/agentshield). + +## When to Activate + +- Setting up a new Claude Code project +- After modifying `.claude/settings.json`, `CLAUDE.md`, or MCP configs +- Before committing configuration changes +- When onboarding to a new repository with existing Claude Code configs +- Periodic security hygiene checks + +## What It Scans + +| File | Checks | +|------|--------| +| `CLAUDE.md` | Hardcoded secrets, auto-run instructions, prompt injection patterns | +| `settings.json` | Overly permissive allow lists, missing deny lists, dangerous bypass flags | +| `mcp.json` | Risky MCP servers, hardcoded env secrets, npx supply chain risks | +| `hooks/` | Command injection via interpolation, data exfiltration, silent error suppression | +| `agents/*.md` | Unrestricted tool access, prompt injection surface, missing model specs | + +## Prerequisites + +AgentShield must be installed. Check and install if needed: + +```bash +# Check if installed +npx ecc-agentshield --version + +# Install globally (recommended) +npm install -g ecc-agentshield + +# Or run directly via npx (no install needed) +npx ecc-agentshield scan . +``` + +## Usage + +### Basic Scan + +Run against the current project's `.claude/` directory: + +```bash +# Scan current project +npx ecc-agentshield scan + +# Scan a specific path +npx ecc-agentshield scan --path /path/to/.claude + +# Scan with minimum severity filter +npx ecc-agentshield scan --min-severity medium +``` + +### Output Formats + +```bash +# Terminal output (default) — colored report with grade +npx ecc-agentshield scan + +# JSON — for CI/CD integration +npx ecc-agentshield scan --format json + +# Markdown — for documentation +npx ecc-agentshield scan --format markdown + +# HTML — self-contained dark-theme report +npx ecc-agentshield scan --format html > security-report.html +``` + +### Auto-Fix + +Apply safe fixes automatically (only fixes marked as auto-fixable): + +```bash +npx ecc-agentshield scan --fix +``` + +This will: +- Replace hardcoded secrets with environment variable references +- Tighten wildcard permissions to scoped alternatives +- Never modify manual-only suggestions + +### Opus 4.6 Deep Analysis + +Run the adversarial three-agent pipeline for deeper analysis: + +```bash +# Requires ANTHROPIC_API_KEY +export ANTHROPIC_API_KEY=your-key +npx ecc-agentshield scan --opus --stream +``` + +This runs: +1. **Attacker (Red Team)** — finds attack vectors +2. **Defender (Blue Team)** — recommends hardening +3. **Auditor (Final Verdict)** — synthesizes both perspectives + +### Initialize Secure Config + +Scaffold a new secure `.claude/` configuration from scratch: + +```bash +npx ecc-agentshield init +``` + +Creates: +- `settings.json` with scoped permissions and deny list +- `CLAUDE.md` with security best practices +- `mcp.json` placeholder + +### GitHub Action + +Add to your CI pipeline: + +```yaml +- uses: affaan-m/agentshield@v1 + with: + path: '.' + min-severity: 'medium' + fail-on-findings: true +``` + +## Severity Levels + +| Grade | Score | Meaning | +|-------|-------|---------| +| A | 90-100 | Secure configuration | +| B | 75-89 | Minor issues | +| C | 60-74 | Needs attention | +| D | 40-59 | Significant risks | +| F | 0-39 | Critical vulnerabilities | + +## Interpreting Results + +### Critical Findings (fix immediately) +- Hardcoded API keys or tokens in config files +- `Bash(*)` in the allow list (unrestricted shell access) +- Command injection in hooks via `${file}` interpolation +- Shell-running MCP servers + +### High Findings (fix before production) +- Auto-run instructions in CLAUDE.md (prompt injection vector) +- Missing deny lists in permissions +- Agents with unnecessary Bash access + +### Medium Findings (recommended) +- Silent error suppression in hooks (`2>/dev/null`, `|| true`) +- Missing PreToolUse security hooks +- `npx -y` auto-install in MCP server configs + +### Info Findings (awareness) +- Missing descriptions on MCP servers +- Prohibitive instructions correctly flagged as good practice + +## Links + +- **GitHub**: [github.com/affaan-m/agentshield](https://github.com/affaan-m/agentshield) +- **npm**: [npmjs.com/package/ecc-agentshield](https://www.npmjs.com/package/ecc-agentshield) diff --git a/.claude/skills/security-scan b/.claude/skills/security-scan new file mode 120000 index 0000000..fbee699 --- /dev/null +++ b/.claude/skills/security-scan @@ -0,0 +1 @@ +../../.agents/skills/security-scan \ No newline at end of file diff --git a/.github/ISSUES/forward-show-tunnel-ratio.md b/.github/ISSUES/forward-show-tunnel-ratio.md new file mode 100644 index 0000000..af5bbb7 --- /dev/null +++ b/.github/ISSUES/forward-show-tunnel-ratio.md @@ -0,0 +1,62 @@ +# 功能请求:在规则页面显示隧道倍率 + +## 问题描述 + +当前规则(Forward)页面在列表中显示隧道名称,但**不显示隧道的流量倍率(trafficRatio)**。管理员在管理规则时无法快速查看该规则所使用的隧道倍率信息,需要跳转到隧道页面才能查看。 + +## 期望行为 + +在规则列表页面中,在隧道名称旁边或单独列显示该隧道的流量倍率(例如:`1x`, `0.5x`, `2x`)。 + +## 建议实现位置 + +### 前端修改 + +1. **`vite-frontend/src/pages/forward.tsx`** + - 在 `Forward` interface 中添加 `tunnelTrafficRatio?: number` 字段 + - 在表格列中添加倍率显示(可以在隧道名称 Chip 旁边或单独一列) + - 从 `userTunnel` 或 `getTunnelList` API 获取隧道倍率信息 + +2. **显示格式建议** + ```tsx + + {forward.tunnelName} ({forward.tunnelTrafficRatio}x) + + ``` + 或者单独一列: + ```tsx + + {forward.tunnelTrafficRatio}x + + ``` + +### 后端修改 + +1. **`go-backend/internal/http/handler/handler.go`** + - 在 `forwardList` 接口返回中添加隧道的 `trafficRatio` 字段 + - 需要在查询 Forward 时 JOIN Tunnel 表获取倍率信息 + +2. **或者在前端加载规则后,批量获取隧道信息** + - 调用 `getTunnelList` 获取所有隧道信息 + - 根据 `tunnelId` 匹配倍率 + +## 相关文件 + +- 前端:`vite-frontend/src/pages/forward.tsx` +- 前端类型:`vite-frontend/src/api/types.ts` +- 后端:`go-backend/internal/http/handler/handler.go` +- 隧道类型定义:`vite-frontend/src/api/types.ts` (TunnelApiItem) + +## 优先级 + +中等 - 不影响核心功能,但能提升管理效率 + +## 截图参考 + +隧道页面已显示倍率: +- 位置:隧道卡片统计信息区域 +- 显示格式:`流量倍率 {trafficRatio}x` + +--- + +**Labels**: `enhancement`, `frontend`, `backend`, `ui/ux` diff --git a/plans/040-gost-udp-vs-realm-analysis.md b/plans/040-gost-udp-vs-realm-analysis.md new file mode 100644 index 0000000..58b2696 --- /dev/null +++ b/plans/040-gost-udp-vs-realm-analysis.md @@ -0,0 +1,178 @@ +# GOST UDP vs Realm UDP Analysis + +**Goal:** Compare GOST UDP forwarding through tunnels with Realm's UDP implementation to understand why users report GOST UDP forwarding has problems while Realm works correctly. + +## Task Checklist + +- [x] Analyze GOST UDP tunnel architecture +- [x] Analyze Realm UDP relay architecture +- [x] Identify architectural differences +- [x] Identify potential issues in GOST implementation +- [ ] Document findings and recommendations + +--- + +## GOST UDP Architecture + +### Core Components + +1. **UDP Relay** (`x/internal/net/udp/relay.go`) + - Simple bidirectional packet copying between two `net.PacketConn` interfaces + - Uses two goroutines: one for each direction + - Sequential packet processing (no batching) + - No idle timeout or association tracking + +2. **UDP over Tunnel** (`x/handler/relay/bind.go`, `x/handler/socks/v5/udp_tun.go`) + - Wraps UDP data with SOCKS5-style framing via `UDPTunServerConn()` + - Adds address headers to each packet + - Uses smux multiplexing for tunnel connections + +3. **SOCKS5 UDP Framing** (`x/internal/util/socks/conn.go`, `x/internal/util/relay/conn.go`) + - `udpTunConn.ReadFrom()`: Parses SOCKS5 UDP header to extract target address + - `udpTunConn.WriteTo()`: Wraps data with SOCKS5 UDP header including: + - RSV (data length, 2 bytes) + - Frag (0xff for tunnel relay, 1 byte) + - Address (4/16/1+n bytes for IPv4/IPv6/domain) + +4. **Multiplexing** (`x/internal/util/mux/mux.go`) + - Uses smux v1.5.31 for connection multiplexing + - Adds stream framing and flow control overhead + +### Data Flow (UDP over Tunnel) + +``` +Client UDP packet + ↓ +[Local GOST] SOCKS5 UDP framing (add ~10-26 bytes) + ↓ +smux stream (add framing, flow control) + ↓ +TCP tunnel to remote + ↓ +[Remote GOST] smux demux + ↓ +SOCKS5 UDP deframing + ↓ +Forward to target +``` + +--- + +## Realm UDP Architecture + +### Core Components + +1. **Association Model** (`realm_core/src/udp/middle.rs`) + - Per-client socket associations stored in `SockMap` + - Creates dedicated remote socket per client address + - Spawns `send_back` task for return path + - Association timeout for cleanup + +2. **Batched I/O** (`realm_core/src/udp/batched.rs`) + - Uses `recvmmsg/sendmmsg` on Linux + - Up to 128 packets per batch + - Significantly higher throughput for high-PPS traffic + +3. **No Protocol Overhead** + - Plain UDP forwarding without adding protocol headers + - No SOCKS5 framing or mux layer + +### Data Flow + +``` +Client UDP packet + ↓ +[Realm] Direct relay via association socket + ↓ +Target server +``` + +--- + +## Key Architectural Differences + +| Aspect | GOST | Realm | +|--------|------|-------| +| **UDP over Tunnel** | SOCKS5 framing + smux multiplexing | N/A (direct relay only) | +| **Protocol Overhead** | Extra ~10-26 bytes per packet | None | +| **I/O Model** | Standard Go net.PacketConn | Batched I/O (recvmmsg/sendmmsg) | +| **Connection Tracking** | Via mux session | SockMap with timeout | +| **Association Model** | None (bidirectional copy only) | Per-client socket association | +| **Idle Timeout** | None on UDP relay | Association timeout | +| **Throughput** | Single packet per syscall | Up to 128 packets per syscall | + +--- + +## Identified Potential Issues in GOST + +### 1. No Batch I/O Support +- **Impact**: High PPS (packets per second) traffic incurs syscall overhead +- **Realm advantage**: `recvmmsg/sendmmsg` batches up to 128 packets +- **Evidence**: Realm's `batched.rs` implements this; GOST uses standard `ReadFrom/WriteTo` + +### 2. Protocol Overhead Per Packet +- **Impact**: Bandwidth waste, extra processing for framing/deframing +- **Overhead**: SOCKS5 UDP header adds ~10 bytes (IPv4) to ~26 bytes (IPv6) per packet +- **Evidence**: `x/internal/util/socks/conn.go:63-78` shows framing overhead + +### 3. Mux Layer Overhead +- **Impact**: Latency and throughput degradation +- **smux adds**: Stream framing, flow control, potential backpressure +- **Evidence**: `x/internal/util/mux/mux.go` wraps all tunnel connections + +### 4. No Association Tracking +- **Impact**: Cannot properly handle NAT translation for return traffic +- **Realm approach**: `SockMap` tracks client→remote socket mapping +- **Evidence**: GOST's `udp.Relay` just copies packets bidirectionally + +### 5. No Idle Timeout on UDP Relay +- **Impact**: Stale connections may persist indefinitely +- **Evidence**: `udp.Relay.Run()` blocks until error or context cancel +- **Contrast**: Realm has association timeout for cleanup + +### 6. Sequential Packet Processing +- **Impact**: Cannot pipeline multiple packets +- **Evidence**: `relay.go:44-77` shows sequential `ReadFrom` → `WriteTo` loop +- **Contrast**: Realm's batched I/O handles multiple packets concurrently + +### 7. Read Deadline on Underlying TCP +- **Impact**: Could cause unexpected connection termination +- **Default**: 15-second read timeout set on connections +- **Evidence**: `x/handler/relay/metadata.go:readTimeout` defaults to 15s +- **Issue**: UDP relay may not handle deadline properly + +--- + +## Recommendations + +1. **Consider Direct UDP Mode**: For scenarios where tunnel is not required, use direct UDP relay (no SOCKS5 framing) + +2. **Add Association Tracking**: Implement client→remote socket mapping with timeout + +3. **Investigate Batch I/O**: Consider using `recvmmsg/sendmmsg` equivalent in Go (via `x/net` or raw syscalls) + +4. **Add Idle Timeout**: Implement timeout-based cleanup for UDP associations + +5. **Reduce Framing Overhead**: Consider more compact framing for tunnel UDP + +--- + +## Files Analyzed + +### GOST Files +- `go-gost/x/internal/net/udp/relay.go` - Core UDP relay logic +- `go-gost/x/internal/util/mux/mux.go` - smux multiplexing +- `go-gost/x/internal/util/socks/conn.go` - SOCKS5 UDP framing +- `go-gost/x/internal/util/relay/conn.go` - Relay UDP framing +- `go-gost/x/handler/relay/bind.go` - Relay BIND handler +- `go-gost/x/handler/socks/v5/udp_tun.go` - SOCKS5 UDP tunnel handler +- `go-gost/x/handler/tunnel/bind.go` - Tunnel BIND handler +- `go-gost/x/connector/tunnel/bind.go` - Tunnel connector BIND +- `go-gost/x/connector/tunnel/conn.go` - Tunnel connection types +- `go-gost/x/connector/tunnel/listener.go` - Tunnel bind listener + +### Realm Files (fetched from GitHub) +- `realm_core/src/udp/mod.rs` - UDP relay entry point +- `realm_core/src/udp/middle.rs` - Association and relay logic with SockMap +- `realm_core/src/udp/socket.rs` - UDP socket binding and association +- `realm_core/src/udp/batched.rs` - Batched I/O implementation \ No newline at end of file diff --git a/plans/041-node-info-popover.md b/plans/041-node-info-popover.md new file mode 100644 index 0000000..fab9e86 --- /dev/null +++ b/plans/041-node-info-popover.md @@ -0,0 +1,18 @@ +# Node Card Info Popover + +## Goal + +Restore node card's remark and renewal info display to the 2.1.8-beta9 style: an info button (ℹ️) in the CardHeader that shows a hover popover with the info, instead of the current inline display in the CardBody. + +## Status: Completed + +PR #327 merged + +## Tasks + +- [x] Add `infoPopoverPlacement` state and `updateInfoPopoverPlacement` callback +- [x] Add info button with popover to CardHeader +- [x] Restore drag handle with touch support +- [x] Remove inline info display from CardBody +- [x] Fix build errors (JSX structure and unused variables) +- [x] Create PR and merge \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..01e0ff2 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "security-scan": { + "source": "affaan-m/everything-claude-code", + "sourceType": "github", + "computedHash": "92cdcaddc554e318402f066ccc073c2e3dbcfda8c2730ec62ec373f805c41a57" + } + } +} diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 22214a4..a11ce5a 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -784,9 +784,9 @@ const SortableTableRow = ({ className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.inbound} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`} > @@ -1010,7 +1008,9 @@ const SortableCompactTableRow = ({ }`} title={formatInAddress(forward.inIp, forward.inPort)} type="button" - onClick={() => showAddressModal(forward.inIp, forward.inPort, "入口端口")} + onClick={() => + showAddressModal(forward.inIp, forward.inPort, "入口端口") + } > {formatInAddress(forward.inIp, forward.inPort)} @@ -1143,23 +1143,80 @@ export default function ForwardPage() { const tokenRoleId = JwtUtil.getRoleIdFromToken(); const isAdmin = tokenRoleId === 0; + const [compactMode, setCompactMode] = useState(false); + + // 在非精简模式下(compactMode=false),管理员默认展示全部用户规则(与 2.1.8-beta9 行为一致)。 + // 精简模式下默认仅展示管理员本人规则,避免在表格里混看不清所属用户。 + const defaultSearchUserId = useMemo(() => { + if (isAdmin) { + if (!compactMode) { + return "all"; + } + + return tokenUserId ? tokenUserId.toString() : "all"; + } + + return tokenUserId ? tokenUserId.toString() : "all"; + }, [compactMode, isAdmin, tokenUserId]); + const [searchParams, setSearchParams] = useState({ name: "", - userId: tokenUserId ? tokenUserId.toString() : "all", + userId: defaultSearchUserId, tunnelId: "all", inPort: "", remoteAddr: "", }); const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); + + const resetSearchParams = useCallback(() => { + setSearchParams({ + name: "", + userId: defaultSearchUserId, + tunnelId: "all", + inPort: "", + remoteAddr: "", + }); + }, [defaultSearchUserId]); + + const lastDefaultSearchUserIdRef = useRef(defaultSearchUserId); + + useEffect(() => { + const previousDefault = lastDefaultSearchUserIdRef.current; + + lastDefaultSearchUserIdRef.current = defaultSearchUserId; + + if (!isAdmin || previousDefault === defaultSearchUserId) { + return; + } + + setSearchParams((prev) => { + const hasOtherFilters = + Boolean(prev.name.trim()) || + prev.tunnelId !== "all" || + Boolean(prev.inPort.trim()) || + Boolean(prev.remoteAddr.trim()); + + if (hasOtherFilters) { + return prev; + } + + if (prev.userId !== previousDefault) { + return prev; + } + + return { + ...prev, + userId: defaultSearchUserId, + }; + }); + }, [defaultSearchUserId, isAdmin]); + const activeFilterCount = - (searchParams.name ? 1 : 0) + - (searchParams.userId !== "all" && - searchParams.userId !== (tokenUserId ? tokenUserId.toString() : "all") - ? 1 - : 0) + + (searchParams.name.trim() ? 1 : 0) + + (searchParams.userId !== defaultSearchUserId ? 1 : 0) + (searchParams.tunnelId !== "all" ? 1 : 0) + - (searchParams.inPort ? 1 : 0) + - (searchParams.remoteAddr ? 1 : 0); + (searchParams.inPort.trim() ? 1 : 0) + + (searchParams.remoteAddr.trim() ? 1 : 0); const [loading, setLoading] = useState(true); const [forwards, setForwards] = useState([]); const [tunnels, setTunnels] = useState([]); @@ -1169,7 +1226,6 @@ export default function ForwardPage() { // const isMobile = useMobileBreakpoint(); // searchKeyword removed // isSearchVisible removed - const [compactMode, setCompactMode] = useState(false); // 显示模式状态 - 从localStorage读取,默认为平铺显示 const [viewMode, setViewMode] = useState<"grouped" | "direct">(() => { @@ -3930,15 +3986,7 @@ export default function ForwardPage() { color="danger" size="sm" variant="light" - onPress={() => - setSearchParams({ - name: "", - userId: tokenUserId ? tokenUserId.toString() : "all", - tunnelId: "all", - inPort: "", - remoteAddr: "", - }) - } + onPress={resetSearchParams} > 清空条件 @@ -6061,15 +6109,7 @@ export default function ForwardPage() {