Author SHA1 Message Date
windyboy d43fc5aa01 docs(dns): correct gfw/.1 facts (W1N-56)
- hosts/gfw.windy.lan.md: 3 NICs (eth2/VLAN10 ubunt_upg live), mosdns is
  now OpenClash's nameserver (not idle), rewrite VLAN10 Wi-Fi section to
  live-verified state
- docs/lan-dns-architecture.md: mosdns on gfw no longer 闲置; note the
  recommended AGH+.36 companion architecture is still pending review
2026-08-12 21:38:57 +08:00
windyboy ab0fd16a92 docs(dns): add LAN DNS architecture research + recommendation (W1N-56) 2026-08-12 08:39:55 +08:00
windyboy 9c1d5ab6fe docs(dns): record agh_ui_access LAN55 allow for Home Assistant (2026-08-12) 2026-08-12 08:36:51 +08:00
windyboy 562de68127 docs: record VLAN10 live verification (DHCP lease, NAT counters, isolation) 2026-08-11 19:36:20 +08:00
windyboy 86bfb5a953 docs: record SE5420 VLAN10 deployment pitfalls (trunk PVID, fw4 dest=lan, minimal gfw migration) 2026-08-11 19:22:22 +08:00
windyboy 3ec51299cc docs: note VLAN1 undeletable on SE5420; disable unused ports first 2026-08-11 13:13:33 +08:00
windyboy 7a54e8d9ec docs: align SE5420 guide with actual Chinese GUI menus and port layout 2026-08-11 12:59:46 +08:00
windyboy 7b4c48eb93 docs: refine SE5420 upgrade guide into granular step-by-step ops 2026-08-11 11:07:25 +08:00
windyboy ffb37a916e docs: finalize SE5420 deployment guide per review
- Reorder §3.3: create VLAN66/55 before SVI; keep VLAN1 until management path verified
- Fix §3.4 verification port wording (enabled VLAN66 access port, e.g. port 6)
- Harden VLAN10 phase: PVE boundary, gfw NAT/IPv6 policy, rollback order, verification chain
2026-08-10 11:00:28 +08:00
windyboy 35577d0c5c docs: add SE5420 core-switch deployment plans; note PVE VM topology 2026-08-09 21:22:02 +08:00
windyboy e90eef9647 docs: record UniFi SSO login change and Ansible management (W1N-51) 2026-08-08 09:17:55 +08:00
windyboy 14a758a28a feat: manage UniFi SSO login setting via Ansible (W1N-51) 2026-08-08 09:15:49 +08:00
windyboy c386fe8136 docs: record gfw OpenClash core v1.19.29 upgrade (W1N-50) 2026-08-06 16:45:11 +08:00
windyboy e2884069a8 docs: add live-verified LAN overview; refresh gfw facts 2026-08-06 16:22:20 +08:00
windyboy b03d7019c4 docs: onboard AdGuard Home to Ansible; retire check-mx2; migrate plan to Linear
- Add dns.windy.lan to Ansible inventory (adguardhome group/profile) with a
  read-only healthcheck template, systemd timer, and on-demand report parity
  with the other active hosts; verified green on the host.
- Add synapse.chans.xyz to the AGENTS.md active-host quick map.
- Remove plans/; migrate decisions/verification to a Linear vps project doc and
  file the remaining gaps as W1N-46..49 with blocking edges.
- Retire scripts/check-mx2.sh; point mailcow health/update runbooks and the
  mx2 host file at the Ansible health report.
- Trim us2 stub software to verified running services; note dormant dirs.
- Normalize repo docs to English; drop the non-existent CONTEXT.md/ADR claim
  and remove docs/agents/domain.md.
2026-08-06 15:48:48 +08:00
windyboy 29bdf06383 Document network operations and WireGuard maintenance 2026-08-06 10:20:43 +08:00
42 changed files with 3800 additions and 291 deletions
+16 -6
View File
@@ -27,7 +27,10 @@ Also readable as `agent.md` (symlink → this file).
| **mx2.windy.me** | mailcow (`/opt/mail`, project `cow`) | `ssh -4 windy@mx2.windy.me` | [hosts/mx2.windy.me.md](hosts/mx2.windy.me.md) |
| **us2.wsvc.info** | Vaultwarden + Traefik (+ Soft Serve, …) | `ssh -4 windy@us2.wsvc.info` | [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md) |
| **hk2.chans.xyz** | PowerDNS auth ns1 (`/opt/pdns`) | `ssh -4 windy@hk2.chans.xyz` | [hosts/hk2.chans.xyz.md](hosts/hk2.chans.xyz.md) |
| **synapse.chans.xyz** | Matrix ESS (Synapse + MAS + Element) on K3s | `ssh -4 windy@synapse.chans.xyz` | [hosts/synapse.chans.xyz.md](hosts/synapse.chans.xyz.md) |
| **dns.windy.lan** | AdGuard Home LAN DNS + Mihomo explicit proxy | `ssh -4 windy@192.168.66.36` | [hosts/dns.windy.lan.md](hosts/dns.windy.lan.md) |
| **gfw.windy.lan** | OpenWrt LAN gateway / OpenClash | `ssh -4 root@192.168.66.1` | [hosts/gfw.windy.lan.md](hosts/gfw.windy.lan.md) |
| **gw** | EdgeRouter X primary LAN gateway | `ssh -4 zhiqiang@192.168.66.254` | [hosts/gw.md](hosts/gw.md) |
| **ubnt** | UniFi Network Controller | `ssh -4 windy@192.168.66.46` | [hosts/ubnt.md](hosts/ubnt.md) |
### Public services
@@ -59,7 +62,7 @@ Also readable as `agent.md` (symlink → this file).
| Task | Path |
|------|------|
| mailcow health | [runbooks/mailcow-health.md](runbooks/mailcow-health.md) · `./scripts/check-mx2.sh` |
| mailcow health | [runbooks/mailcow-health.md](runbooks/mailcow-health.md) |
| mailcow update | [runbooks/mailcow-update.md](runbooks/mailcow-update.md) |
| mailcow SMTP/IMAP client | [runbooks/mailcow-smtp-client.md](runbooks/mailcow-smtp-client.md) |
| Vaultwarden health | [runbooks/vaultwarden-health.md](runbooks/vaultwarden-health.md) |
@@ -68,16 +71,24 @@ Also readable as `agent.md` (symlink → this file).
| PowerDNS health (hk2) | [runbooks/pdns-health.md](runbooks/pdns-health.md) |
| PowerDNS upstream refs | [docs/pdns-upstream.md](docs/pdns-upstream.md) |
| Matrix health | [runbooks/matrix-health.md](runbooks/matrix-health.md) |
| AdGuard Home health | [runbooks/adguard-home-health.md](runbooks/adguard-home-health.md) |
| Matrix upstream refs | [docs/matrix-upstream.md](docs/matrix-upstream.md) |
| Hermes Agent Matrix channel | [docs/hermes-matrix.md](docs/hermes-matrix.md) |
| UniFi local-service proxy bypass | [docs/unifi-openclash-localhost.md](docs/unifi-openclash-localhost.md) |
| UniFi SSO login setting (Ansible) | `cd ansible && ansible-playbook playbooks/unifi-sso.yml --limit unifi` |
| Routine Ansible operations | [runbooks/ansible-operations.md](runbooks/ansible-operations.md) |
```bash
./scripts/check-mx2.sh
```
Routine mailcow health: `cd ansible && ansible-playbook playbooks/health-report.yml --limit mailcow`. The local stub resolver is flaky; DNS probes use `1.1.1.1` / `8.8.8.8`.
`check-mx2.sh` defaults to IPv4; set `MX2_ALLOW_IPV6=1` to allow IPv6. DNS checks use `1.1.1.1` / `8.8.8.8` (local stub is flaky).
## Agent skills
### Issue tracker
Issues are tracked in Linear and created/updated via the Linear MCP (`vps` project). See `docs/agents/issue-tracker.md`.
### Triage labels
Default triage labels: needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See `docs/agents/triage-labels.md`.
## Safety
@@ -115,5 +126,4 @@ inventory/hosts.md # machine index
hosts/ # per-host facts
runbooks/ # step-by-step ops
docs/ # upstream doc indexes / design notes
scripts/ # read-only helper scripts
```
+39
View File
@@ -45,6 +45,26 @@ all:
backup:
compose_args: [--no-deps, --force-recreate]
service_reconcile_traefik_restart_targets: [poweradmin]
us4:
ansible_host: us4.wsvc.info
ansible_host_ipv4: 185.201.226.122
service_role: wireguard
compose_project_dir: /opt/wireguard
healthcheck_profile: wireguard
wireguard_image: >-
lscr.io/linuxserver/wireguard@sha256:ac43e1226878d2611315172d6ea357a95cb326ee73124b91108118efc8666889
service_reconcile_services:
wireguard:
compose_args: [--no-deps, --force-recreate]
dns_windy_lan:
ansible_host: 192.168.66.36
ansible_host_ipv4: 192.168.66.36
service_role: adguardhome
compose_project_dir: /opt/adguardhome
healthcheck_profile: adguardhome
service_reconcile_services:
adguardhome:
compose_args: [--no-deps, --force-recreate]
mailcow:
hosts:
mx2:
@@ -54,11 +74,30 @@ all:
powerdns:
hosts:
hk2:
wireguard:
hosts:
us4:
adguardhome:
hosts:
dns_windy_lan:
unifi:
hosts:
ubnt:
ansible_host: 192.168.66.46
ansible_host_ipv4: 192.168.66.46
vars:
service_role: unifi
compose_project_dir: /home/windy/unifi-9
unifi_container: unifi-controller
unifi_mongo_port: 27117
docker_hosts:
children:
mailcow:
vaultwarden:
powerdns:
wireguard:
adguardhome:
unifi:
# Matrix is a dedicated K3s node and intentionally remains outside the
# Docker-oriented managed group.
k3s_servers:
+25
View File
@@ -0,0 +1,25 @@
---
# Reconcile the UniFi controller SSO login setting
# (super_sdn.sso_login_enabled = "Sync Local Admin with Ubiquiti SSO").
#
# Safe by default: run with --check for a read-only status report, or supply
# unifi_sso_confirm=true to apply the declared target value.
#
# # Read-only status
# ansible-playbook playbooks/unifi-sso.yml --limit ubnt --check
#
# # Apply (disable SSO login; local accounts use local passwords, no MFA)
# ansible-playbook playbooks/unifi-sso.yml --limit ubnt \
# -e '{"unifi_sso_confirm": true, "unifi_sso_target_value": false}'
#
# # Rollback (re-enable SSO login)
# ansible-playbook playbooks/unifi-sso.yml --limit ubnt \
# -e '{"unifi_sso_confirm": true, "unifi_sso_target_value": true}'
- name: Reconcile UniFi SSO login setting
hosts: unifi
become: true
gather_facts: false
serial: 1
roles:
- role: unifi_sso
tags: [unifi, sso, mutating]
+62
View File
@@ -0,0 +1,62 @@
---
# This owns the non-secret Compose declaration only. WireGuard key material
# remains under the server-local /opt/wireguard/config bind mount.
- name: Apply the reviewed WireGuard Compose hardening
hosts: wireguard
become: true
gather_facts: false
serial: 1
vars:
wireguard_harden_confirm: false
wireguard_compose_candidate: "{{ compose_project_dir }}/compose.yml.candidate"
tasks:
- name: Require explicit WireGuard hardening confirmation
ansible.builtin.assert:
that:
- wireguard_harden_confirm | bool
fail_msg: >-
This operation replaces the non-secret Compose declaration. Run only
during the approved maintenance window after a rollback archive exists.
- name: Verify that a protected pre-change archive exists
ansible.builtin.find:
paths: "{{ compose_project_dir }}/backups"
patterns: 'pre-hardening-*.tar.gz'
file_type: file
register: wireguard_pre_hardening_archives
- name: Require a pre-change rollback archive
ansible.builtin.assert:
that:
- wireguard_pre_hardening_archives.matched | int > 0
fail_msg: >-
Create a protected pre-hardening archive before replacing compose.yml.
- name: Render the candidate non-secret Compose declaration
ansible.builtin.template:
src: ../templates/wireguard-compose.yml.j2
dest: "{{ wireguard_compose_candidate }}"
owner: root
group: root
mode: "0644"
- name: Validate the candidate Compose declaration
ansible.builtin.command:
argv:
- docker
- compose
- --project-directory
- "{{ compose_project_dir }}"
- --file
- "{{ wireguard_compose_candidate }}"
- config
- --quiet
changed_when: false
- name: Atomically activate the validated Compose declaration
ansible.builtin.command:
argv:
- mv
- "{{ wireguard_compose_candidate }}"
- "{{ compose_project_dir }}/compose.yml"
changed_when: true
+114
View File
@@ -0,0 +1,114 @@
---
# Deliberately narrow maintenance path for the us4 WireGuard service. The
# Compose file and all private key material remain on the server; this playbook
# only pulls its declared immutable image and recreates the named service.
- name: Update and recreate the approved WireGuard service
hosts: wireguard
become: true
gather_facts: false
serial: 1
vars:
wireguard_maintenance_confirm: false
tasks:
- name: Require explicit WireGuard maintenance confirmation
ansible.builtin.assert:
that:
- wireguard_maintenance_confirm | bool
fail_msg: >-
This operation pulls the image declared in /opt/wireguard/compose.yml
and briefly recreates WireGuard. Set wireguard_maintenance_confirm=true
only during the approved maintenance window.
- name: Create the local WireGuard rollback directory
ansible.builtin.file:
path: "{{ compose_project_dir }}/backups"
state: directory
owner: root
group: root
mode: "0700"
- name: Create a timestamp for the rollback archive
ansible.builtin.command:
argv: [date, +%Y%m%dT%H%M%SZ]
changed_when: false
register: wireguard_backup_timestamp
- name: Create a protected local rollback archive of WireGuard configuration
ansible.builtin.shell:
cmd: >-
umask 077 && tar --create --gzip
--file={{ compose_project_dir }}/backups/config-{{ wireguard_backup_timestamp.stdout }}.tar.gz
--directory={{ compose_project_dir }} config
executable: /bin/bash
changed_when: true
- name: Verify the compose declaration uses an immutable image digest
ansible.builtin.command:
argv:
- docker
- compose
- --project-directory
- "{{ compose_project_dir }}"
- config
changed_when: false
register: wireguard_compose_config
- name: Reject mutable WireGuard image declarations
ansible.builtin.assert:
that:
- "'@sha256:' in wireguard_compose_config.stdout"
fail_msg: >-
Pin the WireGuard image to an approved immutable digest before using
this maintenance playbook.
- name: Pull the declared WireGuard image
ansible.builtin.command:
argv:
- docker
- compose
- --project-directory
- "{{ compose_project_dir }}"
- pull
- wireguard
changed_when: true
- name: Recreate the WireGuard container
ansible.builtin.command:
argv:
- docker
- compose
- --project-directory
- "{{ compose_project_dir }}"
- up
- -d
- --no-deps
- --force-recreate
- wireguard
changed_when: true
- name: Verify WireGuard container state
ansible.builtin.command:
argv:
- docker
- compose
- --project-directory
- "{{ compose_project_dir }}"
- ps
- --all
changed_when: false
register: wireguard_compose_ps
- name: Display WireGuard container state
ansible.builtin.debug:
var: wireguard_compose_ps.stdout_lines
- name: Run the deployed WireGuard health check
ansible.builtin.command:
argv: [/usr/local/lib/vps-health/run]
changed_when: false
register: wireguard_health_report
failed_when: wireguard_health_report.rc not in [0, 1]
- name: Display sanitized WireGuard health result
ansible.builtin.debug:
msg: "{{ wireguard_health_report.stdout | default(wireguard_health_report.stderr, true) }}"
@@ -11,3 +11,5 @@ healthcheck_profiles:
mailcow: mailcow.sh
vaultwarden: vaultwarden.sh
pdns: pdns.sh
wireguard: wireguard.sh
adguardhome: adguardhome.sh
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -uo pipefail
source '{{ healthcheck_install_root }}/health-common.sh'
require_command docker
require_command curl
require_command dig
require_command ss
require_command df
check_compose
container='adguardhome'
if ! docker inspect "$container" >/dev/null 2>&1; then
record critical 'adguardhome_container_missing'
else
running="$(docker inspect "$container" --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)"
[[ "$running" == true ]] || record critical 'adguardhome_container_not_running'
fi
config_check="$(docker exec "$container" /opt/adguardhome/AdGuardHome --check-config \
-c /opt/adguardhome/conf/AdGuardHome.yaml 2>&1 || true)"
if grep -qiE 'error|invalid|fatal' <<<"$config_check"; then
record critical 'adguardhome_config_invalid'
else
record ok 'adguardhome_config_valid'
fi
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)53$'; then
record ok 'dns_tcp_53_listening'
else
record critical 'dns_tcp_53_missing'
fi
if ss -H -uln 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)53$'; then
record ok 'dns_udp_53_listening'
else
record critical 'dns_udp_53_missing'
fi
if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)80$'; then
record ok 'ui_http_80_listening'
else
record critical 'ui_http_80_missing'
fi
dns_a="$(dig +time=5 +tries=1 @192.168.66.36 example.com A +noall +answer 2>/dev/null)"
[[ -n "$dns_a" ]] && record ok 'dns_example_a' || record critical 'dns_example_a_failed'
dns_aaaa="$(dig +time=5 +tries=1 @192.168.66.36 example.com AAAA +noall +answer 2>/dev/null)"
[[ -n "$dns_aaaa" ]] && record ok 'dns_example_aaaa' || record warning 'dns_example_aaaa_none'
dns_local="$(dig +time=5 +tries=1 @192.168.66.36 hass.windy.lan A +noall +answer 2>/dev/null)"
[[ -n "$dns_local" ]] && record ok 'dns_local_name' || record warning 'dns_local_name_unresolved'
ui_code="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 10 http://192.168.66.36/ 2>/dev/null || true)"
case "$ui_code" in
200|301|302) record ok "ui_http_${ui_code}" ;;
'') record critical 'ui_unreachable' ;;
*) record critical "ui_http_${ui_code}" ;;
esac
usage="$(df -P /opt/adguardhome/work 2>/dev/null | awk 'NR==2 {print $5}' | tr -d '%')"
if [[ "$usage" =~ ^[0-9]+$ ]]; then
if (( usage >= 90 )); then record critical 'disk_usage_high'
elif (( usage >= 80 )); then record warning 'disk_usage_elevated'
else record ok 'disk_usage_ok'; fi
else
record unknown 'disk_usage_unknown'
fi
logs="$(docker logs --since 30m "$container" 2>&1 || true)"
if grep -qiE '\[(error|warn|fatal)\]|level=(error|warn|fatal)' <<<"$logs"; then
record warning 'adguardhome_log_errors'
else
record ok 'adguardhome_logs_clean'
fi
emit_result
exit "$EXIT_CODE"
@@ -1,3 +1,4 @@
#!/usr/bin/env bash
set -uo pipefail
exec '{{ healthcheck_install_root }}/{{ healthcheck_profiles[healthcheck_profile] }}' >> '{{ healthcheck_log_dir }}/healthcheck.log' 2>&1
set -o pipefail
'{{ healthcheck_install_root }}/{{ healthcheck_profiles[healthcheck_profile] }}' 2>&1 | tee -a '{{ healthcheck_log_dir }}/healthcheck.log'
exit "${PIPESTATUS[0]}"
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -uo pipefail
source '{{ healthcheck_install_root }}/health-common.sh'
require_command docker
require_command ss
check_compose
container='wireguard'
if ! docker inspect "$container" >/dev/null 2>&1; then
record critical 'wireguard_container_missing'
else
running="$(docker inspect "$container" --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)"
[[ "$running" == true ]] || record critical 'wireguard_container_not_running'
interface="$(docker exec "$container" wg show interfaces 2>/dev/null || true)"
grep -qw 'wg0' <<<"$interface" || record critical 'wireguard_wg0_missing'
configured_peer_count="$(docker exec "$container" sh -c 'find /config -mindepth 1 -maxdepth 1 -type d -name "peer_*" | wc -l' 2>/dev/null || true)"
runtime_peer_count="$(docker exec "$container" wg show wg0 dump 2>/dev/null | awk 'NR > 1 { count++ } END { print count + 0 }')"
[[ "$configured_peer_count" =~ ^[0-9]+$ ]] || record unknown 'wireguard_configured_peer_count_unknown'
[[ "$runtime_peer_count" =~ ^[0-9]+$ ]] || record unknown 'wireguard_runtime_peer_count_unknown'
if [[ "$configured_peer_count" =~ ^[0-9]+$ && "$runtime_peer_count" =~ ^[0-9]+$ ]]; then
[[ "$configured_peer_count" -eq "$runtime_peer_count" ]] || record warning "wireguard_peer_count_mismatch_${configured_peer_count}_${runtime_peer_count}"
fi
fi
if ss -H -uln 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)51820$'; then
record ok 'wireguard_udp_51820_listening'
else
record critical 'wireguard_udp_51820_missing'
fi
emit_result
exit "$EXIT_CODE"
+11
View File
@@ -0,0 +1,11 @@
---
# Reconcile super_sdn.sso_login_enabled on the UniFi Network Controller DB.
# Safe by default: without unifi_sso_confirm=true the role only reports the
# current state and refuses to change anything.
unifi_sso_confirm: false
# Declared desired state (true = Sync Local Admin with Ubiquiti SSO ON).
unifi_sso_target_value: false
# Restart the controller container after applying a change. Only needed when
# the setting was changed directly in the DB while the controller was running
# (the UI path applies it immediately without a restart).
unifi_sso_restart_controller: false
+110
View File
@@ -0,0 +1,110 @@
---
# Reconcile super_sdn.sso_login_enabled on the UniFi controller DB.
# Idempotent: reads the current value, reports compliance when it already
# matches the target, and only mutates with explicit confirmation.
- name: Require a boolean target value
ansible.builtin.assert:
that:
- unifi_sso_target_value | type_debug == 'bool'
fail_msg: "unifi_sso_target_value must be a boolean (true or false)."
- name: Read current SSO login setting
ansible.builtin.command:
argv:
- docker
- exec
- "{{ unifi_container }}"
- mongo
- --port
- "{{ unifi_mongo_port }}"
- --quiet
- ace
- --eval
- 'db.setting.findOne({key:"super_sdn"}).sso_login_enabled'
register: unifi_sso_current
changed_when: false
failed_when: unifi_sso_current.rc != 0
check_mode: false
- name: Report current setting
ansible.builtin.debug:
msg: >-
sso_login_enabled={{ unifi_sso_current.stdout | trim }}
(target={{ unifi_sso_target_value | string | lower }})
- name: Report already compliant
ansible.builtin.debug:
msg: "Already compliant; no change needed."
when: unifi_sso_current.stdout | trim | lower == unifi_sso_target_value | string | lower
- name: Refuse to change without explicit confirmation
ansible.builtin.fail:
msg: >-
Change required (sso_login_enabled={{ unifi_sso_current.stdout | trim }}
!= target {{ unifi_sso_target_value | string | lower }}) but
unifi_sso_confirm is not true. Supply unifi_sso_confirm=true to apply.
when:
- unifi_sso_current.stdout | trim | lower != unifi_sso_target_value | string | lower
- not (unifi_sso_confirm | bool)
- name: Apply SSO login setting change
ansible.builtin.command:
argv:
- docker
- exec
- "{{ unifi_container }}"
- mongo
- --port
- "{{ unifi_mongo_port }}"
- --quiet
- ace
- --eval
- >-
db.setting.updateOne({key:"super_sdn"},
{$set:{sso_login_enabled: {{ unifi_sso_target_value | string | lower }}}})
when:
- unifi_sso_confirm | bool
- unifi_sso_current.stdout | trim | lower != unifi_sso_target_value | string | lower
changed_when: true
register: unifi_sso_update
- name: Restart controller to apply setting when requested
ansible.builtin.command:
argv: [docker, restart, "{{ unifi_container }}"]
when:
- unifi_sso_restart_controller | bool
- unifi_sso_current.stdout | trim | lower != unifi_sso_target_value | string | lower
changed_when: true
- name: Verify setting after change
ansible.builtin.command:
argv:
- docker
- exec
- "{{ unifi_container }}"
- mongo
- --port
- "{{ unifi_mongo_port }}"
- --quiet
- ace
- --eval
- 'db.setting({key:"super_sdn"}).sso_login_enabled'
register: unifi_sso_verify
changed_when: false
check_mode: false
when:
- unifi_sso_current.stdout | trim | lower != unifi_sso_target_value | string | lower
- not ansible_check_mode
- name: Assert verified value matches target
ansible.builtin.assert:
that:
- unifi_sso_verify.stdout | trim | lower == unifi_sso_target_value | string | lower
fail_msg: >-
Verification failed: sso_login_enabled is
{{ unifi_sso_verify.stdout | trim }} but expected
{{ unifi_sso_target_value | string | lower }}.
when:
- unifi_sso_current.stdout | trim | lower != unifi_sso_target_value | string | lower
- not ansible_check_mode
@@ -0,0 +1,24 @@
services:
wireguard:
image: {{ wireguard_image }}
container_name: wireguard
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=Asia/Shanghai
- SERVERURL=us4.wsvc.info
- SERVERPORT=51820
- PEERS=ha,phone,mbp
- PEERDNS=1.1.1.1
# Intentional: client IPv6 traffic is not tunneled.
- ALLOWEDIPS=0.0.0.0/0
ports:
- 51820:51820/udp
volumes:
- ./config:/config
read_only: true
tmpfs:
- /run:exec
restart: unless-stopped
@@ -0,0 +1,164 @@
# AdGuard Home official configuration review basis (2026-08)
This page is the **official source basis and recommendation baseline** used
during the on-site review of AdGuard Home (AGH) on `192.168.66.36`. It does not
claim to have read that host's configuration and contains no credentials.
On-site conclusions must be based on the live `AdGuardHome.yaml`, OpenWrt
`dnsmasq`/DHCP, firewall, routing, and actual client leases.
Official sources (AdGuard only):
- [Configuration](https://adguard-dns.io/kb/adguard-home/configuration/)
- [Setting up AdGuard Home securely](https://adguard-dns.io/kb/adguard-home/running-securely/)
- [Getting started](https://adguard-dns.io/kb/adguard-home/getting-started/)
- [DHCP](https://adguard-dns.io/kb/adguard-home/dhcp/)
- [DNS encryption](https://adguard-dns.io/kb/adguard-home/encryption/)
## Recommended role split (for the existing OpenWrt gateway)
Preferred: keep **OpenWrt/dnsmasq as the single DHCP and local-domain
authority, with AGH as the LAN DNS filter/forwarder**: DHCP option 6 hands
clients `.36`; AGH sets the router's local DNS (usually the gateway LAN IP) as
`local_ptr_upstreams` and, where needed, uses domain-specific upstreams to route
the LAN search domain / local names back to dnsmasq. Clients are filtered while
still keeping DHCP lease names, static hosts, local reverse resolution, and
gateway-internal service discovery.
Rationale: AGH supports dnsmasq-style domain-specific upstreams for internal
private name servers; private PTR should be configured in **Private reverse DNS
servers** / `local_ptr_upstreams` rather than regular upstreams; when not
explicitly configured, it falls back to the OS default resolver, which shifts as
host networking changes. [Official config: domain-specific upstreams](https://adguard-dns.io/kb/adguard-home/configuration/#configuring-upstreams);
[private rDNS](https://adguard-dns.io/kb/adguard-home/configuration/#private-addresses).
Do not have OpenWrt dnsmasq and AGH both listen on `:53` at the same LAN IP;
port conflicts, or pointing dnsmasq's upstream at AGH while AGH points back at
dnsmasq, cause failures/forwarding loops. When AGH runs on a separate `.36`
host, the simplest topology is clients → AGH `.36:53` → public/encrypted
upstreams, forwarding only local resolution (PTR, LAN domain) → gateway dnsmasq.
This is an operational inference based on the official capabilities above and
must be validated against the live `ss -lntup`, dnsmasq config, and query path.
AGH also has built-in DHCP, but it should only be enabled after **disabling the
existing gateway DHCP, explicitly migrating leases/reservations and verifying
options 3/6**. The official docs describe it as an alternative when the router
cannot hand out custom DNS, not as a dual-DHCP setup; the DHCP docs also state
leases/static leases are managed by AGH. [Getting started: router DHCP/DNS
handout and alternative DHCP](https://adguard-dns.io/kb/adguard-home/getting-started/#router);
[DHCP configuration](https://adguard-dns.io/kb/adguard-home/dhcp/).
## Exposure and access control
1. `dns.bind_hosts` should bind only loopback and the actual LAN address (here
prefer `.36`'s LAN address); do not use `0.0.0.0`/public interfaces for a
home recursive resolver. The official docs note full-interface binding
exposes DDoS risk; a small isolated network should pick local service
interfaces and may add loopback alongside. [Secure deployment: server
addresses](https://adguard-dns.io/kb/adguard-home/running-securely/#choosing-server-addresses);
[`bind_hosts` definition](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
2. In the firewall, allow only managed LAN/VLAN → `.36` TCP/UDP 53; bind the web
admin UI to the management network/loopback and avoid WAN forwarding. At the
app layer, further restrict with `allowed_clients` to the local subnets; when
non-empty, only the CIDR/IP/ClientIDs in it are accepted, and it overrides
the effect of `disallowed_clients`. [Official access
control](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
3. Keep `refuse_any: true` and a non-zero `ratelimit` (default 20 qps), unless
on-site evidence shows the service can never be reached from the internet and
LAN high-concurrency needs are data-backed; the official docs list both as
protection against reflection/DDoS. [Configuration
notes](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
4. The web UI must have a user with login-failure lockout enabled;
`users: []` disables authentication, and `auth_attempts` or `block_auth_min`
of 0 disables the corresponding lockout logic. [Web/auth
parameters](https://adguard-dns.io/kb/adguard-home/configuration/#users).
## Upstreams, encryption, and DNSSEC
- Use two trusted, differently-networked DoH/DoT/DoQ upstreams for normal public
resolution; AGH supports these protocols and DNS Stamps. If an upstream
URL/hostname needs resolving, `bootstrap_dns` is its initial resolver; do not
point bootstrap at a name that requires AGH itself to resolve, and use
directly reachable, stable IPs. A `fallback_dns` can be set separately; it is
used only when primary upstreams do not respond. [Upstream protocols and
bootstrap/fallback](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
- If local IPv6's actual egress path is unstable, do not enable
`bootstrap_prefer_ipv6`; the official docs note it makes DoH/DoQ/DoT bootstrap
prefer IPv6. Decide on-site based on each upstream's IPv4/IPv6 reachability,
latency, and failure rate. [Same reference](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
- Consider enabling `enable_dnssec`, choosing recursive upstreams that actually
handle DNSSEC; but understand precisely: officially AGH sets the DO flag on
requests to upstreams; it does not perform recursive validation itself. On
site, query a known-bad-signature domain and observe AD/failure responses to
confirm upstream validation behavior. [DNSSEC parameter
definition](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
- Prefer `upstream_mode: load_balance` unless measurement shows `parallel` first
answer latency is clearly worth the privacy/traffic cost of querying all
upstreams. The former picks by failure rate and average latency weighting; the
latter queries all upstreams concurrently; this is an operational judgment
based on documented behavior. [Upstream
modes](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
## Cache, ECS, and privacy
- Keep DNS cache enabled; use `cache_ttl_min`/`cache_ttl_max` overrides
cautiously to avoid stale or unnecessarily short TTLs. `cache_optimistic`
answers with a 10-second TTL after expiry and attempts refresh — good for
low-latency scenarios that tolerate briefly stale results, not to be enabled
blindly on internal domains sensitive to immediate changes. [Cache
parameters](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
- ECS is off by default: when enabled, AGH adds EDNS Client Subnet to upstream
requests; if CDN geo-scheduling is truly needed, use `use_custom: true` with a
coarse, non-real-client-subnet `custom_ip`, then compare hit rate vs. privacy
impact. [ECS parameters](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
- Review log retention and who can read it. If device identity is not needed,
consider `anonymize_client_ip: true`; this anonymizes client addresses in logs
and stats. [Anonymization
parameter](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
## Proxy / OpenClash coexistence
If OpenClash/transparent proxy redirects DNS, validate hop by hop to ensure:
client DNS requests reach AGH uniquely; AGH's outbound traffic to public
encrypted upstreams is not hijacked back to AGH; and the router's local dnsmasq
PTR/local-domain requests return to dnsmasq. Create explicit proxy/hijack
exceptions for `.36` and its upstreams, or explicitly design the proxy to take
over AGH's outbound resolution; do not rely on "it seems to resolve" to conclude
there is no loop.
This is not an AGH-official OpenClash instruction (the official docs do not
maintain that integration), but an on-site verification requirement derived from
AGH's independent listening, upstream forwarding, and local-PTR-split
capabilities. The officially supported mechanisms are referenced at
[`bind_hosts`/upstreams/PTR](https://adguard-dns.io/kb/adguard-home/configuration/#dns).
If serving DoH via a reverse proxy, `trusted_proxies` must list only the actual
proxy IP/CIDR; AGH will only trust `X-Real-IP`, `X-Forwarded-For`, etc. from
those sources. Do not write the whole LAN/WAN for convenience;
`http.doh.insecure_enabled` should only be enabled when a reverse proxy really
terminates TLS in front. [Proxy and
DoH](https://adguard-dns.io/kb/adguard-home/configuration/#dns); [encrypted DNS
docs](https://adguard-dns.io/kb/adguard-home/encryption/). For a pure-LAN
plaintext-53 service, publishing DoH/DoT/DoQ outward is usually unnecessary.
## On-site review checklist (read-only)
1. Record the version, service method, and the full effective configuration
(secrets redacted); run the AGH config check. Officially provided via
`--check-config`. [Command-line
arguments](https://adguard-dns.io/kb/adguard-home/configuration/#command-line-arguments).
2. On `.36`, the gateway, and a wired/wireless client, record `ss` listeners,
DHCP option 6, default route, `/etc/config/dhcp`, and OpenClash
DNS/firewall-redirect rules.
3. From a client test: public A/AAAA, ad-rule hits, DHCP hostnames, forward LAN
domain, private PTR, DNSSEC bad-signature domain, and the expected failure
mode when AGH stops. Then check whether client IPs in AGH query logs are
still genuinely identifiable.
4. From the AGH host, test each upstream and bootstrap over the actual protocol
for IPv4 and IPv6 reachability; compare with transparent proxy on/off to rule
out loops and leaks.
5. Verify the web UI/DNS ports have no WAN exposure; confirm allowed clients and
firewall match actual LAN/VLAN, guest network, and VPN subnets.
Before changing anything, export the current configuration and DHCP/firewall
state; adjust one layer at a time (DHCP handout, AGH listening, upstreams, or
DNS hijack) and keep a known-good management and DNS fallback path.
+47
View File
@@ -0,0 +1,47 @@
# Issue tracker: Linear
Issues and PRDs for this repo are tracked in Linear, driven entirely through the
Linear MCP configured in the current runtime. VPS operation records use the
Linear **`vps`** project (team `W1ndy`).
## Conventions
- **Create**: `linear_save_issue` (requires `title` + `team`); description as
real-newline Markdown.
- **Read**: `linear_get_issue <identifier>` (e.g. LIN-123), optionally with
`includeRelations`.
- **List**: `linear_list_issues`, filtered by `query` / `state` / `assignee` /
`team` / `project`.
- **Comment**: `linear_save_comment` (pass `issueId` + `body`).
- **Labels**: `linear_save_issue` `labels` field (replaces the whole label set).
- **Status**: `linear_save_issue` `state` field (type / name / ID).
Resolve team, project, and cycle names through the Linear MCP rather than
hard-coding them.
## Cross-issue constraint baseline
The `plans/` directory is retired (removed 2026-08-06). Plans and baselines now
live in the vps project document "VPS 运维重构:已确认决策与验收标准". Open work
items are tracked as individual issues with dependencies expressed via
`blockedBy` / `blocks`. Read that document before adding new tasks.
## When a skill says "publish to the issue tracker"
Create a Linear issue with `linear_save_issue` (`project: vps`).
## When a skill says "fetch the relevant ticket"
Use `linear_get_issue <identifier>`.
## Wayfinding operations
- **Map**: one issue as the map; child tickets link via `parentId`.
- **Child ticket**: `linear_save_issue` with `parentId` set to the map; labels
`wayfinder:<type>` (research / prototype / grilling / task).
- **Blocking**: `linear_save_issue` `blockedBy` / `blocks` (append-only).
- **Frontier query**: list the map's open children, excluding those blocked or
already assigned; take the first in map order.
- **Claim**: `linear_save_issue` `assignee: me`.
- **Resolve**: `linear_save_comment` the answer → update `state` → append a
pointer under the map's decisions-so-far.
+15
View File
@@ -0,0 +1,15 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.
+120
View File
@@ -0,0 +1,120 @@
# EdgeRouter X configuration record
This is a read-only, sanitized configuration record for
[gw](../hosts/gw.md), taken on 2026-08-05. It intentionally excludes PPPoE
credentials, user/password configuration, key material, and full DHCP client
or reservation inventories.
## Platform and topology
| Item | Current configuration |
|---|---|
| Appliance / OS | EdgeRouter X 5-Port; EdgeOS `v3.0.1` build `5862409` |
| WAN physical interface | `eth4`, described as `Internet (PPPoE)` |
| WAN routed interface | `pppoe0`; dynamic IPv4 default route; MTU `1492` |
| LAN66 | `eth0`, `192.168.66.254/24` |
| LAN55 | `switch0`, `192.168.55.254/24`; member ports `eth1``eth3` |
| IPv4 routing | Both LAN prefixes are directly connected; default route is through `pppoe0` |
Despite its name, LAN55 is not currently an 802.1Q VLAN configuration:
`switch0` has `vlan-aware` disabled. It is a separate Layer-3 subnet carried
by the built-in switch ports, while LAN66 is connected through `eth0`.
## IPv6
The PPPoE uplink requests a delegated `/60`, enables rapid commit and assigns
SLAAC-capable `/64` networks to the LANs:
| Interface | Delegated prefix slot | Router address suffix |
|---|---|---|
| `eth0` (LAN66) | `:1` | `::1` |
| `switch0` (LAN55) | `:2` | `::1` |
IPv6 has a default route through `pppoe0`. The currently delegated prefixes
are dynamic and are deliberately not treated as persistent configuration
facts.
## Effective firewall policy
| Ruleset | Direction / attachment | Effective policy |
|---|---|---|
| `WAN_IN` | `pppoe0`, IPv4 `in` | Active; default drop; permits established/related, drops invalid |
| `WAN_LOCAL` | `pppoe0`, IPv4 `local` | Active; default drop; permits established/related, drops invalid |
| `WANv6_IN` | `pppoe0`, IPv6 `in` | Active; default drop; permits established/related and ICMPv6; drops invalid |
| `WANv6_LOCAL` | `pppoe0`, IPv6 `local` | Active; default drop; permits established/related, ICMPv6, and DHCPv6 replies; drops invalid |
| `LAN_IN` | None | Inactive; configured default drop with explicit LAN55 ↔ LAN66 allows |
| `LAN_OUT` | None | Inactive; configured default drop with an Internet allow |
Therefore, LAN55 and LAN66 currently have ordinary bidirectional routed
access. The configured `LAN_IN`/`LAN_OUT` rules do **not** enforce an access
boundary because neither is bound to an interface. From source
`192.168.55.254`, the controller at `192.168.66.46` replied to 3/3 ICMP probes
on 2026-08-05. The online LAN55 AP also confirms the operational Inform path
to `192.168.66.46:9080`.
Global firewall settings enable normal ICMP echo replies, disable broadcast
ping and IPv4/IPv6 source routing, enable SYN cookies and martian logging, set
an MSS clamp of `1412`, and leave source validation disabled. Receive redirects
are disabled; IPv4 send redirects remain enabled.
The [official EdgeOS User Guide](https://dl.ubnt.com/guides/edgemax/EdgeOS_UG.pdf)
describes the distinction: a firewall policy is a ruleset that must be applied
to its target interface and direction before it affects traffic.
## NAT and inbound exposure
IPv4 traffic leaving `pppoe0` is covered by NAT masquerade rule `5010`.
Port-forward auto-firewall and hairpin NAT are enabled. The configured WAN
forwards are below; their external reachability was not tested in this review.
| WAN protocol / port | Internal destination | Purpose |
|---|---|---|
| TCP/UDP `8123` | `192.168.55.11:8123` | Home Assistant |
| TCP/UDP `51413` | `192.168.66.51:51413` | Transmission |
| TCP/UDP `5822` | `192.168.66.36:22` | SSH forward |
| TCP/UDP `1194` | `192.168.66.32:1194` | OpenVPN |
Treat every forward as an intentional public attack surface. Changes require
the normal firewall/change review and external verification from a trusted
network.
## LAN services
| Service | LAN66 | LAN55 |
|---|---|---|
| DHCP pool | `.38``.243` | `.38``.243` |
| Default gateway | `.254` | `.254` |
| DNS server handed out | `192.168.66.36` | `192.168.66.36` |
| Lease duration | 24 hours | 24 hours |
| UniFi Controller DHCP option | `192.168.66.46` | `192.168.66.46` |
DNS forwarding listens on `eth0` and `switch0` with a cache size of `512`.
Management services are SSH/TCP `22`, HTTP/TCP `80`, and HTTPS/TCP `443`.
Since no active LAN-local firewall is documented, treat access to this gateway
management plane from either LAN as permitted unless verified otherwise.
Infrastructure devices, including the two APs and the UniFi controller, have
DHCP static mappings. Keep the authoritative full mapping list only in the
gateway configuration; do not copy unrelated household device inventories into
this operations repository.
## Acceleration and maintenance implications
At verification time, `show ubnt offload` reported that both IPSec and HWNAT
offload modules were not loaded. No `system offload` enablement was present in
the inspected configuration. This may constrain routing/NAT throughput under
load; it is an observation, not authorization to alter the router.
Use these safe effective-state checks after any planned change:
```bash
show interfaces
show ip route
show ipv6 route
show firewall
show nat rules
show ubnt offload
```
Avoid recording raw `show configuration commands` output: it includes PPPoE
credentials and may include other secrets.
+156
View File
@@ -0,0 +1,156 @@
# LAN 核心交换机升级计划(保留 ER-X)
**状态:** SE5420 **已采购**2026-08-09)。**实施与验证以 [lan-se5420-deployment-guide.md](lan-se5420-deployment-guide.md) 为准**
本文为历史规划参考,**不得作为现场执行步骤**;所有实际操作均以部署指南为准。
**锁定硬件:** TP-Link **`TL-SE5420`**16 × 2.5GbE RJ45 + 4 × 10GbE SFP+)。
**目标:** SE5420 承接全部 LAN 物理接入与二层转发;ER-X 继续承担公网、NAT、防火墙、
LAN66/LAN55 网关与 DHCP。
**拓扑与流量的详细说明**(职责、逻辑网、流量路径、Wi-Fi 分工、验收边界)见:
[lan-erx-se5420-network.md](lan-erx-se5420-network.md)。
SE5420 官方资料:静态功耗 8 W、最大功耗 32 WVLAN、LACP、STP/RSTP/MSTP、ACL、
CLI/SNMP、配置导入导出与固件下载。无 PoE——AP 使用本地取电 + 普通网线。
## 已确认边界(摘要)
| 项 | 结论 |
|---|---|
| 硬目标 | 同 VLAN 2.5GSFP+ 先空槽 |
| 核心角色 | 纯 L2;不开 L3 / DHCP Server/Relay / NAT |
| 网关 | 默认一律 ER-X `.254`;升级专用 SSID(后续)才走 `gfw` `.1` |
| 本次 Done | 阶段 03VLAN10 / 客人 SSID / 升级 SSID 另立项目 |
| 切换 | 30–60 分钟维护窗;旧交换迁完后闲置 |
| 首批 2.5G | NAS + 主力 PC(或 PVE |
| 管理 | 先本地 HTTPS/SSH;云以后再说 |
### 保留的 ER-X 职责
| 项目 | 迁移后职责 |
|---|---|
| PPPoE / WAN、NAT、端口转发、WAN 防火墙 | ER-X,不变 |
| LAN66 (`192.168.66.0/24`) 默认网关与 DHCP | ER-X `eth0`,不变 |
| LAN55 (`192.168.55.0/24`) 默认网关与 DHCP | ER-X `switch0`,不变 |
| LAN66 ↔ LAN55 三层转发 | ER-X,不变 |
| 升级专用 Wi-Fi 网关(后续) | `gfw` VM,不是 ER-X 或核心交换机 |
ER-X 与核心之间使用两条**独立无标签 access**LAN66 + LAN55),不向 ER-X 送
VLAN tag。
### 核心交换机职责
- 所有有线设备、AP 与 PVE 的物理接入;
- VLAN66、VLAN55 的二层转发;
- 为日后 U6 Lite 与 PVE 预留 VLAN10 trunk(本次不启用业务);
- 同 VLAN 的 1G/2.5G 转发;
- SFP+ 预留未来 10GDAC/AOC/光;不用 10GBASE-T 作默认)。
## 目标拓扑(简图)
```text
Internet
|
ER-X / PPPoE
+-------+--------+
| |
eth0, LAN66 switch0 port, LAN55
access access
| |
+-------+--------+
|
TL-SE5420 核心(纯 L2
| | |
PVE / gfw U6 Lite LAN66/LAN55 access
(日后 trunk (日后 trunk NAS, PC, dns, ubnt, AC-Lite …
```
完整端口表、流量路径与 Wi-Fi 分工见
[lan-erx-se5420-network.md](lan-erx-se5420-network.md)。
## VLAN 与端口设计
| VLAN / 逻辑网络 | 核心配置 | 网关 / DHCP | 用途 |
|---|---|---|---|
| 66 | accessPVE/U6 日后 trunk 的 native | ER-X `eth0` / `.254` | 主 LAN、管理 |
| 55 | access | ER-X `switch0` / `.254` | LAN55、AC-Lite |
| 10(后续) | 仅 PVE 与 U6 trunk 上 tagged | `gfw` | 升级专用 SSID |
| 管理 | LAN66 管理地址;不新建 VLAN | reservation / 静态 | SE5420 管理面 |
管理地址分配前先查 ER-X DHCP/reservation;禁用不需要的 HTTP/Telnet;云管延后;
保留 HTTPS/SSH 与离线配置备份(备份不进本仓库)。
### 初始端口分配
| 预留 | 对端 | 模式 |
|---|---|---|
| 铜口 1 | ER-X `eth0` | access VLAN66 |
| 铜口 2 | ER-X `switch0` 成员口 | access VLAN55 |
| 铜口 3 | PVE`gfw` | 现 access 66;日后 trunk native66+tag10 |
| 铜口 4 | U6 Lite | 同上;本地取电 |
| 铜口 5 | UAP-AC-Lite | access VLAN55;本地取电 |
| 其余铜口 | NAS、PC、`dns``ubnt`… | 默认 access VLAN66 |
| SFP+ 14 | 未来 | 空槽 |
## 分阶段实施与验收
### 阶段 0:采购前核验
1. 确认 SE5420 revision、保修、手册、固件页。
2. 确认 VLAN trunk、RSTP/MSTP、LACP、镜像、配置导出、错误计数。
3. SFP+ 模块本次不买;有对端后再选 DAC/AOC/光。
4. 盘点线缆与对端(LAN66/55、PVE、两台 AP);确认关键 2.5G 链路可协商。
### 阶段 1:离线初始化核心
1. 仅电源 + 隔离管理本:管理 IP、强密码、时区、NTP、HTTPS/SSH**关闭 L3/DHCP**。
2. 导出初始配置(离线保存,不进仓库)。
3. 建立 VLAN66/55/10 与端口模板;VLAN10 不接生产。
4. 开启 RSTP/MSTP;不制造物理环路。
### 阶段 2:维护窗迁移 LAN66
1. 使用约定维护窗(约 30–60 分钟);保留原接线作回滚。
2. 先接铜口 1 ↔ ER-X `eth0`;验证管理地址、`.254`、DNS `.36`、互联网。
3. 逐台迁到 VLAN66 access;每台确认地址/DNS/路由/业务。
4. 最后迁 `ubnt``dns`、NAS/PC;禁止无意双上行。
### 阶段 3:迁移 LAN55
1. 接铜口 2 ↔ ER-X `switch0` 成员口。
2. 测试设备获 `192.168.55.x`、网关 `.254`、可达 DNS。
3. AC-Lite → 铜口 5;确认 `.55.5` 与 Inform Connected。
4. 旧交换下电闲置。
### 阶段 4VLAN10 升级专用 Wi-Fi(独立项目)
不与本次 Done 捆绑。仅 U6;网关 `gfw`;详见
[lan-erx-se5420-network.md](lan-erx-se5420-network.md) 第 6.5 / 8 节与
[unifi-network.md](unifi-network.md)。
## 性能预期与不变瓶颈
- 同 VLAN、两端在核心上的 2.5G:可测 2.5G 级二层。
- 上网与 LAN66↔LAN55:仍经 ER-X 1G 路径。
- 空 SFP+ 不使网络「变成 10G」。
## 验收清单
- 管理面、口令、离线备份;
- 端口速率与错误计数;
- LAN66/55 的 DHCP、网关、DNS、互联网与关键本地服务;
- 两台 AP ConnectedInform 仍为 `http://192.168.66.46:9080/inform`
- 无无意环路;旧交换已闲置;
- (建议)两台 2.5G 终端同 VLAN `iperf3`
## 回滚
验证失败则停迁、恢复原接线;不改 ER-X WAN/DHCP/SSH。VLAN10 失败只撤新 SSID。
## 参考
- [ER-X + SE5420 网络与拓扑说明](lan-erx-se5420-network.md)
- [LAN 概览](lan-overview.md)
- [ER-X 配置记录](edgerouter-x-configuration.md)
- [UniFi 网络](unifi-network.md)
- [`gfw`](../hosts/gfw.windy.lan.md)
- [TL-SE5420 官方规格](https://www.tp-link.com.cn/product_2899.html?v=specification)
+118
View File
@@ -0,0 +1,118 @@
# 内网 DNS 架构调研与优化建议
> 状态:2026-08-12 调研,Linear **W1N-56**。基于网络工程师视角,方案待实施评审。
> 2026-08-12 现场核查修正:gfw 上 mosdns 已并入 OpenClash DNS 链(作为 clash 的
> `nameserver`,DIRECT 规则真实 IP 解析用),**不再闲置**;推荐方案(AGH 前端 +
> `.36` 伴生 mosdns 后端)仍待评审落地。
> 相关:`docs/lan-overview.md`、`hosts/dns.windy.lan.md`、`hosts/gfw.windy.lan.md`。
## 1. 现状(实测)
| 角色 | 部署 | 职责 | 是否在活动路径 |
|------|------|------|----------------|
| **AdGuard Home** | `192.168.66.36`(PVE VM 120,Docker host 网络) | EdgeRouter DHCP 通告给 LAN55/66 客户端的 DNS;广告/过滤、查询统计、Web 面板 | ✅ **是** |
| **mosdns** | `192.168.66.1`(gfw OpenWrt)监听 `127.0.0.1:6052` | OpenClash custom DNS 的 `nameserver`(DIRECT 规则真实 IP 分流:国内→AGH `.36`,国外→国内公网 DNS `223.5.5.5`/`119.29.29.29`) | ✅ 网关侧(clash 消费,不面向客户端) |
| **OpenClash / clash(meta)** | `192.168.66.1`(gfw) | gateway 自身/被劫持流量的 fake-ip + 代理,DNS 走 dnsmasq→clash `#7874` | 仅网关侧 |
**关键事实:** LAN 客户端 DNS 直连 `66.36`,**不经过** gfw(EdgeRouter `service dns forwarding` cache 512,通告 `.36`)。所以 gfw/clash 的 fake-ip 分流对"直连 AGH 的客户端"不起作用;gfw 上 mosdns 作为 clash 的 `nameserver` 供 DIRECT 规则连接的真实 IP 解析用(国内→AGH,国外→国内公网 DNS),不面向 LAN 客户端。
**现状缺口:**
1. AGH 上游是**固定 DoH**(alidns/doh.pub,兜底 adg.chans.xyz),**没有"国内/国外分流"能力** → 国外域名解析易受 DNS 污染/时延差,也无法为不同 region 选最优上游。
2. 国内/国外分流逻辑(geo)与代理分流逻辑(clash fake-ip)混在网关上,职责不清。
## 2. 两个候选方案评估
### 方案 A:AGH 单独作为统一入口(现状演进)
- 优点:单解析点、面板/拦截/日志集中、维护简单。
- 缺点:AGH 对 geo 分流 + 防污染支持弱(官方定位是"过滤/家长控制",见 adguard README "Encrypted DNS upstream... requires additional software")。固定 DoH 上游无法按域名 region 选路。→ **不足以解决防污染/分流问题。**
### 方案 B:mosdns 作为智能上游分流器
mosdns(v5)用 `sequence` 编排:`geosite/geoip` 匹配器 → 国内域名转发国内 DoH、国外域名转发加密 DoH(防污染),可加 `cache``reject`(屏蔽)。
- 优点:真正解决"国内快 / 国外不被污染"的分流;性能高(百万域名表也不卡)。
- 缺点:纯转发器,无 Web 面板、无每客户端统计、拦截要靠域名表(不如 AGH 体验)。→ 单独当入口会退回原始体验。
**结论:两个方案是互补的,不是二选一。** 单用 A 无法分流防污染,单用 B 失去 AGH 的管理体验。
## 3. 推荐:分层架构(AGH 前端 + mosdns 后端)
```
局域网客户端(DHCP DNS = 192.168.66.36)
AdGuard Home (66.36) ── 前端:广告/过滤、拦截表、每客户端统计、Web 面板
│ 上游 = mosdns
mosdns(66.36 伴生容器) ── 后端:智能分流 + 防污染
│ - geosite:cn → 国内 DoH/UDP(aliDNS / 腾讯 DNSPod)
│ - 其他 → 加密 DoH(Cloudflare/Google/自建 adg.chans.xyz)
上游 DoH
```
职责分离,每个工具只做自己最擅长的事:
- **AGH = 策略/拦截/可观测**(拦截表、每客户端日志、面板)。AGH 原生干不了"按域名选路",所以不做分流。
- **mosdns = 智能转发**(geo 分流 + 加密防污染)。不用它当入口,所以保持 AGH 的 UX。
- **OpenClash(gfw)= 代理选路**(fake-ip + 规则决定"哪些流量走代理")。与"DNS 解析选上游"是**两个独立决策**,分开放在不同工具最干净——DNS 解析在 66.36 做,代理路由在网关做,互不耦合。
### 推荐部署位置:mosdns 与 AGH 同机(66.36),而非 gf(.1)
| 位置 | 评价 |
|------|------|
| **66.36 伴生容器(推荐)** | 单点即 AGH 所在;AGH→mosdns 走本机/近端一跳;不受网关重启/OpenClash churn 影响;可纳入现有 ansible compose 管理;不占用 OpenWrt 资源 |
| gf(.1) | 虽近网络边缘,但该网关已有 clash fake-ip + 多种劫持规则,再叠 mosdns 会与 clash 的 DNS 处理互相干扰、耦合;且网关重启即断 DNS(影响整个 LAN)。**不推荐** |
> 注意:若把 mosdns 放 gfw,必须先理清与 OpenClash `dnsmasq→clash #7874` + `nft fw4 DNS-hijack` 的先后/覆盖关系,否则会出现"部分设备解析走了 clash、部分走了 mosdns"的混乱。放 66.36 则完全避开这个冲突。
## 4. 更优替代方案(也一并考虑)
1. **分层(推荐,见上)**:AGH(66.36)→ mosdns(66.36 伴生)→ 上游。体验最好、职责最清。
2. **纯 mosdns + 前端面板**:不用 AGH,用 mosdns + 其他统计面板。→ 会明显损失拦截/统计管理体验,除非你讨厌 AGH 的 Docker 部署。**不推荐**用于替换。
3. **AGH 只挂一个带分流的上游(第三方 DoH 聚合)**:例如接一个已做分流的公共 DoH。→ 失去可控性,且不可信。不推荐做主路径。
4. **全部交给 OpenClash fake-ip,关闭 AGH**:把 LAN 客户端 DNS 指到 gfw。→ 让"代理网关"成为全 LAN DNS 单点,网关重启/代理抖动整个内网断网;且 AGH 的拦截/日志也没了。**不推荐。**
## 5. mosdns 配置要点(mosdns v5,预留实施)
核心是 `sequence` + 上游拆分 + 缓存 + 屏蔽:
```yaml
plugins:
- tag: main
type: sequence
args:
- exec: cache 1024 # 缓存加速
# 国内分流:命中 geosite:cn → 国内 DoH
- matches: [ qname &geosite:cn ]
exec: forward https://dns.alidns.com/dns-query
# 广告域名可选屏蔽(或交给 AGH 前置拦截,二选一)
# - matches: [ qname &./blocklist.txt ]
# exec: reject 3
# 其余(国外)→ 加密 DoH 防污染
- exec: forward https://1.1.1.1/dns-query
# 备选国外上游/兜底
- matches: [ has_resp ]
exec: accept
- exec: forward_addr https://208.67.222.222:443/dns-query
- type: udp_server
args: { entry: main, listen: "127.0.0.1:5353" }
- type: tcp_server
args: { entry: main, listen: "127.0.0.1:5353" }
```
要点:
- 上游可加 `upstream``concurrent > 1``addr` 做多/故障切换。
- `geosite:cn` / `geoip:cn` 数据插件自动从 repo 更新;国内用 aliDNS/腾讯,国外用 DoH(Cloudflare/Google/自建 adg.chans.xyz)。
- 屏蔽交由 AGH 前置(推荐),不要 AGH 和 mosdns 都自己维护一套拦截表(重复)。
## 6. 迁移 / 实施顺序(待评审)
1. 在 66.36 起 mosdns 伴生容器(`/opt/mosdns` + compose,固定 digest,纳入 ansible)。
2. AGH「上游 DNS 服务器」改为指向 mosdns(`http://127.0.0.1:5353/dns-query``127.0.0.1:5353`)。AGH 的 `bootstrap` 仍用公网 IP(避免 AGH → mosdns → AGH 死循环)。
3. 验证:国内域名(如 `taobao.com`)、国外域名(如 `google.com`)、被拦截域名、每客户端日志。
4. 确认后,`disable`/移除 gfw 上闲置的 mosdns(6052)以免混淆。
5. 回归:EdgeRouter 通告不变(仍 `.36`),因此 LAN 客户端无感;重启 AGH/mosdns 单点验证。
## 7. 风险与备注
- mosdns 仅监听 `127.0.0.1`(不对外),由 AGH 消费;避免 LAN 直连 mosdns 造成两套入口。
- AGH 上游指向本机 mosdns 时,务必配 bootstrap 公网 IP,否则自举死循环。
- 本方案不改 EdgeRouter DHCP/通告,不改 gfw OpenClash 代理规则,只动 66.36 上的 DNS 链路,风险可控。
- 与 W1N-40「审查并修正 AdGuard Home」联动:该 issue 侧重 AGH 本身,本 issue 侧重整体 DNS 分层。
+315
View File
@@ -0,0 +1,315 @@
# ER-X + TL-SE5420 网络与拓扑说明
**状态:** SE5420 **已采购**2026-08-09)。**实施与验证以 [lan-se5420-deployment-guide.md](lan-se5420-deployment-guide.md) 为准**
本文保留为拓扑/流量路径/职责边界的设计说明。
**锁定硬件:** EdgeRouter X(保留)+ TP-Link `TL-SE5420`(新建核心)。
**本文目的:** 说明升级后物理拓扑、逻辑网络、流量路径与职责边界;唯一实施步骤见
[lan-se5420-deployment-guide.md](lan-se5420-deployment-guide.md)。
---
## 1. 一句话架构
```text
公网 ← ER-X(网关 / NAT / DHCP / 防火墙)
↑ 两条独立 access 上联(LAN66 + LAN55,无 VLAN tag
TL-SE5420(纯 L2 核心:所有接入、同 VLAN 转发、未来 trunk
有线终端 / PVE(gfw) / 两台 UniFi AP
```
- **ER-X** 继续当唯一主网关:PPPoE、NAT、端口转发、LAN66/LAN55 的 `.254` 与 DHCP。
- **SE5420** 只做二层:接线、VLAN 隔离转发、端口统计;**不开** L3、静态路由、DHCP
Server/Relay、NAT。
- **`gfw`PVE 上的 OpenWrt)** 仍是旁路由;默认客户端网关仍是 `.254`。以后若启用
「设备升级」专用 SSID,该 SSID 的客户端才把网关指向 `.1`
---
## 2. 已确认的设计决策
| 决策 | 结论 |
|---|---|
| 硬目标 | 同 VLAN **2.5G** 二层转发;10G 仅 SFP+ 预留 |
| 核心型号 | **`TL-SE5420`**16×2.5G RJ45 + 4×10G SFP+ |
| 终局形态 | ER-X 边缘网关 + 核心纯 L2;可分阶段实施 |
| 本次升级 Done | 迁完 LAN66/LAN55 接入与验收(计划阶段 03);不含 VLAN10 |
| 默认网关 | **`192.168.66.254` / `192.168.55.254`ER-X** |
| 专用升级 SSID | 仅 **U6 Lite**;客户端网关 **`gfw` `.1`**(后续项目) |
| 客人网 | 仅 **UAP-AC-Lite**UniFi Guest 隔离,仍在 **LAN55**(后续) |
| 切换方式 | **3060 分钟维护窗** 集中切;旧交换迁完后闲置 |
| 首批 2.5G | NAS + 主力 PC(或 PVE);其余可先 1G |
| SFP+ | 先空槽;有明确 10G 对端再买 DAC/光模块 |
| AP 供电 | **本地取电 + 普通网线**SE5420 **无 PoE** |
| 管理面 | 先本地 HTTPS/SSH;云以后再说;L3/DHCP 严格关闭 |
---
## 3. 设备职责
### 3.1 EdgeRouter X`gw``192.168.66.254`
| 职责 | 说明 |
|---|---|
| WAN | `eth4` → PPPoE`pppoe0`),NAT、WAN 防火墙、端口转发 |
| LAN66 网关/DHCP | `eth0` = `192.168.66.254/24`DHCP 池与静态映射不变 |
| LAN55 网关/DHCP | `switch0``eth1``eth3`= `192.168.55.254/24` |
| 跨网段路由 | LAN66 ↔ LAN55 仍由 ER-X 三层转发 |
| 与核心的连接 | **两条无标签 access**,不是 802.1Q trunk |
ER-X 当前不是 trunk 设计:不要向它发送 VLAN66/55 的 tag。核心上 VLAN66 的
untagged 帧进 `eth0`VLAN55 的 untagged 帧进 `switch0` 的某个成员口。
### 3.2 TL-SE5420(新建核心)
| 职责 | 说明 |
|---|---|
| 物理接入汇聚 | 有线设备、PVE、两台 AP 都接到 SE5420 |
| VLAN66 / VLAN55 | 二层转发(access 或 trunk 的 native/allowed |
| 同 VLAN 高速交换 | 两端都是 2.5G 时可跑满协商速率(受 NIC/线材限制) |
| 未来 trunk | PVE、U6native 66 + tagged 10(仅后续阶段启用) |
| 不做 | 不当默认网关;不开 DHCP;不做 NAT;第一阶段不开 SVI/路由 |
管理地址:落在 **LAN66** 的一个未占用地址(先查 ER-X DHCP reservation,再分配);
不新建管理 VLAN。
### 3.3 旁路由 `gfw``192.168.66.1`
| 现状 | 升级后(阶段 0–3) | 更后续 |
|---|---|---|
| PVE 上的 ImmortalWrt VM | 仍挂在 **untagged LAN66** | 可增 VLAN10 接口 |
| OpenClash 旁路/透明代理 | 路径不变:客户端默认仍走 `.254` | 升级专用 SSID → 网关 `.1` |
| 不能单独「做出」VLAN10 | 需 PVE vSwitch + 核心 trunk + U6 trunk | 见部署指南第二步 |
### 3.4 其它现网角色(地址与角色不变)
| 主机 | 地址 | 角色 |
|---|---|---|
| `dns.windy.lan` | `192.168.66.36` | AdGuard Home + Mihomo |
| `ubnt` | `192.168.66.46` | UniFi ControllerInform `http://192.168.66.46:9080/inform` |
| U6 Lite | `192.168.66.6` | LAN66 AP;后续承载升级专用 SSID |
| UAP-AC-Lite | `192.168.55.5` | LAN55 AP;后续承载客人 SSID |
| PVE | `192.168.66.26`DHCP 静态) | 宿主机;上挂 `gfw` |
| NAS / 主力 PC 等 | 既有 reservation | 首批 2.5G 候选 |
---
## 4. 物理拓扑
### 4.1 目标连线(阶段 3 完成后)
```text
Internet
PPPoE / WAN
┌──────────┴──────────┐
│ EdgeRouter X │
│ (gw / 网关) │
│ │
│ eth0 switch0 成员口
│ LAN66 LAN55
│ untagged untagged
└───┬───────────────┬─┘
│ │
铜口1│ │铜口2
│ │
┌─────────┴───────────────┴─────────┐
│ TL-SE5420 核心 │
│ 16×2.5G RJ45 + 4×SFP+(先空) │
│ 纯 L2 · VLAN66 / VLAN55 │
└───┬─────┬─────┬─────┬─────┬───────┘
│ │ │ │ │
铜口3 铜口4 铜口5 其余铜口 SFP+
│ │ │ │ (预留)
│ │ │ │
PVE U6 AC-Lite NAS/PC/
(gfw) Lite (LAN55) dns/ubnt…
LAN66 LAN66 默认 LAN66
(日后可 (日后可
trunk) trunk)
```
要点:
1. **星型**:终端与 AP 只上联到 SE5420;旧交换机迁完后**闲置**,避免双上行环路。
2. **ER-X 双上联**LAN66、LAN55 各一条独立铜缆;两条都是 access,**不打 tag**。
3. **AP 供电**SE5420 无 PoEU6 / AC-Lite 在安装点**本地取电**,网线只传数据。
4. **SFP+**:四个槽位先空着;有 10G NIC/对端与兼容模块后再上 DAC/AOC/光纤。
### 4.2 与「现在」的差异(概念上)
```text
现在(简化) 目标
───────────── ────
终端/AP ──► ER-X 内置交换/旧交换 终端/AP ──► SE5420 ──► ER-X(仅网关)
同网段 1G 交换受 ER-X/旧设备限制 同 VLAN 2.5G 在 SE5420 上本地交换
跨网段 / 上网 仍经 ER-X 跨网段 / 上网 仍经 ER-X(不变)
```
ER-X **不会被换成**「只接一条 trunk 的路由」;WAN 与双 LAN 接口模型保持不变。
---
## 5. 逻辑网络(VLAN / 子网)
### 5.1 本次升级范围内(阶段 0–3)
| 名称 | 子网 | 核心上的形态 | 网关 | DHCP | 典型成员 |
|---|---|---|---|---|---|
| **LAN66** | `192.168.66.0/24` | VLAN 66access,或 trunk 的 native | ER-X `eth0` `.254` | ER-X | PVE、gfw、dns、ubnt、U6、NAS、PC… |
| **LAN55** | `192.168.55.0/24` | VLAN 55access | ER-X `switch0` `.254` | ER-X | UAP-AC-Lite、原 LAN55 设备 |
客户端 DNS 仍按现网:DHCP 下发 `192.168.66.36`AdGuard)。
LAN55 ↔ LAN66 互通仍走 ER-X(现网 `LAN_IN`/`LAN_OUT` 未挂到接口时,两边默认可达;不要把未生效的防火墙当隔离边界)。
### 5.2 后续能力(不进本次 Done)
| 名称 | 打算 | 网关 | 落点 | 依赖 |
|---|---|---|---|---|
| **升级专用 Wi-Fi** | 独立 SSID → 独立 VLAN(如 VLAN10 | **`gfw` `.1`**(该 VLAN 内) | **仅 U6 Lite** | 核心 trunk + PVE vSwitch + `gfw` DHCP/NAT |
| **客人网** | 客人 SSID + UniFi Guest 隔离 | 仍 **ER-X `.254`LAN55** | **仅 UAP-AC-Lite** | UniFi 策略;**不**新建 VLAN |
客人网与升级 SSID **分工到两台 AP**,避免一上来就做「两台 AP 同一客人 VLAN + ER-X trunk」的改造。
### 5.3 VLAN10 的时序与边界
第一阶段**不**在 SE5420 创建 VLAN10,也**不**把 PVE/U6 改为 trunk。VLAN10 必须在 LAN55/LAN66 稳定后,以独立维护窗完成 PVE VLAN-aware 隔离、SE5420 trunk、gfw IPv4 NAT/内网拒绝策略和 IPv6 策略;仅在这些验收通过后才启用专用 SSID。详见部署指南第二步。
---
## 6. 流量怎么走
### 6.1 同 VLAN、两端都在核心上(例如 NAS ↔ PC,皆 LAN66
```text
NAS ──(2.5G)── SE5420 ──(2.5G)── PC
(二层交换,不经过 ER-X)
```
这是本次升级的**主要收益**。吞吐受 NIC、线材、磁盘限制;与 ER-X 的 1G 无关。
### 6.2 访问互联网(任意 LAN66/LAN55 客户端)
```text
客户端 → SE5420 →(对应 access 上联)→ ER-X → PPPoE → Internet
默认网关 = .254
```
上网路径仍受 **ER-X 上联 1G** 与 PPPoE 限制;换核心**不会**把公网变成 2.5G。
### 6.3 LAN66 ↔ LAN55
```text
LAN66 设备 → SE5420(VLAN66) → ER-X eth0
↕ 三层转发
LAN55 设备 → SE5420(VLAN55) → ER-X switch0
```
跨网段必须过 ER-X;两条上联各 1G,且 ER-X 是否硬件加速需实测,**不要**按 2.5G 承诺跨 VLAN 吞吐。
### 6.4 旁路由 `gfw`(默认主网行为)
默认:DHCP 仍发网关 `.254`。客户端若把代理/策略指到 `gfw`,或由上游策略引流,则
`gfw` 以旁路由身份处理;**主网关身份仍是 ER-X**。
### 6.5 未来:升级专用 SSID(仅说明,非本次范围)
```text
手机 --SSID(升级)--> U6 Lite --tag VLAN10--> SE5420 --tag VLAN10--> PVE --> gfw
网关 .1
gfw 上游仍回 LAN66 → SE5420 → ER-X → Internet
```
主 SSID(如现有 `ubnt-windy`)继续 untagged/LAN66,网关仍是 `.254`,互不影响。
---
## 7. 端口预留(SE5420
端口号以面板为准;下表是角色预留。
| 预留 | 对端 | 模式 | 备注 |
|---|---|---|---|
| 铜口 1 | ER-X `eth0` | access VLAN66 | 主网上联 |
| 铜口 2 | ER-X `switch0` 成员口 | access VLAN55 | LAN55 上联 |
| 铜口 3 | PVE`gfw` 所在宿主机) | 阶段 03access VLAN66;日后 trunk native66+tag10 | 只允许需要的 VLAN |
| 铜口 4 | U6 Lite | 同上 | 本地取电;日后升级 SSID |
| 铜口 5 | UAP-AC-Lite | access VLAN55 | 本地取电;日后客人 SSID |
| 其余铜口 | NAS、PC、`dns``ubnt`… | 默认 access VLAN66 | 首批 2.5GNAS + 主力 PC/PVE |
| SFP+ 14 | 未来 10G | 空槽 | 优先 DAC/AOC/光;不用 10GBASE-T 模块作默认 |
禁止:同一设备同时接旧路径与新核心形成**无意双上行**(环路/环回保护误杀/不稳定)。
---
## 8. Wi-Fi 与 UniFi(目标语义)
| SSID 类型 | AP | 二层落点 | 客户端网关 | 本次升级是否必须完成 |
|---|---|---|---|---|
| 现有主 SSID(如 `ubnt-windy` | 按现网 | LAN66 等 | `.254` | 迁线后保持 Connected 即可 |
| 客人 SSID | **仅 AC-Lite** | LAN55 + UniFi Guest 隔离 | `.254` | **否**(后续) |
| 升级专用 SSID | **仅 U6** | 未来 VLAN10 | **`.1`gfw** | **否**(后续) |
Inform 仍为:`http://192.168.66.46:9080/inform`
AC-Lite 在 LAN55、Controller 在 LAN66:跨网段由 ER-X 转发(现网已验证可达)。
---
## 9. 管理与安全面
| 项 | 做法 |
|---|---|
| SE5420 管理 IP | LAN66 未占用地址;DHCP reservation 或静态;先查 ER-X 再写 |
| 管理协议 | 本地 HTTPS / SSH;禁用不需要的 HTTP/Telnet |
| 云管 | 先本地管稳;商云/App **以后再说** |
| L3 功能 | **全部关闭**(无 SVI、无静态路由、无 DHCP Server/Relay |
| STP | 开启 RSTP/MSTP;合理根桥优先级;迁完后不留生产环路 |
| 配置备份 | 离线导出;**不得**把含口令/SNMP 的备份放进本仓库 |
| AP | 本地电源 + 数据网线;核心不供电 |
---
## 10. 性能预期(诚实边界)
| 场景 | 预期 |
|---|---|
| 同 VLAN、两端 2.5G 接在 SE5420 | 可达约 2.5G 级二层(测 `iperf3` 验收) |
| 上网 / PPPoE | 仍受 ER-X WAN 与 1G 上联限制 |
| LAN66 ↔ LAN55 | 必过 ER-X;按 1G 级规划,实测为准 |
| SFP+ 空槽 | **不等于**网络已是 10G |
| 未来 VLAN10 + `gfw` | 受 PVE NIC/vSwitch 与 `gfw` 处理能力限制,勿预先承诺 2.5G 路由 |
---
## 11. 验收时「网络算正常」的样子
阶段 3 完成后,应同时满足:
1. 有线设备从 SE5420 获取正确网段地址;默认路由为对应 `.254`DNS `.36` 可用。
2. 互联网、端口转发、现有本地服务行为与迁线前一致(除同 VLAN 内网吞吐变快)。
3. U6`.6`)与 AC-Lite`.55.5`)在 UniFi 为 ConnectedInform 未改。
4. SE5420 管理面仅可信 LAN66 可达;默认口令已改;配置已离线备份。
5. 端口协商速率、CRC/错误计数正常;旧交换已下电闲置。
6. (可选强化)两台 2.5G 终端同 VLAN `iperf3` 结果入库/记笔记。
VLAN10 / 升级 SSID / 客人 SSID **失败或未做,不否决**本次核心升级。
---
## 12. 回滚语义
任一阶段失败:停止继续搬家;把刚迁的设备插回原 ER-X/旧交换端口。
**不要**在故障中改 ER-X 的 WAN、DHCP 池、网关地址或 SSH 策略。
后续 VLAN10 失败:只撤 SSID/VLAN 绑定,主 SSID 与 `.254` 路径保持不动。
---
## 13. 参考
- 实施阶段与清单:[lan-core-switch-upgrade-plan.md](lan-core-switch-upgrade-plan.md)
- 现网地图:[lan-overview.md](lan-overview.md)
- ER-X[edgerouter-x-configuration.md](edgerouter-x-configuration.md)、[hosts/gw.md](../hosts/gw.md)
- UniFi / VLAN10 前置:[unifi-network.md](unifi-network.md)
- `gfw`[hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md)
- SE5420 规格:<https://www.tp-link.com.cn/product_2899.html?v=specification>
+183
View File
@@ -0,0 +1,183 @@
# LAN network overview
A standalone, live-verified map of the **internal LAN** hosts and the software
deployed on them. Written for a future self / another agent to get oriented fast.
Focused per-host facts and runbooks live in [`../hosts/`](../hosts/) and are linked
from each section below.
> **Scope:** this document covers the real LAN only. It deliberately excludes the public VPS
> hosts (`mx2`, `us2`, `us4`, `hk2`, `synapse`) and any dormant/stub software. The
> WireGuard VPN server is external (`us4.wsvc.info`); there is **no WireGuard interface on
> the LAN side**.
> **Verified live on 2026-08-06** by read-only SSH from the WSL client. No
> changes were made. `gfw.windy.lan` root SSH was re-verified the same day after
> the key was installed; its facts below are from the fresh probe.
---
## Topology
```
Internet
│ PPPoE (pppoe0, MTU 1492, IPv6 PD /60)
┌───────────────────────────────────────────────┐
│ gw — EdgeRouter X (192.168.66.254) │
│ eth0 = LAN66 · switch0(eth1-3) = LAN55 │
│ WAN_IN active on pppoe0 (default-drop) │
│ port-forwards: hass/transmission/ssh/openvpn │
LAN66 └───────────────┬──────────────────────────────┘
(192.168.66.0/24) │
│ gfw.windy.lan — OpenWrt / OpenClash (192.168.66.1)
│ dns.windy.lan — AdGuard Home + Mihomo (192.168.66.36)
│ ubnt — UniFi Network Controller (192.168.66.46)
```
> **SE5420 purchased (2026-08-09):** TP-Link `TL-SE5420` acquired; deployment plan is
> [lan-se5420-deployment-guide.md](lan-se5420-deployment-guide.md). Design/planning refs:
> [lan-erx-se5420-network.md](lan-erx-se5420-network.md),
> [lan-core-switch-upgrade-plan.md](lan-core-switch-upgrade-plan.md).
---
## Hosts
| Host | Role | SSH | IPv4 | Facts |
|------|------|-----|------|-------|
| **gw** | EdgeRouter X primary LAN gateway | `ssh -4 zhiqiang@192.168.66.254` | `192.168.66.254` | [hosts/gw.md](../hosts/gw.md) |
| **PVE** | Proxmox host (`.66.26`/vmbr0 · `.55.26`/vmbr1) — hosts gfw/dns/ubnt/haos VMs | `ssh -4 root@192.168.66.26` | `192.168.66.26` | — |
| **gfw.windy.lan** | OpenWrt LAN gateway / OpenClash — **PVE VM 140** | `ssh -4 root@192.168.66.1` | `192.168.66.1` | [hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md) |
| **dns.windy.lan** | AdGuard Home LAN DNS + Mihomo explicit proxy — **PVE VM 120** (`pihole`) | `ssh -4 windy@192.168.66.36` | `192.168.66.36` | [hosts/dns.windy.lan.md](../hosts/dns.windy.lan.md) |
| **ubnt** | UniFi Network Controller — **PVE VM 160** | `ssh -4 windy@192.168.66.46` | `192.168.66.46` | [hosts/ubnt.md](../hosts/ubnt.md) |
| **haos** | Home Assistant (HAOS) — **PVE VM 180** (LAN55) | — | `192.168.55.11` | — |
| **NAS/FreeNAS** | NAS; `transmission` jail runs here (`.51`) | — | — | — |
| **U6 Lite** | UniFi AP (LAN66) | `ssh -4 zhiqiangf@192.168.66.6` | `192.168.66.6` | [docs/unifi-network.md](../docs/unifi-network.md) |
| **UAP-AC-Lite** | UniFi AP (LAN55) | `ssh -4 zhiqiangf@192.168.55.5` | `192.168.55.5` | [docs/unifi-network.md](../docs/unifi-network.md) |
> **Positioning facts (verified 2026-08-09):** `dns`/`ubnt`/`gfw`/`haos` are all VMs on PVE
> (no separate physical hosts); `transmission` is a FreeNAS/NAS jail. Only gw, PVE,
> NAS, U6, UAP-AC-Lite, and wired PCs/NAS are physical SE5420 ports. See
> [lan-se5420-deployment-guide.md](lan-se5420-deployment-guide.md) §1.
---
## gw — EdgeRouter X (LAN gateway)
| Item | Value |
|------|-------|
| Platform | EdgeOS v3.0.1 (Build 5862409), kernel 4.14.54-UBNT |
| Interfaces | `eth0`=192.168.66.254/24 (LAN66) · `switch0`=192.168.55.254/24 (LAN55) · `eth4`=WAN PPPoE · `pppoe0`=WAN default route (MTU 1492, IPv6 PD /60) |
| WAN | `pppoe0`=113.68.54.159 (PPPoE) · IPv6 SLAAC /64 on both LANs |
| Firewall | `WAN_IN` **active** on pppoe0 (established/related accept, invalid drop, default drop) · `LAN_IN`/ `LAN_OUT` defined but **inactive** (not attached) |
| DHCP | LAN1 (66) + LAN2 (55), 24h leases, client DNS = `192.168.66.36`, pools .38.243, static mappings for infra hosts |
| DNS | `service dns forwarding` cache 512, listen on eth0 + switch0; DHCP advertises `192.168.66.36` as client DNS |
| Port-forwards | `hass`→192.168.55.11:8123 · `transmission`→192.168.66.51:51413 · `ssh`→192.168.66.36:22 (orig 5822) · `openvpn`→192.168.66.32:1194 · WAN iface pppoe0 |
| Management | SSH TCP 22 · EdgeOS GUI HTTP 80 / HTTPS 443 |
**Static DHCP mappings (LAN66):** `OnePlus-12`=.37, `gfw`=.1, `hp-nas`=.32, `pihole`=.36, `pve`=.26, `transmission`=.51, `ubnt-6`=.6, `ubnt-app`=.46, `windy-pc`=.99. LAN55: `Aqara-Hub-M3-10CB`=.248.
> **Note:** `LAN_IN`/`LAN_OUT` are defined but not applied to an interface, so LAN55
> and LAN66 are bidirectionally reachable by default. Do not rely on those rules as
> an access boundary. See [hosts/gw.md](../hosts/gw.md).
---
## gfw.windy.lan — OpenWrt / OpenClash gateway
| Item | Value |
|------|-------|
| IPv4 | `192.168.66.1` |
| OS | **ImmortalWrt 25.12.0** (r37854), Linux 6.12.87, x86/64 |
| SSH | `ssh -4 root@192.168.66.1` (key-only, verified 2026-08-06) |
| OpenClash | `/etc/openclash/clash` (clash_meta core) + config `/etc/openclash/pass-cat.yaml` |
| Mode | **fake-ip + TPROXY transparent proxy** (`operation_mode=fake-ip`, `en_mode=fake-ip`, `proxy_mode=rule`) |
| DNS | dnsmasq → clash DNS `127.0.0.1#7874`; `mosdns` also listens on `127.0.0.1:6052` (not the active path) |
| nft | `table inet fw4` with OpenClash TPROXY/redirect + DNS-hijack rules; residual `table inet passwall` (0 packets, unused) |
**OpenClash listeners:** HTTP `7890` · SOCKS `7891` · Redirect `7892` · Mixed `7893` · TPROXY `7895` · DNS `7874` · dashboard `9090`. `8443` is **not** an OpenClash listener (only in its TLS-sniffing port list).
> **Operational note:** a LAN client request logged as `<client> --> localhost:8443` + refusal is normally an explicit-proxy client sending its own loopback to the gateway. Fix the
> originating host's `NO_PROXY`/`no_proxy` config; do not create a listener on this router to mask it. See [docs/unifi-openclash-localhost.md](../docs/unifi-openclash-localhost.md).
---
## dns.windy.lan — AdGuard Home + Mihomo (DNS & proxy host)
| Item | Value |
|------|-------|
| IPv4 | `192.168.66.36` |
| OS | Debian 12 (kernel 6.1.0-43) |
| SSH | `ssh -4 windy@192.168.66.36` |
| AdGuard Home | Docker container `adguardhome` (host network, pinned digest), `/opt/adguardhome` |
| DNS | TCP/UDP `53` (no source ACL — serves both LAN55 and LAN66) |
| UI | `192.168.66.36:80` (LAN66 only via nft `agh_ui_access`) |
| Upstreams | `https://dns.alidns.com/dns-query` + `https://doh.pub/dns-query` (load-balanced) · bootstrap `223.5.5.5`/`223.6.6.6` · fallback `https://adg.chans.xyz/dns-query` |
| DNSSEC | **disabled** (upstream path failed known-bad-signature check) |
| Rewrites | `hass.local` + `hass.windy.lan` → same Home Assistant address |
| Query log | 14 days, anonymized client IPs |
| Mihomo | **systemd service** (NOT Docker) — `/usr/local/bin/mihomo -d /etc/mihomo`, config `/etc/mihomo/config.yaml` |
| Mihomo listeners | HTTP `7890` · SOCKS `7891` · Mixed `7892` · DNS `1053` · external-controller `9090` · `allow-lan: true` · mode `rule` |
| nft | `table inet agh_ui_access` permits only `192.168.66.0/24` to TCP/80, drops other TCP/80 input |
**Routing boundary:** read-only checks found no host TUN policy routing or nftables/iptables transparent-redirection on this host. A Mihomo listener alone does not mean it intercepts AdGuard traffic; the default gateway (`gfw.windy.lan`) can still affect outbound, so inspect its OpenClash rules separately before changing upstream routing.
---
## ubnt — UniFi controller host
| Item | Value |
|------|-------|
| IPv4 | `192.168.66.46` |
| Compose | `/home/windy/unifi-9/compose.yml` · service `unifi` · container `unifi-controller` |
| Image | `jacobalberty/unifi:v9.5.21` (host network) |
| HTTPS UI | `https://192.168.66.46:8443` |
| Inform | `http://192.168.66.46:9080/inform` (not 8080) |
| Health | `docker inspect --format '{{.State.Health.Status}}' unifi-controller``healthy` (2026-08-06) |
| Also running | **dockge** (`louislam/dockge:1`, healthy) — compose UI, `/opt/stacks` |
| Dormant | `semaphoreui` (semaphore + postgres, exited 9 months) · `homarr` dir (no container) |
**Proxy contract:** the container keeps `NO_PROXY`/`no_proxy` enabled with
`localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12` so a controller
request to `localhost:8443` stays local instead of going to the OpenClash gateway.
See [docs/unifi-openclash-localhost.md](../docs/unifi-openclash-localhost.md).
---
## Managed access points
| Name | Model | Mgmt IP | Firmware | Network | Inform |
|------|-------|---------|----------|---------|--------|
| U6 Lite | U6 Lite (UAL6) | `192.168.66.6` | 6.7.54.15663 | LAN66 | `:9080` |
| UAP-AC-Lite | U7LT | `192.168.55.5` | 6.8.2.15592 | LAN55 | `:9080` |
Both reported **Connected** to `http://192.168.66.46:9080/inform` on 2026-08-06.
AP SSH account is `zhiqiangf` (key-only, verified). See [docs/unifi-network.md](../docs/unifi-network.md).
---
## Quick orientation (who runs what)
| Service | Host | How to reach |
|---------|------|--------------|
| LAN gateway / routing / DHCP | gw | `ssh -4 zhiqiang@192.168.66.254` |
| Transparent/explicit proxy (OpenClash) | gfw.windy.lan | `ssh -4 root@192.168.66.1` |
| LAN DNS (AdGuard Home) + Mihomo proxy | dns.windy.lan | `ssh -4 windy@192.168.66.36` |
| UniFi controller + dockge | ubnt | `ssh -4 windy@192.168.66.46` |
| Wi-Fi APs | U6 Lite / UAP-AC-Lite | via controller |
---
## Related docs
- [hosts/gw.md](../hosts/gw.md) — EdgeRouter X detail, firewall, verification record
- [hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md) — OpenClash listeners
- [hosts/dns.windy.lan.md](../hosts/dns.windy.lan.md) — AdGuard Home + Mihomo detail
- [hosts/ubnt.md](../hosts/ubnt.md) — UniFi controller + proxy contract
- [docs/unifi-network.md](../docs/unifi-network.md) — APs, inform endpoint, recovery
- [docs/unifi-third-party-vlan10-dhcp.md](unifi-third-party-vlan10-dhcp.md) — VLAN Wi-Fi feasibility and DHCP boundary
- [docs/unifi-openwrt-vlan10-implementation-examples.md](unifi-openwrt-vlan10-implementation-examples.md) — supported topology and examples
- [docs/edgerouter-x-configuration.md](../docs/edgerouter-x-configuration.md) — effective gw config
- [docs/unifi-openclash-localhost.md](../docs/unifi-openclash-localhost.md) — proxy bypass
- [runbooks/adguard-home-health.md](../runbooks/adguard-home-health.md) — AGH health
+397
View File
@@ -0,0 +1,397 @@
# ER-X → RB5009 网关升级:网络形态与实施计划
**状态:** 规划文档(未采购、未接线、未改生产配置)。
**重要变更(2026-08-09):** **SE5420 已采购**,网络升级改为「保留 ER-X + SE5420 核心」路径——
实施与验证以 [lan-se5420-deployment-guide.md](lan-se5420-deployment-guide.md) 为准。
本文保留为「ER-X 网关未来替换为 RB5009」的备选方案;其中 PVE 透传调研与 VLAN10 实现方法仍适用。
---
## 1. 一句话架构
```text
公网 ← RB5009(主网关:PPPoE / NAT / 防火墙 / DHCP / IPv6 / VLAN 66+55 三层)
↑ 全部终端 / AP / PVE 直连其 8×1G 端口
(SFP+ 空槽,为未来 10G 核心预留)
```
- **RB5009** 兼任路由与 LAN 二层交换:所有接入设备直插 RB5009 端口。
- 客户端默认网关仍是 **`.254`**RB5009 沿用 ER-X 地址,客户端零感知)。
- **`gfw`** 仍为旁路由(默认网关 `.254`);OpenClash 行为不变。
- **ER-X 升级后下电闲置,作为已配置的备件保存**(回滚路径)。
- 2.5G/10G 同 VLAN 交换目标**放弃**(无 SE5420);LAN 二层为 1G。
---
## 2. 已确认的设计决策
| 决策 | 结论 |
|---|---|
| 角色 | RB5009 单机:路由 + LAN 交换 + 双 VLAN 三层 |
| WAN | **PPPoE**,走 **`ether1`2.5G 铜口接 ONT/光猫)**MTU 1492 |
| LAN 模型 | **单 bridge + VLAN 过滤**VLAN 66(多数端口)+ VLAN 55VLAN 10 预留 |
| 网关地址 | **沿用 `192.168.66.254` / `192.168.55.254`**;重编址另立项目 |
| IPv6 | **保留对等**PPPoE 上 DHCPv6-PD `/60` + 双 LAN SLAAC `/64` + IPv6 防火墙 |
| 端口转发 | 4 条 **原样迁入**hass / transmission / ssh / openvpn+ hairpin NAT |
| LAN55 成员 | **保持现状**AC-Lite、Aqara 及原 LAN55 设备)不迁到 VLAN66 |
| 管理面 | SSH **仅密钥**`zhiqiang`);禁用默认 `admin`**禁 Winbox/API/WebFig** 或仅限 LAN |
| 迁移方式 | 台面预配置(临时 `.253`)→ 维护窗(30–60 分钟)→ 验证 → ER-X 留作备件 |
| 固件/备份 | 当前稳定 RouterOS 7.x;上线前 `.backup` + 文本导出存离线 |
| 时区/NTP | `Asia/Shanghai` + 现有 NTP 服务器 |
| 安全 | PPPoE 口令、SSH 私钥、`zhiqiang` 口令**只存设备上**,不落本仓库 |
---
## 3. 升级后的网络形态(拓扑图)
### 3.1 目标物理拓扑(升级完成后)
```text
Internet
│ PPPoE (ether1 2.5G, MTU 1492, IPv6 PD /60)
┌──────────────────────────────────────────────┐
│ RB5009 (gw) RouterOS 7.x │
│ ether1 = WAN (PPPoE) │
│ bridge + vlan-filtering=yes │
│ vlan66 = 192.168.66.254/24 (pvid 66) │
│ vlan55 = 192.168.55.254/24 (pvid 55) │
│ SFP+ = 空槽(未来 10G
│ SSH-only 管理(仅 LAN
└─────┬─────────┬──────────┬──────────┬────────┘
│ pvid66 │ pvid66 │ pvid66 │ pvid55
│ │ │ │
gfw (.1) dns (.36) ubnt (.46) UAP-AC-Lite (.55.5)
PVE (.26) NAS/PC… U6 Lite Aqara (.55.248)
(.66.6)
```
端口角色以面板为准;下表是建议预留:
| RB5009 端口 | 对端 | 模式 |
|---|---|---|
| `ether1` | ONT/光猫 | WANPPPoE |
| `ether2``ether8` | PVE/gfw、dns、ubnt、U6 Lite、NAS、PC、杂项 | accesspvid 66 |
| `ether9` | 原 LAN55 成员(AC-Lite、Aqara…) | accesspvid 55 |
| SFP+ `sfp-sfpplus1` | 未来 10G 核心 | 空槽 |
> **禁止双上行:** 任一设备不得同时接旧 ER-X 与 RB5009(环路 / 双默认网关)。
### 3.2 逻辑网络
| VLAN | 子网 | 网关 / DHCP | 形态 | 成员 |
|---|---|---|---|---|
| **66** | `192.168.66.0/24` | RB5009 `vlan66` `.254` | 多数端口 access | gfw、dns、ubnt、U6、PVE、NAS、PC… |
| **55** | `192.168.55.0/24` | RB5009 `vlan55` `.254` | `ether9` access | AC-Lite、Aqara…(原 LAN55 全量) |
| 10(预留) | `192.168.10.0/24` | 未来(`gfw`) | 暂不配业务 | 升级专用 SSID(后续项目) |
- 客户端 DNSDHCP 仍下发 **`192.168.66.36`**AdGuard Home)。
- UniFi Inform 仍为 `http://192.168.66.46:9080/inform`
- LAN66 ↔ LAN55 互通由 RB5009 三层转发(沿用现网默认可达语义,防火墙不设隔离边界)。
### 3.3 流量怎么走
```text
同 VLANNAS ↔ PC): 客户端 → RB5009 端口 → bridge(二层交换,1G)
跨 VLAN(66 ↔ 55): 客户端 → RB5009 路由(vlan66 ↔ vlan551G
上网: 客户端 → RB5009 → PPPoE → Internet(限速 = 线路;RB5009 fasttrack 去掉 ER-X ~900M 天花板)
旁路由: 默认网关仍 `.254`;显式代理走 gfw(不变)
未来 VLAN10 U6 trunk → RB5009 →(转发 VLAN10)→ gfw(后续项目)
```
### 3.4 性能预期(诚实边界)
| 场景 | 预期 |
|---|---|
| WAN / PPPoE | 线路速率以内;若线路 >1GRB5009 fasttrack 可跑满(ER-X 受 900M 限制) |
| 同 VLAN 交换 | **1G**(无 SE5420RB5009 8×1G |
| 跨 VLAN | 1G 级三层 |
| SFP+ 空槽 | 不等于已是 10G |
---
## 4. 分阶段实施计划
### 阶段 0:采购前核验
1. 确认 RB5009 revision、保修、零售渠道、RouterOS 当前稳定版本号(建议 7.x 最新 stable)。
2. 确认硬件:1× 2.5G PoE-inether1)、8× 1G、1× SFP+、1GB RAM。SFP+ 模块本次不买。
3. 盘点线缆与对端:ONT ↔ ether1;原 LAN55 设备清单(AC-Lite、Aqara…)及原 ER-X `switch0` 接线。
### 阶段 1:离线初始化(台面)
1. 仅电源 + 隔离管理本。设置身份、时区 `Asia/Shanghai`、NTP。
2.`zhiqiang` 管理用户(**密钥登录,禁密码**);**删除/禁用默认 `admin`**。
3. 建 bridge + VLAN 66/55 + 端口模板(`ether28` pvid66`ether9` pvid55);**临时地址 `.253`**(不与 ER-X `.254` 冲突)。
4. 配 WAN PPPoEether1)、默认路由、NAT masquerade、4 条端口转发 + hairpin、防火墙(input/forward)、MSS clamp、IPv6 PD + SLAAC + IPv6 防火墙。
5. 配 DHCP 池 `.38.243`、24h 租约、DNS `.36`、UniFi Inform 选项、**静态映射全量照搬**。
6. 管理面收口:**禁 Winbox8291/ API8728/ WebFig80/443**SSH 仅限 LAN66/55。
7. `system backup save` + `/export`,导出到离线存储(**不落仓库**)。
**参考 RouterOS 配置骨架**(口令/密钥用占位符;落地前逐一核对):
```text
# 身份 / 时区 / NTP
/system identity set name=gw
/system clock set time-zone-name=Asia/Shanghai
# WAN
/interface ethernet set ether1 name=wan
/interface pppoe-client add name=pppoe0 interface=wan user=<PPPoE_USER> \
password=<PPPoE_PASS> add-default-route=yes use-peer-dns=no \
mtu=1492 mru=1492
# bridge + VLAN
/interface bridge add name=bridge66 vlan-filtering=yes
/interface bridge port add bridge=bridge66 interface=ether2 pvid=66
... # ether3..ether8 同 pvid 66
/interface bridge port add bridge=bridge66 interface=ether9 pvid=55
/interface bridge vlan add bridge=bridge66 vlan-ids=66 tagged=bridge66
/interface bridge vlan add bridge=bridge66 vlan-ids=55 tagged=bridge66
/interface vlan add name=vlan66 interface=bridge66 vlan-id=66
/interface vlan add name=vlan55 interface=bridge66 vlan-id=55
# 地址(台面先用 .253,维护窗切 .254)
/ip address add address=192.168.66.254/24 interface=vlan66
/ip address add address=192.168.55.254/24 interface=vlan55
# NAT masquerade + 端口转发 + hairpin
/ip firewall nat add chain=srcnat out-interface=pppoe0 action=masquerade
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=tcp dst-port=8123 \
action=dst-nat to-addresses=192.168.55.11 to-ports=8123
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=tcp dst-port=51413 \
action=dst-nat to-addresses=192.168.66.51 to-ports=51413
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=udp dst-port=51413 \
action=dst-nat to-addresses=192.168.66.51 to-ports=51413
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=tcp dst-port=5822 \
action=dst-nat to-addresses=192.168.66.36 to-ports=22
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=udp dst-port=5822 \
action=dst-nat to-addresses=192.168.66.36 to-ports=22
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=tcp dst-port=1194 \
action=dst-nat to-addresses=192.168.66.32 to-ports=1194
/ip firewall nat add chain=dstnat in-interface=pppoe0 protocol=udp dst-port=1194 \
action=dst-nat to-addresses=192.168.66.32 to-ports=1194
/ip firewall nat add chain=srcnat connection-nat-state=dstnat action=masquerade # hairpin
# 防火墙(input / forward),顺序很重要:SSH 放行必须早于默认 drop
/ip firewall filter add chain=input connection-state=established,related action=accept
/ip firewall filter add chain=input connection-state=invalid action=drop
/ip firewall filter add chain=input protocol=icmp action=accept
/ip firewall filter add chain=input in-interface=pppoe0 action=drop comment="drop WAN in"
/ip firewall filter add chain=input protocol=tcp dst-port=22 \
src-address=192.168.66.0/24,192.168.55.0/24 action=accept comment="mgmt SSH"
/ip firewall filter add chain=input action=drop comment="drop other input"
/ip firewall filter add chain=forward connection-state=established,related action=fasttrack-connection \
hw-offload=yes
/ip firewall filter add chain=forward connection-state=established,related action=accept
/ip firewall filter add chain=forward connection-state=invalid action=drop
/ip firewall filter add chain=forward in-interface=pppoe0 action=drop comment="drop WAN fwd"
/ip firewall filter add chain=forward action=accept comment="accept LAN fwd"
# MSS clampPPPoE MTU 1492 → 1452ER-X 旧值 1412 偏小,验证后按标准值收敛)
/ip firewall mangle add chain=forward protocol=tcp tcp-flags=syn \
tcp-mss=1400-65535 action=change-mss new-mss=1452 passthrough=yes
# IPv6PD /60 + SLAAC /64
/ipv6 dhcp-client add interface=pppoe0 request=prefix pool-name=pd6 \
pool-prefix-length=60 add-default-route=yes
/ipv6 address add from-pool=pd6 interface=vlan66 address=::1 adverts=yes
/ipv6 address add from-pool=pd6 interface=vlan55 address=::1 adverts=yes
# /ipv6 firewall filter 参照 IPv4established/related accept、invalid drop、
# icmpv6 accept、pppoe0 in drop、默认 dropforward 同构
# DHCP(池 / DNS / Inform / 静态映射全量照搬 ER-X
/ip pool add name=pool66 ranges=192.168.66.38-192.168.66.243
/ip pool add name=pool55 ranges=192.168.55.38-192.168.55.243
/ip dhcp-server add name=dhcp66 interface=vlan66 address-pool=pool66 lease-time=1d
/ip dhcp-server add name=dhcp55 interface=vlan55 address-pool=pool55 lease-time=1d
/ip dhcp-server network add address=192.168.66.0/24 gateway=192.168.66.254 \
dns-server=192.168.66.36
/ip dhcp-server network add address=192.168.55.0/24 gateway=192.168.55.254 \
dns-server=192.168.66.36
# 静态映射:从 ER-X 导出后逐条 /ip dhcp-server lease add ...
# UniFi InformDHCP option 43hex 编码为 http://192.168.66.46:9080/inform),
# 或依赖已 adopt AP 的 set-inform;与现网 ER-X 行为保持一致。
```
### 阶段 2:上台预验证(ER-X 仍在线)
1. 将 RB5009 `ether2` 接入现有 LAN66 网段(管理本同网段),SSH 登录 `.253`
2. 全配置复查:VLAN 表、防火墙规则顺序(SSH 在 drop 前)、NAT、路由、IPv6。
3. **暂不启用 RA/DHCP**(避免与 ER-X 冲突)。预验证 `.253` 可达、SSH 密钥生效、`zhiqiang` 无密码。
4. 确认 ER-X 当前配置**已备份并保存**(作为回滚依据;`show configuration commands` 过滤敏感行)。
### 阶段 3:维护窗切换(约 30–60 分钟)
1. 变更 RB5009 `vlan66`/`vlan55` 地址 `.253 → .254`
2. **下电 ER-X**(保留原接线与配置,不作任何改动)。
3. ONT 线从 ER-X `eth4` 移到 RB5009 `ether1`
4. 观察 PPPoE 拨号:`/interface pppoe-client monitor pppoe0` 直至 `status=established`;确认 WAN IP 与默认路由。
5. 验证清单(见第 5 节),全绿才算完成。
### 阶段 4:上线收口
1. 再确认管理面:禁 Winbox/API/WebFig、SSH 仅密钥、默认 `admin` 已除。
2. 更新固件/补丁至已核定的稳定版;重新导出备份(`.backup` + 文本)存离线。
3. ER-X 下电收纳为**已配置备件**,把其接线与角色记录到 `hosts/gw.md` 备份节。
### 阶段 5(后续,另立项目)
**时序确认:** VLAN10 升级专用 Wi-Fi 是 RB5009 上线**稳定之后**的独立项目,不并入本次维护窗。
- 4 条端口转发是否仍需的审计。
- 网关重编址(`.254 → 其它`)决策。
- **VLAN10 升级专用 SSID → `gfw`(可行性审查,2026-08-09**。
- 迁移后同步本仓库事实文档:`hosts/gw.md``docs/lan-overview.md``inventory/hosts.md``AGENTS.md` 快速地图。
### 阶段 5 附:VLAN10 可行性审查
**结论:** 换 RB5009 后**物理上可行**,但 RB5009 只解决「交换机侧」路径;PVE 宿主机到 `gfw` 虚拟机这一段(此前判定为不可行的关键缺口)仍必须单独打通。
**为什么当前不能在 AP 上启用 VLAN10:**
U6 接的是 ER-X `eth0` 的普通 untagged LAN66 路径,尚无已验证的 VLAN10 端到端二层通道。给 SSID 选择 VLAN10 后,客户端不会获得可由 gfw 服务的预期 VLAN10 网络;帧究竟被丢弃、被设备错误处理,还是 SSID 实际没有打 tag,必须以 AP/交换机/gfw 抓包和配置核验判断。**不得**将“tagged 帧必然自动去标签并泄漏到 LAN66”作为实施前提。
**RB5009 下实现需要 3 个前提:**
1. **RB5009 bridge 增加 VLAN10 转发:**
`/interface bridge vlan add bridge=bridge66 vlan-ids=10 tagged=<U6口>,<PVE口>`
只在这两个口 tagged。**不要**在 RB5009 上给 VLAN10 配 IP / DHCPgfw 才是网关与唯一 DHCP)。
2. **PVE 宿主机路径(必须做,RB5009 解决不了这一段):**
PVE 物理上联口透传 tagged VLAN10`gfw` 虚拟机有一块 VLAN10 可达的网卡
(如 vmbr0 开 `vlan_filtering` + 给 gfw 加第二块 pvid 10 的 NIC,或 VM 内 `eth0.10`)。
这是此前「无法实现」的同一处缺口,需在 PVE/vSwitch 层单独验证。
3. **`gfw` 侧:** VLAN10 网卡 `192.168.10.1/24` + DHCP`192.168.10.0/24`+ 出 66 口
masquerade。RB5009 无需到 `192.168.10.0/24` 的路由(gfw SNAT 后源地址即 66 网段)。
**两个注意点:**
- **单 DHCP 原则:** VLAN10 上唯一 DHCP 是 gfwRB5009 不得在 VLAN10 提供 DHCP。
- **设计确认:** 升级 SSID 客户端走 gfw 网关后,DNS 为 gfw 的 dnsmasq→clash7874),
**不是** AdGuard `.36`——这是「被代理网络」的预期行为,需接受。
### 阶段 5 附:PVE 上 VLAN10 透传实现(调研 2026-08-09
官方 wiki + 多个社区案例支持两种模型,核心原则是 **「一层只拥有一个 tag」**——要么 PVE 拥有
access VLAN,要么 OpenWrt 拥有 trunk**不能在同一张 NIC 上两层都做**。
**方案 A(推荐):PVE 拥有 access VLANgfw 加第二块 virtio 网卡**
1. `vmbr0` 开 vlan-aware`/etc/network/interfaces`):
```text
auto vmbr0
iface vmbr0 inet static
address 192.168.66.26/24
gateway 192.168.66.254
bridge-ports eno1
bridge-stp off
bridge-fd 0
bridge-vlan-aware yes
bridge-vids 10 # 至少含 10;常见默认 2-4094
```
2. gfw VM 添加 `net1: virtio,bridge=vmbr0,tag=10`。VM 内该网卡是**无标记**接口
(已落在 VLAN10 广播域),**不要再建同名 8021q 子接口**。
3. 现有 `eth0`native/untagged = VLAN66)不动,gfw 原有角色不变。
4. PVE 物理上联口(PVE→RB5009)改为 **trunknative 66 + tagged 10**。
5. OpenWrt 内:新网卡 `192.168.10.1/24` + DHCP`192.168.10.0/24`+ 独立 firewall zone→wan masq。
**方案 BOpenWrt 拥有 trunk(单 NIC 多 VLAN**
- `vmbr0` vlan-awareVM NIC **不加 tag**trunk 原样进 VMOpenWrt 内建 `8021q` 设备
`eth0.10`x86/virtio 用 `option type '8021q'`,不要套用 DSA 教程)。物理上联 trunk。
- 更灵活(一块网卡多 VLAN),是 router VM 的常见做法,但需要 OpenWrt 8021q 配置,
且方案 A 对现有单网卡 gfw 改动更小。
**RB5009 侧配套(U6 口 + PVE 口都做成 trunk):**
```text
/interface bridge port add bridge=bridge66 interface=<U6口> pvid=66
/interface bridge port add bridge=bridge66 interface=<PVE口> pvid=66
/interface bridge vlan add bridge=bridge66 vlan-ids=66 tagged=bridge66 untagged=<U6口>,<PVE口>,<其余66口>
/interface bridge vlan add bridge=bridge66 vlan-ids=55 tagged=bridge66 untagged=<ether9>
/interface bridge vlan add bridge=bridge66 vlan-ids=10 tagged=<U6口>,<PVE口>
```
VLAN10 在 RB5009 上**纯二层桥接**U6 ↔ PVE),三层由 gfw 承担;RB5009 无 VLAN10 IP/DHCP。
**常见坑(社区高复发):**
- **双标签:** PVE 设了 `tag=10` 又在 OpenWrt 里建 `eth1.10` → 一帧被两层改两次。
- **native VLAN 不一致:** trunk 上无标记帧被两端当成不同 VLAN → DHCP 消失 / 拿到错网段
(正是你之前在 AP 上打 VLAN10 坏 66 网的同类故障)。
- **bridge 未 vlan-aware** tagged 帧进 host 后在 bridge 过滤层消失。
**诊断命令:**
```bash
# PVE host
bridge vlan show
ip -br link
tcpdump -eni <上联口> # 帧是否到物理口
tcpdump -eni vmtapXXXXXX # 帧是否到 VM tap
# OpenWrt guest
ip -d link show
logread -e netifd
```
**参考案例:**
- PVE 官方 wiki — Network Configuration / VLAN 802.1Q(三种模式 + vlan-aware bridge):
<https://pve.proxmox.com/wiki/Network_Configuration>
- 「OpenWrt VM on Proxmox」设计(trunk vs access 谁拥有 tag、双标签坑):
<https://phb-crystal-ball.org/run-openwrt-in-proxmox/>
- PVE 论坛「Tagged and Untagged VLAN」(`bridge-vlan-aware yes` + `bridge-vids` 解法):
<https://forum.proxmox.com/threads/tagged-and-untagged-vlan-configuration.144421/>
- OpenWrt 论坛 guest WiFi tagged VLAN 案例(`vmbr0.3`→VM 第三网卡;guest 拿到错误网段的
同型故障,最终归因在 PVE/host 侧):<https://forum.openwrt.org/t/continued-x86-openwrt-proxmox-vlan-issues/182623>
> **PVE host 改动风险:** 给 `vmbr0` 开 vlan-aware 是对宿主机网络栈的修改,有管理面断连风险;
> 需在维护窗内用控制台/独立带外通道进行,先 `ifreload -a`PVE7+ 的 ifupdown2 支持热应用),
> 保留原配置作回滚。
---
## 5. 验收清单(「网络算正常」的样子)
1. 管理面:`zhiqiang` 密钥 SSH 可从 LAN66/55 登录;默认 `admin` 禁用;Winbox/API 不可达;`.254` 管理可达。
2. VLAN:客户端取得正确网段(`.66.x` / `.55.x`),默认网关 `.254`DNS `.36` 可用。
3. 互联网:PPPoE 已建立;IPv4 外网通;端口转发逐条从公网验证(hass 8123、transmission 51413、ssh 5822→.36:22、openvpn 1194)。
4. IPv6:两 VLAN 拿到 SLAAC `/64`,默认路由存在,外部 IPv6 可达;IPv6 防火墙未阻断必要 ICMPv6/DHCPv6。
5. 本地服务:跨 VLAN`.55.x` ↔ `.66.x`)互通;AdGuard Home、UniFi 控制器、`gfw` 旁路由行为与升级前一致。
6. UniFiU6 Lite`.66.6`)与 UAP-AC-Lite`.55.5`)在控制器显示 **Connected**Inform 未变 `:9080`。
7. 无环路、无双默认网关;端口协商与错误计数正常。
8. 备份(`.backup` + 文本导出)已离线保存;ER-X 已下电收纳。
---
## 6. 回滚语义
任一阶段失败:**停手**。
- **维护窗内失败(PPPoE 未起 / 客户端不通 / 防火墙锁死):**
1. 下电 RB5009。
2. ONT 线插回 ER-X `eth4`。
3. 给 ER-X 上电 → 服务在数分钟内恢复。
4. 不要在故障中改 ER-X 的 WAN、DHCP、网关地址或 SSH 策略。
- **维护窗成功后** ER-X 只是备件;后续 VLAN10 失败只撤 SSID/VLAN 绑定,主 SSID 与 `.254` 路径不动。
---
## 7. 安全与记录
- **本仓库永不记录**:PPPoE 口令、`zhiqiang` 口令、SSH 私钥、RouterOS 备份(含口令/密钥)。
- 每次实质性变更(切换、回滚、加固)完成后,在 Linear **`vps` 项目**记录 scope / action / verification / 遗留 follow-up(本次先不建 issue,待执行时补)。
- SSH 与访问策略变更遵循仓库「SSH access safety」流程:ER-X 会话保持为回滚路径,新密钥登录验证成功前不关闭旧通道。
---
## 8. 参考
- 现网地图:[lan-overview.md](lan-overview.md)
- ER-X 现状:[edgerouter-x-configuration.md](edgerouter-x-configuration.md)、[hosts/gw.md](../hosts/gw.md)
- UniFi[unifi-network.md](unifi-network.md)、[hosts/ubnt.md](../hosts/ubnt.md)
- `gfw`[hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md)
- 作废方案(**不再实施**):[lan-erx-se5420-network.md](lan-erx-se5420-network.md)、[lan-core-switch-upgrade-plan.md](lan-core-switch-upgrade-plan.md)
- MikroTik RB5009 官方:<https://mikrotik.com/product/rb5009ug_s_in>、RouterOS v7 手册
+495
View File
@@ -0,0 +1,495 @@
# SE5420 核心交换机升级操作指导(细粒度版)
**目标:** 把 LAN66/LAN55 有线接入从 ER-X 内置交换迁到 TL-SE5420;稳定后再启用 VLAN10 升级专用 SSID。
**全程三条(不再重复):**
1. 先断旧、后接新,一次一根线;同一设备不得同时接旧路径与新核心。
2. 全程保持 SE5420 Type-C Console38400 8N1)连接——SSH 会话移动其承载链路即断,不是回滚保障。
3. 任一步「预期」不符 → 停止,执行 §16 对应回滚;故障中不动 ER-X 的 WAN/DHCP/SSH。
**关于命令的真实性:** SE5420 的 GUI 菜单路径来自官方 Omada VLAN/管理指南;CLI 语法仅采用官方手册出现过的(`enable``telnet disable``copy running-config startup-config``show vlan`)。PVE/gfw 的每步先有只读快照命令,改动均以你实际看到的输出为准,不套用写死的模板。
---
# 第一阶段:主网 55/66 迁移
## 1. 开工前准备
### 1.1 备份(4 个动作,全部留档到 `/tmp/opencode/baseline-YYYYMMDD/`,不进仓库)
1. ER-X 配置快照(滤敏感行):
```bash
mkdir -p /tmp/opencode/baseline-$(date +%Y%m%d)
ssh -4 zhiqiang@192.168.66.254 'show configuration commands' \
| grep -Ev 'pppoe|password|secret|key|pre-shared' \
> /tmp/opencode/baseline-$(date +%Y%m%d)/gw-sanitized.txt
```
- 预期:文件非空,`grep -i 'password\|pppoe' gw-sanitized.txt` 无输出。
2. PVE 配置:
```bash
ssh -4 root@192.168.66.26 'cat /etc/network/interfaces; echo ---; qm config 140' \
> /tmp/opencode/baseline-$(date +%Y%m%d)/pve-interfaces.txt
```
- 预期:能看到 `vmbr0`/`vmbr1` 定义与 VM140 的 `net0`/`net1`。
3. gfw 配置:
```bash
ssh -4 root@192.168.66.1 'cat /etc/config/network /etc/config/dhcp /etc/config/firewall' \
> /tmp/opencode/baseline-$(date +%Y%m%d)/gfw-uci.txt
```
- 预期:三个文件内容都在,且能看到 `ubunt_upg` zone、`ubunt_upg_nat`、`ubunt_upg_to_lan`。
4. 检查备份文件存在且可读:`ls -la /tmp/opencode/baseline-$(date +%Y%m%d)/`。
### 1.2 记录现网状态(迁移后比对用)
1. ER-X 状态:
```bash
ssh -4 zhiqiang@192.168.66.254 'show interfaces; show ip route; show dhcp server leases'
```
- 预期:eth0/switch0 up;两条直连路由 + pppoe0 默认路由;DHCP lease 正常。
2. PVE 地址:
```bash
ssh -4 root@192.168.66.26 'ip -br addr'
```
- 预期:`vmbr0`=192.168.66.26、`vmbr1`=192.168.55.26。
3. gfw 关键状态:
```bash
ssh -4 root@192.168.66.1 'uci show firewall | grep -E "ubunt_upg|wan.*masq"; ip -4 route'
```
- 预期:`wan.masq=1`、`ubunt_upg_nat`、`ubunt_upg_to_lan` 在位;默认路由 `default via 192.168.66.254`。
4. 两台 AP:打开 UniFi 控制台 → Devices,记录 U6 Lite 与 UAP-AC-Lite 的 IP、Connected 状态、Inform URL。
### 1.3 SSID 与标签
1. 禁用不可用 SSIDUniFi → WiFi → 找到 `ubnt-upg` → 关闭 Enabled 开关。
- 预期:开关变灰/显示 Disabled。
- 记录原设置(第二步回滚要用)。
2. 给现网每根线缆贴标签:`旧设备端接口名` + `目标 SE5420 口`,如 `NAS→SE5420口4`、`ERX-eth0→SE5420口1`。
3. 打印 §2 端口表放在操作位。
### 1.4 物料与带外
1. 确认有:SE5420(核对 V1.0 型号)、原装电源线、Type-C 串口线、Cat5e 以上网线数根、标签贴。
2. 管理本装「TP-LINK USB 串口驱动」(官网下载或随机光盘)。
3. 终端仿真软件准备(PuTTY/串口助手/Windows Terminal),预填参数 **38400 / 8 / 无校验 / 1 / 无流控**。
## 2. 端口表(SE5420
> 面板端口为**上下交错**排列:下排 = 1、3、5、7、9、11、13、15;上排 = 2、4、6、8、10、12、14、16(口 1 最左下,口 2 在其正上方)。
> **分组原则:上排全 = LAN66,下排全 = LAN55**55 在 66 下一排);ER-X 两条上联在最左相邻(口 2=66、口 1=55)。
### 上排 = VLAN66(口 2、4、6、8、10、12、14、16
| SE5420 口 | 对端 | 模式 | PVID | 成员 VLAN |
|---|---|---|---|---|
| **2** | ER-X `eth0`66 上联) | access | 66 | untagged 66 |
| **4** | PVE `enp1s0` | access | 66 | untagged 66 |
| **6** | U6 Lite | access | 66 | untagged 66 |
| **8** | NAS/FreeNAS 第 1 口 | access | 66 | untagged 66 |
| **10** | windy-pc`.99`)或 hp-nas`.32`)二选一 | access | 66 | untagged 66 |
| **12** | NAS 第 2 口(后续 LACP 窗用) | 物理断开 | — | — |
| **14** | LAN66 预留 | admin down | — | — |
| **16** | LAN66 预留 | admin down | — | — |
### 下排 = VLAN55(口 1、3、5、7、9、11、13、15
| SE5420 口 | 对端 | 模式 | PVID | 成员 VLAN |
|---|---|---|---|---|
| **1** | ER-X `switch0` 成员口(55 上联) | access | 55 | untagged 55 |
| **3** | PVE `enp2s0` | access | 55 | untagged 55 |
| **5** | UAP-AC-Lite | access | 55 | untagged 55 |
| **7** | 隔壁房间上联(IoT | access | 55 | untagged 55 |
| **9** | 打印机(未来) | admin down | — | — |
| **11** | LAN55 预留 | admin down | — | — |
| **13** | LAN55 预留 | admin down | — | — |
| **15** | LAN55 预留 | admin down | — | — |
| SFP+ 14 | 最右侧 4 个 | 空槽 | — | — |
- 第一步不建 VLAN10、不向 ER-X 送任何 tag、口 4/6PVE、U6)不做 trunk。
- NAS 只接口 8,口 12 断开(LACP 是独立维护窗)。
- 不占口的 VMdns(.36=VM120)、ubnt(.46=VM160)、gfw(.1=VM140)、haos(.55.11=VM180)transmission(.51) 是 NAS jail。
## 3. 开箱与固件升级
1. 核对包装型号 `TL-SE5420`;看设备底部标签硬件版本 `V1.0`。
2. 浏览器打开 `https://www.tp-link.com.cn/product_2899.html?v=download`。
3. 页面「升级软件」分类下下载最新固件(已知 `20230908_1.0.0`,以页面为准);解压得到 `.bin` 文件。
4. 管理本网线接 SE5420 任意 RJ45 口;管理本设静态 IP `10.18.18.10/24`(网关留空)。
5. 浏览器 `http://10.18.18.251`,首次登录创建用户名 + 强密码。
6. WEB 里 `系统管理 → 系统工具 → 软件升级 → 选择文件` 选固件 → 升级 → 等待重启,**期间勿断电**。
7. **验证:** 升级后 `系统管理 → 系统配置 → 系统信息` 版本号正确;面板 SYS 灯绿色闪烁、Link/Act 闪一下后熄灭。
## 4. 台面初始化 SE5420(不接生产)
### 4.1 登录通道确认
1. Console 线接 SE5420 Type-C Console 口 → 管理本 USB。
2. 打开串口终端(38400/8/无/1/无流控),连按回车。
3. **预期:** 出现 `TL-SE5420>` 提示符。
4. 输入 `enable` 回车,进特权模式(提示符变 `#` 或类似)。
5. 若 WEB 也通(第 3 步已创建账号),记录:WEB + Console 双通道可用。
### 4.2 基础安全
1. WEB 登录 → 改强口令:`系统管理 → 用户管理 → 用户配置` → 修改用户名/密码 → 保存。
2. 设时区与 NTP`系统管理 → 系统配置 → 系统时间` → 时区 `Asia/Shanghai` → NTP 服务器填 `ntp.aliyun.com`(或 `pool.ntp.org`)→ 保存。
3. 关 Telnet`系统管理 → 安全管理 → Telnet配置` → 关闭 Enable。
- CLI 替代:`telnet disable`。
4. 关 HTTP`系统管理 → 安全管理 → HTTP配置` → 关闭 HTTP 服务(保留 HTTPS)。
5. 开 SSH`系统管理 → 安全管理 → SSH配置` → 启用 SSH(端口 22)。
6. 关云管理:`系统管理 → 云管理 → 全局配置` → 全局开关保持关闭(不绑定 `smbcloud.tp-link.com.cn`)。
7. **保存:** 顶部/右上角保存按钮,或 `配置保存 → 保存配置`。
8. **验证:** CLI `show running-config` 中 Telnet/HTTP 为 disable;管理本 `https://10.18.18.251` 能开、SSH 22 能连(此时管理 IP 仍是默认 10.18.18.25166.253 的 HTTPS 验证在 §4.3 第 7 步做)。
### 4.3 VLAN 与管理地址(本步只建 66/55)
**步骤顺序依据:先建二层 VLAN(勾选端口成员)→ 再建 SVI → 验证管理路径 → 才清理 VLAN1。**
> **端口成员在 VLAN 配置界面的 Untagged/Tagged 选择器里直接勾选**,无独立端口配置页。Untagged = 该 VLAN 的 access 口;PVID 随之对应(以界面为准)。
1. 建 VLAN66`VLAN → 802.1Q VLAN → VLAN配置` → 新建 → VLAN ID 填 `66` → **Untagged 勾选:口 2、4、6、8、10**Tagged 不选)→ 保存。
2. 建 VLAN55:新建 → VLAN ID 填 `55` → **Untagged 勾选:口 1、3、5、7**Tagged 不选)→ 保存。
3. **验证:** VLAN 列表出现 66、55;VLAN1 仍在(暂不动)。
4. 建管理 SVI`路由功能 → 接口 → 接口设置` → 创建接口 → 接口 ID `VLAN 66` → IP 地址模式 `Static` → IP `192.168.66.253`、掩码 `255.255.255.0` → 管理状态开启 → 保存。默认网关:`路由功能 → 静态路由 → IPv4静态路由条目` → 新建:目的 `0.0.0.0`、掩码 `0.0.0.0`、下一跳 `192.168.66.254`、管理距离 `1`。
5. 管理本改静态 IP `192.168.66.10/24`,网线接 SE5420 **口 6**(口 6 已在 VLAN66)。
6. **验证管理路径:**
- `ping 192.168.66.253` → 通;
- 浏览器 `https://192.168.66.253` → 登录成功;
- (可选)SSH`ssh -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa admin@192.168.66.253` → 成功。
- 全通后再进行下一步;否则停手排查(查 Untagged 勾选/PVID/接线),保持 Console 兜底。
7. **保存配置**(右上角保存或 `配置保存 → 保存配置`)。
8. **验证:** `show vlan`:口 2/4/6/8/10 属 VLAN66,口 1/3/5/7 属 VLAN55,口 9/11/12/13/14/15/16 无成员;VLAN1 暂时还有全口成员(下一步清理)。
9. 清理 VLAN1`VLAN → 802.1Q VLAN → VLAN配置` → 编辑 VLAN1 → 把口 116 从 **Untagged 勾选**中移除(保留 66 或 55 的勾选不变)→ 保存。**改 PVID 不等于移除成员。**
> **VLAN1 不可删除属正常**TP-Link 默认 VLAN),且 **access 口必须属于某 untagged VLANGUI 会拒绝把口从 VLAN1 剔除**——不必强求。做法改为:**先把未用口 Disabled(第 13 步)再清 VLAN1 IP(第 10 步)**disabled 口不转发流量,VLAN1 剩下无管理 IP + 全 disabled 成员 = 无管理面、无转发路径,目标达成。
10. 删 VLAN1 管理 IP`路由功能 → 接口 → 接口设置` → VLAN1 → 清空 IPv4。
11. **保存配置**。
12. **验证:** `show vlan` VLAN1 无任何成员、无管理 IPVLAN66 管理路径仍通(SSH/HTTPS 不断)。
13. 关未用端口:`二层交换 → 端口管理 → 端口配置` → 口 9、11、12、13、14、15、16 **及 SFP+ 1720** → 端口状态 `Disabled`;并确认这些口不在任何 VLAN 的 Untagged/Tagged 勾选中。**口 12 是 NAS LACP 预留口,未来 LACP 维护窗需在此重新启用。**
14. 配管理面源限制:`系统管理 → 安全管理 → 安全配置`(管理主机/管理访问,以界面为准)→ 只允许源 `192.168.66.0/24` 访问 HTTPS/SSH(启用 SNMP 则一并限制)→ 其余拒绝 → 保存。
> ⚠️ **不要**在 `访问控制` 菜单配——那是数据面 ACL(MAC/IP ACL),管不到管理面;管理面限制在 `安全管理 → 安全配置`。
15. 配生成树:
- 全局配置(`生成树 → 基本配置`):功能`启用`、模式`RSTP`、**CIST优先级 `4096`**(让本机当根桥);联络时间 2 / 老化 20 / 时延 15 / 流量限制 5 / 最大跳数 20 保持默认。
- 端口配置(`生成树 → 端口配置`):**终端口 5、6、8、10 的「边缘端口」设`启用`**= PortFast);上联口 1、2、3、4、7 保持`禁用`。状态列显示`禁用`是链路未接/未启用,属正常。
- 端口保护(`生成树 → 安全配置 → 端口保护`):**终端口 5、6、8、10 的「BPDU保护」设`启用`**;上联口 1、2、3、4、7 保持`禁用`。环路保护/根桥保护/TC保护/BPDU过滤全部保持`禁用`。
- 保存。
16. **L3 明确边界检查:** `路由功能` 确认只有 VLAN66 有接口(`路由功能 → 接口 → 接口设置` 仅 VLAN66 有 IPv4);`路由功能 → 静态路由 → IPv4静态路由条目` 只有默认路由一条(0.0.0.0→.254);`路由功能 → DHCP服务器` 与 `DHCP中继` 未启用;`路由功能 → RIP` 未启用。
17. **保存 + 导出备份:** `配置保存 → 保存配置``系统管理 → 系统工具 → 配置导出` → 导出配置文件到管理本(离线保存,不进仓库)。
18. **断电重启验证:** 拔电源 10 秒 → 插回 → 重启完成后 `ping 192.168.66.253` 通、HTTPS/SSH 可登录 → 证明保存生效。
### 4.4 台面完成判定
- `show vlan` 结果与 §2 端口表一致;VLAN1 空;管理仅 LAN66 可达;配置已导出。
## 5. 建立上联(维护窗开始)
1. 找到 ER-X `eth0` 当前所连的旧设备/旧交换线,**先拔掉旧线**(避免 eth0 同时连两条路径形成环路)。
2. 新线一端插 SE5420 **口 2**,另一端插 ER-X `eth0`。
3. **验证:**
- 管理本 `ping 192.168.66.253` → 通(SE5420);
- `ping 192.168.66.254` → 通(ER-X);
- `ping 223.5.5.5` → 通(外网);
- SE5420 `show interface` 口 2:Link up、速率协商正常、错误计数 0。
4. 找到 ER-X `switch0` 某成员口(eth1–eth3 任一口)的旧接线,先拔。
5. 新线一端插 SE5420 **口 1**,另一端插该 switch0 成员口。
6. **验证:**
- 从 55 网段设备(或临时给管理本加 `.55.x` 地址)`ping 192.168.55.254` → 通;
- `ping 192.168.66.46`(控制器,跨网段)→ 通;
- SE5420 `show interface` 口 1Link up、错误计数 0。
## 6. 迁移 PVE
1. 拔 PVE `enp1s0` 旧线 → 插 SE5420 **口 4**。
2. **验证:**
```bash
ssh -4 root@192.168.66.26 'ip -br addr'
```
- 预期:`enp1s0`/`vmbr0` = `192.168.66.26/24`
- `ssh -4 root@192.168.66.26 'ping -c3 192.168.66.254'` → 通。
3. 拔 PVE `enp2s0` 旧线 → 插 SE5420 **口 3**。
4. **验证:**
- `ip -br addr``vmbr1` = `192.168.55.26/24`
- `ping -c3 192.168.55.254` → 通。
5. 逐台验证 VM(顺序:gfw → dns → ubnt → haos):
```bash
ssh -4 root@192.168.66.26 'qm list'
```
- gfw`ping -c3 192.168.66.1` → 通;
- dns`ping -c3 192.168.66.36` → 通;
- ubnt`ping -c3 192.168.66.46` → 通;
- haos`ping -c3 192.168.55.11` → 通(注意是 55 网段)。
6. 每个 VM 再验业务:gfw 的 OpenClash 面板/DNS 正常、dns 的 AdGuard UI 能开、ubnt 控制器 Connected、haos 界面能开。不以"宿主开机"代替。
## 7. 迁移 AP 与接入设备
1. U6 Lite:先拔旧线 → 插 SE5420 **口 6**(本地 PoE 注入器不动,网线只走数据)。
2. **验证:** U6 重连后拿回 `192.168.66.6`UniFi 控制台显示 U6 ConnectedInform 与 §1.2 记录一致。
3. UAP-AC-Lite:拔旧线 → 插 SE5420 **口 5**。
4. **验证:** `192.168.55.5`UniFi 显示 Connected。
5. 隔壁房间上联:拔旧线 → 插 SE5420 **口 7**。
- 插之前确认对端:若对端是小交换机,须满足——只有一条上联、只承载 untagged LAN55、无 VLAN trunk、无第二条路径回 ER-X/SE5420。任一项不满足则不迁移。
6. **验证:** 该网段设备拿到 `.55.x`、网关 `192.168.55.254`、DNS `192.168.66.36` 可达。
7. NAS:拔旧线 → 插 SE5420 **仅口 8**(口 12 保持断开)。
8. **验证:** NAS 拿到 `.66.x`(应为原静态 `.51` 或 reservation),`ping 192.168.66.36` 通,transmission jail 可访问。
9. 口 10 只接一台明确设备(windy-pc `.99` 或 hp-nas `.32`):拔旧线 → 插口 10。
10. **验证:** 该设备拿 `.66.x`、上网正常。
11. 确认没有并行二层路径后,旧交换/旧接线下电、收线。
## 8. 第一阶段验收(全绿才进第二阶段)
1. SE5420`ping 192.168.66.253` 通,且从 55 网段**不通**(依赖 §4.3 第 14 步管理面源限制已配;若 ACL 未生效,从 55 网段经 ER-X 路由仍可达 .253——此条不通过则回查 ACL);默认口令已改;配置已离线备份。
2. LAN66 客户端:`ip addr` 是 `.66.x`、路由 `default via 192.168.66.254`、DNS `192.168.66.36``ping 223.5.5.5` 通。
3. LAN55 客户端:`.55.x`、`default via 192.168.55.254`LAN55↔LAN66 互通(如 `ping 192.168.66.46`)。
4. 端口转发:从公网(手机流量)逐条验——hass `:8123`、transmission `:51413`、ssh `:5822→.36:22`、openvpn `:1194`。
5. UniFiU6、AC-Lite 均 ConnectedInform 与基线一致。
6. SE5420 `show interface`:所有在用口无 CRC/FCS/错误计数增长;ER-X 无广播风暴/ MAC 漂移日志;旧交换已断电。
7. gfw:主网默认网关仍 `.254``ip route` 里没有默认走 `.1`),OpenClash 行为与升级前一致。
> 可选:两台 2.5G 终端同 VLAN 跑 `iperf3 -s` / `iperf3 -c <对端>`,记结果。同 VLAN 才可能超 1G;跨 55/66 与 Internet 受 ER-X/宽带限制。
---
# 第二阶段:VLAN10 升级专用 SSID(第一步稳定 ≥1 天后再做)
> 放行门槛:§8 全绿、稳定 ≥1 天、每步先只读核实现状。SSID 最后才启用。任一步失败只回滚该步。
## 9. PVE VLAN10 边界
### 9.1 快照与备份(在 PVE 上执行:SSH `ssh -4 root@192.168.66.26` 进入或本地终端)
1. 取只读快照(逐条执行并保存输出):
```sh
cat /etc/network/interfaces
qm config 140
bridge vlan show
bridge link
ip -d link show vmbr0
pveversion -v
```
2. 备份(同 §1.1 目录):PVE `/etc/network/interfaces`、`/etc/pve/qemu-server/140.conf`gfw `/etc/config/{network,dhcp,firewall}`。
3. 确认本地 Console/带外路径可用;**无完整备份和回滚能力则停止。**
### 9.2 改造 vmbr0 为 VLAN-aware(以 §9.1 快照为准)
1. 编辑 `/etc/network/interfaces`PVE 本机 `nano /etc/network/interfaces`)。
2. 在 **vmbr0 定义段**加两行:
```text
bridge-vlan-aware yes
bridge-vids 10 66
```
> `bridge-vids` 只放行需要的 tagged VLAN`10`gfw VLAN10 帧)+ `66`(显式声明 native)。不要写 `2-4094` 全放行;将来加 VLAN 再扩展此列表。
> ⚠️ **`bridge-vids` 必须加在 vmbr0 段**,不是 `enp1s0` 段——enp1s0 只是 `bridge-ports` 成员;加错位置 VLAN10 无法上 bridge。
3. **不要**改动 `bridge-ports enp1s0`、现有 tap 口或 `vmbr1`;现有 VM 的无标签语义(untagged/PVID)保持不变。
4. 应用:**优先 `ifreload -a`**PVE 用 ifupdown2,只应用变更);不要用 `systemctl restart networking`(会重载全部接口)。
> ⚠️ `ifreload` 会断当前 SSH 连接(管理本重连即可),`ip -br addr` 重新验证。
5. **验证(关键):** `ssh -4 root@192.168.66.26 'ip -br addr'` 显示 `.66.26` 仍在;`ping 192.168.66.254` 通;dns(.36)/ubnt(.46)/gfw(.1) 仍通。任一断 → 立即恢复备份的 interfaces 文件并 `ifreload -a`。
6. `bridge vlan show` 确认现有 tap 口、物理口的 PVID/untagged 与迁移前一致,只是多出 tag 10 的允许。
### 9.3 为 gfw 加 tag=10 专用 vNIC
1. PVE 上给 VM140 加网卡(从管理本执行,bus 用下一个空闲值,记下 MAC):
```sh
ssh -4 root@192.168.66.26 'qm set 140 -net2 virtio,bridge=vmbr0,tag=10'
```
2. **验证:** `qm config 140` 出现 `net2 ... tag=10`。
3. gfw 内确认新接口名:
```sh
ssh -4 root@192.168.66.1 'ip -br link'
```
- 预期出现新接口(如 `eth2`),状态 DOWN 正常。
### 9.4 迁移 gfw 的 ubunt_upg 到新接口
1. gfw 先只读确认当前绑定与 DHCP 段名:
```sh
ssh -4 root@192.168.66.1 'uci show network; uci show dhcp'
```
预期:`network.ubunt_upg.device='eth0.10'`、`dhcp.ubunt_upg.interface='ubunt_upg'`(按接口名绑定)。
2. **最小迁移——只改 device 一行**(gfw 上执行):
```sh
ifdown ubunt_upg
uci set network.ubunt_upg.device='eth2'
uci commit network
ifup ubunt_upg
```
> `eth0.10` 不再被引用会自动消失,**无需手动删段**;DHCP 池 `dhcp.ubunt_upg.interface='ubunt_upg'` 按接口名绑定,自动跟着走。
3. **验证:** gfw 内 `ip -br addr` → `eth2` 拿 `192.168.10.1/24` UP、`eth0.10` 消失、`eth0` 仍 `.66.1`。
4. 确认 firewall `ubunt_upg` zone 按 `network='ubunt_upg'` 绑定(改 device 后 zone/DHCP/NAT/规则自动跟随)。
5. **验证:**
- PVE 管理 IP 仍在 LAN66
- `bridge vlan show` 确认其他 VM 无 VLAN10 可达性(`tap140i2` 显示 `10 PVID Egress Untagged`dns/ubnt 等 tap 仍只有 PVID 1)。
6. 未通过 → 按 §16 回滚顺序停止 VLAN10 项目。
## 10. SE5420 增加 VLAN10
> **关键:口 4/6 是 access 口时,VLAN10 的 Tagged 选择器里会显示不可选**——必须先在本步第 1 步把口 4/6 改成 trunk,才能勾 Tagged。改 trunk 后口 4/6 会从 VLAN66 untagged 成员中被移除,需在第 3 步恢复。
1. 改口 4/6 为 trunk`VLAN → 802.1Q VLAN → 端口配置` → 口 4、口 6 → 端口类型 `ACCESS`→`TRUNK` → **PVID 改 `66`**(改 TRUNK 后默认 PVID 变 1,必须改回 66)→ 保存。
2. **验证:** 端口配置页口 4/6 显示 `TRUNK` + PVID `66`。
3. **恢复 VLAN66 成员:** 改 trunk 后口 4/6 从 VLAN66 untagged 被移除 → `VLAN → 802.1Q VLAN → VLAN配置` → 编辑 VLAN66 → **Untagged 重新勾选口 4、口 6** → 保存。确认 VLAN66 = 口 2、4、6、8、10。
4. 建 VLAN10`VLAN → 802.1Q VLAN → VLAN配置` → 新建 VLAN ID `10` → **Tagged 勾选:口 4、口 6**Untagged 不勾)→ 保存。口 4/6 即 trunkuntagged 66 + tagged 10。
5. 确认其余口不允许 VLAN10(口 4/6 之外不勾选)。
6. 确认 VLAN10 不建 SVI/DHCP/路由:`路由功能 → 接口 → 接口设置` 无 VLAN10 接口;`路由功能 → DHCP服务器/DHCP中继` 无 10 段。
7. **保存配置:** `配置保存 → 保存配置`。
8. **验证(静态链路):**
- SE5420 `show vlan`VLAN10 仅在口 4/6VLAN66 仍是口 2、4、6、8、10。
- PVE `bridge vlan show``enp1s0` 允许 10 + 66`tap140i2` 显示 `10 PVID Egress Untagged`。
- `ping 192.168.66.253`、`ssh` 均通(VLAN66 管理流量不受影响)。
- > 注:此时抓不到 VLAN10 帧是正常的——还没有客户端(SSID 未启用);真正流量验证在 §12 启用 SSID 后。
## 11. gfw 防火墙、NAT 与 IPv6
### 11.1 先只读确认现场
```sh
ssh -4 root@192.168.66.1 'uci show network; uci show firewall; uci show dhcp; ip -4 route; ip -6 addr; ip -6 route; fw4 print; nft list ruleset'
```
- 记录 `wan` zone 的 masq、`ubunt_upg` zone 段名、`ubunt_upg_nat`、`ubunt_upg_to_lan`、DHCP 段名。以下命令全部用具名段,段名以本次输出为准。
### 11.1b 现场已核实规则(2026-08 实施时确认存在,无需重建)
| 段名 | 类型 | 作用 |
|---|---|---|
| `firewall.ubunt_upg` | zone | `network='ubunt_upg'`input/forward=`REJECT`、output=`ACCEPT` |
| `ubunt_upg_dhcp` | rule | VLAN10 → UDP 67 允许(DHCP |
| `ubunt_upg_dns` | rule | VLAN10 → TCP/UDP 53 允许(DNS |
| `ubunt_upg_block_lan66` | rule | VLAN10 → `192.168.66.0/24` REJECT |
| `ubunt_upg_to_lan` | forwarding | `src=ubunt_upg → dest=lan` |
| `ubunt_upg_nat` | nat | `src=lan`、`src_ip=192.168.10.0/24`、target `MASQUERADE`(出口走 lan 即 eth0/LAN66 |
> **待补缺口:** 只有 `192.168.66.0/24` 被拒,**LAN55 和其余 RFC1918 无显式拒绝**——VLAN10 经 ER-X 仍可达 `192.168.55.0/24`haos/IoT/AP)等内网,必须在 §11.3 补齐。
### 11.2 IPv4 策略(目标)
| 流量 | 动作 |
|---|---|
| VLAN10 → gfw DHCP、DNS | 允许 |
| VLAN10 → gfw SSH/LuCI/SNMP/其他管理服务 | 拒绝 |
| VLAN10 → LAN66、LAN55、其他 RFC1918 内网 | 拒绝 |
| VLAN10 → Internet | 允许 |
| established/related 回包 | 允许 |
### 11.3 执行(段名替换为实际值)
1. 确认 `ubunt_upg` zone 的 `forward` 保持 `REJECT`(不改 ACCEPT)。
2. **NAT 已存在,勿动:** 现场已有 `wan` zone `masq=1` + `ubunt_upg_nat``src=lan`、`src_ip=192.168.10.0/24` masquerade)。**不要**给 `ubunt_upg` zone 加 masq,也不要新增 NAT 规则。核对 `nft list ruleset` 该规则在列。
3. 不新增宽泛的 `ubunt_upg→wan` zone forwarding(会经 ER-X 放行 LAN55/其它内网)。
4. **补内网前缀拒绝**LAN55、10/8、172.16/12`192.168.0.0/16` 已被 66/55/10 三段覆盖,无需再加)。⚠️ **每条必须写 `dest='lan'`**——否则 fw4 会把规则放进 `input_ubunt_upg`(进 gfw 本机方向)而非 `forward_ubunt_upg`VLAN10→其他网段转发),`dest_ip` 永远不命中、等于没配:
```sh
# LAN55
uci add firewall rule
uci set firewall.@rule[-1].name='v10_deny_lan55'
uci set firewall.@rule[-1].src='ubunt_upg'
uci set firewall.@rule[-1].dest='lan'
uci set firewall.@rule[-1].dest_ip='192.168.55.0/24'
uci set firewall.@rule[-1].target='REJECT'
uci set firewall.@rule[-1].proto='all'
uci set firewall.@rule[-1].family='ipv4'
# 10/8
uci add firewall rule
uci set firewall.@rule[-1].name='v10_deny_10net'
uci set firewall.@rule[-1].src='ubunt_upg'
uci set firewall.@rule[-1].dest='lan'
uci set firewall.@rule[-1].dest_ip='10.0.0.0/8'
uci set firewall.@rule[-1].target='REJECT'
uci set firewall.@rule[-1].proto='all'
uci set firewall.@rule[-1].family='ipv4'
# 172.16/12
uci add firewall rule
uci set firewall.@rule[-1].name='v10_deny_172net'
uci set firewall.@rule[-1].src='ubunt_upg'
uci set firewall.@rule[-1].dest='lan'
uci set firewall.@rule[-1].dest_ip='172.16.0.0/12'
uci set firewall.@rule[-1].target='REJECT'
uci set firewall.@rule[-1].proto='all'
uci set firewall.@rule[-1].family='ipv4'
uci commit firewall
fw4 reload
```
5. **验证(看 nft 而非 UCI):** `nft list ruleset | grep -A6 "chain forward_ubunt_upg"` 应显示拒绝规则**全部在 `forward_ubunt_upg` 链**且先于 `accept_to_lan``wan` masq 仍在。若拒绝规则出现在 `input_ubunt_upg` 链 → 说明漏了 `dest='lan'`,回去补。
6. **IPv6 核验:** `ip -6 addr show eth2` 只有 `fe80::``dhcp.ubunt_upg` 的 `ra='disabled'`、`dhcpv6='disabled'` → VLAN10 无 GUA/ULA、无 IPv6 DNS/路由/出口。
### 11.4 IPv6:本阶段不提供 VLAN10 IPv6
1. 显式关闭并核验(不能靠"没配"):RA、DHCPv6、NDP relay/delegation、IPv6 forwarding 对 `ubunt_upg` 相关接口、前缀委派。
2. 现场已知:DHCPv6/RA disabled、无 IPv6 默认路由,但全局 `IPv6 forwarding=1`——启用 VLAN10 前复查 `ip -6 addr`、`sysctl net.ipv6.conf.all.forwarding`。
3. 客户端可留 link-local;不得获得 GUA/ULA、IPv6 DNS 或 IPv6 默认路由。
### 11.5 部署后验证
1. **链归属(已实测):** `nft list ruleset | grep -A6 "chain forward_ubunt_upg"` 确认 66/55/10/172 四条拒绝在 forward 链、先于 `accept_to_lan``192.168.10.0/24 masquerade` 仍在(§11.3 第 5 步已验)。
2. **IPv6(已实测):** `ip -6 addr show eth2` 仅 `fe80::``dhcp.ubunt_upg.ra='disabled'` + `dhcpv6='disabled'`odhcpd 不服务 eth2(§11.4 已验)。
3. **DHCP 租约(已实测):** `cat /tmp/dhcp.leases` 出现 VLAN10 客户端(实测 OnePlus-12 → `192.168.10.168`)。
4. **数据面(已实测):** `nft list ruleset` 中 `192.168.10.0/24 masquerade` counter 增长(实测 215 pkts / 42KB)、`accept_to_ubunt_upg` 回包增长、四条 deny 保持 0 → VLAN10 上网走 gfw NAT、隔离生效。
5. **拒绝规则命中(SSID 启用后验):** 从测试客户端 `ping 192.168.66.1/.26/.36/.46/.254`、`192.168.55.254`、LAN55 主机 → 全不通;访问 gfw 的 22/80/443 管理口 → 拒绝。
6. **客户端上网(SSID 启用后验):** 拿 `192.168.10.x`、网关 `.10.1`、DNS 正常;`curl -4` 国内/海外均通、`curl -6` 失败。
7. 抓包工具:`apk add tcpdump`ImmortalWrt 25.12.0;勿用 opkg)。
## 12. UniFi 改网络 + 启用 SSID(最后做)
1. 在 AP 上核实 InformSSH 进 U6`ssh -4 zhiqiangf@192.168.66.6`)→ `info` → 记录 `inform_url`。不凭文档假定。
2. UniFi → Networks → 编辑 `ubnt-upg`
- Router/网关设置 → 选 **Third-party Gateway**
- VLAN ID`10`
- 子网:`192.168.10.0/24`;网关:`192.168.10.1`
- DHCP:关闭(dhcp 由 gfw 提供)。
3. **验证:** 保存后网络列表 `ubnt-upg` 显示 Third-party Gateway、VLAN 10。
4. UniFi → WiFi → `ubnt-upg` SSID → 分配设备只勾 **U6 Lite**(确保 UAP-AC-Lite 不被勾选)→ 保存。
5. 启用 `ubnt-upg` SSID(最后一步才开)。
6. 单台测试客户端连上后逐项验证:
- `ip addr``192.168.10.100~250``ip route` 默认网关 `192.168.10.1`DNS 为预期 gfw 地址;
- `nslookup` 普通/国内/海外域名均按预期解析(DNS 走 gfw/OpenClash 路径,不能直连 LAN66 `.36` DNS,除非书面允许);
- gfw 出现该客户端 DHCP 租约与 VLAN10 帧;
- `curl -4 https://www.baidu.com` 与 `curl -4 https://www.google.com` 均成功;`curl -6` 失败;
- `ping 192.168.66.1/.26/.36/.46/.254`、`192.168.55.254`、LAN55 主机 → 全不通;gfw 管理口不通;
- 代理路径:在 gfw/OpenClash 日志或按客户端源地址/五元组确认流量走代理,**不以全局 `7892` 计数增长为证明**。
7. 任一验证失败 → 先关 `ubnt-upg` SSID;主 SSID、`.254` 路径、LAN55/LAN66 不受影响。
> **实测记录(2026-08-11):** OnePlus-12 连 `ubnt-upg` 拿到 `192.168.10.168`DHCP 由 gfw 下发,租约见 `/tmp/dhcp.leases`);gfw `192.168.10.0/24 masquerade` counter 增长(215 pkts / 42KB)、`accept_to_ubunt_upg` 回包 5 pkts、四条内网 deny 保持 0 → 上网走 gfw NAT、VLAN10→内网隔离生效。手机 Wi-Fi 详情不显示网关,以 DHCP 租约 + NAT counter 为证。
## 13. 第二阶段验收
1. `ubnt-upg` 客户端满足 §12 第 6 步全部项。
2. gfw 侧 DHCP、拒绝规则、NAT、DNS、代理路径均关联到该测试客户端。
3. LAN55/LAN66 的 IPv6 行为与迁移前基线一致;VLAN10 无 GUA/ULA、IPv6 DNS、默认路由或 IPv6 出口(除 link-local)。
> **状态(2026-08-11):** 第 2 条 DHCP+NAT+隔离已实测通过(见 §12 实测记录);第 1、3 条待补(浏览器 curl 国内/海外、拒绝规则从客户端实测命中、IPv6 `curl -6` 失败)。
## 14. 性能实测(可选但建议)
1. 两台 2.5G 终端(如 NAS 与 windy-pc,均接 SE5420):一端 `iperf3 -s`,另一端 `iperf3 -c <对端IP>`。
2. 记录:单流、4 流结果;预期单流约 2.3–2.5Gbps(受 NIC/线材限制)。
3. 跨网段(66↔55)与 Internet 结果如实记录——预期被 ER-X 上联/宽带限制,不要按 2.5G 承诺。
## 15. 记录
- 每次实质变更后在 Linear `vps` 项目记录 scope / action / verification / 遗留 follow-up。
- 本仓库不记录 SE5420 口令、ER-X 配置快照(含 PPPoE/口令)、gfw 凭据。
- 实施前先读 `se5420-review-claim-verification-2026-08.md` 的现场只读复核结论。
## 16. 回滚
| 失败位置 | 回滚动作 |
|---|---|
| §4 台面 | 还原台面配置或恢复出厂(见下) |
| §5–7 上联/迁移 | 停手 → 刚迁的线逐根插回 ER-X 原口 → 逐根验证(IP/服务恢复、AP Connected、MAC 表回旧拓扑) |
| §9 PVE 边界 | 关 `ubnt-upg` SSID → gfw `ubunt_upg` 恢复绑 `eth0.10` → 恢复 gfw network/DHCP/firewall → 移除 VM140 新增 vNIC`qm set 140 -delete net2`)→ 恢复 VM140 配置 → **最后**恢复 PVE `/etc/network/interfaces` 并 `ifreload -a` → 分别验证 PVE/dns/ubnt/gfw 的 LAN66 |
| §10 SE5420 VLAN10 | 删 VLAN10`VLAN → 802.1Q VLAN → VLAN配置` 删除 VLAN10),口 4/6 恢复 Untagged 仅 VLAN66、Tagged 清空 |
| §11 gfw | 恢复已备份命名规则/NAT;不触碰 LAN55/LAN66 主网规则 |
| §12 SSID | 只关 `ubnt-upg` SSID;主 SSID、`.254`、gfw 66 路径不受影响 |
**SE5420 恢复出厂(忘口令/重配):**
- **WEB 方式:** `系统管理 → 系统工具 → 软件复位`(若还能登录 GUI)。
- **Console 方式(WEB 不可用/忘口令):**
1. 管理本连 Type-C Console38400 8N1)。
2. 交换机断电重启;串口出现 **`Press CTRL-B to enter the bootUtil`** 时按 `Ctrl+B`。
3. 输入 **`2`**(软件复位)→ 恢复出厂;此后首次 WEB 登录创建新用户名密码(初始管理 IP 恢复 `10.18.18.251`)。
4. 恢复后按 §4 重配并保存。
## 参考
- 设计说明:[lan-erx-se5420-network.md](lan-erx-se5420-network.md)
- 评审核实:[se5420-review-claim-verification-2026-08.md](se5420-review-claim-verification-2026-08.md)
- 现网地图:[lan-overview.md](lan-overview.md)
- 官方安装手册(Markdown 版):[se5420-official-manuals/tl-se5420-install-manual.md](se5420-official-manuals/tl-se5420-install-manual.md)
- 官方 PDF<https://service.tp-link.com.cn/download/202310/TL-SE5420%20V1.0安装手册%201.0.2.pdf>
- 规格 / 固件:<https://www.tp-link.com.cn/product_2899.html?v=specification> · <https://www.tp-link.com.cn/product_2899.html?v=download>
- Omada VLAN 指南:<https://support.omadanetworks.com/en/document/12981/> · <https://support.omadanetworks.com/en/document/13135/>
- ER-X[edgerouter-x-configuration.md](edgerouter-x-configuration.md)gfw[hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md)PVE VLAN10[lan-rb5009-upgrade.md#阶段-5-附pve-上-vlan10-透传实现-调研-2026-08-09](lan-rb5009-upgrade.md#阶段-5-附pve-上-vlan10-透传实现-调研-2026-08-09)
@@ -0,0 +1,262 @@
# TL-SE5420 官方安装手册(整理版)
> **来源:** TP-LINK《TL-SE5420 V1.0 安装手册 1.0.2》(PDF),已整理为 Markdown。
> 官方 PDF<https://service.tp-link.com.cn/download/202310/TL-SE5420%20V1.0安装手册%201.0.2.pdf>
> 配套实施文档:[../lan-se5420-deployment-guide.md](../lan-se5420-deployment-guide.md)
> 说明:PDF 中示意图已省略(仅保留文字);正文以官方 PDF 为准。
---
## 声明
- Copyright © 2023 普联技术有限公司,版权所有,保留所有权利。
- 产品规格和资讯仅供参考,如有内容更新,恕不另行通知。本手册仅作为使用指导,所作陈述均不构成任何形式的担保。
## 相关文档
| 文档名称 | 用途 |
|---|---|
| 《安装手册》 | 交换机的安装过程、启动以及常见故障处理 |
| 《用户手册》 | 交换机的各个功能应用以及常见功能使用问题处理 |
| 《命令行手册》 | 交换机 CLICommand Line Interface,命令行界面)参考信息 |
| 《防雷安装手册》 | 安装中如何做好防雷措施以避免设备雷击损坏 |
技术支持:服务热线 `400-8863-400` · 邮箱 `fae@tp-link.com.cn` · 微信公众号「TP-LINK商用网络」/「TP-LINK商云APP」
## 阅读对象与约定
- 适合阅读对象:网络工程师、网络管理员。
- 本手册如无特别说明,均以 **TL-SE5420** 机型为例介绍,所指交换机为 2.5G 三层网管交换机。
- 产品规格标贴位于设备底部,扫描二维码可查看安规符号释义。
---
## 第 1 章 产品介绍
### 1.1 产品简介
2.5G 三层网管交换机,支持静态路由和动态路由 RIP 协议,提供完备的安全防护机制、完善的 QoS 策略和丰富实用的 VLAN 功能。适用于中小型企业网、校园网络、酒店、网吧等使用场景。
### 1.2 产品外观
**前面板组成:** 1 模式切换按键 · 2 指示灯 · 3 1 个 Type-C Console 端口 · 4 16 个 2.5Gbps RJ45 端口 · 5 4 个 SFP+ 端口 · 6 1 个 USB 端口
**模式切换按键:** 用于切换指示灯的指示状态,支持「端口模式」和「堆叠模式」切换。任意模式下超过 45s 不按动按钮,自动恢复为端口模式。(模式切换功能软件后续升级支持,以实际软件功能为准。)
**指示灯状态:**
| 指示灯 | 名称 | 工作状态 | 工作说明 |
|---|---|---|---|
| PWR | 电源指示灯 | 常亮 / 熄灭 / 闪烁 | 供电正常 / 未通电或供电异常 / 供电异常 |
| SYS | 系统指示灯 | 绿色闪烁 / 红色闪烁 | 系统正常 / 系统异常 |
| Master | 堆叠指示灯 | 绿色常亮 / 黄色常亮 / 熄灭 | 堆叠主设备 / 堆叠错误 / 非主设备 |
| Stack | 模式指示灯 | 绿色常亮 / 熄灭 | 端口指示灯工作在堆叠模式 / 其它模式 |
| 1-16(端口模式) | 端口指示灯 | 绿色常亮 / 黄色常亮 / 闪烁 / 熄灭 | 2.5Gbps / 10/100/1000Mbps / 传输中 / 未连接 |
| 17-20(端口模式) | 端口指示灯 | 绿色常亮 / 黄色常亮 / 闪烁 / 熄灭 | 10Gbps / 2.5Gbps 或 1000Mbps / 传输中 / 未连接 |
| USB | 端口指示灯 | 绿色常亮 / 熄灭 | USB 连接正常 / 未连接或异常 |
**Console 端口:** 用于和计算机或其他终端的串口相连以管理或配置交换机。TL-SE5420 提供 **1 个 Type-C Console 端口**
**2.5Gbps RJ45 端口:** 16 个,支持 10M/100M/1000M/2.5Gbps 速率,每个端口对应一个端口指示灯。
**10Gbps SFP+ 端口:** 支持接入 1Gbps/2.5Gbps SFP 光/电模块或 10Gbps SFP+ 光/电模块,每个端口对应 1 个 Link/Act 指示灯。
**USB 端口:** 标准 USB2.0480Mbps。可和交换机 Flash 文件系统交互(上传/下载应用程序、配置文件)。支持**交换机 U 盘开局**(详见《用户手册》)。
> 注意:不同厂商 USB 设备的兼容性和驱动存在差异,TP-LINK 不保证所有 USB 设备都能正常使用;如不能使用不属于交换机故障。
**后面板组成:** 1 防雷接线柱 · 2 电源插座(三相,220V~50Hz 交流电源)。
> 注意:请使用原装电源线;请通过电源线插头连接到有接地连接的输出插座;请远离水、火、潮湿或炎热环境;不要自行拆卸、修理、修改设备或更换电池(有爆炸危险)。
---
## 第 2 章 产品安装
### 2.1 物品清单
- 交换机 ×1 · 电源线 ×1 · 机架 L 型支架 ×2 · 安装手册 ×1 · 接地线 ×1 · 其他配件
### 2.2 安装注意事项
**安装安全:**
- 安装过程中电源保持关闭,佩戴防静电手环并保证接地良好;
- 确认供电电压与交换机标示电压相符;
- 通电前确认不会引起电源电路超负荷;
- 工作时不要打开外壳;
- 清洁前先拔电源插头,勿用湿润面料擦拭或液体清洗;
- 保持交换机底部朝下水平放置。
**环境要求:**
| 环境描述 | 温度 | 相对湿度 |
|---|---|---|
| 工作环境 | 0℃ ~ 40℃ | 10% ~ 90%RH 无凝结 |
| 存储环境 | -40℃ ~ 70℃ | 5% ~ 90%RH 无凝结 |
- 海拔:仅适用于 2000m 以下地区。
- 室内防尘、防电磁干扰、防雷:确认设备与大地接地良好;远离高频大功率设备;室外布线建议使用信号防雷器。
- 安装台:机架或工作台需承重至少 **5.5Kg**;供电插座与交换机距离不超过 1.5 米。
### 2.3 安装工具准备
十字螺丝刀 · 防静电手环 · 网线、光纤
### 2.4 产品安装
**桌面安装:** 底部朝上放置于稳定桌面 → 粘贴 4 个脚垫到机壳底部四角凹槽 → 翻转放平。机壳周围预留 5~10cm 空隙确保散热。
**19 英寸机架安装:** 检查机架接地与稳定性 → 两侧安装 L 型角铁并固定 → 安放到机架内由托架支撑 → 螺钉固定 L 型角铁到机架导槽。机架内一般由下至上安装。
---
## 第 3 章 硬件连接
### 3.1 连接 RJ45 端口
通过 RJ45 端口连接交换机和对端设备。
### 3.2 连接 SFP+ 端口
连接 SFP/SFP+ 光/电模块或 SFP+ 电缆。**SFP+ 端口默认支持 10Gbps;若使用千兆 SFP 光模块,需先将 SFP+ 端口速率设置为 1000Mbps。** 使用 SFP+ 电缆时确保最小弯曲半径符合要求。
### 3.3 连接 Console 端口
Type-C Console 端口,连接后可通过命令行管理交换机(登录方法见 4.2)。
### 3.4 连接电源线
接入 220V~50Hz 交流电源;确认供电电源与大地良好接触,确认电源开关位置以便及时断电。
### 3.5 设备初始化
接通电源后自动初始化:
- PWR 指示灯亮 = 电源连接正确,灭 = 供电异常;
- SYS 指示灯闪烁 = 系统初始化成功,灭或常亮 = 初始化失败;
- Link/Act 指示灯会闪烁一下后熄灭,指示系统已复位。
### 3.6 安装后检查
- 周围散热空间与空气流通;
- 电源插座供电符合规格;
- 电源、交换机、机架正确接地;
- 与其它网络设备连接正常。
---
## 第 4 章 配置指南
### 4.1 WEB 登录
> **TL-SE5420 业务口固定 IP 为 `10.18.18.251`**;电脑连接业务口时需设 IP `10.18.18.x`x 为 2-250 任意值),子网掩码 `255.255.255.0`。
1. 普通 PC 用网线连接交换机任意一个 RJ45 业务端口;
2. 浏览器输入 `http://10.18.18.251` 回车;
3. **设置用户名和密码**,点击<确认>登录 WEB 管理界面;
4. 首页左上角商标可访问 TP-LINK 官网;左侧主菜单进行功能配置。
### 4.2 本地登录(Console
1. 连接 Type-C Console 口到计算机;
2. 安装「**TP-LINK USB 串口驱动程序**」(官网下载或随机光盘);
3. 终端仿真程序参数:
- 波特率 **38400bps** · 数据位 **8** · 奇偶校验 **无** · 停止位 **1** · 数据流控制 **无**
4. 输入回车,出现 **`TL-SE5420>`** 提示符即登录成功。
### 4.3 远程登录(Telnet
1. cmd 输入 `telnet 10.18.18.251`
2. 输入登录用户名和密码;
3. 输入 `enable` 回车进入特权模式。
### 4.4 云管理
1. Web 界面 →<云管理> → 开启<全局开关>
2. 访问 `https://smbcloud.tp-link.com.cn/login.html`,登录 TP-LINK ID
3. 项目集中管理 → 设备列表 → 添加设备(输入 MAC、名称、用户名、密码、分组;MAC 可扫设备底部二维码获取);
4. 设备信息页找到对应条目 →<远程配置> → 云平台远程管理。
---
## 附录 A 常见故障处理
**问题1:忘记用户名和密码(恢复出厂设置)**
1. 串口连 Console 口,打开终端;
2. 交换机断电重启,终端出现 **`Press CTRL-B to enter the bootUtil`** 时按 `Ctrl+B` 进入 bootUtil 菜单;
3. 输入 **`2`** 进行软件复位,恢复到出厂默认设置,登录用户名和密码均为 **admin**
**问题2:电源指示灯显示不正常**
- 电源线连接是否正确;供电电源是否匹配(100-240V~ 50/60Hz)。
**问题3:无法登录 WEB 管理界面**
- 是否达到 HTTP 管理人数上限;
- 端口线缆连接与端口状态;
- 电脑 IP 与交换机 IP 是否同网段;
- `ping 127.0.0.1`(本机 TCP/IP)与 `ping 10.18.18.251`(交换机)。
**问题4:断电后配置消失**
- 配置实时生效,但**断电前必须点保存配置**。
---
## 附录 B 技术参数规格
| 项 | 值 |
|---|---|
| 产品型号 | TL-SE5420 |
| 技术标准 | IEEE 802.3 / 802.3i / 802.3u / 802.3ab / 802.3z / 802.3bz(2.5G/5G) / 802.3ae(10G) / 802.3ad(链路聚合) / 802.3x(流控) / 802.1p(QoS) / 802.1q(VLAN) / 802.1x / 802.1d(STP) / 802.1s(MSTP) / 802.1w(RSTP) / 802.1ab(LLDP) |
| 端口 | 16× 10/100/1000/2.5G RJ45 + 4× 10G SFP+ |
| 传输方式 | 存储转发 |
| 数据传输速率 | 10M半双工/20M全双工 · 100M半/200M全 · 千兆2000M全 · 2.5G 5000M全 · 10G 20000M全 |
| 包转发速率 | 10BASE-T:14881pps · 100BASE-TX:148810pps · 1000BASE:1488095pps · 2.5GBASE-T:3720238pps · 10GBASE:14880952pps(每端口) |
| 电源输入 | 220V~50Hz |
| 工作温度 | 0℃~40℃ |
| 存储温度 | -40℃~70℃ |
| 工作湿度 | 10%~90%RH 无凝结 |
| 存储湿度 | 5%~90%RH 无凝结 |
网络介质(部分):10BASE-T 3类线≤100m · 100BASE-TX 5类线≤100m · 1000BASE-T 超5类≤100m · 2.5GBASE-T 超5类≤100m · 10GBASE-SR OM1/OM2/OM3 MMF 2m~300m · 10GBASE-LR SMF 2m~10000m。
---
## 附录 C 连接 SFP+ 端口补充说明
- **光纤**:单模(芯径 9/10μm,模间色散小,适合远程);多模(芯径 50/62.5μm,传输距离较近,一般几公里)。
- **光模块**:按性质分单模/多模;按速率分百兆/千兆/万兆。SFP/SFP+ 端口兼容单模、多模光模块,推荐使用 TP-LINK 光模块。
**TP-LINK 光模块参考表:**
| 模块类型 | 产品型号 | 速率 | 距离 | 传输介质 |
|---|---|---|---|---|
| 光模块 | TL-SM512LS-10KM | 10Gbps | 10km | SMF 双纤 LC |
| 光模块 | TL-SM512LM-300m | 10Gbps | 300m | OM3 MMF 双纤 LC |
| 光模块 | TL-SM411LSA/B-5KM | 2.5Gbps | 5km | SMF 单纤 LC |
| 光模块 | TL-SM411LSA/B-500m | 2.5Gbps | 500m | SMF 单纤 LC |
| 光模块 | TL-SM411SSA/B-500m | 2.5Gbps | 500m | SMF 单纤 SC |
| 光模块 | TL-SM412LS-5KM | 2.5Gbps | 5km | SMF 双纤 LC |
| 光模块 | TL-SM311LSA/B-20KM | 1Gbps | 20km | SMF 单纤 LC |
| 光模块 | TL-SM311LSA/B-2KM | 1Gbps | 2km | SMF 单纤 LC |
| 光模块 | TL-SM311SSA/B-2KM | 1Gbps | 2km | SMF 单纤 SC |
| 光模块 | TL-SM312LS-40KM | 1Gbps | 40km | SMF 双纤 LC |
| 光模块 | TL-SM312LS-20KM | 1Gbps | 20km | SMF 双纤 LC |
| 光模块 | TL-SM312LS-20KM工业级 | 1Gbps | 20km | SMF 双纤 LC |
| 光模块 | TL-SM312LM-550m | 1Gbps | 550m | OM2 MMF 双纤 LC |
| 电口模块 | TL-SM510U | 10Gbps | 30m | 超六类网线 |
| 电口模块 | TL-SM410U | 2.5Gbps | 100m | 超五类网线 |
| 电口模块 | TL-SM310U | 1Gbps | 100m | 超五类网线 |
---
## 附录 D 产品保修卡(要点)
- 免费保修范围外的情况:超出保换/保修期限、未按要求安装使用、擅自改动设置或拆机、意外或人为损坏、运输损坏、不可抗力等。
- 技术支持热线:`400-8863-400` · 邮箱 `fae@tp-link.com.cn` · 官网 `www.tp-link.com.cn`
- **售后服务承诺:三年保修。**
- 外置电源保换 1 年,电池 6 个月,鼠标 3 个月;无线外置天线及光纤头 3 个月。
- 购机 15 天内出现性能问题且外观无划伤可换新。
---
## 附录 E 有毒有害物质含量声明
(详见官方 PDF 附表。)
---
公司地址:深圳市南山区深南路科技园工业厂房 24 栋南段 1 层、3-5 层、28 栋北段 1-4 层
公司网址:<http://www.tp-link.com.cn> · 技术支持 E-mail`smb@tp-link.com.cn` · 技术支持热线:`400-8863-400`
文档版本:REV1.0.2
@@ -0,0 +1,158 @@
# 希力威视 SR-S25G3218F 调查(2026-08-08
**结论:** 若需求是大量 2.5G 终端、少量 10G 光上联,`SR-S25G3218F` 的端口密度
更合适;厂商已公开该型号的固件页,但仍缺少完整规格书、管理手册与兼容矩阵。若需求是 8 条全部可协商
1/2.5/5/10G 的铜缆链路,且希望有可查的 L3 能力和固件入口,兮克
`SKS8300-8T` 是资料更完整、风险更低的选择;它的代价是主动风扇、外置 12 V 电源、
无 SFP+ 光口,且仍不应把消费级/SMB 设备当作安全边界或唯一核心。两者都应在
到货可退换期内完成实机验收。
本页为采购前资料调查,不代表已接入本地网络;检索日期为 2026-08-08。
## 已能核实的事项
| 项目 | 结论与证据强度 |
|---|---|
| 型号/端口 | 京东的希力威视商品标题称该 SKU 为 `SR-S25G3218F`,有 16 个 2.5G 电口和 2 个万兆光口,并宣传 VLAN、端口隔离与 LACP。该店铺被厂商官网列为可购买的「京东旗舰店」,因此可作为销售规格,非技术手册。[京东商品页](https://item.jd.com/100165071727.html)[厂商购买渠道说明](https://en.sirivision.com/contactus/) |
| 厂商身份 | 厂商官网为 Shenzhen/Guangdong Sirivision Communication;英文官网说明其自 2016 年起提供接入、汇聚和核心交换机方案。[厂商首页](https://en.sirivision.com/) |
| 公开的二手厂家资料 | 同一制造商名义的 Alibaba 出口页将精确型号写成 `16*2.5G+2*10G``120Gbps`,并列出 QoS、VLAN、SNMP、L3 与 stackable。这是制造商发布在平台上的销售资料,**不是**官网数据表;其中后五项不能据此视为已验收的功能承诺。[制造商平台页](https://www.alibaba.com/pla/SR-S25G3218F-QoS-Managed-SFP-Switch-1625G210G_1601494946214.html) |
| 固件入口 | 厂商已发布此精确型号的[固件页](https://www.sirivision.com/sr-s25g3218f%E5%9B%BA%E4%BB%B6/)。公开变更记录提到“光口自适应”和“增加 DAC 配置”;这证明厂商维护过该路径,**不**代表任意 SFP+/DAC/铜模块均兼容。 |
| 本机可计算的带宽 | 端口线速相加为单向 60 Gb/s16 × 2.5 + 2 × 10);若厂商所谓 `120Gbps` 是全双工交换容量,则数学上吻合。它**不**证明缓冲、PPS、表项规模或实际无阻塞性能。 |
## 网管/L2/L3 能力边界
京东标题足以支持把 VLAN、端口隔离、LACP 作为「卖家声称提供」的功能;不得由此推导出
ACL、IPv4/IPv6 静态路由、SVI 数量、DHCP relay、OSPF/RIP、VRRP、IGMP、ERPS、
802.1X、RADIUS/TACACS+、SSH/HTTPS 管理、SNMP 版本、日志/审计、配置备份或固件
安全维护一定存在。
尤其要注意:厂商官网把真正列出的 2.5G L3 产品标为
`SR-S25G3412F (8 × 2.5G + 4 × 10G SFP+)`;其 2.5G 类目只显示 7 个型号,
不含 `SR-S25G3218F`。官网也把 L2+、Web Smart、L3 分成不同产品类别。这个目录
差异**不是**证明 3218F 没有 L3,而是说明「三层」无法通过官网的精确型号文档确认。
[2.5G 产品目录](https://en.sirivision.com/product-category/products/2-5g-switches/)
[官网的 10G L3 目录](https://en.sirivision.com/product-category/products/10g-switches/10g-layer3-managed-switches/)
[官网的 L2+ 分类示例](https://en.sirivision.com/product-category/products/gigabit-switches/gigabit-layer2-managed-switches/)。
采购前请向京东/厂商索取**与机身 SKU、硬件 revision 和固件版本对应**的 PDF
数据表、管理手册和 release notes,并要求书面回答至少以下问题:
1. L3 是只有 VLAN Interface/IPv4 静态路由,还是另有 IPv6、ACL、动态路由、DHCP relay
等;每项的最大 VLAN、MAC、ARP、路由、ACL、LAG 数量分别是多少?
2. LACP 是否符合 802.3ad、一个 LAG 最多多少成员、能否跨两台设备(若销售页的
`stackable` 属实,堆叠的线缆/模块、最大成员、控制面和软件版本为何)?
3. 管理面是否支持 HTTPS/SSH、禁用 HTTP/Telnet、独立管理 VLAN、SNMPv3、syslog、NTP、
配置导出/回滚和已签名或可校验的固件;默认凭据首次登录是否强制修改?
## 供电、散热和光口:当前不能确认
针对该精确 SKU,厂商官网目录与公开搜索未找到说明书/数据表,所以以下均为**待确认,
不能猜测**
- 是否为内置 AC 电源、额定输入范围/最大功耗、是否带电源开关和接地端子;是否完全
不提供 PoE(本型号名和京东标题均未写 PoE,但这不足以替代规格书)。
- 风扇数量、常态/满载噪声、风向、环境温湿度、机架深度与安装耳;不要将「金属壳」
或产品照片等同于无风扇/静音。
- 两个槽是否均为 **10G SFP+**,是否可协商 1G SFP;支持的 SR/LR/BiDi 波长距离、
DAC/AOC 长度、第三方模块/EERPOM 兼容策略、10GBASE-T SFP+ 模块的功耗/温度限制,
以及是否支持 GPON/XPON ONU「猫棒」。
厂商确实单列「SFP Optical Modules」产品分类,但这不构成 3218F 的兼容清单。
[厂商产品导航](https://en.sirivision.com/)。购买光模块/直连线时,应要求厂商按这台
设备的硬件/固件 revision 出具兼容型号清单;没有书面清单时,先在可退换期实测两端的
链路、重启恢复、热插拔与长时间满载错误计数。
## 风险与建议验收
- **文档/生命周期风险(中到高):** 精确型号不在厂商当前官网 2.5G 目录,虽有固件下载页,
但未公开完整型号手册、明确 release notes 或兼容矩阵。官网的售后条款也要求按具体产品查询保修期,配件(含光纤头)
的保修条款与主机不同;不要把平台页的「3 年」当作中国零售 SKU 的已确认保修。
[厂商售后条款](https://en.sirivision.com/after-sale-protection/)
- **功能表述风险(高):** 页面将 L2 特性和「三层网管」并列;在命令/网页菜单、
手册和测试证明之前,将其当作 L2 VLAN/LACP 设备部署,跨 VLAN 路由仍由现有网关承担。
- **双 10G 上联约束(中):** 两个 SFP+ 可作双上联或一个二成员 LAG,但 LAG 增加的是
多流量总吞吐,单一 TCP/UDP 流通常仍受一条 10G 链路限制;上级设备也必须匹配 LACP
配置。
- **管理面风险(中到高):** 家用/低价网管设备常见明文管理、弱默认口令或不透明的固件
更新周期;采购后先置于受限管理 VLAN,改口令、升级已验证固件,且不将管理界面暴露
到 WAN/访客网。
最低验收应包括:逐口协商 100M/1G/2.5G、两只不同厂家 SFP+/DAC(仅在卖家承诺支持的
范围内)、VLAN trunk/access/PVID、STP/环路保护、LACP 故障切换、端口隔离、满载
双向 iperf3 与错误计数、冷启动后的配置保留,以及管理面的 HTTPS/SSH/SNMPv3/配置备份。
如无法提供与型号匹配的正式资料或其中任一关键项失败,应在退换期内退货,并选择公开
数据表、固件与兼容矩阵更完整的型号。
## 备选:兮克 SKS8300-8T 对比
### 已核实的厂商规格
兮克官网的精确型号页明确将 `SKS8300-8T` 定位为三层管理型 10G 全电口交换机,并列出:
- 8 × 1/2.5/5/10GBASE-T RJ45160 Gb/s 交换容量、119.05 Mpps、12 Mbit 缓存、
16K MAC、12 KB 巨帧、512 MB DRAM、32 MB Flash,尺寸 207 × 136 × 35 mm
- QoS、ACL、IP+MAC+端口绑定、流分类/优先级标记、多端口镜像、静态/灵活 QinQ、
sFlow,以及「基于策略的 IPv4/IPv6 单播路由」。
这些是厂商能力声明,并非对每一种路由协议或表项上限的承诺;但相对 3218F 的仅有
销售标题,它给出了精确型号、转发性能和 L3 范围。[兮克 SKS8300-8T
产品页](https://seekswan.com/user/custom-pages/SKS8300-8T.html)
独立的 OpenWrt 设备资料将其识别为 Realtek RTL9303、512 MB RAM,记录了原厂固件
下载入口和串口/TFTP 恢复路径;其硬件数据页列为 12 V / 4 A。这支持「可恢复、可替换
系统」的可操作性,但**不是**兮克对原厂功能的支持承诺。
[OpenWrt 设备页](https://openwrt.org/toh/xikestor/sks8300-8t)
[OpenWrt 硬件数据](https://openwrt.org/toh/hwdata/xikestor/xikestor_sks8300-8t)。
### 能力、物理与运维比较
| 维度 | 希力威视 SR-S25G3218F | 兮克 SKS8300-8T |
|---|---|---|
| 接口/典型用途 | 16 × 2.5G 电口 + 2 × 10G SFP+(销售规格);适合很多 2.5G 终端/NAS,以 10G 光或 DAC 上联。 | 8 × 1/2.5/5/10GBASE-T;适合 10G 铜缆设备、2.5/5G 多速率 NAS/主机。没有 SFP+,光纤上联必须经媒体转换或选另一型号。 |
| 可确认的三层范围 | 仅销售/平台资料称 L3;没有精确型号官方手册,不能确认静态路由以外的功能。 | 官网明确写策略型 IPv4/IPv6 单播路由、ACL/QoS/sFlow/QinQ;动态路由、VRRP、IPv6 ACL/SNMP/认证等仍须按当前固件手册确认。 |
| 冗余/二层 | 卖家声称 VLAN、端口隔离、LACP;STP/环网的实现与规格未知。 | 官网声明 L3 和多项转发特性,但未在产品页给出 STP/LACP/ERPS 的精确限制;购买前仍索取手册。 |
| 散热/噪声 | 无可核实的精确型号风扇、噪声、功耗或风向数据。 | 独立手册镜像和产品图均称智能温控风扇,但厂商产品页未给 dBA;应按「有风扇、可能听得见」规划,不能承诺静音。 |
| 供电 | 未找到精确型号官方输入/功耗资料。 | OpenWrt 硬件数据记录 12 V / 4 A;确认随附电源适配器的插头、余量和地区认证。官方产品页未给满载功耗。 |
| 固件/恢复 | 有精确型号官方固件页;公开记录包含光口自适应与 DAC 配置改动,但未找到完整 release notes、恢复步骤或兼容矩阵。 | 厂商产品页提供「相关下载」区,OpenWrt 还记录原厂固件入口、RJ45 串口和 U-Boot/TFTP 恢复;原厂镜像是否签名、漏洞修复 SLA、配置回退仍未知。 |
关于 8T 的风扇、满载功耗(常见转述为 ≤36 W)、温度范围、芯片型号等,本次未找到
相应的**厂商原始数据表**;不将第三方手册转录当作已核实规格。若噪声、UPS 容量或
机柜散热是购买约束,请先让卖家提供产品铭牌照片、适配器铭牌照片、额定/实测功耗和
dBA 测试条件。
### 选择与验收建议
-**3218F**:必须有 ≥12 个 2.5G 接入端、10G 光/DAC 上联、且 L3 留给现有路由器。
下单前先取得精确型号手册和 SFP+/DAC 兼容承诺;否则端口数量优势不足以抵消资料风险。
-**8T**:最多 8 个设备但需要多速率 10G RJ45、明确的 IPv4/IPv6 静态/策略路由和
以后自行维护/恢复的余地。不要把其 160 Gb/s 标称交换容量误解为 8 端口同时 10G
全双工的性能保证——该标称与端口总线速数学相等,但仍须以实测和厂商 PPS/缓冲说明为准。
- 两台都不应单独承担防火墙、访客/IoT 安全隔离或 WAN 暴露;VLAN 的跨网段策略和公网
边界留在受支持的网关/防火墙上。先为管理面创建专用 VLAN,仅从管理主机访问,禁用
未使用的远程管理协议,备份配置和原厂固件后再接入生产网络。
## 低功耗核心备选(8 × 2.5G + 2 × SFP+
如果核心只需接最多 8 台铜缆终端、上联/连接 NAS 使用 DAC 或光纤 10G,优先考虑没有
PoE 的以下两款。它们都满足 VLAN trunk、LACP 和至少两个 10G SFP+ 的需求;不要为
AP 选 PoE 版来承担核心,因为 PoE 预算、风扇和待机损耗都会明显增加。
| 型号 | 端口与管理能力(厂商声明) | 厂商功耗 / 噪声资料 | 对当前 LAN 的判断 |
|---|---|---|---|
| **TP-Link Omada SG3210X-M2** | 8 × 100M/1G/2.5G RJ45、2 × 10G SFP+,并有 RJ45 和 Micro-USB console。厂商规格列出 802.1Q VLAN、STP/RSTP/MSTP、静态 LAG 和 802.3ad LACP(最多 8 个聚合组、每组最多 8 端口);L3 是 32 个 IPv4/IPv6 接口、48 条静态路由。 | **无风扇**100240 V AC 内置电源。`UN 1.20` 数据表:待机最高 **6.0 W**220 V/50 Hz、25 °C),最高 **15.3 W**220 V)或 **15.0 W**110 V)。 | **首选低功耗方案。** 足以做 LAN66 核心、给 PVE/gfw 与 U6 Lite 做 VLAN 10 trunk,并以 SFP+ DAC/光口连接 10G NAS/主机;它不提供 5G/10G RJ4510G 铜缆需外置转换或 SFP+ 10GBASE-T 模块。 |
| **MikroTik CRS310-8G+2S+IN** | 8 × 2.5G RJ45、2 × 10G SFP+SFP+ 笼支持 1G/2.5G/10G。RouterOS v7(也可选 SwOS)支持 VLAN、链路聚合与 ACL。 | 18–57 V DC 外置供电;官方给出“无附件”最高 **21 W**、总体最高 **34 W**,且机内 **1 个风扇**。厂商没有在该页给出 dBA。 | 可用且软件/文档/恢复路径成熟,但不是本题的静音低功耗优先项:官方最大功耗显著高于 TP-Link,且有风扇。适合明确偏好 RouterOS/SwOS 与其可维护性时选。 |
功耗数字是各厂商的**上限/待机测试条件**,不是你实际墙插读数;SFP+ 光模块、DAC/AOC,尤其
10GBASE-T SFP+ 模块,会另增功耗和热量。对于本网络,用被动 DAC 或短距光模块连接 10G
设备,通常比全 RJ45 10G 核心更容易保持低温、低噪。
`SG3210X-M2` 的上表数据对应 TP-Link 的 `UN 1.20` 数据表;不同地区/硬件版本的包装、
认证和功耗标注可能不同,购买中国零售版本前应让卖家确认**准确硬件版本、保修渠道和固件地区**。
本次未找到 TP-Link 中国官网的该精确型号页,因此不能把海外官方页面当作大陆现货/售后承诺。
MikroTik 同样应通过其官方零售商查询渠道确认本地库存和保修。两台购买前还应确认所选
SFP+/DAC 的兼容清单。
来源:[TP-Link 产品规格](https://www.tp-link.com/uk/business-networking/omada-switch-access-pro/sg3210x-m2/)
[TP-Link `UN 1.20` 数据表](https://static.tp-link.com/upload/product-overview/2025/202512/20251224/SG3210X-M2%28UN%29%201.20_datasheet.pdf)
[MikroTik 产品页](https://mikrotik.com/product/crs310_8g_2s_in)
[MikroTik 用户手册](https://help.mikrotik.com/docs/spaces/UM/pages/214630429/CRS310-8G%2B2S%2BIN)。
+211
View File
@@ -0,0 +1,211 @@
# UniFi network configuration
## Controller
| Item | Value |
|---|---|
| Host | [ubnt](../hosts/ubnt.md) |
| Address | `192.168.66.46` |
| Deployment | Docker, `jacobalberty/unifi:v9.5.21`, host networking |
| Inform endpoint | `http://192.168.66.46:9080/inform` |
| MongoDB database / port | `ace` / `27117` |
Although `8080` is a common UniFi inform port, this controller uses `9080`.
All AP adoption and recovery procedures must use the endpoint above.
## Login and MFA
On 2026-08-08, **"Sync Local Admin with Ubiquiti SSO"** was disabled
(`super_sdn.sso_login_enabled: false`). Previously every login was forced
through Ubiquiti SSO (sso.ui.com) and required MFA, which blocked programmatic
access: the `unifi-network-mcp` write operations failed with
`api.err.Ubic2faTokenRequired`.
Now local accounts (`windy`, `zhiqiangf`) sign in with their local passwords
and no MFA. The `unifi-network-mcp` tool uses the `zhiqiangf` local account and
works for both read and write operations (verified 2026-08-08 with a reversible
client rename).
Impact: unifi.ui.com remote access (Site Manager) no longer works.
Manage the setting idempotently via Ansible:
```bash
cd ansible
ansible-playbook playbooks/unifi-sso.yml --limit unifi --check # read-only
ansible-playbook playbooks/unifi-sso.yml --limit unifi \
-e '{"unifi_sso_confirm": true, "unifi_sso_target_value": false}' # apply
```
See [runbooks/ansible-operations.md](../runbooks/ansible-operations.md).
## Inter-VLAN reachability
The AP management networks are routed by [gw](../hosts/gw.md),
`192.168.66.254`. On 2026-08-05, a probe sourced from the LAN55 gateway
(`192.168.55.254`) reached the controller (`192.168.66.46`) with 3/3 ICMP
replies. The UAP-AC-Lite is online through the `:9080/inform` endpoint, which
also confirms the required application path in practice.
`LAN_IN` contains explicit allow rules for `192.168.55.0/24 ↔
192.168.66.0/24`, but the firewall set is currently inactive (not attached to
an interface). Treat the networks as bidirectionally reachable by default;
do not rely on those inactive rules as an access boundary. Recheck this after
attaching any LAN firewall policy or changing VLAN routing.
To inspect device records from the controller host:
```bash
docker exec -it unifi-controller bash
mongo --port 27117
```
```javascript
use ace
db.device.find(
{ type: "uap" },
{ name: 1, ip: 1, model: 1, inform_url: 1 }
).pretty()
```
## Dedicated Wi-Fi through a third-party gateway
### Architecture boundary discovered on 2026-08-08
`gfw` is a virtual machine connected only to the native LAN 66 network; the
physical Wi-Fi uplink is also an ER-X LAN 66 path. The ER-X provides DHCP for
that LAN. There is no verified 802.1Q-capable path from the U6 Lite to `gfw`.
Consequently, the proposed `ubnt-upg` design is **not implementable on the
current topology without an additional VLAN-capable L2 path**. Giving the VM
an `eth0.10` interface alone does not create VLAN 10 on the physical switch or
deliver tagged frames to it.
UniFi APs associate clients and bridge an SSID into a selected 802.1Q VLAN;
they do not allocate DHCP addresses. The controller configures that AP
behavior, but it is not a DHCP service for a third-party-gateway VLAN. The
gateway on the corresponding VLAN provides DHCP. In the existing topology, the
ER-X DHCP service legitimately answers untagged LAN 66 requests.
The observed `192.168.66.91` lease on `ubnt-upg` is therefore expected when
traffic reaches the existing untagged LAN 66. It is not a failure that can be
fixed by changing the controller or by adding another DHCP server to the same
broadcast domain.
### Conditions for a future implementation
The design becomes viable only after adding and proving one of these:
1. A VLAN-capable switch/trunk from the U6 Lite to a VLAN-10 interface on the
actual gateway, with native LAN 66 retained for AP management; or
2. A hypervisor/vSwitch and physical uplink configured to pass VLAN 10 end to
end to the `gfw` VM, plus the corresponding tagged port towards the U6.
Then configure one UniFi virtual network with **Router: Third-party Gateway**
and VLAN ID `10`, map only `ubnt-upg` to it, and have the VLAN 10 gateway be
the sole DHCP server for `192.168.10.0/24`. A test client must obtain
`192.168.10.x`, with traffic and its lease visible on that gateway, before the
SSID is considered working.
### Protecting existing Wi-Fi
`ubnt-windy` is the existing 5 GHz SSID and has been restored on the U6 Lite.
Keep its network and broadcast/device assignment unchanged. If the dedicated
SSID is ever retried, use the controller's normal **Specific -> U6 Lite**
broadcast selection for `ubnt-upg` only; do not reuse, move, or manually edit
an existing SSID's AP group. An AP configuration push can still briefly
reassociate clients, so zero-impact work on the same AP cannot be guaranteed.
### References
- [Feasibility and DHCP responsibility](unifi-third-party-vlan10-dhcp.md)
- [Supported topology and implementation examples](unifi-openwrt-vlan10-implementation-examples.md)
- Ubiquiti: [Creating Virtual Networks (VLANs)](https://help.ui.com/hc/en-us/articles/9761080275607-Creating-Virtual-Networks-VLANs),
[Creating WiFi SSIDs](https://help.ui.com/hc/en-us/articles/26136823938583-Creating-UniFi-WiFi-SSIDs), and
[Switch Port VLAN Assignment](https://help.ui.com/hc/en-us/articles/26136855808919-Switch-Port-VLAN-Assignment-Trunk-Access-Ports).
## Managed access points
| Name | Model | Management IP | Firmware | Management network | Inform port |
|---|---|---|---|---|---|
| U6 Lite | U6 Lite (`UAL6`) | `192.168.66.6` | `6.7.54.15663` | LAN66 (`192.168.66.0/24`, gateway `192.168.66.254`) | `9080` |
| UAP-AC-Lite | `U7LT` | `192.168.55.5` | `6.8.2.15592` | LAN55 (`192.168.55.0/24`, gateway `192.168.55.254`) | `9080` |
AP MAC addresses are recorded only where operationally needed: U6 Lite
`78:45:58:4d:cc:30`; UAP-AC-Lite `f0:9f:c2:20:04:e9`.
## SSH administration
Adopted APs use the SSH account configured in the controller, rather than the
factory `ubnt/ubnt` credential. The password is stored in the password manager
and must not be added to this repository.
In UniFi Network `9.5.21`, configure it at:
```text
Devices → Device Updates and Settings → Device SSH Settings
→ Device SSH Authentication
```
Use `ssh zhiqiangf@AP_IP` for the adopted-device account. Do not query or copy
the controller's `mgmt` database setting into logs or documentation: it can
contain the managed SSH password.
On 2026-08-06, key-only IPv4 SSH was verified for both managed APs using the
`zhiqiangf` account. Verify future access without permitting password or
keyboard-interactive fallback:
```bash
ssh -4 -o BatchMode=yes -o PasswordAuthentication=no \
-o KbdInteractiveAuthentication=no -o StrictHostKeyChecking=yes \
zhiqiangf@192.168.66.6 'mca-cli-op info'
ssh -4 -o BatchMode=yes -o PasswordAuthentication=no \
-o KbdInteractiveAuthentication=no -o StrictHostKeyChecking=yes \
zhiqiangf@192.168.55.5 'mca-cli-op info'
```
Both devices reported `Connected` to
`http://192.168.66.46:9080/inform` during that verification. The AP shell maps
this management account to UID 0, so treat it as administrative access.
## Offline AP recovery
An AP can be reachable via ICMP and SSH but offline in the controller when its
saved inform URL still points to port `8080`. This occurred for UAP-AC-Lite
after its inform configuration had diverged from the controller endpoint.
From the AP, inspect the active endpoint and test controller reachability:
```bash
info
nc 192.168.66.46 9080
```
The AP's BusyBox `nc` may not support `-zv`; use the plain `nc` invocation
above when testing the TCP path.
If `info` reports `http://192.168.66.46:8080/inform` (or another obsolete
endpoint), correct it. UniFi devices commonly require the command twice while
adoption is completed:
```bash
set-inform http://192.168.66.46:9080/inform
set-inform http://192.168.66.46:9080/inform
```
Verify that the AP changes to connected/online in the controller.
## Controller migration check
After a controller migration or restore, inspect stored device records for a
stale default-port endpoint before treating an AP as faulty:
```javascript
use ace
db.device.find(
{ inform_url: /8080/ },
{ name: 1, ip: 1, inform_url: 1 }
)
```
Do not include credentials or controller database dumps in incident notes.
@@ -0,0 +1,120 @@
# UniFi AP + OpenWrt VLAN-backed Wi-Fi: implementation evidence
**Research date:** 2026-08-08
**Scope:** research only. This file does not assert that the local physical
path currently carries VLAN 10, and no controller, AP, ERX, or OpenWrt setting
was changed while preparing it.
## Conclusion
Yes. This is a normal, supported deployment pattern: UniFi APs and Network
Application manage the SSID-to-VLAN mapping, while a non-UniFi gateway such as
OpenWrt owns the tagged VLAN interface, its subnet, DHCP, routing, and
firewall/policy routing. The existing untagged LAN DHCP server remains in
place; it serves only the native LAN, while OpenWrt serves the tagged VLAN.
Ubiquiti's current third-party-gateway recipe specifically says to configure
the subnet, VLAN ID, and DHCP **on the third-party gateway first**, then create
a UniFi virtual network with **Router = Third-party Gateway** and the same VLAN
ID. [Ubiquiti: Creating Virtual Networks (VLANs)](https://help.ui.com/hc/en-us/articles/9761080275607-Creating-Virtual-Networks-VLANs)
That supported pattern requires an end-to-end tagged path. It is not feasible
on the current local topology: `gfw` is a VM connected only to untagged LAN 66
and there is no verified physical or virtual switch trunk carrying VLAN 10 from
the U6 Lite to that VM. Creating `eth0.10` inside the VM cannot supply the
missing L2 transport.
## The reusable topology
```text
main SSID ── untagged/native LAN66 ── ERX DHCP ── 192.168.66.0/24
U6 Lite ── AP uplink trunk (native LAN66 + tagged VLAN 10) ── gfw/OpenWrt
└── ubnt-upg SSID ── VLAN 10 ── gfw.10: 192.168.10.1/24 + DHCP + OpenClash
```
This is not two DHCP servers answering one broadcast domain. They serve two
separate L2 broadcast domains. An AP bridges a station's DHCP request into the
VLAN selected for its SSID; it does not allocate the address itself. Ubiquiti
states that SSID clients become members of the selected VLAN and that every
port between AP and gateway must allow that VLAN. [Ubiquiti: Creating UniFi
WiFi SSIDs](https://help.ui.com/hc/en-us/articles/26136823938583-Creating-UniFi-WiFi-SSIDs)
OpenWrt's own VLAN documentation shows the same mixed-link model: an untagged
LAN plus tagged VLAN interfaces on a common link, and explicitly notes that a
separate DHCP server can serve each segment. [OpenWrt: VLAN / switch
configuration](https://openwrt.org/docs/guide-user/network/vlan/switch_configuration)
## Evidence of existing deployments
### Protocol and product documentation (authoritative)
* Ubiquiti's third-party-gateway workflow is an exact product-supported recipe
for the UniFi half: gateway first, then matching VLAN in UniFi. It does not
require a UniFi gateway or switch. [Creating Virtual Networks
(VLANs)](https://help.ui.com/hc/en-us/articles/9761080275607-Creating-Virtual-Networks-VLANs)
* Ubiquiti specifies the exact Wi-Fi half: choose the network in the SSID's
**Network** dropdown, select the APs that should broadcast it, and permit the
VLAN on all intervening ports. [Creating UniFi WiFi
SSIDs](https://help.ui.com/hc/en-us/articles/26136823938583-Creating-UniFi-WiFi-SSIDs)
* Ubiquiti's troubleshooting topology specifically calls an SSID VLAN made
native on the AP port, or absent as a tagged VLAN upstream, a broken setup;
it directs operators to trace every AP-to-gateway port. [Virtual Network
(VLAN) Troubleshooting](https://help.ui.com/hc/en-us/articles/9592924981911-Virtual-Network-VLAN-Troubleshooting)
* OpenWrt documents Linux 802.1Q interfaces and bridge VLAN membership. Its
example calls out a trunk carrying untagged traffic and tagged VLANs, which
is the link gfw needs. [OpenWrt: VLAN / switch
configuration](https://openwrt.org/docs/guide-user/network/vlan/switch_configuration)
### User-reported working deployments (official Ubiquiti Community; anecdotal)
These are not Ubiquiti support guarantees, but they demonstrate that the exact
vendor split is in real use:
* A user reports a UniFi site whose upstream router/DHCP server is **OpenWrt**;
they create a Third-Party Gateway network, assign its VLAN tag, then bind it
to the Wi-Fi SSID. The report says it works without a UniFi gateway or switch.
[UniFi Network Application 7.4.156 discussion](https://community.ui.com/releases/UniFi-Network-Application-7-4-156/15ac6260-9cd1-4ac3-a91c-4880c1c87882?page=36)
* A second user reports two SSID VLANs over an **OpenWrt Linksys WRT32X 802.1Q
trunk**, with no USG or UniFi switch; the controller network is selected in
the SSID's Network dropdown. [Assigning VLANs to SSIDs on Controller
6](https://community.ui.com/questions/Assigning-VLANs-to-SSIDs-on-Controller-6/3e656047-685f-470e-954c-703077e89aaa)
* A Ubiquiti Community post describes an existing multi-VLAN UniFi deployment
with an OpenWrt NanoPi R6S gateway and all UniFi networks configured as
Third-party Gateway. [OpenWrt third-party-gateway
deployment](https://community.ui.com/questions/Replacing-an-existing-third-party-router-firewall-with-a-UXG-Lite-on-an-existing-UniFi-network/5de2504a-fac2-437a-b893-495a6be2f3c0)
## Exact mapping for this network
| Layer | Required configuration |
|---|---|
| `gfw` / OpenWrt | Only after a tagged path reaches the VM: create/retain VLAN-10 interface, address `192.168.10.1/24`, run the **only** DHCP server for that segment, and apply the intended OpenClash/policy routing. |
| U6-to-gfw path | Native/untagged LAN66 for U6 management; tagged VLAN 10 allowed at every switch/link. Do not replace the native LAN with VLAN 10. |
| UniFi network | One VLAN 10 virtual network with **Router: Third-party Gateway**. This is controller metadata that makes the AP tag the selected SSID; it is not a DHCP service. |
| `ubnt-upg` Wi-Fi | Select that VLAN 10 network. Do not enable UniFi Hotspot/guest portal unless deliberately required. |
| Existing Wi-Fi | Leave `ubnt-windy`, `ubnt-windy-2`, and `ubnt-haas` network/VLAN choices untouched; they remain on their existing LANs and DHCP servers. |
The acceptance test is therefore unambiguous: after reconnecting, a device on
`ubnt-upg` must receive `192.168.10.x` from the VLAN 10 gateway, while a device
on existing Wi-Fi continues to receive its established LAN address. A
`192.168.66.x` lease on the dedicated SSID means VLAN 10 was not selected or
did not pass the L2 path; it is not evidence that the U6 should run DHCP.
## Protecting the existing Wi-Fi during a retry
The current UniFi UI supports **All**, **Specific**, and **Groups** independently
for each SSID's Broadcasting APs selection. [UniFi WiFi SSID and AP Settings
Overview](https://help.ui.com/hc/en-us/articles/32065480092951-UniFi-WiFi-SSID-and-AP-Settings-Overview)
Given the earlier incident where changing a virtual/AP group removed
`ubnt-windy` from the U6 Lite, the low-risk control is to leave every existing
SSID's Broadcasting APs selection unchanged and set **only `ubnt-upg`** to
**Specific → U6 Lite**. Do not reuse or move the default SSID into a temporary
group. This does not eliminate the short re-provision/reassociation caused by
an AP config push, but it prevents the configuration-level loss of the default
SSID that was observed.
Before enabling the dedicated SSID, inspect the saved settings and AP's
effective configuration, then test with one client. Only the dedicated SSID
should be affected by a failure; LAN66 DHCP and the existing SSIDs require no
change.
+104
View File
@@ -0,0 +1,104 @@
# UniFi Wi-Fi VLAN 10 with a third-party gateway
**Research date:** 2026-08-08
**Scope:** evidence-backed design only; no controller, AP, switch, EdgeRouter,
or OpenWrt settings were changed by this research.
## Answer: who assigns client addresses?
Neither a UniFi Network controller nor a UniFi AP is the DHCP server for a
normal third-party-gateway deployment. The controller configures the AP; the
AP maps a selected SSID to an 802.1Q VLAN and bridges the client's DHCP
broadcast into that VLAN. A UniFi **Gateway** can provide DHCP, but Ubiquiti's
DHCP documentation expressly describes the DHCP server as a service of UniFi
Gateways, not APs or the Network Application. [UniFi DHCP Server](https://help.ui.com/hc/en-us/articles/360012097513-UniFi-DHCP-Server)
Accordingly, an existing DHCP server on the native/untagged LAN (here, the
EdgeRouter X for `192.168.66.0/24`) is expected to answer an SSID that is not
actually VLAN-tagged. It must **not** provide the lease for a genuinely
separate dedicated VLAN. That VLAN's gateway would provide its DHCP service.
## Required architecture
```text
client → SSID ubnt-upg → U6 Lite
│ VLAN 10 (802.1Q tagged)
every intervening L2 link permits VLAN 10
gateway VLAN-10 interface: 192.168.10.1/24 + DHCP + routing/OpenClash
```
The AP's management traffic may remain untagged on LAN66. VLAN 10 is only
the client traffic emitted by `ubnt-upg`; it must be carried, tagged, from the
U6 Lite through every intervening switch/link to `gfw`. Ubiquiti explicitly
states that selecting a network for an SSID makes connected clients members of
that VLAN and that all ports between AP and gateway must permit its VLAN ID;
it recommends AP uplinks be trunked for the relevant VLANs. [Creating UniFi
WiFi SSIDs](https://help.ui.com/hc/en-us/articles/26136823938583-Creating-UniFi-WiFi-SSIDs)
On a managed UniFi switch, this means the AP uplink and every upstream port in
the path have native LAN66 (for AP management) plus tagged VLAN 10. Ubiquiti
defines that as a trunk, and warns that an AP uplink must not restrict VLANs
needed downstream. [Switch Port VLAN Assignment](https://help.ui.com/hc/en-us/articles/26136855808919-Switch-Port-VLAN-Assignment-Trunk-Access-Ports)
If a non-UniFi switch is in the path, its 802.1Q settings must be configured
equivalently (or it must be verified to transparently forward VLAN 10); UniFi
does not manage that switch. This is a physical/L2 requirement, so merely
creating a controller network cannot make a VLAN reach `gfw`.
## Local feasibility result
The local `gfw` instance is a VM attached only to untagged LAN 66, and the U6
Lite's physical path is the same ER-X LAN 66 network. No VLAN-capable physical
trunk or verified hypervisor/vSwitch VLAN handoff exists between them. The
diagram above is therefore a valid design pattern but **not a description of
the current deployable topology**. An `eth0.10` interface inside the VM cannot
create that missing L2 path.
To make this architecture feasible, add a VLAN-capable switch/trunk from AP to
gateway, or configure an end-to-end tagged VLAN handoff through the hypervisor
and its physical uplink. Until then, leave the ER-X LAN 66 DHCP design in place
and do not expect `gfw` to serve an SSID-only subnet.
## Correct controller model (after the L2 prerequisite exists)
Use one UniFi virtual network for VLAN 10 whose **Router** is **Third-party
Gateway** and whose VLAN ID is `10`; assign that network to Wi-Fi
`ubnt-upg`. Configure the subnet, VLAN and DHCP on the VLAN gateway first.
This follows Ubiquiti's documented third-party-gateway workflow: create the
VLAN on the gateway, then add a UniFi virtual network with Router =
Third-party Gateway and the matching VLAN ID. [Creating Virtual Networks
(VLANs)](https://help.ui.com/hc/en-us/articles/9761080275607-Creating-Virtual-Networks-VLANs)
Do not enable a second DHCP service in UniFi for this VLAN. The controller
entry is the AP/switch VLAN definition; the gateway on VLAN 10 remains the
sole DHCP server and default gateway. The EdgeRouter X's LAN66 DHCP service
can remain unchanged because it is for untagged LAN66, not VLAN 10.
## Safe verification sequence
1. Read-only confirm on `gfw` that its VLAN-10 interface has
`192.168.10.1/24`, DHCP is enabled for `192.168.10.0/24`, and no other
DHCP daemon serves that VLAN.
2. In the controller, verify `ubnt-upg` selects the Third-party Gateway VLAN
10 network, not `Default`/LAN66 and not an unassigned VLAN-only placeholder.
3. Trace the U6 Lite-to-`gfw` L2 path and permit tagged VLAN 10 on every link
while retaining LAN66 as the AP's native/management network.
4. Reconnect a test client (forget/rejoin or renew lease). The expected lease
is `192.168.10.x`, gateway/DNS `192.168.10.1` (or the DNS option configured
by `gfw`), never `192.168.66.x`.
5. Confirm a lease appears on `gfw` and that the VLAN-10 interface's RX/TX
counters advance. Ubiquiti's VLAN troubleshooting guide identifies
upstream ports not allowing the VLAN as the common cause when Wi-Fi clients
cannot receive a correct IP address. [Virtual Network (VLAN)
Troubleshooting](https://help.ui.com/hc/en-us/articles/9592924981911-Virtual-Network-VLAN-Troubleshooting)
## What the observed `192.168.66.91` proves
It is evidence that the client's DHCP discover reached the LAN66 broadcast
domain and was answered by the EdgeRouter X. It does not show a DHCP failure
on `gfw`; it shows that the client was bridged untagged (or VLAN 10 was not
carried end-to-end) at that attempt. The corrective work is therefore to make
the SSID-to-VLAN association and L2 trunk path real, then test the lease—not to
make the AP/controller compete with the LAN DHCP server.
+83
View File
@@ -0,0 +1,83 @@
# dns.windy.lan — AdGuard Home LAN DNS
## Role and access
| Item | Value |
|---|---|
| IPv4 | `192.168.66.36` |
| SSH | `ssh -4 windy@192.168.66.36` |
| OS | Debian 12 |
| **Host** | **PVE VM 120 (`pihole`)** — not a separate physical host (verified 2026-08-09) |
| Roles | AdGuard Home LAN DNS; Mihomo explicit-proxy service |
Use `windy` for routine inspection. Do not store credentials, AdGuard Home
user hashes, proxy subscriptions, or query-log contents in this repository.
## AdGuard Home
AdGuard Home runs as the root-owned `adguardhome` Docker container in host
network mode. The persistent deployment is `/opt/adguardhome`:
| Item | Path / setting |
|---|---|
| Compose file | `/opt/adguardhome/docker-compose.yml` |
| Configuration | `/opt/adguardhome/conf/AdGuardHome.yaml` |
| Mutable data and query logs | `/opt/adguardhome/work` |
| DNS | TCP/UDP `53`; intentionally no source ACL so both LAN55 and LAN66 work |
| HTTP UI | `192.168.66.36:80`; LAN66 only |
| Image policy | Pin the reviewed image digest; do not use `latest` for a rebuild |
The EdgeRouter DHCP service advertises this host as DNS for LAN55 and LAN66.
Do not enable AdGuard Home DHCP unless the existing EdgeRouter DHCP service is
explicitly migrated and disabled first.
`agh-ui-access.service` loads `/etc/nftables-agh-ui-access.nft`. It permits
`192.168.66.0/24` (LAN66) and `192.168.55.0/24` (LAN55, for Home Assistant
integration) to TCP/80 and drops other TCP/80 input. It deliberately
does **not** restrict DNS, SSH, Docker, or Mihomo ports. Keep it isolated from
Docker-managed nftables tables.
> 2026-08-12: added `192.168.55.0/24` allow so HAOS (`192.168.55.11`) can reach
the HTTP API on `:80` for the Home Assistant AdGuard Home integration; applied
via `sudo systemctl restart agh-ui-access.service` (file edited first, then
reloaded; syntax verified with `nft -c`). Other firewalls (EdgeRouter LAN_IN/
LAN_OUT inactive, PVE zero rules) were already open for LAN55->LAN66.
Current query-log policy is 14 days with anonymized client IPs. Check free
space before increasing retention. DNSSEC is disabled because the selected
upstream path did not pass the known-bad-signature validation check; do not
enable it without re-testing validated upstreams.
The compatible names `hass.windy.lan` and legacy `hass.local` currently point
to the same Home Assistant address. Migrate clients to `hass.windy.lan`; keep
the legacy rewrite until its planned retirement.
## Mihomo and routing boundary
Mihomo listens for explicit proxy traffic on this host. Read-only checks found
no host TUN policy routing or local nftables/iptables transparent-redirection
rules. A Mihomo listener alone does not mean it intercepts AdGuard Home
traffic. The default gateway (`gfw.windy.lan`) can still affect outbound
traffic, so inspect its OpenClash rules separately before changing upstream
routing.
## Safety
- Back up `/opt/adguardhome/conf/AdGuardHome.yaml` and `docker-compose.yml`
before changing either, then run the official `--check-config` command.
- Recreate only the AdGuard Home container and verify DNS A/AAAA, local names,
the UI from LAN66, and the UI denial from LAN55 afterwards.
- Private PTR currently has no local authoritative source: direct EdgeRouter
PTR queries return NXDOMAIN. Do not point `local_ptr_upstreams` at the
router until that source exists.
See [the health runbook](../runbooks/adguard-home-health.md) and
[the official configuration review](../docs/adguard-home-official-review-2026-08.md).
Routine health through Ansible (`--limit adguardhome`), with a daily timer +
email alerting like the other active hosts:
```bash
cd ansible
ansible-playbook playbooks/health-report.yml --limit adguardhome
```
+39 -6
View File
@@ -6,17 +6,33 @@
|---|---|
| Role | LAN gateway and OpenClash transparent/explicit-proxy host |
| IPv4 | `192.168.66.1` |
| SSH | `ssh -4 root@192.168.66.1` |
| OS | OpenWrt, Linux `6.12.87` |
| SSH | `ssh -4 root@192.168.66.1` (key-only, verified 2026-08-06) |
| OS | ImmortalWrt 25.12.0 (r37854), Linux `6.12.87`, x86/64 |
| **Host** | **PVE VM 140 (`gfw`)** — 3 NICs: `net0`→vmbr0(LAN66/eth0), `net1`→vmbr1(LAN55/eth1, up but unaddressed), `net2`→VLAN10/`ubunt_upg`(eth2, `192.168.10.1/24`) (topology 2026-08-09; eth2/VLAN10 live verified 2026-08-11) |
Do not store the root password in this repository. Prefer a verified SSH key for
future management access.
Do not store the root password in this repository.
## OpenClash
OpenClash runs `/etc/openclash/clash` with configuration
OpenClash runs `/etc/openclash/clash` (clash_meta core) with configuration
`/etc/openclash/pass-cat.yaml`.
- Core: **Mihomo Meta v1.19.29** (linux-amd64, with_gvisor; upgraded from
`alpha-g9ae1cc2` on 2026-08-06, backup `clash_meta.bak.alpha-g9ae1cc2`) —
tracked in Linear W1N-50
- OpenClash app: **luci-app-openclash 0.47.133** (GitHub latest)
- Mode: **fake-ip + TPROXY transparent proxy** (`operation_mode=fake-ip`,
`en_mode=fake-ip`, `proxy_mode=rule`); fake-ip network `198.18.0.0/16`
- DNS path: dnsmasq → clash DNS `127.0.0.1#7874` (`server=127.0.0.1#7874` in
dnsmasq config); OpenClash custom DNS uses `mosdns` on `127.0.0.1:6052` as its
`nameserver`/`default-nameserver` for DIRECT-rule real-IP resolution
(`/etc/mosdns/config.yaml`): domestic domains → AGH `.36:53`, foreign →
`223.5.5.5`/`119.29.29.29` (Chinese public DNS). mosdns is **not** in the
client query path — LAN/VLAN10 clients receive fake-ip from clash :7874.
- nft: OpenClash injects TPROXY/redirect + DNS-hijack rules into
`table inet fw4`; a residual `table inet passwall` exists with 0 packets (unused)
| Listener | Port |
|---|---:|
| HTTP | `7890` |
@@ -25,10 +41,28 @@ OpenClash runs `/etc/openclash/clash` with configuration
| Mixed | `7893` |
| TPROXY | `7895` |
| DNS | `7874` |
| Dashboard | `9090` |
`8443` is not an OpenClash listener and has no runtime nftables forwarding rule.
It is included only in OpenClash's common TLS-sniffing port list.
## VLAN 10 Wi-Fi
`gfw`'s third NIC `eth2` hosts the `ubunt_upg` interface at `192.168.10.1/24`,
serving the dedicated `ubnt-upg` SSID VLAN 10 (untagged access path from a
VLAN-capable switch/trunk; AP management stays untagged on LAN66). The
`ubunt_upg` zone runs the **only** DHCP server for `192.168.10.0/24` (UDP/67),
allows DNS (53), and applies `192.168.10.0/24 → eth0 masquerade` (NAT) for
Internet egress. `forward_ubunt_upg` isolates VLAN10 from LAN66/55 and RFC1918
(deny counters 0, `accept_to_lan` passes).
Live-verified 2026-08-11: an `ubnt-upg` client received `192.168.10.168` (lease
in `/tmp/dhcp.leases`), the `192.168.10.0/24 masquerade` counter climbed
(215 pkts/42KB), and the LAN55/LAN66 deny counters stayed 0 → VLAN10→LAN
isolation holds. See
[docs/lan-se5420-deployment-guide.md](../docs/lan-se5420-deployment-guide.md),
[docs/unifi-openwrt-vlan10-implementation-examples.md](../docs/unifi-openwrt-vlan10-implementation-examples.md)
## Operational note
When OpenClash logs a LAN client request such as
@@ -37,4 +71,3 @@ an explicit-proxy client sending its own loopback request to the gateway. Fix
the originating host's `NO_PROXY` / `no_proxy` configuration; do not create a
listener on this router to mask the problem. See
[the UniFi localhost proxy note](../docs/unifi-openclash-localhost.md).
+102
View File
@@ -0,0 +1,102 @@
# gw — EdgeRouter X primary LAN gateway
## Role and access
| Item | Value |
|---|---|
| Role | Primary router for the `192.168.66.0/24` and `192.168.55.0/24` LANs |
| IPv4 | `192.168.66.254` |
| Verified hostname | `gw` |
| SSH | `ssh -4 zhiqiang@192.168.66.254` |
| Platform | Ubiquiti EdgeRouter X 5-Port, EdgeOS `v3.0.1` (Build `5862409`) |
| Kernel | `4.14.54-UBNT` |
| Time zone | `Asia/Shanghai` |
Use `zhiqiang` as the default account for all routine gateway operations. Its
key-only SSH login and passwordless `sudo` were verified on 2026-08-04; use
`BatchMode=yes` for unattended read-only checks. `ubnt` remains an existing
administrative account, but is not the routine operations entry point.
Do not store account passwords, PPPoE credentials, SSH keys, configuration
password hashes, or UISP/UNMS connection strings in this repository. Both
administrative accounts have EdgeOS configuration and sudo privileges. Verify a
new SSH host key out of band before accepting it.
## Network topology
| Interface | Address / role |
|---|---|
| `eth0` | `192.168.66.254/24`; LAN 66 |
| `switch0` (`eth1``eth3`) | `192.168.55.254/24`; LAN 55 |
| `eth4` | WAN physical port; PPPoE uplink |
| `pppoe0` | WAN default route; MTU `1492`; IPv6 prefix delegation `/60` |
IPv6 prefix delegation assigns SLAAC-capable `/64` networks to both LANs.
`eth4` applies the WAN IPv4 and IPv6 firewall policies.
Detailed effective configuration, including firewall binding and WAN exposure,
is recorded in [the EdgeRouter X configuration record](../docs/edgerouter-x-configuration.md).
## Services and policy
- DHCP serves both LANs with 24-hour leases. Client DNS is `192.168.66.36`.
Pools are `.38``.243` on both networks; selected infrastructure and client
addresses have static mappings. The UniFi controller is advertised as
`192.168.66.46`.
- DNS forwarding listens on `eth0` and `switch0` with a cache size of 512.
- WAN NAT masquerades all IPv4 traffic leaving `pppoe0`.
- `WAN_IN` and `WAN_LOCAL` default to drop, permitting established/related
sessions and rejecting invalid state. Equivalent IPv6 policies permit
necessary ICMPv6 and DHCPv6 traffic.
- `LAN_IN` defines a default drop plus explicit `55 → 66` and `66 → 55`
allows, but it is **not currently applied to an interface**. The active
inter-VLAN posture is therefore ordinary routed access: LAN55 and LAN66 can
communicate bidirectionally unless a different active policy intervenes.
`LAN_OUT` is likewise defined but inactive; WAN policies are active on
`pppoe0`.
- Management services: SSH on TCP 22; EdgeOS GUI on HTTP 80 and HTTPS 443.
## Safe inspection
Use the EdgeOS operational CLI for routine inspection. `show configuration
commands` can expose credentials and remote-management tokens, so either avoid
copying it or filter sensitive lines before recording output.
```bash
ssh -4 -o BatchMode=yes zhiqiang@192.168.66.254
show version
show interfaces
show ip route
show system uptime
show firewall
```
The [official EdgeOS User Guide](https://dl.ubnt.com/guides/edgemax/EdgeOS_UG.pdf)
explains that a firewall policy is a ruleset and must be applied to the
relevant interface/direction to take effect. Use the operational `show
firewall` output—not merely the configured rule definitions—to determine the
effective policy.
## Maintenance notes
- EdgeOS writes persistent changes through its configuration tree: enter
`configure`, make the intended change, then run `commit` and `save`.
- The current SSH service reported a non-post-quantum key-exchange warning.
Treat any cryptography or SSH-policy maintenance as a planned, lockout-safe
change following the repository SSH safety procedure.
- This device is distinct from `gfw.windy.lan` (`192.168.66.1`), the OpenWrt
OpenClash gateway. Do not apply OpenWrt procedures to this router.
## Verification record
Configuration and reachability were checked by read-only SSH on 2026-08-04.
No network policy changes were made during that inspection. The `zhiqiang`
account password was subsequently rotated using the EdgeOS configuration tree
and verified by a separate SSH login; the password is intentionally not
recorded here.
Inter-VLAN access was rechecked by read-only SSH on 2026-08-05. A probe sourced
from `192.168.55.254` reached the UniFi controller at `192.168.66.46` with
3/3 ICMP replies. This supports the AP Inform path to
`192.168.66.46:9080`; the controller listener and an online LAN55 AP provide
the corresponding application-level evidence. No firewall changes were made.
+3 -2
View File
@@ -50,8 +50,9 @@ See [mailcow-smtp-client](../runbooks/mailcow-smtp-client.md).
- [mailcow-health](../runbooks/mailcow-health.md)
- [mailcow-update](../runbooks/mailcow-update.md)
- [mailcow-smtp-client](../runbooks/mailcow-smtp-client.md)
- Script: [`../scripts/check-mx2.sh`](../scripts/check-mx2.sh)
Routine health: `cd ansible && ansible-playbook playbooks/health-report.yml --limit mailcow`.
## Verified
Last checked: 2026-08-01 — `./scripts/check-mx2.sh` exit 0 (compose up, watchdog 100%, empty queue, TLS LE OK, SMTP Postcow, A/AAAA/PTR/MX/SPF OK).
Last checked: 2026-08-01 — Ansible mailcow health profile OK (compose up, watchdog 100%, empty queue, TLS LE OK, SMTP Postcow, A/AAAA/PTR/MX/SPF OK).
+22
View File
@@ -8,6 +8,7 @@
| IPv4 | `192.168.66.46` |
| SSH | `ssh -4 windy@192.168.66.46` |
| Hostname | `ubnt` |
| **Host** | **PVE VM 160 (`ubnt`)** — not a separate physical host (verified 2026-08-09) |
| Privilege | `windy` has passwordless sudo and is in the `docker` group |
The current SSH ED25519 host-key fingerprint is
@@ -25,6 +26,7 @@ Verify a changed key out of band before accepting it.
| Image | `jacobalberty/unifi:v9.5.21` |
| Network mode | host |
| HTTPS UI | `https://192.168.66.46:8443` |
| Device inform endpoint | `http://192.168.66.46:9080/inform` |
The controller binds `*:8443`. A local HTTPS request to
`https://127.0.0.1:8443/` is the primary quick health check.
@@ -43,6 +45,14 @@ Without this bypass, a controller request to `localhost:8443` can be sent to
the OpenClash gateway instead of remaining local. Details and verification are
in [the UniFi localhost proxy note](../docs/unifi-openclash-localhost.md).
## Managed access points
The controller manages the APs listed in [the UniFi network record](../docs/unifi-network.md).
The inform endpoint is deliberately on port `9080`, not UniFi's common default
of `8080`. During adoption or recovery, use the documented `:9080/inform` URL;
an AP left on `:8080` can remain reachable by ping and SSH while showing
offline in the controller.
## Safe reconciliation and verification
```bash
@@ -56,3 +66,15 @@ curl -k -sS -o /dev/null -w '%{http_code}\n' https://127.0.0.1:8443/
Recreating the service briefly interrupts the controller. Do not use `docker
compose down -v`; it can remove persistent data.
## Ansible management
The host is in the Ansible `unifi` group (inventory `ansible/inventory/hosts.yml`),
deliberately outside `managed` so the generic health report does not run here.
- SSO login setting (idempotent, gated): `cd ansible && ansible-playbook
playbooks/unifi-sso.yml --limit unifi` — see
[runbooks/ansible-operations.md](../runbooks/ansible-operations.md).
- Login/MFA state: SSO login disabled 2026-08-08; local accounts
(`windy`, `zhiqiangf`) use local passwords, no MFA. `unifi-network-mcp`
works for read and write operations. See
[docs/unifi-network.md](../docs/unifi-network.md).
+8 -2
View File
@@ -76,9 +76,15 @@ ansible-playbook playbooks/compose-reconcile.yml --limit vaultwarden \
-e '{"service_reconcile_confirm": true, "service_reconcile_targets": ["vaultwarden"], "service_reconcile_restart_traefik": true}'
```
## Other software on this host (stubs)
## Other running services on this host
`/opt/traefik`, `soft-serve`, `authelia`, `dendrite`/`conduit`, `mastodon`, `rustdesk`, `zitadel`, etc.
| Container | Status | Image / notes |
|-----------|--------|---------------|
| `soft-serve` | Up | `ghcr.io/charmbracelet/soft-serve:latest` (`repo.windy.me:2222`) |
| `traefik` | Up | `traefik:v3.6.2` (`/opt/traefik`, public `:80`/`:443`) |
| `nghttpx-proxy` + `squid-backend` | Up | HTTP forward-proxy stack (`/opt/nghttpx`), network `nghttpx_internal-net`; details TBD |
Directories for `authelia`, `conduit`, `dendrite`, `mastodon`, `rustdesk`, `zitadel`, etc. exist under `/opt` but have no running containers; treat them as dormant, not documented services.
## Verified
+61
View File
@@ -0,0 +1,61 @@
# us4.wsvc.info
| Item | Value |
|------|-------|
| Role | WireGuard VPN server (LinuxServer Docker image) |
| SSH | `ssh -4 windy@us4.wsvc.info` (prefer IPv4 from WSL) |
| IPv4 | `185.201.226.122` |
| Compose project | `/opt/wireguard` |
| Compose file | `/opt/wireguard/compose.yml` |
| Container | `wireguard` |
| Image policy | Immutable digest, updated only in an approved maintenance window |
| Public port | UDP `51820` on IPv4 and IPv6 |
| Tunnel subnet | `10.13.13.0/24` |
| Routing policy | IPv4-only full tunnel (`ALLOWEDIPS=0.0.0.0/0`); IPv6 traffic is not guaranteed to use the VPN |
## Safety
- Private keys, preshared keys, peer configuration files, and QR codes remain
only in `/opt/wireguard/config`; do not copy them to this repository or
Linear.
- Local rollback archives are stored in `/opt/wireguard/backups` (directory
mode `0700`, archives mode `0600`). They contain private keys, are not an
off-host disaster-recovery backup, and must never leave the server.
- Do not delete, move, or regenerate `/opt/wireguard/config` during
maintenance.
- Before a container recreation, validate `docker compose config` and retain a
verified SSH session for rollback. Do not run `docker compose down -v`.
## Routine operations
Run read-only checks through Ansible:
```bash
cd ansible
ansible-playbook playbooks/health-report.yml --limit wireguard
```
The image update and recreate procedure is deliberately separate and requires
an immutable image digest in the server-side Compose file plus an explicit
maintenance-window confirmation:
```bash
cd ansible
ansible-playbook playbooks/wireguard-maintenance.yml --limit wireguard \
-e '{"wireguard_maintenance_confirm": true}'
```
Before that recreate, apply the reviewed Compose hardening from the repository:
```bash
ansible-playbook playbooks/wireguard-harden.yml --limit wireguard \
-e '{"wireguard_harden_confirm": true}'
```
## Verification
- Container `wireguard` is running and interface `wg0` exists.
- UDP `51820` is listening on the host.
- Validate a known client can handshake and sends IPv4 traffic through the VPN.
- Do not treat inactive mobile peers as a failure solely because their latest
handshake is old.
+8 -3
View File
@@ -5,6 +5,9 @@ run through Ansible. SSH endpoints below remain the access path for focused
diagnosis and procedures that are deliberately interactive or destructive; see
[the Ansible operations runbook](../runbooks/ansible-operations.md).
For a live-verified map of the **internal LAN** (gw, gfw, dns, ubnt, APs) and
the software deployed there, see [the LAN overview](../docs/lan-overview.md).
| Host | Role | SSH | IPv4 | Status | Facts |
|------|------|-----|------|--------|-------|
| mx2.windy.me | mailcow (primary MX prio 20) | `ssh -4 windy@mx2.windy.me` | 194.163.160.244 | active | [hosts/mx2.windy.me.md](../hosts/mx2.windy.me.md) |
@@ -13,15 +16,17 @@ diagnosis and procedures that are deliberately interactive or destructive; see
| repo.windy.me | Soft Serve git (on us2) | `ssh -p 2222 windy@repo.windy.me` | 193.9.44.165 | stub | see us2 |
| auth.wsvc.info | Vaultwarden public hostname | — (HTTPS) | → us2 | active | see us2 |
| us1.wsvc.info | PowerDNS secondary (ns2 host) | TBD | 202.91.35.141 | stub | Auth 5.0.5; see hk2 |
| us4.wsvc.info | Traefik / services | TBD | TBD | stub | — |
| us4.wsvc.info | WireGuard VPN | `ssh -4 windy@us4.wsvc.info` | 185.201.226.122 | active | [hosts/us4.wsvc.info.md](../hosts/us4.wsvc.info.md) |
| hk2.chans.xyz | PowerDNS auth (ns1) | `ssh -4 windy@hk2.chans.xyz` | 154.36.174.161 | active | [hosts/hk2.chans.xyz.md](../hosts/hk2.chans.xyz.md) |
| ns1.wsvc.info | PowerDNS public NS name | — (DNS) | → hk2 `154.36.174.161` | active | see hk2 |
| ns2.wsvc.info | Secondary NS (AXFR/NOTIFY peer) | — (DNS) | → us1 `202.91.35.141` | active | see hk2 |
| pdns.wsvc.info | Poweradmin UI | — (HTTPS) | → hk2 | active | see hk2 |
| pgweb.wsvc.info | PowerDNS Postgres UI | — (HTTPS) | → hk2 | active | see hk2 |
| **synapse.chans.xyz** | Matrix homeserver (ESS: Synapse + MAS + Element) | `ssh -4 windy@synapse.chans.xyz` | `169.58.86.13` | **active** | [hosts/synapse.chans.xyz.md](../hosts/synapse.chans.xyz.md) |
| **gfw.windy.lan** | OpenWrt LAN gateway / OpenClash | `ssh -4 root@192.168.66.1` | `192.168.66.1` | **active** | [hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md) |
| **ubnt** | UniFi Network Controller | `ssh -4 windy@192.168.66.46` | `192.168.66.46` | **active** | [hosts/ubnt.md](../hosts/ubnt.md) |
| **gfw.windy.lan** | OpenWrt (ImmortalWrt) LAN gateway / OpenClash (PVE VM 140) | `ssh -4 root@192.168.66.1` | `192.168.66.1` | **active** | [hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md) |
| **dns.windy.lan** | AdGuard Home LAN DNS + Mihomo explicit proxy (PVE VM 120) | `ssh -4 windy@192.168.66.36` | `192.168.66.36` | **active** | [hosts/dns.windy.lan.md](../hosts/dns.windy.lan.md) |
| **gw** | EdgeRouter X primary LAN gateway | `ssh -4 zhiqiang@192.168.66.254` | `192.168.66.254` | **active** | [hosts/gw.md](../hosts/gw.md) |
| **ubnt** | UniFi Network Controller (PVE VM 160) | `ssh -4 windy@192.168.66.46` | `192.168.66.46` | **active** | [hosts/ubnt.md](../hosts/ubnt.md) |
`status: stub` = known to exist; fill `hosts/<name>.md` when next touched.
+2
View File
@@ -0,0 +1,2 @@
version: 1
profile: minimal
@@ -1,70 +0,0 @@
# VPS 維護專案重構:Ansible + systemd timers + Restic
## Objective
將現有 VPS 維運知識庫漸進重構為一個以 Ansible 管理一致性、以 systemd timers 執行每日唯讀檢查、以 Restic 建立加密異機備份的個人 SRE 維運專案。現納管 mx2、us2、hk2 與 Matrix K3s 主機。自動化預設只能觀測與告警;受 allowlist 和顯式確認保護的 Compose reconciliation 可處理已審查的服務重建,DNS、機密與破壞性資料操作仍需人工確認。
## Confirmed Decisions
- [x] 定位為個人 SRE/維運手冊庫,而非全自動修復平台。
- [x] 已納管 mx2、us2、hk2 與 Matrix K3smx、us1、us4 待完成盤點與緊急存取驗證後才納管。
- [x] 每日自動做唯讀檢查與 Email 通知;每週人工審查、每月人工維護、每季復原演練與權限盤點。
- [x] 以 Email 作為唯一告警與摘要通道。
- [x] 採用本機快速復原加密異機副本的 3-2-1 最小可行策略。
- [x] 真實機密不進 Git;repo 只保存去敏設定結構與操作/驗證紀錄。
- [x] 日常存取使用非 root SSH key;禁止 root/password SSH,維護 provider/recovery console 緊急存取。
- [x] 採用 Ansible + systemd timers + Restic 作為技術基線。
## Implementation Plan
- [x] **Status: Done — 建立去敏 Ansible inventory。** 為 mx2、us2、hk2 與 Matrix 建立主機與服務群組,將非機密主機資料與現有人類可讀的 `inventory/hosts.md` 對應;理由是提供可審查的機器可讀編排層,且不取代既有事實來源。
- [x] **Status: Done — 實作 audit-only Ansible playbook。** 只收集連線、OS、磁碟、systemd、Docker Compose 和依賴命令狀態,不做設定或服務改動;理由是先確認控制面與真實現況。
- [x] **Status: Done — 定義跨服務健康檢查結果合約。** 統一去敏結構化結果、日誌位置、嚴重度及 exit code,讓人工執行、systemd 和 Ansible audit 可共同消費;理由是避免各服務告警語意漂移。
- [x] **Status: Done — 保留並模組化 Mailcow 健康檢查。** 持續檢查 Compose、watchdog、queue、listeners、HTTP/HTTPS、TLS、SMTP、DNS/PTR/MX/SPF;理由是現有腳本已覆蓋關鍵郵件服務面向。
- [x] **Status: Done — 實作 Vaultwarden 唯讀健康檢查。** 驗證 Compose/Postgres、HTTPS、有效設定、SMTP AUTH、備份新鮮度及機密指紋一致性,禁止輸出機密;理由是 `config.json` 優先於 `.env`,且 SMTP 漂移已有已知風險。
- [x] **Status: Done — 實作 PowerDNS 唯讀健康檢查。** 驗證容器、版本與安全公告、ns1/ns2 served SOA、API、Web UI、備份新鮮度與必要設定;理由是同時覆蓋公開 DNS 與 secondary 同步。
- [x] **Status: Done — 使用 Ansible 部署 systemd healthcheck service/timer。** 每台主機本機執行每日檢查,具 persistent 排程、權限、logrotate 和一致錯誤處理;理由是控制端離線不應阻止巡檢。
- [x] **Status: Done — 部署 Email 告警與每日摘要。** Critical/unknown 立即通知、健康狀態每日摘要、重複失敗抑制;SMTP 真實認證僅在各主機受限路徑保存;理由是已選定 Email 為唯一通知通道。
- [!] **Status: Blocked — 決定 Restic 異機 repository 與存取隔離。** 選擇具加密傳輸、權限隔離與可承受保留需求的 S3/B2/SFTP 或等價目的地;理由是無異機 repository 即無法滿足已確認的 3-2-1 政策。阻塞:本地實作依要求未虛構 backend、repository 或 credentials。
- [x] **Status: Done (templates gated) — 部署 Vaultwarden 與 PowerDNS 的 Restic 備份。** 將既有一致性資料庫 dump、必要資料目錄及復原元資料加密同步,並加入 snapshot 年齡和 `restic check` 驗證;理由是兩者已有本機備份但缺少異機保護。啟用被 repository 決策和主機端受限設定檔阻擋。
- [!] **Status: Blocked — 完成 Mailcow 備份設計審查。** 官方流程已確認:以 `/opt/mail/helper-scripts/backup_and_restore.sh backup all`(或經明確核准的元件集)先產生一致性備份,再由 Restic 同步該輸出;不得直接複製 Docker volumes。阻塞:仍需決定本機備份位置、保留期、排程及異機 Restic repository。
- [x] **Status: Done (templates gated) — 部署 Restic backup、retention、forget/prune 與 check timers。** 與現有資料庫 dump 時段錯開,所有 repository 認證與密碼留在伺服器端受限檔案;理由是保持備份可用與成本可控。啟用被 repository 決策和主機端受限設定檔阻擋。
- [!] **Status: Blocked — 補齊服務級復原 runbook。** 阻塞:需在選定 Restic repository、建立實際 snapshot 並確認各服務的實際備份輸出後,才能編寫可驗證的 restore 前置條件、順序與 rollback;禁止臆造 backend/credentials 或未驗證還原命令。
- [!] **Status: Blocked — 執行隔離式復原演練。** 阻塞:尚未選定/配置 Restic 異機 repository,亦尚無可供還原的異機 snapshot;演練不得對現有生產資料執行。
- [x] **Status: Done — 實作受控 common baseline 與 maintenance playbook。** 僅在 audit 穩定後納入 SSH 稽核、時間同步、logrotate、更新預覽與人工確認的維護操作;理由是避免工具導入期間同時改變服務狀態。
- [x] **Status: Done (2026-08-03) — 將例行操作收斂為 Ansible 入口。** 新增 on-demand health report 與 allowlisted Compose reconciliationhealth、maintenance preview、baseline 現可涵蓋 Matrix。互動式 Mailcow 更新、資料遷移、DNS 與機密操作保留人工程序。
- [x] **Status: Done (separate change defined) — 將 PowerDNS API key、DB password 與 TSIG 輪替列為分離變更。** 已明確保持 API key、DB password 與 TSIG 為三項獨立、需人工核准的變更,並要求逐步驗證 Auth、Poweradmin、AXFR/NOTIFY、DNSSEC 與 ns2 同步;未輪替任何機密。
- [!] **Status: Blocked — 盤點 mx、us1、us4。** 阻塞:inventory 僅記錄 `TBD` SSH/角色或缺少完整事實,尚未提供可驗證的存取方式與 provider/recovery console 資訊;不得猜測或嘗試未授權存取。
## Verification Criteria
- [ ] 四台 active 主機能被 Ansible 正確解析;Compose 主機可執行 audit,且 audit 不產生主機變更。
- [ ] 四台主機每日產生去敏健康結果;服務、容量、TLS、公開端點和備份新鮮度異常均能被偵測。
- [ ] 正常狀態寄送 Email 摘要;受控測試異常能觸發一次可讀且不含機密的 Email 告警。
- [ ] 每個核心服務至少有一份加密、異機、可列出且通過完整性檢查的 Restic snapshot。
- [ ] 至少完成一次隔離式實際還原,並確認其結果符合或明確量化偏離服務 RPO/RTO。
- [ ] 自動化排程不包含更新、重啟、修復、秘密輪替或 DNS 變更。
- [ ] Repo、Ansible vars、產出日誌及 Email 均不包含任何真實機密。
## Potential Risks and Mitigations
1. **Mailcow 備份未保持資料一致性。**
Mitigation: 先依官方方式完成備份範圍與還原設計,再上線排程;以隔離還原驗證作為完成門檻。
2. **Ansible 設定錯誤改動生產系統。**
Mitigation: 先導入 audit-only;變更 playbook 必須使用 dry-run/diff、明確 tag 與人工確認,且先限制單一 host。
3. **機密出現在 Git、Ansible output 或 Email。**
Mitigation: 真實值只存主機受限檔案;去敏輸出、no-log 機制和測試用機密掃描為強制要求。
4. **Timer 成功但公開服務實際不可用。**
Mitigation: 同時執行本機 Compose/DB 檢查與外部 HTTP、SMTP、TLS、DNS 檢查。
5. **PowerDNS 密鑰輪替中斷 DNS 管理或 zone transfer。**
Mitigation: API key、DB password、TSIG 分批處理;每步都驗證 primary/secondary 同步、DNSSEC 和管理 UI。
## Alternative Approaches
1. **純 shell + systemd,不使用 Ansible**:初期較快,但設定與排程容易跨主機漂移,且難以審查或擴充;不建議作為長期方案。
2. **Ansible + 集中式監控平台**:可增加儀表板與趨勢,但平台本身需額外維護;等 Email 摘要無法滿足需求時再評估。
3. **立即導入 SOPS + age**:可使加密設定進 Git,但需先完成密鑰生命週期與緊急存取設計;建議列為後續階段。
+60
View File
@@ -0,0 +1,60 @@
# AdGuard Home health — dns.windy.lan
This runbook is read-only. It does not expose query-log contents or secrets.
Routine checks run through Ansible on demand:
```bash
cd ansible
ansible-playbook playbooks/health-report.yml --limit adguardhome
```
The same sanitized profile is installed as a daily server-local timer
(`vps-healthcheck.timer`, output at `/var/lib/vps-health/latest.json`) via
`playbooks/healthchecks.yml`; the manual checks below are for focused diagnosis.
## Verify service and configuration
```bash
ssh -4 -o BatchMode=yes windy@192.168.66.36
sudo docker ps --filter name=^/adguardhome$
sudo docker exec adguardhome /opt/adguardhome/AdGuardHome --check-config \
-c /opt/adguardhome/conf/AdGuardHome.yaml
sudo ss -lntup | grep -E '(:53|:80)'
```
Expected: DNS listens on TCP/UDP 53, and the HTTP UI listens only on
`192.168.66.36:80`.
## Verify DNS and storage
```bash
dig +time=5 +tries=1 @192.168.66.36 example.com A +noall +comments +answer
dig +time=5 +tries=1 @192.168.66.36 example.com AAAA +noall +comments +answer
dig +time=5 +tries=1 @192.168.66.36 hass.windy.lan A +noall +comments +answer
sudo docker logs --since 30m adguardhome 2>&1 | grep -Ei '\[(error|warn|fatal)\]|level=(error|warn|fatal)' || true
df -h /opt/adguardhome/work
```
The host intentionally accepts DNS from LAN55 and LAN66. Do not infer a
broken DNS policy merely because `allowed_clients` is empty.
## Verify management UI boundary
From LAN66, the UI should return a login redirect:
```bash
curl -sS -o /dev/null -w '%{http_code}\n' http://192.168.66.36/
```
From a LAN55 source, TCP/80 should time out or be refused by the dedicated
`agh-ui-access.service` nftables rule. Do not alter this rule to restrict
port 53 without an explicit network-access review.
## DNSSEC and PTR caveats
DNSSEC is currently disabled. Before enabling it, select upstreams that pass
a known-bad-signature test; an enabled DO bit alone is not validation.
Private PTR forwarding is intentionally absent because the EdgeRouter does
not currently answer private PTR requests.
+23
View File
@@ -59,3 +59,26 @@ ansible-playbook playbooks/maintenance-preview.yml
# Baseline observations; logrotate management remains opt-in
ansible-playbook playbooks/baseline.yml
```
## UniFi SSO login setting (mutating)
Reconciles `super_sdn.sso_login_enabled` on the UniFi controller (host `ubnt`,
group `unifi`). Idempotent and gated: without `unifi_sso_confirm=true` the
playbook only reports the current state and refuses to change anything.
```bash
# Read-only status report
ansible-playbook playbooks/unifi-sso.yml --limit unifi --check
# Apply (disable SSO login; local accounts use local passwords, no MFA)
ansible-playbook playbooks/unifi-sso.yml --limit unifi \
-e '{"unifi_sso_confirm": true, "unifi_sso_target_value": false}'
# Rollback (re-enable SSO login)
ansible-playbook playbooks/unifi-sso.yml --limit unifi \
-e '{"unifi_sso_confirm": true, "unifi_sso_target_value": true}'
```
The controller container is not restarted by default; the UI path applies the
setting immediately. Set `unifi_sso_restart_controller: true` only when the
setting was changed directly in the DB while the controller was running.
+7 -4
View File
@@ -2,9 +2,10 @@
Target: [mx2.windy.me](../hosts/mx2.windy.me.md)
Path: `/opt/mail`
Prefer: the Ansible health report (`ansible/playbooks/health-report.yml`), or
`./scripts/check-mx2.sh` from this repo. The SSH commands below remain useful
for focused, interactive diagnosis after the report identifies a problem.
Prefer: the Ansible health report (`ansible/playbooks/health-report.yml --limit mailcow`),
or the server-local daily timer result at `/var/lib/vps-health/latest.json`. The
SSH commands below remain useful for focused, interactive diagnosis after the
report identifies a problem.
## Focused SSH diagnostics
@@ -66,7 +67,9 @@ dig @1.1.1.1 +short windy.me TXT | grep -i spf
## Scheduled local check
The sanitized Ansible health profile is `mailcow` (`ansible/playbooks/healthchecks.yml`). It preserves the existing standalone `scripts/check-mx2.sh` as the richer operator check, while the server-local timer emits a sanitized result at `/var/lib/vps-health/latest.json`. Neither check changes Mailcow configuration or service state.
The sanitized Ansible health profile is `mailcow` (`ansible/playbooks/healthchecks.yml`).
The server-local timer emits a sanitized result at `/var/lib/vps-health/latest.json`.
It does not change Mailcow configuration or service state.
## Pass criteria
+1 -1
View File
@@ -6,7 +6,7 @@ Path: `/opt/mail`
## Before
1. Run [mailcow-health](mailcow-health.md) (or `./scripts/check-mx2.sh`). Record baseline.
1. Run [mailcow-health](mailcow-health.md) (Ansible health report). Record baseline.
2. Note config quirks in `mailcow.conf`, especially:
- `SKIP_CLAMD=y` — after update, `clamd` may still be running until services are recreated per mailcow docs.
- `SKIP_LETS_ENCRYPT`, ports, hostname — should stay `mx2.windy.me`.
-195
View File
@@ -1,195 +0,0 @@
#!/usr/bin/env bash
# Read-only health check for mx2.windy.me mailcow.
# Usage: ./scripts/check-mx2.sh
# Env:
# MX2_HOST default mx2.windy.me
# MX2_SSH_USER default windy
# MX2_ALLOW_IPV6 set to 1 to allow IPv6 (default forces IPv4; WSL often lacks v6 route)
set -euo pipefail
HOST="${MX2_HOST:-mx2.windy.me}"
SSH_USER="${MX2_SSH_USER:-windy}"
# Default -4: host has AAAA but many clients (WSL) have no IPv6 route → "Network is unreachable"
SSH_AF_OPTS=(-4)
CURL_AF_OPTS=(-4)
OPENSSL_AF_OPTS=(-4)
if [[ "${MX2_ALLOW_IPV6:-0}" == "1" ]]; then
SSH_AF_OPTS=()
CURL_AF_OPTS=()
OPENSSL_AF_OPTS=()
fi
SSH=(ssh "${SSH_AF_OPTS[@]}" -o BatchMode=yes -o ConnectTimeout=10 "${SSH_USER}@${HOST}")
# Prefer public resolvers: local stub (127.0.0.53) is flaky from some WSL setups
DIG_OPTS=(+time=3 +tries=2 +short)
DIG_SERVERS=(1.1.1.1 8.8.8.8)
FAILURES=0
section() { printf '\n===== %s =====\n' "$1"; }
fail() {
printf 'FAIL: %s\n' "$1" >&2
FAILURES=$((FAILURES + 1))
}
# Strip dig commentary (timeouts are printed on stdout as ";; ...")
dig_answers_only() {
grep -v '^;;' | grep -v '^$' || true
}
# dig_short <name> [type] — tries each public resolver; skips timeout noise
dig_short() {
local name="$1"
local typ="${2:-A}"
local server out
for server in "${DIG_SERVERS[@]}"; do
out="$(dig @"${server}" "${DIG_OPTS[@]}" "$name" "$typ" 2>/dev/null | dig_answers_only)" || out=""
if [[ -n "$out" ]]; then
printf '%s\n' "$out"
return 0
fi
done
return 1
}
# dig_ptr <ip>
dig_ptr() {
local ip="$1"
local server out
for server in "${DIG_SERVERS[@]}"; do
out="$(dig @"${server}" "${DIG_OPTS[@]}" -x "$ip" 2>/dev/null | dig_answers_only)" || out=""
if [[ -n "$out" ]]; then
printf '%s\n' "$out"
return 0
fi
done
return 1
}
check_ptrs() {
local ip ptr
while read -r ip; do
[[ -z "$ip" ]] && continue
ptr="$(dig_ptr "$ip" || true)"
printf 'PTR %s -> %s\n' "$ip" "${ptr:-<none>}"
grep -Fiq "${HOST}" <<<"$ptr" || fail "PTR for ${ip} does not mention ${HOST}"
done
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "missing required command: $1" >&2
exit 127
}
}
need_cmd ssh
need_cmd curl
need_cmd openssl
need_cmd dig
need_cmd timeout
section "SSH + compose ps"
PS_OUT="$("${SSH[@]}" 'cd /opt/mail && docker compose ps -a')" || {
fail "ssh or docker compose ps"
echo "Cannot continue without SSH/compose." >&2
exit 1
}
printf '%s\n' "$PS_OUT"
if grep -qiE 'Exited|Restarting|[[:space:]]Dead[[:space:]]' <<<"$PS_OUT"; then
fail "compose has Exited/Restarting/Dead containers"
fi
for svc in nginx-mailcow postfix-mailcow dovecot-mailcow mysql-mailcow; do
grep -Fq "$svc" <<<"$PS_OUT" || fail "missing service ${svc}"
done
section "Watchdog (tail)"
if ! "${SSH[@]}" 'cd /opt/mail && docker compose logs --tail=40 watchdog-mailcow'; then
fail "watchdog logs"
fi
section "Mail queue"
QUEUE_OUT="$("${SSH[@]}" 'cd /opt/mail && docker compose exec -T postfix-mailcow postqueue -p' 2>&1)" || {
fail "postqueue"
QUEUE_OUT=""
}
printf '%s\n' "$QUEUE_OUT"
if [[ -n "$QUEUE_OUT" ]] && ! grep -Fiq 'Mail queue is empty' <<<"$QUEUE_OUT"; then
fail "mail queue not empty"
fi
section "Listeners"
LISTEN_OUT="$("${SSH[@]}" 'ss -tlnp 2>/dev/null | grep -E ":(25|465|587|110|143|993|995|80|443|4190)[[:space:]]" || true')"
printf '%s\n' "$LISTEN_OUT"
for p in 25 465 587 110 143 993 995 80 443 4190; do
if ! grep -qE ":${p}[[:space:]]" <<<"$LISTEN_OUT"; then
fail "port ${p} not listening on host"
fi
done
section "HTTP / HTTPS"
HTTP_OUT="$(curl "${CURL_AF_OPTS[@]}" -sS -I --max-time 10 "http://${HOST}/" 2>&1 | head -8)" || true
printf '%s\n' "$HTTP_OUT"
echo "---"
# No -k: surface TLS trust problems
HTTPS_OUT="$(curl "${CURL_AF_OPTS[@]}" -sS -I --max-time 10 "https://${HOST}/" 2>&1 | head -15)" || true
printf '%s\n' "$HTTPS_OUT"
grep -qE '^HTTP/[0-9.]+ 301' <<<"$HTTP_OUT" || fail "HTTP did not redirect (expect 301)"
grep -qE '^HTTP/[0-9.]+ 200' <<<"$HTTPS_OUT" || fail "HTTPS did not return 200"
section "TLS cert (443)"
CERT_OUT="$(
timeout 15 openssl s_client "${OPENSSL_AF_OPTS[@]}" -connect "${HOST}:443" -servername "${HOST}" </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates 2>/dev/null
)" || true
printf '%s\n' "$CERT_OUT"
if ! grep -Fq "CN=${HOST}" <<<"$CERT_OUT" && ! grep -Fq "CN = ${HOST}" <<<"$CERT_OUT"; then
fail "TLS subject missing ${HOST}"
fi
grep -Fiq "Let's Encrypt" <<<"$CERT_OUT" || fail "TLS issuer not Let's Encrypt"
section "SMTP banner"
# Connect via A record so /dev/tcp does not pick unreachable AAAA first
SMTP_IP="$(dig_short "${HOST}" A 2>/dev/null | head -n1 || true)"
if [[ -z "$SMTP_IP" ]]; then
fail "cannot resolve A for SMTP check"
SMTP_OUT=""
else
SMTP_OUT="$(
timeout 8 bash -c "exec 3<>/dev/tcp/${SMTP_IP}/25; printf 'EHLO test.local\r\nQUIT\r\n' >&3; cat <&3" 2>/dev/null \
| head -20
)" || true
fi
printf 'connect %s:25\n' "${SMTP_IP:-?}"
printf '%s\n' "$SMTP_OUT"
grep -qiE 'Postcow|ESMTP' <<<"$SMTP_OUT" || fail "SMTP banner unexpected"
section "DNS"
A_REC="$(dig_short "${HOST}" A || true)"
AAAA_REC="$(dig_short "${HOST}" AAAA || true)"
printf 'A: %s\n' "${A_REC:-<none>}"
printf 'AAAA: %s\n' "${AAAA_REC:-<none>}"
[[ -n "$A_REC" ]] || fail "missing A record"
[[ -n "$AAAA_REC" ]] || fail "missing AAAA record"
check_ptrs <<<"$A_REC"
check_ptrs <<<"$AAAA_REC"
MX_REC="$(dig_short windy.me MX || true)"
printf 'MX:\n%s\n' "${MX_REC:-<none>}"
grep -Fiq "${HOST}" <<<"$MX_REC" || fail "windy.me MX missing ${HOST}"
SPF="$(dig_short windy.me TXT || true)"
printf 'SPF:\n'
if ! grep -Fi 'v=spf1' <<<"$SPF"; then
fail "no SPF TXT on windy.me"
fi
section "Done"
if [[ "$FAILURES" -gt 0 ]]; then
echo "Health check finished with ${FAILURES} failure(s)."
exit 1
fi
echo "Health check OK. Update hosts/mx2.windy.me.md Verified line if desired."
exit 0