diff options
-rw-r--r-- | docs/Makefile | 2 | ||||
-rw-r--r-- | docs/netctl-auto.1.txt | 90 | ||||
-rw-r--r-- | docs/netctl.1.txt | 2 | ||||
-rw-r--r-- | docs/netctl.special.7.txt | 6 | ||||
-rw-r--r-- | src/lib/wpa | 18 | ||||
-rwxr-xr-x | src/netctl-auto | 260 |
6 files changed, 339 insertions, 39 deletions
diff --git a/docs/Makefile b/docs/Makefile index 2fd858b..c90ead5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,6 +1,6 @@ # Makefile for netctl documentation -MANPAGES = netctl.1 netctl.profile.5 netctl.special.7 +MANPAGES = netctl.1 netctl-auto.1 netctl.profile.5 netctl.special.7 .PHONY: manpages install $(MANPAGES:=-install) clean manpages: $(MANPAGES) diff --git a/docs/netctl-auto.1.txt b/docs/netctl-auto.1.txt new file mode 100644 index 0000000..1fa17d8 --- /dev/null +++ b/docs/netctl-auto.1.txt @@ -0,0 +1,90 @@ +NETCTL-AUTO(1) +============== + +NAME +---- +netctl-auto - Control automatic selection of wireless netctl profiles + + +SYNOPSIS +-------- +*netctl-auto* {*COMMAND*} ... + +*netctl-auto* [--help | --version] + + +DESCRIPTION +----------- +*netctl-auto* may be used to control the automatic network profile selection +offered by the 'netctl-auto@.service' file. See *netctl.special*(7) for +details about the service file. + + +OPTIONS +------- +The following commands are understood: + +*list*:: + List all profiles which are currently available for automatic selection. + Active profiles will be marked with a '*', disabled profiles will be + marked with a '!'. + +*current*:: + Report currently active profiles. + +*switch-to [+PROFILE+]*:: + Switch to the network profile specified on the command line. The + specified profile will be enabled if necessary, the state of all other + profiles is not changed. This command does not force *netctl-auto* to use + the specified profile. If a disconnect occurs, *netctl-auto* may select + an alternative profile. + +*enable [+PROFILE+]*:: + Enable a previously disabled network profile for automatic selection. + Every time the *netctl-auto* service is started, all available profiles + are enabled by default. + +*disable [+PROFILE+]*:: + Disable the specified profile for automatic selection. This will only + take effect until the *netctl-auto* service is stopped. + To permanently exclude a profile from automatic selection, use the + 'ExcludeAuto=yes' option in the profile. + +*enable-all*:: + Enable all profiles for automatic selection. + +*disable-all*:: + Disable all profiles for automatic selection. + +*start [+INTERFACE+]*:: + Start automatic profile selection on the specified interface. + + This command should not be invoked directly, use the following command + instead: +--------------------------------------------- + systemctl start netctl-auto@<interface> +--------------------------------------------- + +*stop [+INTERFACE+]*:: + Stop automatic profile selection on the specified interface. This will + disconnect the currently active profile on the interface. + + This command should not be invoked directly, use the following command + instead: +-------------------------------------------- + systemctl stop netctl-auto@<interface> +-------------------------------------------- + + +EXIT STATUS +----------- +On success 0 is returned, a non-zero failure code otherwise. + + +ENVIRONMENT +----------- +'$NETCTL_DEBUG':: + If set to +"yes"+, debugging output is generated. + + +SEE ALSO +-------- +*netctl*(1), *netctl.profile*(5), *netctl.special*(7) diff --git a/docs/netctl.1.txt b/docs/netctl.1.txt index 954d62a..6440b00 100644 --- a/docs/netctl.1.txt +++ b/docs/netctl.1.txt @@ -89,4 +89,4 @@ ENVIRONMENT SEE ALSO -------- -*netctl.profile*(5), *netctl.special*(7), *systemctl*(1) +*netctl-auto*(1), *netctl.profile*(5), *netctl.special*(7), *systemctl*(1) diff --git a/docs/netctl.special.7.txt b/docs/netctl.special.7.txt index 668f7b1..c61290c 100644 --- a/docs/netctl.special.7.txt +++ b/docs/netctl.special.7.txt @@ -48,7 +48,9 @@ netctl-auto@<interface>.service:: start a profile for a network it finds. It is targeted at wireless interfaces. Profile specific values for WPADriver are ignored, but it is possible to specify WPADriver in an interface hook - ('/etc/netctl/interfaces/<interface>'). + ('/etc/netctl/interfaces/<interface>'). + + Use *netctl-auto*(1) instead of *netctl(1)* to control the automatic + profile selection when using this unit. netctl-ifplugd@<interface>.service:: This unit starts ifplugd on the interface it is used for. It will @@ -59,4 +61,4 @@ netctl-ifplugd@<interface>.service:: SEE ALSO -------- -*netctl*(1), *netctl.profile*(5) +*netctl*(1), *netctl-auto*(1), *netctl.profile*(5) diff --git a/src/lib/wpa b/src/lib/wpa index b80c54d..d79fdbe 100644 --- a/src/lib/wpa +++ b/src/lib/wpa @@ -166,6 +166,24 @@ wpa_quote() { fi } +## Unquotes a string, i.e. returned from wpa_cli +## Quoted: "string" -> string +## Non-Quoted: string -> string +## Hex: 737472696e67 -> string +wpa_unquote() { + local string="$1" + if [[ ${string:0:1} == '"' && ${string:(-1)} == '"' ]]; then + printf "%s" "${string:1:-1}" + elif [[ "$string" =~ ^([[:xdigit:]]{2})+$ ]]; then + while [[ -n "$string" ]]; do + printf "\x"${string:0:2} + string=${string:2} + done + else + printf "%s" "${string}" + fi +} + ## Create a configuration file for wpa_supplicant without any network blocks # $1: interface name wpa_make_config_file() { diff --git a/src/netctl-auto b/src/netctl-auto index ea692ca..7519325 100755 --- a/src/netctl-auto +++ b/src/netctl-auto @@ -1,4 +1,5 @@ #! /bin/bash +# Contributed by: Sebastian Wicki <gandro@gmx.net> . /usr/lib/network/globals . "$SUBR_DIR/wpa" @@ -7,68 +8,257 @@ : ${ACTIOND:=wpa_actiond -p /run/wpa_supplicant} : ${ACTION_SCRIPT:=$SUBR_DIR/auto.action} -if [[ $# != 2 || $1 != @(start|stop) ]]; then - exit_error "Usage: netctl-auto [start|stop] <interface>" -fi +usage() { + cat << END +Usage: netctl-auto {COMMAND} ... + [--help|--version] -STARTSTOP=$1 -INTERFACE=$2 -PIDFILE="$STATE_DIR/wpa_actiond_$INTERFACE.pid" -shift 2 +Commands: + list List available profiles (active='*', disabled='!') + current List currently active profiles + switch-to [PROFILE] Switch to a profile, enable it if necessary + enable [PROFILE] Enable a profile for automatic selection + disable [PROFILE] Disable a profile temporarily for automatic selection + enable-all Enable all profiles for automatic selection + disable-all Disable all profiles temporarily for automatic selection +END +} -case $STARTSTOP in - start) - if wpa_is_active "$INTERFACE"; then - exit_error "The interface ($INTERFACE) is already in use" +## Print a list of interfaces for which wpa_actiond is active +list_actiond_interfaces() { + find "$STATE_DIR" -maxdepth 1 -type f -name 'wpa_actiond_*.pid' \ + -exec basename -s ".pid" -a {} + | cut -d'_' -f3- +} + +## List all profiles available to the WPA supplicant +## Output format: INTERFACE ID FLAG PROFILE.. +## INTERFACE network interface of the profile +## ID wpa_supplicant numerical network id +## FLAG 'e'=enabled, 'd'=disabled, 'a'=active +## PROFILE.. profile name, may contain spaces +list_wpa_profiles() { + local interface + for interface in $(list_actiond_interfaces); do + local id ssid bssid flags + while IFS=$'\t' read -r id ssid bssid flags; do + local flag="e" + if [[ "$flags" =~ \[CURRENT\] ]]; then + flag="a" + elif [[ "$flags" =~ \[DISABLED\] ]]; then + flag="d" + fi + + local profile=$(wpa_call "$interface" get_network "$id" id_str) + profile=$(wpa_unquote "$profile") + + echo "$interface" "$id" "$flag" "$profile" + done < <(wpa_call "$interface" list_networks | tail -n+2) + done +} + +## Get WPA supplicant network id and interface for the given profile +## Output format: INTERFACE ID +# $1: profile name +get_wpa_network_id() { + local interface id flag profile + while read -r interface id flag profile; do + if [[ "$1" == "$profile" ]]; then + echo "$interface" "$id" + return 0 + fi + done < <(list_wpa_profiles) + + report_error "Profile '$1' does not exist or is not available" >&2 + return 1 +} + +## Enable or disable profiles in WPA supplicant +# $1: profile action: "enable", "disable", "enable-all" or "disable-all" +# $2: profile name if action is "enable" or "disable" +profile_enable_disable() { + local action="$1" profile="$2" + local id interfaces wpa_cmd + + if [ -n "$profile" ]; then + read -r interfaces id < <(get_wpa_network_id "$profile") || return 1 + else + interfaces=$(list_actiond_interfaces) + fi + + case $action in + enable) + wpa_cmd=(enable_network "$id");; + disable) + wpa_cmd=(disable_network "$id");; + enable-all) + wpa_cmd=(enable_network all);; + disable-all) + wpa_cmd=(disable_network all);; + *) + return 1; + esac + + local interface + for interface in $interfaces; do + wpa_call "$interface" "${wpa_cmd[@]}" >/dev/null + if [[ "${wpa_cmd[0]}" == "enable_network" ]]; then + wpa_call "$interface" reassociate >/dev/null + fi + done +} + +## Select profile in WPA supplicant, but preserve state of all other networks +# $1: profile name +switch_to() { + local profile="$1" + local id interface timeout + + # Load profile interface, WPA network id and timeout + read -r interface id < <(get_wpa_network_id "$profile") || return 1 + timeout=$(. "$PROFILE_DIR/$profile" >/dev/null; echo ${TimeoutWPA:=15}) + + # List of enabled networks + local enabled_networks=$(wpa_call "$interface" list_networks | tail -n+2 | \ + cut -f 1,4 | grep -Fv "[DISABLED]" | cut -f 1 | tr "\n" ' ') + + reenable_networks() { + for network in $enabled_networks; do + wpa_call "$interface" enable_network "$network" >/dev/null + done + + if [[ $(wpa_get_state "$interface") != "COMPLETED" ]]; then + if ! in_array "$id" $enabled_networks; then + wpa_call "$interface" disable_network "$id" >/dev/null + fi + fi + } + + # Reenable networks in case user aborts + trap "reenable_networks; exit 1" SIGINT SIGTERM + + # select_network will disable all other networks on that interface + wpa_call "$interface" select_network "$id" >/dev/null + if ! wpa_wait_until_state "$timeout" "$interface" "COMPLETED"; then + report_error "WPA association/authentication failed for interface '$interface'" + fi + + reenable_networks +} + +## List currently active profiles +current() { + local interface + for interface in $(list_actiond_interfaces); do + local state=$(wpa_get_state "$interface") + if [[ "$state" == "COMPLETED" ]]; then + wpa_call "$interface" status | sed -n 's/^id_str=//p' + fi + done +} + +## List all available profiles and their status +list() { + local interface id flag profile + while read -r interface id flag profile; do + echo "$(echo $flag | tr 'aed' '* !')" "$profile" + done < <(list_wpa_profiles) +} + +## Start and generate config file for the WPA supplicant, start wpa_actiond +# $1: interface +start() { + local interface="$1" + local pidfile="$STATE_DIR/wpa_actiond_$1.pid" + + if wpa_is_active "$interface"; then + exit_error "The interface ($interface) is already in use" fi - if [[ -x "$PROFILE_DIR/interfaces/$INTERFACE" ]]; then - source "$PROFILE_DIR/interfaces/$INTERFACE" + if [[ -x "$PROFILE_DIR/interfaces/$interface" ]]; then + source "$PROFILE_DIR/interfaces/$interface" fi if [[ $RFKill ]]; then - enable_rf "$INTERFACE" "$RFKill" || exit 1 + enable_rf "$interface" "$RFKill" || return 1 fi - if ! WPA_CONF=$(wpa_make_config_file "$INTERFACE"); then - exit_error "Could not create the configuration file for interface '$INTERFACE'" + + local wpa_conf + if ! wpa_conf=$(wpa_make_config_file "$interface"); then + exit_error "Could not create the configuration file for interface '$interface'" fi + + local profile list_profiles | while read -r profile; do report_debug "Examining profile '$profile'" ( source "$PROFILE_DIR/$profile" - [[ $Interface == "$INTERFACE" ]] || continue + [[ $Interface == "$interface" ]] || continue is_yes "${ExcludeAuto:-no}" && exit 1 [[ $Connection != "wireless" ]] && exit 1 : ${Security:=none} # Exclude wpa-config, the wpa_conf is 'complete' and doesn't fit in this scheme [[ $Security == "wpa-config" ]] && exit 1 - printf "%s\n" "network={" "$(wpa_make_config_block)" "id_str=\"$profile\"" "}" >> "$WPA_CONF" + printf "%s\n" "network={" "$(wpa_make_config_block)" "id_str=\"$profile\"" "}" >> "$wpa_conf" report_notice "Included profile '$profile'" ) done - # Start the WPA supplicant + # Start the WPA supplicant and wpa_actiond : ${WPADriver:=nl80211,wext} WPAOptions+=" -W" - if wpa_start "$INTERFACE" "$WPADriver" "$WPA_CONF"; then - if $ACTIOND -i "$INTERFACE" -P "$PIDFILE" -a "$ACTION_SCRIPT" "$@"; then - exit 0 + if wpa_start "$interface" "$WPADriver" "$wpa_conf"; then + if $ACTIOND -i "$interface" -P "$pidfile" -a "$ACTION_SCRIPT"; then + return 0 fi - wpa_stop "$INTERFACE" + wpa_stop "$interface" fi - exit 1 - ;; - stop) - kill "$(< "$PIDFILE")" - if [[ -x "$PROFILE_DIR/interfaces/$INTERFACE" ]]; then - source "$PROFILE_DIR/interfaces/$INTERFACE" + return 1 +} + +## Stop wpa_supplicant and wpa_actiond +# $1: interface +stop() { + local interface="$1" + local pidfile="$STATE_DIR/wpa_actiond_$1.pid" + + [[ -e "$pidfile" ]] && kill "$(< "$pidfile")" + if [[ -x "$PROFILE_DIR/interfaces/$interface" ]]; then + source "$PROFILE_DIR/interfaces/$interface" fi - timeout_wait 1 '! wpa_is_active "$INTERFACE"' || wpa_stop "$INTERFACE" - ip link set dev "$INTERFACE" down - [[ $RFKill ]] && disable_rf "$INTERFACE" "$RFKill" - exit 0 - ;; -esac + timeout_wait 1 '! wpa_is_active "$interface"' || wpa_stop "$interface" + ip link set dev "$interface" down + [[ $RFKill ]] && disable_rf "$interface" "$RFKill" + return 0 +} +case $# in + 1) + case $1 in + --version) + report_notice "netctl version $NETCTL_VERSION";; + --help) + usage;; + list|current) + "$1";; + enable-all|disable-all) + profile_enable_disable "$1";; + *) + exit_error "$(usage)";; + esac;; + 2) + case $1 in + enable|disable) + profile_enable_disable "$1" "$2";; + switch-to) + switch_to "$2";; + start|stop) + ensure_root "$(basename "$0")" + "$1" "$2";; + *) + exit_error "$(usage)";; + esac;; + *) + exit_error "$(usage)";; +esac # vim: ft=sh ts=4 et sw=4: |