computer-networks

Capitolul 2 - Application Layer

Cuprins

Introducere

În cadrul acestui capitol folosim orchestratia de containere definită aici. Pentru a rula această orchestrație, este suficient să executăm:

cd computer-networks/capitolul2
docker network prune
docker compose up -d

Domain Name System

alt text

Folosim DNS pentru a afla IP-urile corespunzătoare numelor. În general numele sunt (Fully Qualified Domain Names) salvate cu un punct în plus la sfârșit.

În linux și macOS există aplicația dig cu care putem interoga entries de DNS. Pe windows există aplicația nslookup.

Puteți rula exemplele de mai jos dintr-un container docker docker compose exec rt1 bash.

#1. cele 13 root servers de DNS:
dig 
;; ANSWER SECTION:
.     18942 IN  NS  m.root-servers.net.
.     18942 IN  NS  a.root-servers.net.
.     18942 IN  NS  b.root-servers.net.
.     18942 IN  NS  c.root-servers.net.
.     18942 IN  NS  d.root-servers.net.
.     18942 IN  NS  e.root-servers.net.
.     18942 IN  NS  f.root-servers.net.
.     18942 IN  NS  g.root-servers.net.
.     18942 IN  NS  h.root-servers.net.
.     18942 IN  NS  i.root-servers.net.
.     18942 IN  NS  j.root-servers.net.
.     18942 IN  NS  k.root-servers.net.
.     18942 IN  NS  l.root-servers.net.
.     18942 IN  NS  m.root-servers.net.

#2. facem request-uri iterative pentru a afla adresa IP corespunzatoare lui fmi.unibuc.ro
dig @a.root-servers.net fmi.unibuc.ro

;; AUTHORITY SECTION:
ro.     172800  IN  NS  sec-dns-b.rotld.ro.
ro.     172800  IN  NS  dns-c.rotld.ro.
ro.     172800  IN  NS  dns-at.rotld.ro.
ro.     172800  IN  NS  dns-ro.denic.de.
ro.     172800  IN  NS  primary.rotld.ro.
ro.     172800  IN  NS  sec-dns-a.rotld.ro.

#3. interogam un nameserver responsabil de top-level domain .ro
dig @sec-dns-b.rotld.ro fmi.unibuc.ro

;; QUESTION SECTION:
fmi.unibuc.ro.     IN  A

;; AUTHORITY SECTION:
unibuc.ro.    86400 IN  NS  ns.unibuc.ro.

;; ADDITIONAL SECTION:
ns.unibuc.ro.   86400 IN  A 80.96.21.3


#am aflat de la @sec-dns-b.rotld.ro ca ns.unibuc.ro se gaseste la adresa: 80.96.21.3
#4. trimitem un ultim mesaj:
mesaj = "hey, ns.unibuc.ro, care este adresa IP pentru numele fmi.unibuc.ro?"
IP dst:  80.96.21.3 (ns.unibuc.ro)
PORT dst: aplicația de pe portul: 53 - constanta magică (vezi IANA și ICANN)

#cand fac cererea, deschid un port temporar (47632)
#pentru a primi inapoi raspunsul DNS destinat aplicatiei care a făcut cererea

Interogări către serverul DNS 8.8.8.8 de la google.

# interogam serverul 8.8.8.8 pentur a afla la ce IP este fmi.unibuc.ro
dig @8.8.8.8 fmi.unibuc.ro

; <<>> DiG 9.10.3-P4-Ubuntu <<>> @8.8.8.8 fmi.unibuc.ro
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16808
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;fmi.unibuc.ro.         IN  A

;; ANSWER SECTION:
fmi.unibuc.ro.      12925   IN  A   193.226.51.15

;; Query time: 39 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed May 13 13:29:13 EEST 2020
;; MSG SIZE  rcvd: 58

DNS stochează nu doar informații despre IP-ul corespunzător unui hostname, ci există mai multe tipuri de intrări (record types) în baza de date:

Protocolul pentru DNS lucrează la nivelul aplicației și este standardizat pentru UDP, port 53. Acesta se bazează pe request-response iar în cazul în care nu se primesc răspunsuri după un număr de reîncercări (de multe ori 2), programul anunță că nu poate găsi IP-ul pentru hostname-ul cerut (“can’t resolve”). Headerul protocolului este definit aici.

HTTP/S requests

alt text

Intrați în browser și deschideți Developer Tools (de obicei, apăsând tasta F12). Accesați pagina https://fmi.unibuc.ro și urmăriți în tabul Network cererile HTTP.

import requests
from bs4 import BeautifulSoup

# dictionar cu headerul HTTP sub forma de chei-valori
headers = {
    "Accept": "text/html",
    "Accept-Language": "en-US,en",
    "Cookie": "__utmc=177244722",
    "Host": "fmi.unibuc.ro",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36"
}

response = requests.get('https://fmi.unibuc.ro', headers=headers)
print (response.text[:200])

# proceseaza continutul html
supa = BeautifulSoup(response.text)

# cauta lista cu clasa ultimeleor postari
div = supa.find('ul', {'class': 'wp-block-latest-posts__list'})

print(div.text)

HTTP server

Executați următoarele comenzi pe un server AWS sau pe calculatorul vostru personal.

flask

Flask este un framework pentru web simplist și minimal pentru python.

# instalam flask
pip install flask
# ne mutam in directorul unde se gaseste simple_flask.py
cd computer-networks/capitolul2/src
# executam scriptul
python3 simple_flask.py

fastapi

fastapi este un alt framework gândit pentru apeluri HTTP asincrone. Executarea aplicației este preferat a se face prin-un ASGI Asynchronous Server Gateway Interface, în cazul nostru vom folosu uvicorn.

# instalam depedințele
pip install "fastapi[all]"
# daca variabila de environment PATH nu contine /home/$USER/bin
# trebuie adaugat manual
export PATH=$PATH:~/.local/bin/
# ne mutam in directorul unde se gaseste simple_flask.py
cd computer-networks/capitolul2/src
# executăm aplicația cu uvicorn
uvicorn simple_fastapi:app --reload --host 0.0.0.0 --port 8080

Exercițiu Port Forwarding

  1. Verificați cu provider-ul de internet dacă puteți obține un nume de DNS dinamic. De ex, la digi puteți obține un subdomeniu pe go.ro care să trimită de fiecare dată către router-ul vostru de acasă.
  2. Dacă nu aveți opțiune de DNS dinamic, verificați dacă puteți face configurări la routerul vostru pentru IP-ul public pe care îl primiți.
  3. Configurați routerul să facă Port forwarding. Asta presupune următorii pași:
    • asignați calculatorului de acasă același IP în funcție de adresa fizică (ex. pt 192.168.0.15:02:42:ac:1e:00:02)
    • deschideți un server http pe calculatorul de acasă pe un port oarecare (să zicem 8080)
    • în meniul cu port forwarding redirecționați orice mesaj care vine de pe internet pe portul 80 către 192.168.0.15:8080
  4. Daca locuiți în cămin și nu aveți drept ce acces pe routerul pe care îl folosiți pentru internet, puteți face un Mesh VPN prin zerotier astfel încât să puteți accesa calculatorul de acasă prin intermediul unei rețele private. În acel mesh VPN puteți configura un DNS cu orice nume doriți voi.

Exercițiu HTTP + S + DNS

  1. Folosiți Github Stdent Pack, înscrieți-vă cu adresa instituțională și obțineți un domeniu gratuit timp de 1 an de pe name.com.
  2. Creați o instanță EC2 pe AWS cu Ubuntu (sau orice altă instanță de mașină virtuală cu într-un cont de cloud).
  3. În meniul de configurație a intrărilor din DNS introduceți o intrare de tip A care să redirecționeze domeniul către adresa IPv4 a instanței voastre.
  4. Instalați certbot sudo apt install certbot pe instanța voastră.
  5. Executați certbot pentru a genera perechile de chei public-privat pentru domeniul vostru sudo certbot certonly -d NUME_DOMENIU --preferred-challenges dns --manual. Opțiunile din comanda anterioară --preferred-challenges dns --manual indică faptul că veți valida faptul că domeniul vă aparține printr-o intrare adăugată manual de tipul TXT în configurarea domeniului. În general, dacă doriți ca certificatele să se reînnoiască automat, nu folosiți cele două opțiuni, ci deschideți din security groups portul 80 și 443 care va valida automat conexiunea cu serverul.
  6. În urma execuției anterioare, în directorul /etc/letsencrypt/live/NUME_DOMENIU/ vor fi generate perechi de chei public-private pentru domeniile voastre.
  7. Pentru a testa HTTPS, executați codul uvicorn specificând keyfile și certfile: sudo uvicorn simple_fastapi:app --reload --host 0.0.0.0 --port 8080 --ssl-keyfile /etc/letsencrypt/live/NUME_DOMENIU/privkey.pem --ssl-certfile /etc/letsencrypt/live/NUME_DOMENIU/fullchain.pem

DNS over HTTPS

Pentru securitare și privacy, sunt dezvoltate metode noi care encriptează cererile către DNS. DNS over HTTPS sau DoH este explicat in detaliu aici. Privacy-ul oferit de DoH poate fi exploatat și de malware iar mai multe detalii despre securitatea acestuia pot fi citite aici.

Exerciții HTTP/S

  1. Cloudflare are un serviciu DoH care ruleaza pe IP-ul 1.1.1.1. Urmăriți aici documentația pentru request-uri de tip GET către cloudflare-dns și scrieți o funcție care returnează adresa IP pentru un nume dat ca parametru. Indicații: setați header-ul cu {‘accept’: ‘application/dns-json’}.
  2. Executati pe containerul rt1 scriptul ‘simple_flask.py’ care deserveste API HTTP pentru GET si POST. Daca accesati in browser http://localhost:8001 ce observati?
  3. Conectați-vă la containerul docker compose exec rt2 bash. Testati conexiunea catre API-ul care ruleaza pe rt1 folosind curl: curl -X POST http://rt1:8001/post -d '{"value": 10}' -H 'Content-Type: application/json'. Scrieti o metoda POST care ridică la pătrat un numărul definit în value. Apelați-o din cod folosind python requests.
  4. Urmăriți alte exemple de request-uri pe HTTPbin

Tutorial SSH

alt text

SSH este o aplicație client-server care permite instanțierea unui shell pe un calculator care se află într-o altă locație pe rețea. Reprezintă o alternativă la telent și rlogin (aplicații nesecurizate). Folosește protocolul TCP pentru transport. De ce? De obicei portul 22 este rezervat pentru SSH Permite și crearea unui tunel prin care să se transmită date în mod securizat Cea mai sigură metodă este conectarea prin pereche cheie publică-cheie privată, dar funcționează și prin conexiune pe bază de parolă.

Dacă avem un server deschis pe localhost, putem folosi SSH pentru a face un tunel prin TCP prin care să trimitem orice fel de mesaje. Asta înseamnă că putem trimite mesaje TCP prin TCP-ul deschis cu SSH.

Exerciții SSH

  1. Aplicație server deschisă pe localhost pe server, accesibilă local prin tunel SSH Puteți încerca să deschideți un server simplu (folosind simple_flask.py deschis cu localhost:8002 pe un server remote). Folisiți ssh -N -L 8083:localhost:8002 USER@IP_SERVER ca să redirecționați mesajele care vin pe portul local 8083 către adresa localhost:8002 de pe server. Puteți accesa aplicația din browserul local folosind portul local pe care tocmai l-am alocat (8083)

  2. Dynamic Port Forwarding (nerecomandat) Putem transforma server-ul într-un proxy securizat prin care să trimitem toate mesajele ssh -N -D 8081 USER@IP_SERVER. Dynamic port forwarding deschide un canal de comunicare de pe adresa localhost:8081 către server, encapsulând orice mesaj de la nivelele inferioare. La nivelul browserului putem seta SOCKS proxy ca fiind localhost:8081. Dacă verificăm în browser care este adresa IP, vom vedea că este chiar adresa serverului pe care am instanțiat conexiunea SSH cu dynamic port forwarding.

  3. Încercați mai multe exemple de tuneluri SSH

Socket API

Este un API disponibil în mai toate limbajele de programare cu care putem implementa comunicarea pe rețea la un nivel mai înalt. Semnificația flag-urilor este cel mai bine explicată în tutoriale de unix sockets care acoperă partea de C. În limbajul python avem la dispoziție exact aceleași funcții și flag-uri ca în C iar interpretarea lor nu ține de un limbaj de programare particular.

User Datagram Protocol - UDP

Este un protocol simplu la nivelul transport. Header-ul acestuia include portul sursă, portul destinație, lungime și un checksum opțional:

  0      7 8     15 16    23 24      31
  +--------+--------+--------+--------+
  |     Source      |   Destination   |
  |      Port       |      Port       |
  +--------+--------+--------+--------+
  |                 |                 |
  |     Length      |    Checksum     |
  +--------+--------+--------+--------+
  |
  |       data octets / payload
  +---------------- ...

Toate câmpurile din header sunt reprezentate pe câte 16 biți sau 2 octeți:

Câteva caracteristi ale protocolului sunt descrise aici iar partea de curs este acoperită în mare parte aici. UDP este implementat la nivelul sistemului de operare, iar Socket API ne permite să interacționăm cu acest protocol folosind apeluri de sistem.

Tutorial

UDP Server

În primă fază trebuie să importăm librăria socket:

import socket

Se instanțiază un obiect sock cu AF_INET pentru adrese de tip IPv4, SOCK_DGRAM (datagrams - connectionless, unreliable messages of a fixed maximum length) pentru datagrams și IPPROTO_UDP pentru a specifica protocolul UDP:

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP)

Apelăm funcția bind pentru a asocia un port unei adrese și pentru ca aplicația sa își aloce acel port prin care poate primi sau transmite mesaje. In cazul de fata, adresa folosita este localhost, pe interfata loopback, ceea ce inseamnă că aplicația noastră nu va putea comunica cu alte dispozitive pe rețea, ci doar cu alte aplicații care se găsesc pe același calculator/container:

port = 10000
adresa = 'localhost'
# tuplu adresa, port
server_address = (adresa, port)

#functia bind primeste ca parametru un obiect de tip tuplu
sock.bind(server_address)

Pentru a aloca un port astfel încât serverul să poată comunica pe rețea, trebuie fie să folosim adresa IP a interfeței pe care o folosim pentru comunicare (eth0 în cadrul containerelor de docker), fie să folosim o meta-adresă IP rezervată: 0.0.0.0 face ca toate interfețele să fie deschise către comunicare. Mai multe detalii despre această adresă puteți citi aici.

În momentul în care un client trimite serverului mesaje, acestea sunt stocate într-un buffer. Dimensiunea bufferului depinde de configurarea sistemului de operare, detaliile pentru linux sunt aici sau o postare cu mai multe explicații aici. Pentru a primi un mesaj, serverul poate să apeleze funcția recvfrom care are ca parametru numărul de bytes pe care să-l citească din buffer și o serie de flags optionale.

# citeste 16 bytes din buffer
data, address = sock.recvfrom(16)

Funcția produce un apel blocant, deci programul stă în așteptare ca bufferul să se umple de octeți pentru a fi citiți. În cazul în care serverul nu primește mesaje, metoda stă în așteptare. Putem regla timpul de așteptare prin settimeout.

Valoarea returnată este un tuplu cu octeții citiți și adresa de la care au fost trimiși:

print("Date primite: ", data)
print("De la adresa: ", address)

Folosim funcția sendto pentru a transmite octeți către o adresă de tip tuplu. Putem trimite înapoi un string prin care confirmăm primirea mesajului:

payload =  bytes('Am primit: ', 'utf-8') + data
sent = sock.sendto(payload, address)
print ("Au fost trimisi ", sent, ' bytes')

În cele din urmă putem închide socket-ul folosind metoda close()

sock.close()
UDP Client

Pentru a putea trimite mesaje, clientul trebuie să folosească adresa IP și port corespunzătoare serverului:

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP)

port = 10000
adresa = 'localhost'
server_address = (adresa, port)

În python3, un string nu poate fi trimis prin socket decât dacă este convertit în șir de octeți. Conversia se poate face fie prin alegerea unei codificări:

mesaj = "salut de la client, 你好"
print(type(mesaj))
# encoding utf-16
octeti = mesaj.encode('utf-16')
print (octeti)
print(type(octeti))

În linux codificarea default este UTF-8, un format în care unitățile de reprezentare a caracterelor sunt formate din 8 biți. Pentru a printa literele în terminal ar fi bine să folosim UTF-8. Caracterele ASCII putem să le convertim în bytes punând litera b în față:

octeti = b"salut de la client cu" # nu merg caractere non-ascii 'țășîâ'
# apelam string encode
octeti = octeti + "你好".encode('utf-8')
# sau apelam constructorul de bytes cu un encoding
octeti = octeti + bytes("你好", 'utf-8')
print (octeti)

Observăm aici că nu am apelat metoda bind ca în server, dar cu toate astea metoda sendto pe care o folosim pentru a trimite octeții alocă implicit un port efemer pe care îl utilizează pentru a trimite și primi mesaje.

sent = sock.sendto(octeti, server_address)

Prin același socket putem apela si metoda de citire, în cazul de față 18 bytes din buffer. În cazul în care serverul nu trimite înapoi niciun mesaj, apelul va bloca programul indefinit până când va primi mesaje în buffer.

data, adresa_de_la_care_primim = sock.recvfrom(18)
print(data, adresa_de_la_care_primim)

În cele din urmă pentru a închide conexiunea și portul, apelăm funcția close:

sock.close()

O diagramă a procesului anterior este reprezentată aici:
alt text

Exerciții UDP

În directorul capitolul2/src aveți două scripturi udp_server.py și udp_client.py. Spre deosebire de exemplul prezentat mai sus, serverul stă în continuă aștepatre de mesaje iar clientul trimite mesajul primit ca prim argument al programului.

  1. Executați serverul apoi clientul fie într-un container de docker fie pe calculatorul vostru personal: python3 udp_server.py și python3 udp_client.py "mesaj de trimis".
  2. Modificați adresa de pornire a serverului din ‘localhost’ în IP-ul rezervat descris mai sus cu scopul de a permite serverului să comunice pe rețea cu containere din exterior.
  3. Porniți un terminal în directorul capitolul2 și atașați-vă la containerul rt1: docker compose exec rt1 bash. Pe rt1 folositi calea relativă montată în directorul elocal pentru a porni serverul: python3 /elocal/src/udp_server.py.
  4. Modificați udp_client.py ca el să se conecteze la adresa serverului, nu la ‘localhost’. Sfaturi: puteți înlocui localhost cu adresa IP a containerului rt1 sau chiar cu numele ‘rt1’.
  5. Porniți un al doilea terminal în directorul capitolul2 și rulați clientul în containerul rt2 pentru a trimite un mesaj serverului: docker compose exec rt2 bash -c "python3 /elocal/src/udp_client.py salut"
  6. Deschideți un al treilea terminal și atașați-vă containerului rt1: docker compose exec rt1 bash. Utilizați tcpdump -nvvX -i any udp port 10000 pentru a scana mesajele UDP care circulă pe portul 10000. Apoi apelați clientul pentru a genera trafic.
  7. Containerul rt1 este definit în docker compose.yml cu redirecționare pentru portul 8001. Modificați serverul și clientul în așa fel încât să îl puteți executa pe containerul rt1 și să puteți să vă conectați la el de pe calculatorul vostru sau de pe rețeaua pe care se află calculatorul vostru.

Transmission Control Protocol - TCP

Este un protocol mai avansat de la nivelul transport. Header-ul acestuia este mai complex și va fi explicat în detaliu în capitolul3:

  0                   1                   2                   3   Offs.
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 |          Source Port          |       Destination Port        |  1
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 |                        Sequence Number                        |  2
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 |                    Acknowledgment Number                      |  3
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 | Data  |0 0 0| |C|E|U|A|P|R|S|F|                               |
 |Offset | Res.|N|W|C|R|C|S|S|Y|I|            Window             |  4
 |       |     |S|R|E|G|K|H|T|N|N|                               |
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 |           Checksum            |         Urgent Pointer        |  5
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 |                    Options   (if data offset > 5)             | 
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
 |                    Application data                           | 
 -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

Câteva caracteristici ale protocolului sunt descrise aici. Înainte de a face pașii de mai jos, urmăriți partea de curs acoperită în mare parte aici. Între un client și un server se execută un proces de stabilire a conexiunii prin three-way handshake.

Tutorial

TCP Server

Server-ul se instanțiază cu AF_INET, SOCK_STREAM (fiindcă TCP operează la nivel de byte streams) și IPPROTO_TCP pentru specificarea protocolului TCP.

# TCP socket 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=socket.IPPROTO_TCP)

port = 10000
adresa = 'localhost'
server_address = (adresa, port)
sock.bind(server_address)

Protocolul TCP stabilește o conexiune între client și server prin acel 3-way handshake explicat aici. Numărul de conexiuni în aștepare se poate stabili prin metoda listen, apel prin care se marchează în același timp socketul ca fiind gata să accepte conexiuni.

sock.listen(5)

Metoda accept este una blocantă și stă în așteptarea unei conexiuni. În cazul în care nu se conectează niciun clinet la server, metoda va bloca programul indefinit. Altfel, când un client inițializează 3-way handshake, metoda accept construieste un obiect de tip socket nou prin care se menține conexiunea cu acel client în mod specific.

while True:
   conexiune, addr = sock.accept()
   time.sleep(30)
   # citim 16 bytes in bufferul asociat conexiunii
   payload = conexiune.recv(16)
   # trimitem înapoi un mesaj
   conexiune.send("Hello from TCP!".encode('utf-8'))
   # închidem conexiunea, dar nu și socket-ul serverului care
   # așteaptă alte noi conxiuni TCP
   conexiune.close()

sock.close()
TCP Client

Clientul trebuie să folosească adresa IP și portul cu serverului:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=socket.IPPROTO_TCP)

port = 10000
adresa = 'localhost'
server_address = (adresa, port)

Prin apelul funcției connect, se inițializează 3-way handshake și conexiunea cu serverul.

# 3-way handshake creat
sock.connect(server_address)
# trimite un mesaj
sock.send("Mesaj TCP client".encode('utf-8'))
# primeste un mesaj
data = sock.recv(1024)
print (data)
# inchide conexiunea
sock.close()

O diagramă a procesului anterior este reprezentată aici:
alt text

Exerciții TCP

În directorul capitolul2/src aveți două scripturi tcp_server.py și tcp_client.py.

  1. Executați serverul apoi clientul fie într-un container de docker fie pe calculatorul vostru personal: python3 tcp_server.py și python3 tcp_client.py "mesaj de trimis".
  2. Modificați adresa de pornire a serverului din ‘localhost’ în IP-ul rezervat ‘0.0.0.0’ cu scopul de a permite serverului să comunice pe rețea cu containere din exterior. Modificați tcp_client.py ca el să se conecteze la adresa serverului, nu la ‘localhost’. Pentru client, puteți înlocui localhost cu adresa IP a containerului rt1 sau chiar cu numele ‘rt1’.
  3. Într-un terminal, în containerul rt1 rulați serverul: docker compose exec rt1 bash -c "python3 /elocal/src/tcp_server.py".
  4. Într-un alt terminal, în containerul rt2 rulați clientul: docker compose exec rt1 bash -c "python3 /elocal/src/tcp_client.py TCP_MESAJ"
  5. Mai jos sunt explicați pașii din 3-way handshake captați de tcpdump și trimiterea unui singur byte de la client la server. Salvați un exemplu de tcpdump asemănător care conține și partea de finalizare a conexiunii TCP. Sfat: Modificați clientul să trimită un singur byte fără să facă recv. Modificați serverul să citească doar un singur byte cu recv(1) și să nu facă send. Reporniți serverul din rt1. Deschideți un al treilea terminal, tot în capitolul2 și rulați tcpdump: docker compose exec rt1 bash -c "tcpdump -Snnt tcp" pentru a porni tcpdump pe rt1.

3-way handshake

Exemplu 3-way handshake captat cu tcpdump:

tcpdump -Snn tcp

SYN:

Clientul apelează funcția connect((‘198.13.0.14’, 10000)) iar mesajul din spate arată așa:

   IP 172.111.0.14.59004 > 198.13.0.14.10000: Flags [S], seq 2416620956, win 29200, options [mss 1460,sackOK,TS val 897614012 ecr 0,nop,wscale 7], length 0

SYN-ACK:

În acest punct lucrurile se întâmplă undeva în interiorul funcției accept din server la care nu avem acces. Serverul răspunde prin SYN-ACK:

   IP 198.13.0.14.10000 > 172.111.0.14.59004: Flags [S.], seq 409643424, ack 2416620957, win 28960, options [mss 1460,sackOK,TS val 2714984427 ecr 897614012,nop,wscale 7], length 0

ACK:

După primirea cererii de sincronizare a serverului, clientul confirmă primirea, lucru care se execută în spatele funcției connect:

   IP 172.111.0.14.59004 > 198.13.0.14.10000: Flags [.], ack 409643425, win 229, length 0

PSH:

La trimiterea unui mesaj, se folosește flag-ul push (PSH) și intervalul de secventă de dimensiune 1:

   IP 172.111.0.14.59004 > 198.13.0.14.10000: Flags [P.], seq 2416620957:2416620958, ack 409643425, win 229, length 1

ACK:

Serverul dacă primește mesaju, trimite automat un mesaj cu flag-ul ACK și Ack Nr numărul de octeți primiți.

    IP 198.13.0.14.10000 > 172.111.0.14.59004: Flags [.], ack 2416620958, win 227, length 0

Funcțiile send(p), sr(p), sr(p)1 în scapy

În scapy avem mai multe funcții de trimitere a pachetelor:

Pentru a trimite pachete la nivelul legatură de date (layer 2), completând manual câmpuri din secțiunea Ethernet, avem echivalentul funcțiilor de mai sus:

Exercițiu scapy

  1. Urmăriți exemplul de cereri DNS executate în capitolul 6 în secțiunea de DNS. Încercați să executați codul respectiv și să returnați răspunsuri DNS pentru un domeniu arbitrar.
  2. Scrieți un server DNS și setați-l să fie DNS-ul principal pentru calculatorul vostru. Contorizați timp de o zi care domenii sunt cerute în timpul navigărilor obișnuite. Observați care domenii sunt folosite pentru marketing.