FRP内网穿透

FRP内网穿透

前沿

什么是内网穿透

内网穿透,又叫 NET 穿透,是计算机用语。用通俗的说法就是你家里的个人电脑,可以直接被外网的人访问。例如你在公司,不通过远程工具,直接也可以访问到家里的电脑

一句话总结:内网穿透就是让外部的互联网主机,可以访问你局域网内的机器上的HTTP, HTTPS, TCP 或 UDP 服务

为什么需要内网穿透

  • 一是方便访问某些内网环境,不需要使用远程工具

  • 二是方便把个人电脑上的应用开放到外网进行访问

市面上常见内网穿透工具

FRP 内网穿透

基本介绍

frp(Fast Reverse Proxy) 是一个专注于内网穿透的高性能的反向代理应用,可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网;只需要通过在具有公网 IP 的节点上部署 frp 服务端,可以轻松地将内网服务穿透到公网,同时提供诸多专业的功能特性,这包括:

  • 客户端服务端通信支持 TCP、QUIC、KCP 以及 Websocket 等多种协议

  • 采用 TCP 连接流式复用,在单个连接间承载更多请求,节省连接建立时间,降低请求延迟

  • 代理组间的负载均衡

  • 端口复用,多个服务通过同一个服务端端口暴露

  • 支持 P2P 通信,流量不经过服务器中转,充分利用带宽资源

  • 多个原生支持的客户端插件(静态文件查看,HTTPS/HTTP 协议转换,HTTP、SOCK5 代理等),便于独立使用 frp 客户端完成某些工作

  • 高度扩展性的服务端插件系统,易于结合自身需求进行功能扩展

  • 服务端和客户端 UI 页面

功能示意图

支持协议

TCP 多路复用:客户端和服务器端之间的连接支持多路复用,不再需要为每一个用户请求创建一个连接,使连接建立的延迟降低,并且避免了大量文件描述符的占用,使 frp 可以承载更高的并发数

KCP 协议:底层通信协议支持选择 KCP 协议,相比于 TCP,在弱网环境下传输效率提升明显,但是会有一些额外的流量消耗

QUIC 协议:底层通信协议支持选择 QUIC 协议,底层采用 UDP 传输,解决了 TCP 上的一些问题,传输效率更高,连接延迟低

HTTP和HTTPS: HTTP 和 HTTPS 协议的一个特点是发送的请求都具有 Host 字段,通过该字段描述要访问的服务。基于这个特点,frp 服务端只需要监听在一个端口(通过 vhostHTTPPortvhostHTTPSPort 指定)。就可以根据请求的 Host 来决定需要路由给哪一个代理,而不需要像 TCP 类型那样为每一个服务绑定一个端口

Nginx 和 FRP 流量走向区别

PS:这部分内容引用自参考文档(感谢网络活雷锋)

在这里插入图片描述
在这里插入图片描述

FRP流量请求的处理流程

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

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

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

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

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

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

代码框架

frp 名词

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

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

  • workConn 就是指 frpcfrps 所建立的连接

源码解析

Client 逻辑

客户端启动逻辑

Client 先与 frp-Server 建立链接

主程序就做两件事

  • 与 frps 建立连接

  • 保持controller持续工作,但是这个 keepControllerWorking 的逻辑也就是一直调用 loopLoginUntilSuccess 直到成功

func (svr *Service) Run(ctx context.Context) error {
    ctx, cancel := context.WithCancelCause(ctx)
     ....

    // first login to frps
    svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))

    ....

    go svr.keepControllerWorking()

    ....
}

所以我们只需要看 loopLoginUntilSuccess 函数具体干了些什么就行

这里建立连接的逻辑会自动加上重试,一直到重新成功为止

func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
    xl := xlog.FromContextSafe(svr.ctx)

    loginFunc := func() (bool, error) {
        xl.Info("try to connect to server...")
        conn, connector, err := svr.login()
        ......

        基于上面与server建立连接成功后的conn构造了一个sessionCtx
        sessionCtx := &SessionContext{
            Common:        svr.common,
            RunID:         svr.runID,
            Conn:          conn,
            ConnEncrypted: connEncrypted,
            AuthSetter:    svr.authSetter,
            Connector:     connector,
        }

        利用这个SessionCtx来建立一个新的Ctl
        ctl, err := NewControl(svr.ctx, sessionCtx)
        ....
        ctl.SetInWorkConnCallback(svr.handleWorkConnCb)

        ctl.Run(proxyCfgs, visitorCfgs)
        // close and replace previous control
        svr.ctlMu.Lock()
        if svr.ctl != nil {
            svr.ctl.Close()
        }
        svr.ctl = ctl
        svr.ctlMu.Unlock()
        return true, nil
    }

    // try to reconnect to server until success
    这里执行无限重试一只等到frp-client与frp-server建立连接
    wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager(
        wait.FastBackoffOptions{
            Duration:    time.Second,
            Factor:      2,
            Jitter:      0.1,
            MaxDuration: maxInterval,
        }), true, svr.ctx.Done())
}
消息类型
var msgTypeMap = map[byte]interface{}{
    TypeLogin:              Login{},
    TypeLoginResp:          LoginResp{},
    TypeNewProxy:           NewProxy{},
    TypeNewProxyResp:       NewProxyResp{},
    TypeCloseProxy:         CloseProxy{},
    TypeNewWorkConn:        NewWorkConn{},
    TypeReqWorkConn:        ReqWorkConn{},
    TypeStartWorkConn:      StartWorkConn{},
    TypeNewVisitorConn:     NewVisitorConn{},
    TypeNewVisitorConnResp: NewVisitorConnResp{},
    TypePing:               Ping{},
    TypePong:               Pong{},
    TypeUDPPacket:          UDPPacket{},
    TypeNatHoleVisitor:     NatHoleVisitor{},
    TypeNatHoleClient:      NatHoleClient{},
    TypeNatHoleResp:        NatHoleResp{},
    TypeNatHoleSid:         NatHoleSid{},
    TypeNatHoleReport:      NatHoleReport{},
}

Server 逻辑

多路复用

http 反向代理支持多路复用

利用 http.client 的参数 transport 来支持连接多路复用

Transport is an implementation of RoundTripper that supports HTTP, HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT).

By default, Transport caches connections for future re-use.

func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *HTTPReverseProxy {
    ...
    proxy := &httputil.ReverseProxy{
        // Modify incoming requests by route policies.
        Director: func(req *http.Request) {
        ...
        }
        // Create a connection to one proxy routed by route policy.
        Transport: &http.Transport{
            ResponseHeaderTimeout: rp.responseHeaderTimeout,
            IdleConnTimeout:       60 * time.Second,     限制每个空闲连接的超时时间
            MaxIdleConnsPerHost:   5,                    限制单个 Host 最大空闲连接数
            DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
                return rp.CreateConnection(ctx.Value(RouteInfoKey).(*RequestRouteInfo), true)
            },
         ....
        },
        ...
    }
    rp.proxy = proxy
    return rp
}

演示Demo

通过 SSH 访问内网机器

实验环境

机器名称机器系统充当角色IP
开发机Linux代理的 server 端10.37.4.140
机器一Linux代理的 client 端(模拟内网的机器)10.88.172.247
机器二Linux代理的 client 端(模拟内网的机器)10.88.171.160
本地macMac模拟通过代理 server 连接内网的机器10.88.225.137

具体操作

  1. 下载工具
  • 下载 frp 工具 https://github.com/fatedier/frp/releases/tag/v0.53.2 并解压
  1. server 端部署
  • 修改 frps.toml 文件如下

使用 tcpmux 类型的代理,可以实现多个 SSH 服务通过同一端口进行暴露

bindPort = 7000 # 代表服务代理的端口tcpmuxHTTPConnectPort = 5002    
  • 启动服务端
./frps -c ./frps.toml
  1. Client 端部署
  • 机器一修改 frpc.toml 文件如下
serverAddr = "10.37.4.140"        # 服务端的公网
IPserverPort = 7000               # 服务端开放的端口
[[proxies]]                
name = "ssh1"
type = "tcpmux"
multiplexer = "httpconnect"
customDomains = ["machine-a.example.com"]
localIP = "127.0.0.1"   # localIP 和 localPort 配置为需要从公网访问的内网服务的地址和端口localPort = 22                    
  • 机器二修改 frpc.toml 文件如下
serverAddr = "10.37.4.140"
serverPort = 7000
[[proxies]]
name = "ssh2"
type = "tcpmux"
multiplexer = "httpconnect"
customDomains = ["machine-b.example.com"] 
localIP = "127.0.0.1"
localPort = 22

注意标红色字体的区别

  1. 启动 Client 端
./frpc -c ./frpc.toml
  1. 本地mac通过server代理地址访问内网的机器
  • mac访问访问两台client盒子
ssh -o 'proxycommand socat - PROXY:10.37.4.140:machine-a.example.com:22,proxyport=5002' root@machine-a

-o 用于指定 SSH 的配置选项proxycommand 指定代理命令,即 socat 命令,用于建立与代理服务器的连接socat 命令通过代理服务器将流量转发到目标主机 machine-a.example.com 的 SSH 服务端口 22,并使用代理端口 5002root@machine-a 是目标主机的连接地址,其中 root 是登录用户名,machine-a 是目标主机的名称
ssh -o 'proxycommand socat - PROXY:10.37.4.140:machine-b.example.com:22,proxyport=5002' root@machine-b

需要安装 socat 软件:socat介绍

显示如上输入代表访问成功

在这里插入图片描述
在这里插入图片描述

通过自定义域名访问内网的 Web 服务

具体操作

盒子部署服务

我们在其中一个盒子上部署 Nginx 服务来模拟内网的 web 服务

Client 端配置

修改 client 端的 toml 文件配置

serverAddr = "10.37.4.140"
serverPort = 7000
[[proxies]]
name = "web"
type = "http"
localPort = 80
customDomains = ["www.yourdomain.com"]

Server 端配置

bindPort = 7000
vhostHTTPPort = 8080

mac访问内网的服务

手动修改域名解析的地址

cat /etc/hosts
10.37.4.140 yourdomain.com

访问 curl http://www.yourdomain.com:8080

在这里插入图片描述

成功

优秀代码

Panic 转化为 Error

func PanicToError(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("Panic error: %v", r)
        }
    }()

    fn()
    return
}

err = gerr.PanicToError(func() {
        tg.acceptCh <- c
    })

判断为空赋值

package util

func EmptyOr[T comparable](v T, fallback T) T {
    var zero T
    if zero == v {
        return fallback
    }
    return v
}

duration = util.EmptyOr(duration, time.Second)

错误处理

func AppendError(err error, errs ...error) error {
    if len(errs) == 0 {
        return err
    }
    return errors.Join(append([]error{err}, errs...)...)
}

待解决问题

p2p 通信(埋个坑:会在下一篇文章解决)

https://gofrp.org/zh-cn/docs/examples/xtcp/

需要实现一个 natHoleStunServer,否则可能报错如下
在这里插入图片描述

参考文档

内网透传frp使用分享

https://www.modb.pro/db/1700051099159973888

https://zhuanlan.zhihu.com/p/654553693

https://github.com/ehang-io/nps/blob/master/README_zh.md

使用frp进行内网穿透 - 少数派

内网穿透FRP详细教程 - FreeBuf网络安全行业门户

https://github.com/fatedier/frp

frp

使用frp进行内网穿透(内网隧道搭建) - 11阳光 - 博客园

frp v0.5.0 源码分析 - JoXrays’s Blog

https://www.cnblogs.com/skymyyang/p/13392848.html

Go语言http.Transport的连接管理与复用技巧-Golang-PHP中文网

golang net/http HttpClient连接复用的坑位

Golang Http连接池

内网穿透—使用 frp 实现内外网互通 - 小葛师兄 - 博客园

使用Frp内网穿透工具 | Escape

### CTF比赛中FRP内网穿透教程 #### 使用场景说明 在CTF竞赛环境中,尤其是涉及内部网络资源访问的任务中,利用FRP(Fast Reverse Proxy)实现内网穿透成为一种有效手段。通过这种方式可以绕过防火墙或其他安全设备对外部连接的限制,从而获取目标机器上的敏感信息或执行特定操作。 #### 准备工作 为了顺利完成基于FRP内网穿透实验,在本地环境需安装好必要的工具和服务端程序: - 安装Go语言编译器以便于构建最新版本的frp二进制文件; - 下载官方发布的适用于不同平台架构类型的预编译包; #### 配置服务端与客户端参数 配置过程分为两部分——服务器端设置以及客户机侧调整。对于服务器而言,主要关注监听地址、端口转发模式等选项;而针对客户端,则要指定远程主机IP地址及其开放的服务端口号。 ##### 服务端配置 (`frps.ini`) ```ini [common] bind_port = 7000 token = complex_password_for_security_reasons ``` ##### 客户端配置 (`frpc.ini`) ```ini [common] server_addr = x.x.x.x ; 替换成实际公网ip server_port = 7000 token = complex_password_for_security_reasons [tcp_service] type = tcp local_ip = 127.0.0.1 local_port = 3389 ; 假设这里是要映射RDP服务 remote_port = 6000 ; 对外暴露此端口供外部访问 ``` 启动命令如下所示: ```bash ./frps -c ./frps.ini # 启动服务端 ./frpc -c ./frpc.ini # 启动客户端 ``` 一旦完成上述步骤之后,便可以通过互联网上任意位置尝试连接至`x.x.x.x:6000`来间接访问位于局域网内的Windows桌面系统[^1]。 #### 实战演练建议 考虑到实战中的复杂性和不可预见因素的影响,在练习过程中应当尽可能模拟真实的攻防对抗情境。例如,可参照提供的Nmap扫描指令来进行初步的信息收集活动[^2],再结合Ngrok创建临时隧道以测试数据传输功能[^3]。不过需要注意的是,所有行动都应在合法授权范围内开展,并严格遵循赛事规则和道德准则。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值