Hugo+Ngrok+ECS,实同步内外网网络环境

1. 背景

倘若你在本地Mac或者Win环境开发过程中,恰好有一个DEMO或者撰写了一个Markdwon文档,想让另外的同事们看到你的内容,你不想一个个的叫他们到你电脑面前来,你会怎么办?

  1. 截图然后通过IM工具发送,不够直观;
  2. 发布你的代码到公共服务器,改动不够同步实时;
  3. 开发你本地的本地IP和端口网络服务,可能存在网络连通问题;
  4. 开放远程电脑服务,还需要开启相关RDP服务,这个算较为通用的方式,适合多对一的场景,倘若希望多个用户在自己的工位也可以看到,这个就有点困难了;

本文介绍一种将本地网络服务代理到公共网络,然后其他用户可以通过特定的IP或域名,外加端口访问你本地的网络服务。

该行为类似于ssh -R,指定要将远程(服务器)主机上的给定端口,转发到本地端的给定主机和端口。

1.1. ssh的代理/端口转发模型

简单了解下ssh的3个常用代理/端口转发模型:

  • -L: 指定要将本地(客户端)主机上的给定端口转发到远程端上的给定主机和端口;
  • -R: 指定要将远程(服务器)主机上的给定端口转发到本地端的给定主机和端口;
  • -D: 指定本地“动态”应用程序级端口转发。目前支持SOCKS4和SOCKS5协议,并且ssh将充当SOCKS服务器;

可以自行通过man ssh查看!

1.2. Ngrok

一款流行的TCP网络服务代理工具,但目前官方应用部署由于国内网络问题无法访问;

1.3. Hugo

基于Golang编写的一款静态HTML生成工具,支持将MD文件转换成HTML文件,同时支持多套模板功能。

模板参考: https://themes.gohugo.io/tags/minimal/

1.4. ECS

云服务器,或者公司内部大家都可以公共访问的Linux服务器。

2. 解决方案

  1. ECS上面部署ngrokd服务,提供网络代理功能,目前ngrokd开源的是1.7版本的: https://github.com/inconshreveable/ngrok
  2. MAC机器部署hugoWeb服务;
  3. MAC机器部署ngrok的客户端,加入相关通道配置,将本地的http://127.0.0.1:1313,代理到公网;

3. 安装和使用hugo

// 安装
$ brew install hugo

// 初始化MD站点
$ cd /tmp
$ hugo new site quickstart

// 初始化一个主题样式
$ cd quickstart
$ git init
$ git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke

// 将样式加入配置文件中
$ echo 'theme = "ananke"' >> config.toml

// 创建一个hello.md
$ hugo new posts/hello.md
/tmp/quickstart/content/posts/hello.md created

// 编辑hello.md
$ echo "## Welcome\n> First hugo page" >> ./content/posts/hello.md

// 运行Hugo服务
$ hugo server -D -w -v --debug

...
Serving pages from memory
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

到此,hugo这块基本可以提供服务了,相关的内容可以通过hugo new在content目录下进行创建新的Markdown文件,然后正常编写即可!

请求http://localhost:1313/,就应该可以访问到刚刚我们初始化好的Page

有问题的话,还可以参考官方文档: https://gohugo.io/getting-started/quick-start/

4. ngrokd - 服务端部署

4.1. 证书问题

由于ngrokd是基于Golang开发的,编译过程依赖GO编译环境,另外由于HTTPS代理的话,涉及TLS过程,需要提供证书。

证书的解决方案有几种:

  1. 自签名证书,支持自定义签发时间,但存在其他用户访问提升CA不可信问题,需要让其他用户加入信任CA,有关自签名证书可以查看: 自签名证书
  2. 购买通配符域名,贵+繁琐;
  3. 利用Let's Encrypt生成自签名证书,较繁琐;

4.2. https自签名的证书生成

如果不想使用开源库中server生成服务端公钥和私钥, snakeoil.crt, snakeoil.key需要自行生成,同时还会有自签名证书受信问题!

服务端的证书用于https的TLS的过程。

// 1. 下载编译安装ngrokd
$ cd /usr/local/src
$ git clone https://github.com/inconshreveable/ngrok.git

// 2. 创建一个新的CA证书,稍后用于签发服务端证书
$ cd ngrok && make tls
$ NGROK_DOMAIN="ioio.cool"
$ openssl req -new -x509 -newkey rsa:2048 -keyout ngrok-ca.key -nodes -days 36500 -subj "/CN=$NGROK_DOMAIN" -out ngrok-ca.cert
// 检视证书
$ openssl x509 -in ngrok-ca.cert -text -noout

// 3. 创建一个服务端的证书(先生成私钥,再生成CSR证书签发请求)
$ openssl req -new -newkey rsa:2048 -keyout srv-ngrok.key -nodes -subj "/CN=$NGROK_DOMAIN" -out srv-ngrok.csr
// 检视证书请求
$ openssl req -in srv-ngrok.csr -text -noout

// 4. 利用自己生成的证书,签发证书请求
$ openssl x509 -req -in srv-ngrok.csr -CA ngrok-ca.cert -CAkey ngrok-ca.key -CAcreateserial -days 3650 -out srv-ngrok.cert
Signature ok
subject=/CN=ioio.cool
Getting CA Private Key

// 5. 查看
ngrok-ca.cert:  生成的ca证书
ngrok-ca.key:   生成的ca证书私钥
ngrok-ca.srl:   证书签发序列号(用于吊销,这块暂时可以忽略)
srv-ngrok.csr:  服务端证书请求(签发服务端证书模板用途,签发后可以忽略)
srv-ngrok.cert: 服务端证书(用于HTTPS服务的部署,ngrokd会用到)
srv-ngrok.key:  附带证书私钥(用于HTTPS服务端部署,ngrokd会用到)

4.3. ngrokd的编译

查看Makefile, 直接执行make,则会运行make all标签,即all: fmt client server构建客户端和服务端!

// 直接执行make,则会运行make all质量
 # make
go fmt ngrok/...
bin/go-bindata -nomemcopy -pkg=assets -tags=debug \
    -debug=true \
    -o=src/ngrok/client/assets/assets_debug.go \
    assets/client/...
bin/go-bindata -nomemcopy -pkg=assets -tags=debug \
    -debug=true \
    -o=src/ngrok/server/assets/assets_debug.go \
    assets/server/...
go get -tags 'debug' -d -v ngrok/...
go install -tags 'debug' ngrok/main/ngrok
go install -tags 'debug' ngrok/main/ngrokd

// 服务启动
./bin/ngrokd -tlsCrt ./tls/srv-ngrok.cert -tlsKey ./tls/srv-ngrok.key -domain=ioio.cool -httpAddr=:8480 -httpsAddr=:8433 -tunnelAddr=:8422
// 后台运行
/bin/ngrokd -tlsCrt ./tls/srv-ngrok.cert -tlsKey ./tls/srv-ngrok.key  -domain="ioio.cool" -httpAddr=":8480" -httpsAddr=":8433" -tunnelAddr=":8422" >> /var/log/ngrokd.log&

到此,服务端HTTP、HTTPS、以及TCP通道基本都OK了,但注意以下几点:

  1. ngrokd的编译可以是在MAC,也可以是在ECS。操作系统不同,可以通过直接在ECS上面进行直接编译或者在MAC交叉编译(比如在MAC下指定GOOS=linux, darwin, windows, netbsdGOARCH=amd64, 386, arm, ppc64)后,将服务端的程序和证书同步到ECS;
  2. 运行ngrokd的话,注意将服务端证书srv-ngrok.keysrv-ngrok.cert一起放到ECS上;
  3. 客户端方面需要加入对生成的CA证书的认证;
// 交叉编译
GOOS="darwin" GOARCH="amd64" make
GOOS="linux" GOARCH="amd64" make
// 检索
 # file ./bin/darwin_amd64/ngrok
./bin/darwin_amd64/ngrok: Mach-O 64-bit executable
 # file ./bin/ngrok
./bin/ngrok: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

4.4. ngrok客户端使用

假定上面一切都OK,同时ngrokd在我们的ECS服务端(Linux环境)也正常跑起来了,现在是客户端(MAC)部分;

// 1. 客户端创建~/.ngrok.yml文件,编辑完后保存
server_addr: ioio.cool:8422
trust_host_root_certs: false
tunnels:
  hugo:
    subdomain: hugo
    proto:
        http: 1313
        https: 1313
  181-ssh:
    subdomain: ssh181
    proto:
        tcp: 22
  mac-http:
    subdomain: mac80
    proto:
        http: 80

// 默认情况,ngrok 1.x客户端是读取~/.ngrok,我们可以做一个软连,这样不用每次都指定!
ln -s ~/.ngrok.yml ~/.ngrok

// 2. 运行之前编译好的MAC环境支持的ngrok:
$ ngrok list
hugo
181-ssh
mac-http

// 3. 启动我们的hugo通道代理(HTTP和HTTPS)
$ ngrok start hugo
...
Forwarding                    http://hugo.ioio.cool:8480 -> 127.0.0.1:1313
Forwarding                    https://hugo.ioio.cool:8480 -> 127.0.0.1:1313

4.5. 加入到Systemd系统启动中

注意这里我们为了让我的应用日志输出到/var/log/ngrokd.log,而不是系统的默认journal日志系统,我们采用了下面是shell重定向!

另外,考虑到服务安全问题,我们创建了一个系统用户ngrok专用于该ngrokd转发服务!

// 新增ngrok用户
useradd -r ngrok

// 配置启动服务相关, 将以下内容加入到`/usr/lib/systemd/system/ngrokd.service`
[Unit]
Description=ngrokd server

[Service]
Type=forking
User=ngrok
Group=ngrok
ExecStart=/bin/sh -c '/usr/local/ngrok/bin/ngrokd \
    -tlsCrt /usr/local/ngrok/tls/srv-ngrok.cert \
    -tlsKey /usr/local/ngrok/tls/srv-ngrok.key \
    -domain="ioio.cool" \
    -httpAddr=:8480 \
    -httpsAddr=:8433 \
    -tunnelAddr=:8422 >> /var/log/ngrokd.log 2>&1 &'
Restart=always
KillMode=process

[Install]
WantedBy=multi-user.target

// 执行链接 & 启动服务 & 查看日志
systemctl link ngrokd.service
systemctl start ngrokd.service
systemctl status ngrokd.service
systemctl enable ngrokd.service

// 不出意外,可以看到以下内容(21706是sh启动进程,fork出ngrokd子进程21707启动后退出):
 # systemctl status ngrokd.service
● ngrokd.service - ngrokd server
   Loaded: loaded (/usr/lib/systemd/system/ngrokd.service; enabled; vendor preset: disabled)
   Active: active (running) since Wed 2019-11-13 18:59:15 CST; 2s ago
  Process: 21706 ExecStart=/bin/sh -c /usr/local/ngrok/bin/ngrokd      -tlsCrt /usr/local/ngrok/tls/srv-ngrok.cert      -tlsKey /usr/local/ngrok/tls/srv-ngrok.key      -domain="ioio.cool"      -httpAddr=:8480      -httpsAddr=:8433      -tunnelAddr=:8422 >> /var/log/ngrokd.log 2>&1 & (code=exited, status=0/SUCCESS)
 Main PID: 21707 (ngrokd)
   CGroup: /system.slice/ngrokd.service
           └─21707 /usr/local/ngrok/bin/ngrokd -tlsCrt /usr/local/ngrok/tls/srv-ngrok.c...

Nov 13 18:59:15 tkstorm_web systemd[1]: Starting ngrokd server...
Nov 13 18:59:15 tkstorm_web systemd[1]: Started ngrokd server.

4.6. 到此我们的ngrok以及可以通过外网访本地的mac服务了

但我们还有一个问题,CA的问题,我们上面的配置是trust_host_root_certs: true,其源码,这块后续再来分析其内部实现,暂时先到此!

...
var (
    rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"}
)

...
// configure TLS
if config.TrustHostRootCerts {
    m.Info("Trusting host's root certificates")
    m.tlsConfig = &tls.Config{}
} else {
    m.Info("Trusting root CAs: %v", rootCrtPaths)
    var err error
    if m.tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil {
        panic(err)
    }
}

5. 小结

概要叙述了问题背景,即想开发或者代理本地网络服务到公网,提供给到其他用户使用!

我们采用了hugo构建一个简单web实现,同时利用ngrok部署了一个网络代理服务,提供给到公网访问!

针对后续这块,我自己还有个Idea,结合https://github.com/webslides/webslides做一些更简单的事情!:)