Merge pull request #1337 from kernelkit/wifi-ap2

Add WiFi Access point support
This commit is contained in:
Mattias Walström
2026-01-02 23:50:02 +01:00
committed by GitHub
56 changed files with 4068 additions and 538 deletions
@@ -17,7 +17,25 @@
"state": {
"admin-state": "unlocked"
}
},
{
"name": "radio0",
"class": "infix-hardware:wifi",
"infix-hardware:wifi-radio": {
"country-code": "DE",
"band": "2.4GHz",
"channel": "auto"
}
},
{
"name": "radio1",
"class": "infix-hardware:wifi",
"infix-hardware:wifi-radio": {
"country-code": "DE",
"band": "5GHz",
"channel": "auto"
}
}
]
},
"ietf-interfaces:interfaces": {
@@ -150,12 +168,36 @@
}
},
{
"name": "wifi0",
"type": "infix-if-type:wifi"
"name": "wifi0-ap",
"type": "infix-if-type:wifi",
"infix-interfaces:wifi": {
"radio": "radio0",
"access-point": {
"ssid": "Infix",
"security": {
"secret": "wifi"
}
}
},
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
},
{
"name": "wifi1",
"type": "infix-if-type:wifi"
"name": "wifi1-ap",
"type": "infix-if-type:wifi",
"infix-interfaces:wifi": {
"radio": "radio1",
"access-point": {
"ssid": "Infix5Ghz",
"security": {
"secret": "wifi"
}
}
},
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
}
]
},
@@ -171,6 +213,15 @@
"certificates": {}
}
]
},
"symmetric-keys": {
"symmetric-key": [
{
"name": "wifi",
"infix-keystore:symmetric-key": "infixinfix",
"infix-keystore:key-format": "infix-crypto-types:wifi-preshared-key-format"
}
]
}
},
"ietf-netconf-acm:nacm": {
+1 -1
View File
@@ -108,7 +108,7 @@ To configure WiFi as a client, first store your WiFi password in the keystore:
admin@infix:/> configure
admin@infix:/config/> edit keystore symmetric-key mywifi
admin@infix:/config/keystore/…/mywifi/> set key-format wifi-preshared-key-format
admin@infix:/config/keystore/…/mywifi/> set cleartext-symmetric-key YourWiFiPassword
admin@infix:/config/keystore/…/mywifi/> set symmetric-key YourWiFiPassword
admin@infix:/config/keystore/…/mywifi/> leave
```
@@ -0,0 +1,3 @@
service <!> name:hostapd :%i \
[2345] hostapd -P/var/run/hostapd-%i.pid /etc/hostapd-%i.conf \
-- Wi-Fi Access Point @%i
@@ -1,5 +1,5 @@
service name:wpa_supplicant :%i \
[2345] wpa_supplicant -s -i %i -c /etc/wpa_supplicant-%i.conf -P/var/run/wpa_supplicant-%i.pid \
-- WPA supplicant @%i
-- Wi-Fi Station @%i
task name:wifi-scanner :%i [2345] <pid/wpa_supplicant:%i> /usr/libexec/infix/wifi-scanner %i -- Start scanning for SSID @ %i
task name:wifi-scanner :%i [2345] <pid/wpa_supplicant:%i> /usr/libexec/infix/wifi-scanner %i -- Start scanning for SSID @%i
@@ -0,0 +1,3 @@
# Rename WiFi PHY devices from phy0 to radio0 to avoid name clashes
SUBSYSTEM=="ieee80211", ACTION=="add", KERNEL=="phy*", \
RUN+="/bin/sh -c '/usr/sbin/iw phy %k set name radio%n'"
@@ -0,0 +1,7 @@
# Remove kernel-created WiFi interfaces
# All WiFi interfaces are now virtual interfaces created by confd
SUBSYSTEM=="net", ACTION=="add", KERNEL=="wlan*", \
TEST=="/sys/class/net/$name/phy80211/name", \
PROGRAM="/bin/cat /sys/class/net/%k/phy80211/name", \
TEST!="/run/wifi-cleaned-%c", \
RUN+="/bin/sh -c '/usr/sbin/iw dev %k del && touch /run/wifi-cleaned-%c'"
@@ -1 +0,0 @@
SUBSYSTEM=="net", ACTION=="add", TEST=="/sys/class/net/$name/wireless", NAME="wifi%n"
+454
View File
@@ -0,0 +1,454 @@
#!/usr/bin/env python3
"""
iw command wrapper that returns structured JSON data
Usage:
iw.py list - List all PHY devices
iw.py dev - List all interfaces grouped by PHY
iw.py info <device> - Get PHY or interface information
iw.py survey <interface> - Get channel survey data
"""
import sys
import json
import subprocess
import re
def run_iw(*args):
"""Run iw command and return output"""
try:
result = subprocess.run(
['iw'] + list(args),
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout
return None
except Exception:
return None
def normalize_phy_name(name):
"""
Convert radioN to phyN or vice versa based on what exists in sysfs.
Returns the actual phy name that exists.
"""
import os
# Try the name as-is first
if os.path.exists(f'/sys/class/ieee80211/{name}'):
return name
# Try converting radioN <-> phyN
if name.startswith('radio'):
phy_name = 'phy' + name[5:]
if os.path.exists(f'/sys/class/ieee80211/{phy_name}'):
return phy_name
elif name.startswith('phy'):
radio_name = 'radio' + name[3:]
if os.path.exists(f'/sys/class/ieee80211/{radio_name}'):
return radio_name
# Return original if nothing found
return name
def parse_phy_info(phy_name):
"""
Parse 'iw phy <name> info' output or 'iw <name> info' output
Returns: {bands, driver, manufacturer, max_txpower, num_virtual_interfaces, interface_combinations}
"""
# Normalize the phy name
actual_phy = normalize_phy_name(phy_name)
# Try 'iw phy <name> info' first
output = run_iw('phy', actual_phy, 'info')
# If that fails, try 'iw <name> info' (some systems support this)
if not output:
output = run_iw(actual_phy, 'info')
if not output:
return {}
result = {
'name': phy_name,
'bands': [],
'driver': None,
'manufacturer': None,
'max_txpower': None,
'num_virtual_interfaces': 0,
'interface_combinations': []
}
current_band = None
band_num = 0
in_combinations = False
max_power = None
for line in output.splitlines():
stripped = line.strip()
# Detect band sections
if stripped.startswith('Band '):
if current_band and current_band.get('frequencies'):
result['bands'].append(current_band)
band_num += 1
current_band = {
'band': band_num,
'frequencies': [],
'name': None,
'ht_capable': False,
'vht_capable': False,
'he_capable': False
}
in_combinations = False
# Parse frequencies (handle both "2412 MHz" and "2412.0 MHz" formats)
elif current_band and not in_combinations:
freq_match = re.match(r'\* ([0-9.]+) MHz.*?\(([0-9.]+) dBm\)', stripped)
if freq_match:
freq = int(float(freq_match.group(1))) # Convert "2412.0" to 2412
power = float(freq_match.group(2))
current_band['frequencies'].append(freq)
# Track max power
if max_power is None or power > max_power:
max_power = power
# Check capabilities
if 'HT ' in stripped or 'High Throughput' in stripped:
current_band['ht_capable'] = True
if 'VHT' in stripped or 'Very High Throughput' in stripped:
current_band['vht_capable'] = True
if 'HE ' in stripped or 'High Efficiency' in stripped:
current_band['he_capable'] = True
# Detect interface combinations section
if 'valid interface combinations:' in stripped.lower():
in_combinations = True
continue
# Parse interface combinations
if in_combinations:
if stripped.startswith('*'):
# Parse combination line
comb_info = {'limits': []}
# Parse limits: #{ type } <= max
limit_matches = re.findall(r'#\{\s*([^}]+)\s*\}\s*<=\s*(\d+)', stripped)
for types_str, max_val in limit_matches:
types = [t.strip() for t in types_str.split(',')]
comb_info['limits'].append({
'max': int(max_val),
'types': types
})
# Parse total
total_match = re.search(r'total\s*<=\s*(\d+)', stripped)
if total_match:
comb_info['max_total'] = int(total_match.group(1))
# Parse channels
channels_match = re.search(r'#channels\s*<=\s*(\d+)', stripped)
if channels_match:
comb_info['num_channels'] = int(channels_match.group(1))
if comb_info.get('limits') or comb_info.get('max_total'):
result['interface_combinations'].append(comb_info)
elif not stripped.startswith('#') and ':' in stripped and not stripped.startswith('*'):
# End of combinations section
in_combinations = False
# Add last band
if current_band and current_band.get('frequencies'):
result['bands'].append(current_band)
# Determine band names and assign band numbers
for band in result['bands']:
if band['frequencies']:
freq = band['frequencies'][0]
if 2400 <= freq <= 2500:
band['name'] = '2.4GHz'
band['band'] = 1
elif 5150 <= freq <= 5900:
band['name'] = '5GHz'
band['band'] = 2
elif 5955 <= freq <= 7115:
band['name'] = '6GHz'
band['band'] = 3
# Set max TX power
if max_power is not None:
result['max_txpower'] = int(max_power)
# Get driver and manufacturer from sysfs
try:
driver_link = subprocess.run(
['readlink', '-f', f'/sys/class/ieee80211/{actual_phy}/device/driver'],
capture_output=True, text=True, timeout=1
).stdout.strip()
if driver_link:
driver_name = driver_link.split('/')[-1]
result['driver'] = driver_name
# Map driver to manufacturer
driver_lower = driver_name.lower()
if 'mt' in driver_lower or 'mediatek' in driver_lower:
result['manufacturer'] = 'MediaTek Inc.'
elif 'rtw' in driver_lower or 'realtek' in driver_lower:
result['manufacturer'] = 'Realtek Semiconductor Corp.'
elif 'ath' in driver_lower or 'qca' in driver_lower:
result['manufacturer'] = 'Qualcomm Atheros'
elif 'iwl' in driver_lower or 'intel' in driver_lower:
result['manufacturer'] = 'Intel Corporation'
elif 'brcm' in driver_lower or 'broadcom' in driver_lower:
result['manufacturer'] = 'Broadcom Inc.'
except Exception:
pass
# Count virtual interfaces
dev_output = run_iw('dev')
if dev_output:
# Extract phy number from actual phy name
phy_num = None
if actual_phy.startswith('radio'):
phy_num = actual_phy[5:]
elif actual_phy.startswith('phy'):
phy_num = actual_phy[3:]
if phy_num:
count = 0
current_phy = None
for line in dev_output.splitlines():
if line.startswith('phy#'):
current_phy = line.replace('phy#', '').strip()
elif current_phy == phy_num and 'Interface' in line:
count += 1
result['num_virtual_interfaces'] = count
return result
def parse_interface_info(ifname):
"""
Parse 'iw dev <name> info' output
Returns: {ifname, iftype, mac, ssid, frequency, channel, txpower, channel_width}
"""
output = run_iw('dev', ifname, 'info')
if not output:
return {}
result = {'ifname': ifname}
for line in output.splitlines():
stripped = line.strip()
# Interface type
if stripped.startswith('type '):
result['iftype'] = stripped.split()[1]
# MAC address
elif stripped.startswith('addr '):
result['mac'] = stripped.split()[1]
# SSID
elif stripped.startswith('ssid '):
result['ssid'] = ' '.join(stripped.split()[1:])
# Channel/frequency
elif stripped.startswith('channel '):
parts = stripped.split()
if len(parts) >= 2:
result['channel'] = int(parts[1])
if 'MHz' in stripped:
freq_match = re.search(r'\((\d+) MHz', stripped)
if freq_match:
result['frequency'] = int(freq_match.group(1))
# Channel width
if 'width:' in stripped:
width_match = re.search(r'width:\s*(\d+)\s*MHz', stripped)
if width_match:
result['channel_width'] = f"{width_match.group(1)} MHz"
# TX power
elif stripped.startswith('txpower '):
power_match = re.search(r'([0-9.]+) dBm', stripped)
if power_match:
result['txpower'] = float(power_match.group(1))
return result
def parse_survey(ifname):
"""
Parse 'iw dev <name> survey dump' output
Returns: list of {frequency, in_use, noise, active_time, busy_time, receive_time, transmit_time}
"""
output = run_iw('dev', ifname, 'survey', 'dump')
if not output:
return []
channels = []
current_channel = None
for line in output.splitlines():
stripped = line.strip()
# New survey entry
if stripped.startswith('Survey data from'):
if current_channel:
channels.append(current_channel)
current_channel = None
# Frequency
elif stripped.startswith('frequency:'):
parts = stripped.split()
if len(parts) >= 2:
freq = int(parts[1])
in_use = '[in use]' in stripped
current_channel = {
'frequency': freq,
'in_use': in_use
}
# Channel metrics
elif current_channel:
if stripped.startswith('noise:'):
noise_match = re.search(r'(-?\d+) dBm', stripped)
if noise_match:
current_channel['noise'] = int(noise_match.group(1))
elif stripped.startswith('channel active time:'):
time_match = re.search(r'(\d+) ms', stripped)
if time_match:
current_channel['active_time'] = int(time_match.group(1))
elif stripped.startswith('channel busy time:'):
time_match = re.search(r'(\d+) ms', stripped)
if time_match:
current_channel['busy_time'] = int(time_match.group(1))
elif stripped.startswith('channel receive time:'):
time_match = re.search(r'(\d+) ms', stripped)
if time_match:
current_channel['receive_time'] = int(time_match.group(1))
elif stripped.startswith('channel transmit time:'):
time_match = re.search(r'(\d+) ms', stripped)
if time_match:
current_channel['transmit_time'] = int(time_match.group(1))
# Add last channel
if current_channel:
channels.append(current_channel)
return channels
def parse_list():
"""
Parse 'iw list' output
Returns: list of PHY names
"""
output = run_iw('list')
if not output:
return []
phys = []
for line in output.splitlines():
match = re.match(r'Wiphy (phy\d+|radio\d+)', line)
if match:
phys.append(match.group(1))
return phys
def parse_dev():
"""
Parse 'iw dev' output
Returns: dict mapping PHY numbers to list of interfaces
"""
output = run_iw('dev')
if not output:
return {}
result = {}
current_phy = None
for line in output.splitlines():
# PHY line: "phy#0" or "phy#1"
if line.startswith('phy#'):
current_phy = line.replace('phy#', '').strip()
if current_phy not in result:
result[current_phy] = []
# Interface line: " Interface wlan0"
elif current_phy and 'Interface' in line:
ifname = line.split('Interface')[1].strip()
result[current_phy].append(ifname)
return result
def main():
if len(sys.argv) < 2:
print(json.dumps({
'error': 'Usage: iw.py <command> [device]',
'commands': {
'list': 'List all PHY devices',
'dev': 'List all interfaces grouped by PHY',
'info': 'Get PHY or interface information (requires device)',
'survey': 'Get channel survey data (requires interface name)'
},
'examples': [
'iw.py list',
'iw.py dev',
'iw.py info radio0',
'iw.py info phy4',
'iw.py info wlan0',
'iw.py survey wlan0'
]
}, indent=2))
sys.exit(1)
command = sys.argv[1]
try:
if command == 'list':
data = parse_list()
elif command == 'dev':
data = parse_dev()
elif command == 'info':
if len(sys.argv) < 3:
data = {'error': 'info command requires device argument'}
else:
device = sys.argv[2]
# Auto-detect if device is a PHY (phy*/radio*) or interface
if device.startswith('phy') or device.startswith('radio'):
data = parse_phy_info(device)
else:
data = parse_interface_info(device)
elif command == 'survey':
if len(sys.argv) < 3:
data = {'error': 'survey command requires device argument'}
else:
device = sys.argv[2]
data = parse_survey(device)
else:
data = {'error': f'Unknown command: {command}'}
print(json.dumps(data, indent=2))
except Exception as e:
print(json.dumps({'error': str(e)}))
sys.exit(1)
if __name__ == '__main__':
main()
+724
View File
@@ -0,0 +1,724 @@
#!/usr/bin/env python3
"""
WiFi Channel Visualization Tool
Shows graphical representation of WiFi channel overlap and utilization
"""
import sys
import json
import argparse
class Colors:
"""ANSI color codes for terminal output"""
RESET = '\033[0m'
BOLD = '\033[1m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
GRAY = '\033[90m'
BG_RED = '\033[101m'
BG_GREEN = '\033[102m'
BG_YELLOW = '\033[103m'
BG_BLUE = '\033[104m'
BG_GRAY = '\033[100m'
def freq_to_channel(freq):
"""Convert frequency (MHz) to WiFi channel number"""
# 2.4 GHz band
if 2412 <= freq <= 2484:
if freq == 2484:
return 14
return (freq - 2412) // 5 + 1
# 5 GHz band
elif 5170 <= freq <= 5825:
return (freq - 5000) // 5
# 6 GHz band
elif 5955 <= freq <= 7115:
return (freq - 5950) // 5
return None
def get_channel_frequency(channel, band='2.4'):
"""Get center frequency for a channel"""
if band == '2.4':
if channel == 14:
return 2484
return 2412 + (channel - 1) * 5
elif band == '5':
return 5000 + channel * 5
return None
def get_busy_percentage(channel_data):
"""Calculate channel busy percentage"""
active = channel_data.get('active-time', 0)
busy = channel_data.get('busy-time', 0)
if active > 0:
return (busy / active) * 100
return 0
def get_utilization_color(busy_pct):
"""Get color based on channel utilization"""
if busy_pct >= 50:
return Colors.RED
elif busy_pct >= 25:
return Colors.YELLOW
elif busy_pct >= 10:
return Colors.CYAN
else:
return Colors.GREEN
def draw_channel_graph_2_4ghz(survey_data):
"""Draw channel overlap graph for 2.4 GHz band"""
# Parse survey data
channels = {}
in_use_channel = None
for ch_data in survey_data:
freq = ch_data.get('frequency')
ch_num = freq_to_channel(freq)
if ch_num and 1 <= ch_num <= 14:
busy_pct = get_busy_percentage(ch_data)
channels[ch_num] = {
'freq': freq,
'noise': ch_data.get('noise', -100),
'busy': busy_pct,
'in_use': ch_data.get('in-use', False),
'active_time': ch_data.get('active-time', 0),
'busy_time': ch_data.get('busy-time', 0)
}
if ch_data.get('in-use'):
in_use_channel = ch_num
if not channels:
print("No 2.4 GHz channel data available")
return
print(f"\n{Colors.BOLD}2.4 GHz WiFi Channel Overlap Visualization{Colors.RESET}")
print("=" * 80)
print(f"Channel width: 20 MHz | Channel spacing: 5 MHz")
print(f"Non-overlapping channels: 1, 6, 11 (shown in {Colors.GREEN}green{Colors.RESET})")
print()
# Draw frequency scale
print("Frequency (MHz):")
print("2400 2420 2440 2460 2480")
print("|-----------|-----------|-----------|-----------|")
# Draw each channel as a bar showing its 20 MHz width
# Each channel occupies ~4 adjacent channels worth of space
for ch in range(1, 14):
if ch not in channels:
continue
data = channels[ch]
busy_pct = data['busy']
is_in_use = data['in_use']
noise = data['noise']
# Determine color based on status
if is_in_use:
color = Colors.BG_BLUE
marker = ''
elif busy_pct >= 50:
color = Colors.RED
marker = ''
elif busy_pct >= 25:
color = Colors.YELLOW
marker = ''
elif busy_pct > 0:
color = Colors.CYAN
marker = ''
else:
color = Colors.GRAY
marker = '·'
# Non-overlapping channels get green color
if ch in [1, 6, 11] and not is_in_use and busy_pct < 10:
color = Colors.GREEN
# Calculate position (each channel is offset by 5 MHz = 1 position)
# Channel 1 is at 2412 MHz, base is 2400
offset = ((data['freq'] - 2400) // 5)
# Draw channel bar (20 MHz = 4 positions wide)
line = ' ' * 80
line_arr = list(line)
# Mark the channel span (20 MHz width)
for i in range(4):
pos = offset + i - 2 # Center the 20 MHz around channel
if 0 <= pos < len(line_arr):
line_arr[pos] = marker
# Add channel label
label_pos = offset
if 0 <= label_pos < len(line_arr) - 5:
# Clear space for label
for i in range(5):
if label_pos + i < len(line_arr):
line_arr[label_pos + i] = ' '
line = ''.join(line_arr)
# Status indicators
status = ""
if is_in_use:
status = f" {Colors.BOLD}[IN USE]{Colors.RESET}"
busy_color = get_utilization_color(busy_pct)
print(f"{color}Ch{ch:2d}{Colors.RESET} {color}{line}{Colors.RESET} "
f"{busy_color}{busy_pct:5.1f}%{Colors.RESET} "
f"{noise:4d}dBm{status}")
print("\n" + "=" * 80)
print(f"\n{Colors.BOLD}Legend:{Colors.RESET}")
print(f" {Colors.BG_BLUE}██{Colors.RESET} In use (your network)")
print(f" {Colors.RED}▓▓{Colors.RESET} High usage (>50%)")
print(f" {Colors.YELLOW}▒▒{Colors.RESET} Medium usage (25-50%)")
print(f" {Colors.CYAN}░░{Colors.RESET} Low usage (1-25%)")
print(f" {Colors.GRAY}··{Colors.RESET} Idle (<1%)")
print()
def draw_channel_list(survey_data):
"""Draw a simple channel list with utilization bars"""
print(f"\n{Colors.BOLD}Channel Utilization{Colors.RESET}")
print("=" * 80)
print(f"{'Ch':<4} {'Freq':<6} {'Noise':<8} {'Busy%':<8} {'Utilization Bar':<40}")
print("-" * 80)
for ch_data in sorted(survey_data, key=lambda x: x.get('frequency', 0)):
freq = ch_data.get('frequency')
ch_num = freq_to_channel(freq)
if not ch_num:
continue
noise = ch_data.get('noise', -100)
busy_pct = get_busy_percentage(ch_data)
is_in_use = ch_data.get('in-use', False)
# Create utilization bar (40 chars wide = 100%)
bar_length = int(busy_pct * 40 / 100)
bar_color = get_utilization_color(busy_pct)
if is_in_use:
bar = f"{Colors.BG_BLUE}{'' * bar_length}{Colors.RESET}"
marker = f" {Colors.BOLD}◀ IN USE{Colors.RESET}"
else:
bar = f"{bar_color}{'' * bar_length}{Colors.RESET}"
marker = ""
empty = '' * (40 - bar_length)
print(f"{ch_num:<4} {freq:<6} {noise:<8} {busy_pct:5.1f}% {bar}{Colors.GRAY}{empty}{Colors.RESET}{marker}")
print()
def draw_overlap_pie(survey_data):
"""Draw a pie-style visualization of channel group utilization"""
# Parse channel data into 3 non-overlapping groups
# Group 1: channels 1-5 (centered on ch 1)
# Group 2: channels 4-8 (centered on ch 6)
# Group 3: channels 9-13 (centered on ch 11)
channels = {}
in_use_channel = None
for ch_data in survey_data:
freq = ch_data.get('frequency')
ch_num = freq_to_channel(freq)
if ch_num and 1 <= ch_num <= 13:
busy_pct = get_busy_percentage(ch_data)
channels[ch_num] = {
'busy': busy_pct,
'noise': ch_data.get('noise', -100),
'in_use': ch_data.get('in-use', False)
}
if ch_data.get('in-use'):
in_use_channel = ch_num
if not channels:
return
# Calculate group utilization (average of channels in each group)
groups = [
{'name': 'Ch 1', 'channels': [1, 2, 3, 4, 5], 'center': 1},
{'name': 'Ch 6', 'channels': [4, 5, 6, 7, 8], 'center': 6},
{'name': 'Ch 11', 'channels': [9, 10, 11, 12, 13], 'center': 11},
]
for group in groups:
busy_values = [channels.get(ch, {}).get('busy', 0) for ch in group['channels'] if ch in channels]
group['avg_busy'] = sum(busy_values) / len(busy_values) if busy_values else 0
group['center_busy'] = channels.get(group['center'], {}).get('busy', 0)
group['in_use'] = in_use_channel in group['channels'] if in_use_channel else False
total_busy = sum(g['avg_busy'] for g in groups)
print(f"\n{Colors.BOLD}Channel Group Utilization (2.4 GHz){Colors.RESET}")
print("=" * 60)
print("Non-overlapping channel groups with their overlap zones:\n")
# Draw ASCII donut/pie
pie_width = 50
# Calculate proportions
if total_busy > 0:
for group in groups:
group['proportion'] = group['avg_busy'] / total_busy
group['width'] = max(1, int(group['proportion'] * pie_width))
else:
for group in groups:
group['proportion'] = 1/3
group['width'] = pie_width // 3
# Adjust to exactly fill pie_width
total_width = sum(g['width'] for g in groups)
if total_width < pie_width:
groups[0]['width'] += pie_width - total_width
# Draw the pie bar
pie_chars = ['', '', '']
colors = [Colors.GREEN, Colors.YELLOW, Colors.CYAN]
pie_line = ""
for i, group in enumerate(groups):
if group['in_use']:
color = Colors.BG_BLUE
elif group['avg_busy'] >= 50:
color = Colors.RED
elif group['avg_busy'] >= 25:
color = Colors.YELLOW
else:
color = Colors.GREEN
char = pie_chars[i % len(pie_chars)]
pie_line += f"{color}{char * group['width']}{Colors.RESET}"
# Draw centered pie
print(f"{'' * pie_width}")
print(f"{pie_line}")
print(f"{'' * pie_width}")
# Legend with percentages
print()
for i, group in enumerate(groups):
char = pie_chars[i % len(pie_chars)]
if group['in_use']:
color = Colors.BG_BLUE
marker = " ◀ IN USE"
elif group['avg_busy'] >= 50:
color = Colors.RED
marker = ""
elif group['avg_busy'] >= 25:
color = Colors.YELLOW
marker = ""
else:
color = Colors.GREEN
marker = ""
pct_of_total = group['proportion'] * 100
print(f" {color}{char * 3}{Colors.RESET} {group['name']:>5}: "
f"{group['center_busy']:5.1f}% busy (center), "
f"{group['avg_busy']:5.1f}% avg in overlap zone, "
f"{pct_of_total:4.1f}% of total{marker}")
# Draw overlap diagram
print(f"\n{Colors.BOLD}Channel Overlap Diagram:{Colors.RESET}")
print(" Ch: 1 2 3 4 5 6 7 8 9 10 11 12 13")
print(" ╔═══════════════════╗")
print(" G1: ║ 1 ─ 2 ─ 3 ─ 4 ─ 5 ║ (centered on ch 1)")
print(" ╚═══════╦═══════════╝")
print(" ╔═══════════════════╗")
print(" G2: ║ 4 ─ 5 ─ 6 ─ 7 ─ 8 ║ (centered on ch 6)")
print(" ╚═══════════╦═══════╝")
print(" ╔═══════════════════════╗")
print(" G3: ║ 9 ─10 ─11 ─12 ─13 ║ (centered on ch 11)")
print(" ╚═══════════════════════╝")
print()
def generate_svg(survey_data, output_file=None):
"""Generate SVG image(s) showing channel overlap and utilization for both bands"""
# Separate channels by band
channels_2_4 = {}
channels_5 = {}
in_use_2_4 = None
in_use_5 = None
for ch_data in survey_data:
freq = ch_data.get('frequency')
ch_num = freq_to_channel(freq)
if not ch_num:
continue
busy_pct = get_busy_percentage(ch_data)
ch_info = {
'freq': freq,
'busy': busy_pct,
'noise': ch_data.get('noise', -100),
'in_use': ch_data.get('in-use', False)
}
if 2400 <= freq <= 2500:
channels_2_4[ch_num] = ch_info
if ch_info['in_use']:
in_use_2_4 = ch_num
elif 5100 <= freq <= 5900:
channels_5[ch_num] = ch_info
if ch_info['in_use']:
in_use_5 = ch_num
def busy_to_color(busy_pct, is_in_use=False):
if is_in_use:
return "#3b82f6" # Blue
elif busy_pct >= 50:
return "#ef4444" # Red
elif busy_pct >= 25:
return "#f59e0b" # Yellow/Orange
elif busy_pct >= 10:
return "#06b6d4" # Cyan
else:
return "#22c55e" # Green
def generate_band_svg(channels, band, freq_min, freq_max, title, non_overlap_channels=None):
if not channels:
return None
width = 900
height = 400
margin_left = 60
margin_right = 40
margin_top = 60
margin_bottom = 80
chart_width = width - margin_left - margin_right
chart_height = height - margin_top - margin_bottom
def freq_to_x(freq):
return margin_left + (freq - freq_min) / (freq_max - freq_min) * chart_width
def busy_to_height(busy_pct):
return (busy_pct / 100) * chart_height
svg_parts = []
# SVG header
svg_parts.append(f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}">
<defs>
<style>
.title {{ font: bold 18px sans-serif; fill: #333; }}
.label {{ font: 12px sans-serif; fill: #666; }}
.axis {{ font: 10px sans-serif; fill: #333; }}
.channel-label {{ font: bold 10px sans-serif; fill: #333; }}
.legend {{ font: 12px sans-serif; fill: #333; }}
</style>
</defs>
<!-- Background -->
<rect width="{width}" height="{height}" fill="#fafafa"/>
<!-- Title -->
<text x="{width/2}" y="30" text-anchor="middle" class="title">{title}</text>
<!-- Chart area -->
<rect x="{margin_left}" y="{margin_top}" width="{chart_width}" height="{chart_height}" fill="#fff" stroke="#ddd"/>
''')
# Draw grid lines
for pct in [25, 50, 75, 100]:
y = margin_top + chart_height - busy_to_height(pct)
svg_parts.append(f' <line x1="{margin_left}" y1="{y}" x2="{margin_left + chart_width}" y2="{y}" stroke="#eee" stroke-dasharray="4,4"/>')
svg_parts.append(f' <text x="{margin_left - 5}" y="{y + 4}" text-anchor="end" class="axis">{pct}%</text>')
# Y-axis label
svg_parts.append(f' <text x="15" y="{margin_top + chart_height/2}" text-anchor="middle" transform="rotate(-90, 15, {margin_top + chart_height/2})" class="label">Channel Busy %</text>')
# Draw channels as bars
for ch_num, data in sorted(channels.items()):
center_freq = data['freq']
busy_pct = data['busy']
is_in_use = data['in_use']
# 20 MHz width: ±10 MHz from center
x1 = freq_to_x(center_freq - 10)
x2 = freq_to_x(center_freq + 10)
bar_width = x2 - x1
bar_height = busy_to_height(busy_pct)
bar_y = margin_top + chart_height - bar_height
color = busy_to_color(busy_pct, is_in_use)
opacity = 0.6 if not is_in_use else 0.8
# Draw the channel bar
svg_parts.append(f' <rect x="{x1}" y="{bar_y}" width="{bar_width}" height="{bar_height}" fill="{color}" opacity="{opacity}" stroke="{color}" stroke-width="1"/>')
# Channel label at bottom
label_x = freq_to_x(center_freq)
svg_parts.append(f' <text x="{label_x}" y="{margin_top + chart_height + 15}" text-anchor="middle" class="channel-label">{ch_num}</text>')
# Frequency label (only for some channels to avoid clutter)
if band == '2.4' or ch_num in [36, 52, 100, 149, 165]:
svg_parts.append(f' <text x="{label_x}" y="{margin_top + chart_height + 28}" text-anchor="middle" class="axis">{center_freq}</text>')
# Busy percentage on top of bar (if tall enough)
if bar_height > 20:
svg_parts.append(f' <text x="{label_x}" y="{bar_y + 15}" text-anchor="middle" class="channel-label" fill="white">{busy_pct:.0f}%</text>')
# X-axis labels
svg_parts.append(f' <text x="{margin_left + chart_width/2}" y="{height - 15}" text-anchor="middle" class="label">Channel (Center Frequency MHz)</text>')
# Legend
legend_y = margin_top + 10
legend_x = margin_left + chart_width - 150
legend_items = [
("#3b82f6", "In Use"),
("#ef4444", "High (>50%)"),
("#f59e0b", "Medium (25-50%)"),
("#06b6d4", "Low (10-25%)"),
("#22c55e", "Idle (<10%)"),
]
svg_parts.append(f' <rect x="{legend_x - 10}" y="{legend_y - 5}" width="160" height="{len(legend_items) * 18 + 10}" fill="white" stroke="#ddd" rx="4"/>')
for i, (color, label) in enumerate(legend_items):
y = legend_y + 10 + i * 18
svg_parts.append(f' <rect x="{legend_x}" y="{y - 8}" width="12" height="12" fill="{color}" opacity="0.7"/>')
svg_parts.append(f' <text x="{legend_x + 18}" y="{y + 2}" class="legend">{label}</text>')
# Non-overlapping channels note (for 2.4 GHz)
if non_overlap_channels:
svg_parts.append(f' <text x="{margin_left + 5}" y="{margin_top + chart_height + 50}" class="label">Non-overlapping: Ch {", ".join(map(str, non_overlap_channels))}</text>')
for ch in non_overlap_channels:
if ch in channels:
center_freq = channels[ch]['freq']
x = freq_to_x(center_freq)
svg_parts.append(f' <circle cx="{x}" cy="{margin_top + chart_height + 40}" r="8" fill="none" stroke="#22c55e" stroke-width="2"/>')
svg_parts.append(f' <text x="{x}" y="{margin_top + chart_height + 44}" text-anchor="middle" class="channel-label" fill="#22c55e">{ch}</text>')
else:
svg_parts.append(f' <text x="{margin_left + 5}" y="{margin_top + chart_height + 50}" class="label">All channels non-overlapping (20 MHz spacing)</text>')
svg_parts.append('</svg>')
return '\n'.join(svg_parts)
# Generate SVGs for each band
svg_2_4 = generate_band_svg(channels_2_4, '2.4', 2400, 2485,
"2.4 GHz WiFi Channel Overlap &amp; Utilization",
[1, 6, 11])
svg_5 = generate_band_svg(channels_5, '5', 5150, 5850,
"5 GHz WiFi Channel Utilization",
None)
# Combine or output separately
if svg_2_4 and svg_5:
# Combine into one SVG with both bands stacked
combined = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 820">
<g transform="translate(0, 0)">
{svg_2_4.replace('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 400">', '').replace('</svg>', '')}
</g>
<g transform="translate(0, 410)">
{svg_5.replace('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 400">', '').replace('</svg>', '')}
</g>
</svg>'''
svg_content = combined
elif svg_2_4:
svg_content = svg_2_4
elif svg_5:
svg_content = svg_5
else:
return None
if output_file:
with open(output_file, 'w') as f:
f.write(svg_content)
print(f"SVG written to: {output_file}")
else:
print(svg_content)
return svg_content
def draw_recommendations(survey_data, json_output=False):
"""Analyze channels and provide recommendations"""
# Parse channel data
channels = {}
in_use_channel = None
for ch_data in survey_data:
freq = ch_data.get('frequency')
ch_num = freq_to_channel(freq)
if ch_num:
busy_pct = get_busy_percentage(ch_data)
channels[ch_num] = {
'busy': busy_pct,
'noise': ch_data.get('noise', -100),
'in_use': ch_data.get('in-use', False)
}
if ch_data.get('in-use'):
in_use_channel = ch_num
# Find least congested non-overlapping channels
best_channels = []
for ch in [1, 6, 11]:
if ch in channels:
best_channels.append((ch, channels[ch]['busy']))
best_channels.sort(key=lambda x: x[1])
# JSON output
if json_output:
output = {
"recommended_channels": [
{"channel": ch, "busy_percent": round(busy, 1)}
for ch, busy in best_channels
]
}
if in_use_channel:
output["current_channel"] = in_use_channel
output["current_busy_percent"] = round(channels.get(in_use_channel, {}).get('busy', 0), 1)
print(json.dumps(output, indent=2))
return
# Text output
print(f"\n{Colors.BOLD}Channel Recommendations{Colors.RESET}")
print("=" * 80)
if in_use_channel:
print(f"Current channel: {Colors.BOLD}{in_use_channel}{Colors.RESET}")
current_busy = channels.get(in_use_channel, {}).get('busy', 0)
if current_busy > 50:
print(f" {Colors.RED}{Colors.RESET} High congestion detected ({current_busy:.1f}% busy)")
elif current_busy > 25:
print(f" {Colors.YELLOW}{Colors.RESET} Moderate congestion ({current_busy:.1f}% busy)")
else:
print(f" {Colors.GREEN}{Colors.RESET} Good channel utilization ({current_busy:.1f}% busy)")
print(f"\nRecommended non-overlapping channels (2.4 GHz):")
for i, (ch, busy) in enumerate(best_channels[:3], 1):
color = get_utilization_color(busy)
marker = "" if i == 1 else " "
print(f" {marker} Channel {ch:2d}: {color}{busy:5.1f}% busy{Colors.RESET}")
print()
def main():
parser = argparse.ArgumentParser(
description='Visualize WiFi channel overlap and utilization',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
# Read from yanger output (show all sections)
yanger -x "ixll -A ssh host sudo" ietf-hardware | %(prog)s
# Read from file
%(prog)s survey_data.json
# Show only list view
%(prog)s --list survey_data.json
# Show only specific sections
%(prog)s --overlap survey_data.json
%(prog)s --pie survey_data.json
%(prog)s --utilization survey_data.json
%(prog)s --recommendations survey_data.json
%(prog)s --overlap --pie survey_data.json
# Output recommendations as JSON
%(prog)s --recommendations --json survey_data.json
# Generate SVG image
%(prog)s --svg /tmp/wifi-channels.svg survey_data.json
%(prog)s --svg - survey_data.json > output.svg
'''
)
parser.add_argument('file', nargs='?', help='JSON file with hardware data (default: stdin)')
parser.add_argument('--list', action='store_true', help='Show simple list view instead of overlap graph')
parser.add_argument('--no-color', action='store_true', help='Disable colors')
parser.add_argument('--json', action='store_true', help='Output recommendations in JSON format')
parser.add_argument('--svg', metavar='FILE', help='Generate SVG image to FILE (use - for stdout)')
# Section filters
parser.add_argument('--overlap', action='store_true', help='Show only channel overlap visualization (2.4 GHz)')
parser.add_argument('--pie', action='store_true', help='Show only channel group pie chart (2.4 GHz)')
parser.add_argument('--utilization', action='store_true', help='Show only channel utilization list')
parser.add_argument('--recommendations', action='store_true', help='Show only channel recommendations')
args = parser.parse_args()
# Disable colors if requested
if args.no_color:
for attr in dir(Colors):
if not attr.startswith('_'):
setattr(Colors, attr, '')
# Read input
if args.file:
with open(args.file, 'r') as f:
data = json.load(f)
else:
data = json.load(sys.stdin)
# Extract survey data from hardware components
survey_data = []
hardware = data.get('ietf-hardware:hardware', {})
components = hardware.get('component', [])
for component in components:
if component.get('class') == 'infix-hardware:wifi':
wifi_radio = component.get('infix-hardware:wifi-radio', {})
survey = wifi_radio.get('survey', {})
channels = survey.get('channel', [])
if channels:
survey_data.extend(channels)
if not survey_data:
print("No WiFi survey data found in input", file=sys.stderr)
print("Expected format: yanger ietf-hardware output with wifi-radio survey data", file=sys.stderr)
sys.exit(1)
# Generate SVG if requested (exclusive mode)
if args.svg:
output_file = None if args.svg == '-' else args.svg
generate_svg(survey_data, output_file)
return
# Determine which sections to show
# If no section flags are set, show all sections
show_all = not (args.overlap or args.pie or args.utilization or args.recommendations)
show_overlap = show_all or args.overlap
show_pie = show_all or args.pie
show_utilization = show_all or args.utilization
show_recommendations_section = show_all or args.recommendations
# Draw visualization
freqs = [ch.get('frequency', 0) for ch in survey_data]
has_2_4ghz = any(2400 <= f <= 2500 for f in freqs)
if show_overlap and has_2_4ghz and not args.list:
draw_channel_graph_2_4ghz(survey_data)
if show_pie and has_2_4ghz:
draw_overlap_pie(survey_data)
if show_utilization:
draw_channel_list(survey_data)
if show_recommendations_section:
draw_recommendations(survey_data, json_output=args.json)
if __name__ == '__main__':
main()
+23
View File
@@ -6,6 +6,20 @@ All notable changes to the project are documented in this file.
[v26.01.0][UNRELEASED]
-------------------------
> [!WARNING]
> **BREAKING CHANGES:** This release includes breaking changes to WiFi configuration:
>
> - WiFi station/client configuration has been restructured. The `wifi` container
> now requires a `radio` reference, and station configuration has moved under a
> `wifi/station` container. Existing WiFi configurations must be manually updated.
> - WiFi radios are now configured via `ietf-hardware` instead of the interfaces module.
> [!NOTE]
> Noteworthy changes and additions in this release:
>
> - WiFi Access Point (AP) mode support with multi-SSID capability
> - RIPv2 routing support
### Changes
- Upgrade Linux kernel to 6.12.63 (LTS)
@@ -26,11 +40,20 @@ All notable changes to the project are documented in this file.
policy, keeping snapshots from every 5 minutes (recent) to yearly (historical)
- Add support data collection script, useful when troubleshooting issues on
deployed systems. Gathers system information, logs, and more. Issue #1287
- Add WiFi Access Point (AP) mode with multi-SSID support and WPA2/WPA3 security.
**BREAKING:** WiFi architecture refactored with radios configured via
`ietf-hardware` and interfaces requiring `radio` reference. Station config
moved to `wifi/station` container. Existing Wi-Fi interfaces will be
removed during upgrade (for the rest of the configuration to apply)
and you need to reconfigure them again. See [wifi.md](wifi.md) for details
### Fixes
- Fix #1314: Raspberry Pi 4B with 1 or 8 GiB RAM does not boot. This was due
newer EEPROM firmware in newer boards require a newer rpi-firmware package
- Fix #1082: Wi-Fi interfaces always scanned, introduce a `scan-mode`
to the Wi-Fi concept in Infix.
[v25.11.0][] - 2025-12-02
-------------------------
+367 -84
View File
@@ -1,14 +1,29 @@
# Wi-Fi (Wireless LAN)
Infix includes built-in Wi-Fi client support for connecting to
wireless networks. When a compatible Wi-Fi adapter is detected, the
system automatically begins scanning for available networks.
Infix includes comprehensive Wi-Fi support for both client (Station) and
Access Point modes. When a compatible Wi-Fi adapter is detected, the system
automatically creates a WiFi radio (PHY) in factory-config, that can
host virtual interfaces.
## Architecture Overview
Infix uses a two-layer WiFi architecture:
1. **WiFi Radio (PHY layer)**: Represents the physical wireless hardware
- Configured via `ietf-hardware` module
- Controls channel, transmit power, regulatory domain
- One radio can host multiple virtual interfaces
2. **WiFi Interface (Network layer)**: Virtual interface on a radio
- Configured via `infix-interfaces` module
- Can operate in Station (client) or Access Point mode
- Each interface references a parent radio
## Current Limitations
- Only client mode is supported (no access point functionality)
- USB hotplug is not supported - adapters must be present at boot
- Interface naming may be inconsistent with multiple USB Wi-Fi adapters
- AP and Station modes cannot be mixed on the same radio
## Supported Wi-Fi Adapters
@@ -16,6 +31,8 @@ Wi-Fi support is primarily tested with Realtek chipset-based adapters.
### Known Working Chipsets
- Built-in Wi-Fi on Banana Pi r3
- Built-in Wi-Fi on Raspberry Pi 4/CM4
- RTL8821CU
- Other Realtek chipsets may work but are not guaranteed
@@ -24,35 +41,128 @@ Wi-Fi support is primarily tested with Realtek chipset-based adapters.
> Firmware requirements vary by chipset
> Check kernel logs if your adapter is not detected
## Configuration
## Radio Configuration
Add a supported Wi-Fi network device. To verify that it has been
detected, look for `wifi0` in `show interface`
Before configuring WiFi interfaces, you must first configure the WiFi radio.
Radios are automatically discovered and named `radio0`, `radio1`, etc.
### Country Code and Regulatory Compliance
> [!IMPORTANT]
> The `country-code` setting is **legally required** and determines which WiFi channels and power levels are permitted in your location. Using an incorrect country code may violate local wireless regulations.
**Factory default**: Systems may ship with a default country code (typically "DE" for Germany in European builds or "00" for World domain). **You must configure the correct country code for your deployment location.**
**Common country codes**:
- Europe: DE (Germany), SE (Sweden), GB (UK), FR (France), ES (Spain)
- Americas: US (United States), CA (Canada), BR (Brazil)
- Asia-Pacific: JP (Japan), AU (Australia), CN (China)
See [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) for the complete list.
### Basic Radio Setup
Configure the radio with channel, power, and regulatory domain.
**For Station (client) mode:**
```
admin@example:/> configure
admin@example:/config/> edit hardware component radio0 wifi-radio
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
```
**For Access Point mode:**
```
admin@example:/> configure
admin@example:/config/> edit hardware component radio0 wifi-radio
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
```
**Key radio parameters:**
- `country-code`: Two-letter ISO 3166-1 code - determines allowed channels and maximum power. Examples: US, DE, GB, SE, FR, JP. **Must match your physical location for legal compliance.**
- `band`: 2.4GHz, 5GHz, or 6GHz (required for AP mode). Band selection automatically enables appropriate WiFi standards (2.4GHz: 802.11n, 5GHz: 802.11n/ac, 6GHz: 802.11n/ac/ax)
- `channel`: Channel number (1-196) or "auto" (required for AP mode). When set to "auto", defaults to channel 6 for 2.4GHz, channel 36 for 5GHz, or channel 109 for 6GHz
- `enable-wifi6`: Boolean (default: false). Opt-in to enable WiFi 6 (802.11ax) on 2.4GHz and 5GHz bands. The 6GHz band always uses WiFi 6 regardless of this setting
> [!NOTE]
> TX power and channel width are automatically determined by the driver based on regulatory constraints, PHY mode, and hardware capabilities.
### WiFi 6 (802.11ax) Support
WiFi 6 (802.11ax) provides improved performance in congested environments through
features like OFDMA, Target Wake Time, and BSS Coloring. By default, WiFi 6 is
only enabled on the 6GHz band (WiFi 6E requirement).
To enable WiFi 6 on 2.4GHz or 5GHz bands:
```
admin@example:/> show interface
INTERFACE PROTOCOL STATE DATA
lo loopback UP
ipv4 127.0.0.1/8 (static)
ipv6 ::1/128 (static)
e1 ethernet UP 02:00:00:00:00:01
ipv6 fe80::ff:fe00:1/64 (link-layer)
ipv6 fec0::ff:fe00:1/64 (link-layer)
wifi0 ethernet DOWN f0:09:0d:36:5f:86
wifi ssid: ------, signal: ------
admin@example:/> configure
admin@example:/config/> edit hardware component radio0 wifi-radio
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
admin@example:/config/hardware/component/radio0/wifi-radio/> set enable-wifi6 true
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
```
**WiFi 6 Benefits:**
- **OFDMA**: Better multi-user efficiency in dense environments
- **Target Wake Time**: Improved battery life for client devices
- **1024-QAM**: Higher throughput with strong signal conditions
- **BSS Coloring**: Reduced interference from neighboring networks
**Requirements:**
- Hardware must support 802.11ax
- Client devices must support WiFi 6 for full benefits
- Older WiFi 5/4 clients can still connect but won't use WiFi 6 features
> [!NOTE]
> The 6GHz band always uses WiFi 6 (802.11ax) regardless of the `enable-wifi6`
> setting, as WiFi 6E requires 802.11ax support.
## Discovering Available Networks (Scanning)
Before connecting to a WiFi network, you need to discover which networks
are available. Infix automatically scans for networks when a WiFi interface
is created with a radio reference.
### Enable Background Scanning
To enable scanning without connecting, configure the radio and create a WiFi
interface referencing it:
**Step 1: Configure the radio**
```
Add the new Wi-Fi interface to the configuration to start scanning.
admin@example:/> configure
admin@example:/config/> edit hardware component radio0 wifi-radio
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
```
admin@example:/config/> set interface wifi0
admin@example:/config/> leave
```
Now the system will now start scanning in the background. To
see the result read the operational datastore for interface `wifi0` or
use the CLI
**Step 2: Create WiFi interface with radio reference only**
```
admin@infix-00-00-00:/> show interface wifi0
admin@example:/> configure
admin@example:/config/> edit interface wifi0
admin@example:/config/interface/wifi0/> set wifi radio radio0
admin@example:/config/interface/wifi0/> leave
```
The system will now start scanning in the background. The interface will
operate in scan-only mode until you configure a specific mode (station or
access-point).
### View Available Networks
Use `show interface` to see discovered networks and their signal strength:
```
admin@example:/> show interface wifi0
name : wifi0
type : wifi
index : 3
@@ -64,89 +174,262 @@ ipv6 addresses :
SSID : ----
Signal : ----
SSID ENCRYPTION SIGNAL
ssid1 WPA2-Personal excellent
ssid2 WPA2-Personal excellent
ssid3 WPA2-Personal excellent
ssid4 WPA2-Personal good
ssid5 WPA2-Personal good
ssid6 WPA2-Personal good
SSID SECURITY SIGNAL
MyNetwork WPA2-Personal excellent
GuestWiFi WPA2-WPA3-Personal good
CoffeeShop Open fair
IoT-Devices WPA2-Personal good
```
In the CLI, signal strength is reported as: excellent, good, poor or
bad. For precise values, use NETCONF or RESTCONF, where the RSSI (in
dBm) is available in the operational datastore.
In the CLI, signal strength is reported as: excellent, good, fair or bad.
For precise RSSI values in dBm, use NETCONF or RESTCONF to access the
operational datastore directly.
Configure your Wi-Fi secret in the keystore, it should be between 8
and 63 characters
### Connect to a Network
Once you've identified the desired network from the scan results, configure
station mode with the SSID and credentials. First, store your WiFi password
in the keystore:
```
admin@example:/> configure
admin@example:/config/> edit keystore symmetric-key example
admin@example:/config/keystore/…/example/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/example/> set cleartext-symmetric-key mysecret
admin@example:/config/keystore/…/example/> leave
admin@example:/>
admin@example:/config/> edit keystore symmetric-key my-wifi-key
admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key YourWiFiPassword
admin@example:/config/keystore/…/my-wifi-key/> leave
```
Configure the Wi-Fi settings, set secret to the name selected above
for the symmetric key, in this case `example`.
WPA2 or WPA3 encryption will be automatically selected based on what
the access point supports. No manual selection is required unless
connecting to an open network. No support for certificate based
authentication yet.
Unencrypted network is also supported, to connect to an unencrypted
network (generally not recommended):
```
admin@example:/config/interface/wifi0/> set wifi encryption disabled
```
A valid `country-code` is also required for regulatory compliance, the
valid codes are documented in the YANG model `infix-wifi-country-codes`
Then configure the WiFi interface for station mode:
```
admin@example:/> configure
admin@example:/config/> edit interface wifi0
admin@example:/config/interface/wifi0/>
admin@example:/config/interface/wifi0/> set wifi ssid ssid1
admin@example:/config/interface/wifi0/> set wifi secret example
admin@example:/config/interface/wifi0/> set wifi country-code SE
admin@example:/config/interface/wifi0/> set wifi station ssid MyNetwork
admin@example:/config/interface/wifi0/> set wifi station security secret my-wifi-key
admin@example:/config/interface/wifi0/> leave
```
The Wi-Fi negotiation should now start immediately, provided that the
SSID and pre-shared key are correct. You can verify the connection by
running `show interface` again.
The interface will transition from scan-only mode to station mode and
attempt to connect to the specified network.
## Station Mode (Client)
Station mode connects to an existing Wi-Fi network. Before configuring station
mode, follow the "Discovering Available Networks (Scanning)" section above to
scan for available networks and identify the SSID you want to connect to.
### Step 1: Configure WiFi Password
Create a keystore entry for your WiFi password (8-63 characters):
```
admin@example:/> show interface
INTERFACE PROTOCOL STATE DATA
lo loopback UP
ipv4 127.0.0.1/8 (static)
ipv6 ::1/128 (static)
e1 ethernet UP 02:00:00:00:00:01
ipv6 fe80::ff:fe00:1/64 (link-layer)
ipv6 fec0::ff:fe00:1/64 (link-layer)
wifi0 ethernet UP f0:09:0d:36:5f:86
wifi ssid: ssid1, signal: excellent
admin@example:/> configure
admin@example:/config/> edit keystore symmetric-key my-wifi-key
admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key MyPassword123
admin@example:/config/keystore/…/my-wifi-key/> leave
```
admin@example:/>
### Step 2: Connect to Network
Configure station mode with the SSID and password to connect:
```
admin@example:/> configure
admin@example:/config/> edit interface wifi0
admin@example:/config/interface/wifi0/> set wifi station ssid MyHomeNetwork
admin@example:/config/interface/wifi0/> set wifi station security secret my-wifi-key
admin@example:/config/interface/wifi0/> leave
```
The connection attempt will start immediately. You can verify the connection status:
```
admin@example:/> show interface wifi0
name : wifi0
type : wifi
operational status : up
physical address : f0:09:0d:36:5f:86
SSID : MyHomeNetwork
Signal : excellent
```
**Station configuration parameters:**
- `radio`: Reference to the WiFi radio (mandatory) - already set during scanning
- `station ssid`: Network name to connect to (mandatory)
- `station security mode`: `auto` (default, WPA2/WPA3 auto-negotiation) or `disabled` (open network)
- `station security secret`: Reference to keystore entry (required unless mode is `disabled`)
> [!NOTE]
> The `auto` security mode automatically selects WPA3-SAE or WPA2-PSK based on
> what the access point supports, prioritizing WPA3 for better security.
> Certificate-based authentication (802.1X/EAP) is not yet supported.
## Access Point Mode
Access Point (AP) mode allows your device to create a WiFi network that
other devices can connect to. APs are configured as virtual interfaces on
a WiFi radio.
### Basic AP Configuration
First, ensure the radio is configured (see Radio Configuration above). Then
create an AP interface:
```
admin@example:/> configure
admin@example:/config/> edit interface wifi0
admin@example:/config/interface/wifi0/> set wifi radio radio0
admin@example:/config/interface/wifi0/> set wifi access-point ssid MyNetwork
admin@example:/config/interface/wifi0/> set wifi access-point security mode wpa2-personal
admin@example:/config/interface/wifi0/> set wifi access-point security secret example
admin@example:/config/interface/wifi0/> leave
```
> [!NOTE]
> Using `wifiN` as the interface name automatically sets the type to WiFi.
> Alternatively, you can use any name and explicitly set `type wifi`.
**Access Point configuration parameters:**
- `radio`: Reference to the WiFi radio (mandatory)
- `access-point ssid`: Network name (SSID) to broadcast
- `access-point hidden`: Set to `true` to hide SSID (optional, default: false)
- `access-point security mode`: Security mode (see below)
- `access-point security secret`: Reference to keystore entry (for secured networks)
**Security modes:**
- `open`: No encryption (not recommended)
- `wpa2-personal`: WPA2-PSK (most compatible)
- `wpa3-personal`: WPA3-SAE (more secure, requires WPA3-capable clients)
- `wpa2-wpa3-personal`: Mixed mode (maximum compatibility)
### Hidden Network (SSID Hiding)
To create a hidden network that doesn't broadcast its SSID:
```
admin@example:/config/interface/wifi0/> set wifi access-point hidden true
```
### Multi-SSID Configuration
Multiple AP interfaces on the same radio allow broadcasting multiple SSIDs,
each with independent security settings. This is useful for guest networks,
IoT devices, or segregating traffic into different VLANs.
**Step 1: Configure the radio** (shared by all APs)
```
admin@example:/> configure
admin@example:/config/> edit hardware component radio0 wifi-radio
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
```
**Step 2: Configure keystore secrets**
```
admin@example:/> configure
admin@example:/config/> edit keystore symmetric-key main-secret
admin@example:/config/keystore/…/main-secret/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/main-secret/> set symmetric-key MyMainPassword
admin@example:/config/> edit keystore symmetric-key guest-secret
admin@example:/config/keystore/…/guest-secret/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/guest-secret/> set symmetric-key GuestPassword123
admin@example:/config/> edit keystore symmetric-key iot-secret
admin@example:/config/keystore/…/iot-secret/> set key-format wifi-preshared-key-format
admin@example:/config/keystore/…/iot-secret/> set symmetric-key IoTDevices2025
admin@example:/config/keystore/…/iot-secret/> leave
```
**Step 3: Create multiple AP interfaces** (all on radio0)
```
admin@example:/> configure
# Primary AP - Main network (WPA3 for maximum security)
admin@example:/config/> edit interface wifi0
admin@example:/config/interface/wifi0/> set wifi radio radio0
admin@example:/config/interface/wifi0/> set wifi access-point ssid MainNetwork
admin@example:/config/interface/wifi0/> set wifi access-point security mode wpa3-personal
admin@example:/config/interface/wifi0/> set wifi access-point security secret main-secret
# Guest AP - Guest network (WPA2/WPA3 mixed for compatibility)
admin@example:/config/> edit interface wifi1
admin@example:/config/interface/wifi1/> set wifi radio radio0
admin@example:/config/interface/wifi1/> set wifi access-point ssid GuestNetwork
admin@example:/config/interface/wifi1/> set wifi access-point security mode wpa2-wpa3-personal
admin@example:/config/interface/wifi1/> set wifi access-point security secret guest-secret
admin@example:/config/interface/wifi1/> set custom-phys-address static 00:0c:43:26:60:01
# IoT AP - IoT devices (WPA2 for older device compatibility)
admin@example:/config/> edit interface wifi2
admin@example:/config/interface/wifi2/> set wifi radio radio0
admin@example:/config/interface/wifi2/> set wifi access-point ssid IoT-Devices
admin@example:/config/interface/wifi2/> set wifi access-point security mode wpa2-personal
admin@example:/config/interface/wifi2/> set wifi access-point security secret iot-secret
admin@example:/config/interface/wifi2/> set custom-phys-address static 00:0c:43:26:60:02
admin@example:/config/interface/wifi2/> leave
```
> [!IMPORTANT]
> **MAC Address Requirement for Multi-SSID:**
> When creating multiple AP interfaces on the same radio, you **must** configure
> a unique MAC address for each secondary interface (wifi1, wifi2, etc.) using
> `set custom-phys-address static <MAC>`. All interfaces on the same radio inherit
> the radio's hardware MAC address by default, which causes network conflicts. Only
> the primary interface (alphabetically first, e.g., wifi0) should use the default
> hardware MAC address.
>
> Choose MAC addresses from the same locally-administered range:
> - Primary (wifi0): Uses hardware MAC (e.g., `00:0c:43:26:60:00`)
> - Secondary (wifi1): `00:0c:43:26:60:01` (increment last octet)
> - Tertiary (wifi2): `00:0c:43:26:60:02` (increment last octet)
**Result:** Three SSIDs broadcasting simultaneously on radio0:
- `MainNetwork` (WPA3, most secure)
- `GuestNetwork` (WPA2/WPA3 mixed mode)
- `IoT-Devices` (WPA2 for compatibility)
All APs on the same radio share the same channel and physical layer settings
(configured at the radio level). Each AP can have its own:
- SSID (network name)
- Security mode and passphrase
- Hidden/visible SSID setting
- Bridge membership
You can verify the configuration with `show hardware component radio0` to see
radio settings, and `show interface` to see all active AP interfaces.
> [!IMPORTANT]
> AP and Station modes cannot be mixed on the same radio. All virtual interfaces
> on a radio must be the same mode (all APs or all Stations).
### AP as Bridge Port
WiFi AP interfaces can be added to bridges to integrate wireless devices
into your LAN:
```
admin@example:/> configure
admin@example:/config/> edit interface br0
admin@example:/config/interface/br0/> set type bridge
admin@example:/config/> edit interface wifi0
admin@example:/config/interface/wifi0/> set bridge-port bridge br0
admin@example:/config/interface/wifi0/> leave
```
## Troubleshooting Connection Issues
Use `show wifi scan wifi0` and `show interface` to verify signal strength
and connection status. If issues arise, try the following
troubleshooting steps:
Use `show interface wifi0` to verify signal strength and connection status.
If issues arise, try the following troubleshooting steps:
1. **Verify signal strength**: Check that the target network shows "good" or "excellent" signal
2. **Check credentials**: Verify the preshared key in `ietf-keystore`
1. **Verify signal strength**: Check that the target network shows "good" or "excellent" signal in scan results
2. **Check credentials**: Verify the preshared key in the keystore matches the network password
3. **Review logs**: Check system logs with `show log` for Wi-Fi related errors
4. **Regulatory compliance**: Ensure the country-code matches your location
5. **Hardware detection**: Confirm the adapter appears in `show interface`
4. **Regulatory compliance**: Ensure the country-code on the radio matches your location
5. **Hardware detection**: Confirm the WiFi radio appears in `show hardware`
If issues persist, check the system log for specific error messages that can help identify the root cause.
+4
View File
@@ -6,6 +6,10 @@ config BR2_PACKAGE_FEATURE_WIFI
select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
select BR2_PACKAGE_WIRELESS_REGDB
select BR2_PACKAGE_HOSTAPD
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
select BR2_PACKAGE_HOSTAPD_WPA3
select BR2_PACKAGE_HOSTAPD_WPS
select BR2_PACKAGE_IW
help
Enables WiFi in Infix. Enables all requried applications.
+3 -3
View File
@@ -3,8 +3,8 @@
#
########################################################################
# The system factory-config, failure-config and test-config are derived
# from default settings snippets, from /usr/share/confd/factory.d, and
# some generated snippets, e.g., hostname (based on base MAC address)
# from default settings snippets, from /usr/share/confd/factory.d, and
# some generated snippets, e.g., hostname (based on base MAC address)
# and number of interfaces.
#
# The resulting factory-config is used to create the syrepo db (below)
@@ -147,7 +147,7 @@ gen_test_cfg()
# Both factory-config and failure-config are generated every boot
# regardless if there is a static /etc/factory-config.cfg or not.
gen_factory_cfg
gen_factory_cfg
gen_failure_cfg
if [ -f "/mnt/aux/test-mode" ]; then
+46
View File
@@ -6,6 +6,8 @@ if jq -e '.["usb-ports"]' /run/system.json > /dev/null; then
else
usb_ports=""
fi
wifi_radios=$(/usr/libexec/infix/iw.py list 2>/dev/null | jq -r '.[]' || echo "")
gen_port()
{
@@ -20,6 +22,43 @@ gen_port()
}
EOF
}
gen_radio()
{
radio="$1"
# Detect supported bands from iw.py info JSON output
phy_info=$(/usr/libexec/infix/iw.py info "$radio" 2>/dev/null || echo '{"bands":[]}')
# Check if 2.4GHz band exists (band name "2.4GHz")
has_2ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "2.4GHz")] | length')
# Check if 5GHz band exists (band name "5GHz")
has_5ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "5GHz")] | length')
# Determine band setting
# If both bands supported, prefer 5GHz for better performance
if [ "$has_2ghz" -gt 0 ] && [ "$has_5ghz" -gt 0 ]; then
band="5GHz"
elif [ "$has_5ghz" -gt 0 ]; then
band="5GHz"
elif [ "$has_2ghz" -gt 0 ]; then
band="2.4GHz"
else
band="2.4GHz" # Fallback to 2.4GHz for maximum compatibility
fi
cat <<EOF
{
"name": "$radio",
"class": "infix-hardware:wifi",
"infix-hardware:wifi-radio": {
"country-code": "00",
"band": "$band",
"channel": "auto"
}
}
EOF
}
first=1
cat <<EOF
{
@@ -33,6 +72,13 @@ for port in $usb_ports; do
first=0;
gen_port "$port"
done
for radio in $wifi_radios; do
if [ $first -eq 0 ]; then
echo -n ','
fi
first=0;
gen_radio $radio
done
cat <<EOF
]}
}
@@ -1,8 +1,5 @@
#!/bin/sh
# Migrate keystore symmetric key syntax to new YANG schema (RFC 9643)
# The 'cleartext-key' leaf has been renamed to 'cleartext-symmetric-key'
# Old: symmetric-key[]/key-type/cleartext-key
# New: symmetric-key[]/key-type/cleartext-symmetric-key
# Rename cleartext-symmetric-key to symmetric-key
file=$1
temp=${file}.tmp
@@ -14,7 +11,7 @@ if .["ietf-keystore:keystore"]?."symmetric-keys"?."symmetric-key" then
# Rename cleartext-key to cleartext-symmetric-key
."key-type"."cleartext-key" as $key_value |
."key-type" |= (del(."cleartext-key") | . + {
"cleartext-symmetric-key": $key_value
"symmetric-key": $key_value
})
else
.
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Remove interfaces of type infix-if-type:wifi
# Wi-Fi support has been refactored and all radio
# settings have been moved to ietf-hardware.
file=$1
temp=${file}.tmp
jq '
if .["ietf-interfaces:interfaces"]?.interface then
.["ietf-interfaces:interfaces"].interface |= map(
select(.type != "infix-if-type:wifi")
)
else
.
end
' "$file" > "$temp" && mv "$temp" "$file"
+2 -1
View File
@@ -1,3 +1,4 @@
migratedir = $(pkgdatadir)/migrate/1.7
dist_migrate_DATA = 10-netconf-server-tcp-params.sh \
20-keystore-cleartext-key-rename.sh
20-keystore-cleartext-key-rename.sh \
30-remove-wifi-interfaces.sh
+122
View File
@@ -138,6 +138,128 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd
}
}
/* When WiFi interfaces change, add their radios to the diff */
struct lyd_node *difs = lydx_get_descendant(*diff, "interfaces", "interface", NULL);
if (difs) {
struct lyd_node *dif;
LYX_LIST_FOR_EACH(difs, dif, "interface") {
struct lyd_node *dwifi, *radio_node;
const char *ifname, *radio_name;
char xpath[256];
/* Check if this interface has a wifi container in the diff */
dwifi = lydx_get_child(dif, "wifi");
if (!dwifi)
continue;
/* Get the interface name */
ifname = lydx_get_cattr(dif, "name");
if (!ifname)
continue;
/* Get radio reference from config tree using xpath */
radio_node = lydx_get_xpathf(config,
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio",
ifname);
if (!radio_node)
continue;
radio_name = lyd_get_value(radio_node);
if (!radio_name)
continue;
/* Add the radio to the diff */
snprintf(xpath, sizeof(xpath), "/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio_name);
result = add_dependencies(diff, xpath, radio_name);
if (result == CONFD_DEP_ERROR) {
ERROR("Failed to add radio %s to diff for WiFi interface %s", radio_name, ifname);
return result;
}
DEBUG("Added radio %s to diff for WiFi interface %s", radio_name, ifname);
}
}
/* When WiFi radios change, add all interfaces using that radio to the diff */
struct ly_set *radios = lydx_find_xpathf(*diff,
"/ietf-hardware:hardware/component[infix-hardware:wifi-radio]");
if (radios && radios->count > 0) {
for (uint32_t i = 0; i < radios->count; i++) {
struct lyd_node *hradio = radios->dnodes[i];
const char *radio_name = lydx_get_cattr(hradio, "name");
struct ly_set *ifaces;
uint32_t j;
char xpath[256];
if (!radio_name)
continue;
/* Find all interfaces that reference this radio */
ifaces = lydx_find_xpathf(config,
"/ietf-interfaces:interfaces/interface[infix-interfaces:wifi/radio='%s']",
radio_name);
if (ifaces && ifaces->count > 0) {
for (j = 0; j < ifaces->count; j++) {
const char *ifname = lydx_get_cattr(ifaces->dnodes[j], "name");
/* Add the wifi container */
snprintf(xpath, sizeof(xpath),
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi",
ifname);
result = add_dependencies(diff, xpath, ifname);
if (result == CONFD_DEP_ERROR) {
ERROR("Failed to add interface wifi for %s (radio %s)", ifname, radio_name);
ly_set_free(ifaces, NULL);
ly_set_free(radios, NULL);
return result;
}
/* Add the radio leaf */
snprintf(xpath, sizeof(xpath),
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio",
ifname);
result = add_dependencies(diff, xpath, radio_name);
if (result == CONFD_DEP_ERROR) {
ERROR("Failed to add radio leaf for interface %s (radio %s)", ifname, radio_name);
ly_set_free(ifaces, NULL);
ly_set_free(radios, NULL);
return result;
}
/* Add station or access-point container depending on mode */
if (lydx_get_descendant(ifaces->dnodes[j], "interface", "wifi", "station", NULL)) {
snprintf(xpath, sizeof(xpath),
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/station",
ifname);
result = add_dependencies(diff, xpath, ifname);
if (result == CONFD_DEP_ERROR) {
ERROR("Failed to add station for interface %s (radio %s)", ifname, radio_name);
ly_set_free(ifaces, NULL);
ly_set_free(radios, NULL);
return result;
}
} else if (lydx_get_descendant(ifaces->dnodes[j], "interface", "wifi", "access-point", NULL)) {
snprintf(xpath, sizeof(xpath),
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/access-point",
ifname);
result = add_dependencies(diff, xpath, ifname);
if (result == CONFD_DEP_ERROR) {
ERROR("Failed to add access-point for interface %s (radio %s)", ifname, radio_name);
ly_set_free(ifaces, NULL);
ly_set_free(radios, NULL);
return result;
}
}
DEBUG("Added interface %s to diff for radio %s", ifname, radio_name);
}
ly_set_free(ifaces, NULL);
}
}
ly_set_free(radios, NULL);
}
return result;
}
+606 -17
View File
@@ -10,8 +10,14 @@
#include <limits.h>
#include "core.h"
#include "interfaces.h"
#include "dagger.h"
#define XPATH_BASE_ "/ietf-hardware:hardware"
#define XPATH_BASE_ "/ietf-hardware:hardware"
#define HOSTAPD_CONF "/etc/hostapd-%s.conf"
#define HOSTAPD_CONF_NEXT HOSTAPD_CONF"+"
#define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf"
#define WPA_SUPPLICANT_CONF_NEXT WPA_SUPPLICANT_CONF"+"
static int dir_cb(const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf)
@@ -150,6 +156,452 @@ out_free_xpath:
free(xpath);
return err;
}
static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio_name,
struct lyd_node ***iface_list, int *count)
{
struct lyd_node *iface, *wifi;
const char *radio;
struct lyd_node **list = NULL;
int n = 0;
if (!ifs)
return 0;
LYX_LIST_FOR_EACH(ifs, iface, "interface") {
wifi = lydx_get_child(iface, "wifi");
if (!wifi)
continue;
radio = lydx_get_cattr(wifi, "radio");
if (!radio || strcmp(radio, radio_name))
continue;
if (lydx_get_op(iface) == LYDX_OP_DELETE)
continue;
list = realloc(list, sizeof(struct lyd_node *) * n + 1);
list[n++] = iface;
}
*iface_list = list;
*count = n;
return 0;
}
static int wifi_gen_station(const char *ifname, struct lyd_node *station,
const char *radio, struct lyd_node *config)
{
const char *ssid, *secret_name, *secret, *security_mode;
struct lyd_node *security, *secret_node, *radio_node;
FILE *wpa_supplicant = NULL;
char *security_str = NULL;
const char *country;
int rc = SR_ERR_OK;
/* If station is NULL, we're in scan-only mode (no station container) */
if (station) {
ssid = lydx_get_cattr(station, "ssid");
security = lydx_get_child(station, "security");
security_mode = lydx_get_cattr(security, "mode");
secret_name = lydx_get_cattr(security, "secret");
} else {
ssid = NULL;
security = NULL;
security_mode = "disabled";
secret_name = NULL;
}
radio_node = lydx_get_xpathf(config,
"/hardware/component[name='%s']/wifi-radio", radio);
country = radio_node ? lydx_get_cattr(radio_node, "country-code") : NULL;
if (secret_name && strcmp(security_mode, "disabled") != 0) {
secret_node = lydx_get_xpathf(config,
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
secret_name);
secret = secret_node ? lyd_get_value(secret_node) : NULL;
} else {
secret = NULL;
}
wpa_supplicant = fopenf("w", WPA_SUPPLICANT_CONF_NEXT, ifname);
if (!wpa_supplicant) {
rc = SR_ERR_INTERNAL;
goto out;
}
fprintf(wpa_supplicant,
"ctrl_interface=/run/wpa_supplicant\n"
"autoscan=periodic:10\n"
"ap_scan=1\n");
if (country)
fprintf(wpa_supplicant, "country=%s\n", country);
/* If SSID is present, create network block. Otherwise, scan-only mode */
if (ssid) {
/* Station mode with network configured */
if (!strcmp(security_mode, "disabled")) {
asprintf(&security_str, "key_mgmt=NONE");
} else if (secret) {
asprintf(&security_str, "key_mgmt=SAE WPA-PSK\npsk=\"%s\"", secret);
}
fprintf(wpa_supplicant,
"network={\n"
"bgscan=\"simple: 30:-45:300\"\n"
"ssid=\"%s\"\n"
"%s\n"
"}\n", ssid, security_str);
free(security_str);
} else {
/* Scan-only mode - no station container configured */
fprintf(wpa_supplicant, "# Scan-only mode - no network configured\n");
}
out:
if (wpa_supplicant)
fclose(wpa_supplicant);
return rc;
}
/* Helper: Find all AP interfaces on a specific radio */
static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
char ***ap_list, int *count)
{
struct lyd_node *cif, *wifi, *ap;
const char *ifname, *radio;
char **list = NULL;
int n = 0;
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
wifi = lydx_get_child(cif, "wifi");
if (!wifi)
continue;
radio = lydx_get_cattr(wifi, "radio");
if (!radio || strcmp(radio, radio_name))
continue;
ap = lydx_get_child(wifi, "access-point");
if (!ap)
continue;
list = realloc(list, sizeof(char *) *n+1);
ifname = lydx_get_cattr(cif, "name");
list[n++] = strdup(ifname);
}
/* Sort alphabetically for consistent primary selection */
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
if (strcmp(list[i], list[j]) > 0) {
char *tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}
}
*ap_list = list;
*count = n;
return 0;
}
/* Generate BSS section for secondary AP (multi-SSID) */
static int wifi_gen_bss_section(FILE *hostapd, struct lyd_node *cifs, const char *ifname, struct lyd_node *config)
{
const char *ssid, *hidden, *security_mode, *secret_name, *secret;
struct lyd_node *cif, *wifi, *ap, *security, *secret_node;
/* Find the interface node for this BSS */
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
const char *name = lydx_get_cattr(cif, "name");
if (strcmp(name, ifname) == 0)
break;
}
if (!cif) {
ERROR("Failed to find interface %s for BSS section", ifname);
return SR_ERR_INVAL_ARG;
}
wifi = lydx_get_child(cif, "wifi");
ap = lydx_get_child(wifi, "access-point");
fprintf(hostapd, "\n# BSS %s\n", ifname);
fprintf(hostapd, "bss=%s\n", ifname);
/* SSID configuration */
ssid = lydx_get_cattr(ap, "ssid");
hidden = lydx_get_cattr(ap, "hidden");
if (ssid)
fprintf(hostapd, "ssid=%s\n", ssid);
if (hidden && !strcmp(hidden, "true"))
fprintf(hostapd, "ignore_broadcast_ssid=1\n");
/* Security configuration */
security = lydx_get_child(ap, "security");
security_mode = lydx_get_cattr(security, "mode");
if (!security_mode)
security_mode = "open";
/* Get secret from keystore if needed */
secret = NULL;
if (strcmp(security_mode, "open") != 0) {
secret_name = lydx_get_cattr(security, "secret");
if (secret_name) {
secret_node = lydx_get_xpathf(config,
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
secret_name);
if (secret_node)
secret = lyd_get_value(secret_node);
}
}
if (!strcmp(security_mode, "open")) {
fprintf(hostapd, "# Open network\n");
fprintf(hostapd, "auth_algs=1\n");
} else if (!strcmp(security_mode, "wpa2-personal")) {
fprintf(hostapd, "# WPA2-Personal\n");
fprintf(hostapd, "wpa=2\n");
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
fprintf(hostapd, "wpa_pairwise=CCMP\n");
if (secret)
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
} else if (!strcmp(security_mode, "wpa3-personal")) {
fprintf(hostapd, "# WPA3-Personal\n");
fprintf(hostapd, "wpa=2\n");
fprintf(hostapd, "wpa_key_mgmt=SAE\n");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret)
fprintf(hostapd, "sae_password=%s\n", secret);
fprintf(hostapd, "ieee80211w=2\n");
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
fprintf(hostapd, "# WPA2/WPA3 Mixed\n");
fprintf(hostapd, "wpa=2\n");
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret) {
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
fprintf(hostapd, "sae_password=%s\n", secret);
}
fprintf(hostapd, "ieee80211w=1\n");
}
return 0;
}
/* Generate hostapd config for all APs on a radio (multi-SSID support) */
static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs,
struct lyd_node *radio_node, struct lyd_node *config)
{
const char *ssid, *hidden, *security_mode, *secret_name, *secret;
struct lyd_node *primary_cif, *cif;
struct lyd_node *primary_wifi, *primary_ap;
struct lyd_node *security, *secret_node;
const char *country, *channel, *band;
const char *primary_ifname;
char hostapd_conf[256];
char **ap_list = NULL;
FILE *hostapd = NULL;
bool wifi6_enabled;
int ap_count = 0;
int i;
int rc = SR_ERR_OK;
wifi_find_radio_aps(cifs, radio_name, &ap_list, &ap_count);
if (ap_count == 0) {
DEBUG("No APs found on radio %s", radio_name);
goto cleanup;
}
DEBUG("Generating hostapd config for radio %s (%d APs)", radio_name, ap_count);
primary_ifname = ap_list[0];
primary_cif = NULL;
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
if (!strcmp(lydx_get_cattr(cif, "name"), primary_ifname)) {
primary_cif = cif;
break;
}
}
if (!primary_cif) {
ERROR("Failed to find primary AP interface %s", primary_ifname);
rc = SR_ERR_INVAL_ARG;
goto cleanup;
}
primary_wifi = lydx_get_child(primary_cif, "wifi");
primary_ap = lydx_get_child(primary_wifi, "access-point");
/* Get AP configuration */
ssid = lydx_get_cattr(primary_ap, "ssid");
hidden = lydx_get_cattr(primary_ap, "hidden");
security = lydx_get_child(primary_ap, "security");
security_mode = lydx_get_cattr(security, "mode");
secret_name = lydx_get_cattr(security, "secret");
secret = NULL;
/* Get radio configuration */
country = lydx_get_cattr(radio_node, "country-code");
band = lydx_get_cattr(radio_node, "band");
channel = lydx_get_cattr(radio_node, "channel");
wifi6_enabled = lydx_get_bool(radio_node, "enable_wifi6");
/* Get secret from keystore if not open network */
if (secret_name && strcmp(security_mode, "open") != 0) {
secret_node = lydx_get_xpathf(config,
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
secret_name);
if (secret_node) {
secret = lyd_get_value(secret_node);
}
}
snprintf(hostapd_conf, sizeof(hostapd_conf), HOSTAPD_CONF_NEXT, radio_name);
hostapd = fopen(hostapd_conf, "w");
if (!hostapd) {
ERROR("Failed to create hostapd config: %s", hostapd_conf);
rc = SR_ERR_INTERNAL;
goto cleanup;
}
fprintf(hostapd, "# Generated by Infix confd - WiFi Radio %s\n", radio_name);
fprintf(hostapd, "# Primary BSS: %s", primary_ifname);
if (ap_count > 1)
fprintf(hostapd, " (%d total APs)\n\n", ap_count);
else
fprintf(hostapd, "\n\n");
fprintf(hostapd, "interface=%s\n", primary_ifname);
fprintf(hostapd, "driver=nl80211\n");
fprintf(hostapd, "ctrl_interface=/run/hostapd\n\n");
fprintf(hostapd, "ssid=%s\n", ssid);
if (hidden && !strcmp(hidden, "true"))
fprintf(hostapd, "ignore_broadcast_ssid=1\n");
fprintf(hostapd, "\n");
if (country)
fprintf(hostapd, "country_code=%s\n", country);
/* Enable 802.11d (regulatory domain) and 802.11h (spectrum management/DFS) */
fprintf(hostapd, "ieee80211d=1\n");
fprintf(hostapd, "ieee80211h=1\n");
/* Band and channel configuration */
if (band) {
/* Set hardware mode based on band */
if (!strcmp(band, "2.4GHz")) {
fprintf(hostapd, "hw_mode=g\n");
} else if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz")) {
fprintf(hostapd, "hw_mode=a\n");
}
/* Set channel */
if (channel) {
if (strcmp(channel, "auto") == 0) {
/*
Use default channels: 6 for 2.4GHz, 36 for 5GHz, 109 for 6GHz, this
is a temporary hack, replace with logic for finding best free channel.
*/
if (!strcmp(band, "2.4GHz")) {
fprintf(hostapd, "channel=6\n");
} else if (!strcmp(band, "5GHz")) {
fprintf(hostapd, "channel=36\n");
} else if (!strcmp(band, "6GHz")) {
fprintf(hostapd, "channel=109\n");
} else {
/* Unknown band - use ACS */
fprintf(hostapd, "channel=0\n");
}
} else {
fprintf(hostapd, "channel=%s\n", channel);
}
}
}
if (band) {
if (!strcmp(band, "2.4GHz")) {
/* 2.4GHz: Enable 802.11n (HT), optionally WiFi 6 */
fprintf(hostapd, "ieee80211n=1\n");
if (wifi6_enabled) {
fprintf(hostapd, "ieee80211ax=1\n");
}
} else if (!strcmp(band, "5GHz")) {
/* 5GHz: Enable 802.11n and 802.11ac, optionally WiFi 6 */
fprintf(hostapd, "ieee80211n=1\n");
fprintf(hostapd, "ieee80211ac=1\n");
if (wifi6_enabled) {
fprintf(hostapd, "ieee80211ax=1\n");
}
} else if (!strcmp(band, "6GHz")) {
/* 6GHz: Enable 802.11ax (WiFi 6E required) */
fprintf(hostapd, "ieee80211n=1\n");
fprintf(hostapd, "ieee80211ac=1\n");
fprintf(hostapd, "ieee80211ax=1\n");
}
}
fprintf(hostapd, "\n");
/* Security configuration */
if (!strcmp(security_mode, "open")) {
fprintf(hostapd, "# Open network (no encryption)\n");
fprintf(hostapd, "auth_algs=1\n");
} else if (!strcmp(security_mode, "wpa2-personal")) {
fprintf(hostapd, "# WPA2-Personal\n");
fprintf(hostapd, "wpa=2\n");
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
fprintf(hostapd, "wpa_pairwise=CCMP\n");
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
} else if (!strcmp(security_mode, "wpa3-personal")) {
fprintf(hostapd, "# WPA3-Personal\n");
fprintf(hostapd, "wpa=2\n");
fprintf(hostapd, "wpa_key_mgmt=SAE\n");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
fprintf(hostapd, "sae_password=%s\n", secret);
fprintf(hostapd, "ieee80211w=2\n");
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
fprintf(hostapd, "# WPA2/WPA3 Mixed Mode\n");
fprintf(hostapd, "wpa=2\n");
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
fprintf(hostapd, "sae_password=%s\n", secret);
fprintf(hostapd, "ieee80211w=1\n");
}
/* Add BSS sections for secondary APs (multi-SSID) */
for (i = 1; i < ap_count; i++) {
DEBUG("Adding BSS section for secondary AP %s", ap_list[i]);
rc = wifi_gen_bss_section(hostapd, cifs, ap_list[i], config);
if (rc != SR_ERR_OK) {
ERROR("Failed to generate BSS section for %s", ap_list[i]);
fclose(hostapd);
goto cleanup;
}
}
fclose(hostapd);
cleanup:
for (i = 0; i < ap_count; i++)
free(ap_list[i]);
free(ap_list);
return rc;
}
static int hardware_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *xpath, sr_event_t event, unsigned request_id, void *priv)
{
@@ -187,49 +639,186 @@ static int hardware_cand(sr_session_ctx_t *session, uint32_t sub_id, const char
int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
struct lyd_node *difs = NULL, *dif = NULL, *cifs = NULL, *cif = NULL;
struct lyd_node *difs = NULL, *dif = NULL;
int rc = SR_ERR_OK;
if (event != SR_EV_DONE || !lydx_find_xpathf(diff, XPATH_BASE_))
if (!lydx_find_xpathf(diff, XPATH_BASE_))
return SR_ERR_OK;
cifs = lydx_get_descendant(config, "hardware", "component", NULL);
difs = lydx_get_descendant(diff, "hardware", "component", NULL);
LYX_LIST_FOR_EACH(difs, dif, "component") {
enum lydx_op op;
struct lyd_node *state;
struct lyd_node *state, *cif;
const char *admin_state;
const char *class, *name;
op = lydx_get_op(dif);
name = lydx_get_cattr(dif, "name");
if (op == LYDX_OP_DELETE) {
if (usb_authorize(confd->root, name, 0)) {
rc = SR_ERR_INTERNAL;
goto err;;
}
continue;
}
LYX_LIST_FOR_EACH(cifs, cif, "component") {
if (strcmp(name, lydx_get_cattr(cif, "name")))
/* Get the current config node for this component */
cif = lydx_get_xpathf(config, "/hardware/component[name='%s']", name);
if (!cif)
continue;
class = lydx_get_cattr(cif, "class");
/* Handle USB components */
if (!strcmp(class, "infix-hardware:usb")) {
if (event != SR_EV_DONE)
continue;
class = lydx_get_cattr(cif, "class");
if (strcmp(class, "infix-hardware:usb")) {
if (op == LYDX_OP_DELETE) {
/* Handle USB deletion */
if (usb_authorize(confd->root, name, 0)) {
rc = SR_ERR_INTERNAL;
goto err;
}
continue;
}
state = lydx_get_child(dif, "state");
admin_state = lydx_get_cattr(state, "admin-state");
if (usb_authorize(confd->root, name, !strcmp(admin_state, "unlocked"))) {
rc = SR_ERR_INTERNAL;
goto err;;
goto err;
}
} else if (!strcmp(class, "infix-hardware:wifi")) {
struct lyd_node *interfaces_config, *interfaces_diff;
struct lyd_node **wifi_iface_list = NULL;
struct lyd_node *station, *ap;
struct lyd_node *cwifi_radio;
int wifi_iface_count = 0;
char src[40], dst[40];
int ap_interfaces = 0;
switch (event) {
case SR_EV_ABORT:
continue;
case SR_EV_CHANGE:
break;
case SR_EV_DONE:
interfaces_diff = lydx_get_descendant(diff, "interfaces", "interface", NULL);
wifi_find_interfaces_on_radio(interfaces_diff, name,
&wifi_iface_list, &wifi_iface_count);
if (wifi_iface_count > 0) {
bool running, enabled;
station = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "station", NULL);
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
if (station || !ap || lydx_get_op(ap) == LYDX_OP_DELETE) {
const char *ifname = lydx_get_cattr(wifi_iface_list[0], "name");
running = !systemf("initctl -bfq status wpa_supplicant:%s", ifname);
if (lydx_get_op(station) == LYDX_OP_DELETE) {
erasef(WPA_SUPPLICANT_CONF, ifname);
erasef(WPA_SUPPLICANT_CONF_NEXT, ifname);
systemf("initctl -bfq disable wifi@%s", ifname);
} else {
snprintf(src, sizeof(src), WPA_SUPPLICANT_CONF_NEXT, ifname);
snprintf(dst, sizeof(dst), WPA_SUPPLICANT_CONF, ifname);
running = !systemf("initctl -bfq status wpa_supplicant:%s", ifname);
enabled = fexistf(WPA_SUPPLICANT_CONF_NEXT, ifname);
if (enabled) {
(void)rename(src, dst);
if (running)
systemf("initctl -bfq touch wifi@%s", ifname);
else
systemf("initctl -bfq enable wifi@%s", ifname);
}
}
} else if (wifi_iface_count > 0) {
/* AP mode - activate hostapd for radio */
snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name);
snprintf(dst, sizeof(dst), HOSTAPD_CONF, name);
running = !systemf("initctl -bfq status hostapd:%s", name);
enabled = fexistf(HOSTAPD_CONF_NEXT, name);
if (enabled) {
(void)rename(src, dst);
ap_interfaces++;
if (running)
systemf("initctl -bfq touch hostapd@%s", name);
else
systemf("initctl -bfq enable hostapd@%s", name);
}
}
}
if (!ap_interfaces) {
systemf("initctl -bfq disable hostapd@%s", name);
erasef(HOSTAPD_CONF, name);
erasef(HOSTAPD_CONF_NEXT, name);
}
free(wifi_iface_list);
continue;
default:
continue;
}
cwifi_radio = lydx_get_child(cif, "wifi-radio");
interfaces_config = lydx_get_descendant(config, "interfaces", "interface", NULL);
wifi_find_interfaces_on_radio(interfaces_config, name,
&wifi_iface_list, &wifi_iface_count);
if (!wifi_iface_count)
continue;
/*
* A radio operates in one of three modes:
* 1. Station mode: One station interface (client mode)
* 2. AP mode: One or more AP interfaces (hostapd multi-SSID)
* 3. Scan-only mode: WiFi interface with radio but no mode configured
*
* Check for station first - there can be at most one per radio.
* If no station or AP is configured, default to scan-only mode.
*/
station = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "station", NULL);
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
if (wifi_iface_count == 1 && station) {
/* Station mode (with or without SSID for scan-only) */
struct lyd_node *iface = wifi_iface_list[0];
if (lydx_is_enabled(iface, "enabled")) {
const char *ifname = lydx_get_cattr(iface, "name");
rc = wifi_gen_station(ifname, station, name, config);
if (rc != SR_ERR_OK) {
ERROR("Failed to generate station config for %s", ifname);
goto next;
}
}
} else if (!station && !ap) {
/* No station/AP configured - default to scan-only mode */
struct lyd_node *iface = wifi_iface_list[0];
if (lydx_is_enabled(iface, "enabled")) {
const char *ifname = lydx_get_cattr(iface, "name");
rc = wifi_gen_station(ifname, NULL, name, config);
if (rc != SR_ERR_OK) {
ERROR("Failed to generate scan-only config for %s", ifname);
goto next;
}
}
} else {
/* Multiple interfaces or APs */
rc = wifi_gen_aps_on_radio(name, interfaces_config, cwifi_radio, config);
if (rc != SR_ERR_OK) {
ERROR("Failed to generate AP config for radio %s", name);
goto next;
}
}
next:
/* Free the interface list */
free(wifi_iface_list);
wifi_iface_list = NULL;
wifi_iface_count = 0;
}
}
err:
return rc;
}
int hardware_candidate_init(struct confd *confd)
+97 -84
View File
@@ -3,108 +3,121 @@
#include "interfaces.h"
#define WPA_SUPPLICANT_FINIT_CONF "/etc/finit.d/available/wpa_supplicant-%s.conf"
#define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf"
/*
* WiFi Interface Management
*
* This file handles only virtual WiFi interface creation/deletion.
* WiFi daemon configuration (hostapd/wpa_supplicant) is handled by
* hardware.c when the WiFi radio (phy) is configured.
*/
static int wifi_gen_config(const char *ifname, const char *ssid, const char *country, const char *secret, const char* encryption, struct dagger *net)
/*
* Determine WiFi mode from YANG configuration
*/
typedef enum wifi_mode_t {
wifi_station,
wifi_ap,
wifi_unknown
} wifi_mode_t;
static wifi_mode_t wifi_get_mode(struct lyd_node *wifi)
{
FILE *wpa_supplicant = NULL, *wpa = NULL;
char *encryption_str;
if (lydx_get_child(wifi, "access-point"))
return wifi_ap;
else
return wifi_station; /* Need to return station even if "station" also is false, since that is the default scanning mode */
}
int wifi_mode_changed(struct lyd_node *wifi)
{
struct lyd_node *station, *ap;
enum lydx_op station_op, ap_op;
if (!wifi)
return 0;
station = lydx_get_child(wifi, "station");
ap = lydx_get_child(wifi, "access-point");
if (station)
station_op = lydx_get_op(station);
if (ap)
ap_op = lydx_get_op(ap);
return ((station && station_op == LYDX_OP_DELETE) || (ap && ap_op == LYDX_OP_DELETE));
}
/*
* Add WiFi virtual interface using iw
*/
int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
{
const char *ifname, *radio;
struct lyd_node *wifi;
wifi_mode_t mode;
FILE *iw;
int rc = SR_ERR_OK;
if (!secret && (ssid && country && encryption)) {
/* Not an error, updated from two ways, interface cb and keystore cb. */
return 0;
ifname = lydx_get_cattr(cif, "name");
wifi = lydx_get_child(cif, "wifi");
if (!wifi) {
ERROR("WiFi interface %s: no wifi container", ifname);
return SR_ERR_INVAL_ARG;
}
wpa = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "wpa_supplicant.sh");
if (!wpa) {
rc = SR_ERR_INTERNAL;
radio = lydx_get_cattr(wifi, "radio");
if (!radio) {
ERROR("WiFi interface %s: missing radio reference", ifname);
return SR_ERR_INVAL_ARG;
}
iw = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PRE, "wifi-iface.sh");
if (!iw) {
ERROR("Failed to open dagger file for WiFi interface creation");
return SR_ERR_INTERNAL;
}
mode = wifi_get_mode(wifi);
fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n");
fprintf(iw, "# Create %s interface %s on radio %s\n",
mode == wifi_station ? "station" : "access point", ifname, radio);
switch(mode) {
case wifi_station:
fprintf(iw, "iw phy %s interface add %s type managed\n", radio, ifname);
break;
case wifi_ap:
fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname);
break;
default:
ERROR("WiFi mode %d unknown", mode);
rc = SR_ERR_INVAL_ARG;
goto out;
}
fprintf(wpa, "# Generated by Infix confd\n");
fprintf(wpa, "if [ -f '/etc/finit.d/enabled/wifi@%s.conf' ];then\n", ifname);
fprintf(wpa, "initctl -bfqn touch wifi@%s\n", ifname);
fprintf(wpa, "else\n");
fprintf(wpa, "initctl -bfqn enable wifi@%s\n", ifname);
fprintf(wpa, "fi\n");
fclose(wpa);
wpa_supplicant = fopenf("w", WPA_SUPPLICANT_CONF, ifname);
if (!wpa_supplicant) {
rc = SR_ERR_INTERNAL;
goto out;
}
if (!secret || !ssid || !country || !encryption) {
fprintf(wpa_supplicant,
"ctrl_interface=/run/wpa_supplicant\n"
"autoscan=periodic:10\n"
"ap_scan=1\n");
} else {
if (!strcmp(encryption, "disabled")) {
asprintf(&encryption_str, "key_mgmt=NONE");
} else {
asprintf(&encryption_str, "key_mgmt=SAE WPA-PSK\npsk=\"%s\"", secret);
}
fprintf(wpa_supplicant,
"country=%s\n"
"ctrl_interface=/run/wpa_supplicant\n"
"autoscan=periodic:10\n"
"ap_scan=1\n"
"network={\n"
"bgscan=\"simple: 30:-45:300\"\n"
"ssid=\"%s\"\n"
"%s\n"
"}\n", country, ssid, encryption_str);
free(encryption_str);
}
fclose(wpa_supplicant);
out:
fclose(iw);
return rc;
}
int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net)
/*
* Delete WiFi virtual interface using iw
*/
int wifi_del_iface(struct lyd_node *dif, struct dagger *net)
{
const char *ssid, *secret_name, *secret, *ifname, *country, *encryption;
struct lyd_node *wifi, *secret_node;
const char *ifname;
FILE *iw;
bool enabled;
ifname = lydx_get_cattr(cif, "name");
ifname = lydx_get_cattr(dif, "name");
if (cif && !lydx_get_child(cif, "wifi")) {
return wifi_gen_config(ifname, NULL, NULL, NULL, NULL, net);
iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_POST, "wifi-iface.sh");
if (!iw) {
ERROR("Failed to open dagger file for WiFi interface deletion");
return SR_ERR_INTERNAL;
}
enabled = lydx_get_bool(cif, "enabled");
wifi = lydx_get_child(cif, "wifi");
ssid = lydx_get_cattr(wifi, "ssid");
secret_name = lydx_get_cattr(wifi, "secret");
country = lydx_get_cattr(wifi, "country-code");
encryption = lydx_get_cattr(wifi, "encryption");
secret_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name);
secret = lydx_get_cattr(secret_node, "cleartext-symmetric-key");
if (!enabled)
return wifi_gen_del(cif, net);
return wifi_gen_config(ifname, ssid, country, secret, encryption, net);
}
int wifi_gen_del(struct lyd_node *dif, struct dagger *net)
{
const char *ifname = lydx_get_cattr(dif, "name");
FILE *iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_PRE, "iw.sh");
fprintf(iw, "# Generated by Infix confd\n");
fprintf(iw, "# Generated by Infix confd - WiFi Interface Deletion\n");
fprintf(iw, "ip link set %s down\n", ifname); /* Required to change modes. */
fprintf(iw, "iw dev %s disconnect\n", ifname);
fprintf(iw, "initctl -bfqn disable wifi@%s\n", ifname);
fprintf(iw, "iw dev %s del\n", ifname);
fclose(iw);
erasef(WPA_SUPPLICANT_CONF, ifname);
return SR_ERR_OK;
}
+12 -10
View File
@@ -418,7 +418,7 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net,
case IFT_VXLAN:
return vxlan_gen(NULL, cif, ip);
case IFT_WIFI:
return wifi_gen(NULL, cif, net);
return wifi_add_iface(cif, net);
case IFT_ETH:
return netdag_gen_ethtool(net, cif, dif);
case IFT_LO:
@@ -448,7 +448,10 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
case IFT_ETH:
return netdag_gen_ethtool(net, cif, dif);
case IFT_WIFI:
return wifi_gen(dif, cif, net);
/* WiFi daemon config (hostapd/wpa_supplicant) is handled by
* hardware.c when the radio (phy) is configured. Interface
* creation/deletion is handled in netdag_gen_afspec_add(). */
return 0;
case IFT_DUMMY:
case IFT_GRE:
case IFT_GRETAP:
@@ -472,9 +475,10 @@ static bool netdag_must_del(struct lyd_node *dif, struct lyd_node *cif)
case IFT_BRIDGE:
case IFT_DUMMY:
case IFT_LO:
case IFT_WIFI:
return lydx_get_child(dif, "custom-phys-address") || lydx_get_descendant(dif, "wifi", "radio", NULL) || wifi_mode_changed(lydx_get_child(dif, "wifi"));
break;
case IFT_WIFI:
case IFT_ETH:
return lydx_get_child(dif, "custom-phys-address");
@@ -562,13 +566,12 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
case IFT_LO:
eth_gen_del(dif, ip);
break;
case IFT_WIFI:
eth_gen_del(dif, ip);
wifi_gen_del(dif, net);
break;
case IFT_VETH:
veth_gen_del(dif, ip);
break;
case IFT_WIFI:
wifi_del_iface(dif, net);
break;
case IFT_BRIDGE:
case IFT_DUMMY:
case IFT_GRE:
@@ -587,7 +590,7 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
static sr_error_t netdag_gen_iface_timeout(struct dagger *net, const char *ifname, const char *iftype)
{
if (!strcmp(iftype, "infix-if-type:ethernet") || !strcmp(iftype, "infix-if-type:wifi")) {
if (!strcmp(iftype, "infix-if-type:ethernet")) {
FILE *wait = dagger_fopen_net_init(net, ifname, NETDAG_INIT_TIMEOUT, "wait-interface.sh");
if (!wait) {
return -EIO;
@@ -734,10 +737,9 @@ static int netdag_init_iface(struct lyd_node *cif)
return vlan_add_deps(cif);
case IFT_VETH:
return veth_add_deps(cif);
case IFT_WIFI:
case IFT_DUMMY:
case IFT_ETH:
case IFT_WIFI:
case IFT_GRE:
case IFT_GRETAP:
case IFT_LO:
+6 -5
View File
@@ -25,15 +25,15 @@
_map(IFT_BRIDGE, "infix-if-type:bridge") \
_map(IFT_DUMMY, "infix-if-type:dummy") \
_map(IFT_ETH, "infix-if-type:ethernet") \
_map(IFT_WIFI, "infix-if-type:wifi") \
_map(IFT_GRE, "infix-if-type:gre") \
_map(IFT_GRETAP, "infix-if-type:gretap") \
_map(IFT_LAG, "infix-if-type:lag") \
_map(IFT_LAG, "infix-if-type:lag") \
_map(IFT_LO, "infix-if-type:loopback") \
_map(IFT_VETH, "infix-if-type:veth") \
_map(IFT_VLAN, "infix-if-type:vlan") \
_map(IFT_VXLAN, "infix-if-type:vxlan") \
/* */
_map(IFT_WIFI, "infix-if-type:wifi") \
/* */
enum iftype {
#define ift_enum(_enum, _str) _enum,
@@ -122,8 +122,9 @@ int bridge_mcd_gen(struct lyd_node *cifs);
int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
/* if-wifi.c */
int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net);
int wifi_gen_del(struct lyd_node *dif, struct dagger *net);
int wifi_add_iface(struct lyd_node *cif, struct dagger *net);
int wifi_del_iface(struct lyd_node *dif, struct dagger *net);
int wifi_mode_changed(struct lyd_node *wifi);
/* if-gre.c */
int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
+1 -1
View File
@@ -23,7 +23,7 @@ MODULES=(
"infix-syslog@2025-11-17.yang"
"iana-hardware@2018-03-13.yang"
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
"infix-hardware@2025-10-30.yang"
"infix-hardware@2025-12-04.yang"
"ieee802-dot1q-types@2022-10-29.yang"
"infix-ip@2025-11-02.yang"
"infix-if-type@2025-02-12.yang"
+370
View File
@@ -13,10 +13,18 @@ module infix-hardware {
prefix yang;
}
import infix-wifi-country-codes {
prefix iwcc;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Vital Product Data augmentation of ieee-hardware and deviations.";
revision 2025-12-04 {
description "Add WiFi radio survey container for channel utilization data.";
reference "internal";
}
revision 2025-10-30 {
description "Add phys-address leaf for hardware components and enable sensor support.";
reference "internal";
@@ -38,6 +46,38 @@ module infix-hardware {
description "A two-letter country code.";
}
/*
* WiFi-specific typedefs
*/
typedef wifi-radio-ref {
type leafref {
path "/iehw:hardware/iehw:component/iehw:name";
}
description
"Reference to a WiFi radio hardware component.
WiFi radios are hardware components with class 'ih:wifi'.";
}
typedef wifi-band {
type enumeration {
enum "2.4GHz" {
description "2.4 GHz band (channels 1-14, maximum compatibility)";
}
enum "5GHz" {
description "5 GHz band (less congestion, higher throughput, recommended)";
}
enum "6GHz" {
description "6 GHz band (WiFi 6E, requires compatible hardware)";
}
}
description "WiFi frequency band selection.";
}
/*
* Hardware class identities
*/
identity usb {
base iahw:hardware-class;
description "This identity is used to describe a USB port";
@@ -143,5 +183,335 @@ module infix-hardware {
}
}
}
/*
* WiFi Radio configuration (when class = 'ih:wifi')
*/
container wifi-radio {
when "derived-from-or-self(../iehw:class, 'ih:wifi')";
presence "WiFi radio configuration";
description
"WiFi radio/PHY configuration and operational data.
This container is present when the hardware component represents
a WiFi radio (class 'ih:wifi'). WiFi radios are physical devices
that can host multiple virtual WiFi interfaces (APs or Stations).";
leaf country-code {
type iwcc:country-code;
mandatory true;
description
"Two-letter ISO 3166-1 country code for regulatory compliance.
Sets the regulatory domain for this radio, determining:
- Allowed channels and frequencies
- Maximum transmit power
- DFS (Dynamic Frequency Selection) requirements
Examples: 'US', 'DE', 'JP'.
WARNING: Incorrect values may violate local laws and regulations.";
}
leaf channel {
type union {
type uint16 {
range "1..196";
}
type enumeration {
enum "auto" {
description "Automatic channel selection (ACS)";
}
}
}
default "auto";
description
"Operating channel number.
Required for Access Point mode.
Not used in Station mode (station uses AP's channel).
Channel availability depends on:
- Configured band (2.4/5/6 GHz)
- Regulatory domain (country-code)
- Hardware capabilities
Common channels:
- 2.4 GHz: 1-14 (channels 12-14 restricted in some countries)
- 5 GHz: 36, 40, 44, 48, 149, 153, 157, 161, 165 (varies by region)
- 6 GHz: 1-233 (WiFi 6E, where permitted)
Set to 'auto' for automatic channel selection.";
}
leaf band {
type wifi-band;
description
"Frequency band selection.
Required for Access Point mode.
Not used in Station mode (station uses AP's band).
Constraints:
- Hardware must support the selected band
- Regulatory domain affects channel availability
- PHY mode must be compatible with selected band
Recommendation: Use 5GHz for better performance and less
congestion in most environments.";
}
leaf enable-wifi6 {
type boolean;
default false;
description
"Enable WiFi 6 (802.11ax) on 2.4GHz and 5GHz bands.
By default, WiFi 6 is enabled only on 6GHz (WiFi 6E).
Set to 'true' to enable WiFi 6 on 2.4GHz and 5GHz bands.
WiFi 6 provides:
- OFDMA (better multi-user efficiency)
- Target Wake Time (better battery life)
- 1024-QAM (higher throughput)
- BSS Coloring (reduced interference)
Requires:
- Hardware support for 802.11ax
- Compatible clients for full benefits
Note: 6GHz band always uses WiFi 6 regardless of this setting.";
}
/*
* Operational state
*/
leaf frequency {
config false;
type uint32;
units "MHz";
description
"Current operating frequency in MHz.
Derived from the configured channel and band.
Example values:
- 2412 MHz (channel 1, 2.4 GHz)
- 5180 MHz (channel 36, 5 GHz)
- 5955 MHz (channel 1, 6 GHz)";
}
leaf noise {
config false;
type int16;
units "dBm";
description
"Background noise level on current channel in dBm.
Lower (more negative) values indicate a cleaner RF environment.
Typical values:
- -95 to -100 dBm: Very low noise (excellent)
- -85 to -95 dBm: Low noise (good)
- -75 to -85 dBm: Moderate noise
- -65 to -75 dBm: High noise (congested)";
}
leaf-list supported-channels {
config false;
type uint16;
description
"List of channels supported by this radio in the current
regulatory domain.
Channels depend on:
- Hardware capabilities
- Configured country-code
- Band selection
This list reflects actual usable channels after applying
regulatory constraints.";
}
leaf max-txpower {
config false;
type uint8;
units "dBm";
description
"Maximum transmit power allowed by the regulatory domain
for the current channel.
This is the regulatory limit for the current band and channel.";
}
leaf num-virtual-interfaces {
config false;
type uint8;
description
"Number of virtual interfaces (AP/Station) currently
configured on this radio.";
}
leaf driver {
config false;
type string;
description
"WiFi driver name (e.g., mt798x-wmac, ath10k).";
}
container max-interfaces {
config false;
description
"Maximum number of virtual interfaces supported by this radio.";
leaf ap {
type uint8;
description
"Maximum number of AP interfaces.";
}
leaf station {
type uint8;
description
"Maximum number of station interfaces.";
}
}
list bands {
config false;
key "band";
description
"Supported frequency bands and their capabilities.";
leaf band {
type string;
description
"Band identifier from iw (e.g., '1' for 2.4GHz, '2' for 5GHz).";
}
leaf name {
type string;
description
"Human-readable band name (e.g., '2.4GHz', '5GHz', '6GHz').";
}
leaf ht-capable {
type boolean;
description
"High Throughput (802.11n) support.";
}
leaf vht-capable {
type boolean;
description
"Very High Throughput (802.11ac) support.";
}
leaf he-capable {
type boolean;
description
"High Efficiency (802.11ax/WiFi 6) support.";
}
}
/*
* Channel survey data (operational state)
*/
container survey {
config false;
description
"WiFi channel survey data providing channel utilization
and interference information.
This data is collected from the WiFi driver and provides
insights into channel occupancy, noise levels, and RF activity.";
list channel {
key "frequency";
description
"Per-channel survey information.
Includes utilization metrics for all channels scanned by
the radio, not just the currently active channel.";
leaf frequency {
type uint32;
units "MHz";
description
"Channel center frequency in MHz.
Examples:
- 2412 MHz (2.4 GHz channel 1)
- 5180 MHz (5 GHz channel 36)
- 5955 MHz (6 GHz channel 1)";
}
leaf in-use {
type boolean;
description
"Whether this channel is currently in use by the radio.
Only one channel will have this set to true at a time.";
}
leaf noise {
type int16;
units "dBm";
description
"Background noise level on this channel in dBm.
Lower (more negative) values indicate cleaner RF environment.
Typical values:
- -95 to -100 dBm: Very low noise (excellent)
- -85 to -95 dBm: Low noise (good)
- -75 to -85 dBm: Moderate noise
- -65 to -75 dBm: High noise (congested)";
}
leaf active-time {
type uint32;
units "milliseconds";
description
"Total time the radio was active on this channel.
This is the survey measurement period for this channel.";
}
leaf busy-time {
type uint32;
units "milliseconds";
description
"Time the channel was detected as busy.
Includes time spent receiving frames, transmitting frames,
and time the channel was busy due to other sources.
Channel utilization = (busy-time / active-time) * 100%";
}
leaf receive-time {
type uint32;
units "milliseconds";
description
"Time spent receiving frames on this channel.
Subset of busy-time spent on frame reception.";
}
leaf transmit-time {
type uint32;
units "milliseconds";
description
"Time spent transmitting frames on this channel.
Subset of busy-time spent on frame transmission.";
}
}
}
}
}
}
@@ -928,6 +928,9 @@ submodule infix-if-bridge {
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
error-message "Bridge ports cannot have IP addresses configured.";
}
must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point" {
error-message "WiFi interfaces can only be bridge ports when configured as Access Points.";
}
description "Bridge association and port specific settings.";
uses bridge-port-common;
uses bridge-port-lower {
+1 -1
View File
@@ -110,6 +110,6 @@ module infix-if-type {
if-feature wifi;
base infix-interface-type;
base ianaift:ieee80211;
description "WiFi interface";
description "WiFi (802.11) interface";
}
}
+370 -109
View File
@@ -21,8 +21,11 @@ submodule infix-if-wifi {
import infix-if-type {
prefix infixift;
}
import infix-wifi-country-codes {
prefix iwcc;
import ietf-hardware {
prefix iehw;
}
import infix-hardware {
prefix ih;
}
organization "KernelKit";
@@ -30,12 +33,30 @@ submodule infix-if-wifi {
description
"WiFi-specific extensions to the standard IETF interfaces model.
This submodule defines configuration and operational data relevant to
WiFi interfaces, including security settings, network
discovery, and regulatory compliance.
This submodule defines configuration and operational data for WiFi
virtual interfaces, supporting both Access Point (AP) and Station
(client) modes.
It supports WiFi client mode and enables comprehensive management of
wireless connections, including encryption, country codes, and scanning.";
WiFi virtual interfaces are created on top of WiFi radios (PHYs)
defined in the infix-wifi-radio module. The radio provides physical
layer configuration (channel, power, PHY mode), while virtual
interfaces provide network-layer configuration (SSID, security).
Key features:
- Dual mode support: AP and Station
- Multi-SSID: Multiple APs on same radio
- Security: WPA2/WPA3 with keystore integration
- Operational state: Connection status, RSSI, client lists";
revision 2025-12-17 {
description
"Major refactoring for AP mode support (BREAKING CHANGE):
- Added radio reference (parent PHY)
- Added wifi-mode choice (AP vs Station)
- Reorganized configuration hierarchy
- Old configurations must be migrated manually";
reference "internal";
}
revision 2025-12-12 {
description "Adapt to new revision of model ietf-keystore.";
@@ -43,7 +64,7 @@ submodule infix-if-wifi {
}
revision 2025-05-27 {
description "Initial revision.";
description "Initial revision (Station mode only).";
reference "internal";
}
@@ -51,29 +72,6 @@ submodule infix-if-wifi {
description "WiFi support is an optional build-time feature in Infix.";
}
typedef encryption {
type enumeration {
enum auto {
description
"Enables WPA/WPA2/WPA3 encryption with automatic protocol
negotiation. The system uses the strongest supported variant supported by Access Point.";
}
enum disabled {
description
"Disables encryption for an open network.
WARNING: Open networks transmit data unencrypted and should only
be used in trusted environments.";
}
}
description
"Encryption modes available for WiFi connections.
- auto: Secure connection using WPA3/WPA2/WPA (auto-selected)
- disabled: Open network (unencrypted)";
}
augment "/if:interfaces/if:interface" {
when "derived-from-or-self(if:type, 'infixift:wifi')" {
description
@@ -82,107 +80,370 @@ submodule infix-if-wifi {
container wifi {
if-feature wifi;
presence "Configure Wi-Fi settings";
presence "Configure Wi-Fi virtual interface";
description
"WiFi-specific configuration and operational data.";
"WiFi virtual interface configuration.
leaf country-code {
type iwcc:country-code;
mandatory true;
description
"Two-letter ISO 3166-1 country code for regulatory compliance.
Each WiFi interface represents a virtual interface (VAP - Virtual
Access Point, or Station) created on a physical radio.
Examples: 'US', 'DE', 'JP'.
The interface must reference a radio defined in infix-wifi-radio
module, which provides the physical layer configuration.";
WARNING: Incorrect values may violate local laws.";
}
leaf encryption {
default auto;
type encryption;
description
"WiFi encryption method.
- auto (default): Enables WPA2/WPA3 auto-negotiation
- disabled: Disables encryption (open network)";
}
leaf ssid {
type string {
length "1..32";
leaf radio {
type leafref {
path "/iehw:hardware/iehw:component/iehw:name";
}
mandatory true;
must "derived-from-or-self(/iehw:hardware/iehw:component[iehw:name=current()]/iehw:class, 'ih:wifi')" {
error-message "Referenced hardware component must be a WiFi radio (class 'ih:wifi')";
}
description
"WiFi network name (SSID).
"Reference to parent WiFi radio (PHY).
Case-sensitive, must match the target network.
References a hardware component with class 'ih:wifi'.
The radio must exist and be configured before creating
virtual interfaces.
Length: 132 characters.";
Example: 'phy0' for the first WiFi radio.
All physical layer settings (channel, power, regulatory)
are inherited from the radio configuration.";
}
leaf secret {
type ks:central-symmetric-key-ref;
mandatory true;
must "../encryption != 'disabled'" {
error-message
"Pre-shared key required unless encryption is disabled.";
}
choice wifi-mode {
description
"Pre-shared key (PSK) for WPA-secured networks.";
}
"WiFi interface operating mode.
leaf rssi {
config false;
type int16;
units "dBm";
description
"Current received signal strength (RSSI) in dBm.
When no mode is configured, the interface operates in scan-only
mode, allowing discovery of available WiFi networks.
Lower (more negative) values indicate stronger signals.";
}
Once you've identified a network, configure either:
- Station mode: Connect to an existing WiFi network
- Access Point mode: Create a WiFi network for clients
list scan-results {
config false;
key ssid;
description
"List of discovered networks.";
Note: A radio can host either:
- Multiple AP interfaces (multi-SSID), OR
- A single Station interface
leaf ssid {
type string;
description
"SSID of the discovered network.";
Mixing AP and Station on the same radio is not supported.";
case station {
container station {
presence "Configure WiFi station (client) mode";
description
"WiFi Station mode configuration.
In station mode, the interface acts as a WiFi client,
connecting to an existing Access Point.
Only one station interface is allowed per radio.
Example use case: Connect to upstream WiFi network.";
leaf ssid {
type string {
length "1..32";
}
mandatory true;
description
"WiFi network name (SSID) to connect to.
Case-sensitive, must match the target network exactly.
Length: 132 characters.";
}
container security {
description
"WiFi security configuration.";
leaf mode {
type enumeration {
enum auto {
description
"Automatic security negotiation.
Tries WPA3-SAE, then WPA2-PSK, in that order.
Recommended for maximum compatibility and security.";
}
enum disabled {
description
"Open network (no security).
WARNING: All traffic is transmitted unencrypted!
Only use in trusted environments.";
}
}
default auto;
description
"Security mode for WiFi connection.
- auto (default): WPA3/WPA2 auto-negotiation (secure)
- disabled: Open network (insecure)";
}
leaf secret {
when "../mode != 'disabled'";
type ks:central-symmetric-key-ref;
mandatory true;
description
"Pre-shared key (PSK) reference.
References a symmetric key in the keystore.
For WPA2/WPA3 networks, this is the WiFi password.";
}
}
/* Operational state */
leaf rssi {
config false;
type int16;
units "dBm";
description
"Current received signal strength indication (RSSI) in dBm.
More negative values indicate weaker signal.
Typical values:
- -30 to -50 dBm: Excellent
- -50 to -60 dBm: Good
- -60 to -70 dBm: Fair
- -70 to -80 dBm: Weak
- Below -80 dBm: Very weak";
}
list scan-results {
config false;
key ssid;
description
"List of discovered WiFi networks.
Updated periodically by background scanning.";
leaf ssid {
type string;
description "SSID of the discovered network.";
}
leaf bssid {
type yang:mac-address;
description "BSSID (MAC address) of the AP.";
}
leaf rssi {
type int16;
units "dBm";
description "Signal strength of the network.";
}
leaf channel {
type uint16;
description "Channel on which the network was detected.";
}
leaf-list encryption {
ordered-by user;
type string;
description
"Human-readable security information.
Examples: 'WPA2-Personal', 'WPA3-SAE', 'Open'";
}
}
}
}
leaf bssid {
type string;
description
"BSSID of the discovered network.";
}
case access-point {
container access-point {
presence "Configure WiFi Access Point mode";
leaf rssi {
type int16;
units "dBm";
description
"Signal strength of the network.";
}
description
"WiFi Access Point mode configuration.
leaf channel {
type int16;
description
"Channel on which the network was detected.";
}
In AP mode, the interface provides a WiFi network that
clients can connect to.
leaf-list encryption {
ordered-by user;
type string;
description
"Human-readable description of the detected security.";
Multiple AP interfaces can be created on the same radio
for multi-SSID support (Guest network, IoT network, etc.).
Example use case: Create WiFi hotspot.";
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:band" {
error-message "Parent radio must have 'band' configured for Access Point mode";
}
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:channel" {
error-message "Parent radio must have 'channel' configured for Access Point mode";
}
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:country-code != '00'" {
error-message "Country code '00' (world regulatory domain) is not allowed for Access Point mode. Please configure a specific country code on the radio.";
}
leaf ssid {
type string {
length "1..32";
}
mandatory true;
description
"WiFi network name (SSID) to broadcast.
This is the network name that clients will see when
scanning for WiFi networks.
Length: 132 characters.";
}
leaf hidden {
type boolean;
default false;
description
"Hide the SSID from broadcast beacons.
When true, the network will not appear in WiFi scans.
Clients must know the exact SSID to connect.
Note: This provides minimal security benefit and may
cause compatibility issues with some clients.";
}
container security {
description
"WiFi security configuration.";
leaf mode {
type enumeration {
enum open {
description
"Open network (no encryption).
WARNING: All client traffic is unencrypted!
Only use in controlled environments (captive portal, etc.).";
}
enum wpa2-personal {
description
"WPA2-Personal (WPA2-PSK).
Widely compatible, secure for most use cases.";
}
enum wpa3-personal {
description
"WPA3-Personal (WPA3-SAE).
Enhanced security with forward secrecy.
Requires WPA3-capable clients.";
}
enum wpa2-wpa3-personal {
description
"WPA2/WPA3 transitional mode.
Accepts both WPA2 and WPA3 clients.
Recommended for maximum compatibility + security.";
}
}
default wpa2-wpa3-personal;
description
"WiFi security mode.
Determines authentication and encryption methods.
Recommended: wpa2-wpa3-personal for best security
and compatibility.";
}
leaf secret {
when "../mode != 'open'";
type ks:central-symmetric-key-ref;
mandatory true;
description
"Pre-shared key (PSK) reference.
References a symmetric key in the keystore.
This is the WiFi password that clients must provide
to connect to the network.
Requirements:
- WPA2/WPA3: 8-63 characters (configured in keystore)";
}
}
/* Operational state */
container stations {
list station {
config false;
key mac-address;
description
"List of currently connected clients (stations).";
leaf mac-address {
type yang:mac-address;
description "Client MAC address.";
}
leaf rssi {
type int16;
units "dBm";
description "Client signal strength in dBm.";
}
leaf connected-time {
type uint32;
units "seconds";
description "Time since client connected, in seconds.";
}
leaf rx-packets {
type uint32;
description "Packets received from this client.";
}
leaf tx-packets {
type uint32;
description "Packets transmitted to this client.";
}
leaf rx-bytes {
type uint32;
units "octets";
description "Bytes received from this client.";
}
leaf tx-bytes {
type uint32;
units "octets";
description "Bytes transmitted to this client.";
}
leaf rx-speed {
type uint32;
units "100 kbit/s";
description
"Last received data rate from this client in 100 kbit/s.
Examples:
- 10 = 1 Mbit/s
- 65 = 6.5 Mbit/s
- 866 = 86.6 Mbit/s";
}
leaf tx-speed {
type uint32;
units "100 kbit/s";
description
"Last transmitted data rate to this client in 100 kbit/s.
Examples:
- 10 = 1 Mbit/s
- 65 = 6.5 Mbit/s
- 866 = 86.6 Mbit/s";
}
}
}
}
}
}
}
@@ -18,6 +18,12 @@ module infix-interfaces {
import infix-if-type {
prefix infix-ift;
}
import ietf-hardware {
prefix iehw;
}
import infix-hardware {
prefix ih;
}
include infix-if-base;
include infix-if-bridge;
+2 -2
View File
@@ -55,9 +55,9 @@ module infix-keystore {
}
augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type" {
case cleartext-symmetric-key {
leaf cleartext-symmetric-key {
leaf symmetric-key {
type string;
must "../infix-ks:key-format != 'infix-ct:wifi-preshared-key-format' or " +
must "../../key-format != 'infix-ct:wifi-preshared-key-format' or " +
"(string-length(.) >= 8 and string-length(.) <= 63)" {
error-message "WiFi pre-shared key must be 8-63 characters long";
}
@@ -16,7 +16,12 @@ module infix-wifi-country-codes {
The regulatory domain configuration follows the principles
established in IETF RFCs for wireless access point management.";
revision 2025-11-28 {
description
"Add support for 00 - World regulatory domain.";
reference
"Internal";
}
revision 2025-06-02 {
description
"Initial revision for WiFi country code support.";
@@ -27,6 +32,7 @@ module infix-wifi-country-codes {
typedef country-code {
type enumeration {
enum "00" { description "World regulatory domain (no country restrictions)"; }
enum "AD" { description "Andorra"; }
enum "AE" { description "United Arab Emirates"; }
enum "AF" { description "Afghanistan"; }
+1 -15
View File
@@ -422,19 +422,6 @@ def lldp(args: List[str]):
return
cli_pretty(data, "show-lldp")
def wifi(args: List[str]):
iface = args[0]
if len(args) == 0:
print("Illigal usage")
return
if is_valid_interface_name(iface):
if not os.path.exists(f"/sys/class/net/{iface}/wireless"):
print("Not a Wi-Fi interface")
return
data = run_sysrepocfg("/ietf-interfaces:interfaces")
cli_pretty(data, "show-wifi-scan", "-n", iface)
else:
print(f"Invalid interface name: {iface}")
def system(args: List[str]) -> None:
# Get system state from sysrepo
@@ -556,8 +543,7 @@ def execute_command(command: str, args: List[str]):
'services': services,
'software': software,
'stp': stp,
'system': system,
'wifi': wifi
'system': system
}
if command in command_mapping:
+183 -70
View File
@@ -159,18 +159,6 @@ class PadDhcpServer:
exp = 10
class PadUsbPort:
title = 30
name = 20
state = 10
oper = 10
@classmethod
def table_width(cls):
"""Total width of USB port table"""
return cls.name + cls.state + cls.oper
class PadSensor:
name = 30
value = 20
@@ -190,12 +178,6 @@ class PadNtpSource:
poll = 14
class PadWifiScan:
ssid = 40
encryption = 30
signal = 9
class PadLldp:
interface = 16
rem_idx = 10
@@ -544,7 +526,7 @@ class Decore():
@staticmethod
def bright_green(txt):
return Decore.decorate("1;32", txt, "39")
return Decore.decorate("1;32", txt, "0")
@staticmethod
def yellow(txt):
@@ -847,12 +829,6 @@ class USBport:
self.state = get_json_data('', self.data, 'state', 'admin-state')
self.oper = get_json_data('', self.data, 'state', 'oper-state')
def print(self):
row = f"{self.name:<{PadUsbPort.name}}"
row += f"{self.state:<{PadUsbPort.state}}"
row += f"{self.oper:<{PadUsbPort.oper}}"
print(row)
class Sensor:
def __init__(self, data):
@@ -1248,20 +1224,73 @@ class Iface:
print(row)
def pr_wifi_ssids(self):
hdr = (f"{'SSID':<{PadWifiScan.ssid}}"
f"{'ENCRYPTION':<{PadWifiScan.encryption}}"
f"{'SIGNAL':<{PadWifiScan.signal}}")
print("\nAVAILABLE NETWORKS:")
ssid_table = SimpleTable([
Column('SSID'),
Column('SECURITY'),
Column('SIGNAL'),
Column('CHANNEL')
])
print(Decore.invert(hdr))
results = self.wifi.get("scan-results", {})
station = self.wifi.get("station", {})
results = station.get("scan-results", {})
for result in results:
encstr = ", ".join(result["encryption"])
status = rssi_to_status(result["rssi"])
row = f"{result['ssid']:<{PadWifiScan.ssid}}"
row += f"{encstr:<{PadWifiScan.encryption}}"
row += f"{status:<{PadWifiScan.signal}}"
encstr = ", ".join(result.get("encryption", ["Unknown"]))
status = rssi_to_status(result.get("rssi", -100))
channel = result.get("channel", "?")
print(row)
ssid_table.row(result.get('ssid', 'Hidden'), encstr, status, channel)
ssid_table.print()
def pr_wifi_stations(self):
"""Display connected stations for AP mode"""
if not self.wifi:
return
# Get stations from access-point container
ap = self.wifi.get("access-point", {})
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
if not stations:
return
print("\nCONNECTED STATIONS:")
stations_table = SimpleTable([
Column('MAC'),
Column('SIGNAL'),
Column('TIME'),
Column('RX PKT'),
Column('TX PKT'),
Column('RX BYTES'),
Column('TX BYTES'),
Column('RX SPEED'),
Column('TX SPEED')
])
for station in stations:
mac = station.get("mac-address", "unknown")
rssi = station.get("rssi")
signal_str = rssi_to_status(rssi) if rssi is not None else "------"
conn_time = station.get("connected-time", 0)
time_str = f"{conn_time}s"
rx_pkt = station.get("rx-packets", 0)
tx_pkt = station.get("tx-packets", 0)
rx_bytes = station.get("rx-bytes", 0)
tx_bytes = station.get("tx-bytes", 0)
# Speed in 100 kbit/s units, convert to Mbps for display
rx_speed = station.get("rx-speed", 0)
tx_speed = station.get("tx-speed", 0)
rx_speed_str = f"{rx_speed / 10:.1f}" if rx_speed else "-"
tx_speed_str = f"{tx_speed / 10:.1f}" if tx_speed else "-"
stations_table.row(mac, signal_str, time_str, rx_pkt, tx_pkt,
rx_bytes, tx_bytes, rx_speed_str, tx_speed_str)
stations_table.print()
def pr_proto_wifi(self, pipe=''):
@@ -1269,20 +1298,35 @@ class Iface:
print(row)
ssid = None
rssi = None
mode = None
if self.wifi:
rssi=self.wifi.get("rssi")
ssid=self.wifi.get("ssid")
if ssid is None:
ssid="------"
if rssi is None:
signal="------"
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
ap=self.wifi.get("access-point", {})
if ap:
ssid = ap.get("ssid", "------")
mode = "AP"
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
station_count = len(stations)
data_str = f"{mode}, ssid: {ssid}, stations: {station_count}"
else:
station=self.wifi.get("station", {})
ssid = station.get("ssid", "------")
rssi = station.get("rssi")
mode = "Station"
if rssi is not None:
signal = rssi_to_status(rssi)
data_str = f"{mode}, ssid: {ssid}, signal: {signal}"
else:
data_str = f"{mode}, ssid: {ssid}"
else:
signal=rssi_to_status(rssi)
data_str = f"ssid: {ssid}, signal: {signal}"
data_str = "ssid: ------"
row = f"{'':<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row = f"{'':<{Pad.flags}}"
row += f"{pipe:<{Pad.iface}}"
row += f"{'wifi':<{Pad.proto}}"
row += f"{'':<{Pad.state}}{data_str}"
print(row)
@@ -1550,13 +1594,37 @@ class Iface:
else:
print(f"{'ipv6 addresses':<{20}}:")
if self.in_octets and self.out_octets:
print(f"{'in-octets':<{20}}: {self.in_octets}")
print(f"{'out-octets':<{20}}: {self.out_octets}")
frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet',
'statistics', 'frame')
if self.wifi:
ssid=self.wifi.get('ssid', "----")
rssi=self.wifi.get('rssi', "----")
print(f"{'SSID':<{20}}: {ssid}")
print(f"{'Signal':<{20}}: {rssi}")
print("")
self.pr_wifi_ssids()
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
ap = self.wifi.get('access-point')
if ap:
mode = "access-point"
ssid = ap.get('ssid', "----")
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
print(f"{'mode':<{20}}: {mode}")
print(f"{'ssid':<{20}}: {ssid}")
print(f"{'connected stations':<{20}}: {len(stations)}")
self.pr_wifi_stations()
else:
mode = "station"
station = self.wifi.get('station', {})
rssi = station.get('rssi')
ssid = station.get('ssid', "----")
print(f"{'mode':<{20}}: {mode}")
print(f"{'ssid':<{20}}: {ssid}")
if rssi is not None:
signal_status = rssi_to_status(rssi)
print(f"{'signal':<{20}}: {rssi} dBm ({signal_status})")
if "scan-results" in station:
self.pr_wifi_ssids()
if self.gre:
print(f"{'local address':<{20}}: {self.gre['local']}")
@@ -1567,12 +1635,6 @@ class Iface:
print(f"{'remote address':<{20}}: {self.vxlan['remote']}")
print(f"{'VxLAN id':<{20}}: {self.vxlan['vni']}")
if self.in_octets and self.out_octets:
print(f"{'in-octets':<{20}}: {self.in_octets}")
print(f"{'out-octets':<{20}}: {self.out_octets}")
frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet',
'statistics', 'frame')
if frame:
print("")
for key, val in frame.items():
@@ -1953,9 +2015,6 @@ def show_services(json):
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
services = services_data.get("service", [])
# This is the first usage of simple table. I assume this will be
# copied so I left a lot of comments. If you copy it feel free
# to be less verbose..
service_table = SimpleTable([
Column('NAME'),
Column('STATUS'),
@@ -2005,9 +2064,9 @@ def show_hardware(json):
motherboard = [c for c in components if c.get("class") == "iana-hardware:chassis"]
usb_ports = [c for c in components if c.get("class") == "infix-hardware:usb"]
sensors = [c for c in components if c.get("class") == "iana-hardware:sensor"]
wifi_radios = [c for c in components if c.get("class") == "infix-hardware:wifi"]
# Determine overall width (use the wider of the two sections)
width = max(PadUsbPort.table_width(), PadSensor.table_width())
width = max(PadSensor.table_width(), 100)
# Display full-width inverted heading
print(Decore.invert(f"{'HARDWARE COMPONENTS':<{width}}"))
@@ -2027,18 +2086,70 @@ def show_hardware(json):
if board.get("hardware-rev"):
print(f"Hardware Revision : {board['hardware-rev']}")
if wifi_radios:
Decore.title("WiFi radios", width)
radios_table = SimpleTable([
Column('NAME'),
Column('MANUFACTURER'),
Column('BANDS', 'right'),
Column('STANDARDS', 'right'),
Column('MAX AP', 'right')
])
for component in wifi_radios:
phy = component.get("name", "")
manufacturer = component.get("mfg-name", "Unknown")
radio_data = component.get("infix-hardware:wifi-radio", {})
bands = radio_data.get("bands", [])
band_names = []
has_ht = False
has_vht = False
has_he = False
for band in bands:
if band.get("name"):
band_names.append(band["name"])
if band.get("ht-capable"):
has_ht = True
if band.get("vht-capable"):
has_vht = True
if band.get("he-capable"):
has_he = True
bands_str = "/".join(band_names) if band_names else "Unknown"
standards = []
if has_ht:
standards.append("11n")
if has_vht:
standards.append("11ac")
if has_he:
standards.append("11ax")
standard_str = "/".join(standards) if standards else "Unknown"
max_if = radio_data.get("max-interfaces", {})
max_ap = max_if.get('ap', 'N/A') if max_if else 'N/A'
radios_table.row(phy, manufacturer, bands_str, standard_str, max_ap)
radios_table.print()
if usb_ports:
Decore.title("USB Ports", width)
hdr = (f"{'NAME':<{PadUsbPort.name}}"
f"{'STATE':<{PadUsbPort.state}}"
f"{'OPER':<{PadUsbPort.oper}}")
# Pad header to full width
hdr = f"{hdr:<{width}}"
print(Decore.invert(hdr))
usb_table = SimpleTable([
Column('NAME'),
Column('STATE'),
Column('OPER')
])
for component in usb_ports:
port = USBport(component)
port.print()
usb_table.row(port.name, port.state, port.oper)
usb_table.print()
if sensors:
Decore.title("Sensors", width)
@@ -4342,6 +4453,8 @@ def main():
show_firewall_logs(args.limit)
elif args.command == "show-ntp":
show_ntp(json_data)
elif args.command == "show-wifi-radio":
show_wifi_radio(json_data)
elif args.command == "show-bfd":
show_bfd(json_data)
elif args.command == "show-bfd-status":
+3
View File
@@ -87,6 +87,9 @@ def main():
elif args.model == 'ietf-bfd-ip-sh':
from . import ietf_bfd_ip_sh
yang_data = ietf_bfd_ip_sh.operational()
elif args.model == 'infix-wifi-radio':
from . import infix_wifi_radio
yang_data = infix_wifi_radio.operational()
else:
common.LOG.warning("Unsupported model %s", args.model)
sys.exit(1)
+233 -61
View File
@@ -1,6 +1,7 @@
import datetime
import os
import glob
import re
import sys
from .common import insert, YangDate
from .host import HOST
@@ -150,65 +151,44 @@ def normalize_sensor_name(name):
def get_wifi_phy_info():
"""
Discover WiFi PHYs and map them to bands and interface names.
Discover WiFi PHYs using iw list command.
Returns dict: {phy_name: {band: str, iface: str, description: str}}
Example: {"phy0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
Example: {"radio0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
"""
phy_info = {}
try:
# Enumerate PHYs from /sys/class/ieee80211/
ieee80211_path = "/sys/class/ieee80211"
if not os.path.exists(ieee80211_path):
# Use iw.py to list all PHYs
phys = HOST.run_json(("/usr/libexec/infix/iw.py", "list"), default=[])
if not phys:
return phy_info
for phy in os.listdir(ieee80211_path):
if not phy.startswith("phy"):
continue
# Initialize PHY info for each PHY
for phy in phys:
phy_info[phy] = {"band": "Unknown", "iface": None, "description": None}
phy_path = os.path.join(ieee80211_path, phy)
info = {"band": "Unknown", "iface": None, "description": None}
# Create a mapping from PHY number to PHY name
phy_num_to_name = {}
for phy_name in phy_info.keys():
# Extract number from radio/phy name (e.g., "0" from "radio0" or "phy0")
num_match = re.search(r'(\d+)$', phy_name)
if num_match:
phy_num = num_match.group(1)
phy_num_to_name[phy_num] = phy_name
# Try to determine band from device path or hwmon name
# The hwmon device usually tells us: mt7915_phy0, mt7915_phy1, etc.
# We'll check supported frequencies to determine band
try:
# Read supported bands - check if device supports 5 GHz
# Most dual-band chips expose phy0 as 2.4 GHz and phy1 as 5 GHz
device_path = os.path.join(phy_path, "device")
if os.path.exists(device_path):
# Simple heuristic: phy0 is usually 2.4 GHz, phy1 is 5 GHz
# This works for most MediaTek chips (mt7915, mt7921, etc.)
if phy == "phy0":
info["band"] = "2.4 GHz"
elif phy == "phy1":
info["band"] = "5 GHz"
elif phy == "phy2":
info["band"] = "6 GHz" # WiFi 6E
except:
pass
# Find associated virtual interfaces using iw.py dev
dev_map = HOST.run_json(("/usr/libexec/infix/iw.py", "dev"), default={})
# Find associated interface by checking which interface has a phy80211 link to this PHY
try:
net_path = "/sys/class/net"
if os.path.exists(net_path):
for iface in os.listdir(net_path):
phy_link = os.path.join(net_path, iface, "phy80211")
if os.path.islink(phy_link):
# Read the link target and extract PHY name
try:
link_target = os.readlink(phy_link)
linked_phy = os.path.basename(link_target)
if linked_phy == phy:
info["iface"] = iface
break
except:
continue
except:
pass
# dev_map is a dict mapping PHY numbers to list of interfaces
for phy_num, interfaces in dev_map.items():
phy_name = phy_num_to_name.get(phy_num)
if phy_name and phy_name in phy_info and interfaces:
# Use the first interface
phy_info[phy_name]["iface"] = interfaces[0]
# Build description
# Build descriptions
for phy, info in phy_info.items():
if info["iface"] and info["band"] != "Unknown":
info["description"] = f"WiFi Radio {info['iface']} ({info['band']})"
elif info["band"] != "Unknown":
@@ -218,8 +198,6 @@ def get_wifi_phy_info():
else:
info["description"] = "WiFi Radio"
phy_info[phy] = info
except Exception:
pass
@@ -248,7 +226,8 @@ def hwmon_sensor_components():
device_sensors[base_name].append(sensor_component)
try:
hwmon_devices = glob.glob("/sys/class/hwmon/hwmon*")
hwmon_entries = HOST.run(("ls", "/sys/class/hwmon"), default="").split()
hwmon_devices = [os.path.join("/sys/class/hwmon", entry) for entry in hwmon_entries if entry.startswith("hwmon")]
for hwmon_path in hwmon_devices:
try:
@@ -257,6 +236,12 @@ def hwmon_sensor_components():
continue
device_name = HOST.read(name_path).strip()
# Check if device/name exists (e.g., for WiFi radios) and use that instead
device_name_path = os.path.join(hwmon_path, "device", "name")
if HOST.exists(device_name_path):
device_name = HOST.read(device_name_path).strip()
base_name = normalize_sensor_name(device_name)
# Helper to create sensor component with human-readable description
@@ -281,7 +266,9 @@ def hwmon_sensor_components():
return component
# Temperature sensors
for temp_file in glob.glob(os.path.join(hwmon_path, "temp*_input")):
temp_entries = HOST.run(("ls", hwmon_path), default="").split()
temp_files = [os.path.join(hwmon_path, e) for e in temp_entries if e.startswith("temp") and e.endswith("_input")]
for temp_file in temp_files:
try:
sensor_num = os.path.basename(temp_file).split('_')[0].replace('temp', '')
value = int(HOST.read(temp_file).strip())
@@ -298,7 +285,9 @@ def hwmon_sensor_components():
continue
# Fan sensors (RPM from tachometer)
for fan_file in glob.glob(os.path.join(hwmon_path, "fan*_input")):
fan_entries = HOST.run(("ls", hwmon_path), default="").split()
fan_files = [os.path.join(hwmon_path, e) for e in fan_entries if e.startswith("fan") and e.endswith("_input")]
for fan_file in fan_files:
try:
sensor_num = os.path.basename(fan_file).split('_')[0].replace('fan', '')
value = int(HOST.read(fan_file).strip())
@@ -316,9 +305,11 @@ def hwmon_sensor_components():
# PWM fan sensors (duty cycle percentage)
# Only add if no fan*_input exists for this device (avoid duplicates)
has_rpm_sensor = bool(glob.glob(os.path.join(hwmon_path, "fan*_input")))
has_rpm_sensor = bool(fan_files)
if not has_rpm_sensor:
for pwm_file in glob.glob(os.path.join(hwmon_path, "pwm[0-9]*")):
pwm_entries = HOST.run(("ls", hwmon_path), default="").split()
pwm_files = [os.path.join(hwmon_path, e) for e in pwm_entries if e.startswith("pwm") and e[3:].replace('_', '').isdigit() if len(e) > 3]
for pwm_file in pwm_files:
# Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc.
pwm_basename = os.path.basename(pwm_file)
if not pwm_basename.replace('pwm', '').isdigit():
@@ -345,7 +336,9 @@ def hwmon_sensor_components():
continue
# Voltage sensors
for voltage_file in glob.glob(os.path.join(hwmon_path, "in*_input")):
voltage_entries = HOST.run(("ls", hwmon_path), default="").split()
voltage_files = [os.path.join(hwmon_path, e) for e in voltage_entries if e.startswith("in") and e.endswith("_input")]
for voltage_file in voltage_files:
try:
sensor_num = os.path.basename(voltage_file).split('_')[0].replace('in', '')
value = int(HOST.read(voltage_file).strip())
@@ -363,7 +356,9 @@ def hwmon_sensor_components():
continue
# Current sensors
for current_file in glob.glob(os.path.join(hwmon_path, "curr*_input")):
current_entries = HOST.run(("ls", hwmon_path), default="").split()
current_files = [os.path.join(hwmon_path, e) for e in current_entries if e.startswith("curr") and e.endswith("_input")]
for current_file in current_files:
try:
sensor_num = os.path.basename(current_file).split('_')[0].replace('curr', '')
value = int(HOST.read(current_file).strip())
@@ -381,7 +376,9 @@ def hwmon_sensor_components():
continue
# Power sensors
for power_file in glob.glob(os.path.join(hwmon_path, "power*_input")):
power_entries = HOST.run(("ls", hwmon_path), default="").split()
power_files = [os.path.join(hwmon_path, e) for e in power_entries if e.startswith("power") and e.endswith("_input")]
for power_file in power_files:
try:
sensor_num = os.path.basename(power_file).split('_')[0].replace('power', '')
value = int(HOST.read(power_file).strip())
@@ -426,8 +423,8 @@ def hwmon_sensor_components():
wifi_info = get_wifi_phy_info()
for component in components:
name = component.get("name", "")
# Match phy0, phy1, etc. sensors
if name.startswith("phy") and name in wifi_info:
# Match radio0, radio1, etc. sensors
if name.startswith("radio") and name in wifi_info:
phy = wifi_info[name]
# Add WiFi-specific description
component["description"] = phy["description"]
@@ -448,7 +445,8 @@ def thermal_sensor_components():
try:
# Find all thermal zones
thermal_zones = glob.glob("/sys/class/thermal/thermal_zone*")
thermal_entries = HOST.run(("ls", "/sys/class/thermal"), default="").split()
thermal_zones = [os.path.join("/sys/class/thermal", entry) for entry in thermal_entries if entry.startswith("thermal_zone")]
for zone_path in thermal_zones:
try:
@@ -496,6 +494,179 @@ def thermal_sensor_components():
return components
def get_survey_data(ifname):
"""Get channel survey data using iw.py script"""
channels = []
try:
survey_data = HOST.run_json(("/usr/libexec/infix/iw.py", "survey", ifname), default=[])
for entry in survey_data:
channel = {
"frequency": entry.get("frequency"),
"in-use": entry.get("in_use", False)
}
# Add optional fields if present
if "noise" in entry:
channel["noise"] = entry["noise"]
if "active_time" in entry:
channel["active-time"] = entry["active_time"]
if "busy_time" in entry:
channel["busy-time"] = entry["busy_time"]
if "receive_time" in entry:
channel["receive-time"] = entry["receive_time"]
if "transmit_time" in entry:
channel["transmit-time"] = entry["transmit_time"]
channels.append(channel)
except Exception:
pass
return channels
def get_phy_info(phy_name):
"""Get complete PHY information using iw.py script"""
try:
return HOST.run_json(("/usr/libexec/infix/iw.py", "info", phy_name), default={})
except Exception:
return {}
def convert_iw_phy_info_for_yanger(phy_info):
"""
Convert iw.py phy_info format to yanger format.
Input: iw.py format with 'bands', 'driver', 'manufacturer', 'interface_combinations'
Output: yanger format with renamed/restructured fields
"""
result = {"bands": [], "driver": None, "manufacturer": "Unknown", "max-interfaces": {}}
# Convert bands - iw.py already uses snake_case for capabilities
for band in phy_info.get("bands", []):
band_data = {
"band": str(band.get("band", 0)),
"name": band.get("name", "Unknown")
}
# Add capability flags (iw.py uses snake_case: ht_capable, vht_capable, he_capable)
if band.get("ht_capable"):
band_data["ht-capable"] = True
if band.get("vht_capable"):
band_data["vht-capable"] = True
if band.get("he_capable"):
band_data["he-capable"] = True
result["bands"].append(band_data)
# Copy driver and manufacturer
if phy_info.get("driver"):
result["driver"] = phy_info["driver"]
if phy_info.get("manufacturer"):
result["manufacturer"] = phy_info["manufacturer"]
# Convert interface combinations to max-interfaces
# Find max AP interfaces from combinations
for comb in phy_info.get("interface_combinations", []):
for limit in comb.get("limits", []):
if "AP" in limit.get("types", []):
ap_max = limit.get("max", 0)
if "ap" not in result["max-interfaces"] or ap_max > result["max-interfaces"]["ap"]:
result["max-interfaces"]["ap"] = ap_max
return result
def wifi_radio_components():
"""
Create WiFi radio components with complete operational data.
Returns a list of hardware components for WiFi radios.
"""
components = []
wifi_info = get_wifi_phy_info()
for phy_name, phy_data in wifi_info.items():
component = {
"name": phy_name,
"class": "infix-hardware:wifi",
"description": phy_data.get("description", "WiFi Radio")
}
# Initialize wifi-radio data structure
wifi_radio_data = {}
# Get complete PHY information from iw.py script
iw_info = get_phy_info(phy_name)
# Convert iw.py format to yanger format
phy_details = convert_iw_phy_info_for_yanger(iw_info)
# Add manufacturer to component
if phy_details.get("manufacturer") and phy_details["manufacturer"] != "Unknown":
component["mfg-name"] = phy_details["manufacturer"]
# Add bands
if phy_details.get("bands"):
wifi_radio_data["bands"] = phy_details["bands"]
# Add driver
if phy_details.get("driver"):
wifi_radio_data["driver"] = phy_details["driver"]
# Add max-interfaces
if phy_details.get("max-interfaces"):
wifi_radio_data["max-interfaces"] = phy_details["max-interfaces"]
# Add max TX power from iw info
if iw_info.get("max_txpower"):
wifi_radio_data["max-txpower"] = iw_info["max_txpower"]
# Add supported channels from band frequencies
supported_channels = []
for band in iw_info.get("bands", []):
for freq in band.get("frequencies", []):
# Convert frequency to channel number
if 2412 <= freq <= 2484:
channel = (freq - 2407) // 5
elif 5170 <= freq <= 5825:
channel = (freq - 5000) // 5
elif 5955 <= freq <= 7115:
channel = (freq - 5950) // 5
else:
continue
supported_channels.append(channel)
if supported_channels:
wifi_radio_data['supported-channels'] = sorted(set(supported_channels))
# Count virtual interfaces from iw info
num_ifaces = iw_info.get('num_virtual_interfaces', 0)
wifi_radio_data['num-virtual-interfaces'] = num_ifaces
# Get survey data if we have an interface
iface = phy_data.get("iface")
if iface:
try:
channels = get_survey_data(iface)
if channels:
wifi_radio_data["survey"] = {
"channel": channels
}
except Exception:
# If survey fails, continue without survey data
pass
# Add wifi-radio data to component
if wifi_radio_data:
component["infix-hardware:wifi-radio"] = wifi_radio_data
components.append(component)
return components
def operational():
systemjson = HOST.read_json("/run/system.json")
@@ -507,6 +678,7 @@ def operational():
usb_port_components(systemjson) +
hwmon_sensor_components() +
thermal_sensor_components() +
wifi_radio_components() +
[],
},
}
+204 -15
View File
@@ -2,38 +2,213 @@ from ..host import HOST
import json
import re
def wifi(ifname):
wifi_data={}
def detect_wifi_mode(ifname):
"""Detect if interface is in AP or Station mode"""
try:
output = HOST.run(tuple(f"iw dev {ifname} info".split()), default="")
for line in output.splitlines():
if 'type' in line.lower():
if 'ap' in line.lower():
return 'ap'
else:
return 'station'
except Exception:
pass
# Default to station mode
return 'station'
def find_primary_interface_from_config(ifname):
"""Find primary interface by reading hostapd config files"""
try:
file_list = HOST.run(tuple("ls /etc/hostapd-*.conf".split()), default="")
if not file_list:
return None
for config_file in file_list.splitlines():
config_file = config_file.strip()
if not config_file:
continue
try:
content = HOST.run(tuple(f"cat {config_file}".split()), default="")
if not content:
continue
if f"interface={ifname}" in content or f"bss={ifname}" in content:
for line in content.splitlines():
if line.startswith("interface="):
return line.split("=", 1)[1].strip()
except Exception:
continue
except Exception:
pass
return None
def wifi_ap(ifname):
"""Get operational data for AP mode using hostapd_cli"""
ap_data = {}
try:
data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="")
primary_if = find_primary_interface_from_config(ifname)
if not primary_if:
return {}
data = HOST.run(tuple(f"hostapd_cli -i {primary_if} status".split()), default="")
if not data:
return {}
# Find our interface's SSID, different for bss and primary, because it is
if ifname == primary_if:
# Primary interface - get ssid[0] or ssid
for line in data.splitlines():
if "=" in line:
try:
k, v = line.split("=", 1)
if k in ("ssid[0]", "ssid"):
ap_data["ssid"] = v
break
except ValueError:
continue
else:
# Secondary BSS - find in BSS array
bss_idx = None
for line in data.splitlines():
if "=" in line:
try:
k, v = line.split("=", 1)
if v == ifname and k.startswith("bss["):
bss_idx = k[4:-1] # Extract index from bss[N]
break
except ValueError:
continue
if bss_idx:
for line in data.splitlines():
if "=" in line:
try:
k, v = line.split("=", 1)
if k == f"ssid[{bss_idx}]":
ap_data["ssid"] = v
break
except ValueError:
continue
stations_data = HOST.run(tuple(f"iw dev {ifname} station dump".split()), default="")
stations = parse_iw_stations(stations_data)
if stations:
ap_data["stations"] = {
"station": stations
}
except Exception:
pass
# Nest data inside access-point container to match YANG schema
return {
"access-point": ap_data
} if ap_data else {}
def parse_iw_stations(output):
"""Parse iw station dump output to get connected stations"""
stations = []
current_station = None
for line in output.splitlines():
line = line.strip()
# Station line: "Station aa:bb:cc:dd:ee:ff (on wifiX)"
if line.startswith("Station "):
if current_station:
stations.append(current_station)
# Extract MAC address
parts = line.split()
if len(parts) >= 2:
current_station = {
"mac-address": parts[1].lower()
}
elif current_station:
# Parse station attributes
try:
# Lines are in format "key: value" with tabs
if ":" not in line:
continue
parts = line.split(":", 1)
key = parts[0].strip()
value = parts[1].strip()
if key == "signal":
# Format: "-42 dBm" or "-42 [-44] dBm"
rssi = int(value.split()[0])
current_station["rssi"] = rssi
elif key == "connected time":
# Format: "123 seconds"
seconds = int(value.split()[0])
current_station["connected-time"] = seconds
elif key == "rx packets":
current_station["rx-packets"] = int(value)
elif key == "tx packets":
current_station["tx-packets"] = int(value)
elif key == "rx bytes":
current_station["rx-bytes"] = int(value)
elif key == "tx bytes":
current_station["tx-bytes"] = int(value)
elif key == "tx bitrate":
# Format: "866.7 MBit/s ..." - extract speed and convert to 100kbit/s units
speed_mbps = float(value.split()[0])
current_station["tx-speed"] = int(speed_mbps * 10)
elif key == "rx bitrate":
# Format: "780.0 MBit/s ..." - extract speed and convert to 100kbit/s units
speed_mbps = float(value.split()[0])
current_station["rx-speed"] = int(speed_mbps * 10)
except (ValueError, KeyError, IndexError):
# Skip invalid values
continue
# Add last station
if current_station:
stations.append(current_station)
return stations
def wifi_station(ifname):
"""Get operational data for Station mode using wpa_cli"""
station_data = {}
try:
data = HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="")
if data != "":
for line in data.splitlines():
try:
if "=" not in line:
continue
k,v = line.split("=", 1)
k, v = line.split("=", 1)
if k == "ssid":
wifi_data["ssid"] = v
if k == "wpa_state" and v == "DISCONNECTED": # wpa_suppicant has most likely restarted, restart scanning
HOST.run(tuple(f"wpa_cli -i {ifname} scan".split()), default="")
station_data["ssid"] = v
except ValueError:
# Skip malformed lines
continue
try:
data=HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL")
data = HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL")
# signal_poll return FAIL not connected
# signal_poll return FAIL if not connected
if data.strip() != "FAIL":
for line in data.splitlines():
try:
if "=" not in line:
continue
k,v = line.strip().split("=", 1)
k, v = line.strip().split("=", 1)
if k == "RSSI":
wifi_data["rssi"]=int(v)
station_data["rssi"] = int(v)
except (ValueError, KeyError):
# Skip malformed lines or invalid integers
continue
@@ -45,14 +220,28 @@ def wifi(ifname):
pass
try:
data=HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL")
data = HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL")
if data != "FAIL":
wifi_data["scan-results"] = parse_wpa_scan_result(data)
scan_results = parse_wpa_scan_result(data)
if scan_results:
station_data["scan-results"] = scan_results
except Exception:
# If scan results fail, just omit them
pass
return wifi_data
# Always nest data inside station container to match YANG schema
# In scan-only mode, this will be just scan-results with no ssid/rssi
return {"station": station_data} if station_data else {}
def wifi(ifname):
"""Main entry point - detect mode and return appropriate data"""
mode = detect_wifi_mode(ifname)
if mode == 'ap':
return wifi_ap(ifname)
else:
return wifi_station(ifname)
def parse_wpa_scan_result(scan_output):
@@ -106,7 +295,7 @@ def parse_wpa_scan_result(scan_output):
# Convert to list and sort by RSSI (best first)
result = list(networks.values())
result.sort(key=lambda x: x['rssi'], reverse=False)
result.sort(key=lambda x: x['rssi'], reverse=True)
return result
+8 -7
View File
@@ -1,10 +1,11 @@
HARDWARE COMPONENTS 
────────────────────────────────────────────────────────────
HARDWARE COMPONENTS 
────────────────────────────────────────────────────────────────────────────────────────────────────
Board Information
Model : VM
Model : Standard PC (i440FX + PIIX, 1996)
Manufacturer : QEMU
────────────────────────────────────────────────────────────
Base MAC Address : 00:a0:85:00:03:00
────────────────────────────────────────────────────────────────────────────────────────────────────
USB Ports
NAME STATE OPER 
USB locked enabled
USB2 locked enabled
NAME STATE OPER 
USB1 locked enabled
USB2 locked enabled
+2 -2
View File
@@ -5,7 +5,7 @@
"name": "mainboard",
"class": "iana-hardware:chassis",
"mfg-name": "QEMU",
"model-name": "VM",
"model-name": "Standard PC (i440FX + PIIX, 1996)",
"infix-hardware:phys-address": "00:a0:85:00:03:00",
"state": {
"admin-state": "unknown",
@@ -21,7 +21,7 @@
"admin-state": "locked",
"oper-state": "enabled"
},
"name": "USB",
"name": "USB1",
"class": "infix-hardware:usb"
},
{
+36 -4
View File
@@ -120,13 +120,13 @@
"search": []
},
"clock": {
"boot-datetime": "2025-04-30T09:47:32+00:00",
"current-datetime": "2025-04-30T09:48:01+00:00"
"boot-datetime": "2026-01-02T20:42:11+00:00",
"current-datetime": "2026-01-02T20:43:23+00:00"
},
"platform": {
"os-name": "Infix",
"os-version": "v25.04.0-rc1-3-g8daf1571-dirty",
"os-release": "v25.04.0-rc1-3-g8daf1571-dirty",
"os-version": "v25.11.0-135-g44d2af57-dirty",
"os-release": "v25.11.0-135-g44d2af57-dirty",
"machine": "x86_64"
},
"infix-system:services": {
@@ -385,6 +385,38 @@
}
}
]
},
"infix-system:resource-usage": {
"memory": {
"total": "355076",
"free": "134936",
"available": "225652"
},
"load-average": {
"load-1min": "0.12",
"load-5min": "0.05",
"load-15min": "0.02"
},
"filesystem": [
{
"mount-point": "/",
"size": "142720",
"used": "142720",
"available": "0"
},
{
"mount-point": "/var",
"size": "86427",
"used": "281",
"available": "79270"
},
{
"mount-point": "/cfg",
"size": "14073",
"used": "67",
"available": "12860"
}
]
}
}
}
+21 -20
View File
@@ -2,10 +2,11 @@
"ietf-hardware:hardware": {
"component": [
{
"name": "mainboard",
"class": "iana-hardware:chassis",
"infix-hardware:phys-address": "00:a0:85:00:03:00",
"mfg-name": "QEMU",
"model-name": "VM",
"model-name": "Standard PC (i440FX + PIIX, 1996)",
"name": "mainboard",
"state": {
"admin-state": "unknown",
"oper-state": "enabled"
@@ -17,7 +18,7 @@
},
{
"class": "infix-hardware:usb",
"name": "USB",
"name": "USB1",
"state": {
"admin-state": "locked",
"oper-state": "enabled"
@@ -49,8 +50,8 @@
},
"ietf-system:system-state": {
"clock": {
"boot-datetime": "2025-04-30T09:47:32+00:00",
"current-datetime": "2025-04-30T09:48:01+00:00"
"boot-datetime": "2026-01-02T20:42:11+00:00",
"current-datetime": "2026-01-02T20:43:23+00:00"
},
"infix-system:dns-resolver": {
"options": {},
@@ -95,30 +96,30 @@
{
"available": "0",
"mount-point": "/",
"size": "70912",
"used": "70912"
"size": "142720",
"used": "142720"
},
{
"available": "79314",
"available": "79270",
"mount-point": "/var",
"size": "86459",
"used": "267"
"size": "86427",
"used": "281"
},
{
"available": "12861",
"available": "12860",
"mount-point": "/cfg",
"size": "14073",
"used": "66"
"used": "67"
}
],
"load-average": {
"load-15min": "0.01",
"load-1min": "0.16",
"load-5min": "0.03"
"load-15min": "0.02",
"load-1min": "0.12",
"load-5min": "0.05"
},
"memory": {
"available": "259640",
"free": "187776",
"available": "225652",
"free": "134936",
"total": "355076"
}
},
@@ -449,8 +450,8 @@
"platform": {
"machine": "x86_64",
"os-name": "Infix",
"os-release": "v25.04.0-rc1-3-g8daf1571-dirty",
"os-version": "v25.04.0-rc1-3-g8daf1571-dirty"
"os-release": "v25.11.0-135-g44d2af57-dirty",
"os-version": "v25.11.0-135-g44d2af57-dirty"
}
}
}
}
@@ -1,16 +1,16 @@
NAME="Infix"
ID=infix
PRETTY_NAME="Infix OS — Immutable.Friendly.Secure v25.04.0-rc1-3-g8daf1571-dirty"
PRETTY_NAME="Infix OS — Immutable.Friendly.Secure v25.11.0-135-g44d2af57-dirty"
ID_LIKE="buildroot"
DEFAULT_HOSTNAME=ix
VERSION="v25.04.0-rc1-3-g8daf1571-dirty"
VERSION_ID=v25.04.0-rc1-3-g8daf1571-dirty
BUILD_ID="v25.04.0-rc1-3-g8daf1571-dirty"
DEFAULT_HOSTNAME=infix
VERSION="v25.11.0-135-g44d2af57-dirty"
VERSION_ID=v25.11.0-135-g44d2af57-dirty
BUILD_ID="v25.11.0-135-g44d2af57-dirty"
IMAGE_ID="infix-x86_64"
ARCHITECTURE="x86_64"
HOME_URL=https://github.com/kernelkit/infix/
VENDOR_NAME="KernelKit"
VENDOR_HOME="https://github.com/kernelkit"
DOCUMENTATION_URL="https://github.com/kernelkit/infix/tree/main/doc"
VENDOR_HOME="https://kernelkit.org"
DOCUMENTATION_URL="https://kernelkit.org/infix/"
SUPPORT_URL="mailto:kernelkit@googlegroups.com"
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from 5 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
@@ -0,0 +1 @@
0.12 0.05 0.02 2/117 4690
@@ -0,0 +1,40 @@
MemTotal: 355076 kB
MemFree: 134936 kB
MemAvailable: 225652 kB
Buffers: 4252 kB
Cached: 91640 kB
SwapCached: 0 kB
Active: 34012 kB
Inactive: 136744 kB
Active(anon): 1588 kB
Inactive(anon): 75512 kB
Active(file): 32424 kB
Inactive(file): 61232 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 40 kB
Writeback: 0 kB
AnonPages: 74952 kB
Mapped: 41652 kB
Shmem: 2232 kB
KReclaimable: 8072 kB
Slab: 30180 kB
SReclaimable: 8072 kB
SUnreclaim: 22108 kB
KernelStack: 1872 kB
PageTables: 2372 kB
SecPageTables: 0 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 177536 kB
Committed_AS: 278488 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 9564 kB
VmallocChunk: 0 kB
Percpu: 304 kB
DirectMap4k: 41872 kB
DirectMap2M: 346112 kB
DirectMap1G: 0 kB
@@ -1 +1 @@
28.16 15.60
71.22 64.17
@@ -1 +1 @@
{"vendor": "QEMU", "product-name": "VM", "part-number": null, "serial-number": null, "mac-address": "00:a0:85:00:03:00", "factory-password-hash": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A", "vpd": {"product": {"board": "product", "available": false, "trusted": true, "data": {}}}, "usb-ports": [{"name": "USB", "path": "/sys/bus/usb/devices/usb1"}, {"name": "USB2", "path": "/sys/bus/usb/devices/usb2"}]}
{"vendor": "QEMU", "product-name": "Standard PC (i440FX + PIIX, 1996)", "product-version": "pc-i440fx-8.0", "part-number": null, "serial-number": null, "mac-address": "00:a0:85:00:03:00", "factory-password-hash": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A", "vpd": {"product": {"board": "product", "available": false, "trusted": true, "data": {}}}, "usb-ports": [{"name": "USB1", "path": "/sys/bus/usb/devices/usb1"}, {"name": "USB2", "path": "/sys/bus/usb/devices/usb2"}]}
@@ -0,0 +1 @@
[]
@@ -0,0 +1,2 @@
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/root 142720 142720 0 100% /
@@ -0,0 +1,2 @@
Filesystem 1K-blocks Used Available Use% Mounted on
cfg-overlay 14073 67 12860 1% /cfg
@@ -0,0 +1,2 @@
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/vda6 86427 281 79270 0% /mnt/var
@@ -0,0 +1 @@
cooling_device0
+1 -1
View File
@@ -1,2 +1,2 @@
1746006481
1767386603