分类 大观园 下的文章

在 上一篇 文章中,我们介绍了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


为什么需要内网穿透功能

从公网中访问自己的私有设备向来是一件难事儿。
自己的主力台式机、NAS等等设备,它们可能处于路由器后,或者运营商因为IP地址短缺不给你分配公网IP地址。如果我们想直接访问到这些设备(远程桌面,远程文件,SSH等等),一般来说要通过一些转发或者P2P组网软件的帮助。
我有一台计算机位于一个很复杂的局域网中,我想要实现远程桌面和文件访问,目前来看其所处的网络环境很难通过简单的端口映射将其暴露在公网之中,我试过这么几种方法:

  1. 远程桌面使用TeamViewer。可用,但需要访问端也拥有TeamViewer软件,不是很方便,希望能使用Windows自带的远程桌面。且TeamViewer不易实现远程文件访问。

  2. 使用蒲公英VPN软件进行组网,可用,但免费版本网络速度极慢,体验不佳,几乎无法正常使用。

  3. 使用花生壳软件进行DDNS解析,可用,但同第二点所述,免费版本有带宽限制,无法实际使用。

  4. 搭建frp服务器进行内网穿透,可用且推荐,可以达到不错的速度,且理论上可以开放任何想要的端口,可以实现的功能远不止远程桌面或者文件共享。

frp是什么

简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP、TCP、UDP等众多协议。我们今天仅讨论TCP和UDP相关的内容。
截至本文完成,frp的最新版本为v0.22.0,本文使用最新版本。

准备工作

搭建一个完整的frp服务链,我们需要

  1. VPS一台(也可以是具有公网IP的实体机)

  2. 访问目标设备(就是你最终要访问的设备)

  3. 简单的Linux基础(会用cp等几个简单命令即可)

VPS相关

  • 因为frp的原理是利用服务端(所准备的VPS)进行转发,因而VPS的速度直接决定了之后连接的质量,请根据自己的需要选择相应主机配置。

  • 我使用了DigitalOcean的新加坡机房,北京移动、中国教育网下测试速度尚可。

  • 系统使用Ubuntu 16.04.5 x64,frp客户端和服务端本身同时均支持Linux和Windows,且配置方法一样,请根据实际环境自行测试,如果你是新建的VPS,那选择Ubuntu 16.04.5 x64就可以了。

服务端设置

SSH连接到VPS之后运行如下命令查看处理器架构,根据架构下载不同版本的frp
arch
查看结果,如果是“X86_64“即可选择”amd64”,
运行如下命令,根据架构不同,选择相应版本并进行下载
wget https://github.com/fatedier/frp/releases/download/v0.22.0/frp_0.22.0_linux_amd64.tar.gz
然后解压
tar -zxvf frp_0.22.0_linux_amd64.tar.gz
文件夹改个名,方便使用
cp -r frp_0.22.0_linux_amd64 frp
把解压出来的文件夹复制到你想要的目录下,为了方便我直接放在用户目录下了,进入该目录
cd frp
查看一下文件
ls -a
我们只需要关注如下几个文件

  • frps

  • frps.ini

  • frpc

  • frpc.ini

前两个文件(s结尾代表server)分别是服务端程序和服务端配置文件,后两个文件(c结尾代表client)分别是客户端程序和客户端配置文件。
因为我们正在配置服务端,可以删除客户端的两个文件
rm frpc
rm frpc.ini
然后修改frps.ini文件
vim frps.ini
这个文件应有如下格式

[common]
bind_port = 7000
dashboard_port = 7500
token = 12345678
dashboard_user = admin
dashboard_pwd = admin
vhost_http_port = 10080
vhost_https_port = 10443

如果没有必要,端口均可使用默认值,token、user和password项请自行设置。

  • “bind_port”表示用于客户端和服务端连接的端口,这个端口号我们之后在配置客户端的时候要用到。

  • “dashboard_port”是服务端仪表板的端口,若使用7500端口,在配置完成服务启动后可以通过浏览器访问 x.x.x.x:7500 (其中x.x.x.x为VPS的IP)查看frp服务运行信息。

  • “token”是用于客户端和服务端连接的口令,请自行设置并记录,稍后会用到。

  • “dashboard_user”和“dashboard_pwd”表示打开仪表板页面登录的用户名和密码,自行设置即可。

  • “vhost_http_port”和“vhost_https_port”用于反向代理HTTP主机时使用,本文不涉及HTTP协议,因而照抄或者删除这两条均可。

编辑完成后保存(vim保存如果不会请自行搜索)。
之后我们就可以运行frps的服务端了
./frps -c frps.ini
如果看到屏幕输出这样一段内容,即表示运行正常,如果出现错误提示,请检查上面的步骤。

2019/01/12 15:22:39 [I] [service.go:130] frps tcp listen on 0.0.0.0:7000
2019/01/12 15:22:39 [I] [service.go:172] http service listen on 0.0.0.0:10080
2019/01/12 15:22:39 [I] [service.go:193] https service listen on 0.0.0.0:10443
2019/01/12 15:22:39 [I] [service.go:216] Dashboard listen on 0.0.0.0:7500
2019/01/12 15:22:39 [I] [root.go:210] Start frps success

此时访问 x.x.x.x:7500 并使用自己设置的用户名密码登录,即可看到仪表板界面

frp服务端仪表板界面frp服务端仪表板界面


服务端后台运行

至此,我们的服务端仅运行在前台,如果Ctrl+C停止或者关闭SSH窗口后,frps均会停止运行,因而我们使用 nohup命令将其运行在后台。

nohup后台程序管理或关闭相关命令可自行查询资料,上面这个连接中也有所提及。

nohup ./frps -c frps.ini &
输出如下内容即表示正常运行

nohup: ignoring input and appending output to 'nohup.out'

此时可先使用Ctrl+C关闭nohup,frps依然会在后台运行,使用jobs命令查看后台运行的程序
jobs
在结果中我们可以看到frps正在后台正常运行

[1]+  Running                 nohup ./frps -c frps.ini &

此时访问 x.x.x.x:7500 依然可以打开仪表板界面,至此,服务端即设置完成,你可以关闭SSH窗口了。

客户端设置

frp的客户端就是我们想要真正进行访问的那台设备,大多数情况下应该会是一台Windows主机,因而本文使用Windows主机做例子;Linux配置方法类似,不再赘述。

同样地,根据客户端设备的情况选择相应的frp程序进行下载,Windows下下载和解压等步骤不再描述。
假定你下载了“frp_0.22.0_windows_amd64.zip”,将其解压在了C盘根目录下,并且将文件夹重命名为“frp”,可以删除其中的frps和frps.ini文件。
用文本编辑器打开frpc.ini,与服务端类似,内容如下。

[common]
server_addr = x.x.x.x
server_port = 7000
token = won517574356
[rdp]
type = tcp
local_ip = 127.0.0.1           
local_port = 3389
remote_port = 7001  
[smb]
type = tcp
local_ip = 127.0.0.1
local_port = 445
remote_port = 7002

其中common字段下的三项即为服务端的设置。

  • “server_addr”为服务端IP地址,填入即可。

  • “server_port”为服务器端口,填入你设置的端口号即可,如果未改变就是7000

  • “token”是你在服务器上设置的连接口令,原样填入即可。

自定义规则

frp实际使用时,会按照端口号进行对应的转发,原理如下图所示。

frp的原理frp的原理
上面frpc.ini的rdp、smb字段都是自己定义的规则,自定义端口对应时格式如下。


  • “[xxx]”表示一个规则名称,自己定义,便于查询即可。

  • “type”表示转发的协议类型,有TCP和UDP等选项可以选择,如有需要请自行查询frp手册。

  • “local_port”是本地应用的端口号,按照实际应用工作在本机的端口号填写即可。

  • “remote_port”是该条规则在服务端开放的端口号,自己填写并记录即可。

RDP,即Remote Desktop 远程桌面,Windows的RDP默认端口是3389,协议为TCP,建议使用frp远程连接前,在局域网中测试好,能够成功连接后再使用frp穿透连接。

SMB,即Windows文件共享所使用的协议,默认端口号445,协议TCP,本条规则可实现远程文件访问。

配置完成frpc.ini后,就可以运行frpc了

frpc程序不能直接双击运行!

使用命令提示符或Powershell进入该目录下
cd C:\frp
并执行
./frpc -c frpc.ini
运行frpc程序,窗口中输出如下内容表示运行正常。

2019/01/12 16:14:56 [I] [service.go:205] login to server success, get run id [2b65b4e58a5917ac], server udp port [0]
2019/01/12 16:14:56 [I] [proxy_manager.go:136] [2b65b4e58a5917ac] proxy added: [rdp smb]
2019/01/12 16:14:56 [I] [control.go:143] [smb] start proxy success
2019/01/12 16:14:56 [I] [control.go:143] [rdp] start proxy success

不要关闭命令行窗口,此时可以在局域网外使用相应程序访问 x.x.x.x:xxxx (IP为VPS的IP,端口为自定义的remote_port)即可访问到相应服务。

客户端后台运行及开机自启

frpc运行时始终有一个命令行窗口运行在前台,影响美观,我们可以使用一个批处理文件来将其运行在后台,而且可以双击执行,每次打开frpc不用再自己输命令了。
在任何一个目录下新建一个文本文件并将其重命名为“frpc.bat”,编辑,粘贴如下内容并保存。

@echo off
if "%1" == "h" goto begin
mshta vbscript:createobject("wscript.shell").run("""%~nx0"" h",0)(window.close)&&exit
:begin
REM
cd C:\frp
frpc -c frpc.ini
exit

将cd后的路径更改为你的frpc实际存放的目录。

之后直接运行这个 .bat 文件即可启动frpc并隐藏窗口(可在任务管理器中退出)。
至于开机启动,把这个 .bat 文件直接扔进Windows的开机启动文件夹就好了 :)
至此,客户端配置完成,之后就是你自己根据需要在frpc.ini后追加规则即可。
强烈建议你在使用frp直接测试内网穿透前,先在局域网内测试好相关功能的正常使用,并配置好可能会影响的Windows防火墙等内容,在内网调试通过后再使用frp进行内网穿透测试。