From 4c9d0ec3ac7b88af225118b60f5aa01e6f3d29f1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 6 Oct 2019 17:20:34 +0200 Subject: wireless: T1627: initial rewrite in XML/Python style Working: - Wireless modes b, g, n, ac - WPA/WPA2 psk and RADIUS (tested using Microsoft NPS) --- src/completion/list_wireless_phys.sh | 5 + src/conf_mode/interfaces-wireless.py | 1351 +++++++++++++++++++++++++++++++ src/conf_mode/system-wifi-regdom.py | 111 +++ src/migration-scripts/interfaces/3-to-4 | 52 ++ 4 files changed, 1519 insertions(+) create mode 100755 src/completion/list_wireless_phys.sh create mode 100755 src/conf_mode/interfaces-wireless.py create mode 100755 src/conf_mode/system-wifi-regdom.py create mode 100755 src/migration-scripts/interfaces/3-to-4 (limited to 'src') diff --git a/src/completion/list_wireless_phys.sh b/src/completion/list_wireless_phys.sh new file mode 100755 index 000000000..70b8d1ff9 --- /dev/null +++ b/src/completion/list_wireless_phys.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +if [ -d /sys/class/ieee80211 ]; then + ls -x /sys/class/ieee80211 +fi diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py new file mode 100755 index 000000000..713b592e1 --- /dev/null +++ b/src/conf_mode/interfaces-wireless.py @@ -0,0 +1,1351 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from jinja2 import Template +from copy import deepcopy +from sys import exit +from stat import S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH +from pwd import getpwnam +from grp import getgrnam + +from subprocess import Popen, PIPE +from psutil import pid_exists + +from vyos.ifconfig import EthernetIf, VLANIf +from vyos.configdict import list_diff, vlan_to_dict +from vyos.config import Config +from vyos import ConfigError + +user = 'root' +group = 'vyattacfg' + +# Please be careful if you edit the template. +config_hostapd_tmpl = """ +### Autogenerated by interfaces-wireless.py ### +{% if description %} +# Description: {{ description }} +# User-friendly description of device; up to 32 octets encoded in UTF-8 +device_name={{ description | truncate(32, True) }} +{% endif %} + +# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for +# management frames with the Host AP driver); wlan0 with many nl80211 drivers +# Note: This attribute can be overridden by the values supplied with the '-i' +# command line parameter. +interface={{ intf }} + +# Driver interface type (hostap/wired/none/nl80211/bsd); +# default: hostap). nl80211 is used with all Linux mac80211 drivers. +# Use driver=none if building hostapd as a standalone RADIUS server that does +# not control any wireless/wired driver. +driver=nl80211 + +# +# What about bridge? +# bridge=br0 +# wds_sta=1 +# + +# Levels (minimum value for logged events): +# 0 = verbose debugging +# 1 = debugging +# 2 = informational messages +# 3 = notification +# 4 = warning +logger_syslog=-1 +logger_syslog_level=0 +logger_stdout=-1 +logger_stdout_level=0 + +{%- if country_code %} + +# Country code (ISO/IEC 3166-1). Used to set regulatory domain. +# Set as needed to indicate country in which device is operating. +# This can limit available channels and transmit power. +country_code={{ country_code }} + +# Enable IEEE 802.11d. This advertises the country_code and the set of allowed +# channels and transmit power levels based on the regulatory limits. The +# country_code setting must be configured with the correct country for +# IEEE 802.11d functions. +ieee80211d=1 +{% endif %} + +{%- if ssid %} + +# SSID to be used in IEEE 802.11 management frames +ssid={{ ssid }} +{% endif %} + +{%- if channel %} + +# Channel number (IEEE 802.11) +# (default: 0, i.e., not set) +# Please note that some drivers do not use this value from hostapd and the +# channel will need to be configured separately with iwconfig. +# +# If CONFIG_ACS build option is enabled, the channel can be selected +# automatically at run time by setting channel=acs_survey or channel=0, both of +# which will enable the ACS survey based algorithm. +channel={{ channel }} +{% endif %} + +{%- if mode %} + +# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), +# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used +# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this +# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs +# to be set to hw_mode=a. When using ACS (see channel parameter), a +# special value "any" can be used to indicate that any support band can be used. +# This special case is currently supported only with drivers with which +# offloaded ACS is used. +{% if 'n' in mode -%} +hw_mode=g +ieee80211n=1 +{% elif 'ac' in mode -%} +hw_mode=a +ieee80211h=1 +ieee80211ac=1 +{% else -%} +hw_mode={{ mode }} +{% endif %} +{% endif %} + +# ieee80211w: Whether management frame protection (MFP) is enabled +# 0 = disabled (default) +# 1 = optional +# 2 = required +{% if 'disabled' in mgmt_frame_protection -%} +ieee80211w=0 +{% elif 'optional' in mgmt_frame_protection -%} +ieee80211w=1 +{% elif 'required' in mgmt_frame_protection -%} +ieee80211w=2 +{% endif %} + +# ht_capab: HT capabilities (list of flags) +# LDPC coding capability: [LDPC] = supported +# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary +# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz +# with secondary channel above the primary channel +# (20 MHz only if neither is set) +# Note: There are limits on which channels can be used with HT40- and +# HT40+. Following table shows the channels that may be available for +# HT40- and HT40+ use per IEEE 802.11n Annex J: +# freq HT40- HT40+ +# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan) +# 5 GHz 40,48,56,64 36,44,52,60 +# (depending on the location, not all of these channels may be available +# for use) +# Please note that 40 MHz channels may switch their primary and secondary +# channels if needed or creation of 40 MHz channel maybe rejected based +# on overlapping BSSes. These changes are done automatically when hostapd +# is setting up the 40 MHz channel. +# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC] +# (SMPS disabled if neither is set) +# HT-greenfield: [GF] (disabled if not set) +# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set) +# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set) +# Tx STBC: [TX-STBC] (disabled if not set) +# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial +# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC +# disabled if none of these set +# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set) +# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not +# set) +# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) +# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) +# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) +ht_capab= + +{%- if cap_ht_40mhz_incapable -%} +[40-INTOLERANT] +{%- endif -%} + +{%- if cap_ht_delayed_block_ack -%} +[DELAYED-BA] +{%- endif -%} + +{%- if cap_ht_dsss_cck_40 -%} +[DSSS_CCK-40] +{%- endif -%} + +{%- if cap_ht_greenfield -%} +[GF] +{%- endif -%} + +{%- if cap_ht_ldpc -%} +[LDPC] +{%- endif -%} + +{%- if cap_ht_lsig_protection -%} +[LSIG-TXOP-PROT] +{%- endif -%} + +{%- if cap_ht_max_amsdu -%} +[MAX-AMSDU-{{ cap_ht_max_amsdu }}] +{%- endif -%} + +{%- if cap_ht_smps -%} +[SMPS-{{ cap_ht_smps | upper }}] +{%- endif -%} + +{%- if cap_ht_chan_set_width -%} +{%- for csw in cap_ht_chan_set_width -%} +[{{ csw | upper }}] +{%- endfor -%} +{%- endif -%} + +{%- if cap_ht_short_gi -%} +{%- for gi in cap_ht_short_gi -%} +[SHORT-GI-{{ gi }}] +{%- endfor -%} +{%- endif -%} + +{%- if cap_ht_stbc_tx -%} +[TX-STBC] +{%- endif -%} +{%- if cap_ht_stbc_rx -%} +[RX-STBC{{ cap_ht_stbc_rx }}] +{%- endif %} + +# Required for full HT and VHT functionality +wme_enabled=1 + +{% if cap_ht_powersave -%} +# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] +# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) +uapsd_advertisement_enabled=1 +{%- endif %} + +{% if cap_req_ht -%} +# Require stations to support HT PHY (reject association if they do not) +require_ht=1 +{% endif %} + +# vht_capab: VHT capabilities (list of flags) +# +# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] +# Indicates maximum MPDU length +# 0 = 3895 octets (default) +# 1 = 7991 octets +# 2 = 11454 octets +# 3 = reserved +# +# supported_chan_width: [VHT160] [VHT160-80PLUS80] +# Indicates supported Channel widths +# 0 = 160 MHz & 80+80 channel widths are not supported (default) +# 1 = 160 MHz channel width is supported +# 2 = 160 MHz & 80+80 channel widths are supported +# 3 = reserved +# +# Rx LDPC coding capability: [RXLDPC] +# Indicates support for receiving LDPC coded pkts +# 0 = Not supported (default) +# 1 = Supported +# +# Short GI for 80 MHz: [SHORT-GI-80] +# Indicates short GI support for reception of packets transmitted with TXVECTOR +# params format equal to VHT and CBW = 80Mhz +# 0 = Not supported (default) +# 1 = Supported +# +# Short GI for 160 MHz: [SHORT-GI-160] +# Indicates short GI support for reception of packets transmitted with TXVECTOR +# params format equal to VHT and CBW = 160Mhz +# 0 = Not supported (default) +# 1 = Supported +# +# Tx STBC: [TX-STBC-2BY1] +# Indicates support for the transmission of at least 2x1 STBC +# 0 = Not supported (default) +# 1 = Supported +# +# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234] +# Indicates support for the reception of PPDUs using STBC +# 0 = Not supported (default) +# 1 = support of one spatial stream +# 2 = support of one and two spatial streams +# 3 = support of one, two and three spatial streams +# 4 = support of one, two, three and four spatial streams +# 5,6,7 = reserved +# +# SU Beamformer Capable: [SU-BEAMFORMER] +# Indicates support for operation as a single user beamformer +# 0 = Not supported (default) +# 1 = Supported +# +# SU Beamformee Capable: [SU-BEAMFORMEE] +# Indicates support for operation as a single user beamformee +# 0 = Not supported (default) +# 1 = Supported +# +# Compressed Steering Number of Beamformer Antennas Supported: +# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4] +# Beamformee's capability indicating the maximum number of beamformer +# antennas the beamformee can support when sending compressed beamforming +# feedback +# If SU beamformer capable, set to maximum value minus 1 +# else reserved (default) +# +# Number of Sounding Dimensions: +# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4] +# Beamformer's capability indicating the maximum value of the NUM_STS parameter +# in the TXVECTOR of a VHT NDP +# If SU beamformer capable, set to maximum value minus 1 +# else reserved (default) +# +# MU Beamformer Capable: [MU-BEAMFORMER] +# Indicates support for operation as an MU beamformer +# 0 = Not supported or sent by Non-AP STA (default) +# 1 = Supported +# +# VHT TXOP PS: [VHT-TXOP-PS] +# Indicates whether or not the AP supports VHT TXOP Power Save Mode +# or whether or not the STA is in VHT TXOP Power Save mode +# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS +# mode +# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save +# mode +# +# +HTC-VHT Capable: [HTC-VHT] +# Indicates whether or not the STA supports receiving a VHT variant HT Control +# field. +# 0 = Not supported (default) +# 1 = supported +# +# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7] +# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv +# This field is an integer in the range of 0 to 7. +# The length defined by this field is equal to +# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets +# +# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3] +# Indicates whether or not the STA supports link adaptation using VHT variant +# HT Control field +# If +HTC-VHTcapable is 1 +# 0 = (no feedback) if the STA does not provide VHT MFB (default) +# 1 = reserved +# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB +# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the +# STA provides unsolicited VHT MFB +# Reserved if +HTC-VHTcapable is 0 +# +# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN] +# Indicates the possibility of Rx antenna pattern change +# 0 = Rx antenna pattern might change during the lifetime of an association +# 1 = Rx antenna pattern does not change during the lifetime of an association +# +# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] +# Indicates the possibility of Tx antenna pattern change +# 0 = Tx antenna pattern might change during the lifetime of an association +# 1 = Tx antenna pattern does not change during the lifetime of an association +vht_capab= + +{%- if cap_vht_max_mpdu -%} +[MAX-MPDU-{{ cap_vht_max_mpdu }}] +{%- endif -%} + +{%- if cap_vht_max_mpdu_exp -%} +[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] +{%- endif -%} + +{%- if cap_vht_chan_set_width -%} +[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] +{%- endif -%} + +{%- if cap_vht_chan_set_width -%} +{%- if '2' in cap_vht_chan_set_width -%} +[VHT160] +{%- elif '3' in cap_vht_chan_set_width -%} +[VHT160-80PLUS80] +{%- endif -%} +{%- endif -%} + +{%- if cap_vht_stbc_tx -%} +[TX-STBC-2BY1] +{%- endif -%} + +{%- if cap_vht_stbc_rx -%} +[RX-STBC-{{ cap_vht_stbc_rx }}] +{%- endif -%} + +{%- if cap_vht_link_adaptation -%} +{%- if 'unsolicited' in cap_vht_link_adaptation -%} +[VHT-LINK-ADAPT2] +{%- elif 'both' in cap_vht_link_adaptation -%} +[VHT-LINK-ADAPT3] +{%- endif -%} +{%- endif -%} + +{%- if cap_vht_short_gi -%} +{%- for gi in cap_vht_short_gi -%} +[SHORT-GI-{{ gi }}] +{%- endfor -%} +{%- endif -%} + +{%- if cap_vht_ldpc -%} +[RXLDPC] +{%- endif -%} + +{%- if cap_vht_tx_powersave -%} +[VHT-TXOP-PS] +{%- endif -%} + +{%- if cap_vht_vht_cf -%} +[HTC-VHT] +{%- endif -%} + +{%- if cap_vht_beamform -%} +{%- for beamform in cap_vht_beamform -%} +{%- if 'single-user-beamformer' in beamform -%} +[SU-BEAMFORMER] +{%- elif 'single-user-beamformee' in beamform -%} +[SU-BEAMFORMEE] +{%- elif 'multi-user-beamformer' in beamform -%} +[MU-BEAMFORMER] +{%- elif 'multi-user-beamformee' in beamform -%} +[MU-BEAMFORMEE] +{%- endif -%} +{%- endfor -%} +{%- endif -%} + +{%- if cap_vht_antenna_fixed -%} +[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] +{%- endif -%} + +{%- if cap_vht_antenna_cnt -%} +{%- for beamform in cap_vht_beamform -%} +{%- if 'single-user-beamformer' in beamform -%} +[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] +{%- else -%} +[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] +{%- endif -%} +{%- endfor -%} +{%- endif %} + +# ieee80211n: Whether IEEE 802.11n (HT) is enabled +# 0 = disabled (default) +# 1 = enabled +# Note: You will also need to enable WMM for full HT functionality. +# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. +{% if cap_req_vht -%} +ieee80211n=0 +# Require stations to support VHT PHY (reject association if they do not) +require_vht=1 +{% endif %} + +{% if cap_vht_center_freq_1 -%} +# center freq = 5 GHz + (5 * index) +# So index 42 gives center freq 5.210 GHz +# which is channel 42 in 5G band +vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} +{% endif %} + +{% if cap_vht_center_freq_2 -%} +# center freq = 5 GHz + (5 * index) +# So index 159 gives center freq 5.795 GHz +# which is channel 159 in 5G band +vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} +{% endif %} + +{% if disable_broadcast_ssid -%} +# Send empty SSID in beacons and ignore probe request frames that do not +# specify full SSID, i.e., require stations to know SSID. +# default: disabled (0) +# 1 = send empty (length=0) SSID in beacon and ignore probe request for +# broadcast SSID +# 2 = clear SSID (ASCII 0), but keep the original length (this may be required +# with some clients that do not support empty SSID) and ignore probe +# requests for broadcast SSID +ignore_broadcast_ssid=1 +{% endif %} + +# Station MAC address -based authentication +# Please note that this kind of access control requires a driver that uses +# hostapd to take care of management frame processing and as such, this can be +# used with driver=hostap or driver=nl80211, but not with driver=atheros. +# 0 = accept unless in deny list +# 1 = deny unless in accept list +# 2 = use external RADIUS server (accept/deny lists are searched first) +macaddr_acl=0 + +{% if max_stations -%} +# Maximum number of stations allowed in station table. New stations will be +# rejected after the station table is full. IEEE 802.11 has a limit of 2007 +# different association IDs, so this number should not be larger than that. +# (default: 2007) +max_num_sta={{ max_stations }} +{% endif %} + +{% if isolate_stations -%} +# Client isolation can be used to prevent low-level bridging of frames between +# associated stations in the BSS. By default, this bridging is allowed. +ap_isolate=1 +{% endif %} + +{% if reduce_transmit_power -%} +# Add Power Constraint element to Beacon and Probe Response frames +# This config option adds Power Constraint element when applicable and Country +# element is added. Power Constraint element is required by Transmit Power +# Control. This can be used only with ieee80211d=1. +# Valid values are 0..255. +local_pwr_constraint={{ reduce_transmit_power }} +{% endif %} + +{% if expunge_failing_stations -%} +# Disassociate stations based on excessive transmission failures or other +# indications of connection loss. This depends on the driver capabilities and +# may not be available with all drivers. +disassoc_low_ack=1 +{% endif %} + +{% if sec_wep -%} +# IEEE 802.11 specifies two authentication algorithms. hostapd can be +# configured to allow both of these or only one. Open system authentication +# should be used with IEEE 802.1X. +# Bit fields of allowed authentication algorithms: +# bit 0 = Open System Authentication +# bit 1 = Shared Key Authentication (requires WEP) +auth_algs=2 + +# WEP rekeying (disabled if key lengths are not set or are set to 0) +# Key lengths for default/broadcast and individual/unicast keys: +# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits) +# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits) +wep_key_len_broadcast=5 +wep_key_len_unicast=5 + +# Static WEP key configuration +# +# The key number to use when transmitting. +# It must be between 0 and 3, and the corresponding key must be set. +# default: not set +wep_default_key=0 + +# The WEP keys to use. +# A key may be a quoted string or unquoted hexadecimal digits. +# The key length should be 5, 13, or 16 characters, or 10, 26, or 32 +# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or +# 128-bit (152-bit) WEP is used. +# Only the default key must be supplied; the others are optional. +{% if sec_wep_key -%} +{% for key in sec_wep_key -%} +wep_key{{ loop.index -1 }}={{ key}} +{% endfor %} +{%- endif %} + +{% elif sec_wpa -%} +##### WPA/IEEE 802.11i configuration ########################################## + +# Enable WPA. Setting this variable configures the AP to require WPA (either +# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either +# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK. +# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice. +# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys), +# RADIUS authentication server must be configured, and WPA-EAP must be included +# in wpa_key_mgmt. +# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0) +# and/or WPA2 (full IEEE 802.11i/RSN): +# bit0 = WPA +# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) +{% if 'both' in sec_wpa_mode -%} +wpa=3 +{%- elif 'wpa2' in sec_wpa_mode -%} +wpa=2 +{%- elif 'wpa' in sec_wpa_mode -%} +wpa=1 +{%- endif %} + +{% if sec_wpa_cipher -%} +# Set of accepted cipher suites (encryption algorithms) for pairwise keys +# (unicast packets). This is a space separated list of algorithms: +# CCMP = AES in Counter mode with CBC-MAC (CCMP-128) +# TKIP = Temporal Key Integrity Protocol +# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key +# GCMP = Galois/counter mode protocol (GCMP-128) +# GCMP-256 = Galois/counter mode protocol with 256-bit key +# Group cipher suite (encryption algorithm for broadcast and multicast frames) +# is automatically selected based on this configuration. If only CCMP is +# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, +# TKIP will be used as the group cipher. The optional group_cipher parameter can +# be used to override this automatic selection. +{% if 'wpa2' in sec_wpa_mode -%} +# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) +rsn_pairwise={{ sec_wpa_cipher | join(" ") }} +{% else -%} +# Pairwise cipher for WPA (v1) (default: TKIP) +wpa_pairwise={{ sec_wpa_cipher | join(" ") }} +{%- endif -%} +{% endif %} + +{% if sec_wpa_passphrase -%} +# IEEE 802.11 specifies two authentication algorithms. hostapd can be +# configured to allow both of these or only one. Open system authentication +# should be used with IEEE 802.1X. +# Bit fields of allowed authentication algorithms: +# bit 0 = Open System Authentication +# bit 1 = Shared Key Authentication (requires WEP) +auth_algs=1 + +# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The +# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be +# added to enable SHA256-based stronger algorithms. +# WPA-PSK = WPA-Personal / WPA2-Personal +# WPA-PSK-SHA256 = WPA2-Personal using SHA256 +wpa_key_mgmt=WPA-PSK + +# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit +# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase +# (8..63 characters) that will be converted to PSK. This conversion uses SSID +# so the PSK changes when ASCII passphrase is used and the SSID is changed. +wpa_passphrase={{ sec_wpa_passphrase }} + +{% elif sec_wpa_radius -%} +##### IEEE 802.1X-2004 related configuration ################################## +# Require IEEE 802.1X authorization +ieee8021x=1 + +# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The +# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be +# added to enable SHA256-based stronger algorithms. +# WPA-EAP = WPA-Enterprise / WPA2-Enterprise +# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 +wpa_key_mgmt=WPA-EAP + +{% for radius in sec_wpa_radius -%} +auth_server_addr={{ radius.server }} +auth_server_port={{ radius.port }} +auth_server_shared_secret={{ radius.secret }} +{% if radius.accounting -%} +acct_server_addr={{ radius.server }} +acct_server_port={{ radius.acc_port }} +acct_server_shared_secret={{ radius.secret }} +{% endif %} +{% endfor %} + +{% endif %} + +{% else %} +# Open system +auth_algs=1 +{% endif %} + +# TX queue parameters (EDCF / bursting) +# tx_queue__ +# queues: data0, data1, data2, data3 +# (data0 is the highest priority queue) +# parameters: +# aifs: AIFS (default 2) +# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, +# 16383, 32767) +# cwmax: cwMax (same values as cwMin, cwMax >= cwMin) +# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for +# bursting +# +# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): +# These parameters are used by the access point when transmitting frames +# to the clients. +# +# Low priority / AC_BK = background +tx_queue_data3_aifs=7 +tx_queue_data3_cwmin=15 +tx_queue_data3_cwmax=1023 +tx_queue_data3_burst=0 +# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0 +# +# Normal priority / AC_BE = best effort +tx_queue_data2_aifs=3 +tx_queue_data2_cwmin=15 +tx_queue_data2_cwmax=63 +tx_queue_data2_burst=0 +# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0 +# +# High priority / AC_VI = video +tx_queue_data1_aifs=1 +tx_queue_data1_cwmin=7 +tx_queue_data1_cwmax=15 +tx_queue_data1_burst=3.0 +# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0 +# +# Highest priority / AC_VO = voice +tx_queue_data0_aifs=1 +tx_queue_data0_cwmin=3 +tx_queue_data0_cwmax=7 +tx_queue_data0_burst=1.5 + +# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): +# for 802.11a or 802.11g networks +# These parameters are sent to WMM clients when they associate. +# The parameters will be used by WMM clients for frames transmitted to the +# access point. +# +# note - txop_limit is in units of 32microseconds +# note - acm is admission control mandatory flag. 0 = admission control not +# required, 1 = mandatory +# note - Here cwMin and cmMax are in exponent form. The actual cw value used +# will be (2^n)-1 where n is the value given here. The allowed range for these +# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin. +# +wmm_enabled=1 + +# Low priority / AC_BK = background +wmm_ac_bk_cwmin=4 +wmm_ac_bk_cwmax=10 +wmm_ac_bk_aifs=7 +wmm_ac_bk_txop_limit=0 +wmm_ac_bk_acm=0 +# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10 +# +# Normal priority / AC_BE = best effort +wmm_ac_be_aifs=3 +wmm_ac_be_cwmin=4 +wmm_ac_be_cwmax=10 +wmm_ac_be_txop_limit=0 +wmm_ac_be_acm=0 +# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7 +# +# High priority / AC_VI = video +wmm_ac_vi_aifs=2 +wmm_ac_vi_cwmin=3 +wmm_ac_vi_cwmax=4 +wmm_ac_vi_txop_limit=94 +wmm_ac_vi_acm=0 +# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188 +# +# Highest priority / AC_VO = voice +wmm_ac_vo_aifs=2 +wmm_ac_vo_cwmin=2 +wmm_ac_vo_cwmax=3 +wmm_ac_vo_txop_limit=47 +wmm_ac_vo_acm=0 + +""" + +default_config_data = { + 'address': [], + 'address_remove': [], + 'cap_ht_40mhz_incapable' : False, + 'cap_ht_powersave' : False, + 'cap_ht_chan_set_width' : '', + 'cap_ht_delayed_block_ack' : False, + 'cap_ht_dsss_cck_40' : False, + 'cap_ht_greenfield' : False, + 'cap_ht_ldpc' : False, + 'cap_ht_lsig_protection' : False, + 'cap_ht_max_amsdu' : '', + 'cap_ht_short_gi' : [], + 'cap_ht_smps' : '', + 'cap_ht_stbc_rx' : '', + 'cap_ht_stbc_tx' : False, + 'cap_req_ht' : False, + 'cap_req_vht' : False, + 'cap_vht_antenna_cnt' : '', + 'cap_vht_antenna_fixed' : False, + 'cap_vht_beamform' : '', + 'cap_vht_center_freq_1' : '', + 'cap_vht_center_freq_2' : '', + 'cap_vht_chan_set_width' : '', + 'cap_vht_ldpc' : False, + 'cap_vht_link_adaptation' : False, + 'cap_vht_max_mpdu_exp' : '', + 'cap_vht_max_mpdu' : '', + 'cap_vht_short_gi' : [], + 'cap_vht_stbc_rx' : '', + 'cap_vht_stbc_tx' : False, + 'cap_vht_tx_powersave' : False, + 'cap_vht_vht_cf' : False, + 'channel': '', + 'country_code': '', + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_broadcast_ssid' : False, + 'disable_link_detect' : 1, + 'expunge_failing_stations' : False, + 'hw_id' : '', + 'intf': '', + 'isolate_stations' : False, + 'mac' : '', + 'max_stations' : '', + 'mgmt_frame_protection' : 'disabled', + 'mode' : 'g', + 'phy' : '', + 'reduce_transmit_power' : '', + 'sec_wep' : False, + 'sec_wep_key' : [], + 'sec_wpa' : False, + 'sec_wpa_cipher' : [], + 'sec_wpa_mode' : 'both', + 'sec_wpa_passphrase' : '', + 'sec_wpa_radius' : [], + 'ssid' : '', + 'type' : 'monitor', + 'vif': [], + 'vif_remove': [] +} + +def get_config_name(intf): + cfg_file = r'/etc/hostapd/{}.cfg'.format(intf) + return cfg_file + + +def wifi_mkdir(directory): + # create directory on demand + if not os.path.exists(directory): + os.mkdir(directory) + + # fix permissions - corresponds to mode 755 + os.chmod(directory, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(directory, uid, gid) + + +def subprocess_cmd(command): + p = Popen(command, stdout=PIPE, shell=True) + p.communicate() + + +def apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(VLANIf("lo")): + raise TypeError() + + # update interface description used e.g. within SNMP + vlan.set_alias(config['description']) + # ignore link state changes + vlan.set_link_detect(config['disable_link_detect']) + # Maximum Transmission Unit (MTU) + vlan.set_mtu(config['mtu']) + # Change VLAN interface MAC address + if config['mac']: + vlan.set_mac(config['mac']) + + # enable/disable VLAN interface + if config['disable']: + vlan.set_state('down') + else: + vlan.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + +def get_config(): + wifi = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if wireless interface has been removed + cfg_base = 'interfaces wireless ' + wifi['intf'] + if not conf.exists(cfg_base): + wifi['deleted'] = True + # we can not bail out early as wireless interface can not be removed + # Kernel will complain with: RTNETLINK answers: Operation not supported. + # Thus we need to remove individual settings + return wifi + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + wifi['address'] = conf.return_values('address') + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + wifi['address_remove'] = list_diff(eff_addr, wifi['address']) + + # 40MHz intolerance, use 20MHz only + if conf.exists('capabilities ht 40MHz-incapable'): + wifi['cap_ht_40mhz_incapable'] = True + + # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] + if conf.exists('capabilities ht auto-powersave'): + wifi['cap_ht_powersave'] = True + + # Supported channel set width + if conf.exists('capabilities ht channel-set-width'): + wifi['cap_ht_chan_set_width'] = conf.return_values('capabilities ht channel-set-width') + + # HT-delayed Block Ack + if conf.exists('capabilities ht delayed-block-ack'): + wifi['cap_ht_delayed_block_ack'] = True + + # DSSS/CCK Mode in 40 MHz + if conf.exists('capabilities ht dsss-cck-40'): + wifi['cap_ht_dsss_cck_40'] = True + + # HT-greenfield capability + if conf.exists('capabilities ht greenfield'): + wifi['cap_ht_greenfield'] = True + + # LDPC coding capability + if conf.exists('capabilities ht ldpc'): + wifi['cap_ht_ldpc'] = True + + # L-SIG TXOP protection capability + if conf.exists('capabilities ht lsig-protection'): + wifi['cap_ht_lsig_protection'] = True + + # Set Maximum A-MSDU length + if conf.exists('capabilities ht max-amsdu'): + wifi['cap_ht_max_amsdu'] = conf.return_value('capabilities ht max-amsdu') + + # Short GI capabilities + if conf.exists('capabilities ht short-gi'): + wifi['cap_ht_short_gi'] = conf.return_values('capabilities ht short-gi') + + # Spatial Multiplexing Power Save (SMPS) settings + if conf.exists('capabilities ht smps'): + wifi['cap_ht_smps'] = conf.return_value('capabilities ht smps') + + # Support for receiving PPDU using STBC (Space Time Block Coding) + if conf.exists('capabilities ht stbc rx'): + wifi['cap_ht_stbc_rx'] = conf.return_value('capabilities ht stbc rx') + + # Support for sending PPDU using STBC (Space Time Block Coding) + if conf.exists('capabilities ht stbc tx'): + wifi['cap_ht_stbc_tx'] = True + + # Require stations to support HT PHY (reject association if they do not) + if conf.exists('capabilities require-ht'): + wifi['cap_req_ht'] = True + + # Require stations to support VHT PHY (reject association if they do not) + if conf.exists('capabilities require-vht'): + wifi['cap_req_vht'] = True + + # Number of antennas on this card + if conf.exists('capabilities vht antenna-count'): + wifi['cap_vht_antenna_cnt'] = conf.return_value('capabilities vht antenna-count') + + # set if antenna pattern does not change during the lifetime of an association + if conf.exists('capabilities vht antenna-pattern-fixed'): + wifi['cap_vht_antenna_fixed'] = True + + # Beamforming capabilities + if conf.exists('capabilities vht beamform'): + wifi['cap_vht_beamform'] = conf.return_values('capabilities vht beamform') + + # VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes) + if conf.exists('capabilities vht center-channel-freq freq-1'): + wifi['cap_vht_center_freq_1'] = conf.return_value('capabilities vht center-channel-freq freq-1') + + # VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode) + if conf.exists('capabilities vht center-channel-freq freq-2'): + wifi['cap_vht_center_freq_2'] = conf.return_value('capabilities vht center-channel-freq freq-2') + + # VHT operating Channel width + if conf.exists('capabilities vht channel-set-width'): + wifi['cap_vht_chan_set_width'] = conf.return_value('capabilities vht channel-set-width') + + # LDPC coding capability + if conf.exists('capabilities vht ldpc'): + wifi['cap_vht_ldpc'] = True + + # VHT link adaptation capabilities + if conf.exists('capabilities vht link-adaptation'): + wifi['cap_vht_link_adaptation'] = True + + # Set the maximum length of A-MPDU pre-EOF padding that the station can receive + if conf.exists('capabilities vht max-mpdu-exp'): + wifi['cap_vht_max_mpdu_exp'] = conf.return_value('capabilities vht max-mpdu-exp') + + # Increase Maximum MPDU length + if conf.exists('capabilities vht max-mpdu'): + wifi['cap_vht_max_mpdu'] = conf.return_value('capabilities vht max-mpdu') + + # Increase Maximum MPDU length + if conf.exists('capabilities vht short-gi'): + wifi['cap_vht_short_gi'] = conf.return_values('capabilities vht short-gi') + + # Support for receiving PPDU using STBC (Space Time Block Coding) + if conf.exists('capabilities vht stbc rx'): + wifi['cap_vht_stbc_rx'] = conf.return_value('capabilities vht stbc rx') + + # Support for the transmission of at least 2x1 STBC (Space Time Block Coding) + if conf.exists('capabilities vht stbc tx'): + wifi['cap_vht_stbc_tx'] = True + + # Support for VHT TXOP Power Save Mode + if conf.exists('capabilities vht tx-powersave'): + wifi['cap_vht_tx_powersave'] = True + + # STA supports receiving a VHT variant HT Control field + if conf.exists('capabilities vht vht-cf'): + wifi['cap_vht_vht_cf'] = True + + # Wireless radio channel + if conf.exists('channel'): + wifi['channel'] = conf.return_value('channel') + + # retrieve interface description + if conf.exists('description'): + wifi['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + wifi['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + wifi['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCP client vendor identifier + if conf.exists('dhcp-options vendor-class-id'): + wifi['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + wifi['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + wifi['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # Disable broadcast of SSID from access-point + if conf.exists('disable-broadcast-ssid'): + wifi['disable_broadcast_ssid'] = True + + # ignore link state changes on this interface + if conf.exists('disable-link-detect'): + wifi['disable_link_detect'] = 2 + + # Disassociate stations based on excessive transmission failures + if conf.exists('expunge-failing-stations'): + wifi['expunge_failing_stations'] = True + + # retrieve real hardware address + if conf.exists('hw-id'): + wifi['hw_id'] = conf.return_value('hw-id') + + # Isolate stations on the AP so they cannot see each other + if conf.exists('isolate-stations'): + wifi['isolate_stations'] = True + + # Media Access Control (MAC) address + if conf.exists('mac'): + wifi['mac'] = conf.return_value('mac') + + # Maximum number of wireless radio stations + if conf.exists('max-stations'): + wifi['max_stations'] = conf.return_value('max-stations') + + # Management Frame Protection (MFP) according to IEEE 802.11w + if conf.exists('mgmt-frame-protection'): + wifi['mgmt_frame_protection'] = conf.return_value('mgmt-frame-protection') + + # Wireless radio mode + if conf.exists('mode'): + wifi['mode'] = conf.return_value('mode') + + # Wireless physical device + if conf.exists('phy'): + wifi['phy'] = conf.return_value('phy') + + # Transmission power reduction in dBm + if conf.exists('reduce-transmit-power'): + wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power') + + # WEP enabled? + if conf.exists('security wep'): + wifi['sec_wep'] = True + + # WEP encryption key(s) + if conf.exists('security wep key'): + wifi['sec_wep_key'] = conf.return_values('security wep key') + + # WPA enabled? + if conf.exists('security wpa'): + wifi['sec_wpa'] = True + + # WPA Cipher suite + if conf.exists('security wpa cipher'): + wifi['sec_wpa_cipher'] = conf.return_values('security wpa cipher') + + # WPA mode + if conf.exists('security wpa mode'): + wifi['sec_wpa_mode'] = conf.return_value('security wpa mode') + + # WPA default ciphers depend on WPA mode + if not wifi['sec_wpa_cipher']: + if wifi['sec_wpa_mode'] == 'wpa': + wifi['sec_wpa_cipher'].append('TKIP') + wifi['sec_wpa_cipher'].append('CCMP') + + elif wifi['sec_wpa_mode'] == 'wpa2': + wifi['sec_wpa_cipher'].append('CCMP') + + elif wifi['sec_wpa_mode'] == 'both': + wifi['sec_wpa_cipher'].append('CCMP') + wifi['sec_wpa_cipher'].append('TKIP') + + # WPA personal shared pass phrase + if conf.exists('security wpa passphrase'): + wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase') + + # WPA radius server goes here + for server in conf.list_nodes('security wpa radius-server'): + # set new configuration level + conf.set_level(cfg_base + ' security wpa radius-server ' + server) + + radius = { + 'server' : server, + 'acc_port' : '', + 'port' : 1812, + 'secret' : '' + } + + # receive RADIUS accounting info + if conf.exists('accounting'): + radius['acc_port'] = conf.return_value('accounting') + + # RADIUS server port + if conf.exists('port'): + radius['port'] = conf.return_value('port') + + # RADIUS server shared-secret + if conf.exists('secret'): + radius['secret'] = conf.return_value('secret') + + # append RADIUS server to list of servers + wifi['sec_wpa_radius'].append(radius) + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + + # Wireless access-point service set identifier (SSID) + if conf.exists('ssid'): + wifi['ssid'] = conf.return_value('ssid') + + # Wireless device type for this interface + if conf.exists('type'): + wifi['type'] = conf.return_value('type') + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + wifi['vif_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif'): + for vif in conf.list_nodes('vif'): + # set config level to vif interface + conf.set_level(cfg_base + ' vif ' + vif) + wifi['vif'].append(vlan_to_dict(conf)) + + + # retrieve configured regulatory domain + conf.set_level('system') + if conf.exists('wifi-regulatory-domain'): + wifi['country_code'] = conf.return_value('wifi-regulatory-domain') + + return wifi + + +def verify(wifi): + if wifi['deleted']: + return None + + if wifi['type'] != 'monitor' and not wifi['ssid']: + raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) + + if wifi['type'] == 'access-point' and not wifi['channel']: + raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) + + if len(wifi['sec_wep_key']) > 4: + raise ConfigError('No more then 4 WEP keys configurable') + + if wifi['cap_vht'] and not wifi['cap_ht']: + raise ConfigError('Specify HT flags if you want to use VHT!') + + if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1: + raise ConfigError('Cannot use beam forming with just one antenna!') + + if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0): + raise ConfigError('Missing WEP keys') + + if wifi['sec_wpa'] and not (wifi['sec_wpa_passphrase'] or wifi['sec_wpa_radius']): + raise ConfigError('Misssing WPA key or RADIUS server') + + conf = Config() + if not conf.exists('system wifi-regulatory-domain'): + raise ConfigError('Wireless regulatory domain is mandatory.\n' \ + 'Use "set system wifi-regulatory-domain" to set.') + + return None + +def generate(wifi): + if wifi['deleted']: + if os.path.isfile(get_config_name(wifi['intf'])): + os.unlink(get_config_name(wifi['intf'])) + + return None + + # create config directory on demand + directory = os.path.dirname(get_config_name(wifi['intf'])) + wifi_mkdir(directory) + + tmpl = Template(config_hostapd_tmpl) + config_text = tmpl.render(wifi) + with open(get_config_name(wifi['intf']), 'w') as f: + f.write(config_text) + + return None + +def apply(wifi): + pid = 0 + pidfile = '/var/run/hostapd/{}.pid'.format(wifi['intf']) + if os.path.isfile(pidfile): + pid = 0 + with open(pidfile, 'r') as f: + pid = int(f.read()) + + # always stop hostapd service first before reconfiguring it + if pid_exists(pid): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) + + w = EthernetIf(wifi['intf']) + if wifi['deleted']: + # delete interface + w.remove() + else: + # Some parts e.g. MAC address can't be changed when interface is up + w.set_state('down') + + # update interface description used e.g. within SNMP + w.set_alias(wifi['description']) + + # get DHCP config dictionary and update values + opt = w.get_dhcp_options() + + if wifi['dhcp_client_id']: + opt['client_id'] = wifi['dhcp_client_id'] + + if wifi['dhcp_hostname']: + opt['hostname'] = wifi['dhcp_hostname'] + + if wifi['dhcp_vendor_class_id']: + opt['vendor_class_id'] = wifi['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + w.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = w.get_dhcpv6_options() + + if wifi['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if wifi['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + w.set_dhcpv6_options(opt) + + # ignore link state changes + w.set_link_detect(wifi['disable_link_detect']) + + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed + if wifi['mac']: + w.set_mac(wifi['mac']) + else: + w.set_mac(wifi['hw_id']) + + # enable interface + if not wifi['disable']: + w.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in wifi['address_remove']: + w.del_addr(addr) + for addr in wifi['address']: + w.add_addr(addr) + + # remove no longer required VLAN interfaces (vif) + for vif in wifi['vif_remove']: + e.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in wifi['vif']: + # QoS priority mapping can only be set during interface creation + # so we delete the interface first if required. + if vif['egress_qos_changed'] or vif['ingress_qos_changed']: + try: + # on system bootup the above condition is true but the interface + # does not exists, which throws an exception, but that's legal + e.del_vlan(vif['id']) + except: + pass + + vlan = e.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) + + # Physical interface is now configured. Proceed by starting hostapd + cmd = 'start-stop-daemon --start --quiet' + cmd += ' --pidfile ' + pidfile + cmd += ' --exec /usr/sbin/hostapd' + # now pass arguments to hostapd binary + cmd += ' --' + cmd += ' -P ' + pidfile + cmd += ' -B ' + get_config_name(wifi['intf']) + + # execute assembled command + subprocess_cmd(cmd) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py new file mode 100755 index 000000000..01dc92a20 --- /dev/null +++ b/src/conf_mode/system-wifi-regdom.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import jinja2 + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos import ConfigError + +config_80211_file='/etc/modprobe.d/cfg80211.conf' +config_crda_file='/etc/default/crda' + +# Please be careful if you edit the template. +config_80211_tmpl = """ +{%- if regdom -%} +options cfg80211 ieee80211_regdom={{ regdom }} +{% endif %} +""" + +# Please be careful if you edit the template. +config_crda_tmpl = """ +{%- if regdom -%} +REGDOMAIN={{ regdom }} +{% endif %} +""" + +default_config_data = { + 'regdom' : '', + 'deleted' : False +} + + +def get_config(): + regdom = deepcopy(default_config_data) + conf = Config() + + # set new configuration level + conf.set_level('system') + + # Check if interface has been removed + if not conf.exists('wifi-regulatory-domain'): + regdom['deleted'] = True + return regdom + + # retrieve configured regulatory domain + if conf.exists('wifi-regulatory-domain'): + regdom['regdom'] = conf.return_value('wifi-regulatory-domain') + + return regdom + +def verify(regdom): + if regdom['deleted']: + return None + + if not regdom['regdom']: + raise ConfigError("Wireless regulatory domain is mandatory.") + + return None + +def generate(regdom): + print("Changing the wireless regulatory domain requires a system reboot.") + + if regdom['deleted']: + if os.path.isfile(config_80211_file): + os.unlink(config_80211_file) + + if os.path.isfile(config_crda_file): + os.unlink(config_crda_file) + + return None + + tmpl = jinja2.Template(config_80211_tmpl) + config_text = tmpl.render(regdom) + with open(config_80211_file, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(config_crda_tmpl) + config_text = tmpl.render(regdom) + with open(config_crda_file, 'w') as f: + f.write(config_text) + + return None + +def apply(regdom): + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/migration-scripts/interfaces/3-to-4 b/src/migration-scripts/interfaces/3-to-4 new file mode 100755 index 000000000..4a2fd9c0d --- /dev/null +++ b/src/migration-scripts/interfaces/3-to-4 @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +# Change syntax of wireless interfaces +# Migrate boolean nodes to valueless + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'wireless'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + for wifi in config.list_nodes(base): + # as converting a node to bool is always the same, we can script it + to_bool_nodes = ['capabilities ht 40MHz-incapable', 'capabilities ht auto-powersave', + 'capabilities ht delayed-block-ack', 'capabilities ht dsss-cck-40', + 'capabilities ht greenfield', 'capabilities ht ldpc', 'capabilities ht lsig-protection', + 'capabilities ht stbc tx', 'capabilities require-ht', 'capabilities require-vht', + 'capabilities vht antenna-pattern-fixed', 'capabilities vht ldpc', + 'capabilities vht stbc tx', 'capabilities vht tx-powersave', + 'capabilities vht vht-cf', 'expunge-failing-stations', 'isolate-stations'] + + for node in to_bool_nodes: + if config.exists(base + [wifi, node]): + tmp = config.return_value(base + [wifi, node]) + # delete old node + config.delete(base + [wifi, node]) + if tmp == 'true': + config.set(base + [wifi, node]) + + if config.exists(base + [wifi, 'debug']): + tmp = config.return_value(base + [wifi, 'debug']) + config.delete(base + [wifi, 'debug']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From fc65bb35be37878d4cc4cbb5d330f56febaaa409 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 17 Nov 2019 21:49:06 +0100 Subject: wireless: T1627: initial rewrite of show-wireless.pl in Python --- op-mode-definitions/wireless.xml | 6 +- src/op_mode/show_wireless.py | 158 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) create mode 100755 src/op_mode/show_wireless.py (limited to 'src') diff --git a/op-mode-definitions/wireless.xml b/op-mode-definitions/wireless.xml index 992d53ba4..c3c6dee59 100644 --- a/op-mode-definitions/wireless.xml +++ b/op-mode-definitions/wireless.xml @@ -57,7 +57,7 @@ Show wireless interface configuration - echo TBD. + ${vyos_op_scripts_dir}/show_wireless.py --brief @@ -80,7 +80,7 @@ Show summary of the specified wireless interface information - echo TBD. + sudo ${vyos_op_scripts_dir}/show_wireless.py --scan "$4" @@ -94,7 +94,7 @@ Show specified wireless interface information - echo "TBD." + ${vyos_op_scripts_dir}/show_wireless.py --stations "$4" diff --git a/src/op_mode/show_wireless.py b/src/op_mode/show_wireless.py new file mode 100755 index 000000000..aff882559 --- /dev/null +++ b/src/op_mode/show_wireless.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import re + +from sys import exit +from copy import deepcopy +from subprocess import Popen, PIPE, STDOUT + +from vyos.config import Config + +parser = argparse.ArgumentParser() +parser.add_argument("-s", "--scan", help="Scan for Wireless APs on given interface, e.g. 'wlan0'") +parser.add_argument("-b", "--brief", action="store_true", help="Show wireless configuration") +parser.add_argument("-c", "--stations", help="Show wireless clients connected on interface, e.g. 'wlan0'") + +def _cmd(command): + p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) + tmp = p.communicate()[0].strip() + return tmp.decode() + +def show_brief(): + config = Config() + if len(config.list_effective_nodes('interfaces wireless')) == 0: + print("No Wireless interfaces configured") + exit(0) + + interfaces = [] + for intf in config.list_effective_nodes('interfaces wireless'): + config.set_level('interfaces wireless {}'.format(intf)) + data = { + 'name': intf, + 'type': '', + 'ssid': '', + 'channel': '' + } + data['type'] = config.return_effective_value('type') + data['ssid'] = config.return_effective_value('ssid') + data['channel'] = config.return_effective_value('channel') + + interfaces.append(data) + + return interfaces + +def ssid_scan(intf): + tmp = _cmd('/sbin/iw dev {} scan ap-force'.format(intf)) + networks = [] + data = { + 'ssid': '', + 'mac': '', + 'channel': '', + 'signal': '' + } + re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})') + for line in tmp.splitlines(): + if line.startswith('BSS '): + ssid = deepcopy(data) + ssid['mac'] = re.search(re_mac, line).group() + + elif line.lstrip().startswith('SSID: '): + # SSID can be " SSID: WLAN-57 6405", thus strip all leading whitespaces + ssid['ssid'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('signal: '): + # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces + ssid['signal'] = line.lstrip().split(':')[-1].split()[0] + + elif line.lstrip().startswith('DS Parameter set: channel'): + # Channel can be " DS Parameter set: channel 6" , thus + # strip all leading whitespaces + ssid['channel'] = line.lstrip().split(':')[-1].split()[-1] + networks.append(ssid) + continue + + return networks + +def show_clients(intf): + tmp = _cmd('/sbin/iw dev {} station dump'.format(intf)) + clients = [] + data = { + 'mac': '', + 'signal': '', + 'rx_bytes': '', + 'rx_packets': '', + 'tx_bytes': '', + 'tx_packets': '' + } + re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})') + for line in tmp.splitlines(): + if line.startswith('Station'): + client = deepcopy(data) + client['mac'] = re.search(re_mac, line).group() + + elif line.lstrip().startswith('signal avg:'): + client['signal'] = line.lstrip().split(':')[-1].lstrip().split()[0] + + elif line.lstrip().startswith('rx bytes:'): + client['rx_bytes'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('rx packets:'): + client['rx_packets'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('tx bytes:'): + client['tx_bytes'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('tx packets:'): + client['tx_packets'] = line.lstrip().split(':')[-1].lstrip() + clients.append(client) + continue + + return clients + +if __name__ == '__main__': + args = parser.parse_args() + + if args.scan: + print("Address SSID Channel Signal (dbm)") + for network in ssid_scan(args.scan): + print("{:<17} {:<32} {:>3} {}".format(network['mac'], + network['ssid'], + network['channel'], + network['signal'])) + exit(0) + + elif args.brief: + print("Interface Type SSID Channel") + for intf in show_brief(): + print("{:<9} {:<12} {:<32} {:>3}".format(intf['name'], + intf['type'], + intf['ssid'], + intf['channel'])) + exit(0) + + elif args.stations: + print("Station Signal RX: bytes packets TX: bytes packets") + for client in show_clients(args.stations): + print("{:<17} {:>3} {:>15} {:>9} {:>15} {:>10} ".format(client['mac'], + client['signal'], client['rx_bytes'], client['rx_packets'], client['tx_bytes'], client['tx_packets'])) + + exit(0) + + else: + parser.print_help() + exit(1) -- cgit v1.2.3 From 3308bc150646abcf523c001a7b6086c46703b204 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 18 Nov 2019 16:07:37 +0100 Subject: wireless: T1627: config migrator does not support camel casing convert all nodes to lowercase --- interface-definitions/interfaces-wireless.xml | 2 +- src/migration-scripts/interfaces/3-to-4 | 30 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/interface-definitions/interfaces-wireless.xml b/interface-definitions/interfaces-wireless.xml index c9b9618e8..4d098ac37 100644 --- a/interface-definitions/interfaces-wireless.xml +++ b/interface-definitions/interfaces-wireless.xml @@ -43,7 +43,7 @@ HT (High Throughput) settings - + 40MHz intolerance, use 20MHz only! diff --git a/src/migration-scripts/interfaces/3-to-4 b/src/migration-scripts/interfaces/3-to-4 index 4a2fd9c0d..8b9bf7f96 100755 --- a/src/migration-scripts/interfaces/3-to-4 +++ b/src/migration-scripts/interfaces/3-to-4 @@ -24,24 +24,36 @@ if not config.exists(base): else: for wifi in config.list_nodes(base): # as converting a node to bool is always the same, we can script it - to_bool_nodes = ['capabilities ht 40MHz-incapable', 'capabilities ht auto-powersave', - 'capabilities ht delayed-block-ack', 'capabilities ht dsss-cck-40', - 'capabilities ht greenfield', 'capabilities ht ldpc', 'capabilities ht lsig-protection', - 'capabilities ht stbc tx', 'capabilities require-ht', 'capabilities require-vht', - 'capabilities vht antenna-pattern-fixed', 'capabilities vht ldpc', - 'capabilities vht stbc tx', 'capabilities vht tx-powersave', - 'capabilities vht vht-cf', 'expunge-failing-stations', 'isolate-stations'] + to_bool_nodes = ['capabilities ht 40MHz-incapable', + 'capabilities ht auto-powersave', + 'capabilities ht delayed-block-ack', + 'capabilities ht dsss-cck-40', + 'capabilities ht greenfield', + 'capabilities ht ldpc', + 'capabilities ht lsig-protection', + 'capabilities ht stbc tx', + 'capabilities require-ht', + 'capabilities require-vht', + 'capabilities vht antenna-pattern-fixed', + 'capabilities vht ldpc', + 'capabilities vht stbc tx', + 'capabilities vht tx-powersave', + 'capabilities vht vht-cf', + 'expunge-failing-stations', + 'isolate-stations'] for node in to_bool_nodes: if config.exists(base + [wifi, node]): tmp = config.return_value(base + [wifi, node]) # delete old node config.delete(base + [wifi, node]) + # set new node if it was enabled if tmp == 'true': - config.set(base + [wifi, node]) + # OLD CLI used camel casing in 40MHz-incapable which is + # not supported in the new backend. Convert all to lower-case + config.set(base + [wifi, node.lower()]) if config.exists(base + [wifi, 'debug']): - tmp = config.return_value(base + [wifi, 'debug']) config.delete(base + [wifi, 'debug']) try: -- cgit v1.2.3 From 0c90fcec3a9b32c24ca8afb70ff35f03b63d4a81 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 18 Nov 2019 16:29:51 +0100 Subject: wireless: T1627: fix generated ht_capab and vht_capab If no capabilities are configured on the CLI - there should also be no ht_capab or vht_capab entry in the resulting hostapd.conf --- src/conf_mode/interfaces-wireless.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 713b592e1..3a17b1480 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -172,7 +172,9 @@ ieee80211w=2 # DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) # 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) # L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) +{% if cap_ht %} ht_capab= +{%- endif -%} {%- if cap_ht_40mhz_incapable -%} [40-INTOLERANT] @@ -356,7 +358,9 @@ require_ht=1 # Indicates the possibility of Tx antenna pattern change # 0 = Tx antenna pattern might change during the lifetime of an association # 1 = Tx antenna pattern does not change during the lifetime of an association +{% if cap_vht %} vht_capab= +{%- endif -%} {%- if cap_vht_max_mpdu -%} [MAX-MPDU-{{ cap_vht_max_mpdu }}] @@ -741,6 +745,7 @@ wmm_ac_vo_acm=0 default_config_data = { 'address': [], 'address_remove': [], + 'cap_ht' : False, 'cap_ht_40mhz_incapable' : False, 'cap_ht_powersave' : False, 'cap_ht_chan_set_width' : '', @@ -756,6 +761,7 @@ default_config_data = { 'cap_ht_stbc_tx' : False, 'cap_req_ht' : False, 'cap_req_vht' : False, + 'cap_vht' : False, 'cap_vht_antenna_cnt' : '', 'cap_vht_antenna_fixed' : False, 'cap_vht_beamform' : '', @@ -894,55 +900,68 @@ def get_config(): wifi['address_remove'] = list_diff(eff_addr, wifi['address']) # 40MHz intolerance, use 20MHz only - if conf.exists('capabilities ht 40MHz-incapable'): + if conf.exists('capabilities ht 40mhz-incapable'): + wifi['cap_ht'] = True wifi['cap_ht_40mhz_incapable'] = True # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] if conf.exists('capabilities ht auto-powersave'): + wifi['cap_ht'] = True wifi['cap_ht_powersave'] = True # Supported channel set width if conf.exists('capabilities ht channel-set-width'): + wifi['cap_ht'] = True wifi['cap_ht_chan_set_width'] = conf.return_values('capabilities ht channel-set-width') # HT-delayed Block Ack if conf.exists('capabilities ht delayed-block-ack'): + wifi['cap_ht'] = True wifi['cap_ht_delayed_block_ack'] = True # DSSS/CCK Mode in 40 MHz if conf.exists('capabilities ht dsss-cck-40'): + wifi['cap_ht'] = True wifi['cap_ht_dsss_cck_40'] = True # HT-greenfield capability if conf.exists('capabilities ht greenfield'): + wifi['cap_ht'] = True wifi['cap_ht_greenfield'] = True # LDPC coding capability if conf.exists('capabilities ht ldpc'): + wifi['cap_ht'] = True wifi['cap_ht_ldpc'] = True # L-SIG TXOP protection capability if conf.exists('capabilities ht lsig-protection'): + wifi['cap_ht'] = True wifi['cap_ht_lsig_protection'] = True # Set Maximum A-MSDU length if conf.exists('capabilities ht max-amsdu'): + wifi['cap_ht'] = True wifi['cap_ht_max_amsdu'] = conf.return_value('capabilities ht max-amsdu') # Short GI capabilities if conf.exists('capabilities ht short-gi'): + wifi['cap_ht'] = True wifi['cap_ht_short_gi'] = conf.return_values('capabilities ht short-gi') # Spatial Multiplexing Power Save (SMPS) settings if conf.exists('capabilities ht smps'): + wifi['cap_ht'] = True wifi['cap_ht_smps'] = conf.return_value('capabilities ht smps') # Support for receiving PPDU using STBC (Space Time Block Coding) if conf.exists('capabilities ht stbc rx'): + wifi['cap_ht'] = True wifi['cap_ht_stbc_rx'] = conf.return_value('capabilities ht stbc rx') # Support for sending PPDU using STBC (Space Time Block Coding) if conf.exists('capabilities ht stbc tx'): + wifi['cap_ht'] = True wifi['cap_ht_stbc_tx'] = True # Require stations to support HT PHY (reject association if they do not) @@ -955,62 +974,77 @@ def get_config(): # Number of antennas on this card if conf.exists('capabilities vht antenna-count'): + wifi['cap_vht'] = True wifi['cap_vht_antenna_cnt'] = conf.return_value('capabilities vht antenna-count') # set if antenna pattern does not change during the lifetime of an association if conf.exists('capabilities vht antenna-pattern-fixed'): + wifi['cap_vht'] = True wifi['cap_vht_antenna_fixed'] = True # Beamforming capabilities if conf.exists('capabilities vht beamform'): + wifi['cap_vht'] = True wifi['cap_vht_beamform'] = conf.return_values('capabilities vht beamform') # VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes) if conf.exists('capabilities vht center-channel-freq freq-1'): + wifi['cap_vht'] = True wifi['cap_vht_center_freq_1'] = conf.return_value('capabilities vht center-channel-freq freq-1') # VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode) if conf.exists('capabilities vht center-channel-freq freq-2'): + wifi['cap_vht'] = True wifi['cap_vht_center_freq_2'] = conf.return_value('capabilities vht center-channel-freq freq-2') # VHT operating Channel width if conf.exists('capabilities vht channel-set-width'): + wifi['cap_vht'] = True wifi['cap_vht_chan_set_width'] = conf.return_value('capabilities vht channel-set-width') # LDPC coding capability if conf.exists('capabilities vht ldpc'): + wifi['cap_vht'] = True wifi['cap_vht_ldpc'] = True # VHT link adaptation capabilities if conf.exists('capabilities vht link-adaptation'): + wifi['cap_vht'] = True wifi['cap_vht_link_adaptation'] = True # Set the maximum length of A-MPDU pre-EOF padding that the station can receive if conf.exists('capabilities vht max-mpdu-exp'): + wifi['cap_vht'] = True wifi['cap_vht_max_mpdu_exp'] = conf.return_value('capabilities vht max-mpdu-exp') # Increase Maximum MPDU length if conf.exists('capabilities vht max-mpdu'): + wifi['cap_vht'] = True wifi['cap_vht_max_mpdu'] = conf.return_value('capabilities vht max-mpdu') # Increase Maximum MPDU length if conf.exists('capabilities vht short-gi'): + wifi['cap_vht'] = True wifi['cap_vht_short_gi'] = conf.return_values('capabilities vht short-gi') # Support for receiving PPDU using STBC (Space Time Block Coding) if conf.exists('capabilities vht stbc rx'): + wifi['cap_vht'] = True wifi['cap_vht_stbc_rx'] = conf.return_value('capabilities vht stbc rx') # Support for the transmission of at least 2x1 STBC (Space Time Block Coding) if conf.exists('capabilities vht stbc tx'): + wifi['cap_vht'] = True wifi['cap_vht_stbc_tx'] = True # Support for VHT TXOP Power Save Mode if conf.exists('capabilities vht tx-powersave'): + wifi['cap_vht'] = True wifi['cap_vht_tx_powersave'] = True # STA supports receiving a VHT variant HT Control field if conf.exists('capabilities vht vht-cf'): + wifi['cap_vht'] = True wifi['cap_vht_vht_cf'] = True # Wireless radio channel -- cgit v1.2.3 From 6ab442158bd036605b792b9345530541ff45213a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 18 Nov 2019 20:32:40 +0100 Subject: wireless: T1627: re-order WPA key in hostapd config --- src/conf_mode/interfaces-wireless.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 3a17b1480..b25205590 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -608,6 +608,12 @@ wpa_pairwise={{ sec_wpa_cipher | join(" ") }} # bit 1 = Shared Key Authentication (requires WEP) auth_algs=1 +# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit +# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase +# (8..63 characters) that will be converted to PSK. This conversion uses SSID +# so the PSK changes when ASCII passphrase is used and the SSID is changed. +wpa_passphrase={{ sec_wpa_passphrase }} + # Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The # entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be # added to enable SHA256-based stronger algorithms. @@ -615,12 +621,6 @@ auth_algs=1 # WPA-PSK-SHA256 = WPA2-Personal using SHA256 wpa_key_mgmt=WPA-PSK -# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit -# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase -# (8..63 characters) that will be converted to PSK. This conversion uses SSID -# so the PSK changes when ASCII passphrase is used and the SSID is changed. -wpa_passphrase={{ sec_wpa_passphrase }} - {% elif sec_wpa_radius -%} ##### IEEE 802.1X-2004 related configuration ################################## # Require IEEE 802.1X authorization -- cgit v1.2.3 From dfa2f0e8ecd8a117bf47b64d7099d613f487d799 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 18 Nov 2019 21:07:07 +0100 Subject: wireless: T1627: change RADIUS CLI syntax Adopt RADIUS configuration and harmonize it with the rest of VyOS. Move the following configuration block: security { wpa { cipher CCMP mode wpa2 radius-server 172.16.100.10 { port 1812 secret secretkey } radius-server 172.16.100.11 { port 1812 secret secretkey } } } to the harmonized version of: security { wpa { cipher CCMP mode wpa2 radius { server 172.16.100.10 { port 1812 secret secretkey } server 172.16.100.11 { port 1812 secret secretkey } } } } And add the new "set interfaces wireless wlan0 security wpa radius source-address" CLI command to specify the origin of any RADIUS query on systems having multiple IP addresses. --- interface-definitions/interfaces-wireless.xml | 65 ++++++++++++++------------- src/conf_mode/interfaces-wireless.py | 43 +++++++++++------- src/migration-scripts/interfaces/3-to-4 | 33 ++++++++++++++ 3 files changed, 94 insertions(+), 47 deletions(-) (limited to 'src') diff --git a/interface-definitions/interfaces-wireless.xml b/interface-definitions/interfaces-wireless.xml index a1712cb33..f1928ee0c 100644 --- a/interface-definitions/interfaces-wireless.xml +++ b/interface-definitions/interfaces-wireless.xml @@ -744,46 +744,47 @@ Invalid WPA pass phrase, must be 8 to 63 printable characters! - + - RADIUS server authentication - - ipv4 - IPv4 address of RADIUS server - + RADIUS specific configuration - - - RADIUS server to receive accounting info (default: 1813) - - 1-65535 - RADIUS server accounting port - - - - - - - + - RADIUS server port (default: 1812) + RADIUS server - 1-65535 - RADIUS server port + ipv4 + IPv4 address of RADIUS server - - - - - - - - Secret for radius access - + + + + Enable RADIUS server to receive accounting info + + + + + + RADIUS server port (default: 1812) + + 1-65535 + RADIUS server port + + + + + + + + + RADIUS shared secret key + + + + - + diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index b25205590..3b270a064 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -633,14 +633,24 @@ ieee8021x=1 # WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 wpa_key_mgmt=WPA-EAP +{% if sec_wpa_radius_source -%} +# RADIUS client forced local IP address for the access point +# Normally the local IP address is determined automatically based on configured +# IP addresses, but this field can be used to force a specific address to be +# used, e.g., when the device has multiple IP addresses. +radius_client_addr={{ sec_wpa_radius_source }} +{% endif %} + {% for radius in sec_wpa_radius -%} +# RADIUS authentication server auth_server_addr={{ radius.server }} auth_server_port={{ radius.port }} -auth_server_shared_secret={{ radius.secret }} -{% if radius.accounting -%} +auth_server_shared_secret={{ radius.key }} +{% if radius.acc_port -%} +# RADIUS accounting server acct_server_addr={{ radius.server }} acct_server_port={{ radius.acc_port }} -acct_server_shared_secret={{ radius.secret }} +acct_server_shared_secret={{ radius.key }} {% endif %} {% endfor %} @@ -1156,29 +1166,32 @@ def get_config(): if conf.exists('security wpa passphrase'): wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase') - # WPA radius server goes here - for server in conf.list_nodes('security wpa radius-server'): - # set new configuration level - conf.set_level(cfg_base + ' security wpa radius-server ' + server) + # WPA RADIUS source address + if conf.exists('security wpa radius source-address'): + wifi['sec_wpa_radius_source'] = conf.return_value('security wpa radius source-address') + # WPA RADIUS server + for server in conf.list_nodes('security wpa radius server'): + # set new configuration level + conf.set_level(cfg_base + ' security wpa radius server ' + server) radius = { 'server' : server, 'acc_port' : '', 'port' : 1812, - 'secret' : '' + 'key' : '' } - # receive RADIUS accounting info - if conf.exists('accounting'): - radius['acc_port'] = conf.return_value('accounting') - # RADIUS server port if conf.exists('port'): - radius['port'] = conf.return_value('port') + radius['port'] = int(conf.return_value('port')) + + # receive RADIUS accounting info + if conf.exists('accounting'): + radius['acc_port'] = radius['port'] + 1 # RADIUS server shared-secret - if conf.exists('secret'): - radius['secret'] = conf.return_value('secret') + if conf.exists('key'): + radius['key'] = conf.return_value('key') # append RADIUS server to list of servers wifi['sec_wpa_radius'].append(radius) diff --git a/src/migration-scripts/interfaces/3-to-4 b/src/migration-scripts/interfaces/3-to-4 index 8b9bf7f96..e3bd25a68 100755 --- a/src/migration-scripts/interfaces/3-to-4 +++ b/src/migration-scripts/interfaces/3-to-4 @@ -53,9 +53,42 @@ else: # not supported in the new backend. Convert all to lower-case config.set(base + [wifi, node.lower()]) + # Remove debug node if config.exists(base + [wifi, 'debug']): config.delete(base + [wifi, 'debug']) + # RADIUS servers + if config.exists(base + [wifi, 'security', 'wpa', 'radius-server']): + for server in config.list_nodes(base + [wifi, 'security', 'wpa', 'radius-server']): + base_server = base + [wifi, 'security', 'wpa', 'radius-server', server] + + # Migrate RADIUS shared secret + if config.exists(base_server + ['secret']): + key = config.return_value(base_server + ['secret']) + # write new configuration node + config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'key'], value=key) + # format as tag node + config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server']) + + # Migrate RADIUS port + if config.exists(base_server + ['port']): + port = config.return_value(base_server + ['port']) + # write new configuration node + config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'port'], value=port) + # format as tag node + config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server']) + + # Migrate RADIUS accounting + if config.exists(base_server + ['accounting']): + port = config.return_value(base_server + ['accounting']) + # write new configuration node + config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'accounting']) + # format as tag node + config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server']) + + # delete old radius-server nodes + config.delete(base + [wifi, 'security', 'wpa', 'radius-server']) + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From 704fbfd32ef14ab1c79dc9bdb17c409574f189c9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 18 Nov 2019 21:12:45 +0100 Subject: wireless: T1627: RADIUS servers must have a key specified --- src/conf_mode/interfaces-wireless.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 3b270a064..9cfc0bfc5 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -1255,6 +1255,10 @@ def verify(wifi): if wifi['sec_wpa'] and not (wifi['sec_wpa_passphrase'] or wifi['sec_wpa_radius']): raise ConfigError('Misssing WPA key or RADIUS server') + for radius in wifi['sec_wpa_radius']: + if not radius['key']: + raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) + conf = Config() if not conf.exists('system wifi-regulatory-domain'): raise ConfigError('Wireless regulatory domain is mandatory.\n' \ -- cgit v1.2.3 From b7038311f72b2666e847d08d4b5fc70aede458d3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 23 Nov 2019 09:42:19 +0100 Subject: wireless: T1627: support station mode Tested using: ------------- set interfaces wireless wlan0 address 'dhcp' set interfaces wireless wlan0 channel '0' set interfaces wireless wlan0 description '1' set interfaces wireless wlan0 physical-device 'phy0' set interfaces wireless wlan0 security wpa passphrase '12345678' set interfaces wireless wlan0 ssid 'VyOS-TEST' set interfaces wireless wlan0 type 'station' --- src/conf_mode/interfaces-wireless.py | 141 ++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 9cfc0bfc5..17b0876a0 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -752,6 +752,18 @@ wmm_ac_vo_acm=0 """ +# Please be careful if you edit the template. +config_wpa_suppl_tmpl = """ +# WPA supplicant config +network={ + ssid="{{ ssid }}" +{%- if sec_wpa_passphrase %} + psk="{{ sec_wpa_passphrase }}" +{% endif %} +} + +""" + default_config_data = { 'address': [], 'address_remove': [], @@ -822,22 +834,51 @@ default_config_data = { 'vif_remove': [] } -def get_config_name(intf): - cfg_file = r'/etc/hostapd/{}.cfg'.format(intf) +def get_conf_file(conf_type, intf): + cfg_dir = '/var/run/' + conf_type + + # create directory on demand + if not os.path.exists(cfg_dir): + os.mkdir(cfg_dir) + # fix permissions - corresponds to mode 755 + os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(cfg_dir, uid, gid) + + cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file +def get_pid(conf_type, intf): + cfg_dir = '/var/run/' + conf_type -def wifi_mkdir(directory): # create directory on demand - if not os.path.exists(directory): - os.mkdir(directory) + if not os.path.exists(cfg_dir): + os.mkdir(cfg_dir) + # fix permissions - corresponds to mode 755 + os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(cfg_dir, uid, gid) + + cfg_file = cfg_dir + r'/{}.pid'.format(intf) + return cfg_file - # fix permissions - corresponds to mode 755 - os.chmod(directory, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(directory, uid, gid) +def get_wpa_suppl_config_name(intf): + cfg_dir = '/var/run/wpa_supplicant' + + # create directory on demand + if not os.path.exists(cfg_dir): + os.mkdir(cfg_dir) + # fix permissions - corresponds to mode 755 + os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(cfg_dir, uid, gid) + + cfg_file = cfg_dir + r'/{}.cfg'.format(intf) + return cfg_file def subprocess_cmd(command): p = Popen(command, stdout=PIPE, shell=True) @@ -1267,37 +1308,57 @@ def verify(wifi): return None def generate(wifi): - if wifi['deleted']: - if os.path.isfile(get_config_name(wifi['intf'])): - os.unlink(get_config_name(wifi['intf'])) - - return None - - # create config directory on demand - directory = os.path.dirname(get_config_name(wifi['intf'])) - wifi_mkdir(directory) - - tmpl = Template(config_hostapd_tmpl) - config_text = tmpl.render(wifi) - with open(get_config_name(wifi['intf']), 'w') as f: - f.write(config_text) + pid = 0 + # always stop hostapd service first before reconfiguring it + pidfile = get_pid('hostapd', wifi['intf']) + if os.path.isfile(pidfile): + pid = 0 + with open(pidfile, 'r') as f: + pid = int(f.read()) - return None + if pid_exists(pid): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) -def apply(wifi): - pid = 0 - pidfile = '/var/run/hostapd/{}.pid'.format(wifi['intf']) + # always stop wpa_supplicant service first before reconfiguring it + pidfile = get_pid('wpa_supplicant', wifi['intf']) if os.path.isfile(pidfile): pid = 0 with open(pidfile, 'r') as f: pid = int(f.read()) - # always stop hostapd service first before reconfiguring it if pid_exists(pid): cmd = 'start-stop-daemon --stop --quiet' cmd += ' --pidfile ' + pidfile subprocess_cmd(cmd) + # Delete config files if interface is removed + if wifi['deleted']: + if os.path.isfile(get_conf_file('hostapd', wifi['intf'])): + os.unlink(get_conf_file('hostapd', wifi['intf'])) + + if os.path.isfile(get_conf_file('wpa_supplicant', wifi['intf'])): + os.unlink(get_conf_file('wpa_supplicant', wifi['intf'])) + + return None + + # render appropriate new config files depending on access-point or station mode + if wifi['type'] == 'access-point': + tmpl = Template(config_hostapd_tmpl) + config_text = tmpl.render(wifi) + with open(get_conf_file('hostapd', wifi['intf']), 'w') as f: + f.write(config_text) + + elif wifi['type'] == 'station': + tmpl = Template(config_wpa_suppl_tmpl) + config_text = tmpl.render(wifi) + with open(get_conf_file('wpa_supplicant', wifi['intf']), 'w') as f: + f.write(config_text) + + return None + +def apply(wifi): w = EthernetIf(wifi['intf']) if wifi['deleted']: # delete interface @@ -1377,14 +1438,22 @@ def apply(wifi): vlan = e.add_vlan(vif['id']) apply_vlan_config(vlan, vif) - # Physical interface is now configured. Proceed by starting hostapd + # Physical interface is now configured. Proceed by starting hostapd or + # wpa_supplicant daemon cmd = 'start-stop-daemon --start --quiet' - cmd += ' --pidfile ' + pidfile - cmd += ' --exec /usr/sbin/hostapd' - # now pass arguments to hostapd binary - cmd += ' --' - cmd += ' -P ' + pidfile - cmd += ' -B ' + get_config_name(wifi['intf']) + if wifi['type'] == 'access-point': + cmd += ' --exec /usr/sbin/hostapd' + # now pass arguments to hostapd binary + cmd += ' -- -B' + cmd += ' -P {}'.format(get_pid('hostapd', wifi['intf'])) + cmd += ' {}'.format(get_conf_file('hostapd', wifi['intf'])) + elif wifi['type'] == 'station': + cmd += ' --exec /sbin/wpa_supplicant' + # now pass arguments to hostapd binary + cmd += ' -- -s -B -D nl80211' + cmd += ' -P {}'.format(get_pid('wpa_supplicant', wifi['intf'])) + cmd += ' -i {}'.format(wifi['intf']) + cmd += ' -c {}'.format(get_conf_file('wpa_supplicant', wifi['intf'])) # execute assembled command subprocess_cmd(cmd) -- cgit v1.2.3