Locked History Actions

namespace

ĮVADAS

Keli pastarieji metai pasižymi „konteinerinių“ sprendimų operacinei sistemai Linux populiarumo augimu. Apie tai, kaip ir kokiais tikslais galima naudoti konteinerius, šiandien daug kalbama ir rašoma. Tuo tarpu mechanizmams, sudarantiems konteinerizacijos pagrindą, skiriama daug mažiau dėmesio. Visų konteinerizacijos instrumentų – Docker, LXC ar systemd-nspawn – pagrindą sudaro dvi Linux branduolio posistemės: namespaces ir cgroups. Šiame referate norėčiau plačiau apžvelgti namespaces (vardų srities) mechanizmą. Pradėsime nuo istorijos. Idėjos, sudarančios vardų srities mechanizmo pagrindą, nėra naujos. Dar 1979 metais į UNIX buvo pridėtas sisteminis iškvietimas chroot() – kaip tik su tikslu suteikti izoliavimo galimybę ir atskirtą nuo pagrindinės sistemos testavimo platformą programuotojams. Pravartu bus prisiminti, kaip jis veikia. O po to apžvelgsime vardų srities mechanizmo funkcionavimo ypatumus šiuolaikinėse Linux sistemose.

Chroot(): pirmasis izoliavimo bandymas

Pavadinimas chroot yra santrumpa nuo change root, kas pažodžiui verčiama kaip „pakeisti šaknį“. Su sisteminio iškvietimo chroot() ir atitinkamos komandos pagalba galima pakeisti šakninį katalogą. Programai, paleistai su pakeistu šakniniu katalogu, bus prieinami tik failai, esantys šiame kataloge. Failų sistema UNIX turi medžio šakninę hierarchiją (1 pav.):

1 pav. Unix failų sistemos hierarchija.

Šios hierarchijos viršūnė yra katalogas /, kuris ir yra root. Visi kiti katalogai – usr, local, bin ir kt. – surišti su juo. Su chroot pagalba į sistemą galima pridėti ir antrą šakninį katalogą, kuris vartotojo atžvilgiu niekuo nesiskirs nuo pirmojo. Failo sistemą, kurioje yra pakeistas šakninis katalogas, galima schemoje pavaizduoti taip (2 pav.):

2 pav. Chroot įtaka.

Failų sistema padalinta į dvi dalis, kurios nedaro jokios įtakos viena kitai. Kaip veikia chroot? Pirma pažiūrėsime pradinį kodą. Kaip pavyzdį paimsime chroot realizavimą OS 4.4 BSD-Lite. Sisteminis iškvietimas chroot aprašytas faile vfs_syscall.c: сhroot(p, uap, retval)

  • struct proc *p; struct chroot_args *uap; int *retval;

{

  • register struct filedesc *fdp = p->p_fd; int error; struct nameidata nd; if (error = suser(p->p_ucred, &p->p_acflag))

    • return (error);

    NDINIT(&nd, LOOKUP, FOLLOW | LOCKLEAF, UIO_USERSPACE, uap->path, p); if (error = change_dir(&nd, p))

    • return (error);

    if (fdp->fd_rdir != NULL)

    • vrele(fdp->fd_rdir);

    fdp->fd_rdir = nd.ni_vp; return (0);

}

Svarbiausia vyksta priešpaskutinėje pateikto fragmento eilutėje: einamoji direktorija tampa šaknine. Linux branduolyje sisteminis iškvietimas chroot realizuotas kiek sudėtingiau:

SYSCALL_DEFINE1(chroot, const char user *, filename) {

  • struct path path; int error; unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;

retry:

  • error = user_path_at(AT_FDCWD, filename, lookup_flags, &path); if (error)

    • goto out;

    error = inode_permission(path.dentry->d_inode, MAY_EXEC | MAY_CHDIR); if (error)

    • goto dput_and_out;
    error = -EPERM; if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT)) . goto dput_and_out;

error = security_path_chroot(&path); if (error)

  • goto dput_and_out;

set_fs_root(current->fs, &path); error = 0;

dput_and_out:

  • path_put(&path); if (retry_estale(error, lookup_flags)) {

    • lookup_flags |= LOOKUP_REVAL; goto retry;
    }

out:

  • return error;

}

Išnagrnėsime chroot veikimo ypatumus praktiniuose pavyzdžiuose. Atliksime tokias komandas:

$ mkdir test

$ chroot test /bin/bash

Atlikus antrą komandą gausime klaidos pranešimą:

chroot: failed to run command ‘/bin/bash’: No such file or directory

Klaida yra ta, kad nebuvo surastas komandinis apvalkalas. Atkreipsime dėmesį į šį svarbų momentą: su chroot pagalba sukursime naują, izoliuotą failų sistemą, kuri neturi jokios prieigos prie einamosios. Bandome dar kartą:

$ mkdir test/bin

$ cp /bin/bash test/bin

$ chroot test chroot: failed to run command ‘/bin/bash’: No such file or directory

Vėl klaida – nežiūrint į tai, kad klaidos pranešimo formuluotė yra tokia pati, pati klaida yra kitokia, nei praeitą kartą. Praeitą klaidą išmetė komandinis apvalkalas, nes nerado reikiamo vykdomojo failo. Antrame gi pavyzdyje apie klaidą pranešė dinaminis linkeris: jis nerado reikiamų bibliotekų. Kad gautume prie jų prieigą, jas irgi reikia nukopijuoti į chroot. Pažiūrėti, kokias būtent dinamines bibliotekas reikia nukopijuoti, galima taip:

$ ldd /bin/bash

  • linux-vdso.so.1 => (0x00007fffd08fa000)

  • libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f30289b2000)

  • libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f30287ae000)

  • libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f30283e8000)

  • /lib64/ld-linux-x86-64.so.2 (0x00007f3028be6000)

Po to atliksime tokias komandas:

$ mkdir test/lib test/lib64

$ cp /lib/x86_64-linux-gnu/libtinfo.so.5 test/lib/

$ cp /lib/x86_64-linux-gnu/libdl.so.2 test/lib/

$ cp /lib64/ld-linux-x86-64.so.2 test/lib64/

$ cp /lib/x86_64-linux-gnu/libc.so.6 test/lib

$ chroot test

bash-4.3#

Dabar rezultatas pasiektas. Pabandysime atlikti naujojoje failų sistemoje, pvz., komandą Is:

bash-4.3# ls

Atsakyme gausime pranešimą dėl klaidos:

bash: ls: command not found

Priežastis aiški: naujojoje failų sistemoje Is komandos nėra. Reikia vėl kopijuoti vykdomąjį failą ir dinamines bibliotekas, kaip jau buvo parodyta aukščiau. Tai ir yra svarbus chroot trūkumas: visus reikiamus failus reikia dubliuoti. Taip pat chroot turi trūkumų ir saugumo atžvilgiu. Bandymai patobulinti chroot mechanizmą ir suteikti patikimesnę izoliaciją buvo atliekami ne kartą: taip atsirado, pvz., tokios žymios technologijos kaip FreeBSD Jail ir Solaris Zones. Linux branduolyje procesų izoliacija buvo patobulinta pridėjus naujas posistemes ir naujus sisteminius iškvietimus. Kai kuriuos iš jų apžvelgsime žemiau.

Vardų srities mechanizmas

Vardų sritys (angl. namespace) - tai Linux branduolio mechanizmas, leidžiantis izoliuoti procesus vienus nuo kitų. Darbai realizuojant jį buvo pradėti branduolio versijoje 2.4.19. Šiam momentui Linux palaiko 6 vardų srities rūšis:

  • PID PID procesus
  • NETWORK Tinklo įrenginius, stekus, jungtis
  • USER Vartotojų ir grupių
  • ID MOUNT Montavimo taškus
  • IPC SystemV IPC, POSIX pranešimų eiles
  • UTS Serverio ir domeno vardas NIS

Visas šias rūšis šiuolaikinės konteinerizacijos sistemos (Docker, LXC ir kt.) naudoja paleisdamos programas.

PID: PID procesų izoliavimas

Istoriškai Linux branduolyje buvo palaikomas tik vienas procesų medis. Procesų medis yra hierarchinė struktūra, panaši į failų sistemos katalogų medį. Su namespaces mechanizmo atsiradimu atsirado galimybė palaikyti kelis procesų medžius, visiškai izoliuotus vieni nuo kitų. Paleidžiant Linux pirma paleidžiamas procesas su identifikaciniu numeriu (PID) 1. Procesų medyje jis yra šakninis. Jis paleidžia kitus procesus ir tarnybas. Namespaces mechanizmas leidžia sukurti atskirą procesų medžio šaką su savo PID 1. Procesas, kuris kuria šią šaką, yra pagrindinio medžio dalis, bet jo dukterinis procesas jau bus šakninis naujame medyje. Naujo medžio procesai niekaip nesąveikauja su motininiu procesu ir net nemato jo. Tuo tarpu pagrindinio medžio procesams prieinami visi dukterinio medžio procesai. Vaizdžiai tai pavaizduota šioje schemoje:

3 pav.

PID sirtis Galima kurti kelis įtrauktus vienas į kitą vardų sričių PID: vienas procesas paleidžia dukterinį procesą naujoje PID vardų srityje, o jis savo ruožtu sukuria naują procesą naujoje srityje. Vienas procesas gali turėti kelis PID identifikatorius (atskiras identifikatorius atskirai vardų sričiai). Naujų PID vardų sričių kūrimui naudojamas sisteminis iškvietimas clone() su parametru CLONE_NEWPID.

Šio parametro pagalba galima paleisti naują procesą naujoje vardų srityje ir naujame medyje. Kaip pavyzdį paimsime nedidelę programėlę, parašytą kalba C:

#define _GNU_SOURCE

#include <sched.h>

#include <stdio.h>

#include <stdlib.h>

#include <sys/wait.h>

#include <unistd.h> static char child_stack[1048576]; static int child_fn() {

  • printf("PID: %ld\n", (long)getpid()); return 0;

} int main() {

  • pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); printf("clone() = %ld\n", (long)child_pid); waitpid(child_pid, NULL, 0); return 0;

}

Sukompiliuosime ir paleisime šią programėlę. Matysime tokį rezultatą:

clone() = 9910 PID: 1

Programos eigoje atsitiko daug įdomaus. Funkcija clone() sukūrė naują procesą, klonavusi einamąjį, ir pradėjo jo atlikimą. Tuo pačiu ji atskyrė naują procesą nuo pagrindinio medžio ir sukūrė jam atskirą procesų medį. Dabar pabandysime pakeisti programos kodą ir sužinoti motininį PID izoliuoto proceso atžvilgiu:

static int child_fn() {

  • printf("Motininis PID: %ld\n", (long)getppid()); return 0;

}

Pakeistos programos išvestis atrodys taip:

clone() = 9985

Motininis PID: 0 Eilutė «Motininis PID: 0» reiškia, kad šis procesas motininio proceso neturi. Įtrauksime į programą dar vieną pakeitimą ir nuimsime parametrą CLONE_NEWPID iš iškvietimo clone():

pid_t child_pid = clone(child_fn, child_stack+1048576, SIGCHLD, NULL);

Sisteminis iškvietimas clone šiuo atveju suveikė beveik taip pat, kaip ir fork () ir tiesiog sukūrė naują procesą. Tačiau fork() ir clone() turi esminį skirtumą, kurį verta aptarti. Fork() kuria dukterinį procesą, kuris yra motininio kopija. Motininis procesas kopijuojamas kartu su visu atlikimo kontekstu: išskirta atmintimi, atvirais failais ir t.t. Skirtingai nuo fork() iškvietimas clone() ne tik sukuria kopiją, bet ir leidžia padalinti atlikimo konteksto elementus tarp dukterinio ir motininio procesų. Aukščiau pateiktame kodo pavyzdyje kartu su funkcija clone naudojamas argumentas child_stack, kuris nustato steko padėtį dukteriniam procesui. Kai tik dukterinis ir motininis procesas gali padalinti atmintį, dukterinis procesas negali būti atliekamas tame pačiame steke, kaip ir motininis. Todėl motininis procesas turi nustatyti atminties sritį dukteriniam ir perduoti nurodymą į jį iškvietime clone(). Dar vienas argumentas, naudojamas su funkcija clone() – tai parametrai, kurie parodo, ką būtent reikia dalinti tarp motininio ir dukterinio procesų. Aukščiau pateiktame pavyzdyje panaudotas parametras CLONE_NEWPID, kuris nurodo, kad dukterinis procesas turi būti sukurtas naujoje PID vardų srityje. Kitų parametrų naudojimo pavyzdžiai bus pateikti žemiau. Taigi izoliavimą procesų lygmenyje mes aptarėme. Bet tai – tik pirmas žingsnis. Atskiroje vardų srityje paleistas procesas vis tiek turės priėjimą prie visų sistemos resursų. Jeigu toks procesas klausys, pvz., 80-tosios jungties, ši jungtis bus užblokuota visiems kitiems procesams. Išvengti tokių situacijų padeda kitos vardų sritys.

NET: tinklų izoliavimas

Vardų srities NET dėka mes galime išskirti izoliuotiems procesams savo tinklo interfeisus. Net loopback-interfeisas kiekvienai vardų sričiai bus atskiras. Tinklo vardų sritis galima kurti sisteminio iškvietimo clone() su parametru CLONE_NEWNET pagalba. Taip pat tai galima atlikti iproute2 pagalba: $ ip netns add netns1 Pasinaudosime strace ir pažiūrėsime, kas atsitiko sistemoje nurodytos komandos atlikimo eigoje:

socket(PF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, 0) = 3

setsockopt(3, SOL_SOCKET, SO_SNDBUF, [32768], 4) = 0

setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1048576], 4) = 0

bind(3, {sa_family=AF_NETLINK, pid=0, groups=00000000}, 12) = 0

getsockname(3, {sa_family=AF_NETLINK, pid=1270, groups=00000000}, [12]) = 0

mkdir("/var/run/netns", 0755) = 0

mount("", "/var/run/netns", "none", MS_REC|MS_SHARED, NULL) = -1 EINVAL (Invalid argument)

mount("/var/run/netns", "/var/run/netns", 0x4394fd, MS_BIND, NULL) = 0

mount("", "/var/run/netns", "none", MS_REC|MS_SHARED, NULL) = 0

open("/var/run/netns/netns1", O_RDONLY|O_CREAT|O_EXCL, 0) = 4 close(4) = 0

unshare(CLONE_NEWNET) = 0

mount("/proc/self/ns/net", "/var/run/netns/netns1", 0x4394fd, MS_BIND, NULL) = 0

exit_group(0) = ? +++ exited with 0 +++

Atkreipsime dėmesį: čia naujos vardų srities kūrimui panaudotas sisteminis iškvietimas unshare(), o ne clone. Unshare() leidžia procesui ar tredui atskirti atlikimo konteksto dalis, bendras su kitais procesais ar tredais. Kaip galima įdėti procesus į naują tinklo vardų sritį Pirma, procesas, sukūręs naują vardų sritį, gali kurti kitus procesus, ir kiekvienas iš šių procesų paveldės motininio proceso tinklo vardų sritį. Antra, branduolyje yra specialus sisteminis iškvietimas setns(). Jo pagalba galima įdėti iškviečiantį procesą arba tredą į reikiamą vardų sritį. Tam reikalingas failų deskriptorius, kuris nurodo šią vardų sritį. Jis saugomas faile /proc/<proceso PID>/ns/net.Atidarę failą mes galime perduoti failų deskriptorių funkcijai setns(). Galima eiti ir kitu keliu. Kuriant naują vardų sritį komandos ip pagalba kuriamas failas direkotorijoje /var/run/netns/. Tam, kad gautume failų deskriptorių, pakanka tiesiog atidaryti šį failą. Tinklo vardų sritį negalima ištrinti jokio sisteminio iškvietimo pagalba. Ji egzistuos, kol ją naudoja bent vienas procesas.

MOUNT: failų sistemos izoliavimas

Izoliavimą failų sistemos lygmenyje jau buvo minėta aukščiau, kai buvo aptariamas sisteminis iškvietimas chroot(). Jau buvo pažymėta, kad sisteminis iškvietimas chroot() nesuteikia reikalingos izoliacijos. Tuo tarpu MOUNT vardų sričių dėka galima kurti visiškai nepriklausomas failų sistemas, asocijuojamas su įvairiais procesais. Failų sistemos izoliavimui naudojamas sisteminis iškvietimas clone() su parametru CLONE_NEWNS:

clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL)

Pirmiausia dukterinis procesas „mato“ tuos pačius jungimosi taškus, kaip ir motininis. Kai tik dukterinis procesas perkeltas į atskirą vardų sritį, prie jo galima prijungti bet kokią failų sistemą, ir tai niekaip nepalies nei motininio proceso, nei kitų vardų sričių.