mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
314 lines
11 KiB
Go
314 lines
11 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/openwrt"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/wifi"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/wireless"
|
|
)
|
|
|
|
type WifiSetupParams struct {
|
|
Job string `json:"job"`
|
|
Session string `json:"session,omitempty"`
|
|
SSID string `json:"ssid"`
|
|
Key string `json:"key,omitempty"`
|
|
Encryption string `json:"encryption,omitempty"`
|
|
Iface string `json:"iface,omitempty"`
|
|
Radio string `json:"radio,omitempty"`
|
|
}
|
|
|
|
type wizardConnection struct {
|
|
Iface, Radio, Section, SSID, Encryption, Key string
|
|
Reused bool
|
|
}
|
|
|
|
func validateWifiSetup(p WifiSetupParams) error {
|
|
if len(p.Job) != 32 || strings.Trim(p.Job, "0123456789abcdef") != "" {
|
|
return domain.Errorf(domain.CodeInvalidArgument, "无线任务编号无效")
|
|
}
|
|
if p.SSID == "" || len(p.SSID) > 32 || strings.ContainsAny(p.SSID+p.Key, "\x00\r\n") || len(p.Key) > 64 {
|
|
return domain.Errorf(domain.CodeInvalidArgument, "无线名称或密码格式无效")
|
|
}
|
|
if p.Radio != "" && !openwrt.IsLogicalInterfaceName(p.Radio) || p.Iface != "" && !openwrt.IsLogicalInterfaceName(p.Iface) {
|
|
return domain.Errorf(domain.CodeInvalidArgument, "无线电或接口名称无效")
|
|
}
|
|
if !slices.Contains([]string{"", "auto", "none", "psk", "psk2", "psk-mixed", "sae", "sae-mixed"}, p.Encryption) {
|
|
return domain.Errorf(domain.CodeUnsupportedCapability, "向导仅支持开放网络及 WPA/WPA2/WPA3 个人网络")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setupKeyValid(encryption, key string) bool {
|
|
if encryption == "none" {
|
|
return key == ""
|
|
}
|
|
if encryption == "sae" {
|
|
return len(key) >= 1 && len(key) <= 63
|
|
}
|
|
if len(key) >= 8 && len(key) <= 63 {
|
|
return true
|
|
}
|
|
if len(key) == 64 && encryption != "sae-mixed" {
|
|
_, err := hex.DecodeString(key)
|
|
return err == nil
|
|
}
|
|
return false
|
|
}
|
|
|
|
func scanEncryption(p WifiSetupParams, found openwrt.ScanResult) string {
|
|
offered := wifi.ClassifyScan(found.Encryption.Enabled, found.Encryption.Authentication, found.Encryption.WPA)
|
|
if p.Encryption != "" && p.Encryption != "auto" {
|
|
if !wifi.ParseSecurity(p.Encryption).Accepts(offered) {
|
|
return ""
|
|
}
|
|
// PSK's security class spans WPA1/2; an explicitly chosen version must
|
|
// actually be advertised by this AP.
|
|
if p.Encryption == "psk2" && !slices.Contains(found.Encryption.WPA, 2) || p.Encryption == "psk" && !slices.Contains(found.Encryption.WPA, 1) {
|
|
return ""
|
|
}
|
|
return p.Encryption
|
|
}
|
|
if p.Key == "" && offered == wifi.SecurityOpen && !found.Encryption.Enabled {
|
|
return "none"
|
|
}
|
|
if p.Key != "" {
|
|
if offered.Accepts(wifi.SecuritySAE) {
|
|
return "sae"
|
|
}
|
|
if offered.Accepts(wifi.SecurityPSK) {
|
|
if slices.Contains(found.Encryption.WPA, 2) {
|
|
return "psk2"
|
|
}
|
|
if slices.Contains(found.Encryption.WPA, 1) {
|
|
return "psk"
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// wizardPlan never rewrites an AP, LAN, or an unrelated enabled station. It
|
|
// first reuses a verified existing association, then scans eligible radios.
|
|
func (w *deviceWireless) wizardPlan(ctx context.Context, p WifiSetupParams) (wizardConnection, []wireless.PackagePlan, error) {
|
|
if err := validateWifiSetup(p); err != nil {
|
|
return wizardConnection{}, nil, err
|
|
}
|
|
uc, err := w.adapter.UCI(ctx, "wireless")
|
|
if err != nil {
|
|
return wizardConnection{}, nil, err
|
|
}
|
|
radios, err := w.adapter.WirelessStatus(ctx)
|
|
if err != nil {
|
|
return wizardConnection{}, nil, err
|
|
}
|
|
var reusable []wizardConnection
|
|
for _, station := range openwrt.Stations(uc) {
|
|
section, _ := uc.Section(station.Section)
|
|
if station.Disabled || station.SSID != p.SSID || p.Radio != "" && p.Radio != station.Radio || p.Iface != "" && p.Iface != station.Network {
|
|
continue
|
|
}
|
|
encryption := station.Encryption
|
|
if encryption == "" {
|
|
encryption = "none"
|
|
}
|
|
if p.Encryption != "" && p.Encryption != "auto" && p.Encryption != encryption || section.Get("key") != p.Key {
|
|
continue
|
|
}
|
|
connection := wizardConnection{Iface: station.Network, Radio: station.Radio, Section: station.Section, SSID: p.SSID, Encryption: encryption, Key: p.Key, Reused: true}
|
|
if w.wizardAssociated(ctx, connection) {
|
|
reusable = append(reusable, connection)
|
|
}
|
|
}
|
|
if len(reusable) == 1 {
|
|
return reusable[0], nil, nil
|
|
}
|
|
if len(reusable) > 1 {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeConflict, "多个无线出口已连接同名网络,请明确选择无线电和接口")
|
|
}
|
|
|
|
iface := p.Iface
|
|
if iface == "" {
|
|
iface = w.settings.Snapshot().STAIface
|
|
}
|
|
if iface == "" {
|
|
iface = "wwan"
|
|
}
|
|
if !openwrt.IsLogicalInterfaceName(iface) || slices.Contains([]string{"lan", "wan", "wan6", "loopback"}, iface) {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeConflict, "临时无线连接需要独立的 DHCP 客户端接口")
|
|
}
|
|
for _, account := range w.settings.Snapshot().CampusAccounts {
|
|
if account.IsWired() && account.WiredIface == iface {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeConflict, "所选接口已用于有线账号")
|
|
}
|
|
}
|
|
type choice struct {
|
|
connection wizardConnection
|
|
signal int
|
|
bssid string
|
|
}
|
|
var choices []choice
|
|
sawOpen, sawProtected := false, false
|
|
for _, radio := range openwrt.Radios(uc) {
|
|
if p.Radio != "" && p.Radio != radio.Name {
|
|
continue
|
|
}
|
|
sectionName := stationSection(radio.Name)
|
|
if existing, ok := uc.Section(sectionName); ok && (existing.Type != openwrt.SectionWifiIface || existing.Get("mode") != "sta" || existing.Get(openwrt.ManagedMarker) != "1") {
|
|
continue
|
|
}
|
|
busy := false
|
|
for _, station := range openwrt.Stations(uc) {
|
|
if !station.Disabled && (station.Radio == radio.Name || station.Network == iface) && !(station.Section == sectionName && station.Managed && station.SSID == p.SSID) {
|
|
busy = true
|
|
}
|
|
}
|
|
if busy {
|
|
continue
|
|
}
|
|
live, ok := openwrt.FindRadio(radios, radio.Name)
|
|
if !ok || live.Disabled || live.Pending {
|
|
continue
|
|
}
|
|
device, ok := live.AnyDevice()
|
|
if !ok {
|
|
continue
|
|
}
|
|
results, err := w.adapter.Scan(ctx, device)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, found := range results {
|
|
if found.SSID != p.SSID || !found.Joinable() {
|
|
continue
|
|
}
|
|
if found.Encryption.Enabled {
|
|
sawProtected = true
|
|
} else {
|
|
sawOpen = true
|
|
}
|
|
encryption := scanEncryption(p, found)
|
|
if encryption == "" || !setupKeyValid(encryption, p.Key) {
|
|
continue
|
|
}
|
|
choices = append(choices, choice{wizardConnection{Iface: iface, Radio: radio.Name, Section: sectionName, SSID: p.SSID, Encryption: encryption, Key: p.Key}, found.Signal, found.BSSID})
|
|
}
|
|
}
|
|
if (p.Encryption == "" || p.Encryption == "auto") && p.Key == "" && sawOpen && sawProtected {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeConflict, "发现同名开放和加密网络,请明确选择加密方式")
|
|
}
|
|
if len(choices) == 0 {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeNotFound, "没有可安全连接的匹配网络,请检查无线电、加密方式和密码;不会替换其他活动连接")
|
|
}
|
|
slices.SortStableFunc(choices, func(a, b choice) int {
|
|
if a.signal != b.signal {
|
|
return b.signal - a.signal
|
|
}
|
|
return strings.Compare(a.connection.Radio+a.bssid, b.connection.Radio+b.bssid)
|
|
})
|
|
connection := choices[0].connection
|
|
network, err := w.adapter.UCI(ctx, "network")
|
|
if err != nil {
|
|
return wizardConnection{}, nil, err
|
|
}
|
|
interfaceSection, exists := network.Section(iface)
|
|
if exists && (interfaceSection.Type != "interface" || interfaceSection.Get("proto") != "dhcp" || interfaceSection.Get("device") != "" || interfaceSection.Get("ifname") != "") {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeConflict, "所选接口已有其他网络配置,未予修改")
|
|
}
|
|
for _, section := range uc.SectionsOfType(openwrt.SectionWifiIface) {
|
|
if section.Name != connection.Section && slices.Contains(strings.Fields(section.Get("network")), iface) {
|
|
return wizardConnection{}, nil, domain.Errorf(domain.CodeConflict, "所选接口已被其他无线配置使用")
|
|
}
|
|
}
|
|
firewall, err := w.adapter.UCIExport(ctx, "firewall")
|
|
if err != nil {
|
|
return wizardConnection{}, nil, err
|
|
}
|
|
fwChanges, err := wizardFirewall(firewall, iface)
|
|
if err != nil {
|
|
return wizardConnection{}, nil, err
|
|
}
|
|
var plans []wireless.PackagePlan
|
|
if !exists {
|
|
plans = append(plans, wireless.PackagePlan{Package: "network", Changes: []wireless.Change{
|
|
{Key: wireless.Key{Section: iface}, Text: "interface"}, {Key: wireless.Key{Section: iface, Option: "proto"}, Text: "dhcp"},
|
|
}})
|
|
}
|
|
if len(fwChanges) > 0 {
|
|
plans = append(plans, wireless.PackagePlan{Package: "firewall", Changes: fwChanges})
|
|
}
|
|
section := connection.Section
|
|
changes := []wireless.Change{{Key: wireless.Key{Section: section}, Text: openwrt.SectionWifiIface}}
|
|
for _, pair := range [][2]string{{"device", connection.Radio}, {"mode", "sta"}, {"network", iface}, {"ssid", connection.SSID}, {"encryption", connection.Encryption}, {"disabled", "0"}, {openwrt.ManagedMarker, "1"}, {openwrt.APSelectionOption, "auto"}} {
|
|
changes = append(changes, wireless.Change{Key: wireless.Key{Section: section, Option: pair[0]}, Text: pair[1]})
|
|
}
|
|
changes = append(changes, wireless.Change{Key: wireless.Key{Section: section, Option: "bssid"}, Delete: true}, wireless.Change{Key: wireless.Key{Section: section, Option: "key"}, Text: connection.Key, Delete: connection.Key == ""})
|
|
plans = append(plans, wireless.PackagePlan{Package: "wireless", Changes: changes})
|
|
return connection, plans, nil
|
|
}
|
|
|
|
func wizardFirewall(cfg openwrt.UCIConfig, iface string) ([]wireless.Change, error) {
|
|
var wan []openwrt.UCISection
|
|
attached := false
|
|
for _, zone := range cfg.SectionsOfType("zone") {
|
|
if zone.Get("name") == "wan" {
|
|
wan = append(wan, zone)
|
|
}
|
|
value, _ := zone.Lookup("network")
|
|
networks := strings.Fields(value.Text)
|
|
if value.IsList {
|
|
networks = value.List
|
|
}
|
|
if slices.Contains(networks, iface) {
|
|
if zone.Get("name") != "wan" {
|
|
return nil, domain.Errorf(domain.CodeConflict, "无线接口已属于其他防火墙区域")
|
|
}
|
|
attached = true
|
|
}
|
|
}
|
|
forwarded := false
|
|
for _, rule := range cfg.SectionsOfType("forwarding") {
|
|
if rule.Get("src") == "lan" && rule.Get("dest") == "wan" && rule.Get("enabled") != "0" {
|
|
forwarded = true
|
|
}
|
|
}
|
|
if len(wan) != 1 || wan[0].Get("input") != "REJECT" || wan[0].Get("masq") != "1" || !forwarded {
|
|
return nil, domain.Errorf(domain.CodeConflict, "现有 WAN 防火墙不满足临时连接条件,请先检查 WAN 隔离、转发和地址转换设置")
|
|
}
|
|
if attached {
|
|
return nil, nil
|
|
}
|
|
value, _ := wan[0].Lookup("network")
|
|
members := strings.Fields(value.Text)
|
|
if value.IsList {
|
|
members = slices.Clone(value.List)
|
|
}
|
|
members = append(members, iface)
|
|
return []wireless.Change{wireless.ListChange(wireless.Key{Section: wan[0].Name, Option: "network"}, members...)}, nil
|
|
}
|
|
|
|
func (w *deviceWireless) wizardAssociated(ctx context.Context, c wizardConnection) bool {
|
|
radios, err := w.adapter.WirelessStatus(ctx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
radio, ok := openwrt.FindRadio(radios, c.Radio)
|
|
if !ok {
|
|
return false
|
|
}
|
|
station, ok := radio.FindInterface(c.Section)
|
|
if !ok || !station.Station() || !slices.Contains(station.Network, c.Iface) || station.IfName == "" {
|
|
return false
|
|
}
|
|
info, err := w.adapter.RadioInfo(ctx, station.IfName)
|
|
if err != nil || !info.Associated() || info.SSID != c.SSID || info.Encrypted != (c.Encryption != "none") {
|
|
return false
|
|
}
|
|
status, err := w.adapter.InterfaceStatus(ctx, c.Iface)
|
|
return err == nil && status.Up && len(status.IPv4) > 0 && status.L3Device == station.IfName
|
|
}
|