Merge pull request #51 from svc-design/vpn-overlay

Vpn overlay
This commit is contained in:
shenlan 2025-04-06 17:58:25 +08:00 committed by GitHub
commit aea056f04b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 397 additions and 68 deletions

View File

@ -2,4 +2,4 @@
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
sudo apt-get install -y vim inetutils-ping
sudo apt-get install -y vim iputils-ping rsync wireguard-tools

View File

@ -0,0 +1,7 @@
---
# Default VXLAN settings for site nodes
vxlan_dev_if: wg0
vxlan_vni: 100
vxlan_cidr_suffix: 16
overlay_config_path: config/sit/vpn-overlay.yaml

View File

@ -0,0 +1,83 @@
#!/bin/bash
# 安全增强版 VXLAN Overlay 脚本(支持 wg0 作为通道设备 + 可选公网 DNAT 映射)
# 用法: ./setup_sit_vxlan.sh <dev_if> <local_ip> <remote_ip> <br0_ip> [cidr_suffix] [vxlan_id] [mtu] [expose_port]
set -e
DEV_IF="$1"
LOCAL_IP="$2"
REMOTE_IP="$3"
BRIDGE_IP="$4"
CIDR_SUFFIX="${5:-16}"
VNI="${6:-100}"
MTU="${7:-1400}"
EXPOSE_PORT="${8:-443}"
if [[ -z "$DEV_IF" || -z "$LOCAL_IP" || -z "$REMOTE_IP" || -z "$BRIDGE_IP" ]]; then
echo "Usage: $0 <dev_if> <local_ip> <remote_ip> <br0_ip> [cidr_suffix] [vxlan_id] [mtu] [expose_port]"
exit 1
fi
VXLAN_IF="vxlan${VNI}"
BR_IF="br0"
BRIDGE_CIDR="${BRIDGE_IP}/${CIDR_SUFFIX}"
SUBNET="$(echo "$BRIDGE_IP" | cut -d. -f1-2).0.0/${CIDR_SUFFIX}"
# 自动判断 dev 是否能用于 VXLAN需支持广播
function is_vxlan_dev_usable() {
[[ -d "/sys/class/net/$1" ]] && grep -q "broadcast" "/sys/class/net/$1/flags"
}
echo "🔍 检查 $DEV_IF 是否可用于 VXLAN..."
USE_DEV_PARAM=true
if ! is_vxlan_dev_usable "$DEV_IF"; then
echo "⚠️ $DEV_IF 不支持广播,将省略 dev 参数(通过路由走隧道)"
USE_DEV_PARAM=false
fi
# 清理旧接口
for iface in "$VXLAN_IF" "$BR_IF"; do
ip link show "$iface" &>/dev/null && ip link set "$iface" down && ip link del "$iface"
done
# 创建 VXLAN 接口
echo "🛠️ 创建 VXLAN 接口 $VXLAN_IF ..."
if $USE_DEV_PARAM; then
ip link add "$VXLAN_IF" type vxlan id "$VNI" dstport 4789 local "$LOCAL_IP" remote "$REMOTE_IP" dev "$DEV_IF"
else
ip link add "$VXLAN_IF" type vxlan id "$VNI" dstport 4789 local "$LOCAL_IP" remote "$REMOTE_IP"
fi
ip link set "$VXLAN_IF" mtu "$MTU"
ip link set "$VXLAN_IF" up
# 创建 br0 桥
ip link add "$BR_IF" type bridge
ip link set "$BR_IF" mtu "$MTU"
ip link set "$VXLAN_IF" master "$BR_IF"
ip link set "$BR_IF" up
# 配置 IP
ip addr add "$BRIDGE_CIDR" dev "$BR_IF"
# 启用转发 + SNAT仅主机出网时
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -C POSTROUTING -s "$SUBNET" -o "$DEV_IF" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s "$SUBNET" -o "$DEV_IF" -j MASQUERADE
# ⚠️ 可选:仅在 EXPOSE_PORT 被定义时启用 DNAT 公网端口映射
if [[ -n "$EXPOSE_PORT" ]]; then
echo "🌐 添加 DNAT 映射规则:公网:$EXPOSE_PORT${BRIDGE_IP}:443"
iptables -t nat -C PREROUTING -p tcp -m tcp --dport "$EXPOSE_PORT" -j DNAT --to-destination "${BRIDGE_IP}:443" 2>/dev/null || \
iptables -t nat -A PREROUTING -p tcp -m tcp --dport "$EXPOSE_PORT" -j DNAT --to-destination "${BRIDGE_IP}:443"
fi
# 展示结果
echo ""
echo "✅ VXLAN Overlay 已建立"
echo " - bridge: $BR_IF ($BRIDGE_CIDR)"
echo " - vxlan: $VXLAN_IF ($LOCAL_IP$REMOTE_IP, id=$VNI, mtu=$MTU)"
echo " - SNAT: $SUBNET$DEV_IF"
if [[ -n "$EXPOSE_PORT" ]]; then
echo " - DNAT: 公网:$EXPOSE_PORT${BRIDGE_IP}:443"
fi

View File

@ -0,0 +1,54 @@
---
- name: Load site overlay config
set_fact:
overlay_data: "{{ lookup('file', overlay_config_path) | from_yaml }}"
- name: Select current site from config
set_fact:
current_site: "{{ overlay_data.sites | selectattr('name', 'equalto', inventory_hostname) | list | first }}"
- name: Fail if no matching site found
fail:
msg: "当前主机 {{ inventory_hostname }} 未在 config 中定义"
when: current_site is not defined
- name: Select first hub as default
set_fact:
selected_hub: "{{ overlay_data.hubs[0] }}"
- name: Set VXLAN parameters
set_fact:
vxlan_dev_if: "{{ current_site.interface}}"
vxlan_local_ip: "{{ current_site.local_ip }}"
vxlan_remote_ip: "{{ current_site.remote_ip }}"
vxlan_br_ip: "{{ current_site.br_ip }}"
vxlan_cidr_suffix: "{{ overlay_data.vxlan_cidr_suffix | default(16) }}"
vxlan_vni: "{{ overlay_data.vxlan_id | default(100) }}"
- name: 使用 rsync 分发 VXLAN 脚本
synchronize:
src: "files/setup_sit_vxlan.sh"
dest: /tmp/setup_sit_vxlan.sh
mode: push
- name: 移动脚本并赋权到 /usr/local/bin
shell: |
mv /tmp/setup_sit_vxlan.sh /usr/local/bin/setup_sit_vxlan.sh
chmod +x /usr/local/bin/setup_sit_vxlan.sh
become: true
- name: Render systemd unit for VXLAN
template:
src: vxlan-setup.service.j2
dest: /etc/systemd/system/vxlan-setup.service
mode: '0644'
- name: Reload systemd daemon
systemd:
daemon_reload: true
- name: Enable and start VXLAN overlay setup
systemd:
name: vxlan-setup
enabled: true
state: started

View File

@ -0,0 +1,12 @@
[Unit]
Description=VXLAN Overlay Setup for {{ inventory_hostname }}
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup_sit_vxlan.sh {{ vxlan_dev_if }} {{ vxlan_local_ip }} {{ vxlan_remote_ip }} {{ vxlan_br_ip }} {{ vxlan_cidr_suffix }} {{ vxlan_vni }}
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,8 @@
wg_interface: "wg0"
# 默认配置文件路径,可在 playbook 中覆盖
overlay_config_path: "{{ playbook_dir }}/../../config/sit/vpn-overlay.yaml"
overlay_keys_path: "{{ playbook_dir }}/../../config/sit/vpn-keys.yaml"
# WireGuard 默认端口(若 overlay_data.hub_port 未定义)
wg_port: "51820"

View File

@ -0,0 +1,58 @@
---
- name: 加载 overlay 配置(标准 YAML
set_fact:
overlay_data: "{{ lookup('file', overlay_config_path) | from_yaml }}"
- name: 加载密钥配置(支持 !vault
include_vars:
file: "{{ overlay_keys_path }}"
name: overlay_keys
- name: 提取当前节点信息(作为 current
set_fact:
current_node: >-
{{ (overlay_data.sites + overlay_data.hubs)
| selectattr('name', 'equalto', inventory_hostname)
| list | first }}
current_key: >-
{{ overlay_keys['keys']
| selectattr('name', 'equalto', inventory_hostname)
| list | first }}
- name: 提取对端 peer 节点列表
set_fact:
peer_node_list: "{{ (overlay_data.sites + overlay_data.hubs) | selectattr('name', 'equalto', current_node.wireguard_peer) | list }}"
peer_key_list: "{{ overlay_keys['keys'] | selectattr('name', 'equalto', current_node.wireguard_peer) | list }}"
- name: 校验 wireguard_peer 是否匹配成功
fail:
msg: "未找到对端节点 '{{ current_node.wireguard_peer }}' 或其密钥"
when: peer_node_list | length == 0 or peer_key_list | length == 0
- name: 设置对端节点与密钥
set_fact:
peer_nodes: "{{ peer_node_list }}"
peer_keys: "{{ peer_key_list }}"
- name: 提取最终配置变量(私钥、公钥、端口等)
set_fact:
wg_port: "{{ wg_port }}"
current_wg_ip: "{{ current_node.wg_ip }}"
current_private_key: "{{ current_key.private_key }}"
current_allowed_ips: "{{ current_node.allowed_ips }}"
peer_public_key: "{{ peer_keys[0].public_key }}"
peer_endpoint: "{{ peer_nodes[0].public_ip }}:{{ overlay_data.hub_port | default(wg_port) }}"
- name: 渲染 wg0.conf
template:
src: wg0.conf.j2
dest: /etc/wireguard/wg0.conf
mode: '0600'
become: true
- name: 启用并启动 wg-quick@wg0
systemd:
name: wg-quick@wg0
enabled: true
state: started
become: true

View File

@ -0,0 +1,12 @@
[Interface]
PrivateKey = {{ current_private_key }}
Address = {{ current_wg_ip }}/32
ListenPort = {{ wg_port }}
DNS = 8.8.8.8
MTU = 1400
[Peer]
PublicKey = {{ peer_public_key }}
AllowedIPs = {{ current_allowed_ips }}
Endpoint = {{ peer_endpoint }}
PersistentKeepalive = 25

View File

@ -0,0 +1,8 @@
- name: Run infrastructure setup
hosts: all
become: yes
gather_facts: yes
vars:
overlay_config_path: "{{ playbook_dir }}/../../config/sit/vpn-overlay.yaml"
roles:
- vhosts/vpn-overlay/vxlan/site

View File

@ -0,0 +1,8 @@
- name: Setup WireGuard for site
hosts: all
become: true
vars:
overlay_config_path: "{{ playbook_dir }}/../../config/sit/vpn-overlay.yaml"
overlay_keys_path: "{{ playbook_dir }}/../../config/sit/vpn-keys.yaml"
roles:
- vhosts/vpn-overlay/wireguard/site

17
config/sit/vpn-keys.md Normal file
View File

@ -0,0 +1,17 @@
只加密 private_key 字段
1. 原始 vpn-keys.yaml
yaml
keys:
- name: master-1
private_key: <master_private_key>
public_key: <master_public_key>
2. 使用 ansible-vault encrypt_string 加密 private_key
bash
ansible-vault encrypt_string 'my-secret-private-key' --name 'private_key'
示例输出(加密后是 YAML 结构):
yaml
private_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
62326432376162336462343864333933356363373235623262306463326432363737623732613763
3962613662616565393463343030653733623066626137610a313465323462623261303031323337

48
config/sit/vpn-keys.yaml Normal file
View File

@ -0,0 +1,48 @@
keys:
- name: master-1
private_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
35363663306235633966383634393535366462633039383137316163653166643832376264653965
3163363463663535363734343135643061653834353134610a393465636431346635643231613632
66393139656238333637353132663937633632396230313438656135353066383234353139343666
6230626332326437310a626639363063613062616239623734333131633865393063343030353133
32323465393434343634646238636531646230356365343264656364333132316636353338353133
6261663131646237316635306438613938396639383439653665
public_key: <master_public_key>
- name: slave-1
private_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
64393432666564616632613366663264326338643931653966653961643362306562643537663636
6434343630366165623261623363376333306339333164360a353037383261616335623561386538
64633762323939656633313433306330333533373434323664393965373038623431653539633561
3461626530613863640a353931346639653961303136303763303432653532636438636532613936
64313264643465303663646135383537623930626137656264323433373536326530613131363134
6334613964626261323838616534623639616133303564653063
public_key: <slave_public_key>
- name: site_c
private_key: <site_c_private_key>
public_key: <site_c_public_key>
- name: hub-1
private_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
33643635306332303761356562383035353333373234393132313162613834323963313635326562
3932356235303234356561623762393862666438386565310a376235306238343139386532336162
65623164666665353435653432396530303634666438656566656466643866366139613961363631
6363306631393038320a613163313338313237383837303966356333303737643331616433396430
33316331333766613438356462313130326433363961316162313761616561616466363939613033
3837623938376434656434386135333739613939653133373733
public_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
38336537383061383333643431643261343739323864316235303366623930633366336139386636
6162336232336533636134353863386233303631626363360a376533336664636661373933623230
34333765346661383335663034393561646436333135613838373438396336633061396533613061
3031326364353036630a373862396266653961346334663139626633313362656131663163383563
34376231306239636536313830333962323934343035333263643234363363396164626366353061
3833613132373666303563623863373735396566666239316536
- name: hub2
private_key: <hub2_private_key>
public_key: <hub2_public_key>

View File

@ -0,0 +1,66 @@
# 基础网络参数
wg_network: 172.16.0.0/24
bridge_network: 10.253.0.0/16
vxlan_id: 100
hub_port: 51820
# 全局功能开关
features:
enable_vxlan_between_sits: true # 是否开启 vxlan 桥接(站点接入 Hub
enable_vxlan_between_hubs: true # 是否开启 Hub 之间的 VXLAN Mesh
enable_vless: true # 是否通过 VLESS 中转 WG 流量
enable_multi_hub: true # 是否支持多 Hub 架构false 则为单 Hub star 架构)
only_wireguard: false # 若为 true仅使用 WireGuard 点对点,忽略 gretap/vxlan
# WireGuard Hub 节点配置
hubs:
- name: hub-1
wg_ip: 10.255.0.1
br_ip: 172.16.0.1
public_ip: global-proxy.onwalk.net
- name: hub2
wg_ip: 10.100.1.1
br_ip: 172.16.1.1
public_ip: hub2.example.com
# 各个站点定义
sites:
- name: master-1
interface: ens5
allowed_ips: "10.255.0.0/16,10.253.0.0/16"
wireguard_peer: hub-1
wg_ip: 10.255.0.200
br_ip: 172.16.0.2
local_ip: 10.255.0.200
remote_ip: 10.255.0.100
vless:
enabled: false
uuid: "11111111-1111-1111-1111-111111111111"
transport: ws
tls: true
path: /wg
- name: slave-1
interface: ens5
wireguard_peer: hub-1
allowed_ips: "10.255.0.0/16,10.253.0.0/16"
br_ip: 172.16.0.2
wg_ip: 10.255.0.100
local_ip: 10.255.0.100
remote_ip: 10.255.0.200
vless:
enabled: false # 此节点不通过 VLESS直接访问 Hub
- name: site_c
wg_ip: 10.100.1.2
br_ip: 172.16.1.2
hub: hub2
public_ip: c.example.com
vless:
enabled: true
endpoint: vless-c.example.com
uuid: "33333333-3333-3333-3333-333333333333"
transport: ws
tls: true
path: /xray

View File

@ -1,61 +0,0 @@
# 基础网络参数
wg_network: 10.100.0.0/24
bridge_network: 172.16.0.0/16
vxlan_id: 100
hub_port: 51820
# 全局功能开关
features:
enable_gretap: true # 是否开启 gretap 桥接(站点接入 Hub
enable_vxlan_between_hubs: true # 是否开启 Hub 之间的 VXLAN Mesh
enable_vless: true # 是否通过 VLESS 中转 WG 流量
enable_multi_hub: true # 是否支持多 Hub 架构false 则为单 Hub star 架构)
only_wireguard: false # 若为 true仅使用 WireGuard 点对点,忽略 gretap/vxlan
# WireGuard Hub 节点配置
hubs:
- name: hub1
wg_ip: 10.100.0.1
br_ip: 172.16.0.1
public_ip: hub1.example.com
- name: hub2
wg_ip: 10.100.1.1
br_ip: 172.16.1.1
public_ip: hub2.example.com
# 各个站点定义
sites:
- name: site_a
wg_ip: 10.100.0.2
br_ip: 172.16.0.2
hub: hub1
public_ip: a.example.com
vless:
enabled: true
endpoint: vless-a.example.com
uuid: "11111111-1111-1111-1111-111111111111"
transport: ws
tls: true
path: /wg
- name: site_b
wg_ip: 10.100.0.3
br_ip: 172.16.0.3
hub: hub1
public_ip: b.example.com
vless:
enabled: false # 此节点不通过 VLESS直接访问 Hub
- name: site_c
wg_ip: 10.100.1.2
br_ip: 172.16.1.2
hub: hub2
public_ip: c.example.com
vless:
enabled: true
endpoint: vless-c.example.com
uuid: "33333333-3333-3333-3333-333333333333"
transport: ws
tls: true
path: /xray

View File

@ -14,22 +14,29 @@ def deep_merge(dict1, dict2):
result[k] = v
return result
def load_merged_config(config_dir=None):
def load_merged_config(config_dir=None, ignore_files=None):
config_dir = config_dir or os.environ.get("CONFIG_PATH", "config")
ignore_files = set(ignore_files or ['vpn-keys.yaml']) # 默认忽略敏感 key 文件
if not os.path.isdir(config_dir):
raise FileNotFoundError(f"❌ 配置目录不存在: {config_dir}")
merged = {}
files = sorted(glob.glob(os.path.join(config_dir, "*.yaml")) + glob.glob(os.path.join(config_dir, "*.yml")))
files = sorted(
glob.glob(os.path.join(config_dir, "*.yaml")) + glob.glob(os.path.join(config_dir, "*.yml"))
)
if not files:
raise FileNotFoundError(f"⚠️ 未找到任何 YAML 配置文件于: {config_dir}")
for file in files:
filename = os.path.basename(file)
if filename in ignore_files:
continue
with open(file) as f:
part = yaml.safe_load(f) or {}
merged = deep_merge(merged, part)
merged["__config_path__"] = config_dir # 可选调试字段
merged["__config_path__"] = config_dir
return merged

View File

@ -262,12 +262,14 @@ pulumi_run() {
# ========== 执行 Ansible ==========
run_ansible() {
if [ ! -f scripts/inventory.py ]; then
echo "❌ 未找到 scripts/inventory.py"
if [ ! -f scripts/dynamic_inventory.py ]; then
echo "❌ 未找到 scripts/dynamic_inventory.py"
exit 1
fi
echo "🧪 执行 Ansible Playbook"
ansible-playbook -i scripts/inventory.py "$ANSIBLE_DIR/playbooks/setup.yml"
ansible-playbook -i scripts/dynamic_inventory.py ansible/playbooks/common_setup.yml -D
ansible-playbook -i scripts/dynamic_inventory.py ansible/playbooks/vpn-wireguard-site.yaml -D -l slave-1,master-1
ansible-playbook -i scripts/dynamic_inventory.py ansible/playbooks/vpn-overlay.yaml -D -l slave-1,master-1
}
# ========== 分发 ==========