Like on many portable devices with custom operating systems, Android runs on top of Linux. Apps run as separate users, and have very limited visibility to Linux’s underlying pseudo-filesystem.

You may have heard of ‘container’ software on desktop systems, such as the ones below. They allow you to run a local copy of Linux on top of another without using a Virtual Machine:

  • The terminal command chroot
  • The command schroot (aka chroot)
  • Docker (or Kubernetes)

Implementing working containers on Android is much harder, but it is still possible with the PRoot binary. PRoot uses a ‘tracer’, similar to a debugging interface, to simulate root access and system calls. Like the other tools, it also changes the root directory, remaps paths, and converts links. The big difference is that PRoot can be started as a regular user. This is how other apps, available on the Play Store, accomplish the magic of containers on Android.

It is possible to create our own app that does this, but initially it is far easier to try the PRoot binary in another app and experiment with starting a Linux session first. This is a screenshot of the ARMv7 build of Debian Jessie running locally on a tablet — it is not an SSH session to another machine! It’s also important to note that the visible app is only providing the text interface:

Debian Linux in Android Terminal
Debian Linux in Android Terminal

This gives scope, for example, to design an Android app, perhaps on top of Open Source Terminal software like ConnectBot, which has a ‘local’ feature.

Why not Use a VM?

An obvious alternative to using a container would be to run an app that acts like a Virtual Machine, like QEmu. However, running VMs on a mobile device is:

  • Incredibly slow. In my case, the speed was around one-hundredth the speed of the host device, even when the CPU architecture of the host matched the guest. This is because there are no hypervisor extensions or hardware acceleration.
  • Extremely heavy for a lightweight device. A huge block of RAM on the host needs to be reserved for the guest, even if that RAM is not entirely used.
  • Very likely to be terminated by the host OS.

On a portable Android device, in many cases, you can be lucky if the OS even boots, due to excessive timeouts when starting kernel drivers!

Download PRoot and the OS into the Terminal Emulator

To get your own container running, you first need to get hold of PRoot and the desired OS compiled for the correct CPU architecture. This is usually called ‘ARM Hard-Float’ or ‘ARMv7’. The OS images can be obtained from locations such as https://www.armhf.com/. I used the kernel-less version of Debian Jessie, which had the fastest compatible architecture available.

The Terminal interface should then be used to extract the PRoot binaries and the OS to the app’s own files directory.

Sharing the Kernel and Initialising the Guest OS Boot Process

When you run binaries in the guest OS, they are communicating directly or indirectly with the host’s Linux kernel. This means some things will behave a bit differently to normal, such as the network and other hardware features. Unlike on a Virtual Machine, the guest’s kernel is never loaded because it cannot directly talk to the hardware. This means we have to look after some things for the guest so it can properly communicate with the host, such as:

  1. Mapping specific OS folders from the host’s file system into the guest.
  2. Setting up the guest environment to work with the host.
  3. Starting the Linux TTY shell (usually bash) and providing the login prompt.

Mapping the correct folders from the host’s pseudo-filesystem into the guest is critical for the container to work.

Mapping Host Folders into the Guest

Folders linked from the host are usually:

  • /dev (device I/O)
  • /proc (live process/kernel/system information)
  • /sys (system settings)

The remaining folders on the guest will stay as they are. Another issue to note is that we can’t create hardlinks as a standard user under Android. This means we have to start PRoot with the option that converts hardlinks to symlinks on-the-fly.

You can get a basic prompt by doing this, but it won’t be running any processes at all, besides Bash. It helps to handle some of the kernel’s init process yourself, by:

  • Running parts of the start-up script chain from /etc/inittab and the /etc/rc.d folder
  • Manually starting some services

Doing this greatly improves the guest experience.

Services

No services are started when you login for the first time, but this can easily be fixed with a startup script. My script starts resolvconf and ssh before the root user logs in, although SSH isn’t initially used. At the very least, we need the DNS resolver working to be able to fetch and install software from the Internet.

Fixing DNS Resolution

Despite starting the resolvconf service, you’ll only be able to access machines on the Internet by IP address. In order to use DNS, you must add a line to /etc/resolv.conf on the emulated file system:

nameserver 1.1.1.1

Alternatively, use 1.0.0.1, 8.8.8.8, 8.8.4.4, or your own DNS server.

You must now restart either the resolvconf service or the host’s Terminal Emulator app.

Running a Linux Web Server in Android

Assuming the guest interface is working, you can now run web servers like NGINX and Apache on Android. They also shouldn’t be terminated by Android, even when the screen turns off, as the overlay is running as an Android service within the Terminal Emulator. Naturally, that is only true if the service doesn’t use too much RAM.

Here is a screenshot of NGINX’s installation:

Installing NGINX in a Debian Guest
Installing NGINX in a Debian Guest

Network Restrictions

All programs running in this way (with ptrace attached) have some limitations. This consists of:

  • Listening on sockets is only permitted on port 1024 and above, since the Terminal Emulator app isn’t really running as a superuser.
  • No access to other protocols is allowed. This includes ICMP.
  • Lower layer network access is not permitted.

All outbound TCP and UDP network data is permitted.

Note that you also cannot ping hosts, even as root! This is because ICMP packet types have to pass through a special check done by the host kernel. Those checks fail inside a proot — programs running under ptrace cannot use anything that may interfere with network management. That includes ICMP echo requests and replies, even though those types just check for availability:

ICMP Ping in a Debian Guest
ICMP Ping in a Debian Guest