# introduction

## ToughRADIUS Handbook

Welcome to the **ToughRADIUS Handbook** — the canonical, in-repository, bilingual documentation site for the [ToughRADIUS](https://github.com/talkincode/toughradius) RADIUS server. It is built with [mdBook](https://rust-lang.github.io/mdBook/) and lives in the repository under `docs-site/`, so the documentation is versioned, reviewed, and validated by CI together with the code it describes.

![ToughRADIUS philosophy cover](https://22172318-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNLHQY4r236YqfIH6dIMj%2Fuploads%2Fgit-blob-d25abeebeb04e5719ce9f51a72c920dafd85cf0e%2Ftoughradius-philosophy-cover.png?alt=media)

The handbook is organized into two mirrored language sections. Pick a language to begin:

* **English** — start with the [Overview](/overview).
* **中文** — 从[概述](/overview-1)开始。

Each chapter exists in both languages with a matching structure, and the pages cross-link to their counterparts so you can switch language at any point.

> **Relationship to the existing GitBook site.** This handbook is published to GitHub Pages at its custom domain <https://www.toughradius.net/>, while GitBook renders the same sources at `docs.toughradius.net`. The mdBook handbook **coexists** with GitBook from a single source of truth rather than replacing it; see [mdbook & GitBook Coexistence](/overview/gitbook-coexistence) / [mdbook 与 GitBook 并存](/overview-1/gitbook-coexistence) for the single-source-of-truth policy and the boundary between the two pipelines.

### Build it locally

```bash
# Install mdBook (https://rust-lang.github.io/mdBook/guide/installation.html)
cargo install mdbook            # or: brew install mdbook

# Build the static site into docs-site/book/
mdbook build docs-site

# Or serve with live reload at http://localhost:3000
mdbook serve docs-site
```

***

## ToughRADIUS 使用手册

欢迎来到 **ToughRADIUS 使用手册** —— 这是 [ToughRADIUS](https://github.com/talkincode/toughradius) RADIUS 服务器 随仓库维护的中英文双语文档站点。它基于 [mdBook](https://rust-lang.github.io/mdBook/) 构建，位于仓库的 `docs-site/` 目录下，因此文档与代码一起被版本化、评审，并由 CI 校验。

手册分为结构一一对应的两个语言区。请选择语言开始阅读：

* **English** — 从 [Overview](/overview) 开始。
* **中文** — 从[概述](/overview-1)开始。

每个章节都提供中英文两个版本且结构对应，页面之间相互交叉链接，方便随时切换语言。

> **与现有 GitBook 站点的关系。** 本手册部署到 GitHub Pages，使用自定义域名 <https://www.toughradius.net/> 对外服务；GitBook 则在 `docs.toughradius.net` 渲染同一份源文件。本 mdBook 手册基于单一事实来源与 GitBook **并存**而非替代； 单一事实来源策略与两套管线的边界详见 [mdbook 与 GitBook 并存](/overview-1/gitbook-coexistence) / [mdbook & GitBook Coexistence](/overview/gitbook-coexistence)。

### 本地构建

```bash
# 安装 mdBook（https://rust-lang.github.io/mdBook/guide/installation.html）
cargo install mdbook            # 或：brew install mdbook

# 将静态站点构建到 docs-site/book/
mdbook build docs-site

# 或开启热重载预览，访问 http://localhost:3000
mdbook serve docs-site
```


# English

> 中文版本：[概述](/overview-1)

ToughRADIUS is a powerful, open-source RADIUS server written in Go, designed for ISPs, enterprise networks, and carriers. It implements the standard RADIUS protocols together with RadSec (RADIUS over TLS) and ships with a modern React Admin web management interface.

## Core capabilities

* **Standard RADIUS** — full RFC 2865 (authentication) and RFC 2866 (accounting) support.
* **RadSec** — TLS-encrypted RADIUS over TCP (RFC 6614).
* **Dynamic authorization** — CoA and Disconnect messages (RFC 5176).
* **EAP / 802.1X suite** — challenge methods (EAP-MD5, EAP-MSCHAPv2) and tunneled methods: EAP-TLS (RFC 5216, certificate-based), PEAPv0/EAP-MSCHAPv2 (Windows / AD compatibility), and EAP-TTLS (RFC 5281, inner PAP / MS-CHAPv2). MS-CHAPv2-based methods are compatibility-oriented and carry an NTLMv1-like attack surface; prefer EAP-TLS where you control client certificates. TLS 1.3 (RFC 9190), TEAP, and EAP-PWD are tracked on the roadmap.
* **Multi-vendor support** — compatible with Cisco, MikroTik, Huawei, and other major network devices through vendor-specific attributes (VSAs).
* **Modern management UI** — a React Admin dashboard for users, profiles, online sessions, accounting, and audit logs.
* **Multi-database** — PostgreSQL (default) and SQLite (pure Go, no CGO).

## Service model

The server runs several independent services concurrently; if any one fails, the process exits so a supervisor can restart it cleanly.

| Service         | Protocol / Port     | Purpose                    |
| --------------- | ------------------- | -------------------------- |
| Web / Admin API | HTTP, TCP `1816`    | Management UI and REST API |
| RADIUS Auth     | UDP `1812`          | Authentication             |
| RADIUS Acct     | UDP `1813`          | Accounting                 |
| RadSec          | TLS over TCP `2083` | Encrypted RADIUS transport |

## Where to go next

* [Concepts & Terminology](/overview/concepts) — AAA vocabulary, the request flow, and how each concept maps to the product.
* [Quick Start](/overview/quickstart) — install, initialize, create a user, and verify with `radtest` in about ten minutes.
* [Vendor Integration Guide](/overview/vendor-guide) — case studies for MikroTik, Huawei, Cisco, H3C, ZTE, iKuai, and standard devices.
* [Admin UI Manual](/overview/admin-manual) — every page of the management console.
* [Operations Guide](/overview/ops-guide) — production configuration, certificates, monitoring, backup, and CLI tools.
* [FAQ](/overview/faq) — answers to the questions everyone asks.
* [Documentation Map](/overview/documentation-map) — find the existing README, agent guide, security policy, feature checklist, roadmap, and RFC index.
* [mdbook & GitBook Coexistence](/overview/gitbook-coexistence) — how this handbook relates to the GitBook site and the single-source-of-truth policy.


# Concepts & Terminology

> 中文版本：[核心术语与概念](/overview-1/concepts)

This chapter explains the core AAA terminology used throughout ToughRADIUS and maps each concept to where it lives in the product. For the authoritative list of protocol specifications and how each one maps to the code, see the [Protocol & RFC Reference](/overview/rfc-index).

## AAA in one paragraph

RADIUS (Remote Authentication Dial In User Service) is the protocol that lets a network device ask a central server three questions: *who is this user* (**Authentication**), *what are they allowed to do* (**Authorization**), and *what did they consume* (**Accounting**). ToughRADIUS answers all three: it validates credentials, returns authorization attributes (IP address, bandwidth, VLAN, session limits) in the `Access-Accept`, and records usage from accounting packets.

## Core terms

| Term                                  | Meaning in ToughRADIUS                                                                                                                                                                                                                                                                                    |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **NAS** (Network Access Server)       | The network device — a router, switch, BRAS, or wireless controller — that sends RADIUS requests. Each NAS is registered under **NAS Devices** in the admin UI with its IP address, shared secret, and vendor code. Requests from unregistered addresses are dropped.                                     |
| **Shared secret**                     | A per-NAS password that authenticates RADIUS packets themselves (RFC 2865 §3). It must match on both the NAS and the ToughRADIUS NAS record.                                                                                                                                                              |
| **Subscriber / RADIUS user**          | An account under **RADIUS Users**: username, password, expiration, optional static IPv4/IPv6 addresses, MAC/VLAN bindings, and a billing profile.                                                                                                                                                         |
| **Billing profile** (`RadiusProfile`) | A reusable template under **Billing Profiles**: concurrent-session limit (`active_num`), upload/download rate in **Kbps**, address pool, IPv6 prefix, domain, and MAC/VLAN binding switches. A user inherits profile values for fields it leaves empty.                                                   |
| **VSA** (Vendor-Specific Attribute)   | RADIUS attribute 26 (RFC 2865 §5.26) — a container that lets each vendor define private attributes. ToughRADIUS ships attribute dictionaries for 15+ vendors and emits vendor-specific authorization attributes based on the NAS vendor code. See the [Vendor Integration Guide](/overview/vendor-guide). |
| **Vendor code**                       | The IANA Private Enterprise Number that selects vendor behavior, e.g. `9` Cisco, `2011` Huawei, `14988` MikroTik, `25506` H3C, `3902` ZTE, `10055` iKuai, `0` standard. Set per NAS record; it controls which request parser and which Access-Accept attributes are used.                                 |
| **CoA / Disconnect** (RFC 5176)       | Dynamic Authorization: server-initiated messages that change a live session (`CoA-Request`, e.g. new `Session-Timeout` or `Filter-Id`) or terminate it (`Disconnect-Request`). Sent from the **Online Sessions** page to the NAS on UDP port 3799 (overridable per NAS).                                  |
| **RadSec** (RFC 6614)                 | RADIUS over TLS on TCP port 2083. Wraps RADIUS in a TLS tunnel so packets can safely cross untrusted networks; plain UDP RADIUS relies only on the shared secret.                                                                                                                                         |
| **EAP** (RFC 3748)                    | The Extensible Authentication Protocol used by 802.1X networks. ToughRADIUS implements EAP-MD5, EAP-MSCHAPv2, EAP-TLS, PEAPv0/EAP-MSCHAPv2, and EAP-TTLS; the active method and certificates are chosen in **System Config → RADIUS**.                                                                    |
| **Accounting session**                | The lifecycle reported by the NAS via `Accounting-Request` packets: Start → Interim-Update(s) → Stop (RFC 2866). Live sessions appear under **Online Sessions** (`radius_online` table); history under **Accounting** (`radius_accounting`).                                                              |
| **Acct-Interim-Interval**             | How often (seconds) the NAS should send interim accounting updates. Returned in every Access-Accept from the `radius.AcctInterimInterval` setting.                                                                                                                                                        |
| **Session-Timeout**                   | Maximum remaining session time in seconds. ToughRADIUS sets it to the time left until the user's expiration date, so a session never outlives the account.                                                                                                                                                |
| **Address pool** (`Framed-Pool`)      | A named IP pool configured on the NAS. ToughRADIUS only returns the pool *name*; the NAS allocates the actual address. Static addresses, by contrast, are returned directly as `Framed-IP-Address` / `Framed-IPv6-Address`.                                                                               |
| **MAC binding**                       | When a profile enables `bind_mac`, the first calling MAC seen is stored on the user and later requests must match it.                                                                                                                                                                                     |
| **VLAN binding**                      | When a profile enables `bind_vlan`, the inner/outer VLAN IDs parsed from `NAS-Port-Id` are stored and enforced the same way. Requires a vendor parser that extracts VLANs (Huawei, H3C, ZTE).                                                                                                             |

## How an authentication request flows

```
NAS ──Access-Request──▶ UDP 1812
        │
        ▼
  goroutine pool (ants, TOUGHRADIUS_RADIUS_POOL, default 1024)
        │
        ▼
  1. NAS lookup ─ source IP / identifier must match a registered NAS record
  2. Vendor parser ─ extracts MAC (Calling-Station-Id) and VLANs (NAS-Port-Id)
       · Huawei / H3C / ZTE parsers extract VLAN IDs
       · all other vendors use the default parser (MAC only)
  3. Credential validation ─ PAP / CHAP / MS-CHAPv2, or the EAP state machine
  4. Checkers ─ account status, expiration, MAC bind, VLAN bind, concurrent
     session count (active_num)
  5. Accept enhancers ─ standard attributes (Session-Timeout, pools, static
     IPs, IPv6) plus vendor attributes selected by the NAS vendor code
        │
        ▼
NAS ◀─Access-Accept / Access-Reject──
```

Failed authentications are classified into Prometheus-style counters (`radus_reject_passwd_error`, `radus_reject_expire`, …) that feed the dashboard; see the [Operations Guide](/overview/ops-guide#metrics). A reject-delay guard slows brute-force attempts: after `radius.RejectDelayMaxRejects` consecutive rejects (default 7) inside `radius.RejectDelayWindowSeconds` (default 10 s), responses are delayed.

## Password protocols at a glance

| Protocol           | Where the password travels                                                  | Notes                                                             |
| ------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| **PAP**            | In the request, XOR-protected by the shared secret (RFC 2865 §5.2)          | Universally supported; pair with RadSec or a trusted network.     |
| **CHAP**           | Never — MD5 challenge/response (RFC 2865 §5.3)                              | Requires the server to know the cleartext password.               |
| **MS-CHAPv2**      | Never — NT-hash challenge/response (RFC 2548)                               | Used by Windows; carries a well-known NTLMv1-like attack surface. |
| **EAP (tunneled)** | Inside a TLS tunnel (PEAP / EAP-TTLS) or replaced by certificates (EAP-TLS) | Preferred for Wi-Fi / 802.1X deployments.                         |

## Related chapters

* [Protocol & RFC Reference](/overview/rfc-index) — every RFC cited above, mapped to code.
* [Vendor Integration Guide](/overview/vendor-guide) — per-vendor attributes and formulas.
* [Quick Start](/overview/quickstart) — see these concepts in action in 10 minutes.


# Quick Start

> 中文版本：[快速开始](/overview-1/quickstart)

This chapter takes you from nothing to a working RADIUS server with one test user, then shows how to debug what the server is doing. Default ports: web UI `1816`, RADIUS authentication UDP `1812`, accounting UDP `1813`, RadSec TCP `2083`.

## 1. Install

Pick one of three paths.

### Option A — pre-built binary

Download the binary for your platform from the [GitHub Releases](https://github.com/talkincode/toughradius/releases) page (`toughradius_linux_amd64`, `toughradius_linux_arm64`, `toughradius_darwin_arm64`, `toughradius_windows_amd64.exe`, …), then:

```bash
chmod +x toughradius_linux_amd64
sudo mv toughradius_linux_amd64 /usr/local/bin/toughradius
```

### Option B — Docker

```bash
docker pull talkincode/toughradius:latest

docker run -d --name toughradius \
  -p 1816:1816 -p 1812:1812/udp -p 1813:1813/udp -p 2083:2083 \
  -v toughradius-data:/var/toughradius \
  talkincode/toughradius:latest -c /etc/toughradius.yml
```

The image exposes `1816/tcp`, `1812/udp`, `1813/udp`, and `2083/tcp`. Mount a volume at `/var/toughradius` (the default working directory) so the SQLite database, logs, and certificates survive container restarts.

### Option C — build from source

Requires Go 1.25+ and Node.js 20+ (the React Admin frontend is embedded into the binary):

```bash
git clone https://github.com/talkincode/toughradius.git
cd toughradius
make build          # builds web/ then the Go binary → release/toughradius
```

For backend-only development with hot SQLite defaults:

```bash
make runs           # CGO_ENABLED=0 go run main.go -c toughradius.yml
make runf           # frontend dev server at http://localhost:3000/admin
```

## 2. Configure

ToughRADIUS looks for its configuration in this order: the `-c <file>` flag, `./toughradius.yml`, `/etc/toughradius.yml`, and finally built-in defaults. Environment variables override the file (see the [Operations Guide](/overview/ops-guide#environment-variables)).

A minimal production-style configuration:

```yaml
system:
  appid: ToughRADIUS
  location: Asia/Shanghai
  workdir: /var/toughradius     # data/logs/certs live here
  debug: false

web:
  host: 0.0.0.0
  port: 1816
  secret: change-me-to-a-long-random-string   # JWT signing secret

database:
  type: sqlite                  # or: postgres (+ host/port/user/passwd)
  name: toughradius.db          # stored under {workdir}/data/

radiusd:
  enabled: true
  host: 0.0.0.0
  auth_port: 1812
  acct_port: 1813
  radsec_port: 2083
  debug: true                   # log full packet dumps; disable in production

logger:
  mode: production
  file_enable: true
  filename: /var/toughradius/toughradius.log
```

> **Change `web.secret`.** It signs admin login tokens. The first-start admin password is random (or `TOUGHRADIUS_ADMIN_PASSWORD`); copy it from `{workdir}/private/admin-bootstrap-password` and change it after the first login.

## 3. Initialize the database and run

```bash
# FIRST TIME ONLY — drops and recreates all tables
toughradius -initdb -c /etc/toughradius.yml

# start the server
toughradius -c /etc/toughradius.yml
```

`-initdb` is destructive; on later upgrades just start the server — schema migration runs automatically at startup. Other flags: `-v` prints the version, `-printcfg` prints the merged configuration as JSON.

## 4. Log in to the admin UI

Open `http://<server>:1816`. The bootstrap administrator is:

* Username: `admin`
* Password: written to `{workdir}/private/admin-bootstrap-password`, or the value of `TOUGHRADIUS_ADMIN_PASSWORD` if you set it before first start

There is no well-known default password. Change the bootstrap password under **Account Settings**, or reset a lost one with `cmd/reset-password` (see the [FAQ](/overview/faq)).

## 5. Register a NAS and create a user

1. **Network Nodes → Create** — make a node (a logical group), e.g. `default`.
2. **NAS Devices → Create** — register your network device:
   * *IP address*: the address the device sends RADIUS from.
   * *Secret*: the shared secret, e.g. `testing123`.
   * *Vendor code*: pick the device vendor (Standard / Cisco / Huawei / MikroTik / H3C / ZTE / iKuai) — this controls vendor-specific attributes; see the [Vendor Integration Guide](/overview/vendor-guide).
   * *CoA port*: leave `3799` unless your device uses another port.
3. **Billing Profiles → Create** — e.g. `100M`: concurrency `1`, up rate `51200` Kbps, down rate `102400` Kbps.
4. **RADIUS Users → Create** — username `test1`, password `111111`, pick the profile and an expiration date.

## 6. Verify with radtest

The repository ships a small RADIUS client (defaults shown):

```bash
go run ./cmd/radtest auth \
  -server 127.0.0.1 -secret testing123 \
  -username test1 -password 111111

go run ./cmd/radtest flow ...   # auth + acct-start + acct-stop in one run
```

A successful run prints `Access-Accept` with the returned attributes. The session then appears under **Online Sessions** (for `flow`) and the dashboard counters increase.

## 7. Debugging

| What you need            | How                                                                                                                                             |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Full RADIUS packet dumps | `radiusd.debug: true` in the YAML (or env `TOUGHRADIUS_RADIUS_DEBUG=true`), or set **System Config → RADIUS → Log Level** to `debug` at runtime |
| Log file location        | `logger.filename`, default `{workdir}/toughradius.log`; `logger.mode: development` for human-readable console output                            |
| Why a user is rejected   | Reject reasons are counted per cause (wrong password, expired, bound MAC mismatch, …) and shown on the dashboard; the log carries the detail    |
| Inspect effective config | `toughradius -printcfg -c <file>`                                                                                                               |
| Load testing             | `go run ./cmd/benchmark` — see the [Operations Guide](/overview/ops-guide#command-line-tools)                                                   |

## Next steps

* [Vendor Integration Guide](/overview/vendor-guide) — configure Cisco, Huawei, MikroTik, H3C, ZTE, iKuai and standard devices.
* [Admin UI Manual](/overview/admin-manual) — every page of the management UI.
* [Operations Guide](/overview/ops-guide) — production deployment, TLS, EAP certificates, backups, and monitoring.


# Vendor Integration Guide

> 中文版本：[厂商对接指南](/overview-1/vendor-guide)

ToughRADIUS speaks standard RADIUS to every device and adds **vendor-specific attributes (VSAs)** for the vendors it knows. This chapter walks through the integration steps shared by all devices, then gives a case study per vendor: what ToughRADIUS sends, what it parses, and a reference configuration for the device side.

> **The vendor code on the NAS record decides everything.** Attribute enhancement is selected by the *Vendor* field of the NAS device record in the admin UI — not by inspecting packets. If you leave a MikroTik registered as `Standard`, it will authenticate fine but receive **no** `Mikrotik-Rate-Limit` (no bandwidth enforcement). Pick the right vendor first.

> 📖 Want end-to-end operational examples (PPPoE speed tiers, Hotspot + MAC auth, CoA / forced disconnect)? See the [Scenario Cookbook](/overview/cookbook). This chapter is the attribute reference card; the cookbook is the playbook.

> **Portal boundary:** ToughRADIUS is the RADIUS AAA backend. It does not host captive portal login pages or guest onboarding flows. See [Portal / Hotspot Integration Boundary](/overview/portal-hotspot-boundary).

## Integration steps for any device

1. **Register the NAS** under **NAS Devices → Create**: source IP address (or identifier), shared secret, and the correct *Vendor*.
2. **Point the device** at the server: authentication UDP `1812`, accounting UDP `1813`, the same shared secret.
3. **Optional CoA**: ToughRADIUS sends CoA/Disconnect (RFC 5176) to the NAS on UDP `3799` by default; set the *CoA port* field on the NAS record if your device listens elsewhere. Each exchange waits up to 5 s and retransmits twice.
4. **Create a billing profile and users**, then test with `go run ./cmd/radtest auth …` ([Quick Start](/overview/quickstart)).

### What every vendor receives (standard attributes)

Regardless of vendor, every `Access-Accept` may carry: `Session-Timeout` (seconds until the account expires), `Acct-Interim-Interval`, `Framed-Pool`, `Framed-IP-Address` (static IPv4), `Framed-IPv6-Prefix` / `Framed-IPv6-Address` (RFC 6911), `Framed-IPv6-Pool`, `Delegated-IPv6-Prefix` (RFC 4818), and `Delegated-IPv6-Prefix-Pool` — depending on which user/profile fields are set.

### Rate-limit units

Profile rates are stored in **Kbps**. Each vendor enhancer converts:

| Vendor                  | Attributes                                                          | Value sent                                                                                                                |
| ----------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Huawei (2011)           | `Huawei-Input/Output-Average-Rate`, `Huawei-Input/Output-Peak-Rate` | average = `rate_kbps × 1024` (bit/s); peak = `× 4` further; clamped to Int32 max                                          |
| MikroTik (14988)        | `Mikrotik-Rate-Limit`                                               | string `"{up}k/{down}k"`, e.g. `51200k/102400k` (rx/tx from the router's view)                                            |
| H3C (25506)             | `H3C-Input/Output-Average-Rate`, peak variants                      | same ×1024 / ×4 scheme as Huawei                                                                                          |
| ZTE (3902)              | `ZTE-Rate-Ctrl-SCR-Up/Down`                                         | `rate_kbps × 1024`                                                                                                        |
| iKuai (10055)           | `RP-Upstream/Downstream-Speed-Limit`                                | `rate_kbps × 1024 × 8`, clamped to Int32 max                                                                              |
| Cisco (9), Standard (0) | —                                                                   | no rate VSA — bandwidth is device-side; the `accept-cisco` enhancer emits a non-rate `Cisco-AVPair="ip:addr-pool=<pool>"` |

### Request parsing (MAC and VLAN)

The default parser reads the MAC address from `Calling-Station-Id`. Vendor parsers add request-side VSA or `NAS-Port-Id` handling where the device family has a stable encoding:

* `slot/subslot/port:vlan[.vlan2]` — e.g. `3/0/1:2814.727`
* `vlanid=<n>;vlanid2=<n>;` — e.g. `slot=2;...;vlanid=503;vlanid2=100;`

| Vendor                           | MAC source                                                       | VLAN source                                                 | Notes                                                                         |
| -------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Huawei (2011)                    | `Calling-Station-Id`                                             | `NAS-Port-Id`                                               | Supports both encodings above.                                                |
| H3C (25506)                      | `Calling-Station-Id`                                             | `NAS-Port-Id`                                               | Supports both encodings above.                                                |
| ZTE (3902)                       | `Calling-Station-Id`                                             | `NAS-Port-Id`                                               | Supports both encodings above.                                                |
| Radback (2352)                   | `Mac-Addr` VSA, falling back to `Calling-Station-Id`             | `Bind-Dot1q-Vlan-Tag-Id` VSA, falling back to `NAS-Port-Id` | Request parser only; no Radback response enhancer is shipped.                 |
| Alcatel (3041)                   | `AAT-User-MAC-Address` VSA, falling back to `Calling-Station-Id` | `NAS-Port-Id` when present                                  | Request parser only; response attributes remain deployment-specific.          |
| Aruba/HPE (14823)                | `Calling-Station-Id`                                             | `Aruba-User-Vlan` VSA, falling back to `NAS-Port-Id`        | Also has an Access-Accept enhancer; see below.                                |
| Juniper (2636)                   | `Calling-Station-Id`                                             | `Juniper-VoIP-Vlan` VSA, falling back to `NAS-Port-Id`      | Request parser only; response attributes remain deployment-specific.          |
| MikroTik, iKuai, Cisco, Standard | `Calling-Station-Id`                                             | —                                                           | No vendor-specific VLAN parser; use device-side policy or custom integration. |

MAC binding works with every vendor that sends a recognizable MAC address. VLAN binding requires one of the VLAN-aware parsers above and a matching request encoding from the NAS.

> Device-side snippets below are **reference examples** — command syntax varies by model and OS version; always consult the vendor documentation.

***

## MikroTik (RouterOS) — vendor code 14988

Best-known integration: PPPoE / Hotspot with `Mikrotik-Rate-Limit`.

ToughRADIUS sends `Mikrotik-Rate-Limit = "{up}k/{down}k"`; RouterOS applies it as a dynamic simple queue (rx-rate/tx-rate from the router's perspective, i.e. subscriber upload first).

```routeros
/radius add service=ppp,hotspot address=<TOUGHRADIUS_IP> secret=<SECRET> \
    timeout=3s
/radius incoming set accept=yes port=3799
/ppp aaa set use-radius=yes accounting=yes interim-update=5m
```

* `radius incoming accept=yes` enables CoA/Disconnect on UDP 3799.
* For Hotspot: enable RADIUS in the hotspot server profile (`/ip hotspot profile set ... use-radius=yes`).

## Huawei — vendor code 2011

Typical BRAS (ME60/NE) / aggregation deployments. ToughRADIUS sends the rate quartet (`Huawei-Input/Output-Average-Rate`, peaks ×4), `Huawei-Domain-Name` (when the user/profile has a domain), and `Huawei-Framed-IPv6-Address` for static IPv6. The Huawei parser extracts VLANs from `NAS-Port-Id`, so MAC *and* VLAN binding both work.

```
radius-server template tr_tpl
 radius-server shared-key cipher <SECRET>
 radius-server authentication <TOUGHRADIUS_IP> 1812
 radius-server accounting <TOUGHRADIUS_IP> 1813
#
aaa
 authentication-scheme auth_radius
  authentication-mode radius
 accounting-scheme acct_radius
  accounting-mode radius
  accounting interim interval 5
 domain default
  authentication-scheme auth_radius
  accounting-scheme acct_radius
  radius-server tr_tpl
```

For CoA/Disconnect, enable the RADIUS dynamic authorization extension (`radius-server authorization` on the device) toward the server address.

## Cisco — vendor code 9

ToughRADIUS authenticates Cisco devices with standard attributes (PAP / CHAP / MS-CHAPv2 / EAP all work; sessions, accounting, CoA likewise). When the user's plan has an address pool, the `accept-cisco` enhancer emits `Cisco-AVPair="ip:addr-pool=<pool>"` alongside the standard `Framed-Pool`; no other Cisco-specific attributes are sent. Apply bandwidth policy on the device (Cisco has no portable numeric rate VSA), or extend via the bundled `Cisco-AVPair` dictionary if you build a custom integration.

```
aaa new-model
radius server TOUGHRADIUS
 address ipv4 <TOUGHRADIUS_IP> auth-port 1812 acct-port 1813
 key <SECRET>
aaa authentication ppp default group radius
aaa accounting network default start-stop group radius
aaa server radius dynamic-author
 client <TOUGHRADIUS_IP> server-key <SECRET>
```

`aaa server radius dynamic-author` enables CoA/Disconnect (default port 3799).

## H3C — vendor code 25506

Same rate semantics as Huawei (`H3C-Input/Output-Average-Rate` ×1024, peak ×4). The H3C parser extracts VLANs, so VLAN binding is supported.

```
radius scheme tr_scheme
 primary authentication <TOUGHRADIUS_IP> 1812
 primary accounting <TOUGHRADIUS_IP> 1813
 key authentication simple <SECRET>
 key accounting simple <SECRET>
 user-name-format without-domain
#
domain default enable system
 authentication ppp radius-scheme tr_scheme
 accounting ppp radius-scheme tr_scheme
```

## ZTE — vendor code 3902

ToughRADIUS sends `ZTE-Rate-Ctrl-SCR-Up/Down` (rate ×1024) and parses VLANs from `NAS-Port-Id`. Configuration on ZTE BRAS follows the same radius-template + domain pattern as Huawei; bind the authentication/accounting template to the server address, secret, and ports 1812/1813.

## iKuai — vendor code 10055

Popular SMB gateway in China. ToughRADIUS sends `RP-Upstream-Speed-Limit` / `RP-Downstream-Speed-Limit` (= `rate_kbps × 8192`, clamped). On the iKuai web console: **认证计费 → RADIUS 计费** — set the server address, ports 1812/1813, and the shared secret; enable RADIUS in the PPPoE server settings.

## Aruba/HPE — vendor code 14823

Aruba/HPE devices have both request parsing and Access-Accept enhancement. The parser extracts VLAN information from `Aruba-User-Vlan` when the request carries it, with the same `NAS-Port-Id` fallback used by other VLAN-aware parsers.

On `Access-Accept`, ToughRADIUS sends:

| Attribute         | Source                    | Boundary / no-op rule                                                                                                                             |
| ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Aruba-User-Vlan` | user or profile `Vlanid1` | Sent only for VLAN IDs `1..4094`; missing, `0`, `4095`, or negative values are omitted. `Vlanid2` has no Aruba attribute mapping and is not sent. |
| `Aruba-User-Role` | user or profile `Domain`  | Sent when the inherited domain / role field is non-empty and not `N/A`.                                                                           |

No Aruba VSAs are added for non-Aruba NAS records, and unset fields are left out instead of emitting placeholder values.

## Standard / other devices — vendor code 0

Any RFC-compliant NAS (pfSense, strongSwan, FreeRADIUS clients, Wi-Fi controllers, …) can authenticate against ToughRADIUS with vendor code `Standard`: full credential validation, session control, accounting, IPv4/IPv6 attributes — but no proprietary rate attributes. Attribute **dictionaries** for more vendors (Microsoft, F5, PfSense, Hillstone, …) ship in the codebase for custom development. For Juniper, Alcatel, Aruba, and Radback, the shipped capability is more specific than "dictionary only": see the request parser and Aruba enhancer boundaries above. A dictionary alone still does not parse requests or enhance accepts unless a parser or enhancer is registered.

## Troubleshooting an integration

| Symptom                                            | Likely cause                                                                                                        |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Device gets `Access-Accept` but no bandwidth limit | NAS record vendor is `Standard`, or the device ignores the VSA — check the vendor code first                        |
| All requests silently dropped                      | Source IP not registered as a NAS, or shared secret mismatch                                                        |
| VLAN binding never matches                         | NAS vendor is not one of the VLAN-aware parsers above, or the request uses an unexpected VSA / `NAS-Port-Id` format |
| CoA/Disconnect times out                           | Device CoA listener disabled, or non-default port — set *CoA port* on the NAS record                                |

More in the [FAQ](/overview/faq).


# Portal / Hotspot Integration Boundary

> 中文版本：[Portal / Hotspot 对接边界](/overview-1/portal-hotspot-boundary)

This chapter defines the hard product boundary for captive portal and hotspot deployments.

## Iron rule

**ToughRADIUS does not provide, host, or operate a captive portal login page.**

Portal server, guest onboarding, voucher issuance, SMS/WeChat login, payment flows, advertising pages, and captive network enforcement belong to a separate portal / gateway product. They are not part of the ToughRADIUS product scope.

ToughRADIUS stays a RADIUS AAA system:

* authentication on UDP `1812`;
* accounting on UDP `1813`;
* session policy and audit through RADIUS attributes, accounting records, and optional CoA / Disconnect;
* vendor-specific RADIUS attributes where they can be expressed safely inside the existing vendor parser / enhancer model.

## Supported shape

The supported integration model is:

```
Client -> NAS / WLAN controller / gateway portal -> RADIUS -> ToughRADIUS
```

The NAS, WLAN controller, hotspot gateway, or external portal product owns:

* HTTP/HTTPS captive-portal redirection;
* the login page and user interaction;
* pre-auth / post-auth network enforcement;
* vendor portal callbacks or proprietary portal protocols;
* device-side session admission and release.

ToughRADIUS owns:

* user, profile, NAS, and policy data;
* Access-Accept / Access-Reject decisions;
* accounting records and online-session state;
* standard and vendor RADIUS attributes such as timeout, pool, rate, VLAN, role, or portal URL where a supported vendor enhancer emits them;
* CoA / Disconnect where the NAS supports it.

## What this means for Hotspot

MikroTik Hotspot, Huawei/H3C/iKuai/Cisco WLAN controllers, Aruba captive portal flows, and similar devices can still integrate with ToughRADIUS when the device uses RADIUS as its backend. The device remains the portal implementation; this project remains the AAA backend.

Common supported cases:

* Hotspot / PPPoE / WLAN controller sends Access-Request to ToughRADIUS.
* MAC authentication admits known devices without showing a portal page.
* Accounting updates keep online-session and traffic data current.
* CoA / Disconnect removes or refreshes a session when supported by the NAS.
* Vendor attributes may steer device-side behavior, but only as RADIUS attributes. They do not make ToughRADIUS a portal server.

## Explicit non-goals

Do not add these to ToughRADIUS:

* hosted login pages for guests or subscribers;
* voucher, coupon, QR-code, SMS, WeChat, OAuth, or payment onboarding flows;
* captive-portal JavaScript applications or customer self-service portals;
* proprietary portal-server callback protocols as first-class subsystems;
* per-vendor portal state machines that duplicate the NAS / controller role;
* generic campaign, advertisement, CRM, or visitor-management features.

If a deployment needs those functions, use a dedicated portal product in front of the NAS / controller and integrate it with ToughRADIUS through RADIUS.

## Allowed narrow extensions

Portal-related work is acceptable only when it stays inside the existing RADIUS boundary:

1. Add or fix vendor dictionaries, parsers, or enhancers for RADIUS attributes such as captive-portal URL, user role, filter ID, VLAN, session timeout, or rate limit.
2. Document a specific NAS / controller configuration that uses ToughRADIUS as the RADIUS backend.
3. Add tests for request parsing, response attributes, accounting, or CoA behavior.

Any request to build a hosted portal must be rejected or moved to another product before implementation starts.


# Scenario Cookbook

> 中文版本：[场景实战手册](/overview-1/cookbook)

The [Vendor Integration Guide](/overview/vendor-guide) is a **reference card** — it tells you which attributes ToughRADIUS sends to / parses for a given vendor. This cookbook goes one step further: it is organized around **real operational scenarios** and translates a business need, end to end, into "server config + device config + verification + troubleshooting".

## The five-part shape of every scenario

So you can follow along and debug effectively, every scenario uses the same structure:

1. **Need / scenario** — the problem in business language, no protocol detail.
2. **On the ToughRADIUS side** — exactly what to configure in the admin UI, and which attributes are **actually emitted** after a successful auth, produced by which piece of code.
3. **On the device side** — reference configuration for the NAS/router.
4. **Verification** — how to confirm it really works (radtest, device commands, admin UI).
5. **Troubleshooting** — the most common traps for that scenario, as "symptom → locate → fix".

## Reading conventions

* **Every ToughRADIUS-side claim is anchored to code**: emitted attributes come from the enhancers in `internal/radiusd/plugins/auth/enhancers/`; the accept/reject decisions come from the checkers in `internal/radiusd/plugins/auth/checkers/`. This describes the system's **real behaviour**, not an aspiration.
* **Device-side config is always a reference example**: command syntax varies by model and OS version — defer to the vendor docs and your actual firmware.
* **CoA / Disconnect port is 3799** (RFC 5176). The `1700` you often see online is a client-side local port, not the destination port this system uses.
* Rates are stored in **Kbps** in the rate profile (the UI labels the unit). See [Vendor Integration Guide · Rate units](/overview/vendor-guide#rate-limit-units) for the per-vendor conversion.

## Available cookbooks

* [MikroTik RouterOS](/overview/cookbook/cookbook-mikrotik) — PPPoE broadband ISP speed tiers, Hotspot + MAC authentication, CoA / forced disconnect and FUP, and WPA2/WPA3-Enterprise 802.1X EAP passthrough (EAP-TLS / PEAP / TTLS).
* [Huawei BRAS / NetEngine](/overview/cookbook/cookbook-huawei) — broadband speed tiers with peak rate and AAA domain, line anti-fraud (MAC + VLAN binding) with dual-stack IPv6, CoA / forced disconnect and FUP.
* [H3C, ZTE, iKuai & Cisco](/overview/cookbook/cookbook-vendors) — the per-vendor diff (emitted rate attributes, unit multipliers, MAC / VLAN parsing) for the remaining mainstream vendors; the scenario mechanics follow the two flagship cookbooks.

> **Planned (roadmap M13.8 later batches)**: additional standard-attribute / Wi-Fi-controller scenarios as needed. For attribute-level details of any vendor, see the [Vendor Integration Guide](/overview/vendor-guide).

## Related chapters

* [Quick Start](/overview/quickstart) — install, first login, `radtest` verification.
* [Vendor Integration Guide](/overview/vendor-guide) — per-vendor attribute reference.
* [Admin UI Manual](/overview/admin-manual) — users, rate profiles, online sessions, CoA.
* [FAQ](/overview/faq) — cross-scenario troubleshooting Q\&A.


# MikroTik RouterOS

> 中文版本：[实战手册：MikroTik RouterOS](/overview-1/cookbook/cookbook-mikrotik)
>
> This chapter is part of the [Scenario Cookbook](/overview/cookbook) and follows its [five-part shape and reading conventions](/overview/cookbook#the-five-part-shape-of-every-scenario).

MikroTik RouterOS (vendor code **14988**) is the most common integration target. ToughRADIUS registers a dedicated vendor enhancer for it; on a successful auth it emits:

* `Mikrotik-Rate-Limit = "{up}k/{down}k"` — a string rate limit (produced by `mikrotik_enhancer.go`; `rx/tx` is from the **router's perspective**, so the first field is the subscriber's upload).
* Plus the standard attributes common to all devices: `Session-Timeout`, `Acct-Interim-Interval`, `Framed-Pool`, `Framed-IP-Address`, etc. (produced by `default_enhancer.go`).

> **Prerequisite**: register this router under **NAS devices** with *vendor = MikroTik*, the correct source IP and shared secret; ToughRADIUS must be reachable by the device (auth 1812, accounting 1813). If you register it as `Standard`, auth still succeeds but `Mikrotik-Rate-Limit` is **not** emitted.

***

## Scenario A: PPPoE broadband ISP — speed tiers + address pool + expiry disconnect + concurrency

### Need / scenario

A neighbourhood / small ISP serves Internet over PPPoE and needs: several speed tiers (e.g. Home = 30M down, Business = 100M down), accounts that disconnect on expiry and cannot redial, a concurrency cap per account (to stop credential sharing), and IPs assigned from a shared address pool.

### On the ToughRADIUS side

1. **Create one rate profile per tier** (**Rate profiles → New**):
   * **Up / down rate**: the unit is **Kbps**. 30M down means `30720`, **not** `30`; 10M up means `10240`.
   * **Concurrency `active_num`**: e.g. `1` = at most one online session per account (`0` = unlimited).
   * **Address pool**: a pool name (e.g. `pppoe-pool`) that must match the `/ip pool` name on the router.
2. **Create a user** (**Users → New**): username / password, the matching **rate profile** (rate, concurrency, pool are inherited from it), an **expiry time**, status = enabled. For a fixed IP, set a static IPv4 on the user (it overrides the pool).
3. **Accounting interval**: the config item `radius.AcctInterimInterval` (default `120` seconds) sets the emitted `Acct-Interim-Interval`.

After a successful auth, the `Access-Accept` actually carries (anchored to code):

| Attribute               | Value                                                                      | Source                 |
| ----------------------- | -------------------------------------------------------------------------- | ---------------------- |
| `Mikrotik-Rate-Limit`   | `"{up_kbps}k/{down_kbps}k"`, e.g. `10240k/30720k`                          | `mikrotik_enhancer.go` |
| `Session-Timeout`       | seconds remaining until expiry (drops the current session at the deadline) | `default_enhancer.go`  |
| `Acct-Interim-Interval` | `radius.AcctInterimInterval` (default 120)                                 | `default_enhancer.go`  |
| `Framed-Pool`           | the pool name from the profile / user (emitted only if set)                | `default_enhancer.go`  |
| `Framed-IP-Address`     | the user's static IPv4 (emitted only if set)                               | `default_enhancer.go`  |

> **The concurrency cap is not implemented by emitting an attribute**: it is enforced by `online_count_checker` at auth time, comparing `active_num` to the current online count — an over-limit new session is rejected (`Access-Reject`).

### On the device side (RouterOS, reference example, verify on your firmware)

```routeros
# Point at ToughRADIUS (same shared secret for auth/accounting)
/radius add service=ppp address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s
/radius incoming set accept=yes port=3799

# Enable RADIUS auth + accounting + periodic interim updates
/ppp aaa set use-radius=yes accounting=yes interim-update=5m

# The pool name must match the emitted Framed-Pool
/ip pool add name=pppoe-pool ranges=10.10.0.2-10.10.255.254

# PPPoE service (let the RADIUS Framed-Pool/Framed-IP decide remote-address)
/ppp profile add name=radius-pppoe local-address=10.10.0.1
/interface pppoe-server server add service-name=isp interface=<bridge> \
    default-profile=radius-pppoe disabled=no
```

> No manual queue is needed for rate limiting: on receiving `Mikrotik-Rate-Limit` the router creates a dynamic simple queue automatically.

### Verification

* **radtest (server side)**:

  ```bash
  go run ./cmd/radtest auth -server <TOUGHRADIUS_IP> -nas-ip <NAS_IP> \
    -username <username> -password <password> -secret <SECRET>
  ```

  On success it prints `Access-Accept`; you should see `Mikrotik-Rate-Limit`, `Session-Timeout`, and (if a pool is set) `Framed-Pool`.
* **Router side**: `/ppp active print` (online sessions), `/queue simple print` (the dynamic queue and its rate), `/log print where topics~"radius"`.
* **Admin UI**: the **Online sessions** page should list the session (Framed IP, duration, up/down traffic).

### Troubleshooting (symptom → locate → fix)

* **Rate limit has no effect at all** → ① confirm the NAS is registered as *MikroTik* (registered as Standard emits no VSA); ② wrong rate unit (`30` instead of `30720`); ③ direction reversed (`rx/tx` is the router's view, the first field is the subscriber's **upload**).
* **Dials up but gets no / wrong IP** → the `Framed-Pool` name does not match the `/ip pool` name, or no pool is set on the profile; align the names or set a static IP on the user.
* **Account still online after expiry** → `Session-Timeout` only drops the **current** session at the deadline; an already-online session waits for the timeout or a manual **forced disconnect** on the sessions page. A **redial** after expiry is rejected by `expire_checker` (counted under `user expire`).
* **The second connection is rejected** → with `active_num=1`, `online_count_checker` rejects the concurrent session — this is expected; raise the profile's concurrency to allow multiple dials.
* **Server slows down / stops replying after repeated auth failures** → `reject_delay_guard` introduces a delay once a username is rejected more than a threshold (default 7) in a row, to throttle brute force; it recovers automatically.

***

## Scenario B: Hotspot + MAC authentication

### Need / scenario

In a public WiFi / Hotspot environment, you want certain enrolled devices (printers, IoT, long-term guest devices) to **skip the portal** and be admitted and rate-limited by MAC address.

### On the ToughRADIUS side

ToughRADIUS decides a request is **MAC authentication** by this rule (anchored to `auth_stages.go` / `eap_helper.go`):

> When the MAC address parsed from the request **equals the username**, the request is treated as MAC auth; the password check then compares against the user record's **MAC address** field rather than an ordinary password.

So configure it as follows:

1. **Create a user** with **username = the device MAC** (the string must match exactly what the router sends), and put the same MAC in the user record's **MAC address** field.
2. Assign the user a rate profile (rate and concurrency apply as usual).

> **The biggest trap is MAC format**: case and separators must exactly match the `User-Name` the RouterOS hotspot sends (ToughRADIUS compares strings exactly). RouterOS's format is influenced by settings such as `mac-auth-mode` — **what you store is what must be sent**.

### On the device side (RouterOS, reference example, verify on your firmware)

```routeros
/radius add service=hotspot address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s

# Enable RADIUS and MAC login on the hotspot server profile
/ip hotspot profile set <profile> use-radius=yes login-by=mac,http-chap
# mac-auth-mode / mac-auth-password depend on firmware
```

### Verification

* **radtest**: use `-user <MAC> -pwd <MAC>` to simulate a MAC auth and check for `Access-Accept`.
* **Router side**: `/ip hotspot active print` should list the device; `/log print` for the auth log.
* **Admin UI**: the session appears on the Online sessions page.

### Troubleshooting (symptom → locate → fix)

* **MAC auth always fails** → the username / MAC field does not match the MAC string the router actually sends (case, separators). Capture one request, read the literal `User-Name`, and recreate the user from that exact string.
* **It is being treated as an ordinary account** → only "request MAC == username" takes the MAC-auth path; if the hotspot does not log in by MAC, the request goes through the portal username / password logic.

***

## Scenario C: Live control — CoA, forced disconnect and FUP

### Need / scenario

Control online users in real time: rate-limit after a quota is exceeded (FUP), force a re-authentication, or simply kick a session offline.

### On the ToughRADIUS side

Select a session on the **Online sessions** page and run one of two actions (anchored to `session_actions.go` and [Admin UI Manual · Online sessions](/overview/admin-manual#online-sessions)):

* **Change of Authorization (CoA-Request)**: currently carries only **`Session-Timeout` (#27)** and / or **`Filter-Id` (#11)**.
  * Use `Session-Timeout` to shorten the session's life and force the client to re-authenticate sooner.
  * Use `Filter-Id` to have RouterOS apply a **pre-defined** filter / address-list (e.g. a rate or site-restriction rule).
* **Forced disconnect (Disconnect-Request)**: terminate the session directly (with a confirmation step).

> **The correct path to live FUP "speed change"**: ToughRADIUS's CoA does **not** rewrite `Mikrotik-Rate-Limit`. To change a user's speed live, the standard approach is — change the rate on the profile / user first, **then force a disconnect**; the client redials automatically and is re-authorized at the new speed. This path matches the system's actual capability and works for any vendor.

Operator-initiated CoA / Disconnect uses a short timeout with one automatic retry, targeting the **CoA port (default 3799)** on the NAS record.

### On the device side (RouterOS, reference example, verify on your firmware)

```routeros
# Required, otherwise CoA/Disconnect is not received
/radius incoming set accept=yes port=3799
```

* The firewall must allow **inbound UDP 3799** (from ToughRADIUS to the router).
* For the `Filter-Id` approach, pre-define a filter / queue / address-list of the same name on RouterOS (verify on your firmware).

### Verification

* Click **Change authorization** / **Forced disconnect** on the sessions page; the result is reported as a notification.
* On the router, `/log print` shows the incoming request; after a **forced disconnect**, the session disappears from `/ppp active print` and the client then redials automatically.

### Troubleshooting (symptom → locate → fix)

* **CoA / Disconnect does not respond or times out** → ① RouterOS did not set `radius incoming accept=yes`; ② the firewall blocks UDP 3799; ③ the NAS record's CoA port differs from the device's `incoming port`; ④ the shared secret differs.
* **Changed the rate but the online user's speed did not change** → expected: CoA does not change the rate; **force a disconnect** so the client redials.
* **Port confusion** → the CoA `1700` you often see online is a client-side local port; this system and RFC 5176 use **3799**.

***

## Scenario D: WPA2/WPA3-Enterprise Wi-Fi — 802.1X EAP passthrough to ToughRADIUS

### Need / scenario

You run enterprise Wi-Fi (`WPA2-EAP` / `WPA3-EAP`, i.e. 802.1X) on MikroTik APs and want **one central place** for accounts, certificates and policy: ToughRADIUS. Staff authenticate either with a **client certificate** (EAP-TLS, password-less), or with **username + password** (PEAP-MSCHAPv2 for Windows/AD-style clients, or EAP-TTLS for legacy / LDAP back ends).

> **How this differs from** [**`multiduplikator/mikrotik_EAP`**](https://github.com/multiduplikator/mikrotik_EAP)**.** That well-known guide makes RouterOS itself the EAP server (ROS6 against the certificate store, or ROS7 via **User Manager v5**). Here MikroTik is **only the authenticator**: its `eap-methods=passthrough` relays the 802.1X/EAP conversation to ToughRADIUS, and **ToughRADIUS terminates EAP**. Two consequences follow that you must plan for:
>
> 1. **The trust anchor moves to ToughRADIUS.** Clients no longer trust `RouterCA`; they must trust the **CA that signed ToughRADIUS's server certificate** selected in `EapTlsServerCert`. You distribute *that* CA to client devices.
> 2. **The router holds no user/cert material for auth.** All identities live on ToughRADIUS, so you get its account lifecycle, rate profiles, online-session view and accounting for free.

### On the ToughRADIUS side

#### 0. Prepare the certificates to import

Generate PEM material first, then import it into ToughRADIUS's managed certificate store. A minimal in-house CA with `openssl` — EC keys keep the TLS records small, which matters for EAP fragmentation:

```bash
# Root CA (distribute ca.pem to every client device)
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.pem \
  -subj "/CN=ToughRADIUS EAP Root CA"

# RADIUS server certificate (CN/SAN is what clients pin as the server identity)
openssl ecparam -name prime256v1 -genkey -noout -out server.key
openssl req -new -key server.key -out server.csr -subj "/CN=radius.example.com"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -days 825 -sha256 -out server.pem \
  -extfile <(printf "subjectAltName=DNS:radius.example.com\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment")

# EAP-TLS client certificate (only for EAP-TLS users). The SAN email becomes the
# RADIUS identity — see the identity-binding rule below.
openssl ecparam -name prime256v1 -genkey -noout -out alice.key
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice"
openssl x509 -req -in alice.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -days 825 -sha256 -out alice.pem \
  -extfile <(printf "subjectAltName=email:alice@example.com\nextendedKeyUsage=clientAuth\nkeyUsage=digitalSignature")

# Bundle the client key+cert+CA as PKCS#12 for Windows/Android/iOS enrolment
openssl pkcs12 -export -inkey alice.key -in alice.pem -certfile ca.pem \
  -out alice.p12 -passout pass:'<long-passphrase>'
```

> **EAP-TLS identity binding (anchored to `tlsengine/identity.go`, RFC 5216 §5.2).** After the client certificate passes chain validation, ToughRADIUS derives the **Peer-Id** in this order: **SAN `rfc822Name` (email) → SAN `dnsName` → subject `CN`**, and it must equal the RADIUS `User-Name` (case-insensitive). When a SAN is present the `CN` is **not** accepted as an alternate. So with the cert above the matching ToughRADIUS username is **`alice@example.com`**, not `alice`.

#### 1. Import the material and select managed certificates

Open **Certificates** and import:

* `server.pem` with its matching `server.key` as a server certificate, for example local name `eap-server`.
* `ca.pem` as the client-CA certificate, for example local name `eap-client-ca`.

Then open **System config → RADIUS** and select those names on the EAP settings (anchored to `internal/app/config_schemas.json` / `eap/handlers/tls_config.go`):

| Config item (`radius.*`)                             | EAP-TLS                     | PEAP / TTLS             | Notes                                                                                                        |
| ---------------------------------------------------- | --------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| **EAP Method** (`EapMethod`)                         | `eap-tls`                   | `eap-peap` / `eap-ttls` | The method **offered first** on EAP-Identity.                                                                |
| **Enabled EAP Handlers** (`EapEnabledHandlers`)      | `eap-tls,eap-peap,eap-ttls` | same                    | Allow-list; `*` = all. A client may **NAK** to another method, honoured only if it is enabled here.          |
| **EAP-TLS Server Certificate** (`EapTlsServerCert`)  | `eap-server`                | same                    | Managed certificate presented for **all three** tunnelled methods; it must include the matching private key. |
| **EAP-TLS Client CA** (`EapTlsClientCa`)             | `eap-client-ca`             | *(unused)*              | **Required for EAP-TLS** (verifies client certs); PEAP/TTLS are server-only and ignore it.                   |
| **EAP-TLS Minimum TLS Version** (`EapTlsMinVersion`) | `1.2`                       | `1.2`                   | PEAP/TTLS are pinned to **TLS 1.2** regardless.                                                              |

> Managed certificate selections are resolved **between handshakes**, so you can rotate imported certificates without restarting the RADIUS service. Until the required managed certificates are selected, EAP **safe-rejects** (`ErrTLSNotConfigured`) rather than authenticating without trust anchors — so a half-configured server never lets anyone in.

#### 2. Create the users

| Method                           | Username to create                             | Password field               | Comes from                                             |
| -------------------------------- | ---------------------------------------------- | ---------------------------- | ------------------------------------------------------ |
| **EAP-TLS**                      | the cert **Peer-Id**, e.g. `alice@example.com` | **unused** (any placeholder) | cert identity matched in `tlsengine/identity.go`       |
| **PEAP-MSCHAPv2**                | the login name, e.g. `bob`                     | the user's real password     | inner MSCHAPv2 vs `GetPassword` in `peap_inner.go`     |
| **EAP-TTLS** (PAP or MS-CHAP-V2) | the login name, e.g. `carol`                   | the user's real password     | inner PAP/MSCHAPv2 vs `GetPassword` in `ttls_inner.go` |

Each user still needs **status = enabled**, a **future expiry**, and a **rate profile** (rate / pool / concurrency apply exactly as in Scenario A).

> **The single biggest passthrough trap — the outer (anonymous) identity.** ToughRADIUS loads the user record from the **outer `User-Name`** and looks up the password from *that* record; mapping a separate *anonymous* outer identity to the real account is not yet implemented (deferred to M8.4). So for **PEAP / TTLS the outer identity must equal the real username** — on the supplicant, **leave “anonymous identity” blank** (it then sends the real username in the clear outer identity) **or set it equal to the username**. An outer identity of `anonymous` is rejected as *user not found*. EAP-TLS is unaffected (its identity comes from the certificate).

### On the device side (RouterOS, reference example, verify on your firmware)

First register this router under **NAS devices** in ToughRADIUS (its source IP + shared secret; vendor *MikroTik* if you also want `Mikrotik-Rate-Limit`, otherwise *Standard* — EAP itself needs no VSA). Then point the AP at ToughRADIUS and make the security profile **pass EAP through**:

```routeros
# 1) RADIUS server for the wireless service (same secret as the NAS record)
/radius add service=wireless address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s

# 2a) CLASSIC /interface wireless — passthrough is the key word
/interface wireless security-profiles add name=eap-passthrough \
    authentication-types=wpa2-eap eap-methods=passthrough \
    radius-eap-accounting=yes \
    unicast-ciphers=aes-ccm group-ciphers=aes-ccm
/interface wireless set wlan1 security-profile=eap-passthrough \
    ssid="ToughRADIUS-EAP" mode=ap-bridge disabled=no

# 2b) …or CAPsMAN (matches the reference repo's topology)
/caps-man security add name=eap-passthrough authentication-types=wpa2-eap \
    encryption=aes-ccm group-encryption=aes-ccm eap-methods=passthrough \
    eap-radius-accounting=yes
# attach security=eap-passthrough to your /caps-man configuration, then provision

# 2c) …or the new /interface wifi (wifiwave2, ROS 7.13+): passthrough is IMPLICIT
/interface wifi security add name=eap-sec authentication-types=wpa2-eap,wpa3-eap
/interface wifi set wifi1 security=eap-sec \
    configuration.ssid="ToughRADIUS-EAP" disabled=no
```

> `eap-methods=passthrough` (classic / CAPsMAN) is exactly what makes the router a relay instead of an EAP terminator. The new `/interface wifi` stack has no `eap-methods` knob — selecting a `wpa2-eap`/`wpa3-eap` security profile makes it relay to the configured `service=wireless` RADIUS automatically.

### Verification

`radtest` **cannot** drive EAP. Use `eapol_test` (from `wpa_supplicant` / hostap) — the same tool the project's [EAP acceptance reports](/overview/eap-acceptance-reports) run (v2.10). It talks RADIUS straight to ToughRADIUS, so you can validate the server **before** touching a real radio. Save one of these and run `eapol_test -c <file>.conf -a <TOUGHRADIUS_IP> -p 1812 -s <SECRET>` — a pass prints `SUCCESS`:

```ini
# eap-tls.conf  — certificate, password-less
network={
    key_mgmt=WPA-EAP
    eap=TLS
    identity="alice@example.com"      # == the cert SAN email == the TR username
    ca_cert="/etc/eap/ca.pem"         # trust ToughRADIUS's server cert
    client_cert="/etc/eap/alice.pem"
    private_key="/etc/eap/alice.key"
}
```

```ini
# peap-mschapv2.conf
network={
    key_mgmt=WPA-EAP
    eap=PEAP
    identity="bob"                    # outer == real username (no anonymous id)
    password="<bob-password>"
    ca_cert="/etc/eap/ca.pem"
    phase2="auth=MSCHAPV2"
}
```

```ini
# ttls-pap.conf
network={
    key_mgmt=WPA-EAP
    eap=TTLS
    identity="carol"
    anonymous_identity="carol"        # must equal identity (see the trap above)
    password="<carol-password>"
    ca_cert="/etc/eap/ca.pem"
    phase2="auth=PAP"                 # or auth=MSCHAPV2; CHAP/MS-CHAP unsupported
}
```

* **ToughRADIUS log**: a successful run logs `radius auth success … is_eap=true result=success`; the session appears on the **Online sessions** page (with accounting once the radio is live).
* **Router side**: `/log print where topics~"radius,wireless"`; for an associated client `/interface wireless registration-table print` (classic) or `/interface wifi registration-table print` (wifi).
* **EAP-TLS and EAP-TTLS (PAP & MS-CHAP-V2)** verify cleanly end-to-end with `eapol_test`. For **PEAP-MSCHAPv2**, `eapol_test` currently hits a documented inner-framing interop gap, so the acceptance harness validates PEAP with the in-process integration test instead (see [EAP Acceptance Reports](/overview/eap-acceptance-reports)); real Windows / Android / iOS PEAP supplicants interoperate, so test PEAP with an actual client.

### Troubleshooting (symptom → locate → fix)

* **EAP-TLS reject, reply `… handshake failed`** → the client certificate does not chain to the managed **`EapTlsClientCa`** CA (or the wrong CA was selected). Re-issue the client cert from the same `ca.pem`, or select the correct imported client CA.
* **EAP-TLS reject, reply `… identity … does not match`** → the user's **username ≠ the cert Peer-Id**. Remember the order **SAN email → SAN DNS → CN** and that a SAN cert's `CN` is ignored: for the sample cert the username must be `alice@example.com`. Either rename the user or issue a CN-only cert and set the username to the CN.
* **Client refuses to connect / “can’t verify server”** → the device does not trust **ToughRADIUS's** CA. Install `ca.pem` on the client; on **Android 11+** also set the **Domain** field to the server cert CN/SAN (`radius.example.com`), which Android now enforces.
* **PEAP / TTLS reject as “user not found” although the password is right** → an **anonymous outer identity** was used. Clear it (or set it to the real username) so the outer `User-Name` equals the account — see the trap above.
* **No EAP challenge at all / immediate reject** → `EapTlsServerCert` (and `EapTlsClientCa` for EAP-TLS) is not selected, or the selected server certificate has no private key, so EAP safe-rejects. Import the certificate material and select the managed names; no restart needed.
* **The method you want is never offered** → the default `EapMethod` is `eap-md5`, which is invalid for WPA-Enterprise. Set `EapMethod` to your tunnelled method and include it (and any you accept via client NAK) in **Enabled EAP Handlers**.
* **EAP-TTLS inner auth rejected** → only **inner PAP and MS-CHAP-V2** are implemented; inner CHAP / MS-CHAP / tunnelled-EAP are not. Also the TTLS tunnel is pinned to **TLS 1.2** — a TLS-1.3-only supplicant won't complete phase 2.
* **Auth works but no accounting / no online session** → enable `radius-eap-accounting=yes` (classic) / `eap-radius-accounting=yes` (CAPsMAN) and make sure UDP **1813** reaches ToughRADIUS.

***

## Related chapters

* [Vendor Integration Guide · MikroTik](/overview/vendor-guide) — attribute reference card.
* [Admin UI Manual](/overview/admin-manual) — users / rate profiles / online sessions / CoA forms.
* [FAQ](/overview/faq) — more cross-scenario troubleshooting Q\&A.


# Huawei BRAS / NetEngine

> 中文版本：[实战手册：华为 BRAS / NetEngine](/overview-1/cookbook/cookbook-huawei)
>
> This chapter is part of the [Scenario Cookbook](/overview/cookbook) and follows its [five-part shape and reading conventions](/overview/cookbook#the-five-part-shape-of-every-scenario).

Huawei (vendor code **2011**) is the dominant broadband BRAS / enterprise gateway in Chinese carrier and enterprise networks (NetEngine / ME60 / older MA5200 lines). ToughRADIUS registers a dedicated vendor enhancer for it; on a successful auth it emits:

* A **four-attribute rate quartet** (produced by `huawei_enhancer.go`): `Huawei-Input-Average-Rate`, `Huawei-Input-Peak-Rate`, `Huawei-Output-Average-Rate`, `Huawei-Output-Peak-Rate`.
* `Huawei-Domain-Name` — only when the user / profile has a **domain** set.
* `Huawei-Framed-IPv6-Address` — only when the user has a **static IPv6**.
* Plus the standard attributes common to all devices (`Session-Timeout`, `Acct-Interim-Interval`, `Framed-Pool`, `Framed-IP-Address`, …) produced by `default_enhancer.go`.

On the request side the **Huawei parser** (`huawei_parser.go`) extracts the MAC from `Calling-Station-Id` (normalising `-` to `:`) and the inner / outer VLAN IDs from `NAS-Port-Id`. That is what makes MAC binding *and* VLAN binding possible for Huawei devices.

> **Prerequisite**: register this BRAS under **NAS devices** with *vendor = Huawei*, the correct source IP and shared secret; ToughRADIUS must be reachable (auth 1812, accounting 1813). If you register it as `Standard`, auth still succeeds but **none** of the Huawei VSAs above are emitted, and the VLAN IDs are not parsed.

***

## Scenario A: PPPoE / IPoE broadband — speed tiers, peak rate and AAA domain

### Need / scenario

A carrier or enterprise runs broadband on a Huawei BRAS and needs several speed tiers (e.g. Home = 30M down / 10M up, Business = 100M down). Huawei honours both an **average** and a **peak** (burst) rate, and groups subscribers into AAA **domains** so the BRAS applies the right domain policy.

### On the ToughRADIUS side

1. **Create one rate profile per tier** (**Rate profiles → New**):
   * **Up / down rate**: the unit is **Kbps**. 30M down means `30720`, **not** `30`; 10M up means `10240`.
   * **Domain** (optional): the Huawei AAA domain name (e.g. `isp`), pushed as `Huawei-Domain-Name` so the BRAS binds the session to that domain.
2. **Create a user** (**Users → New**): username / password, the matching **rate profile**, an **expiry time**, status = enabled. A per-user **domain** field overrides the profile domain.

How the stored Kbps rates become the four Huawei VSAs (anchored to `huawei_enhancer.go`):

| Attribute                                                                      | Value                      | Note                                                        |
| ------------------------------------------------------------------------------ | -------------------------- | ----------------------------------------------------------- |
| `Huawei-Input-Average-Rate`                                                    | `up_kbps × 1024` (bit/s)   | "Input" = traffic **into** the BRAS = subscriber **upload** |
| `Huawei-Input-Peak-Rate`                                                       | `up_kbps × 1024 × 4`       | average × 4                                                 |
| `Huawei-Output-Average-Rate`                                                   | `down_kbps × 1024` (bit/s) | "Output" = traffic **out** to the subscriber = **download** |
| `Huawei-Output-Peak-Rate`                                                      | `down_kbps × 1024 × 4`     | average × 4                                                 |
| `Huawei-Domain-Name`                                                           | the user / profile domain  | emitted **only** if a domain is set                         |
| `Session-Timeout`, `Acct-Interim-Interval`, `Framed-Pool`, `Framed-IP-Address` | standard                   | from `default_enhancer.go`                                  |

> **Two traps unique to Huawei.** ① **Unit**: the rate VSAs are in **bit/s**, not Kbps — ToughRADIUS multiplies the stored Kbps by **1024** (binary), and the **peak** is the average **× 4**. So a "30M down" tier is sent as `Output-Average-Rate = 30720 × 1024 = 31457280` and `Output-Peak-Rate = 125829120`. ② **Direction naming**: Huawei "Input" is the subscriber's **upload** and "Output" is the **download** (BRAS perspective) — the opposite words from MikroTik's `rx/tx`, but the same physical meaning. All four values are clamped to the Int32 maximum.

### On the device side (Huawei VRP, reference example, verify on your firmware)

```
# RADIUS server template
radius-server template tr-tmpl
 radius-server shared-key cipher <SECRET>
 radius-server authentication <TOUGHRADIUS_IP> 1812 weight 80
 radius-server accounting <TOUGHRADIUS_IP> 1813 weight 80
#
# AAA domain bound to the template (matches Huawei-Domain-Name)
aaa
 authentication-scheme radius-auth
  authentication-mode radius
 accounting-scheme radius-acct
  accounting-mode radius
 domain isp
  authentication-scheme radius-auth
  accounting-scheme radius-acct
  radius-server tr-tmpl
```

> The `Huawei-Output-Average-Rate` / peak values map to the BRAS's per-user CAR / QoS; no manual per-user queue is required.

### Verification

* **radtest (server side)**:

  ```bash
  go run ./cmd/radtest auth -server <TOUGHRADIUS_IP> -nas-ip <NAS_IP> \
    -username <username> -password <password> -secret <SECRET>
  ```

  On success it prints `Access-Accept`; you should see the four Huawei rate attributes and (if set) `Huawei-Domain-Name`.
* **BRAS side**: `display access-user username <name>` (online session, rate, domain), `display aaa online-fail-record` for failures.
* **Admin UI**: the **Online sessions** page should list the session.

### Troubleshooting (symptom → locate → fix)

* **Rate limit has no effect** → ① confirm the NAS is registered as *Huawei* (registered as Standard emits no VSA); ② remember the value is bit/s (`× 1024`), so a tier that looks "1000× too small" usually means the unit was read as Kbps on the device side.
* **Speeds look swapped (upload capped at the download value)** → the Input/Output direction confusion: Huawei **Input = upload**, **Output = download**; check which profile field feeds which.
* **Domain policy not applied** → the user / profile has no **domain** set (then `Huawei-Domain-Name` is not emitted), or the domain name does not match a `domain` configured on the BRAS.
* **Peak / burst seems too high** → expected: peak is hard-coded as average **× 4** in the enhancer; tune the average tier, not the peak.

***

## Scenario B: Line anti-fraud — MAC + VLAN binding and dual-stack IPv6

### Need / scenario

A broadband operator wants to stop account sharing / theft by **binding each account to its access line** — the subscriber's MAC and / or the access VLAN (inner / outer, i.e. QinQ) — and to hand out a **static IPv6** for dual-stack service.

### On the ToughRADIUS side

Huawei encodes the access line in attributes the parser already reads:

* the **MAC** comes from `Calling-Station-Id`;
* the **inner / outer VLAN** come from `NAS-Port-Id`.

Binding is then enforced by two checkers (anchored to `mac_bind_checker.go` / `vlan_bind_checker.go`):

1. **MAC binding** — enable **bind MAC** on the user / profile and store the allowed **MAC** on the user. At auth time, if the request MAC differs from the stored MAC, the session is rejected (`MacBindError`).
2. **VLAN binding** — enable **bind VLAN** on the user / profile and store the allowed **VLAN ID 1 / VLAN ID 2** on the user. If a stored VLAN differs from the parsed one, the session is rejected (`VlanBindError`).
3. **Static IPv6** — set the user's **IPv6 address**; the enhancer then emits `Huawei-Framed-IPv6-Address` (the prefix length, if written as `addr/len`, is stripped before sending).

> **Binding only enforces what you have stored.** Each checker is **skipped** when its toggle is off, *and also* when either side (stored value or request value) is empty / zero — it never silently auto-learns. So to bind a line you must (a) turn the toggle on **and** (b) fill in the MAC / VLAN on the user record (typically captured from the first successful login).

### On the device side (Huawei VRP, reference example, verify on your firmware)

```
# IPoE / PPPoE access that reports the access line to RADIUS.
# Huawei carries the inner/outer VLAN inside NAS-Port-Id and the
# subscriber MAC inside Calling-Station-Id by default on BRAS access.
radius-server template tr-tmpl
 radius-server shared-key cipher <SECRET>
 radius-server authentication <TOUGHRADIUS_IP> 1812 weight 80
 radius-server accounting <TOUGHRADIUS_IP> 1813 weight 80
#
# Enable IPv6 address/prefix delivery on the BRAS as required by your design
ipv6
```

### Verification

* Capture one real `Access-Request` (or read the auth log) and confirm the `Calling-Station-Id` and `NAS-Port-Id` values, then store **exactly** those on the user before turning binding on.
* **radtest** with the matching MAC succeeds; a different MAC / VLAN is rejected.
* **BRAS side**: `display access-user username <name>` shows the bound line and any delivered IPv6 address.

### Troubleshooting (symptom → locate → fix)

* **Binding rejects the legitimate user** → the stored MAC / VLAN does not match what the BRAS actually sends. Read the real `Calling-Station-Id` / `NAS-Port-Id`, store those exact values, then re-enable binding.
* **VLAN binding never triggers** → the parsed VLAN is `0` (the BRAS does not put the VLAN in `NAS-Port-Id` in your topology), or **bind VLAN** is off, or the user's stored VLAN is `0`; any of these skips the check.
* **No IPv6 delivered** → the user has no static **IPv6 address** set, or the BRAS is not configured for IPv6 / dual-stack on that domain.
* **Binding is silently ignored** → the toggle is off or the stored value is empty; the checker only enforces when the toggle is on **and** both sides are present.

***

## Scenario C: Live control — CoA, forced disconnect and FUP

### Need / scenario

Control online users on a Huawei BRAS in real time: shorten a session, force re-authentication, rate-limit after a quota is exceeded (FUP), or kick a session offline.

### On the ToughRADIUS side

Select a session on the **Online sessions** page and run one of two actions (anchored to `session_actions.go` and [Admin UI Manual · Online sessions](/overview/admin-manual#online-sessions)):

* **Change of Authorization (CoA-Request)**: currently carries only **`Session-Timeout` (#27)** and / or **`Filter-Id` (#11)** — it does **not** rewrite the Huawei rate VSAs.
* **Forced disconnect (Disconnect-Request)**: terminate the session directly.

> **The correct path to live FUP "speed change" on Huawei.** Because CoA does not rewrite `Huawei-Output-Average-Rate`, changing a user's speed live follows the same vendor-agnostic path as everywhere else: change the rate on the profile / user first, **then force a disconnect**; the client redials and is re-authorized at the new rate quartet. (Some Huawei firmware also reacts to a vendor-specific CoA rate attribute, but ToughRADIUS does **not** emit one — so do not rely on it.)

Operator-initiated CoA / Disconnect uses a short timeout with one automatic retry, targeting the **CoA port (default 3799)** on the NAS record.

### On the device side (Huawei VRP, reference example, verify on your firmware)

```
# The RADIUS template must accept dynamic authorization (CoA/DM).
radius-server template tr-tmpl
 radius-server authorization <TOUGHRADIUS_IP> shared-key cipher <SECRET>
```

* The firewall must allow **inbound UDP 3799** from ToughRADIUS to the BRAS.
* For the `Filter-Id` approach, pre-define a matching ACL / user-group of the same name on the BRAS (verify on your firmware).

### Verification

* Click **Change authorization** / **Forced disconnect** on the sessions page; the result is reported as a notification.
* **BRAS side**: after a forced disconnect, `display access-user username <name>` no longer lists the session, and the client redials automatically.

### Troubleshooting (symptom → locate → fix)

* **CoA / Disconnect does not respond or times out** → ① the RADIUS template has no `radius-server authorization`; ② the firewall blocks UDP 3799; ③ the NAS record's CoA port differs from the BRAS's authorization port; ④ the shared secret differs.
* **Changed the rate but the online user's speed did not change** → expected: CoA does not change the rate; **force a disconnect** so the client redials.
* **Port confusion** → the CoA `1700` you often see online is a client-side local port; this system and RFC 5176 use **3799**.

***

## Related chapters

* [Vendor Integration Guide · Huawei](/overview/vendor-guide) — attribute reference card.
* [Cookbook: MikroTik RouterOS](/overview/cookbook/cookbook-mikrotik) — the same scenarios on RouterOS.
* [Admin UI Manual](/overview/admin-manual) — users / rate profiles / online sessions / CoA forms.
* [FAQ](/overview/faq) — more cross-scenario troubleshooting Q\&A.


# H3C, ZTE, iKuai & Cisco

> 中文版本：[实战手册：H3C / 中兴 / 爱快 / Cisco](/overview-1/cookbook/cookbook-vendors)
>
> This chapter is part of the [Scenario Cookbook](/overview/cookbook) and follows its [reading conventions](/overview/cookbook#the-five-part-shape-of-every-scenario).

These four vendors run the **same operational scenarios** as the two flagship cookbooks — PPPoE / IPoE speed tiers, address pool, expiry disconnect, per-account concurrency, MAC binding, CoA / forced disconnect and FUP. The *mechanics* (how a tier, an expiry, a concurrency cap, or a CoA behaves) are **identical**, because they come from the shared `default_enhancer` and the checkers — not from the vendor.

So instead of repeating the full five-part playbook four times, this chapter is the **per-vendor diff**: for each device it states exactly **(1)** which rate attributes ToughRADIUS emits and the unit multiplier, **(2)** how the request MAC / VLAN is parsed (hence which bindings work), and **(3)** which flagship scenario to follow for the end-to-end steps.

> **Read this with a flagship chapter open.** For the full step-by-step of any scenario, follow the matching section in the [MikroTik](/overview/cookbook/cookbook-mikrotik) or [Huawei](/overview/cookbook/cookbook-huawei) cookbook; only the emitted attributes below differ.

## Which playbook applies

| You want to…                                      | Follow                                                                                                                                                                                                                                                                                  | What changes per vendor (this chapter)                         |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Speed tiers + address pool + expiry + concurrency | [MikroTik Scenario A](/overview/cookbook/cookbook-mikrotik#scenario-a-pppoe-broadband-isp--speed-tiers--address-pool--expiry-disconnect--concurrency) or [Huawei Scenario A](/overview/cookbook/cookbook-huawei#scenario-a-pppoe--ipoe-broadband--speed-tiers-peak-rate-and-aaa-domain) | the **rate attribute(s)** emitted + unit                       |
| Line anti-fraud (MAC / VLAN binding)              | [Huawei Scenario B](/overview/cookbook/cookbook-huawei#scenario-b-line-anti-fraud--mac--vlan-binding-and-dual-stack-ipv6)                                                                                                                                                               | **MAC parse format** + whether **VLAN binding** is available   |
| Live control (CoA / disconnect / FUP)             | [MikroTik Scenario C](/overview/cookbook/cookbook-mikrotik#scenario-c-live-control--coa-forced-disconnect-and-fup)                                                                                                                                                                      | nothing — CoA is vendor-agnostic (Session-Timeout + Filter-Id) |

> **The concurrency cap, expiry disconnect, address pool and CoA work the same for every vendor here** — they are enforced by the shared checkers / `default_enhancer`, not by a vendor VSA. The only real per-vendor variable is the rate-limit attribute and the MAC / VLAN parsing below.

***

## H3C — vendor code 25506

**Rate attributes** (anchored to `h3c_enhancer.go`) — the **same quartet shape as Huawei**:

| Attribute                 | Value                                        |
| ------------------------- | -------------------------------------------- |
| `H3C-Input-Average-Rate`  | `up_kbps × 1024` (bit/s) — subscriber upload |
| `H3C-Input-Peak-Rate`     | `up_kbps × 1024 × 4`                         |
| `H3C-Output-Average-Rate` | `down_kbps × 1024` (bit/s) — download        |
| `H3C-Output-Peak-Rate`    | `down_kbps × 1024 × 4`                       |

So a "30M down" tier (`30720` Kbps) is sent as `H3C-Output-Average-Rate = 31457280`, peak `125829120`; all values clamp to the Int32 max. Unlike Huawei, H3C emits **no** domain or IPv6 VSA — only the rate quartet.

**Request parsing** (`h3c_parser.go`): MAC comes from **`H3C-IP-Host-Addr`** (the last 17 characters, i.e. the trailing `aa:bb:cc:dd:ee:ff`) and falls back to `Calling-Station-Id` (`-`→`:`) when that VSA is absent; the inner / outer VLAN come from `NAS-Port-Id`. **Both MAC and VLAN binding are supported.**

**Device side (H3C Comware, reference, verify on your firmware):**

```
radius scheme tr_scheme
 primary authentication <TOUGHRADIUS_IP> 1812
 primary accounting <TOUGHRADIUS_IP> 1813
 key authentication simple <SECRET>
 key accounting simple <SECRET>
 user-name-format without-domain
#
domain default enable system
 authentication ppp radius-scheme tr_scheme
 accounting ppp radius-scheme tr_scheme
```

> **Trap**: register the NAS as *H3C* (not *Huawei*). Although the rate maths is identical, the VSA **vendor IDs differ** (25506 vs 2011); a Huawei-registered H3C device would receive Huawei VSAs it ignores, and the rate limit would not apply.

***

## ZTE — vendor code 3902

**Rate attributes** (anchored to `zte_enhancer.go`) — **two attributes, no peak**:

| Attribute                | Value                      |
| ------------------------ | -------------------------- |
| `ZTE-Rate-Ctrl-SCR-Up`   | `up_kbps × 1024` (bit/s)   |
| `ZTE-Rate-Ctrl-SCR-Down` | `down_kbps × 1024` (bit/s) |

A "30M down" tier is sent as `ZTE-Rate-Ctrl-SCR-Down = 31457280`. There is no peak / burst attribute — the average rate is the cap.

**Request parsing** (`zte_parser.go`): MAC comes from `Calling-Station-Id`, but ZTE sends it as a **bare 12-hex-digit string**; ToughRADIUS reformats it to `aa:bb:cc:dd:ee:ff`. VLAN is parsed from `NAS-Port-Id`. **Both MAC and VLAN binding are supported** — but for MAC binding, store the MAC in the `aa:bb:cc:dd:ee:ff` form (that is what the parser produces).

**Device side**: ZTE BRAS uses the same radius-template + domain pattern as Huawei — bind the authentication / accounting template to the ToughRADIUS address, shared secret and ports 1812 / 1813 (verify on your firmware).

***

## iKuai — vendor code 10055

A popular SMB / SOHO gateway in China.

**Rate attributes** (anchored to `ikuai_enhancer.go`) — **two attributes, a different multiplier**:

| Attribute                   | Value                             |
| --------------------------- | --------------------------------- |
| `RP-Upstream-Speed-Limit`   | `up_kbps × 8192` (= `× 1024 × 8`) |
| `RP-Downstream-Speed-Limit` | `down_kbps × 8192`                |

So a "30M down" tier (`30720` Kbps) is sent as `RP-Downstream-Speed-Limit = 251658240`.

> **Trap — high tiers clamp.** Because the multiplier is `× 8192` and the value is clamped to the **Int32 max (2147483647)**, any tier above roughly **256 Mbps** (`262144` Kbps) overflows and is **clamped** — a 300M tier would be sent at the clamped ceiling, not 300M. For very high tiers, apply the policy on the iKuai device instead.

**Request parsing**: iKuai uses the **default parser** — MAC from `Calling-Station-Id` (`-`→`:`), and **VLAN is not parsed** (always `0`). So **MAC binding works, but VLAN binding does not** (the VLAN check is always skipped).

**Device side**: on the iKuai web console, go to **认证计费 → RADIUS 计费**, set the server address, ports 1812 / 1813 and the shared secret, then enable RADIUS in the PPPoE server settings.

***

## Cisco — vendor code 9

**Rate attributes**: **none** — Cisco has no portable numeric rate VSA, so bandwidth stays device-side. The `accept-cisco` enhancer emits one non-rate attribute — `Cisco-AVPair="ip:addr-pool=<pool>"` — when the plan has an address pool, complementing the standard `Framed-Pool`; otherwise a Cisco NAS receives only the standard attributes (`Session-Timeout`, `Acct-Interim-Interval`, `Framed-Pool`, `Framed-IP-Address`, …) from `default_enhancer.go`. Authentication (PAP / CHAP / MS-CHAPv2 / EAP), session control, accounting and CoA all work normally — **apply bandwidth policy on the device**, or build a custom integration using the bundled `Cisco-AVPair` dictionary.

**Request parsing**: default parser — MAC from `Calling-Station-Id`, no VLAN. So **MAC binding works, VLAN binding does not**.

**Device side (Cisco IOS, reference, verify on your platform):**

```
aaa new-model
radius server TOUGHRADIUS
 address ipv4 <TOUGHRADIUS_IP> auth-port 1812 acct-port 1813
 key <SECRET>
aaa authentication ppp default group radius
aaa accounting network default start-stop group radius
aaa server radius dynamic-author
 client <TOUGHRADIUS_IP> server-key <SECRET>
```

> `aaa server radius dynamic-author` enables CoA / Disconnect (default port 3799). Do **not** expect a rate limit from RADIUS on Cisco — set it with a service-policy / QoS on the device.

***

## Vendor capability matrix

| Vendor        | Rate attribute(s)                       | Unit multiplier  | Peak      | MAC bind | VLAN bind |
| ------------- | --------------------------------------- | ---------------- | --------- | -------- | --------- |
| H3C (25506)   | `H3C-Input/Output-Average-Rate` + peaks | `× 1024` (bit/s) | ✅ avg × 4 | ✅        | ✅         |
| ZTE (3902)    | `ZTE-Rate-Ctrl-SCR-Up/Down`             | `× 1024` (bit/s) | ❌         | ✅        | ✅         |
| iKuai (10055) | `RP-Upstream/Downstream-Speed-Limit`    | `× 8192`         | ❌         | ✅        | ❌         |
| Cisco (9)     | none (device-side QoS)                  | —                | —         | ✅        | ❌         |

(For comparison: MikroTik emits the `Mikrotik-Rate-Limit` string in Kbps; Huawei emits the rate quartet `× 1024` with peak `× 4` plus domain / IPv6.)

## Troubleshooting (symptom → locate → fix)

* **Rate limit has no effect** → ① the NAS is not registered with this exact vendor (the VSA vendor ID must match — H3C ≠ Huawei even though the maths is the same); ② Cisco has **no** RADIUS rate VSA by design — set QoS on the device; ③ wrong unit on the device side (all values here are bit/s, except the iKuai `× 8192`).
* **iKuai high-tier speed is wrong / capped low** → the `× 8192` value overflowed Int32 and was clamped; apply high tiers on the device.
* **VLAN binding never triggers on iKuai / Cisco** → expected: their parser does not extract VLANs (always `0`), so the VLAN check is always skipped. Use MAC binding instead, or a vendor that parses VLAN (Huawei / H3C / ZTE).
* **ZTE MAC binding never matches** → store the MAC as `aa:bb:cc:dd:ee:ff` (ZTE sends 12 bare hex digits; ToughRADIUS reformats to colon form before comparing).

***

## Related chapters

* [Cookbook: MikroTik RouterOS](/overview/cookbook/cookbook-mikrotik) — full PPPoE / Hotspot / CoA playbook.
* [Cookbook: Huawei BRAS / NetEngine](/overview/cookbook/cookbook-huawei) — full speed-tier / line-binding / CoA playbook.
* [Vendor Integration Guide](/overview/vendor-guide) — the attribute reference card for every vendor.
* [FAQ](/overview/faq) — more cross-scenario troubleshooting Q\&A.


# Admin UI Manual

> 中文版本：[管理系统用户手册](/overview-1/admin-manual)

The management console runs on port `1816` (HTTP) and is built with React Admin. It is bilingual (中文 default, English via the app-bar language menu) and supports light/dark themes. This chapter walks through every page.

## Logging in and accounts

Sign in at `http://<server>:1816` with an operator account. The initial administrator is `admin` plus a one-time password in `{workdir}/private/admin-bootstrap-password` (or `TOUGHRADIUS_ADMIN_PASSWORD`). Change it immediately under **Account Settings** (top-right avatar), which also edits your profile info. Passwords must be at least 6 characters and must not be the historical default.

Operator roles (set under **Operators**):

| Level      | Meaning                                                        |
| ---------- | -------------------------------------------------------------- |
| `super`    | Full access, including System Config and Operators             |
| `admin`    | Same menu visibility as super                                  |
| `operator` | Day-to-day pages only — System Config and Operators are hidden |

## Dashboard

The landing page summarizes the deployment:

* **Stat cards** — total users (with disabled/expired counts), online users, today's authentication and accounting request counts, today's upload/download traffic in GB.
* **IPv6 strip** — online sessions with IPv6, IPv6 address/prefix/delegated prefix counts, users with static IPv6 configuration.
* **Charts** — authentication trend, profile distribution pie, and a 24-hour upload/download traffic chart.

Counters are driven by in-memory metrics; see the [Operations Guide](/overview/ops-guide#metrics).

## Network Nodes

Logical groups (name, tags, remark) used to organize NAS devices. Create nodes first — each NAS belongs to one.

## NAS Devices

Registry of network devices allowed to talk RADIUS to this server. Requests from unknown source addresses are dropped.

| Field                         | Notes                                                                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name / Identifier             | Free-form name; `identifier` matches the RADIUS `NAS-Identifier`                                                                                        |
| IP address / Hostname         | The source address of RADIUS packets                                                                                                                    |
| Vendor                        | **Decides vendor attribute behavior** — Standard, Cisco, Huawei, MikroTik, H3C, ZTE, iKuai (see the [Vendor Integration Guide](/overview/vendor-guide)) |
| Secret                        | RADIUS shared secret                                                                                                                                    |
| CoA port                      | Where CoA/Disconnect are sent; default `3799`                                                                                                           |
| Node / Status / Tags / Remark | Organization and lifecycle                                                                                                                              |

## RADIUS Users

Subscriber accounts. List supports filtering by username, real name, email, mobile, and IP, plus CSV export and column sorting.

Key form fields: username/password, status (enabled/disabled), **billing profile** (required — rates, concurrency, pools, and Class inherit from it), **expire time** (drives `Session-Timeout`), static `ip_addr` / `ipv6_addr`, IPv6 prefix pools and delegated prefixes (edit view), `radius_class` (RFC 2865 Class, emitted as-is), contact info, remark.

**Batch import**: the list toolbar's import button accepts `.xlsx`, `.csv`, or `.json` files and reports created/failed counts. Export the current list as a template reference.

The show view groups everything including IPv6 details, MAC/VLAN binding values, and timestamps.

## Billing Profiles

Reusable authorization templates:

| Field                    | Meaning                                                                                              |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `active_num`             | Max concurrent sessions per user (0 = unlimited)                                                     |
| `up_rate` / `down_rate`  | Bandwidth in **Kbps**; converted per vendor (list shows Mbps for values ≥ 1024)                      |
| `addr_pool`              | `Framed-Pool` name the NAS allocates from                                                            |
| `radius_class`           | RFC 2865 Class (Type 25), written into Access-Accept as-is; for ocserv groups use `OU=group1;group2` |
| `ipv6_prefix` / domain   | IPv6 and Huawei domain authorization                                                                 |
| `bind_mac` / `bind_vlan` | Lock users to first-seen MAC / VLANs                                                                 |

## Online Sessions

Live sessions (`radius_online`). Columns include session ID, framed IP, NAS address/port, start time, duration, timeout, and traffic counters. Filters cover username, session ID, IPv4/IPv6 addresses and prefixes, NAS address, MAC, and start-time ranges.

Per-row actions (this is where RFC 5176 dynamic authorization lives):

* **修改授权 / CoA** — send a `CoA-Request` with a new session timeout and/or `Filter-Id`.
* **强制下线 / Disconnect** — send a `Disconnect-Request` to terminate the session (with confirmation).

Both target the NAS CoA port and report success/failure as a notification.

## Accounting

Historical accounting records (`radius_accounting`), read-only: session ID, addresses, start/stop time, total input/output traffic. Same filter set as Online Sessions plus stop-time data; CSV export supported.

## System Config

Visible to `super`/`admin` only. Settings are schema-driven, grouped into accordions (RADIUS group expanded by default), and live in the `sys_config` table — changes apply without restart. The 13 RADIUS settings:

| Key                        | Default   | Purpose                                                                                                                                                          |
| -------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EapMethod`                | `eap-md5` | Active EAP method: `eap-md5`, `eap-mschapv2`, `eap-tls`, `eap-peap`, `eap-ttls`                                                                                  |
| `EapEnabledHandlers`       | `*`       | Comma allow-list of permitted EAP handlers                                                                                                                       |
| `EapTlsServerCert`         | empty     | Name of the managed server certificate (import and select it on the Certificates page) presented for EAP-TLS/PEAP/TTLS; **empty disables certificate-based EAP** |
| `EapTlsClientCa`           | empty     | Name of the managed CA certificate used to validate client certificate chains (EAP-TLS only)                                                                     |
| `EapTlsMinVersion`         | `1.2`     | Minimum TLS version (`1.2`/`1.3`)                                                                                                                                |
| `EapTlsCipherProfile`      | `modern`  | TLS 1.2 cipher policy: `modern` (ECDHE+AEAD), `legacy-rsa-cbc` (opt-in RSA+AES-CBC for hostapd internal TLS; needs an RSA server cert), or `custom`              |
| `EapTlsCipherSuites`       | empty     | Custom TLS 1.2 suite list when the profile is `custom`                                                                                                           |
| `IgnorePassword`           | `false`   | Skip password verification (testing only)                                                                                                                        |
| `AccountingHistoryDays`    | `90`      | Accounting retention days (`@daily` cleanup; `0` disables)                                                                                                       |
| `AcctInterimInterval`      | `300`     | Seconds between NAS interim updates                                                                                                                              |
| `SessionTimeout`           | `3600`    | Default session timeout seconds                                                                                                                                  |
| `LogLevel`                 | `info`    | RADIUS log verbosity (`debug`/`info`/`warn`/`error`)                                                                                                             |
| `RejectDelayMaxRejects`    | `7`       | Consecutive rejects before delaying responses                                                                                                                    |
| `RejectDelayWindowSeconds` | `10`      | Window for the reject counter                                                                                                                                    |

Toolbar: **Save**, **Refresh**, **Reset** (restore defaults, confirmed), and **Backup / Restore** — export or re-import a JSON snapshot of nodes, NAS, profiles, users, settings, and operators (see the [Operations Guide](/overview/ops-guide#backup-and-restore) for what it does *not* include).

## Operators

Management of console accounts (`super`/`admin` only): username, password, contact info, level, status. The operator action log is kept in the database (`sys_opr_log`) and purged automatically after one year.

## UI conveniences

* **Language switcher** (app bar) — 简体中文 / English, persisted per browser.
* **Theme toggle** — light/dark.
* **CSV export** on users, sessions, accounting, NAS, nodes, operators.
* Server-side pagination and active-filter chips on all list pages.


# Operations Guide

> 中文版本：[运维指南](/overview-1/ops-guide)

Everything you need to run ToughRADIUS in production: configuration reference, environment variables, TLS/EAP certificates, storage, monitoring, backup, and the bundled command-line tools.

## Process model

One static binary runs several services concurrently (web/admin API, RADIUS auth, RADIUS accounting, RadSec). **If any service fails, the whole process exits** so a supervisor can restart it — run it under systemd, Docker, or an equivalent.

```ini
# /etc/systemd/system/toughradius.service (reference)
[Unit]
Description=ToughRADIUS server
After=network-online.target

[Service]
ExecStart=/usr/local/bin/toughradius -c /etc/toughradius.yml
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

## Ports

| Port | Protocol       | Service                                                  | Config key                         |
| ---- | -------------- | -------------------------------------------------------- | ---------------------------------- |
| 1816 | TCP HTTP       | Admin UI + REST API                                      | `web.port`                         |
| 1817 | TCP HTTPS      | Admin UI over TLS (optional; start failure is non-fatal) | `web.tls_enabled` / `web.tls_port` |
| 1812 | UDP            | RADIUS authentication                                    | `radiusd.auth_port`                |
| 1813 | UDP            | RADIUS accounting                                        | `radiusd.acct_port`                |
| 2083 | TCP TLS        | RadSec (RFC 6614)                                        | `radiusd.radsec_port`              |
| 3799 | UDP (outbound) | CoA/Disconnect **to** the NAS                            | per-NAS *CoA port* field           |

## Configuration

Lookup order: `-c <file>` → `./toughradius.yml` → `/etc/toughradius.yml` → embedded defaults. Inspect the merged result with `toughradius -printcfg`.

```yaml
system:
  appid: ToughRADIUS
  location: Asia/Shanghai        # cron/timestamp timezone
  workdir: /var/toughradius      # default in production builds
  debug: false
web:
  host: 0.0.0.0
  port: 1816
  tls_enabled: true              # Enabled by default for compatibility; set false to disable the built-in HTTPS listener
  tls_port: 1817
  secret: <random-string>        # JWT signing secret — change it
database:
  type: sqlite                   # sqlite | postgres
  host: 127.0.0.1                # postgres only
  port: 5432
  name: toughradius.db           # sqlite filename (under {workdir}/data/) or pg database
  user: postgres
  passwd: <password>
  max_conn: 100
  idle_conn: 10
  debug: false
radiusd:
  enabled: true
  host: 0.0.0.0
  auth_port: 1812
  acct_port: 1813
  radsec_port: 2083
  radsec_worker: 100
  radsec_ca_cert: private/ca.crt        # relative paths resolve against workdir
  radsec_cert: private/radsec.tls.crt
  radsec_key: private/radsec.tls.key
  debug: false                   # true = full packet dumps
logger:
  mode: production               # development | production
  file_enable: true
  filename: /var/toughradius/toughradius.log
```

### Working directory layout

On startup ToughRADIUS creates under `system.workdir`:

```
/var/toughradius/
├── data/        # SQLite database, metrics data
├── logs/
├── private/     # TLS material (mode 0700)
├── public/
└── backup/      # server-side copies of configuration backups
```

### Environment variables

Environment variables override the YAML file:

| Variable                                                                                                                             | Overrides                                                                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `TOUGHRADIUS_SYSTEM_WORKER_DIR`                                                                                                      | `system.workdir`                                                                                                          |
| `TOUGHRADIUS_SYSTEM_DEBUG`                                                                                                           | `system.debug`                                                                                                            |
| `TOUGHRADIUS_WEB_HOST` / `_WEB_PORT` / `_WEB_TLS_ENABLED` / `_WEB_TLS_PORT` / `_WEB_SECRET`                                          | `web.*`                                                                                                                   |
| `TOUGHRADIUS_DB_TYPE` / `_DB_HOST` / `_DB_PORT` / `_DB_NAME` / `_DB_USER` / `_DB_PWD` / `_DB_DEBUG`                                  | `database.*`                                                                                                              |
| `TOUGHRADIUS_RADIUS_ENABLED` / `_RADIUS_HOST` / `_RADIUS_AUTHPORT` / `_RADIUS_ACCTPORT` / `_RADIUS_DEBUG`                            | `radiusd.*`                                                                                                               |
| `TOUGHRADIUS_RADIUS_RADSEC_PORT` / `_RADIUS_RADSEC_WORKER` / `_RADIUS_RADSEC_CA_CERT` / `_RADIUS_RADSEC_CERT` / `_RADIUS_RADSEC_KEY` | RadSec settings                                                                                                           |
| `TOUGHRADIUS_LOGGER_MODE` / `_LOGGER_FILE_ENABLE`                                                                                    | `logger.*`                                                                                                                |
| `TOUGHRADIUS_RADIUS_POOL`                                                                                                            | RADIUS worker pool size (default 1024)                                                                                    |
| `TOUGHRADIUS_ADMIN_PASSWORD`                                                                                                         | Optional first-start / rotation password for the bootstrap `admin` account. Ignored if it matches the historical default. |

### CLI flags

| Flag        | Effect                                      |
| ----------- | ------------------------------------------- |
| `-c <file>` | Configuration file path                     |
| `-initdb`   | **Drop and recreate all tables**, then exit |
| `-printcfg` | Print merged configuration as JSON, exit    |
| `-v`        | Print version / build time / commit, exit   |
| `-h`        | Usage                                       |

Runtime RADIUS settings (EAP method, certificates, intervals, reject-delay…) live in the database and are edited in **System Config** — no restart needed. See the [Admin UI Manual](/overview/admin-manual#system-config).

## Database

* **SQLite** (default) — pure-Go driver, zero CGO, file at `{workdir}/data/<name>`. Fine for small/medium deployments; back up the file.
* **PostgreSQL** — set `database.type: postgres` plus host/user/password. Recommended for production scale and concurrent accounting load.

Schema migration (GORM `AutoMigrate`) runs automatically at every startup, so upgrades are: stop, replace the binary, start. `-initdb` is for first installation only — it **destroys all data**.

> **Upgrade note — EAP certificate file paths removed.** The legacy file-path settings `EapTlsCertFile` / `EapTlsKeyFile` / `EapTlsCaFile` have been **removed**: the managed certificate store (`sys_cert`) is now the only source of EAP-TLS/PEAP/TTLS certificate material. If your deployment configured certificate-based EAP via disk file paths, after upgrading you must import the PEM files on the **Certificates** page (the server certificate must include its private key) and select them by name in **System Config → `EapTlsServerCert` / `EapTlsClientCa`**. Until a managed server certificate is selected, certificate-based EAP methods safely reject all requests (`ErrTLSNotConfigured`) — plan the re-import before upgrading to avoid an EAP outage. PAP/CHAP/MSCHAPv2 (non-EAP) deployments are unaffected.

Large tables to watch: `radius_accounting` (grows with every session) and `radius_online`. The `radius.AccountingHistoryDays` setting (default 90, set to `0` to disable) defines the accounting retention window: a `@daily` job deletes **terminated** `radius_accounting` rows older than that many days (active sessions are untouched) and clears dangling `radius_online` rows that have missed several interim updates. The operator action log (`sys_opr_log`) is purged automatically after one year. For very high volumes, still consider database-level archiving as part of your own ops.

## TLS and certificates

Three independent certificate consumers:

| Consumer                | Files                                                                                                                                                                  | Notes                                                                                                                                                                            |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **RadSec**              | `radiusd.radsec_ca_cert` / `radsec_cert` / `radsec_key`                                                                                                                | TLS 1.2+; client certificates are verified **if presented** (`VerifyClientCertIfGiven`)                                                                                          |
| **Web HTTPS**           | `{workdir}/private/toughradius.tls.crt` + `.key` (fixed paths)                                                                                                         | Listens on `web.tls_port` when `web.tls_enabled` is true; failure to load is logged, HTTP keeps running                                                                          |
| **EAP (TLS/PEAP/TTLS)** | System Config → `EapTlsServerCert`, `EapTlsClientCa`, `EapTlsMinVersion`, `EapTlsCipherProfile` (import certificates on the Certificates page and select them by name) | No server certificate selected disables certificate-based EAP methods; default cipher profile is `modern`; CBC-only clients need opt-in `legacy-rsa-cbc` plus an RSA certificate |

Generate a complete CA/server/client set with the bundled tool:

```bash
go run ./cmd/certgen -type all -output /var/toughradius/private \
  -server-cn radius.example.com -server-dns radius.example.com \
  -days 3650
# then point radsec_cert/radsec_key (or the EAP settings) at the files
```

## Logging

zap structured logging. `logger.mode: development` = human-readable console; `production` = JSON. File output controlled by `logger.file_enable` + `logger.filename`. RADIUS verbosity is additionally tunable at runtime via **System Config → LogLevel**; `radiusd.debug: true` dumps full packets (keep off in production).

## Metrics

Counters are kept in memory and surfaced through the admin dashboard (there is no Prometheus `/metrics` HTTP endpoint). RADIUS counters include: `radus_accept`, `radus_online`/`radus_offline`, `radus_accounting`, `radus_auth_drop` / `radus_acct_drop`, `radus_radsec_saturated`, and per-cause reject counters — `radus_reject_passwd_error`, `radus_reject_not_exists`, `radus_reject_expire`, `radus_reject_disabled`, `radus_reject_limit`, `radus_reject_bind_error`, `radus_reject_ldap_error`, `radus_reject_unauthorized`, `radus_reject_other`. `radus_reject_ldap_error` means the LDAP/AD backend could not give an authentication answer — for example the directory is unreachable, TLS/StartTLS failed, the service account bind failed, or the LDAP configuration is wrong; wrong passwords are still counted under `radus_reject_passwd_error`. Accounting-Requests dropped at ingress are classified by reason — `radus_acct_drop_nas` (unknown or unauthorized NAS), `radus_acct_drop_username` (missing username), and `radus_acct_drop_secret` (bad Request Authenticator) — while `radus_acct_drop` remains the catch-all for back-pressure and response-write drops. System gauges (CPU/memory, process CPU/memory) are sampled every 30 s.

For external monitoring, probe the service ports and watch the log file; treat process exit as the failure signal (the process model is fail-fast).

## Backup and restore

**System Config → Backup** downloads a JSON snapshot (schema version 9.0) of: nodes, NAS devices, profiles, users, system settings, operators, and managed certificates (including their private keys, so certificate-based EAP keeps working after a restore). A copy is also written to `{workdir}/backup/`. **Restore** re-imports such a file.

> The snapshot does **not** include accounting history or online sessions. For a full disaster-recovery story, also back up the database itself (copy the SQLite file or use `pg_dump`).

> **Security**: the snapshot contains secrets — RADIUS user passwords, operator password hashes, and the PEM private keys of managed EAP certificates. Store and transfer backup files as you would any credential.

## Command-line tools

All live under `cmd/` and run with `go run ./cmd/<tool>`:

| Tool             | Purpose                                                                                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `radtest`        | Mini RADIUS client: `auth`, `acct`, `flow` (auth + start + stop). Flags: `-server`, `-secret`, `-username`, `-password`, `-calling-station`, `-framed-ip`, `-session-id` |
| `certgen`        | Generate CA / server / client certificates (see above)                                                                                                                   |
| `benchmark`      | Load tester: total requests `-n`, concurrency `-c`, auth/acct modes, CSV stats output                                                                                    |
| `reset-password` | Reset a console operator password: `go run ./cmd/reset-password -c <cfg> -u admin -p <new>`                                                                              |
| `demo-seed`      | Populate demo nodes/NAS/profiles/users/sessions for evaluation                                                                                                           |
| `config-tool`    | Validate / summarize the settings schema JSON                                                                                                                            |

## Production hardening checklist

* [ ] Change `web.secret`. Confirm the bootstrap `admin` password is unique (`{workdir}/private/admin-bootstrap-password` or `TOUGHRADIUS_ADMIN_PASSWORD`); never reuse the historical default.
* [ ] `radiusd.debug: false`, `logger.mode: production`.
* [ ] Restrict UDP 1812/1813 and TCP 1816 to trusted networks (firewall).
* [ ] Use RadSec (2083) or a trusted L2/VPN path for RADIUS across untrusted networks.
* [ ] Unique, strong shared secret per NAS.
* [ ] EAP: prefer `eap-tls`/`eap-peap`/`eap-ttls` with real certificates; mind the MS-CHAPv2 caveats in the [Security Policy](/overview/security-policy).
* [ ] Database backups scheduled (config snapshot + DB dump).
* [ ] Supervisor with restart policy; alert on process exit.


# LDAP / AD Authentication

> 中文版本：[LDAP / AD 认证后端](/overview-1/auth-ldap)

ToughRADIUS can verify a user's password against an external LDAP directory or Microsoft Active Directory by performing an LDAP **bind**, instead of (or in addition to) the password stored in its own database. This lets you reuse an existing corporate directory without copying or migrating passwords.

> **PAP-family only.** The LDAP backend authenticates by binding with the cleartext password, so it serves only **bare PAP** and **EAP-TTLS inner PAP** (RFC 5281). Challenge/response methods — CHAP, MS-CHAP, MS-CHAPv2, EAP-MD5 and PEAP-MSCHAPv2 — **cannot** be served, because the server never holds the cleartext password it would need to recompute their responses. When LDAP is enabled those methods are deliberately rejected with a diagnostic reason; see [How authentication flows](#how-authentication-flows) below.

## When to use it (and when not to)

Use the LDAP backend when you must serve users whose passwords already live in a directory — an OpenLDAP server, FreeIPA, or Active Directory — and you do not want to provision a certificate per client. It is the pragmatic bridge for mixed and legacy estates: protect the tunnel with a server certificate, then send the username and password inside it.

Be honest about the trade-off. **EAP-TTLS/PAP transmits the cleartext password inside the TLS tunnel** — it is protected *only* by that tunnel. The security of the deployment therefore rests on a strong, properly verified server certificate and TLS 1.2+. If every client can present a certificate, prefer EAP-TLS. If you need directory-backed passwords for legacy clients, LDAP + EAP-TTLS/PAP is the right tool — just size your expectations accordingly.

## Deployment model

The LDAP backend **replaces only the password check**. It does **not** create accounts or carry authorization data. For every user you still need a local `RadiusUser` row, because authorization (profile/plan, rate limits, expiry, concurrent-session limit, address pool, VLAN, MAC binding) is loaded from the local database *before* authentication runs.

In other words:

* **Authentication** (is the password correct?) → the LDAP directory, by bind.
* **Authorization** (what is this user allowed to do?) → the local `RadiusUser` row, exactly as without LDAP.

A global `ldap.Enabled` switch turns the backend on or off for the whole server; there is no per-user "authentication source" field. MAC-address authentication always bypasses LDAP.

## Configuration

Configure the backend on the **System Configuration** page of the admin UI, under the **LDAP** group. All items are also editable through the settings API. The backend re-reads its configuration on every authentication attempt, so changes take effect immediately without a restart.

| Key                       | Type    | Default    | Applies to | Description                                                                                                                     |
| ------------------------- | ------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `ldap.Enabled`            | bool    | `false`    | both       | Turn the LDAP backend on. Off by default.                                                                                       |
| `ldap.ServerURL`          | string  | *(empty)*  | both       | Directory URL, e.g. `ldap://dc.example.com:389` or `ldaps://dc.example.com:636`.                                                |
| `ldap.BindMode`           | enum    | `template` | both       | `template` or `search` (see below).                                                                                             |
| `ldap.BindDNTemplate`     | string  | *(empty)*  | template   | DN template with a single `%s` for the username, e.g. `uid=%s,ou=people,dc=example,dc=com` or the AD UPN form `%s@example.com`. |
| `ldap.BaseDN`             | string  | *(empty)*  | search     | Subtree base for the user lookup, e.g. `dc=example,dc=com`.                                                                     |
| `ldap.UserFilter`         | string  | `(uid=%s)` | search     | Filter with a single `%s` for the username (escaped before substitution), e.g. `(uid=%s)` or AD `(sAMAccountName=%s)`.          |
| `ldap.SearchBindDN`       | string  | *(empty)*  | search     | DN of the read-only service account used to find users, e.g. `cn=svc-radius,ou=svc,dc=example,dc=com`.                          |
| `ldap.SearchBindPassword` | string  | *(empty)*  | search     | Password for the service-account DN.                                                                                            |
| `ldap.StartTLS`           | bool    | `false`    | both       | Upgrade an `ldap://` connection to TLS with StartTLS (RFC 4513 §3) before binding. Leave off for `ldaps://`.                    |
| `ldap.TLSSkipVerify`      | bool    | `false`    | both       | Skip TLS certificate verification. **Insecure — lab / self-signed only.**                                                       |
| `ldap.Timeout`            | int (s) | `5`        | both       | Dial and per-operation timeout in seconds (1–60).                                                                               |

### Bind modes

**Template mode** (`ldap.BindMode = template`) is the simplest: the username is substituted into `BindDNTemplate` to form a DN, and the server binds directly as that DN with the supplied password. Use it when every user's DN follows a fixed pattern.

```
BindMode       = template
BindDNTemplate = uid=%s,ou=people,dc=example,dc=com
# Active Directory alternative:
# BindDNTemplate = %s@example.com
```

**Search mode** (`ldap.BindMode = search`) handles directories where the DN is not predictable. The server first binds as a read-only **service account** (`SearchBindDN` / `SearchBindPassword`), searches under `BaseDN` with `UserFilter` to locate the user's DN, and then **re-binds as the user** with the supplied password to verify it.

```
BindMode           = search
ServerURL          = ldaps://dc.example.com:636
BaseDN             = dc=example,dc=com
UserFilter         = (sAMAccountName=%s)      # Active Directory
SearchBindDN       = cn=svc-radius,ou=svc,dc=example,dc=com
SearchBindPassword = ********
```

### Transport security

* Use an `ldaps://` URL for implicit TLS (port 636), **or** an `ldap://` URL together with `StartTLS = true` to upgrade the plaintext connection (RFC 4513 §3). Do not enable StartTLS on an `ldaps://` URL.
* Leave `TLSSkipVerify = false` in production. Enable it only against a lab or self-signed directory — it disables certificate verification and exposes the bind to interception.

## How authentication flows

| Method                     | Behaviour when `ldap.Enabled = true`                                        |
| -------------------------- | --------------------------------------------------------------------------- |
| Bare PAP                   | Authenticated by LDAP bind.                                                 |
| EAP-TTLS / inner PAP       | Authenticated by LDAP bind; MS-MPPE keys are still derived for the tunnel.  |
| CHAP / MS-CHAP / MS-CHAPv2 | **Rejected** with a diagnostic reason (no cleartext password to bind with). |
| EAP-MD5 / PEAP-MSCHAPv2    | **Rejected** for the same reason.                                           |
| MAC authentication         | Bypasses LDAP entirely.                                                     |

The rejection of challenge/response methods is deliberate and centralized at the password-retrieval boundary, not forked at the protocol ingress. When LDAP is active the local `RadiusUser.Password` is usually empty, and a challenge/response method that derived its expected value from an empty secret could otherwise **falsely accept any user** — so those methods are failed closed instead.

## Security model

The backend is conservative by design:

* **Empty username or password is rejected before any network operation.** A bind with a DN and an empty password is treated as an *anonymous* bind by many servers and would otherwise succeed (RFC 4513 §5.1.2), so it is refused up front.
* **Injection is neutralized.** In search mode the username is escaped with LDAP filter escaping (RFC 4515 §3); in template mode it is DN-escaped before substitution.
* **An ambiguous search is refused.** If `UserFilter` matches more than one entry, the attempt is rejected rather than guessing.
* **A directory outage is never reported as a wrong password.** An invalid- credentials result (LDAP code 49) maps to a password rejection; every other failure — unreachable directory, misconfiguration, service-account bind failure — maps to a *backend-unavailable* outcome. This distinction also drives the metrics below.

## Observability

Rejections are counted by reason (see the [Operations Guide](/overview/ops-guide) for the metrics endpoint):

* `radus_reject_passwd_error` — the password was wrong (bind returned invalid-credentials).
* `radus_reject_ldap_error` — the backend could not give an answer (directory unreachable, StartTLS failure, service-account bind failure, misconfiguration).

Keeping these separate means a directory outage shows up as `radus_reject_ldap_error`, not as a spike of "wrong password" — so an alert on `radus_reject_ldap_error` cleanly signals a directory problem rather than user error.

> **Note.** `radus_accept` increments once per successful authentication — both the bare PAP/CHAP and the EAP flows converge on a single accept chokepoint — so it pairs with the reject counters above for a success-rate or SLO view. A directory outage still surfaces only as `radus_reject_ldap_error`, never as a false `radus_accept`, keeping the success and failure signals separate.

## Troubleshooting

| Symptom                                               | Likely cause                                                                                                                               |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Every login fails, `radus_reject_ldap_error` climbing | `ServerURL` wrong/unreachable, TLS handshake failing, or (search mode) the service account cannot bind.                                    |
| Windows/AD clients rejected, PAP users fine           | The clients are using PEAP-MSCHAPv2 or MS-CHAPv2, which LDAP cannot serve — switch them to EAP-TTLS/PAP or use the local password backend. |
| Search mode finds no user                             | Check `BaseDN` and `UserFilter`; confirm the service account can read user entries.                                                        |
| Bind works in the lab but fails in production         | `TLSSkipVerify` was masking an invalid certificate — install a trusted certificate and turn it off.                                        |
| Passwords accepted but the session has no plan/limits | The local `RadiusUser` row is missing — LDAP only checks the password; create the local account for authorization.                         |

## See also

* [Operations Guide](/overview/ops-guide) — EAP/TLS certificates, metrics, process model.
* [Concepts & Terminology](/overview/concepts) — PAP, EAP, EAP-TTLS.
* [Protocol & RFC Reference](/overview/rfc-index) — RFC 5281 (EAP-TTLS), RFC 4511/4513 (LDAP).


# FAQ

> 中文版本：[常见问题解答](/overview-1/faq)

Frequently asked questions, grouped by theme. If your question is not covered, search the [GitHub issues](https://github.com/talkincode/toughradius/issues) or open a new one.

## Installation & access

### I forgot the admin password — how do I reset it?

Use the bundled tool against the same configuration file the server runs with:

```bash
go run ./cmd/reset-password -c /etc/toughradius.yml -u admin -p <new-password>
```

The first-start account is `admin` with a one-time generated password written to `{workdir}/private/admin-bootstrap-password` (or `TOUGHRADIUS_ADMIN_PASSWORD` if you set it before the first start). The historical `toughradius` password is no longer created or accepted.

### Which database should I choose, SQLite or PostgreSQL?

SQLite (the default) requires nothing extra — pure-Go driver, single file under `{workdir}/data/` — and suits labs and small deployments. Choose PostgreSQL for production scale, high accounting volume, or when you need external backup tooling (`pg_dump`, replication).

### Can I run it on a port other than 1812/1813/1816?

Yes — every port is configurable (`radiusd.auth_port`, `radiusd.acct_port`, `web.port`, …) via YAML or environment variables. See the [Operations Guide](/overview/ops-guide#ports).

### The HTTPS admin port (1817) doesn't work

The web TLS listener needs `{workdir}/private/toughradius.tls.crt` and `.key`. If they are missing or invalid the failure is logged and **only** the HTTPS listener stops — plain HTTP on 1816 keeps serving. Generate certificates with `cmd/certgen` or provide your own. If you do not need the built-in HTTPS listener, set `web.tls_enabled: false` or `TOUGHRADIUS_WEB_TLS_ENABLED=false`.

### Is `-initdb` safe to run again?

**No.** It drops and recreates every table. Run it only at first installation. Regular upgrades need no manual schema step — migration runs automatically at startup.

## Authentication

### All requests are ignored / time out

The two most common causes:

1. The request's **source IP is not registered** as a NAS device — add it under **NAS Devices** (or fix NAT so the expected address is seen).
2. **Shared secret mismatch** — RADIUS silently discards packets that fail authentication of the packet itself.

Turn on `radiusd.debug: true` or set LogLevel to `debug` to see what arrives.

### Users authenticate but get no bandwidth limit

Rate attributes are sent only when the NAS record's **vendor** is one that has a rate VSA (Huawei, MikroTik, H3C, ZTE, iKuai). A NAS registered as `Standard` or `Cisco` receives no proprietary rate attribute — see the [Vendor Integration Guide](/overview/vendor-guide#rate-limit-units). Also check that the user's profile actually sets `up_rate`/`down_rate` (Kbps).

### ocserv users cannot join groups / Class is missing

ocserv uses RFC 2865 Class (Type 25) as its group channel. Set `radius_class` on the user or billing profile; a successful authentication writes that string **as-is** into Access-Accept. An empty value omits the attribute.

ocserv requires:

* `auth = "radius[config=...,groupconfig=true]"`
* Class format `OU=group1;group2` (semicolon-separated; the server does **not** add an `OU=` prefix)
* Those groups must also appear in ocserv `select-group`

`Domain` is the Huawei domain attribute, not Class.

### Dial-up authenticates but gets no IP / the address pool has no effect

`Framed-Pool` is sent only when a pool is **set on the user or its rate profile**, and the emitted pool name **must match the pool name on the NAS** (e.g. RouterOS `/ip pool`). A name mismatch — or no pool at all — leaves the client without a (correct) address. For a fixed IP, set a static IPv4 on the user (emits `Framed-IP-Address`, overriding the pool). See the end-to-end [Scenario Cookbook · MikroTik](/overview/cookbook/cookbook-mikrotik).

### I have multiple NAS devices — must each be configured separately?

Yes. **Every NAS must be registered individually under NAS Devices**, each with its own source IP (or identifier) and **its own shared secret**. ToughRADIUS matches the packet's source address (or NAS identifier) to the NAS record and its secret; requests from an unregistered source are logged and rejected as an unauthorized NAS. Different NAS devices may use different secrets — they need not be uniform.

### Why was a specific user rejected?

Rejects are categorized (wrong password, user not found, expired, disabled, session limit, MAC/VLAN binding mismatch, unauthorized NAS…) — the dashboard shows per-cause counters and the log records the detail. The most surprising one is the **binding mismatch**: with `bind_mac`/`bind_vlan` enabled, the first seen MAC/VLAN is stored and later requests must match; clear the stored value on the user after replacing hardware.

### Repeated wrong passwords respond slowly — why?

That is the reject-delay brute-force guard: after `RejectDelayMaxRejects` (default 7) consecutive rejects within `RejectDelayWindowSeconds` (default 10 s), responses are delayed. Tune both in **System Config**.

### Does ToughRADIUS support 802.1X / Wi-Fi Enterprise?

Yes. Supported EAP methods: EAP-MD5, EAP-MSCHAPv2, EAP-TLS, PEAPv0/EAP-MSCHAPv2 and EAP-TTLS (inner PAP / MS-CHAP-V2). Select the method in **System Config → EapMethod**.

### EAP-TLS / PEAP / EAP-TTLS doesn't start

TLS-based EAP requires selecting `EapTlsServerCert` in System Config — when it is empty the methods are disabled by design. Generate a server certificate with `cmd/certgen`, import it on the Certificates page, and select it by name. `EapTlsClientCa` is needed only to validate client certificates (EAP-TLS).

### PEAP / eapol\_test fails with `no cipher suite supported`

The default outer TLS list is ECDHE+AEAD (`EapTlsCipherProfile=modern`). `eapol_test` built with hostapd **internal TLS** only offers RSA/DHE+CBC, so there is no overlap. Either:

1. Rebuild `eapol_test` with `CONFIG_TLS=openssl` (preferred; keep modern).
2. Set `EapTlsCipherProfile=legacy-rsa-cbc` and present an **RSA** server certificate. That profile has no forward secrecy, and Go does not implement DHE; do not use it as the default production policy.

### What breaks if the server and device clocks drift apart?

Clock skew causes subtle problems: TLS-based EAP (EAP-TLS / PEAP / TTLS) validates certificate validity windows (`NotBefore` / `NotAfter`), so a large time gap can fail the handshake; accounting start/stop times and durations become wrong; and the brute-force reject-delay window (default 10 s) is measured on the server clock. Run NTP on both the server and the NAS to keep time in sync.

### Which EAP method should I pick?

* **EAP-TLS** — strongest (mutual certificates), needs client-cert rollout.
* **PEAPv0/EAP-MSCHAPv2** — Windows/AD compatibility; mind the MS-CHAPv2 NTLMv1-like attack surface (see [Security Policy](/overview/security-policy)).
* **EAP-TTLS** — legacy/LDAP backends via inner PAP, keeping passwords inside the TLS tunnel.

## Sessions, CoA & accounting

### Disconnect / CoA from Online Sessions has no effect

Check, in order: the device has dynamic authorization enabled (e.g. `radius incoming` on RouterOS, `aaa server radius dynamic-author` on IOS); the **CoA port** on the NAS record matches the device (default 3799); and the device accepts requests from the server address. ToughRADIUS waits 5 s and retries twice before reporting failure.

### How do I change a user's speed live (FUP / over-quota throttling)?

ToughRADIUS's Change of Authorization (CoA) carries **only `Session-Timeout` and `Filter-Id`** — it does **not** rewrite rate attributes like `Mikrotik-Rate-Limit` live. The standard way to change speed in real time is to change the rate on the profile / user first, **then force a disconnect**; the client redials and is re-authorized at the new speed. Alternatively use `Filter-Id` to apply a pre-defined rate rule on the device. See [Scenario Cookbook · MikroTik · Live control](/overview/cookbook/cookbook-mikrotik).

### CoA / disconnect says the session was not found

An operator-initiated CoA / Disconnect first locates the session on the Online Sessions page, then addresses the device using its NAS record and session identity (e.g. `Acct-Session-Id`). If the online row is stale (the NAS never sent an accounting stop) or the session already ended, there is nothing to match — refresh the online list, confirm accounting works on the device, and retry.

### Online sessions show users that already disconnected

Online entries are created/refreshed by NAS accounting packets. If the NAS stops sending (reboot, link loss) the row can linger. Ensure accounting and interim updates are enabled on the device (`Acct-Interim-Interval` is sent in every Access-Accept, default 300 s). Stale entries can also be cleared manually from the UI with Disconnect/delete.

### Accounting table grows forever — what is cleaned automatically?

Two `@daily` jobs purge data automatically: one deletes operation logs (`SysOprLog`) older than one year; the other, `SchedClearExpireData`, deletes **terminated** `radius_accounting` history older than `AccountingHistoryDays` (default 90 days; active sessions are never purged) and removes dangling `radius_online` rows that have missed several interim updates. Set `AccountingHistoryDays` to `0` to **disable** accounting cleanup (keep history indefinitely). For very high volumes you may still want **database-level archiving** in your ops routine. Configuration backups do **not** include accounting history — see [Backup and restore](/overview/ops-guide#backup-and-restore).

### Concurrent sessions are not limited

`active_num` in the billing profile is the per-user concurrency cap (0 means unlimited). The check counts rows in `radius_online`, which requires working accounting from the NAS — without accounting Start packets the server cannot know who is online.

## Operations

### How do I monitor it with Prometheus?

There is currently no `/metrics` HTTP endpoint; counters are in-memory and shown on the dashboard. For external monitoring probe the ports, watch the log file, and alert on process exit (the process is fail-fast by design).

### How do I upgrade safely?

Stop the service, replace the binary (or pull the new Docker tag), start. Schema migration is automatic. Take a configuration backup (System Config → Backup) and a database backup first.

### Where are logs / data / certificates stored?

Everything lives under `system.workdir` (default `/var/toughradius`): `data/` (SQLite DB), `logs/`, `private/` (TLS material), `backup/` (config snapshots). See the [Operations Guide](/overview/ops-guide#working-directory-layout).


# Protocol & RFC Reference

> 中文版本：[协议与 RFC 索引](/overview-1/rfc-index)

ToughRADIUS implements standard RADIUS, EAP, dynamic-authorization, and secure-transport protocols. This chapter is the curated, implementation-oriented index of the standards the project relies on, with each RFC mapped to where it is used in the code and on the [roadmap](https://github.com/talkincode/toughradius/blob/main/docs/roadmap.md).

The full RFC texts are archived under [`docs/rfcs/`](https://github.com/talkincode/toughradius/tree/main/docs/rfcs); the raw catalog of every file lives in [`docs/rfcs/README.md`](https://github.com/talkincode/toughradius/blob/main/docs/rfcs/README.md). Where the two differ, the citations in this chapter take precedence.

## Implemented standards

### RADIUS core

* **RFC 2865** — RADIUS authentication; the base request/response protocol.
* **RFC 2866** — RADIUS accounting; session start / interim-update / stop records.

### RADIUS + EAP integration

* **RFC 3579** — RADIUS support for EAP (EAP-Message and Message-Authenticator).
* **RFC 3580** — IEEE 802.1X RADIUS usage guidelines.

### EAP framework and methods

* **RFC 3748** — Extensible Authentication Protocol (EAP). The framework itself; also defines EAP-MD5, the MD5-Challenge method (§5.4).
* **RFC 5216** — EAP-TLS; certificate-based mutual authentication (milestone M1).
* **RFC 5281** — EAP-TTLSv0; a TLS tunnel carrying inner PAP / MS-CHAPv2 (M9).
* **RFC 2759** — MS-CHAP-V2, used as the inner method of EAP-MSCHAPv2 and PEAPv0 (M8). MS-MPPE session keys are derived per RFC 2548 / RFC 5705.

> PEAPv0 has no standalone RFC; it follows the Microsoft/Cisco PEAP definition and carries inner EAP-MSCHAPv2. It is compatibility-oriented — MS-CHAPv2 exchanges carry an NTLMv1-like attack surface — so prefer EAP-TLS where you control client certificates.

### Dynamic authorization

* **RFC 5176** — CoA and Disconnect-Request, superseding RFC 3576 (milestone M2).

### Secure transport

* **RFC 6614** — RADIUS over TLS (RadSec).
* **RFC 6613** — RADIUS over TCP.

### Vendor-specific attributes

* **RFC 2548** — Microsoft VSAs, including the MS-MPPE-Send/Recv-Key attributes used to carry EAP key material.

### IPv6

* **RFC 3162**, **RFC 4818**, **RFC 6911** — RADIUS IPv6 addressing and delegated-prefix attributes (milestone M3).

## Roadmap standards

| RFC                   | Standard                                            | Milestone |
| --------------------- | --------------------------------------------------- | --------- |
| RFC 9190 (+ RFC 9427) | EAP-TLS 1.3 and TLS 1.3 key derivation              | M10       |
| RFC 7170 / RFC 9930   | TEAP v1 — tunnel EAP, machine + user chaining       | M11       |
| RFC 5931              | EAP-PWD — password-based, no per-client certificate | M12       |

> **Catalog accuracy note.** In `docs/rfcs/`, the file `rfc7542-eap-pwd.txt` is mislabeled: **RFC 7542 is the Network Access Identifier (NAI)**, whereas **EAP-PWD is RFC 5931**. Likewise, EAP-MD5 is defined by **RFC 3748 §5.4**, not RFC 3851 (an S/MIME specification). This chapter uses the correct citations.

## See also

* [Overview](/overview) — capability summary, including the EAP suite.
* [Documentation Map](/overview/documentation-map) — where every source document lives.
* [RFC Editor](https://www.rfc-editor.org/) — authoritative online RFC texts.


# EAP Acceptance Reports

Weekly EAP acceptance runs validate ToughRADIUS with an external `eapol_test` supplicant and publish the latest retained reports here.

**Latest verdict:** PARTIAL

Coverage note: PEAP/MSCHAPv2 external `eapol_test` scenarios are still skipped and tracked by [#495](https://github.com/talkincode/toughradius/issues/495), so this report is partial external coverage rather than complete PEAP acceptance.

## Latest Scenario Summary

| Scenario                             | Method            | Expected        | Status  | Duration | Detail                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------ | ----------------- | --------------- | ------- | -------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EAP-TLS valid client certificate     | EAP-TLS           | Access-Accept   | passed  |   186 ms | external supplicant received the expected Access-Accept                                                                                                                                                                                                                                                                              |
| EAP-TLS untrusted client certificate | EAP-TLS           | Access-Reject   | passed  |    37 ms | external supplicant was rejected as expected                                                                                                                                                                                                                                                                                         |
| PEAP/MSCHAPv2 valid credentials      | PEAP/MSCHAPv2     | Access-Accept   | skipped |     0 ms | Skipped intentionally: eapol\_test currently exposes a PEAP inner-framing interop gap (server rejects the decrypted phase-2 payload as an invalid inner EAP message). The in-process PEAP/MSCHAPv2 integration test remains the current acceptance coverage. Tracking issue: <https://github.com/talkincode/toughradius/issues/495>. |
| PEAP/MSCHAPv2 wrong password         | PEAP/MSCHAPv2     | Access-Reject   | skipped |     0 ms | Skipped intentionally: eapol\_test currently exposes a PEAP inner-framing interop gap (server rejects the decrypted phase-2 payload as an invalid inner EAP message). The in-process PEAP/MSCHAPv2 integration test remains the current acceptance coverage. Tracking issue: <https://github.com/talkincode/toughradius/issues/495>. |
| EAP-TTLS/PAP valid credentials       | EAP-TTLS/PAP      | Access-Accept   | passed  |   138 ms | external supplicant received the expected Access-Accept                                                                                                                                                                                                                                                                              |
| EAP-TTLS/MSCHAPv2 valid credentials  | EAP-TTLS/MSCHAPv2 | Access-Accept   | passed  |   138 ms | external supplicant received the expected Access-Accept                                                                                                                                                                                                                                                                              |
| Malformed external EAP client config | tooling           | documented skip | skipped |     0 ms | Skipped intentionally: eapol\_test parser failures do not exercise ToughRADIUS over RADIUS/EAP. Negative server behavior is covered by untrusted certificate and wrong password scenarios.                                                                                                                                           |

## Retained Reports

* [2026-08-10](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-08-10.md)
* [2026-07-27](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-07-27.md)
* [2026-07-20](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-07-20.md)
* [2026-07-13](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-07-13.md)


# Performance Benchmark Reports

Weekly benchmark runs record ToughRADIUS performance signals from existing Go `Benchmark*` functions. Reports are informational and do not fail on timing drift from GitHub hosted runners.

**Latest verdict:** RECORDED

## Latest Summary

| Metric       |                                                                 Value |
| ------------ | --------------------------------------------------------------------: |
| Benchmarks   |                                                                    15 |
| Packages     |                                                                     6 |
| Slowest      | github.com/talkincode/toughradius/v9/pkg/excel / BenchmarkWriteToFile |
| Highest B/op | github.com/talkincode/toughradius/v9/pkg/excel / BenchmarkWriteToFile |

## Retained Reports

* [2026-08-10](https://github.com/talkincode/toughradius/blob/main/docs/reports/performance/2026-08-10.md)
* [2026-07-20](https://github.com/talkincode/toughradius/blob/main/docs/reports/performance/2026-07-20.md)
* [2026-07-13](https://github.com/talkincode/toughradius/blob/main/docs/reports/performance/2026-07-13.md)


# Security Policy

> 中文版本：[安全策略](/overview-1/security-policy)

This chapter is the canonical home for ToughRADIUS security advisories and the guidance that goes with them. The repository's [`SECURITY.md`](https://github.com/talkincode/toughradius/blob/main/SECURITY.md) keeps a short pointer back to this chapter so there is a single source of truth.

## Security advisories

### Default super-admin credentials (GHSA-2gwm-6gf5-8699)

ToughRADIUS no longer creates or accepts the historical first-start password. A fresh install generates a one-time bootstrap password (or uses `TOUGHRADIUS_ADMIN_PASSWORD`) and writes it to `{workdir}/private/admin-bootstrap-password`. Upgrades that still store the historical default rotate it on startup. Login and operator-password APIs reject that credential.

| Item               | Details                                        |
| ------------------ | ---------------------------------------------- |
| Vulnerability type | Use of Default Credentials (CWE-1392)          |
| Severity           | Critical                                       |
| Advisory           | GHSA-2gwm-6gf5-8699                            |
| Affected component | Super-admin bootstrap and `/api/v1/auth/login` |

#### Recommended actions

Upgrade, then confirm every operator uses a unique password. If you cannot read `{workdir}/private/admin-bootstrap-password`, reset it with `cmd/reset-password`.

### XSS vulnerability fix (v8.0.8)

Version **v8.0.8** addresses a critical cross-site scripting (XSS) vulnerability. The issue was found in the `errmsg` parameter handling in the login endpoint.

| Item               | Details                             |
| ------------------ | ----------------------------------- |
| Vulnerability type | Cross-Site Scripting (XSS)          |
| Severity           | Critical                            |
| Affected versions  | v8.0.1 – v8.0.7                     |
| Fixed version      | v8.0.8                              |
| Affected component | Login endpoint (`errmsg` parameter) |

#### Recommended actions

We strongly recommend that all users update to the latest version immediately. See the [Documentation Map](/overview/documentation-map) for the README and build instructions you can follow to upgrade your deployment.


# Agent Development Guide

> 中文版本：[Agent 开发指南](/overview-1/agent-guide)

This chapter is a **contributor-oriented** digest of how ToughRADIUS is built with AI coding agents. It summarizes the working rules, quality gates, and the auto-delegation loop so the workflow is discoverable from the handbook.

The **canonical** rules live in the repository's [`AGENT.md`](https://github.com/talkincode/toughradius/blob/main/AGENT.md); that file stays authoritative and is referenced directly by the agent tooling. This chapter does not replace it — when in doubt, follow `AGENT.md`.

## Product scope baseline

Development stays anchored to the feature checklist; it never drifts into unrelated product directions.

* The canonical scope baseline is [`docs/feature-checklist.md`](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.md) (with an English copy at [`docs/feature-checklist.en.md`](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.en.md)).
* Every task, issue, PR, test, and review note maps to a feature ID such as `TR-F004`.
* If a request does not map to an existing ID, the checklist is updated first (scope, status, acceptance boundary, rationale) before any code changes.
* Non-goals `TR-N001`–`TR-N005` (payment, CRM, generic monitoring stack, multi-tenancy, full rewrite) are out of scope unless the checklist is explicitly revised first.

## Roadmap and skill library

Agent-driven development is organized around three artifacts:

* [`docs/roadmap.md`](https://github.com/talkincode/toughradius/blob/main/docs/roadmap.md) — long-term roadmap and milestones, each mapped to `TR-F` IDs. This is the task source for agent work.
* [`.agents/skills/`](https://github.com/talkincode/toughradius/tree/main/.agents/skills/README.md) — reusable skill SOPs, one folder per skill (`.agents/skills/<name>/SKILL.md`).
* [`.agents/README.md`](https://github.com/talkincode/toughradius/blob/main/.agents/README.md) — the delegation reference and shared guardrails.

A **coordinator layer** drives the loop, while the execution SOPs do the domain-specific work:

| Role           | Skill                 | Purpose                                                                                                                            |
| -------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Coordinator    | `orchestrate-roadmap` | Entry role for "auto-delegate development": selects the next unchecked subtask, picks the matching SOP, enforces gates, opens a PR |
| Gate           | `review-pr`           | Independent, CI-anchored review; requests changes via labels/comments and auto-merges only when approved **and** CI is green       |
| Self-iteration | `groom-roadmap`       | After each merge, checks off the delivered subtask and re-grooms the roadmap                                                       |

Execution SOPs include: add vendor VSA (`add-radius-vendor`), add EAP method (`add-eap-method`), add Admin API (`add-adminapi-endpoint`), add React Admin resource (`add-react-admin-resource`), add config schema (`add-config-schema`), add acceptance test (`add-acceptance-test`), sync upstream radius (`sync-upstream-radius`), reference RFC (`reference-rfc`), align checklist (`align-feature-checklist`), write Go tests (`write-go-tests`), and document Go APIs (`document-go-apis`). Pick the matching skill before starting a task type.

Agents run **on your own host** with your own agent/CLI, not via a CI workflow, so credentials never enter CI and the execution environment stays under your control.

## Working guidelines

### Understand existing code before editing

Never change code blindly. Locate the existing implementation, related tests, and documentation first, then mirror the project's naming, error handling, and data flow. Trace the full execution path before fixing a bug, and map dependencies and side effects before refactoring.

### Continuous verification

Do not wait until the end to run tests. Every logical change is followed by a test run so regressions surface immediately rather than at the end of a large batch.

### Code is the best documentation

* **All exported APIs carry comprehensive godoc comments** — purpose, parameters with constraints, return values, and error conditions (with usage examples for complex APIs). See the `document-go-apis` skill for the standard-library-style conventions.
* **Complex logic carries inline comments** explaining the *why*, not the *what*.
* **Vendor-specific code references the protocol spec** (RFC numbers, VSA docs).
* **No redundant standalone summary documents** — information belongs in code comments and Git history.

## Core development principles

### Test-driven development (TDD)

Write the test first, then the code: define the expected behavior in a failing test (red), write the minimum code to pass it (green), then refactor while the tests stay green. If `internal/radiusd/auth.go` changes, the test lives in `internal/radiusd/auth_test.go`. See the `write-go-tests` skill for the unified conventions.

### GitHub workflow

* **Pull requests only** — never push to `main`; the protected branch rejects direct pushes.
* **Conventional commits** — `<type>(<scope>): <subject>` with types such as `feat`, `fix`, `test`, `docs`, `refactor`, `perf`, `chore`.
* **Small, atomic changes** over giant PRs.

### Repository issue/PR automation

Several GitHub Actions add lightweight triage around issues and pull requests. They help maintainers scan the queue, but they do not replace reading the original issue, PR diff, and CI output.

| Workflow      | Trigger and result                                                                                                                                                                                              | Maintainer notes                                                                                                                                                                                                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Stale         | `.github/workflows/stale.yml` runs daily at `04:24 UTC` and by manual dispatch. After 60 days without activity it adds `stale`; after 14 more inactive days it closes the item.                                 | Comment, push a commit, or remove `stale` to keep work open. Issues with `pinned`, `security`, `help wanted`, `agent-roadmap`, or `needs-human` are exempt; PRs with `pinned`, `security`, `agent-roadmap`, or `needs-human` are exempt; all milestones are exempt.                              |
| Labeler       | `.github/workflows/labeler.yml` runs on `pull_request_target` and applies labels from `.github/labeler.yml` based on changed paths.                                                                             | It labels `go`, `javascript`, `github_actions`, `dependencies`, and `doc`. The action reads the changed-file list and base-branch config; it does not check out or execute PR code.                                                                                                              |
| Workflow Lint | `.github/workflows/workflow-lint.yml` runs on PRs that change `.github/workflows/**` or `.github/actionlint.yml` / `.github/actionlint.yaml`, on pushes to `main` touching those paths, and by manual dispatch. | It runs `actionlint -shellcheck=` so the gate covers GitHub Actions YAML, expressions, and action inputs without turning existing shell-style warnings into this gate's scope. It is static validation only; it does not exercise release tags, Docker publishing, Pages deployment, or secrets. |
| Greetings     | `.github/workflows/greetings.yml` runs when a contributor opens their first issue or PR and posts onboarding guidance.                                                                                          | The comment is informational. It does not change review requirements or issue priority.                                                                                                                                                                                                          |

### Release tag and Docker publish automation

Pushing a `v*` tag triggers two release workflows. Release operators need to understand both outputs and their credential boundaries before tagging.

| Workflow        | Trigger and output                                                                                                                                                                                                                 | Release prerequisites                                                                                                                                                                                                                                                                                           |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Release publish | `.github/workflows/release-publish.yml` runs on `v*` tag pushes, builds multi-platform binaries, generates checksums, and creates the GitHub Release.                                                                              | The tag must point at the verified `origin/main` target SHA; release notes are generated by the workflow from the tag and closed issues.                                                                                                                                                                        |
| Docker publish  | `.github/workflows/docker-publish.yml` runs on `v*` tag pushes, publishes `talkincode/toughradius:latest` and the version tag to Docker Hub, then independently attempts `ghcr.io/<owner>/toughradius:latest` and the version tag. | Docker Hub requires `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`. GHCR requires package repository access / inherited access that allows this repository's `GITHUB_TOKEN` to write, or `PKG_GITHUB_TOKEN` with `write:packages`; `PKG_GITHUB_USERNAME` is optional when the token owner differs from the tag actor. |

Docker Hub publishing is the required gate. GHCR publishing depends on external GitHub Packages access settings, so the workflow isolates the GHCR push in its own step and runs a non-destructive write-access preflight probe first: when the credentials cannot write the package (the `permission_denied: write_package` signature), the GHCR build is skipped instead of failing mid-push, and the run summary reports a warning and recovery guidance without making maintainers guess whether Docker Hub published.

Before tagging:

* Confirm the target `origin/main` CI is green and the tag version is unused.
* Confirm the Docker Hub secrets exist and still have write access.
* Confirm the GHCR package `talkincode/toughradius` inherits this repository's access, or configure `PKG_GITHUB_TOKEN` (optionally `PKG_GITHUB_USERNAME`); docs record only secret names and permission requirements, never token values.
* After tagging, check the GitHub Release, Docker Hub `latest` / version tag, GHCR `latest` / version tag, and both workflow conclusions.

Failure recovery:

* If the GitHub Release and Docker Hub succeed but GHCR fails, fix GHCR package access or token permissions, then rerun the tag workflow; do not create a duplicate bad version tag for the same source.
* If Docker Hub publishing fails, treat the release as incomplete and rerun the workflow after fixing the secret or registry problem.
* If a new patch tag is needed for validation, rerun the `release-version` SOP first so the version decision is not skipped.

### Report PR automation

Weekly report workflows publish generated reports through signed commits on short-lived automation branches. The workflows always upload their generated artifact before the report PR step, so maintainers can inspect the report even when PR creation is skipped or blocked.

| Workflow                     | Trigger and published files                                                                                                                                                                                                                    | PR requirements and fallback                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EAP acceptance weekly        | `.github/workflows/eap-acceptance-weekly.yml` runs every Monday at `09:17 UTC` and by manual dispatch. It publishes `docs/reports/eap/<date>.md`, `docs/reports/eap/latest.md`, and the EAP report index pages.                                | When the external EAP acceptance step succeeds and report files changed, the PR step requires the repository secret `EAP_REPORT_SIGNING_KEY` and repository variables `EAP_REPORT_SIGNING_EMAIL` and optional `EAP_REPORT_SIGNING_NAME`. Missing key or email configuration blocks the PR step because protected `main` requires verified signed commits; use the uploaded `eap-acceptance-<run_id>` artifact for the generated report.                                                                                                                                  |
| Performance benchmark weekly | `.github/workflows/performance-benchmark-weekly.yml` runs every Monday at `10:37 UTC` and by manual dispatch. It publishes `docs/reports/performance/<date>.md`, `docs/reports/performance/latest.md`, and the performance report index pages. | When benchmarks complete and report files changed, the PR step uses `PERFORMANCE_REPORT_SIGNING_KEY`, `PERFORMANCE_REPORT_SIGNING_EMAIL`, and optional `PERFORMANCE_REPORT_SIGNING_NAME`. If these are absent, it reuses `EAP_REPORT_SIGNING_KEY`, `EAP_REPORT_SIGNING_EMAIL`, and optional `EAP_REPORT_SIGNING_NAME`. If no signing key/email pair is configured, the workflow keeps the generated report in the `performance-benchmark-<run_id>` artifact, writes a workflow step summary explaining that PR creation was skipped, and exits the PR step successfully. |

The signing key values are never documented here or in issue/PR comments. Only record the secret and variable names, their purpose, and whether they are configured.

When reviewing a generated report run:

* Check the workflow summary first for skip or credential messages.
* Download the run artifact if no report PR was opened.
* If a report PR exists, verify that its head commit is shown as verified by GitHub before merging.
* Check the published file list in the PR body against the workflow's expected report paths.
* Treat report PR CI job state and `mergeStateStatus=UNSTABLE` behavior as the separate implementation boundary tracked by issue #494.

### Minimum viable product (MVP)

Each change is delivered as a minimal, independently usable, rollback-safe unit that does not break existing behavior. Large efforts are broken into MVP increments (for example, vendor attribute parsing → auth integration → accounting → management UI) rather than landing in one oversized PR.

## Quality gates

Every agent change must pass these gates before merging:

* `go build ./...` — no compilation errors.
* `go test ./...` — all unit tests pass.
* `golangci-lint run` — clean (pinned to **v2.12.2**, matching CI).
* `cd web && npm run build` — for any frontend change.
* `Workflow Lint` — required when `.github/workflows/**` or `.github/actionlint.yml` / `.github/actionlint.yaml` changes; it runs `actionlint -shellcheck=` for GitHub Actions YAML, expression, and action input validation.
* **Protocol / end-to-end changes** ship a CI-executable acceptance test under [`test/integration/`](https://github.com/talkincode/toughradius/tree/main/test/integration/README.md) and cite the relevant spec under [`docs/rfcs/`](https://github.com/talkincode/toughradius/tree/main/docs/rfcs/README.md).
* Output goes through a PR labeled `agent-roadmap`, gated by `review-pr`, and is merged only when `agent-approved` with green CI.

## Technical constraints

* **No CGO** — the project builds with `CGO_ENABLED=0` for easy cross-platform deployment. Use pure Go drivers only (for example `github.com/glebarez/sqlite` instead of `github.com/mattn/go-sqlite3`).
* **Database dual-compatibility** — every schema change must work on both PostgreSQL (default) and SQLite.
* **Upstream dependency** — the core `layeh.com/radius` library is `replace`d in `go.mod` to the organization fork `github.com/talkincode/radius`; important upstream fixes are evaluated via the `sync-upstream-radius` skill.

## Common anti-patterns (prohibited)

* Exporting APIs without documentation.
* Committing implementation without tests.
* Giant PRs that mix many concerns.
* Implementing before writing tests.
* Pushing directly to `main` or skipping review.
* Creating redundant standalone summary/report documents.
* Introducing CGO dependencies.

## Where to go next

* [`AGENT.md`](https://github.com/talkincode/toughradius/blob/main/AGENT.md) — the full, canonical agent development guide.
* [Documentation Map](/overview/documentation-map) — find the README, security policy, feature checklist, roadmap, and RFC index.
* [Protocol & RFC Reference](/overview/rfc-index) — protocol standards mapped to the code and milestones.


# Documentation Map

> 中文版本：[文档地图](/overview-1/documentation-map)

This handbook is being assembled incrementally. The first table lists the handbook's own chapters; the second points to documents that still live elsewhere in the repository, so everything is reachable from a single place.

## Handbook chapters

| Chapter                                                                    | Contents                                                                                                                                                       |
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Overview](/overview)                                                      | Project introduction, core capabilities, service model                                                                                                         |
| [Concepts & Terminology](/overview/concepts)                               | AAA vocabulary, the authentication flow, password protocols                                                                                                    |
| [Quick Start](/overview/quickstart)                                        | Install, initialize, first user, debugging                                                                                                                     |
| [Vendor Integration Guide](/overview/vendor-guide)                         | Case studies: MikroTik, Huawei, Cisco, H3C, ZTE, iKuai, …                                                                                                      |
| [Portal / Hotspot Integration Boundary](/overview/portal-hotspot-boundary) | Hard boundary: ToughRADIUS is the RADIUS AAA backend, not a hosted captive portal product                                                                      |
| [Scenario Cookbook](/overview/cookbook)                                    | End-to-end ops scenarios (five-part): MikroTik PPPoE / Hotspot / CoA; Huawei BRAS speed tiers / line binding / CoA; H3C / ZTE / iKuai / Cisco per-vendor diffs |
| [Admin UI Manual](/overview/admin-manual)                                  | The management console, page by page                                                                                                                           |
| [Operations Guide](/overview/ops-guide)                                    | Configuration reference, certificates, monitoring, backup, CLI tools                                                                                           |
| [LDAP / AD Authentication](/overview/auth-ldap)                            | Verify passwords against an LDAP/AD directory (PAP-family only): bind modes, TLS, security, metrics                                                            |
| [FAQ](/overview/faq)                                                       | Frequently asked questions by theme                                                                                                                            |
| [Protocol & RFC Reference](/overview/rfc-index)                            | Protocol standards mapped to the code                                                                                                                          |
| [Security Policy](/overview/security-policy)                               | Security advisories and update guidance                                                                                                                        |
| [Agent Development Guide](/overview/agent-guide)                           | Contributor digest of the AI-agent workflow, quality gates, and auto-delegation loop                                                                           |

## Repository documents

| Document          | Description                                     | Current location                                                                                                                                                                                         |
| ----------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| README            | Project introduction, features, and quick start | [README.md](https://github.com/talkincode/toughradius/blob/main/README.md)                                                                                                                               |
| Agent guide       | AI-agent development guide and working rules    | [Agent Development Guide](/overview/agent-guide) (handbook digest) · [AGENT.md](https://github.com/talkincode/toughradius/blob/main/AGENT.md) (canonical)                                                |
| Security policy   | Security advisories and update guidance         | [Security Policy](/overview/security-policy) (canonical) · [SECURITY.md](https://github.com/talkincode/toughradius/blob/main/SECURITY.md) (pointer)                                                      |
| Feature checklist | Feature scope baseline (`TR-F` IDs)             | [docs/feature-checklist.md](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.md) · [English](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.en.md) |
| Roadmap           | Long-term roadmap and milestones                | [docs/roadmap.md](https://github.com/talkincode/toughradius/blob/main/docs/roadmap.md)                                                                                                                   |
| RFC index         | Protocol standards index used by the project    | [Protocol & RFC Reference](/overview/rfc-index) (canonical) · [docs/rfcs/README.md](https://github.com/talkincode/toughradius/blob/main/docs/rfcs/README.md) (raw catalog)                               |

> **Migration plan.** The handbook now covers the README's user-facing content (overview, quick start, vendor integration, admin manual, operations, FAQ); the README remains the GitHub landing page and links here. The agent guide now has a handbook [digest chapter](/overview/agent-guide) that points to the canonical [`AGENT.md`](https://github.com/talkincode/toughradius/blob/main/AGENT.md), which stays in the repository root because the agent tooling references it directly. The feature checklist and roadmap are **living documents** maintained in `docs/` by dedicated workflows and are linked rather than migrated.


# mdbook & GitBook Coexistence

> 中文版本：[mdbook 与 GitBook 并存](/overview-1/gitbook-coexistence)

ToughRADIUS also publishes documentation through **GitBook** (`docs.toughradius.net`). This section records the decision (roadmap item **M13.0**) for how the new mdBook handbook relates to that existing pipeline.

## Decision: coexistence, not replacement

The mdBook handbook **coexists** with GitBook. It does not replace or disable the GitBook site. The two pipelines are kept separate and non-conflicting:

* **mdBook handbook** — lives in the repository under `docs-site/`, is written in bilingual chapters, and is built and link-checked by CI on every pull request. It is the canonical, version-controlled, review-gated home for documentation that must evolve together with the code.
* **GitBook site** — synchronizes from the repository through GitBook's own GitHub integration (an external service). It is configured by a committed **`.gitbook.yaml`** at the repository root, which points GitBook at the same `docs-site/src/` sources, uses `introduction.md` as the landing page, and reads the **shared `SUMMARY.md`** as its table of contents. GitBook therefore renders the curated bilingual handbook instead of inferring a tree from the whole repo.

Both pipelines build from the **same** `docs-site/src/` sources, but each keeps its own independent configuration (`book.toml` for mdBook, `.gitbook.yaml` for GitBook). They do not share a build step and cannot break each other, yet they never drift because they read the same chapters and the same `SUMMARY.md`.

## Where each site is served

The two pipelines publish to **separate** domains, so they never shadow each other:

* **mdBook handbook** — deployed to **GitHub Pages** by `.github/workflows/pages.yml` (roadmap item **M13.5**) and served at the custom domain [**https://www.toughradius.net/**](https://www.toughradius.net/). The deploy workflow writes a `CNAME` file into the published artifact, so GitHub Pages keeps that custom domain on every release. For the domain to resolve, `www.toughradius.net` must point at GitHub Pages — an `A` record set to `185.199.108.153`–`185.199.111.153`, or a DNS-only `CNAME` to `talkincode.github.io`. The default project URL <https://talkincode.github.io/toughradius/> stays available and redirects to the custom domain.
* **GitBook** — serves **`docs.toughradius.net`**, which resolves to GitBook's own hosting (Cloudflare / Fastly), not GitHub Pages. Because `www.toughradius.net` moves to GitHub Pages, GitBook should be configured to keep only `docs.toughradius.net` and release `www`, so the two services never claim the same host.

## Single source of truth

To avoid content drift between the two pipelines, every document has exactly one canonical home. Because mdBook and GitBook now read the **same** `docs-site/src/` chapters and the same `SUMMARY.md`, the handbook sources are the single source for both rendered sites. As scattered documents are migrated into the handbook (roadmap items M13.2 / M13.3), the original file keeps a short pointer back to the corresponding chapter instead of duplicating its content.

## Editing the shared table of contents

`docs-site/src/SUMMARY.md` is read by **both** tools, so keep it to the subset of Markdown that they parse the same way: a top `# Summary` title, a non-bulleted `[Introduction / 引言](./introduction.md)` landing link, and **nested bullet lists** for grouping. The two language sections are expressed as top-level entries (`- [English](./en/overview.md)` and `- [中文](./zh/overview.md)`) with their pages nested beneath them. Avoid `#` / `##` part headers for grouping: mdBook only groups on `#` while GitBook only groups on `##`, so a nested list is the one form that both render identically.

## Language toggle in the menu bar

The mdBook output injects a small EN / 中文 switch into the top menu bar (`docs-site/assets/lang-toggle.{js,css}`, wired via `additional-js` / `additional-css` in `book.toml`). It rewrites the last `/en/` or `/zh/` path segment, which works because the two language trees keep identical file names. This toggle exists **only** on the mdBook/Pages pipeline; GitBook ignores `book.toml` extras, so GitBook readers switch languages through the sidebar sections and per-chapter cross-links instead.

## Build and validation

* Local: `mdbook build docs-site` produces the static site in `docs-site/book/`, and `mdbook serve docs-site` previews it with live reload.
* CI: a dedicated job builds the handbook and runs an **offline link check** over the generated HTML, so a build failure or a broken internal link fails the pipeline (roadmap item M13.4). The build output (`docs-site/book/`) is a build artifact and is not committed.


# 中文

> English version: [Overview](/overview)

ToughRADIUS 是一款使用 Go 语言编写、功能强大的开源 RADIUS 服务器，面向 ISP、 企业网络与运营商场景。它实现了标准 RADIUS 协议，并支持 RadSec（RADIUS over TLS）， 同时附带基于 React Admin 的现代化 Web 管理界面。

## 核心能力

* **标准 RADIUS** —— 完整支持 RFC 2865（认证）与 RFC 2866（计费）。
* **RadSec** —— 基于 TCP 的 TLS 加密 RADIUS（RFC 6614）。
* **动态授权** —— CoA 与 Disconnect 报文（RFC 5176）。
* **EAP / 802.1X 套件** —— 挑战类方法（EAP-MD5、EAP-MSCHAPv2）与隧道类方法： EAP-TLS（RFC 5216，基于证书）、PEAPv0/EAP-MSCHAPv2（Windows / AD 兼容）、 EAP-TTLS（RFC 5281，内层 PAP / MS-CHAPv2）。MS-CHAPv2 类方法以兼容为先，存在类似 NTLMv1 的攻击面，可控证书环境优先选用 EAP-TLS。TLS 1.3（RFC 9190）、TEAP 与 EAP-PWD 在路线图中持续推进。
* **多厂商支持** —— 通过厂商私有属性（VSA）兼容 Cisco、MikroTik、华为等主流网络设备。
* **现代化管理界面** —— 基于 React Admin 的控制台，管理用户、套餐、在线会话、计费与审计日志。
* **多数据库** —— PostgreSQL（默认）与 SQLite（纯 Go，无需 CGO）。

## 服务模型

服务器以并发方式运行多个相互独立的服务；任意一个服务失败都会让进程退出，便于由 守护进程干净地重启。

| 服务              | 协议 / 端口             | 用途             |
| --------------- | ------------------- | -------------- |
| Web / Admin API | HTTP，TCP `1816`     | 管理界面与 REST API |
| RADIUS 认证       | UDP `1812`          | 认证             |
| RADIUS 计费       | UDP `1813`          | 计费             |
| RadSec          | TLS over TCP `2083` | 加密的 RADIUS 传输  |

## 下一步

* [核心术语与概念](/overview-1/concepts) —— AAA 术语、请求流转，以及各概念在产品中的落点。
* [快速开始](/overview-1/quickstart) —— 约十分钟完成安装、初始化、建用户并用 `radtest` 验证。
* [厂商对接指南](/overview-1/vendor-guide) —— MikroTik、华为、Cisco、H3C、中兴、爱快及 标准设备的对接案例。
* [管理系统用户手册](/overview-1/admin-manual) —— 管理控制台每个页面的说明。
* [运维指南](/overview-1/ops-guide) —— 生产配置、证书、监控、备份与命令行工具。
* [常见问题解答](/overview-1/faq) —— 大家都会问到的问题。
* [文档地图](/overview-1/documentation-map) —— 找到现有的 README、Agent 指南、安全策略、 功能清单、路线图与 RFC 索引。
* [mdbook 与 GitBook 并存](/overview-1/gitbook-coexistence) —— 本手册与 GitBook 站点的关系， 以及单一事实来源策略。


# 核心术语与概念

> English version: [Concepts & Terminology](/overview/concepts)

本章解释贯穿 ToughRADIUS 的 AAA 核心术语，并说明每个概念在产品中的落点。 协议规范的权威清单及其与代码的对应关系，请见[协议与 RFC 索引](/overview-1/rfc-index)。

## 一段话理解 AAA

RADIUS（Remote Authentication Dial In User Service）协议让网络设备向中心服务器 提出三个问题：*这个用户是谁*（**认证 Authentication**）、*他能做什么* （**授权 Authorization**）、*他用了多少*（**计费 Accounting**）。ToughRADIUS 对三者全部作答：校验凭据、在 `Access-Accept` 中下发授权属性（IP 地址、带宽、 VLAN、会话限制），并从计费报文中记录用量。

## 核心术语

| 术语                             | 在 ToughRADIUS 中的含义                                                                                                                                        |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **NAS**（网络接入服务器）               | 发出 RADIUS 请求的网络设备——路由器、交换机、BRAS 或无线控制器。每台 NAS 须在管理界面的 **NAS 设备** 中登记 IP 地址、共享密钥和厂商代码；未登记地址的请求会被丢弃。                                                        |
| **共享密钥**（shared secret）        | 每台 NAS 一个的口令，用于认证 RADIUS 报文本身（RFC 2865 §3），NAS 侧与 ToughRADIUS 的 NAS 记录必须一致。                                                                               |
| **拨入用户 / RADIUS 用户**           | **RADIUS 用户** 中的账号：用户名、密码、过期时间、可选的静态 IPv4/IPv6 地址、MAC/VLAN 绑定以及计费策略。                                                                                      |
| **计费策略**（`RadiusProfile`）      | **计费策略** 中的可复用模板：并发会话数（`active_num`）、上行/下行速率（单位 **Kbps**）、地址池、IPv6 前缀、域、MAC/VLAN 绑定开关。用户留空的字段自动继承策略值。                                                     |
| **VSA**（厂商私有属性）                | RADIUS 26 号属性（RFC 2865 §5.26），允许各厂商定义私有属性的容器。ToughRADIUS 内置 15+ 厂商的属性字典，并按 NAS 厂商代码下发对应厂商的授权属性。详见[厂商对接指南](/overview-1/vendor-guide)。                      |
| **厂商代码**（vendor code）          | 选择厂商行为的 IANA 私有企业号，例如 `9` Cisco、`2011` 华为、`14988` MikroTik、`25506` H3C、`3902` 中兴、`10055` 爱快、`0` 标准。按 NAS 记录设置，决定使用哪个请求解析器及下发哪些 Access-Accept 属性。          |
| **CoA / Disconnect**（RFC 5176） | 动态授权：由服务器主动发起、修改在线会话（`CoA-Request`，如新的 `Session-Timeout` 或 `Filter-Id`）或终止会话（`Disconnect-Request`）的报文。在 **在线会话** 页面发起，发往 NAS 的 UDP 3799 端口（可按 NAS 覆盖）。    |
| **RadSec**（RFC 6614）           | TCP 2083 端口上的 RADIUS over TLS。用 TLS 隧道包裹 RADIUS，使报文可以安全穿越不可信网络；纯 UDP RADIUS 仅靠共享密钥保护。                                                                     |
| **EAP**（RFC 3748）              | 802.1X 网络使用的可扩展认证协议。ToughRADIUS 实现 EAP-MD5、EAP-MSCHAPv2、EAP-TLS、PEAPv0/EAP-MSCHAPv2 和 EAP-TTLS；当前方法与证书在 **系统配置 → RADIUS** 中选择。                            |
| **计费会话**                       | NAS 通过 `Accounting-Request` 报文汇报的生命周期：Start → Interim-Update（多次）→ Stop（RFC 2866）。在线会话见 **在线会话**（`radius_online` 表），历史记录见 **计费记录**（`radius_accounting` 表）。 |
| **Acct-Interim-Interval**      | NAS 发送中间计费更新的间隔（秒）。每个 Access-Accept 都会携带，取自 `radius.AcctInterimInterval` 配置。                                                                              |
| **Session-Timeout**            | 会话最长剩余时间（秒）。ToughRADIUS 将其设为距用户过期时间的剩余秒数，确保会话不会超过账号有效期。                                                                                                   |
| **地址池**（`Framed-Pool`）         | 配置在 NAS 上的命名 IP 池。ToughRADIUS 只下发池*名称*，实际地址由 NAS 分配；静态地址则直接通过 `Framed-IP-Address` / `Framed-IPv6-Address` 下发。                                             |
| **MAC 绑定**                     | 策略开启 `bind_mac` 后，首次出现的主叫 MAC 被记录到用户上，后续请求必须匹配。                                                                                                           |
| **VLAN 绑定**                    | 策略开启 `bind_vlan` 后，从 `NAS-Port-Id` 解析出的内/外层 VLAN ID 会被记录并校验。需要支持 VLAN 提取的厂商解析器（华为、H3C、中兴）。                                                                |

## 一次认证请求的流转

```
NAS ──Access-Request──▶ UDP 1812
        │
        ▼
  goroutine 池（ants，TOUGHRADIUS_RADIUS_POOL，默认 1024）
        │
        ▼
  1. NAS 查找 ─ 源 IP / identifier 必须匹配已登记的 NAS 记录
  2. 厂商解析器 ─ 提取 MAC（Calling-Station-Id）与 VLAN（NAS-Port-Id）
       · 华为 / H3C / 中兴解析器可提取 VLAN ID
       · 其余厂商走默认解析器（仅 MAC）
  3. 凭据校验 ─ PAP / CHAP / MS-CHAPv2，或 EAP 状态机
  4. 检查器 ─ 账号状态、过期时间、MAC 绑定、VLAN 绑定、并发数（active_num）
  5. Accept 增强器 ─ 标准属性（Session-Timeout、地址池、静态 IP、IPv6）
     以及按 NAS 厂商代码选择的厂商属性
        │
        ▼
NAS ◀─Access-Accept / Access-Reject──
```

认证失败会被归类到 Prometheus 风格计数器（`radus_reject_passwd_error`、 `radus_reject_expire` 等）并呈现在仪表盘，见[运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#指标)。 拒绝延迟防护可减缓暴力破解：在 `radius.RejectDelayWindowSeconds`（默认 10 秒） 窗口内连续拒绝达到 `radius.RejectDelayMaxRejects`（默认 7 次）后，响应将被延迟。

## 密码协议速览

| 协议            | 密码如何传输                                      | 说明                              |
| ------------- | ------------------------------------------- | ------------------------------- |
| **PAP**       | 随请求传输，由共享密钥 XOR 保护（RFC 2865 §5.2）           | 兼容性最好；建议配合 RadSec 或可信网络使用。      |
| **CHAP**      | 不传输——MD5 质询/应答（RFC 2865 §5.3）               | 要求服务器保存明文口令。                    |
| **MS-CHAPv2** | 不传输——NT 哈希质询/应答（RFC 2548）                   | Windows 常用；存在众所周知的类 NTLMv1 攻击面。 |
| **EAP（隧道类）**  | 在 TLS 隧道内传输（PEAP / EAP-TTLS）或以证书代替（EAP-TLS） | Wi-Fi / 802.1X 部署的推荐选择。         |

## 相关章节

* [协议与 RFC 索引](/overview-1/rfc-index) —— 上文引用的全部 RFC 与代码的对应关系。
* [厂商对接指南](/overview-1/vendor-guide) —— 各厂商属性与计算公式。
* [快速开始](/overview-1/quickstart) —— 用 10 分钟实际体验这些概念。


# 快速开始

> English version: [Quick Start](/overview/quickstart)

本章带你从零搭建一个可用的 RADIUS 服务器并创建一个测试用户，然后介绍如何调试 服务器行为。默认端口：管理界面 `1816`、RADIUS 认证 UDP `1812`、计费 UDP `1813`、RadSec TCP `2083`。

## 1. 安装

三种方式任选其一。

### 方式 A —— 预编译二进制

从 [GitHub Releases](https://github.com/talkincode/toughradius/releases) 页面下载对应平台的二进制（`toughradius_linux_amd64`、`toughradius_linux_arm64`、 `toughradius_darwin_arm64`、`toughradius_windows_amd64.exe` 等），然后：

```bash
chmod +x toughradius_linux_amd64
sudo mv toughradius_linux_amd64 /usr/local/bin/toughradius
```

### 方式 B —— Docker

```bash
docker pull talkincode/toughradius:latest

docker run -d --name toughradius \
  -p 1816:1816 -p 1812:1812/udp -p 1813:1813/udp -p 2083:2083 \
  -v toughradius-data:/var/toughradius \
  talkincode/toughradius:latest -c /etc/toughradius.yml
```

镜像暴露 `1816/tcp`、`1812/udp`、`1813/udp`、`2083/tcp`。请将卷挂载到 `/var/toughradius`（默认工作目录），使 SQLite 数据库、日志与证书在容器重启后 得以保留。

### 方式 C —— 源码构建

需要 Go 1.25+ 与 Node.js 20+（React Admin 前端会被嵌入二进制）：

```bash
git clone https://github.com/talkincode/toughradius.git
cd toughradius
make build          # 先构建 web/ 再编译 Go 二进制 → release/toughradius
```

仅后端开发（SQLite 默认配置）：

```bash
make runs           # CGO_ENABLED=0 go run main.go -c toughradius.yml
make runf           # 前端开发服务器 http://localhost:3000/admin
```

## 2. 配置

ToughRADIUS 按以下顺序查找配置：`-c <文件>` 参数、`./toughradius.yml`、 `/etc/toughradius.yml`、内置默认值。环境变量优先于配置文件 （见[运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#环境变量)）。

一份精简的生产风格配置：

```yaml
system:
  appid: ToughRADIUS
  location: Asia/Shanghai
  workdir: /var/toughradius     # 数据/日志/证书都在这里
  debug: false

web:
  host: 0.0.0.0
  port: 1816
  secret: change-me-to-a-long-random-string   # JWT 签名密钥

database:
  type: sqlite                  # 或 postgres（需配 host/port/user/passwd）
  name: toughradius.db          # 存放于 {workdir}/data/ 下

radiusd:
  enabled: true
  host: 0.0.0.0
  auth_port: 1812
  acct_port: 1813
  radsec_port: 2083
  debug: true                   # 输出完整报文转储；生产环境建议关闭

logger:
  mode: production
  file_enable: true
  filename: /var/toughradius/toughradius.log
```

> **务必修改 `web.secret`**，它用于签发管理端登录令牌。首次启动的管理员口令是 随机生成的（或由 `TOUGHRADIUS_ADMIN_PASSWORD` 指定）；请从 `{workdir}/private/admin-bootstrap-password` 读取，并在首次登录后立即修改。

## 3. 初始化数据库并启动

```bash
# 仅第一次执行 —— 会删除并重建全部数据表
toughradius -initdb -c /etc/toughradius.yml

# 启动服务
toughradius -c /etc/toughradius.yml
```

`-initdb` 是破坏性操作；后续升级直接启动即可——结构迁移在启动时自动完成。 其他参数：`-v` 打印版本，`-printcfg` 以 JSON 打印合并后的配置。

## 4. 登录管理界面

打开 `http://<服务器>:1816`。引导管理员：

* 用户名：`admin`
* 密码：写入 `{workdir}/private/admin-bootstrap-password`；若在首次启动前设置了 `TOUGHRADIUS_ADMIN_PASSWORD`，则使用该值

没有公开的默认口令。请在 **账户设置** 中修改引导口令；忘记密码可用 `cmd/reset-password` 重置（见[常见问题](/overview-1/faq)）。

## 5. 登记 NAS 并创建用户

1. **网络节点 → 新建** —— 创建一个节点（逻辑分组），如 `default`。
2. **NAS 设备 → 新建** —— 登记你的网络设备：
   * *IP 地址*：设备发出 RADIUS 报文的源地址。
   * *密钥*：共享密钥，如 `testing123`。
   * *厂商代码*：选择设备厂商（标准 / Cisco / 华为 / MikroTik / H3C / 中兴 / 爱快）——它决定下发哪些厂商私有属性，见[厂商对接指南](/overview-1/vendor-guide)。
   * *CoA 端口*：除非设备使用其他端口，保持 `3799`。
3. **计费策略 → 新建** —— 例如 `100M`：并发数 `1`、上行 `51200` Kbps、 下行 `102400` Kbps。
4. **RADIUS 用户 → 新建** —— 用户名 `test1`、密码 `111111`，选择策略并设置 过期时间。

## 6. 用 radtest 验证

仓库自带一个小型 RADIUS 客户端（示例即默认值）：

```bash
go run ./cmd/radtest auth \
  -server 127.0.0.1 -secret testing123 \
  -username test1 -password 111111

go run ./cmd/radtest flow ...   # 一次跑完 认证 + 计费开始 + 计费结束
```

成功时会打印 `Access-Accept` 及返回的属性。`flow` 模式的会话会出现在 **在线会话** 中，仪表盘计数随之增长。

## 7. 调试

| 需求             | 方法                                                                                                              |
| -------------- | --------------------------------------------------------------------------------------------------------------- |
| 完整 RADIUS 报文转储 | YAML 中 `radiusd.debug: true`（或环境变量 `TOUGHRADIUS_RADIUS_DEBUG=true`），也可在运行时将 **系统配置 → RADIUS → 日志级别** 设为 `debug` |
| 日志文件位置         | `logger.filename`，默认 `{workdir}/toughradius.log`；`logger.mode: development` 输出适合人读的控制台格式                        |
| 用户为何被拒绝        | 拒绝原因按类别计数（密码错误、已过期、MAC 绑定不符等）并显示在仪表盘；细节见日志                                                                      |
| 查看生效配置         | `toughradius -printcfg -c <文件>`                                                                                 |
| 压力测试           | `go run ./cmd/benchmark` —— 见[运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#命令行工具)   |

## 下一步

* [厂商对接指南](/overview-1/vendor-guide) —— 配置 Cisco、华为、MikroTik、H3C、 中兴、爱快及标准设备。
* [管理系统用户手册](/overview-1/admin-manual) —— 管理界面每个页面的说明。
* [运维指南](/overview-1/ops-guide) —— 生产部署、TLS、EAP 证书、备份与监控。


# 厂商对接指南

> English version: [Vendor Integration Guide](/overview/vendor-guide)

ToughRADIUS 对所有设备都讲标准 RADIUS，并为其认识的厂商追加**厂商私有属性 （VSA）**。本章先介绍所有设备通用的对接步骤，再按厂商给出对接案例： ToughRADIUS 下发什么、解析什么，以及设备侧的参考配置。

> **NAS 记录上的厂商代码决定一切。** 属性增强按管理界面中 NAS 设备记录的 *厂商* 字段选择，而不是靠嗅探报文。如果把一台 MikroTik 登记为 `Standard`， 认证不受影响，但**不会**下发 `Mikrotik-Rate-Limit`（即不限速）。请先选对厂商。

> 📖 想要端到端的运维范例（PPPoE 分级套餐、Hotspot + MAC 认证、CoA / 强制下线）？ 见[场景实战手册](/overview-1/cookbook)。本章是属性参考卡，实战手册是照着做的剧本。

> **Portal 边界：** ToughRADIUS 是 RADIUS AAA 后端，不托管 Captive Portal 登录页或访客开户注册流程。见 [Portal / Hotspot 对接边界](/overview-1/portal-hotspot-boundary)。

## 任意设备的通用对接步骤

1. **登记 NAS**：在 **NAS 设备 → 新建** 填写源 IP 地址（或 identifier）、 共享密钥，并选择正确的*厂商*。
2. **设备指向服务器**：认证 UDP `1812`、计费 UDP `1813`、相同的共享密钥。
3. **可选 CoA**：ToughRADIUS 默认向 NAS 的 UDP `3799` 发送 CoA/Disconnect （RFC 5176）；若设备监听其他端口，请在 NAS 记录上设置 *CoA 端口*。 每次交互最多等待 5 秒并重传 2 次。
4. **创建计费策略与用户**，用 `go run ./cmd/radtest auth …` 验证 （见[快速开始](/overview-1/quickstart)）。

### 所有厂商都会收到的标准属性

无论何种厂商，`Access-Accept` 中都可能携带：`Session-Timeout`（距账号过期的 秒数）、`Acct-Interim-Interval`、`Framed-Pool`、`Framed-IP-Address`（静态 IPv4）、`Framed-IPv6-Prefix` / `Framed-IPv6-Address`（RFC 6911）、 `Framed-IPv6-Pool`、`Delegated-IPv6-Prefix`（RFC 4818）与 `Delegated-IPv6-Prefix-Pool` —— 取决于用户/策略中设置了哪些字段。

### 限速单位

策略中的速率以 **Kbps** 存储，各厂商增强器按如下规则换算：

| 厂商              | 属性                                                                 | 下发值                                                                                  |
| --------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| 华为（2011）        | `Huawei-Input/Output-Average-Rate`、`Huawei-Input/Output-Peak-Rate` | 平均 = `速率_kbps × 1024`（bit/s）；峰值再 `× 4`；上限 Int32                                      |
| MikroTik（14988） | `Mikrotik-Rate-Limit`                                              | 字符串 `"{上行}k/{下行}k"`，如 `51200k/102400k`（按路由器视角 rx/tx）                                 |
| H3C（25506）      | `H3C-Input/Output-Average-Rate` 及峰值                                | 与华为相同的 ×1024 / ×4 规则                                                                 |
| 中兴（3902）        | `ZTE-Rate-Ctrl-SCR-Up/Down`                                        | `速率_kbps × 1024`                                                                     |
| 爱快（10055）       | `RP-Upstream/Downstream-Speed-Limit`                               | `速率_kbps × 1024 × 8`，上限 Int32                                                        |
| Cisco（9）、标准（0）  | ——                                                                 | 无限速 VSA——带宽在设备侧；`accept-cisco` enhancer 会下发非限速的 `Cisco-AVPair="ip:addr-pool=<pool>"` |

### 请求解析（MAC 与 VLAN）

默认解析器从 `Calling-Station-Id` 读取 MAC 地址。厂商解析器会在设备族存在 稳定编码时，额外处理请求侧 VSA 或 `NAS-Port-Id`：

* `slot/subslot/port:vlan[.vlan2]` —— 如 `3/0/1:2814.727`
* `vlanid=<n>;vlanid2=<n>;` —— 如 `slot=2;...;vlanid=503;vlanid2=100;`

| 厂商                   | MAC 来源                                                | VLAN 来源                                          | 说明                               |
| -------------------- | ----------------------------------------------------- | ------------------------------------------------ | -------------------------------- |
| 华为（2011）             | `Calling-Station-Id`                                  | `NAS-Port-Id`                                    | 支持上面两种常见编码。                      |
| H3C（25506）           | `Calling-Station-Id`                                  | `NAS-Port-Id`                                    | 支持上面两种常见编码。                      |
| 中兴（3902）             | `Calling-Station-Id`                                  | `NAS-Port-Id`                                    | 支持上面两种常见编码。                      |
| Radback（2352）        | `Mac-Addr` VSA，缺省回退到 `Calling-Station-Id`             | `Bind-Dot1q-Vlan-Tag-Id` VSA，缺省回退到 `NAS-Port-Id` | 仅请求侧 parser；当前未内置 Radback 响应增强器。 |
| Alcatel（3041）        | `AAT-User-MAC-Address` VSA，缺省回退到 `Calling-Station-Id` | 有值时解析 `NAS-Port-Id`                              | 仅请求侧 parser；响应属性仍按部署需求定制。        |
| Aruba/HPE（14823）     | `Calling-Station-Id`                                  | `Aruba-User-Vlan` VSA，缺省回退到 `NAS-Port-Id`        | 同时具备 Access-Accept 增强器，见下文。      |
| Juniper（2636）        | `Calling-Station-Id`                                  | `Juniper-VoIP-Vlan` VSA，缺省回退到 `NAS-Port-Id`      | 仅请求侧 parser；响应属性仍按部署需求定制。        |
| MikroTik、爱快、Cisco、标准 | `Calling-Station-Id`                                  | ——                                               | 无厂商专用 VLAN parser；请使用设备侧策略或二次开发。 |

只要设备发送可识别的 MAC，MAC 绑定即可工作；VLAN 绑定则要求 NAS 命中上表中 支持 VLAN 的解析器，并且请求报文使用对应编码。

> 下文设备侧片段均为**参考示例**——命令语法随型号与系统版本而异，请以厂商 文档为准。

***

## MikroTik（RouterOS）—— 厂商代码 14988

最经典的对接：PPPoE / Hotspot 配合 `Mikrotik-Rate-Limit`。

ToughRADIUS 下发 `Mikrotik-Rate-Limit = "{up}k/{down}k"`，RouterOS 将其应用为 动态 simple queue（rx/tx 按路由器视角，即先用户上行）。

```routeros
/radius add service=ppp,hotspot address=<TOUGHRADIUS_IP> secret=<SECRET> \
    timeout=3s
/radius incoming set accept=yes port=3799
/ppp aaa set use-radius=yes accounting=yes interim-update=5m
```

* `radius incoming accept=yes` 开启 UDP 3799 上的 CoA/Disconnect。
* Hotspot 场景：在 hotspot server profile 中启用 RADIUS （`/ip hotspot profile set ... use-radius=yes`）。

## 华为 —— 厂商代码 2011

典型的 BRAS（ME60/NE 系列）/ 汇聚场景。ToughRADIUS 下发速率四元组 （`Huawei-Input/Output-Average-Rate`，峰值 ×4）、`Huawei-Domain-Name` （用户/策略配置了域时）以及静态 IPv6 的 `Huawei-Framed-IPv6-Address`。 华为解析器可从 `NAS-Port-Id` 提取 VLAN，MAC 与 VLAN 绑定均可用。

```
radius-server template tr_tpl
 radius-server shared-key cipher <SECRET>
 radius-server authentication <TOUGHRADIUS_IP> 1812
 radius-server accounting <TOUGHRADIUS_IP> 1813
#
aaa
 authentication-scheme auth_radius
  authentication-mode radius
 accounting-scheme acct_radius
  accounting-mode radius
  accounting interim interval 5
 domain default
  authentication-scheme auth_radius
  accounting-scheme acct_radius
  radius-server tr_tpl
```

如需 CoA/Disconnect，请在设备上启用 RADIUS 动态授权扩展 （`radius-server authorization` 指向服务器地址）。

## Cisco —— 厂商代码 9

Cisco 设备使用标准属性认证（PAP / CHAP / MS-CHAPv2 / EAP 均可；会话、计费、 CoA 同样可用）。当用户套餐配置了地址池时，`accept-cisco` enhancer 会在标准 `Framed-Pool` 之外下发 `Cisco-AVPair="ip:addr-pool=<pool>"`；除此之外不下发其它 Cisco 私有属性。带宽策略请在设备侧实施（Cisco 无可移植的数值限速 VSA），或基于 内置的 `Cisco-AVPair` 字典自行扩展。

```
aaa new-model
radius server TOUGHRADIUS
 address ipv4 <TOUGHRADIUS_IP> auth-port 1812 acct-port 1813
 key <SECRET>
aaa authentication ppp default group radius
aaa accounting network default start-stop group radius
aaa server radius dynamic-author
 client <TOUGHRADIUS_IP> server-key <SECRET>
```

`aaa server radius dynamic-author` 启用 CoA/Disconnect（默认端口 3799）。

## H3C —— 厂商代码 25506

速率语义与华为一致（`H3C-Input/Output-Average-Rate` ×1024，峰值 ×4）。 H3C 解析器可提取 VLAN，支持 VLAN 绑定。

```
radius scheme tr_scheme
 primary authentication <TOUGHRADIUS_IP> 1812
 primary accounting <TOUGHRADIUS_IP> 1813
 key authentication simple <SECRET>
 key accounting simple <SECRET>
 user-name-format without-domain
#
domain default enable system
 authentication ppp radius-scheme tr_scheme
 accounting ppp radius-scheme tr_scheme
```

## 中兴 —— 厂商代码 3902

ToughRADIUS 下发 `ZTE-Rate-Ctrl-SCR-Up/Down`（速率 ×1024），并从 `NAS-Port-Id` 解析 VLAN。中兴 BRAS 的配置与华为类似，采用 radius 模板 + 域 的模式：将认证/计费模板绑定到服务器地址、密钥及 1812/1813 端口即可。

## 爱快（iKuai）—— 厂商代码 10055

国内常见的中小企业网关。ToughRADIUS 下发 `RP-Upstream-Speed-Limit` / `RP-Downstream-Speed-Limit` （= `速率_kbps × 8192`，上限 Int32）。在爱快 Web 控制台：**认证计费 → RADIUS 计费** —— 填写服务器地址、端口 1812/1813 与共享密钥，并在 PPPoE 服务端设置中启用 RADIUS。

## Aruba/HPE —— 厂商代码 14823

Aruba/HPE 同时具备请求解析与 `Access-Accept` 响应增强能力。请求解析器会优先 读取报文中的 `Aruba-User-Vlan`，并保留与其他 VLAN-aware 解析器一致的 `NAS-Port-Id` 回退。

`Access-Accept` 中 ToughRADIUS 会下发：

| 属性                | 来源               | 边界 / no-op 规则                                                                  |
| ----------------- | ---------------- | ------------------------------------------------------------------------------ |
| `Aruba-User-Vlan` | 用户或策略的 `Vlanid1` | 仅在 VLAN ID 为 `1..4094` 时下发；缺省、`0`、`4095` 或负值都会省略。`Vlanid2` 没有 Aruba 属性映射，不会下发。 |
| `Aruba-User-Role` | 用户或策略的 `Domain`  | 继承后的 domain / role 字段非空且不为 `N/A` 时下发。                                          |

非 Aruba NAS 不会追加 Aruba VSA；字段未设置时也不会写入占位值。

## 标准 / 其他设备 —— 厂商代码 0

任何符合 RFC 的 NAS（pfSense、strongSwan、各类 Wi-Fi 控制器等）都能以 `Standard` 厂商代码对接 ToughRADIUS：凭据校验、会话控制、计费、IPv4/IPv6 属性一应俱全——只是没有私有限速属性。代码库还内置了更多厂商的属性**字典** （Microsoft、F5、PfSense、Hillstone 等）供二次开发。对 Juniper、Alcatel、 Aruba、Radback 而言，当前能力不再只是“仅字典”：请以上文的请求解析器与 Aruba 增强器边界为准。单纯字典仍只定义属性；只有注册 parser 或 enhancer 后， 才会解析请求或增强响应。

## 对接排障

| 现象                         | 可能原因                                                              |
| -------------------------- | ----------------------------------------------------------------- |
| 设备收到 `Access-Accept` 但没有限速 | NAS 记录厂商为 `Standard`，或设备忽略该 VSA——先检查厂商代码                          |
| 所有请求被静默丢弃                  | 源 IP 未登记为 NAS，或共享密钥不一致                                            |
| VLAN 绑定始终不匹配               | NAS 厂商未命中上表中的 VLAN-aware parser，或请求使用了非预期的 VSA / `NAS-Port-Id` 格式 |
| CoA/Disconnect 超时          | 设备未开启 CoA 监听，或端口非默认——在 NAS 记录上设置 *CoA 端口*                         |

更多见[常见问题](/overview-1/faq)。


# Portal / Hotspot 对接边界

> English version: [Portal / Hotspot Integration Boundary](/overview/portal-hotspot-boundary)

本章定义 Captive Portal / Hotspot 场景的产品边界。

## 铁律

**ToughRADIUS 不提供、不托管、不运营 Portal 登录页。**

Portal Server、访客开户注册、临时券码、短信/微信登录、支付流程、广告页、 准入放行控制等能力，属于独立的 Portal / 网关产品，不属于 ToughRADIUS 的产品 范围。

ToughRADIUS 只做 RADIUS AAA：

* UDP `1812` 上的认证；
* UDP `1813` 上的计费；
* 通过 RADIUS 属性、计费记录和可选 CoA / Disconnect 完成会话策略与审计；
* 在现有厂商 parser / enhancer 模型内，安全下发可表达为 RADIUS 属性的厂商能力。

## 支持的形态

支持的对接模型是：

```
客户端 -> NAS / WLAN 控制器 / 网关 Portal -> RADIUS -> ToughRADIUS
```

NAS、WLAN 控制器、Hotspot 网关或外部 Portal 产品负责：

* HTTP/HTTPS Portal 跳转；
* 登录页与用户交互；
* 认证前 / 认证后的网络放行控制；
* 厂商 Portal 回调或私有 Portal 协议；
* 设备侧会话准入与释放。

ToughRADIUS 负责：

* 用户、Profile、NAS 与策略数据；
* Access-Accept / Access-Reject 决策；
* 计费记录和在线会话状态；
* 标准与厂商 RADIUS 属性，例如超时、地址池、速率、VLAN、角色，或已支持厂商 enhancer 下发的 Portal URL；
* NAS 支持时的 CoA / Disconnect。

## 对 Hotspot 的含义

MikroTik Hotspot、华为 / H3C / 爱快 / Cisco WLAN 控制器、Aruba Captive Portal 等设备，仍然可以在设备自身负责 Portal 的前提下对接 ToughRADIUS。 设备是 Portal 实现方；本项目是 AAA 后端。

常见支持场景：

* Hotspot / PPPoE / WLAN 控制器向 ToughRADIUS 发送 Access-Request。
* 已登记设备通过 MAC 认证跳过 Portal 页面。
* Accounting update 维护在线会话与流量数据。
* NAS 支持时，通过 CoA / Disconnect 刷新或踢下会话。
* 厂商属性可以引导设备侧行为，但只能作为 RADIUS 属性下发；这不会把 ToughRADIUS 变成 Portal Server。

## 明确不做

不要把以下能力加入 ToughRADIUS：

* 面向访客或用户的托管登录页；
* 券码、二维码、短信、微信、OAuth 或支付开户注册流程；
* Captive Portal 前端应用或客户自助门户；
* 厂商私有 Portal Server 回调协议的一等子系统；
* 复制 NAS / 控制器职责的厂商 Portal 状态机；
* 通用营销、广告、CRM 或访客管理功能。

如果部署确实需要这些能力，应使用独立 Portal 产品，并通过 RADIUS 与 ToughRADIUS 对接。

## 允许的窄扩展

Portal 相关工作只有在留在 RADIUS 边界内时才允许：

1. 为 Captive Portal URL、用户角色、Filter-Id、VLAN、Session-Timeout、 速率等 RADIUS 属性补充或修复厂商字典、parser、enhancer。
2. 编写特定 NAS / 控制器如何把 ToughRADIUS 作为 RADIUS 后端的配置文档。
3. 为请求解析、响应属性、计费或 CoA 行为补测试。

任何“让 ToughRADIUS 自带 Portal”的需求，在实现前都必须拒绝或转移到其他产品。


# 场景实战手册

> English version: [Scenario Cookbook](/overview/cookbook)

[厂商对接指南](/overview-1/vendor-guide)是一张**属性参考卡**——它告诉你某个厂商 ToughRADIUS 会下发/解析哪些属性。本手册更进一步：以**真实运维场景**为单位， 端到端地把「业务诉求」翻译成「服务端配置 + 设备侧配置 + 验证 + 排障」。

## 每个场景的五段式

为了便于照着做、也便于排错，本手册的每个场景都用同一结构：

1. **需求 / 场景** —— 用业务语言描述要解决的问题，不谈协议细节。
2. **ToughRADIUS 侧** —— 在管理后台具体配置什么；以及认证通过后**实际下发** 哪些属性、由哪段代码产生。
3. **设备侧** —— NAS/路由器侧的参考配置。
4. **验证** —— 如何确认它真的生效（radtest、设备命令、管理后台）。
5. **排障** —— 以「症状 → 定位 → 解决」列出该场景最常见的坑。

## 阅读约定

* **ToughRADIUS 侧的每条声明都锚定代码**：下发的属性来自 `internal/radiusd/plugins/auth/enhancers/` 的增强器，准入/拒绝判定来自 `internal/radiusd/plugins/auth/checkers/` 的检查器。它描述的是系统**真实 行为**，而非美好设想。
* **设备侧配置均为参考示例**：命令语法随型号与系统版本而异，请以厂商文档与 实际固件为准。
* **CoA / Disconnect 端口为 3799**（RFC 5176）。网络上常见的 `1700` 是某些 客户端的本地端口，不是本系统使用的目标端口。
* 速率在计费策略中以 **Kbps** 存储（界面标注单位）。换算规则见 [厂商对接指南 · 限速单位](https://docs.toughradius.net/overview-1/pages/9XEd83OYKE5jDCwuDPVA#限速单位)。

## 现有实战手册

* [MikroTik RouterOS](/overview-1/cookbook/cookbook-mikrotik) —— PPPoE 宽带 ISP 分级套餐、 Hotspot + MAC 认证、CoA / 强制下线与 FUP，以及 WPA2/WPA3 企业级 802.1X EAP 透传（EAP-TLS / PEAP / TTLS）。
* [华为 BRAS / NetEngine](/overview-1/cookbook/cookbook-huawei) —— 带峰值速率与 AAA 域的宽带 分级套餐、线路防盗用（MAC + VLAN 绑定）与双栈 IPv6、CoA / 强制下线与 FUP。
* [H3C / 中兴 / 爱快 / Cisco](/overview-1/cookbook/cookbook-vendors) —— 其余主流厂商的差异速查 （下发的限速属性、单位倍率、MAC / VLAN 解析）；场景机制照两本旗舰手册即可。

> **规划中（路线图 M13.8 后续批次）**：按需补充标准属性 / Wi-Fi 控制器等场景。 任意厂商的属性级细节，见[厂商对接指南](/overview-1/vendor-guide)。

## 相关章节

* [快速开始](/overview-1/quickstart) —— 安装、首登、`radtest` 验证。
* [厂商对接指南](/overview-1/vendor-guide) —— 各厂商属性与解析参考卡。
* [管理系统用户手册](/overview-1/admin-manual) —— 用户、计费策略、在线会话、CoA。
* [常见问题解答](/overview-1/faq) —— 跨场景的排障问答。


# MikroTik RouterOS

> English version: [Cookbook: MikroTik RouterOS](/overview/cookbook/cookbook-mikrotik)
>
> 本章是[场景实战手册](/overview-1/cookbook)的一部分，沿用其[五段式与阅读约定](https://docs.toughradius.net/overview-1/cookbook/pages/fxPJ6fqamdijKi5TOxQO#每个场景的五段式)。

MikroTik RouterOS（厂商代码 **14988**）是最常见的对接对象。ToughRADIUS 为其 注册了专门的厂商增强器，认证通过后下发：

* `Mikrotik-Rate-Limit = "{上行}k/{下行}k"` —— 字符串限速（由 `mikrotik_enhancer.go` 产生；`rx/tx` 按**路由器视角**，即第一段是用户上行）。
* 以及所有设备通用的标准属性：`Session-Timeout`、`Acct-Interim-Interval`、 `Framed-Pool`、`Framed-IP-Address` 等（由 `default_enhancer.go` 产生）。

> **前置条件**：已在 **NAS 设备** 中把这台路由器登记为 *厂商 = MikroTik*、填写 正确的源 IP 与共享密钥；ToughRADIUS 可被设备访问（认证 1812、计费 1813）。 若登记成 `Standard`，认证仍可成功，但**不会**下发 `Mikrotik-Rate-Limit`。

***

## 场景 A：PPPoE 宽带 ISP —— 分级套餐 + 地址池 + 到期断网 + 并发限制

### 需求 / 场景

一个小区 / 小型 ISP 用 PPPoE 拨号上网，需要：多档套餐（如家庭版下行 30M、 企业版下行 100M），账号到期自动断网且无法再拨，每账号限定并发会话数（防一号 多拨），IP 由统一地址池分配。

### ToughRADIUS 侧

1. **为每档套餐建一个计费策略**（**计费策略 → 新建**）：
   * **上行 / 下行速率**：单位为 **Kbps**。下行 30M 应填 `30720`，**不是** `30`； 上行 10M 填 `10240`。
   * **并发数 `active_num`**：如 `1` 表示单账号最多 1 个在线会话（`0` = 不限）。
   * **地址池**：填地址池名（如 `pppoe-pool`），需与路由器上的 `/ip pool` 同名。
2. **创建用户**（**用户 → 新建**）：用户名 / 密码、选择对应**计费策略**（速率、 并发、地址池由策略继承）、设置**过期时间**、状态 = 启用。如需固定 IP，可在用户 上设置静态 IPv4（将覆盖地址池）。
3. **计费上报间隔**：系统配置项 `radius.AcctInterimInterval`（默认 `120` 秒）决定 下发的 `Acct-Interim-Interval`。

认证通过后，`Access-Accept` 实际携带（锚定代码）：

| 属性                      | 取值                                        | 来源                     |
| ----------------------- | ----------------------------------------- | ---------------------- |
| `Mikrotik-Rate-Limit`   | `"{上行kbps}k/{下行kbps}k"`，如 `10240k/30720k` | `mikrotik_enhancer.go` |
| `Session-Timeout`       | 距过期时间的剩余秒数（到点断开当前会话）                      | `default_enhancer.go`  |
| `Acct-Interim-Interval` | `radius.AcctInterimInterval`（默认 120）      | `default_enhancer.go`  |
| `Framed-Pool`           | 策略 / 用户设置的地址池名（设置了才下发）                    | `default_enhancer.go`  |
| `Framed-IP-Address`     | 用户的静态 IPv4（设置了才下发）                        | `default_enhancer.go`  |

> **并发限制不是靠下发属性实现的**：它由 `online_count_checker` 在认证阶段依据 `active_num` 与当前在线数判定——超限的新会话直接被拒（`Access-Reject`）。

### 设备侧（RouterOS，参考示例，以实际固件为准）

```routeros
# 指向 ToughRADIUS（认证/计费同一共享密钥）
/radius add service=ppp address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s
/radius incoming set accept=yes port=3799

# 开启 RADIUS 认证 + 计费 + 周期上报
/ppp aaa set use-radius=yes accounting=yes interim-update=5m

# 地址池名必须与下发的 Framed-Pool 一致
/ip pool add name=pppoe-pool ranges=10.10.0.2-10.10.255.254

# PPPoE 服务（remote-address 留给 RADIUS 的 Framed-Pool/Framed-IP 决定）
/ppp profile add name=radius-pppoe local-address=10.10.0.1
/interface pppoe-server server add service-name=isp interface=<bridge> \
    default-profile=radius-pppoe disabled=no
```

> 限速无需手工建队列：路由器收到 `Mikrotik-Rate-Limit` 后会自动生成动态 simple queue。

### 验证

* **radtest（服务端）**：

  ```bash
  go run ./cmd/radtest auth -server <TOUGHRADIUS_IP> -nas-ip <NAS_IP> \
    -username <用户名> -password <密码> -secret <SECRET>
  ```

  成功时打印 `Access-Accept`，应能看到 `Mikrotik-Rate-Limit`、`Session-Timeout`， 以及（若设置了池）`Framed-Pool`。
* **路由器侧**：`/ppp active print`（在线连接）、`/queue simple print`（动态队列 及其速率）、`/log print where topics~"radius"`。
* **管理后台**：**在线会话** 页应出现该会话（Framed IP、时长、上下行流量）。

### 排障（症状 → 定位 → 解决）

* **限速完全不生效** → ① 确认 NAS 登记为 *MikroTik*（登记成 Standard 不下发 VSA）； ② 速率单位填错（填了 `30` 而非 `30720`）；③ 方向记反（`rx/tx` 是路由器视角， 第一段是用户**上行**）。
* **拨号成功但拿不到 IP / 地址不对** → `Framed-Pool` 名与 `/ip pool` 名不一致， 或策略未设地址池；改为同名，或在用户上设静态 IP。
* **账号到期后仍能上网** → `Session-Timeout` 只在到点时断开**当前**会话；已在线 的旧会话需等超时或在会话页手动**强制下线**。到期后**再次拨号**会被 `expire_checker` 拒（拒绝指标计入 `user expire`）。
* **第二条连接被拒** → `active_num=1` 时 `online_count_checker` 拒绝并发，这是预期 行为；要允许多拨，调大计费策略的并发数。
* **连续认证失败后服务端变慢 / 不回包** → `reject_delay_guard` 在某用户名连续被拒 超过阈值（默认 7 次）后引入延迟以抑制暴力破解，稍后自动恢复。

***

## 场景 B：Hotspot + MAC 认证

### 需求 / 场景

公共 WiFi / Hotspot 环境下，希望部分已登记设备（打印机、IoT、长期访客设备） **免门户**、直接按 MAC 地址放行并限速。

### ToughRADIUS 侧

ToughRADIUS 判定一次请求是否为 **MAC 认证**的条件是（锚定代码 `auth_stages.go` / `eap_helper.go`）：

> 当请求中解析出的 MAC 地址 **等于用户名** 时，即视为 MAC 认证；此时密码比对使用 该用户记录的 **MAC 地址** 字段，而非普通密码。

因此配置方式为：

1. **创建用户**，**用户名 = 设备 MAC**（字符串需与路由器实际发送的格式完全一致）， 并在用户记录的 **MAC 地址** 字段填入同一 MAC。
2. 给该用户分配计费策略（速率、并发照常生效）。

> **最大的坑是 MAC 格式**：大小写与分隔符必须与 RouterOS hotspot 发送的 `User-Name` 完全一致（ToughRADIUS 按字符串精确匹配）。RouterOS 的发送格式受 `mac-auth-mode` 等设置影响——**存什么就必须发什么**。

### 设备侧（RouterOS，参考示例，以实际固件为准）

```routeros
/radius add service=hotspot address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s

# 在 hotspot server profile 上启用 RADIUS 与 MAC 登录
/ip hotspot profile set <profile> use-radius=yes login-by=mac,http-chap
# mac-auth-mode / mac-auth-password 视固件而定
```

### 验证

* **radtest**：用 `-user <MAC> -pwd <MAC>` 模拟一次 MAC 认证，观察是否 `Access-Accept`。
* **路由器侧**：`/ip hotspot active print` 应出现该设备；`/log print` 查看认证日志。
* **管理后台**：在线会话页出现对应会话。

### 排障（症状 → 定位 → 解决）

* **MAC 认证总是失败** → 用户名 / MAC 字段与路由器实际发送的 MAC 字符串 格式不一致（大小写、分隔符）。抓一次请求看 `User-Name` 原文，按原文重建用户。
* **被当成普通账号在认证** → 只有「请求 MAC == 用户名」才触发 MAC 认证路径；若 hotspot 未按 MAC 登录，请求会走门户用户名 / 密码逻辑。

***

## 场景 C：在线管控 —— CoA、强制下线与 FUP

### 需求 / 场景

对在线用户做实时管控：超出流量配额后限速（FUP）、临时强制重新认证，或直接把 某会话踢下线。

### ToughRADIUS 侧

在 **在线会话** 页选中某会话，可执行两类动作（锚定 `session_actions.go` 与 [管理系统用户手册 · 在线会话](https://docs.toughradius.net/overview-1/cookbook/pages/bvFfYEZ7jlIollwyH4WI#在线会话)）：

* **修改授权（CoA-Request）**：当前仅携带 **`Session-Timeout`（#27）** 和 / 或 **`Filter-Id`（#11）**。
  * 用 `Session-Timeout` 缩短会话寿命，促使客户端尽快重新认证。
  * 用 `Filter-Id` 让 RouterOS 套用一个**预先定义好的** filter / address-list （例如一条限速或限站规则）。
* **强制下线（Disconnect-Request）**：直接终止该会话（操作需确认）。

> **实现 FUP「在线变速」的正确路径**：ToughRADIUS 的 CoA **不直接改写** `Mikrotik-Rate-Limit`。要让某用户实时变速，标准做法是——先在计费策略 / 用户上 改速率，**再对其发强制下线**；客户端自动重拨后即按新速率重新授权。这条路径与 系统代码能力一致，适用于任意厂商。

操作员发起的 CoA / Disconnect 使用较短超时并自动重传一次，目标为 NAS 记录上的 **CoA 端口（默认 3799）**。

### 设备侧（RouterOS，参考示例，以实际固件为准）

```routeros
# 必须开启，否则收不到 CoA/Disconnect
/radius incoming set accept=yes port=3799
```

* 防火墙需放行**入向 UDP 3799**（从 ToughRADIUS 到路由器）。
* 若使用 `Filter-Id` 方案，需在 RouterOS 预先定义同名的 filter / queue / address-list（以实际固件为准）。

### 验证

* 在会话页点击 **修改授权** / **强制下线**，结果以通知形式反馈。
* 路由器侧 `/log print` 可见 incoming 请求；**强制下线**后 `/ppp active print` 中该会话消失，随后客户端自动重拨。

### 排障（症状 → 定位 → 解决）

* **CoA / Disconnect 无响应或超时** → ① RouterOS 未 `radius incoming accept=yes`； ② 防火墙挡了 UDP 3799；③ NAS 记录的 CoA 端口与设备 `incoming port` 不一致； ④ 共享密钥不一致。
* **改了速率但在线用户没变速** → 这是预期：CoA 不改限速，需**强制下线**让客户端 重拨后生效。
* **端口困惑** → 网上常说的 CoA `1700` 是某些客户端的本地端口；本系统与 RFC 5176 使用 **3799**。

***

## 场景 D：WPA2/WPA3 企业级 Wi-Fi —— 802.1X EAP 透传到 ToughRADIUS

### 需求 / 场景

你在 MikroTik AP 上跑企业级 Wi-Fi（`WPA2-EAP` / `WPA3-EAP`，即 802.1X），希望把 账号、证书与策略**集中到一处**：ToughRADIUS。员工既可以用**客户端证书**登录 （EAP-TLS，无需密码），也可以用**用户名 + 密码**登录（面向 Windows/AD 风格客户端 的 PEAP-MSCHAPv2，或面向老账号库 / LDAP 后端的 EAP-TTLS）。

> **本场景与** [**`multiduplikator/mikrotik_EAP`**](https://github.com/multiduplikator/mikrotik_EAP) **的区别。** 那篇广为流传的指南让 RouterOS **自己**充当 EAP 服务器（ROS6 直连证书库，或 ROS7 用 **User Manager v5**）。而这里 MikroTik 只是**认证者（authenticator）**：用 `eap-methods=passthrough` 把 802.1X/EAP 会话**透传**给 ToughRADIUS，由 **ToughRADIUS 终结 EAP**。由此带来两个必须提前规划的差异：
>
> 1. **信任锚转移到 ToughRADIUS。** 客户端不再信任 `RouterCA`，而要信任 **签发 `EapTlsServerCert` 所选 ToughRADIUS 服务器证书的 CA**。你要把*那个* CA 分发到客户端设备。
> 2. **路由器不再保存任何用于认证的用户/证书材料。** 所有身份都在 ToughRADIUS 上， 因此账号生命周期、限速策略、在线会话视图与计费都直接复用 ToughRADIUS。

### ToughRADIUS 侧

#### 0. 准备用于导入的证书

先生成 PEM 材料，再导入 ToughRADIUS 托管证书库。用 `openssl` 建一个最小自建 CA —— 用 EC 密钥可让 TLS 记录更小，对 EAP 分片更友好：

```bash
# 根 CA（把 ca.pem 分发到每台客户端设备）
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.pem \
  -subj "/CN=ToughRADIUS EAP Root CA"

# RADIUS 服务器证书（CN/SAN 即客户端钉住的服务器身份）
openssl ecparam -name prime256v1 -genkey -noout -out server.key
openssl req -new -key server.key -out server.csr -subj "/CN=radius.example.com"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -days 825 -sha256 -out server.pem \
  -extfile <(printf "subjectAltName=DNS:radius.example.com\nextendedKeyUsage=serverAuth\nkeyUsage=digitalSignature,keyEncipherment")

# EAP-TLS 客户端证书（仅 EAP-TLS 用户需要）。SAN email 将成为 RADIUS 身份
# —— 见下方身份绑定规则。
openssl ecparam -name prime256v1 -genkey -noout -out alice.key
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice"
openssl x509 -req -in alice.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -days 825 -sha256 -out alice.pem \
  -extfile <(printf "subjectAltName=email:alice@example.com\nextendedKeyUsage=clientAuth\nkeyUsage=digitalSignature")

# 把客户端 私钥+证书+CA 打包成 PKCS#12，便于 Windows/Android/iOS 导入
openssl pkcs12 -export -inkey alice.key -in alice.pem -certfile ca.pem \
  -out alice.p12 -passout pass:'<长口令>'
```

> **EAP-TLS 身份绑定（锚定 `tlsengine/identity.go`，RFC 5216 §5.2）。** 客户端证书通过链校验后，ToughRADIUS 按此顺序推导 **Peer-Id**：**SAN `rfc822Name` （email）→ SAN `dnsName` → 主题 `CN`**，并要求其与 RADIUS `User-Name` 大小写不敏感地相等。当存在 SAN 时，**不再**接受 `CN` 作为备选。因此用上面这张证书， ToughRADIUS 里要匹配的用户名是 **`alice@example.com`**，不是 `alice`。

#### 1. 导入材料并选择托管证书

打开 **证书管理** 并导入：

* 将 `server.pem` 与匹配的 `server.key` 作为服务器证书导入，例如本地名称 `eap-server`。
* 将 `ca.pem` 作为客户端 CA 证书导入，例如本地名称 `eap-client-ca`。

然后打开 **系统配置 → RADIUS**，在 EAP 设置中选择这些名称（锚定 `internal/app/config_schemas.json` / `eap/handlers/tls_config.go`）：

| 配置项（`radius.*`）                           | EAP-TLS                     | PEAP / TTLS             | 说明                                                |
| ----------------------------------------- | --------------------------- | ----------------------- | ------------------------------------------------- |
| **EAP 认证方式**（`EapMethod`）                 | `eap-tls`                   | `eap-peap` / `eap-ttls` | EAP-Identity 阶段**首先提供**的方式。                       |
| **启用的 EAP 处理器**（`EapEnabledHandlers`）     | `eap-tls,eap-peap,eap-ttls` | 同上                      | 允许清单；`*` 表示全部。客户端可 **NAK** 改用其它方式，但仅当此处启用了该方式才生效。 |
| **EAP-TLS 服务器证书**（`EapTlsServerCert`）     | `eap-server`                | 同上                      | 三种隧道方式**都**出示它；该托管证书必须包含匹配私钥。                     |
| **EAP-TLS 客户端 CA 证书**（`EapTlsClientCa`）   | `eap-client-ca`             | *(不使用)*                 | **EAP-TLS 必需**（校验客户端证书）；PEAP/TTLS 仅服务器认证，忽略它。     |
| **EAP-TLS 最低 TLS 版本**（`EapTlsMinVersion`） | `1.2`                       | `1.2`                   | PEAP/TTLS 无论如何都钉死在 **TLS 1.2**。                   |

> 托管证书选择会在**两次握手之间**重新解析，因此可不重启 RADIUS 服务直接轮换导入的 证书。在所需托管证书未选择前，EAP 会**安全拒绝**（`ErrTLSNotConfigured`），而不会在 缺少信任锚的情况下放行 —— 半配置状态绝不会放任何人进来。

#### 2. 创建用户

| 方式                             | 要创建的用户名                              | 密码字段          | 出处                                               |
| ------------------------------ | ------------------------------------ | ------------- | ------------------------------------------------ |
| **EAP-TLS**                    | 证书 **Peer-Id**，如 `alice@example.com` | **不使用**（任意占位） | `tlsengine/identity.go` 中匹配证书身份                  |
| **PEAP-MSCHAPv2**              | 登录名，如 `bob`                          | 用户真实密码        | `peap_inner.go` 内层 MSCHAPv2 比对 `GetPassword`     |
| **EAP-TTLS**（PAP 或 MS-CHAP-V2） | 登录名，如 `carol`                        | 用户真实密码        | `ttls_inner.go` 内层 PAP/MSCHAPv2 比对 `GetPassword` |

每个用户仍需 **状态 = 启用**、**未过期** 且绑定 **限速策略**（速率 / 地址池 / 并发与 场景 A 完全一致）。

> **透传场景最大的坑 —— 外层（匿名）身份。** ToughRADIUS 用**外层 `User-Name`** 加载用户记录，并从*该*记录取密码；把单独的 *匿名*外层身份映射到真实账号的能力尚未实现（推迟到 M8.4）。因此 **PEAP / TTLS 的外层身份必须等于真实用户名** —— 在客户端上把“匿名身份”**留空** （此时它会在明文外层身份里发送真实用户名），**或**把它设成与用户名相同。外层身份写成 `anonymous` 会被判为*用户不存在*而拒绝。EAP-TLS 不受影响（其身份来自证书）。

### 设备侧（RouterOS，参考示例，请按你的固件核对）

先在 ToughRADIUS 的 **NAS 设备** 中登记这台路由器（源 IP + 共享密钥；若你还想下发 `Mikrotik-Rate-Limit` 就选厂商 *MikroTik*，否则 *Standard* —— EAP 本身不需要 VSA）。 然后把 AP 指向 ToughRADIUS，并让安全配置文件**透传 EAP**：

```routeros
# 1) wireless 服务的 RADIUS 服务器（密钥与 NAS 记录一致）
/radius add service=wireless address=<TOUGHRADIUS_IP> secret=<SECRET> timeout=3s

# 2a) 经典 /interface wireless —— passthrough 是关键词
/interface wireless security-profiles add name=eap-passthrough \
    authentication-types=wpa2-eap eap-methods=passthrough \
    radius-eap-accounting=yes \
    unicast-ciphers=aes-ccm group-ciphers=aes-ccm
/interface wireless set wlan1 security-profile=eap-passthrough \
    ssid="ToughRADIUS-EAP" mode=ap-bridge disabled=no

# 2b) …或 CAPsMAN（与参考仓库的拓扑一致）
/caps-man security add name=eap-passthrough authentication-types=wpa2-eap \
    encryption=aes-ccm group-encryption=aes-ccm eap-methods=passthrough \
    eap-radius-accounting=yes
# 把 security=eap-passthrough 挂到你的 /caps-man configuration，再 provision

# 2c) …或新版 /interface wifi（wifiwave2，ROS 7.13+）：透传是隐式的
/interface wifi security add name=eap-sec authentication-types=wpa2-eap,wpa3-eap
/interface wifi set wifi1 security=eap-sec \
    configuration.ssid="ToughRADIUS-EAP" disabled=no
```

> `eap-methods=passthrough`（经典 / CAPsMAN）正是让路由器从 EAP 终结者变成中继的开关。 新版 `/interface wifi` 没有 `eap-methods` 选项 —— 选用 `wpa2-eap`/`wpa3-eap` 安全配置 后，它会自动把请求中继给配置了 `service=wireless` 的 RADIUS。

### 验证

`radtest` **无法**驱动 EAP。请用 `eapol_test`（来自 `wpa_supplicant` / hostap）—— 即本项目 [EAP 验收测试报告](/overview-1/eap-acceptance-reports) 所用的工具（v2.10）。它直接对 ToughRADIUS 讲 RADIUS，因此你能在**接触真实射频之前**先验证服务器。存成下列任一文件后运行 `eapol_test -c <文件>.conf -a <TOUGHRADIUS_IP> -p 1812 -s <SECRET>`，通过会打印 `SUCCESS`：

```ini
# eap-tls.conf  —— 证书，无需密码
network={
    key_mgmt=WPA-EAP
    eap=TLS
    identity="alice@example.com"      # == 证书 SAN email == TR 用户名
    ca_cert="/etc/eap/ca.pem"         # 信任 ToughRADIUS 的服务器证书
    client_cert="/etc/eap/alice.pem"
    private_key="/etc/eap/alice.key"
}
```

```ini
# peap-mschapv2.conf
network={
    key_mgmt=WPA-EAP
    eap=PEAP
    identity="bob"                    # 外层 == 真实用户名（不要匿名身份）
    password="<bob-密码>"
    ca_cert="/etc/eap/ca.pem"
    phase2="auth=MSCHAPV2"
}
```

```ini
# ttls-pap.conf
network={
    key_mgmt=WPA-EAP
    eap=TTLS
    identity="carol"
    anonymous_identity="carol"        # 必须等于 identity（见上面的坑）
    password="<carol-密码>"
    ca_cert="/etc/eap/ca.pem"
    phase2="auth=PAP"                 # 或 auth=MSCHAPV2；不支持 CHAP/MS-CHAP
}
```

* **ToughRADIUS 日志**：成功时记录 `radius auth success … is_eap=true result=success`；会话出现在**在线会话**页 （射频上线后即带计费）。
* **路由器侧**：`/log print where topics~"radius,wireless"`；查看已关联客户端用 `/interface wireless registration-table print`（经典）或 `/interface wifi registration-table print`（wifi）。
* **EAP-TLS 与 EAP-TTLS（PAP 与 MS-CHAP-V2）** 用 `eapol_test` 可端到端干净通过。 而 **PEAP-MSCHAPv2** 目前在 `eapol_test` 上存在已记录的内层封帧互通缺口，因此验收流水 线改用进程内集成测试验证 PEAP（见 [EAP 验收测试报告](/overview-1/eap-acceptance-reports)）；真实的 Windows / Android / iOS PEAP 请求方可正常互通，因此 PEAP 请用真实客户端测试。

### 排障（症状 → 定位 → 解决）

* **EAP-TLS 被拒，回包 `… handshake failed`** → 客户端证书未链到托管 **`EapTlsClientCa`** CA（或选错了 CA）。用同一张 `ca.pem` 重签客户端证书，或选择正确 导入的客户端 CA。
* **EAP-TLS 被拒，回包 `… identity … does not match`** → 用户**用户名 ≠ 证书 Peer-Id**。记住顺序 **SAN email → SAN DNS → CN**，且带 SAN 的证书会忽略 `CN`：对示例 证书，用户名须为 `alice@example.com`。要么改用户名，要么签一张仅含 CN 的证书并把用户名 设为该 CN。
* **客户端拒绝连接 / “无法验证服务器”** → 设备不信任 **ToughRADIUS** 的 CA。在客户端 安装 `ca.pem`；在 **Android 11+** 上还要把 **域（Domain）** 字段填成服务器证书的 CN/SAN（`radius.example.com`），这是 Android 现在强制要求的。
* **PEAP / TTLS 密码正确却被拒为“用户不存在”** → 用了**匿名外层身份**。清空它（或设为真实 用户名），让外层 `User-Name` 等于账号 —— 见上面的坑。
* **完全没有 EAP 挑战 / 立即拒绝** → 未选择 `EapTlsServerCert`（EAP-TLS 还需 `EapTlsClientCa`），或所选服务器证书没有私钥，于是 EAP 安全拒绝。导入证书材料并选择 托管名称即可，无需重启。
* **想要的方式始终没被提供** → 默认 `EapMethod` 是 `eap-md5`，对 WPA-Enterprise 无效。 把 `EapMethod` 设成你的隧道方式，并把它（以及你允许客户端 NAK 切换到的方式）列入 **启用的 EAP 处理器**。
* **EAP-TTLS 内层认证被拒** → 仅实现了**内层 PAP 与 MS-CHAP-V2**；不支持内层 CHAP / MS-CHAP / 隧道内 EAP。此外 TTLS 隧道钉死在 **TLS 1.2** —— 仅支持 TLS 1.3 的请求方无法 完成第二阶段。
* **认证成功但无计费 / 无在线会话** → 启用 `radius-eap-accounting=yes`（经典）/ `eap-radius-accounting=yes`（CAPsMAN），并确保 UDP **1813** 能到达 ToughRADIUS。

***

## 相关章节

* [厂商对接指南 · MikroTik](/overview-1/vendor-guide) —— 属性参考卡。
* [管理系统用户手册](/overview-1/admin-manual) —— 用户 / 计费策略 / 在线会话 / CoA 表单。
* [常见问题解答](/overview-1/faq) —— 更多跨场景排障问答。


# 华为 BRAS / NetEngine

> English version: [Cookbook: Huawei BRAS / NetEngine](/overview/cookbook/cookbook-huawei)
>
> 本章是[场景实战手册](/overview-1/cookbook)的一部分，沿用其[五段式与阅读约定](https://docs.toughradius.net/overview-1/cookbook/pages/fxPJ6fqamdijKi5TOxQO#每个场景的五段式)。

华为（厂商代码 **2011**）是国内运营商与企业网络中占主导地位的宽带 BRAS / 企业网关 （NetEngine / ME60 / 较老的 MA5200 系列）。ToughRADIUS 为其注册了专门的厂商增强器， 认证通过后下发：

* 一组**四属性限速四元组**（由 `huawei_enhancer.go` 产生）： `Huawei-Input-Average-Rate`、`Huawei-Input-Peak-Rate`、 `Huawei-Output-Average-Rate`、`Huawei-Output-Peak-Rate`。
* `Huawei-Domain-Name` —— 仅当用户 / 策略设置了\*\*域名（domain）\*\*时下发。
* `Huawei-Framed-IPv6-Address` —— 仅当用户设置了**静态 IPv6** 时下发。
* 以及所有设备通用的标准属性（`Session-Timeout`、`Acct-Interim-Interval`、 `Framed-Pool`、`Framed-IP-Address` 等），由 `default_enhancer.go` 产生。

请求侧，**华为解析器**（`huawei_parser.go`）从 `Calling-Station-Id` 提取 MAC （把 `-` 规整为 `:`），从 `NAS-Port-Id` 提取内 / 外层 VLAN ID。正是这两项让华为设备 具备了 MAC 绑定**与** VLAN 绑定能力。

> **前置条件**：已在 **NAS 设备** 中把这台 BRAS 登记为 *厂商 = Huawei*、填写正确的 源 IP 与共享密钥；ToughRADIUS 可被访问（认证 1812、计费 1813）。若登记成 `Standard`，认证仍可成功，但上述华为 VSA **全都不会**下发，VLAN 也不会被解析。

***

## 场景 A：PPPoE / IPoE 宽带 —— 分级套餐、峰值速率与 AAA 域

### 需求 / 场景

运营商或企业用华为 BRAS 跑宽带，需要多档套餐（如家庭版下行 30M / 上行 10M、 企业版下行 100M）。华为同时支持**平均**速率与**峰值**（突发）速率，并把用户归入 AAA **域（domain）**，以便 BRAS 套用对应的域策略。

### ToughRADIUS 侧

1. **为每档套餐建一个计费策略**（**计费策略 → 新建**）：
   * **上行 / 下行速率**：单位为 **Kbps**。下行 30M 应填 `30720`，**不是** `30`； 上行 10M 填 `10240`。
   * **域名 domain**（可选）：华为 AAA 域名（如 `isp`），将作为 `Huawei-Domain-Name` 下发，让 BRAS 把会话绑定到该域。
2. **创建用户**（**用户 → 新建**）：用户名 / 密码、对应**计费策略**、**过期时间**、 状态 = 启用。用户级的**域名**字段会覆盖策略上的域名。

存储的 Kbps 速率如何变成四个华为 VSA（锚定 `huawei_enhancer.go`）：

| 属性                                                                          | 取值                     | 说明                              |
| --------------------------------------------------------------------------- | ---------------------- | ------------------------------- |
| `Huawei-Input-Average-Rate`                                                 | `上行kbps × 1024`（bit/s） | 「Input」= 进入 BRAS 的流量 = 用户**上行** |
| `Huawei-Input-Peak-Rate`                                                    | `上行kbps × 1024 × 4`    | 平均值 × 4                         |
| `Huawei-Output-Average-Rate`                                                | `下行kbps × 1024`（bit/s） | 「Output」= 流向用户的流量 = **下行**      |
| `Huawei-Output-Peak-Rate`                                                   | `下行kbps × 1024 × 4`    | 平均值 × 4                         |
| `Huawei-Domain-Name`                                                        | 用户 / 策略的域名             | **设置了才**下发                      |
| `Session-Timeout`、`Acct-Interim-Interval`、`Framed-Pool`、`Framed-IP-Address` | 标准                     | 由 `default_enhancer.go` 产生      |

> **华为独有的两个坑。** ① **单位**：限速 VSA 是 **bit/s** 而非 Kbps——ToughRADIUS 把存储的 Kbps 乘以 **1024**（二进制），且**峰值**是平均值的 **× 4**。所以「下行 30M」会被下发为 `Output-Average-Rate = 30720 × 1024 = 31457280`、 `Output-Peak-Rate = 125829120`。② **方向命名**：华为「Input」是用户**上行**， 「Output」是**下行**（BRAS 视角）——与 MikroTik 的 `rx/tx` 用词相反，但物理含义 相同。四个值都会被钳制到 Int32 上限。

### 设备侧（华为 VRP，参考示例，以实际固件为准）

```
# RADIUS 服务器模板
radius-server template tr-tmpl
 radius-server shared-key cipher <SECRET>
 radius-server authentication <TOUGHRADIUS_IP> 1812 weight 80
 radius-server accounting <TOUGHRADIUS_IP> 1813 weight 80
#
# 绑定到模板的 AAA 域（与 Huawei-Domain-Name 对应）
aaa
 authentication-scheme radius-auth
  authentication-mode radius
 accounting-scheme radius-acct
  accounting-mode radius
 domain isp
  authentication-scheme radius-auth
  accounting-scheme radius-acct
  radius-server tr-tmpl
```

> `Huawei-Output-Average-Rate` / 峰值会映射到 BRAS 的每用户 CAR / QoS，无需手工 为每个用户建队列。

### 验证

* **radtest（服务端）**：

  ```bash
  go run ./cmd/radtest auth -server <TOUGHRADIUS_IP> -nas-ip <NAS_IP> \
    -username <用户名> -password <密码> -secret <SECRET>
  ```

  成功时打印 `Access-Accept`，应能看到四个华为限速属性以及（若设置了） `Huawei-Domain-Name`。
* **BRAS 侧**：`display access-user username <name>`（在线会话、速率、域）， `display aaa online-fail-record` 查看失败记录。
* **管理后台**：**在线会话** 页应出现该会话。

### 排障（症状 → 定位 → 解决）

* **限速不生效** → ① 确认 NAS 登记为 *Huawei*（登记成 Standard 不下发 VSA）； ② 记住取值是 bit/s（`× 1024`），若某档看起来「小了 1000 倍」，多半是设备侧把单位 当成了 Kbps。
* **速率方向反了（上行被限到下行的值）** → Input/Output 方向搞混：华为 **Input = 上行**、**Output = 下行**；检查哪个策略字段喂给了哪个属性。
* **域策略未生效** → 用户 / 策略没设**域名**（则不下发 `Huawei-Domain-Name`）， 或域名与 BRAS 上配置的 `domain` 不一致。
* **峰值 / 突发看起来过高** → 这是预期：增强器把峰值硬编码为平均值的 **× 4**； 要调就调平均档，而非峰值。

***

## 场景 B：线路防盗用 —— MAC + VLAN 绑定与双栈 IPv6

### 需求 / 场景

宽带运营商希望通过**把账号绑定到接入线路**来防止共享 / 盗用——绑定用户 MAC 和 / 或 接入 VLAN（内 / 外层，即 QinQ），并为双栈业务下发**静态 IPv6**。

### ToughRADIUS 侧

华为把接入线路编码在解析器已读取的属性里：

* **MAC** 来自 `Calling-Station-Id`；
* **内 / 外层 VLAN** 来自 `NAS-Port-Id`。

随后由两个检查器执行绑定（锚定 `mac_bind_checker.go` / `vlan_bind_checker.go`）：

1. **MAC 绑定** —— 在用户 / 策略上开启 **绑定 MAC**，并在用户上存好允许的 **MAC**。 认证时若请求 MAC 与所存 MAC 不同，会话被拒（`MacBindError`）。
2. **VLAN 绑定** —— 在用户 / 策略上开启 **绑定 VLAN**，并存好允许的 **VLAN ID 1 / VLAN ID 2**。若所存 VLAN 与解析出的不一致，会话被拒（`VlanBindError`）。
3. **静态 IPv6** —— 设置用户的 **IPv6 地址**；增强器随即下发 `Huawei-Framed-IPv6-Address`（若写成 `地址/前缀长度`，下发前会去掉前缀长度）。

> **绑定只校验你存了的东西。** 每个检查器在其开关关闭时会**跳过**，*并且*在任一侧 （所存值或请求值）为空 / 为 0 时也会跳过——它绝不会悄悄自动学习。所以要绑定一条 线路，必须（a）打开开关**并且**（b）在用户记录里填上 MAC / VLAN（通常取自首次 成功登录的抓取值）。

### 设备侧（华为 VRP，参考示例，以实际固件为准）

```
# IPoE / PPPoE 接入，向 RADIUS 上报接入线路。
# BRAS 接入默认把内/外层 VLAN 放进 NAS-Port-Id、把用户 MAC 放进 Calling-Station-Id。
radius-server template tr-tmpl
 radius-server shared-key cipher <SECRET>
 radius-server authentication <TOUGHRADIUS_IP> 1812 weight 80
 radius-server accounting <TOUGHRADIUS_IP> 1813 weight 80
#
# 按设计需要在 BRAS 上启用 IPv6 地址/前缀下发
ipv6
```

### 验证

* 抓一次真实 `Access-Request`（或看认证日志），确认 `Calling-Station-Id` 与 `NAS-Port-Id` 的取值，把它们**原样**存到用户上，再开启绑定。
* 用匹配的 MAC 做 **radtest** 成功；换一个 MAC / VLAN 则被拒。
* **BRAS 侧**：`display access-user username <name>` 可见绑定的线路与下发的 IPv6。

### 排障（症状 → 定位 → 解决）

* **绑定把合法用户也拒了** → 所存 MAC / VLAN 与 BRAS 实际发送的不一致。读取真实的 `Calling-Station-Id` / `NAS-Port-Id`，按原值存好，再重新开启绑定。
* **VLAN 绑定从不触发** → 解析出的 VLAN 为 `0`（你的组网里 BRAS 没把 VLAN 放进 `NAS-Port-Id`），或 **绑定 VLAN** 没开，或用户存的 VLAN 为 `0`；任一情况都会跳过。
* **没有下发 IPv6** → 用户没设静态 **IPv6 地址**，或该域在 BRAS 上没配 IPv6 / 双栈。
* **绑定被静默忽略** → 开关没开或所存值为空；检查器只有在开关开启**且**两侧都有值时 才生效。

***

## 场景 C：在线管控 —— CoA、强制下线与 FUP

### 需求 / 场景

对华为 BRAS 上的在线用户做实时管控：缩短会话、强制重新认证、超出配额后限速 （FUP），或把某会话踢下线。

### ToughRADIUS 侧

在 **在线会话** 页选中某会话，可执行两类动作（锚定 `session_actions.go` 与 [管理系统用户手册 · 在线会话](https://docs.toughradius.net/overview-1/cookbook/pages/bvFfYEZ7jlIollwyH4WI#在线会话)）：

* **修改授权（CoA-Request）**：当前仅携带 **`Session-Timeout`（#27）** 和 / 或 **`Filter-Id`（#11）**——**不会**改写华为的限速 VSA。
* **强制下线（Disconnect-Request）**：直接终止该会话。

> **华为上实现 FUP「在线变速」的正确路径。** 由于 CoA 不改写 `Huawei-Output-Average-Rate`，在线变速沿用与其他厂商一致的与厂商无关路径：先在 计费策略 / 用户上改速率，**再对其发强制下线**；客户端重拨后即按新的限速四元组重新 授权。（部分华为固件也会响应某个厂商私有的 CoA 限速属性，但 ToughRADIUS **不** 下发这种属性——不要依赖它。）

操作员发起的 CoA / Disconnect 使用较短超时并自动重传一次，目标为 NAS 记录上的 **CoA 端口（默认 3799）**。

### 设备侧（华为 VRP，参考示例，以实际固件为准）

```
# RADIUS 模板必须接受动态授权（CoA/DM）。
radius-server template tr-tmpl
 radius-server authorization <TOUGHRADIUS_IP> shared-key cipher <SECRET>
```

* 防火墙需放行从 ToughRADIUS 到 BRAS 的**入向 UDP 3799**。
* 若使用 `Filter-Id` 方案，需在 BRAS 预先定义同名的 ACL / 用户组（以实际固件为准）。

### 验证

* 在会话页点击 **修改授权** / **强制下线**，结果以通知形式反馈。
* **BRAS 侧**：强制下线后 `display access-user username <name>` 不再列出该会话， 客户端随后自动重拨。

### 排障（症状 → 定位 → 解决）

* **CoA / Disconnect 无响应或超时** → ① RADIUS 模板未配 `radius-server authorization`；② 防火墙挡了 UDP 3799；③ NAS 记录的 CoA 端口与 BRAS 的 authorization 端口不一致；④ 共享密钥不一致。
* **改了速率但在线用户没变速** → 这是预期：CoA 不改限速，需**强制下线**让客户端 重拨后生效。
* **端口困惑** → 网上常说的 CoA `1700` 是某些客户端的本地端口；本系统与 RFC 5176 使用 **3799**。

***

## 相关章节

* [厂商对接指南 · Huawei](/overview-1/vendor-guide) —— 属性参考卡。
* [实战手册：MikroTik RouterOS](/overview-1/cookbook/cookbook-mikrotik) —— RouterOS 上的同类场景。
* [管理系统用户手册](/overview-1/admin-manual) —— 用户 / 计费策略 / 在线会话 / CoA 表单。
* [常见问题解答](/overview-1/faq) —— 更多跨场景排障问答。


# H3C / 中兴 / 爱快 / Cisco

> English version: [Cookbook: H3C, ZTE, iKuai & Cisco](/overview/cookbook/cookbook-vendors)
>
> 本章是[场景实战手册](/overview-1/cookbook)的一部分，沿用其[阅读约定](https://docs.toughradius.net/overview-1/cookbook/pages/fxPJ6fqamdijKi5TOxQO#每个场景的五段式)。

这四家厂商跑的是与两本旗舰手册**完全相同的运维场景**——PPPoE / IPoE 分级套餐、 地址池、到期断网、单账号并发限制、MAC 绑定、CoA / 强制下线与 FUP。这些场景的*机制* （一个套餐档、一次到期、一个并发上限、一次 CoA 如何生效）都**一致**，因为它们来自共享的 `default_enhancer` 与各 checker——而非来自厂商。

因此本章不重复四遍完整的五段式，而是各厂商的**差异速查**：对每台设备只说清 **（1）** ToughRADIUS 下发哪个限速属性、单位倍率，**（2）** 请求 MAC / VLAN 如何解析 （从而哪些绑定可用），以及 **（3）** 端到端步骤该照哪本旗舰手册做。

> **请配合一本旗舰手册阅读。** 任何场景的完整分步，请照 [MikroTik](/overview-1/cookbook/cookbook-mikrotik) 或 [Huawei](/overview-1/cookbook/cookbook-huawei) 手册中对应小节 操作；只有下面列出的下发属性不同。

## 该照哪本手册

| 你想要…                 | 照做                                                                                                                                                                                                                                                                    | 各厂商差异（本章）                                 |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| 分级套餐 + 地址池 + 到期 + 并发 | [MikroTik 场景 A](https://docs.toughradius.net/overview-1/cookbook/pages/ziC2PMo9UoeSs0zbuItl#场景-apppoe-宽带-isp--分级套餐--地址池--到期断网--并发限制) 或 [Huawei 场景 A](https://docs.toughradius.net/overview-1/cookbook/pages/YVexcn0OKhMJlJma5XSg#场景-apppoe--ipoe-宽带--分级套餐峰值速率与-aaa-域) | 下发的**限速属性**与单位                            |
| 线路防盗用（MAC / VLAN 绑定） | [Huawei 场景 B](https://docs.toughradius.net/overview-1/cookbook/pages/YVexcn0OKhMJlJma5XSg#场景-b线路防盗用--mac--vlan-绑定与双栈-ipv6)                                                                                                                                            | **MAC 解析格式** + 是否支持 **VLAN 绑定**           |
| 在线管控（CoA / 下线 / FUP） | [MikroTik 场景 C](https://docs.toughradius.net/overview-1/cookbook/pages/ziC2PMo9UoeSs0zbuItl#场景-c在线管控--coa强制下线与-fup)                                                                                                                                                   | 无——CoA 与厂商无关（Session-Timeout + Filter-Id） |

> **并发上限、到期断网、地址池与 CoA 对这里每家厂商都一样**——它们由共享 checker / `default_enhancer` 执行，而非靠某个厂商 VSA。唯一真正的逐厂商变量就是下面的限速属性 和 MAC / VLAN 解析。

***

## H3C —— 厂商代码 25506

**限速属性**（锚定 `h3c_enhancer.go`）——与华为**同样的四元组形态**：

| 属性                        | 取值                            |
| ------------------------- | ----------------------------- |
| `H3C-Input-Average-Rate`  | `上行kbps × 1024`（bit/s）—— 用户上行 |
| `H3C-Input-Peak-Rate`     | `上行kbps × 1024 × 4`           |
| `H3C-Output-Average-Rate` | `下行kbps × 1024`（bit/s）—— 下行   |
| `H3C-Output-Peak-Rate`    | `下行kbps × 1024 × 4`           |

即「下行 30M」档（`30720` Kbps）会下发 `H3C-Output-Average-Rate = 31457280`、 峰值 `125829120`；所有值钳制到 Int32 上限。与华为不同，H3C **不**下发域或 IPv6 VSA， 只有限速四元组。

**请求解析**（`h3c_parser.go`）：MAC 来自 **`H3C-IP-Host-Addr`**（取末 17 位，即尾部的 `aa:bb:cc:dd:ee:ff`），该 VSA 缺失时回退到 `Calling-Station-Id`（`-`→`:`）；内 / 外层 VLAN 来自 `NAS-Port-Id`。**MAC 与 VLAN 绑定均支持。**

**设备侧（H3C Comware，参考，以实际固件为准）：**

```
radius scheme tr_scheme
 primary authentication <TOUGHRADIUS_IP> 1812
 primary accounting <TOUGHRADIUS_IP> 1813
 key authentication simple <SECRET>
 key accounting simple <SECRET>
 user-name-format without-domain
#
domain default enable system
 authentication ppp radius-scheme tr_scheme
 accounting ppp radius-scheme tr_scheme
```

> **坑**：把 NAS 登记为 *H3C*（不是 *Huawei*）。虽然限速算法一致，但 VSA 的**厂商 ID 不同**（25506 vs 2011）；把 H3C 设备登记成 Huawei，会收到它忽略的华为 VSA，限速将 不生效。

***

## ZTE 中兴 —— 厂商代码 3902

**限速属性**（锚定 `zte_enhancer.go`）——**两个属性，无峰值**：

| 属性                       | 取值                     |
| ------------------------ | ---------------------- |
| `ZTE-Rate-Ctrl-SCR-Up`   | `上行kbps × 1024`（bit/s） |
| `ZTE-Rate-Ctrl-SCR-Down` | `下行kbps × 1024`（bit/s） |

「下行 30M」档下发 `ZTE-Rate-Ctrl-SCR-Down = 31457280`。没有峰值 / 突发属性——平均 速率即为上限。

**请求解析**（`zte_parser.go`）：MAC 来自 `Calling-Station-Id`，但中兴发的是**12 位 纯十六进制字符串**；ToughRADIUS 会重排成 `aa:bb:cc:dd:ee:ff`。VLAN 从 `NAS-Port-Id` 解析。**MAC 与 VLAN 绑定均支持**——但做 MAC 绑定时，所存 MAC 要用 `aa:bb:cc:dd:ee:ff` 形式（这是解析器产出的格式）。

**设备侧**：中兴 BRAS 与华为同样使用 radius-template + domain 模式——把认证 / 计费 模板绑定到 ToughRADIUS 地址、共享密钥与端口 1812 / 1813（以实际固件为准）。

***

## iKuai 爱快 —— 厂商代码 10055

国内常见的 SMB / SOHO 网关。

**限速属性**（锚定 `ikuai_enhancer.go`）——**两个属性，倍率不同**：

| 属性                          | 取值                              |
| --------------------------- | ------------------------------- |
| `RP-Upstream-Speed-Limit`   | `上行kbps × 8192`（= `× 1024 × 8`） |
| `RP-Downstream-Speed-Limit` | `下行kbps × 8192`                 |

即「下行 30M」档（`30720` Kbps）下发 `RP-Downstream-Speed-Limit = 251658240`。

> **坑 —— 高档位会被钳制。** 因为倍率是 `× 8192` 且取值钳制到 **Int32 上限 （2147483647）**，任何高于约 **256 Mbps**（`262144` Kbps）的档位都会溢出并被**钳制** ——300M 档会按钳制上限下发，而非 300M。超高档位请改在 iKuai 设备侧做策略。

**请求解析**：iKuai 使用**默认解析器**——MAC 来自 `Calling-Station-Id`（`-`→`:`）， 且**不解析 VLAN**（恒为 `0`）。故 **MAC 绑定可用，但 VLAN 绑定不可用**（VLAN 检查总被 跳过）。

**设备侧**：在 iKuai web 控制台进入 **认证计费 → RADIUS 计费**，设置服务器地址、端口 1812 / 1813 与共享密钥，再在 PPPoE 服务器设置中启用 RADIUS。

***

## Cisco —— 厂商代码 9

**限速属性**：**无**——Cisco 无可移植的数值限速 VSA，带宽仍在设备侧。`accept-cisco` enhancer 会在套餐配置了地址池时下发一个非限速属性 `Cisco-AVPair="ip:addr-pool=<pool>"`， 与标准 `Framed-Pool` 互补；否则 Cisco NAS 只会收到 `default_enhancer.go` 下发的标准属性 （`Session-Timeout`、`Acct-Interim-Interval`、`Framed-Pool`、`Framed-IP-Address` 等）。认证 （PAP / CHAP / MS-CHAPv2 / EAP）、会话控制、计费与 CoA 都正常工作——**限速请在设备侧做**， 或用随仓库附带的 `Cisco-AVPair` 字典做自定义集成。

**请求解析**：默认解析器——MAC 来自 `Calling-Station-Id`，不解析 VLAN。故 **MAC 绑定 可用，VLAN 绑定不可用**。

**设备侧（Cisco IOS，参考，以实际平台为准）：**

```
aaa new-model
radius server TOUGHRADIUS
 address ipv4 <TOUGHRADIUS_IP> auth-port 1812 acct-port 1813
 key <SECRET>
aaa authentication ppp default group radius
aaa accounting network default start-stop group radius
aaa server radius dynamic-author
 client <TOUGHRADIUS_IP> server-key <SECRET>
```

> `aaa server radius dynamic-author` 启用 CoA / Disconnect（默认端口 3799）。不要指望 Cisco 从 RADIUS 拿限速——用设备侧 service-policy / QoS 设置。

***

## 厂商能力矩阵

| 厂商         | 限速属性                                 | 单位倍率            | 峰值       | MAC 绑定 | VLAN 绑定 |
| ---------- | ------------------------------------ | --------------- | -------- | ------ | ------- |
| H3C（25506） | `H3C-Input/Output-Average-Rate` + 峰值 | `× 1024`（bit/s） | ✅ 平均 × 4 | ✅      | ✅       |
| 中兴（3902）   | `ZTE-Rate-Ctrl-SCR-Up/Down`          | `× 1024`（bit/s） | ❌        | ✅      | ✅       |
| 爱快（10055）  | `RP-Upstream/Downstream-Speed-Limit` | `× 8192`        | ❌        | ✅      | ❌       |
| Cisco（9）   | 无（设备侧 QoS）                           | —               | —        | ✅      | ❌       |

（对比：MikroTik 下发 Kbps 单位的 `Mikrotik-Rate-Limit` 字符串；华为下发 `× 1024`、 峰值 `× 4` 的限速四元组外加域 / IPv6。）

## 排障（症状 → 定位 → 解决）

* **限速不生效** → ① NAS 没按该厂商精确登记（VSA 厂商 ID 必须匹配——即便算法相同， H3C ≠ Huawei）；② Cisco **本就没有** RADIUS 限速 VSA——请在设备侧做 QoS；③ 设备侧 单位搞错（这里除 iKuai 是 `× 8192` 外，其余都是 bit/s）。
* **iKuai 高档速率不对 / 被压低** → `× 8192` 的值溢出了 Int32 而被钳制；高档请在设备侧 做。
* **iKuai / Cisco 的 VLAN 绑定从不触发** → 这是预期：它们的解析器不提取 VLAN（恒为 `0`），故 VLAN 检查总被跳过。改用 MAC 绑定，或换支持 VLAN 解析的厂商（华为 / H3C / 中兴）。
* **中兴 MAC 绑定从不匹配** → 把 MAC 按 `aa:bb:cc:dd:ee:ff` 存储（中兴发 12 位纯十六 进制，ToughRADIUS 比对前会重排成冒号形式）。

***

## 相关章节

* [实战手册：MikroTik RouterOS](/overview-1/cookbook/cookbook-mikrotik) —— 完整 PPPoE / Hotspot / CoA 剧本。
* [实战手册：华为 BRAS / NetEngine](/overview-1/cookbook/cookbook-huawei) —— 完整分级套餐 / 线路绑定 / CoA 剧本。
* [厂商对接指南](/overview-1/vendor-guide) —— 各厂商属性参考卡。
* [常见问题解答](/overview-1/faq) —— 更多跨场景排障问答。


# 管理系统用户手册

> English version: [Admin UI Manual](/overview/admin-manual)

管理控制台运行在 `1816` 端口（HTTP），基于 React Admin 构建，提供中英双语 （默认中文，可在顶栏语言菜单切换）及明暗两套主题。本章逐页介绍。

## 登录与账户

访问 `http://<服务器>:1816`，使用操作员账号登录。初始管理员为 `admin`，口令来自 `{workdir}/private/admin-bootstrap-password` 中的一次性随机值（或 `TOUGHRADIUS_ADMIN_PASSWORD`）—— 请立即在 **账户设置**（右上角头像）中修改，该页 同时可编辑个人资料。密码至少 6 位，且不得使用历史默认口令。

操作员角色（在 **操作员** 页面设置）：

| 级别         | 含义                   |
| ---------- | -------------------- |
| `super`    | 完整权限，包括系统配置与操作员管理    |
| `admin`    | 菜单可见性与 super 相同      |
| `operator` | 仅日常页面——系统配置与操作员菜单不可见 |

## 仪表盘

首页汇总部署状态：

* **统计卡片** —— 用户总数（含停用/过期数）、在线用户数、今日认证与计费 请求数、今日上行/下行流量（GB）。
* **IPv6 指标条** —— 携带 IPv6 的在线会话、IPv6 地址/前缀/委派前缀计数、 配置了静态 IPv6 的用户数。
* **图表** —— 认证趋势、策略分布饼图、24 小时上下行流量图。

计数来自内存指标，见[运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#指标)。

## 网络节点

用于组织 NAS 设备的逻辑分组（名称、标签、备注）。请先建节点——每台 NAS 都归属一个节点。

## NAS 设备

允许与本服务器通信的网络设备清单。未知源地址的请求会被丢弃。

| 字段                | 说明                                                                                  |
| ----------------- | ----------------------------------------------------------------------------------- |
| 名称 / Identifier   | 自由命名；`identifier` 与 RADIUS `NAS-Identifier` 匹配                                      |
| IP 地址 / 主机名       | RADIUS 报文的源地址                                                                       |
| 厂商                | **决定厂商属性行为** —— 标准、Cisco、华为、MikroTik、H3C、中兴、爱快（见[厂商对接指南](/overview-1/vendor-guide)） |
| 密钥                | RADIUS 共享密钥                                                                         |
| CoA 端口            | CoA/Disconnect 的目标端口，默认 `3799`                                                      |
| 节点 / 状态 / 标签 / 备注 | 归属与生命周期管理                                                                           |

## RADIUS 用户

拨入用户账号。列表支持按用户名、姓名、邮箱、手机、IP 过滤，支持 CSV 导出与 列排序。

主要表单字段：用户名/密码、状态（启用/停用）、**计费策略**（必选——速率、 并发、地址池、Class 由其继承）、**过期时间**（决定 `Session-Timeout`）、静态 `ip_addr` / `ipv6_addr`、IPv6 前缀池与委派前缀（编辑视图）、`radius_class` （RFC 2865 Class，原样下发）、联系方式、备注。

**批量导入**：列表工具栏的导入按钮接受 `.xlsx`、`.csv`、`.json` 文件，并 报告成功/失败数量。可先导出现有列表作为模板参考。

详情视图分组展示全部信息，包括 IPv6 细节、MAC/VLAN 绑定值与时间戳。

## 计费策略

可复用的授权模板：

| 字段                       | 含义                                                                       |
| ------------------------ | ------------------------------------------------------------------------ |
| `active_num`             | 单用户最大并发会话数（0 = 不限）                                                       |
| `up_rate` / `down_rate`  | 带宽，单位 **Kbps**；按厂商换算（列表对 ≥1024 的值以 Mbps 展示）                              |
| `addr_pool`              | NAS 用于分配地址的 `Framed-Pool` 池名                                             |
| `radius_class`           | RFC 2865 Class（Type 25），原样写入 Access-Accept；ocserv 群组填 `OU=group1;group2` |
| `ipv6_prefix` / 域        | IPv6 与华为域授权                                                              |
| `bind_mac` / `bind_vlan` | 将用户锁定到首次出现的 MAC / VLAN                                                   |

## 在线会话

实时会话（`radius_online`）。列包括会话 ID、Framed IP、NAS 地址/端口、开始 时间、时长、超时与流量计数。过滤器覆盖用户名、会话 ID、IPv4/IPv6 地址与 前缀、NAS 地址、MAC 及开始时间范围。

行级操作（RFC 5176 动态授权的入口）：

* **修改授权（CoA）** —— 发送 `CoA-Request`，携带新的会话超时和/或 `Filter-Id`。
* **强制下线（Disconnect）** —— 发送 `Disconnect-Request` 终止会话（需确认）。

两者都发往 NAS 的 CoA 端口，结果以通知形式反馈。

## 计费记录

历史计费数据（`radius_accounting`），只读：会话 ID、地址、起止时间、上下行 总流量。过滤条件与在线会话一致并增加结束时间；支持 CSV 导出。

## 系统配置

仅 `super`/`admin` 可见。配置项由 schema 驱动、按组折叠展示（RADIUS 组默认 展开），存储在 `sys_config` 表——修改即时生效，无需重启。13 项 RADIUS 配置：

| 键                          | 默认值       | 用途                                                                                                                |
| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
| `EapMethod`                | `eap-md5` | 当前 EAP 方法：`eap-md5`、`eap-mschapv2`、`eap-tls`、`eap-peap`、`eap-ttls`                                                |
| `EapEnabledHandlers`       | `*`       | 允许的 EAP 处理器逗号白名单                                                                                                  |
| `EapTlsServerCert`         | 空         | EAP-TLS/PEAP/TTLS 出示的托管服务器证书名称（在「证书管理」中导入并选择）；**留空即禁用基于证书的 EAP**                                                  |
| `EapTlsClientCa`           | 空         | 校验客户端证书链的托管 CA 证书名称（仅 EAP-TLS 需要）                                                                                 |
| `EapTlsMinVersion`         | `1.2`     | 最低 TLS 版本（`1.2`/`1.3`）                                                                                            |
| `EapTlsCipherProfile`      | `modern`  | TLS 1.2 套件策略：`modern`（ECDHE+AEAD）、`legacy-rsa-cbc`（为 hostapd internal TLS 选择加入 RSA+AES-CBC，需 RSA 服务器证书）或 `custom` |
| `EapTlsCipherSuites`       | 空         | `custom` 档使用的 TLS 1.2 套件列表                                                                                        |
| `IgnorePassword`           | `false`   | 跳过密码校验（仅测试用）                                                                                                      |
| `AccountingHistoryDays`    | `90`      | 计费历史保留天数（`@daily` 清理；`0` 关闭）                                                                                      |
| `AcctInterimInterval`      | `300`     | NAS 中间计费更新间隔（秒）                                                                                                   |
| `SessionTimeout`           | `3600`    | 默认会话超时（秒）                                                                                                         |
| `LogLevel`                 | `info`    | RADIUS 日志级别（`debug`/`info`/`warn`/`error`）                                                                        |
| `RejectDelayMaxRejects`    | `7`       | 触发延迟响应的连续拒绝次数                                                                                                     |
| `RejectDelayWindowSeconds` | `10`      | 拒绝计数窗口（秒）                                                                                                         |

工具栏：**保存**、**刷新**、**重置**（恢复默认值，需确认）以及 **备份 / 恢复** —— 导出或回导一份包含节点、NAS、策略、用户、配置与操作员的 JSON 快照（不包含的内容见[运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#备份与恢复)）。

## 操作员

控制台账号管理（仅 `super`/`admin`）：用户名、密码、联系方式、级别、状态。 操作日志保存在数据库（`sys_opr_log`），一年后自动清理。

## 界面便利功能

* **语言切换**（顶栏）—— 简体中文 / English，按浏览器记忆。
* **主题切换** —— 明/暗。
* 用户、会话、计费、NAS、节点、操作员列表均支持 **CSV 导出**。
* 所有列表页支持服务端分页与已激活过滤条件标签。


# 运维指南

> English version: [Operations Guide](/overview/ops-guide)

在生产环境运行 ToughRADIUS 所需的一切：配置参考、环境变量、TLS/EAP 证书、 存储、监控、备份以及随附的命令行工具。

## 进程模型

一个静态二进制并发运行多个服务（Web/管理 API、RADIUS 认证、RADIUS 计费、 RadSec）。**任一服务失败，整个进程退出**，交由守护程序重启——请使用 systemd、Docker 或同类工具托管。

```ini
# /etc/systemd/system/toughradius.service（参考）
[Unit]
Description=ToughRADIUS server
After=network-online.target

[Service]
ExecStart=/usr/local/bin/toughradius -c /etc/toughradius.yml
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

## 端口

| 端口   | 协议        | 服务                      | 配置键                                |
| ---- | --------- | ----------------------- | ---------------------------------- |
| 1816 | TCP HTTP  | 管理界面 + REST API         | `web.port`                         |
| 1817 | TCP HTTPS | 管理界面 TLS（可选；启动失败不影响整体）  | `web.tls_enabled` / `web.tls_port` |
| 1812 | UDP       | RADIUS 认证               | `radiusd.auth_port`                |
| 1813 | UDP       | RADIUS 计费               | `radiusd.acct_port`                |
| 2083 | TCP TLS   | RadSec（RFC 6614）        | `radiusd.radsec_port`              |
| 3799 | UDP（出向）   | 发往 NAS 的 CoA/Disconnect | 按 NAS 的 *CoA 端口* 字段                |

## 配置

查找顺序：`-c <文件>` → `./toughradius.yml` → `/etc/toughradius.yml` → 内置默认值。可用 `toughradius -printcfg` 查看合并结果。

```yaml
system:
  appid: ToughRADIUS
  location: Asia/Shanghai        # 定时任务/时间戳所用时区
  workdir: /var/toughradius      # 生产构建的默认值
  debug: false
web:
  host: 0.0.0.0
  port: 1816
  tls_enabled: true              # 兼容旧版本默认启用；设为 false 可关闭内置 HTTPS 监听
  tls_port: 1817
  secret: <随机字符串>            # JWT 签名密钥——务必修改
database:
  type: sqlite                   # sqlite | postgres
  host: 127.0.0.1                # 仅 postgres
  port: 5432
  name: toughradius.db           # sqlite 文件名（位于 {workdir}/data/）或 pg 库名
  user: postgres
  passwd: <密码>
  max_conn: 100
  idle_conn: 10
  debug: false
radiusd:
  enabled: true
  host: 0.0.0.0
  auth_port: 1812
  acct_port: 1813
  radsec_port: 2083
  radsec_worker: 100
  radsec_ca_cert: private/ca.crt        # 相对路径基于 workdir 解析
  radsec_cert: private/radsec.tls.crt
  radsec_key: private/radsec.tls.key
  debug: false                   # true = 完整报文转储
logger:
  mode: production               # development | production
  file_enable: true
  filename: /var/toughradius/toughradius.log
```

### 工作目录结构

启动时 ToughRADIUS 在 `system.workdir` 下创建：

```
/var/toughradius/
├── data/        # SQLite 数据库、指标数据
├── logs/
├── private/     # TLS 材料（权限 0700）
├── public/
└── backup/      # 配置备份的服务器侧副本
```

### 环境变量

环境变量优先于 YAML 文件：

| 变量                                                                                                                                   | 覆盖项                                          |
| ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
| `TOUGHRADIUS_SYSTEM_WORKER_DIR`                                                                                                      | `system.workdir`                             |
| `TOUGHRADIUS_SYSTEM_DEBUG`                                                                                                           | `system.debug`                               |
| `TOUGHRADIUS_WEB_HOST` / `_WEB_PORT` / `_WEB_TLS_ENABLED` / `_WEB_TLS_PORT` / `_WEB_SECRET`                                          | `web.*`                                      |
| `TOUGHRADIUS_DB_TYPE` / `_DB_HOST` / `_DB_PORT` / `_DB_NAME` / `_DB_USER` / `_DB_PWD` / `_DB_DEBUG`                                  | `database.*`                                 |
| `TOUGHRADIUS_RADIUS_ENABLED` / `_RADIUS_HOST` / `_RADIUS_AUTHPORT` / `_RADIUS_ACCTPORT` / `_RADIUS_DEBUG`                            | `radiusd.*`                                  |
| `TOUGHRADIUS_RADIUS_RADSEC_PORT` / `_RADIUS_RADSEC_WORKER` / `_RADIUS_RADSEC_CA_CERT` / `_RADIUS_RADSEC_CERT` / `_RADIUS_RADSEC_KEY` | RadSec 配置                                    |
| `TOUGHRADIUS_LOGGER_MODE` / `_LOGGER_FILE_ENABLE`                                                                                    | `logger.*`                                   |
| `TOUGHRADIUS_RADIUS_POOL`                                                                                                            | RADIUS 工作池大小（默认 1024）                        |
| `TOUGHRADIUS_ADMIN_PASSWORD`                                                                                                         | 可选。首次启动或轮换引导 `admin` 账号时使用的口令；若等于历史默认口令则被忽略。 |

### 命令行参数

| 参数          | 作用                   |
| ----------- | -------------------- |
| `-c <文件>`   | 指定配置文件               |
| `-initdb`   | **删除并重建全部数据表**后退出    |
| `-printcfg` | 以 JSON 打印合并后的配置并退出   |
| `-v`        | 打印版本 / 构建时间 / 提交号并退出 |
| `-h`        | 帮助                   |

RADIUS 运行时配置（EAP 方法、证书、间隔、拒绝延迟等）存储在数据库中，通过 **系统配置** 页面修改——无需重启。见 [管理系统用户手册](https://docs.toughradius.net/overview-1/pages/bvFfYEZ7jlIollwyH4WI#系统配置)。

## 数据库

* **SQLite**（默认）—— 纯 Go 驱动，无 CGO，文件位于 `{workdir}/data/<name>`。适合中小规模部署；备份即拷贝文件。
* **PostgreSQL** —— 设置 `database.type: postgres` 及 host/user/password。 推荐用于生产规模与高并发计费负载。

结构迁移（GORM `AutoMigrate`）在每次启动时自动执行，升级流程即：停止、 替换二进制、启动。`-initdb` 仅用于首次安装——它**销毁全部数据**。

> **升级提示 —— EAP 证书文件路径配置项已移除。** 旧的文件路径配置项 `EapTlsCertFile` / `EapTlsKeyFile` / `EapTlsCaFile` 已**移除**：托管证书库 （`sys_cert`）现在是 EAP-TLS/PEAP/TTLS 证书材料的唯一来源。如果你此前通过 磁盘文件路径配置了证书型 EAP，升级后必须在 **证书管理** 页重新导入 PEM （服务器证书需包含私钥），并在 **系统配置 → `EapTlsServerCert` / `EapTlsClientCa`** 中按名称选择。在选择托管服务器证书之前，证书型 EAP 方法会安全拒绝所有请求（`ErrTLSNotConfigured`）——请在升级前规划好重新 导入，避免 EAP 接入中断。纯 PAP/CHAP/MSCHAPv2（非 EAP）部署不受影响。

需要关注的大表：`radius_accounting`（随会话持续增长）与 `radius_online`。 `radius.AccountingHistoryDays` 配置（默认 90，设为 `0` 关闭）定义计费历史的保留 窗口：`@daily` 定时任务会删除超过该天数的**已结束** `radius_accounting` 记录（在线 会话不受影响），并清理连续多个计费中间更新周期未刷新的 `radius_online` 残留行。操作 日志（`sys_opr_log`）一年后自动清理。若数据量很大，仍建议把表增长监控与数据库级归档 纳入自己的运维流程。

## TLS 与证书

三处相互独立的证书使用方：

| 使用方                    | 文件                                                                                                      | 说明                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| **RadSec**             | `radiusd.radsec_ca_cert` / `radsec_cert` / `radsec_key`                                                 | TLS 1.2+；客户端证书**提供则校验**（`VerifyClientCertIfGiven`）                                    |
| **Web HTTPS**          | `{workdir}/private/toughradius.tls.crt` + `.key`（固定路径）                                                  | `web.tls_enabled` 为 true 时监听 `web.tls_port`；加载失败仅记日志，HTTP 继续运行                        |
| **EAP（TLS/PEAP/TTLS）** | 系统配置 → `EapTlsServerCert`、`EapTlsClientCa`、`EapTlsMinVersion`、`EapTlsCipherProfile`（证书在「证书管理」中导入并按名称选择） | 未选择服务器证书即禁用基于证书的 EAP 方法；默认 `modern` 套件，CBC-only 客户端需 opt-in `legacy-rsa-cbc` + RSA 证书 |

用内置工具一次生成 CA/服务器/客户端全套证书：

```bash
go run ./cmd/certgen -type all -output /var/toughradius/private \
  -server-cn radius.example.com -server-dns radius.example.com \
  -days 3650
# 然后将 radsec_cert/radsec_key（或 EAP 配置）指向生成的文件
```

## 日志

zap 结构化日志。`logger.mode: development` 输出适合人读的控制台格式； `production` 输出 JSON。文件输出由 `logger.file_enable` + `logger.filename` 控制。RADIUS 日志级别还可在运行时通过 **系统配置 → LogLevel** 调整； `radiusd.debug: true` 会转储完整报文（生产环境请关闭）。

## 指标

计数器保存在内存中，经由管理仪表盘展示（没有 Prometheus `/metrics` HTTP 端点）。RADIUS 计数器包括：`radus_accept`、`radus_online`/`radus_offline`、 `radus_accounting`、`radus_auth_drop` / `radus_acct_drop`、 `radus_radsec_saturated`，以及按原因细分的拒绝计数—— `radus_reject_passwd_error`、`radus_reject_not_exists`、`radus_reject_expire`、 `radus_reject_disabled`、`radus_reject_limit`、`radus_reject_bind_error`、 `radus_reject_ldap_error`、`radus_reject_unauthorized`、`radus_reject_other`。 `radus_reject_ldap_error` 表示 LDAP/AD 后端无法给出认证答案——例如目录不可达、 TLS/StartTLS 失败、服务账号 bind 失败或 LDAP 配置错误；口令错误仍归入 `radus_reject_passwd_error`。计费请求在入口被丢弃时按 原因细分——`radus_acct_drop_nas`（未知/未授权 NAS）、`radus_acct_drop_username` （缺少用户名）、`radus_acct_drop_secret`（Request Authenticator 校验失败）—— 而 `radus_acct_drop` 作为背压与响应写入丢弃的兜底计数。系统仪表（CPU/内存、进程 CPU/内存）每 30 秒采样一次。

外部监控建议探测服务端口并采集日志文件；进程退出即故障信号（fail-fast 进程模型）。

## 备份与恢复

**系统配置 → 备份** 下载一份 JSON 快照（schema 版本 9.0），包含：节点、NAS 设备、计费策略、用户、系统配置、操作员，以及托管证书（含私钥，确保恢复后 证书型 EAP 可继续工作）。服务器侧同时在 `{workdir}/backup/` 留存一份。**恢复** 可回导该文件。

> 快照**不包含**计费历史与在线会话。完整的灾备方案还需备份数据库本身 （拷贝 SQLite 文件或使用 `pg_dump`）。

> **安全提示**：快照包含敏感凭据 —— RADIUS 用户密码、操作员密码哈希， 以及托管 EAP 证书的 PEM 私钥。备份文件应按机密数据妥善存储与传输。

## 命令行工具

均位于 `cmd/` 下，以 `go run ./cmd/<工具>` 运行：

| 工具               | 用途                                                                                                                                            |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `radtest`        | 迷你 RADIUS 客户端：`auth`、`acct`、`flow`（认证 + 开始 + 结束）。参数：`-server`、`-secret`、`-username`、`-password`、`-calling-station`、`-framed-ip`、`-session-id` |
| `certgen`        | 生成 CA / 服务器 / 客户端证书（见上文）                                                                                                                      |
| `benchmark`      | 压力测试：总请求数 `-n`、并发 `-c`、认证/计费模式、CSV 统计输出                                                                                                       |
| `reset-password` | 重置控制台操作员密码：`go run ./cmd/reset-password -c <配置> -u admin -p <新密码>`                                                                            |
| `demo-seed`      | 填充演示用节点/NAS/策略/用户/会话数据                                                                                                                        |
| `config-tool`    | 校验 / 汇总配置 schema JSON                                                                                                                         |

## 生产加固清单

* [ ] 修改 `web.secret`。确认引导 `admin` 口令是唯一的（`{workdir}/private/admin-bootstrap-password` 或 `TOUGHRADIUS_ADMIN_PASSWORD`），不要使用历史默认口令。
* [ ] `radiusd.debug: false`、`logger.mode: production`。
* [ ] 用防火墙将 UDP 1812/1813 与 TCP 1816 限制在可信网络内。
* [ ] 跨不可信网络传输 RADIUS 时使用 RadSec（2083）或可信二层/VPN 通道。
* [ ] 每台 NAS 使用独立且强度足够的共享密钥。
* [ ] EAP：优先 `eap-tls`/`eap-peap`/`eap-ttls` 并使用正式证书；MS-CHAPv2 的注意事项见[安全策略](/overview-1/security-policy)。
* [ ] 制定数据库备份计划（配置快照 + 数据库转储）。
* [ ] 守护进程配置自动重启，并对进程退出告警。


# LDAP / AD 认证后端

> English version: [LDAP / AD Authentication Backend](/overview/auth-ldap)

ToughRADIUS 可以通过执行 LDAP **bind（绑定）** 操作，针对外部 LDAP 目录或 Microsoft Active Directory 校验用户口令，而不必（或不仅仅）使用自身数据库中 存储的口令。这样即可复用既有的企业目录，无需复制或迁移口令。

> **仅支持 PAP 族。** LDAP 后端通过「以明文口令执行 bind」来认证，因此只服务 **裸 PAP** 与 **EAP-TTLS 内层 PAP**（RFC 5281）。挑战/响应类方法——CHAP、 MS-CHAP、MS-CHAPv2、EAP-MD5、PEAP-MSCHAPv2——**无法**被服务，因为服务端 从不持有重算其响应所需的明文口令。启用 LDAP 后，这些方法会被有意拒绝并记录 可诊断原因；详见下文「认证流程」一节。

## 何时使用（以及何时不要用）

当你必须服务那些口令已存在于目录（OpenLDAP、FreeIPA 或 Active Directory）中 的用户，又不想给每个客户端签发证书时，就该用 LDAP 后端。它是混合环境与遗留 系统的务实桥梁：先用服务器证书保护隧道，再把用户名和口令塞进隧道里传输。

但要正视这笔权衡。**EAP-TTLS/PAP 在 TLS 隧道内传输明文口令**——它*只*受该隧道 保护。因此部署的安全性完全系于一张强壮且被正确校验的服务器证书与 TLS 1.2+。 如果每个客户端都能出示证书，请优先选 EAP-TLS；如果你需要为遗留客户端提供 目录后端的口令校验，那么 LDAP + EAP-TTLS/PAP 正是合适的工具——只是别对它的 安全性抱有不切实际的期待。

## 部署模型

LDAP 后端**只替换口令校验**这一步。它**不会**创建账号，也**不携带**授权数据。 每个用户仍需要一条本地 `RadiusUser` 记录，因为授权信息（套餐/资费、限速、 到期时间、并发会话上限、地址池、VLAN、MAC 绑定）是在认证*之前*由本地数据库 加载的。

换句话说：

* **认证**（口令对不对？）→ 由 LDAP 目录通过 bind 完成。
* **授权**（这个用户能做什么？）→ 仍由本地 `RadiusUser` 记录决定，与不启用 LDAP 时完全一致。

全局开关 `ldap.Enabled` 对整台服务器启用或关闭该后端；没有「按用户指定认证源」 的字段。MAC 地址认证始终绕过 LDAP。

## 配置

在管理后台的**系统配置**页面、**LDAP** 分组下配置该后端；所有配置项也可通过 设置 API 修改。后端在*每次*认证尝试时都会重新读取配置，因此改动**即时生效**、 无需重启。

| 配置键                       | 类型     | 默认值        | 适用模式     | 说明                                                                                            |
| ------------------------- | ------ | ---------- | -------- | --------------------------------------------------------------------------------------------- |
| `ldap.Enabled`            | bool   | `false`    | 两者       | 启用 LDAP 后端。默认关闭。                                                                              |
| `ldap.ServerURL`          | string | *(空)*      | 两者       | 目录 URL，如 `ldap://dc.example.com:389` 或 `ldaps://dc.example.com:636`。                          |
| `ldap.BindMode`           | 枚举     | `template` | 两者       | `template` 或 `search`（见下文）。                                                                   |
| `ldap.BindDNTemplate`     | string | *(空)*      | template | 含单个 `%s`（代入用户名）的 DN 模板，如 `uid=%s,ou=people,dc=example,dc=com`，或 AD 的 UPN 形式 `%s@example.com`。 |
| `ldap.BaseDN`             | string | *(空)*      | search   | 用户查找的子树基点，如 `dc=example,dc=com`。                                                              |
| `ldap.UserFilter`         | string | `(uid=%s)` | search   | 含单个 `%s`（代入用户名，代入前已转义）的过滤器，如 `(uid=%s)` 或 AD 的 `(sAMAccountName=%s)`。                         |
| `ldap.SearchBindDN`       | string | *(空)*      | search   | 用于查找用户的只读服务账号 DN，如 `cn=svc-radius,ou=svc,dc=example,dc=com`。                                  |
| `ldap.SearchBindPassword` | string | *(空)*      | search   | 服务账号 DN 的口令。                                                                                  |
| `ldap.StartTLS`           | bool   | `false`    | 两者       | 在 bind 前用 StartTLS 将 `ldap://` 连接升级为 TLS（RFC 4513 §3）。用 `ldaps://` 时请关闭。                      |
| `ldap.TLSSkipVerify`      | bool   | `false`    | 两者       | 跳过 TLS 证书校验。**不安全——仅限实验/自签名环境。**                                                              |
| `ldap.Timeout`            | int（秒） | `5`        | 两者       | 拨号与每次操作的超时秒数（1–60）。                                                                           |

### 两种 bind 模式

**模板模式**（`ldap.BindMode = template`）最简单：把用户名代入 `BindDNTemplate` 组成 DN，服务端直接以该 DN 加上所给口令执行 bind。适用于每个用户的 DN 都遵循 固定模式的场景。

```
BindMode       = template
BindDNTemplate = uid=%s,ou=people,dc=example,dc=com
# Active Directory 写法：
# BindDNTemplate = %s@example.com
```

**搜索模式**（`ldap.BindMode = search`）适配 DN 不可预测的目录。服务端先以只读 **服务账号**（`SearchBindDN` / `SearchBindPassword`）bind，在 `BaseDN` 下用 `UserFilter` 搜索定位用户的 DN，然后**以用户身份重新 bind**（用所给口令）来 完成校验。

```
BindMode           = search
ServerURL          = ldaps://dc.example.com:636
BaseDN             = dc=example,dc=com
UserFilter         = (sAMAccountName=%s)      # Active Directory
SearchBindDN       = cn=svc-radius,ou=svc,dc=example,dc=com
SearchBindPassword = ********
```

### 传输安全

* 使用 `ldaps://` URL 走隐式 TLS（636 端口），**或者**使用 `ldap://` URL 并设 `StartTLS = true` 将明文连接升级（RFC 4513 §3）。不要在 `ldaps://` URL 上再 开 StartTLS。
* 生产环境请保持 `TLSSkipVerify = false`。仅在实验或自签名目录上启用它——它会 关闭证书校验，使 bind 暴露于被截获的风险。

## 认证流程

| 方法                         | `ldap.Enabled = true` 时的行为          |
| -------------------------- | ----------------------------------- |
| 裸 PAP                      | 通过 LDAP bind 认证。                    |
| EAP-TTLS / 内层 PAP          | 通过 LDAP bind 认证；仍会为隧道派生 MS-MPPE 密钥。 |
| CHAP / MS-CHAP / MS-CHAPv2 | **拒绝**并记录可诊断原因（无明文口令可用于 bind）。      |
| EAP-MD5 / PEAP-MSCHAPv2    | 同理**拒绝**。                           |
| MAC 认证                     | 完全绕过 LDAP。                          |

对挑战/响应类方法的拒绝是有意为之的，且集中在「取口令」边界统一处理，而不是 在协议入口分叉。启用 LDAP 后，本地 `RadiusUser.Password` 通常为空，挑战/响应 方法若仍以空口令推算期望值，可能**误判通过任意用户**——因此这些方法被「失败 即关闭」。

## 安全模型

该后端在设计上保守：

* **空用户名或空口令在任何网络操作前即被拒绝。** 带 DN 而口令为空的 bind 会被 许多服务器当作*匿名*绑定而通过（RFC 4513 §5.1.2），因此在最前端直接拒绝。
* **注入被中和。** 搜索模式下用户名经 LDAP 过滤器转义（RFC 4515 §3）；模板模式 下代入前经 DN 转义。
* **歧义搜索被拒绝。** 若 `UserFilter` 命中超过一条记录，则拒绝该次尝试而非 猜测。
* **目录故障绝不被报告为口令错误。** 凭证无效（LDAP code 49）映射为口令拒绝； 其余任何失败——目录不可达、配置错误、服务账号 bind 失败——均映射为 *后端不可用*。这一区分同样驱动下文的指标。

## 可观测性

拒绝按原因计数（指标端点见[运维指南](/overview-1/ops-guide)）：

* `radus_reject_passwd_error`——口令错误（bind 返回凭证无效）。
* `radus_reject_ldap_error`——后端无法给出答案（目录不可达、StartTLS 失败、 服务账号 bind 失败、配置错误）。

将二者分开，意味着目录故障会体现为 `radus_reject_ldap_error`，而不是一片 「口令错误」的尖峰——于是对 `radus_reject_ldap_error` 的告警能清晰地指向目录 问题而非用户失误。

> **注意。** `radus_accept` 在每次认证成功时自增一次——裸 PAP/CHAP 流程与 EAP 流程都汇聚到同一个 Access-Accept 出口——因此可与上述拒绝计数器配对，用于成功率 或 SLO 观测。目录故障只会体现为 `radus_reject_ldap_error`，不会产生虚假的 `radus_accept`，从而让成功与失败信号保持清晰分离。

## 故障排查

| 现象                                     | 可能原因                                                                    |
| -------------------------------------- | ----------------------------------------------------------------------- |
| 所有登录都失败，`radus_reject_ldap_error` 持续上升 | `ServerURL` 错误/不可达、TLS 握手失败，或（搜索模式）服务账号无法 bind。                         |
| Windows/AD 客户端被拒，PAP 用户正常              | 这些客户端在用 PEAP-MSCHAPv2 或 MS-CHAPv2，LDAP 无法服务——改用 EAP-TTLS/PAP，或改用本地口令后端。 |
| 搜索模式找不到用户                              | 检查 `BaseDN` 与 `UserFilter`；确认服务账号能读取用户条目。                               |
| 实验环境 bind 正常、生产失败                      | `TLSSkipVerify` 此前掩盖了无效证书——安装受信任证书并关闭该开关。                               |
| 口令通过了但会话没有套餐/限速                        | 缺少本地 `RadiusUser` 记录——LDAP 只校验口令，请创建本地账号以提供授权。                          |

## 参见

* [运维指南](/overview-1/ops-guide)——EAP/TLS 证书、指标、进程模型。
* [核心术语与概念](/overview-1/concepts)——PAP、EAP、EAP-TTLS。
* [协议与 RFC 索引](/overview-1/rfc-index)——RFC 5281（EAP-TTLS）、RFC 4511/4513（LDAP）。


# 常见问题解答

> English version: [FAQ](/overview/faq)

按主题分组的常见问题。如果这里没有覆盖你的问题，请检索 [GitHub issues](https://github.com/talkincode/toughradius/issues) 或新开一个。

## 安装与访问

### 忘记管理员密码怎么办？

使用内置工具，指向服务器实际使用的配置文件：

```bash
go run ./cmd/reset-password -c /etc/toughradius.yml -u admin -p <新密码>
```

首次启动账号为 `admin`，口令是一次性随机值并写入 `{workdir}/private/admin-bootstrap-password`（若在首次启动前设置了 `TOUGHRADIUS_ADMIN_PASSWORD` 则使用该值）。历史默认口令 `toughradius` 不再创建，也不会被登录接受。

### SQLite 和 PostgreSQL 怎么选？

SQLite（默认）零依赖——纯 Go 驱动、单文件存于 `{workdir}/data/`——适合实验 与小规模部署。生产规模、高计费量，或需要外部备份工具（`pg_dump`、复制）时 选 PostgreSQL。

### 能不能不用 1812/1813/1816 这些端口？

可以——所有端口都可通过 YAML 或环境变量配置（`radiusd.auth_port`、 `radiusd.acct_port`、`web.port` 等）。见[运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#端口)。

### 管理界面的 HTTPS 端口（1817）不工作

Web TLS 监听需要 `{workdir}/private/toughradius.tls.crt` 与 `.key`。文件缺失 或无效时只记录日志并**仅**停掉 HTTPS 监听——1816 上的 HTTP 继续服务。可用 `cmd/certgen` 生成证书或提供自己的证书。若不需要内置 HTTPS 监听，设置 `web.tls_enabled: false` 或 `TOUGHRADIUS_WEB_TLS_ENABLED=false`。

### `-initdb` 可以再跑一次吗？

**不可以。** 它会删除并重建所有表。仅在首次安装时执行。常规升级无需手动 处理表结构——迁移在启动时自动完成。

## 认证

### 所有请求都被忽略 / 超时

最常见的两个原因：

1. 请求的**源 IP 未登记**为 NAS 设备——在 **NAS 设备** 中添加（或修正 NAT 使源地址符合预期）。
2. **共享密钥不一致**——RADIUS 对报文本身校验失败的包会静默丢弃。

打开 `radiusd.debug: true` 或把日志级别调到 `debug` 查看实际到达的报文。

### 用户认证成功但没有限速

只有 NAS 记录的**厂商**具备限速 VSA（华为、MikroTik、H3C、中兴、爱快）时才 会下发限速属性。登记为 `Standard` 或 `Cisco` 的 NAS 不会收到私有限速属性—— 见[厂商对接指南](https://docs.toughradius.net/overview-1/pages/9XEd83OYKE5jDCwuDPVA#限速单位)。同时确认用户的策略确实设置了 `up_rate`/`down_rate`（Kbps）。

### ocserv 对接后用户进不了群组 / Class 没下发

ocserv 把 RFC 2865 Class（Type 25）当成群组通道。在用户或计费策略上填写 `radius_class`，认证成功后会**原样**写入 Access-Accept。空值不下发。

ocserv 要求：

* `auth = "radius[config=...,groupconfig=true]"`
* Class 格式为 `OU=group1;group2`（多个组用分号；服务端**不会**自动加 `OU=`）
* 这些组还必须出现在 ocserv 的 `select-group` 里

`Domain` 是华为域属性，不是 Class。

### 拨号能认证成功，却拿不到 IP / 地址池没生效

只有在用户或其计费策略上**设置了地址池**时，`Access-Accept` 才会下发 `Framed-Pool`；且下发的池名**必须与 NAS 上的地址池同名**（如 RouterOS 的 `/ip pool`）。名字不一致、或根本没设池，客户端就拿不到（正确的）地址。需要固定 IP 时，可在用户上设置静态 IPv4（下发 `Framed-IP-Address`，覆盖地址池）。端到端 示例见[场景实战手册 · MikroTik](/overview-1/cookbook/cookbook-mikrotik)。

### 有多台 NAS，需要为每台单独配置吗？

需要。**每台 NAS 都要在「NAS 设备」中单独登记**，各自填源 IP（或 identifier） 与**各自的共享密钥**。ToughRADIUS 按报文源地址（或 NAS identifier）匹配对应的 NAS 记录及其密钥；来自未登记源地址的请求会被记录并以「未授权 NAS」拒绝。不同 NAS 可用不同密钥，无需统一。

### 某个用户为什么被拒绝？

拒绝按原因分类（密码错误、用户不存在、已过期、已停用、并发超限、MAC/VLAN 绑定不符、未授权 NAS 等）——仪表盘展示各类计数，日志记录细节。最容易让人 意外的是**绑定不符**：开启 `bind_mac`/`bind_vlan` 后，首次出现的 MAC/VLAN 会被记录，后续请求必须匹配；更换硬件后请清空用户上的已存绑定值。

### 连续输错密码后响应变慢，为什么？

这是拒绝延迟防爆破机制：在 `RejectDelayWindowSeconds`（默认 10 秒）窗口内 连续拒绝达到 `RejectDelayMaxRejects`（默认 7 次）后，响应会被延迟。两个参数 都可在 **系统配置** 中调整。

### ToughRADIUS 支持 802.1X / 企业级 Wi-Fi 吗？

支持。可用的 EAP 方法：EAP-MD5、EAP-MSCHAPv2、EAP-TLS、PEAPv0/EAP-MSCHAPv2 与 EAP-TTLS（内层 PAP / MS-CHAP-V2）。在 **系统配置 → EapMethod** 中选择。

### EAP-TLS / PEAP / EAP-TTLS 启动不了

基于 TLS 的 EAP 需要在系统配置中选择 `EapTlsServerCert`—— 留空时这些方法按设计处于禁用状态。先用 `cmd/certgen` 生成服务器证书，在 「证书管理」中导入后，于系统配置选择该证书名称即可。`EapTlsClientCa` 仅在需要校验客户端证书（EAP-TLS）时选择。

### PEAP / eapol\_test 报 `no cipher suite supported`

默认外层 TLS 只提供 ECDHE+AEAD（`EapTlsCipherProfile=modern`）。hostapd **internal TLS** 编译的 `eapol_test` 只报 RSA/DHE+CBC，两边没有交集。可选：

1. 把 `eapol_test` 编成 `CONFIG_TLS=openssl`（推荐，保留默认 modern）。
2. 将 `EapTlsCipherProfile` 设为 `legacy-rsa-cbc`，并使用 **RSA** 服务器证书。 该档无前向保密，且 Go 不实现 DHE；不要把它当成默认生产策略。

### 服务器与设备时钟不同步会有什么影响？

时钟漂移会带来隐蔽问题：基于 TLS 的 EAP（EAP-TLS / PEAP / TTLS）会校验证书的 有效期（`NotBefore` / `NotAfter`），双方时间偏差过大可能导致握手失败；计费的 起止时间与时长会失真；拒绝延迟防爆破窗口（默认 10 秒）也以服务器时钟为准。请在 服务器与 NAS 上启用 NTP 保持时间同步。

### EAP 方法怎么选？

* **EAP-TLS** —— 最强（双向证书），需要给客户端发证。
* **PEAPv0/EAP-MSCHAPv2** —— Windows/AD 兼容；注意 MS-CHAPv2 的类 NTLMv1 攻击面（见[安全策略](/overview-1/security-policy)）。
* **EAP-TTLS** —— 通过内层 PAP 对接遗留/LDAP 后端，密码受 TLS 隧道保护。

## 会话、CoA 与计费

### 在线会话页的强制下线 / CoA 没有效果

依次检查：设备已开启动态授权（如 RouterOS 的 `radius incoming`、IOS 的 `aaa server radius dynamic-author`）；NAS 记录上的 **CoA 端口** 与设备一致 （默认 3799）；设备接受来自服务器地址的请求。ToughRADIUS 等待 5 秒并重试 2 次后才报告失败。

### 想在线给用户改速率（FUP 超额限速），怎么做？

ToughRADIUS 的「修改授权（CoA）」**只携带 `Session-Timeout` 与 `Filter-Id`**， **不会**在线改写 `Mikrotik-Rate-Limit` 等限速属性。实时变速的标准做法是：先在 计费策略 / 用户上改速率，**再对其执行「强制下线」**，客户端自动重拨后即按新速率 重新授权；也可用 `Filter-Id` 让设备套用预先定义好的限速规则。详见 [场景实战手册 · MikroTik · 在线管控](/overview-1/cookbook/cookbook-mikrotik)。

### CoA / 强制下线提示找不到会话

操作员发起的 CoA / Disconnect 需要先在「在线会话」中定位该会话，再据其 NAS 记录与会话标识（如 `Acct-Session-Id`）向设备发起。若在线记录已残留失效（NAS 未 上报计费停止）或会话已结束，就会匹配不到——刷新在线列表、确认设备计费正常后 重试。

### 在线会话里出现早已下线的用户

在线记录由 NAS 的计费报文创建/刷新。NAS 停发（重启、断链）时记录可能残留。 请确认设备开启了计费与中间更新（每个 Access-Accept 都会下发 `Acct-Interim-Interval`，默认 300 秒）。残留记录也可在界面上手动下线/删除。

### 计费表一直增长，哪些会自动清理？

两个 `@daily` 定时任务会自动清理数据：一个删除一年前的操作日志（`SysOprLog`）； 另一个 `SchedClearExpireData` 会按 `AccountingHistoryDays`（默认 90 天）删除**已结束** 的 `radius_accounting` 计费历史（在线会话的记录不会被删），并清理连续多个计费中间更新 周期未刷新的 `radius_online` 残留在线行。将 `AccountingHistoryDays` 设为 `0` 可 **关闭**计费历史清理（永久保留）。若流量很大、需要更激进的瘦身，仍建议把**数据库级 归档**纳入运维流程。配置备份**不包含**计费历史——见[备份与恢复](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#备份与恢复)。

### 并发会话数没有被限制

计费策略中的 `active_num` 是单用户并发上限（0 表示不限）。该检查统计 `radius_online` 表中的行数，依赖 NAS 正常上报计费——没有计费 Start 报文， 服务器无从得知谁在线。

## 运维

### 怎么接 Prometheus 监控？

目前没有 `/metrics` HTTP 端点；计数器在内存中并通过仪表盘展示。外部监控 建议探测端口、采集日志文件，并对进程退出告警（进程模型为 fail-fast）。

### 怎么安全升级？

停止服务、替换二进制（或拉取新的 Docker 标签）、启动。表结构迁移自动完成。 升级前先做配置备份（系统配置 → 备份）和数据库备份。

### 日志 / 数据 / 证书都存在哪里？

全部位于 `system.workdir`（默认 `/var/toughradius`）之下：`data/`（SQLite 数据库）、`logs/`、`private/`（TLS 材料）、`backup/`（配置快照）。见 [运维指南](https://docs.toughradius.net/overview-1/pages/zKSTnggCnbtzlXqtfAxt#工作目录结构)。


# 协议与 RFC 索引

> English version: [Protocol & RFC Reference](/overview/rfc-index)

ToughRADIUS 实现了标准 RADIUS、EAP、动态授权与安全传输等协议。本章是项目所依赖标准的 **精选、面向实现**的索引，将每个 RFC 映射到它在代码中的使用位置及 [路线图](https://github.com/talkincode/toughradius/blob/main/docs/roadmap.zh.md)里程碑。

完整 RFC 文本归档于 [`docs/rfcs/`](https://github.com/talkincode/toughradius/tree/main/docs/rfcs)； 逐文件的原始目录见 [`docs/rfcs/README.md`](https://github.com/talkincode/toughradius/blob/main/docs/rfcs/README.md)。 当两者出现差异时，以本章的引用为准。

## 已实现的标准

### RADIUS 核心

* **RFC 2865** —— RADIUS 认证；基础的请求/响应协议。
* **RFC 2866** —— RADIUS 计费；会话开始 / 中间更新 / 结束记录。

### RADIUS + EAP 集成

* **RFC 3579** —— RADIUS 对 EAP 的支持（EAP-Message 与 Message-Authenticator）。
* **RFC 3580** —— IEEE 802.1X 的 RADIUS 使用指南。

### EAP 框架与方法

* **RFC 3748** —— 可扩展认证协议（EAP）。框架本身，同时定义了 EAP-MD5，即 MD5-Challenge 方法（§5.4）。
* **RFC 5216** —— EAP-TLS；基于证书的双向认证（里程碑 M1）。
* **RFC 5281** —— EAP-TTLSv0；用 TLS 隧道承载内层 PAP / MS-CHAPv2（M9）。
* **RFC 2759** —— MS-CHAP-V2，用作 EAP-MSCHAPv2 与 PEAPv0 的内层方法（M8）。 MS-MPPE 会话密钥按 RFC 2548 / RFC 5705 派生。

> PEAPv0 没有独立 RFC，遵循 Microsoft/Cisco 的 PEAP 定义，内层承载 EAP-MSCHAPv2。 它以兼容为先——MS-CHAPv2 交换存在类似 NTLMv1 的攻击面——在可控证书环境下优先选用 EAP-TLS。

### 动态授权

* **RFC 5176** —— CoA 与 Disconnect-Request，取代 RFC 3576（里程碑 M2）。

### 安全传输

* **RFC 6614** —— 基于 TLS 的 RADIUS（RadSec）。
* **RFC 6613** —— 基于 TCP 的 RADIUS。

### 厂商私有属性

* **RFC 2548** —— Microsoft 厂商私有属性，包含承载 EAP 密钥材料的 MS-MPPE-Send/Recv-Key 属性。

### IPv6

* **RFC 3162**、**RFC 4818**、**RFC 6911** —— RADIUS 的 IPv6 地址与委派前缀属性 （里程碑 M3）。

## 路线图标准

| RFC                  | 标准                                        | 里程碑 |
| -------------------- | ----------------------------------------- | --- |
| RFC 9190（+ RFC 9427） | EAP-TLS 1.3 与 TLS 1.3 密钥派生                | M10 |
| RFC 7170 / RFC 9930  | TEAP v1 —— 隧道 EAP，machine + user chaining | M11 |
| RFC 5931             | EAP-PWD —— 基于口令，无需为每客户端签发证书               | M12 |

> **目录准确性说明。** 在 `docs/rfcs/` 中，文件 `rfc7542-eap-pwd.txt` 标注有误： **RFC 7542 是网络访问标识符（NAI）**，而 **EAP-PWD 是 RFC 5931**。同样，EAP-MD5 由 **RFC 3748 §5.4** 定义，而非 RFC 3851（一份 S/MIME 规范）。本章采用正确的引用。

## 另见

* [概述](/overview-1) —— 含 EAP 套件的能力概览。
* [文档地图](/overview-1/documentation-map) —— 各源文档的存放位置。
* [RFC Editor](https://www.rfc-editor.org/) —— 权威的在线 RFC 文本。


# EAP 验收测试报告

每周 EAP 验收任务使用外部 `eapol_test` supplicant 验证 ToughRADIUS，并在这里展示最近保留的报告。

**最近结论：** 部分通过

覆盖说明：PEAP/MSCHAPv2 外部 `eapol_test` 场景仍为 skipped，并由 [#495](https://github.com/talkincode/toughradius/issues/495) 跟踪，因此本报告代表部分外部覆盖，不宣称完整 PEAP 外部验收。

## 最近场景摘要

| 场景                                   | 方法                | 预期              | 状态 |     耗时 | 说明                                                                                                                                                                                                                                                                                                                                   |
| ------------------------------------ | ----------------- | --------------- | -- | -----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EAP-TLS valid client certificate     | EAP-TLS           | Access-Accept   | 通过 | 186 ms | external supplicant received the expected Access-Accept                                                                                                                                                                                                                                                                              |
| EAP-TLS untrusted client certificate | EAP-TLS           | Access-Reject   | 通过 |  37 ms | external supplicant was rejected as expected                                                                                                                                                                                                                                                                                         |
| PEAP/MSCHAPv2 valid credentials      | PEAP/MSCHAPv2     | Access-Accept   | 跳过 |   0 ms | Skipped intentionally: eapol\_test currently exposes a PEAP inner-framing interop gap (server rejects the decrypted phase-2 payload as an invalid inner EAP message). The in-process PEAP/MSCHAPv2 integration test remains the current acceptance coverage. Tracking issue: <https://github.com/talkincode/toughradius/issues/495>. |
| PEAP/MSCHAPv2 wrong password         | PEAP/MSCHAPv2     | Access-Reject   | 跳过 |   0 ms | Skipped intentionally: eapol\_test currently exposes a PEAP inner-framing interop gap (server rejects the decrypted phase-2 payload as an invalid inner EAP message). The in-process PEAP/MSCHAPv2 integration test remains the current acceptance coverage. Tracking issue: <https://github.com/talkincode/toughradius/issues/495>. |
| EAP-TTLS/PAP valid credentials       | EAP-TTLS/PAP      | Access-Accept   | 通过 | 138 ms | external supplicant received the expected Access-Accept                                                                                                                                                                                                                                                                              |
| EAP-TTLS/MSCHAPv2 valid credentials  | EAP-TTLS/MSCHAPv2 | Access-Accept   | 通过 | 138 ms | external supplicant received the expected Access-Accept                                                                                                                                                                                                                                                                              |
| Malformed external EAP client config | tooling           | documented skip | 跳过 |   0 ms | Skipped intentionally: eapol\_test parser failures do not exercise ToughRADIUS over RADIUS/EAP. Negative server behavior is covered by untrusted certificate and wrong password scenarios.                                                                                                                                           |

## 保留报告

* [2026-08-10](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-08-10.md)
* [2026-07-27](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-07-27.md)
* [2026-07-20](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-07-20.md)
* [2026-07-13](https://github.com/talkincode/toughradius/blob/main/docs/reports/eap/2026-07-13.md)


# 性能基准测试报告

每周 benchmark 任务基于现有 Go `Benchmark*` 函数记录 ToughRADIUS 性能信号。报告仅用于观察趋势，不会因为 GitHub 托管 runner 的耗时波动直接失败。

**最近结论：** 已记录

## 最近摘要

| 指标           |                                                                     值 |
| ------------ | --------------------------------------------------------------------: |
| Benchmark 数量 |                                                                    15 |
| 包数量          |                                                                     6 |
| 最慢项          | github.com/talkincode/toughradius/v9/pkg/excel / BenchmarkWriteToFile |
| 最高 B/op      | github.com/talkincode/toughradius/v9/pkg/excel / BenchmarkWriteToFile |

## 保留报告

* [2026-08-10](https://github.com/talkincode/toughradius/blob/main/docs/reports/performance/2026-08-10.md)
* [2026-07-20](https://github.com/talkincode/toughradius/blob/main/docs/reports/performance/2026-07-20.md)
* [2026-07-13](https://github.com/talkincode/toughradius/blob/main/docs/reports/performance/2026-07-13.md)


# 安全策略

> English version: [Security Policy](/overview/security-policy)

本章是 ToughRADIUS 安全公告及其配套指引的权威归处。仓库根目录的 [`SECURITY.md`](https://github.com/talkincode/toughradius/blob/main/SECURITY.md) 保留一个指向本章的简短指针，以保证单一事实来源。

## 安全公告

### 默认超级管理员口令（GHSA-2gwm-6gf5-8699）

ToughRADIUS 不再创建或接受历史首次启动口令。全新安装会生成一次性引导口令 （或使用 `TOUGHRADIUS_ADMIN_PASSWORD`）并写入 `{workdir}/private/admin-bootstrap-password`。仍保存历史默认口令的升级会在启动时 轮换。登录与操作员改密接口会拒绝该口令。

| 项目    | 详情                            |
| ----- | ----------------------------- |
| 漏洞类型  | 使用默认凭据（CWE-1392）              |
| 严重程度  | 严重                            |
| 公告    | GHSA-2gwm-6gf5-8699           |
| 受影响组件 | 超级管理员引导与 `/api/v1/auth/login` |

#### 建议措施

升级后确认每个操作员都使用独立口令。如果无法读取 `{workdir}/private/admin-bootstrap-password`，请用 `cmd/reset-password` 重置。

### XSS 漏洞修复（v8.0.8）

**v8.0.8** 版本修复了一个严重的跨站脚本（XSS）漏洞。该问题位于登录接口对 `errmsg` 参数的处理。

| 项目    | 详情                |
| ----- | ----------------- |
| 漏洞类型  | 跨站脚本（XSS）         |
| 严重程度  | 严重                |
| 影响版本  | v8.0.1 – v8.0.7   |
| 修复版本  | v8.0.8            |
| 受影响组件 | 登录接口（`errmsg` 参数） |

#### 建议措施

强烈建议所有用户立即升级到最新版本。可参考[文档地图](/overview-1/documentation-map) 中的 README 与构建说明完成部署升级。


# Agent 开发指南

> English version: [Agent Development Guide](/overview/agent-guide)

本章是一份**面向贡献者**的摘要，介绍 ToughRADIUS 如何借助 AI 编码 agent 进行 开发，归纳工作规则、质量门禁与自动委托循环，使该工作流可从手册统一发现。

**权威**规则位于仓库根目录的 [`AGENT.md`](https://github.com/talkincode/toughradius/blob/main/AGENT.md)；该文件 保持权威地位，并被 agent 工具链直接引用。本章不替代它——如有歧义，以 `AGENT.md` 为准。

## 产品范围基线

开发始终锚定功能清单，绝不漂移到无关的产品方向。

* 权威范围基线为 [`docs/feature-checklist.md`](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.md) （英文版见 [`docs/feature-checklist.en.md`](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.en.md)）。
* 每个任务、issue、PR、测试与评审记录都映射到形如 `TR-F004` 的功能编号。
* 若某需求无法映射到现有编号，先更新功能清单（范围、状态、验收边界、理由）， 再改动代码。
* 非目标 `TR-N001`–`TR-N005`（支付、CRM、通用监控栈、多租户、整体重写）除非先 显式修订清单，否则一律不在范围内。

## 路线图与技能库

agent 驱动开发围绕三份产物组织：

* [`docs/roadmap.zh.md`](https://github.com/talkincode/toughradius/blob/main/docs/roadmap.zh.md) —— 长期路线图与里程碑，每项映射到 `TR-F` 编号，是 agent 工作的任务来源。
* [`.agents/skills/`](https://github.com/talkincode/toughradius/tree/main/.agents/skills/README.md) —— 可复用的技能 SOP，每个技能一个目录（`.agents/skills/<name>/SKILL.md`）。
* [`.agents/README.md`](https://github.com/talkincode/toughradius/blob/main/.agents/README.md) —— 委托参考与共享护栏。

一个**总调度层**驱动循环，执行 SOP 负责领域内的具体工作：

| 角色   | 技能                    | 职责                                         |
| ---- | --------------------- | ------------------------------------------ |
| 总调度  | `orchestrate-roadmap` | “自动委托开发”入口：选取下一个未勾选子任务、匹配 SOP、执行门禁、开 PR    |
| 门禁   | `review-pr`           | 以 CI 为锚的独立审查；通过标签/评论打回，仅在审过**且** CI 绿时自动合并 |
| 自我迭代 | `groom-roadmap`       | 每次合并后勾选已交付子任务并重新梳理路线图                      |

执行 SOP 包括：新增厂商 VSA（`add-radius-vendor`）、新增 EAP 方法 （`add-eap-method`）、新增 Admin API（`add-adminapi-endpoint`）、新增 React Admin 资源（`add-react-admin-resource`）、新增配置项（`add-config-schema`）、新增验收 测试（`add-acceptance-test`）、同步上游 radius（`sync-upstream-radius`）、引用 RFC （`reference-rfc`）、对齐清单（`align-feature-checklist`）、编写 Go 测试 （`write-go-tests`）、编写 Go API 文档（`document-go-apis`）。开始某类任务前先选用 匹配的技能。

agent **在你自己的主机**上用你自己的 agent/CLI 运行，而非通过 CI 工作流执行， 因此密钥不会进入 CI，执行环境完全自控。

## 工作准则

### 动手前先理解现有代码

绝不盲目改代码。先用检索定位现有实现、相关测试与文档，再模仿项目的命名、错误 处理与数据流。修 bug 前先追踪完整执行路径，重构前先梳理依赖与副作用。

### 持续验证

不要等到最后才跑测试。每个逻辑改动后都跑一次测试，使回归立即暴露，而不是堆到 大批量改动的末尾。

### 代码是最好的文档

* **所有导出 API 均带完整 godoc 注释**——用途、带约束的参数、返回值与错误条件 （复杂 API 附使用示例）。标准库风格的约定见 `document-go-apis` 技能。
* **复杂逻辑带行内注释**，解释*为什么*而非*做什么*。
* **厂商相关代码引用协议规范**（RFC 编号、VSA 文档）。
* **不产出冗余的独立总结文档**——信息应留在代码注释与 Git 历史中。

## 核心开发原则

### 测试驱动开发（TDD）

先写测试再写代码：用失败的测试定义预期行为（红），写最少代码使其通过（绿）， 随后在测试保持通过的前提下重构。若改动 `internal/radiusd/auth.go`，测试位于 `internal/radiusd/auth_test.go`。统一约定见 `write-go-tests` 技能。

### GitHub 工作流

* **仅走 Pull Request**——禁止直接推 `main`；受保护分支会拒绝直接推送。
* **约定式提交**——`<type>(<scope>): <subject>`，类型如 `feat`、`fix`、`test`、 `docs`、`refactor`、`perf`、`chore`。
* **小而原子的改动**优于巨型 PR。

### 仓库 issue/PR 自动化

仓库通过若干 GitHub Actions 做轻量分流，帮助维护者扫描队列；它们不替代阅读 原始 issue、PR diff 与 CI 输出。

| Workflow      | 触发与结果                                                                                                                                    | 维护者注意事项                                                                                                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stale         | `.github/workflows/stale.yml` 每天 `04:24 UTC` 自动运行，也可手动触发。60 天无活动后添加 `stale`，再过 14 天仍无活动则关闭。                                              | 评论、推送提交或移除 `stale` 可保活。带 `pinned`、`security`、`help wanted`、`agent-roadmap`、`needs-human` 的 issue 豁免；带 `pinned`、`security`、`agent-roadmap`、`needs-human` 的 PR 豁免；所有 milestone 豁免。 |
| Labeler       | `.github/workflows/labeler.yml` 在 `pull_request_target` 上运行，并按 `.github/labeler.yml` 的路径规则打标签。                                           | 自动标签包括 `go`、`javascript`、`github_actions`、`dependencies`、`doc`。该 action 只读取变更文件列表和基础分支配置，不 checkout 或执行 PR 代码。                                                                   |
| Workflow Lint | `.github/workflows/workflow-lint.yml` 在修改 `.github/workflows/**` 或 `.github/actionlint.y*ml` 的 PR、触及这些路径的 `main` push，以及手动 dispatch 时运行。 | 它运行 `actionlint -shellcheck=`，因此门禁范围限定为 GitHub Actions YAML、表达式与 action 输入验证，不把既有 shell 风格告警混入本门禁。这只是静态验证，不执行 release tag、Docker 发布、Pages 部署，也不读取 secrets。                       |
| Greetings     | `.github/workflows/greetings.yml` 在贡献者首次打开 issue 或 PR 时运行，并发送入门提示评论。                                                                     | 评论仅用于引导，不改变评审要求或 issue 优先级。                                                                                                                                                      |

### Release tag 与 Docker 发布自动化

推送 `v*` tag 会触发两条发布 workflow；发版前需要同时理解二者的输出与凭据边界。

| Workflow        | 触发与输出                                                                                                                                                                    | 发版前置条件                                                                                                                                                                                                                             |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Release publish | `.github/workflows/release-publish.yml` 在 `v*` tag push 时运行，构建多平台二进制、生成 checksums，并创建 GitHub Release。                                                                    | tag 必须指向已验证的 `origin/main` 目标 SHA；release notes 由 workflow 基于 tag 和 closed issues 生成。                                                                                                                                              |
| Docker publish  | `.github/workflows/docker-publish.yml` 在 `v*` tag push 时运行，发布 `talkincode/toughradius:latest` 与版本 tag 到 Docker Hub，并独立尝试发布 `ghcr.io/<owner>/toughradius:latest` 与版本 tag。 | Docker Hub 需要 `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN`。GHCR 需要 package repository access / inherited access 允许本仓库 `GITHUB_TOKEN` 写入，或配置具备 `write:packages` 的 `PKG_GITHUB_TOKEN`；若 token 所属账号不同于 tag 触发者，可选配 `PKG_GITHUB_USERNAME`。 |

Docker Hub 发布是必选门禁。GHCR 发布由于依赖 GitHub Packages 的外部 package access 设置，workflow 会把 GHCR push 独立成单独步骤，并先执行一次非破坏性的 写权限预检：若凭据不具备 package 写权限（即 `permission_denied: write_package` 签名），会直接跳过 GHCR 构建而不是在 push 中途失败，run summary 会给出 warning 与恢复提示，不再让维护者误判 Docker Hub 是否已发布。

发版前检查：

* 确认 tag 前的 `origin/main` CI 已绿，且 tag 版本号未被占用。
* 确认 Docker Hub secrets 存在且仍可写。
* 确认 GHCR package `talkincode/toughradius` 继承本仓库权限，或配置了 `PKG_GITHUB_TOKEN`（可选 `PKG_GITHUB_USERNAME`）；文档只记录 secret 名称和权限要求， 不记录真实值。
* tag 后检查 GitHub Release、Docker Hub `latest` / 版本 tag、GHCR `latest` / 版本 tag，以及两条 workflow 的最终结论。

失败恢复：

* 若 GitHub Release 成功、Docker Hub 成功、GHCR 失败，先修复 GHCR package access 或 token 权限，再重跑对应 tag workflow；不要为了同一源码重复创建错误版本 tag。
* 若 Docker Hub 发布失败，视为发布未完成，修复 secret 或 registry 问题后重跑 workflow。
* 若需要用新 patch tag 验证，先按 `release-version` SOP 重新审查未发布变更，避免跳过 版本判断。

### 周报 PR 自动化

每周报告 workflow 会通过短期自动化分支上的签名提交发布生成报告。workflow 在进入 报告 PR 步骤前都会先上传生成 artifact，因此即使 PR 创建被跳过或阻塞，维护者仍可 从本次运行的 artifact 检查报告内容。

| Workflow                     | 触发与发布文件                                                                                                                                                                 | PR 前置条件与 fallback                                                                                                                                                                                                                                                                                                                                                              |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EAP acceptance weekly        | `.github/workflows/eap-acceptance-weekly.yml` 每周一 `09:17 UTC` 定时运行，也可手动触发。发布 `docs/reports/eap/<date>.md`、`docs/reports/eap/latest.md` 与 EAP 报告索引页。                     | 当外部 EAP 验收步骤成功且报告文件有变化时，PR 步骤需要 repository secret `EAP_REPORT_SIGNING_KEY`，以及 repository variables `EAP_REPORT_SIGNING_EMAIL` 和可选的 `EAP_REPORT_SIGNING_NAME`。缺少签名 key 或 email 会阻塞 PR 步骤，因为受保护的 `main` 要求 verified signed commit；此时可从已上传的 `eap-acceptance-<run_id>` artifact 读取生成报告。                                                                                            |
| Performance benchmark weekly | `.github/workflows/performance-benchmark-weekly.yml` 每周一 `10:37 UTC` 定时运行，也可手动触发。发布 `docs/reports/performance/<date>.md`、`docs/reports/performance/latest.md` 与性能报告索引页。 | 当 benchmark 完成且报告文件有变化时，PR 步骤使用 `PERFORMANCE_REPORT_SIGNING_KEY`、`PERFORMANCE_REPORT_SIGNING_EMAIL` 和可选的 `PERFORMANCE_REPORT_SIGNING_NAME`。若这些未配置，则复用 `EAP_REPORT_SIGNING_KEY`、`EAP_REPORT_SIGNING_EMAIL` 和可选的 `EAP_REPORT_SIGNING_NAME`。若没有任何签名 key/email 组合，workflow 会把生成报告保留在 `performance-benchmark-<run_id>` artifact 中，在 workflow step summary 说明已跳过 PR 创建，并让 PR 步骤成功退出。 |

这里和 issue/PR 评论中都不记录签名 key 的真实值。只记录 secret / variable 名称、 用途，以及是否已配置。

检查生成报告运行时：

* 先查看 workflow summary，确认是否存在跳过 PR 或凭据配置提示。
* 若没有打开报告 PR，从本次 run 的 artifact 下载生成报告。
* 若已生成报告 PR，合并前确认 head commit 在 GitHub 上显示为 verified。
* 对照 PR 正文中的发布文件列表与 workflow 预期的报告路径。
* 报告 PR 的 CI job 状态与 `mergeStateStatus=UNSTABLE` 行为属于 issue #494 继续跟踪的实现边界，本说明不改变该决策。

### 最小可行产品（MVP）

每次改动以最小、可独立使用、可回滚且不破坏既有行为的单元交付。大型工作拆成 MVP 增量（例如：厂商属性解析 → 认证集成 → 计费 → 管理界面），而非一次性塞进 一个超大 PR。

## 质量门禁

每个 agent 改动在合并前必须通过以下门禁：

* `go build ./...` —— 无编译错误。
* `go test ./...` —— 全部单元测试通过。
* `golangci-lint run` —— 干净（钉 **v2.12.2**，与 CI 一致）。
* `cd web && npm run build` —— 任何前端改动。
* `Workflow Lint` —— 修改 `.github/workflows/**` 或 `.github/actionlint.y*ml` 时必须通过；该门禁运行 `actionlint -shellcheck=`， 用于验证 GitHub Actions YAML、表达式与 action 输入。
* **协议 / 端到端改动**在 [`test/integration/`](https://github.com/talkincode/toughradius/tree/main/test/integration/README.md) 下附 CI 可执行的验收测试，并引用 [`docs/rfcs/`](https://github.com/talkincode/toughradius/tree/main/docs/rfcs/README.md) 下对应的规范。
* 产出一律走打了 `agent-roadmap` 标签的 PR，由 `review-pr` 门禁把关，仅在 `agent-approved` 且 CI 全绿时合并。

## 技术约束

* **禁用 CGO**——项目以 `CGO_ENABLED=0` 构建以便跨平台部署。仅使用纯 Go 驱动 （例如用 `github.com/glebarez/sqlite` 而非 `github.com/mattn/go-sqlite3`）。
* **数据库双兼容**——每次 schema 变更都必须同时兼容 PostgreSQL（默认）与 SQLite。
* **上游依赖**——核心库 `layeh.com/radius` 经 `go.mod` `replace` 指向组织 fork `github.com/talkincode/radius`；上游重要修复通过 `sync-upstream-radius` 技能评估。

## 常见反模式（禁止）

* 导出 API 不写文档。
* 不写测试就提交实现。
* 混杂多个关注点的巨型 PR。
* 先实现后补测试。
* 直接推 `main` 或跳过评审。
* 产出冗余的独立总结/报告文档。
* 引入 CGO 依赖。

## 下一步

* [`AGENT.md`](https://github.com/talkincode/toughradius/blob/main/AGENT.md) —— 完整、权威的 agent 开发指南。
* [文档地图](/overview-1/documentation-map) —— 找到 README、安全策略、功能清单、路线图 与 RFC 索引。
* [协议与 RFC 索引](/overview-1/rfc-index) —— 协议标准与代码、里程碑的对应关系。


# 文档地图

> English version: [Documentation Map](/overview/documentation-map)

本手册采用分批收编的方式构建。下表先列出手册自身的章节，再列出仍位于仓库中的 文档位置，使所有文档都能从同一入口访问。

## 手册章节

| 章节                                                           | 内容                                                                                               |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| [概述](/overview-1)                                            | 项目简介、核心能力与服务模型                                                                                   |
| [核心术语与概念](/overview-1/concepts)                              | AAA 术语、认证请求流转与密码协议                                                                               |
| [快速开始](/overview-1/quickstart)                               | 安装、初始化、首个用户与调试                                                                                   |
| [厂商对接指南](/overview-1/vendor-guide)                           | MikroTik、华为、Cisco、H3C、中兴、爱快等对接案例                                                                 |
| [Portal / Hotspot 对接边界](/overview-1/portal-hotspot-boundary) | 铁律：ToughRADIUS 是 RADIUS AAA 后端，不托管 Captive Portal 登录页                                            |
| [场景实战手册](/overview-1/cookbook)                               | 端到端运维场景（五段式）：MikroTik PPPoE / Hotspot / CoA；华为 BRAS 分级套餐 / 线路绑定 / CoA；H3C / 中兴 / 爱快 / Cisco 差异速查 |
| [管理系统用户手册](/overview-1/admin-manual)                         | 管理控制台逐页说明                                                                                        |
| [运维指南](/overview-1/ops-guide)                                | 配置参考、证书、监控、备份与命令行工具                                                                              |
| [LDAP / AD 认证后端](/overview-1/auth-ldap)                      | 针对 LDAP/AD 目录校验口令（仅 PAP 族）：bind 模式、TLS、安全、指标                                                     |
| [常见问题解答](/overview-1/faq)                                    | 按主题分组的常见问题                                                                                       |
| [协议与 RFC 索引](/overview-1/rfc-index)                          | 协议标准与代码的对应关系                                                                                     |
| [安全策略](/overview-1/security-policy)                          | 安全公告与升级指引                                                                                        |
| [Agent 开发指南](/overview-1/agent-guide)                        | 面向贡献者的 AI-agent 工作流、质量门禁与自动委托循环摘要                                                                |

## 仓库内文档

| 文档       | 说明                 | 当前位置                                                                                                                                                                                                     |
| -------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| README   | 项目介绍、特性与快速上手       | [README.md](https://github.com/talkincode/toughradius/blob/main/README.md)                                                                                                                               |
| Agent 指南 | AI Agent 开发指南与协作规则 | [Agent 开发指南](/overview-1/agent-guide)（手册摘要） · [AGENT.md](https://github.com/talkincode/toughradius/blob/main/AGENT.md)（权威）                                                                               |
| 安全策略     | 安全公告与升级指引          | [安全策略](/overview-1/security-policy)（权威） · [SECURITY.md](https://github.com/talkincode/toughradius/blob/main/SECURITY.md)（指针）                                                                             |
| 功能清单     | 功能范围基线（`TR-F` 编号）  | [docs/feature-checklist.md](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.md) · [English](https://github.com/talkincode/toughradius/blob/main/docs/feature-checklist.en.md) |
| 路线图      | 长期路线图与里程碑          | [docs/roadmap.zh.md](https://github.com/talkincode/toughradius/blob/main/docs/roadmap.zh.md)                                                                                                             |
| RFC 索引   | 项目使用的协议标准索引        | [协议与 RFC 索引](/overview-1/rfc-index)（权威） · [docs/rfcs/README.md](https://github.com/talkincode/toughradius/blob/main/docs/rfcs/README.md)（原始目录）                                                           |

> **迁移计划。** 手册现已覆盖 README 中面向用户的内容（概述、快速开始、厂商 对接、管理手册、运维、FAQ）；README 继续作为 GitHub 首页并链接到这里。 Agent 指南现已新增手册[摘要章节](/overview-1/agent-guide)，指向权威的 [`AGENT.md`](https://github.com/talkincode/toughradius/blob/main/AGENT.md)； 由于 agent 工具链直接引用，`AGENT.md` 仍保留在仓库根目录。功能清单与路线图是 由专门流程维护的**活文档**，保持在 `docs/` 中并以链接方式纳入。


# mdbook 与 GitBook 并存

> English version: [mdbook & GitBook Coexistence](/overview/gitbook-coexistence)

ToughRADIUS 也通过 **GitBook** 发布文档（`docs.toughradius.net`）。本节记录路线图 条目 **M13.0** 的决策：新的 mdBook 手册与既有 GitBook 管线如何相处。

## 决策：并存而非替代

mdBook 手册与 GitBook **并存**，不替代、也不停用 GitBook 站点。两套管线相互独立、 互不冲突：

* **mdBook 手册** —— 位于仓库的 `docs-site/` 目录，以双语章节编写，并在每个 Pull Request 上由 CI 构建与坏链校验。它是需要与代码同步演进的文档的权威来源：受版本 控制、走评审门禁。
* **GitBook 站点** —— 通过 GitBook 自有的 GitHub 集成（外部服务）从仓库同步。它由仓库 根目录提交的 **`.gitbook.yaml`** 配置：将 GitBook 指向同一份 `docs-site/src/` 源文件， 以 `introduction.md` 作为首页，并读取**共享的 `SUMMARY.md`** 作为目录。因此 GitBook 渲染的是经过整理的双语手册，而不是从整个仓库推断出的目录树。

两套管线都从**同一份** `docs-site/src/` 源文件构建，但各自保留独立配置（mdBook 用 `book.toml`，GitBook 用 `.gitbook.yaml`）。它们不共享构建步骤、不会相互破坏；同时由于 读取相同的章节与同一个 `SUMMARY.md`，也不会发生内容漂移。

## 各站点的对外地址

两套管线发布到**不同**域名，因此不会相互遮蔽：

* **mdBook 手册** —— 由 `.github/workflows/pages.yml`（路线图条目 **M13.5**）部署到 **GitHub Pages**，使用自定义域名 [**https://www.toughradius.net/**](https://www.toughradius.net/) 对外服务。部署 工作流会在产物中写入 `CNAME` 文件，因此每次发布都会保持该自定义域名。要让该域名 解析生效，`www.toughradius.net` 必须指向 GitHub Pages —— 将 `A` 记录设为 `185.199.108.153`–`185.199.111.153`，或将 `CNAME` 以「仅 DNS」方式指向 `talkincode.github.io`。仓库默认项目地址 <https://talkincode.github.io/toughradius/> 仍然可用，并会重定向到该自定义域名。
* **GitBook** —— 服务 **`docs.toughradius.net`**，解析到 GitBook 自有托管 （Cloudflare / Fastly），而非 GitHub Pages。由于 `www.toughradius.net` 迁移到 GitHub Pages，GitBook 应当配置为仅保留 `docs.toughradius.net` 并释放 `www`，使两个 服务不会争用同一主机名。

## 单一事实来源

为避免两套管线之间出现内容漂移，每份文档只有唯一的权威位置。由于 mdBook 与 GitBook 现在读取**同一份** `docs-site/src/` 章节与同一个 `SUMMARY.md`，手册源文件即是两个渲染 站点的单一事实来源。随着散落文档逐步迁入手册（路线图条目 M13.2 / M13.3），原始文件会 保留一个指向对应章节的简短入口，而不是复制其内容。

## 编辑共享目录

`docs-site/src/SUMMARY.md` 会被**两个**工具同时读取，因此只能使用两者解析方式一致的 Markdown 子集：顶部的 `# Summary` 标题、一个非列表项的首页链接 `[Introduction / 引言](./introduction.md)`，以及用于分组的**嵌套列表**。两个语言分区 表示为顶层条目（`- [English](./en/overview.md)` 与 `- [中文](./zh/overview.md)`），其余 页面嵌套其下。请避免使用 `#` / `##` 标题来分组：mdBook 只按 `#` 分组，而 GitBook 只按 `##` 分组，因此嵌套列表是两者渲染方式完全一致的唯一形式。

## 导航栏语言切换

mdBook 输出会在顶部菜单栏注入一个 EN / 中文 切换链接 （`docs-site/assets/lang-toggle.{js,css}`，通过 `book.toml` 的 `additional-js` / `additional-css` 接入）。它只改写路径中最后一段 `/en/` 或 `/zh/`，之所以可行，是因为两个语言目录 保持完全相同的文件名。该切换**仅**存在于 mdBook/Pages 管线；GitBook 不读取 `book.toml` 的附加资源，GitBook 读者请使用侧边栏分区与每章的交叉链接切换语言。

## 构建与校验

* 本地：`mdbook build docs-site` 会在 `docs-site/book/` 生成静态站点； `mdbook serve docs-site` 可开启热重载预览。
* CI：一个独立的任务负责构建手册，并对生成的 HTML 执行**离线坏链检查**，因此构建 失败或内部坏链都会让流水线变红（路线图条目 M13.4）。构建产物（`docs-site/book/`） 属于构建工件，不纳入版本控制。


