Debian家庭NAS解决方案(内网穿透/DDNS)
内网穿透 - 云端 #
笔者采用 Debian bookworm 镜像
apt update && apt install -y ca-certificates curl tree tmux python-is-python3
fastfetch(替代 neofetch 和 screenfetch)
wget -q https://github.com/fastfetch-cli/fastfetch/releases/download/2.46.0/fastfetch-linux-amd64.deb -O /tmp/fastfetch.deb && apt install -y /tmp/fastfetch.deb
Debian 13 更新: fastfetch 已被加入官方源
docker 和 lazydocker(视情况安装) #
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null && apt update
apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
wget -qO- https://github.com/jesseduffield/lazydocker/releases/download/v0.24.1/lazydocker_0.24.1_Linux_x86_64.tar.gz | tar -zxO lazydocker > /usr/local/bin/lazydocker && chmod a+x /usr/local/bin/lazydocker
境内云需要在 /etc/docker/daemon.json 配置加速源
配置以 阿里云提供的个人加速源 为例
frps #
wget -qO- /tmp/frp.tar.gz https://github.com/fatedier/frp/releases/download/v0.62.1/frp_0.62.1_linux_amd64.tar.gz | tar -zxC /tmp && \
install /tmp/frp*/frps /usr/local/bin
useradd -r -d /var/frp -s /usr/sbin/nologin frp
mkdir -p /etc/frp && mkdir -p /var/log/frp && chown frp: /var/log/frp
tee /etc/frp/frps.toml <<-EOF
auth.token = "$(openssl rand -hex 16)"
bindPort = 7000
log.to = "/var/log/frp/frps.log"
log.level = "info"
log.maxDays = 5
EOF
/etc/systemd/system/frps.service
[Unit]
Description=Server of frp: A fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.
After = network.target syslog.target
Wants = network.target
[Service]
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
User=frp
Group=frp
[Install]
WantedBy=multi-user.target
测试连通性(需要 nmap) sudo nping --tcp -p 7000 1.2.3.4
fail2ban SSH #
apt update && apt install -y rsyslog fail2ban
/etc/fail2ban/jail.local
[sshd]
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
enabled = true
maxretry = 3
findtime = 10m
bantime = 24h
fail2ban frp #
先添加过滤日志的 filter
/etc/fail2ban/filter.d/frps-app.conf
[Definition]
failregex = ^.+get a user connection \[<HOST>:[0-9]+\]$
ignoreregex =
测试 filter fail2ban-regex /var/log/frp/frps.log frps-app --print-all-matched
jail 直接封禁短期内大量的请求防止爆破,在 jail.local 里加上
[frps-app]
enabled = true
filter = frps-app
logpath = /var/log/frp/frps.log
maxretry = 100
findtime = 10m
bantime = 24h
测试 jail fail2ban-server -t
爆破来测试是否被封 nmap -sV -vvv -p[port] [ip]
查看封禁情况 fail2ban-client status frps-app
解封 fail2ban-client unban [ip]
备注:如果要给 frps 自身上保护可以加上这个 filter,并给 7000 上 jail
^.+client login info: ip \[<HOST>:[0-9]+\].+$
但问题在于 frps 的日志本身不是很详细,如果不是合法的 frp 连接,日志中将不会显示(就算开了最高级别日志,也不会记录 IP),因此给 frps 自身上 jail 作用不大,只能防止别人爆破 token,不能防止别人扫描
NAS #
将用户加入 sudo 组 gpasswd -a user sudo
锁定 root 的密码 sudo passwd -l root
安装软件包
sudo apt update && sudo apt install -y ssh git rsyslog vim net-tools tmux curl tree python-is-python3 nginx nmap zip
NetworkManager #
在没有显示器的情况下无干扰自动配置 WIFI
sudo apt install -y network-manager wpasupplicant wireless-tools
接管 ifupdown
/etc/NetworkManager/NetworkManager.conf
[main]
plugins=ifupdown,keyfile
[ifupdown]
managed=true
接管 networking service
sudo systemctl disable --now networking.service
NetworkManager 不会管理在 /etc/network/interfaces 中配置的网卡,需要只留一个本地环回
/etc/network/interfaces
source /etc/network/interfaces.d/*
auto lo
iface lo inet loopback
解决 GitHub 下载问题 #
/etc/hosts 加入 CNAME 解析到镜像即可,例如
raw.githubusercontent.com raw.gitmirror.com
frpc(SSH 内网穿透为例) #
wget -qO- /tmp/frp.tar.gz https://github.com/fatedier/frp/releases/download/v0.62.1/frp_0.62.1_linux_amd64.tar.gz | tar -zxC /tmp && \
cd /tmp/frp* && sudo install frpc /usr/local/bin
/etc/frp/frpc.toml
auth.token = "x"
serverAddr = "example.com"
serverPort = 7000
log.to = "/var/log/frp/frpc.log"
log.level = "info"
log.maxDays = 5
[[proxies]]
name = "nas-sshd"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 10000
transport.useCompression = true
创建服务,使 NAS 来电自启后能够自动建立穿透
useradd -r -d /var/frp -s /usr/sbin/nologin frp
mkdir -p /etc/frp && mkdir -p /var/log/frp && chown frp: /var/log/frp
/usr/local/lib/systemd/system/frpc.service
需要 nss-lookup.target 而不只是 network.target
[Unit]
Description=Client of frp: A fast reverse proxy to help you expose a local server behind a NAT or firewall to the internet.
After = network.target nss-lookup.target
[Service]
User=frp
Group=frp
ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml
Restart=on-failure
[Install]
WantedBy=multi-user.target
你 可能 会需要让 SSH 只运作在 v4 上 (ListenAddress 0.0.0.0) 来防止暴露在 v6 公网
一些权限问题
- 家目录 700
- sftp 目录 770,然后将想要访问 sftp 的用户加支援组
gpasswd -a foobar sftp_user
文件传输 / 同步 #
rsync / sftp 可以走 ssh 内网穿透
sudo apt install rsync
useradd -s /bin/false -m -d /home/sftp_user sftp_user
ssh-keygen -t ed25519 -C "foo@example.com"
配置 /etc/ssh/sshd_config 的全局 PasswordAuthentication 为 no,让管理用户通过私钥连接,sftp_user 可以单独配置密码认证,方便移动端连接
Match User sftp_user
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication yes
ForceCommand internal-sftp
ChrootDirectory /mnt
PermitTunnel no
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
添加 ftp 文件夹
chown root:root /mnt
chmod 755 /mnt
mkdir -p /mnt/ftp
chown sftp_user:sftp_user /mnt/ftp
chmod 755 /mnt/ftp
添加公钥,然后修改权限
mkdir -p /home/sftp_user/.ssh
chown sftp_user:sftp_user /home/sftp_user/.ssh
chmod 700 /home/sftp_user/.ssh
chown sftp_user:sftp_user /home/sftp_user/.ssh/authorized_keys
chmod 600 /home/sftp_user/.ssh/authorized_keys
设备温度检测 #
sudo apt install lm-sensors
sudo sensors-detect --auto 来生成配置文件,重启即可
硬盘信息检测 #
sudo apt install smartmontools
sudo smartctl --all 即可检测硬盘信息,如启动时长、启停次数和硬盘温度
家庭影院 #
sudo apt install minidlna
编辑 /etc/minidlna.conf 将 media_dir 改为存放影视的路径即可
除了专用的投屏软件如贝当、乐播外,现在常见视频软件如爱优腾都自带 DLNA 支持(在笔者的电视上这些软件会定期广播 SSDP NOTIFY,频率堪称疯狂)
电视推流还需在手机上安装 BubbleUPnP 将手机作为 DLNA 控制器,然后选择电视上的视频软件作为 DLNA 渲染器
贴一个 ffmpeg 提取内嵌字幕的办法,可用于电视投屏
先查看所有的字幕 (subtitle) 流 ffprobe -select_streams s -show_streams input.mkv
选定流编号来导出,srt 模式为例 ffmpeg -i input.mkv -vn -an -codec:s:0 srt output.srt
主机可以直接用 VLC,选择 View - Playlist - UPnP 即可
另外贴一个笔者遇到的 bug:
VLC 的 UPnP 主动扫描模块只会扫描多个网卡中的一个,有时会被 docker0 干扰
目前还没有看到官方的修复
除了主动扫描外,其实也可以等待 miniDLNA 的周期 NOTIFY(通过配置文件的 notify_interval 配置)
然而 Linux 默认不会将网卡加到 UPnP 多播组,只能手动加入 sudo ip addr add 239.255.255.250 dev xxxxxx autojoin
https://gist.github.com/PhilipSchmid/2597c23c68f21d938779aa92683d30b2
防火墙的配置:被动扫描是多播出站,然后服务器单播入站回来,等待 NOTIFY 则需要监听多播入站
UPS 断电通知 #
WIP
等买了 UPS 再更吧,基本不停电,也不是很急
更新:IPv6 动态 DNS 免穿透 #
现在 IPv6 普及,可以走 v6 来避免内网穿透
一般家宽的 v6 地址不是固定的,所以需要动态 DNS (DDNS)
DDNS 有三种解决方案:一是买 DDNS 服务商的服务然后本地装 agent 定时通信,如 DynDNS;二是利用现有域名服务商的 API 手搓一个,配合 crontab 自动更新;三是在域名服务商处注册一个 ns 记录到 VPS,然后在 VPS 上搭 DNS 服务,本地和 VPS 定时通信
笔者这里就用 API 手搓的办法
先贴一个提取公网 IPv6 主机地址的正则
ip -6 addr show | sed -En 's/\s+inet6 ([0-9a-fA-F:]+)\/128 scope global.+/\1/p'
基本流程都大差不差:先提取本地 v6 地址,然后查询服务商的 AAAA 记录,如果不同则添加一个新的 AAAA 记录,再删掉旧的记录(添加和删除的顺序不建议调换,为什么可以自己想一下);如果服务商没有存储 AAAA 记录,直接添加一个新的记录就好
ddns.py 脚本如下,笔者使用的服务商是 spaceship
#!/usr/bin/env python3
import requests
import json
import subprocess
import sys
DEBUG = len(sys.argv) > 1
CMD = r"ip -6 addr show | sed -En 's/\s+inet6 ([0-9a-fA-F:]+)\/128 scope global.+/\1/p'"
X_API_KEY = "{X_API_KEY}"
X_SECRET_KEY = "{X_SECRET_KEY}"
DOMAIN = "example.com"
RECORD = "foobar"
URL = f"https://spaceship.dev/api/v1/dns/records/{DOMAIN}"
QUERY_DNS_PARAMS = {"take": "100", "skip": "0", "orderBy": "name"}
HEADERS = {
"X-API-Key": X_API_KEY,
"X-API-Secret": X_SECRET_KEY,
"Content-Type": "application/json",
}
def get_ipv6_address():
addr = subprocess.check_output(CMD, shell=True, text=True)
addr = addr.strip()
if len(addr) == 0:
raise Exception("Unable to get IPv6 address")
return addr
def query_dns_record():
response = requests.get(URL, params=QUERY_DNS_PARAMS, headers=HEADERS)
if not (200 <= response.status_code < 300):
print(response.status_code)
print(response.text)
raise Exception("DNS record query request failed")
records = json.loads(response.text)
for r in records["items"]:
if r["name"] == RECORD:
break
else:
print("Warning: Unable to find specified DNS record")
return None
addr = r["address"].strip()
return addr
def delete_dns_record(addr):
payload = [{"type": "AAAA", "address": addr, "name": RECORD}]
response = requests.delete(URL, json=payload, headers=HEADERS)
if not (200 <= response.status_code < 300):
print(response.status_code)
print(response.text)
raise Exception("DNS record delete request failed")
def update_dns_record(addr):
payload = {
"force": True,
"items": [{"type": "AAAA", "address": addr, "name": RECORD, "ttl": 1800}],
}
response = requests.put(URL, json=payload, headers=HEADERS)
if not (200 <= response.status_code < 300):
print(response.status_code)
print(response.text)
raise Exception("DNS record update request failed")
def main():
addr = get_ipv6_address()
print(addr)
prev_addr = query_dns_record()
if prev_addr is not None and prev_addr == addr:
print("IPv6 address not updated")
return 1
else:
print(prev_addr)
if DEBUG is True:
print("Debug: should update DNS record now")
else:
update_dns_record(addr)
print("DNS record updated successfully")
if prev_addr is None:
print("No old DNS record found, so not deleting")
else:
if DEBUG is True:
print("Debug: Should delete old DNS record now")
else:
# 一定要确保 update 成功了再 delete
delete_dns_record(prev_addr)
print("Old DNS record deleted successfully")
return 0
if __name__ == "__main__":
main()
crontab 每小时过 5 分的时候更新
5 * * * * /path/to/ddns.py
PT #
编译 transmission-daemon 4.1.0
sudo apt install git build-essential cmake libcurl4-openssl-dev libssl-dev python3 libsystemd-dev
mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release -DINSTALL_DOC=OFF -DENABLE_UTILS=OFF -DWITH_SYSTEMD=ON
更新:因为 tr 功能和性能都逊色于 qbt,现迁移至 qbittorrent-nox
sudo apt install qbittorrent-nox
useradd -r -s /usr/sbin/nologin -d /var/bt -m bt_user
服务启动前需要以 bt_user 单跑一下 qbittorrent-nox 选择同意隐私声明
sudo -u bt_user qbittorrent-nox
/etc/systemd/system/qbittorrent-nox.service
[Unit]
Description=qBittorrent Command Line Client
After=network.target nss-lookup.target
[Service]
Type=forking
User=bt_user
Group=bt_user
ExecStart=/usr/bin/qbittorrent-nox -d --webui-port=8080
Restart=on-failure
[Install]
WantedBy=multi-user.target
最好在配置中设置 WebUI 监听在 0.0.0.0,防止 IPv6 公网访问
更新:笔者曾误以为 qbt 采用单 socket 监听全部 ip 地址,然而 qbt 其实会在每个 ip 地址上各开一个套接字,如果某个 IPv6 地址遭到运营商更改,实测下来会导致 qbt 直接摆烂
总之,笔者这里还是采用和 DDNS 一样的思想,监听单个地址,然后定时触发 qbt 的 API 来更新
update_bt_ip.py
#!/usr/bin/env python3
import requests
import json
import subprocess
import sys
DEBUG = len(sys.argv) > 1
CMD = r"ip -6 addr show | sed -En 's/\s+inet6 ([0-9a-fA-F:]+)\/128 scope global.+/\1/p'"
QUERY_URL = "http://127.0.0.1:8080/api/v2/app/preferences"
UPDATE_URL = "http://127.0.0.1:8080/api/v2/app/setPreferences"
def get_ipv6_address():
addr = subprocess.check_output(CMD, shell=True, text=True)
addr = addr.strip()
if len(addr) == 0:
raise Exception("Unable to get IPv6 address")
return addr
def query_listening_address():
response = requests.get(QUERY_URL, headers={"Accept": "application/json"})
if response.status_code != 200:
print(response.status_code)
print(response.text)
raise Exception("Unable to get preferences")
preferences = json.loads(response.text)
addr = preferences["current_interface_address"].strip()
return addr
def update_listening_address(addr):
json_param = '{"current_interface_address":"%s"}' % addr
response = requests.post(
UPDATE_URL,
data={"json": json_param},
headers={"Content-Type": "application/x-www-form-urlencoded"})
if response.status_code != 200:
print(response.status_code)
print(response.text)
raise Exception("Unable to update preferences")
def main():
addr = get_ipv6_address()
print(addr)
prev_addr = query_listening_address()
if prev_addr is not None and prev_addr == addr:
print("IPv6 address not updated")
return 1
else:
print(prev_addr)
if DEBUG is True:
print("Debug: should update interface address now")
else:
update_listening_address(addr)
print("Interface address updated successfully")
return 0
if __name__ == "__main__":
main()
一点吐槽:怎么会有人用 form 参数传 json 的?
SSH 代理本地管理页面 #
想远程访问如 miniDLNA 8200 和 qbt 8080 等管理页面可以借助 SSH 动态代理
ssh -D 1080 -N -o ServerAliveInterval=60 foo@example.com
走 socks5 1080 即可