零、版本履历
日期 | 说明 |
---|---|
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,用法很简单,不清楚有没有别的坑,但可以满足我的需求。
三、入坑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
平台上下发命令,控制现场机开启端口转发。如转发TeamViewer
的5938
端口到服务器13389
端口,并允许现场机Teamviewer
的LAN连接,可在本地TeamViewer
上输入服务器IP:13389,即可远程到现场,如下。
命令下发成功:
现场机响应成功
如果下发如下命令则停止现场机的端口转发。
{"Start":"False"}
五、后记
不仅是TeamViewer
,Windows
系统自带的远程桌面也能用,甚至还可以通过它在家里远程公司电脑,或者调用公司电脑上的部署的服务,临时处理紧急事情。总之,通过端口转发可以随心所欲地干任何想干的事情~
2021年8月16日星期一