blob: 1de49ded61075b4d7bd4580991d2b3cc75dabd3d [file] [log] [blame] [edit]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package network
import (
"context"
"strings"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/bundles/cros/network/dns"
"chromiumos/tast/local/crostini"
"chromiumos/tast/local/multivm"
"chromiumos/tast/testing"
)
type dnsProxyTestParams struct {
mode dns.DoHMode
}
func init() {
testing.AddTest(&testing.Test{
Func: DNSProxy,
Desc: "Ensure that DNS proxies are working correctly",
Contacts: []string{"jasongustaman@google.com", "garrick@google.com", "cros-networking@google.com"},
Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"chrome", "vm_host", "arc", "dlc"},
Data: []string{crostini.GetContainerMetadataArtifact("buster", false), crostini.GetContainerRootfsArtifact("buster", false)},
Pre: multivm.ArcCrostiniStartedWithDNSProxy(),
HardwareDeps: crostini.CrostiniStable,
Timeout: 7 * time.Minute,
Params: []testing.Param{{
Name: "doh_off",
Val: dnsProxyTestParams{
mode: dns.DoHOff,
},
}, {
Name: "doh_automatic",
Val: dnsProxyTestParams{
mode: dns.DoHAutomatic,
},
}, {
Name: "doh_always_on",
Val: dnsProxyTestParams{
mode: dns.DoHAlwaysOn,
},
}},
})
}
// DNSProxy tests DNS functionality with DNS proxy active.
// There are 2 parts to this test:
// 1. Ensuring that DNS queries are successful.
// 2. Ensuring that DNS queries are using proper mode (Off, Automatic, Always On) by blocking the expected ports, expecting the queries to fail.
func DNSProxy(ctx context.Context, s *testing.State) {
const (
// Randomly generated domains to be resolved. Different domains are used to avoid caching.
domainDefaultDoHOff = "a2ffec2cb85be5e7.com"
domainDefaultDoHAutomatic = "b3ae8819fed33ac3.com"
domainDefaultDoHAlwaysOn = "c103afeaadbc112a.com"
domainDNSBlockedDoHOff = "da39a3ee5e6b4b0d.com"
domainDNSBlockedDoHAutomatic = "eb39510b23affe12.com"
domainDNSBlockedDoHAlwaysOn = "ff3e2abb9002aba1.com"
)
// If the main body of the test times out, we still want to reserve a few
// seconds to allow for our cleanup code to run.
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(cleanupCtx, 3*time.Second)
defer cancel()
pre := s.PreValue().(*multivm.PreData)
cr := pre.Chrome
tconn := pre.TestAPIConn
a := multivm.ARCFromPre(pre)
cont := multivm.CrostiniFromPre(pre)
// Install dig in container.
if err := dns.InstallDigInContainer(ctx, cont); err != nil {
s.Fatal("Failed to install dig in container: ", err)
}
// Toggle plain-text DNS or secureDNS depending on test parameter.
params := s.Param().(dnsProxyTestParams)
if err := dns.SetDoHMode(ctx, cr, tconn, params.mode, dns.GoogleDoHProvider); err != nil {
s.Fatal("Failed to set DNS-over-HTTPS mode: ", err)
}
// Wait for the updated DoH mode to be propagated to the proxy.
// TODO(jasongustaman): Update to polling shill DBus property.
testing.Sleep(ctx, 10*time.Second)
var domainDefault, domainDNSBlocked string
switch params.mode {
case dns.DoHOff:
domainDefault = domainDefaultDoHOff
domainDNSBlocked = domainDNSBlockedDoHOff
case dns.DoHAutomatic:
domainDefault = domainDefaultDoHAutomatic
domainDNSBlocked = domainDNSBlockedDoHAutomatic
case dns.DoHAlwaysOn:
domainDefault = domainDefaultDoHAlwaysOn
domainDNSBlocked = domainDNSBlockedDoHAlwaysOn
}
// By default, DNS query should work.
var defaultTC = []dns.ProxyTestCase{
{Client: dns.System},
{Client: dns.User},
{Client: dns.Chrome},
{Client: dns.Crostini},
{Client: dns.ARC},
}
if errs := dns.TestQueryDNSProxy(ctx, defaultTC, a, cont, domainDefault); len(errs) != 0 {
for _, err := range errs {
s.Error("Failed DNS query check: ", err)
}
}
nss, err := dnsProxyNamespaces(ctx)
if err != nil {
s.Fatal("Failed to get DNS proxy's network namespaces: ", err)
}
physIfs, err := physicalInterfaces(ctx)
if err != nil {
s.Fatal("Failed to get phyiscal interfaces: ", err)
}
// Block plain-text or secure DNS through iptables.
var dnsBlockedTC []dns.ProxyTestCase
switch params.mode {
case dns.DoHOff:
defer modifyPlaintextDNSDropRule(ctx, "-D" /*op*/, nss, physIfs)
if errs := modifyPlaintextDNSDropRule(ctx, "-I" /*op*/, nss, physIfs); len(errs) != 0 {
s.Fatal("Failed to block DNS: ", errs)
}
dnsBlockedTC = []dns.ProxyTestCase{
{Client: dns.System, ExpectErr: true},
{Client: dns.User, ExpectErr: true},
{Client: dns.Chrome, ExpectErr: true},
{Client: dns.Crostini, ExpectErr: true},
{Client: dns.ARC}}
case dns.DoHAutomatic:
return
case dns.DoHAlwaysOn:
defer modifyDoHDropRule(ctx, "-D" /*op*/, nss, physIfs)
if errs := modifyDoHDropRule(ctx, "-I" /*op*/, nss, physIfs); len(errs) != 0 {
s.Fatal("Failed to block DNS: ", errs)
}
dnsBlockedTC = []dns.ProxyTestCase{
{Client: dns.System, ExpectErr: true},
{Client: dns.User, ExpectErr: true},
{Client: dns.Crostini, ExpectErr: true},
{Client: dns.ARC}}
}
// DNS queries should fail if corresponding DNS packets (plain-text or secure) are dropped.
if errs := dns.TestQueryDNSProxy(ctx, dnsBlockedTC, a, cont, domainDNSBlocked); len(errs) != 0 {
for _, err := range errs {
s.Error("Failed DNS query check: ", err)
}
}
}
// dnsProxyNamespaces iterates through available network namespaces and return the namespaces with DNS proxy.
// DNS proxy namespaces are identified by checking if the namespace contain a listening process named dnsproxyd.
func dnsProxyNamespaces(ctx context.Context) ([]string, error) {
out, err := testexec.CommandContext(ctx, "ip", "netns", "list").Output(testexec.DumpLogOnError)
if err != nil {
return nil, err
}
var nss []string
for _, o := range strings.Split(strings.TrimSpace(string(out)), "\n") {
ns := strings.Fields(o)[0]
ss, err := testexec.CommandContext(ctx, "ip", "netns", "exec", ns, "ss", "-lptun").Output(testexec.DumpLogOnError)
if err != nil {
return nil, err
}
if strings.Contains(string(ss), "dnsproxyd") {
nss = append(nss, ns)
}
}
return nss, nil
}
func physicalInterfaces(ctx context.Context) ([]string, error) {
out, err := testexec.CommandContext(ctx, "/usr/bin/find", "/sys/class/net", "-type", "l", "-not", "-lname", "*virtual*", "-printf", "%f\n").Output(testexec.DumpLogOnError)
if err != nil {
return nil, errors.Wrap(err, "failed to get physical interfaces")
}
return strings.Split(strings.TrimSpace(string(out)), "\n"), nil
}
// modifyPlaintextDNSDropRule blocks plaintext DNS outbound packets that go through the physical interfaces or DNS proxy namespace.
// Blocking is done by dropping outbound UDP and TCP packets with port 53.
// The caller of this function is required to tear down the updated state.
func modifyPlaintextDNSDropRule(ctx context.Context, op string, nss, physIfs []string) []error {
var errs []error
for _, cmd := range []string{"iptables", "ip6tables"} {
for _, ns := range nss {
if err := testexec.CommandContext(ctx, "ip", "netns", "exec", ns, cmd, op, "OUTPUT", "-p", "udp", "--dport", "53", "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to modify UDP plaintext DNS block rule on %s", ns))
}
if err := testexec.CommandContext(ctx, "ip", "netns", "exec", ns, cmd, op, "OUTPUT", "-p", "tcp", "--dport", "53", "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to modify TCP plaintext DNS block rule on %s", ns))
}
}
for _, ifname := range physIfs {
if err := testexec.CommandContext(ctx, cmd, op, "OUTPUT", "-p", "udp", "--dport", "53", "-o", ifname, "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to modify UDP plaintext DNS block rule for %s", ifname))
}
if err := testexec.CommandContext(ctx, cmd, op, "OUTPUT", "-p", "tcp", "--dport", "53", "-o", ifname, "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to modify TCP plaintext DNS block rule for %s", ifname))
}
}
if err := testexec.CommandContext(ctx, cmd, op, "OUTPUT", "-p", "udp", "--dport", "53", "-m", "owner", "--uid-owner", "chronos", "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrap(err, "failed to modify UDP plaintext DNS block rule for Chrome"))
}
if err := testexec.CommandContext(ctx, cmd, op, "OUTPUT", "-p", "tcp", "--dport", "53", "-m", "owner", "--uid-owner", "chronos", "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrap(err, "failed to modify TCP plaintext DNS block rule for Chrome"))
}
}
return errs
}
// modifyDoHDropRule blocks secure DNS outbound packets that go through the physical interfaces or DNS proxy namespace.
// Blocking is done by dropping outbound TCP packets with port 443 (HTTPS packets).
// The caller of this function is required to tear down the updated state.
func modifyDoHDropRule(ctx context.Context, op string, nss, physIfs []string) []error {
var errs []error
for _, cmd := range []string{"iptables", "ip6tables"} {
for _, ns := range nss {
if err := testexec.CommandContext(ctx, "ip", "netns", "exec", ns, cmd, op, "OUTPUT", "-p", "tcp", "--dport", "443", "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to modify secure DNS block rule on %s", ns))
}
}
for _, ifname := range physIfs {
if err := testexec.CommandContext(ctx, cmd, op, "OUTPUT", "-p", "tcp", "--dport", "443", "-o", ifname, "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to modify secure DNS block rule for %s", ifname))
}
}
if err := testexec.CommandContext(ctx, cmd, op, "OUTPUT", "-p", "tcp", "--dport", "443", "-m", "owner", "--uid-owner", "chronos", "-j", "DROP", "-w").Run(testexec.DumpLogOnError); err != nil {
errs = append(errs, errors.Wrap(err, "failed to modify secure DNS block rule for Chrome"))
}
}
return errs
}