Compare commits

...

1 Commits

Author SHA1 Message Date
sakuradairong
b33acc2a65 chore: snapshot backup before rainycy push (20260624-032434)
Auto-committed by MiMo for migration to git.rainycy.top
2026-06-24 03:27:09 +08:00
27 changed files with 893 additions and 241 deletions

16
.codegraph/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty

279
AGENTS.md Normal file
View File

@@ -0,0 +1,279 @@
# Repository Guidelines
## Project Overview
NetBird is a WireGuard-based peer-to-peer private network platform with centralized access control. It combines automatic encrypted tunneling with granular policy management for secure remote access across any infrastructure.
**Key characteristics:**
- Go monorepo with multiple services (client, management, signal, relay)
- gRPC-based communication between components
- WireGuard kernel/userspace networking
- Cross-platform support (Linux, macOS, Windows, Android, iOS)
## Architecture & Data Flow
### Core Components
1. **Client** (`/client`)
- NetBird agent that runs on endpoints
- Establishes WireGuard tunnels to peers
- Communicates with management service for configuration
- Includes CLI, daemon, and UI components
2. **Management Service** (`/management`)
- Central control plane for network policies and peer configuration
- Stores accounts, users, groups, peers, routes, DNS settings
- REST API for dashboard integration
- gRPC API for client communication
3. **Signal Service** (`/signal`)
- Handles peer discovery and connection signaling
- Facilitates hole-punching for NAT traversal
- gRPC-based messaging between peers
4. **Relay Service** (`/relay`)
- Fallback relay server when direct P2P connections fail
- WebSocket-based protocol for data relay
5. **Shared Libraries** (`/shared`)
- Common code for management, signal, relay, authentication
- Proto definitions and generated gRPC code
- Utility packages (auth, context, metrics, hashing)
### Data Flow
```
Client ↔ Management Service (policy/config sync via gRPC)
Client ↔ Signal Service (peer discovery via gRPC)
Client ↔ Client (direct WireGuard tunnel or via Relay)
Management ↔ Dashboard (REST API)
```
## Key Directories
| Directory | Purpose |
|-----------|---------|
| `/client` | NetBird agent: CLI, daemon, UI, internal logic |
| `/client/cmd` | CLI entry points and command definitions |
| `/client/internal` | Core agent business logic |
| `/client/iface` | WireGuard interface management |
| `/client/firewall` | Platform-specific firewall rules |
| `/management` | Management service server |
| `/management/server` | Core management logic, accounts, policies |
| `/management/server/http` | REST API handlers |
| `/signal` | Signal service for peer signaling |
| `/relay` | Relay server for connection fallback |
| `/shared` | Shared libraries, proto definitions |
| `/encryption` | Encryption utilities for agent communication |
| `/infrastructure_files` | Docker compose templates, setup scripts |
| `/docs` | Documentation, configuration examples |
## Development Commands
### Build Commands
```bash
# Build client
cd client && go build -o netbird .
# Build management service
cd management && go build -o management .
# Build signal service
cd signal && go build -o signal .
# Build relay service
cd relay && go build -o relay .
```
### Linting
```bash
# Lint changed files (fast, for pre-push)
make lint
# Lint entire codebase (slow, matches CI)
make lint-all
# Install linter locally
make lint-install
# Setup git hooks
make setup-hooks
```
### Testing
```bash
# Run all tests (requires sudo for network operations)
go test -exec sudo ./...
# Run tests for specific package
go test -exec sudo ./management/server/...
# Run tests with verbose output
go test -v -exec sudo ./...
```
### Generate gRPC Code
```bash
# Generate proto files (run in each proto directory)
cd shared/management/proto && ./generate.sh
cd shared/signal/proto && ./generate.sh
cd client/proto && ./generate.sh
```
## Code Conventions & Common Patterns
### Go Style
- Follow standard Go conventions and `gofmt` formatting
- Use `golangci-lint` with project configuration (`.golangci.yaml`)
- Private functions/constants preferred; comment public APIs
- Error handling: return structured errors, avoid bare strings
### Naming Conventions
| Type | Convention | Example |
|------|------------|---------|
| Packages | lowercase, single word | `server`, `client`, `auth` |
| Interfaces | Suffix with `-er` or descriptive | `Manager`, `AuthProvider` |
| Structs | PascalCase | `Account`, `Peer`, `Policy` |
| Functions | camelCase | `getAccount()`, `validatePeer()` |
| Constants | UPPER_SNAKE or camelCase | `MaxRetries`, `DefaultTimeout` |
| Files | snake_case | `account.go`, `peer_test.go` |
### Error Handling
```go
// Return wrapped errors with context
if err != nil {
return fmt.Errorf("failed to get account: %w", err)
}
// Use sentinel errors for known conditions
var ErrAccountNotFound = errors.New("account not found")
```
### Testing Patterns
- Use `testify/assert` and `testify/require` for assertions
- Table-driven tests for multiple scenarios
- Mock interfaces for external dependencies
- Test files colocated with source (`*_test.go`)
- Run tests with `sudo` for network namespace operations
### gRPC Patterns
- Proto files in `proto/` directories
- Generated code committed to repository
- Service interfaces define RPC methods
- Client/server implementations in separate packages
## Important Files
### Entry Points
| Component | Entry Point | Description |
|-----------|-------------|-------------|
| Client | `client/main.go` | CLI agent entry point |
| Management | `management/main.go` | Management server entry point |
| Signal | `signal/main.go` | Signal server entry point |
| Relay | `relay/main.go` | Relay server entry point |
### Configuration Files
| File | Purpose |
|------|---------|
| `.golangci.yaml` | Linter configuration |
| `.goreleaser.yaml` | Release automation config |
| `go.mod` / `go.sum` | Go module dependencies |
| `Makefile` | Build/lint commands |
| `infrastructure_files/docker-compose.yml.tmpl` | Docker compose template |
| `infrastructure_files/management.json.tmpl` | Management config template |
### Key Source Files
| File | Purpose |
|------|---------|
| `management/server/account.go` | Account management logic |
| `management/server/peer.go` | Peer management and validation |
| `management/server/policy.go` | Access policy engine |
| `management/server/route.go` | Network route management |
| `client/internal/engine.go` | Client network engine |
| `client/internal/connect.go` | Connection management |
| `client/iface/iface.go` | WireGuard interface abstraction |
## Runtime/Tooling Preferences
### Requirements
- **Go 1.21+** (check `go.mod` for exact version)
- **Docker** for containerized deployment
- **gRPC tools** for proto generation
- **golangci-lint** for code quality
- **Goreleaser** for release automation
### Platform-Specific Notes
- **Linux**: Full WireGuard kernel module support
- **macOS**: Uses userspace WireGuard implementation
- **Windows**: Requires wintun driver, NSIS for installer
- **Android/iOS**: Mobile clients with platform-specific bindings
### Development Environment
- Dev container support available (`.devcontainer/`)
- Local setup requires network namespaces for testing
- Management service needs configuration file (see `infrastructure_files/`)
## Testing & QA
### Test Framework
- Standard Go testing package
- `testify` for assertions and mocking
- Ginkgo/Gomega for BDD-style tests (limited use)
### Running Tests
```bash
# All tests (requires root/sudo)
go test -exec sudo ./...
# Specific package tests
go test -exec sudo ./management/server/...
# With race detection
go test -race -exec sudo ./...
# With coverage
go test -coverprofile=coverage.out -exec sudo ./...
go tool cover -html=coverage.out
```
### CI/CD Pipeline
- GitHub Actions workflows in `.github/workflows/`
- Multi-platform testing (Linux, macOS, Windows, FreeBSD)
- Linting with golangci-lint
- Release automation with Goreleaser
- Docker image building and publishing
### Test Patterns
- Unit tests for business logic
- Integration tests for gRPC services
- Platform-specific tests for firewall/networking
- Mock external dependencies (IDP, cloud providers)
## Module Contexts
For detailed information about specific subsystems, see:
- `docs/contexts/client.md` - Client agent architecture
- `docs/contexts/management.md` - Management service details
- `docs/contexts/signal.md` - Signal service implementation
- `docs/contexts/relay.md` - Relay server architecture
- `docs/contexts/shared.md` - Shared libraries and utilities

82
docs/contexts/client.md Normal file
View File

@@ -0,0 +1,82 @@
# Client Agent
## Purpose
NetBird client agent that establishes WireGuard tunnels to peers, manages network configuration, and communicates with management service for policy enforcement.
## API Surface
### CLI Commands (`client/cmd/`)
| Command | File | Description |
|---------|------|-------------|
| `up` | `up.go` | Connect to NetBird network |
| `down` | `down.go` | Disconnect from network |
| `status` | `status.go` | Show connection status |
| `login` | `login.go` | Authenticate with management |
| `logout` | `logout.go` | Disconnect and remove credentials |
| `ssh` | `ssh.go` | SSH to remote peers |
| `debug` | `debug.go` | Debug/diagnostic commands |
| `service` | `service.go` | Service management (install/uninstall) |
### Core Engine (`client/internal/`)
| Type | File | Description |
|------|------|-------------|
| `Engine` | `engine.go` | Main network engine, manages connections |
| `ConnectionManager` | `conn_mgr.go` | Manages peer connections lifecycle |
| `ConnectClient` | `connect.go` | Handles management service communication |
### Key Interfaces
```go
// client/internal/iface_common.go
type IFace interface {
Create() error
Close() error
SetFilter(iface.PacketFilter) error
// ... other methods
}
```
## File Paths
### Entry Points
- `client/main.go` - CLI entry point
- `client/cmd/root.go` - Root command definition
- `client/cmd/up.go` - Connection establishment
### Core Logic
- `client/internal/engine.go` - Network engine (77k lines)
- `client/internal/connect.go` - Management connection
- `client/internal/conn_mgr.go` - Connection manager
- `client/internal/dns.go` - DNS configuration
### Platform-Specific
- `client/internal/dns/` - DNS resolver implementations
- `client/internal/firewall/` - Firewall rule management
- `client/iface/` - WireGuard interface abstraction
### Configuration
- `client/internal/config/` - Configuration management
- `client/configs/` - Platform-specific configs
## Commands
```bash
# Build client
cd client && go build -o netbird .
# Run tests
go test -exec sudo ./client/...
# Run specific test
go test -exec sudo -run TestEngine ./client/internal/
```
## Gotchas
- Tests require sudo for network namespace operations
- Engine.go is very large (77k+ lines) - be careful with edits
- Platform-specific code uses build tags
- WireGuard interface creation requires root privileges

View File

@@ -0,0 +1,90 @@
# Management Service
## Purpose
Central control plane for NetBird network management. Stores accounts, users, groups, peers, policies, routes, and DNS configurations. Provides gRPC API for clients and REST API for dashboard.
## API Surface
### gRPC Services (`shared/management/proto/`)
| Service | Proto File | Description |
|---------|------------|-------------|
| `ManagementService` | `management.proto` | Main management RPC for clients |
| `ProxyService` | `proxy_service.proto` | Proxy service for dashboard |
### REST API (`management/server/http/`)
| Endpoint Group | Handler Directory | Description |
|----------------|-------------------|-------------|
| `/api/accounts` | `handlers/accounts/` | Account management |
| `/api/peers` | `handlers/peers/` | Peer management |
| `/api/groups` | `handlers/groups/` | Group management |
| `/api/policies` | `handlers/policies/` | Policy management |
| `/api/routes` | `handlers/routes/` | Route management |
| `/api/dns` | `handlers/dns/` | DNS management |
| `/api/users` | `handlers/users/` | User management |
| `/api/setup-keys` | `handlers/setupkeys/` | Setup key management |
### Core Types (`management/server/`)
| Type | File | Description |
|------|------|-------------|
| `Account` | `account.go` | Account with users, groups, peers, policies |
| `Peer` | `peer.go` | Network peer representation |
| `Policy` | `policy.go` | Access control policy |
| `Route` | `route.go` | Network route configuration |
| `Group` | `group.go` | Peer grouping |
| `NameServer` | `nameserver.go` | DNS configuration |
## File Paths
### Entry Points
- `management/main.go` - Server entry point
- `management/cmd/root.go` - CLI command definition
### Core Logic
- `management/server/account.go` - Account management (97k lines)
- `management/server/peer.go` - Peer operations
- `management/server/policy.go` - Policy engine
- `management/server/group.go` - Group management
- `management/server/route.go` - Route management
- `management/server/dns.go` - DNS management
### HTTP API
- `management/server/http/handler.go` - HTTP handler setup
- `management/server/http/handlers/` - Endpoint handlers
- `management/server/http/middleware/` - Auth/logging middleware
### Storage
- `management/server/store/` - Data persistence layer
- `management/server/sqlite/` - SQLite implementation
- `management/server/postgres/` - PostgreSQL implementation
### Authentication
- `management/server/auth/` - Auth providers
- `management/server/idp/` - Identity provider integrations
## Commands
```bash
# Build management service
cd management && go build -o management .
# Run management service
./management management --log-level debug --log-file console --config ./management.json
# Run tests
go test -exec sudo ./management/...
# Run specific tests
go test -exec sudo -run TestAccount ./management/server/
```
## Gotchas
- account.go is very large (97k+ lines) - consider refactoring
- Tests require database setup (SQLite/PostgreSQL)
- gRPC proto changes require regeneration
- IDP integrations have vendor-specific logic
- Account operations are complex with many edge cases

62
docs/contexts/relay.md Normal file
View File

@@ -0,0 +1,62 @@
# Relay Service
## Purpose
Fallback relay server for NetBird when direct P2P connections fail. Provides WebSocket-based data relay for peers that cannot establish direct WireGuard tunnels.
## API Surface
### WebSocket Protocol
| Endpoint | Description |
|----------|-------------|
| `/relay` | WebSocket endpoint for relay connections |
### Key Types
| Type | File | Description |
|------|------|-------------|
| `Server` | `relay/server/server.go` | Main relay server |
| `Peer` | `relay/server/peer.go` | Connected peer representation |
| `Relay` | `relay/server/relay.go` | Relay logic |
| `Handshake` | `relay/server/handshake.go` | Connection handshake |
## File Paths
### Entry Points
- `relay/main.go` - Server entry point
- `relay/cmd/root.go` - CLI command definition
### Core Logic
- `relay/server/server.go` - Server implementation
- `relay/server/peer.go` - Peer management
- `relay/server/relay.go` - Relay logic
- `relay/server/handshake.go` - Connection handshake
### Configuration
- `relay/server/listener/` - Network listeners
- `relay/server/store/` - Data storage
### Health & Metrics
- `relay/healthcheck/` - Health check endpoints
- `relay/metrics/` - Prometheus metrics
## Commands
```bash
# Build relay service
cd relay && go build -o relay .
# Run relay service
./relay --log-level debug --log-file console
# Run tests
go test -exec sudo ./relay/...
```
## Gotchas
- WebSocket-based protocol (not gRPC)
- Performance-sensitive - handles all relayed traffic
- Health check endpoint required for load balancers
- Metrics for monitoring relay usage

68
docs/contexts/shared.md Normal file
View File

@@ -0,0 +1,68 @@
# Shared Libraries
## Purpose
Common code shared across NetBird services. Includes proto definitions, generated gRPC code, authentication utilities, and shared utilities.
## API Surface
### Proto Definitions
| Directory | Proto Files | Description |
|-----------|-------------|-------------|
| `shared/management/proto/` | `management.proto`, `proxy_service.proto` | Management service gRPC definitions |
| `shared/signal/proto/` | `signalexchange.proto` | Signal service gRPC definitions |
| `shared/relay/` | Relay protocol definitions | Relay service protocol |
### Authentication (`shared/auth/`)
| Package | Description |
|---------|-------------|
| `auth` | Authentication utilities and providers |
### Utilities
| Package | File | Description |
|---------|------|-------------|
| `context` | `shared/context/` | Context utilities |
| `hash` | `shared/hash/` | Hashing utilities |
| `metrics` | `shared/metrics/` | Prometheus metrics helpers |
| `netiputil` | `shared/netiputil/` | Network IP utilities |
| `sshauth` | `shared/sshauth/` | SSH authentication helpers |
## File Paths
### Proto Definitions
- `shared/management/proto/management.proto` - Management service proto
- `shared/management/proto/proxy_service.proto` - Proxy service proto
- `shared/signal/proto/signalexchange.proto` - Signal exchange proto
### Generated Code
- `shared/management/proto/*.pb.go` - Generated gRPC code
- `shared/signal/proto/*.pb.go` - Generated gRPC code
### Utilities
- `shared/auth/` - Authentication utilities
- `shared/context/` - Context helpers
- `shared/hash/` - Hashing functions
- `shared/metrics/` - Metrics utilities
- `shared/netiputil/` - Network IP utilities
- `shared/sshauth/` - SSH auth helpers
## Commands
```bash
# Generate gRPC code
cd shared/management/proto && ./generate.sh
cd shared/signal/proto && ./generate.sh
# Run tests
go test ./shared/...
```
## Gotchas
- Proto changes require regeneration of gRPC code
- Generated code is committed to repository
- Shared code is used by multiple services - changes affect all
- Authentication providers have vendor-specific implementations

54
docs/contexts/signal.md Normal file
View File

@@ -0,0 +1,54 @@
# Signal Service
## Purpose
Handles peer discovery and connection signaling for NetBird. Facilitates hole-punching and NAT traversal to establish direct WireGuard tunnels between peers.
## API Surface
### gRPC Service (`shared/signal/proto/`)
| Service | Proto File | Description |
|---------|------------|-------------|
| `SignalExchange` | `signalexchange.proto` | Peer signaling and discovery |
### Key Types
| Type | File | Description |
|------|------|-------------|
| `SignalServer` | `signal/server/signal.go` | Main signal server implementation |
| `PeerMessage` | `shared/signal/proto/` | Signaling message format |
## File Paths
### Entry Points
- `signal/main.go` - Server entry point
- `signal/cmd/root.go` - CLI command definition
### Core Logic
- `signal/server/signal.go` - Signal server implementation
- `signal/peer/` - Peer message handling
### Shared Code
- `shared/signal/proto/signalexchange.proto` - Proto definition
- `shared/signal/` - Shared signal utilities
## Commands
```bash
# Build signal service
cd signal && go build -o signal .
# Run signal service
./signal run --log-level debug --log-file console
# Run tests
go test -exec sudo ./signal/...
```
## Gotchas
- Signal service is stateless - no persistent storage
- Requires proper NAT traversal configuration
- gRPC streaming for real-time signaling
- Metrics endpoint for monitoring

View File

@@ -2,17 +2,19 @@
{{ range $method, $value := .Methods }}
{{ if eq $method "pin" }}
<form>
<label for={{ $value }}>PIN:</label>
<label for={{ $value }}>PIN:</label>
<input name={{ $value }} id={{ $value }} />
<button type=submit>Submit</button>
<button type=submit>提交</button>
</form>
{{ else if eq $method "password" }}
<form>
<label for={{ $value }}>Password:</label>
<label for={{ $value }}>密码:</label>
<input name={{ $value }} id={{ $value }}/>
<button type=submit>Submit</button>
<button type=submit>提交</button>
</form>
{{ else if eq $method "oidc" }}
<a href={{ $value }}>Click here to log in with SSO</a>
<a href={{ $value }}>点击此处使用 SSO 登录</a>
{{ end }}
{{ end }}
</content>
</invoke></content>

View File

@@ -128,7 +128,7 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler {
if mw.forwardWithTunnelPeer(w, r, host, config, next) {
return
}
http.Error(w, "Forbidden", http.StatusForbidden)
http.Error(w, "禁止访问", http.StatusForbidden)
return
}
@@ -194,7 +194,7 @@ func (mw *Middleware) blockOIDCOnPlainHTTP(w http.ResponseWriter, r *http.Reques
"host": r.Host,
"remote": r.RemoteAddr,
}).Warn("OIDC scheme reached on plain HTTP path; rejecting with 400 — use port 443")
http.Error(w, "OIDC requires TLS — use port 443", http.StatusBadRequest)
http.Error(w, "OIDC 需要 TLS请使用 443 端口", http.StatusBadRequest)
return true
}
@@ -223,7 +223,7 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request
clientIP := mw.resolveClientIP(r)
if !clientIP.IsValid() {
mw.logger.Debugf("IP restriction: cannot resolve client address for %q, denying", r.RemoteAddr)
http.Error(w, "Forbidden", http.StatusForbidden)
http.Error(w, "禁止访问", http.StatusForbidden)
return false
}
@@ -258,7 +258,7 @@ func (mw *Middleware) checkIPRestrictions(w http.ResponseWriter, r *http.Request
reason := verdict.String()
mw.blockIPRestriction(r, reason)
http.Error(w, "Forbidden", http.StatusForbidden)
http.Error(w, "禁止访问", http.StatusForbidden)
return false
}
@@ -306,11 +306,11 @@ func (mw *Middleware) handleOAuthCallbackError(w http.ResponseWriter, r *http.Re
}
errDesc := r.URL.Query().Get("error_description")
if errDesc == "" {
errDesc = "An error occurred during authentication"
errDesc = "身份验证过程中发生错误"
} else {
errDesc = html.EscapeString(errDesc)
}
web.ServeAccessDeniedPage(w, r, http.StatusForbidden, "Access Denied", errDesc, requestID)
web.ServeAccessDeniedPage(w, r, http.StatusForbidden, "访问被拒绝", errDesc, requestID)
return true
}
@@ -464,10 +464,10 @@ func (mw *Middleware) tryHeaderScheme(w http.ResponseWriter, r *http.Request, ho
if err != nil {
setHeaderCapturedData(r.Context(), "", "", nil, nil)
status := http.StatusBadRequest
msg := "invalid session token"
msg := "会话令牌无效"
if errors.Is(err, errValidationUnavailable) {
status = http.StatusBadGateway
msg = "authentication service unavailable"
msg = "身份验证服务不可用"
}
http.Error(w, msg, status)
return true
@@ -475,7 +475,7 @@ func (mw *Middleware) tryHeaderScheme(w http.ResponseWriter, r *http.Request, ho
if !result.Valid {
setHeaderCapturedData(r.Context(), result.UserID, result.UserEmail, result.Groups, result.GroupNames)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
http.Error(w, "未授权", http.StatusUnauthorized)
return true
}
@@ -495,14 +495,14 @@ func (mw *Middleware) tryHeaderScheme(w http.ResponseWriter, r *http.Request, ho
func (mw *Middleware) handleHeaderAuthError(w http.ResponseWriter, r *http.Request, err error) bool {
if errors.Is(err, ErrHeaderAuthFailed) {
setHeaderCapturedData(r.Context(), "", "", nil, nil)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
http.Error(w, "未授权", http.StatusUnauthorized)
return true
}
mw.logger.WithField("scheme", "header").Warnf("header auth infrastructure error: %v", err)
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
cd.SetOrigin(proxy.OriginAuth)
}
http.Error(w, "authentication service unavailable", http.StatusBadGateway)
http.Error(w, "身份验证服务不可用", http.StatusBadGateway)
return true
}
@@ -532,7 +532,7 @@ func (mw *Middleware) authenticateWithSchemes(w http.ResponseWriter, r *http.Req
if cd := proxy.CapturedDataFromContext(r.Context()); cd != nil {
cd.SetOrigin(proxy.OriginAuth)
}
http.Error(w, "authentication service unavailable", http.StatusBadGateway)
http.Error(w, "身份验证服务不可用", http.StatusBadGateway)
return
}
@@ -573,10 +573,10 @@ func (mw *Middleware) handleAuthenticatedToken(w http.ResponseWriter, r *http.Re
cd.SetAuthMethod(scheme.Type().String())
}
status := http.StatusBadRequest
msg := "invalid session token"
msg := "会话令牌无效"
if errors.Is(err, errValidationUnavailable) {
status = http.StatusBadGateway
msg = "authentication service unavailable"
msg = "身份验证服务不可用"
}
http.Error(w, msg, status)
return
@@ -593,7 +593,7 @@ func (mw *Middleware) handleAuthenticatedToken(w http.ResponseWriter, r *http.Re
cd.SetAuthMethod(scheme.Type().String())
requestID = cd.GetRequestID()
}
web.ServeAccessDeniedPage(w, r, http.StatusForbidden, "Access Denied", "You are not authorized to access this service", requestID)
web.ServeAccessDeniedPage(w, r, http.StatusForbidden, "访问被拒绝", "你无权访问此服务", requestID)
return
}

View File

@@ -1182,7 +1182,7 @@ func TestProtect_OIDCOnPlainHTTP_BlockedWith400(t *testing.T) {
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusBadRequest, rec.Code, "OIDC over plain HTTP should be rejected")
assert.Contains(t, rec.Body.String(), "OIDC requires TLS", "response body should explain the rejection")
assert.Contains(t, rec.Body.String(), "OIDC 需要 TLS", "response body should explain the rejection")
}
// TestProtect_OIDCOverTLS_NotBlocked confirms the same configuration works

View File

@@ -52,33 +52,33 @@ func (c *Client) Health(ctx context.Context) error {
}
func (c *Client) printHealth(data map[string]any) {
_, _ = fmt.Fprintf(c.out, "Status: %v\n", data["status"])
_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "Management Connected: %s\n", boolIcon(data["management_connected"]))
_, _ = fmt.Fprintf(c.out, "All Clients Healthy: %s\n", boolIcon(data["all_clients_healthy"]))
_, _ = fmt.Fprintf(c.out, "状态: %v\n", data["status"])
_, _ = fmt.Fprintf(c.out, "运行时长: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "管理端已连接: %s\n", boolIcon(data["management_connected"]))
_, _ = fmt.Fprintf(c.out, "所有客户端健康: %s\n", boolIcon(data["all_clients_healthy"]))
total, _ := data["certs_total"].(float64)
ready, _ := data["certs_ready"].(float64)
pending, _ := data["certs_pending"].(float64)
failed, _ := data["certs_failed"].(float64)
if total > 0 {
_, _ = fmt.Fprintf(c.out, "Certificates: %d ready, %d pending, %d failed (%d total)\n",
_, _ = fmt.Fprintf(c.out, "证书: %d 个已就绪,%d 个等待中,%d 个失败(共 %d 个)\n",
int(ready), int(pending), int(failed), int(total))
}
if domains, ok := data["certs_ready_domains"].([]any); ok && len(domains) > 0 {
_, _ = fmt.Fprintf(c.out, " Ready:\n")
_, _ = fmt.Fprintf(c.out, " 已就绪:\n")
for _, d := range domains {
_, _ = fmt.Fprintf(c.out, " %v\n", d)
}
}
if domains, ok := data["certs_pending_domains"].([]any); ok && len(domains) > 0 {
_, _ = fmt.Fprintf(c.out, " Pending:\n")
_, _ = fmt.Fprintf(c.out, " 等待中:\n")
for _, d := range domains {
_, _ = fmt.Fprintf(c.out, " %v\n", d)
}
}
if domains, ok := data["certs_failed_domains"].(map[string]any); ok && len(domains) > 0 {
_, _ = fmt.Fprintf(c.out, " Failed:\n")
_, _ = fmt.Fprintf(c.out, " 失败:\n")
for d, errMsg := range domains {
_, _ = fmt.Fprintf(c.out, " %s: %v\n", d, errMsg)
}
@@ -94,7 +94,7 @@ func (c *Client) printHealthClients(data map[string]any) {
}
_, _ = fmt.Fprintf(c.out, "\n%-38s %-9s %-7s %-8s %-8s %-16s %s\n",
"ACCOUNT ID", "HEALTHY", "MGMT", "SIGNAL", "RELAYS", "PEERS (P2P/RLY)", "DEGRADED")
"账户 ID", "健康", "管理", "信令", "中继", "PEER (P2P/RLY)", "降级")
_, _ = fmt.Fprintln(c.out, strings.Repeat("-", 110))
for accountID, v := range clients {
@@ -133,9 +133,9 @@ func boolIcon(v any) string {
return "?"
}
if b {
return "yes"
return ""
}
return "no"
return ""
}
// ListClients fetches the list of all clients.
@@ -144,16 +144,16 @@ func (c *Client) ListClients(ctx context.Context) error {
}
func (c *Client) printClients(data map[string]any) {
_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "Clients: %v\n\n", data["client_count"])
_, _ = fmt.Fprintf(c.out, "运行时长: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "客户端数: %v\n\n", data["client_count"])
clients, ok := data["clients"].([]any)
if !ok || len(clients) == 0 {
_, _ = fmt.Fprintln(c.out, "No clients connected.")
_, _ = fmt.Fprintln(c.out, "没有已连接的客户端。")
return
}
_, _ = fmt.Fprintf(c.out, "%-38s %-12s %-40s %s\n", "ACCOUNT ID", "AGE", "SERVICES", "HAS CLIENT")
_, _ = fmt.Fprintf(c.out, "%-38s %-12s %-40s %s\n", "账户 ID", "时长", "服务", "已连接")
_, _ = fmt.Fprintln(c.out, strings.Repeat("-", 110))
for _, item := range clients {
@@ -196,8 +196,8 @@ func (c *Client) printInboundListeners(clients []any) {
}
_, _ = fmt.Fprintln(c.out)
_, _ = fmt.Fprintln(c.out, "Inbound listeners (per-account):")
_, _ = fmt.Fprintf(c.out, " %-38s %-20s %-7s %s\n", "ACCOUNT ID", "TUNNEL IP", "HTTPS", "HTTP")
_, _ = fmt.Fprintln(c.out, "入站监听器(按账户):")
_, _ = fmt.Fprintf(c.out, " %-38s %-20s %-7s %s\n", "账户 ID", "隧道 IP", "HTTPS", "HTTP")
_, _ = fmt.Fprintln(c.out, " "+strings.Repeat("-", 78))
for _, r := range rows {
_, _ = fmt.Fprintf(c.out, " %-38s %-20s %-7d %d\n", r.accountID, r.tunnelIP, r.httpsPort, r.httpPort)
@@ -211,9 +211,9 @@ func (c *Client) printClientRow(item any) {
}
services := c.extractServiceKeys(client)
hasClient := "no"
hasClient := ""
if hc, ok := client["has_client"].(bool); ok && hc {
hasClient = "yes"
hasClient = ""
}
_, _ = fmt.Fprintf(c.out, "%-38s %-12v %s %s\n",
@@ -261,12 +261,12 @@ func (c *Client) ClientStatus(ctx context.Context, accountID string, filters Sta
}
func (c *Client) printClientStatus(data map[string]any) {
_, _ = fmt.Fprintf(c.out, "Account: %v\n", data["account_id"])
_, _ = fmt.Fprintf(c.out, "账户: %v\n", data["account_id"])
if inbound, ok := data["inbound_listener"].(map[string]any); ok {
tunnelIP, _ := inbound["tunnel_ip"].(string)
httpsPort, _ := inbound["https_port"].(float64)
httpPort, _ := inbound["http_port"].(float64)
_, _ = fmt.Fprintf(c.out, "Inbound listener: %s (https=%d, http=%d)\n", tunnelIP, int(httpsPort), int(httpPort))
_, _ = fmt.Fprintf(c.out, "入站监听器: %s (https=%d, http=%d)\n", tunnelIP, int(httpsPort), int(httpPort))
}
_, _ = fmt.Fprintln(c.out)
if status, ok := data["status"].(string); ok {
@@ -303,13 +303,13 @@ func (c *Client) printPingResult(data map[string]any) {
if success {
remote, _ := data["remote"].(string)
if remote != "" && remote != host {
_, _ = fmt.Fprintf(c.out, "Success: %s (via %s)\n", host, remote)
_, _ = fmt.Fprintf(c.out, "成功: %s (经由 %s)\n", host, remote)
} else {
_, _ = fmt.Fprintf(c.out, "Success: %s\n", host)
_, _ = fmt.Fprintf(c.out, "成功: %s\n", host)
}
_, _ = fmt.Fprintf(c.out, "Latency: %v\n", data["latency"])
_, _ = fmt.Fprintf(c.out, "延迟: %v\n", data["latency"])
} else {
_, _ = fmt.Fprintf(c.out, "Failed: %s\n", host)
_, _ = fmt.Fprintf(c.out, "失败: %s\n", host)
c.printError(data)
}
}
@@ -326,9 +326,9 @@ func (c *Client) SetLogLevel(ctx context.Context, accountID, level string) error
func (c *Client) printLogLevelResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintf(c.out, "Log level set to: %v\n", data["level"])
_, _ = fmt.Fprintf(c.out, "日志级别已设置为: %v\n", data["level"])
} else {
_, _ = fmt.Fprintln(c.out, "Failed to set log level")
_, _ = fmt.Fprintln(c.out, "设置日志级别失败")
c.printError(data)
}
}
@@ -347,10 +347,10 @@ func (c *Client) printPerfSet(data map[string]any) {
}
val, _ := data["value"].(float64)
applied, _ := data["applied"].(float64)
_, _ = fmt.Fprintf(c.out, "Pool cap set to: %d\n", uint32(val))
_, _ = fmt.Fprintf(c.out, "Applied to %d live clients\n", int(applied))
_, _ = fmt.Fprintf(c.out, "缓冲池上限已设置为: %d\n", uint32(val))
_, _ = fmt.Fprintf(c.out, "已应用到 %d 个在线客户端\n", int(applied))
if failed, ok := data["failed"].(map[string]any); ok && len(failed) > 0 {
_, _ = fmt.Fprintln(c.out, "Failed:")
_, _ = fmt.Fprintln(c.out, "失败:")
for k, v := range failed {
_, _ = fmt.Fprintf(c.out, " %s: %v\n", k, v)
}
@@ -369,25 +369,25 @@ func (c *Client) printRuntime(data map[string]any) {
}
mb := func(n uint64) string { return fmt.Sprintf("%.1f MB", float64(n)/(1<<20)) }
_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "Go: %v on %d CPU (GOMAXPROCS=%d)\n", data["go_version"], uint32(i("num_cpu")), uint32(i("gomaxprocs")))
_, _ = fmt.Fprintf(c.out, "Goroutines: %d\n", i("goroutines"))
_, _ = fmt.Fprintf(c.out, "Live objects: %d\n", i("live_objects"))
_, _ = fmt.Fprintf(c.out, "GC: %d cycles, %v pause total\n", i("num_gc"), time.Duration(i("pause_total_ns")))
_, _ = fmt.Fprintln(c.out, "Heap:")
_, _ = fmt.Fprintf(c.out, " alloc: %s\n", mb(i("heap_alloc")))
_, _ = fmt.Fprintf(c.out, " in-use: %s\n", mb(i("heap_inuse")))
_, _ = fmt.Fprintf(c.out, " idle: %s\n", mb(i("heap_idle")))
_, _ = fmt.Fprintf(c.out, " released: %s\n", mb(i("heap_released")))
_, _ = fmt.Fprintf(c.out, " sys: %s\n", mb(i("heap_sys")))
_, _ = fmt.Fprintf(c.out, "Total sys: %s\n", mb(i("sys")))
_, _ = fmt.Fprintf(c.out, "运行时长: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "Go: %v %d CPU 上运行 (GOMAXPROCS=%d)\n", data["go_version"], uint32(i("num_cpu")), uint32(i("gomaxprocs")))
_, _ = fmt.Fprintf(c.out, "协程数: %d\n", i("goroutines"))
_, _ = fmt.Fprintf(c.out, "活动对象: %d\n", i("live_objects"))
_, _ = fmt.Fprintf(c.out, "GC: %d 次循环,累计暂停 %v\n", i("num_gc"), time.Duration(i("pause_total_ns")))
_, _ = fmt.Fprintln(c.out, "堆内存:")
_, _ = fmt.Fprintf(c.out, " 已分配: %s\n", mb(i("heap_alloc")))
_, _ = fmt.Fprintf(c.out, " 使用中: %s\n", mb(i("heap_inuse")))
_, _ = fmt.Fprintf(c.out, " 空闲: %s\n", mb(i("heap_idle")))
_, _ = fmt.Fprintf(c.out, " 已释放: %s\n", mb(i("heap_released")))
_, _ = fmt.Fprintf(c.out, " 系统占用: %s\n", mb(i("heap_sys")))
_, _ = fmt.Fprintf(c.out, "总系统占用: %s\n", mb(i("sys")))
if _, ok := data["vm_rss"]; ok {
_, _ = fmt.Fprintln(c.out, "Process:")
_, _ = fmt.Fprintln(c.out, "进程:")
_, _ = fmt.Fprintf(c.out, " VmRSS: %s\n", mb(i("vm_rss")))
_, _ = fmt.Fprintf(c.out, " VmSize: %s\n", mb(i("vm_size")))
_, _ = fmt.Fprintf(c.out, " VmData: %s\n", mb(i("vm_data")))
}
_, _ = fmt.Fprintf(c.out, "Clients: %d (%d started)\n", i("clients"), i("started"))
_, _ = fmt.Fprintf(c.out, "客户端: %d (已启动 %d)\n", i("clients"), i("started"))
}
// StartClient starts a specific client.
@@ -399,9 +399,9 @@ func (c *Client) StartClient(ctx context.Context, accountID string) error {
func (c *Client) printStartResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintln(c.out, "Client started")
_, _ = fmt.Fprintln(c.out, "客户端已启动")
} else {
_, _ = fmt.Fprintln(c.out, "Failed to start client")
_, _ = fmt.Fprintln(c.out, "启动客户端失败")
c.printError(data)
}
}
@@ -415,16 +415,16 @@ func (c *Client) StopClient(ctx context.Context, accountID string) error {
func (c *Client) printStopResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintln(c.out, "Client stopped")
_, _ = fmt.Fprintln(c.out, "客户端已停止")
} else {
_, _ = fmt.Fprintln(c.out, "Failed to stop client")
_, _ = fmt.Fprintln(c.out, "停止客户端失败")
c.printError(data)
}
}
func (c *Client) printError(data map[string]any) {
if errMsg, ok := data["error"].(string); ok {
_, _ = fmt.Fprintf(c.out, "Error: %s\n", errMsg)
_, _ = fmt.Fprintf(c.out, "错误: %s\n", errMsg)
}
}
@@ -444,10 +444,10 @@ type CaptureOptions struct {
// connection or the context is cancelled.
func (c *Client) Capture(ctx context.Context, opts CaptureOptions) error {
if opts.AccountID == "" {
return fmt.Errorf("account ID is required")
return fmt.Errorf("必须提供账户 ID")
}
if opts.Output == nil {
return fmt.Errorf("output writer is required")
return fmt.Errorf("必须提供输出 writer")
}
params := url.Values{}
@@ -475,20 +475,20 @@ func (c *Client) Capture(ctx context.Context, opts CaptureOptions) error {
fullURL := c.baseURL + path
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
return fmt.Errorf("创建请求失败: %w", err)
}
// Use a separate client without timeout since captures stream for their full duration.
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
return fmt.Errorf("请求失败: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("server error (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
return fmt.Errorf("服务器错误 (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
_, err = io.Copy(opts.Output, resp.Body)
@@ -549,22 +549,22 @@ func (c *Client) fetch(ctx context.Context, path string) (map[string]any, []byte
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, nil, fmt.Errorf("create request: %w", err)
return nil, nil, fmt.Errorf("创建请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("request failed: %w", err)
return nil, nil, fmt.Errorf("请求失败: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("read response: %w", err)
return nil, nil, fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode >= 400 {
return nil, nil, fmt.Errorf("server error (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
return nil, nil, fmt.Errorf("服务器错误 (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var data map[string]any

View File

@@ -330,9 +330,9 @@ func (h *Handler) handleIndex(w http.ResponseWriter, _ *http.Request, wantJSON b
if services == "" {
services = "-"
}
status := "No client"
status := "无客户端"
if info.HasClient {
status = "Active"
status = "活跃"
}
data.Clients = append(data.Clients, clientData{
AccountID: string(info.AccountID),
@@ -398,9 +398,9 @@ func (h *Handler) handleListClients(w http.ResponseWriter, _ *http.Request, want
if services == "" {
services = "-"
}
status := "No client"
status := "无客户端"
if info.HasClient {
status = "Active"
status = "活跃"
}
data.Clients = append(data.Clients, clientData{
AccountID: string(info.AccountID),
@@ -422,13 +422,13 @@ type clientDetailData struct {
func (h *Handler) handleClientStatus(w http.ResponseWriter, r *http.Request, accountID types.AccountID, wantJSON bool) {
client, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "Client not found: "+string(accountID), http.StatusNotFound)
http.Error(w, "未找到客户端: "+string(accountID), http.StatusNotFound)
return
}
fullStatus, err := client.Status()
if err != nil {
http.Error(w, "Error getting status: "+err.Error(), http.StatusInternalServerError)
http.Error(w, "获取状态时出错: "+err.Error(), http.StatusInternalServerError)
return
}
@@ -500,18 +500,18 @@ func (h *Handler) inboundInfoFor(accountID types.AccountID) (InboundListenerInfo
func (h *Handler) handleClientSyncResponse(w http.ResponseWriter, _ *http.Request, accountID types.AccountID, wantJSON bool) {
client, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "Client not found: "+string(accountID), http.StatusNotFound)
http.Error(w, "未找到客户端: "+string(accountID), http.StatusNotFound)
return
}
syncResp, err := client.GetLatestSyncResponse()
if err != nil {
http.Error(w, "Error getting sync response: "+err.Error(), http.StatusInternalServerError)
http.Error(w, "获取同步响应时出错: "+err.Error(), http.StatusInternalServerError)
return
}
if syncResp == nil {
http.Error(w, "No sync response available for client: "+string(accountID), http.StatusNotFound)
http.Error(w, "该客户端没有可用的同步响应: "+string(accountID), http.StatusNotFound)
return
}
@@ -524,7 +524,7 @@ func (h *Handler) handleClientSyncResponse(w http.ResponseWriter, _ *http.Reques
jsonBytes, err := opts.Marshal(syncResp)
if err != nil {
http.Error(w, "Error marshaling sync response: "+err.Error(), http.StatusInternalServerError)
http.Error(w, "序列化同步响应时出错: "+err.Error(), http.StatusInternalServerError)
return
}
@@ -550,7 +550,7 @@ type toolsData struct {
func (h *Handler) handleClientTools(w http.ResponseWriter, _ *http.Request, accountID types.AccountID) {
_, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "Client not found: "+string(accountID), http.StatusNotFound)
http.Error(w, "未找到客户端: "+string(accountID), http.StatusNotFound)
return
}
@@ -564,20 +564,20 @@ func (h *Handler) handleClientTools(w http.ResponseWriter, _ *http.Request, acco
func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"})
h.writeJSON(w, map[string]any{"error": "未找到客户端"})
return
}
host := r.URL.Query().Get("host")
portStr := r.URL.Query().Get("port")
if host == "" || portStr == "" {
h.writeJSON(w, map[string]any{"error": "host and port parameters required"})
h.writeJSON(w, map[string]any{"error": "需要提供主机和端口参数"})
return
}
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
h.writeJSON(w, map[string]any{"error": "invalid port"})
h.writeJSON(w, map[string]any{"error": "端口无效"})
return
}
@@ -629,13 +629,13 @@ func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountI
func (h *Handler) handleLogLevel(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"})
h.writeJSON(w, map[string]any{"error": "未找到客户端"})
return
}
level := r.URL.Query().Get("level")
if level == "" {
h.writeJSON(w, map[string]any{"error": "level parameter required (trace, debug, info, warn, error)"})
h.writeJSON(w, map[string]any{"error": "需要提供级别参数 (trace, debug, info, warn, error)"})
return
}
@@ -658,7 +658,7 @@ const clientActionTimeout = 30 * time.Second
func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"})
h.writeJSON(w, map[string]any{"error": "未找到客户端"})
return
}
@@ -675,14 +675,14 @@ func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, acco
h.writeJSON(w, map[string]any{
"success": true,
"message": "client started",
"message": "客户端已启动",
})
}
func (h *Handler) handleClientStop(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"})
h.writeJSON(w, map[string]any{"error": "未找到客户端"})
return
}
@@ -699,19 +699,19 @@ func (h *Handler) handleClientStop(w http.ResponseWriter, r *http.Request, accou
h.writeJSON(w, map[string]any{
"success": true,
"message": "client stopped",
"message": "客户端已停止",
})
}
func (h *Handler) handlePerf(w http.ResponseWriter, r *http.Request) {
raw := r.URL.Query().Get("value")
if raw == "" {
http.Error(w, "value parameter is required", http.StatusBadRequest)
http.Error(w, "需要提供 value 参数", http.StatusBadRequest)
return
}
n, err := strconv.ParseUint(raw, 10, 32)
if err != nil {
http.Error(w, fmt.Sprintf("invalid value %q: %v", raw, err), http.StatusBadRequest)
http.Error(w, fmt.Sprintf("value 无效 %q: %v", raw, err), http.StatusBadRequest)
return
}
@@ -821,7 +821,7 @@ const maxCaptureDuration = 30 * time.Minute
func (h *Handler) handleCapture(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "client not found", http.StatusNotFound)
http.Error(w, "未找到客户端", http.StatusNotFound)
return
}
@@ -829,11 +829,11 @@ func (h *Handler) handleCapture(w http.ResponseWriter, r *http.Request, accountI
if durationStr := r.URL.Query().Get("duration"); durationStr != "" {
d, err := time.ParseDuration(durationStr)
if err != nil {
http.Error(w, "invalid duration: "+err.Error(), http.StatusBadRequest)
http.Error(w, "duration 参数无效: "+err.Error(), http.StatusBadRequest)
return
}
if d < 0 {
http.Error(w, "duration must not be negative", http.StatusBadRequest)
http.Error(w, "duration 参数不能为负数", http.StatusBadRequest)
return
}
if d > 0 {
@@ -859,7 +859,7 @@ func (h *Handler) handleCapture(w http.ResponseWriter, r *http.Request, accountI
cs, err := client.StartCapture(opts)
if err != nil {
http.Error(w, "start capture: "+err.Error(), http.StatusServiceUnavailable)
http.Error(w, "启动抓包失败: "+err.Error(), http.StatusServiceUnavailable)
return
}
defer cs.Stop()
@@ -941,12 +941,12 @@ func (h *Handler) renderTemplate(w http.ResponseWriter, name string, data any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl := h.getTemplates()
if tmpl == nil {
http.Error(w, "Templates not loaded", http.StatusInternalServerError)
http.Error(w, "模板未加载", http.StatusInternalServerError)
return
}
if err := tmpl.ExecuteTemplate(w, name, data); err != nil {
h.logger.Errorf("execute template %s: %v", name, err)
http.Error(w, "Template error", http.StatusInternalServerError)
http.Error(w, "模板错误", http.StatusInternalServerError)
}
}

View File

@@ -1,17 +1,17 @@
{{define "clientDetail"}}
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<title>Client {{.AccountID}}</title>
<title>客户端 {{.AccountID}}</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>Client: {{.AccountID}}</h1>
<h1>客户端: {{.AccountID}}</h1>
<div class="nav">
<a href="/debug">&larr; Back</a>
<a href="/debug/clients/{{.AccountID}}/tools"{{if eq .ActiveTab "tools"}} class="active"{{end}}>Tools</a>
<a href="/debug/clients/{{.AccountID}}"{{if eq .ActiveTab "status"}} class="active"{{end}}>Status</a>
<a href="/debug/clients/{{.AccountID}}/syncresponse"{{if eq .ActiveTab "syncresponse"}} class="active"{{end}}>Sync Response</a>
<a href="/debug">&larr; 返回</a>
<a href="/debug/clients/{{.AccountID}}/tools"{{if eq .ActiveTab "tools"}} class="active"{{end}}>工具</a>
<a href="/debug/clients/{{.AccountID}}"{{if eq .ActiveTab "status"}} class="active"{{end}}>状态</a>
<a href="/debug/clients/{{.AccountID}}/syncresponse"{{if eq .ActiveTab "syncresponse"}} class="active"{{end}}>同步响应</a>
</div>
<pre>{{.Content}}</pre>
</body>

View File

@@ -1,20 +1,20 @@
{{define "clients"}}
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<title>Clients</title>
<title>客户端</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>All Clients</h1>
<p class="info">Uptime: {{.Uptime}} | <a href="/debug">&larr; Back</a></p>
<h1>所有客户端</h1>
<p class="info">运行时间: {{.Uptime}} | <a href="/debug">&larr; 返回</a></p>
{{if .Clients}}
<table>
<tr>
<th>Account ID</th>
<th>Services</th>
<th>Age</th>
<th>Status</th>
<th>账户 ID</th>
<th>服务</th>
<th>时长</th>
<th>状态</th>
</tr>
{{range .Clients}}
<tr>
@@ -26,7 +26,7 @@
{{end}}
</table>
{{else}}
<p>No clients connected</p>
<p>没有已连接的客户端</p>
{{end}}
</body>
</html>

View File

@@ -1,40 +1,40 @@
{{define "index"}}
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<title>NetBird Proxy Debug</title>
<title>NetBird 代理调试</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>NetBird Proxy Debug</h1>
<p class="info">Version: {{.Version}} | Uptime: {{.Uptime}}</p>
<h2>Certificates: {{.CertsReady}} ready, {{.CertsPending}} pending, {{.CertsFailed}} failed ({{.CertsTotal}} total)</h2>
<h1>NetBird 代理调试</h1>
<p class="info">版本: {{.Version}} | 运行时间: {{.Uptime}}</p>
<h2>证书: {{.CertsReady}} 个已就绪,{{.CertsPending}} 个等待中,{{.CertsFailed}} 个失败(共 {{.CertsTotal}} 个)</h2>
{{if .CertsReadyDomains}}
<details>
<summary>Ready domains ({{.CertsReady}})</summary>
<summary>已就绪域名 ({{.CertsReady}})</summary>
<ul>{{range .CertsReadyDomains}}<li>{{.}}</li>{{end}}</ul>
</details>
{{end}}
{{if .CertsPendingDomains}}
<details open>
<summary>Pending domains ({{.CertsPending}})</summary>
<summary>等待中的域名 ({{.CertsPending}})</summary>
<ul>{{range .CertsPendingDomains}}<li>{{.}}</li>{{end}}</ul>
</details>
{{end}}
{{if .CertsFailedDomains}}
<details open>
<summary>Failed domains ({{.CertsFailed}})</summary>
<summary>失败域名 ({{.CertsFailed}})</summary>
<ul>{{range .CertsFailedDomains}}<li>{{.Domain}}: {{.Error}}</li>{{end}}</ul>
</details>
{{end}}
<h2>Clients ({{.ClientCount}}) | Services ({{.TotalServices}})</h2>
<h2>客户端 ({{.ClientCount}}) | 服务 ({{.TotalServices}})</h2>
{{if .Clients}}
<table>
<tr>
<th>Account ID</th>
<th>Services</th>
<th>Age</th>
<th>Status</th>
<th>账户 ID</th>
<th>服务</th>
<th>时长</th>
<th>状态</th>
</tr>
{{range .Clients}}
<tr>
@@ -46,13 +46,13 @@
{{end}}
</table>
{{else}}
<p>No clients connected</p>
<p>没有已连接的客户端</p>
{{end}}
<h2>Endpoints</h2>
<h2>端点</h2>
<ul>
<li><a href="/debug/clients">/debug/clients</a> - all clients detail</li>
<li><a href="/debug/clients">/debug/clients</a> - 所有客户端详情</li>
</ul>
<p class="info">Add ?format=json or /json suffix for JSON output</p>
<p class="info">添加 ?format=json /json 后缀可获取 JSON 输出</p>
</body>
</html>
{{end}}

View File

@@ -1,36 +1,36 @@
{{define "tools"}}
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<title>Client {{.AccountID}} - Tools</title>
<title>客户端 {{.AccountID}} - 工具</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>Client: {{.AccountID}}</h1>
<h1>客户端: {{.AccountID}}</h1>
<div class="nav">
<a href="/debug">&larr; Back</a>
<a href="/debug/clients/{{.AccountID}}/tools" class="active">Tools</a>
<a href="/debug/clients/{{.AccountID}}">Status</a>
<a href="/debug/clients/{{.AccountID}}/syncresponse">Sync Response</a>
<a href="/debug">&larr; 返回</a>
<a href="/debug/clients/{{.AccountID}}/tools" class="active">工具</a>
<a href="/debug/clients/{{.AccountID}}">状态</a>
<a href="/debug/clients/{{.AccountID}}/syncresponse">同步响应</a>
</div>
<h2>Client Control</h2>
<h2>客户端控制</h2>
<div class="form-row">
<div class="form-group">
<span>&nbsp;</span>
<button onclick="startClient()">Start</button>
<button onclick="startClient()">启动</button>
</div>
<div class="form-group">
<span>&nbsp;</span>
<button onclick="stopClient()">Stop</button>
<button onclick="stopClient()">停止</button>
</div>
</div>
<div id="client-result" class="result"></div>
<h2>Log Level</h2>
<h2>日志级别</h2>
<div class="form-row">
<div class="form-group">
<label for="log-level">Level</label>
<label for="log-level">级别</label>
<select id="log-level" style="width: 120px;">
<option value="trace">trace</option>
<option value="debug">debug</option>
@@ -41,7 +41,7 @@
</div>
<div class="form-group">
<span>&nbsp;</span>
<button onclick="setLogLevel()">Set Level</button>
<button onclick="setLogLevel()">设置级别</button>
</div>
</div>
<div id="log-result" class="result"></div>
@@ -49,16 +49,16 @@
<h2>TCP Ping</h2>
<div class="form-row">
<div class="form-group">
<label for="tcp-host">Host</label>
<input type="text" id="tcp-host" placeholder="100.0.0.1 or hostname.netbird.cloud" style="width: 300px;">
<label for="tcp-host">主机</label>
<input type="text" id="tcp-host" placeholder="100.0.0.1 hostname.netbird.cloud" style="width: 300px;">
</div>
<div class="form-group">
<label for="tcp-port">Port</label>
<label for="tcp-port">端口</label>
<input type="number" id="tcp-port" placeholder="80" style="width: 80px;">
</div>
<div class="form-group">
<span>&nbsp;</span>
<button onclick="doTcpPing()">Connect</button>
<button onclick="doTcpPing()">连接</button>
</div>
</div>
<div id="tcp-result" class="result"></div>
@@ -68,7 +68,7 @@
async function startClient() {
const resultDiv = document.getElementById('client-result');
resultDiv.innerHTML = '<span class="info">Starting client...</span>';
resultDiv.innerHTML = '<span class="info">正在启动客户端...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/start');
const data = await resp.json();
@@ -78,13 +78,13 @@
resultDiv.innerHTML = '<span class="error">✗ ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
resultDiv.innerHTML = '<span class="error">错误: ' + e.message + '</span>';
}
}
async function stopClient() {
const resultDiv = document.getElementById('client-result');
resultDiv.innerHTML = '<span class="info">Stopping client...</span>';
resultDiv.innerHTML = '<span class="info">正在停止客户端...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/stop');
const data = await resp.json();
@@ -94,24 +94,24 @@
resultDiv.innerHTML = '<span class="error">✗ ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
resultDiv.innerHTML = '<span class="error">错误: ' + e.message + '</span>';
}
}
async function setLogLevel() {
const level = document.getElementById('log-level').value;
const resultDiv = document.getElementById('log-result');
resultDiv.innerHTML = '<span class="info">Setting log level...</span>';
resultDiv.innerHTML = '<span class="info">正在设置日志级别...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/loglevel?level=' + level);
const data = await resp.json();
if (data.success) {
resultDiv.innerHTML = '<span class="success">✓ Log level set to: ' + data.level + '</span>';
resultDiv.innerHTML = '<span class="success">✓ 日志级别已设置为: ' + data.level + '</span>';
} else {
resultDiv.innerHTML = '<span class="error">✗ ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
resultDiv.innerHTML = '<span class="error">错误: ' + e.message + '</span>';
}
}
@@ -119,21 +119,21 @@
const host = document.getElementById('tcp-host').value;
const port = document.getElementById('tcp-port').value;
if (!host || !port) {
alert('Host and port required');
alert('需要填写主机和端口');
return;
}
const resultDiv = document.getElementById('tcp-result');
resultDiv.innerHTML = '<span class="info">Connecting...</span>';
resultDiv.innerHTML = '<span class="info">正在连接...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/pingtcp?host=' + encodeURIComponent(host) + '&port=' + port);
const data = await resp.json();
if (data.success) {
resultDiv.innerHTML = '<span class="success">✓ ' + data.host + ':' + data.port + ' connected in ' + data.latency + '</span>';
resultDiv.innerHTML = '<span class="success">✓ ' + data.host + ':' + data.port + ' 连接成功,用时 ' + data.latency + '</span>';
} else {
resultDiv.innerHTML = '<span class="error">✗ ' + data.host + ':' + data.port + ': ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
resultDiv.innerHTML = '<span class="error">错误: ' + e.message + '</span>';
}
}
</script>

View File

@@ -60,8 +60,8 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cd.SetOrigin(OriginNoRoute)
}
requestID := getRequestID(r)
web.ServeErrorPage(w, r, http.StatusNotFound, "Service Not Found",
"The requested service could not be found. Please check the URL, try refreshing, or check if the peer is running. If that doesn't work, see our documentation for help.",
web.ServeErrorPage(w, r, http.StatusNotFound, "未找到服务",
"找不到请求的服务。请检查 URL、尝试刷新或确认对应 peer 正在运行。如果问题仍然存在,请查看文档获取帮助。",
requestID, web.ErrorStatus{Proxy: true, Destination: false})
return
}
@@ -76,8 +76,8 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cd.SetOrigin(OriginNoRoute)
}
requestID := getRequestID(r)
web.ServeErrorPage(w, r, http.StatusMisdirectedRequest, "Loop Detected",
"This peer is the target of the requested service. Reach the backend directly instead of dialing the public service URL from the same machine.",
web.ServeErrorPage(w, r, http.StatusMisdirectedRequest, "检测到循环访问",
" peer 是请求服务的目标。请直接访问后端,不要在同一台机器上访问公开服务 URL。",
requestID, web.ErrorStatus{Proxy: true, Destination: false})
return
}
@@ -391,51 +391,51 @@ func classifyProxyError(err error) (title, message string, code int, status web.
switch {
case errors.Is(err, context.DeadlineExceeded),
isNetTimeout(err):
return "Request Timeout",
"The request timed out while trying to reach the service. Please refresh the page and try again.",
return "请求超时",
"尝试访问服务时请求超时。请刷新页面后重试。",
http.StatusGatewayTimeout,
web.ErrorStatus{Proxy: true, Destination: false}
case errors.Is(err, context.Canceled):
return "Request Canceled",
"The request was canceled before it could be completed. Please refresh the page and try again.",
return "请求已取消",
"请求在完成前已被取消。请刷新页面后重试。",
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Destination: false}
case errors.Is(err, roundtrip.ErrNoAccountID):
return "Configuration Error",
"The request could not be processed due to a configuration issue. Please refresh the page and try again.",
return "配置错误",
"由于配置问题,请求无法处理。请刷新页面后重试。",
http.StatusInternalServerError,
web.ErrorStatus{Proxy: false, Destination: false}
case errors.Is(err, roundtrip.ErrNoPeerConnection),
errors.Is(err, roundtrip.ErrClientStartFailed):
return "Proxy Not Connected",
"The proxy is not connected to the NetBird network. Please try again later or contact your administrator.",
return "代理未连接",
"代理未连接到 NetBird 网络。请稍后重试或联系管理员。",
http.StatusBadGateway,
web.ErrorStatus{Proxy: false, Destination: false}
case errors.Is(err, roundtrip.ErrTooManyInflight):
return "Service Overloaded",
"The service is currently handling too many requests. Please try again shortly.",
return "服务过载",
"服务当前正在处理过多请求。请稍后重试。",
http.StatusServiceUnavailable,
web.ErrorStatus{Proxy: true, Destination: false}
case isConnectionRefused(err):
return "Service Unavailable",
"The connection to the service was refused. Please verify that the service is running and try again.",
return "服务不可用",
"服务拒绝了连接。请确认服务正在运行后重试。",
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Destination: false}
case isHostUnreachable(err):
return "Peer Not Connected",
"The connection to the peer could not be established. Please ensure the peer is running and connected to the NetBird network.",
return "Peer 未连接",
"无法建立到 peer 的连接。请确认 peer 正在运行并已连接到 NetBird 网络。",
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Destination: false}
}
return "Connection Error",
"An unexpected error occurred while connecting to the service. Please try again later.",
return "连接错误",
"连接服务时发生意外错误。请稍后重试。",
http.StatusBadGateway,
web.ErrorStatus{Proxy: true, Destination: false}
}

View File

@@ -935,42 +935,42 @@ func TestClassifyProxyError(t *testing.T) {
{
name: "context deadline exceeded",
err: context.DeadlineExceeded,
wantTitle: "Request Timeout",
wantTitle: "请求超时",
wantCode: http.StatusGatewayTimeout,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
{
name: "wrapped deadline exceeded",
err: fmt.Errorf("dial: %w", context.DeadlineExceeded),
wantTitle: "Request Timeout",
wantTitle: "请求超时",
wantCode: http.StatusGatewayTimeout,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
{
name: "context canceled",
err: context.Canceled,
wantTitle: "Request Canceled",
wantTitle: "请求已取消",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
{
name: "no account ID",
err: roundtrip.ErrNoAccountID,
wantTitle: "Configuration Error",
wantTitle: "配置错误",
wantCode: http.StatusInternalServerError,
wantStatus: web.ErrorStatus{Proxy: false, Destination: false},
},
{
name: "no peer connection",
err: fmt.Errorf("%w for account: abc", roundtrip.ErrNoPeerConnection),
wantTitle: "Proxy Not Connected",
wantTitle: "代理未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: false, Destination: false},
},
{
name: "client not started",
err: fmt.Errorf("%w: %w", roundtrip.ErrClientStartFailed, errors.New("engine init failed")),
wantTitle: "Proxy Not Connected",
wantTitle: "代理未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: false, Destination: false},
},
@@ -981,7 +981,7 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: &os.SyscallError{Syscall: "connect", Err: syscall.ECONNREFUSED},
},
wantTitle: "Service Unavailable",
wantTitle: "服务不可用",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
@@ -992,7 +992,7 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: errors.New("connection was refused"),
},
wantTitle: "Service Unavailable",
wantTitle: "服务不可用",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
@@ -1003,7 +1003,7 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: &os.SyscallError{Syscall: "connect", Err: syscall.EHOSTUNREACH},
},
wantTitle: "Peer Not Connected",
wantTitle: "Peer 未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
@@ -1014,7 +1014,7 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: &os.SyscallError{Syscall: "connect", Err: syscall.ENETUNREACH},
},
wantTitle: "Peer Not Connected",
wantTitle: "Peer 未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
@@ -1025,7 +1025,7 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: errors.New("host is unreachable"),
},
wantTitle: "Peer Not Connected",
wantTitle: "Peer 未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
@@ -1036,7 +1036,7 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: errors.New("network is unreachable"),
},
wantTitle: "Peer Not Connected",
wantTitle: "Peer 未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
@@ -1047,14 +1047,14 @@ func TestClassifyProxyError(t *testing.T) {
Net: "tcp",
Err: &os.SyscallError{Syscall: "connect", Err: syscall.EHOSTUNREACH},
},
wantTitle: "Peer Not Connected",
wantTitle: "Peer 未连接",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},
{
name: "unknown error falls to default",
err: errors.New("something unexpected"),
wantTitle: "Connection Error",
wantTitle: "连接错误",
wantCode: http.StatusBadGateway,
wantStatus: web.ErrorStatus{Proxy: true, Destination: false},
},

File diff suppressed because one or more lines are too long

View File

@@ -23,7 +23,7 @@ const methods: NonNullable<Data["methods"]> =
function App() {
useEffect(() => {
document.title = "Authentication Required - NetBird Service";
document.title = "需要身份验证 - NetBird 服务";
}, []);
const [error, setError] = useState<string | null>(null);
@@ -69,11 +69,11 @@ function App() {
setSubmitting("redirect");
globalThis.location.reload();
} else {
handleAuthError(method, "Authentication failed. Please try again.");
handleAuthError(method, "身份验证失败,请重试。");
}
})
.catch(() => {
handleAuthError(method, "An error occurred. Please try again.");
handleAuthError(method, "发生错误,请重试。");
});
};
@@ -92,14 +92,14 @@ function App() {
const hasCredentialAuth = methods.password || methods.pin;
const hasBothCredentials = methods.password && methods.pin;
const buttonLabel = activeTab === "password" ? "Sign in" : "Submit";
const buttonLabel = activeTab === "password" ? "登录" : "提交";
if (submitting === "redirect") {
return (
<main className="mt-20">
<Card className="max-w-105 mx-auto">
<Title>Authenticated</Title>
<Description>Loading service...</Description>
<Title></Title>
<Description>...</Description>
<div className="flex justify-center mt-7">
<Loader2 className="animate-spin" size={24} />
</div>
@@ -112,9 +112,9 @@ function App() {
return (
<main className="mt-20">
<Card className="max-w-105 mx-auto">
<Title>Authentication Required</Title>
<Title></Title>
<Description>
The service you are trying to access is protected. Please authenticate to continue.
访
</Description>
<div className="flex flex-col gap-4 mt-7 z-10 relative">
@@ -128,7 +128,7 @@ function App() {
onClick={() => { globalThis.location.href = methods.oidc!; }}
>
<LogIn size={16} />
Sign in with SSO
使 SSO
</Button>
)}
@@ -158,7 +158,7 @@ function App() {
<SegmentedTabs.List className="rounded-lg border mb-4">
<SegmentedTabs.Trigger value="password">
<Lock size={14} />
Password
</SegmentedTabs.Trigger>
<SegmentedTabs.Trigger value="pin">
<Binary size={14} />
@@ -171,12 +171,12 @@ function App() {
<div className="mb-4">
{methods.password && (activeTab === "password" || !methods.pin) && (
<>
{!hasBothCredentials && <Label htmlFor="password">Password</Label>}
{!hasBothCredentials && <Label htmlFor="password"></Label>}
<Input
ref={passwordRef}
type="password"
id="password"
placeholder="Enter password"
placeholder="请输入密码"
disabled={submitting !== null}
showPasswordToggle
autoFocus
@@ -187,7 +187,7 @@ function App() {
)}
{methods.pin && (activeTab === "pin" || !methods.password) && (
<>
{!hasBothCredentials && <Label htmlFor="pin-0">Enter PIN Code</Label>}
{!hasBothCredentials && <Label htmlFor="pin-0"> PIN </Label>}
<PinCodeInput
ref={pinRef}
value={pin}
@@ -210,7 +210,7 @@ function App() {
) : (
<>
<Loader2 className="animate-spin" size={16} />
Verifying...
...
</>
)}
</Button>

View File

@@ -9,7 +9,7 @@ import type { ErrorData } from "@/data";
export function ErrorPage({ code, title, message, proxy = true, destination = true, requestId, simple = false, retryUrl }: Readonly<ErrorData>) {
useEffect(() => {
document.title = `${title} - NetBird Service`;
document.title = `${title} - NetBird 服务`;
}, [title]);
const [timestamp] = useState(() => new Date().toISOString());
@@ -18,7 +18,7 @@ export function ErrorPage({ code, title, message, proxy = true, destination = tr
<main className="flex flex-col items-center mt-24 px-4 max-w-3xl mx-auto">
{/* Error Code */}
<div className="text-sm text-netbird font-normal font-mono mb-3 z-10 relative">
Error {code}
{code}
</div>
{/* Title */}
@@ -30,9 +30,9 @@ export function ErrorPage({ code, title, message, proxy = true, destination = tr
{/* Status Cards - hidden in simple mode */}
{!simple && (
<div className="hidden sm:flex items-start justify-center w-full mt-6 mb-16 z-10 relative">
<StatusCard icon={UserIcon} label="You" line={false} />
<StatusCard icon={WaypointsIcon} label="Proxy" success={proxy} />
<StatusCard icon={Globe} label="Destination" success={destination} />
<StatusCard icon={UserIcon} label="" line={false} />
<StatusCard icon={WaypointsIcon} label="代理" success={proxy} />
<StatusCard icon={Globe} label="目标服务" success={destination} />
</div>
)}
@@ -46,24 +46,24 @@ export function ErrorPage({ code, title, message, proxy = true, destination = tr
}
}}>
<RotateCw size={16} />
Refresh Page
</Button>
<Button
variant="secondary"
onClick={() => globalThis.open("https://docs.netbird.io", "_blank", "noopener,noreferrer")}
>
<BookText size={16} />
Documentation
</Button>
</div>
{/* Request Info */}
<div className="text-center text-xs text-nb-gray-300 uppercase z-10 relative font-mono flex flex-col sm:flex-row gap-2 sm:gap-10 mt-4 mb-3">
<div>
<span className="text-nb-gray-400">REQUEST-ID:</span> {requestId}
<span className="text-nb-gray-400"> ID:</span> {requestId}
</div>
<div>
<span className="text-nb-gray-400">TIMESTAMP:</span> {timestamp}
<span className="text-nb-gray-400">:</span> {timestamp}
</div>
</div>

View File

@@ -62,7 +62,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type="button"
onClick={() => setShowPassword(!showPassword)}
className="hover:text-white transition-all"
aria-label="Toggle password visibility"
aria-label="切换密码可见性"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>

View File

@@ -29,7 +29,7 @@ export const NetBirdLogo = ({ size = "default", mobile = true }: Props) => {
src={netbirdFull}
height={sizes[size].desktop}
style={{ height: sizes[size].desktop }}
alt="NetBird Logo"
alt="NetBird 标志"
className={cn(mobile && "hidden md:block", "group-hover:opacity-80 transition-all")}
/>
{mobile && (
@@ -37,7 +37,7 @@ export const NetBirdLogo = ({ size = "default", mobile = true }: Props) => {
src={netbirdMark}
width={sizes[size].mobile}
style={{ width: sizes[size].mobile }}
alt="NetBird Logo"
alt="NetBird 标志"
className={cn(mobile && "md:hidden ml-4")}
/>
)}

View File

@@ -9,9 +9,9 @@ export function PoweredByNetBird() {
className="flex items-center justify-center mt-8 gap-2 group cursor-pointer"
>
<span className="text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all">
Powered by
</span>
<NetBirdLogo size="small" mobile={false} />
</a>
);
}
}

View File

@@ -2,7 +2,7 @@ export const Separator = () => {
return (
<div className="flex items-center justify-center relative my-4">
<span className="bg-nb-gray-940 relative z-10 px-4 text-xs text-nb-gray-400 font-medium">
OR
</span>
<span className="h-px bg-nb-gray-900 w-full absolute z-0" />
</div>

View File

@@ -25,7 +25,7 @@ export function StatusCard({
</div>
<span className="text-sm text-nb-gray-200 font-normal mt-1">{label}</span>
<span className={`text-xs font-medium uppercase ${success ? "text-green-500" : "text-netbird"}`}>
{success ? "Connected" : "Unreachable"}
{success ? "已连接" : "无法访问"}
</span>
{detail && (
<span className="text-xs text-nb-gray-400 truncate text-center">

View File

@@ -24,7 +24,6 @@ export interface Data {
}
declare global {
// eslint-disable-next-line no-var
var __DATA__: Data | undefined
}
@@ -41,8 +40,8 @@ export function getData(): Data {
page: 'error',
error: data.error ?? {
code: 503,
title: 'Service Unavailable',
message: 'The service you are trying to access is temporarily unavailable. Please try again later.',
title: '服务不可用',
message: '你正在访问的服务暂时不可用,请稍后重试。',
proxy: true,
destination: false,
},