内网穿透:路由器拨号+端口转发+DDNS实现自定义域名访问内网服务器

前言

经常用各种云盘,但感觉还是不够好:

  • 1.文件在别人服务器上终究不可靠,最近阿里云盘还曝出隐私文件泄露问题
  • 2.这些个免费云盘又是限速又是要下载客户端才能用,很麻烦,这些个垃圾客户端一个都不想装

于是就想搞一个私有云盘,电脑端直接用网页访问。

正好之前买的国内的云服务器到期了,甲骨文云上免费的两台服务器国内访问又很慢,做不了什么东西,于是买了个低功耗迷你主机,直接把物理服务器放家里,牢牢掌握所有权,还能把一些小服务迁移过来。

研究了一下开源的一些云盘系统,要么功能很复杂,要么UI太难看或者没有安卓客户端可用,要么是英文文档不想看。

最后决定用国内的一个不开源的网盘系统“可道云”,界面看起来比较舒服,也有许多插件可以用(在线预览视频图片、图片编辑、在线文档、markdown编辑器等等很方便),可以外链分享,不需要下载客户端,网页可以直接访问,支持用户管理,而且可以用宝塔面板一键部署

云盘是部署好了,内网访问一点毛病没有,问题是公网到底应该怎么访问?

总的来说两种方式:

  • 1.依赖中转服务器的,缺点:慢,数据经过别人手上
    • 一些内网穿透服务商,花生壳之类的,一般速度比较慢,还有一个国内的chmlfrphttps://panel.chmlfrp.cn/, 带宽可以给8M,算是很好了
    • 或者各种frp服务,都需要一台云服务器进行转发,也有免费提供服务的,也是慢
    • 也试过Cloudflare的Tunnel,直接建立隧道,而且默认开启SSL,还是相当不错的,但是在国内Cloudflare的速度实在是,一个字,超级慢!
  • 2.P2P的模式,用公网IP通过端口映射连接内网设备,快的要命,缺点是麻烦,主要有以下一些问题:
    • 运营商通常不会分配公网IP,很难申请
    • 有公网IP也没用,光猫防火墙默认开启无法关闭,并且禁用ping,并且端口映射功能阉割没法用,必须把光猫改成桥接模式,改用路由器拨号
    • 即使光猫能端口映射也没用,光猫的DHCP服务器非常垃圾,所以一般用路由器承担DHCP服务器,这样的话内网设备通常和光猫不在同一网段,即使光猫可以做端口转发,也需要路由器和光猫转发两次,有点复杂
    • 用路由器拨号也没用,路由器本身也有防护墙,可能没法关闭

光猫改桥接用路由器拨号

这一步联系运营商解决,改桥接后至少解决了光猫防火墙无法关闭的问题,路由器能够设置端口转发了

路由器设置端口转发

我用的是小米路由器,自带端口转发功能,还有一个DMZ功能,可以把内网某个设备的所有端口映射到路由器公网IP的端口上,此时访问公网IP:端口等同于访问内网IP:端口,但是要注意:公网IP的80和443端口是被禁用的,用这个端口转发不会生效

高级设置-端口转发,这样会暴露所有端口,因此要在内网服务器上设置防火墙
image
此时在 常用设置-上网设置 查看路由器公网IP,在后面添加内网服务的端口号,即可访问内网服务,我这里ipv4和ipv6(需要路由器开启ipv6功能)都可以
image

设置DDNS

这样设置完成后就只剩下一个问题了,运营商给的公网IP是会变的,即使网络一直在用不中断,也会随机改变,公网ipv6地址不确定会不会变,目前没看到变化,但姑且认为他也是会变的,这时候就需要用到DDNS了,

DDNS(动态域名系统)是一种服务,它允许你将动态IP地址映射到一个固定的域名上。这样,即使你的IP地址变化,你也能通过域名来访问你的服务器或服务。DDNS服务对于家庭用户和小型企业尤其有用,因为他们通常没有静态IP地址,但可能需要远程访问家中或办公室的网络资源。

实现DDNS的方式也有很多,有很多DDNS服务商可以提供相关服务,我没有用这些服务,而是利用了Cloudflare的API,定时主动更新DNS记录,需要申请API令牌
微信截图_20240930192049
微信截图_20240930192159
代码如下,实现的功能是每隔10分钟自动读取一次设备的公网IP(我这里设备是没有公网ipv4地址的,由于开启了端口转发,因此只要获取路由器的公网IP即可,如果路由器没有公网IPv4地址,可以用IPv6地址)并调用API对DNS记录进行更新,我这里同时添加了ipv4和ipv6地址到pan.xinhaojin.top,客户端如果支持ipv6会优先使用ipv6访问

  • while True:
  • 读公网IP
  • 通过API查二级域名对应的DNS记录值
    • 如果没有记录,则添加一条
    • 如果与当前获取的公网IP不一致,则进行更新
  • sleep一段时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/bin/sh
# 定义日志文件路径
logFile="/root/DdnsOnCloudFlare/ddns.log"

# 定义日志记录函数,包含时间戳
log() {
local level=$1
local message=$2
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> "$logFile"
}
# 定义 Cloudflare 区域 ID、记录名和 API 密钥
zoneId='209367ebxxxxxxxx'
recordName='pan.xinhaojin.top'
apiKey='zRIxxxxxxx'

# 获取公网 IPv4 地址的函数
getIpv4Address() {
wget -qO- https://ipinfo.io/ip
}

# 获取公网 IPv6 地址的函数
getIpv6Address() {
ip -6 addr show enp3s0 | grep 240e | awk '{print $2}' | cut -d'/' -f1 | head -n 1
}

# 从 Cloudflare 获取 DNS 记录的函数
listRecord() {
local zoneId=$1
local recordName=$2
local apiKey=$3
local recordType=$4 # 记录类型

# 构建查询 URL,包含记录类型
local url="https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records?name=$recordName&type=$recordType"
local result=$(
curl -s -X GET "$url" \
-H "Content-Type:application/json" \
-H "Authorization: Bearer $apiKey"
)
local resourceId=$(echo "$result" | grep -Po '(?<="id":")[^"]+')
local currentValue=$(echo "$result" | grep -Po '(?<="content":")[^"]+')

local successStat=$(echo "$result" | grep -Po '(?<="success":)[^,]+')
if [ $successStat != "true" ]; then
return 1
fi

printf '%s\n%s' "$resourceId" "$currentValue"
}

# 更新 DNS 记录的函数
updateRecord() {
local zoneId=$1
local recordName=$2
local apiKey=$3
local resourceId=$4
local type=$5
local value=$6

local result=$(
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/$resourceId" \
-H "Authorization: Bearer $apiKey" \
-H "Content-Type: application/json" \
--data "{\"type\":\"$type\",\"name\":\"$recordName\",\"content\":\"$value\",\"ttl\":600,\"proxied\":false}"
)

local successStat=$(echo "$result" | grep -Po '(?<="success":)[^,]+')
[ "$successStat" = "true" ]
return $?
}

# 创建 DNS 记录的函数
createRecord() {
local zoneId=$1
local recordName=$2
local apiKey=$3
local type=$4
local value=$5

local result=$(
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records" \
-H "Authorization: Bearer $apiKey" \
-H "Content-Type: application/json" \
--data "{\"type\":\"$type\",\"name\":\"$recordName\",\"content\":\"$value\",\"ttl\":600,\"proxied\":false}"
)
local successStat=$(echo "$result" | grep -Po '(?<="success":)[^,]+')
if [ $successStat != "true" ]; then
return 1
fi
local recordId=$(echo "$result" | grep -Po '(?<="id":")[^"]+')
echo "$recordId"
}

while true; do
# 获取当前的公网 IPv4 地址
currentIpv4=$(getIpv4Address)
log "INFO" "获取到的外部 IPv4 地址为: $currentIpv4"

# 获取当前的公网 IPv6 地址
currentIpv6=$(getIpv6Address)
log "INFO" "获取到的外部 IPv6 地址为: $currentIpv6"

# 更新 IPv4 地址

currentStat=$(listRecord "$zoneId" "$recordName" "$apiKey" "A")

if [ $? -eq 0 ]; then
resourceId=$(echo "$currentStat" | sed -n '1p')
currentValue=$(echo "$currentStat" | sed -n '2p')
if [ -z "$resourceId" ]; then
log "INFO" "未找到 IPv4 记录,正在创建..."
resourceId=$(createRecord "$zoneId" "$recordName" "$apiKey" "A" "$currentIpv4")
if [ $? -eq 0 ]; then
log "INFO" "IPv4 记录创建成功"
else
log "ERROR" "IPv4 记录创建失败"
fi
elif [ $currentValue != $currentIpv4 ]; then
log "INFO" "当前 IPv4 记录为: $currentValue"
if updateRecord "$zoneId" "$recordName" "$apiKey" "$resourceId" "A" "$currentIpv4"; then
log "INFO" "IPv4 更新成功"
else
log "ERROR" "IPv4 更新失败"
fi
else
log "INFO" "当前 IPv4 记录为: $currentValue"
log "INFO" "当前 IPv4 记录与预更新 IPv4 相同,无需更新"
fi
else
log "ERROR" "获取当前 IPv4 记录失败"
fi

# 更新 IPv6 地址

currentStat=$(listRecord "$zoneId" "$recordName" "$apiKey" "AAAA")
if [ $? -eq 0 ]; then
resourceId=$(echo "$currentStat" | sed -n '1p')
currentValue=$(echo "$currentStat" | sed -n '2p')
if [ -z "$resourceId" ]; then
log "INFO" "未找到 IPv6 记录,正在创建..."
resourceId=$(createRecord "$zoneId" "$recordName" "$apiKey" "AAAA" "$currentIpv6")
if [ $? -eq 0 ]; then
log "INFO" "IPv6 记录创建成功"
else
log "ERROR" "IPv6 记录创建失败"
fi
elif [ $currentValue != $currentIpv6 ]; then
echo "当前 IPv6 记录为: $currentValue"
if updateRecord "$zoneId" "$recordName" "$apiKey" "$resourceId" "AAAA" "$currentIpv6"; then
log "INFO" "IPv6 更新成功"
else
log "ERROR" "IPv6 更新失败"
fi
else
log "INFO" "当前 IPv6 记录为: $currentValue"
log "INFO" "当前 IPv6 记录与预更新 IPv6 相同,无需更新"
fi
else
log "ERROR" "获取当前 IPv6 记录失败"
fi

sleep 600 # 等待 10 分钟
done

添加一个服务到/etc/systemd/system/ddns.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Update DNS Records Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/root/DdnsOnCloudFlare/
ExecStart=/bin/bash /root/DdnsOnCloudFlare/autoUpdateDNS.sh
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
1
2
3
systemctl daemon-reload
systemctl enable ddns
systemctl start ddns

正常log

1
2
3
4
5
6
[2024-10-16 20:58:16] [INFO] 获取到的外部 IPv4 地址为: 60.163.110.17
[2024-10-16 20:58:16] [INFO] 获取到的外部 IPv6 地址为: 240e:390:485b:ac40::e1a
[2024-10-16 20:58:19] [INFO] 当前 IPv4 记录为: 60.163.110.17
[2024-10-16 20:58:19] [INFO] 当前 IPv4 记录与预更新 IPv4 相同,无需更新
[2024-10-16 20:58:36] [INFO] 未找到 IPv6 记录,正在创建...
[2024-10-16 20:58:54] [INFO] IPv6 记录创建成功

nginx配置注意事项

由于公网访问pan.xinhaojin.top需要增加端口号88,不是很优雅,所以会想到通过nginx转发,如下配置

1
2
3
4
5
6
7
8
server {
listen 80;
server_name pan.xinhaojin.top;

location / {
return 301 http://pan.xinhaojin.top:88$request_uri;
}
}

但实际配置后在内网可省略端口号,但公网访问不可省略端口号,且可能导致添加端口号也无法访问,原因是运营商禁80和443端口

下图配置的时候我应用了nginx端口转发,同时启用了https,网站访问起来很优雅,但还是不建议添加这个nginx配置,还是多一步手动添加端口号吧

成果展示

image
WPS拼图0
上传速度可以达到40MB/s,哪个网盘有我快?(按理我上传没有有这么大带宽啊),反正5G网络测试下载里面的视频速度可以有10MB以上
image
下一步想看看能不能把夸克网盘挂载到这上面,这样就可以用这一个网页来管理我的私有云和常用的夸克云盘了


内网穿透:路由器拨号+端口转发+DDNS实现自定义域名访问内网服务器
https://xinhaojin.github.io/2024/10/01/内网穿透路由器拨号+端口转发+DDNS实现自定义域名访问内网服务器/
作者
xinhaojin
发布于
2024年10月1日
许可协议