A network-isolated container in LXD

In this post we see how to get different types of network-isolated containers in LXD. Even if you are not interested in such things, doing this tutorial will help you understand better LXD proxy devices.

LXD container with no networking

To get a LXD container without networking, you omit the networking configuration in the profile that is used to create it. Therefore, we create such a profile and then use it for all our containers that have no networking.

Creating the nonetwork profile

First, we copy the default LXD profile as nonetwork profile, then edit nonetwork to remove the networking bits. We use this profile from now on to create container with no networking support.

$ lxc profile copy default nonetwork
$ lxc profile show nonetwork
config: {}
description: Default LXD profile
devices:
  eth0:
    name: eth0
    nictype: bridged
    parent: lxdbr0
    type: nic
  root:
    path: /
    pool: default
    type: disk
name: nonetwork
used_by: []
$ lxc profile device list nonetwork
root
eth0
$ lxc profile device remove nonetwork eth0
Device eth0 removed from nonetwork
$ lxc profile show nonetwork
config: {}
description: Default LXD profile
devices:
  root:
    path: /
    pool: default
    type: disk
name: nonetwork
used_by: []
$ 

As a side-note, I would like to change the description of the profile to something like Profile without networking. There is no direct command for this yet, and we need to edit the whole configuration with lxc profile edit. To do so, run EDITOR=nano lxc profile edit nonetwork and change the text of the description. Save, exit, and you are done. Here is the final profile for nonetwork.

$ lxc profile show nonetwork
config: {}
description: Profile without networking
devices:
  root:
    path: /
    pool: lxd
    type: disk
name: nonetwork
used_by: []
$ 

Creating a nonetwork container

We can now create a container that uses the nonetwork profile. When we run lxc launch, we specify the nonetwork profile, and use the default ubuntu container image (ubuntu:) which is currently Ubuntu 18.04 LTS. In a few months this will switch to Ubuntu 20.04 LTS. We are happy with any LTS container image. If you wanted to specify specifically Ubuntu 18.04 LTS, replace ubuntu: with ubuntu:18.04. Finally, we give the name withoutnetworking. Once the container is created, we lxc list it to verify there is no IP address and finally we get a shell into it with lxc ubuntu containername.

$ lxc launch --profile nonetwork ubuntu: withoutnetworking
Creating withoutnetworking

The instance you are starting doesn't have any network attached to it.
  To create a new network, use: lxc network create
  To attach a network to an instance, use: lxc network attach

Starting withoutnetworking
$ lxc list withoutnetworking
+-------------------+---------+------+-----------+
|       NAME        |  STATE  | IPV4 |   TYPE    |
+-------------------+---------+------+-----------+
| withoutnetworking | RUNNING |      | CONTAINER |
+-------------------+---------+------+-----------+
$ lxc ubuntu withoutnetworking
ubuntu@withoutnetworking:~$ 

What’s the state of networking in this container? Only loopback is there, no routes.

ubuntu@withoutnetworking:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
ubuntu@withoutnetworking:~$ ip route
ubuntu@withoutnetworking:~$ 

We have created a container without any networking. We can still get a shell into it with lxc exec (or the handy alias lxc ubuntu). We can move files and programs into and out of this container with lxc file push and lxc file pull. By doing so, we can be sure that whatever runs in this container, cannot be communicated through the network.

How to enable networking with SOCKS5

There is no networking in the container but how do we install packages? How can we add networking temporarily in some controlled way? One way is to attach a network device using lxc commands, then remove it. Another is to use a proxy. The benefit with using a proxy is that, depending on your needs, you can switch to one that provides fine-grained control on what is being accessed. For this tutorial, we are using SOCKS5 and a SOCKS5 server running on the host. The container communicates with this proxy server over a LXD proxy device.

Creating the SOCK5 server on the host

We are using a SOCK5 server written in the Go language. Install golang on the host, then run the following command to setup and run the server. Grab this Go file for a minimal SOCKS5 server. The filename is `simplesocke5proxyserver.go`.

$ sudo snap install go                               # Install Go as a snap package.
$ go get github.com/armon/go-socks5                  # Install the Go package for SOCKS5
$ go run simplesocke5proxyserver.go                  # Run this SOCKS5 server.
Listening on 0.0.0.0:10080...
Press Ctrl+C to interrupt: 

Leave this program running as long as you want the proxy server running. This specific server is unauthenticated (anyone can connect), which means that anyone on the local LAN of the host is able to use this service as an open proxy.

To verify that the SOCKS5 server is running, use the following command. It is a curl command that connects to ubuntu.com. The command is successful if you get any output.

$ curl -x socks5h://127.0.0.1:10080/ https://www.ubuntu.com
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty/1.15.8.2</center>
</body>
</html>
$ 

Creating the LXD proxy device

The following command adds a proxy device to the container. The device is called socks5port10080 (arbitrary name). It connects to the loopback interface on port 10080 (where the SOCKS5 server is active) and it listens (binds) for connections on the loopback interface on port 1080. We specify that the listen/bind will happen in the container, hence the connect will be on the host. We need to specify bind=container because if we omit it, the default is bind=host.

$ lxc config device add withoutnetworking socks5port10080 proxy connect=tcp:127.0.0.1:10080 listen=tcp:127.0.0.1:1080 bind=container
Device socks5port10080 added to withoutnetworking 
$ 

To verify that there are open ports on both the host and the container, use ss -tuna (lsof -i cannot see the port!?!). It should show an open port on loopback on the host at port 10080, and an open port in the container on port 1080. Here is how it looks.

$ lxc ubuntu withoutnetworking
ubuntu@withoutnetworking:~$ sudo lsof -i
COMMAND   PID            USER   FD   TYPE DEVICE SIZE NODE NAME
systemd-r 207 systemd-resolve   12u  IPv4 915825       UDP localhost:domain 
systemd-r 207 systemd-resolve   13u  IPv4 915826       TCP localhost:domain (LISTEN)
sshd      275            root    3u  IPv4 914329       TCP *:ssh (LISTEN)
sshd      275            root    4u  IPv6 914340       TCP *:ssh (LISTEN)
ubuntu@withoutnetworking:~$ ss -tuna
Netid     State       Recv-Q      Send-Q            Local Address:Port            Peer Address:Port      
udp       UNCONN      0           0                 127.0.0.53%lo:53                   0.0.0.0:*         
tcp       LISTEN      0           128               127.0.0.53%lo:53                   0.0.0.0:*         
tcp       LISTEN      0           128                     0.0.0.0:22                   0.0.0.0:*         
tcp       LISTEN      0           128                   127.0.0.1:1080                 0.0.0.0:*         
tcp       LISTEN      0           128                        [::]:22                      [::]:*         
ubuntu@withoutnetworking:~$ exit
$ 

Configuring the container to use a SOCKS5 proxy

Get a shell into the container and add the proxy to the APT configuration.

$ lxc ubuntu withoutnetworking
ubuntu@withoutnetworking:~$ echo 'Acquire::http::Proxy "socks5h://localhost:1080/";' | sudo tee /etc/apt/apt.conf.d/12proxy
Acquire::http::Proxy "socks5h://localhost:1080/";
ubuntu@withoutnetworking:~$ 

Now we can use apt in the container. Other parts of the container cannot get access to the network unless they are configured to use a SOCKS5 client. Here we are running apt update. Note that the command mentions the use of the proxy.

ubuntu@withoutnetworking:~$ sudo apt update
0% [Connecting to SOCKS5h proxy (socks5h://localhost:1080)] ...

In case of an error, test with the following. We connect to ubuntu.com using curl and specifying the SOCKS5 proxy directly in the command line. Just like we did earlier on the host.

$ curl -x socks5h://localhost:1080/ https://www.ubuntu.com
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty/1.15.8.2</center>
</body>
</html>
$ 

At this point, the container has access to the Internet only through port 1080 (service SOCKS5). When we terminate the SOCKS5 server, the access if lost.

How to setup a Web server

apt is working from the previous step. Let’s install nginx, then tear apart the SOCKS5 proxy, and finally create a LXD proxy device to access the Web server. By doing so, it is somewhat similar to setting up a firewall in the container (disallow traffic originating from inside the container), without using one.

We install the nginx Web server.

ubuntu@withoutnetworking:~$ sudo apt update
ubuntu@withoutnetworking:~$ sudo apt install nginx -y

Then, on the host we create a LXD proxy device to expose the Web server as port 8880 on the host (you may change this to 80, if there is no Web server already running on that port on the host). We create a proxy device called nonetwebserver, that listens for connections on port 8880 on the host and connects them to port 80 in the container. The service (Web server) is in the container, therefore the LXD proxy device binds/listens on the host (bind=host) in order to connect to the existing, binded, port in the container. We could omit bind=host as it is the default to bind/listen on the host.

ubuntu@withoutnetworking:~$ exit
$ lxc config device add withoutnetworking nonetwebserver proxy listen=tcp:127.0.0.1:8880 connect=tcp:127.0.0.1:80 bind=host
Device nonetwebserver added to withoutnetworking

If you want to expose the Web server to your LAN, then you can replace listen=tcp:127.0.0.1:8880 with listen=tcp:0.0.0.0:8880.

Here is a screenshot of the website. Note that I took the liberty to edit /var/www/html/index.nginx-debian.html as shown below.

Screenshot of the web server running in a LXD container without network connectivity.

You may tear up the SOCKS5 server now. Remove the proxy device and stop the SOCKS5 server by pressing Control+C. The web server (or other service you may setup) will continue to work as long as it does not require connectivity to the Internet.

$ lxc config device remove withoutnetworking socks5port10080
Device socks5port10080 removed from withoutnetworking
$ go run simplesocks5proxyserver.go 
Listening on 127.0.0.1:10080...
Press Ctrl+C to interrupt: ^Csignal: interrupt
$ 

Summary

We have created a LXD container that has no Internet connectivity. We then provided temporary Internet connectivity using a SOCKS5 proxy in order to install the nginx web server. We could have added temporarily a network interface instead, but for the purpose of this tutorial, we went full SOCKS5 proxy. You can replace our SOCKS5 proxy with another that allows to inspect the network traffic in detail.

Doing all these steps is sort of like a poor-man’s firewall. You can assume that we have setup a firewall on the container so that no incoming or outgoing traffic is allowed. A SOCKS5 proxy can selectively bypass the firewall. A proxy device may allow selective incoming traffic to the container.

If none of these interest you, you may replicate this tutorial anyway in order to practice using LXD proxy devices.

Permanent link to this article: https://blog.simos.info/a-network-isolated-container-in-lxd/

5 comments

Skip to comment form

    • Derek on May 16, 2020 at 02:00
    • Reply

    The service is in the container, therefore it binds to the container (bind=host). We could omit bind=host as it is the default to listen on the host.

    Would the first sentence be clearer if you were to replace “it” with “the LXD proxy”?

    1. Thanks! I updated the wording in that section. Please have a look and report back whether it is better or whether it requires more attention.

        • Derek on May 16, 2020 at 14:38
        • Reply

        Thank you! Much clearer. Very good tutorial!

    • Derek on May 17, 2020 at 21:22
    • Reply

    This tutorial is very good! I like that you explicitly explain every step and don’t assume that your readers have any prerequisite knowledge about the topic. Have you considered submitting an article to https://www.wikihow.com/ or writing a larger series of “recipes” that might evolve into a “cookbook” like those that O’Reilly publishes (https://ssearch.oreilly.com/?q=cookbook)?

    1. Thanks! Will look into them.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.