# Architecture

## System Overview

```
Client (Clash/Shadowrocket/browser)
  │
  ▼
┌──────────────────────── NeTube ────────────────────────┐
│                                                        │
│  ┌─ sing-box server ────────────────────────────────┐  │
│  │  Inbounds: SOCKS5 / Shadowsocks (public ports)    │  │
│  │  Routes:                                         │  │
│  │    policy endpoints → fwd-{policy} (SOCKS5)      │  │
│  │    direct endpoints → direct-out                 │  │
│  └──────────┬───────────────────────────────────────┘  │
│             │                                          │
│  ┌─ SOCKS5 Relay (per-policy) ──────────────────────┐  │
│  │  fwd-{policy} port → client inbound port         │  │
│  └──────────┬───────────────────────────────────────┘  │
│             │                                          │
│  ┌─ sing-box client ────────────────────────────────┐  │
│  │  Inbounds: SOCKS5 (per-policy, 127.0.0.1)        │  │
│  │  Outbounds: selector (per-policy) → proxy node    │  │
│  │  Clash API: node switching / speed tests         │  │
│  └──────────────────────────────────────────────────┘  │
│                                                        │
│  ┌─ Node.js ────────────────────────────────────────┐  │
│  │  SubscriptionManager  — fetch/parse subscriptions│  │
│  │  Router               — selection + health data  │  │
│  │  HealthChecker        — scheduled speed tests    │  │
│  │  Fastify API          — REST API + Web UI        │  │
│  └──────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────┘
```

## Process Model

NeTube manages two sing-box child processes:

- **server** — Exposes SOCKS5 and Shadowsocks ports and routes traffic by policy. It only restarts if the process crashes.
- **client** — Manages proxy-node outbounds and selectors. It restarts with a regenerated configuration when a subscription's node list changes.

The processes are connected by SOCKS5 relays: server outbound traffic is forwarded
through local SOCKS5 ports to client inbounds.

Endpoints with `policy: "direct"` bypass the client and use the server's `direct`
outbound.

## Data Flow

### Subscription Fetch

```
SubscriptionManager.fetchOne(backendName)
  → fetch(url) → parse (Clash YAML / Shadowrocket Base64)
  → compare the new and previous node lists
  → if changed, call onNodesChanged → rebuildClient()
    → regenerate the client configuration → client.restart()
    → wait for the Clash API → synchronize selector choices
```

### Health Check

```
HealthChecker.checkPolicy(policy)
  → test all nodes concurrently (ClashApi.testNodeDelay)
  → update the policy's isolated health data in Router
  → clear the manual override and select according to the strategy
  → switch if necessary → ClashApi.switchSelector
```

### Request Routing

```
client request → sing-box server inbound
  → route rule matches policy → fwd-{policy} outbound
  → SOCKS5 Relay → sing-box client inbound
  → selector outbound (chosen by Router) → proxy node → destination
```

## Module Responsibilities

| Module | Files | Responsibility |
|--------|-------|----------------|
| Config | `src/config/` | YAML loading, Zod validation, path resolution, and reference validation |
| Subscription | `src/subscription/` | Subscription fetching, Clash/Shadowrocket parsing, and scheduled refreshes |
| Singbox | `src/singbox/` | Configuration generation, process management, and the Clash API client |
| Router | `src/router/` | Selection strategies, per-policy health data, and manual overrides |
| Health | `src/health/` | Scheduled/manual speed tests, concurrency control, and node status evaluation |
| Proxy | `src/proxy/` | SOCKS5 relays connecting the server and client |
| API | `src/api/` | Fastify REST API, authentication, subscription generation, and static file serving |
| Web | `web/src/` | React SPA with Status, Policies, Backends, and Subscribe tabs |

## Strategies

| Strategy | Automatic Tests | Automatic Switching | Manual Selection Duration |
|----------|-----------------|---------------------|---------------------------|
| `lowest-latency` | Yes | Select the lowest-latency node after each test | Until the next test |
| `first-available` | Yes | Switch only when the current node is unavailable | Until the next test |
| `round-robin` | Yes | Select the next healthy node after each test | Until the next test |

Without `healthCheck`, NeTube does not run automatic tests or switch nodes. It
initially uses the first node and preserves manual selections made through the web
UI or API.

Health data is isolated per policy. The same node can have different latency values
in different policies because each policy can use a different test target.

## Authentication

NeTube has two independent password layers:

- **Management password** (`server.password`) — Protects the web UI and all management APIs. Session tokens are kept in server runtime state with a 180-day TTL.
- **Subscription password** (`subscriptions.clash.password` / `subscriptions.shadowrocket.password`) — Protects subscription URLs and is supplied through `?password=xxx`.
