0%

无论是家庭内希望完成 HomeLab 的数字基建, 还是实验室服务器需要组网提供外部连接能力,WireGuard 往往都是一个不错的方案。它的配置简单,兼容广泛,效果也不错,兼具兼具安全与自由。相比 TailScale 等魔改方案,直接使用 WireGuard 可以作为代理节点融入“通用网络代理方案”从而实现更加无感的互联网冲浪体验,更重要的是同时带来了公网暴露的便捷性与内网隐蔽安全性。

接下来分享我搭建实验室内网的方案。首先交代下实验室的网络情况,我们实验室的服务器散落在不同的网络环境中,但都处在校园网下,没有公网IP,没有IPv6。我们通过 WireGuard 将服务器连接在一个虚拟内网中,并提供校园网下的连接能力。为了能够实现进一步的公网远程连接,我们在购置的VPS 中部署 frp 来转发 WireGuard。这样做的好处是在校园网下可以使用 WireGuard 校园网的接口从而享受较高速率的连接,同时在公网环境下也可以通过 VPS 转发后的端口有效连接。

配置


尽管 WireGuard 支持多种网络结构,但对于常见的需要内网穿透的场景,建设 “服务端–客户端”这种方式较为简单。

首先建议在 Linux 服务器端安装 WireGuard,安装方式可以参考官网

服务端的配置

  1. 生成私钥公钥

    wg genkey | tee privatekey | wg pubkey > publickey
  2. 编辑网口配置文件
    接下来需要新建虚拟网口,就叫它 wg0 吧,为了给其准备配置文件输入以下命令:

sudo vim /etc/wireguard/wg0.conf
[Interface]
Address = 10.2.100.1/24 # 服务端虚拟 IP
PrivateKey = <privatekey> # 填入上面生成的私钥
ListenPort = 16384

然后使用 systemd 启动 wireguard。这种方式便于管理与配置 wiregurad 自启。

sudo systemctl start wg-quick@wg0
sudo systemctl enable wg-quick@wg0

VPS 配置 FRP 转发

首先在 VPS 中下载 frp 并配置

# frps.toml
bindPort = 7066 # 连接的端口,记得在安全组中放行
auth.token = <生成一个无序的密码>

log.to = "/var/log/frps.log"
log.level = "info"
log.maxDays = 3
# disable log colors when log.to is console, default is false
log.disablePrintColor = false

同样选择在 systemd 中管理 frps,这里可以参考 frp 安装文档

然后在需要在服务器中下载 frp 并配置

# frpc.toml
serverAddr = <服务器的IP>
serverPort = <上面指定的端口>
auth.token = <上面指定的token>

log.to = "/var/log/frpc.log"
# trace, debug, info, warn, error
log.level = "info"
log.maxDays = 3
log.disablePrintColor = false


[[proxies]]
name = "wg_rter"
type = "udp" # wireguard 基于 udp
localIP = "127.0.0.1"
localPort = 16384 # 这个是服务器在 wg0 指定的端口
remotePort = 16385 # 这个是希望 vps 转发出来的端口

启动 frpc 并连接,并在 VPS 安全组中放行上面设置的 remotePort

客户端的配置

在安装完客户端之后,如果是带有 GUI 的客户端,如 macOS,安卓等会为我们自动生成私钥密钥,我们只需要调整即可。否则依然可以使用上面的命令生成。

[Interface]
Address = 10.2.100.2/24 # 服务端虚拟 IP
PrivateKey = <privatekey>
DNS = 10.2.100.1 # 如果我们需要解析内网自定的域名,请在这里填入自建 DNS 的 IP

[Peer]
PublicKey = <publickey> # 这里填上服务器的 publickey
AllowIPs = 10.2.100.0/24
Endpoint = <server_addr>:<Port> # 上面 vps 的 IP 与 转发的端口

分流路由

到这里,我们应该实现了实验室内网访问 / 回家访问自建服务的功能。但对于大多数人来说,我们还需要同时满足向内与向外两个方向的需求。实际上,常见的通用代理软件已经提供了这方面的能力,帮助我们丝滑的满足需求。

mihomo

以下内容直接摘自 mihomo doc

WireGuard 标准配置文件

[Interface]
Address = <本机组网IP>
ListenPort = <本地监听端口>
PrivateKey = <本机私钥>
DNS = <使用的DNS>
MTU = <预设MTU>

[Peer]
AllowedIPs = <转发IP段>
Endpoint = <远端地址>:<远端端口>
PublicKey = <远端公钥>

转换为 mihomo 格式的配置

- name: "wg"
type: wireguard
ip: <本机组网IP,IPv4往这里填>
ipv6: <本机组网IP,IPv6往这里填> # 没有v6地址直接删除
private-key: <本机私钥>
peers:
- server: <远端地址>
port: <远端端口>
public-key: <远端公钥>
allowed-ips: ['0.0.0.0/0'] # 分流由clash处理
# reserved: [209,98,59] # 如果需要自己填
udp: true
mtu: <预设MTU> # 按需设置,不用直接删除
remote-dns-resolve: true # 按需设置,不用直接删除
dns: <使用的DNS> # 按需设置,不用直接删除

上面的配置需要在 mihomo config 的 proxies 部分,下面给出节点与规则配置示例:


... 上面略去

proxies:
- name: "wg"
type: wireguard
ip: <本机组网IP,IPv4往这里填>
ipv6: <本机组网IP,IPv6往这里填> # 没有v6地址直接删除
private-key: <本机私钥>
peers:
- server: <远端地址>
port: <远端端口>
public-key: <远端公钥>
allowed-ips: ['0.0.0.0/0'] # 分流由clash处理
# reserved: [209,98,59] # 如果需要自己填
udp: true
mtu: <预设MTU> # 按需设置,不用直接删除
remote-dns-resolve: true # 按需设置,不用直接删除
dns: <使用的DNS> # 按需设置,不用直接删除

... 中间略去

rules:
- DOMAIN-SUFFIX,lab.cc,wg # 假设 lab.cc 是内网自定的域名
- IP-CIDR,192.168.1.0/24,wg,no-resolve # 假设 192.168.1.0/24 是内网网段

... 下面略去

这样保存设置并生效后应该就可以将内网访问融入到整体的分流代理中。类似的道理,我们同样可以调整 Shadowrocket 等软件的配置,添加 WireGuard 的节点配置与分流规则。

总结

WireGuard 轻量且强大,并具有良好的“生态”,融入到整体的分流路由中,提供超出 TailScale 等魔改版更加自由无缝的冲浪体验。能为接下来搭建 self-hosted 服务提供坚实的基础,借助诸多代理软件的力量兼具安全与便携。

为自己的 Homeserver 提供 https 证书服务可能是一个比较繁琐的工作,但提供 https 服务能力可能是诸如文件同步,密码管理等的必须要求,尽管这项服务实际上是由自行搭建的内网服务。

如果使用自签证书,各个设备系统的导入程度/信任程度也各不相同。幸好,我们可以通过 Let’s encrpyt 提供的一系列 Chanllenge 证明我们拥有某些域名从而获得 SSL 证书。其中,DNS-01 挑战 特别适合对内网或者不能直接被公网访问的服务器

接下来我们将使用 Traefik 自带的 acme 方案搭配 Cloudflare 实现证书的自动生成并自动续期。

生成 Cloudflare token

这里将默认我们已经购买了一个域名并已经交由 cloudflare 托管。打开 Cloudflare 管理面板 点击左下角 Manage Account –> Account API tokens –> Create_token。
选择 Edit zone DNS 模版,为我们的域名赋予修改的权限,复制 Token 备用。

Traefik 配置

在 Traefik Compose 文件中增加如下选项

services:

traefik:
image: "traefik:v3.5"
container_name: "traefik"
command:
#- "--log.level=DEBUG"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
- "--entryPoints.websecure.address=:443"

# 开启 DNS 挑战
- "--certificatesresolvers.myresolver.acme.dnschallenge=true"
# 指定 provider
- "--certificatesresolvers.myresolver.acme.dnschallenge.provider=cloudflare"
#- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
# 这里提供邮箱
- "--certificatesresolvers.myresolver.acme.email=postmaster@example.com"
# 指定证书的存储地址,这里建议放入下面 volumes 持久化存储
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
# 延后验证时间以便 DNS 记录生效
- "--certificatesresolvers.runci.acme.dnschallenge.delaybeforecheck=60"
ports:
- "80:80"
- "443:443"
- "8080:8080"
environment:
# 这里记得放入上一步获取的 token
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
labels:
# label 这里我们选择显式触发去生成 wildcard 证书
- "traefik.tls.stores.default.defaultgeneratedcert.resolver=myresolver"
- "traefik.tls.stores.default.defaultgeneratedcert.domain.main=lab.cc"
- "traefik.tls.stores.default.defaultgeneratedcert.domain.sans=*.lab.cc"

启动 Traefik

docker compose up -d

不出意外的话,应该可以申请到证书了,可以检查 ./letsencrypt/acme.json 文件中的内容,如果没有可以将 Traefik 的 LOG 等级调整为 DEBUG 并关注日志。

总结

DNS Chanllenge 配合 Traefik 可以很方便地为 Homeserver 的服务提供 https 能力,解决自己搭建的 WebDAV / S3 服务因为协议的问题无法使用的情况,并且可以将 VaultWarden 等较为敏感的服务置于内网中,配合设备端的分流代理提供更加安全又便利的体验。

参考


问题


在使用 PEFT prefix tuning 模型的时候发现报错:

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

  File "/opt/conda/lib/python3.11/site-packages/transformers/models/qwen3/modeling_qwen3.py", line 284, in forward 

    hidden_states, self_attn_weights = self.self_attn( 

                                       ^^^^^^^^^^^^^^^ 

  File "/opt/conda/lib/python3.11/site-packages/torch/nn/modules/module.py", line 1736, in _wrapped_call_impl 

    return self._call_impl(*args, **kwargs) 

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

  File "/opt/conda/lib/python3.11/site-packages/torch/nn/modules/module.py", line 1747, in _call_impl 

    return forward_call(*args, **kwargs) 

           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

  File "/opt/conda/lib/python3.11/site-packages/transformers/models/qwen3/modeling_qwen3.py", line 223, in forward 

    key_states, value_states = past_key_value.update(key_states, value_states, self.layer_idx, cache_kwargs) 

                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

  File "/opt/conda/lib/python3.11/site-packages/transformers/cache_utils.py", line 545, in update 

    self.key_cache[layer_idx] = torch.cat([self.key_cache[layer_idx], key_states], dim=-2

                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

RuntimeError: Sizes of tensors must match except in dimension 2. Expected size 320 but got size 128 for tensor number 1 in the list.

相关设置如下:

prefix_config = PrefixTuningConfig(
task_type=TaskType.FEATURE_EXTRACTION,
num_virtual_tokens=64
inference_mode=False
)
peft_model = get_peft_model(base_hf_model, prefix_config)

排查过程

初步怀疑是因为现在的 LLM 使用 GQA 的技术从而与 PEFT 的默认实现产生冲突。Ptuning v2 通过在 attention 层引入额外的 key 和 value 以增强模型表示,从而实现微调效果。

现在我们尝试打印下模型的结构和关键参数。

Qwen3-0.6b 的结果如下所示:

model_config.num_key_value_heads: 8
model_config.head_dim: 128
model_config.num_attention_heads: 16
model_config.hidden_size:1024

(self_attn): Qwen3Attention(
(q_proj): Linear(in_features=1024, out_features=2048, bias=False) (k_proj): Linear(in_features=1024, out_features=1024, bias=False) (v_proj): Linear(in_features=1024, out_features=1024, bias=False) (o_proj): Linear(in_features=2048, out_features=1024, bias=False)
(q_norm): Qwen3RMSNorm ( (128, ), eps=1e-06)
(k_norm) : Qwen3RMSNorm ( (128, ), eps=1e-06)
)

我们可以看到 k,v 的 注意力头有 8 个, 而 q (attention_heads) 有 16 个,q, k, v 的注意力头数并不相同。

另外 k,v 的头数与头的维度相乘似乎刚好等于hidden_size (8*128=1024)

让我们来看更大一点的模型 Qwen3-4b 的结果:

model_config.num_key_value_heads: 8
model_config.head_dim: 128
model_config.num_attention_heads: 32
model_config.hidden_size: 2560

(self_attn): Qwen3Attention(
(q_proj): Linear (in_features=2560, out_features=4096, bias=False) (k_proj): Linear(in_features=2560, out_features=1024, bias=False) (v_proj): Linear(in_features=2560, out_features=1024, bias=False) (o_proj): Linear (in_features=4096, out_features=2560, bias=False)
(q_norm): Qwen3RMSNorm ( (128, ), eps=1e-06)
(k_norm) : Qwen3RMSNorm ( (128, ), eps=1e-06)
)

我们可以看到 k,v 的 注意力头有 8 个, 而 q (attention_heads) 有 16 个,q, k, v 的注意力头数并不相同。

另外 k,v 的头数与头的维度相乘似乎刚好等于hidden_size (8* 128=1024)

让我们来看更大一点的模型 Qwen3-4b 的结果:

解决方法


只需要将 num_attention_heads 锚定 nums_kv_heads, token_dim 锚定 model_config.num_key_value_heads * model_config.head_dim 即可解决。

model_config = model.config
prefix_config = PrefixTuningConfig(
task_type=TaskType.FEATURE_EXTRACTION,
num_virtual_tokens=getattr(self.model_config, 'num_virtual_tokens', 20),
inference_mode=False,
token_dim=model_config.num_key_value_heads * model_config.head_dim,
num_layers = model_config.num_hidden_layers,
num_attention_heads = getattr(model_config, "num_key_value_heads", model_config.num_attention_heads)
)

这篇日志主要记录为应用在 Postgresql 创建用户、对应数据库并赋予必要的权限。

1. 进入 PostgreSQL 命令行界面

使用 psql 工具连接到 PostgreSQL 数据库。

sudo -u postgres psql

# 如果是运行在 Docker 容器中
docker exec -it postgresql psql -U postgres

2. 创建数据库

使用以下命令创建一个新的数据库(假设数据库名为 mydb):

CREATE DATABASE mydb;

3. 创建用户并设置密码

创建一个新的用户(假设用户名为 myuser,密码为 mypassword):

CREATE USER myuser WITH PASSWORD 'mypassword';

4. 授权用户访问数据库

授予该用户对新创建的数据库的所有权限:

GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser;

然后,切换到 nextcloud_db 数据库并为 public schema 授予权限:

\c nextcloud_db
GRANT ALL PRIVILEGES ON SCHEMA public TO nextcloud_user;

此外,确保用户可以在 public schema 中创建表:

ALTER SCHEMA public OWNER TO nextcloud_user;

5. 取消其他用户的访问权限

为了取消其他用户对该数据库的访问权限,你可以撤销默认的 public 模式的访问权限:

REVOKE ALL ON DATABASE mydb FROM PUBLIC;

此外,如果需要,你还可以从现有的其他特定用户那里撤销访问权限:

REVOKE ALL PRIVILEGES ON DATABASE mydb FROM otheruser;

6. 退出 PostgreSQL

完成配置后,可以退出 PostgreSQL 命令行界面:

\q