checking in before i do some major restructuring of wifi stuff in the xml/xsd

This commit is contained in:
brent s. 2019-12-01 05:07:20 -05:00
parent 3e33abe0a6
commit edc78ea18e
10 changed files with 314 additions and 22 deletions

10
aif.xsd
View File

@ -347,8 +347,6 @@
</xs:element>
</xs:sequence>
<xs:attribute name="auto" type="xs:boolean" use="optional" default="true"/>
<xs:attribute name="defaultGateway" type="xs:boolean"
use="optional" default="false"/>
</xs:complexType>
<xs:unique name="uniq_ipv4_route">
<xs:selector xpath="aif:route"/>
@ -377,8 +375,6 @@
<!-- https://datatracker.ietf.org/doc/draft-ietf-mif-dhcpv6-route-option/
expired. Shame, that. -->
<xs:attribute name="auto" type="xs:boolean" use="optional" default="true"/>
<xs:attribute name="defaultGateway" type="xs:boolean"
use="optional" default="false"/>
</xs:complexType>
<xs:unique name="uniq_ipv6_route">
<xs:selector xpath="aif:route"/>
@ -443,6 +439,12 @@
<xs:simpleContent>
<xs:extension base="xs:token">
<xs:attribute name="type" use="optional" default="psk">
<!-- TODO: change this to sub-elements. <psk> or a <radius> thinger. -->
<!-- <psk raw="false">PSK_HERE</psk> -->
<!-- or e.g. wpa_passphrase test testingpsk -->
<!-- <psk raw="true">
124153ff24015a16d1993323b1840f3e6309ae24c07df7007d9fff8cff22f74c
</psk> -->
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="psk"/>

View File

@ -1,8 +1,10 @@
import binascii
import ipaddress
import os
import pathlib
import re
##
from passlib.crypto.digest import pbkdf2_hmac
from pyroute2 import IPDB
##
import aif.utils
@ -47,7 +49,22 @@ def convertIpTuples(addr_xmlobj):
return((addr, net, gw))


def convertWifiCrypto(crypto_xmlobj):
def convertPSK(ssid, passphrase):
try:
passphrase = passphrase.encode('utf-8').decode('ascii').strip('\r').strip('\n')
except UnicodeDecodeError:
raise ValueError('passphrase must be an ASCII string')
if len(ssid) > 32:
raise ValueError('ssid must be <= 32 characters')
if not 7 < len(passphrase) < 64:
raise ValueError('passphrase must be >= 8 and <= 32 characters')
raw_psk = pbkdf2_hmac('sha1', str(passphrase), str(ssid), 4096, 32)
hex_psk = binascii.hexlify(raw_psk)
str_psk = hex_psk.decode('utf-8')
return(str_psk)


def convertWifiCrypto(crypto_xmlobj, ssid):
crypto = {'type': crypto_xmlobj.find('type').text.strip()}
# if crypto['type'] in ('wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
@ -61,7 +78,8 @@ def convertWifiCrypto(crypto_xmlobj):
creds = crypto_xmlobj.find('creds')
crypto['auth'] = {'type': creds.attrib.get('type', 'psk').strip()}
if crypto['auth']['type'] == 'psk':
crypto['auth']['psk'] = creds.text
crypto['auth']['passphrase'] = creds.text.strip('\r').strip('\n')
crypto['auth']['psk'] = convertPSK(ssid, creds.text)
# TODO: enterprise support
return(crypto)

@ -211,3 +229,8 @@ class BaseConnection(object):
if addrset not in self.routes[addrtype]:
self.routes[addrtype].append(addrset)
return()

def _writeConnCfg(self, chroot_base = None):
# Dummy method.
pass
return()

View File

@ -274,6 +274,7 @@ class Wireless(Connection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
self.connection_type = 'wireless'
self.packages.add('wpa_supplicant')
self._initCfg()
self._initConnCfg()

@ -291,8 +292,7 @@ class Wireless(Connection):
self._cfg['BASE']['AP'] = bssid
crypto = self.xml.find('encryption')
if crypto:
self.packages.add('wpa_supplicant')
crypto = aif.network._common.convertWifiCrypto(crypto)
crypto = aif.network._common.convertWifiCrypto(crypto, self.xml.attrib['essid'])
# if crypto['type'] in ('wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
# TODO: WPA2 enterprise

View File

@ -0,0 +1,19 @@
# Generated by AIF-NG.
{%- for section_name, section_items in cfg.items() %}
{%- if section_items|isList %}
{#- We *only* use lists-of-dicts because they should always render to their own sections.
INI doesn't support nesting, thankfully. #}
{%- for i in section_items %}
[{{ section_name }}]
{%- for k, v in i.items() %}
{{ k }}={{ v }}
{%- endfor %}
{% endfor %}
{%- else %}
{#- It's a single-level dict. #}
[{{ section_name }}]
{%- for k, v in section_items.items() %}
{{ k }}={{ v }}
{%- endfor %}
{%- endif %}
{% endfor %}

View File

@ -1,8 +1,157 @@
import ipaddress
import socket
import os
##
# We have to use Jinja2 because while there are ways to *parse* an INI with duplicate keys
# (https://stackoverflow.com/a/38286559/733214), there's no way to *write* an INI with them using configparser.
# So we use Jinja2 logic.
import jinja2
##
import aif.utils
import aif.network._common


class Connection(aif.network._common.BaseConnection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
self.provider_type = 'systemd-networkd'
self.packages = set()
self.services = {
('/usr/lib/systemd/system/systemd-networkd.service'): ('etc/systemd/system/'
'multi-user.target.wants/'
'systemd-networkd.service'),
('/usr/lib/systemd/system/systemd-networkd.service'): ('etc/systemd/system/'
'dbus-org.freedesktop.network1.service'),
('/usr/lib/systemd/system/systemd-networkd.socket'): ('etc/systemd/system/'
'sockets.target.wants/systemd-networkd.socket'),
('/usr/lib/systemd/system/systemd-networkd.socket'): ('etc/systemd/system/'
'network-online.target.wants/'
'systemd-networkd-wait-online.service'),
# We include these *even if* self.auto['resolvers'][*] are false.
('/usr/lib/systemd/system/systemd-resolved.service'): ('etc/systemd/system/'
'dbus-org.freedesktop.resolve1.service'),
('/usr/lib/systemd/system/systemd-resolved.service'): ('etc/systemd/'
'system/multi-user.target.wants/'
'systemd-resolved.service')}
self._wpasupp = {}
self._initJ2()

def _initCfg(self):
if self.device == 'auto':
self.device = aif.network._common.getDefIface(self.connection_type)
self._cfg = {'Match': {'Name': self.device},
'Network': {'Description': ('A {0} profile for {1} '
'(generated by AIF-NG)').format(self.connection_type,
self.device),
'DefaultRouteOnDevice': ('true' if self.is_defroute else 'false'),
# This (may) get modified by logic below.
'IPv6AcceptRA': 'false',
'LinkLocalAddressing': 'no'}}
if self.domain:
self._cfg['Network']['Domains'] = self.domain
if self.resolvers:
self._cfg['Network']['DNS'] = [str(ip) for ip in self.resolvers]
if all((self.auto['addresses']['ipv4'], self.auto['addresses']['ipv6'])):
self._cfg['Network']['IPv6AcceptRA'] = 'true'
self._cfg['Network']['LinkLocalAddressing'] = 'ipv6'
self._cfg['Network']['DHCP'] = 'yes'
elif self.auto['addresses']['ipv4'] and not self.auto['addresses']['ipv6']:
self._cfg['Network']['DHCP'] = 'ipv4'
elif (not self.auto['addresses']['ipv4']) and self.auto['addresses']['ipv6']:
self._cfg['Network']['IPv6AcceptRA'] = 'true'
self._cfg['Network']['LinkLocalAddressing'] = 'ipv6'
self._cfg['Network']['DHCP'] = 'ipv6'
else:
self._cfg['Network']['DHCP'] = 'no'
if any((self.auto['addresses']['ipv4'], self.auto['routes']['ipv4'], self.auto['resolvers']['ipv4'])):
t = 'ipv4'
self._cfg['DHCPv4'] = {'UseDNS': ('true' if self.auto['resolvers'][t] else 'false'),
'UseRoutes': ('true' if self.auto['routes'][t] else 'false')}
if any((self.auto['addresses']['ipv6'], self.auto['routes']['ipv6'], self.auto['resolvers']['ipv6'])):
t = 'ipv6'
self._cfg['Network']['IPv6AcceptRA'] = 'true'
self._cfg['DHCPv6'] = {'UseDNS': ('true' if self.auto['resolvers'][t] else 'false')}
for t in ('ipv4', 'ipv6'):
if self.addrs[t]:
if t == 'ipv6':
self._cfg['Network']['LinkLocalAddressing'] = 'ipv6'
if 'Address' not in self._cfg.keys():
self._cfg['Address'] = []
for addr, net, gw in self.addrs[t]:
a = {'Address': '{0}/{1}'.format(str(addr), str(net.prefixlen))}
self._cfg['Address'].append(a)
if self.routes[t]:
if 'Route' not in self._cfg.keys():
self._cfg['Route'] = []
for route, net, gw in self.routes[t]:
r = {'Gateway': str(gw),
'Destination': '{0}/{1}'.format(str(route), str(net.prefixlen))}
self._cfg['Route'].append(r)
if self._cfg['Network']['IPv6AcceptRA'] == 'true':
self._cfg['Network']['LinkLocalAddressing'] = 'ipv6'
if 'IPv6AcceptRA' not in self._cfg.keys():
self._cfg['IPv6AcceptRA'] = {'UseDNS': ('true' if self.auto['resolvers']['ipv6'] else 'false')}
self._initConnCfg()
return()

def _initJ2(self):
self.j2_env = jinja2.Environment(loader = jinja2.FileSystemLoader(searchpath = './'))
self.j2_env.filters.update(aif.utils.j2_filters)
self.j2_tpl = self.j2_env.get_template('networkd.conf.j2')
return()

def writeConf(self, chroot_base):
cfgroot = os.path.join(chroot_base, 'etc', 'systemd', 'network')
cfgfile = os.path.join(cfgroot, self.id)
os.makedirs(cfgroot, exist_ok = True)
os.chown(cfgroot, 0, 0)
os.chmod(cfgroot, 0o0755)
with open(cfgfile, 'w') as fh:
fh.write(self.j2_tpl.render(cfg = self._cfg))
os.chmod(cfgfile, 0o0644)
os.chown(cfgfile, 0, 0)
self._writeConnCfg(chroot_base)
return()


class Ethernet(Connection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
self.connection_type = 'ethernet'
self._initCfg()


class Wireless(Connection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
self.connection_type = 'wireless'
self.packages.add('wpa_supplicant')
self.services['src'] = 'dest'
self._initCfg()

def _initConnCfg(self):
self._wpasupp['ssid'] = self.xml.attrib['essid']
hidden = aif.utils.xmlBool(self.xml.attrib.get('hidden', 'false'))
if hidden:
self._wpasupp['scan_ssid'] = 1
try:
bssid = self.xml.attrib.get('bssid').strip()
except AttributeError:
bssid = None
if bssid:
bssid = aif.network._common.canonizeEUI(bssid)
self._cfg['BASE']['AP'] = bssid
crypto = self.xml.find('encryption')
if crypto:
crypto = aif.network._common.convertWifiCrypto(crypto, self._cfg['BASE']['ESSID'])
# if crypto['type'] in ('wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
# TODO: WPA2 enterprise
self._cfg['BASE']['Security'] = 'wpa'
# if crypto['type'] in ('wep', 'wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
self._cfg['BASE']['Key'] = crypto['auth']['psk']
return()

def _writeConnCfg(self, chroot_base):
cfgroot = os.path.join(chroot_base, 'etc', 'wpa_supplicant')
cfgbase = os.path.join(cfgroot, 'wpa_supplicant.conf')
cfgfile = os.path.join(cfgroot, self.id)

View File

@ -145,7 +145,7 @@ class Wireless(Connection):
if crypto:
self.packages.add('wpa_supplicant')
self._cfg['wifi-security'] = {}
crypto = aif.network._common.convertWifiCrypto(crypto)
crypto = aif.network._common.convertWifiCrypto(crypto, self._cfg['wifi']['ssid'])
# if crypto['type'] in ('wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
# TODO: WPA2 enterprise

View File

@ -57,6 +57,20 @@ def isPowerofTwo(n):
return(isPowerOf2)


# custom Jinja2 filters
def j2_isDict(value):
return(isinstance(value, dict))


def j2_isList(value):
return(isinstance(value, list))


j2_filters = {'isDict': j2_isDict,
'isList': j2_isList}
# end custom Jinja2 filters


def kernelCmdline(chroot_base = '/'):
cmds = {}
chroot_base = pathlib.PosixPath(chroot_base)

View File

@ -574,32 +574,35 @@ LVM (LVs, in particular), however, aren't consecutive. There *is* no concept of
=== "How do I specify packages from the AUR?"
You'd have to https://wiki.archlinux.org/index.php/Makepkg[build the package(s)^], https://wiki.archlinux.org/index.php/Pacman/Tips_and_tricks#Custom_local_repository[set up a repository^], serve it via e.g. https://www.nginx.com/[nginx^], and add it as a repo (`/aif/pacman/repos/repo`) first. Then you can specify the package as normal as a `/aif/pacman/software/package` item.

=== "Why aren't the network settings in <network> being applied during install?"
=== "Why can't the network settings in <network> be applied during install?"
Simply put, a logical race condition. In order for probably 90+% of AIF-NG deploys to bootstrap, they fetch their XML configuration via a network URI (rather than a file URI). This means it needs a network connection that pre-exists in the *install environment* (LiveCD, LiveUSB, PXE/iPXE, etc.) before it even knows what network configuration you want the *persistent environment* to have.

Granted, this is a moot point if you're using a *`file://`* URI for the XML configuration, but this is not a very flexible means regardless. If demand increases for this, future releases may include this functionality.
Granted, this is a moot point if you're using a *`file://`* URI for the XML configuration, but this is not a very flexible means regardless. The installation host itself is outside the scope of AIF-NG.

If you desire the configuration to be applied *during* the install, you can do it yourself in an `/aif/scripts/pre/script` or `/aif/scripts/pkg/script` script. The fetched XML file can be found at `/var/tmp/AIF.xml` in the install environment. (Alternatively, configure the network yourself procedurally using one of those scripts).
If you desire the configuration to be applied *during* the install, you can do it yourself in an `/aif/scripts/pre/script` or `/aif/scripts/pkg/script` script. The fetched XML file can be found at `/var/tmp/AIF.xml` in the install environment.

If you wish to SSH into the install environment to check the status/progress of the install, it is recommended that you set up a static lease (if using DHCP) or use SLAAC (if using IPv6) beforehand and configure your install environment beforehand. Remember, AIF-NG only *installs* Arch Linux; it tries very hard to *not* interact with the install environment.

=== "Why isn't enabling/disabling automatic DNS resolvers working?"
This is going to be highly unpredictable based on the networking provider you choose. This is a limitation of underlying network provider intercompatibility, resolver libraries, and technology architecture. This may be changed in the future, but because of how DNS servers are handled via DHCP/RDNSS and glibc (and the fact that IPv4 resolver addresses can serve IPv6 -- e.g. AAAA -- records and vice versa) and inherent limitations in some network providers like netctl, I wouldn't hold your breath.
=== "Why isn't enabling/disabling automatic DNS resolvers/routes/addresses working?"
This is going to be highly unpredictable based on the networking provider you choose. This is a limitation of underlying network provider intercompatibility, resolver libraries, there being no way to tell DHCP/DHCP6/SLAAC clients to *only* fetch information about a network and *not* assign a lease, and technology architecture. This may be changed in the future, but because of how DNS servers are handled via DHCP/RDNSS and glibc (and the fact that IPv4 resolver addresses can serve IPv6 -- e.g. AAAA -- records and vice versa) and inherent limitations in some network providers like netctl, I wouldn't hold your breath.

=== "I'm using netctl as my network provider, and-"
I'ma let you finish, but netctl is a *really* simple network provider. I mean REALLY simple. As such, a lot of things (like mixing auto DNS and non-auto addressing) don't work at all feasibly, and probably might not ever. It's great for simple and flat configurations (i.e. all static everything, all automatic everything, etc.) and I even use it on my own machines where I can, but it just simply doesn't make allowances for more complex setups. (This is why init scripts were replaced by systemd for init, remember? Script-and-shell-based utilities, such as netctl -- seriously, the entire thing's written in Bash -- just can't handle more complex jobs reliably.)
I'ma let you finish, but netctl is a *really* simple network provider. I mean REALLY simple. As such, a lot of things don't work at all feasibly, and probably might not ever. It's great for simple and flat configurations (i.e. all static everything, all automatic everything, etc.) and I even use it on my own machines where I can, but it just simply doesn't make allowances for more complex setups. (This is why init scripts were replaced by systemd for init, remember? Script-and-shell-based utilities, such as netctl -- seriously, the entire thing's written in Bash -- just can't handle more complex jobs reliably.)

If you need more advanced functionality but don't want a lot of cruft or bloat, I recommend `networkd` as your network provider. It requires no extra packages (other than wpa_supplicant, if you're using wireless) because it's part of the systemd package (which is part of the most basic install of Arch) and handles more advanced configurations a lot more reliably.

=== "How do I specify WEP for a wireless network?"
You can't. WEP's pretty broken. I understand some legacy networks may still use it, but I'm incredibly uncomfortable supporting it.

If absolutely necessary, you can manually configure it yourself via a `/aif/scripts/post/script` script.
If absolutely necessary, you can manually configure it yourself via a `/aif/scripts/post/script` script (or just configure it once you boot the newly-installed system).

=== "How do I connect to a WPA2 Enterprise network?"
==== "Then why do you allow connecting to open wireless networks in the config?"
Because captive portals are a thing. *Authing* to them, however; that's out of my scope.

=== "How do I configure connecting to a WPA2 Enterprise network?"
You can't, currently; support is only stubbed out for now. If absolutely necessary, you can manually configure it yourself via a `/aif/scripts/post/script` script.

This hopefully will be changed in the future, however, as I'm interested in adding support. For now, WPA/WPA2 PSK only are considered supported.
This hopefully will be changed in the future, however, as I'm interested in adding support. For now, open and WPA/WPA2 PSK only are considered supported.

== Bug Reports/Feature Requests
NOTE: It is possible to submit a bug or feature request without registering in my bugtracker. One of my pet peeves is needing to create an account/register on a bugtracker simply to report a bug! The following links only require an email address to file a bug (which is necessary in case I need any further clarification from you or to keep you updated on the status of the bug/feature request -- so please be sure to use a valid email address).

View File

@ -130,11 +130,11 @@
</ipv6>
</addresses>
<routes>
<ipv4 defaultGateway="true" auto="true">
<ipv4 auto="true">
<route gateway="192.168.1.1">10.1.1.0/24</route>
<route gateway="10.1.1.4">172.16.1.20/32</route>
</ipv4>
<ipv6 defaultGateway="false" auto="true"/>
<ipv6 auto="true"/>
</routes>
<resolvers>
<ipv4 auto="false"/>

82
extras/genPSK.py Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3

import argparse
import binascii
import getpass
import sys
##
# from passlib.utils import pbkdf2 # deprecated
from passlib.crypto.digest import pbkdf2_hmac


def pskGen(ssid, passphrase):
# raw_psk = pbkdf2.pbkdf2(str(passphrase), str(ssid), 4096, 32) # deprecated
raw_psk = pbkdf2_hmac('sha1', str(passphrase), str(ssid), 4096, 32)
hex_psk = binascii.hexlify(raw_psk)
str_psk = hex_psk.decode('utf-8')
return(str_psk)


def parseArgs():
def essidchk(essid):
essid = str(essid)
if len(essid) > 32:
raise argparse.ArgumentTypeError('The maximum length of an ESSID is 32 characters')
return(essid)

def passphrasechk(passphrase):
if passphrase:
is_piped = False
passphrase = str(passphrase)
if passphrase == '-':
if sys.stdin.isatty():
raise argparse.ArgumentTypeError(('[STDIN] You specified a passphrase to be entered but did not '
'provide one via a pipe.'))
else:
is_piped = True
try:
# WPA-PSK only accepts ASCII for passphrase.
raw_pass = sys.stdin.read().encode('utf-8').decode('ascii').strip('\r').strip('\n')
except UnicodeDecodeError:
raise argparse.ArgumentTypeError('[STDIN] WPA-PSK passphrases must be an ASCII string')
if not 7 < len(passphrase) < 64:
raise argparse.ArgumentTypeError(('{0}WPA-PSK passphrases must be no shorter than 8 characters'
' and no longer than 63 characters. '
'Please ensure you have provided the '
'correct passphrase.').format(('[STDIN] ' if is_piped else '')))
return(passphrase)

args = argparse.ArgumentParser(description = 'Generate a PSK from a passphrase')
args.add_argument('-p', '--passphrase',
dest = 'passphrase',
default = None,
type = passphrasechk,
help = ('If specified, use this passphrase (otherwise securely interactively prompt for it). '
'If "-" (without quotes), read from stdin (via a pipe). '
'WARNING: THIS OPTION IS INSECURE AND MAY EXPOSE THE PASSPHRASE GIVEN '
'TO OTHER PROCESSES ON THIS SYSTEM'))
args.add_argument('ssid',
metavar = 'ESSID',
type = essidchk,
help = ('The ESSID (network name) to use for this passphrase. '
'(This is required because WPA-PSK uses it to salt the key derivation)'))
return(args)


def main():
args = parseArgs().parse_args()
if not args.passphrase:
args.passphrase = getpass.getpass(('Please enter the passphrase for '
'network "{0}" (will NOT echo back): ').format(args.ssid))
args.passphrase = args.passphrase.encode('utf-8').decode('ascii').strip('\r').strip('\n')
if not 7 < len(args.passphrase) < 64:
raise ValueError(('WPA-PSK passphrases must be no shorter than 8 characters'
' and no longer than 63 characters. '
'Please ensure you have provided the correct passphrase.'))
psk = pskGen(args.ssid, args.passphrase)
print('PSK for network "{0}": {1}'.format(args.ssid, psk))
return()


if __name__ == '__main__':
main()