231 lines
7.3 KiB
Markdown
231 lines
7.3 KiB
Markdown
# Поведение `Amnezia limits (old bash)`
|
|
|
|
Этот документ фиксирует фактический behavior старой bash-системы. Он нужен как migration contract: Rust-версия должна воспроизвести не только видимый лимит 70 Mbit/s, но и всю сетевую topology, lifecycle-логику и диагностику.
|
|
|
|
## 1. Файлы старой системы
|
|
|
|
```text
|
|
Amnezia limits (old bash)/
|
|
├── limit_peers.sh.txt # основной apply/reconcile traffic-control rules
|
|
├── docker_watcher.sh.txt # watcher Docker container start events
|
|
├── check-vpn-limits.txt # status/diagnostic CLI
|
|
├── amnezia-tc.service.txt # systemd oneshot apply
|
|
├── amnezia-tc-watcher.service.txt # systemd watcher
|
|
├── traffic_control-was.txt # production snapshot of tc qdisc/classes/filters
|
|
├── wg-peers.txt # production snapshot of wg peers
|
|
├── iproute2_a_core.txt # kernel/iproute2/docker/container facts
|
|
├── permissions.txt # installed paths and enabled units
|
|
└── Нюансы.txt # operational notes and known risks
|
|
```
|
|
|
|
## 2. Runtime environment from snapshots
|
|
|
|
Зафиксированное окружение:
|
|
|
|
- Debian 13 cloud amd64 kernel line: `6.12.57+deb13-cloud-amd64`.
|
|
- `tc` from `iproute2-6.15.0`.
|
|
- Docker `26.1.5+dfsg1`.
|
|
- Amnezia container name in snapshot: `amnezia-awg2`.
|
|
- Interface in snapshot: `awg0`.
|
|
- Systemd units `amnezia-tc.service` and `amnezia-tc-watcher.service` enabled.
|
|
- Scripts installed in `/opt/amnezia/` and executable by root.
|
|
|
|
## 3. Container discovery
|
|
|
|
`limit_peers.sh` searches running containers in this order:
|
|
|
|
```text
|
|
amnezia-awg2
|
|
amnezia-awg
|
|
```
|
|
|
|
`check-vpn-limits` uses the opposite order in its array:
|
|
|
|
```text
|
|
amnezia-awg
|
|
amnezia-awg2
|
|
```
|
|
|
|
Rust should normalize this into one configured ordered list. Recommended default: `amnezia-awg2`, then `amnezia-awg`, because `amnezia-awg2` is the current AWG 2.0 production name in the snapshot.
|
|
|
|
## 4. Interface discovery
|
|
|
|
`limit_peers.sh` starts from `WG_IF`, default `wg0`. If this interface is absent inside the container, it auto-detects:
|
|
|
|
```text
|
|
wg[0-9]+ | wg | awg[0-9]* | amnezia[0-9]* | tun[0-9]+
|
|
```
|
|
|
|
`check-vpn-limits` detects:
|
|
|
|
```text
|
|
wg[0-9]+ | wg | amnezia[0-9]* | tun[0-9]+ | veth[0-9]+
|
|
```
|
|
|
|
Operational note says the important risk is future Amnezia interface rename between `awg0`, `wg0`, `amnezia0`. Rust must use a single detector shared by apply, daemon, watcher and status.
|
|
|
|
## 5. Peer discovery
|
|
|
|
Primary source:
|
|
|
|
```bash
|
|
wg show all allowed-ips
|
|
```
|
|
|
|
The script extracts the third field and keeps IPv4 addresses matching:
|
|
|
|
```text
|
|
10.8.1.[0-9]
|
|
```
|
|
|
|
Fallback source:
|
|
|
|
```bash
|
|
wg show
|
|
```
|
|
|
|
Then regex extracts the same `10.8.1.x` addresses.
|
|
|
|
The production peer snapshot contains 23 peers in `10.8.1.0/24`, with a mix of online peers and peers without endpoint/handshake. Rust must shape all peers with allowed IPs, not only currently online peers.
|
|
|
|
## 6. Traffic-control topology
|
|
|
|
The old script owns both egress and ingress shaping.
|
|
|
|
Egress on the AWG/WG interface:
|
|
|
|
```text
|
|
<iface> root qdisc: handle 1: htb
|
|
<iface> root class: 1:1 rate 1000mbit ceil 1000mbit
|
|
per peer class: 1:<class_id> rate LIMIT ceil LIMIT
|
|
per peer qdisc: fq_codel under 1:<class_id>
|
|
per peer filter: match ip dst <peer_ip> flowid 1:<class_id>
|
|
```
|
|
|
|
Ingress is implemented through IFB:
|
|
|
|
```text
|
|
modprobe ifb
|
|
ip link add name ifb0 type ifb
|
|
ip link set dev ifb0 up
|
|
<iface> ingress qdisc: handle ffff:
|
|
<iface> ingress filter: action mirred egress redirect dev ifb0
|
|
ifb0 root qdisc: handle 2: htb
|
|
ifb0 root class: 2:1 rate 1000mbit ceil 1000mbit
|
|
per peer class: 2:<class_id> rate LIMIT ceil LIMIT
|
|
per peer qdisc: fq_codel under 2:<class_id>
|
|
per peer filter: match ip src <peer_ip> flowid 2:<class_id>
|
|
```
|
|
|
|
Default limit:
|
|
|
|
```text
|
|
LIMIT=70mbit
|
|
```
|
|
|
|
Default root rate:
|
|
|
|
```text
|
|
1000mbit
|
|
```
|
|
|
|
Default first class id:
|
|
|
|
```text
|
|
10
|
|
```
|
|
|
|
The snapshot `traffic_control-was.txt` shows classes `1:10` through `1:32`, which corresponds to 23 peers. Class assignment follows bash `sort -u`, so it is lexicographic string order, not numeric IPv4 order. For example, `10.8.1.10` sorts before `10.8.1.2`. Rust v1 must preserve this if strict parity with production class IDs is required.
|
|
|
|
## 7. Rebuild behavior
|
|
|
|
Before applying new rules, the script deletes old topology:
|
|
|
|
```bash
|
|
tc qdisc del dev <iface> root || true
|
|
tc qdisc del dev <iface> ingress || true
|
|
ip link del ifb0 || true
|
|
```
|
|
|
|
Then it recreates everything. This is important: the production model is full rebuild on detected mismatch. Rust v1 should keep this behavior unless a well-tested incremental diff is implemented later.
|
|
|
|
## 8. Drift detection in bash
|
|
|
|
The script reads existing filters:
|
|
|
|
```bash
|
|
tc filter show dev <iface> parent 1:
|
|
```
|
|
|
|
It extracts hex IP matches and compares count/content with expected peer IPs. If count differs or any peer IP is missing, it rebuilds rules.
|
|
|
|
Weakness: this only strongly verifies egress filters on `<iface>`. It does not fully verify `ifb0`, ingress redirect, qdisc leaves or per-peer ingress filters. Rust must improve this and treat any partial state as drift.
|
|
|
|
## 9. Docker watcher behavior
|
|
|
|
`docker_watcher.sh` listens to:
|
|
|
|
```bash
|
|
docker events --filter event=start --format '{{.Actor.Attributes.name}}'
|
|
```
|
|
|
|
If the started container is `amnezia-awg2` or `amnezia-awg`, it waits up to 20 seconds for:
|
|
|
|
```bash
|
|
ip link show awg0
|
|
```
|
|
|
|
Then it runs:
|
|
|
|
```bash
|
|
WG_IF=awg0 /opt/amnezia/limit_peers.sh
|
|
```
|
|
|
|
Weakness: hardcoded `awg0`. Rust watcher must reuse the same interface detector as `apply-limits`.
|
|
|
|
## 10. Status behavior
|
|
|
|
`check-vpn-limits` reports:
|
|
|
|
- status of `amnezia-tc.service`;
|
|
- status of `amnezia-tc.timer`, although timer file is not present in the archive;
|
|
- running container and Docker status string;
|
|
- connected clients from `wg show`;
|
|
- TC classes with `rate 70Mbit` on the AWG/WG interface;
|
|
- filter statistics from `tc -s filter show dev <iface> parent 1:`;
|
|
- last 3 events from `/var/log/amnezia-tc.log`.
|
|
|
|
Rust `status` must be at least as useful, but should not hardcode 70 Mbit/s; it should use config and display desired vs actual.
|
|
|
|
## 11. Operational guarantees stated in old notes
|
|
|
|
The old notes claim the system currently survives:
|
|
|
|
- host reboot;
|
|
- Docker restart;
|
|
- container restart;
|
|
- Amnezia update;
|
|
- user addition;
|
|
- user removal.
|
|
|
|
They also identify two breakage scenarios:
|
|
|
|
- Amnezia fully changes `wg show` output format;
|
|
- container loses `tc` or `iproute2` tools.
|
|
|
|
In the Rust architecture, the second risk should be reduced by host namespace control: `tc/ip` should preferably come from the host while operating in the container netns, rather than requiring them inside the container.
|
|
|
|
## 12. Migration contract
|
|
|
|
Rust migration is not accepted until all of the following are true:
|
|
|
|
- same peers are discovered as in old bash;
|
|
- same default limit is applied: 70 Mbit/s per peer;
|
|
- both egress and ingress shaping exist;
|
|
- `ifb0` exists and is up when ingress shaping is enabled;
|
|
- class ids are deterministic, start from 10 and use lexicographic bash `sort -u` peer ordering;
|
|
- full rebuild removes stale peer rules after peer deletion;
|
|
- repeated apply is safe;
|
|
- daemon recovers after Docker/container lifecycle events;
|
|
- status explains actual drift rather than printing only a success banner.
|