How To Apply ChromeOS's Linux Container's /etc/hosts Outside of the Container in Chrome

Thanks to a little help from Dnsmasq and systemd

Feb 2nd, 2021 · 9 min read

Today, you can run a web application within ChromeOS's Linux container, go to localhost in Chrome, and it will just work. When the ChromeOS team rolled out this feature, it made things more familiar to those coming from other development setups. However, it also is a bit miss-leading.

Seeing that localhost works, someone may also assume that if they modify /etc/hosts within the container, that it'll also just work. This is very much not the case.

Before the localhost feature was added to ChromeOS, the only way to access web applications running within the container was by going to penguin.linux.test. This still works today, but can be problematic for some applications. For example, localhost is special in that it allows some things that are normally only allowed on secure connections. Also, some applications may support running on localhost, but not on other arbitrary domain names.

Today, if you have something running on port 8000 in the Linux container and then go to http://localhost:8000, you'll get what is running within the container. This is a bit misleading though, since you are not actually accessing the parent ChromeOS system, but instead the Linux container.

So, what if you have some application you are working on where you need to run it as http://app.foobar:8000? Many online will just say this isn't possible. Some have some solutions, but they are not as simple as just letting you modify /etc/hosts. Some suggest enabling developer mode, which comes with some big downsides both in terms of security and annoyance.

You want it to just work! What if I told you there is a way?

How To Make Modifying /etc/hosts "Just Work"

The goal is to allow you to modify /etc/hosts anyway you like, then be able to make use of those changes within Chrome with no other action on your part. For the sake of example, we have an application on port 8000 that we want to access by going to http://app.foobar:8000 outside of the Linux container.

What we can do is set up our own DNS server within the container that is aware of what is in /etc/hosts. We then configure ChromeOS to use the DNS server we have running in the container.

1. Add hosts to /etc/hosts

The first obstacle is how to properly modify /etc/hosts. Normally, we'd do something like so:

/etc/hosts
127.0.0.1 app.foobar

However, localhost, or in other words, 127.0.0.1 applies only to within the container.

It turns out that we can get the external IP of the container by querying penguin.lxd with dig.

dig +short penguin.lxd
100.115.92.200

dig is a tool for querying DNS. On Debian, it's a part of the dnsutils package.

sudo apt-get install dnsutils

We now have the external IP of the container. For example, if you go to http://100.115.92.200:8000 in Chrome, you'll see your running application.

So, since our hosts file needs to use IPs the parent ChromeOS system can access, we instead use the above IP instead of localhost:

/etc/hosts
100.115.92.200 app.foobar

2. Use Dnsmasq to make /etc/hosts changes accessible via DNS

Dnsmasq provides a light-weight DNS server (among other things). One lesser-known thing it does by default is read /etc/hosts for the sake of DNS look-ups. This means that any changes we make to /etc/hosts can become queryable by our locally running DNS server.

Setting up dnsmasq is pretty straight forward:

sudo apt-get install dnsmasq

Just by installing it, the service will automatically be enabled and started. We can verify that hosts in /etc/hosts are queryable with dig:

dig +short app.foobar @127.0.0.1
100.115.92.200

The @127.0.0.1 asks dig to explicitly query a given DNS server directly. You'll see above that we get the IP we set earlier in /etc/hosts.

3. Set upstream DNS servers

Since we plan to use our local DNS server in ChromeOS, that also means we still want some way to query domains on the Internet.

You can use any upstream DNS servers you'd like. Personally, I use Cloudflare's DNS. You can also change it to your router's DNS server, but unfortunately that isn't a very portable solution if you switch networks often.

After choosing your DNS servers, you'll need to create /etc/dnsmasq.d/upstream-dns as root.

/etc/dnsmasq.d/upstream-dns
# Upstream Cloudflare DNS servers
server=1.1.1.1
server=1.0.0.1

After restarting Dnsmasq with systemctl restart dnsmasq, you'll then be able to query external domain names.

3. Use Dnsmasq locally for DNS within the container

While technically we don't need dnsmasq within the container, it is nice to have. Apart from just making sure everything is consistent, Dnsmasq lets you write more complex rules than what /etc/hosts allows. For example, if we wanted to create a rule that returns 100.115.92.200 for all of *.foobar, we could create /etc/dnsmasq.d/foobar-rules with the following:

/etc/dnsmasq.d/foobar-rules
address=/foobar/100.115.92.200

I'll leave it to the reader to research more if they are interested.

So, on Linux, DNS servers are configured in /etc/resolv.conf. However, it is almost always managed by some other service. The ChromeOS Linux container is no exception. This means that any changes we make it to it will just be overwritten during the next reboot.

Instead, we will add our local dns server via /etc/dhcp/dhclient.conf. This approach has the added benefit that if a domain name is not found or the Dnsmasq service has been turned off, it'll fall back to the DNS server LXD on ChromeOS runs. This means queries for things like penguin.lxd continue to work and that things will also still work if you shut down Dnsmasq.

echo 'prepend domain-name-servers 127.0.0.1;' | sudo tee -a /etc/dhcp/dhclient.conf > /dev/null

The easiest way to apply this change is to simply reboot your container. Running sudo reboot is a quick way to do this.

After rebooting, you'll see that /etc/resolv.conf now looks something like this:

/etc/resolv.conf
domain lxd
search lxd
nameserver 127.0.0.1
nameserver 100.115.92.193

4. Use the DNS server within the container for all of ChromeOS

Above, we found out that the external IP of our container is 100.115.92.200. We now have a running DNS server. So all we have to do is point ChromeOS to it. Go to settings, Search for "DNS", Click "Custom name servers", then enter 100.115.92.200. Ensure everything else is 0.0.0.0.

ChromeOS DNS Configuration
ChromeOS DNS Configuration

Now, if you go to http://app.foobar:8000 in Chrome, you'll see your app!

Unfortunately, you'll soon realize that adding new entries to /etc/hosts will not automatically apply. Dnsmasq will only read the hosts file upon startup. You'll have to run systemctl restart dnsmasq every time you make a change to /etc/hosts.

That's pretty annoying though. We can do better.

5. Use systemd to automatically restart dnsmasq on host file changes

Thanks to systemd, we can create a user service that runs whenever a file changes with just a couple files:

~/.config/systemd/user/etc-hosts-watcher.path
[Path]
PathModified=/etc/hosts

[Install]
WantedBy=default.target
~/.config/systemd/user/etc-hosts-watcher.service
[Unit]
Description=Watches /etc/hosts and restarts dnsmasq upon changes

[Service]
Type=oneshot
ExecStart=sudo systemctl restart dnsmasq

[Install]
WantedBy=default.target

The above configuration will monitor /etc/hosts for any changes, then execute sudo systemctl restart dnsmasq. Sudo works because these services will run as your personal user, which also has sudo access with no password needed.

Now all you have to do is enable the service so it starts whenever your container starts.

systemctl --user enable etc-hosts-watcher.service
systemctl --user enable etc-hosts-watcher.path
systemctl --user start etc-hosts-watcher.path
systemctl --user start etc-hosts-watcher.service

Now whenever you make changes to /etc/hosts, you'll seamlessly be able to make use of those changes from within Chrome outside of the container!

Pitfalls

There are a few things to keep in mind with this approach.

No DNS upon reboots

The Linux container does not start automatically when you reboot ChromeOS. This means that you will not have a working DNS server until you start a Linux app. Personally, I only configure the local DNS server when I need it. You can always get the correct IP later by running dig +short penguin.lxd.

IPs may change

From what I've seen and understand, the IP of your container will not change between reboots. However, the possibility exists that it does change for one reason or another. If things are not working, simply verify that the IP returned by dig +short penguin.lxd is what you are using in /etc/hosts and for your DNS server in ChromeOS.

Why does it have to be this way?

While this solution is pretty hands-off once you set it up, it's far from user-friendly. Many people using ChromeOS's Linux feature are new to programming and Linux as well. I hope that the ChromeOS team can come up with a more streamlined solution for this one day like they did with localhost.