Туннельный прокси для https-трафика

Я хочу реализовать прокси-сервер для пересылки трафика на вышестоящий прокси-сервер.

client <-> proxy server <-> upstream proxy server <-> ..... <-> target server

После долгих поисков и обращения за помощью к GTP ниже приведен туннельный прокси http/https.

use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

async fn handle_connection(mut inbound: TcpStream, target_addr: &str) -> io::Result<()> {
    let mut buffer = [0; 1024];
    let n = inbound.read(&mut buffer).await?;
    let request = String::from_utf8_lossy(&buffer[..n]);
    println!("request {:?}", request);

    let mut outbound = TcpStream::connect(target_addr).await?;
    println!("outbound {:?}", outbound);

    if request.starts_with("CONNECT") {
        inbound
            .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
            .await?;
    }
    let (mut ri, mut wi) = inbound.split();
    let (mut ro, mut wo) = outbound.split();
    let initial_data = &buffer[..n];
    let write_initial = async {
        if !initial_data.is_empty() {
            wo.write_all(initial_data).await?;
        }
        io::copy(&mut ri, &mut wo).await
    };
    tokio::try_join!(write_initial, io::copy(&mut ro, &mut wi))?;

    // tokio::try_join!(io::copy(&mut ri, &mut wo), io::copy(&mut ro, &mut wi))?;
    Ok(())
}

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8181").await?;
    loop {
        let (inbound, _) = listener.accept().await?;

        tokio::spawn(async move {
            let target_addr = "127.0.0.1:12345";
            if let Err(e) = handle_connection(inbound, target_addr).await {
                eprintln!("Failed to forward connection: {}", e);
            }
        });
    }
}

Эта работа для http-трафика

$ curl -v 'http://httpbin.org/anything' -x http://localhost:8181
*   Trying 127.0.0.1:8181...
* Connected to (nil) (127.0.0.1) port 8181 (#0)
> GET http://httpbin.org/anything HTTP/1.1
> Host: httpbin.org
> User-Agent: curl/7.81.0
> Accept: */*
> Proxy-Connection: Keep-Alive
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Length: 373
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Origin: *
< Connection: keep-alive
< Content-Type: application/json
< Date: Mon, 02 Sep 2024 12:39:53 GMT
< Keep-Alive: timeout=4
< Proxy-Connection: keep-alive
< Server: gunicorn/19.9.0
< 
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.81.0", 
    "X-Amzn-Trace-Id": "Root=1-66d5b219-3fc519364e7b7fc100db86cc"
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "x.y.z.k", 
  "url": "http://httpbin.org/anything"
}
* Connection #0 to host (nil) left intact

Не работает с https-трафиком

$ curl -v 'https://httpbin.org/anything' -x http://localhost:8181
*   Trying 127.0.0.1:8181...
* Connected to (nil) (127.0.0.1) port 8181 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to httpbin.org:443
> CONNECT httpbin.org:443 HTTP/1.1
> Host: httpbin.org:443
> User-Agent: curl/7.81.0
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 Connection Established
< 
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* (5454) (IN), , Unknown (72):
* error:0A00010B:SSL routines::wrong version number
* Closing connection 0
curl: (35) error:0A00010B:SSL routines::wrong version number

Не знаю, где я ошибся, но когда я несколько раз спрашивал gtp, он давал одинаковые и неверные ответы :neutral_face:

Я очень благодарен за любые предложения.

[править] Извините, я также задаю этот вопрос на форуме ржавчины. https://users.rust-lang.org/t/tunnel-proxy-for-https-traffic/116955

Кстати, 127.0.0.1:12345 работает для https-трафика.

$ curl -v 'https://httpbin.org/anything' -x http://localhost:12345
*   Trying 127.0.0.1:12345...
* Connected to (nil) (127.0.0.1) port 12345 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to httpbin.org:443
> CONNECT httpbin.org:443 HTTP/1.1
> Host: httpbin.org:443
> User-Agent: curl/7.81.0
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 Connection established
< 
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=httpbin.org
*  start date: Aug 20 00:00:00 2024 GMT
*  expire date: Sep 17 23:59:59 2025 GMT
*  subjectAltName: host "httpbin.org" matched cert's "httpbin.org"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x562af2195eb0)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /anything HTTP/2
> Host: httpbin.org
> user-agent: curl/7.81.0
> accept: */*
> 
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 200 
< date: Tue, 03 Sep 2024 00:21:55 GMT
< content-type: application/json
< content-length: 342
< server: gunicorn/19.9.0
< access-control-allow-origin: *
< access-control-allow-credentials: true
< 
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.81.0", 
    "X-Amzn-Trace-Id": "Root=1-66d656a3-3b632abc5c2a7cff6f66e16d"
  }, 
  "json": null, 
  "method": "GET", 
  "origin": "a.b.e.f", 
  "url": "https://httpbin.org/anything"
}
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection #0 to host (nil) left intact

Это дубликат users.rust-lang.org/t/tunel-proxy-for-https-traffic/116955

moy2010 02.09.2024 18:58

@moy2010 Это тот же вопрос, конечно. Но я не думаю, что это можно квалифицировать как дубликат. Есть ли в stackoverflow политика, запрещающая задавать одни и те же вопросы на других, несвязанных форумах?

Aleksander Krauze 02.09.2024 19:39

Насколько я понимаю, ваш код ваш прокси всегда подключается к 127.0.0.1:12345, т. е. он не получает цель из запроса клиентов. Так что же делает 127.0.0.1:12345? Если это восходящий HTTP-прокси, как предложено в первых строках вашего вопроса, вы также должны отправить ему прокси-запросы, т. е. отправить CONNECT и дождаться ответа, аналогичного тому, как Curl делает с вашим прокси.

Steffen Ullrich 02.09.2024 20:04

@AleksanderKrauze Мы не закрываем вопрос как дубликат чего-то стороннего. Если ответ доступен где-то кроме самого Stack Overflow, кто-то должен опубликовать здесь ответ с кратким изложением решения и ссылкой для указания авторства, если это необходимо (хотя имейте в виду, что ответ должен быть самостоятельным, без переходов по ссылкам).

kmdreko 02.09.2024 21:00

@moy2010. Извините, я задаю этот вопрос обеим сторонам.

GooT 03.09.2024 02:16

@SteffenUllrich, 127.0.0.1:12345 — это http(s)-сервер. Я редактирую свой вопрос более подробно, чтобы доказать, что этот прокси-сервер работает.

GooT 03.09.2024 02:23
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
6
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

TL;DR: ваш прокси-сервер должен пересылать запрос CONNECT от клиента восходящему прокси-серверу, а не только внутренний трафик.

Ваша реализация HTTP-прокси по адресу 127.0.0.1:8181 имеет фиксированный восходящий прокси-сервер 127.0.0.1:12345. Оба этих прокси ожидают запроса CONNECT для HTTPS.

Но хотя ваш прокси-сервер по адресу 127.0.0.18181 обрабатывает CONNECT от клиента, он не отправляет CONNECT восходящему прокси-серверу по адресу 127.0.0.1:12345. Вместо этого он будет напрямую пытаться пройти через все после запроса CONNECT, т. е. он будет рассматривать восходящий поток как HTTPS-сервер (ожидающий прямой TLS), а не HTTP-прокси (ожидающий запрос CONNECT).

Поскольку восходящий прокси-сервер получает не правильный HTTP-запрос (то есть ожидаемый CONNECT), а вместо этого, по-видимому, тарабарщину (начало внутреннего рукопожатия TLS от клиента - в частности, TLS ClientHello), он ответит ошибкой HTTP. Эта ошибка HTTP передается через клиент, который будет рассматривать ее как часть внутреннего подтверждения TLS, поскольку именно это (в частности, TLS ServerHello) отправляется в ожиданиях клиента.

На первый взгляд странная ошибка «неправильный номер версии» возникает из-за того, что клиент пытается интерпретировать ошибку HTTP от восходящего прокси-сервера как запись TLS, в частности, пытается интерпретировать второй и третий байт как версию TLS — см. структуру уровня записи TLS.. Но вместо того, чтобы эти байты были чем-то вроде 0x0303 (TLS 1.2), они будут больше похожи на 0x5454, то есть начало HTTP-ответа (который начинается с HTTP/1..) и, таким образом, совершенно неожиданно.

Чтобы решить эту проблему, ваш прокси-сервер должен не только обрабатывать запрос CONNECT от клиента, как вы уже это делаете, но и пересылать запрос CONNECT вышестоящему прокси-серверу, чего вы не делаете.

Другие вопросы по теме