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 readToday, 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:
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
:
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.
# 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:
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:
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
.
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:
[Path]
PathModified=/etc/hosts
[Install]
WantedBy=default.target
[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
.