ssh 隧道技术窥探

382 阅读9分钟

在介绍ssh隧道之前,我们先回顾一下,我们经常使用的ssh命令,看看它的一些重要的命令选项:

  • -1 Use protocol version 1 only.

  • -2 Use protocol version 2 only.

  • -4 Use IPv4 addresses only.

  • -6 Use IPv6 addresses only.

  • -A Enable forwarding of the authentication agent connection.

  • -a Disable forwarding of the authentication agent connection.

  • -C Use data compression

  • -c cipher_spec Selects the cipher specification for encrypting the session.

  • -D [bind_address:]port Dynamic application-level port forwarding. This allocates a socket to listen to port on the local side. When a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine.

  • -E log_file Append debug logs to log_file instead of standard error.

  • -F configfile Specifies a per-user configuration file. The default for the per-user configuration file is ~/.ssh/config.

  • -g Allows remote hosts to connect to local forwarded ports.

  • -i identity_file A file from which the identity key (private key) for public key authentication is read.

  • -J [user@]host[:port] Connect to the target host by first making a ssh connection to the pjump host[(/iam/jump-host) and then establishing a TCP forwarding to the ultimate destination from there.

  • -l login_name Specifies the user to log in as on the remote machine.

  • -p port Port to connect to on the remote host.

  • -q Quiet mode.

  • -V Display the version number.

  • -v Verbose mode.

  • -X Enables X11 forwarding.

我们常常使用ssh root@1.1.1.1这样的命令来登录服务器,复杂一点的,或许增加上-i 使用秘钥来登录。这仅仅是作为客户端来进行连接操作的基本方式。如何使用更加复杂的ssh功能来实现我们一些高级功能呢,比如我想通过本地代理的方式,来访问服务器上的mysql,redis等等服务。此时就可以用到ssh隧道技术了,又名端口转发。

由于开启端口转发需要配置ssh server,所以稍微插入一下ssh server配置文件的配置方式。OpenSSH服务器启动时读取配置文件。通常,此文件是/etc/ssh/sshd_config,但是在启动sshd时,可以使用-f命令行选项更改位置.有些公司使用不同的端口号运行多个SSH服务器,使用此选项为每个服务器指定不同的配置文件。

image.png OpenSSH服务器配置文件中的AllowTcpForwarding选项必须在服务器上启用,才能允许端口转发。缺省情况下,允许转发。此选项的可能值为yes或all,表示允许所有TCP转发;no表示阻止所有TCP转发;local表示允许本地转发;remote表示允许远程转发.另一个有趣的选项是AllowStreamLocalForwarding,它可用于转发Unix域套接字。它允许与AllowTcpForwarding相同的值。默认为yes。其他情况的实例:

    AllowTcpForwarding remote     AllowStreamLocalForwarding no
    

那到底什么是ssh tunnel呢?

确切地说,ssh tunnel 是一个通过ssh连接传输任意网络数据的方法。它可用于将加密添加到旧应用程序中。它也可用于实现VPN(虚拟专用网络)和跨防火墙访问Intranet服务。

SSH是通过不信任网络的安全远程登录和文件传输的标准。它还提供了一种使用端口转发保护任何给定应用程序的数据流量的方法,基本上是通过SSH上的任何TCP/IP端口进行隧穿。这意味着应用程序数据流量被引导到加密的SSH连接中,这样在传输过程中就不会被窃听或拦截。SSH隧道允许向本地不支持加密的遗留应用程序添加网络安全性。

image.png

该图展示了SSH隧道的简化概述。SSH客户端与SSH服务器之间通过非可信网络建立安全连接。这个SSH连接是加密的,保护机密性和完整性,并对通信各方进行身份验证。SSH连接用于应用程序连接到应用服务器。启用隧道功能后,应用程序连接到SSH客户端侦听的本地主机上的一个端口。然后SSH客户端通过其加密隧道将应用程序转发到服务器。然后服务器连接到实际的应用程序服务器—通常与SSH服务器在同一台机器上或位于同一数据中心。因此,应用程序通信是安全的,而不必修改应用程序或最终用户工作流。

SSH端口转发是SSH中的一种机制,用于将应用程序端口从客户机传输到服务器,反之亦然。它可以用于向遗留应用程序添加加密,通过防火墙,运维人员使用它从他们的家庭机器打开进入内部网络的后门。 它可以用于向遗留应用程序添加加密,通过防火墙,运维人员使用它从他们的家庭机器打开进入内部网络的后门。

  • 本地转发

本地转发用于将端口从客户机转发到服务器。基本上,SSH客户端在配置的端口上侦听连接,当它接收到连接时,它通过隧道将连接连接到SSH服务器。服务器连接到已配置的目标端口,可能位于与SSH服务器不同的机器上。

本地端口转发的典型用途包括:

  1. 隧道会话和通过跳服务器的文件传输

  2. 从外部连接到内部网络上的服务

  3. 通过Internet连接到远程文件共享

很多公司通过单个跳板机对所有传入的SSH访问。

许多跳转服务器允许传入端口转发,一旦连接被验证。这种端口转发很方便,因为它允许精通技术的用户相当透明地使用内部资源。

  ssh -L 80:intra.example.com:80 gw.example.com

本例打开到gw.example.com跳转服务器的连接,并将任何到本地机器上的端口80的连接转发到intr.example.com上的端口80。默认情况下,任何人(甚至在不同的机器上)都可以连接到SSH客户端机器上的指定端口。但是,可以通过提供绑定地址将此限制为同一主机上的程序

ssh -L 127.0.0.1:80:intra.example.com:80 gw.example.com

  • 远程转发

在OpenSSH中,远程SSH端口转发使用-R选项指定。例如

   ssh -R 8080:localhost:80 public.example.com

这允许远程服务器上的任何人连接到远程服务器上的TCP端口8080。然后,该连接将通过隧道返回到客户端主机,然后客户端与localhost上的端口80建立TCP连接。可以使用任何其他主机名或IP地址代替localhost来指定要连接的主机. 这个特殊的例子对于让外部的人访问内部web服务器非常有用。或者将内部web应用程序暴露给公共互联网。这可能是由在家工作的员工或攻击者完成的. 默认情况下,OpenSSH只允许从服务器主机连接到远程转发端口。但是,服务器配置文件sshd config中的GatewayPorts选项可以用来控制这一点。以下选项是可能的:

  • GatewayPorts no 这样可以防止从服务器计算机外部连接到转发的端口。

  • GatewayPorts yes 这使任何人都可以连接到转发的端口。如果服务器在公共Internet上,则Internet上的任何人都可以连接到端口。

  • GatewayPorts clientspecified 这意味着客户端可以指定允许连接到端口的IP地址。它的语法是:

ssh -R 52.194.1.73:8080:localhost:80 host147.aws.example.com

在本例中,只允许IP地址52.194.1.73连接到8080端口。

OpenSSH还允许将转发的远程端口指定为0。在这种情况下,服务器将动态分配端口并将其报告给客户端。当与-O forward选项一起使用时,客户机将把分配的端口号打印到标准输出.

最后,我们通过一个实例,来看看如何进行端口转发。这里使用golang实现了一个ssh server:

package main

import (
	"fmt"
	"io"
	"log"
	"net"

	"github.com/pires/go-proxyproto"
	gossh "golang.org/x/crypto/ssh"
	"github.com/gliderlabs/ssh"
)

const (
	sshChannelSession     = "session"
	sshChannelDirectTCPIP = "direct-tcpip"

	ChannelTCPIPForward       = "tcpip-forward"
	ChannelCancelTCPIPForward = "cancel-tcpip-forward"
	ChannelForwardedTCPIP     = "forwarded-tcpip"
)

var (
	supportedMACs = []string{"hmac-sha2-256-etm@openssh.com",
		"hmac-sha2-256", "hmac-sha1"}

	supportedKexAlgos = []string{
		"curve25519-sha256", "curve25519-sha256@libssh.org",
		"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
	}
)

func main() {

	log.Println("starting ssh server on port 2222...")

	forwardHandler := &ssh.ForwardedTCPHandler{}

	server := ssh.Server{
		Addr: "localhost:2222",
		Handler: ssh.Handler(func(s ssh.Session) {
			io.WriteString(s, "Remote forwarding available...\n")
			select {}
		}),
		ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
			cfg := gossh.Config{MACs: supportedMACs, KeyExchanges: supportedKexAlgos}
			return &gossh.ServerConfig{Config: cfg}
		},
		LocalPortForwardingCallback:   LocalPortForwardingPermission,
		ReversePortForwardingCallback: ReversePortForwardingPermission,
		ChannelHandlers: map[string]ssh.ChannelHandler{
			sshChannelSession:     ssh.DefaultSessionHandler,
			sshChannelDirectTCPIP: ssh.DirectTCPIPHandler,
		},
		RequestHandlers: map[string]ssh.RequestHandler{
			ChannelTCPIPForward:       forwardHandler.HandleSSHRequest,
			ChannelCancelTCPIPForward: forwardHandler.HandleSSHRequest,
		},
	}

	ln, err := net.Listen("tcp", "localhost:2222")
	if err != nil {
		log.Println(err)
		return
	}
	proxyListener := &proxyproto.Listener{Listener: ln}
	log.Fatal(server.Serve(proxyListener))
}
func ReversePortForwardingPermission(ctx ssh.Context, dstHost string, dstPort uint32) bool {
	log.Println(fmt.Sprintf("Reverse Port Forwarding: %s %s %d", ctx.User(), dstHost, dstPort))
	return true
}

// LocalPortForwardingPermission is a callback function used by the SSH server to determine whether
// a client is allowed to request local port forwarding.
//
// The function takes three parameters:
// - ctx: An ssh.Context object representing the client's connection and session.
// - dstHost: A string representing the destination host for the forwarded connection.
// - dstPort: An uint32 representing the destination port for the forwarded connection.
//
// The function returns a boolean value indicating whether the client is allowed to request local port forwarding.
// If the function returns true, the local port forwarding request is granted.
// If the function returns false, the local port forwarding request is denied.
//
// The function logs the details of the local port forwarding request using the logx.Info function.
func LocalPortForwardingPermission(ctx ssh.Context, dstHost string, dstPort uint32) bool {
	log.Println(fmt.Sprintf("LocalPortForwardingPermission: %s %s %d", ctx.User(), dstHost, dstPort))
	return true
}

此时,我们再本地开启端口转发


ssh -NL 8091:10.2.4.114:16379 root@localhost -p 2222

这样,我们通过这样就能够进行服务访问了

redis-cli -h localhost -p 8091

这里ChannelHandlers就是可以用来进行session handler的地方。如果我们想发起隧道使用,这里可以进行定制化的处理,默认仅提供session类型的handler,可以根据自己的需要进行添加handler.我们来看一下tcp直连的handler.

func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
	d := localForwardChannelData{}
	if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
		newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
		return
	}

	if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) {
		newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
		return
	}

	dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))

	var dialer net.Dialer
	dconn, err := dialer.DialContext(ctx, "tcp", dest)
	if err != nil {
		newChan.Reject(gossh.ConnectionFailed, err.Error())
		return
	}

	ch, reqs, err := newChan.Accept()
	if err != nil {
		dconn.Close()
		return
	}
	go gossh.DiscardRequests(reqs)

	go func() {
		defer ch.Close()
		defer dconn.Close()
		io.Copy(ch, dconn)
	}()
	go func() {
		defer ch.Close()
		defer dconn.Close()
		io.Copy(dconn, ch)
	}()
}

在这个handler里面,可以发现,不仅可以获取远端和源端的地址,还创建了一个ssh client.通过io.Copy在ssh client和隧道ch之间进行数据同步。直到停止。

参考链接: