mirror of
https://github.com/therootcompany/sclient
synced 2026-04-29 10:47:59 +00:00
feat: add --ssh <fallback-port> for TLS+SSH with direct SSH fallback
Try TLS connection with SSH ALPN first; if the remote doesn't accept it, fall back to a plain TCP connection on the specified port (e.g. 22).
This commit is contained in:
parent
d806c1853c
commit
3551f8e963
@ -33,6 +33,9 @@ func usage() {
|
|||||||
" ex: sclient example.com:8443 0.0.0.0:4080\n"+
|
" ex: sclient example.com:8443 0.0.0.0:4080\n"+
|
||||||
"\n"+
|
"\n"+
|
||||||
" ex: sclient example.com:443 -\n"+
|
" ex: sclient example.com:443 -\n"+
|
||||||
|
"\n"+
|
||||||
|
" ex: sclient --ssh 22 example.com 3000\n"+
|
||||||
|
" (try TLS+ssh ALPN on 443, fall back to SSH on port 22)\n"+
|
||||||
"\n", ver())
|
"\n", ver())
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@ -51,10 +54,12 @@ func main() {
|
|||||||
var insecure bool
|
var insecure bool
|
||||||
var servername string
|
var servername string
|
||||||
var silent bool
|
var silent bool
|
||||||
|
var sshFallbackPort int
|
||||||
|
|
||||||
flag.Usage = usage
|
flag.Usage = usage
|
||||||
|
|
||||||
flag.StringVar(&alpnList, "alpn", "", "acceptable protocols, ex: 'h2,http/1.1' 'http/1.1' 'ssh'")
|
flag.StringVar(&alpnList, "alpn", "", "acceptable protocols, ex: 'h2,http/1.1' 'http/1.1' 'ssh'")
|
||||||
|
flag.IntVar(&sshFallbackPort, "ssh", 0, "enable ssh ALPN and fall back to direct SSH on <port> if TLS+ssh fails (ex: 22)")
|
||||||
flag.BoolVar(&insecure, "k", false, "alias for --insecure")
|
flag.BoolVar(&insecure, "k", false, "alias for --insecure")
|
||||||
flag.BoolVar(&silent, "s", false, "alias of --silent")
|
flag.BoolVar(&silent, "s", false, "alias of --silent")
|
||||||
flag.StringVar(&servername, "servername", "", "specify a servername different from <remote> (to disable SNI use an IP as <remote> and do not use this option)")
|
flag.StringVar(&servername, "servername", "", "specify a servername different from <remote> (to disable SNI use an IP as <remote> and do not use this option)")
|
||||||
@ -64,6 +69,9 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
alpns := parseOptionList(alpnList)
|
alpns := parseOptionList(alpnList)
|
||||||
|
if sshFallbackPort > 0 && len(alpns) == 0 {
|
||||||
|
alpns = []string{"ssh"}
|
||||||
|
}
|
||||||
remotestr := flag.Arg(0)
|
remotestr := flag.Arg(0)
|
||||||
localstr := flag.Arg(1)
|
localstr := flag.Arg(1)
|
||||||
|
|
||||||
@ -85,6 +93,7 @@ func main() {
|
|||||||
ServerName: servername,
|
ServerName: servername,
|
||||||
Silent: silent,
|
Silent: silent,
|
||||||
NextProtos: alpns,
|
NextProtos: alpns,
|
||||||
|
SSHFallbackPort: sshFallbackPort,
|
||||||
}
|
}
|
||||||
|
|
||||||
remote := strings.Split(remotestr, ":")
|
remote := strings.Split(remotestr, ":")
|
||||||
|
|||||||
66
sclient.go
66
sclient.go
@ -20,23 +20,18 @@ type Tunnel struct {
|
|||||||
NextProtos []string
|
NextProtos []string
|
||||||
ServerName string
|
ServerName string
|
||||||
Silent bool
|
Silent bool
|
||||||
|
SSHFallbackPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialAndListen will create a test TLS connection to the remote address and then
|
// DialAndListen will create a test TLS connection to the remote address and then
|
||||||
// begin listening locally. Each local connection will result in a separate remote connection.
|
// begin listening locally. Each local connection will result in a separate remote connection.
|
||||||
func (t *Tunnel) DialAndListen() error {
|
func (t *Tunnel) DialAndListen() error {
|
||||||
remote := t.RemoteAddress + ":" + strconv.Itoa(t.RemotePort)
|
remote := t.RemoteAddress + ":" + strconv.Itoa(t.RemotePort)
|
||||||
conn, err := tls.Dial("tcp", remote,
|
testConn, _, testErr := t.dialRemote(remote)
|
||||||
&tls.Config{
|
if testErr != nil {
|
||||||
ServerName: t.ServerName,
|
fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, testErr)
|
||||||
NextProtos: t.NextProtos,
|
|
||||||
InsecureSkipVerify: t.InsecureSkipVerify,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, err)
|
|
||||||
} else {
|
} else {
|
||||||
_ = conn.Close()
|
_ = testConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// use stdin/stdout
|
// use stdin/stdout
|
||||||
@ -142,13 +137,7 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
|
func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
|
||||||
sclient, err := tls.Dial("tcp", remote,
|
upstream, fallback, err := t.dialRemote(remote)
|
||||||
&tls.Config{
|
|
||||||
ServerName: t.ServerName,
|
|
||||||
NextProtos: t.NextProtos,
|
|
||||||
InsecureSkipVerify: t.InsecureSkipVerify,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
|
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
@ -156,15 +145,46 @@ func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !t.Silent {
|
if !t.Silent {
|
||||||
|
target := fmt.Sprintf("%s:%d", t.RemoteAddress, t.RemotePort)
|
||||||
|
if fallback {
|
||||||
|
target = t.RemoteAddress + ":" + strconv.Itoa(t.SSHFallbackPort)
|
||||||
|
}
|
||||||
if conn.RemoteAddr().Network() == "stdio" {
|
if conn.RemoteAddr().Network() == "stdio" {
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "(connected to %s:%d and reading from %s)\n",
|
_, _ = fmt.Fprintf(os.Stdout, "(connected to %s and reading from %s)\n",
|
||||||
t.RemoteAddress, t.RemotePort, conn.RemoteAddr().String())
|
target, conn.RemoteAddr().String())
|
||||||
} else {
|
} else {
|
||||||
_, _ = fmt.Fprintf(os.Stdout, "[connect] %s => %s:%d\n",
|
_, _ = fmt.Fprintf(os.Stdout, "[connect] %s => %s\n",
|
||||||
strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), t.RemoteAddress, t.RemotePort)
|
strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go pipe(conn, sclient, "local")
|
go pipe(conn, upstream, "local")
|
||||||
pipe(sclient, conn, "remote")
|
pipe(upstream, conn, "remote")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) dialRemote(remote string) (netReadWriteCloser, bool, error) {
|
||||||
|
tlsConn, err := tls.Dial("tcp", remote,
|
||||||
|
&tls.Config{
|
||||||
|
ServerName: t.ServerName,
|
||||||
|
NextProtos: t.NextProtos,
|
||||||
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
return tlsConn, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.SSHFallbackPort <= 0 {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackAddr := t.RemoteAddress + ":" + strconv.Itoa(t.SSHFallbackPort)
|
||||||
|
if !t.Silent {
|
||||||
|
fmt.Fprintf(os.Stderr, "[info] TLS+ssh failed (%s), falling back to %s\n", err, fallbackAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpConn, tcpErr := net.Dial("tcp", fallbackAddr)
|
||||||
|
if tcpErr != nil {
|
||||||
|
return nil, false, fmt.Errorf("TLS failed: %w; fallback to %s also failed: %v", err, fallbackAddr, tcpErr)
|
||||||
|
}
|
||||||
|
return tcpConn, true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user