mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 21:13:00 +08:00
214 lines
6.9 KiB
Go
214 lines
6.9 KiB
Go
package openwrt
|
|
|
|
import (
|
|
"cmp"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// UCI section types. These are what identifies a radio or a client, not the
|
|
// section's name.
|
|
const (
|
|
SectionWifiDevice = "wifi-device"
|
|
SectionWifiIface = "wifi-iface"
|
|
)
|
|
|
|
// ManagedMarker is the option this project writes on the wireless sections it
|
|
// owns. The spelling is deliberately unchanged from 1.x: an installed router
|
|
// already has sections marked this way, and renaming the marker would make this
|
|
// program stop recognising its own client and create a second one beside it.
|
|
const ManagedMarker = "jxnu_auto"
|
|
|
|
// APSelectionOption records the AP-selection policy on a managed client.
|
|
const APSelectionOption = "smart_srun_ap_selection"
|
|
|
|
// Radio is one wifi-device: the physical radio a client can be attached to.
|
|
type Radio struct {
|
|
Name string
|
|
Band string
|
|
Channel string
|
|
HTMode string
|
|
Path string
|
|
}
|
|
|
|
// Station is one wifi-iface in client mode.
|
|
type Station struct {
|
|
Section string
|
|
Radio string
|
|
SSID string
|
|
BSSID string
|
|
Encryption string
|
|
Network string
|
|
Disabled bool
|
|
Anonymous bool
|
|
|
|
// Managed reports that this section belongs to this project. Everything
|
|
// else on the radio is somebody's home network and must come back
|
|
// unchanged.
|
|
Managed bool
|
|
APSelection string
|
|
}
|
|
|
|
// AccessPoint is one wifi-iface in AP mode -- a network this router serves.
|
|
//
|
|
// This program never configures one. They are read so that a change can be
|
|
// checked against them afterwards: spec 04 forbids touching the home AP, and
|
|
// proving that requires knowing what it looked like before.
|
|
type AccessPoint struct {
|
|
Section string
|
|
Radio string
|
|
SSID string
|
|
Encryption string
|
|
Network string
|
|
Disabled bool
|
|
}
|
|
|
|
// Radios lists the radios in a parsed wireless configuration.
|
|
//
|
|
// Selection is by section type. mac80211 names them radio0 and radio1, but
|
|
// MediaTek's closed mtwifi driver on MT798x boards names them MT7981_1_1 and
|
|
// MT7981_1_2, and matching on the name finds no radios there at all. The band
|
|
// comes from `band`, or from the older `hwmode` when a firmware still writes
|
|
// that -- and only from a section that really is a wifi-device, so a wifi-iface
|
|
// with an option of the same name cannot invent a radio.
|
|
func Radios(config UCIConfig) []Radio {
|
|
sections := config.SectionsOfType(SectionWifiDevice)
|
|
radios := make([]Radio, 0, len(sections))
|
|
for _, section := range sections {
|
|
radio := Radio{
|
|
Name: section.Name,
|
|
Band: strings.ToLower(section.Get("band")),
|
|
Channel: section.Get("channel"),
|
|
HTMode: section.Get("htmode"),
|
|
Path: section.Get("path"),
|
|
}
|
|
if radio.Band == "" {
|
|
radio.Band = bandFromHWMode(section.Get("hwmode"))
|
|
}
|
|
radios = append(radios, radio)
|
|
}
|
|
return radios
|
|
}
|
|
|
|
// bandFromHWMode maps the pre-band spelling. "11a" and "11na" are 5 GHz;
|
|
// everything else that firmware wrote was 2.4.
|
|
func bandFromHWMode(hwmode string) string {
|
|
mode := strings.ToLower(strings.TrimSpace(hwmode))
|
|
if mode == "" {
|
|
return ""
|
|
}
|
|
if strings.Contains(mode, "a") {
|
|
return "5g"
|
|
}
|
|
return "2g"
|
|
}
|
|
|
|
// Stations lists the client sections, in file order.
|
|
func Stations(config UCIConfig) []Station {
|
|
var stations []Station
|
|
for _, section := range config.SectionsOfType(SectionWifiIface) {
|
|
if !strings.EqualFold(strings.TrimSpace(section.Get("mode")), "sta") {
|
|
continue
|
|
}
|
|
stations = append(stations, Station{
|
|
Section: section.Name,
|
|
Radio: section.Get("device"),
|
|
SSID: section.Get("ssid"),
|
|
BSSID: strings.ToLower(section.Get("bssid")),
|
|
Encryption: section.Get("encryption"),
|
|
Network: firstNetwork(section.Get("network")),
|
|
Disabled: section.Get("disabled") == "1",
|
|
Anonymous: section.Anonymous,
|
|
Managed: section.Get(ManagedMarker) == "1",
|
|
APSelection: section.Get(APSelectionOption),
|
|
})
|
|
}
|
|
return stations
|
|
}
|
|
|
|
// AccessPoints lists the AP sections, in file order.
|
|
func AccessPoints(config UCIConfig) []AccessPoint {
|
|
var points []AccessPoint
|
|
for _, section := range config.SectionsOfType(SectionWifiIface) {
|
|
if !strings.EqualFold(strings.TrimSpace(section.Get("mode")), "ap") {
|
|
continue
|
|
}
|
|
points = append(points, AccessPoint{
|
|
Section: section.Name,
|
|
Radio: section.Get("device"),
|
|
SSID: section.Get("ssid"),
|
|
Encryption: section.Get("encryption"),
|
|
Network: firstNetwork(section.Get("network")),
|
|
Disabled: section.Get("disabled") == "1",
|
|
})
|
|
}
|
|
return points
|
|
}
|
|
|
|
// firstNetwork takes the first name from a `network` option, which UCI allows
|
|
// to list several.
|
|
func firstNetwork(value string) string {
|
|
fields := strings.Fields(value)
|
|
if len(fields) == 0 {
|
|
return ""
|
|
}
|
|
return fields[0]
|
|
}
|
|
|
|
// ManagedStations narrows to the sections this project owns.
|
|
//
|
|
// A section counts as owned when it carries the marker, or when its SSID is one
|
|
// the configuration names. The SSID rule is what adopts a client the user
|
|
// created by hand for the campus network before installing this, instead of
|
|
// building a second one beside it and having two clients fight over the radio.
|
|
// It is also the rule that must not be loosened: a home network whose SSID is
|
|
// not in the configuration is never adopted.
|
|
func ManagedStations(config UCIConfig, preferredSection string, knownSSIDs []string) []Station {
|
|
known := make(map[string]struct{}, len(knownSSIDs))
|
|
for _, ssid := range knownSSIDs {
|
|
if trimmed := strings.TrimSpace(ssid); trimmed != "" {
|
|
known[trimmed] = struct{}{}
|
|
}
|
|
}
|
|
|
|
var managed []Station
|
|
for _, station := range Stations(config) {
|
|
_, namedInConfig := known[strings.TrimSpace(station.SSID)]
|
|
if station.Managed || namedInConfig || station.Section == preferredSection {
|
|
station.Managed = true
|
|
managed = append(managed, station)
|
|
}
|
|
}
|
|
return managed
|
|
}
|
|
|
|
// StationOnRadio picks the managed client attached to one radio.
|
|
//
|
|
// More than one is an ambiguity, not a tie to break: two managed clients on the
|
|
// same radio mean an earlier run left one behind, and picking either could
|
|
// disable a working uplink. The caller is told to resolve it. The result is
|
|
// otherwise deterministic -- sorted by section name -- so the same
|
|
// configuration always produces the same answer.
|
|
func StationOnRadio(stations []Station, radio string) (Station, []Station) {
|
|
var candidates []Station
|
|
for _, station := range stations {
|
|
if station.Radio == radio {
|
|
candidates = append(candidates, station)
|
|
}
|
|
}
|
|
slices.SortFunc(candidates, func(a, b Station) int {
|
|
return cmp.Compare(a.Section, b.Section)
|
|
})
|
|
if len(candidates) == 1 {
|
|
return candidates[0], nil
|
|
}
|
|
return Station{}, candidates
|
|
}
|
|
|
|
// Deliberately absent: matching a configured encryption against what a scan
|
|
// reports. Spec 07 puts "加密匹配" in M09 along with AP selection and the
|
|
// association check, and the useful part of that rule -- never joining an open
|
|
// network because it shares a name with the protected one -- needs the scan
|
|
// results M09 introduces to mean anything. Writing the taxonomy here would put
|
|
// a guess in the tree that M09 would inherit as if it had been verified.
|