shadowsocks-rust icon indicating copy to clipboard operation
shadowsocks-rust copied to clipboard

[Feature request] SOCKS Proxy Support for sslocal

Open Pyvonix opened this issue 6 months ago • 11 comments

Summary

Currently, sslocal does not natively support forwarding traffic through another SOCKS proxy. I am requesting a new feature and config option that allows sslocal to route its traffic through a specified SOCKS5 proxy.

Use Case

The use case involves chaining sslocal traffic through another SOCKS proxy. For example, if a local SOCKS proxy is exposed on 127.0.0.1:1234 and only this socks proxy allowed to connect to the internet, I would like to configure sslocal to route its traffic through this proxy natively (without relying on other package/binary).

This feature is especially useful in scenarios like SSH tunneling (ssh -D) to a VPS, where sslocal could utilize the local SSH SOCKS proxy for further forwarding.

Workarounds Tried

  1. local-tunnel
openbsd# sslocal -v --protocol tunnel  -b "127.0.0.1:1080" -f "127.0.0.1:1234" ...
2025-05-27T04:59:22.080355343-04:00 DEBUG main ThreadId(01) shadowsocks_rust::sys: rlimit NOFILE adjusted rlimit { rlim_cur: 1024, rlim_max: 1024 }    
2025-05-27T04:59:22.080621565-04:00  INFO main ThreadId(01) shadowsocks_rust::service::local: shadowsocks local 1.23.4 build 2025-05-27T08:01:34.889851441+00:00    
2025-05-27T04:59:22.081944065-04:00  INFO tokio-runtime-worker ThreadId(02) shadowsocks_service::local::tunnel::tcprelay: shadowsocks TCP tunnel listening on 127.0.0.1:1080

But the service that try to get through don't succeed:

socks_handshake: TCP port read failed on recv(): Operation now in progress (errno=36)
  1. External binary/package

I attempted to use socksify (similar to proxychains on BSD) to achieve this, but it results in the following error:

openbsd# socksify sslocal -v ....  
2025-05-27T04:44:45.61702666-04:00 DEBUG main ThreadId(01) shadowsocks_rust::sys: rlimit NOFILE adjusted rlimit { rlim_cur: 1024, rlim_max: 1024 }    
2025-05-27T04:44:45.617247157-04:00  INFO main ThreadId(01) shadowsocks_rust::service::local: shadowsocks local 1.23.4 build 2025-05-27T08:01:34.889851441+00:00    

thread 'main' panicked at /home/shadowsocks-rust/src/service/local.rs:1002:50:  
create local: Os { code: 61, kind: ConnectionRefused, message: "Connection refused" }  
stack backtrace:  
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.  
Abort trap

Note: I also got sslocal to start but probably due to socksify hooking, sslocal start listening on 0.0.0.0:XXXX where “XXXX” is a random port. It doesn't seem to apply the config file.

Proposed Solution

Add native support for specifying a SOCKS proxy in sslocal's configuration or command-line arguments.

For example:

sslocal --socks-proxy 127.0.0.1:1234  ...

Or allow forward to support socks5?

This would ensure sslocal encapsulates its traffic within the specified SOCKS5 proxy without requiring external tools like socksify, proxychains, etc...

Benefits

  • Simplifies integration in environments where a SOCKS proxy is the only allowed outbound connection.
  • Reduces dependency on third-party tools for SOCKS chaining.
  • Improves usability and flexibility for users requiring advanced routing scenarios.

Environment

  • Version: shadowsocks local 1.23.4 (commit c28c40cc56648257139a6f3cc85406983906c8e9)
  • Platform: OpenBSD 7.7
  • Additional Context: SSH-based SOCKS proxy (ssh -D).

Thank you for considering this feature request!

Pyvonix avatar May 27 '25 09:05 Pyvonix

Similar request is #1397 .

I personally totally Ok with this feature. But it may require quite a lot of work. Adding a proxy option inside ConnectOpts and makes TcpStream as an enum type.

zonyitoo avatar May 27 '25 15:05 zonyitoo

All you need is a tool that listens on a port and forwards traffic via a SOCKS server to an arbitrary destination, which in this case is the Shadowsocks server. Easy. With shadowsocks-go, you can have:

{
    "servers": [
        {
            "name": "ss-via-socks5",
            "protocol": "direct",
            "tcpListeners": [
                {
                    "network": "tcp",
                    "address": ":1234"
                }
            ],
            "udpListeners": [
                {
                    "network": "udp",
                    "address": ":1234"
                }
            ],
            "mtu": 1500,
            "tunnelRemoteAddress": "ssserverhost:port"
        }
    ],
    "clients": [
        {
            "name": "socks5",
            "protocol": "socks5",
            "endpoint": "socks5host:port",
            "enableTCP": true,
            "enableUDP": true,
            "mtu": 1500,
            "socks5": {
                "username": "username",
                "password": "password",
                "enableUserPassAuth": true
            }
        }
    ]
}

Then you ask sslocal to connect to [::1]:1234. Of course, you can just add your Shadowsocks configuration to the above example and run the whole thing in a single instance.

database64128 avatar May 27 '25 16:05 database64128

I personally totally Ok with this feature. But it may require quite a lot of work. Adding a proxy option inside ConnectOpts and makes TcpStream as an enum type.

Sounds like a lot of engineering effort for seemingly little to no benefit other than supporting very niche use cases like this.

In fact, there are so many different ways of combining protocols, that you can keep making changes to add support, and users will keep finding new unsupported combinations.

I recently made some changes in shadowsocks-go that had incidentally made it possible to write code to "chain" clients indefinitely. However, I have decided against exposing this capability through configuration. Doing so would just add extra complexity and failure modes with little to no practical value to users.

database64128 avatar May 27 '25 16:05 database64128

Thank you both for your responses.

@zonyitoo, I agree to close this issue in favor of #1397 but I will answer here to keep the conversation follow.

But it may require quite a lot of work.

I have no idea how much work this would require, but I'm just in favor of it. 😄

@database64128, regarding your first comment: if I understand correctly, the shadowsocks's Rust implementation separates ssslocal and ssserver, which means running two binaries to have the excepted feature. Is that accurate? I want to ensure I'm understanding this correctly what you suggest.

As for your second comment: unfortunately, I can't give an opinion on "lot of engineering effort". I find it interesting that two issues already exist for this same feature. That suggests it may have more merit than "seemingly little to no benefit".

From a practical perspective, the most widely used proxy protocols—SOCKS and HTTP/HTTPS—are standard enough to be directly exportable as environment variables on modern Linux and macOS systems. It's a bit ironic for a proxy service like shadowsocks to lack support for other proxies, considering their ubiquity. 😅

While I want to avoid introducing "risky features" into shadowsocks, I do see value in enabling SOCKS proxy support for outgoing traffic. It will provide a flexible way to root the client traffic without requiring further changes in shadowsocks in near future 😃

Pyvonix avatar Jun 01 '25 15:06 Pyvonix

if I understand correctly, the shadowsocks's Rust implementation separates ssslocal and ssserver, which means running two binaries to have the excepted feature. Is that accurate? I want to ensure I'm understanding this correctly what you suggest.

I'm not sure I understand what you mean. You are already running an ssserver instance on your server, and you want your local sslocal instance to forward encapsulated Shadowsocks traffic via your SOCKS server to ssserver. Did I get that right? My first comment suggests that you use another tool like shadowsocks-go to do the forwarding via SOCKS part. You could also just use shadowsocks-go for the whole thing, since it speaks the encrypted proxy protocols as well.

database64128 avatar Jun 01 '25 16:06 database64128

I just remember that I made a "proxy plugin":

https://github.com/zonyitoo/shadowsocks-proxy-plugin

It should fix this issue perfectly.

zonyitoo avatar Jun 02 '25 09:06 zonyitoo

@zonyitoo this is exactly what I expected!

This should be mentioned somewhere in the README.md and/or included in the project as feature (in my opinion). Thank you for sharing it

Pyvonix avatar Jun 02 '25 17:06 Pyvonix

After having had time to seriously consider the suggested solution, I am forced to reopen this issue because shadowsocks-proxy-plugin opens a random port that I cannot know in advance in order to authorize it in my firewall.

With the following configuration file:

{
  "server": "1.1.1.1",
  "server_port": 443,
  "password": "password",
  "method": "aes-256-gcm",
  "local_address": "127.0.0.1",
  "local_port": 1234,
  "plugin": "./proxy-plugin-local",
  "plugin_opts": "proxy_protocol=socks5&proxy_addr=192.168.0.1:1080"
}

I get the following debug logs:

2025-08-24T10:40:29.772858249-04:00 INFO main ThreadId(01) shadowsocks_rust::service::local: shadowsocks local 1.23.5 build 2025-08-24T14:05:49.774144491+00:00 
2025-08-24T10:40:29.775513798-04:00 DEBUG main ThreadId(01) shadowsocks::plugin: started plugin "proxy-plugin-local" on 127.0.0.1:44027 <-> 1.1.1.1:443 (79271) tcp_only 
2025-08-24T10:40:32.775975089-04:00 DEBUG main ThreadId(01) shadowsocks_rust::service::local: server-loader task is now listening USR1    
2025-08-24T10:40:32.776233764-04:00 INFO tokio-runtime-worker ThreadId(02) shadowsocks_service::local::socks::server::server: shadowsocks socks TCP listening on 127.0.0.1:1234 

Where started plugin "proxy-plugin-local" on <IP> changes each time it is run.

So I ask again, is it not possible to consider chaining socks5 directly with shadowsocks? 😅

Pyvonix avatar Aug 24 '25 14:08 Pyvonix

The port 44027 is only for communicating with sslocal in local network. You could bypass all traffics in the local network (IP: ::1 and 127.0.0.1).

zonyitoo avatar Aug 25 '25 02:08 zonyitoo

I understood what the random port (here 44027) was for, but I preferred “native chaining” with sslocal rather than binding/opening a new port that would change at each execution.

I admit that I am a bit biased due to the “constraints” I set 😄 However, I support the value of such a native feature to avoid the use of an external plugin, the opening of an additional port and to facilitate the usage of sslocal through an HTTP or SOCKS5 proxy.

Pyvonix avatar Aug 25 '25 12:08 Pyvonix

Wrapping shadowsocks' data traffic with other protocols are supported by "plugin", which allow other "plugin" implementations to provide services as a separated project instead of commiting all of them into shadowsocks' repository.

zonyitoo avatar Aug 26 '25 07:08 zonyitoo