Tema 2

* Publicare:

  • 2023-12-07 08:00

* Termen de predare:

  • 2023-12-22 23:55 - deadline HARD!

Revizii:

  • 2023-12-18 22:10: Actualizat checker, v2023.2: relaxat verificările de la task 2 (acum este nevoie doar de conectivitate IPv6 în cadrul aceleiași rețele – deci merge și fără rutare)!
  • 2023-12-08 12:15: Actualizat checker, v2023.1: acum va face o verificare mai amănunțită – toate serviciile vor fi repornite când rulați --save! Re-citiți rubrica de persistență pentru a vedea cum salvați e.g., regulile iptables!

Tema constă în realizarea configurației unui set de exerciții pe o topologie (vedeți mai jos) simulată folosind containere (implementare în ContainerNet + Docker) într-o mașină virtuală (ori în cloud - OpenStack, ori descărcată și rulată local).

Notare

Fiecare exercițiu are un punctaj propriu. Nota pe întreaga temă este dată de suma punctajelor acumulate în urma rezolvării fiecărui exercițiu.

Punctajul maxim care se poate obține pe întreaga temă este 100 de puncte. Acest punctaj este echivalent cu 2 puncte din nota finală.

Există și exerciții bonus, cu ajutorul cărora puteți obține 125 de puncte.

Nu este obligatorie rezolvarea tuturor exercițiilor. Exercițiile pot fi rezolvate în orice ordine, mai puțin în situația în care un exercițiu depinde de rezolvarea unui alt exercițiu (de obicei, primele 4 de stabilire a conectivității containere – Internet).

Mașina virtuală

Utilizare

  • Puteți rezolva tema ori folosind infrastructura OpenStack (aveți proiect special numit rl_tema_prj - meniu stânga sus, lângă logo OpenStack - unde găsiți și imaginea), ori o mașină virtuală locală (citiți mai jos).
  • Notă: pe OpenStack, autentificarea cu username și parolă nu este posibilă! Folosiți autentificarea cu chei publice, prezentată aici.
  • Pentru autentificare pe VM local, utilizați credențialele student/student.

Pași pentru accesare OpenStack

  • Urmăriți pașii din laboratoare.
  • Va trebui să vă importați cheia publică pe acest proiect nou rl_tema_prj (proiectul de laborator este diferit, rl_prj);
  • Nume imagine (de selectat la Sources): RL Tema2 v2023.0;
  • Tip instanță: m1.small (NU aveți nevoie de mai mult);
  • Puneți la VM un nume care să vă conține prefixul TEMA2_ apoi username-ul vostru de Moodle (dacă veți avea nevoie de ajutor din partea unui asistent, să vă găsim ușor);
  • OBLIGATORIU: autentificarea cu user și parolă a fost dezactivată, folosiți EXCLUSIV chei publice ssh (pe care ar trebui s-o aveți deja configurată în cadrul laboratoarelor).
  • NU modificați parola la conturile root / student! Dacă vă tăiați accesul la VM din greșeală, cereți ajutorul unui asistentului preferat pe Teams ;)

Atenție! NU distrugeți instanța mașinii virtuale până nu ați încărcat arhiva finală pe RL Checker și ați obținut punctajul dorit (și să fiți siguri că nu veți mai avea nevoie să modificați nimic).

Rulare în VM local

  • Pentru a rula mașina virtuală a temei local, o puteți descărca de la acest URL (4.6GB dezarhivat).
  • VM-ul este compatibil atât cu VirtualBox (testat cu 6), cât și VMWare (testat cu Workstation >= 16). Pe Linux, poate fi rulat și prin qemu+kvm.
  • Va trebui să vă creați mașină virtuală nouă în hipervizorul preferat (VMWare / VirtualBox / etc.) și să importați fișierul vmdk (căutați pe Google documentație, pașii diferă în funcție de programul de virtualizare folosit).
  • Accesul prin ssh cu parolă (student:student) este activat, deoarece VM-ul rulează pe o rețea privată.
  • Atenție: Imaginea VM-ului diferă de cea a laboratorului, asigurați-vă că îl folosiți pe cel corect!

Verificarea temei

  • Pentru verificarea temei este disponibil un checker local. Atenție: nu este substitut pentru depanare!
    • Altfel spus, contează soluția voastră, nu checker-ul.
    • Dacă, dintr-o greșeală, checker-ul dă rezulat pozitiv în cazul unui exercițiu rezolvat greșit nu înseamnă că se va puncta. O eventuală actualizare a checker-ul va puncta corect (însă veți fi notificați să re-verificați pe canalele obișnuite, forum și Teams).
    • Obiectivul trebuie să fie rezolvarea corectă a enunțului. Checker-ul vine ca o confirmare (ne dorim cât mai sigură) a acelei rezolvări.
    • Punctajul afișat de RL Checker la final va fi și cel acordat în catalog la final.
  • Play fair: orice tentativă de fraudare / atac la infrastructură va avea ca efect pierderea definitivă a punctajului (sau chiar repetarea materiei, în anumite cazuri), chiar dacă checkerul este păcălit :P.
  • Înainte de a rula checkerul, se recomandă descărcarea ultimului update prin rularea:
    root@host$ t2update
  • Comenzile de mai jos pot fi rulate indiferent de directorul in care ne aflăm.
  • Pentru a actualiza checker-ul:
    t2update
  • Pentru a rula checkerul:
    t2check
  • Exemple de folosire:
    root@host:~# t2check 
    task01       .........................  10.0/10.0
    task02       .........................  10.0/10.0
    task03       .........................  10.0/10.0
    task04       .........................  10.0/10.0
    ...
    
    root@host:~# t2check 10
    task10       .........................  0.0/10.0
       mail not received
  • Dacă sunt probleme, puteți să postați un mesaj pe thread-ul aferent de pe forum;

Predarea temei

  1. Pentru împachetarea soluției, rulați:
    root@host$ t2check --save

    (atenție: această comandă va reseta rețelistica și reporni toate serviciile, echivalent cu un reboot mai rapid – asigurați-vă că ați lucrat persistent (vedeți mai jos, la task-uri, cum)!)

  2. Rezultatul este o arhivă semnată în directorul în care invocați comanda ($PWD).
  3. Folosind utilitarul scp, copiați acest fișier pe stația voastră locală (atenție să nu încurcați directoarele și să copiați o arhivă veche / salvată în altă parte!). Dacă sunteți conectat la OpenStack prin serverul intermediar fep.grid.pub.ro, va trebui să copiați mai întâi acolo, apoi s-o preluați pe stația voastră de lucru (alternativ, puteți folosi funcționalitatea de ProxyCommand a clientului ssh pentru o conexiune directă).
  4. Încărcați arhiva pe RL Checker (Moodle).

Este obligatoriu ca rezolvarea exercițiilor să se facă în mod persistent. La o repornire a mașinii virtuale, rezolvările trebuie să rămână active, altfel puteți întâmpina dificultăți la o revenire ulterioară asupra temei.

Folosiți comanda reboot înainte să testați de-a întregul (checkerul local doar simulează ceva mai rapid).

De asemenea, puteți folosi comanda systemctl restart rl-topology pentru a reporni rapid toate containerele (sistemul lor de fișiere este și el persistent).

Pentru a rezolva tema este suficient să prelucrați fișiere doar din căile /etc, /home și /root (atât pe host, cât și pe containere). NU instalați alte pachete în plus!

NU opriți / ștergeți manual containerele Docker (decât dacă doriți să luați de la zero cu configurarea acestora, vedeți mai jos comanda).

Pentru a șterge și reseta configurația de pe un container, se folosiți secvența:

systemctl stop rl-topology && docker container rm <nume container> && systemctl restart rl-topology;

Personalizarea temei

Rezolvările sunt particularizate pentru fiecare student (pe baza contului Moodle). Pentru a obține aceste date, accesați link-ul Moodle RL Checker.

Atenție: datele de particularizare afișate pe Moodle vor fi introduse şi în fişierul /root/assignment.txt de pe host, unde va trebui să păstrați formatul text VARIABILA=valoare (liniile noi în plus nu contează)! Nu modificați aceste valori, ele fiind verificate cu strictețe de către checker-ul online! Includeți ȘI variabila USERNAME!

Informațiile generate anterior vor fi folosite în enunțul temei cu următoarele notații:

  • $A - valoarea variabilei A
  • $B - valoarea variabilei B
  • $C - valoarea variabilei C

Discuții legate de temă

Toate discuțiile legate de probleme/întrebări/exerciții din tema de RL trebuie puse pe forumul temei. Reguli de utilizare ale acestui forum:

  • Nu se pun rezolvări directe pe forum.
  • Fiecare întrebare legată de un task trebuie pusă pe thread-ul dedicat acelui task.
  • În momentul în care puneți o întrebare, includeți în mesaj:
    • contextul în care a apărut problema pe care o semnalați
    • ce ați încercat să faceți pentru a repara problema
    • alte informații care pot descrie mai bine problema
    • dacă este vorba de rezolvarea unui task, cum ați verificat rezolvarea task-ului
  • Verificați dacă cineva nu a mai întâmpinat aceeași problemă și i-a fost oferit un hint sau o soluție (lurk before you leap).
  • Post-uri care nu sunt puse pe thread-ul corespunzător vor fi șterse.
  • Nu creați thread-uri noi (off-topic) de discuție! Vor fi șterse.

Subiecte

Este recomandat să citiți enunțul în întregime prima oară și de oricâte ori aveți întrebări!. Și, desigur, recitirea completă a task-ului următor de care vă apucați / după o pauză îndelungată de la rezolvarea acestuia.

Topologie & infrastructură

În topologie aveți ruterele host (fiind VM-ul care găzduiește toată infrastructura), Router0 (containerul are denumirea R0) și Router-X (sub ID-ul R-X), PC1, PC2, PC3 conectate la SW (switch – nu veți avea nimic de configurat la el) prin 2 VLAN-uri (10 și 20) și PC-X (rețea cu R-X).

Atenție: din motive tehnice, identificatorii echipamentelor Router0 și Router-X au fost prescurtate la R0 respectiv R-X. Folosiți denumirile prescurtate DOAR ATUNCI când vă conectați la container (folosind comanda go sau comenzi docker – dacă va fi cazul), în rest utilizați denumirea full pentru orice altceva (e.g., alias / hosts).

Fapt divers: VLAN9 se numește rețeaua de pe OpenStack, dacă rulați local, acolo va fi rețeaua hivervizorului (VMWare / VirtualBox) cu mașina fizică.

Va trebui să realizați primele 4 exerciții în ordine. Întrucât aceste exerciții oferă, în final, conectivitate la Internet, restul vor depinde de acestea!

Configurații persistente

Toate configurațiile să fie persistente. Trebuie să fie active și după repornirea mașinii virtuale.

Pentru modificarea unor fișiere speciale din containerele Docker (/etc/hosts, /etc/resolv.conf) va trebui să folosiți un script de init sau hook de ifupdown-ng. Găsiți mențiuni adecvate la exercițiile unde va fi necesar să faceți asta!

Pentru configurație persistentă, atât host-ul, cât și containerele vin cu ifupdown-ng pre-instalat.

Pentru a începe să configurați, verificați fișierele existente la căile /etc/network/interfaces și /etc/network/interfaces.d/* (⇒ RTFM here ;)).

Pentru a configura persistent rute adiționale (și nu numai!), puteți folosi hook-ul up din sintaxa de configurare interfaces. Exemplu (fragment):

iface <intf>
    address A.B.C.D/xx
    up ip route add X.Y.Z.T/yy via Q.W.E.R

Fișierele /etc/network/interfaces* se parsează și execută linie cu linie, scripturile if[up|down] oprindu-se la prima eroare întâlnită. De exemplu, dacă aveți într-o secțiune iface un hook ”up ip ro add invalid route…” și pe rândurile următoare aveți alte declarații, acestea nu vor mai ajunge să fie aplicate!

Puteți folosi următorul oneliner pentru a verifica rapid o interfață:

ifdown --force <intf>; ifup <intf>; ip a sh; ip ro sh
# hint: folosiți ifdown și ifup cu parametrul -a pentru a porni TOATE interfețele declarate!
# ÎNSĂ: NU RULAȚI `ifdown -a` pe host! VĂ VEȚI PIERDE CONECTIVITATEA PE eth0 !!!

Pentru a salva regulile iptables, urmați pașii de aici: https://www.serveracademy.com/courses/linux-fundamentals/how-to-save-iptables-rules-permanently/ (există mai multe modalități, e.g. puteți pune hook-uri de up la o interfață etc.). SUB NICI O FORMĂ SĂ NU INSTALAȚI PACHETELE DESCRISE DIN TUTORIALE (mai ales ifupdown – vă strică VM-ul, aveți deja ifupdown-ng!!).

Dacă folosiți mai multe fișiere în scripturi (e.g., apelați dintr-un script alt script), folosiți căi absolute. Adică folosiți /root/scripts/make-juju.py în loc de ./make-juju.py pentru a nu se baza pe directorul actual de lucru (working directory). NU uitați să le faceți executabile și să includeți shebang-ul!

Ex. 1 [25p] Adresare + rutare IPv4

  1. Configurați cu adrese IPv4 toate legăturile din topologie, astfel:
    • Subnetați în două rețele fixe (de dimensiuni egale) spațiul 10.13.$A.0/24; distribuiți prima subrețea către VLAN10, iar cealaltă lui VLAN20; atribuiți prima adresă asignabilă ruterului (Router0), apoi următoarele în ordinea denumirilor stațiilor (i.e., Router0, PC1, PC3 respectiv Router0, PC2)! Atenție: pe router aveți deja declarate VLAN-urile în fișierul din /etc/network/interfaces.d/!
    • Pentru rețelele host – Router0 și host – Router-X, subnetați optim spațiul 10.11.$B.0/29 și distribuiți-le în ordinea mențiunii din enunț; host trebuie să aibă, mereu, prima adresă asignabilă!
    • Pentru rețeaua Router-X – PC-X: 172.18.$C.64/29; ruterul va avea prima adresă asignabilă, iar stația PC-X PE ULTIMA (atenție: nu broadcast!, cea dinaintea acestuia)!
    • Recomandare: nu modificați nimic pe containerul SW (switch), altfel riscați să vă stricați infrastructura!
  2. Configurați rutarea IPv4 (atât default gateways, cât și rute statice!) astfel încât toate stațiile să se poată accesa unele pe altele prin adresă IP!

Atenție mare: adresa IP de pe eth0 a sistemului host este asignată dinamic de către hipervizor, prin DHCP; NU vă atingeți (never go full ifdown!) de această interfață, altfel riscați să vă pierdeți accesul la VM!

Ex. 2 [15p] Adresare IPv6

  1. Configurați adrese IPv6 pentru rețeaua VLAN10 și VLAN20 (notă: variabila $VLANID va avea valoarea 10, respectiv 20):
    • Folosiți spațiul 2023:E666:$B:$A:$VLANID::/80.
    • Aceeași ordine de mai sus (ca la IPv4): Router0 va avea prima adresă asignabilă, apoi PCn sortate numeric.
  2. Configurați conectivitate IPv6 între Router0 și host:
    • Folosiți spațiul FEC0:5017:$C:$D::/64.
    • Prima adresă asignabilă este pentru host, a doua a lui Router0.
  3. Configurați rutarea IPv6 pentru a permite comunicarea între toate sistemele cu adresă IPv6.
    • Rețeaua Router-X – PC-X NU va avea adresă IPv6 ;)

Ex. 3 [5p] Hosts

  • Realizați configurațiile necesare astfel încât echipamentele să poată fi accesate prin numele lor (folosiți numele host, Router0, PC1, PC2, PC3, Router-X, PC-X - atenție la MAJUSCULE!). Adăugați intrări doar pentru adresele IPv4.

Fișierul /etc/hosts din containere este mai special (montat ca volum --bind de către Docker). Acest lucru face ca orice modificare a acestuia să se piardă la fiecare restart al containerului (aka reboot al VM-ului sau restart al serviciului rl-topology). Dacă (și sigur) vreți să persiste, puteți să-l salvați în altă cale (e.g., /etc/hosts.orig și să-l restaurați mereu când pornește containerul printr-un hook de up în /etc/network/interfaces). Ah, și nu folosiți cp, folosiți cat + redirectare bash (fiind volum, nu puteți șterge + recrea fișierul, trebuie trunchiat & suprascris), e.g.:

iface <intf>
	up cat /etc/hosts.orig >/etc/hosts

Ex. 4 [5p] Internet connectivity

  • Realizați configurațiile necesare pentru ca cele 4 containere sa aibă acces la Internet:
    • Configurați translatarea pe sistemul host astfel încât containerele să poată primi răspunsuri din Internet.
    • Configurați cele 4 containere pentru a putea accesa resurse din Internet pe baza numelor de domeniu al acestora (DNS).
    • Atenție: nu translatați orice pachet este rutat (e.g., cele între containere ar trebui să-și vadă IP-urile originale)!

Fișierul resolv.conf este gestionat ca volum de către Docker :( … aceeași poveste ca la exercițiul anterior ⇒ aceeași soluție (e.g., faceți un fișier /etc/resolv.conf.orig pe care îl veți suprascrie peste /etc/resolv.conf cu un hook pe up din interfaces).

Ex. 5 [10p] Network Address Translation

  • Configurați reguli de NAT pe sistemele host și Router0 (după caz, ori unul ori ambele :P), astfel:
    • Conexiunile pe Router0 la porturile (14000 + $E), (14000 + $F) și (14000 + $G) să conducă la conectarea ssh pe sistemele PC1, PC2 respectiv PC3.
    • Conectarea pe host la portul (6000 + $K) să conducă la conectarea pe tracker-ul de pe sistemul PC-X.
  • Sfat: aveți grijă cum testați: DNAT-ul va funcționa DOAR dacă veniți dintr-o rețea externă ruterului ;)

Ex. 6 [10p] Filtrare pachete (iptables)

  • Configurați filtrarea de pachete pe host și / sau Router0 (după caz) astfel încât:
    • conexiunile SMTP și SSH inițiate de pe sistemul PC3 în afara rețelei lui să fie blocate (inclusiv către alte rutere!);
    • conexiunile către tracker-ul ce rulează pe sistemul PC-X să nu fie permise de la PC1, PC2, PC3.
    • blocați TOATE conexiunile externe (i.e., de pe IP-urile din afara stației) către PC2, mai puțin protocoalele icmp și ssh.
    • atenție: NU blocați conexiunile inițiate de PC2 și nici răspunsurile de la acestea! folosiți reguli stateful (i.e. connection tracking)!

Ex. 7 [10p] Chei SSH

  • Configurați serviciul de SSH pe sistemele PC1, PC2, PC-X și host astfel încât autentificarea cu utilizatorul student de pe oricare sistem să fie permisă pe toate celelate sisteme (tot în cadrul utilizatorului student) folosind chei publice;
  • Folosind alias-uri SSH să fie folosite comenzi simplificate pentru conectarea prin SSH între sistemele menționate mai sus:
    • ssh pc1 să ducă către sistemul PC1 cu utilizatorul student;
    • ssh pc2 să ducă către sistemul PC2 cu utilizatorul student;
    • ssh pc-x să ducă către sistemul PC-X cu utilizatorul student;
    • ssh host să ducă către sistemul host cu utilizatorul student;
  • Observație: ssh-copy-id utilizează autentificare ssh pe bază de parolă pentru a copia cheia. Mașina virtuală pe OpenStack nu permite astfel de autentificare pe host, așadar veți fi nevoiți să autorizați cheia publică manual in fișierul corespunzător!

Atenție: NU VĂ ȘTERGEȚI CHEIA DE LA OPENSTACK DIN FIȘIERUL DE AUTORIZAȚII! Vă veți pierde accesul la VM.. asigurați-vă când editați fișierul că nu modificați acea linie!!

Ex. 8 [10p] Căutare recursivă pe server HTTP cu autentificare

  • Creați, pe sistemul PC-X scriptul /home/student/scripts/pass care să caute recursiv prin paginile de la http://host/.secret/ și să găsească fișierul password.txt (e.g., http://host/.secret/sub/cale/passwords.txt).
  • Scriptul va trebui să afișeze DOAR conținutul fișierului găsit!
  • Va trebui să vă autentificați prin HTTP cu credențialele:
    • username: corina
    • password: RL2023
  • Desigur, trebuie mai întâi să descoperiți ce tip de autentificare să folosiți!

Ex. 9 [10p] Descărcare / încărcare fișiere între servere

  • Pe PC3, realizați un script la /home/student/scripts/send-music care va prelua DOAR fișiere care conțin DOAR MAJUSCULE cu extensia .mp3 (i.e. [A-Z]+\.mp3, litere mici extensia!) din directorul curent (i.e.: $PWD) și le va uploada pe PC1, în destinația /home/fs/music; folosiți credențialele fs:5$4l0m. NU uitați să-l faceți executabil (+ shebang)!
  • Pe host, creați script-ul /root/scripts/copy-firmware care, în primul rând, descarcă DOAR fișierele cu forma [a-zA-Z0-9]+\.bin (caractere alfa-numerice + extensia .bin) de pe stația PC-X din /opt/my-firmware și le încarcă (uploadează) pe stația PC2, în directorul /tmp/firmware-files/ (va trebui să-l creați dacă nu există!).
  • Folosi un utilitar aplicabil protocolului (dintre ftp / wget / curl / scp / rsync), după caz (recomandăm studierea tuturor înainte de a lua o decizie!)!
  • În toate cazurile, aveți de a face DOAR cu fișiere (i.e., transfer non-recursiv); nu le copiați în alte subdidrectoare (trebuie duse/preluate direct la/în calea menționată), altfel acestea nu vor fi văzute de către checker (+ rezolvare incorectă)!
  • Atenție: notația folosită în enunț este PCRE (Perl-Compatible Regular Expressions), însă acest format nu este obligatoriu implementat ca pattern de utilitarele folosite! Citiți documentația / man page-ul utilitarului înainte (atenție la glob vs regex – au diferențe mari de sintaxă)!
  • Pentru testare, puteți ori să vă creați fișierele necesare într-un director temporar, ori să lăsați checkerul să facă acest lucru (automat), într-un director _temporar_ (va trebui să-l căutați, though, dar e evident unde îl va pune ;).
  • Checkerul pentru acest task va refuza să ruleze dacă nu există scripturile / nu sunt executabile (va afișa pur și simplu eroarea skipped)!

Ex. 10 [Bonus - 10p] Wireguard tunnelling

  • Dorim să conectăm rețeaua VLAN10 (cea cu PC1 și PC3, aka un site întreg) la stația PC-X (endpoint).
    • Veți folosi 2 rețele noi: subnetați OPTIM spațiul IPv4 10.66.$J.0/28; asignați prima subrețea către site-ul VLAN10, iar pe cea de-a doua pentru capetele de tunel (Router0 și PC-X) – folosiți convențiile de ordinea ale IP-urilor de la primul exercițiu!
    • Denumiți interfețele de tunel wg-rl la ambele capete.
    • Pe stațiile vizate, toate pachetele destinate către 10.66.$J.0/28 trebuie trecute prin acest tunel!
    • Hint ^: o interfață Linux poate avea mai multe adrese asignate ;) pur și simplu, adăugați mai multe intrări de tipul address în interfaces.
    • Tunelul wireguard trebuie să fie persistent! (folosiți hook-uri în interfaces).
  • Hint: nu uitați să activați rutarea la ameble capete de tunel (unul ar trebui să aibă deja, însă celălalt sigur nu)!

Ex. 11 [Bonus - 15p] Advanced NAT

  • Pe stația PC3 va fi pornit un serviciu special pe portul TCP 999 care va accepta DOAR pachete din rețeaua securizată prin wireguard creată la task-ul anterior (checkerul îl va porni automat; pentru testare puteți folosi nc cu argumentul -l și IP destinație de pe rețeaua WireGuard (+ desigur, portul), trebuie să puteți trimite mesaje bidirecționale prin portul forwardat).
  • Configurați firewallul pe PC-X (aveți iptables instalat) pentru a realiza DNAT astfel încât conexiunile la PC-X portul 666 să ducă către acest serviciu ce rulează pe PC3.
    • Accesul la serviciu prin PC-X:666 va trebui să fie funcțional din orice rețea (e.g., încercați să vă conectați de pe host)!
  • Restricție: este obligatoriu să folosiți DOAR iptables și/sau rutare (care ar trebui să fie deja configurată la ex. anterior) pentru a rezolva acest exercițiu, e.g., nu e voie să folosiți un serviciu auxiliar care să asculte pe portul 666 și să redirecționeze pachetele la PC3!
  • Hint: unvecva ang folosiți tcpdump cu încredere când nu funcționează ceva ;)
rl/teme/tema2.txt · Last modified: 2023/12/20 09:27 by florin.stancu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0