Skip to content

Commit

Permalink
Implement configuration of Vivid-Hosting access point.
Browse files Browse the repository at this point in the history
  • Loading branch information
patfair committed Oct 11, 2023
1 parent d16c284 commit 4c2c4d1
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 29 deletions.
64 changes: 42 additions & 22 deletions network/access_point.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright 2017 Team 254. All Rights Reserved.
// Author: [email protected] (Patrick Fairbank)
//
// Methods for configuring a Linksys WRT1900ACS access point running OpenWRT for team SSIDs and VLANs.
// Methods for configuring a Linksys WRT1900ACS or Vivid-Hosting VH-109 access point running OpenWRT for team SSIDs and
// VLANs.

package network

Expand All @@ -20,10 +21,10 @@ import (
const (
accessPointSshPort = 22
accessPointConnectTimeoutSec = 1
accessPointCommandTimeoutSec = 5
accessPointCommandTimeoutSec = 30
accessPointPollPeriodSec = 3
accessPointRequestBufferSize = 10
accessPointConfigRetryIntervalSec = 5
accessPointConfigRetryIntervalSec = 30
)

type AccessPoint struct {
Expand Down Expand Up @@ -101,12 +102,16 @@ func (ap *AccessPoint) handleTeamWifiConfiguration(teams [6]*model.Team) {
return
}

if ap.configIsCorrectForTeams(teams) {
err := ap.updateTeamWifiStatuses()
if err == nil && ap.configIsCorrectForTeams(teams) {
log.Printf("WiFi configuration is already correct; skipping configuration cycle.")
return
}

// Clear the state of the radio before loading teams.
ap.configureTeams([6]*model.Team{nil, nil, nil, nil, nil, nil})
if !ap.isVividType {
// Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise.
ap.configureTeams([6]*model.Team{nil, nil, nil, nil, nil, nil})
}
ap.configureTeams(teams)
}

Expand All @@ -116,12 +121,13 @@ func (ap *AccessPoint) configureTeams(teams [6]*model.Team) {
for {
teamIndex := 0
for teamIndex < 6 {
config, err := generateTeamAccessPointConfig(teams[teamIndex], teamIndex+1)
config, err := ap.generateTeamAccessPointConfig(teams[teamIndex], teamIndex+1)
if err != nil {
log.Printf("Failed to generate WiFi configuration: %v", err)
}

command := addConfigurationHeader(config)
log.Printf("Configuring access point with command: %s\n", command)

_, err = ap.runCommand(command)
if err != nil {
Expand All @@ -133,7 +139,9 @@ func (ap *AccessPoint) configureTeams(teams [6]*model.Team) {

teamIndex++
}
time.Sleep(time.Second * accessPointConfigRetryIntervalSec)

_, _ = ap.runCommand("uci commit wireless")
_, _ = ap.runCommand("wifi reload")
err := ap.updateTeamWifiStatuses()
if err == nil && ap.configIsCorrectForTeams(teams) {
log.Printf("Successfully configured WiFi after %d attempts.", retryCount)
Expand Down Expand Up @@ -170,6 +178,7 @@ func (ap *AccessPoint) updateTeamWifiStatuses() error {

output, err := ap.runCommand("iwinfo")
if err == nil {
log.Printf("Access point status: %s\n", output)
err = decodeWifiInfo(output, ap.TeamWifiStatuses[:])
}

Expand Down Expand Up @@ -217,31 +226,37 @@ func (ap *AccessPoint) runCommand(command string) (string, error) {
}

func addConfigurationHeader(commandList string) string {
return fmt.Sprintf("uci batch <<ENDCONFIG && wifi radio0\n%s\ncommit wireless\nENDCONFIG\n", commandList)
return fmt.Sprintf("uci batch <<ENDCONFIG\n%s\nENDCONFIG\n", commandList)
}

// Verifies WPA key validity and produces the configuration command for the given team.
func generateTeamAccessPointConfig(team *model.Team, position int) (string, error) {
func (ap *AccessPoint) generateTeamAccessPointConfig(team *model.Team, position int) (string, error) {
if position < 1 || position > 6 {
return "", fmt.Errorf("invalid team position %d", position)
}

commands := &[]string{}
var ssid, key string
if team == nil {
*commands = append(*commands, fmt.Sprintf("set wireless.@wifi-iface[%d].disabled='0'", position),
fmt.Sprintf("set wireless.@wifi-iface[%d].ssid='no-team-%d'", position, position),
fmt.Sprintf("set wireless.@wifi-iface[%d].key='no-team-%d'", position, position))
ssid = fmt.Sprintf("no-team-%d", position)
key = fmt.Sprintf("no-team-%d", position)
} else {
if len(team.WpaKey) < 8 || len(team.WpaKey) > 63 {
return "", fmt.Errorf("invalid WPA key '%s' configured for team %d", team.WpaKey, team.Id)
}
ssid = strconv.Itoa(team.Id)
key = team.WpaKey
}

*commands = append(*commands, fmt.Sprintf("set wireless.@wifi-iface[%d].disabled='0'", position),
fmt.Sprintf("set wireless.@wifi-iface[%d].ssid='%d'", position, team.Id),
fmt.Sprintf("set wireless.@wifi-iface[%d].key='%s'", position, team.WpaKey))
commands := []string{
fmt.Sprintf("set wireless.@wifi-iface[%d].disabled='0'", position),
fmt.Sprintf("set wireless.@wifi-iface[%d].ssid='%s'", position, ssid),
fmt.Sprintf("set wireless.@wifi-iface[%d].key='%s'", position, key),
}
if ap.isVividType {
commands = append(commands, fmt.Sprintf("set wireless.@wifi-iface[%d].sae_password='%s'", position, key))
}

return strings.Join(*commands, "\n"), nil
return strings.Join(commands, "\n"), nil
}

// Parses the given output from the "iwinfo" command on the AP and updates the given status structure with the result.
Expand All @@ -266,16 +281,21 @@ func decodeWifiInfo(wifiInfo string, statuses []TeamWifiStatus) error {
return nil
}

// Polls the 6 wlans on the ap for bandwith use and updates data structure.
// Polls the 6 wlans on the ap for bandwidth use and updates data structure.
func (ap *AccessPoint) updateTeamWifiBTU() error {
if !ap.networkSecurityEnabled {
return nil
}

infWifi := []string{"0", "0-1", "0-2", "0-3", "0-4", "0-5"}
for i := range ap.TeamWifiStatuses {
var infWifi []string
if ap.isVividType {
infWifi = []string{"1", "11", "12", "13", "14", "15"}
} else {
infWifi = []string{"0", "0-1", "0-2", "0-3", "0-4", "0-5"}
}

output, err := ap.runCommand(fmt.Sprintf("luci-bwc -i wlan%s", infWifi[i]))
for i := range ap.TeamWifiStatuses {
output, err := ap.runCommand(fmt.Sprintf("luci-bwc -i ath%s", infWifi[i]))
if err == nil {
btu := parseBtu(output)
ap.TeamWifiStatuses[i].MBits = btu
Expand Down
78 changes: 73 additions & 5 deletions network/access_point_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,32 @@ import (
"github.com/stretchr/testify/assert"
)

func TestGenerateTeamAccessPointConfig(t *testing.T) {
func TestGenerateTeamAccessPointConfigForLinksys(t *testing.T) {
model.BaseDir = ".."
ap := AccessPoint{isVividType: false}

ifaceRe := regexp.MustCompile("^set wireless\\.@wifi-iface\\[(\\d)\\]\\.")
disabledRe := regexp.MustCompile("disabled='([-\\w ]+)'")
ssidRe := regexp.MustCompile("ssid='([-\\w ]*)'")
wpaKeyRe := regexp.MustCompile("key='([-\\w ]*)'")

// Should reject invalid positions.
for _, position := range []int{-1, 0, 7, 8, 254} {
_, err := generateTeamAccessPointConfig(nil, position)
_, err := ap.generateTeamAccessPointConfig(nil, position)
if assert.NotNil(t, err) {
assert.Equal(t, err.Error(), fmt.Sprintf("invalid team position %d", position))
}
}

// Should configure dummy values for all team SSIDs if there are no teams.
for position := 1; position <= 6; position++ {
config, _ := generateTeamAccessPointConfig(nil, position)
config, _ := ap.generateTeamAccessPointConfig(nil, position)
ifaces := ifaceRe.FindAllStringSubmatch(config, -1)
disableds := disabledRe.FindAllStringSubmatch(config, -1)
ssids := ssidRe.FindAllStringSubmatch(config, -1)
wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1)
if assert.Equal(t, 1, len(disableds)) && assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) {
assert.Equal(t, strconv.Itoa(position), ifaces[0][1])
assert.Equal(t, "0", disableds[0][1])
assert.Equal(t, fmt.Sprintf("no-team-%d", position), ssids[0][1])
assert.Equal(t, fmt.Sprintf("no-team-%d", position), wpaKeys[0][1])
Expand All @@ -45,17 +49,81 @@ func TestGenerateTeamAccessPointConfig(t *testing.T) {
// Should configure a different SSID for each team.
for position := 1; position <= 6; position++ {
team := &model.Team{Id: 254 + position, WpaKey: fmt.Sprintf("aaaaaaa%d", position)}
config, _ := generateTeamAccessPointConfig(team, position)
config, _ := ap.generateTeamAccessPointConfig(team, position)
ifaces := ifaceRe.FindAllStringSubmatch(config, -1)
disableds := disabledRe.FindAllStringSubmatch(config, -1)
ssids := ssidRe.FindAllStringSubmatch(config, -1)
wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1)
if assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) {
assert.Equal(t, strconv.Itoa(position), ifaces[0][1])
assert.Equal(t, "0", disableds[0][1])
assert.Equal(t, strconv.Itoa(team.Id), ssids[0][1])
assert.Equal(t, fmt.Sprintf("aaaaaaa%d", position), wpaKeys[0][1])
}
}

// Should reject a missing WPA key.
_, err := ap.generateTeamAccessPointConfig(&model.Team{Id: 254}, 4)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "invalid WPA key")
}
}

func TestGenerateTeamAccessPointConfigForVividHosting(t *testing.T) {
model.BaseDir = ".."
ap := AccessPoint{isVividType: true}

ifaceRe := regexp.MustCompile("^set wireless\\.@wifi-iface\\[(\\d)\\]\\.")
disabledRe := regexp.MustCompile("disabled='([-\\w ]+)'")
ssidRe := regexp.MustCompile("ssid='([-\\w ]*)'")
wpaKeyRe := regexp.MustCompile("key='([-\\w ]*)'")
saePasswordRe := regexp.MustCompile("sae_password='([-\\w ]*)'")

// Should reject invalid positions.
for _, position := range []int{-1, 0, 7, 8, 254} {
_, err := ap.generateTeamAccessPointConfig(nil, position)
if assert.NotNil(t, err) {
assert.Equal(t, err.Error(), fmt.Sprintf("invalid team position %d", position))
}
}

// Should configure dummy values for all team SSIDs if there are no teams.
for position := 1; position <= 6; position++ {
config, _ := ap.generateTeamAccessPointConfig(nil, position)
ifaces := ifaceRe.FindAllStringSubmatch(config, -1)
disableds := disabledRe.FindAllStringSubmatch(config, -1)
ssids := ssidRe.FindAllStringSubmatch(config, -1)
wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1)
saePasswords := saePasswordRe.FindAllStringSubmatch(config, -1)
if assert.Equal(t, 1, len(disableds)) && assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) {
assert.Equal(t, strconv.Itoa(position), ifaces[0][1])
assert.Equal(t, "0", disableds[0][1])
assert.Equal(t, fmt.Sprintf("no-team-%d", position), ssids[0][1])
assert.Equal(t, fmt.Sprintf("no-team-%d", position), wpaKeys[0][1])
assert.Equal(t, fmt.Sprintf("no-team-%d", position), saePasswords[0][1])
}
}

// Should configure a different SSID for each team.
for position := 1; position <= 6; position++ {
team := &model.Team{Id: 254 + position, WpaKey: fmt.Sprintf("aaaaaaa%d", position)}
config, _ := ap.generateTeamAccessPointConfig(team, position)
ifaces := ifaceRe.FindAllStringSubmatch(config, -1)
disableds := disabledRe.FindAllStringSubmatch(config, -1)
ssids := ssidRe.FindAllStringSubmatch(config, -1)
wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1)
saePasswords := saePasswordRe.FindAllStringSubmatch(config, -1)
if assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) {
assert.Equal(t, strconv.Itoa(position), ifaces[0][1])
assert.Equal(t, "0", disableds[0][1])
assert.Equal(t, strconv.Itoa(team.Id), ssids[0][1])
assert.Equal(t, fmt.Sprintf("aaaaaaa%d", position), wpaKeys[0][1])
assert.Equal(t, fmt.Sprintf("aaaaaaa%d", position), saePasswords[0][1])
}
}

// Should reject a missing WPA key.
_, err := generateTeamAccessPointConfig(&model.Team{Id: 254}, 4)
_, err := ap.generateTeamAccessPointConfig(&model.Team{Id: 254}, 4)
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "invalid WPA key")
}
Expand Down
4 changes: 2 additions & 2 deletions templates/setup_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@
</fieldset>
<fieldset>
<legend>Networking</legend>
<p>Enable this setting if you have a Linksys WRT1900ACS or Vivid-Hosting access point and Cisco Catalyst
3500-series switch available, for isolating each team to its own SSID and VLAN.</p>
<p>Enable this setting if you have a Linksys WRT1900ACS or Vivid-Hosting VH-109 access point and Cisco
Catalyst 3500-series switch available, for isolating each team to its own SSID and VLAN.</p>
<div class="form-group">
<label class="col-lg-7 control-label">Enable advanced network security</label>
<div class="col-lg-1 checkbox">
Expand Down

0 comments on commit 4c2c4d1

Please sign in to comment.