[fx emu][gn sdk] Script to implement unsecure internet access to FEMU
Change-Id: I857ebeea7d146b0ef09559297cdca64bd8d8fd40
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/463182
Commit-Queue: Wayne Piekarski <waynepie@google.com>
Reviewed-by: Adam Barth <abarth@google.com>
Reviewed-by: Yilong Li <liyl@google.com>
Reviewed-by: Yuan Zhi <yuanzhi@google.com>
diff --git a/scripts/sdk/gn/base/bin/femu-meta.json b/scripts/sdk/gn/base/bin/femu-meta.json
index f783138..b9a77712 100644
--- a/scripts/sdk/gn/base/bin/femu-meta.json
+++ b/scripts/sdk/gn/base/bin/femu-meta.json
@@ -9,6 +9,7 @@
"bin/devshell/emu",
"bin/devshell/lib/image_build_vars.sh",
"bin/devshell/lib/fvm.sh",
+ "bin/start-unsecure-internet.sh",
"bin/devshell/lib/emu-ifup-macos.sh"
],
"name": "femu",
diff --git a/scripts/sdk/gn/bash_tests/BUILD.gn b/scripts/sdk/gn/bash_tests/BUILD.gn
index f2b4eab..b28a294 100644
--- a/scripts/sdk/gn/bash_tests/BUILD.gn
+++ b/scripts/sdk/gn/bash_tests/BUILD.gn
@@ -86,6 +86,11 @@
]
},
{
+ source_base_dir = "//scripts"
+ target_base_dir = "scripts"
+ files = [ "//scripts/start-unsecure-internet.sh" ]
+ },
+ {
source_base_dir = "//tools/devshell"
target_base_dir = "tools/devshell"
files = [
diff --git a/scripts/sdk/gn/bash_tests/femu-test.sh b/scripts/sdk/gn/bash_tests/femu-test.sh
index 17cb15e..9e4168c 100755
--- a/scripts/sdk/gn/bash_tests/femu-test.sh
+++ b/scripts/sdk/gn/bash_tests/femu-test.sh
@@ -191,6 +191,61 @@
gn-test-check-mock-partial --unknown-arg2-to-qemu
}
+TEST_femu_unsecure_internet() {
+ # Most of the commands used go through a sudo wrapper
+ cat >"${PATH_DIR_FOR_TEST}/ip.mock_side_effects" <<INPUT
+echo "qemu: tap persist user 238107"
+INPUT
+ cat >"${PATH_DIR_FOR_TEST}/sudo.mock_side_effects" <<INPUT
+echo "\$@"
+INPUT
+ cat >"${PATH_DIR_FOR_TEST}/hostname.mock_side_effects" <<INPUT
+echo "test.nongoogle.com"
+INPUT
+ cat >"${PATH_DIR_FOR_TEST}/lsb_release.mock_side_effects" <<INPUT
+echo "unknown"
+INPUT
+
+ if is-mac; then
+ # The upscript doesn't support OSX, so need to expect it will fail so the test suite passes
+ BT_EXPECT_FAIL "${BT_TEMP_DIR}/scripts/sdk/gn/base/bin/start-unsecure-internet.sh" "fakenetwork" >/dev/null 2>/dev/null
+ else
+ # Run the upscript with a fake qemu network interface argument
+ BT_EXPECT "${BT_TEMP_DIR}/scripts/sdk/gn/base/bin/start-unsecure-internet.sh" "fakenetwork" >/dev/null 2>/dev/null
+
+ source "${PATH_DIR_FOR_TEST}/ip.mock_state"
+ gn-test-check-mock-partial --oneline address show to 172.16.243.1
+ # There are a lot of commands executed, only test for the last one. Note that the
+ # number of tests can vary per-machine, and there are many sudo commands, so we
+ # can't look for a specific version and need to look at the last output file.
+ LAST_SUDO_MOCK_STATE="$(ls -1 ${PATH_DIR_FOR_TEST}/sudo.mock_state.* | sort -V | tail -1)"
+ source "${LAST_SUDO_MOCK_STATE}"
+ gn-test-check-mock-partial iptables -A POSTROUTING
+ fi
+}
+
+# Check that hostname checks work properly
+TEST_femu_unsecure_internet_fail_hostname() {
+ cat >"${PATH_DIR_FOR_TEST}/hostname.mock_side_effects" <<INPUT
+echo "test.domain.google.com"
+INPUT
+ cat >"${PATH_DIR_FOR_TEST}/lsb_release.mock_side_effects" <<INPUT
+echo "unknown"
+INPUT
+ BT_EXPECT_FAIL "${BT_TEMP_DIR}/scripts/sdk/gn/base/bin/start-unsecure-internet.sh" "fakenetwork" >/dev/null 2>/dev/null
+}
+
+# Check that rodete checks work properly
+TEST_femu_unsecure_internet_fail_hostname() {
+ cat >"${PATH_DIR_FOR_TEST}/hostname.mock_side_effects" <<INPUT
+echo "test.non-google.com"
+INPUT
+ cat >"${PATH_DIR_FOR_TEST}/lsb_release.mock_side_effects" <<INPUT
+echo "rodete"
+INPUT
+ BT_EXPECT_FAIL "${BT_TEMP_DIR}/scripts/sdk/gn/base/bin/start-unsecure-internet.sh" "fakenetwork" >/dev/null 2>/dev/null
+}
+
# Verifies that fx emu starts up grpcwebproxy correctly
TEST_femu_grpcwebproxy() {
@@ -351,6 +406,7 @@
tools/devshell/emu
tools/devshell/lib/fvm.sh
tools/devshell/lib/emu-ifup-macos.sh
+ scripts/start-unsecure-internet.sh
)
# shellcheck disable=SC2034
BT_MOCKED_TOOLS=(
@@ -371,6 +427,8 @@
_isolated_path_for/ip
_isolated_path_for/fakekill
_isolated_path_for/sudo
+ _isolated_path_for/hostname
+ _isolated_path_for/lsb_release
# Create fake "stty sane" command so that fx emu test succeeds when < /dev/null is being used
_isolated_path_for/stty
)
@@ -428,6 +486,7 @@
# Stage the files we copy from the fx emu implementation, replicating behavior of generate.py
cp -r "${BT_TEMP_DIR}/tools/devshell" "${BT_TEMP_DIR}/scripts/sdk/gn/base/bin/"
+ cp -r "${BT_TEMP_DIR}/scripts/start-unsecure-internet.sh" "${BT_TEMP_DIR}/scripts/sdk/gn/base/bin/"
}
BT_RUN_TESTS "$@"
diff --git a/scripts/sdk/gn/generate.py b/scripts/sdk/gn/generate.py
index 254edde..12b2d0e 100755
--- a/scripts/sdk/gn/generate.py
+++ b/scripts/sdk/gn/generate.py
@@ -53,6 +53,11 @@
src=os.path.join(FUCHSIA_ROOT, 'tools', 'devshell', 'lib', 'fvm.sh'),
base=os.path.join(FUCHSIA_ROOT, 'tools'),
dest='bin'),
+ # {fuchsia}/scripts/start-unsecure-internet.sh -> {out}/bin/start-unsecure-internet.sh
+ CopyArgs(
+ src=os.path.join(FUCHSIA_ROOT, 'scripts', 'start-unsecure-internet.sh'),
+ base=os.path.join(FUCHSIA_ROOT, 'scripts'),
+ dest='bin'),
# {fuchsia}/tools/devshell/lib/emu-ifup-macos.sh -> {out}/bin/devshell/lib/emu-ifup-macos.sh
CopyArgs(
src=os.path.join(
diff --git a/scripts/start-unsecure-internet.sh b/scripts/start-unsecure-internet.sh
new file mode 100755
index 0000000..4d6979f
--- /dev/null
+++ b/scripts/start-unsecure-internet.sh
@@ -0,0 +1,132 @@
+#!/bin/bash
+# Copyright 2021 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This is an upscript to use with qemu. It creates a bridge device that
+# connects to the qemu network interfaces and sets up network forwarding.
+# The emulator is exposed to the internet, and there is no security
+# implemented, so should be used with care.
+#
+# Example usage with the Fuchsia emulator:
+# The -I can be any device name, this script creates br${INTERFACE}.
+# fx emu --headless -N -I qemu -u ./start-unsecure-internet.sh
+#
+# This script is designed to be invoked from qemu with an interface argument.
+# It can also be run directly for testing as:
+# scripts/start-unsecure-internet.sh <interface>
+#
+# If a --cleanup argument is provided instead of <interface>, this
+# script will remove the previously created bridge device.
+
+usage() {
+ echo "Usage: $(basename "$0") <interface>"
+ echo " [--cleanup]"
+ echo " Remove previously created bridge device and stop"
+ echo " <interface>"
+ echo " Interface name provided to fx emu -I, typically 'qemu'"
+}
+
+
+# Check that we do not run this script in unsupported environments
+if [[ "$(lsb_release --release --short)" == "rodete" ]]; then
+ echo "This script cannot be run on rodete"
+ exit 1
+fi
+if [[ "$(hostname --fqdn)" == *".google.com" ]]; then
+ echo "This script cannot be run on *.google.com"
+ exit 1
+fi
+
+
+CLEANUP=0
+if [[ "$1" == "--cleanup" ]]; then
+ CLEANUP=1
+ shift
+fi
+if [[ $1 == -* ]]; then
+ echo "Unknown argument provided"
+ usage
+ exit 1
+fi
+if [[ "$2" != "" ]]; then
+ echo "Unexpected arguments provided"
+ echo
+ usage
+ exit 1
+fi
+
+INTERFACE="$1"
+if [[ "${INTERFACE}" == "" ]]; then
+ echo "No network interface name provided by qemu -u"
+ echo
+ usage
+ exit 1
+fi
+BRIDGE="br${INTERFACE}"
+
+# Enable error checking for all commands
+err_print() {
+ echo "Error at $1"
+ stty sane
+}
+trap 'err_print $0:$LINENO' ERR
+set -eu
+
+# Show every command being executed
+set -x
+
+# qemu has already brought up ${INTERFACE}, but delete any previous ${BRIDGE} to start from a known state.
+# If any other interface is using the 172.16.243.1 address, everything will fail silently.
+sudo ip link delete "${BRIDGE}" || echo "No existing bridge ${BRIDGE} to delete"
+sudo iptables -D POSTROUTING -t nat -s 172.16.243.0/24 ! -d 172.16.243.0/24 -j MASQUERADE || echo "No existing POSTROUTING nat rule to delete"
+for delbridge in $(ip --oneline address show to "172.16.243.1" | awk '{ print $2 }'); do
+ sudo ip link delete "${delbridge}"
+done
+
+# Exit out if the purpose was just to clean up
+if (( CLEANUP )); then
+ exit 0
+fi
+
+# Enable packet forwarding
+sudo sysctl -w net.ipv4.ip_forward=1
+CHECK="$(cat /proc/sys/net/ipv4/ip_forward)"
+if [[ "${CHECK}" != "1" ]]; then
+ echo "ERROR: Could not enable ip_forward"
+ exit 1
+fi
+
+# Create bridge with DHCP and default gateway.
+sudo ip link add name "${BRIDGE}" type bridge
+sudo ip addr add 172.16.243.1/24 dev "${BRIDGE}"
+
+# UFW blocks DHCP/BOOTP and bridge traffic, so open this up if installed
+if (command -v ufw && grep -q "^ENABLED=yes" /etc/ufw/ufw.conf) >/dev/null 2>&1; then
+ sudo ufw allow proto udp from 172.16.243.0/24 to 172.16.243.0/24 port 67,68 comment 'Fuchsia qemu DHCP'
+ sudo ufw allow dns comment 'Fuchsia qemu DNS'
+ sudo ufw allow proto udp from 0.0.0.0 to 255.255.255.255 port 67 comment 'Fuchsia qemu DHCP'
+ sudo ufw allow proto udp from 172.16.243.1 to 255.255.255.255 port 68 comment 'Fuchsia qemu DHCP'
+ sudo ufw route allow in on "${BRIDGE}" comment 'Fuchsia qemu bridge in'
+ sudo ufw route allow out on "${BRIDGE}" comment 'Fuchsia qemu bridge out'
+fi
+
+# Remove any previous dnsmasq attached to the IP range we use
+# Searching for the ${BRIDGE} is another alternative, but it may be different
+DNSMASQ_PID=$(ps -axww | grep dnsmasq | grep "\-\-dhcp-range=172\.16\.243\.2," | awk '{ print $1 }')
+if [[ "${DNSMASQ_PID}" != "" ]]; then
+ sudo kill "${DNSMASQ_PID}"
+fi
+
+# I've noticed that --listen-interface= is needed in some situations previously, but doesn't seem to be needed here
+sudo dnsmasq --interface="${BRIDGE}" --bind-interfaces --dhcp-range=172.16.243.2,172.16.243.254 --except-interface=lo || { echo "Failed to start dnsmasq"; exit 1; }
+sudo ip link set "${BRIDGE}" up
+
+# Remove pre-existing addresses from the interface as they'll no longer be routable
+sudo ip addr flush "${INTERFACE}"
+
+# Add qemu to the bridge
+sudo ip link set "${INTERFACE}" master "${BRIDGE}"
+
+# NAT packets arriving to the bridge
+sudo iptables -A POSTROUTING -t nat -s 172.16.243.0/24 ! -d 172.16.243.0/24 -j MASQUERADE