summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/Makefile2
-rw-r--r--docs/netctl-auto.1.txt90
-rw-r--r--docs/netctl.1.txt2
-rw-r--r--docs/netctl.special.7.txt6
-rw-r--r--src/lib/wpa18
-rwxr-xr-xsrc/netctl-auto260
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: