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.
This commit is contained in:
sagit
2026-03-16 18:06:41 +08:00
committed by GitHub
parent ff94406945
commit 7507507fd9
7 changed files with 507 additions and 33 deletions
+165
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/security-scan
@@ -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
<Chip className="...">
{forward.tunnelName} ({forward.tunnelTrafficRatio}x)
</Chip>
```
或者单独一列:
```tsx
<TableCell>
{forward.tunnelTrafficRatio}x
</TableCell>
```
### 后端修改
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`
+178
View File
@@ -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
+18
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"security-scan": {
"source": "affaan-m/everything-claude-code",
"sourceType": "github",
"computedHash": "92cdcaddc554e318402f066ccc073c2e3dbcfda8c2730ec62ec373f805c41a57"
}
}
}
+73 -33
View File
@@ -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" : ""}`}
>
<button
type="button"
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
title={formatInAddress(forward.inIp, forward.inPort)}
type="button"
onClick={() =>
showAddressModal(forward.inIp, forward.inPort, "入口端口")
}
@@ -798,12 +798,10 @@ const SortableTableRow = ({
className={`${FORWARD_GROUPED_TABLE_COLUMN_CLASS.target} max-w-[280px] ${selectedIds.has(forward.id) ? "bg-primary-50/70 dark:bg-primary-900/40" : ""}`}
>
<button
type="button"
className="w-full truncate rounded-md bg-default-100/50 px-2.5 py-1.5 text-left font-mono text-xs font-medium text-default-700 transition-all hover:bg-default-200 hover:shadow-sm cursor-pointer"
title={formatRemoteAddress(forward.remoteAddr)}
onClick={() =>
showAddressModal(forward.remoteAddr, null, "目标地址")
}
type="button"
onClick={() => showAddressModal(forward.remoteAddr, null, "目标地址")}
>
{formatRemoteAddress(forward.remoteAddr)}
</button>
@@ -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)}
</button>
@@ -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<Forward[]>([]);
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
@@ -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}
>
清空条件
</Button>
@@ -6061,15 +6109,7 @@ export default function ForwardPage() {
<Button
color="primary"
variant="flat"
onPress={() => {
setSearchParams({
name: "",
userId: tokenUserId ? tokenUserId.toString() : "all",
tunnelId: "all",
inPort: "",
remoteAddr: "",
});
}}
onPress={resetSearchParams}
>
重置
</Button>