代理
没有公网 IP?内网穿透神器,轻松访问所有网络!
给内网穿透FRP套上坚固的盾牌
又来一款轻量超好用的内网穿透项目,支持web管理,太6了!
本文档使用 MrDoc 发布
-
+
首页
给内网穿透FRP套上坚固的盾牌
### 背景 有一天,我突然发现无法从外部连接家里的NAS了。我开始慌了,预感到不妙。莫非是公网IP被运营商回收了?我也成为一个大内网用户了。所幸已经有不少成熟的方案,而FRP就是其中之一。它开源免费,易上手,并且支持的协议还比较多(当然,部署服务器的费用得另算)。晚上回到家,我决定面对现实,好好折腾一番。虽然网上现有的FRP教程多数只完成了‘能用’的第一步,但距离‘好用易用’还有点距离。 本文简要描述一下我使用FRP的过程,并且看一下我们如何给FRP套上坚固的盾牌,配上易用的武器。我假定你已经知道FRP是什么,并且最基本的FRP使用已经了解。不了解也没关系,继续看你也大概能懂。 > 虽然咱数据或许对别人而言也没那么重要,但自我保护意识也不可松懈。 ### 目标制定 > frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP、UDP、HTTP、HTTPS 等多种协议,且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。 为了方便迁移和管理,在使用FRP时我首推容器化方案。不过似乎没看到官方的镜像,但dockerhub上有一个社区广泛使用且下载量很高的镜像,大抵错不了:snowdreamtech/frpc和snowdreamtech/frps。 FRP是分客户端和服务端的,需要在不同的机器分别配置。frpc一般部署在内网,用于将内部需要对外暴露的服务定义出来。而frps一般部署在有公网IP的服务器上,用于接收外部连接并转发到内部服务。这里有几个安全事项需要关注: 1. 内网frpc和公网frps之间需要建立安全的连接。 2. 公网frps暴露的端口需要进一步限制连接来源。 3. 公网frps暴露的端口仅在必要时开放。 很多分享的方案里基本不启用TLS,也对暴露的端口没有进一步的限制,这其实是不安全的。秉承这些目标,我们开始行动吧。 ### 部署服务 内网部署frpc端 这里我直接使用docker-compose部署,并使用snowdreamtech/frpc镜像。 ``` version: '3.8' services: frpc: image: snowdreamtech/frpc:debian container_name: frpc restart: always network_mode: host volumes: - ./frpc.toml:/etc/frp/frpc.toml - ./client.crt:/etc/frp/client.crt - ./client.key:/etc/frp/client.key - ./ca.crt:/etc/frp/ca.crt environment: - TZ=Asia/Shanghai env_file: - ./.env ``` 这里需要的TLS证书一会我们再生成。这里的.env文件内容如下: ``` # 服务端连接信息 FRPC_SERVER_ADDR=<your-server-address> FRPC_SERVER_PORT=<your-server-port> # 服务器域名 FRPC_SERVER_NAME="<your-domain-for-tls>" # 认证令牌 - 必须与服务端一致 FRPC_AUTH_TOKEN=<your-auth-token> # 客户端仪表盘配置 FRPC_DASHBOARD_USER=admin FRPC_DASHBOARD_PWD=admin ``` 为了方便修改和对齐,我们将frpc.toml文件中的一部分配置放在.env文件中定义。而frpc.toml文件内容如下: ``` user = "kevin" serverAddr = "{{ .Envs.FRPC_SERVER_ADDR }}" serverPort = {{ .Envs.FRPC_SERVER_PORT }} loginFailExit = true log.to = "./frpc.log" log.level = "trace" log.maxDays = 3 log.disablePrintColor = false auth.method = "token" auth.token = "{{ if .Envs.FRPC_AUTH_TOKEN }}{{ .Envs.FRPC_AUTH_TOKEN }}{{ else }}{{ .Envs.FRP_AUTH_TOKEN }}{{ end }}" transport.poolCount = 5 transport.protocol = "tcp" transport.connectServerLocalIP = "0.0.0.0" transport.tls.enable = true transport.tls.certFile = "/etc/frp/client.crt" transport.tls.keyFile = "/etc/frp/client.key" transport.tls.trustedCaFile = "/etc/frp/ca.crt" transport.tls.serverName = "{{ .Envs.FRPC_SERVER_NAME }}" udpPacketSize = 1500 webServer.addr = "127.0.0.1" webServer.port = 7400 webServer.user = "{{ .Envs.FRPC_DASHBOARD_USER }}" webServer.password = "{{ .Envs.FRPC_DASHBOARD_PWD }}" [[proxies]] name = "router-web" type = "tcp" localIP = "192.168.1.1" localPort = 80 remotePort = 17603 [[proxies]] name = "external-http" type = "tcp" localIP = "192.168.50.96" localPort = 8080 remotePort = 9443 ``` ### 公网部署frps端 我们的服务端一般是部署在一台有公网IP的服务器上,用于我们从任何地方通过它连接回家里内网。这个服务器可以从国内国外各种云上买一台或者找机会白嫖一台。我是在腾讯云上有一台机器,安装好docker以及docker-compose后,使用snowdreamtech/frps镜像部署。部署在公网的服务,基于安全性考虑,我们希望即使frps被攻击,其它服务也是安全的,所以除了放在容器中,把网络也隔离出来。这里便不再使用network_mode: host,而是使用默认的network_mode: bridge,同时预留一些端口用于后续我们的服务暴露。 ``` version: '3.8' services: frps: image: snowdreamtech/frps:debian container_name: frps restart: always ports: - "9443:9443" - "17600-17610:17600-17610" # TCP/UDP 代理端口范围 (allowPorts),视你需要开放一些端口 volumes: - ./frps.toml:/etc/frp/frps.toml - ./server.crt:/etc/frp/server.crt - ./server.key:/etc/frp/server.key - ./ca.crt:/etc/frp/ca.crt environment: - TZ=Asia/Shanghai env_file: - ./.env ``` 同样的,为了配置修改方便,我们将frps.toml文件中的一部分配置放在.env文件中定义: ``` # 仪表盘访问凭证 FRPS_DASHBOARD_USER=<your-dashboard-username> FRPS_DASHBOARD_PWD=<your-dashboard-password> # 认证令牌 - 必须与客户端一致 FRPS_AUTH_TOKEN=<your-auth-token> # 绑定地址和端口配置 FRPS_BIND_ADDR=0.0.0.0 FRPS_BIND_PORT=17600 FRPS_KCP_BIND_PORT=17600 FRPS_DASHBOARD_PORT=17601 ``` 而frps.toml文件内容如下: ``` bindAddr = "{{ .Envs.FRPS_BIND_ADDR }}" bindPort = {{ .Envs.FRPS_BIND_PORT }} kcpBindPort = {{ .Envs.FRPS_KCP_BIND_PORT }} transport.maxPoolCount = 5 transport.tls.force = true transport.tls.certFile = "/etc/frp/server.crt" transport.tls.keyFile = "/etc/frp/server.key" transport.tls.trustedCaFile = "/etc/frp/ca.crt" webServer.addr = "0.0.0.0" webServer.port = {{ .Envs.FRPS_DASHBOARD_PORT }} webServer.user = "{{ .Envs.FRPS_DASHBOARD_USER }}" webServer.password = "{{ .Envs.FRPS_DASHBOARD_PWD }}" webServer.pprofEnable = false # 开放的端口范围,这里可以配置适大一些,更多的映射(限制)在docker-compose中 allowPorts = [ { start = 17000, end = 17999 }, { single = 9443 } ] enablePrometheus = true log.to = "./frps.log" log.level = "trace" log.maxDays = 3 log.disablePrintColor = false detailedErrorsToClient = true auth.method = "token" auth.token = "{{ if .Envs.FRPS_AUTH_TOKEN }}{{ .Envs.FRPS_AUTH_TOKEN }}{{ else }}{{ .Envs.FRP_AUTH_TOKEN }}{{ end }}" auth.oidc.issuer = "" auth.oidc.audience = "" auth.oidc.skipExpiryCheck = false auth.oidc.skipIssuerCheck = false maxPortsPerClient = 0 udpPacketSize = 1500 natholeAnalysisDataReserveHours = 168 ``` ### 生成TLS证书 为了让FRP的连接更安全,我们使用TLS证书来加密连接。它确保我们的frpc和frps之间的连接是安全的,并且防止中间人攻击。我们使用自签名证书来生成TLS证书。以下提供了一段脚本,方便一键生成我们需要的证书。 ``` #!/bin/bash # 脚本用于生成 FRP 配置所需的自签名证书 set -e # 任何命令失败立即退出 echo "开始生成 FRP 通信证书..." # 设置默认值,但要求用户至少提供一个 SERVER_DOMAIN="" SERVER_IP="" # 仅重新生成服务器证书的选项 REGENERATE_SERVER_ONLY=false # 显示帮助信息 show_help() { echo "用法: $0 [选项]" echo "" echo "选项:" echo " --server-only 仅重新生成服务器证书,保留现有CA证书" echo " --domain=<域名> 指定服务器域名" echo " --ip=<IP地址> 指定服务器IP地址" echo " --help 显示此帮助信息" echo "" echo "至少需要指定域名或IP地址中的一个" exit 1 } # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in --server-only) REGENERATE_SERVER_ONLY=true shift ;; --domain=*) SERVER_DOMAIN="${1#*=}" shift ;; --ip=*) SERVER_IP="${1#*=}" shift ;; --help) show_help ;; *) echo "未知选项: $1" show_help ;; esac done # 检查是否提供了至少一个参数 if [ -z "$SERVER_DOMAIN" ] && [ -z "$SERVER_IP" ]; then echo "错误: 必须至少指定域名(--domain)或IP地址(--ip)中的一个" show_help fi # 显示配置信息 echo "证书配置:" if [ -n "$SERVER_DOMAIN" ]; then echo "- 域名: $SERVER_DOMAIN" fi if [ -n "$SERVER_IP" ]; then echo "- IP地址: $SERVER_IP" fi if [ "$REGENERATE_SERVER_ONLY" = false ]; then # 1. 生成 CA 根证书 echo "生成 CA 根证书..." openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \ -subj "/C=CN/ST=Beijing/L=Beijing/O=FRP-Private/OU=DevOps/CN=frp-ca" else echo "跳过 CA 证书生成,使用现有 CA 证书..." # 检查CA证书是否存在 if [ ! -f "ca.key" ] || [ ! -f "ca.crt" ]; then echo "错误: CA证书文件不存在。请先运行不带 --server-only 参数的脚本生成完整证书集。" exit 1 fi fi # 2. 生成服务端证书 echo "生成服务端证书..." openssl genrsa -out frps/server.key 4096 # 设置默认CN CN_VALUE="${SERVER_DOMAIN}" if [ -z "$CN_VALUE" ]; then CN_VALUE="frps" fi # 创建OpenSSL配置文件 cat > frps/openssl.cnf << EOF [ req ] default_bits = 4096 prompt = no default_md = sha256 req_extensions = req_ext distinguished_name = dn [ dn ] C = CN ST = Beijing L = Beijing O = FRP-Private OU = DevOps CN = ${CN_VALUE} [ req_ext ] subjectAltName = @alt_names [ alt_names ] EOF # 动态添加DNS和IP到配置文件 DNS_COUNT=1 if [ -n "$SERVER_DOMAIN" ]; then echo "DNS.${DNS_COUNT} = ${SERVER_DOMAIN}" >> frps/openssl.cnf DNS_COUNT=$((DNS_COUNT+1)) fi echo "DNS.${DNS_COUNT} = localhost" >> frps/openssl.cnf IP_COUNT=1 if [ -n "$SERVER_IP" ]; then echo "IP.${IP_COUNT} = ${SERVER_IP}" >> frps/openssl.cnf IP_COUNT=$((IP_COUNT+1)) fi echo "IP.${IP_COUNT} = 127.0.0.1" >> frps/openssl.cnf # 使用配置文件生成CSR openssl req -new -key frps/server.key -out frps/server.csr -config frps/openssl.cnf # 创建扩展配置文件 - 正确的格式 cat > frps/v3.ext << EOF subjectAltName = @alt_names [alt_names] EOF # 添加DNS和IP到扩展配置 if [ -n "$SERVER_DOMAIN" ]; then echo "DNS.1 = ${SERVER_DOMAIN}" >> frps/v3.ext echo "DNS.2 = localhost" >> frps/v3.ext else echo "DNS.1 = localhost" >> frps/v3.ext fi if [ -n "$SERVER_IP" ]; then echo "IP.1 = ${SERVER_IP}" >> frps/v3.ext echo "IP.2 = 127.0.0.1" >> frps/v3.ext else echo "IP.1 = 127.0.0.1" >> frps/v3.ext fi # 签署证书,应用SAN扩展 openssl x509 -req -in frps/server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out frps/server.crt -days 3650 -sha256 -extfile frps/v3.ext if [ "$REGENERATE_SERVER_ONLY" = false ]; then # 3. 生成客户端证书 echo "生成客户端证书..." openssl genrsa -out frpc/client.key 4096 openssl req -new -key frpc/client.key -out frpc/client.csr \ -subj "/C=CN/ST=Beijing/L=Beijing/O=FRP-Private/OU=DevOps/CN=frpc" openssl x509 -req -in frpc/client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out frpc/client.crt -days 3650 -sha256 # 4. 分发 CA 证书到 frps 和 frpc 目录 echo "分发 CA 证书到各个目录..." cp ca.crt frps/ca.crt cp ca.crt frpc/ca.crt else echo "跳过客户端证书生成,仅更新服务器证书..." fi # 5. 清理中间文件 echo "清理临时文件..." rm -f frps/server.csr ca.srl frps/openssl.cnf frps/v3.ext if [ "$REGENERATE_SERVER_ONLY" = false ]; then rm -f frpc/client.csr fi echo "设置文件权限..." chmod 600 ca.key frps/server.key if [ "$REGENERATE_SERVER_ONLY" = false ]; then chmod 600 frpc/client.key fi echo "证书生成完成!" if [ "$REGENERATE_SERVER_ONLY" = true ]; then echo "已使用现有CA证书重新生成服务器证书" else echo "注意:ca.key 是敏感文件,请妥善保管,建议不要上传到代码仓库。" fi # 输出证书信息和提示 echo "" echo "===== 证书信息 =====" if [ -n "$SERVER_DOMAIN" ]; then echo "服务器证书包含域名: ${SERVER_DOMAIN}" echo "如果使用域名连接,请在frpc.toml中设置: transport.tls.serverName = \"${SERVER_DOMAIN}\"" fi if [ -n "$SERVER_IP" ]; then echo "服务器证书包含IP地址: ${SERVER_IP}" fi # 提示下一步操作 echo "" echo "===== 下一步操作 =====" echo "1. 复制证书文件到相应位置" echo "2. 更新frpc.toml中的服务器地址和相关配置" echo "3. 使用docker-compose restart重启服务" # 验证证书内容 echo "" echo "===== 验证证书 =====" echo "查看证书内容(包括SAN扩展):" openssl x509 -in frps/server.crt -text -noout | grep -A1 "Subject Alternative Name" ``` 使用方式: ``` ./generate-frp-certs.sh --domain=<your-domain> --ip=<your-ip> ``` 这样我们的服务器与客户端双向认证,谁都不可被冒充。 ### 安全加固 到这里,你的FRP连接已经通过TLS和双向认证得到了很好的保护,即使token不慎泄露,没有匹配的证书也无法建立连接。接下来,我们在此基础上更进一步,结合云主机的防火墙(安全组)策略,实现更精细的访问控制。我们希望达成: * 仅允许必要的端口放通 * 仅允许必要的IP连接 相信很多人都明白这个道理,但手动操作实在繁琐,所以我们需要一个工具来帮忙。在我看来,最便捷的莫过于再次祭出alfred workflow来实现。于是我抽空写了一个,并开源在github上,感兴趣的可以看这里:alfred-workflow-sg-manager[1]。 我们基于frpc.toml中的一部分配置[[proxies]],来动态开启与关闭相关的端口。一点点前置工作还需要你做的,便是将你的服务器绑定一个安全组,至于安全组后续规则添加维护等就交给这个工具了。 比如先查看一下当前列表:  你上面可以看到这些内网服务还未开放,我们选择一个,比如想从外部访问家里的Mac mini了,我们在alfred框中输入frp open可以看到可开放的列表:  选中Mac mini,然后回车,这个通路便打开了。再次查看可以看到它开放并且绑定了本机的出口IP:  现在你可以开心的从外部VNC到你家内网的Mac mini了,并且安全得多了。 用过之后想关闭的一些端口,比如Mac mini的VNC端口,我们输入frp close,然后选择对应的要关闭的服务回车即可:  现在咱是不是对于FRP的安全使用更有信心了。有些人可能会说:何苦呢,有谁会看中咱攻击咱呢?或许可能Maybe:我们就是控制欲作祟而已:) ### 一些技巧 还有两个小窍门我也想让你知道,看在这么认真的份上小手点点赞不过份吧! 第一:如上面截图出现了external-http,我们可以借助于将内网服务统一在一个ingress服务(反向代理)下,然后通过这个ingress服务进一步路由,这样我们只需要穿透一个端口即可访问内网的各种服务,免去了配置FRP的手续。也通过让ingress服务走TLS,或者后端服务对接OAuth2等更安全的认证方式,可以进一步保护我们的内网服务。 第二:FRPS除了默认有Dashboard外,还支持Prometheus。我们可以复用这种生态。Prometheus用于收集监控数据,Grafana用于可视化数据和配置告警。比如我们可监控FRP的连接访问情况,并且添加告警,这样某个敏感服务有连接,我们便可以及时收到通知。 ### 后记 本文整体到这里就结束了。我们尝试用docker来部署了FRP的客户端和服务端,并且基于安全的考虑,我们启用了TLS和创建了一个便捷的工具来快速修改云上的安全策略。这犹如一块坚固的盾牌,避免我们可能受到的攻击。 当我折腾好FRP,并且安全地将它保护起来后。有一天,我查看我家的外网IP,发现它居然是一个公网IP。我的天啦,我这折腾一番可是为了啥!我要不要重新回归到公网IP的路线呢?可是我却放不下这份安全了呢。 感谢阅读,如果你觉得文章对你有启发,欢迎点赞、分享,有疑问也欢迎留言探讨。你们的互动是我继续产出的最大动力!期待下篇文章再见。
jaunt2005
2025年5月13日 17:10
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码