基于SSH端口转发实现内网客户机的远程

Posted 钩鸿踏月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于SSH端口转发实现内网客户机的远程相关的知识,希望对你有一定的参考价值。

零、版本履历

日期说明
2021.08.16初稿

一、一个看似奇葩的需求

最近项目上有这么个需求,看似奇葩,但很有必要。

现场通过4G物联网卡上网,并且只开通了部分IP白名单,要求实现远程到现场。换句话说,通过这个4G物联网卡组的网络就相当于内网,再跟之前一样从外网通过Teamviewer向日葵之类的工具远程访问是不可能的了。此外,客户机没有固定IP,也不可能登录服务器后通过远程桌面连接到现场。

后来想到公司另一个业务组在Linux下有相同场景的的远程经验,经过跟大佬的多次探讨,得出如下方案,经测试在Windows下可行。

二、通过OpenSSH的端口转发实现

主要思路是利用SSH的端口远程转发功能,将现场客户机的远程桌面端口、或者Teamviewer端口转发到服务器指定端口,此时远程服务器即实现远程到现场。

本文的目的是想聊聊端口转发在物联网项目上的实际应用,端口转发的命令很简单,网上资料也很多,此处不再赘述。

比如现场安装了OpenSSH后,执行如下命令,密码确认通过后即可打开端口转发。

ssh -C -g -NR 13389:127.0.0.1:9880 Administrator@192.168.1.240

直接通过SSH命令打开或关闭端口转发,都必须有人在现场机上操作,既不方便,也不现实。我想实现无人值守,有问题能够及时远程,不需要人工干预。只能从程序角度出发,改造现有与Iot平台通讯的MQTT程序,增加远程端口转发参数的下发与响应,即可实现通过Iot平台打开或关闭指定设备的端口转发功能。

使用OpenSSH有个问题,需要两个步骤,先执行转发命令,再输入密码确认通过后,端口转发才真正打开。经测试无法在C#里通过Process实现密码的输入,看来这条路走不下去了。

后来想到既然使用微软全家桶开发,何不在Nuget上找找有没有相应的开发库呢?于是根据下载量找到了SSH.NET这个库,没有详细的文档,看了下Demo,用法很简单,不清楚有没有别的坑,但可以满足我的需求。

GitHub - sshnet/SSH.NET: SSH.NET is a Secure Shell (SSH) library for .NET, optimized for parallelism.

三、入坑SSH.NET

首先定义如下命令格式:

{
	"Start": "True",                                   // true,开启端口转发,false,关闭端口转发
	"Params": {
		"Server": "xxx.xx.xx.xxx",                     // 服务器
        "Port": 22,                                    // 服务器端口,默认端口22,可以省略
		"UserAccount": "xxx",                          // 服务器ssh账户
		"UserPassword": "xxx**..123",                  // 服务器ssh账户密码
		"ForwardPort": 5938,                           // 待转发端口
		"TargetPort": 13389                            // 转发后的目标端口
	}
}

MQTT程序中定义主题,其中AuthCode为站点唯一识别码,Iot平台根据站点识别码推送消息到现场机。

public static readonly string ForwardPortRemote = $"110/{AppConfig.AuthCode}/$forwardportremote";

简单封装了SSH服务,提供开启/关闭端口转发功能,同时只支持连接一台服务器,转发一个端口。如果两次下发的都是打开端口转发命令,则先关闭上一个转发,再打开新的转发。

class SshService
{
    /// <summary>
    /// SSH客户端
    /// </summary>
    private SshClient m_SshClient = null;

    /// <summary>
    /// 远程转发端口
    /// </summary>
    private ForwardedPortRemote m_ForwardedPortRemote = null;

    /// <summary>
    /// 打开端口转发
    /// </summary>
    /// <param name="param"></param>
    public void StartForwardPort(ForwardedPortRemoteCommandParams param)
    {
        //必须先停止已有的端口转发
        StopForwardPort();

        m_SshClient = new SshClient(param.Server, param.Port, param.UserAccount, param.UserPassword);
        m_SshClient.Connect();

        m_ForwardedPortRemote = new ForwardedPortRemote(param.TargetPort, "127.0.0.1", param.ForwardPort);
        m_SshClient.AddForwardedPort(m_ForwardedPortRemote);
        m_ForwardedPortRemote.Start();
    }

    /// <summary>
    /// 停止已有的端口转发
    /// </summary>
    public void StopForwardPort()
    {
        if (m_SshClient != null)
        {
            if (m_ForwardedPortRemote != null)
            {
                if (m_ForwardedPortRemote.IsStarted)
                {
                    m_ForwardedPortRemote.Stop();
                }

                m_SshClient.RemoveForwardedPort(m_ForwardedPortRemote);
                m_ForwardedPortRemote.Dispose();
                m_ForwardedPortRemote = null;
            }

            if (m_SshClient.IsConnected)
            {
                m_SshClient.Disconnect();
                m_SshClient.Dispose();
                m_SshClient = null;
            }
        }
    }
}

这边有个地方要注意,SshClient.AddForwardedPort()后还需要执行ForwardedPortRemote.Start()方法,否则端口转发并没有真正开启。这一点没有文档说明,我翻看类定义后试出来的。

在MQTT的通知事件的订阅方法中处理端口转发命令

private void OnReceive(string topic, string desc, string content)
{
    try
    {
        if (topic == DaatsMqttClient.DaatsMqttTopics.UpdaterResponse)
        {
            ……
        }
        else if (topic == DaatsMqttClient.DaatsMqttTopics.ForwardPortRemote) // 开启/关闭远程访问
        {
            ForwardedPortRemoteCommand command = ForwardedPortRemoteCommand.Parse(content);
            if (command == null) { return; }

            LogTool.Current.AddLog(LogLevel.Info, $"【{desc}】准备{(command.Start ? "启动" : "停止")}");

            if (command.Start)
            {
                m_SshService.StartForwardPort(command.Params);
                LogTool.Current.AddLog(LogLevel.Info, $"【{desc}】启动成功:{command.Params.ForwardPort} -> {command.Params.Server}:{command.Params.TargetPort}");
            }
            else
            {
                m_SshService.StopForwardPort();
                LogTool.Current.AddLog(LogLevel.Info, $"【{desc}】停止成功");
            }
        }
    }
    catch (Exception ex)
    {
        LogUtil.WriteLog($"{this.GetType()}.{nameof(OnReceive)}", ex);
    }
}

四、效果

此时在Iot平台上下发命令,控制现场机开启端口转发。如转发TeamViewer5938端口到服务器13389端口,并允许现场机Teamviewer的LAN连接,可在本地TeamViewer上输入服务器IP:13389,即可远程到现场,如下。

命令下发成功:

现场机响应成功

如果下发如下命令则停止现场机的端口转发。

{"Start":"False"}

五、后记

不仅是TeamViewerWindows系统自带的远程桌面也能用,甚至还可以通过它在家里远程公司电脑,或者调用公司电脑上的部署的服务,临时处理紧急事情。总之,通过端口转发可以随心所欲地干任何想干的事情~

2021年8月16日星期一

以上是关于基于SSH端口转发实现内网客户机的远程的主要内容,如果未能解决你的问题,请参考以下文章

利用SSH端口转发实现远程访问内网主机远程桌面 SSH免密登陆

ssh的代理和端口转发机制介绍

SSH 内网端口转发实战

利用ssh转发功能做端口映射,实现内网穿透

SSH 远程端口转发

SSH -R 远程端口转发