内网穿透你真的了解吗?

news2024/11/19 4:46:20

前言

内网穿透作为程序员常用的调试手段之一,我们可以通过在个人电脑上运行花生壳或者 frp 等方式,让他人访问我们本地启动的服务,而且这种访问可以不受局域网的限制,当我们使用ngrok,frp等开源框架时,你是否有好奇过它神奇的作用?明明没有将服务部署到服务器,程序员们究竟是怎么通过这种特殊方式让所有人访问自己的主机的?本文将以frp开源框架为例,介绍内网穿透的原理。

公网 IP 与内网 IP

能否在公网中访问服务器的决定性因素:公网 IP

IP 地址的作用

众所周知, IP 地址是每一位使用互联网的网民都会拥有的标识, IP 地址在互联网中起到的作用是定位,通过 IP 地址我们可以精确的定位到所需资源所在的服务器,这是对于一般用户来讲的,而对于程序员而言,我们需要的则是让用户通过 IP 地址定位到我们部署的资源,既然每个互联网用户都拥有 IP 地址,为什么用户无法直接访问部署在个人PC上的服务呢?

事实上, IP 地址分为两种:公网 IP 和内网 IP

内网 IP : 内网 IP 是用户在使用局域网时,由局域网的网关所分配的 IP 地址,每一个内网 IP 实际上都可以映射到当前所在局域网网关的某一端口( IPV4 地址通过 NAT 与端口映射方式实现,具体原理下文详解),拥有内网 IP 可以被同一局域网下的其他设备所访问到;

公网 IP : 内网的设备想要访问非同一局域网下的资源则必须通过公网 IP ,公网 IP 是没有经过 NAT 转换的由互联网供应商(ISP)提供的最原始的 IP 地址,每一个公网 IP 都可以直接在互联网中被直接定位到。

一个最简单的例子(以前端开发为例)

当我们使用 webpack-dev-server 来启动一个 node 项目时,我们除了通过localhost:[端口号]的方式以外,与我们的开发设备处于同一局域网下的设备可以通过内网 IP :[端口号]的方式对我们的项目进行访问,但当我们使用自己的流量或者连接其他非当前开发设备所在局域网的设备使用内网 IP :[端口号]的方式进行进行访问时,则无法访问。

原因:

内网 IP 地址仅在当前局域网下可以被定位并访问到,而当我们想要跨局域网访问时,我们的访问请求则需要先映射为公网 IP 然后访问到另一局域网的公网 IP ,最后由另一局域网的网关将其映射到相应的局域网设备,但我们访问的地址属于局域网中的内网 IP ,因此无法定位到其相应的公网 IP

综上所述,当我们想要让处于其他局域网下的设备访问到我们本地资源,必不可缺的就是公网 IP

公网 IP 的稀有程度

相较于内网 IP ,公网 IP 明显比内网 IP 更加有用,为什么不可以人手一个公网 IP 呢?

IPV4和 IPV6

尽管 IPV6 的概念在几年前已经被提出,但实际的普及程度并没有很高,现在大部分网络用户使用的依旧是 IPV4 的 IP 地址,这也是限制公网 IP 个数的最大原因。

** IPV4:** IPV4 由 32 位二进制数组成,一共有 2^32 个不同的 IPV4 地址

** IPV6**: IPV6 由 128 位二进制数组成,理论上共有 2^128 个不同的 IPV6 地址

由此可见, IPV4地址的个数并不足以满足当前全世界网络用户的人手一个 IP 地址的需求,那么当前的网络为什么可以让这么多用户同时在网络上冲浪呢?

NAT(网络地址转换)技术

网络地址转化技术的核心作用在于实现对公网 IP 地址的复用,即所有的内网主机共用同一个 IP 地址,NAT 的实现方式共有三种:

  • 静态转换:将内网 IP 直接转换为公网 IP 地址,形成一一对应的方式

  • 动态转换:将内网 IP 地址转换为公网 IP 地址,与静态转换不同的是动态转换会在 IP 池中选择空闲 IP 地址进行转换,即每次同一个内网 IP 对应的公网 IP 会发生改变

  • 端口多路复用(PAT 技术):将内网 IP 与公网 IP 的某一端口进行映射,通过公网 IP 的某一端口访问公网

可以看出以上三种形式中端口多路复用(PAT)技术可以最大程度上缓解 IPV4 地址紧张的现状,也是最为广泛使用的实现方式,三种 NAT 实现方式共同点在于:对于内网用户来说自己对应的公网 IP 是不可知的,就好像我们可以知道自己的门牌号但无法知道自己所在的小区,因此无法准确告诉别人我们的具体地址。

内网穿透

在已知了当前内外网工作方式后,我们再来看一看作为程序员常用的技术手段内网穿透

在此之前或许很多人都曾使用过如花生壳、ngrok、frp等方式在没有服务器的情况下将一些服务部署到网络上让别人使用

那么内网穿透的原理究竟是怎么样的呢?

内网穿透原理解析

目前市面上主流的内网穿透工具实现的原理如下:

可见,内网穿透的核心原理在于将外网 IP 地址与内网 IP 地址建立联系,市面上常用的如花生壳工具其核心原理就是依靠一台具有公网 IP 的服务器作为请求的中转站以此来达到从公网访问内网主机的目的。

当我们启动花生壳的服务时,花生壳会将本地配置好的端口和服务器上的端口进行映射,告知服务器请求转发的路径,花生壳的公网服务器则会监听相应端口的请求,当用户访问花生壳提供的 IP 地址时,花生壳的对应 IP 地址的公网主机将会根据访问的端口映射到相应的内网主机,并通过预先配置好的服务端口将请求转发,以达到访问内网主机相应服务的效果。

更多C++后台开发技术点知识内容包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux内核,TCP/IP,协程,DPDK多个高级知识点。

C/C++Linux服务器开发高级架构师/C++后台开发架构师​免费学习地址

【文章福利】另外还整理一些C++后台开发架构师 相关学习资料,面试题,教学视频,以及学习路线图,免费分享有需要的可以点击领取

实现内网穿透

花生壳作为一款商业产品,对于配置端口等一系列工作进行了封装,使得用户可以更快捷的使用内网穿透,但我们在了解原理后完全可以通过一些开源的框架以及一台公网服务器实现对应的内网穿透功能,我们以 frp 为例。

如何搭建最简单的 frp 服务

服务端设置(frps.ini):
[common]
bind_port = 7000       //此处填写客户端监听的服务端端口号
vhost_http_port = 8080 //此处填写用户访问的端口号

客户端配置(frpc.ini):
[common]
server_addr = x.x.x.x //此处填写服务端 IP 地址
server_port = 7000    //此处填写服务端配置的bind_port

[web]
type = http         //此处规定转发请求的协议类型
local_port = 80     //此处规定本地服务启动的地址
custom_domains = www.example.com   //此处可以填写自定义域名(需要在 IP 地址下配置域名解析)

当我们配置完上述的文件后,用户的访问请求将会经过如下的步骤:

用户的请求将会经过域名解析,公网端口的转发以及内网主机的监听三个步骤成功将请求发送到对应的内网服务,当然 frp 相较于花生壳提供了更多的自定义配置项,此处不做详细讲解,有兴趣的读者可以访问:frp中文文档

当我们使用 frp 去配置我们自己的内网穿透服务时,我们可以使用一台服务器为大量的内网主机提供公网访问的功能,以此来实现公网 IP 的复用,其原理与上文提到的 PAT 端口多路复用技术相类似,当我们临时需要使用服务器时,只需要向拥有公网服务器的朋友申请两个闲置端口即可。

frp 核心代码解析

本文以 http 请求为例解析当一个公网请求发送到frp服务器后究竟会经过哪些步骤

frps 初始化

func runServer(cfg config.ServerCommonConf) (err error) {
  log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLogColor)

  if cfgFile != "" {
    log.Info("frps uses config file: %s", cfgFile)
  } else {
    log.Info("frps uses command line arguments for config")
  }
  
  // !important 核心代码1
  svr, err := server.NewService(cfg)
  if err != nil {
    return err
  }
  log.Info("frps started successfully")
  // !important 核心代码2
  svr.Run()
  return
}

在frp/cmd/frps/root.go中

  • 核心代码1: server.NewService() 方法对我们在frps中的配置进行解析,初始化frp服务端
  • 核心代码2: serever.Run() 方法启动frp服务

frpc 初始化

for{    
  // !important 核心代码3
conn, session, err := svr.login()
    if err != nil {
      xl.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 svr.cfg.LoginFailExit {
        return err
      }
      util.RandomSleep(10*time.Second, 0.9, 1.1)
    } else {
      // login success
      // !important 核心代码4
      ctl := NewControl(svr.ctx, svr.runID, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
      ctl.Run()
      svr.ctlMu.Lock()
      svr.ctl = ctl
      svr.ctlMu.Unlock()
      break
    }
}

在frp/cmd/client/service.go中

  • 核心代码3: for 循环不断去发起和服务端的连接,失败后会再次发起
  • 核心代码4: 连接成功后,客户端会使用连接的信息调用 NewControl()

frpc 和 frps 通信

frps 发起连接

func (pxy *BaseProxy) GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, err error) {
  xl := xlog.FromContextSafe(pxy.ctx)
  // try all connections from the pool
  for i := 0; i < pxy.poolCount+1; i++ {
    // !important 核心代码5
    if workConn, err = pxy.getWorkConnFn(); err != nil {
      xl.Warn("failed to get work connection: %v", err)
      return
    }
    xl.Debug("get a new work connection: [%s]", workConn.RemoteAddr().String())
    xl.Spawn().AppendPrefix(pxy.GetName())
    workConn = frpNet.NewContextConn(pxy.ctx, workConn)
    ......
    // !important 核心代码6
    err := msg.WriteMsg(workConn, &msg.StartWorkConn{
      ProxyName: pxy.GetName(),
      SrcAddr:   srcAddr,
      SrcPort:   uint16(srcPort),
      DstAddr:   dstAddr,
      DstPort:   uint16(dstPort),
      Error:     "",
    })
  }
}

在frp/server/proxy.go中

  • 核心代码5: frps从多个连接中通过依次遍历的方式来获取第一个成功获取到的连接
  • 核心代码6:frps通过获取到的连接向 frpc 发出 &msg.StartWorkConn 的消息,告诉frpc建立连接的相应信息

frpc 响应连接

func (pxy *TCPProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
  // !important 核心代码7
  HandleTCPWorkConnection(pxy.ctx, &pxy.cfg.LocalSvrConf, pxy.proxyPlugin, pxy.cfg.GetBaseInfo(), pxy.limiter,
    conn, []byte(pxy.clientCfg.Token), m)
}

在frp/client/proxy/proxy.go中

  • 核心代码7:frpc接收到frps的信息后发起 TCP 连接

frps发送消息

func (ctl *Control) writer() {
  xl := ctl.xl
  defer func() {
    if err := recover(); err != nil {
      xl.Error("panic error: %v", err)
      xl.Error(string(debug.Stack()))
    }
  }()

  defer ctl.allShutdown.Start()
  defer ctl.writerShutdown.Done()

  encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Token))
  if err != nil {
    xl.Error("crypto new writer error: %v", err)
    ctl.allShutdown.Start()
    return
  }
  for {
    m, ok := <-ctl.sendCh
    if !ok {
      xl.Info("control writer is closing")
      return
    }
    // !important 核心代码8
    if err := msg.WriteMsg(encWriter, m); err != nil {
      xl.Warn("write message to control connection error: %v", err)
      return
    }
  }
}

在frp/server/control.go中

  • 核心代码8: frps发送信息到 crypto.NewWriter() 创建的 writer 中

frpc 接收和响应

// !important 核心代码9
func (ctl *Control) reader() {
  xl := ctl.xl
  defer func() {
    if err := recover(); err != nil {
      xl.Error("panic error: %v", err)
      xl.Error(string(debug.Stack()))
    }
  }()
  defer ctl.readerShutdown.Done()
  defer close(ctl.closedCh)

  encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token))
  for {
    m, err := msg.ReadMsg(encReader)
    if err != nil {
      if err == io.EOF {
        xl.Debug("read from control connection EOF")
        return
      }
      xl.Warn("read error: %v", err)
      ctl.conn.Close()
      return
    }
    ctl.readCh <- m
  }
}
  • 核心代码9: frpc 读取 frps 转发的信息

到这里,我们的 frps 已经成功将公网中接收到的请求转发到 frpc 相应的端口了,这就是一个最简单的请求通过 frp 进行代理转发的流程。

总结

本文所介绍的内网穿透技术相关的实现方式其实在我们的日常开发生活中有更多的使用场景,当我们深入了解了当前 IP 地址以及内外网的实现方式后,我们不难发现,当我们将内网穿透的图片稍加修改后就成为了我们常用的另一种功能的实现方式(VPN实现原理):

原文作者:内网穿透你真的了解吗? - 掘金

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/125016.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣sql入门篇(一)

力扣sql入门篇(一) 1 大的国家 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 SELECT name,population,area FROM World WHERE area>3000000 OR population>250000001.3 运行截图 2 可回收且低碳的产品 2.1 题目内容 2.1.1 基本题目信息 2.…

c++使用icu国际化(i18n)

icu International Components for Unicode&#xff0c;https://github.com/unicode-org/icu.git https://icu.unicode.org/ 帮助文档&#xff1a; https://unicode-org.github.io/icu/userguide/icu/howtouseicu.html i18n&#xff0c;Internationalization (in/i18n) libra…

第五章:开机,重启和用户登录注销-[实操篇]

一&#xff1a;开机&重启命令 1.1基本介绍 shutdown shutdown -h now&#xff1a;表示立即关机 shutdown -h 1 : 表示1分钟后关机 shutdown -r now : 立即重启 halt 就是直接使用&#xff0c;效果等价于关机 reboot 就是直接使用&#xff0c;效果等价于关机 syn &…

uTools V3.3.0 效率工具集

前言 uTools是一款基于electron开发的工具集软件&#xff0c;通过快捷唤醒搜索&#xff0c;直接打开各种功能&#xff0c;非常方便。 uTools uTools是一个极简、插件化、跨平台的现代化桌面软件。通过自由选配丰富的插件&#xff0c;打造你得心应手的工具集合。 通过快捷键…

制作gltf动态夜景模型

kele 前言 要制作一个充满科技感的场景&#xff0c;离不开动画特效。众所周知&#xff0c;Unity软件可制作各种炫酷动画效果&#xff0c;如果将Unity中的动画添加到WebGL网页项目中&#xff0c;那一定会非常美妙吧~接下来小编将介绍如何通过Unity制作纹理动画&#xff0c;并导…

RISC-V IDE MRS使用笔记(七) :常用开发技巧汇总

RISC-V IDE MRS使用笔记(七) &#xff1a;常用开发技巧汇总 Q1: MRS调试时如何查看外设寄存器内容&#xff1f; A1: 在调试配置界面添加相应的.svd文件。 Q2: MRS如何调用数学库? A2: #include ”math.h”头文件,并在工程属性页面增加”m”参数。 Q3: 如何设置MRS调试前默认…

还在对比IT培训机构,行内人告诉你怎么比?

还在对比IT培训机构&#xff0c;行内人告诉你怎么比&#xff1f;因为身处IT培训行业6年多点时间 &#xff0c;从讲师到校区经营的成长履历中。见识过众多机构的运营和发展&#xff0c;当然也包含我所在的正厚软件。平时在知乎上看的多&#xff0c;自然推荐的关于机构对比的话题…

蓝牙无线运动耳机排行榜、口碑最好的运动蓝牙耳机推荐

越来越多的人开始喜欢蓝牙耳机的便携性&#xff0c;尤其对于运动的慢跑族&#xff0c;蓝牙耳机更为方便携带和佩戴&#xff0c;毕竟没有了线束的烦恼&#xff0c;目前蓝牙耳机市场很大&#xff0c;用户人群很多&#xff0c;我体验过的这类产品比较多&#xff0c;如何挑选一款满…

MySQL5.7安装配置教程-Windows(企业常用版)超详细

引言 MySQL 5.7 为公司、企业常用的MySQL版本。 正在学习或正在工作的小伙伴推荐相关教程按照5.7的版本来学习、操作。 1. 下载MySQL 阿里云盘不限速高速下载地址&#xff1a; https://www.aliyundrive.com/s/YTMa5LfSFB2 因为阿里云盘不支持zip压缩包的分享&#xff0c;我用…

【财务】FMS财务管理系统---应付结算

本篇文章中&#xff0c;笔者对应付结算各个部分进行了梳理和分析&#xff0c;与大家分享。 前面相继介绍了财务系统的组成、财务数据流转、应收管理、质保金、预付款、费用管理以及合同管理。 我个人觉得应付结算管理部分的逻辑是整个FMS财务管理系统中较为复杂的一部分&#…

APP测试面试题汇总

一、基础篇 1、请介绍一下&#xff0c;APP测试流程&#xff1f; APP测试流程与web测试流程类似&#xff0c;分为如下七个阶段&#xff1a; 1.根据需求说明书编写测试计划&#xff1b; 2.制定测试方案&#xff0c;主要是测试任务、测试人员和测试时间的分配&#xff1b; 3.…

Vue3 Composition API: 对比ref和reactive

Vue2 回顾 首先回顾一下在Vue2中我们是如何创建一个响应式数据 (reactive data)的&#xff1a; Vue3新特性 ref的使用 而在Vue3中&#xff0c;我们可以用Composition API: ref 来改写上述代码&#xff1a; ref 的作用就是将一个原始数据类型&#xff08;primitive data t…

NPM 2FA双重认证的设置方法

NPM在使用用户名和密码登录后&#xff0c;他觉得还不是很安全&#xff0c;需要用一个手机软件来生成一个随机验证码&#xff0c;两者同步&#xff0c;来做二次验证。类似于手机银行的优盾或者验证码验证。 双重验证&#xff1a;标准验证方法&#xff0c;其中一个因素是密码。 …

gdb调试跟踪代码

编译 用gcc/g编译源程序的时候&#xff0c;编译后的可执行文件不会包含源程序代码&#xff0c;如果要使用gdb调试的话&#xff0c;编译的时候要加 -g 的参数。 gcc test.c -o test -g调试 gdb test常用命令&#xff1a; &#xff08;gdb&#xff09;help&#xff1a;查看命令…

jira-dev-tool插件安装失败的解决方法

最近&#xff0c;在运行【React React Hook TS 最佳实践仿 Jira 企业级项目】的时候&#xff0c;安装jira-dev-tool 插件出现问题&#xff0c;参考&#xff1a;https://www.npmjs.com/package/jira-dev-tool。解决方法&#xff1a;在项目中执行&#xff1a; npx msw init pu…

Layer2 实现方式之 Rollup

一. 1. Rollup 简介 Rollup 是类似于 Plasma 的第 2 层扩展解决方案&#xff1a;单个主链合约持有所有资金和对更大“侧链”状态&#xff08;通常是账户、余额及其状态的 Merkle 树&#xff09;的简洁加密承诺。侧链状态由链下用户和运营商维护&#xff0c;不依赖 L1 存储&…

C语言文件读写

上一章我们讲解了 C 语言处理的标准输入和输出设备。本章我们将介绍 C 程序员如何创建、打开、关闭文本文件或二进制文件。 一个文件&#xff0c;无论它是文本文件还是二进制文件&#xff0c;都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数&#xff0c;也提供了底层…

linux------基本操作指令(1)

1)JAVA是一个跨平台的编程语言&#xff0c;多线程编程&#xff0c;网络编程在JAVA标准库里面本身就有 2)C的标准库相比于JAVA的标准库还是要弱一些&#xff0c;网络编程已经不支持了 3)linux是世界上第一大操作系统 李纽克斯 李娜克斯 李呢克斯 李妮克斯 一:背景知识 1)内网I…

串口监听:以共享模式打开端口CreateFile,居然无法再打开?但文件可以。

想以共享模式来监听串口 结果打开失败&#xff0c;错误5 拒绝访问。为什么呢&#xff1f; 但是文件却正常 看了两个监听串口的软件bus hound和commMonitor都是需要驱动才能实现 单纯共享打开&#xff0c;可以无法监听串口&#xff0c;那学习一下windows驱动吧。 windows驱动 …

为加快项目开发进度,如何快速合理分配开发任务?

1、对任务进行合理分类 软件开发项目&#xff0c;任务繁多&#xff0c;需要对任务进行分类&#xff0c;以便优先完成重要任务。一般最常规的方法是时间四象限法&#xff0c;即按照重要和紧急程度对任务进行划分&#xff1a;重要紧急、重要不紧急、紧急不重要、不重要不紧急四类…