Feature(proxy): support ssh proxy protocol (#519)

This commit is contained in:
Jason Lyu
2026-01-11 00:32:24 -05:00
committed by GitHub
parent 9ea92e91b2
commit 8fae79e889
8 changed files with 126 additions and 14 deletions
+5
View File
@@ -96,6 +96,11 @@ jobs:
args: ssserver -s 0.0.0.0:8388 -k pass -m aes-128-gcm -U -v
proxy: ss://aes-128-gcm:pass@192.168.1.2:8388
udp: true
- protocol: SSH
image: gogost/gost:3.0.0-nightly.20241002
args: -L "sshd://user:pass@:2222"
proxy: ssh://user:pass@192.168.1.2:2222
udp: false
env:
TUN_IF: tun0
RESTAPI_PORT: 8080
+1 -3
View File
@@ -11,7 +11,7 @@
## Features
- **Universal Proxying**: Transparently routes all network traffic from any application through a proxy.
- **Multi-Protocol**: Supports HTTP/SOCKS4/SOCKS5/Shadowsocks proxies with optional authentication.
- **Multi-Protocol**: Supports HTTP/SOCKS/Shadowsocks/SSH/Relay proxies with optional authentication.
- **Cross-Platform**: Runs on Linux/macOS/Windows/FreeBSD/OpenBSD with platform-specific optimizations.
- **Gateway Mode**: Acts as a Layer 3 gateway to route traffic from other devices on the same network.
- **Full IPv6 Compatibility**: Natively supports IPv6; seamlessly tunnels IPv4 over IPv6 and vice versa.
@@ -47,8 +47,6 @@ Welcome and feel free to ask any questions at [Discussions](https://github.com/x
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fxjasonlyu%2Ftun2socks.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fxjasonlyu%2Ftun2socks?ref=badge_large)
All versions starting from `v2.6.0` are available under the terms of the [MIT License](https://github.com/xjasonlyu/tun2socks/blob/main/LICENSE).
## Star History
<a href="https://star-history.com/#xjasonlyu/tun2socks&Date">
+1
View File
@@ -8,4 +8,5 @@ import (
_ "github.com/xjasonlyu/tun2socks/v2/proxy/shadowsocks"
_ "github.com/xjasonlyu/tun2socks/v2/proxy/socks4"
_ "github.com/xjasonlyu/tun2socks/v2/proxy/socks5"
_ "github.com/xjasonlyu/tun2socks/v2/proxy/ssh"
)
+2
View File
@@ -48,6 +48,8 @@ golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
+3 -9
View File
@@ -1,7 +1,6 @@
package utils
import (
"context"
"net"
"time"
@@ -10,15 +9,15 @@ import (
)
const (
tcpConnectTimeout = 5 * time.Second
tcpKeepAlivePeriod = 30 * time.Second
TCPConnectTimeout = 5 * time.Second
TCPKeepAlivePeriod = 30 * time.Second
)
// SetKeepAlive sets tcp keepalive option for tcp connection.
func SetKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(tcpKeepAlivePeriod)
tcp.SetKeepAlivePeriod(TCPKeepAlivePeriod)
}
}
@@ -33,8 +32,3 @@ func SafeConnClose(c net.Conn, err error) {
func SerializeSocksAddr(m *M.Metadata) socks5.Addr {
return socks5.SerializeAddr("", m.DstIP, m.DstPort)
}
// WithTCPConnectTimeout returns a derived context with the default TCP connect timeout.
func WithTCPConnectTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, tcpConnectTimeout)
}
+4 -1
View File
@@ -46,7 +46,10 @@ func (rl *Relay) DialContext(ctx context.Context, metadata *M.Metadata) (c net.C
}
func (rl *Relay) DialUDP(metadata *M.Metadata) (net.PacketConn, error) {
ctx, cancel := utils.WithTCPConnectTimeout(context.Background())
ctx, cancel := context.WithTimeout(
context.Background(),
utils.TCPConnectTimeout,
)
defer cancel()
return rl.dialContext(ctx, metadata)
+4 -1
View File
@@ -76,7 +76,10 @@ func (ss *Socks5) DialUDP(*M.Metadata) (_ net.PacketConn, err error) {
return nil, fmt.Errorf("%w when unix domain socket is enabled", errors.ErrUnsupported)
}
ctx, cancel := utils.WithTCPConnectTimeout(context.Background())
ctx, cancel := context.WithTimeout(
context.Background(),
utils.TCPConnectTimeout,
)
defer cancel()
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
+106
View File
@@ -0,0 +1,106 @@
package ssh
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"golang.org/x/crypto/ssh"
"github.com/xjasonlyu/tun2socks/v2/dialer"
M "github.com/xjasonlyu/tun2socks/v2/metadata"
"github.com/xjasonlyu/tun2socks/v2/proxy"
"github.com/xjasonlyu/tun2socks/v2/proxy/internal/utils"
)
type SSH struct {
addr string
config *ssh.ClientConfig
}
func New(addr, user, pass, keyFile, passphrase string) (*SSH, error) {
var auth []ssh.AuthMethod
if pass != "" {
auth = append(auth, ssh.Password(pass))
}
if keyFile != "" {
key, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("ssh: read file: %w", err)
}
var signer ssh.Signer
if passphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase(
key, []byte(passphrase))
} else {
signer, err = ssh.ParsePrivateKey(key)
}
if err != nil {
return nil, fmt.Errorf("ssh: parse private key: %w", err)
}
auth = append(auth, ssh.PublicKeys(signer))
}
return &SSH{
addr: addr,
config: &ssh.ClientConfig{
User: user,
Auth: auth,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: utils.TCPConnectTimeout,
},
}, nil
}
func (s *SSH) DialContext(ctx context.Context, metadata *M.Metadata) (_ net.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil {
return nil, fmt.Errorf("connect to %s: %w", s.addr, err)
}
utils.SetKeepAlive(c)
defer func(c net.Conn) {
utils.SafeConnClose(c, err)
}(c)
sc, ch, reqs, err := ssh.NewClientConn(c, s.addr, s.config)
if err != nil {
return nil, err
}
client := ssh.NewClient(sc, ch, reqs)
conn, err := client.Dial("tcp", metadata.DestinationAddress())
if err != nil {
client.Close()
return nil, err
}
return &sshConn{conn, client}, nil
}
func (s *SSH) DialUDP(*M.Metadata) (net.PacketConn, error) {
return nil, errors.ErrUnsupported
}
type sshConn struct {
net.Conn
client *ssh.Client
}
func (c *sshConn) Close() error {
defer c.client.Close()
return c.Conn.Close()
}
func Parse(u *url.URL) (proxy.Proxy, error) {
address, username := u.Host, u.User.Username()
password, _ := u.User.Password()
keyFile := u.Query().Get("privateKeyFile")
passphrase := u.Query().Get("passphrase")
return New(address, username, password, keyFile, passphrase)
}
func init() {
proxy.RegisterProtocol("ssh", Parse)
}