blob: 0f912d08b6c1081d8e738bad5a99c4a7a50519fe [file] [log] [blame]
#!/bin/sh
#
# Copyright (c) 2019 Google LLC
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# virtual-ap.h -- Configure the local host as a virtual WiFi access point
#
# --------------------------------------------------
# Default values for user configurable options
# --------------------------------------------------
VIRT_AP_IF=${VIRT_AP_IF-wlan0}
VIRT_AP_CHANNEL=${VIRT_AP_CHANNEL-1}
# --------------------------------------------------
# Constants
# --------------------------------------------------
IP_BIN=/sbin/ip
IPTABLES_BIN=/sbin/iptables
HOSTAPD_BIN=/usr/sbin/hostapd
DNSMASQ_BIN=/usr/sbin/dnsmasq
NMCLI_BIN=/usr/bin/nmcli
TMPDIR=${TMPDIR-/tmp}
PASSPHRASE_SEED_FILE=.virtual-ap-seed
PASSPHRASE_LEN=10
IPV4_SUBNET_BASE=192.168.243
IPV4_SUBNET_MASK=255.255.255.0
HOSTAPD_DEBUG_OPTS=
DNSMASQ_DEBUG_OPTS=
TOOL_NAME=`basename $0`
# --------------------------------------------------
# Functions
# --------------------------------------------------
select_external_interface()
{
# If an external interface has not been specified by the user...
[ "${VIRT_AP_EXTERNAL_IF}" = "" ] && {
# Use the destination interface for the first default route as the
# external interface for traffic from the virtual AP.
VIRT_AP_EXTERNAL_IF=`${IP_BIN} route show default | sed -e '1!d' -e 's/.* dev \([^ ]*\).*/\1/'`
}
}
select_ssid()
{
# If an SSID has not been specified by the user...
[ "${VIRT_AP_SSID}" = "" ] && {
# Generate an SSID with the form 'VIRT-AP-XXXX' where XXXX is the last 4 digits of the
# WiFi interface MAC address.
VIRT_AP_SSID=VIRT-AP-`sed -e 's/://g' /sys/class/net/${VIRT_AP_IF}/address | tail -c 5`
}
}
select_passphrase()
{
# If passphrase has not been specified by the user...
[ "${VIRT_AP_PASSPHRASE}" = "" ] && {
# If not already done, create a file containing a random seed for generating passphrases.
[ ! -f ${HOME}/${PASSPHRASE_SEED_FILE} ] && {
(umask 177 && cat /dev/urandom | head -c 128 > ${HOME}/${PASSPHRASE_SEED_FILE})
}
# Generate a passphrase from the SSID, the MAC address of the AP interface, and
# the random seed.
VIRT_AP_PASSPHRASE=`(echo ${VIRT_AP_SSID}; cat /sys/class/net/${VIRT_AP_IF}/address ${HOME}/${PASSPHRASE_SEED_FILE}) | openssl sha256 -binary | base64 | head -c ${PASSPHRASE_LEN}`
}
}
error_exit()
{
# Stop the daemons.
stop_dnsmasq
stop_hostapd
# Remove the firewall rules.
remove_firewall
# Shutdown the AP interface.
ap_interface_down
# Remove the control directory.
sudo rm -rf ${CONTROL_DIR}
exit 1
}
config_ap_interface()
{
echo -n "Configuring ${VIRT_AP_IF} interface ... "
# If Network Manager is in use...
[ -x ${NMCLI_BIN} ] && {
# Determine the current management status of the AP interface.
MGMT_STATUS=`${NMCLI_BIN} device status | sed -ne 's/^${VIRT_AP_IF} *[[:alnum:]]* *\([[:alnum:]]*\).*/\1/p'`
# If necessary, set the interface to unmanaged.
[ "${MGMT_STATUS}" != "unmanaged" ] && {
${NMCLI_BIN} device set ${VIRT_AP_IF} managed no
}
}
# Bring up the AP interface.
sudo ${IP_BIN} link set ${VIRT_AP_IF} up || {
echo "ERROR: ip link set up failed"
error_exit
}
# Assign the host IP address to the AP interface.
sudo ${IP_BIN} addr replace ${HOST_IPV4}/${IPV4_SUBNET_MASK} dev ${VIRT_AP_IF} || {
echo "ERROR: ip addr replace failed"
error_exit
}
echo 'done'
}
ap_interface_down()
{
echo -n "Stopping ${VIRT_AP_IF} interface ... "
sudo ${IP_BIN} link set ${VIRT_AP_IF} down >/dev/null 2>&1
echo 'done'
}
config_firewall()
{
echo -n "Configuring firewall rules for ${VIRT_AP_IF} ... "
# Remove any lingering firewall rules from a previous run of the script.
remove_firewall silent
# --------------------------------------------------
# Create and populate the input rules chain.
# --------------------------------------------------
(
# Create the input chain.
sudo ${IPTABLES_BIN} -N ${INPUT_CHAIN} &&
# Permit inbound established connections/conversations to the local host.
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -m state --state RELATED,ESTABLISHED -j ACCEPT &&
# Permit inbound ICMP traffic.
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -p icmp -j ACCEPT &&
# Permit new inbound DHCP traffic on virtual AP interface.
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -p udp -s 0.0.0.0/32 -d 255.255.255.255/32 --dport 67 -j ACCEPT &&
# Permit new inbound DNS traffic on virtual AP interface.
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -p udp -s ${AP_IPV4_SUBNET} -d ${HOST_IPV4} --dport 53 -j ACCEPT &&
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -p tcp -m state --state NEW -m tcp -s ${AP_IPV4_SUBNET} -d ${HOST_IPV4} --dport 53 -j ACCEPT &&
# Permit new inbound Weave traffic on virtual AP interface.
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -p udp -s ${AP_IPV4_SUBNET} -d 192.168.243.1 --dport 11095 -j ACCEPT &&
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -p tcp -m state --state NEW -m tcp -s ${AP_IPV4_SUBNET} -d ${HOST_IPV4} --dport 11095 -j ACCEPT &&
# Silently drop all other inbound traffic on virtual AP interface.
sudo ${IPTABLES_BIN} -A ${INPUT_CHAIN} -i ${VIRT_AP_IF} -j DROP
) || {
echo "ERROR: iptables failed"
error_exit
}
# --------------------------------------------------
# Create and populate the forward rules chain.
# --------------------------------------------------
(
# Create the forward chain.
sudo ${IPTABLES_BIN} -N ${FORWARD_CHAIN} &&
# Permit established connections/conversations forwarded through the AP host.
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -m state --state RELATED,ESTABLISHED -j ACCEPT &&
# Permit all forwarded ICMP traffic from AP interface.
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -p icmp -j ACCEPT &&
# Drop any forwarded traffic from AP interface destined to a non-public address.
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -d 10.0.0.0/8 -j DROP &&
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -d 172.16.0.0/12 -j DROP &&
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -d 192.168.0.0/16 -j DROP &&
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -d 224.0.0.0/8 -j DROP &&
# Permit all traffic forwarded from AP interface to the external interface.
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -o ${VIRT_AP_EXTERNAL_IF} -j ACCEPT &&
# Drop all other forwarded traffic from AP interface.
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -i ${VIRT_AP_IF} -j DROP &&
# Drop all other forwarded traffic to AP interface.
sudo ${IPTABLES_BIN} -A ${FORWARD_CHAIN} -o ${VIRT_AP_IF} -j DROP
) || {
echo "ERROR: iptables failed"
error_exit
}
# --------------------------------------------------
# Create and populate the NAT postrouting rules chain.
# --------------------------------------------------
(
# Create the NAT postrouting chain.
sudo ${IPTABLES_BIN} -t nat -N ${POSTROUTING_CHAIN} &&
# Perform source NAT on traffic from AP network exiting via the external interface
sudo ${IPTABLES_BIN} -t nat -A ${POSTROUTING_CHAIN} -s ${AP_IPV4_SUBNET} -o ${VIRT_AP_EXTERNAL_IF} -j MASQUERADE
) || {
echo "ERROR: iptables failed"
error_exit
}
# --------------------------------------------------
# Add jump rules to the main chains
# --------------------------------------------------
(
# Direct all NAT post-routing traffic on virtual AP interface through the virtual AP NAT postrouting chain.
sudo ${IPTABLES_BIN} -t nat -I POSTROUTING 1 -j ${POSTROUTING_CHAIN} &&
# Direct all inbound traffic on virtual AP interface through the virtual AP input chain.
sudo ${IPTABLES_BIN} -I INPUT 1 -i ${VIRT_AP_IF} -j ${INPUT_CHAIN} &&
# Direct all forwarded traffic on virtual AP interface through the virtual AP forward chain.
sudo ${IPTABLES_BIN} -I FORWARD 1 -j ${FORWARD_CHAIN}
) || {
echo "ERROR: iptables failed"
error_exit
}
echo "done"
}
remove_firewall()
{
[ "$1" = "silent" ] || echo -n "Removing firewall rules for ${VIRT_AP_IF} ... "
# Remove the interface-specific jump rules.
sudo ${IPTABLES_BIN} -D INPUT -i ${VIRT_AP_IF} -j ${INPUT_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -D FORWARD -j ${FORWARD_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -t nat -D POSTROUTING -j ${POSTROUTING_CHAIN} > /dev/null 2>&1
# Remove the interface-specific chains.
sudo ${IPTABLES_BIN} -F ${INPUT_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -X ${INPUT_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -F ${FORWARD_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -X ${FORWARD_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -t nat -F ${POSTROUTING_CHAIN} > /dev/null 2>&1
sudo ${IPTABLES_BIN} -t nat -X ${POSTROUTING_CHAIN} > /dev/null 2>&1
[ "$1" = "silent" ] || echo "done"
}
start_hostapd()
{
echo -n "Starting hostapd for ${VIRT_AP_IF} ... "
# Construct hostapd config file
(umask 177 && cat > ${CONTROL_DIR}/hostapd.conf) <<END
interface=${VIRT_AP_IF}
ssid=${VIRT_AP_SSID}
hw_mode=g
wpa=2
wpa_passphrase=${VIRT_AP_PASSPHRASE}
wpa_key_mgmt=WPA-PSK WPA-PSK-SHA256
country_code=US
channel=${VIRT_AP_CHANNEL}
END
# Create the hostapd control directory with restricted permissions.
(umask 077 && mkdir ${CONTROL_DIR}/hostapd.control)
# Start hostapd daemon.
sudo ${HOSTAPD_BIN} -B -P ${CONTROL_DIR}/hostapd.pid -f ${CONTROL_DIR}/hostapd.log -g ${CONTROL_DIR}/hostapd.control/${VIRT_AP_IF} ${HOSTAPD_DEBUG_OPTS} ${CONTROL_DIR}/hostapd.conf || {
echo 'ERROR: hostapd failed.'
[ -f ${CONTROL_DIR}/hostapd.log ] && cat ${CONTROL_DIR}/hostapd.log
error_exit
exit 1
}
# Give the user access to the hostapd control and log files
sudo chown $USER ${CONTROL_DIR}/hostapd.pid ${CONTROL_DIR}/hostapd.log ${CONTROL_DIR}/hostapd.control/${VIRT_AP_IF}
echo 'done'
}
stop_hostapd()
{
echo -n "Stopping hostapd for ${VIRT_AP_IF} ... "
[ -f ${CONTROL_DIR}/hostapd.pid ] && sudo kill -TERM `cat ${CONTROL_DIR}/hostapd.pid` >/dev/null 2>&1
echo 'done'
}
start_dnsmasq()
{
echo -n "Starting dnsmasq for ${VIRT_AP_IF} ... "
# Construct dnsmasq config file
(umask 177 && cat > ${CONTROL_DIR}/dnsmasq.conf) <<END
# Bind only to the host IP address on the AP interface
listen-address=${HOST_IPV4}
bind-interfaces
# Don't read upstream nameservers from /etc/resolv.conf.
no-resolv
# Don't read hosts from /etc/hosts
no-hosts
# Enable DHCP on AP network
dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},${IPV4_SUBNET_MASK},24h
# Answer queries using Google DNS servers by default.
server=8.8.8.8
server=8.8.4.4
END
# Start dnsmasq daemon.
sudo ${DNSMASQ_BIN} -C ${CONTROL_DIR}/dnsmasq.conf -x ${CONTROL_DIR}/dnsmasq.pid -l ${CONTROL_DIR}/dnsmasq.leases -8 ${CONTROL_DIR}/dnsmasq.log ${DNSMASQ_DEBUG_OPTS} || {
echo 'ERROR: dnsmasq failed.'
[ -f ${CONTROL_DIR}/dnsmasq.log ] && cat ${CONTROL_DIR}/dnsmasq.log
error_exit
exit 1
}
# Give the user access to the dnsmasq control and log files.
sudo chown $USER ${CONTROL_DIR}/dnsmasq.pid ${CONTROL_DIR}/dnsmasq.leases ${CONTROL_DIR}/dnsmasq.log
echo 'done'
}
stop_dnsmasq()
{
echo -n "Stopping dnsmasq for ${VIRT_AP_IF} ... "
[ -f ${CONTROL_DIR}/dnsmasq.pid ] && sudo kill -TERM `cat ${CONTROL_DIR}/dnsmasq.pid` >/dev/null 2>&1
echo 'done'
}
enable_ip_forwarding()
{
[ `cat /proc/sys/net/ipv4/ip_forward` -eq 1 ] || {
echo -n "Enabling IPv4 forwarding ... "
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward >/dev/null
echo 'done'
}
}
virtual_ap_up()
{
# Prompt for sudo password
sudo true || exit 1
[ -d /sys/class/net/${VIRT_AP_IF} ] || {
echo "ERROR: WiFi device ${VIRT_AP_IF} not found"
exit 1
}
# Check if already running.
[ -d ${CONTROL_DIR} ] && {
echo "Virtual AP already running on ${VIRT_AP_IF}"
echo "Use '$0 down' to stop"
exit 1
}
# Select an SSID if not specified by the user.
select_ssid
# Select a passphrase if not specified by the user
select_passphrase
# Select the interface through which outbound traffic from the virtual AP
# will be directed.
select_external_interface
# Make control directory.
(umask 077 && mkdir ${CONTROL_DIR}) || {
echo "ERROR: Failed to create control directory at ${CONTROL_DIR}"
exit 1
}
# Configure the AP interface.
config_ap_interface
# Configure firewall rules.
config_firewall
# Start hostapd
start_hostapd
# Start dnsmasq
start_dnsmasq
# Enable IP forwarding
enable_ip_forwarding
echo ""
echo "Virtual AP enabled on ${VIRT_AP_IF}"
echo " SSID: ${VIRT_AP_SSID}"
echo " Passphrase: ${VIRT_AP_PASSPHRASE}"
echo " WiFi Channel: ${VIRT_AP_CHANNEL}"
}
virtual_ap_down()
{
# Prompt for sudo password
sudo true || exit 1
# Stop the daemons.
stop_dnsmasq
stop_hostapd
# Remove the firewall rules.
remove_firewall
# Shutdown the AP interface.
ap_interface_down
# Remove the control directory.
sudo rm -rf ${CONTROL_DIR}
}
display_help()
{
select_external_interface
cat <<END
${TOOL_NAME}: Configure the local host as a virtual WiFi access point
Usage:
${TOOL_NAME} [options] <command>
Commands:
up Bring up a virtual WiFi access point
down Shutdown the virtual WiFi access point
help Show this help message.
Options:
--ssid <ssid> The SSID for the virtual access point. If not
specified, an SSID of 'VIRT-AP-XXXX' will be used,
where XXXX is derived from the MAC address of the
WiFi interface.
--channel <chan> The WiFi channel for the virtual access point;
defaults to ${VIRT_AP_CHANNEL}.
--passphrase <pass> The passphrase for the virtual access point. If not
specified, a random passphrase will be generated.
--interface <if> The WiFi interface to use for the virtual access
point; defaults to ${VIRT_AP_IF}.
--external <if> The network interface through which outbound traffic
from the virtual access point will be directed;
defaults to ${VIRT_AP_EXTERNAL_IF}.
END
}
no_op()
{
cat <<END
${TOOL_NAME}: Configure the local host as a virtual WiFi access point
Type '${TOOL_NAME} help' for usage.
END
}
# --------------------------------------------------
# Main Code
# --------------------------------------------------
# Default operation
OP=no_op
# Parse command line arguments
while [ $# -gt 0 ]; do
OPT=$1
shift
case ${OPT} in
up)
OP=virtual_ap_up
;;
down)
OP=virtual_ap_down
;;
--ssid)
VIRT_AP_SSID=$1
shift
;;
--channel)
VIRT_AP_CHANNEL=$1
shift
;;
--interface)
VIRT_AP_IF=$1
shift
;;
--passphrase)
VIRT_AP_PASSPHRASE=$1
shift
;;
--external)
VIRT_AP_EXTERNAL_IF=$1
shift
;;
-h|--help|help)
OP=display_help
;;
*)
echo "$0: Unknown option: ${OPT}"
exit 1
;;
esac
done
# Setup derived variables
CONTROL_DIR=${TMPDIR}/virtual-ap-${VIRT_AP_IF}
AP_IPV4_SUBNET=${IPV4_SUBNET_BASE}.0/${IPV4_SUBNET_MASK}
HOST_IPV4=${IPV4_SUBNET_BASE}.1
INPUT_CHAIN=vap-in-${VIRT_AP_IF}
FORWARD_CHAIN=vap-fw-${VIRT_AP_IF}
POSTROUTING_CHAIN=vap-pr-${VIRT_AP_IF}
DHCP_RANGE_START=${IPV4_SUBNET_BASE}.100
DHCP_RANGE_END=${IPV4_SUBNET_BASE}.199
# Invoked the requested operation
${OP}