Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions base/debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Depends: adduser,
iptables,
jq,
less,
nftables,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a duplicate entry.

libarchive-tools | bsdtar,
libcap2-bin,
libcurl4,
Expand Down
10 changes: 6 additions & 4 deletions base/debian/cuttlefish-base.cuttlefish-host-resources.default
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@

# IPv6 prefixes allocated to the managed bridges. Require
# 'bridge_interface' not to be set.
#wifi_ipv6_prefix=
#wifi_ipv6_prefix_length=
#ethernet_ipv6_prefix=
#ethernet_ipv6_prefix_length=
wifi_ipv6_prefix=fd00:cf:22::
wifi_ipv6_prefix_length=64
ethernet_ipv6_prefix=fd00:cf:24::
ethernet_ipv6_prefix_length=64
ipv6_enable_nat66=1

35 changes: 32 additions & 3 deletions base/debian/cuttlefish-base.cuttlefish-host-resources.init
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ mkdir -p /run /var/run
# $4 = IPv6 address prefix ("a:b::")
# $5 = IPv6 address prefix length
start_dnsmasq() {
if [ -n "${4}" -a -n "${5}" ]; then
ipv6_args="--dhcp-range=${4},ra-stateless,${5} --enable-ra"
if [ -n "${4}" ]; then
ipv6_args="--dhcp-range=${4},ra-only --enable-ra"
else
ipv6_args=""
fi
Expand Down Expand Up @@ -113,13 +113,17 @@ setup_nftables() {
nft add table ip cuttlefish_nat
nft add chain ip cuttlefish_nat postrouting '{ type nat hook postrouting priority 100 ; }'

nft add table ip6 cuttlefish_nat6
nft add chain ip6 cuttlefish_nat6 postrouting '{ type nat hook postrouting priority 100 ; }'

nft add table bridge cuttlefish_bridge
nft add chain bridge cuttlefish_bridge prerouting '{ type filter hook prerouting priority -250 ; }'
nft add chain bridge cuttlefish_bridge forward '{ type filter hook forward priority 0 ; }'
}

delete_nftables() {
nft delete table ip cuttlefish_nat
nft delete table ip6 cuttlefish_nat6 2>/dev/null || true
nft delete table bridge cuttlefish_bridge
}

Expand All @@ -128,7 +132,7 @@ manage_nft_rule() {
local op="$1"
local type="$2"
local id="$3"
local contents="$4"
local contents="${4:-}"

local family=""
local table=""
Expand All @@ -140,6 +144,11 @@ manage_nft_rule() {
table="cuttlefish_nat"
chain="postrouting"
;;
masq6)
family="ip6"
table="cuttlefish_nat6"
chain="postrouting"
;;
broute4|broute6)
family="bridge"
table="cuttlefish_bridge"
Expand Down Expand Up @@ -212,6 +221,11 @@ create_interface() {
ip -6 addr add "${ipv6_prefix}1/${ipv6_prefix_length}" dev "${tap}"
fi
manage_nft_rule add masq "${tap}" "ip saddr ${network} masquerade"
if [ -n "${ipv6_prefix}" -a -n "${ipv6_prefix_length}" ]; then
if [ "${ipv6_enable_nat66:-1}" = "1" ] || [ "${ipv6_enable_nat66}" = "true" ]; then
manage_nft_rule add masq6 "${tap}" "ip6 saddr ${ipv6_prefix}/${ipv6_prefix_length} masquerade"
fi
fi
}

# Destroy a tap interface
Expand All @@ -228,6 +242,11 @@ destroy_interface() {
ipv6_prefix="${4}"
ipv6_prefix_length="${5}"

if [ -n "${ipv6_prefix}" -a -n "${ipv6_prefix_length}" ]; then
if [ "${ipv6_enable_nat66:-1}" = "1" ] || [ "${ipv6_enable_nat66}" = "true" ]; then
manage_nft_rule delete masq6 "${tap}"
fi
fi
manage_nft_rule delete masq "${tap}"
ip addr del "${gateway}${netmask}" dev "${tap}"
if [ -n "${ipv6_prefix}" -a -n "${ipv6_prefix_length}" ]; then
Expand Down Expand Up @@ -281,6 +300,11 @@ create_bridged_interfaces() {
"${2}" "${gateway}" "${dhcp_range}" \
"${ipv6_prefix}" "${ipv6_prefix_length}"
manage_nft_rule add masq "br-${2}" "ip saddr ${network} masquerade"
if [ -n "${ipv6_prefix}" -a -n "${ipv6_prefix_length}" ]; then
if [ "${ipv6_enable_nat66:-1}" = "1" ] || [ "${ipv6_enable_nat66}" = "true" ]; then
manage_nft_rule add masq6 "br-${2}" "ip6 saddr ${ipv6_prefix}/${ipv6_prefix_length} masquerade"
fi
fi
fi
}

Expand All @@ -297,6 +321,11 @@ destroy_bridged_interfaces() {
network="${1}.0${netmask}"
ipv6_prefix="${4}"
ipv6_prefix_length="${5}"
if [ -n "${ipv6_prefix}" -a -n "${ipv6_prefix_length}" ]; then
if [ "${ipv6_enable_nat66:-1}" = "1" ] || [ "${ipv6_enable_nat66}" = "true" ]; then
manage_nft_rule delete masq6 "br-${2}"
fi
fi
manage_nft_rule delete masq "br-${2}"
stop_dnsmasq "${2}"
if [ -n "${ipv6_prefix}" -a -n "${ipv6_prefix_length}" ]; then
Expand Down
31 changes: 31 additions & 0 deletions e2etests/cvd/network_tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (C) 2026 The Android Open Source Project
#
# 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.

load("@rules_go//go:def.bzl", "go_test")

go_test(
name = "network_tests",
size = "large",
srcs = ["main_test.go"],
tags = [
"exclusive",
"external",
"no-sandbox",
"requires_ab",
"supports-graceful-termination",
],
deps = [
"//cvd/common",
],
)
159 changes: 159 additions & 0 deletions e2etests/cvd/network_tests/main_test.go

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test depend on the test host actually having the defaults updated?

How can you run the test on a v4-only host?

Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright (C) 2026 The Android Open Source Project

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to have been added in another directory "network_tests" at the same level as the "networking_tests" directory. Perhaps this would be better with either:

  • a different directory name, if the goal of these tests is different from the goal of tests in networking_tests/
  • rolled into networking_tests/ as a new test file or a new test case

//
// 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.

package main

import (
"fmt"
"strings"
"testing"
"time"

"github.com/google/android-cuttlefish/e2etests/cvd/common"
)

func TestIPv6DualStackAndIPv6OnlyMode(t *testing.T) {
c := e2etests.TestContext{}
c.SetUp(t)
defer c.TearDown()

t.Log("Fetching AOSP build artifacts...")
if _, err := c.CVDFetch(e2etests.FetchArgs{
DefaultBuildBranch: "aosp-android-latest-release",
DefaultBuildTarget: "aosp_cf_x86_64_only_phone-userdebug",
}); err != nil {
t.Fatal(err)
}

t.Log("Launching Cuttlefish instance...")
if err := c.LaunchCVD(e2etests.CreateArgs{}); err != nil {
t.Fatal(err)
}

t.Log("Waiting for ADB device connection...")
var adbErr error
for i := 0; i < 10; i++ {
c.RunCmd("adb", "connect", "127.0.0.1:6520")
_, adbErr = c.RunCmd("timeout", "15s", "adb", "wait-for-device")
if adbErr == nil {
break
}
t.Logf("Waiting for ADB connection (attempt %d/10)...", i+1)
}
if adbErr != nil {
t.Fatalf("Timed out waiting for ADB device connection: %v", adbErr)
}

// 1. Verify SLAAC IPv6 assignment on in-guest network interface

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factorize or split up the test further instead of doing this

t.Log("Verifying in-guest SLAAC IPv6 address assignment...")
var ip6Output e2etests.CommandOutput
var activeDev string
var gatewayIP string
for attempt := 1; attempt <= 15; attempt++ {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

// Check all potential network interfaces (wireless and ethernet bridges)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following block is hard to reason about and this comment doesn't help.

for _, dev := range []string{"eth1", "buried_eth0", "eth0", "wlan0", "wlan1"} {
out, err := c.RunCmd("adb", "shell", fmt.Sprintf("ip -6 addr show dev %s 2>/dev/null || true", dev))
if err == nil {
if strings.Contains(out.Stdout, "fd00:cf:22:") {
activeDev = dev
gatewayIP = "fd00:cf:22::1"
ip6Output = out
break
} else if strings.Contains(out.Stdout, "fd00:cf:24:") {
activeDev = dev
gatewayIP = "fd00:cf:24::1"
ip6Output = out
break
}
}
}
if activeDev != "" {
break
}
time.Sleep(2 * time.Second)
}
if activeDev == "" {
allIPs, _ := c.RunCmd("adb", "shell", "ip -6 addr show")
t.Fatalf("Guest missing SLAAC ULA prefix (fd00:cf:22:: or fd00:cf:24::). All IPv6 interfaces:\n%s", allIPs.Stdout)
}
var guestIP string
for _, line := range strings.Split(ip6Output.Stdout, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "inet6 fd00:cf:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
guestIP = strings.Split(fields[1], "/")[0]
break
}
}
}
t.Logf("Acquired IPv6 ULA address %s on %s (gateway %s):\n%s", guestIP, activeDev, gatewayIP, ip6Output.Stdout)

// Ensure on-link routing table contains activeDev prefix (requires root on Android)
prefix := "fd00:cf:24::/64"
if strings.Contains(gatewayIP, "22") {
prefix = "fd00:cf:22::/64"
}
c.RunCmd("adb", "shell", "su 0 sh -c \"ip -6 rule add pref 50 lookup main 2>/dev/null || true\"")
c.RunCmd("adb", "shell", "su 0 sh -c \"ip -4 rule add pref 50 lookup main 2>/dev/null || true\"")
c.RunCmd("adb", "shell", fmt.Sprintf("su 0 sh -c \"ip -6 route add %s dev %s 2>/dev/null || true\"", prefix, activeDev))
c.RunCmd("adb", "shell", fmt.Sprintf("su 0 sh -c \"ip -6 route add default via %s dev %s 2>/dev/null || true\"", gatewayIP, activeDev))

// 2. Verify guest-to-host IPv6 connectivity
t.Logf("Verifying dual-stack guest-to-host IPv6 connectivity (gateway %s on %s)...", gatewayIP, activeDev)
// Query detected router link-local address from neighbor table
neighOut, _ := c.RunCmd("adb", "shell", fmt.Sprintf("ip -6 neigh show dev %s", activeDev))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factorize

var routerLL string
for _, line := range strings.Split(neighOut.Stdout, "\n") {
if strings.Contains(line, "router") || strings.Contains(line, "fe80:") {
fields := strings.Fields(line)
if len(fields) > 0 && strings.HasPrefix(fields[0], "fe80:") {
routerLL = fields[0]
break
}
}
}
t.Logf("Guest ULA: %s, Router LL: %s, Gateway ULA: %s", guestIP, routerLL, gatewayIP)

connCmd := fmt.Sprintf("su 0 toybox ping -6 -c 3 -I %s %s || su 0 toybox ping -6 -c 3 -I %s %s || su 0 toybox ping -6 -c 3 %s", guestIP, gatewayIP, activeDev, routerLL, gatewayIP)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

factorize

connOut, err := c.RunCmd("adb", "shell", connCmd)
if err != nil {
diag, _ := c.RunCmd("adb", "shell", fmt.Sprintf("su 0 ip -6 route show; su 0 ip -6 neigh show; su 0 toybox ping -6 -c 3 -I %s %s 2>&1 || true", guestIP, gatewayIP))
t.Fatalf("Dual-stack IPv6 connectivity failed: %v\nStdout: %s\nStderr: %s\nDiag:\n%s", err, connOut.Stdout, connOut.Stderr, diag.Stdout)
}
t.Logf("Dual-stack IPv6 connectivity successful:\n%s", connOut.Stdout)

// 3. Uninstall / Flush IPv4 (Simulate IPv6-Only environment)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deserves a different test; you are testing too many variables within the one test.

t.Logf("Flushing IPv4 addresses and routes on %s to test IPv6-only operation...", activeDev)
if _, err := c.RunCmd("adb", "shell", fmt.Sprintf("su 0 sh -c \"ip -4 addr flush dev %s\"", activeDev)); err != nil {
t.Fatalf("Failed to flush IPv4 address on %s: %v", activeDev, err)
}
c.RunCmd("adb", "shell", "su 0 sh -c \"ip -4 route flush table all\"")

// 4. Assert IPv4 is completely uninstalled/absent
ip4Output, _ := c.RunCmd("adb", "shell", fmt.Sprintf("ip -4 addr show dev %s", activeDev))
if strings.Contains(ip4Output.Stdout, "inet ") {
t.Fatalf("IPv4 address still present after flush: %s", ip4Output.Stdout)
}
t.Log("IPv4 successfully removed. Operating in pure IPv6 mode.")

// 5. Verify IPv6 communication continues to function 100% with zero IPv4
t.Logf("Verifying IPv6-only gateway connectivity...")
connOut, err = c.RunCmd("adb", "shell", connCmd)
if err != nil {
diag, _ := c.RunCmd("adb", "shell", fmt.Sprintf("su 0 ip -6 route show; su 0 ip -6 neigh show; su 0 toybox ping -6 -c 3 -I %s %s 2>&1 || true", guestIP, gatewayIP))
t.Fatalf("IPv6-only connectivity to gateway failed: %v\nStdout: %s\nStderr: %s\nDiag:\n%s", err, connOut.Stdout, connOut.Stderr, diag.Stdout)
}
t.Log("Pure IPv6 communication validated successfully.")
}
Loading