[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