📜 ⬆️ ⬇️

How to start using User Mode in Linux

Introduction from the translator: Against the background of the mass entry into our life, various kinds of containers can be quite interesting and useful to find out with what technology it all began sometime. Some of them can be usefully applied to this day, but not all of these methods are remembered (or know, if they did not find it during their rapid development). One of these technologies is User Mode Linux. The original author pretty much rummaged, figuring out that the old developments still work, and which is not very good, and collected something like step-by-step instructions on how to get homegrown UML in 2K19 to yourself. And yes, we invited the author of the original post of Cadey to Habr, so if you have questions, ask in English in the comments.

image

Linux user mode is, in fact, the port of the Linux kernel itself. This mode allows you to run a full-fledged Linux kernel as a user process and is usually used by developers to test drivers. But this mode is also useful as a tool for general isolation, the principle of which is similar to the work of virtual machines. This mode provides greater isolation than Docker, but less than a full-fledged virtual machine like KVM or Virtual Box.

In general, User Mode may seem strange and difficult to use tool, but it still has its own areas of application. After all, this is a full-fledged Linux-kernel, working from an unprivileged user. This feature allows you to run potentially unreliable code without any threats to the host machine. And since this is a full-fledged kernel, its processes are isolated from the host machine, that is, processes running inside User Mode will not be visible to the host . This is not similar to the usual Docker-container, in which case the host machine always sees the processes inside the repository. Look at this piece of pstree from one of my servers:
')
containerd─┬─containerd-shim─┬─tini─┬─dnsd───19*[{dnsd}] │ │ └─s6-svscan───s6-supervise │ └─10*[{containerd-shim}] ├─containerd-shim─┬─tini─┬─aerial───21*[{aerial}] │ │ └─s6-svscan───s6-supervise │ └─10*[{containerd-shim}] ├─containerd-shim─┬─tini─┬─s6-svscan───s6-supervise │ │ └─surl │ └─9*[{containerd-shim}] ├─containerd-shim─┬─tini─┬─h───13*[{h}] │ │ └─s6-svscan───s6-supervise │ └─10*[{containerd-shim}] ├─containerd-shim─┬─goproxy───14*[{goproxy}] │ └─9*[{containerd-shim}] └─32*[{containerd}] 

And compare this with the pstree of the Linux kernel in User Mode:

 linux─┬─5*[linux] └─slirp 

When working with Docker containers, I can see from the host the names of the processes that are running in the guest system. With Linux User Mode, this is not possible. What does it mean? This means that monitoring tools that work through the Linux audit system (Linux's auditing subsystem) do not see the processes that are running in the guest system. But in some situations, this feature can become a double-edged sword.

In general, the whole post below is a set of research and crude attempts to achieve the desired result. To do this, I had to use different ancient tools, read the kernel sources, intensively debug code written in the days when I still went to primary school, and also poke around in the Heroku builds using a special binary to find the tools I needed. All this work led to the fact that the guys in my IRC began to call me a sorceress (magic). I hope that this post will serve someone with reliable documentation in order to do the same thing, but with newer kernels and OS versions.

Customization


Setting up a Linux User Mode is done in several steps:


I assume that if you decide to do it all yourself, you will most likely do everything described in some Ubuntu or Debian-like system. I tried to implement all of the above in my favorite distribution, Alpine, but it didn’t work, apparently due to the fact that the Linux kernel has a hard glibc-isms binding for drivers in User Mode. I plan to report this to upstream after I finally deal with the problem.

Installing dependencies on a host


Ubuntu requires at least the following packages to build the Linux kernel (assuming a clean installation):

- 'build-essential'
- 'flex'
- 'bison'
- 'xz-utils'
- 'wget'
- 'ca-certificates'
- 'bc'
- 'linux-headers'


You can install them using the following command (as root or using sudo):

 apt-get -y install build-essential flex bison xz-utils wget ca-certificates bc \ linux-headers-$(uname -r) 

Please note that running the menu customization program for the Linux kernel will require installing libncurses-dev . Please ensure that it is installed using the following command (as root or using sudo):

 apt-get -y install libncurses-dev 

Kernel download


Determine the place to boot and build the kernel. For this operation, you need to allocate about 1.3 GB of hard disk space, so make sure you have it.

Then go to kernel.org and get the URL to download the latest stable version of the kernel. At the time of writing this post is: https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.1.16.tar.xz

Download this file using 'wget' :

 wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.1.16.tar.xz 

And extract it using 'tar' :

 tar xJf linux-5.1.16.tar.xz 

Now we enter the directory created when unpacking the tarball:

 cd linux-5.1.16 

Kernel build setup


The kernel build system is a set of Makefiles with many custom tools and scripts to automate the process. To get started, open the online setup program:

 make ARCH=um menuconfig 

It will partially build and bring you a dialog box. When ' [Select] ' is displayed at the bottom of the window, you can do the adjustment with the Spacebar or the Enter key. Navigating through the window, as usual, the keyboard arrows "up" and "down", and the selection of elements - "left" or "right".

The view index ---> means that you are in a submenu, which you can enter using the Enter key. Exit from it, obviously, through ' [Exit] '.

Include the following parameters in ' [Select] ' and make sure that next to them is the symbol '[*]':

 UML-specific Options: - Host filesystem Networking support (enable this to get the submenu to show up): - Networking options: - TCP/IP Networking UML Network devices: - Virtual network device - SLiRP transport 

Everything, from this window it is possible to leave, consistently selecting ' [Exit] '. Just make sure that at the end you are prompted to save the configuration and select ' [Yes] '.

I recommend that you play around with the kernel build options after reading this post. Thanks to these experiments, you can learn a lot in terms of understanding the work of low-level kernel mechanics and the effect of various flags on its assembly.

Kernel build


The Linux kernel is a large program that does a lot of things. Even with such a minimal configuration on old equipment, its assembly may take a long time. Therefore, build the kernel with the following command:

 make ARCH=um -j$(nproc) 

What for? This command will tell our builder to use all available cores and processor threads during the build process. The $(nproc) at the end of Build substitutes the output of the nproc , which is part of coreutils in the standard Ubuntu build.

After some time, our kernel will be compiled into a ./linux executable file.

Binary installation


Since the Linux User Mode creates a regular binary, you can install it just like any other utility. Here's how I did it:

 mkdir -p ~/bin cp linux ~/bin/linux 

You should also make sure that ~/bin is in your $PATH :

 export PATH=$PATH:$HOME/bin 

Setting up the guest file system


Create a directory for the guest file system:

 mkdir -p $HOME/prefix/uml-demo cd $HOME/prefix 

Open alpinelinux.org and in the download section, find the current download link for MINI ROOT FILESYSTEM . At the time of writing this was:

 http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.0-x86_64.tar.gz 

Download this tarball using wget:

 wget -O alpine-rootfs.tgz http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.0-x86_64.tar.gz 

Now enter the directory of the guest file system and unpack the archive:

 cd uml-demo tar xf ../alpine-rootfs.tgz 

The steps described will create a small file system template. Due to the nature of the system, installing packages through the apk Alpine manager will be extremely difficult. But this FS will be enough to evaluate the general idea.

We will also need the tini tool to prevent the zombie processes from using our guest kernel.

 wget -O tini https://github.com/krallin/tini/releases/download/v0.18.0/tini-static chmod +x tini 

Creating a kernel command line


In the Linux kernel, as in most other programs, there are command line arguments that can be viewed by specifying the key --help .

Himself - help
 linux --help User Mode Linux v5.1.16 available at http://user-mode-linux.sourceforge.net/ --showconfig Prints the config file that this UML binary was generated from. iomem=<name>,<file> Configure <file> as an IO memory region named <name>. mem=<Amount of desired ram> This controls how much "physical" memory the kernel allocates for the system. The size is specified as a number followed by one of 'k', 'K', 'm', 'M', which have the obvious meanings. This is not related to the amount of memory in the host. It can be more, and the excess, if it's ever used, will just be swapped out. Example: mem=64M --help Prints this message. debug this flag is not needed to run gdb on UML in skas mode root=<file containing the root fs> This is actually used by the generic kernel in exactly the same way as in any other kernel. If you configure a number of block devices and want to boot off something other than ubd0, you would use something like: root=/dev/ubd5 --version Prints the version number of the kernel. umid=<name> This is used to assign a unique identity to this UML machine and is used for naming the pid file and management console socket. con[0-9]*=<channel description> Attach a console or serial line to a host channel. See http://user-mode-linux.sourceforge.net/old/input.html for a complete description of this switch. eth[0-9]+=<transport>,<options> Configure a network device. aio=2.4 This is used to force UML to use 2.4-style AIO even when 2.6 AIO is available. 2.4 AIO is a single thread that handles one request at a time, synchronously. 2.6 AIO is a thread which uses the 2.6 AIO interface to handle an arbitrary number of pending requests. 2.6 AIO is not available in tt mode, on 2.4 hosts, or when UML is built with /usr/include/linux/aio_abi.h not available. Many distributions don't include aio_abi.h, so you will need to copy it from a kernel tree to your /usr/include/linux in order to build an AIO-capable UML nosysemu Turns off syscall emulation patch for ptrace (SYSEMU). SYSEMU is a performance-patch introduced by Laurent Vivier. It changes behaviour of ptrace() and helps reduce host context switch rates. To make it work, you need a kernel patch for your host, too. See http://perso.wanadoo.fr/laurent.vivier/UML/ for further information. uml_dir=<directory> The location to place the pid and umid files. quiet Turns off information messages during boot. hostfs=<root dir>,<flags>,... This is used to set hostfs parameters. The root directory argument is used to confine all hostfs mounts to within the specified directory tree on the host. If this isn't specified, then a user inside UML can mount anything on the host that's accessible to the user that's running it. The only flag currently supported is 'append', which specifies that all files opened by hostfs will be opened in append mode. 


This panel covers the main launch parameters. Let's run the kernel with the minimum required set of options:

 linux \ root=/dev/root \ rootfstype=hostfs \ rootflags=$HOME/prefix/uml-demo \ rw \ mem=64M \ init=/bin/sh 

The lines above tell our kernel the following:


Run this command and you should get something like the following:

Another sheet
 Core dump limits : soft - 0 hard - NONE Checking that ptrace can change system call numbers...OK Checking syscall emulation patch for ptrace...OK Checking advanced syscall emulation patch for ptrace...OK Checking environment variables for a tempdir...none found Checking if /dev/shm is on tmpfs...OK Checking PROT_EXEC mmap in /dev/shm...OK Adding 32137216 bytes to physical memory to account for exec-shield gap Linux version 5.1.16 (cadey@kahless) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #30 Sun Jul 7 18:57:19 UTC 2019 Built 1 zonelists, mobility grouping on. Total pages: 23898 Kernel command line: root=/dev/root rootflags=/home/cadey/dl/uml/alpine rootfstype=hostfs rw mem=64M init=/bin/sh Dentry cache hash table entries: 16384 (order: 5, 131072 bytes) Inode-cache hash table entries: 8192 (order: 4, 65536 bytes) Memory: 59584K/96920K available (2692K kernel code, 708K rwdata, 588K rodata, 104K init, 244K bss, 37336K reserved, 0K cma-reserved) SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1 NR_IRQS: 15 clocksource: timer: mask: 0xffffffffffffffff max_cycles: 0x1cd42e205, max_idle_ns: 881590404426 ns Calibrating delay loop... 7479.29 BogoMIPS (lpj=37396480) pid_max: default: 32768 minimum: 301 Mount-cache hash table entries: 512 (order: 0, 4096 bytes) Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes) Checking that host ptys support output SIGIO...Yes Checking that host ptys support SIGIO on close...No, enabling workaround devtmpfs: initialized random: get_random_bytes called from setup_net+0x48/0x1e0 with crng_init=0 Using 2.6 host AIO clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604462750000 ns futex hash table entries: 256 (order: 0, 6144 bytes) NET: Registered protocol family 16 clocksource: Switched to clocksource timer NET: Registered protocol family 2 tcp_listen_portaddr_hash hash table entries: 256 (order: 0, 4096 bytes) TCP established hash table entries: 1024 (order: 1, 8192 bytes) TCP bind hash table entries: 1024 (order: 1, 8192 bytes) TCP: Hash tables configured (established 1024 bind 1024) UDP hash table entries: 256 (order: 1, 8192 bytes) UDP-Lite hash table entries: 256 (order: 1, 8192 bytes) NET: Registered protocol family 1 console [stderr0] disabled mconsole (version 2) initialized on /home/cadey/.uml/tEwIjm/mconsole Checking host MADV_REMOVE support...OK workingset: timestamp_bits=62 max_order=14 bucket_order=0 Block layer SCSI generic (bsg) driver version 0.4 loaded (major 254) io scheduler noop registered (default) io scheduler bfq registered loop: module loaded NET: Registered protocol family 17 Initialized stdio console driver Using a channel type which is configured out of UML setup_one_line failed for device 1 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 2 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 3 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 4 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 5 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 6 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 7 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 8 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 9 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 10 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 11 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 12 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 13 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 14 : Configuration failed Using a channel type which is configured out of UML setup_one_line failed for device 15 : Configuration failed Console initialized on /dev/tty0 console [tty0] enabled console [mc-1] enabled Failed to initialize ubd device 0 :Couldn't determine size of device's file VFS: Mounted root (hostfs filesystem) on device 0:11. devtmpfs: mounted This architecture does not have kernel memory protection. Run /bin/sh as init process /bin/sh: can't access tty; job control turned off random: fast init done / # 


The manipulations above will give us a guest system at a minimum rate, without such things as /proc or assigned to a hostname. For example, try the following commands:

- uname -av
- cat /proc/self/pid
- hostname


To exit the guest system, type exit or press control-d. This will fire the shell, followed by kernel panic:

 / # exit Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000 fish: “./linux root=/dev/root rootflag…” terminated by signal SIGABRT (Abort) 

We got this kernel panic for the reason that the Linux kernel believes that the initialization process is always running. Without it, the system can no longer function and shuts down. But since this is a user mode process, the result is sent by itself to SIGABRT , which results in an exit.

Customize guest network


And here we all begin to go not according to plan. Network in User Mode Linux is where the whole concept of limited “user mode” begins to fall apart. After all, usually at the system level the network is limited to privileged execution modes for all of us understandable reasons.

Note Lane: more about different ways of working with the network in the UML can be read here .

Travel in slirp


However, there is an ancient and practically unsupported tool called Slirp , with which the Linux User Mode can interact with the network. It works approximately like a TCP / IP stack at the user level and does not require any system permissions to run. This tool was released in 1995 , and the latest update dates back to 2006 . Slirp is very old. For a time without support and updates, compilers have gone so far that now this tool can only be described as “code rot” .

So let's roll Slirp from the Ubuntu repositories and try to run it:

 sudo apt-get install slirp /usr/bin/slirp Slirp v1.0.17 (BETA) Copyright (c) 1995,1996 Danny Gasparovski and others. All rights reserved. This program is copyrighted, free software. Please read the file COPYRIGHT that came with the Slirp package for the terms and conditions of the copyright. IP address of Slirp host: 127.0.0.1 IP address of your DNS(s): 1.1.1.1, 10.77.0.7 Your address is 10.0.2.15 (or anything else you want) Type five zeroes (0) to exit. [autodetect SLIP/CSLIP, MTU 1500, MRU 1500, 115200 baud] SLiRP Ready ... fish: “/usr/bin/slirp” terminated by signal SIGSEGV (Address boundary error) 

Oh, gods. Let's install the debugger for Slirp and see if we can figure out what's going on here:

 sudo apt-get install gdb slirp-dbgsym gdb /usr/bin/slirp GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from /usr/bin/slirp...Reading symbols from /usr/lib/debug/.build-id/c6/2e75b69581a1ad85f72ac32c0d7af913d4861f.debug...done. done. (gdb) run Starting program: /usr/bin/slirp Slirp v1.0.17 (BETA) Copyright (c) 1995,1996 Danny Gasparovski and others. All rights reserved. This program is copyrighted, free software. Please read the file COPYRIGHT that came with the Slirp package for the terms and conditions of the copyright. IP address of Slirp host: 127.0.0.1 IP address of your DNS(s): 1.1.1.1, 10.77.0.7 Your address is 10.0.2.15 (or anything else you want) Type five zeroes (0) to exit. [autodetect SLIP/CSLIP, MTU 1500, MRU 1500, 115200 baud] SLiRP Ready ... Program received signal SIGSEGV, Segmentation fault. ip_slowtimo () at ip_input.c:457 457 ip_input.c: No such file or directory. 

Error beats us in this line . Let's look at stacktrace, maybe something will help us there:

 (gdb) bt full #0 ip_slowtimo () at ip_input.c:457 fp = 0x55784a40 #1 0x000055555556a57c in main_loop () at ./main.c:980 so = <optimized out> so_next = <optimized out> timeout = {tv_sec = 0, tv_usec = 0} ret = 0 nfds = 0 ttyp = <optimized out> ttyp2 = <optimized out> best_time = <optimized out> tmp_time = <optimized out> #2 0x000055555555b116 in main (argc=1, argv=0x7fffffffdc58) at ./main.c:95 No locals. 

Here we see that a crash occurs during the start of the main loop, when slirp tries to check timeouts. It was at this point that I had to give up debugging attempts. But let's see if Slirp, assembled from sorts, works. I re-uploaded the archive directly from the Sourceforge site, because dragging something from there via the command line is a pain:

 cd ~/dl wget https://xena.greedo.xeserv.us/files/slirp-1.0.16.tar.gz tar xf slirp-1.0.16.tar.gz cd slirp-1.0.16/src ./configure --prefix=$HOME/prefix/slirp make 

Here we see alerts about undefined built-in functions, that is, about the impossibility of linking the resulting binary file. It seems that between 2006 and this moment gcc stopped creating symbols used in built-in functions of intermediately compiled files. Let's try replacing the inline with an empty comment and look at the result:

 vi slirp.h :6 a <enter> #define inline /**/ <escape> :wq make 

Nah That doesn't work either. Still can not find the characters of these functions.

At this stage, I gave up and started looking for Heroku build packages on Github. My theory was based on the fact that some Heroku package would contain the binaries I needed. As a result, the search led me here . I downloaded and unpacked uml.tar.gz and found the following:

 total 6136 -rwxr-xr-x 1 cadey cadey 79744 Dec 10 2017 ifconfig* -rwxr-xr-x 1 cadey cadey 373 Dec 13 2017 init* -rwxr-xr-x 1 cadey cadey 149688 Dec 10 2017 insmod* -rwxr-xr-x 1 cadey cadey 66600 Dec 10 2017 route* -rwxr-xr-x 1 cadey cadey 181056 Jun 26 2015 slirp* -rwxr-xr-x 1 cadey cadey 5786592 Dec 15 2017 uml* -rwxr-xr-x 1 cadey cadey 211 Dec 13 2017 uml_run* 

This is a slirp binary file! Does he work?

 ./slirp Slirp v1.0.17 (BETA) FULL_BOLT Copyright (c) 1995,1996 Danny Gasparovski and others. All rights reserved. This program is copyrighted, free software. Please read the file COPYRIGHT that came with the Slirp package for the terms and conditions of the copyright. IP address of Slirp host: 127.0.0.1 IP address of your DNS(s): 1.1.1.1, 10.77.0.7 Your address is 10.0.2.15 (or anything else you want) Type five zeroes (0) to exit. [autodetect SLIP/CSLIP, MTU 1500, MRU 1500] SLiRP Ready ... 

It does not fall - so it should work! Let's drop this binary in ~/bin/slirp :

 cp slirp ~/bin/slirp 

In case the creator of the package removes it, I made a mirror .

Network configuration


Now let's configure the network on our guest core. Update startup parameters :

 linux \ root=/dev/root \ rootfstype=hostfs \ rootflags=$HOME/prefix/uml-demo \ rw \ mem=64M \ eth0=slirp,,$HOME/bin/slirp \ init=/bin/sh 

Now let's turn on the network:

 mount -t proc proc proc/ mount -t sysfs sys sys/ ifconfig eth0 10.0.2.14 netmask 255.255.255.240 broadcast 10.0.2.15 route add default gw 10.0.2.2 

The first two configuration commands, /proc and /sys required for the operation of ifconfig , which sets up a network interface for communicating with Slirp. The route command sets the kernel's routing table to force all traffic through the Slirp tunnel. Let's check it with a DNS query:

 nslookup google.com 8.8.8.8 Server: 8.8.8.8 Address 1: 8.8.8.8 dns.google Name: google.com Address 1: 172.217.12.206 lga25s63-in-f14.1e100.net Address 2: 2607:f8b0:4006:81b::200e lga25s63-in-x0e.1e100.net 

Works!

Note: The initial post seems to have been written on the desktop with a wired network card, or some other configuration that does not require additional drivers. On a laptop with WiFi 8265 from Intel, an error occurs when you raise the network

 / # ifconfig eth0 10.0.2.14 netmask 255.255.255.240 broadcast 10.0.2.15 slirp_tramp failed - errno = 2 ifconfig: ioctl 0x8914 failed: No such file or directory / # 

Apparently, the kernel cannot communicate with the network driver. Attempt to compile the firmware in the kernel situation, unfortunately, did not fix it. At the time of publication, it was not possible to find a solution in this configuration. On simpler configs (for example, in Virtualbox), the interface is raised correctly.

Let's automate redirection with the following shell script:

 #!/bin/sh # init.sh mount -t proc proc proc/ mount -t sysfs sys sys/ ifconfig eth0 10.0.2.14 netmask 255.255.255.240 broadcast 10.0.2.15 route add default gw 10.0.2.2 echo "networking set up" exec /tini /bin/sh 

And we note its executable:

 chmod +x init.sh 

And then we make changes to the kernel command line:

 linux \ root=/dev/root \ rootfstype=hostfs \ rootflags=$HOME/prefix/uml-demo \ rw \ mem=64M \ eth0=slirp,,$HOME/bin/slirp \ init=/init.sh 

And repeat:

 SLiRP Ready ... networking set up /bin/sh: can't access tty; job control turned off nslookup google.com 8.8.8.8 Server: 8.8.8.8 Address 1: 8.8.8.8 dns.google Name: google.com Address 1: 172.217.12.206 lga25s63-in-f14.1e100.net Address 2: 2607:f8b0:4004:800::200e iad30s09-in-x0e.1e100.net 

The network is stable!

Docker file


, Dockerfile , . , , . , .



, , . - , , User Mode Linux . . Docker — tar-, docker export , . , shell-.

Rkeene #lobsters Freenode. Slirp . , Slackware slirp, Ubuntu Alpine slirp Rkeene . , -.

Source: https://habr.com/ru/post/459558/


All Articles