Chromium Background Connections

  • Date:
Chromium Logo

I recently became curious about why Chromium, the open-source core of the Google Chrome web browser, creates various outgoing connections that are not explicitly initiated by the user and whether or not they can be disabled without modifying & recompiling the browser. Under chrome://settings, there are a number of features that can be toggled to reduce, but not eliminate, background traffic.

The Google Chrome Privacy Whitepaper "... describes the features in Chrome that communicate with Google, as well as with third-party services (for example, if you've changed your default search engine). It also describes the controls available to you regarding how your data is used by Chrome." The document was a useful blueprint, but not all of the browser's behaviors are addressed there. My investigation was conducted with the Chromium binary provided by Debian, a slightly modified version of the upstream v57.0.2987.98 at the time of this writing. Source code references come from the upstream version.

In this article, I use the host address 0.0.0.1 as a blackhole instead of the more commonly used 0.0.0.0 or 127.0.0.1. On a properly configured machine, 0.0.0.1 is never routed to a functioning host unlike the other two addresses, so using 0.0.0.1 is less likely to have implications for the security of the host and host's network.

This article, my findings, opinions and conclusions are wholly my own. They are not sanctioned by, approved by or affiliated with Google Incorporated or the Chromium project in any capacity.

Initial Findings ¶

I already had a set of flags that I use when creating ephemeral Chromium profiles that cut down on unwanted network traffic that I used as the starting point:

To monitor network operations, I used strace(1). Chromium normally runs with a sandbox that cannot be monitored with ptrace(3) by an unprivileged user, but this can be disabled with the "--no-sandbox" command line option. I first ran strace -f chromium ... which displays all syscalls made by the spawned process and any of its children to get an idea of what to look for. Reviewing connect(2) calls seemed like a good place to start:

$ strace -f -e connect \
   chromium \
    --disable-client-side-phishing-detection \
    --disable-suggestions-service \
    --safebrowsing-disable-download-protection \
    --user-data-dir="/tmp/experiment/" \
    --no-first-run \
    --no-sandbox 2>&1 \
  | egrep -o 'htons.*inet_(addr|pton)\([^)]+\)' \
  | sort -u

That displays any connect(2) syscalls made by Chromium, filters the output with grep(1) to make it easier to digest and sort(1) to eliminate redundant information. The resulting output looks something like this:

htons(0), sin_addr=inet_addr("172.217.6.46")
htons(443), sin_addr=inet_addr("172.217.6.42")
htons(443), sin_addr=inet_addr("172.217.6.46")
htons(443), sin_addr=inet_addr("74.125.170.201")
htons(53), inet_pton(AF_INET6, "2001:4860:4860::8888", &sin6_addr)
htons(53), sin_addr=inet_addr("10.0.0.1")

All of the publicly routable IP addresses in the output belong to Google which is reflected in the rDNS / PTR records for most of the addresses:

~$ host 172.217.6.46
46.6.217.172.in-addr.arpa domain name pointer sfo03s08-in-f14.1e100.net.
46.6.217.172.in-addr.arpa domain name pointer sfo03s08-in-f46.1e100.net.
~$ host 172.217.6.42
42.6.217.172.in-addr.arpa domain name pointer sfo03s08-in-f42.1e100.net.
42.6.217.172.in-addr.arpa domain name pointer sfo03s08-in-f10.1e100.net.
~$ host 74.125.170.201
Host 201.170.125.74.in-addr.arpa. not found: 3(NXDOMAIN)
(1)
~$ whois 74.125.170.201 | fgrep -i google
NetName:        GOOGLE
Organization:   Google Inc. (GOGL)
OrgName:        Google Inc.
OrgAbuseEmail:  network-abuse@google.com
OrgTechName:   Google Inc
OrgTechEmail:  arin-contact@google.com
~$ host 2001:4860:4860::8888
8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.ip6.arpa domain name pointer google-public-dns-a.google.com.

The 1e100.net domain is owned by Google and is a pun of sorts. These PTR records do not provide enough information for us to identify what domain was being queried. The port for most of the connections is 443, the default port for HTTPS. Visiting the IP addresses in a browser results in a redirect to https://www.google.com, but that does not mean much since a single IP address can be routed to multiple services. For example, running dig +short youtube.com on my desktop currently returns the IP address 172.217.6.46, but going to http://172.217.6.46/ takes me to the Google search engine instead of YouTube:

~$ wget -qO- youtube.com | grep -o '<title>.*</title>'
<title>YouTube</title>
~$ dig +short youtube.com
172.217.6.46
172.217.6.46
~$ wget -qO- 172.217.6.46 | grep -o '<title>.*</title>'
<title>Google</title>

I added sendto(2) and recvfrom(2) to the list of monitored syscalls, and I increased the number of characters shown for char [] arguments from the default of 32 to 1,000 using "-s 1000". Arguments for commands working with binary data are usually riddled with octal escape sequences. Using grep(1) and sed(1) to clean up the output made it easier to visually scan through the text:

$ grep -o '"[^"]*"' chromium.strace | sed 's/\\[0-9]\+\|"//g'
[...]
    The X.Org Foundation           wZ\fA!!\
{AABB..ZZ--tt&&'',,,,,,,,>>nnssVVxxyy(())
{AABB..ZZ--tt&&'',,,,,,,,>>nnssVVxxyy(())
2001:4860:4860::8888
X
/var/run/nscd/socket
/var/run/nscd/socket
8DZ(dfghhpQ
10.0.0.1
jwwwgooglecom
X
10.0.0.1
\nclients2googlecom
jwwwgooglecom\f:
10.0.0.1
\nclients2googlecom\f\fclientsl...
10.0.0.1
172.217.6.46
172.217.6.46
X
10.0.0.1
\ttranslate\ngoogleapiscom
ZgaG\f R-Fh +/,/\n}clients2.google.com#\r\fh2http/1.1\v\n\n
\ttranslate\ngoogleapiscom\f:J
10.0.0.1
216.58.195.74
8DZ(dfgd_}9
[...]

The output reflects DNS queries for www.google.com, clients2.google.com and translate.googleapis.com being sent to my router then the returned IP address being used to connect to the host in question. Here are excerpts from the corresponding output of strace(1) for some of those lines:

[...]
connect(88, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.0.1")}, 16) = 0
sendto(88<socket:[46341848]>, "\346\252\1\0\0\1\0\0\0\0\0\0\3www\6google\3com\0\0\1\0\1", 32, MSG_NOSIGNAL, NULL, 0)
[...]
recvfrom(88, "\346\252\201\200\0\1\0\1\0\0\0\0\3www\6google\3com\0\0\1\0\1\300\f\0\1\0\1\0\0\0Q\0\4\330:\300\4", 1024, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.0.1")}, [16])
[...]

DNS queries typically replace periods with bytes representing the length of the following label hence the "\3" (sizeof("www"), sizeof("com")) and "\6" (sizeof("google")) where one might expect "." to be.

Modifying Profile Preferences ¶

I first searched for "clients2" in the Chromium source code, but it appeared around 120 times across 70 files, and I did not see a readily-apparent pattern, so I moved on to translate.googleapis.com. It is the longest and least ambiguous of the three domains discovered earlier which made finding where it is used straightforward:

chromium$ fgrep -IR translate.googleapis.com
components/translate/core/browser/translate_script.cc:    "https://translate.googleapis.com/translate_a/element.js";
components/translate/core/common/translate_util.cc:const char kSecurityOrigin[] = "https://translate.googleapis.com/";
net/http/transport_security_state_static.json:    { "name": "translate.googleapis.com", "include_subdomains": true, "mode": "force-https", "pins": "google" },

On chrome://settings hidden behind the clickable link that reads "Show advanced settings…" is a "Languages" section with a toggle labeled "Offer to translate pages that aren't in a language you read," but I was unable to find a command line option to control this feature. I learned that many of Chromium's settings are stored in the browser profile directory as minified JSON in "./Defaults/Preferences". I made a copy of the existing "Preferences" file, clicked the toggle, restarted the browser then diffed the files to figure out what changed. Since the minified files have every property on a single line with no superfluous whitespace, I filtered them through python -m json.tool which returns sorted, pretty-printed JSON resulting in far more readable diffs:

$ diff -u <(python -m json.tool "$OLD_PREFERENCES_FILE") \
          <(python -m json.tool "$NEW_PREFERENCES_FILE")
--- /dev/fd/63
+++ /dev/fd/62
[...]
@@ -1243,5 +1265,14 @@
                 "startup_urls": "8BB8DBC1D7CA5C58F821C38254FB2B9C874F8EE9B9905B57DE48C731C6C91837"
             }
         }
+    },
+    "spellcheck": {
+        "dictionaries": [
+            "en-US"
+        ],
+        "dictionary": ""
+    },
+    "translate": {
+        "enabled": false
     }
 }

Now that I figured out what needed to be changed, I had to apply the settings before the browser started. This turned out to be pretty easy because values in "./Defaults/Preferences" take precedence over Chromium's defaults if the file already exists when the browser runs for the first time as demonstrated here:

$ USER_DATA_DIR="$(mktemp -d)" && echo "$USER_DATA_DIR"
/tmp/tmp.ZEaa2PemQE
$ cat experiment.json
{
    "translate": {
        "enabled": false
    }
}
$ cp experiment.json "$USER_DATA_DIR/Defaults/Preferences"
$ chromium --user-data-dir="$USER_DATA_DIR"
^C
$ diff -u experiment.json \
          <(python -m json.tool "$USER_DATA_DIR/Defaults/Preferences")
--- experiment.json
+++ /dev/fd/63
@@ -1,4 +1,1254 @@
 {
+    "account_id_migration_state": 2,
+    "account_tracker_service_last_update": "13137537559705370",
[...]
+            }
+        }
+    },
    "translate": {
        "enabled": false
    }

When I repeated this process with strace(1) attached to Chromium, there were no DNS queries for translate.googleapis.com. At some point later on, I noticed that the DNS request for translate.googleapis.com was still being sent with translate.enabled = false but only after visiting chrome://settings. Request prefetching seemed like the most likely culprit, but this same behavior was observed when "Use a prediction service to load page more quickly" is disabled and chrome://dns/ displays "DNS pre-resolution and TCP pre-connection is disabled." How I finally resolved this is discussed in a later section.

redirector.gvt1.com ¶

Spell Checker ¶

I noticed a DNS request for redirector.gvt1.com while reviewing the results of my experiments with the preferences. A request for this domain was sent regardless of whether or not I used my customized preferences, but the request was sent markedly -- about 450 ms -- later than the others making it easy to overlook. Not including unit tests, this domain only appears in one location, ".../spellcheck_hunspell_dictionary.cc". I disabled the spell checker from chrome://settings and observed that this changed browser.enable_spellchecking.

Chromium language settings dialogue

The syscall logs still showed DNS requests being sent for redirector.gvt1.com, so I tried modifying some other properties. Failed attempts to squelch the DNS request included using spellcheck.dictionaries = [] and spellcheck.use_spelling_service = false, a property I saw during a cursory review of the spell checking code.

Upon closer inspection of the syscall logs, I noticed that redirector.gvt1.com also hosted the Chrome Web Store Payments extension which is automatically installed when the browser runs for the first time. The URL comes from a remote server. Had I paid more attention to the test case I ignored earlier, I may have been able to figure out what was going on sooner:

chromium$ xmllint --format chrome/test/data/mac_installer/responseExample.xml \
            | fgrep -C1 redirector.gvt1.com
6-      <urls>
7:          <url codebase="http://redirector.gvt1.com/edgedl/release2/vqrm4ffupbfudxhoaio56oavtexllf94tladghchanr56wmkitmeucwtyehngk441aquxeu0pihv6q5zq1dbh5mk3zd2fnoqhqd/"/>
8-          <url codebase="http://www.google.com/dl/release2/vqrm4ffupbfudxhoaio56oavtexllf94tladghchanr56wmkitmeucwtyehngk441aquxeu0pihv6q5zq1dbh5mk3zd2fnoqhqd/"/>

Chrome Web Store Payments Extension ¶

A number of extensions are automatically registered the first time Chromium runs, behavior that has drawn criticism in the past. This is the result of creating a new profile and enumerating the extensions registered in the preferences:

$ python -m json.tool "$USER_DATA_DIR/Defaults/Preferences" \
    | egrep -o '"(description|path)".*'
"description": "Discover great apps, games, extensions and themes for Chromium.",
"path": "/usr/lib/chromium/resources/web_store",
"description": "Bookmark Manager",
"path": "/usr/lib/chromium/resources/bookmark_manager",
"description": "CryptoToken Component Extension",
"path": "/usr/lib/chromium/resources/cryptotoken",
"description": "Cloud Print",
"path": "/usr/lib/chromium/resources/cloud_print",
"description": "",
"path": "/usr/lib/chromium/resources/pdf",
"description": "Chrome Web Store Payments",
"path": "nmmhkkegccagdldgiimedpiccmgmieda/1.0.0.2_0",

The extensions that have filesystem paths for the "path" property are loaded from packed resources in "/usr/lib/chromium/resources.pak". For example, here are some strings for Cloud Print preferences:

$ strings /usr/lib/chromium/resources.pak | egrep -i -m4 'cloud.?print'
  "name": "Cloud Print",
  "description": "Cloud Print",
      "web_url": "https://www.google.com/cloudprint"
      "https://www.google.com/cloudprint/enable_chrome_connector"

I ignored everything but the Chrome Web Store Payments extension since it is the only one downloaded from a remote path. Chromium supports administrative policy options that can be used to blacklist extensions. The default extensions seem to be exempt from this, so I deleted the extension's folder, ".../Defaults/Extensions/nmmhkkegccagdldgiimedpiccmgmieda". Chromium ran without issue, and it did not download the extension again. This indicated that the browser used information stored in the preferences in lieu of re-reading the extensions files. I began editing the copy of the extension's manifest.json stored in preferences under extensions.settings.nmmhkkegccagdldgiimedpiccmgmieda.manifest. Only a handful of fields are required per the format documentation, so I deleted all of the properties listed as optional then launched Chromium again. The extension was automatically reinstalled by the browser. I took a brute force approach to figuring out which properties could be removed without triggering a reinstallation of the deleted files. I also changed the values of "update_url" and "path" to locations that will not resolve to remote hosts. This was the end result:

"nmmhkkegccagdldgiimedpiccmgmieda": {
    "from_bookmark": false,
    "location": 10,
    "manifest": {
        "description": "Chrome Web Store Payments",
        "display_in_launcher": false,
        "display_in_new_tab_page": false,
        "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrKfMnLqViEyokd1wk57FxJtW2XXpGXzIHBzv9vQI/01UsuP0IV5/lj0wx7zJ/xcibUgDeIxobvv9XD+zO1MdjMWuqJFcKuSS4Suqkje6u+pMrTSGOSHq1bmBVh0kpToN8YoJs/P/yrRd7FEtAXTaFTGxQL4C385MeXSjaQfiRiQIDAQAB",
        "manifest_version": 2,
        "minimum_chrome_version": "1",
        "name": "Chrome Web Store Payments",
        "update_url": "http://0.0.0.1/",
        "version": "0"
    },
    "never_activated_since_loaded": true,
    "path": "/dev/null",
    "running": false,
    "state": 1,
    "was_installed_by_default": true,
    "was_installed_by_oem": false
}

The original value of nmmhkkegccagdldgiimedpiccmgmieda.key must be preserved, but some of the other properties I left may not actually be required. When I launched Chromium using the trimmed extension metadata with my other changes, redirector.gvt1.com no longer appeared in the syscall logs. Further review showed that clients2.google.com was also absent.

Spell Checker and Chrome Web Store Payments Extension ¶

I tested different combinations of changes to the preferences and came to the conclusion that, to eliminate DNS requests for clients2.google.com and redirector.gvt.google.com, all of the following requirements must be met:

translate.googleapis.com ¶

After disabling "Offer to translate pages that aren't in a language you read" (translate.enabled = false in the preferences), translate.googleapis.com still appeared in the syscall logs. I was unable to find any other promising options, so I indiscriminately disabled the domain with '--host-resolver-rules="MAP translate.googleapis.com 0.0.0.1"' added to Chromium's command line arguments. This null routes all requests for translate.googleapis.com. This is less than ideal because it blocks virtually all access to the domain from the browser regardless of the nature and origin of the requests.

I returned to this problem later and read through more of Chromium's code. In "components/translate/core/browser/translate_script.cc", this block of code caught my eye:

75    GURL translate_script_url;
76    // Check if command-line contains an alternative URL for translate service.
77    const base::CommandLine& command_line =
78        *base::CommandLine::ForCurrentProcess();
79    if (command_line.HasSwitch(translate::switches::kTranslateScriptURL)) {
80      translate_script_url = GURL(command_line.GetSwitchValueASCII(
81          translate::switches::kTranslateScriptURL));

Looking for the definition of "kTranslateScriptURL" lead me to "translate_switches.cc" which documents translation-related command line options. The unwanted DNS request still appeared in the syscall logs with "--disable-translate", but using '--translate-security-origin="http://0.0.0.1/"' eliminated them. This is effectively another form of null route that only applies to the translation APIs.

2001:4860:4860::8888 ¶

Figuring out where 2001:4860:4860::8888 was coming from proved to be trickier than I initially expected. The only place that particular string exists is in unit tests:

chromium$ grep -l -R '2001:4860:4860:.*:8888'
remoting/protocol/http_ice_config_request_unittest.cc
chrome/browser/printing/cloud_print/privet_http_unittest.cc
components/favicon/core/fallback_url_util_unittest.cc
net/dns/address_sorter_unittest.cc

The syscall in question looked suspiciously like the beginning of a DNS request:

connect(154, {sa_family=AF_INET6, sin6_port=htons(53), inet_pton(AF_INET6, "2001:4860:4860::8888", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28)

My home network is not configured to use IPv6, and 2001:4860:4860::8888 appears in neither my desktop's "/etc/resolv.conf" nor my router's negotiated nameservers. It seemed unlikely I was going to figure where in Chromium's code the connection was being made using strace(1) alone. I installed Chromium's debug symbols with sudo apt-get install chromium-dbg and started a gdb session with Chromium. I setup a breakpoint for every connect(2) syscall, disabled pagination then started Chromium:

/usr/bin/gdb /usr/lib/chromium/chromium -x /tmp/chromiumargs.uRdQbQ
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
[...]
Reading symbols from /usr/lib/chromium/chromium...Reading symbols from /usr/lib/debug/.build-id/e2/06e3acde7124fca936e2a5b2fed8196cc5fb46.debug...(no debugging symbols found)...done.
(no debugging symbols found)...done.
(gdb) catch syscall connect
Catchpoint 1 (syscall 'connect' [42])
(gdb) set pagination off
(gdb) run
Starting program: /usr/lib/chromium/chromium --disable-client-side-phishing-detection --disable-suggestions-service --no-default-browser-check --no-first-run --safebrowsing-disable-download-protection --user-data-dir=/tmp/tmp.d6Eb4XwPuX --no-sandbox --disable-gpu https://www.codevat.com/
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fffeb3dc700 (LWP 30178)]

After that, I used "bt" to review the call stack followed by "c" to resume execution until the next connect(2) call when I saw nothing of interest in the stack. I repeated these commands several times before I saw this:

(gdb) c
Continuing.
[Thread 0x7fffdd02f700 (LWP 30479) exited]
[...]
[New Thread 0x7fffd8e70700 (LWP 30609)]
[Switching to Thread 0x7fffe1f2e700 (LWP 30475)]

Catchpoint 1 (returned from syscall connect), 0x00007ffff7bcdced in connect () at ../sysdeps/unix/syscall-template.S:81
81      in ../sysdeps/unix/syscall-template.S
(gdb) bt
#0  0x00007ffff7bcdced in connect () at ../sysdeps/unix/syscall-template.S:81
#1  0x0000555557bad228 in net::UDPSocketPosix::InternalConnect(net::IPEndPoint const&) ()
#2  0x0000555557bad140 in net::UDPSocketPosix::Connect(net::IPEndPoint const&) ()
#3  0x0000555557afb0cb in net::HostResolverImpl::IsIPv6Reachable(net::NetLogWithSource const&) ()
[...]
#33 0x00007ffff0c4362d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
(gdb)

Since I thought net::UDPSocketPosix sounded like it was part of generic network library code, I turned my attention to IsIPv6Reachable:

2360  bool HostResolverImpl::IsIPv6Reachable(const NetLogWithSource& net_log) {
2361    // Cache the result for kIPv6ProbePeriodMs (measured from after
2362    // IsGloballyReachable() completes).
2363    bool cached = true;
2364    if ((base::TimeTicks::Now() - last_ipv6_probe_time_).InMilliseconds() >
2365        kIPv6ProbePeriodMs) {
2366      last_ipv6_probe_result_ =
2367          IsGloballyReachable(IPAddress(kIPv6ProbeAddress), net_log);
2368      last_ipv6_probe_time_ = base::TimeTicks::Now();
2369      cached = false;
2370    }

Toward the top of the same file, kIPv6ProbeAddress is defined using an integer array:

  97  // Google DNS address used for IPv6 probes.
  98  const uint8_t kIPv6ProbeAddress[] =
  99      { 0x20, 0x01, 0x48, 0x60, 0x48, 0x60, 0x00, 0x00,
 100        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x88 };

That explained why the string literal was not shown when looking through the code using grep(1); when strace(1) displays certain arguments that are normally represented by non-ASCII bytes, it will convert them to a more human-readable form.

Since IsGloballyReachable, which is called by IsIPv6Reachable on line 2367, does not explicitly send data to the probing address, this code does not actually generate traffic even if the system is configured for IPv6 because UDP is connectionless.

Omnibox ¶

Suggestions ¶

Typing text in the omnibox normally returns auto-completed suggestions which can be disabled by unchecking "Use a prediction service to help complete searches and URLs typed in the address bar" which sets search.suggest_enabled = false in the preferences.

Screenshot demonstrating Chromium search suggestions for the text "kyou"

Implicit Searches ¶

I have never been a fan of implicit address bar searching because I have inadvertently pasted things are not search queries or URLs -- including sensitive pieces of information like passwords -- into the address bar far too many times. There is no option to disable omnibox searching in Chromium, so I work around this by defining a custom search provider that does not map to an external service and making it my default.

Chromium "Search Engines" dialogue showing a custom search

Creating a search engine that translates to the query "file:///%s/" means that anything pasted into the address bar that Chromium is unable to process as a web address will be treated as a file path. In the browser preferences, this is represented by properties like this:

"default_search_provider": {
    "synced_guid": "b12967fd-e7d3-4b8c-83b4-b01964248d75"
},
"default_search_provider_data": {
    "template_url_data": {
        "alternate_urls": [],
        "contextual_search_url": "",
        "created_by_policy": false,
        "date_created": "0",
        "favicon_url": "",
        "id": "7",
        "image_url": "",
        "image_url_post_params": "",
        "input_encodings": [],
        "instant_url": "",
        "instant_url_post_params": "",
        "keyword": "file",
        "last_modified": "0",
        "last_visited": "0",
        "new_tab_url": "",
        "originating_url": "",
        "prepopulate_id": 0,
        "safe_for_autoreplace": false,
        "search_terms_replacement_key": "",
        "search_url_post_params": "",
        "short_name": "File",
        "suggestions_url": "",
        "suggestions_url_post_params": "",
        "synced_guid": "b12967fd-e7d3-4b8c-83b4-b01964248d75",
        "url": "file:///{searchTerms}/",
        "usage_count": 0
    }
}

Additional Tweaks and Details ¶

Background Networking ¶

Several seconds after starting up, Chromium will send DNS requests for a few randomly generated strings. When I first saw these requests, I had a moment of panic because I thought my computer might be infected with malware, a fear echoed by a comment in Chromium bug 47262. Long story short, the behavior is normal. This can be disabled by using the command line option "--disable-background-networking". I had seen the flag before, and I thought it did something along the lines of disabling network requests on unfocused tabs. Its actual effects are much less deleterious, and use of the flag makes some of the other changes mentioned in this article unnecessary. The comment for the option explains that it disables "... several subsystems which run network requests in the background." Based on the results of some grep(1) commands and some light reading, affected features include:

Network Time ¶

The privacy whitepaper has a section on network time where it states at "... random intervals or when Chrome encounters an expired SSL certificate, Chrome may send requests to Google to obtain the time from a trusted source." By grepping for "network.?time", I found the address for the time service defined as "http://clients2.google.com/time/1/current" in "network_time_tracker.cc". The server returns some JSON containing the time:

$ wget -qO- http://clients2.google.com/time/1/current
)]}'
{"current_time_millis":1493518072494,"server_nonce":-3.303237525511677E-272}

I did not actually observe Chromium sending one of these requests even when using libfaketime to adjust the time of day reported to the browser. While trying to figure out how to generate a request, I discovered Chromium's feature framework. Code using the framework can be controlled with command line flags. Network time in particular is disabled with "--disable-features=NetworkTimeServiceQuerying". Since I was unable to determine how to make the browser send a network time request, I used gdb to make sure the command line flag had the desired effect:

(gdb) run
Starting program: /usr/lib/chromium/chromium --disable-features=NetworkTimeServiceQuerying
[...]
^C
Program received signal SIGINT, Interrupt.
0x00007ffff0a30aed in poll () at ../sysdeps/unix/syscall-template.S:81
81      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) p ('base::FeatureList::IsEnabled'(&'network_time::kNetworkTimeServiceQuerying'))
$1 = 0

Printing ¶

The Cloud Print extension will generate DNS requests after clicking "Manage" under "Google Cloud Print" in the advanced settings. The requests I saw were for Privet, a "... Cloud Device Local Discovery API used by cloud services," and they were sent to my workstation's address instead of my router's. Under some circumstances, opening the print dialogue box or clicking the "Change" button will generate traffic when the browser probes available printers. In both cases, I was unable to find a way to completely disable these features. I believe the traffic is restricted to the local host or local network for most configurations, but an HTTPS request was made to a Google IP address while visiting chrome://devices/ when the spell checking was not disabled as mentioned in the Spell Checker section.

Chromium printer selection dialogue

Settings (chrome://settings), Behaviors and Corresponding Preferences ¶

First Run ¶

In addition to using the "--no-first-run" command line option, these preferences can be set to inhibit some behaviors not covered by that flag:

chrome://settings ¶

Wrap-up ¶

Using what I learned, I wrote a wrapper for Chromium that would automatically handle making changes mentioned in this article (chromium.sh), and I also wrote a script that runs Chromium inside of Xephyr with strace(1) to monitor connections (monitor-connections.sh). The connection monitoring script provides an interface for writing simulated user interactions and verifying that no connections are made to unknown hosts. The interface could be used to automate testing of future Chromium releases.