Compare commits
1 Commits
main
...
rainycy-sn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b33acc2a65 |
16
.codegraph/.gitignore
vendored
Normal file
16
.codegraph/.gitignore
vendored
Normal 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
279
AGENTS.md
Normal 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
82
docs/contexts/client.md
Normal 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
|
||||
90
docs/contexts/management.md
Normal file
90
docs/contexts/management.md
Normal 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
62
docs/contexts/relay.md
Normal 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
68
docs/contexts/shared.md
Normal 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
54
docs/contexts/signal.md
Normal 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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">← 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">← 返回</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>
|
||||
|
||||
@@ -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">← Back</a></p>
|
||||
<h1>所有客户端</h1>
|
||||
<p class="info">运行时间: {{.Uptime}} | <a href="/debug">← 返回</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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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">← 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">← 返回</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> </span>
|
||||
<button onclick="startClient()">Start</button>
|
||||
<button onclick="startClient()">启动</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span> </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> </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> </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>
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
|
||||
12
proxy/web/dist/assets/index.js
vendored
12
proxy/web/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user