Esercizio - Proxy con tunnel CONNECT (sul modello della prova pratica)
In questa pagina 6
Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).
Scrivere in C un proxy che supporta il metodo CONNECT (RFC 9110 par. 9.3.6) e crea tunnel TCP:
- il client invia
CONNECT host:porta HTTP/1.1; il proxy apre una connessione TCP versohost:portae, se riesce, rispondeHTTP/1.1 200 Connection established; da quel momento inoltra i byte in entrambe le direzioni senza interpretarli, fino alla chiusura; - accetta solo porte consentite: di default la 443, più quelle elencate sulla riga di comando; per le altre risponde
403 Forbidden; - risponde
400a una richiesta malformata (porta mancante o non valida),405a ogni metodo diverso daCONNECT,502 Bad Gatewayse non riesce a collegarsi; - usa
pollper aspettare dati dai due lati, gestisce la chiusura di una sola direzione (half-close) e chiude i tunnel inattivi da 60 secondi; - serve più tunnel insieme con un processo figlio per tunnel.
Teoria: DNS, proxy web, HTTP CONNECT e gateway applicativiUn programma risolve i nomi con getaddrinfo (file hosts e poi DNS); un messaggio DNS (RFC 1035) ha un header di 12 byte (ID, flag QR/RD/RA/TC e RCODE, quattro contatori), una domanda (QNAME a etichette con lunghezza, QTYPE, QCLASS) e record di risposta con nomi eventualmente compressi da puntatori, su UDP porta 53 con ripiego su TCP se il bit TC e' acceso; un web proxy e' un intermediario scelto dal client che riceve richieste con URI assoluto, le inoltra al server (togliendo gli header hop-by-hop, aggiungendo Via e X-Forwarded-For) e puo' memorizzare le risposte; le cache si organizzano in gerarchie e con funzioni hash coerenti, le CDN le replicano vicino agli utenti; il metodo CONNECT chiede al proxy un tunnel TCP verso host:porta e dopo la risposta 2xx il proxy inoltra i byte in entrambe le direzioni senza interpretarli (HTTPS, con restrizione delle porte); un gateway o reverse proxy e' un intermediario scelto dal server che traduce o inoltra le richieste ad altri sistemi (bilanciamento, terminazione TLS, FastCGI, API gateway).DNS, proxy web, HTTP CONNECT e gateway applicativi →, System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →, Client e server TCP in C - bind, listen, accept e processi concorrentiUn client TCP in C fa getaddrinfo, socket, connect, poi write e read; un server fa socket, setsockopt(SO_REUSEADDR), bind, listen e un ciclo di accept. Un server iterativo serve un client alla volta e un client lento blocca tutti; un server concorrente con fork crea un processo figlio per ogni connessione: il figlio chiude il socket di ascolto e serve la connessione, il padre chiude il socket di connessione e torna ad accept. I figli terminati diventano zombie finche' il padre non li raccoglie con waitpid, di solito in un gestore di SIGCHLD con WNOHANG in un ciclo; i descrittori non chiusi tengono viva la connessione (nessun FIN) o esauriscono EMFILE; uno stato CLOSE_WAIT che si accumula indica una close mancante. Alternative: thread, pre-fork, I/O multiplexing con poll o epoll.Client e server TCP in C - bind, listen, accept e processi concorrenti →, Crittografia asimmetrica, RSA e TLSCrittografia asimmetrica (a chiave pubblica): chiave di cifratura pubblica v_B, chiave di decifratura privata s_B, matematicamente legate; C = E_vB(P), P = D_sB(C); dà riservatezza. I certificati digitali, emessi da una Certificate Authority e firmati con la sua chiave privata, legano un'identità a una chiave pubblica. Firma digitale: hash del messaggio cifrato con la chiave privata; garantisce autenticità, integrità e non ripudio. RSA: si scelgono due primi grandi p e q, N = pq, phi(N) = (p-1)(q-1), e coprimo con phi(N), d = e^(-1) mod phi(N); chiave pubblica (N, e), segreta (N, d); cifratura c = m^e mod N, decifratura m = c^d mod N con m < N (teorema di Eulero); sicura finché la fattorizzazione è difficile (N di almeno 2048 bit); il padding casuale (PKCS#1 v1.5: 00 02 [casuale] 00 [m]) difende da malleabilità e determinismo. La firma RSA è s = m^d mod N, verificata con s^e mod N. TLS (su TCP) autentica gli estremi con il certificato, cifra i dati con una chiave di sessione simmetrica e garantisce l'integrità con i MAC; da TLS 1.0 a 1.3 la chiave si ricava con Diffie-Hellman e l'handshake si accorcia. DTLS è la versione per UDP.Crittografia asimmetrica, RSA e TLS →.
Idea
Con HTTPS il browser cifra il traffico con TLS direttamente verso il server: un proxy che legge le richieste HTTP non vede niente di utile. Allora il browser chiede al proxy di collegarlo al server e di fare solo da ponte:
browser proxy server:443
|-- CONNECT www.example.com:443 HTTP/1.1 -->|
| |------- connect() -------->|
|<-- HTTP/1.1 200 Connection established ----|
|============== da qui il proxy copia i byte nei due versi ==============|
|-- ClientHello (TLS) ---------------------->|-------------------------->|
|<-- ServerHello, certificato ---------------|<--------------------------|
|== richiesta HTTP cifrata =================>|==========================>|Il codice si divide in tre parti:
- Gestione del
CONNECT(handle): legge e controlla la richiesta (targethost:porta, metodo, porta consentita), apre la connessione verso il server (connect_tocongetaddrinfo), invia il200. tunnel(a, b): il cuore, un ciclo dipollsu due socket, con half-close.- Concorrenza:
forkper ogni client; il padre accetta, i figli fanno i tunnel;SIGCHLDraccoglie i figli.
Codice
/* connect_proxy.c - proxy che accetta SOLO il metodo CONNECT e apre un tunnel TCP verso host:porta.
* Dopo "200 Connection established" il proxy inoltra i byte in entrambe le direzioni senza guardarli
* (e' cosi' che passa HTTPS: il TLS e' fra browser e server, il proxy vede solo byte cifrati).
* Un processo figlio per ogni tunnel.
*
* Compilare: gcc -Wall -Wextra -o connect_proxy connect_proxy.c
* Avviare: ./connect_proxy 8889 [porta_consentita ...] (senza argomenti: solo la 443)
* Provare: curl -x http://127.0.0.1:8889 https://example.com/ (HTTPS attraverso il tunnel)
* curl -p -x http://127.0.0.1:8889 http://127.0.0.1:8000/ -v (-p: forza CONNECT anche per http; avviare con 8000)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <poll.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define MAX_LINE 2048
static int allowed[16] = {443};
static int nallowed = 1;
static int write_all(int fd, const char *buf, size_t n)
{
while (n > 0) {
ssize_t w = write(fd, buf, n);
if (w < 0) {
if (errno == EINTR)
continue;
return -1;
}
buf += w;
n -= (size_t)w;
}
return 0;
}
struct rbuf {
int fd;
char buf[4096];
size_t pos, len;
};
static int rb_line(struct rbuf *r, char *line, size_t max)
{
size_t n = 0;
for (;;) {
if (r->pos == r->len) {
ssize_t k = read(r->fd, r->buf, sizeof r->buf);
if (k < 0 && errno == EINTR)
continue;
if (k <= 0)
return -1;
r->pos = 0;
r->len = (size_t)k;
}
char c = r->buf[r->pos++];
if (c == '\n') {
if (n > 0 && line[n - 1] == '\r')
n--;
line[n] = '\0';
return (int)n;
}
if (n + 1 >= max)
return -2;
line[n++] = c;
}
}
static int connect_to(const char *host, const char *port)
{
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, port, &hints, &res) != 0)
return -1;
int fd = -1;
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd;
}
static void send_status(int fd, int code, const char *reason)
{
char head[256];
int n = snprintf(head, sizeof head, "HTTP/1.1 %d %s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", code, reason);
write_all(fd, head, (size_t)n);
}
/* Il cuore del tunnel: poll aspetta dati su uno qualunque dei due socket e li gira all'altro.
* Quando un lato finisce di scrivere (read == 0) si fa shutdown(SHUT_WR) sull'altro (half-close):
* il flusso opposto puo' continuare finche' anche l'altro lato non ha finito. */
static void tunnel(int a, int b)
{
struct pollfd pf[2];
int open_dir[2] = {1, 1}; /* open_dir[0]: a -> b ancora aperta; open_dir[1]: b -> a */
char buf[8192];
while (open_dir[0] || open_dir[1]) {
pf[0].fd = open_dir[0] ? a : -1; /* poll ignora i descrittori negativi */
pf[1].fd = open_dir[1] ? b : -1;
pf[0].events = pf[1].events = POLLIN;
pf[0].revents = pf[1].revents = 0;
int rc = poll(pf, 2, 60000);
if (rc < 0) {
if (errno == EINTR)
continue;
return;
}
if (rc == 0)
return; /* 60 secondi di silenzio: tunnel inattivo, si chiude */
for (int i = 0; i < 2; i++) {
if (!(pf[i].revents & (POLLIN | POLLHUP | POLLERR)))
continue;
int from = (i == 0) ? a : b, to = (i == 0) ? b : a;
ssize_t r = read(from, buf, sizeof buf);
if (r < 0 && errno == EINTR)
continue;
if (r <= 0) {
shutdown(to, SHUT_WR); /* EOF o errore: si comunica la fine all'altro lato */
open_dir[i] = 0;
} else if (write_all(to, buf, (size_t)r) < 0) {
return; /* l'altro lato non c'e' piu' */
}
}
}
}
static void handle(int cfd)
{
struct rbuf cl = {.fd = cfd};
char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
if (rb_line(&cl, line, sizeof line) < 0 || sscanf(line, "%15s %2047s %15s", method, target, ver) != 3) {
send_status(cfd, 400, "Bad Request");
return;
}
for (;;) { /* header: letti e scartati (qui si potrebbe controllare Proxy-Authorization) */
int len = rb_line(&cl, line, sizeof line);
if (len < 0) {
send_status(cfd, 400, "Bad Request");
return;
}
if (len == 0)
break;
}
if (strcmp(method, "CONNECT") != 0) {
send_status(cfd, 405, "Method Not Allowed");
return;
}
/* il target di CONNECT e' "host:porta" (la porta e' obbligatoria; per IPv6 "[::1]:443") */
char host[256], *colon = strrchr(target, ':');
if (colon == NULL || colon == target || colon[1] == '\0') {
send_status(cfd, 400, "Bad Request");
return;
}
*colon = '\0';
char *portstr = colon + 1, *end;
long port = strtol(portstr, &end, 10);
if (*end != '\0' || port < 1 || port > 65535) {
send_status(cfd, 400, "Bad Request");
return;
}
const char *h = target;
if (h[0] == '[') { /* IPv6 fra parentesi quadre */
size_t l = strlen(h);
if (h[l - 1] != ']') {
send_status(cfd, 400, "Bad Request");
return;
}
target[l - 1] = '\0';
h++;
}
snprintf(host, sizeof host, "%s", h);
/* Un CONNECT verso porte qualsiasi trasformerebbe il proxy in un ponte verso SMTP, SSH, servizi interni... */
int ok = 0;
for (int i = 0; i < nallowed; i++)
if (allowed[i] == port)
ok = 1;
if (!ok) {
fprintf(stderr, "CONNECT %s:%ld -> 403 (porta non consentita)\n", host, port);
send_status(cfd, 403, "Forbidden");
return;
}
int sfd = connect_to(host, portstr);
if (sfd < 0) {
fprintf(stderr, "CONNECT %s:%ld -> 502\n", host, port);
send_status(cfd, 502, "Bad Gateway");
return;
}
/* Risposta di successo: da qui in poi la connessione e' un tunnel di byte (niente Content-Length). */
const char *ok_resp = "HTTP/1.1 200 Connection established\r\n\r\n";
write_all(cfd, ok_resp, strlen(ok_resp));
fprintf(stderr, "CONNECT %s:%ld -> tunnel aperto\n", host, port);
if (cl.pos < cl.len) /* byte del client gia' arrivati dopo la richiesta: vanno al server */
write_all(sfd, cl.buf + cl.pos, cl.len - cl.pos);
tunnel(cfd, sfd);
close(sfd);
fprintf(stderr, "CONNECT %s:%ld -> tunnel chiuso\n", host, port);
}
static void on_sigchld(int sig)
{
(void)sig;
int saved = errno;
while (waitpid(-1, NULL, WNOHANG) > 0)
;
errno = saved;
}
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "uso: %s porta [porta_consentita ...]\n", argv[0]);
return 1;
}
if (argc > 2) {
nallowed = 0;
for (int i = 2; i < argc && nallowed < 16; i++)
allowed[nallowed++] = atoi(argv[i]);
}
signal(SIGPIPE, SIG_IGN);
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) {
perror("socket");
return 1;
}
int yes = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons((unsigned short)atoi(argv[1]));
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0 || listen(lfd, 32) < 0) {
perror("bind/listen");
return 1;
}
fprintf(stderr, "proxy CONNECT in ascolto sulla porta %s\n", argv[1]);
for (;;) {
int cfd = accept(lfd, NULL, NULL);
if (cfd < 0) {
if (errno == EINTR)
continue;
perror("accept");
continue;
}
pid_t pid = fork();
if (pid == 0) {
close(lfd); /* il figlio non accetta connessioni */
handle(cfd);
close(cfd);
_exit(0);
}
close(cfd); /* il padre non usa questa connessione */
}
}Compilare e provare
$ gcc -Wall -Wextra -o connect_proxy connect_proxy.c
$ (cd www && python3 -m http.server 8000 --bind 127.0.0.1) &
$ ./connect_proxy 8889 443 8000 & # porte consentite: 443 e 8000 (solo per la prova locale)
proxy CONNECT in ascolto sulla porta 8889Con nc, per vedere il protocollo a mano (dopo il 200 ciò che segue è un normale scambio con il server di origine):
$ (printf 'CONNECT 127.0.0.1:8000 HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n\r\nGET /index.html HTTP/1.0\r\n\r\n'; sleep 1) | nc 127.0.0.1 8889
HTTP/1.1 200 Connection established
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.12.10
...
<html><body><h1>Ciao</h1></body></html>Il proxy risponde con il suo 200 (e nessun Content-Length); la seconda parte è la risposta del server Python alla GET, passata dal tunnel. Sul terminale del proxy:
CONNECT 127.0.0.1:8000 -> tunnel aperto
CONNECT 127.0.0.1:8000 -> tunnel chiusoCon curl: -p (--proxytunnel) forza CONNECT anche per un URL http://.
$ curl -v -p -x http://127.0.0.1:8889 http://127.0.0.1:8000/index.html
* Establish HTTP proxy tunnel to 127.0.0.1:8000
> CONNECT 127.0.0.1:8000 HTTP/1.1
> Host: 127.0.0.1:8000
...
< HTTP/1.1 200 Connection established
* Proxy replied 200 to CONNECT request
> GET /index.html HTTP/1.1
...
<html><body><h1>Ciao</h1></body></html>Con HTTPS vero (serve Internet; porta 443 consentita di default): curl -x http://127.0.0.1:8889 https://example.com/. Altre prove sul filtro e sugli errori:
$ printf 'CONNECT example.com:25 HTTP/1.1\r\nHost: x\r\n\r\n' | nc 127.0.0.1 8889 # porta non consentita
HTTP/1.1 403 Forbidden
$ printf 'CONNECT example.com HTTP/1.1\r\nHost: x\r\n\r\n' | nc 127.0.0.1 8889 # manca la porta
HTTP/1.1 400 Bad Request
$ printf 'GET http://example.com/ HTTP/1.1\r\n\r\n' | nc 127.0.0.1 8889 # non e' CONNECT
HTTP/1.1 405 Method Not Allowed
$ printf 'CONNECT 127.0.0.1:9 HTTP/1.1\r\nHost: x\r\n\r\n' | nc 127.0.0.1 8889 # se il proxy e' avviato con la porta 9 consentita ma nessuno ascolta: 502Spiegazione dei punti chiave
La richiesta CONNECT. Il target è in authority-form: host:porta, e la porta è obbligatoria (non c'è porta predefinita). Il codice:
strrchr(target, ':')trova l'ultimo:(la porta); senza:o con porta vuota →400.strtolconverte la porta e controlla che sia un intero fra 1 e 65535.- Se l'host è fra parentesi quadre (
[::1]:443, IPv6) toglie le parentesi. - Gli header si leggono e scartano fino alla riga vuota (qui si potrebbe controllare
Proxy-Authorization, il modo con cui un proxy autentica chi può aprire tunnel).
La lista delle porte consentite. È una misura di sicurezza, non un dettaglio: un CONNECT verso porte qualsiasi trasforma il proxy in un ponte verso qualunque servizio raggiungibile: CONNECT mail.example.com:25 per inviare spam, CONNECT 10.0.0.5:22 per raggiungere macchine interne (SSRF). Le porte ammesse sono un array (allowed), di default solo la 443; la lista si può cambiare dalla riga di comando.
La risposta 200. HTTP/1.1 200 Connection established\r\n\r\n: nessun Content-Length né Transfer-Encoding (RFC 9110 par. 9.3.6: il server non deve mandarli e il client deve ignorarli). Dopo la riga vuota la connessione non è più HTTP: è un tubo di byte. Gli errori prima del tunnel sono invece normali risposte HTTP (send_status: 400, 403, 405, 502).
Byte già arrivati. Il client potrebbe inviare dati subito dopo la richiesta, prima di vedere il 200 (per esempio un ClientHello anticipato): quei byte si trovano già nel buffer del lettore cl. Il programma li gira al server con write_all(sfd, cl.buf + cl.pos, cl.len - cl.pos) prima di entrare nel ciclo di poll: altrimenti andrebbero persi.
tunnel(a, b).
pf[0].fd = open_dir[0] ? a : -1; pf[1].fd = open_dir[1] ? b : -1; /* poll ignora i descrittori negativi */
poll(pf, 2, 60000); /* aspetta dati da a o da b, al massimo 60 s */
/* per ogni lato pronto: */
r = read(from, buf, sizeof buf);
if (r <= 0) { shutdown(to, SHUT_WR); open_dir[i] = 0; } /* EOF: si propaga la chiusura all'altro lato */
else if (write_all(to, buf, r) < 0) return; /* l'altro lato non c'e' piu' */pollè necessario perchéreadbloccante su un lato impedirebbe di servire l'altro: i dati possono arrivare in qualunque ordine, indipendentemente nei due versi.open_dir[0](daaversob) eopen_dir[1](dabversoa) tengono traccia delle direzioni ancora aperte. Quandoreadda un lato restituisce0(EOF: quel lato ha finito di scrivere), si fashutdown(altro, SHUT_WR), che invia il FIN solo in quella direzione: l'altro lato lo vede come EOF. La direzione opposta resta attiva: il server può ancora rispondere a una richiesta che il client ha già chiuso dalla sua parte (half-close). Il ciclo finisce quando entrambe le direzioni sono chiuse.-1al posto del descrittore nelpollfd:polllo ignora. Se si lasciasse un descrittore già in EOF,polllo segnalerebbe pronto di continuo (POLLHUP) e il ciclo girerebbe a vuoto consumando la CPU.- Il timeout di 60 secondi (
rc == 0) chiude i tunnel muti. - Dopo il ciclo
handlechiudesfd;mainchiudecfd.
Concorrenza. main fa accept in ciclo e fork: il figlio chiude lfd, esegue handle(cfd), chiude cfd ed esce con _exit(0); il padre chiude cfd. sigaction(SIGCHLD) con waitpid(-1, NULL, WNOHANG) in ciclo evita gli zombie (Client e server TCP in C - bind, listen, accept e processi concorrentiUn client TCP in C fa getaddrinfo, socket, connect, poi write e read; un server fa socket, setsockopt(SO_REUSEADDR), bind, listen e un ciclo di accept. Un server iterativo serve un client alla volta e un client lento blocca tutti; un server concorrente con fork crea un processo figlio per ogni connessione: il figlio chiude il socket di ascolto e serve la connessione, il padre chiude il socket di connessione e torna ad accept. I figli terminati diventano zombie finche' il padre non li raccoglie con waitpid, di solito in un gestore di SIGCHLD con WNOHANG in un ciclo; i descrittori non chiusi tengono viva la connessione (nessun FIN) o esauriscono EMFILE; uno stato CLOSE_WAIT che si accumula indica una close mancante. Alternative: thread, pre-fork, I/O multiplexing con poll o epoll.Client e server TCP in C - bind, listen, accept e processi concorrenti →). Un tunnel dura a lungo, quindi la versione iterativa servirebbe un solo browser alla volta.
Errori tipici
- Aspettare
Content-Lengthnella risposta alCONNECT, o mandarlo: dopo il200non c'è niente di HTTP. - Chiudere entrambi i socket al primo EOF: si perdono i dati che l'altro lato sta ancora per inviare (risposte ancora in transito). Serve l'half-close.
- Non inoltrare i byte già bufferizzati dopo la richiesta.
- Usare due
readbloccanti in sequenza (un lato, poi l'altro): un tunnel è bidirezionale e indipendente nei due versi; servepoll(o due processi, o due thread). - Lasciare nel
pollfdun descrittore già chiuso (ciclo a vuoto), o non gestirePOLLHUPePOLLERR. - Accettare qualsiasi porta di destinazione (ponte verso SMTP, SSH, servizi interni) e non richiedere autenticazione.
- Dimenticare il timeout di inattività: i tunnel morti occupano processi per sempre.
- Pensare che il proxy possa leggere il traffico: dentro il tunnel c'è TLS e il proxy vede solo byte cifrati.
- Dimenticare
signal(SIGPIPE, SIG_IGN): un lato che sparisce uccide il processo che scrive.
Varianti per esercitarsi
- Unire questo programma al proxy HTTP (Esercizio - Proxy HTTP con filtro degli host (sul modello della prova pratica)): stesso processo per
GETeCONNECT. - Autenticare i client con
407eProxy-Authenticate: Basic, controllandoProxy-Authorization(Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica)). - Limitare gli host di destinazione con un elenco, e registrare in un file ogni tunnel (host, porta, byte trasferiti, durata).
- Contare i byte trasferiti nei due versi e stamparli alla chiusura.
- Sostituire
forkcon un ciclo di eventi che serve più tunnel in un solo processo.
Versione ripasso
- Testo. Proxy
./connect_proxy porta [porta_consentita ...]:CONNECT host:porta HTTP/1.1-> connessione al server eHTTP/1.1 200 Connection established, poi byte inoltrati nei due versi; porte consentite (default 443) altrimenti403;400,405,502;poll, half-close, timeout 60 s; un figlio per tunnel. - Richiesta. Target authority-form
host:portacon porta obbligatoria (strrchr(':'),strtol1-65535,[::1]:443per IPv6); header scartati (qui si potrebbe controllareProxy-Authorization); metodo diverso ->405. - Sicurezza. Lista delle porte consentite (array
allowed): senza, ponte verso SMTP (:25), SSH, rete interna (SSRF). In più autenticazione, log, limiti sugli host. - Risposta.
HTTP/1.1 200 Connection established\r\n\r\n: nienteContent-Length/Transfer-Encoding(il client li ignora). Errori prima del tunnel = risposte HTTP normali (400,403,405,502). - Byte già arrivati. Il buffer
clpuò contenere dati dopo la richiesta:write_all(sfd, cl.buf + cl.pos, cl.len - cl.pos)prima del ciclo. tunnel(a, b).
open_dir[0] = open_dir[1] = 1;
while (open_dir[0] || open_dir[1]) {
pf[0].fd = open_dir[0] ? a : -1; pf[1].fd = open_dir[1] ? b : -1; /* -1: ignorato da poll */
pf[0].events = pf[1].events = POLLIN;
rc = poll(pf, 2, 60000); if (rc < 0 && errno != EINTR) return; if (rc == 0) return; /* inattivo */
for (i = 0; i < 2; i++) if (pf[i].revents & (POLLIN | POLLHUP | POLLERR)) {
r = read(from, buf, sizeof buf);
if (r <= 0) { shutdown(to, SHUT_WR); open_dir[i] = 0; } /* half-close */
else if (write_all(to, buf, r) < 0) return; } }- Perché.
pollserve perché i due versi sono indipendenti;shutdown(SHUT_WR)propaga l'EOF ma lascia aperta l'altra direzione;-1evita un ciclo a vuoto suPOLLHUP. - Concorrenza.
accept+fork; figlio:close(lfd),handle,close(cfd),_exit(0); padre:close(cfd);SIGCHLDconwaitpid(WNOHANG)in ciclo;SIGPIPEignorato. - Prove.
(printf 'CONNECT 127.0.0.1:8000 HTTP/1.1\r\nHost: x\r\n\r\nGET /index.html HTTP/1.0\r\n\r\n'; sleep 1) | nc 127.0.0.1 8889->200 Connection established+ risposta del server;curl -p -x http://127.0.0.1:8889 http://127.0.0.1:8000/;curl -x ... https://example.com/;CONNECT example.com:25->403; senza porta ->400;GET->405. - Codice essenziale (le funzioni centrali, senza commenti):
static void handle(int cfd)
{
struct rbuf cl = {.fd = cfd};
char line[MAX_LINE], method[16], target[MAX_LINE], ver[16];
if (rb_line(&cl, line, sizeof line) < 0 || sscanf(line, "%15s %2047s %15s", method, target, ver) != 3) {
send_status(cfd, 400, "Bad Request");
return;
}
for (;;) {
int len = rb_line(&cl, line, sizeof line);
if (len < 0) {
send_status(cfd, 400, "Bad Request");
return;
}
if (len == 0)
break;
}
if (strcmp(method, "CONNECT") != 0) {
send_status(cfd, 405, "Method Not Allowed");
return;
}
char host[256], *colon = strrchr(target, ':');
if (colon == NULL || colon == target || colon[1] == '\0') {
send_status(cfd, 400, "Bad Request");
return;
}
*colon = '\0';
char *portstr = colon + 1, *end;
long port = strtol(portstr, &end, 10);
if (*end != '\0' || port < 1 || port > 65535) {
send_status(cfd, 400, "Bad Request");
return;
}
const char *h = target;
if (h[0] == '[') {
size_t l = strlen(h);
if (h[l - 1] != ']') {
send_status(cfd, 400, "Bad Request");
return;
}
target[l - 1] = '\0';
h++;
}
snprintf(host, sizeof host, "%s", h);
int ok = 0;
for (int i = 0; i < nallowed; i++)
if (allowed[i] == port)
ok = 1;
if (!ok) {
fprintf(stderr, "CONNECT %s:%ld -> 403 (porta non consentita)\n", host, port);
send_status(cfd, 403, "Forbidden");
return;
}
int sfd = connect_to(host, portstr);
if (sfd < 0) {
fprintf(stderr, "CONNECT %s:%ld -> 502\n", host, port);
send_status(cfd, 502, "Bad Gateway");
return;
}
const char *ok_resp = "HTTP/1.1 200 Connection established\r\n\r\n";
write_all(cfd, ok_resp, strlen(ok_resp));
fprintf(stderr, "CONNECT %s:%ld -> tunnel aperto\n", host, port);
if (cl.pos < cl.len)
write_all(sfd, cl.buf + cl.pos, cl.len - cl.pos);
tunnel(cfd, sfd);
close(sfd);
fprintf(stderr, "CONNECT %s:%ld -> tunnel chiuso\n", host, port);
}- Errori tipici:
Content-Lengthatteso nella risposta; chiusura di entrambi i lati al primo EOF; byte bufferizzati non inoltrati;readbloccanti in sequenza; descrittore chiuso lasciato inpoll; qualsiasi porta accettata; niente timeout; il proxy creduto capace di leggere TLS;SIGPIPEattivo.