Penampung Linux dalam beberapa baris kode

Sebagai kelanjutan dari artikel sebelumnya di KVM, kami menerbitkan terjemahan baru dan memahami cara kerja container menggunakan contoh menjalankan image busybox Docker.


Artikel tentang container ini merupakan lanjutan dari artikel sebelumnya di KVM. Saya ingin menunjukkan dengan tepat bagaimana container bekerja dengan menjalankan image Docker busybox di container kecil kita sendiri.



Tidak seperti mesin virtual, container sangat kabur dan tidak jelas. Apa yang biasanya kita sebut kontainer adalah paket kode yang berdiri sendiri dengan semua dependensi yang diperlukan yang dapat dikirim bersama dan dijalankan dalam lingkungan yang terisolasi di dalam sistem operasi host. Jika menurut Anda ini adalah deskripsi mesin virtual, mari selami lebih dalam dan lihat bagaimana container diimplementasikan.



BusyBox Docker



Tujuan utama kami adalah menjalankan image busybox biasa untuk Docker, tetapi tanpa Docker. Docker menggunakan btrfs sebagai filesystem untuk image-nya. Mari kita coba mengunduh gambar dan mengekstraknya ke dalam direktori:



mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -


Sekarang kita memiliki filesystem image busybox yang telah diekstrak ke dalam folder rootfs . Tentu saja, Anda dapat menjalankan ./rootfs/bin/sh dan mendapatkan shell yang berfungsi, tetapi jika kita melihat daftar proses, file, atau antarmuka jaringan, kita dapat melihat bahwa kita memiliki akses ke seluruh OS.



Jadi mari kita coba membuat lingkungan yang terisolasi.



Klon



Karena kita ingin mengontrol akses ke proses anak, kita akan menggunakan clone (2), bukan fork (2) . Clone melakukan hal yang hampir sama, tetapi mengizinkan flag untuk diteruskan, yang menunjukkan resource mana yang ingin Anda bagikan (dengan host).



Bendera berikut diperbolehkan:



  • CLONE_NEWNET - perangkat jaringan yang terisolasi
  • CLONE_NEWUTS - host dan nama domain (sistem berbagi waktu UNIX)
  • CLONE_NEWIPC - Objek IPC
  • CLONE_NEWPID - pengidentifikasi proses (PID)
  • CLONE_NEWNS - titik pemasangan (sistem file)
  • CLONE_NEWUSER - pengguna dan grup.


Dalam percobaan kami, kami akan mencoba mengisolasi proses, IPC, jaringan dan sistem file. Jadi ayo mulai:



static char child_stack[1024 * 1024];

int child_main(void *arg) {
  printf("Hello from child! PID=%d\n", getpid());
  return 0;
}

int main(int argc, char *argv[]) {
  int flags =
      CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
  int pid = clone(child_main, child_stack + sizeof(child_stack),
                  flags | SIGCHLD, argv + 1);
  if (pid < 0) {
    fprintf(stderr, "clone failed: %d\n", errno);
    return 1;
  }
  waitpid(pid, NULL, 0);
  return 0;
}


Kode harus dijalankan dengan hak superuser, jika tidak, kloning akan gagal.



Eksperimen ini memberikan hasil yang menarik: child PID adalah 1. Kami sangat menyadari bahwa proses init biasanya memiliki PID 1. Namun dalam kasus ini, proses anak mendapatkan daftar prosesnya sendiri yang terisolasi, yang menjadi proses pertama.



Bekerja shell



Untuk mempermudah mempelajari lingkungan baru, mari kita mulai shell dalam proses anak. Mari kita jalankan perintah sewenang-wenang seperti docker run :



int child_main(void *arg) {
  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


Sekarang meluncurkan aplikasi kita dengan argumen / bin / sh membuka shell nyata tempat kita dapat memasukkan perintah. Hasil ini membuktikan betapa salahnya kami ketika berbicara tentang isolasi:



# echo $$
1
# ps
  PID TTY          TIME CMD
 5998 pts/31   00:00:00 sudo
 5999 pts/31   00:00:00 main
 6001 pts/31   00:00:00 sh
 6004 pts/31   00:00:00 ps


Seperti yang bisa kita lihat, proses shell itu sendiri memiliki PID 1, tetapi, pada kenyataannya, ia dapat melihat dan mengakses semua proses lain dari OS utama. Alasannya adalah daftar proses dibaca dari procfs , yang masih diwarisi.



Jadi, lepaskan procfs :



umount2("/proc", MNT_DETACH);




Sekarang perintah ps , mount dan lainnya berhenti saat memulai shell , karena procfs belum dipasang. Namun, ini masih lebih baik daripada kebocoran procfs induk.



Chroot



Biasanya chroot digunakan untuk membuat direktori root , tetapi kami akan menggunakan alternatif pivot_root . Panggilan sistem ini memindahkan root sistem saat ini ke subdirektori dan menetapkan direktori berbeda ke root:



int child_main(void *arg) {
  /* Unmount procfs */
  umount2("/proc", MNT_DETACH);
  /* Pivot root */
  mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");
  mkdir("./rootfs/oldrootfs", 0755);
  syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");
  chdir("/");
  umount2("/oldrootfs", MNT_DETACH);
  rmdir("/oldrootfs");
  /* Re-mount procfs */
  mount("proc", "/proc", "proc", 0, NULL);
  /* Run the process */
  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


Masuk akal untuk me-mount tmpfs ke / tmp , sysfs ke / sys dan membuat filesystem / dev yang valid , tetapi saya akan melewatkan langkah ini untuk singkatnya.



Sekarang kita hanya melihat file dari image busybox, seolah-olah kita menggunakan chroot :



/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var

/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    4 root      0:00 ps

/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps ax


Saat ini, wadah tersebut terlihat cukup terisolasi, bahkan mungkin terlalu banyak. Kami tidak dapat melakukan ping apa pun dan jaringan sepertinya tidak berfungsi sama sekali.



Jaringan



Membuat namespace jaringan baru hanyalah permulaan! Anda perlu menetapkannya antarmuka jaringan dan mengkonfigurasinya untuk meneruskan paket dengan benar.



Jika Anda tidak memiliki antarmuka br0, Anda perlu membuatnya secara manual (brctl adalah bagian dari paket bridge-utils di Ubuntu):



brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0  -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE


Dalam kasus saya, wlp3s0 adalah antarmuka jaringan WiFi utama dan 172.16.xx adalah jaringan untuk container tersebut.



Peluncur kontainer kami perlu membuat sepasang antarmuka, veth0 dan veth1, mengaitkannya dengan br0, dan menyiapkan perutean dalam penampung.



Dalam fungsi main () , kita akan menjalankan perintah ini sebelum mengkloning:



system("ip link add veth0 type veth peer name veth1");
system("ip link set veth0 up");
system("brctl addif br0 veth0");


Saat panggilan ke clone () berakhir, kami menambahkan veth1 ke namespace anak baru:



char ip_link_set[4096];
snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",
         pid);
system(ip_link_set);


Sekarang jika kita menjalankan ip link di shell kontainer, kita akan melihat antarmuka loopback dan beberapa antarmuka veth1 @ xxxx. Tetapi jaringan masih tidak berfungsi. Mari tetapkan nama host unik di penampung dan konfigurasikan rute:



int child_main(void *arg) {

  ....

  sethostname("example", 7);
  system("ip link set veth1 up");

  char ip_addr_add[4096];
  snprintf(ip_addr_add, sizeof(ip_addr_add),
           "ip addr add 172.16.0.101/24 dev veth1");
  system(ip_addr_add);
  system("route add default gw 172.16.0.100 veth1");

  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


Mari kita lihat tampilannya:



/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff

/ # hostname
example

/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...


Bekerja!



Kesimpulan



Kode sumber lengkap tersedia di sini . Jika Anda menemukan bug atau punya saran, silakan tinggalkan komentar!



Tentu saja, Docker dapat melakukan lebih banyak lagi! Tapi sungguh menakjubkan betapa banyak API yang cocok yang dimiliki kernel Linux dan betapa mudahnya menggunakannya untuk mencapai virtualisasi tingkat OS.



Semoga Anda menikmati artikelnya. Anda dapat menemukan proyek penulis di Github dan mengikuti Twitter untuk mengikuti berita, serta melalui rss .



All Articles