我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Nodejs实现内网穿透服务

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Nodejs实现内网穿透服务

也许你很难从网上找到一篇从代码层面讲解内网穿透的文章,我曾搜过,未果,遂成此文。

1. 局域网内代理

我们先来回顾上篇,如何实现一个局域网内的服务代理?因为这个非常简单,所以,直接上代码。


const net = require('net')

const proxy = net.createServer(socket => {
  const localServe = new net.Socket()
  localServe.connect(5502, '192.168.31.130') // 局域网内的服务端口及ip。

  socket.pipe(localServe).pipe(socket)
})

proxy.listen(80)

这就是一个非常简单的服务端代理,代码简单清晰明了,如果有疑问的话,估计就是管道(pipe)这里,简单说下。socket是一个全双工流,也就是既可读又可写的数据流。代码中,当socket接收到客户端数据的时候,它会把数据写入localSever,当localSever有数据的时候,它会把数据写入socket,socket再把数据发送给客户端。

2. 内网穿透

局域网代理简单,内网穿透就没这么简单了,但是,它却是核心的代码,需要在其上做相当的逻辑处理。具体实现之前,我们先梳理一下内网穿透。

什么是内网穿透?

简单来说,就是公网客户端,可以访问局域网内的服务。比如,本地启动的服务。公网客户端怎么会知道本地启的serve呢?这里必然要借助公网服务端。那么公网服务端又怎么知道本地服务呢?这就需要本地和服务端建立socket链接了。

四个角色

通过上面的描述,我们引出四个角色。

  1. 公网客户端,我们取名叫client。
  2. 公网服务端,因为有代理的作用,我们取名叫proxyServe。
  3. 本地服务,取名localServe。
  4. 本地与服务端的socket长连接,它是proxyServe与localServe之前的桥梁,负责数据的中转,我们取名叫bridge。

其中,client和localServe不需要我们关心,因为client可以是浏览器或者其它,localServe就是一个普通的本地服务。我们只需要关心proxyServe和bridge就可以了。我们这里介绍的依然是最简单的实现方式,提供一种思路与思考,那我们先从最简单的开始。

bridge

我们从四个角色一节知道, bridge是一个与proxyServe之间socket连接,且是数据的中转,上代码捋捋思路。


const net = require('net')

const proxyServe = '10.253.107.245'

const bridge = new net.Socket()
bridge.connect(80, proxyServe, _ => {
  bridge.write('GET /regester?key=sq HTTP/1.1\r\n\r\n')
})

bridge.on('data', data => {
  const localServer = new net.Socket()
  localServer.connect(8088, 'localhost', _ => {
    localServer.write(data)
    localServer.on('data', res => bridge.write(res))
  })
})

代码清晰可读,甚至朗朗上口。引入net库,声明公网地址,创建bridge,使bridge连接proxyServe,成功之后,向proxyServe注册本地服务,接着,bridge监听数据,有请求到达时,创建与本地服务的连接,成功之后,把请求数据发送给localServe,同时监听响应数据,把响应流写入到bridge。

其余没什么好解释的了,毕竟这只是示例代码。不过示例代码中有段/regester?key=sq,这个key可是有大作用的,在这里key=sq。那么角色client通过代理服务访问本地服务的是,需要在路径上加上这个key,proxyServe才能对应的上bridge,从而对应上localServe。

例如:lcoalServe是:http://localhost:8088 ,rpoxyServe是example.com ,注册的key是sq。那么要想通过prxoyServe访问到localServe,需要如下写法:example.com/sq 。为什么要这样写?当然只是一个定义而已,你读懂这篇文章的代码之后,可以修改这样的约定。

那么,且看以下关键代码:

proxyServe

这里的proxyServe虽然是一个简化后的示例代码,讲起来依然有些复杂,要想彻底弄懂,并结合自己的业务做成可用代码,是要下一番功夫的。这里我把代码拆分成一块一块,试着把它讲明白,我们给代码块取个名字,方便讲解。
代码块一:createServe

该块的主要功能是创建代理服务,与client和bridge建立socket链接,socket监听数据请求,在回调函数里做逻辑处理,具体代码如下:


const net = require('net')

const bridges = {} // 当有bridge建立socket连接时,缓存在这里
const clients = {} // 当有client建立socket连接时,缓存在这里,具体数据结构看源代码

net.createServer(socket => {
  socket.on('data', data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url
    
    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket, url)
      return
    }

    const { bridge, key } = findBridge(request, url)
    if (!bridge) return

    cacheClientRequest(bridge, key, socket, request, url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

看一下数据监听里的代码逻辑:

  1. 把请求数据转换成字符串。
  2. 从请求里查找URL,找不到URL直接结束本次请求。
  3. 通过URL判断是不是bridge,如果是,注册这个bridge,否者,认为是一个client请求。
  4. 查看client请求有没有已经注册过的bridge -- 记住,这是一个代理服务,没有已经注册的bridge,就认为请求无效。
  5. 缓存这次请求。
  6. 接着再把请求发送给bridge。

结合代码及逻辑梳理,应该能看得懂,但是,对5或许有疑问,接下来一一梳理。

代码块二:isBridge

判断是不是一个bridge的注册请求,这里写的很简单,不过,真实业务,或许可以定义更加确切的数据。


function isBridge (url) {
  return url.startsWith('/regester?')
}

代码块三:regesterBridge
简单,看代码再说明:


function regesterBridge (socket, url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}
  1. 通过URL查找要注册的bridge的key。
  2. 把改socket连接缓存起来。
  3. 移除bridge的数据监听 -- 代码块一里每个socket都有默认的数据监听回调函说,如果不移除,会导致后续数据混乱。

代码块四:findBridge

逻辑走到代码块4的时候,说明这已经是一个client请求了,那么,需要先找到它对应的bridge,没有bridge,就需要先注册bridge,然后需要用户稍后再发起client请求。代码如下:


function findBridge (request, url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return {}
}

  • 从URL中匹配出要代理的bridge的key,找到就返回对应的bridge及key。
  • 找不到再从请求头里的referer里找,找到就返回bridge及key。
  • 都找不到,我们知道在代码块一里会结束掉本次请求。

代码块五:cacheClientRequest

代码执行到这里,说明已经是一个client请求了,我们先把这个请求缓存起来,缓存的时候,我们一并把请求对应的bridge、key绑定一起缓存,方便后续操作。

为什么要缓存client请求?

在目前的方案里,我们希望请求和响应都是成对有序的。我们知道网络传输都是分片传输的,目前来看,如果我们不在应用层控制请求和响应成对且有序,会导致数据包之间的混乱现象。暂且这样,后续如果有更好方案,可以不在应用层强制控制数据的请求响应有序,可以信赖tcp/ip层。
讲完原因,我们先来看缓存代码,这里比较简单,复杂的在于逐个取出请求并有序返回整个响应。


function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, url}]
  }
}

我们先判断该bridge对应的key下是不是已经有client的请求缓存了,如果有,就push进去。

如果没有,我们就创建一个对象,把本次请求初始化进去。

接下来就是最复杂的,取出请求缓存,发送给bridge,监听bridge的响应,直到本次响应结束,在删除bridge的数据监听,再试着取出下一个请求,重复上面的动作,直到处理完client的所有请求。

代码块六:sendRequestToBridgeByKey

在代码块五的最后,对该块做了概括性的说明。可以先稍作理解,在看下面代码,因为代码里会有一些响应完整性的判断,去除这一些,代码就好理解一些。整个方案,我们没有对请求完整性进行处理,原因是,一个请求的基本都在一份数据包大小内,除非是文件上传接口,我们暂不处理,不然,代码又会复杂一些。


function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge, socket, request, url} = requests.shift()

  const newUrl = url.replace(key, '')
  const newRequest = request.replace(url, newUrl)

  bridge.write(newRequest)
  bridge.on('data', data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}

从clients里取出bridge key对应的client。
判断该client是不是有请求正在发送,如果有,结束执行。如果没有,继续。
判断该client下是否有请求,如果有,继续,没有,结束执行。
从队列中取出第一个,它包含请求的socket及缓存的bridge。
替换掉约定的数据,把最终的请求数据发送给bridge。
监听bridge的数据响应。

  • 获取响应code
    • 如果响应是200,我们从中获取content length,如果有,我们对本次请求做一些初始化的操作。设置请求长度,设置已经发送的请求长度。
    • 如果不是200,我们把数据发送给client,并且结束本次请求,移除本次数据监听,递归调用sendRequestToBridgeByKey
  • 如果没有获取的code,我们认为本次响应非第一次,于是,把其长度累加到已发送字段上。
  • 我们接着发送该数据到client。
  • 再判断响应的长度是否和已经发送的过的数据长度一致,如果一致,设置client的数据发送状态为false,移除数据监听,递归调用递归调用sendRequestToBridgeByKey。

至此,核心代码逻辑已经全部结束。

总结

理解这套代码之后,就可以在其上做扩展,丰富代码,为你所用。理解完这套代码,你能想到,它还有哪些使用场景吗?是不是这个思路也可以用在远程控制上,如果你要控制客户端时,从这段代码找找,是不是会有灵感。
这套代码或许会有难点,可能要对tcp/ip所有了解,也需要对http有所了解,并且知道一些关键的请求头,知道一些关键的响应信息,当然,对于http了解的越多越好。
如果有什么需要交流,欢迎留言。

proxyServe源码


const net = require('net')

const bridges = {}
const clients = {}

net.createServer(socket => {
  socket.on('data', data => {
    const request = data.toString()
    const url = request.match(/.+ (?<url>.+) /)?.groups?.url
    
    if (!url) return

    if (isBridge(url)) {
      regesterBridge(socket, url)
      return
    }

    const { bridge, key } = findBridge(request, url)
    if (!bridge) return

    cacheClientRequest(bridge, key, socket, request, url)

    sendRequestToBridgeByKey(key)
  })
}).listen(80)

function isBridge (url) {
  return url.startsWith('/regester?')
}

function regesterBridge (socket, url) {
  const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
  bridges[key] = socket
  socket.removeAllListeners('data')
}

function findBridge (request, url) {
  let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
  let bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
  if (!referer) return {}

  key = referer.split('//')[1].split('/')[1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return {}
}

function cacheClientRequest (bridge, key, socket, request, url) {
  if (clients[key]) {
    clients[key].requests.push({bridge, key, socket, request, url})
  } else {
    clients[key] = {}
    clients[key].requests = [{bridge, key, socket, request, url}]
  }
}

function sendRequestToBridgeByKey (key) {
  const client = clients[key]
  if (client.isSending) return

  const requests = client.requests
  if (requests.length <= 0) return

  client.isSending = true
  client.contentLength = 0
  client.received = 0

  const {bridge, socket, request, url} = requests.shift()

  const newUrl = url.replace(key, '')
  const newRequest = request.replace(url, newUrl)

  bridge.write(newRequest)
  bridge.on('data', data => {
    const response = data.toString()

    let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
    if (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
        if (contentLength) {
          contentLength = parseInt(contentLength)
          client.contentLength = contentLength
          client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
        }
      } else {
        socket.write(data)
        client.isSending = false
        bridge.removeAllListeners('data')
        sendRequestToBridgeByKey(key)
        return
      }
    } else {
      client.received += data.length
    }

    socket.write(data)

    if (client.contentLength <= client.received) {
      client.isSending = false
      bridge.removeAllListeners('data')
      sendRequestToBridgeByKey(key)
    }
  })
}

到此这篇关于Nodejs实现内网穿透服务的文章就介绍到这了,更多相关Node 内网穿透内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Nodejs实现内网穿透服务

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

使用Nodejs怎么实现内网穿透服务

这篇文章给大家介绍使用Nodejs怎么实现内网穿透服务,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。1. 局域网内代理我们先来回顾上篇,如何实现一个局域网内的服务代理?因为这个非常简单,所以,直接上代码。const n
2023-06-15

利用云服务器实现内网穿透

利用云服务器实现内网穿透有许多潜在的好处,例如:保护敏感数据:当你的数据通过云服务器传输到公共云或其他第三方应用程序,你的敏感信息就会被加密或匿名化处理,以确保只有授权用户能够访问和操作。这样就可以确保数据不被未经授权的人访问,从而保护你的数据。减少延迟:通过使用云服务器,你可以减少你的客户端和云服务器之间的交互,从而减少云服务器的延迟。当你在云服务器的托管端运行客户端时,你可以将它们在客
2023-10-26

云服务器内网穿透的实现方法

1.了解内网穿透的概念和原理内网穿透是指通过互联网将内网中的服务暴露给外网访问的一种技术。在传统的网络环境中,内网中的设备无法直接被外网访问,而通过内网穿透技术,可以实现将内网中的服务映射到公网上,使得外网用户可以直接访问内网中的服务。2.选择合适的内网穿透工具目前市面上有很多内网穿透工具可供选择,如花生壳、Ngrok、frp等。这些工具都可以实现内网穿透的功能,但具体选择哪个工具需要根据自己的需求和实际情况...
2023-10-27

云服务器内网穿透

云服务器内网穿透(cloudover-the-Internet,简称Cross-Internet)是一种网络攻击行为,指黑客在云服务器上通过某种方法绕过设备的网络访问控制列表(ACL),从而达到访问云服务器的目的。Cross-Internet攻击的常见手段包括:跨站脚本攻击:黑客通过在浏览器中加载恶意脚本,利用浏览器的漏洞获取云服务器的用户名和密码,然后通过某种手段访问云服务器;钓鱼攻击
2023-10-26

怎么配置frp服务器实现内网穿透

本篇内容主要讲解“怎么配置frp服务器实现内网穿透”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么配置frp服务器实现内网穿透”吧!  内网服务器ip地址:192.168.2.100 ,作为代
2022-12-16

怎么使用云服务器实现内网穿透

使用云服务器实现内网穿透可能需要您提供内网IP地址和端口号,以便管理员可以在云主机上创建私有网络。内网穿透的过程可以大致如下:确定需要穿透的内网地址和端口号:在云服务器上安装相应的客户端程序和脚本,例如:https://ycloud.cloudflare.com/your-host.aspx等,这些脚本将用于创建私有网络。在云服务器上创建私有网络:使用以下命令创建一个私有网络,并配置IP地
2023-10-26

云服务器frp内网穿透

云服务器frp内网穿透,也叫做虚拟内网穿透,是一种常见的网络攻击手段之一。它可以绕过云服务器服务器的防火墙或安全审计,访问被黑客控制的内网系统,从而获取被保护的敏感信息或控制权。云服务器服务器的防火墙和安全审计系统都可以检测到这些虚拟内网穿透行为。但是,攻击者通常可以通过更先进的技术手段绕过这些防御措施。例如,通过使用虚拟私有网络(VPN)或内网代理(NAT)技术,攻击者就可以轻松地穿过防火墙和安
2023-10-26

云服务器做内网穿透

云服务器提供了一个虚拟网络拓扑,可以在内网中穿透到公网。以下是一些可以使用GitHub库的Python代码示例:首先,在GitHub中创建一个新的项目,名为"cloudflare.py"。然后,将其复制到一个新的目录下,例如这里的"usr/local/cloudflare.bin"。在项目目录下创建一个名为"usr/local/cloudflare.bin"的directory文件夹,并将其命
2023-10-26

怎么使用云服务器实现内网穿透服务

使用云服务器实现内网穿透服务的具体步骤如下:选择适当的云服务器:选择一款适合您需要服务的云服务器。有多种云服务器供您选择,包括AmazonWebServices(AMS)、AWS、GoogleCloud等。注册并获取许可证:按照许可证要求在云服务器上注册ID。使用服务:使用云服务器提供的服务。配置服务:配置云服务器的功能和配置。可以使用命令行工具(如mkdir命令、cd命令、pubco
2023-10-26

内网穿透和云服务器

内网穿透和云服务器都是常用的网络安全技术,它们都可以帮助企业网络在内部网(Intranet)和外部网络(Extranet)之间移动业务数据,从而减少网络攻击的风险。内网穿透主要是通过在内部网中部署一些安全设备(如防火墙、防病毒软件等),这些设备可以监视网络流量,并检测内部网络是否存在未授权的活动或者敏感信息泄露的风险。通过检测到这些活动,企业可以及时采取措施阻止这些活动,防止网络攻击的发生和扩
2023-10-26

云服务器内网穿透联机

云服务器内网穿透联机指的是在云服务器上通过VPN或者其他网络设备连接到内网网关,从而实现内网连接的方式。这种方法在服务器上进行数据传输时,服务器会对传输的数据进行加密,并以加密后的数据进行通信,从而保障数据传输的安全性。云服务器内网穿透联机的实现原理如下:安装VPN服务器在云服务器上安装VPN服务器,可以实现内网连接。VPN服务器通常需要与其他VPN服务器进行互连,以确保数据传输的可靠
2023-10-26

云服务器内网穿透速度

云服务器内网穿透速度取决于许多因素,例如网络带宽、服务器硬件配置、服务器操作系统、用户身份认证、防火墙等等。以下是一个通用的参考数值,仅供参考:网络带宽:如果采用100Mbps的VPN,那么使用云服务器进行内网穿透的速度可能会低于采用传统路由器的速度,但也可能非常快,例如使用10Gbps的VPN可以在短时间内实现内网穿透。服务器硬件配置:云服务器通常比其他服务器更强大、更快、更可靠,因此采
2023-10-26

云服务器搭建内网穿透

云服务器搭建内网穿透主要分为两种情况:内网隔离:云服务器将数据存储在内网,不允许访问外部互联网或与外部互联网相通,只能访问内网服务器。内网穿透可以有效地防止数据泄露和网络攻击,但由于内网的安全措施相对薄弱,容易被黑客攻击。私有云架构:将数据存储在私有云服务器中,允许内部员工访问外部互联网。这种架构需要更严格的安全措施,以保护内网数据的安全。在这种情况下,云服务器需要提供相应的认证和授权机制,以便将
2023-10-25

阿里云服务器穿透内网

什么是阿里云服务器?阿里云服务器是阿里云提供的一种云计算服务,它是一种虚拟化的计算资源,可以在云端快速创建和部署,提供高性能、高可靠性、高安全性的计算能力。用户可以根据自己的需求选择不同的配置和规格,灵活地使用阿里云服务器。什么是内网穿透?内网穿透是指在局域网内部署的服务可以通过互联网访问的一种技术。在局域网内部署的服
阿里云服务器穿透内网
2024-01-17

编程热搜

目录