Jblog 是一个基于JAVA;适合JAVA初、中级水平编程人员使用、阅读并修改源代码的个人博客系统,不如来丁丁的 Jblog Git OSChina 瞧瞧?

Windows VPS上部署Ngrok服务器,实现多平台客户端(树莓派)SSH内网穿透

场景需求:家里的几台树莓派通过家用WIFI路由器上网,虽然装了Teamviewer可以远程穿透内网控制图形界面,但远程时屏幕分辨率太小,体验不佳,于是想让树莓派上的SSH也能拥有穿透内网的功能。

前言,关于Ngrok

查阅了很多资料,发现大多数内网穿透功能的实现都与一款Go语言编写的Ngrok项目相关,Ngrok是一整套的服务器+客户端解决方案。因为其是由Go语言编写的,所以天生具备很强的跨平台能力(但目前基本上所有资料都指向在Linux上部署该服务器端)。博主使用的是阿里云的Windows VPS,并且这段时间正好也在使用Go编程,因此这篇文章将具体介绍如何在Windows上部署Ngrok项目,并且在家用树莓派上部署客户端,实现树莓派的SSH内网穿透。

关于Ngrok,值得注意的有以下几点:

  • Ngrok并不仅仅用来22端口的SSH内网穿透,通过简单配置,它可以将遵循TCP/UDP协议的多个端口进行内网穿透。
  • Ngrok是由一台服务器端和一个或者多个客户端组成的体系。
  • Ngrok需要一台部署在公网固定IP上的服务器,最好有可正常指向的域名,Ngrok的服务器端就部署在上面,如本次实验的阿里云主机。
  • Ngrok的客户端装在需要映射的各个没有公网IP的机器上,比如本次实验的树莓派。
  • Ngrok服务器会在接受到Ngrok客户端请求时分配固定或者随机的端口号,将客户端请求的端口与之映射,从而达到内网穿透的目的。
  • 用户在远程使用SSH客户端进行连接时,不需要任何Ngrok程序,对终端用户来说,Ngrok是透明的。
  • Ngrok自身有一台欧洲的公网服务器,供没有VPS的用户注册使用,该服务器并不是以分配端口的方式映射,而是以分配子域名的方式,随机子域名是免费使用的,固定子域名映射是收费的。
  • 普通玩家可以直接使用Ngrok的官网服务器,缺点就是有一点点慢,而且每次客户端重启后子域名会随之变化,无法固定使用。
  • 国内也有一些商家提供了Ngrok服务,比如Sunny-Ngrok,同样是收费的,速度可能快一点。

Ngrok源代码是开源的,部署在Git上,但是博主使用下来略觉繁琐,涉及到很多自认为没必要使用的技术,后期我重新在Git上发布了我重构后的Ngrok项目,配有码云同步镜像,以供其他朋友直接使用诸如go build或者go install简单命令进行跨平台编译和安装。

为了致敬原作者,我们对这些高端的Golang编程技术做一个简单的了解,对Go语言没有兴致的朋友可以直接跳过下面这段:

  • go-bindata 技术,该技术是一套将任意资源文件转成二进制数据反向生成到Go代码供hash调用的技术,Ngrok的源码利用这一技术将公网证书和密钥,以及站点的HTML生成到go代码中去了,使其变成了一堆byte数组,在程序中hash调用。这么做固然有它的好处,但缺点也显而易见,因为这套技术要提前用一个项目的程序生成另一个项目的Go代码,所以编译起来难免繁琐,于是Ngrok提供了makefile文件以供用户make编译,这就有点尴尬了,本来好好的跨平台Go语言项目,被make和makefile活生生憋成了只能Linux使用的了。博主初期使用时硬着头皮安装了Cygwin,以及其中的make命令,个中的苦逼无以言表,索性后来了解原理后,将官方的Ngrok重构了一遍,避免了使用几乎是专属于Linux的make指令。
  • build -tags 技术,通过Ngrok项目,我第一次知道Go的这个特性,可以在代码顶端加上// +build !release等标记,注明编译时的模式,比如前面这种表达方式,就是在非release编译模式下才会编译这个文件,然后使用go build -tags 'release'或者go build -tags 'debug'来控制是否编译这个代码文件。这个特性确实灵活,可以在项目中编写不同的编译模式下的代码,缺点呢,就是没有详细文档的情况下,第三方用户并不知道有哪些编译模式,加大了源码本地编译的复杂度,所以这也是原版Ngrok项目使用makefile文件的原因之一。
  • equinox.io 技术,这项技术可以让Go代码在编译和运行期间保持更新,代码作者更新代码后,能够在第三方用户实时更新,具体没有了解,我看原版Ngrok在进行release编译时增加了很多equinox.io令牌密钥,以连接远程仓库,保证代码更新。

总之,上面的这些特性,虽然优点多多,但使用起来难免加大了难度,并且牺牲了Go优良的跨平台特性,这是我不能忍的,于是我重新构造了Ngrok项目,旨在让更多玩家,在众多平台上简单轻松的编译Ngrok项目。

言归正传,准备工作

下面我们言归正传,从零开始部署Ngrok,说说准备工作:

硬件部分

  • 一台公网IP服务器,本实验采用Windows系统,拥有域名hexcode.cn。
  • 一台装有RASPBIAN操作系统的树莓派,用来部署Ngrok客户端,使其可以SSH内网穿透
  • 一台Windows PC,开发环境

软件部分

  • Win32 OpenSSL v1.1.0f Light证书生成程序
  • MobaXterm几乎是最好的SSH客户端
  • go1.9.2.windows-amd64.msiGo语言编译器,我现在用得最多的语言,一股子的亲切感,交叉编译Linux,ARM,X86等平台的目标程序就跟玩一样
  • Ngrok源码这是我自己重构的Ngrok项目,原版的要想用起来还得安装Cygwin等一堆工具,现在非常纯净,拿到手直接用最简单的命令编译,各平台通吃。这份代码不需要手工download,下面会介绍直接用命令获取。
  • Git-windows-amd64这个就不赘述了,程序猿都知道。

从零开始操作

证书生成

Ngrok需要使用SSL证书确保穿透过程中的通信是加密安全的,因此需要SSL证书,值得庆幸的是并不强制去购买CA认证机构颁发的证书,普通用户直接使用OpenSSL工具就可以自行生成证书。方法如下:

  • 下载上述准备工作软件部分的SSL证书生成程序,安装后,将安装路径下的bin目录添加到环境变量,方便cmd直接运行openssl.exe
  • 任意路径打开cmd,执行下面命令:(注意,将两处MY_DOMAIN全部替换成自己的域名,比如我的hexcode.cn)
    openssl genrsa -out rootCA.key 2048
    openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=MY_DOMAIN" -days 5000 -out rootCA.pem
    openssl genrsa -out device.key 2048
    openssl req -new -key device.key -subj "/CN=MY_DOMAIN" -out device.csr
    openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000
    
  • 全部执行完毕后,会在当前路径下生成6个文件,我们服务器端会用到其中的 snakeoil.crt , snakeoil.key ,客户端会用到 ngrokroot.crt 文件,先不管它们,留到下面使用。

编译Windows平台下的服务器端程序

得益于Go的众多优秀的特性,让一份跨平台的代码能够快速地适配不同平台编译和安装。相比其他语言不同的版本,不同的编译方式,编译工具,甚至有些是超级重量级的,比如JAVA,各个平台下的JDK少说也要300M,Go的编译器全平台覆盖,并且只有90M左右,非常轻巧,并且只需要在某一平台下下载一次,就可以轻松靠着两个环境变量编译出全平台覆盖的可执行程序,交叉编译从未如此简单过。

一聊起Go的优点,我就差点收不住。我们来实际体验一下吧,在上方准备工作软件部分下载Go的Windows平台编译器,按照提示安装,设置Windows环境变量PATH路径添加Go安装目录下的bin文件夹,方便cmd中直接使用go命令,为了还能直接使用go get命令,我们需要安装Git,下载地址在上方,下载回来直接根据提示全部以默认选项安装即可。
另外需要配置GOPATH,GOROOT两个环境变量,这两个并不是必须的,但为了减少麻烦还是建议设置,其中GOROOT变量指向Go的安装路径,比如C:\GoRootGOPATH是我们的工作区路径,随意填写,写完了就在此路径下编写程序就行了,当然如果你仅仅用来完成这个项目,以后不会学习Go也不会编写Go语言的代码,这个路径对你来说仅仅是接下来自动下载源代码的保存路径而已。这里我们简单设置成C:\GoPath
如图所示:

OK,接下来正式下载,编译,和安装Ngrok的服务器端。
你一定会以为这一系列操作很复杂,其实对于Go来说仅仅是一条命令行的指令:

go get github.com/newflydd/ngrok/main/ngrokd

就这么简单,就这么神奇,简单到你不管在什么路径下执行这行命令都没有问题。这行命令的背后可不简单,go首先会去下载我上传到git上的Ngrok代码,然后试着编译,编译过程中它会发现Ngrok项目还七零八落地依赖了其他各个项目,于是Go用递归的方式再去执行go get命令,直到将所有依赖通通下载回来,最终它成功编译后,将生成的目标程序存放在%GOPATH%\bin目录下,默认情况下它是根据当前机器的环境来生成可执行文件,如果做一些简单的调整,它即可以交叉编译不同平台下的可执行文件。

执行过程可能会有些漫长,跟网络环境相关,建议使用全局科学上网工具。
成功编译后,在%GOPATH%\bin目录下会出现ngrokd.exe可执行程序,这就是你要上传到Windows服务器的程序,大小在8M左右,同时你需要上传第一步生成SSL证书中的 snakeoil.crt , snakeoil.key 两份文件到同目录。ngrokd.exe双击直接运行是没有意义的,它需要配有参数,因此我们编写bat脚本来方便运行(将下面的MY_DOMAIN替换成自己的域名,比如我的hexcode.cn):

ngrokd.exe -domain="MY_DOMAIN" -log ./ngrokd.log &

将上面脚本保存为run.bat,双击运行是一个命令行界面,同目录下会出现ngrokd.log,打开看一眼,如果出现以下字样,就说明服务器端程序已经正常运行了:

Listening for public http connections on [::]:80
Listening for public https connections on [::]:443
Listening for control and proxy connections on [::]:4443

运行时的画面如下:

编译Linux ARM平台下的树莓派客户端

试想一下,如果没有交叉编译,我们该如何在树莓派上安装Ngrok客户端:我们必须同样在树莓派上安装Go的Linux ARM版的编译器;RASPBIAN系统已经安装了git,所以我们不需要独立安装git;我们还得用go get指令下载和编译Ngrok的源码;openssl指令不用装了,因为证书已经生成了。

虽然也不是特别复杂,但还是挺费时间的,而且还会占用树莓派的资源,平白无故地要去安装接近90M的Go编译环境。得益于Go的强大,交叉编译发挥了巨大作用,我们在Windows X86 AMD64上就能编译出能在Linux ARM上跑的可执行文件。

打开一个新的cmd命令行,任意路径,执行下面命令:

set GOOS=linux
set GOARCH=arm
go get github.com/newflydd/ngrok/main/ngrok

OK,就这么简单,就这么神奇,Windows AMD64上的Go利用简单的两个环境变量,直接构造出了Linux ARM上的可执行文件,多么伟大的语言。。。
GOOS GOARCH的组合可以参照:

GOOS GOARCH
darwin 386
darwin amd64
freebsd 386
freebsd amd64
linux 386
linux amd64
linux arm
windows 386
windows amd64

在树莓派中运行Ngrok客户端程序

Go将生成的Linux平台下的程序保存在%GOPATH%\bin\linux_arm目录下,名为ngrok,我们需要将其copy到树莓派中。
我们使用超级强大的终端工具MobaXterm来连接树莓派并上传ngrok,MobaXterm的下载地址在本文上方,下载后按照提示就能安装,因为目前树莓派的SSH还不具备内网穿透功能,因此下面操作必须在局域网中完成。

使用MobaXterm建立与树莓派的SSH连接,这里并不是重点,相关操作如果不清楚可以自行百谷。
MobaXterm具有强大的文件上传功能,直接将使用下面图例中的按钮将Windows本地的ngrok客户端程序上传到指定目录,同时将我们第一步SSL证书环节生成的 ngrokroot.crt 文件上传进去。

我这里操作的目录为:/home/pi/workspace/go-apps/ngrok/,此时上传上去的ngrok文件不具备执行能力,需要使用下面命令为其添加执行权限:

chmod a+x ngrok

同样,ngrok程序不可以直接运行,也需要参数,并且还需要一个配置文件,我们执行下面操作新建配置文件,并编写sh脚本,以便直接运行。

nano ngrok.cfg

新建ngrok.cfg配置文件,并用nano文本编辑器打开,输入以下文本(将MY_DOMAIN替换成自己的域名,比如我的hexcode.cn):

server_addr: "MY_DOMAIN:4443"
trust_host_root_certs: false

tunnels:
    ssh:
        remote_port: 50000
        proto:
            tcp: 22

我这里仅仅需要让SSH的22端口具备穿透能力,因此我只配置了ssh任务,指定了远程端口固定为50000,映射本地的SSH22端口。Ctrl+O并回车保存,Ctrl+X退出文本编辑器。

nano run.sh

新建run.sh脚本文件,并用文本编辑器打开,输入以下文本:

nohup ./ngrok -log ./ngrok.ssh.log -config ./ngrok.cfg start ssh &

Ctrl+O并回车保存,Ctrl+X退出文本编辑器。

chmod a+x run.sh

为run.sh添加可执行权限。OK,大功告成,运行./run.sh即可建立树莓派与公网服务器的通信隧道,将其22端口映射到了公网服务器的50000端口,我们接下来就可以在世界任何角落,使用任何SSH工具,连接自己公网服务器的50000端口,访问家中的树莓派了,下图用MobaXterm做一个测试:

QtWebEngine使用体验

折腾了几天QtQtWebEngine,使用起来确实方便,特别是QWebChannel可以直接与内部页面的JS通信。但问题也多,比如直接构建的QtWebEngine无法解析mp4,只支持ogg资源,国内视频网站一个都看不了,不过可以看国外YouTuBe的视频,网上搜了一堆说要ffmpegsumo.dll的,也不知道怎么加,据说要下载Qt的SRC源码重新编译,感觉超级复杂的样子,不想折腾了,直接将资源转格式成ogg了。
另外也不支持WebGL,在WebGL网站上测试一直报错。
前台提示错误:

Hmm. While your browser seems to support WebGL, it is disabled or unavailable. If possible, please ensure that you are running the latest drivers for your video card.

后台提示错误:

[.Offscreen-For-WebGL-06ED1100]GL ERROR :GL_INVALID_OPERATION : glRenderbufferStorageMultisampleCHROMIUM:

貌似是什么开关没有开,具体也不清楚,资料太少。
另外Qt写出来的Web浏览器界面在触摸屏上,双手捏合居然可以放大,这个功能并不是预期的,想要禁用居然研究了好久都没法实现,实在有点郁闷。
(PS:该问题后来使用CSS样式:*{touch-action:none;}解决了)

如果下次遇到类似项目,我想我会尝试一下libcef构建浏览器,还发现了一个基于libcef的go项目,测试了一下,同样无法打开国内的视频网站,也能看YouTuBe,能用WebGL,因为应用层用Go实现的,而我最近对Go是情有独钟,可以试着用用看。这个项目封装好的浏览器,连libcef的依赖在里面,压缩状态只有27M,解压后60M左右,而Qt的打包好,非压缩状态足足有132M。

13行代码使用JAVA去除Win下任意软件标题栏

项目需求将VNC Viewer的标题栏隐藏,也就是将一个远程桌面视口无边框地嵌入屏幕规定区域之中,之前的做法是将VNC Viewer放到屏幕最顶部,设置其XY坐标为负,将标题栏隐藏到视界之外。这样做既不美观,也容易被触摸屏或者鼠标给拖下来。

可以使用下面这些代码,将任意软件的标题栏隐藏(前提是32位系统,32位应用):

package com.newflypig.win32;

import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HWND;

public class DeleteWindowTitle {

    public static void main(String[] args) {
        HWND hWnd = User32.INSTANCE.FindWindow(null, "HMI - VNC Viewer");
        System.out.println(hWnd == null);
        if(hWnd != null){
            User32.INSTANCE.ShowWindow(hWnd, 1);
            int style = User32.INSTANCE.GetWindowLong(hWnd, User32.GWL_STYLE);
            style = style & ~User32.WS_CAPTION & ~User32.WS_SYSMENU & ~User32.WS_SIZEBOX;
            User32.INSTANCE.SetWindowLong(hWnd, User32.GWL_STYLE, style);
            User32.INSTANCE.SetWindowPos(hWnd, new HWND(new Pointer(-1)), 100, 100, 800, 600, 0x0040 | 0x0020);
        }
    }
}

依赖项:jna
gradle配置文件:

apply plugin: "java"
apply plugin: "war"
apply plugin: "eclipse"

sourceCompatibility = 1.8
version = '1.0'

ext{
    jna_version = "4.2.2"
}

configurations {
    provided
}

sourceSets {
    main.compileClasspath += configurations.provided
    test.compileClasspath += configurations.provided
    test.runtimeClasspath += configurations.provided
}

repositories {
    maven {url "http://maven.aliyun.com/nexus/content/groups/public" }
}

dependencies {
    compile(
          "net.java.dev.jna:jna:${jna_version}",
        "net.java.dev.jna:jna-platform:${jna_version}",
    )
}

eclipse.classpath.plusConfigurations += [configurations.provided]

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区