Skip to content

Commit 7857d7d

Browse files
committed
:s5s supports -x upstream proxy chain
1 parent d5fd9e9 commit 7857d7d

7 files changed

Lines changed: 119 additions & 16 deletions

File tree

apps/proxyclient.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ type ProxyClientConfig struct {
184184
ServerPort string
185185
PresharedKey string // -psk <psk-string>
186186
KcpWithUDP bool
187+
LocalAddr net.Addr
187188
UpstreamDialer Dialer // 上游拨号器,用于链式代理。nil则使用net.DialTimeout直连
188189
}
189190

@@ -193,7 +194,11 @@ func (c *ProxyClientConfig) dialToServer(timeout time.Duration) (net.Conn, error
193194
if c.UpstreamDialer != nil {
194195
return c.UpstreamDialer.DialTimeout(c.Network, serverAddress, timeout)
195196
}
196-
return net.DialTimeout(c.Network, serverAddress, timeout)
197+
dialer := &net.Dialer{
198+
Timeout: timeout,
199+
LocalAddr: c.LocalAddr,
200+
}
201+
return dialer.Dial(c.Network, serverAddress)
197202
}
198203

199204
func BuildNTConfigFromPCConfig(config *ProxyClientConfig) *secure.NegotiationConfig {
@@ -502,6 +507,10 @@ func IsProxyURLFormat(s string) bool {
502507
// ParseProxyChainURL 解析逗号分隔的代理链 URL,返回链式 ProxyClient
503508
// 例如: "socks5://user:pass@1.1.1.1:1080,https://2.2.2.2:8080"
504509
func ParseProxyChainURL(logWriter io.Writer, chain string) (*ProxyClient, error) {
510+
return ParseProxyChainURLWithLocalAddr(logWriter, chain, nil)
511+
}
512+
513+
func ParseProxyChainURLWithLocalAddr(logWriter io.Writer, chain string, firstHopLocalAddr net.Addr) (*ProxyClient, error) {
505514
parts := splitProxyChain(chain)
506515
if len(parts) == 0 {
507516
return nil, fmt.Errorf("empty proxy chain")
@@ -514,6 +523,9 @@ func ParseProxyChainURL(logWriter io.Writer, chain string) (*ProxyClient, error)
514523
if err != nil {
515524
return nil, fmt.Errorf("parse proxy URL %q: %w", part, err)
516525
}
526+
if len(configs) == 0 {
527+
config.LocalAddr = firstHopLocalAddr
528+
}
517529
configs = append(configs, config)
518530
}
519531

apps/proxyserver.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func handleSocks5Proxy(conn net.Conn, keyingMaterial [32]byte, config *AppS5SCon
2727
ServerIP: config.ServerIP,
2828
Localbind: config.Localbind,
2929
AccessCtrl: config.AccessCtrl,
30+
Outbound: config.UpstreamClient,
3031
}
3132
s5auth := &Socks5AuthConfig{
3233
AuthenticateUser: nil,
@@ -57,6 +58,12 @@ func handleSocks5Proxy(conn net.Conn, keyingMaterial [32]byte, config *AppS5SCon
5758

5859
conn.SetReadDeadline(time.Time{})
5960

61+
if config.UpstreamClient != nil && req.Command != "CONNECT" {
62+
config.Logger.Printf("SOCKS5 %s rejected for %s->%s: -x upstream mode supports CONNECT only", req.Command, conn.RemoteAddr(), reqTarget)
63+
sendSocks5Response(conn, REP_COMMAND_NOT_SUPPORTED, "0.0.0.0", 0)
64+
return
65+
}
66+
6067
if req.Command == "CONNECT" && config.EnableConnect {
6168
err = handleDirectTCPConnect(&s5config, conn, req.Host, req.Port)
6269
if err != nil {
@@ -97,6 +104,12 @@ func handleHTTPProxy(conn *netx.BufferedConn, config *AppS5SConfig) {
97104
}
98105

99106
// 普通 HTTP
107+
if config.UpstreamClient != nil {
108+
config.Logger.Printf("HTTP %s rejected for %s->%s: -x upstream mode supports CONNECT only", req.Method, conn.RemoteAddr(), req.Host)
109+
_, _ = conn.Write([]byte("HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n"))
110+
return
111+
}
112+
100113
err = handleHTTPForwardSimple(conn, req, config)
101114
if err != nil {
102115
config.Logger.Printf("HTTP CONNECT failed for %s->%s: %v", conn.RemoteAddr(), req.Host, err)
@@ -273,6 +286,18 @@ func handleHTTPConnect(clientConn net.Conn, req *http.Request, config *AppS5SCon
273286
config.Logger.Printf("HTTP-CONNECT: %s->%s connecting...", clientConn.RemoteAddr().String(), req.Host)
274287
defer config.Logger.Printf("HTTP: %s client disconnected.", clientConn.RemoteAddr().String())
275288

289+
if config.UpstreamClient != nil {
290+
targetConn, err := config.UpstreamClient.DialTimeout("tcp", req.Host, 25*time.Second)
291+
if err != nil {
292+
clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
293+
return fmt.Errorf("upstream HTTP CONNECT failed: %w", err)
294+
}
295+
defer targetConn.Close()
296+
clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
297+
bidirectionalCopy(clientConn, targetConn)
298+
return nil
299+
}
300+
276301
dialer := &net.Dialer{}
277302
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
278303
defer cancel()

apps/s5s.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,19 @@ import (
2121
// VERSION 1.1.0 (Refactored)
2222

2323
type AppS5SConfig struct {
24-
Logger *log.Logger
25-
Username string
26-
Password string
27-
EnableConnect bool
28-
EnableUDP bool
29-
EnableBind bool
30-
Localbind []string
31-
ServerIP string
32-
AccessCtrl *acl.ACL
33-
EnableSocks5 bool
34-
EnableHTTP bool
24+
Logger *log.Logger
25+
Username string
26+
Password string
27+
EnableConnect bool
28+
EnableUDP bool
29+
EnableBind bool
30+
Localbind []string
31+
ServerIP string
32+
AccessCtrl *acl.ACL
33+
EnableSocks5 bool
34+
EnableHTTP bool
35+
UpstreamProxy string
36+
UpstreamClient *ProxyClient
3537
}
3638

3739
// AppS5SConfigByArgs 解析给定的 []string 参数,生成 AppS5SConfig
@@ -47,6 +49,7 @@ func AppS5SConfigByArgs(logWriter io.Writer, args []string) (*AppS5SConfig, erro
4749
var authString string // 用于接收 -auth 的值
4850
var localBind string
4951
fs.StringVar(&authString, "auth", "", "Username and password for SOCKS5 authentication (format: user:pass)")
52+
fs.StringVar(&config.UpstreamProxy, "x", "", "Upstream proxy chain for TCP CONNECT only (URL format, comma-separated)")
5053
fs.BoolVar(&config.EnableConnect, "c", true, "Allow SOCKS5 CONNECT command")
5154
fs.BoolVar(&config.EnableBind, "b", false, "Allow SOCKS5 BIND command")
5255
fs.BoolVar(&config.EnableUDP, "u", false, "Allow SOCKS5 UDP ASSOCIATE command")
@@ -82,6 +85,17 @@ func AppS5SConfigByArgs(logWriter io.Writer, args []string) (*AppS5SConfig, erro
8285
}
8386
}
8487

88+
if config.UpstreamProxy != "" {
89+
localAddr, err := resolveS5SFirstHopLocalAddr(config.Localbind)
90+
if err != nil {
91+
return nil, fmt.Errorf("invalid -local for -x first hop: %w", err)
92+
}
93+
config.UpstreamClient, err = ParseProxyChainURLWithLocalAddr(logWriter, config.UpstreamProxy, localAddr)
94+
if err != nil {
95+
return nil, fmt.Errorf("invalid upstream proxy chain: %w", err)
96+
}
97+
}
98+
8599
// 如果 -auth 标志被提供
86100
if authString != "" {
87101
authParts := strings.SplitN(authString, ":", 2)
@@ -96,6 +110,17 @@ func AppS5SConfigByArgs(logWriter io.Writer, args []string) (*AppS5SConfig, erro
96110
}
97111

98112
// App_s5s_usage_flagSet 接受一个 *flag.FlagSet 参数,用于打印其默认用法信息
113+
func resolveS5SFirstHopLocalAddr(localbind []string) (net.Addr, error) {
114+
for _, ip := range localbind {
115+
ip = strings.TrimSpace(ip)
116+
if ip == "" {
117+
continue
118+
}
119+
return net.ResolveTCPAddr("tcp", net.JoinHostPort(ip, "0"))
120+
}
121+
return nil, nil
122+
}
123+
99124
func App_s5s_usage_flagSet(fs *flag.FlagSet) {
100125
fmt.Fprintln(fs.Output(), ":s5s Usage: [options]")
101126
fmt.Fprintln(fs.Output(), "\nOptions:")

apps/socks5u.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type Socks5uConfig struct {
7575
ServerIP string
7676
Localbind []string
7777
AccessCtrl *acl.ACL
78+
Outbound Dialer
7879
// ExpectProxyHeader:若为 true,从 mux stream 收到 OK 之后会先读取一个完整的
7980
// PROXY v2 头部(用于解析源/目的地址记日志),并原样转发到 targetConn。
8081
ExpectProxyHeader bool
@@ -1052,6 +1053,23 @@ func handleLocalUDPToTunnel(config *Socks5uConfig, localUDPConn net.PacketConn,
10521053

10531054
func handleDirectTCPConnect(config *Socks5uConfig, clientConn net.Conn, targetHost string, targetPort int) error {
10541055
targetAddr := net.JoinHostPort(targetHost, strconv.Itoa(targetPort))
1056+
if config.Outbound != nil {
1057+
config.Logger.Printf(
1058+
"TCP: %s->%s connecting via upstream proxy...",
1059+
clientConn.RemoteAddr(),
1060+
targetAddr,
1061+
)
1062+
targetConn, err := config.Outbound.DialTimeout("tcp", targetAddr, 25*time.Second)
1063+
if err != nil {
1064+
sendSocks5Response(clientConn, REP_GENERAL_SOCKS_SERVER_FAIL, "0.0.0.0", 0)
1065+
return fmt.Errorf("upstream TCP connect failed: %w", err)
1066+
}
1067+
defer targetConn.Close()
1068+
sendSocks5Response(clientConn, REP_SUCCEEDED, "0.0.0.0", 0)
1069+
bidirectionalCopy(clientConn, targetConn)
1070+
return nil
1071+
}
1072+
10551073
dialer := &net.Dialer{}
10561074

10571075
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
@@ -2354,8 +2372,8 @@ func (pc *Socks5UDPPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err erro
23542372
buf := pc.writeBuf
23552373
buf[0] = SOCKS5_UDP_RSV >> 8
23562374
buf[1] = SOCKS5_UDP_RSV & 0xFF // RSV
2357-
buf[2] = 0x00 // FRAG
2358-
buf[3] = atyp // ATYP
2375+
buf[2] = 0x00 // FRAG
2376+
buf[3] = atyp // ATYP
23592377
off := 4
23602378
off += copy(buf[off:], addrBytes)
23612379
buf[off] = byte(port >> 8)

docs/cookbook.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ gonc -e ":s5s -b -u -http -auth user:simplekey123" -k -l 1080
140140

141141
```
142142

143+
如果希望这个 `:s5s` 本身不直接访问目标,而是把 TCP CONNECT 请求继续交给上游代理链,可以使用 `:s5s -x`
144+
145+
```bash
146+
# 本地监听 1080,客户端发来的 SOCKS5 CONNECT / HTTP CONNECT 通过上游代理链出站
147+
gonc -e ":s5s -http -x 'socks5s://1.2.3.4:3080?psk=key1,socks5s://2.3.4.5:3080?psk=key2'" -k -l 1080
148+
```
149+
150+
`-x` 模式下,目标域名由上游代理解析,本机不做目标出站 ACL 检查;`:s5s -local` 只用于选择连接第一跳上游代理时的本地源 IP。SOCKS5 UDP ASSOCIATE、SOCKS5 BIND 和普通 HTTP 请求不会走该链路。
151+
143152
如果把SOCKS5运行在公网,建议使用TLS+PSK的加密认证,不过其他应用客户端就不支持直接接入了,需要本地再开一个gonc协助加密转发。
144153
<div class="interactive-box">
145154
<label>🛠️设置示例server-ip:</label>

docs/guide/modules.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@
6969

7070
* `-http`: 开启兼容HTTP代理协议,这样同时能支持HTTP代理的客户端了。
7171

72+
* `-x <proxy-chain>`: 为 `:s5s` 的出站 TCP CONNECT 请求指定上游代理链。格式与全局 `-x` 的 URL 形式一致,例如 `socks5://host:port``socks5s://host:port?psk=key``https://host:port`,多个代理用逗号连接。
73+
74+
启用后,SOCKS5 CONNECT 和 HTTP CONNECT 的目标地址会原样交给上游代理,由上游代理服务器解析 DNS;本机不会对目标地址做出站 ACL 检查。`-local` 此时只用于绑定连接第一跳上游代理时使用的本地源 IP。
75+
76+
注意:`:s5s -x` 只支持 TCP CONNECT。SOCKS5 UDP ASSOCIATE、SOCKS5 BIND,以及普通 HTTP 请求(GET/POST 等非 CONNECT)会被明确拒绝。
77+
7278
* `-local`: 限定使用的出站源 IP,如果服务器有多个 IP,可指定多个,用 **逗号分隔**
7379
**支持顺序优先**,即列表靠前的 IP 优先尝试路由连通性。 例如:
7480

@@ -91,6 +97,11 @@
9197
gonc -k -e ":s5s -auth user:simplekey123" -l 1080
9298
```
9399

100+
```bash
101+
# 启动本地代理入口,所有 TCP CONNECT 通过上游加密 SOCKS5 代理链转发
102+
gonc -k -e ":s5s -x 'socks5s://1.2.3.4:3080?psk=key1,socks5s://2.3.4.5:3080?psk=key2'" -l 1080
103+
```
104+
94105
**高级组合 (Socks5 over TLS)**
95106
利用 `gonc` 的加密通道保护 SOCKS5 流量。
96107
<div class="interactive-box">

docs/reference.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@
6363
| :--- | :--- | :--- |
6464
| `-e :sh` | `shell` | **远程 Shell**。将连接的 stdin/stdout 重定向到系统的 shell (cmd.exe 或 /bin/sh)。 |
6565
| `-e :nc` | `netcat` | **标准 Netcat**。仅进行标准输入输出的转发。 |
66-
| `-e :s5s` | `SOCKS5` | **SOCKS5 代理服务器**。将 `gonc` 变为一个标准的 SOCKS5 代理,支持 UDP Associate。 |
66+
| `-e :s5s` | `SOCKS5` | **SOCKS5 代理服务器**。将 `gonc` 变为一个标准的 SOCKS5 代理,支持 UDP Associate;子参数 `:s5s -x <proxy-chain>` 可将 TCP CONNECT 转发到上游代理链|
6767
| `-e :httpserver` | `http` | **HTTP 文件服务器**。快速共享当前目录下的文件。 |
6868
| `-e :mux` | `mux` | **多路复用代理**。配合客户端的 `-link` 参数使用,实现单端口承载多服务。 |
6969

7070
**高级用法示例:**
7171
```bash
7272
# 启动一个带用户认证的 SOCKS5 代理
73-
gonc -l 1080 -auth "user:pass" -e ":s5s"
73+
gonc -l 1080 -auth "user:pass" -e ":s5s"
74+
75+
# 启动一个通过上游代理链出站的 TCP CONNECT 代理入口
76+
gonc -k -l 1080 -e ":s5s -x 'socks5s://1.2.3.4:3080?psk=key1,socks5s://2.3.4.5:3080?psk=key2'"

0 commit comments

Comments
 (0)