diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 435c070..fd9cdd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/README.md b/README.md index d92d40f..574843c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/engine/register.go b/engine/register.go index 040b533..4bf7447 100644 --- a/engine/register.go +++ b/engine/register.go @@ -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" ) diff --git a/go.sum b/go.sum index 93c54e8..01d0113 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/proxy/internal/utils/utils.go b/proxy/internal/utils/utils.go index 6ab544d..4a1e179 100644 --- a/proxy/internal/utils/utils.go +++ b/proxy/internal/utils/utils.go @@ -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) -} diff --git a/proxy/relay/relay.go b/proxy/relay/relay.go index 30038fe..f382d47 100644 --- a/proxy/relay/relay.go +++ b/proxy/relay/relay.go @@ -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) diff --git a/proxy/socks5/socks5.go b/proxy/socks5/socks5.go index d2e0fc4..a2174d0 100644 --- a/proxy/socks5/socks5.go +++ b/proxy/socks5/socks5.go @@ -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) diff --git a/proxy/ssh/ssh.go b/proxy/ssh/ssh.go new file mode 100644 index 0000000..b6ed03d --- /dev/null +++ b/proxy/ssh/ssh.go @@ -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) +}