Accessing a web service running on linux VM from host browser

When I created my website, I quickly realized that I don’t want to always deploy to my VPS right away, without checking how it looks and feels first. I needed a way to preview changes locally, which that led to a simple, yet unintuitive question:

How do you view a website that’s running inside a VM on your own PC?

This post is really more of a note to self than anything, though if you are looking to answer the same question, then feel free to read along anyway.

Sidenote on linux/environment

I use debian as my distro of choice on my host, VM and VPS, which kept things nice and simple in my case. Still, most of what’s written here should apply to other linux distros, though some commands, especially package management might vary.

I run my test environment in VirtualBox, if you’re using KVM or another hypervisor, you can still follow along, but you might need to handle some assumed steps yourself, especially in the case of KVM, where the network configuration is a bit less straight forward (see ANNEX I).

The Problem

My problem was quite simple: Instead of typing IP addresses into the address bar, I just wanted to have something simple, like http://test-server/mywebsite. I’ve also got MariaDB running in my VM, so I’ll need to be able to run queries against the DB in there, as well as be able to SSH in order to do general maintenance tasks. (just some extra fluff, as this website isn’t my only project I’m working on)

Configuring the VM

Quick notes on Bridged Adapter

I’m using a bridged adapter, which means the VM is actually connecting to the host’s physical network interface. If that meant pretty much nothing to you, then you’re not alone. Here’s a breakdown of what that means from a practical point of view:

Inside the VM, this interface should show up as enp0s8, but you should double check with the ip addr command.

Set up the VM to have its own address and talk to the gateway

cat <<EOF | sudo tee /etc/network/interfaces
auto lo
iface lo inet loopback

auto enp0s8
iface enp0s8 inet static
    address 192.168.2.99
    netmask 255.255.255.0
    gateway 192.168.2.254
    dns-nameservers 8.8.8.8 1.1.1.1
EOF

Restart networking when done:

sudo systemctl restart networking

YMMV on what exactly the gateway is, mine is 192.168.2.254 but you should check on your HOST with route -n

This is actually the only piece of configuration needed on the VM, so let’s jump over to the HOST.

Configuring the HOST to talk to the VM

All steps in this section should be done on the HOST machine.

/etc/hosts

We need to teach the HOST how to reach the VM. This is easily done by adding an entry into /etc/hosts.

This is also the time and place to decide what to name the VM: I’ve gone for test-server.

Modify /etc/hosts and add the following line:

192.168.2.99   test-server

This means that test-server should resolve to 192.168.2.99 without even needing a DNS look-up. (you can double check it with a cat /etc/nsswitch.conf | grep hosts command: if files comes first, then it means the system will look inside /etc/hosts for domain name resolutions first)

And that is pretty much it, since we’re basically acting like another device on the network, there’s no need to do anything else.

Going to http://192.168.2.99 on your HOST browser should net you with the apache welcome page, and you should be able to catch yourself in action if you check it with lsof -itcp in the VM:

apache2  1311    www-data   17u  IPv6  17019      0t0  TCP debian-test:http->192.168.2.37:52268 (ESTABLISHED)

Routing traffic between HOST and VM in case the VM is NAT attached

If your VM is NAT attached, you can still do something like this, but there are some extra steps.

First of all, in your VM, your interface will probably be enp0s3, and you do not need (and in fact, should NOT, add a gateway entry, nor a dns-nameserver entry, as the VM itself will act as a gateway. So just leave /etc/network/interfaces as it is in your VM (assuming you’re using NetworkManager).

In NAT attached mode, your VM is hidden from the local network, other than HOST no one else on the LAN can see it. This way, the VM does not have its own IP address, effectively meaning that the VM lives in a private network, so it should have an IP like 10.0.2.x, in my case it is 10.0.2.15.

First of all on HOST’s /etc/hosts we do still want to add a line, but in this instance, we’ll just give localhost an additional new name:

127.0.0.1   test-server

Now, since the VM is hidden from the local network, any services started on the VM will either need to use a different, non-standard port, or we need to find a way to use different ports on the HOST and route the traffic to the standard ports on the VM.

The iptables tool is able to do just that: it allows us to forward traffic from HOST to VM, while also allowing to specify what ports should forward to what.

Executing the following commands should do the trick:

sudo iptables -t nat -A OUTPUT     -p tcp --dport 8080 -d 127.0.0.1 -j DNAT --to-destination 10.0.2.15:80
sudo iptables -t nat -A OUTPUT     -p tcp --dport 10022 -d 127.0.0.1 -j DNAT --to-destination 10.0.2.15:22
sudo iptables -t nat -A OUTPUT     -p tcp --dport 6033 -d 127.0.0.1 -j DNAT --to-destination 10.0.2.15:3306
sudo iptables -t nat -A POSTROUTING -p tcp -d 10.0.2.15 --dport 80 -j MASQUERADE
sudo iptables -t nat -A POSTROUTING -p tcp -d 10.0.2.15 --dport 22 -j MASQUERADE
sudo iptables -t nat -A POSTROUTING -p tcp -d 10.0.2.15 --dport 3306 -j MASQUERADE

Of course there is a lot to unpack here, so let’s break down what these commands actually mean:

How to make the rules permanent?

By default iptables rules vanish on reboot, this can easily be made permanent by installing and using the iptables-persistent package:

sudo apt install iptables-persistent -y
sudo sh -c "iptables-save > /etc/iptables/rules.v4"

Main takeaway for differences between Bridged Adapters and NAT

Bridged Adapter:

NAT:

Conclusion

Using just a few basic unix tools, you can give your test VM a nice name and stop typing IP addresses and ports into your browser.

ANNEX I - KVM how to

In KVM, everything works a bit differently, because it runs in kernel space, unlike VirtualBox which runs entirely in user space, meaning it sits on top of the OS like a regular application. It doesn’t have deep, native access to the Linux kernel networking stack, it has to build its own isolated network. To get a packet out or in, VirtualBox handles all the translation internally, which is also why all the need to manually map ports for almost everything.

With KVM, there’s none of that happening, because it effectively turns the linux host box into a hardware router. Basically, here, as I am using Wi-FI rather than ethernet as kvm typically assumes, I want to be able to create a virtual switch, otherwise standard network bridging completely breaks. The first step is to create a new libvirt network, so create the file /usr/share/libvirt/networks/routed-network.xml with the following contents:

<network>
  <name>routed-wifi</name>
  <bridge name="virbr-wifi" stp="on" delay="0"/>
  <forward mode="nat" dev="wlo1"/>
  <ip address="192.168.100.1" netmask="255.255.255.0">
    <dhcp>
      <range start="192.168.100.2" end="192.168.100.254"/>
    </dhcp>
  </ip>
</network>

If you are using ethernet, then you can stick with the default.xml, and you will also not need the iptables rule that comes later.

The setting outlined above allows for specific IPs to be set for VMs within the VMs themselves, because the entire handshake process happens completely inside the host machine on the virtual switch, it doesn’t matter that if the physical Wi-Fi network blocks bridging, the VM gets its IP locally from the host machine, before any traffic ever attempts to touch the outside world.

Without nat mode, the host would try to pass the VM’s raw, unmasked packets directly onto the host’s Wi-Fi network (wlo1). The Wi-Fi ro uter dropped these packets because it didn’t recognize the VM’s IP or MAC address. Changing the mode to nat forces the host machine to act more like a home router, hiding the VM behind the host’s own legitimate Wi-Fi IP address.

Then, as root, make sure that libvirt is aware of this brand new network definition:

virsh net-define routed-network.xml
virsh net-start routed-wifi
virsh net-autostart routed-wifi

Finally, it is useful to note that the Linux host kernel typically blocks host-to-VM forwarding by default, so we need to turn on IP forwarding net.ipv4.ip_forward=1 (add to /etc/sysctl.d/99-ipforward.conf and run sysctl -p) and add an iptables masquerade rule. The rule is to order the host to stop dropping the VM’s packets and let them out to the internet.

iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o wlo1 -j MASQUERADE

Remember to make the rule persistent. Then still as root, make sure that qemu is also configured accordingly, in /etc/qemu/bridge.conf make sure this network is set to allowed

echo "allow virbr-wifi" >> /etc/qemu/bridge.conf

In the VM, set a custom IP in /etc/network/interfaces:

auto enp1s0
iface enp1s0 inet static
    address 192.168.100.99
    netmask 255.255.255.0
    gateway 192.168.100.1
    dns-nameservers 8.8.8.8 1.1.1.1

If this setting isn’t automatically detected, try adding allow-hotplug enp1s0 under auto. Sometimes network links are only detected after the boot sequence, which will fail to be handled with auto.

Be sure to double check your interface with ip addr and gateway with route -n.

After that, the VM should be reacheable from the host with a custom IP, and the VM should have internet access.