dizda / fast-socks5

Fast SOCKS5 client/server implementation written in Rust async/.await (with tokio)
https://anyip.io/
MIT License
339 stars 66 forks source link

question: how can one forward to another tcp proxy? As in chain proxies? #15

Open GlenDC opened 2 years ago

GlenDC commented 2 years ago

From the documentation or examples it is not clear what the intended approach with this library is when one would want to forward from a fast-socks5 proxy to another proxy?

I'll keep digging myself once i find time, and if I find it myself I'll post it here. But if someone knows the answer from the top of their head, that would be even better :)

GlenDC commented 2 years ago

Found how I can do it myself, here is how I did it:

async fn handle_socket<T>(socket: Socks5Socket<T>, _request_timeout: u64)
where
    T: AsyncRead + AsyncWrite + Unpin,
{
    // upgrade socket to SOCKS5 proxy
    let mut socks5_socket = match socket.upgrade_to_socks5().await {
        Err(e) => {
            error!("upgrade incoming socket to socks5: {:#}", e);
            return;
        },
        Ok(socket) => socket,
    };

    // get resolved target addr
    if let Err(e) = socks5_socket.resolve_dns().await {
        error!("resolve target dns for incoming socket: {:#}", e);
        return;
    }
    let target_addr = match socks5_socket.target_addr() {
        Some(addr) => addr,
        None => {
            error!("no target address found for incoming socket");
            return;
        },
    };
    let mut socket_addrs_it = match target_addr.to_socket_addrs() {
        Err(e) =>  {
            error!("no socket addresses found for target addr of incoming socket: {:#}", e);
            return;
        },
        Ok(it) => it,
    };
    let socket_addr = match socket_addrs_it.next() {
        None =>  {
            error!("target of incoming socket unreachable");
            return;
        },
        Some(addr) => addr,
    };

    let stream_config = client::Config::default();

    // connect to downstream proxy
    let mut stream = match Socks5Stream::connect_with_password(
        "127.0.0.1:1338",
        socket_addr.ip().to_string(),
        socket_addr.port(),
        String::from("john"),
        String::from("foo"),
        stream_config,
    ).await {
        Err(e) =>  {
            error!("connect to downstream proxy for incoming socket: {:#}", e);
            return;
        },
        Ok(stream) => stream,
    };

    // copy data between our incoming client and the used downstream proxy
    match tokio::io::copy_bidirectional(&mut stream, &mut socks5_socket).await {
        Ok(res) => info!("socket transfer closed ({}, {})", res.0, res.1),
        Err(err) => if err.kind() == ErrorKind::NotConnected {
            info!("socket transfer closed by client")
        } else {
            error!("socket transfer error: {:?}", err)
        },
    };
}

Does this make sense, or am I making it more difficult than it is? If you're interested I could make a PR to add an example to add it as another server example, cannot imagine that I'm the only one who would like the ability to be able to go through yet another proxy, in order to establish a gateway / router in front of actual proxies on location.

GlenDC commented 2 years ago

This code was only put in a function at the end of my struggles, so I was still logging everywhere instead of handling these errors more neatly as a single error log where I call it. I now refactored it a bit so that makes the logic already seem a lot simpler:

async fn handle_socket<T>(socket: Socks5Socket<T>, _request_timeout: u64) -> Result<()>
where
    T: AsyncRead + AsyncWrite + Unpin {
    // upgrade socket to SOCKS5 proxy
    let mut socks5_socket = socket.upgrade_to_socks5().await.context("upgrade incoming socket to socks5")?;

    // get resolved target addr
    socks5_socket.resolve_dns().await.context("resolve target dns for incoming socket")?;
    let socket_addr = socks5_socket
        .target_addr()
        .context("find target address for incoming socket")?
        .to_socket_addrs()
        .context("convert target address of incoming socket to socket addresses")?
        .next()
        .context("reach out to target of incoming socket")?;

    let stream_config = client::Config::default();

    // connect to downstream proxy
    let mut stream = Socks5Stream::connect_with_password(
        "127.0.0.1:1338",
        socket_addr.ip().to_string(),
        socket_addr.port(),
        String::from("john"),
        String::from("foo"),
        stream_config,
    ).await.context("connect to downstream proxy for incoming socket")?;

    // copy data between our incoming client and the used downstream proxy
    match tokio::io::copy_bidirectional(&mut stream, &mut socks5_socket).await {
        Ok(res) => {
            info!("socket transfer closed ({}, {})", res.0, res.1);
            Ok(())
        },
        Err(err) => if err.kind() == ErrorKind::NotConnected {
            info!("socket transfer closed by client");
            Ok(())
        } else {
            Err(SocksError::Other(anyhow!("socket transfer error: {:#}", err)))
        },
    }
}

Same position though, what is your opinion on this kind of gateway concept, is that a good use case of this library, or am I shoe-horning a solution here?

GlenDC commented 2 years ago

My example won't work. This is related to #21 I believe. Within the upgrade_to_socks5 logic we already request and process it. As such the first actual usage of the proxy is already going via the router directly to the target.

In order for a streamer like this to work we would need to also expose the authentication (and other setup part) from the actual request part.

This way we even have access to the unresolved target address (see #19). So that's another issue resolved. Seems the building blocks are all there, it's just that the library doesn't expose it yet.

GlenDC commented 2 years ago

New attempt is made to have this shown in an example: https://github.com/dizda/fast-socks5/pull/24