一、准备条件
一台公网服务器和一个已备案域名
这里以Centos7为例
# 安装go环境和git环境
yum install gcc mercurial git bzr subversion golang golang-pkg-windows-amd64 golang-pkg-windows-386 -y
二、下载源码
git clone https://github.com/inconshreveable/ngrok.git
三、生成证书
进入到ngrok根目录,xiaoqiangzai.xyz域名换成自己的
cd ngrok
mkdir cert
cd cert
export NGROK_DOMAIN="xiaoqiangzai.xyz"
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=$NGROK_DOMAIN" -days 5000 -out rootCA.pem
openssl genrsa -out device.key 2048
openssl req -new -key device.key -subj "/CN=$NGROK_DOMAIN" -out device.csr
openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000
覆盖原来的证书,提示按 y 确定
cp rootCA.pem ../assets/client/tls/ngrokroot.crt
cp device.crt ../assets/server/tls/snakeoil.crt
cp device.key ../assets/server/tls/snakeoil.key
cd ..
四、生成服务端与客户端
<!--linux服务端/客户端-->
GOOS=linux GOARCH=386 make release-server (32位)
GOOS=linux GOARCH=amd64 make release-server(64位)
GOOS=linux GOARCH=386 make release-client (32位)
GOOS=linux GOARCH=amd64 make release-client(64位)
<!--Mac OS服务端/客户端-->
GOOS=darwin GOARCH=386 make release-server
GOOS=darwin GOARCH=amd64 make release-server
GOOS=darwin GOARCH=386 make release-client
GOOS=darwin GOARCH=amd64 make release-client
<!--windows服务端/客户端-->
GOOS=windows GOARCH=386 make release-server
GOOS=windows GOARCH=amd64 make release-server
GOOS=windows GOARCH=386 make release-client
GOOS=windows GOARCH=amd64 make release-client
这里选择生成Linux服务端和Window客户端
GOOS=linux GOARCH=amd64 make release-server
GOOS=windows GOARCH=amd64 make release-client
生成的文件都在bin目录下
服务端:ngrok
客户端:ngrok.exe
五、启动服务
启动服务端
./ngrokd -domain="xiaoqiangzai.xyz"
参数说明
-httpAddr=":80" http服务的访问端口 默认80
-httpsAddr=":443" https服务的访问端口 默认443
-tunnelAddr=":4443" 客户端连接服务端的端口 默认4443
端口冲突时修改默认端口
# 前台启动
./ngrokd -domain="xiaoqiangzai.xyz" -httpAddr=":88" -httpsAddr=":888" -tunnelAddr=":8888"
# 后台启动
./ngrokd -domain="xiaoqiangzai.xyz" -httpAddr=":88" -httpsAddr=":888" -tunnelAddr=":8888" > ngrok.log 2>&1 &
# 使用证书启动,trust_host_root_certs: true
./ngrokd -domain="xiaoqiangzai.xyz" -tlsKey="/usr/local/ngrok/2_www.xiaoqiangzai.xyz.key" -tlsCrt="/usr/local/ngrok/1_www.xiaoqiangzai.xyz_bundle.crt" -httpAddr=":80" -httpsAddr=":443" -tunnelAddr=":4443" > ngrok.log 2>&1 &
准备客户端配置文件
将生成的客户端ngrok.exe复制到本地,创建配置文件ngrok.cfg
ngrok.cfg
域名和端口要对应服务端
server_addr: "xiaoqiangzai.xyz:4443"
trust_host_root_certs: false
启动客户端
# -subdomain 指定域名前缀,如ngrok为ngrok.xiaoqiangzai.xyz,8888则为映射本地端口
ngrok -config ngrok.cfg -subdomain ngrok 8888
启动多个
ngrok -config=ngrok.cfg -subdomain www 80
# 可以指定IP
ngrok -config=ngrok.cfg -subdomain centosb 192.168.17.101:8888
访问前端页面
六、验证是否成功
外网访问 http://ngrok.xiaoqiangzai.xyz,https://www.xiaoqiangzai.xyz
手机访问
电脑访问
七、配置多个IP和端口
ngrok.cfg
server_addr: "xiaoqiangzai.xyz:8888"
trust_host_root_certs: false
tunnels:
tiktop:
subdomain: tiktop
proto:
http: 80
# 配置http
xxljob:
subdomain: xxljob
proto:
# 配置IP和端口
http: 192.168.17.1:8888
# 配置https
xxljobs:
subdomain: xxljob
proto:
https: 192.168.17.1:8888
# 我的centos-50100
centos-50100:
remote_port: 50100
proto:
tcp: 192.168.17.101:50100
启动命令
# 启动全部
ngrok -config ngrok.cfg start-all
# 启动指定配置
ngrok -config ngrok.cfg start tiktop xxljob
# 启动配置日志
ngrok -config ngrok.cfg -log log/ngrok.log start-all
配置服务器转发
tunnels:
centos:
remote_port: 50001
proto:
tcp: 192.168.17.101:22
Linux客户端
ngrok.cfg
server_addr: "xiaoqiangzai.xyz:4443"
trust_host_root_certs: true
auth_token: qiang:xxxxxxx
tunnels:
# 我的博客
solo:
subdomain: "www"
proto:
https: 123.57.188.214:8080
# 后台启动
nohup ./ngrok -config=ngrok.cfg -log=stdout start-all &
# 停止服务端
ps -ef | grep ngrokd | grep -v grep | awk '{print $2}' | xargs kill -9
# 停止客户端
ps -ef | grep ngrok.cfg | grep -v grep | awk '{print $2}' | xargs kill -9
八、添加认证
当前只要知道地址,拥有客户端都可以使用,所以我们要添加一个简单的认证。
修改源码ngrok/src/ngrok/server/control.go
源码control.go
package server
import (
"fmt"
"io"
"ngrok/conn"
"ngrok/msg"
"ngrok/util"
"ngrok/version"
"runtime/debug"
"strings"
"time"
)
const (
pingTimeoutInterval = 30 * time.Second
connReapInterval = 10 * time.Second
controlWriteTimeout = 10 * time.Second
proxyStaleDuration = 60 * time.Second
proxyMaxPoolSize = 10
)
type Control struct {
// auth message
auth *msg.Auth
// actual connection
conn conn.Conn
// put a message in this channel to send it over
// conn to the client
out chan (msg.Message)
// read from this channel to get the next message sent
// to us over conn by the client
in chan (msg.Message)
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
// all of the tunnels this control connection handles
tunnels []*Tunnel
// proxy connections
proxies chan conn.Conn
// identifier
id string
// synchronizer for controlled shutdown of writer()
writerShutdown *util.Shutdown
// synchronizer for controlled shutdown of reader()
readerShutdown *util.Shutdown
// synchronizer for controlled shutdown of manager()
managerShutdown *util.Shutdown
// synchronizer for controller shutdown of entire Control
shutdown *util.Shutdown
}
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
var err error
// create the object
c := &Control{
auth: authMsg,
conn: ctlConn,
out: make(chan msg.Message),
in: make(chan msg.Message),
proxies: make(chan conn.Conn, 10),
lastPing: time.Now(),
writerShutdown: util.NewShutdown(),
readerShutdown: util.NewShutdown(),
managerShutdown: util.NewShutdown(),
shutdown: util.NewShutdown(),
}
failAuth := func(e error) {
_ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()})
ctlConn.Close()
}
// register the clientid
c.id = authMsg.ClientId
if c.id == "" {
// it's a new session, assign an ID
if c.id, err = util.SecureRandId(16); err != nil {
failAuth(err)
return
}
}
// set logging prefix
ctlConn.SetType("ctl")
ctlConn.AddLogPrefix(c.id)
if authMsg.Version != version.Proto {
failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version))
return
}
// register the control
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
replaced.shutdown.WaitComplete()
}
// start the writer first so that the following messages get sent
go c.writer()
// Respond to authentication
c.out <- &msg.AuthResp{
Version: version.Proto,
MmVersion: version.MajorMinor(),
ClientId: c.id,
}
// As a performance optimization, ask for a proxy connection up front
c.out <- &msg.ReqProxy{}
// manage the connection
go c.manager()
go c.reader()
go c.stopper()
}
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") {
tunnelReq := *rawTunnelReq
tunnelReq.Protocol = proto
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(&tunnelReq, c)
if err != nil {
c.out <- &msg.NewTunnel{Error: err.Error()}
if len(c.tunnels) == 0 {
c.shutdown.Begin()
}
// we're done
return
}
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// acknowledge success
c.out <- &msg.NewTunnel{
Url: t.url,
Protocol: proto,
ReqId: rawTunnelReq.ReqId,
}
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
}
}
func (c *Control) manager() {
// don't crash on panics
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the control manager stops
defer c.shutdown.Begin()
// notify that manager() has shutdown
defer c.managerShutdown.Complete()
// reaping timer for detecting heartbeat failure
reap := time.NewTicker(connReapInterval)
defer reap.Stop()
for {
select {
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
c.shutdown.Begin()
}
case mRaw, ok := <-c.in:
// c.in closes to indicate shutdown
if !ok {
return
}
switch m := mRaw.(type) {
case *msg.ReqTunnel:
c.registerTunnel(m)
case *msg.Ping:
c.lastPing = time.Now()
c.out <- &msg.Pong{}
}
}
}
}
func (c *Control) writer() {
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the writer() stops
defer c.shutdown.Begin()
// notify that we've flushed all messages
defer c.writerShutdown.Complete()
// write messages to the control channel
for m := range c.out {
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := msg.WriteMsg(c.conn, m); err != nil {
panic(err)
}
}
}
func (c *Control) reader() {
defer func() {
if err := recover(); err != nil {
c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the reader stops
defer c.shutdown.Begin()
// notify that we're done
defer c.readerShutdown.Complete()
// read messages from the control channel
for {
if msg, err := msg.ReadMsg(c.conn); err != nil {
if err == io.EOF {
c.conn.Info("EOF")
return
} else {
panic(err)
}
} else {
// this can also panic during shutdown
c.in <- msg
}
}
}
func (c *Control) stopper() {
defer func() {
if r := recover(); r != nil {
c.conn.Error("Failed to shut down control: %v", r)
}
}()
// wait until we're instructed to shutdown
c.shutdown.WaitBegin()
// remove ourself from the control registry
controlRegistry.Del(c.id)
// shutdown manager() so that we have no more work to do
close(c.in)
c.managerShutdown.WaitComplete()
// shutdown writer()
close(c.out)
c.writerShutdown.WaitComplete()
// close connection fully
c.conn.Close()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// shutdown all of the proxy connections
close(c.proxies)
for p := range c.proxies {
p.Close()
}
c.shutdown.Complete()
c.conn.Info("Shutdown complete")
}
func (c *Control) RegisterProxy(conn conn.Conn) {
conn.AddLogPrefix(c.id)
conn.SetDeadline(time.Now().Add(proxyStaleDuration))
select {
case c.proxies <- conn:
conn.Info("Registered")
default:
conn.Info("Proxies buffer is full, discarding.")
conn.Close()
}
}
// Remove a proxy connection from the pool and return it
// If not proxy connections are in the pool, request one
// and wait until it is available
// Returns an error if we couldn't get a proxy because it took too long
// or the tunnel is closing
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
var ok bool
// get a proxy connection from the pool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
default:
// no proxy available in the pool, ask for one over the control channel
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
return
}
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
case <-time.After(pingTimeoutInterval):
err = fmt.Errorf("Timeout trying to get proxy connection")
return
}
}
return
}
// Called when this control is replaced by another control
// this can happen if the network drops out and the client reconnects
// before the old tunnel has lost its heartbeat
func (c *Control) Replaced(replacement *Control) {
c.conn.Info("Replaced by control: %s", replacement.conn.Id())
// set the control id to empty string so that when stopper()
// calls registry.Del it won't delete the replacement
c.id = ""
// tell the old one to shutdown
c.shutdown.Begin()
}
在 func NewControl(){} 内添加了一个方法
import (
//省略
//多两个包
"os"
"bufio"
)
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
//省略
//读取文件方法
readLine := func(token string, filename string) (bool, error) {
if token == "" {
return false, nil;
}
f, err := os.Open(filename)
if err != nil {
return false, err
}
buf := bufio.NewReader(f)
for {
line, err := buf.ReadString('\n')
line = strings.TrimSpace(line)
if line == token {
return true, nil
}
if err != nil {
if err == io.EOF {
return false, nil
}
return false, err
}
}
return false, nil
}
//省略
//调用验证
authd, err := readLine(authMsg.User, "authtokens.txt")
if authd != true {
failAuth(fmt.Errorf("authtoken %s invalid", "is"));
return
}
//省略
}
修改后的control.go
package server
import (
"fmt"
"io"
"ngrok/conn"
"ngrok/msg"
"ngrok/util"
"ngrok/version"
"runtime/debug"
"strings"
"time"
//多两个包
"os"
"bufio"
)
const (
pingTimeoutInterval = 30 * time.Second
connReapInterval = 10 * time.Second
controlWriteTimeout = 10 * time.Second
proxyStaleDuration = 60 * time.Second
proxyMaxPoolSize = 10
)
type Control struct {
// auth message
auth *msg.Auth
// actual connection
conn conn.Conn
// put a message in this channel to send it over
// conn to the client
out chan (msg.Message)
// read from this channel to get the next message sent
// to us over conn by the client
in chan (msg.Message)
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
// all of the tunnels this control connection handles
tunnels []*Tunnel
// proxy connections
proxies chan conn.Conn
// identifier
id string
// synchronizer for controlled shutdown of writer()
writerShutdown *util.Shutdown
// synchronizer for controlled shutdown of reader()
readerShutdown *util.Shutdown
// synchronizer for controlled shutdown of manager()
managerShutdown *util.Shutdown
// synchronizer for controller shutdown of entire Control
shutdown *util.Shutdown
}
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
var err error
// create the object
c := &Control{
auth: authMsg,
conn: ctlConn,
out: make(chan msg.Message),
in: make(chan msg.Message),
proxies: make(chan conn.Conn, 10),
lastPing: time.Now(),
writerShutdown: util.NewShutdown(),
readerShutdown: util.NewShutdown(),
managerShutdown: util.NewShutdown(),
shutdown: util.NewShutdown(),
}
failAuth := func(e error) {
_ = msg.WriteMsg(ctlConn, &msg.AuthResp{Error: e.Error()})
ctlConn.Close()
}
//读取文件方法
readLine := func(token string, filename string) (bool, error) {
if token == "" {
return false, nil;
}
f, err := os.Open(filename)
if err != nil {
return false, err
}
buf := bufio.NewReader(f)
for {
line, err := buf.ReadString('\n')
line = strings.TrimSpace(line)
if line == token {
return true, nil
}
if err != nil {
if err == io.EOF {
return false, nil
}
return false, err
}
}
return false, nil
}
// register the clientid
c.id = authMsg.ClientId
if c.id == "" {
// it's a new session, assign an ID
if c.id, err = util.SecureRandId(16); err != nil {
failAuth(err)
return
}
}
// set logging prefix
ctlConn.SetType("ctl")
ctlConn.AddLogPrefix(c.id)
if authMsg.Version != version.Proto {
failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version))
return
}
//调用验证
authd, err := readLine(authMsg.User, "authtokens.txt")
if authd != true {
failAuth(fmt.Errorf("authtoken %s invalid", "is"));
return
}
// register the control
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
replaced.shutdown.WaitComplete()
}
// start the writer first so that the following messages get sent
go c.writer()
// Respond to authentication
c.out <- &msg.AuthResp{
Version: version.Proto,
MmVersion: version.MajorMinor(),
ClientId: c.id,
}
// As a performance optimization, ask for a proxy connection up front
c.out <- &msg.ReqProxy{}
// manage the connection
go c.manager()
go c.reader()
go c.stopper()
}
// Register a new tunnel on this control connection
func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
for _, proto := range strings.Split(rawTunnelReq.Protocol, "+") {
tunnelReq := *rawTunnelReq
tunnelReq.Protocol = proto
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(&tunnelReq, c)
if err != nil {
c.out <- &msg.NewTunnel{Error: err.Error()}
if len(c.tunnels) == 0 {
c.shutdown.Begin()
}
// we're done
return
}
// add it to the list of tunnels
c.tunnels = append(c.tunnels, t)
// acknowledge success
c.out <- &msg.NewTunnel{
Url: t.url,
Protocol: proto,
ReqId: rawTunnelReq.ReqId,
}
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
}
}
func (c *Control) manager() {
// don't crash on panics
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the control manager stops
defer c.shutdown.Begin()
// notify that manager() has shutdown
defer c.managerShutdown.Complete()
// reaping timer for detecting heartbeat failure
reap := time.NewTicker(connReapInterval)
defer reap.Stop()
for {
select {
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
c.shutdown.Begin()
}
case mRaw, ok := <-c.in:
// c.in closes to indicate shutdown
if !ok {
return
}
switch m := mRaw.(type) {
case *msg.ReqTunnel:
c.registerTunnel(m)
case *msg.Ping:
c.lastPing = time.Now()
c.out <- &msg.Pong{}
}
}
}
}
func (c *Control) writer() {
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the writer() stops
defer c.shutdown.Begin()
// notify that we've flushed all messages
defer c.writerShutdown.Complete()
// write messages to the control channel
for m := range c.out {
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := msg.WriteMsg(c.conn, m); err != nil {
panic(err)
}
}
}
func (c *Control) reader() {
defer func() {
if err := recover(); err != nil {
c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the reader stops
defer c.shutdown.Begin()
// notify that we're done
defer c.readerShutdown.Complete()
// read messages from the control channel
for {
if msg, err := msg.ReadMsg(c.conn); err != nil {
if err == io.EOF {
c.conn.Info("EOF")
return
} else {
panic(err)
}
} else {
// this can also panic during shutdown
c.in <- msg
}
}
}
func (c *Control) stopper() {
defer func() {
if r := recover(); r != nil {
c.conn.Error("Failed to shut down control: %v", r)
}
}()
// wait until we're instructed to shutdown
c.shutdown.WaitBegin()
// remove ourself from the control registry
controlRegistry.Del(c.id)
// shutdown manager() so that we have no more work to do
close(c.in)
c.managerShutdown.WaitComplete()
// shutdown writer()
close(c.out)
c.writerShutdown.WaitComplete()
// close connection fully
c.conn.Close()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// shutdown all of the proxy connections
close(c.proxies)
for p := range c.proxies {
p.Close()
}
c.shutdown.Complete()
c.conn.Info("Shutdown complete")
}
func (c *Control) RegisterProxy(conn conn.Conn) {
conn.AddLogPrefix(c.id)
conn.SetDeadline(time.Now().Add(proxyStaleDuration))
select {
case c.proxies <- conn:
conn.Info("Registered")
default:
conn.Info("Proxies buffer is full, discarding.")
conn.Close()
}
}
// Remove a proxy connection from the pool and return it
// If not proxy connections are in the pool, request one
// and wait until it is available
// Returns an error if we couldn't get a proxy because it took too long
// or the tunnel is closing
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
var ok bool
// get a proxy connection from the pool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
default:
// no proxy available in the pool, ask for one over the control channel
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
return
}
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
case <-time.After(pingTimeoutInterval):
err = fmt.Errorf("Timeout trying to get proxy connection")
return
}
}
return
}
// Called when this control is replaced by another control
// this can happen if the network drops out and the client reconnects
// before the old tunnel has lost its heartbeat
func (c *Control) Replaced(replacement *Control) {
c.conn.Info("Replaced by control: %s", replacement.conn.Id())
// set the control id to empty string so that when stopper()
// calls registry.Del it won't delete the replacement
c.id = ""
// tell the old one to shutdown
c.shutdown.Begin()
}
重新编译服务端
GOOS=linux GOARCH=amd64 make release-server
在同级目录下创建authtokens.txt
touch authtokens.txt
# 将账号密码添加到文件
echo "username:password" > authtokens.txt
重新启动服务端即可,客户端需要配置账号密码否则无法连接
server_addr: "xiaoqiangzai.xyz:8888"
trust_host_root_certs: false
auth_token: username:password
tunnels:
nacos:
subdomain: nacos
proto:
http: 192.168.17.101:8848
xxljob:
subdomain: xxljob
proto:
http: 192.168.17.1:8888
fileupload:
subdomain: fileupload
proto:
http: 127.0.0.1:8080
九、常见问题
防火墙问题:关闭防火墙或者暴露相应的端口
域名解析问题:添加三种解析
端口占用问题:修改默认端口,但是修改