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" : ""}`}
>
@@ -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() {
{
- setSearchParams({
- name: "",
- userId: tokenUserId ? tokenUserId.toString() : "all",
- tunnelId: "all",
- inPort: "",
- remoteAddr: "",
- });
- }}
+ onPress={resetSearchParams}
>
重置