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

本文最后更新于 2025年1月7日 晚上

前言

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

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

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

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

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

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

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

总的来说两种方式:

  • 1.依赖中转服务器的,缺点:慢,数据经过别人手上
    • 一些内网穿透服务商,花生壳之类的,一般速度比较慢,还有一个国内的chmlfrphttps://appel.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访问

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
import requests
import time
import logging
import subprocess
import re
# 配置日志
logging.basicConfig(
filename="/root/DdnsOnCloudFlare/ddns.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)

# Cloudflare 配置
ZONE_ID='209367eb6d8cxxxx91252'
RECORD_NAME='pan.xinhaojin.top'
API_KEY='zRImW_R8RjxxxxxEQyy_ecoRTzeXKR4'

# 获取公网 IPv4 地址

def get_ipv4_address():
# 模拟浏览器的 User-Agent
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
}
url = "https://tool.lu/ip/"
try:
# 发送请求
response = requests.get(url, headers=headers)
# 检查请求是否成功
if response.status_code == 200:
# 提取包含 IP 地址的行
pattern = re.compile(r'你的外网IP地址是:([0-9.]+)</p>')
match = pattern.search(response.text)
if match:
# 提取 IP 地址
ip_address = match.group(1)
return ip_address
else:
logging.error("未找到包含 IP 地址的信息")
else:
logging.error(f"请求失败,状态码: {response.status_code}")
except Exception as e:
logging.error(f"发生异常: {e}")



# 获取公网 IPv6 地址
def get_ipv6_address():
try:
result = subprocess.run(
["ip", "-6", "addr", "show", "enp3s0"],
capture_output=True,
text=True,
)
lines = result.stdout.splitlines()
for line in lines:
if "240e" in line:
return line.strip().split()[1].split("/")[0]
except Exception as e:
logging.error(f"获取 IPv6 地址失败: {e}")
return None

# 列出 Cloudflare DNS 记录
def list_records(zone_id, record_name, api_key, record_type):
url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
params = {"name": record_name, "type": record_type}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
if data["success"]:
return [
(record["id"], record["content"])
for record in data["result"]
]
except requests.RequestException as e:
logging.error(f"获取 {record_type} 记录失败: {e}")
return []

# 删除 Cloudflare DNS 记录
def delete_record(zone_id, api_key, record_id):
url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
try:
response = requests.delete(url, headers=headers)
response.raise_for_status()
data = response.json()
if data["success"]:
logging.info(f"记录删除成功 (ID: {record_id})")
return True
except requests.RequestException as e:
logging.error(f"删除记录失败 (ID: {record_id}): {e}")
return False

# 创建 Cloudflare DNS 记录
def create_record(zone_id, record_name, api_key, record_type, value):
url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {
"type": record_type,
"name": record_name,
"content": value,
"ttl": 600,
"proxied": False,
}
try:
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
if data["success"]:
logging.info(f"{record_type} 记录创建成功 (值: {value})")
return True
except requests.RequestException as e:
logging.error(f"创建记录失败 ({record_type}, {value}): {e}")
return False

# 确保记录存在并更新
def ensure_record(zone_id, record_name, api_key, record_type, current_ip):
records = list_records(zone_id, record_name, api_key, record_type)
found = False
for record_id, record_value in records:
if record_value == current_ip:
found = True
logging.info(f"{record_type} 记录值匹配,无需更新 (值: {record_value})")
else:
logging.info(f"{record_type} 记录值不同,删除旧记录 (当前值: {record_value})")
delete_record(zone_id, api_key, record_id)
if not found:
logging.info(f"未找到匹配的 {record_type} 记录,正在创建新记录...")
create_record(zone_id, record_name, api_key, record_type, current_ip)

# 主循环
def main():
while True:
ipv4 = get_ipv4_address()
ipv6 = get_ipv6_address()

if ipv4:
logging.info(f"获取到的外部 IPv4 地址为: {ipv4}")
ensure_record(ZONE_ID, RECORD_NAME, API_KEY, "A", ipv4)
else:
logging.error("无法获取 IPv4 地址")

if ipv6:
logging.info(f"获取到的外部 IPv6 地址为: {ipv6}")
ensure_record(ZONE_ID, RECORD_NAME, API_KEY, "AAAA", ipv6)
else:
logging.error("无法获取 IPv6 地址")

time.sleep(600) # 等待 10 分钟

if __name__ == "__main__":
main()

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

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

[Service]
Type=simple
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
WorkingDirectory=/root/DdnsOnCloudFlare/
ExecStart=/home/jxh/miniconda3/bin/python3 /root/DdnsOnCloudFlare/autoUpdateDNS.py
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
2025-01-03 23:57:19,913 [INFO] 获取到的外部 IPv4 地址为: 218.73.126.186
2025-01-03 23:57:23,117 [INFO] A 记录值匹配,无需更新 (值: 218.73.126.186)
2025-01-03 23:57:23,117 [INFO] 获取到的外部 IPv6 地址为: 240e:390:4831:fdf0::360
2025-01-03 23:57:24,820 [INFO] AAAA 记录值匹配,无需更新 (值: 240e:390:4831:fdf0::360)
2025-01-03 23:57:24,820 [INFO] AAAA 记录值不同,删除旧记录 (当前值: 240e:390:4831:fdf0::a62)
2025-01-03 23:57:25,774 [INFO] 记录删除成功 (ID: 68acfa54b5ada2098976dfbf9a974294)

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日
更新于
2025年1月7日
许可协议