mirror of
https://github.com/xjasonlyu/tun2socks.git
synced 2026-04-22 15:57:28 +08:00
Feature(proxy): support ssh proxy protocol (#519)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
[](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">
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user