内网穿透的目的:简单来说就是让外网能访问你本地的应用

frp内网穿透实例傻瓜化讲解

一、frp的作用

简单应用来说frp是一个可以实现内网穿透的一个软件,如果你的nas在内网,那么你只需要一个有公网ip的服务器当桥梁,即可通过这个公网服务器来访问家里的nas。

二、下载地址有windows版本也有linux版本https://github.com/fatedier/frp/releases

三、实现的前提

1、nas处于内网,比如小区宽带、移动宽带。

2、有个有公网ip的服务器,比如阿里云的ECS服务器

四、实现原理

1、在阿里云的ECS服务器上安装frp服务端软件,当然你得有固定ip,然后在nas上安装frp客户端软件。

2、这样外网访问nas的时候就是通过阿里云的ECS服务器转发到家里的nas来实现。

五、安装

服务端安装:
知道了上面的原理安装就很简单了,你可以购买个阿里云ECS服务器,会分配你一个固定ip,然后在ECS服务器上安装个windows,然后在windows里面安装frp软件,然后运行即可;当然你也可以在ECS上安装linux系统比如centos、ubuntu等,然后安装linux版本的frp软件。

客户端安装:
在你的nas上面安装frp客户端软件,然后配置下即可。群晖的话论坛有人分享了客户端套件:http://www.gebi1.com/thread-283618-1-1.html

如果是arm构架的可以手动这样安装ssh登录,然后输入sudo -i  切换到root权限

cd /usr/local     #进入这个目录

mkdir -p frp      #建立frp文件夹

cd frp               #进入frp文件夹

wgethttps://github.com/fatedier/frp/ … .1_linux_arm.tar.gz    #下载对应你机器cpu的frp压缩包

tar -zxvf frp_0.16.1_linux_arm.tar.gz    #解压下载好的压缩包

cd frp_0.16.1_linux_arm       #进入解压的压缩包

rm -rf frps                #删除服务端程序frps

rm -rf frps.ini           #删除服务端配置文件frps.ini

vi ./frpc.ini               #编辑客户端配置文件,根据第六配置那设置即可

./frpc -c ./frpc.ini     #运行frpc客户端和配置文件frpc.ini

六、配置

服务端只需要修改frps.ini这个文件就可以了,里面只需要修改端口号没其他修改的了,默认即可

  1. [common]

  2. bind_port =7000

  3. 这是frps.ini里面的配置命令,默认端口号是7000,不需要去改动了。

  4. 客户端就修改frpc.ini这个文件就可以了,

  5. [common]

  6. server_addr =127.0.0.1这里填写你服务端也就是阿里云ECS分配给你的固定ip地址

  7. server_port =7000 这个是端口号,nas上的客户端和阿里云ECS上的服务端要一致

  8. [ssh]如果需要外网ssh访问nas,按照下面配置,你也可以根据需要添加其他访问端口了比如群晖的5000

  9. type = tcp

  10. local_ip =127.0.0.1这里填写你局域网nas的ip地址

  11. local_port =22 这个是ssh默认的端口号

  12. remote_port =6000这个是ssh自定义的端口号

  13. 公网访问内部web服务器以http方式

  14. [web]

  15. type = http         访问协议

  16. local_port =8081  内网web服务的端口号

  17. custom_domains = www.gebi1.com   所绑定的公网服务器域名,一级、二级域名都可以,记得你的域名和ECS服务器绑定!

上面备注文字说明记得删除。

访问方式:

ip:比如ip:5000  这里的ip就是阿里云ECS分配给你的固定ip,端口就是你nas客户端配置的端口号

域名方式:www.gebi1.com:8081

注意:这个成本还是挺高的,购买个阿里云ECS的费用也不低,特别是带宽费用,带宽可以购买和你nas上行速度一样的带宽即可,不然太高速度也没意义,入门配置加4兆带宽,估计一年都要1000以上。

内网穿透的实现和原理解析

需求场景:

       基于微信平台开发服务号,本地移动端测试时,需要在微信平台注册测试号,然后填写接口配置信息,此信息需要你有自己的服务器资源,填写的URL需要正确响应微信发送的Token验证。如何能让外网访问到本地服务器呢,就需要用到内网穿透技术(NAT)。

注意:微信平台只支持80端口和443端口

内网穿透的目的:简单来说就是让外网能访问你本地的应用

几个概念:

1.OSI网络七层协议以及每层的作用

1、物理层:该层包括物理连网媒介,如电缆连线连接器,物理层的协议产生并检测电压以便能够发送和接受携带数据的信号。如中继器、集线器、网线、HUB。

    这一层的数据叫做比特。

2、数据链路层:控制网络层和物理层之间的通信,主要功能是如何在不可靠的物理线路上进行数据的可靠传递。如:网卡、网桥、交换机。

      这一层是和包结构和字段打交道的和事佬。一方面接收来自网络层(第三层)的数据帧并为物理层封装这些帧;另一方面数据链路层把来自物理层的原始数据比特封装到网络层的帧中。起着重要的中介作用。

3、网络层:主要功能是将网络地址翻译成对应的无聊地址,并决定如何将数据从发送方路由到接收方。

如路由器、防火墙、多层交换机。

      网络层确定把数据包传送到其目的地的路径。就是把逻辑网络地址转换为物理地址。如果数据包太大不能通过路径中的一条链路送到目的地,那么网络层的任务就是把这些包分成较小的包。

4、传输层:最重要的层,传输协议同时进行流量控制或者是基于对方可接受数据的快慢程度规定适当的发送速率。包括全双工半双工、流控制、错误恢复服务。同时按照网络能处理的最大尺寸将较长的数据包进行强行分割。进程和端口,TCP UDP协议

5、会话层:负责在网络中的两点之间建立和维护通信。如建立回话、断点续传

       在分开的计算机上的两种应用程序之间建立一种虚拟链接,这种虚拟链接称为会话(session)。会话层通过在数据流中设置检查点而保持应用程序之间的同步。允许应用程序进行通信的名称识别和安全性的工作就由会话层完成。

6、表示层:应该程序和网络之间的翻译官,管理数据的加密和解密。如编码方式,图像编解码、交换机

       定义由应用程序用来交换数据的格式。在这种意义上,表示层也称为转换器(translator)。该层负责协议转换、数据编码和数据压缩。转发程序在该层进行服务操作。

7、应用层:负责对软件提供接口使能网络服务。如应用程序,如FTP、SMTP、HTTP

2.IP地址

网络中唯一定位一台设备的逻辑地址,类似我们的电话号码。

在互联网中我们访问一个网站或使用一个网络服务最终都需要通过IP定位到每一台主机,如访问baidu网站:

其中220.181.112.244就是一个公网的IP地址,他最终指向了一台服务器。

IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

内网IP可以同时出现在多个不同的局域网络中,如A公司的U1用户获得了192.168.0.5,B公司的U3用户也可以获得192.168.0.5;但公网IP是唯一的,因为我们只有一个Internet。

3.域名

域名是IP的别名,便于记忆,域名最终通过DNS解析成IP地址。

P V4是一个32位的数字,IP V6有128位,要记住一串毫无意义的数字非常困难,域名解决了这个问题。

DNS查询过程如下,最终将域名变成IP地址

4.NAT

NAT(Network Address Translation)即网络地址转换,NAT能将其本地地址转换成全球IP地址。

内网的一些主机本来已经分配到了本地IP地址(如局域网DHCP分配的IP),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。

通过使用少量的公有IP 地址代表较多的私有IP 地址的方式,将有助于减缓可用的IP地址空间的枯竭。

NAT不仅能解决了lP地址不足与共享上网的问题,而且还能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机。

多路由器可完成NAT功能。

NAT的实现方式:

静态转换是指将内部网络的私有IP地址转换为公有IP地址,IP地址对是一对一。

动态转换是指将内部网络的私有IP地址转换为公用IP地址时,IP地址是不确定的,是随机的。

端口多路复用(Port address Translation,PAT),内部网络的所有主机均可共享一个合法外部IP地址实现对Internet的访问,从而可以最大限度地节约IP地址资源。同时又可隐藏网络内部的所有主机,有效避免来自internet的攻击。因此,目前网络中应用最多的就是端口多路复用方式。

应用程序级网关技术(Application Level Gateway)ALG:传统的NAT技术只对IP层和传输层头部进行转换处理,ALG它能对这些应用程序在通信时所包含的地址信息也进行相应的NAT转换

5. Proxy

Proxy即代理,被广泛应用于计算机领域,主要分为正向代理与反向代理:

 正向代理

比如X花店代A,B,C,D,E五位男生向Candy女生送匿名的生日鲜花,这里的X花店就是5位顾客的代理,花店代理的是客户,隐藏的是客户。这就是我们常说的代理。

正向代理隐藏了真实的请求客户端。服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求,某些科学上网工具扮演的就是典型的正向代理角色。用浏览器访问http://www.google.com时被墙了,于是你可以在国外搭建一台代理服务器,让代理帮我去请求google.com,代理把请求返回的相应结构再返回给我。

当多个客户端访问服务器时服务器不知道真正访问自己的客户端是那一台。正向代理中,proxy和client同属一个LAN,对server透明;

反向代理

拨打10086客服电话,接线员可能有很多个,调度器会智能的分配一个接线员与你通话。这里的调度器就是一个代理,只不过他代理的是接线员,客户端不能确定真正与自己通话的人,隐藏与保护的是目标对象。

反向代理隐藏了真实的服务端,当我们请求 ww.baidu.com 的时候,就像拨打10086一样,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,ww.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。Nginx就是性能非常好的反向代理服务器,用来做负载均衡。

反向代理中,proxy和server同属一个LAN,对client透明。

6. DDNS

DDNS即动态域名解析,是将用户的动态IP地址映射到一个固定的域名解析服务上,用户每次连接网络的时候,客户端程序就会通过信息传递把该主机的动态IP地址传送给位于服务商主机上的服务器程序,服务程序负责提供DNS服务并实现动态域名解析。就是说DDNS捕获用户每次变化的IP地址,然后将其与域名相对应,这样域名就可以始终解析到非固定IP的服务器上,互联网用户通过本地的域名服务器获得网站域名的IP地址,从而可以访问网站的服务。

7. 为什么需要内网穿透

当内网中的主机没有静态IP地址要被外网稳定访问时可以使用内网穿透

在互联网中唯一定位一台主机的方法是通过公网的IP地址,但固定IP是一种非常稀缺的资源,不可能给每个公司都分配一个,且许多中小公司不愿意为高昂的费用买单,多数公司直接或间接的拨号上网,电信部门会给接入网络的用户分配IP地址,以前上网用户少的时候基本分配的都是临时的静态IP地址,租约过了之后可能会更换成另一个IP地址,这样外网访问就不稳定,因为内网的静态IP地址一直变化,为了解决这个问题可以使用动态域名解析的办法变换域名指向的静态IP地址。但是现在越来越多的上网用户使得临时分配的静态IP地址也不够用了,电信部门开始分配一些虚拟的静态IP地址,这些IP是公网不能直接访问的,如以125开头的一些IP地址,以前单纯的动态域名解析就不好用了。

8. 内网穿透的定义与障碍

简单来说实现不同局域网内的主机之间通过互联网进行通信的技术叫内网穿透。

障碍一:位于局域网内的主机有两套 IP 地址,一套是局域网内的 IP 地址,通常是动态分配的,仅供局域网内的主机间通信使用;一套是经过网关转换后的外网 IP 地址,用于与外网程序进行通信。

障碍二:位于不同局域网内的两台主机,即使是知道了对方的 IP 地址和端口号,“一厢情愿”地将数据包发送过去,对方也是接收不到的。

因为出于安全起见,除非是主机主动向对方发出了连接请求(这时会在该主机的数据结构中留下一条记录),否则,当主机接收到数据包时,如果在其数据结构中查询不到对应的记录,那些不请自来的数据包将会被丢弃。

解决办法:要想解决以上两大障碍,我们需要借助一台具有公网 IP 的服务器进行桥接。

内网穿透的产品和工具

免费:www.natfrp.com

1.花生壳

花生壳既是内网穿透软件、内网映射软件,也是端口映射软件。规模最大,较正规,完善。

付费,限制端口,限制流量。

注册送免费域名,6元体验版内网穿透

官网:http://www.oray.com/

踩坑 : 外网可正常访问,但域名配置微信平台url失败

2.nat123

nat123是内网端口映射与动态域名解析软件,在内网启动映射后,可在外网访问连接内网网站等应用。

收费,使用简单,需支付宝充值300T币,即30元

官网:http://www.nat123.com

全端口映射时,需要配置端口

踩坑 : 外网可正常访问,域名配置微信平台url成功,但免费的泛域问题严重,付费的没有短期的

3.NATAPP

NATAPP基于ngrok的国内内网穿透服务,免费版会强制更换域名,临时用一下可以

收费,使用简单,有免费隧道,一级vip9元一个月

官网:https://natapp.cn/

需要配置config.ini     主要authtoken 

  1. #将本文件放置于natapp同级目录 程序将读取 [default] 段

  2. #在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置

  3. #命令行参数 -config= 可以指定任意config.ini文件

  4. [default]

  5. authtoken=                    #对应一条隧道的authtoken

  6. clienttoken=                    #对应客户端的clienttoken,将会忽略authtoken,若无请留空,

  7. log=none                        #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none

  8. loglevel=ERROR                  #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG

  9. http_proxy=                    #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空

踩坑 : 外网可正常访问,域名配置微信平台url成功,但会强制更换域名

4.ngrok

ngrok是一个反向代理,通过在公共的端点和本地运行的Web服务器之间建立一个安全的通道。ngrok可捕获和分析所有通道上的流量,便于后期分析与响应。

开源,  收费,使用简单,有免费隧道,一级vip10元一个月

官网:https://ngrok.com/

源码:https://github.com/inconshreveable/ngrok

无需配置,输入隧道id即可

踩坑 : 外网可正常访问,域名配置微信平台url可成功,但有泛域现象,隧道不稳定,有时连不上

其他:

3322动态域名

提供了一个XXX.3322.org随机动态域名。

dnspod

1.不提供域名

2.免费域名解析。不需要转入域名即可使用。URL隐性转发不支持。

3.动态域名解析。提供API实现。

总结 :

1. 有免费的可用 Ngrok 和NatApp

2. 微信平台开发踩坑,花生壳用不了;nat123泛域严重,vip比较贵 ;NatApp域名会强制变更,不稳定;Ngrok 有泛域问题,隧道不稳定。

3.如果微信平台开发,先试ngrok,再看NatApp,最后nat123


在 上一篇 文章中,我们介绍了frp中的一些 概念和基础知识,这一篇中,我们在此前的基础之上,来看看frp是怎么实现TCP内网穿透的。

我们知道,要使用frp,必须有个服务端,然后要有个客户端。因此,我们从这里开始入手。

可以参考 《如何阅读源代码》:https://jiajunhuang.com/articles/2018_08_04-how_to_read_source_code.md.html

frps

cmd/frps/main.go 是frps的入口处,我们从这里开始,main 函数的主体:

func main() {
	crypto.DefaultSalt = "frp"

	Execute()
}

因此我们需要看到 Execute() 函数的内容,其实它是使用了 cobra 这个库,所以实际的入口在

var rootCmd = &cobra.Command{
        Use:   "frps",
        Short: "frps is the server of frp (https://github.com/fatedier/frp)",
        RunE: func(cmd *cobra.Command, args []string) error {                if showVersion {
                        fmt.Println(version.Full())                        return nil
                }                var err error                if cfgFile != "" {                        var content string
                        content, err = config.GetRenderedConfFromFile(cfgFile)                        if err != nil {                                return err
                        }
                        g.GlbServerCfg.CfgFile = cfgFile
                        err = parseServerCommonCfg(CfgFileTypeIni, content)
                } else {
                        err = parseServerCommonCfg(CfgFileTypeCmd, "")
                }                if err != nil {                        return err
                }

                err = runServer()                if err != nil {
                        fmt.Println(err)
                        os.Exit(1)
                }                return nil
        },
}

最终,也就是 runServer() 这个函数:

func runServer() (err error) {
        log.InitLog(g.GlbServerCfg.LogWay, g.GlbServerCfg.LogFile, g.GlbServerCfg.LogLevel,
                g.GlbServerCfg.LogMaxDays)
        svr, err := server.NewService()        if err != nil {                return err
        }
        log.Info("Start frps success")
        server.ServerService = svr
        svr.Run()        return}

svr.Run(),其中的 svr 是来自 server.NewService(),仔细看一下,server.NewService() 其实就是初始化了一大堆东西。 我们直接看 svr.Run() 做了什么:

func (svr *Service) Run() {        if svr.rc.NatHoleController != nil {                go svr.rc.NatHoleController.Run()
        }        if g.GlbServerCfg.KcpBindPort > 0 {                go svr.HandleListener(svr.kcpListener)
        }        go svr.HandleListener(svr.websocketListener)        go svr.HandleListener(svr.tlsListener)

        svr.HandleListener(svr.listener)
}

可以看到,最后frps会执行到 svr.HandleListener(svr.listener),前面的都是什么 nat hole punching, kcp, websocket, tls等等,我们不看。直接看tcp。

func (svr *Service) HandleListener(l frpNet.Listener) {	// Listen for incoming connections from client.
	for {
		c, err := l.Accept()		if err != nil {
			log.Warn("Listener for incoming connections from client closed")			return
		}
		c = frpNet.CheckAndEnableTLSServerConn(c, svr.tlsConfig)		// Start a new goroutine for dealing connections.
		go func(frpConn frpNet.Conn) {
        ...
        }
    }
}

这里就是监听之后,每来一个新的连接,就起一个goroutine去处理,也就是 go func()... 这一段,然后我们看看内容:

switch m := rawMsg.(type) {case *msg.Login:
    err = svr.RegisterControl(conn, m)    // If login failed, send error message there.
    // Otherwise send success message in control's work goroutine.
    if err != nil {
        conn.Warn("%v", err)
        msg.WriteMsg(conn, &msg.LoginResp{
            Version: version.Full(),
            Error:   err.Error(),
        })
        conn.Close()
    }case *msg.NewWorkConn:
    svr.RegisterWorkConn(conn, m)case *msg.NewVisitorConn:    if err = svr.RegisterVisitorConn(conn, m); err != nil {
        conn.Warn("%v", err)
        msg.WriteMsg(conn, &msg.NewVisitorConnResp{
            ProxyName: m.ProxyName,
            Error:     err.Error(),
        })
        conn.Close()
    } else {
        msg.WriteMsg(conn, &msg.NewVisitorConnResp{
            ProxyName: m.ProxyName,
            Error:     "",
        })
    }default:
    log.Warn("Error message type for the new connection [%s]", conn.RemoteAddr().String())
    conn.Close()
}

这就是服务端启动之后,卡住的地方了。客户端建立连接之后,会发送一个消息,它的类型可能是 msg.Loginmsg.NewWorkConnmsg.NewVisitorConn。上一篇我们说了,visitor 是用于stcp也就是端对端加密通信的,我们不看。workConn就是用于转发流量的,Login就是新的客户端连上去之后进行启动。

frpc

同样,我们从 cmd/frpc/main.go 看起:

func main() {
        crypto.DefaultSalt = "frp"

        sub.Execute()
}

跳转到 sub.Execute()

var rootCmd = &cobra.Command{
        Use:   "frpc",
        Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
        RunE: func(cmd *cobra.Command, args []string) error {                if showVersion {
                        fmt.Println(version.Full())                        return nil
                }                // Do not show command usage here.
                err := runClient(cfgFile)                if err != nil {
                        fmt.Println(err)
                        os.Exit(1)
                }                return nil
        },
}func Execute() {        if err := rootCmd.Execute(); err != nil {
                os.Exit(1)
        }
}

然后我们看 runClient 函数:

func runClient(cfgFilePath string) (err error) {        var content string
        content, err = config.GetRenderedConfFromFile(cfgFilePath)        if err != nil {                return
        }
        g.GlbClientCfg.CfgFile = cfgFilePath

        err = parseClientCommonCfg(CfgFileTypeIni, content)        if err != nil {                return
        }

        pxyCfgs, visitorCfgs, err := config.LoadAllConfFromIni(g.GlbClientCfg.User, content, g.GlbClientCfg.Start)        if err != nil {                return err
        }

        err = startService(pxyCfgs, visitorCfgs)        return}

基本上就是解析配置文件(因为frpc启动的时候要一个配置文件),然后执行 startService

func startService(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) (err error) {
        log.InitLog(g.GlbClientCfg.LogWay, g.GlbClientCfg.LogFile, g.GlbClientCfg.LogLevel, g.GlbClientCfg.LogMaxDays)        if g.GlbClientCfg.DnsServer != "" {
                s := g.GlbClientCfg.DnsServer                if !strings.Contains(s, ":") {
                        s += ":53"
                }                // Change default dns server for frpc
                net.DefaultResolver = &net.Resolver{
                        PreferGo: true,
                        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {                                return net.Dial("udp", s)
                        },
                }
        }
        svr, errRet := client.NewService(pxyCfgs, visitorCfgs)        if errRet != nil {
                err = errRet                return
        }        // Capture the exit signal if we use kcp.
        if g.GlbClientCfg.Protocol == "kcp" {                go handleSignal(svr)
        }

        err = svr.Run()        if g.GlbClientCfg.Protocol == "kcp" {
                <-kcpDoneCh
        }        return}

同样的,执行 client.NewService 之后执行 svr.Run(),我们看看 svr.Run() 是什么:

func (svr *Service) Run() error {	// first login
	for {
		conn, session, err := svr.login()		if err != nil {
			log.Warn("login to server failed: %v", err)			// if login_fail_exit is true, just exit this program
			// otherwise sleep a while and try again to connect to server
			if g.GlbClientCfg.LoginFailExit {				return err
			} else {
				time.Sleep(10 * time.Second)
			}
		} else {			// login success
			ctl := NewControl(svr.runId, conn, session, svr.pxyCfgs, svr.visitorCfgs)
			ctl.Run()
			svr.ctlMu.Lock()
			svr.ctl = ctl
			svr.ctlMu.Unlock()			break
		}
	}	go svr.keepControllerWorking()	if g.GlbClientCfg.AdminPort != 0 {
		err := svr.RunAdminServer(g.GlbClientCfg.AdminAddr, g.GlbClientCfg.AdminPort)		if err != nil {
			log.Warn("run admin server error: %v", err)
		}
		log.Info("admin server listen on %s:%d", g.GlbClientCfg.AdminAddr, g.GlbClientCfg.AdminPort)
	}

	<-svr.closedCh	return nil}

可以看到,客户端启动之后,就是一个 for 循环,写入 login 信息,也就是刚才 frps 里的 msg.loginMsg, 然后起一个 goroutine 执行 keepControllerWorking(),之后主 goroutine 就阻塞在 <-svr.closedCh。 看看 keepControllerWorking() 的内容:

func (svr *Service) keepControllerWorking() {
	maxDelayTime := 20 * time.Second
	delayTime := time.Second	for {
		<-svr.ctl.ClosedDoneCh()		if atomic.LoadUint32(&svr.exit) != 0 {			return
		}		for {
			log.Info("try to reconnect to server...")
			conn, session, err := svr.login()			if err != nil {
				log.Warn("reconnect to server error: %v", err)
				time.Sleep(delayTime)
				delayTime = delayTime * 2
				if delayTime > maxDelayTime {
					delayTime = maxDelayTime
				}				continue
			}			// reconnect success, init delayTime
			delayTime = time.Second

			ctl := NewControl(svr.runId, conn, session, svr.pxyCfgs, svr.visitorCfgs)
			ctl.Run()
			svr.ctlMu.Lock()
			svr.ctl = ctl
			svr.ctlMu.Unlock()			break
		}
	}
}

基本上就是一个循环,里面最终是为了成功连接然后执行 ctl.Run()

func (ctl *Control) Run() {	go ctl.worker()	// start all proxies
	ctl.pm.Reload(ctl.pxyCfgs)	// start all visitors
	go ctl.vm.Run()	return}// If controler is notified by closedCh, reader and writer and handler will exitfunc (ctl *Control) worker() {	go ctl.msgHandler()	go ctl.reader()	go ctl.writer()	select {	case <-ctl.closedCh:		// close related channels and wait until other goroutines done
		close(ctl.readCh)
		ctl.readerShutdown.WaitDone()
		ctl.msgHandlerShutdown.WaitDone()		close(ctl.sendCh)
		ctl.writerShutdown.WaitDone()

		ctl.pm.Close()
		ctl.vm.Close()		close(ctl.closedDoneCh)		if ctl.session != nil {
			ctl.session.Close()
		}		return
	}
}// msgHandler handles all channel events and do corresponding operations.func (ctl *Control) msgHandler() {	defer func() {		if err := recover(); err != nil {
			ctl.Error("panic error: %v", err)
			ctl.Error(string(debug.Stack()))
		}
	}()	defer ctl.msgHandlerShutdown.Done()

	hbSend := time.NewTicker(time.Duration(g.GlbClientCfg.HeartBeatInterval) * time.Second)	defer hbSend.Stop()
	hbCheck := time.NewTicker(time.Second)	defer hbCheck.Stop()

	ctl.lastPong = time.Now()	for {		select {		case <-hbSend.C:			// send heartbeat to server
			ctl.Debug("send heartbeat to server")
			ctl.sendCh <- &msg.Ping{}		case <-hbCheck.C:			if time.Since(ctl.lastPong) > time.Duration(g.GlbClientCfg.HeartBeatTimeout)*time.Second {
				ctl.Warn("heartbeat timeout")				// let reader() stop
				ctl.conn.Close()				return
			}		case rawMsg, ok := <-ctl.readCh:			if !ok {				return
			}			switch m := rawMsg.(type) {			case *msg.ReqWorkConn:				go ctl.HandleReqWorkConn(m)			case *msg.NewProxyResp:
				ctl.HandleNewProxyResp(m)			case *msg.Pong:
				ctl.lastPong = time.Now()
				ctl.Debug("receive heartbeat from server")
			}
		}
	}
}// reader read all messages from frps and send to readChfunc (ctl *Control) reader() {	defer func() {		if err := recover(); err != nil {
			ctl.Error("panic error: %v", err)
			ctl.Error(string(debug.Stack()))
		}
	}()	defer ctl.readerShutdown.Done()	defer close(ctl.closedCh)

	encReader := crypto.NewReader(ctl.conn, []byte(g.GlbClientCfg.Token))	for {		if m, err := msg.ReadMsg(encReader); err != nil {			if err == io.EOF {
				ctl.Debug("read from control connection EOF")				return
			} else {
				ctl.Warn("read error: %v", err)
				ctl.conn.Close()				return
			}
		} else {
			ctl.readCh <- m
		}
	}
}// writer writes messages got from sendCh to frpsfunc (ctl *Control) writer() {	defer ctl.writerShutdown.Done()
	encWriter, err := crypto.NewWriter(ctl.conn, []byte(g.GlbClientCfg.Token))	if err != nil {
		ctl.conn.Error("crypto new writer error: %v", err)
		ctl.conn.Close()		return
	}	for {		if m, ok := <-ctl.sendCh; !ok {
			ctl.Info("control writer is closing")			return
		} else {			if err := msg.WriteMsg(encWriter, m); err != nil {
				ctl.Warn("write message to control connection error: %v", err)				return
			}
		}
	}
}

reader 从frps收信息,然后写到 ctl.readCh 这个 channel 里,writer 则相反, 从 ctl.sendCh 收信息,写到 frps,而 msgHandler 则从frpc里读取信息,放到 ctl.sendCh, 从 ctl.readCh 读取信息,处理之。

之所以这样设计,是为了能够异步处理所有消息。看看 msgHandler 的关键部分:

switch m := rawMsg.(type) {case *msg.ReqWorkConn:    go ctl.HandleReqWorkConn(m)case *msg.NewProxyResp:
    ctl.HandleNewProxyResp(m)case *msg.Pong:
    ctl.lastPong = time.Now()
    ctl.Debug("receive heartbeat from server")
}

我们说过了,frps 每次收到一个请求之后,然后下发一个指令给frpc,要求frpc建立连接, 然后frps再把新来的连接与请求所在的连接串起来,完成代理,msg.ReqWorkConn 就是这个指令。

那么 frps 是在哪里下发指令的呢?

frps 下发指令

每当公网来一个新的请求的时候,frps就会下发一个指令给frpc,要求建立一个新的连接,代码如下:

func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn frpNet.Conn, err error) {        // try all connections from the pool
        for i := 0; i < pxy.poolCount+1; i++ {                if workConn, err = pxy.getWorkConnFn(); err != nil {
                        pxy.Warn("failed to get work connection: %v", err)                        return
                }
                pxy.Info("get a new work connection: [%s]", workConn.RemoteAddr().String())
                workConn.AddLogPrefix(pxy.GetName())                var (
                        srcAddr    string
                        dstAddr    string
                        srcPortStr string
                        dstPortStr string
                        srcPort    int
                        dstPort    int
                )                if src != nil {
                        srcAddr, srcPortStr, _ = net.SplitHostPort(src.String())
                        srcPort, _ = strconv.Atoi(srcPortStr)
                }                if dst != nil {
                        dstAddr, dstPortStr, _ = net.SplitHostPort(dst.String())
                        dstPort, _ = strconv.Atoi(dstPortStr)
                }
                message := msg.StartWorkConn{
                                ProxyName: pxy.GetName(),
                                SrcAddr:   srcAddr,
                                SrcPort:   uint16(srcPort),
                                DstAddr:   dstAddr,
                                DstPort:   uint16(dstPort),
                        }
                err := msg.WriteMsg(workConn, &message)
                workConn.Warn("===== HERE! HERE! HERE!, message is: %+v", message)                if err != nil {
                        workConn.Warn("failed to send message to work connection from pool: %v, times: %d", err, i)
                        workConn.Close()
                } else {                        break
                }
        }        if err != nil {
                pxy.Error("try to get work connection failed in the end")                return
        }        return}

那个 ===== HERE! HERE! HERE!, message is: %+v 是我加上去的,为了方便看每次服务端下发什么信息。我们看的是TCP的代理, 它继承于 server/proxy/proxy.go 里的 BaseProxy,实现是在 server/proxy/tcp.go 里的 type TcpProxy struct

那么什么时候会初始化 TcpProxy 呢?我们注意到,frps 接收的消息里,就有一种是消息类型是 msg.NewProxy,这是客户端和 服务端都启动,并且客户端成功login之后,客户端发送给服务端的消息。也就是 frpc 的配置文件里具体的代理,例如:

[common]server_addr = xxxxxserver_port = 12345tls_enable = true[ssh]type = tcplocal_ip = 127.0.0.1local_port = 22remote_port = 12346

中的 ssh 就是一个TCP代理。收到 msg.NewProxy 之后,服务端会起一个新的监听器监听在对应的端口,然后开始处理请求。

总结

这一篇中,我们在第一篇的基础之上看了frpc和frps的交互流程,了解了frp是如何进行TCP代理的。


frp 是一个比较流行的用于内网穿透的反向代理服务器,与Nginx不同,一般我们使用Nginx做负载均衡,而frp我们一般用来做内网穿透。

先来看看Nginx和frp流量走向的区别。这是Nginx的流量走向示意图:

nginx 流量走向示意图

这是frp的流量走向示意图:

frp 流量走向示意图

了解frp里的一些名词

说实话,frp里名词太多了,再加上 Golang 奇葩的命名规则(要求短) , 实在是不好阅读。我们要先来了解一下在阅读frp源码之前,有哪些名词是我们需要了解的:

  • frps: frp由两部分组成,frps 是服务端的名称,负责处理请求,转发流量

  • frpc: frp由两部分组成,frpc 是客户端的名称,负责把本地的流量连到服务器,让服务器读取&写入

  • proxyproxy 就是代理,例如下面的配置文件里,[tcp] 和 [http] 就是要代理的东西

  • visitorvisitor 是指使用 stcp 和 xtcp 的时候,请求公网服务器的那台电脑也要装一个客户端,那个就是所谓的 visitor

  • workConnworkConn 就是指 frpc 和 frps 所建立的连接

  • serviceservice 是服务端和客户端里,管辖一切的一个全家桶。可以直接把它当 frpc 和 frps 看待。

  • controlcontrol 是用来管理连接用的,比如连接、断开等等

  • xxx wrapper: 这个就顾名思义了,就是一个包装,一般是包了一个 logger

  • xxx manager: 同样顾名思义,就是用来管理的

# frpc.ini[common]server_addr = x.x.x.xserver_port = 7000[ssh]type = tcplocal_ip = 127.0.0.1local_port = 22remote_port = 6000[http]type = tcplocal_ip = 127.0.0.1local_port = 80remote_port = 6001

一个请求的大概处理流程

接下来我们看一下frp的工作流程:

  • 首先,frpc 启动之后,连接 frps,并且发送一个请求 login(),之后保持住这个长连接,如果断开了,就重试

  • frps 收到请求之后,会建立一个 listener 监听来自公网的请求

  • 当 frps 接受到请求之后,会在本地看是否有可用的连接( frp 可以设置连接池),如果没有,就下发一个 msg.StartWorkConn 并且 等待来自 frpc 的请求

  • frpc 收到之后,对 frps 发起请求,请求的最开始会指名这个连接是去向哪个 proxy 的

  • frps 收到来自 frpc 的连接之后,就把新建立的连接与来自公网的连接进行流量互转

  • 如果请求断开了,那么就把另一端的请求也断开

难点

难点在于 frp 的程序代码为了糅合 frpc 和 frps 之间的请求,自己在TCP之上进行协议封装,因此大量使用了 channel, 所以代码被割散到各处,很不容易连贯起来。所以大家做好心理准备。

总结

这一篇文章中,我们了解了frp的一些概念和流程。下一篇我们会深入到代码看一下一个TCP代理是怎么工作的。

2019.06.19:第二篇请戳 这里


需求场景:


       基于微信平台开发服务号,本地移动端测试时,需要在微信平台注册测试号,然后填写接口配置信息,此信息需要你有自己的服务器资源,填写的URL需要正确响应微信发送的Token验证。如何能让外网访问到本地服务器呢,就需要用到内网穿透技术(NAT)。


注意:微信平台只支持80端口和443端口


内网穿透的目的:简单来说就是让外网能访问你本地的应用


几个概念:


1.OSI网络七层协议以及每层的作用




1、物理层:该层包括物理连网媒介,如电缆连线连接器,物理层的协议产生并检测电压以便能够发送和接受携带数据的信号。如中继器、集线器、网线、HUB。


    这一层的数据叫做比特。


2、数据链路层:控制网络层和物理层之间的通信,主要功能是如何在不可靠的物理线路上进行数据的可靠传递。如:网卡、网桥、交换机。


      这一层是和包结构和字段打交道的和事佬。一方面接收来自网络层(第三层)的数据帧并为物理层封装这些帧;另一方面数据链路层把来自物理层的原始数据比特封装到网络层的帧中。起着重要的中介作用。


3、网络层:主要功能是将网络地址翻译成对应的无聊地址,并决定如何将数据从发送方路由到接收方。


如路由器、防火墙、多层交换机。


      网络层确定把数据包传送到其目的地的路径。就是把逻辑网络地址转换为物理地址。如果数据包太大不能通过路径中的一条链路送到目的地,那么网络层的任务就是把这些包分成较小的包。


4、传输层:最重要的层,传输协议同时进行流量控制或者是基于对方可接受数据的快慢程度规定适当的发送速率。包括全双工半双工、流控制、错误恢复服务。同时按照网络能处理的最大尺寸将较长的数据包进行强行分割。进程和端口,TCP UDP协议


5、会话层:负责在网络中的两点之间建立和维护通信。如建立回话、断点续传


       在分开的计算机上的两种应用程序之间建立一种虚拟链接,这种虚拟链接称为会话(session)。会话层通过在数据流中设置检查点而保持应用程序之间的同步。允许应用程序进行通信的名称识别和安全性的工作就由会话层完成。


6、表示层:应该程序和网络之间的翻译官,管理数据的加密和解密。如编码方式,图像编解码、交换机


       定义由应用程序用来交换数据的格式。在这种意义上,表示层也称为转换器(translator)。该层负责协议转换、数据编码和数据压缩。转发程序在该层进行服务操作。


7、应用层:负责对软件提供接口使能网络服务。如应用程序,如FTP、SMTP、HTTP


 


2.IP地址


网络中唯一定位一台设备的逻辑地址,类似我们的电话号码。


在互联网中我们访问一个网站或使用一个网络服务最终都需要通过IP定位到每一台主机,如访问baidu网站:




其中220.181.112.244就是一个公网的IP地址,他最终指向了一台服务器。


IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。


内网IP可以同时出现在多个不同的局域网络中,如A公司的U1用户获得了192.168.0.5,B公司的U3用户也可以获得192.168.0.5;但公网IP是唯一的,因为我们只有一个Internet。


3.域名


域名是IP的别名,便于记忆,域名最终通过DNS解析成IP地址。




P V4是一个32位的数字,IP V6有128位,要记住一串毫无意义的数字非常困难,域名解决了这个问题。




DNS查询过程如下,最终将域名变成IP地址




4.NAT


NAT(Network Address Translation)即网络地址转换,NAT能将其本地地址转换成全球IP地址。


内网的一些主机本来已经分配到了本地IP地址(如局域网DHCP分配的IP),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。


通过使用少量的公有IP 地址代表较多的私有IP 地址的方式,将有助于减缓可用的IP地址空间的枯竭。


NAT不仅能解决了lP地址不足与共享上网的问题,而且还能够有效地避免来自网络外部的攻击,隐藏并保护网络内部的计算机。


多路由器可完成NAT功能。


NAT的实现方式:


静态转换是指将内部网络的私有IP地址转换为公有IP地址,IP地址对是一对一。


动态转换是指将内部网络的私有IP地址转换为公用IP地址时,IP地址是不确定的,是随机的。


端口多路复用(Port address Translation,PAT),内部网络的所有主机均可共享一个合法外部IP地址实现对Internet的访问,从而可以最大限度地节约IP地址资源。同时又可隐藏网络内部的所有主机,有效避免来自internet的攻击。因此,目前网络中应用最多的就是端口多路复用方式。


应用程序级网关技术(Application Level Gateway)ALG:传统的NAT技术只对IP层和传输层头部进行转换处理,ALG它能对这些应用程序在通信时所包含的地址信息也进行相应的NAT转换




 




 


5. Proxy


Proxy即代理,被广泛应用于计算机领域,主要分为正向代理与反向代理:


 正向代理


比如X花店代A,B,C,D,E五位男生向Candy女生送匿名的生日鲜花,这里的X花店就是5位顾客的代理,花店代理的是客户,隐藏的是客户。这就是我们常说的代理。


正向代理隐藏了真实的请求客户端。服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求,某些科学上网工具扮演的就是典型的正向代理角色。用浏览器访问http://www.google.com时被墙了,于是你可以在国外搭建一台代理服务器,让代理帮我去请求google.com,代理把请求返回的相应结构再返回给我。


当多个客户端访问服务器时服务器不知道真正访问自己的客户端是那一台。正向代理中,proxy和client同属一个LAN,对server透明;




反向代理


拨打10086客服电话,接线员可能有很多个,调度器会智能的分配一个接线员与你通话。这里的调度器就是一个代理,只不过他代理的是接线员,客户端不能确定真正与自己通话的人,隐藏与保护的是目标对象。


反向代理隐藏了真实的服务端,当我们请求 ww.baidu.com 的时候,就像拨打10086一样,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,ww.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。Nginx就是性能非常好的反向代理服务器,用来做负载均衡。




反向代理中,proxy和server同属一个LAN,对client透明。


6. DDNS


DDNS即动态域名解析,是将用户的动态IP地址映射到一个固定的域名解析服务上,用户每次连接网络的时候,客户端程序就会通过信息传递把该主机的动态IP地址传送给位于服务商主机上的服务器程序,服务程序负责提供DNS服务并实现动态域名解析。就是说DDNS捕获用户每次变化的IP地址,然后将其与域名相对应,这样域名就可以始终解析到非固定IP的服务器上,互联网用户通过本地的域名服务器获得网站域名的IP地址,从而可以访问网站的服务。


7. 为什么需要内网穿透


当内网中的主机没有静态IP地址要被外网稳定访问时可以使用内网穿透


在互联网中唯一定位一台主机的方法是通过公网的IP地址,但固定IP是一种非常稀缺的资源,不可能给每个公司都分配一个,且许多中小公司不愿意为高昂的费用买单,多数公司直接或间接的拨号上网,电信部门会给接入网络的用户分配IP地址,以前上网用户少的时候基本分配的都是临时的静态IP地址,租约过了之后可能会更换成另一个IP地址,这样外网访问就不稳定,因为内网的静态IP地址一直变化,为了解决这个问题可以使用动态域名解析的办法变换域名指向的静态IP地址。但是现在越来越多的上网用户使得临时分配的静态IP地址也不够用了,电信部门开始分配一些虚拟的静态IP地址,这些IP是公网不能直接访问的,如以125开头的一些IP地址,以前单纯的动态域名解析就不好用了。


 


8. 内网穿透的定义与障碍


简单来说实现不同局域网内的主机之间通过互联网进行通信的技术叫内网穿透。


障碍一:位于局域网内的主机有两套 IP 地址,一套是局域网内的 IP 地址,通常是动态分配的,仅供局域网内的主机间通信使用;一套是经过网关转换后的外网 IP 地址,用于与外网程序进行通信。




障碍二:位于不同局域网内的两台主机,即使是知道了对方的 IP 地址和端口号,“一厢情愿”地将数据包发送过去,对方也是接收不到的。


因为出于安全起见,除非是主机主动向对方发出了连接请求(这时会在该主机的数据结构中留下一条记录),否则,当主机接收到数据包时,如果在其数据结构中查询不到对应的记录,那些不请自来的数据包将会被丢弃。




解决办法:要想解决以上两大障碍,我们需要借助一台具有公网 IP 的服务器进行桥接。


 


内网穿透的产品和工具


1.花生壳


花生壳既是内网穿透软件、内网映射软件,也是端口映射软件。规模最大,较正规,完善。


付费,限制端口,限制流量。


注册送免费域名,6元体验版内网穿透


官网:http://www.oray.com/


 




 




 


踩坑 : 外网可正常访问,但域名配置微信平台url失败


2.nat123


nat123是内网端口映射与动态域名解析软件,在内网启动映射后,可在外网访问连接内网网站等应用。


收费,使用简单,需支付宝充值300T币,即30元


官网:http://www.nat123.com




全端口映射时,需要配置端口




踩坑 : 外网可正常访问,域名配置微信平台url成功,但免费的泛域问题严重,付费的没有短期的


3.NATAPP


NATAPP基于ngrok的国内内网穿透服务,免费版会强制更换域名,临时用一下可以


收费,使用简单,有免费隧道,一级vip9元一个月


官网:https://natapp.cn/


 


需要配置config.ini     主要authtoken 


#将本文件放置于natapp同级目录 程序将读取 [default] 段

#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置

#命令行参数 -config= 可以指定任意config.ini文件

[default]

authtoken=                     #对应一条隧道的authtoken

clienttoken=                    #对应客户端的clienttoken,将会忽略authtoken,若无请留空,

log=none                        #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none

loglevel=ERROR                  #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG

http_proxy=                     #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空

 




踩坑 : 外网可正常访问,域名配置微信平台url成功,但会强制更换域名


4.ngrok

ngrok是一个反向代理,通过在公共的端点和本地运行的Web服务器之间建立一个安全的通道。ngrok可捕获和分析所有通道上的流量,便于后期分析与响应。


开源,  收费,使用简单,有免费隧道,一级vip10元一个月


官网:https://ngrok.com/


源码:https://github.com/inconshreveable/ngrok


 


无需配置,输入隧道id即可




 




踩坑 : 外网可正常访问,域名配置微信平台url可成功,但有泛域现象,隧道不稳定,有时连不上


其他:


3322动态域名


提供了一个XXX.3322.org随机动态域名。


dnspod


1.不提供域名


2.免费域名解析。不需要转入域名即可使用。URL隐性转发不支持。


3.动态域名解析。提供API实现。


总结 :


1. 有免费的可用 Ngrok 和NatApp


2. 微信平台开发踩坑,花生壳用不了;nat123泛域严重,vip比较贵 ;NatApp域名会强制变更,不稳定;Ngrok 有泛域问题,隧道不稳定。


3.如果微信平台开发,先试ngrok,再看NatApp,最后nat123


 


微信本地测试内网穿透案例ngrok可看上篇(用Sunny_ngrok免费地址映射工具解决微信公众平台开发本地测试问题)


https://blog.csdn.net/xinpz/article/details/80760326


 


感谢博客资源:https://blog.csdn.net/Mind_programmonkey/article/details/8028559

————————————————

版权声明:本文为CSDN博主「菜鸟柱子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/xinpz/article/details/82732217