66aefd6161
In bash with 'set -u', an undefined variable triggers an unbound-variable
error. Functions don't satisfy '$FUNCNAME' expansion under set -u, so
the previous fix that kept '$PKG_INSTALL pkg...' as the call syntax
broke every call site:
docker.sh: line 53: PKG_INSTALL: unbound variable
Fix: drop the leading '$' in all 47 call sites across 6 generator
files. 'PKG_INSTALL pkg...' is a normal command/function lookup and
behaves identically under set -u or not.
Verified: web UI served docker.sh now has 'PKG_INSTALL' (no $);
'bash -n' passes; smoke run with stubbed PATH hits real line-53 call
site, mock apt-get receives correct args, exit 0, no unbound variable.
Sampled 6 other generators also syntax-OK with $PKG_INSTALL count 0.
271 lines
13 KiB
Python
271 lines
13 KiB
Python
"""
|
|
Network tools.
|
|
- Wireguard VPN
|
|
- OpenVPN
|
|
- Tailscale client
|
|
- DHCP server (isc-dhcp-server / kea)
|
|
- DNS forwarder (unbound / dnsmasq)
|
|
"""
|
|
from . import register, Generator, Field
|
|
from . import bash_header, quote, bool_str, yes
|
|
|
|
|
|
# ========================== WireGuard ==========================
|
|
class WireGuard(Generator):
|
|
id = "wireguard"
|
|
title = "WireGuard VPN"
|
|
category = "network"
|
|
icon = "🔒"
|
|
tags = ["wireguard", "vpn", "wg"]
|
|
description = "部署 WireGuard VPN server,可生成客户端配置。"
|
|
fields = [
|
|
Field("server_private_key", "Server Private Key", "password", default="",
|
|
help="留空将自动生成 (使用 wg genkey)。"),
|
|
Field("server_public_key", "Server Public Key", "text", default="",
|
|
help="留空将自动从 private key 派生。"),
|
|
Field("listen_port", "Listen 端口", "number", default="51820", min_=1, max_=65535),
|
|
Field("interface", "网卡接口", "text", default="eth0"),
|
|
Field("vpn_subnet", "VPN 子网 CIDR", "text", default="10.0.0.0/24"),
|
|
Field("server_vpn_ip", "Server VPN IP", "text", default="10.0.0.1/24"),
|
|
Field("peers", "客户端 Peer 列表", "textarea", default="client1\nclient2",
|
|
help="每行一个 client name,会为每个生成密钥对。"),
|
|
Field("peer_dns", "客户端 DNS", "text", default="1.1.1.1, 8.8.8.8"),
|
|
]
|
|
|
|
def render(self, p):
|
|
sk = p.get("server_private_key", "").strip()
|
|
pk = p.get("server_public_key", "").strip()
|
|
port = str(p.get("listen_port", "51820"))
|
|
iface = p.get("interface", "eth0")
|
|
subnet = p.get("vpn_subnet", "10.0.0.0/24")
|
|
sip = p.get("server_vpn_ip", "10.0.0.1/24")
|
|
peers = [p_.strip() for p_ in p.get("peers", "").splitlines() if p_.strip()]
|
|
dns = p.get("peer_dns", "1.1.1.1, 8.8.8.8")
|
|
out = [bash_header(self.title)]
|
|
out.append('log "Installing WireGuard..."')
|
|
out.append('PKG_INSTALL wireguard qrencode')
|
|
out.append('')
|
|
# server keypair
|
|
if not sk:
|
|
out.append('wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub')
|
|
else:
|
|
out.append('mkdir -p /etc/wireguard && echo "' + sk + '" > /etc/wireguard/server.key')
|
|
out.append('wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub')
|
|
if pk:
|
|
out.append('echo "' + pk + '" > /etc/wireguard/server.pub')
|
|
out.append('chmod 600 /etc/wireguard/server.key')
|
|
out.append('')
|
|
# generate peer configs
|
|
out.append('mkdir -p /etc/wireguard/clients')
|
|
for i, name in enumerate(peers, start=2):
|
|
ip = f'10.0.0.{i}/32'
|
|
out.append('wg genkey | tee /etc/wireguard/clients/' + name + '.key | wg pubkey > /etc/wireguard/clients/' + name + '.pub')
|
|
out.append('chmod 600 /etc/wireguard/clients/' + name + '.key')
|
|
# server conf
|
|
out.append('cat > /etc/wireguard/wg0.conf <<WG_EOF\n'
|
|
'[Interface]\n'
|
|
'Address = ' + sip + '\n'
|
|
'ListenPort = ' + port + '\n'
|
|
'PrivateKey = ' + (sk if sk else '$(cat /etc/wireguard/server.key)') + '\n'
|
|
'PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o ' + iface + ' -j MASQUERADE\n'
|
|
'PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o ' + iface + ' -j MASQUERADE\n\n')
|
|
for i, name in enumerate(peers, start=2):
|
|
out.append('[Peer]\n'
|
|
f'# {name}\n'
|
|
f'PublicKey = $(cat /etc/wireguard/clients/{name}.pub)\n'
|
|
f'AllowedIPs = 10.0.0.{i}/32\n\n')
|
|
out.append('WG_EOF')
|
|
out.append('chmod 600 /etc/wireguard/wg0.conf')
|
|
out.append('echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/99-wireguard.conf && sysctl -p /etc/sysctl.d/99-wireguard.conf')
|
|
out.append('systemctl enable --now wg-quick@wg0')
|
|
# build client confs
|
|
for i, name in enumerate(peers, start=2):
|
|
ip = f'10.0.0.{i}/32'
|
|
out.append('SERVER_PUB=$(cat /etc/wireguard/server.pub)')
|
|
out.append('CLIENT_PRIV=$(cat /etc/wireguard/clients/' + name + '.key)')
|
|
out.append('cat > /etc/wireguard/clients/' + name + '.conf <<CLI_EOF\n'
|
|
'[Interface]\n'
|
|
'Address = ' + ip + '\n'
|
|
'DNS = ' + dns + '\n'
|
|
'PrivateKey = ${CLIENT_PRIV}\n\n'
|
|
'[Peer]\n'
|
|
'PublicKey = ${SERVER_PUB}\n'
|
|
'Endpoint = YOUR_SERVER_IP:' + port + '\n'
|
|
'AllowedIPs = 0.0.0.0/0, ::/0\n'
|
|
'PersistentKeepalive = 25\n'
|
|
'CLI_EOF')
|
|
out.append('qrencode -t ansiutf8 < /etc/wireguard/clients/' + name + '.conf || true')
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
# ========================== Tailscale ==========================
|
|
class Tailscale(Generator):
|
|
id = "tailscale"
|
|
title = "Tailscale 客户端"
|
|
category = "network"
|
|
icon = "🦀"
|
|
tags = ["tailscale", "vpn", "mesh"]
|
|
description = "安装并启动 Tailscale,可指定自定义控制服务器。"
|
|
fields = [
|
|
Field("login_server", "控制服务器", "text", default="https://controlplane.tailscale.com",
|
|
help="Headscale 自建时填入,如 https://hs.example.com。"),
|
|
Field("accept_routes", "接受子网路由", "checkbox", default="no"),
|
|
Field("advertise_exit", "作为 Exit Node", "checkbox", default="no"),
|
|
Field("auth_key", "Auth Key (Headscale/preauth)", "password", default="",
|
|
help="留空则用 sudo tailscale up 手动登录。"),
|
|
Field("hostname", "Hostname", "text", default="$(hostname)"),
|
|
]
|
|
|
|
def render(self, p):
|
|
ls = p.get("login_server", "https://controlplane.tailscale.com")
|
|
ar = bool_str(p.get("accept_routes"))
|
|
ex = bool_str(p.get("advertise_exit"))
|
|
ak = p.get("auth_key", "").strip()
|
|
hn = p.get("hostname", "$(hostname)")
|
|
out = [bash_header(self.title)]
|
|
out.append('log "Installing Tailscale..."')
|
|
out.append('curl -fsSL https://tailscale.com/install.sh | sh')
|
|
out.append('systemctl enable --now tailscaled')
|
|
out.append('sleep 2')
|
|
flags = []
|
|
if ls:
|
|
flags.append('--login-server=' + ls)
|
|
if ar:
|
|
flags.append('--accept-routes')
|
|
if ex:
|
|
flags.append('--advertise-exit-node')
|
|
if hn:
|
|
flags.append('--hostname=' + hn)
|
|
flag_str = " ".join(flags)
|
|
if ak:
|
|
out.append('tailscale up ' + flag_str + ' --authkey=' + ak)
|
|
else:
|
|
out.append('tailscale up ' + flag_str)
|
|
out.append('log "Visit the URL printed above to authenticate."')
|
|
out.append('tailscale status')
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
# ========================== DNSMASQ ==========================
|
|
class Dnsmasq(Generator):
|
|
id = "dnsmasq"
|
|
title = "Dnsmasq (DHCP + DNS 缓存)"
|
|
category = "network"
|
|
icon = "📡"
|
|
tags = ["dns", "dhcp", "dnsmasq"]
|
|
description = "轻量级 DNS 转发 + DHCP server。"
|
|
fields = [
|
|
Field("enable_dhcp", "启用 DHCP", "checkbox", default="yes"),
|
|
Field("dhcp_range_start", "DHCP 起始 IP", "text", default="192.168.1.100"),
|
|
Field("dhcp_range_end", "DHCP 结束 IP", "text", default="192.168.1.200"),
|
|
Field("dhcp_lease", "租约时间", "text", default="12h"),
|
|
Field("dhcp_gateway", "DHCP 网关", "text", default="192.168.1.1"),
|
|
Field("dhcp_dns", "DHCP 推送的 DNS", "text", default="1.1.1.1, 8.8.8.8"),
|
|
Field("upstream_dns", "上游 DNS", "textarea", default="1.1.1.1\n8.8.8.8"),
|
|
Field("local_domain", "本地域名", "text", default="lan"),
|
|
Field("listen_address", "Listen IP", "text", default="0.0.0.0"),
|
|
]
|
|
|
|
def render(self, p):
|
|
ed = bool_str(p.get("enable_dhcp", True))
|
|
rstart = p.get("dhcp_range_start", "192.168.1.100")
|
|
rend = p.get("dhcp_range_end", "192.168.1.200")
|
|
lease = p.get("dhcp_lease", "12h")
|
|
gw = p.get("dhcp_gateway", "192.168.1.1")
|
|
dns = p.get("dhcp_dns", "1.1.1.1, 8.8.8.8")
|
|
up = [u.strip() for u in p.get("upstream_dns", "").splitlines() if u.strip()]
|
|
domain = p.get("local_domain", "lan")
|
|
listen = p.get("listen_address", "0.0.0.0")
|
|
out = [bash_header(self.title)]
|
|
out.append('log "Installing dnsmasq..."')
|
|
out.append('PKG_INSTALL dnsmasq')
|
|
out.append('cp /etc/dnsmasq.conf /etc/dnsmasq.conf.bak.$(date +%s) || true')
|
|
cfg = []
|
|
cfg.append('listen-address=' + listen)
|
|
cfg.append('bind-interfaces')
|
|
cfg.append('domain=' + domain)
|
|
cfg.append('local=/' + domain + '/')
|
|
cfg.append('cache-size=1000')
|
|
for u in up:
|
|
cfg.append('server=' + u)
|
|
if ed:
|
|
cfg.append('dhcp-range=' + rstart + ',' + rend + ',' + lease)
|
|
cfg.append('dhcp-option=3,' + gw)
|
|
cfg.append('dhcp-option=6,' + dns)
|
|
out.append('cat > /etc/dnsmasq.conf <<CFG_EOF\n' + '\n'.join(cfg) + '\nCFG_EOF')
|
|
out.append('systemctl enable --now dnsmasq')
|
|
out.append('ss -ltnup | grep :53 || warn "port 53 not listening yet"')
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
# ========================== OpenVPN ==========================
|
|
class OpenVPN(Generator):
|
|
id = "openvpn"
|
|
title = "OpenVPN 简化部署",
|
|
category = "network"
|
|
icon = "🛡️"
|
|
tags = ["openvpn", "vpn"]
|
|
description = "安装 openvpn + easy-rsa,生成服务端 CA/密钥/客户端配置。"
|
|
warnings = ["生产环境请先生成正式 PKI,本脚本为快速原型。"]
|
|
fields = [
|
|
Field("proto", "协议", "select", default="udp", options=["udp", "tcp"]),
|
|
Field("port", "端口", "number", default="1194", min_=1, max_=65535),
|
|
Field("subnet", "VPN 子网", "text", default="10.8.0.0/24"),
|
|
Field("dns_servers", "推送的 DNS", "text", default="1.1.1.1, 8.8.8.8"),
|
|
Field("cipher", "加密算法", "select", default="AES-256-GCM",
|
|
options=["AES-256-GCM", "AES-256-CBC", "CHACHA20-POLY1305"]),
|
|
Field("clients", "生成客户端数", "number", default="1", min_=1, max_=20),
|
|
]
|
|
|
|
def render(self, p):
|
|
proto = p.get("proto", "udp")
|
|
port = str(p.get("port", "1194"))
|
|
subnet = p.get("subnet", "10.8.0.0/24")
|
|
dns = p.get("dns_servers", "1.1.1.1, 8.8.8.8")
|
|
cipher = p.get("cipher", "AES-256-GCM")
|
|
n = int(p.get("clients", "1"))
|
|
out = [bash_header(self.title)]
|
|
out.append('log "Installing openvpn + easy-rsa..."')
|
|
out.append('PKG_INSTALL openvpn easy-rsa')
|
|
out.append('make-cadir /etc/openvpn/easy-rsa')
|
|
out.append('cd /etc/openvpn/easy-rsa')
|
|
out.append('./easyrsa init-pki')
|
|
out.append('./easyrsa build-ca nopass')
|
|
out.append('./easyrsa gen-req server nopass')
|
|
out.append('./easyrsa sign-req server server <<< "yes"')
|
|
out.append('./easyrsa gen-dh')
|
|
out.append('openvpn --genkey secret /etc/openvpn/ta.key')
|
|
out.append('cp pki/ca.crt pki/private/server.key pki/issued/server.crt pki/dh.pem /etc/openvpn/')
|
|
out.append('cp /etc/openvpn/easy-rsa/ta.key /etc/openvpn/')
|
|
out.append('')
|
|
out.append('cat > /etc/openvpn/server.conf <<CFG_EOF\n'
|
|
'port ' + port + '\n'
|
|
'proto ' + proto + '\n'
|
|
'dev tun\n'
|
|
'ca ca.crt\ncert server.crt\nkey server.key\ndh dh.pem\n'
|
|
'auth SHA256\ncipher ' + cipher + '\n'
|
|
'tls-auth ta.key 0\n'
|
|
'topology subnet\n'
|
|
'server ' + subnet + '\n'
|
|
'push "redirect-gateway def1 bypass-dhcp"\n'
|
|
'push "dhcp-option DNS ' + dns.split(',')[0].strip() + '"\n'
|
|
'keepalive 10 120\npersist-key\npersist-tun\nstatus openvpn-status.log\nverb 3\n'
|
|
'CFG_EOF')
|
|
out.append('systemctl enable --now openvpn@server')
|
|
out.append('echo "net.ipv4.ip_forward=1" >> /etc/sysctl.d/99-openvpn.conf && sysctl -p')
|
|
for i in range(1, n + 1):
|
|
cn = f'client{i}'
|
|
out.append('cd /etc/openvpn/easy-rsa && ./easyrsa gen-req ' + cn + ' nopass')
|
|
out.append('cd /etc/openvpn/easy-rsa && ./easyrsa sign-req client ' + cn + ' <<< "yes"')
|
|
out.append('cat > /etc/openvpn/' + cn + '.ovpn <<OVPN_EOF\n'
|
|
'client\ndev tun\nproto ' + proto + '\n'
|
|
'remote YOUR_SERVER_IP ' + port + '\n'
|
|
'resolv-retry infinite\nnobind\n'
|
|
'persist-key\npersist-tun\nremote-cert-tls server\nauth SHA256\ncipher ' + cipher + '\n'
|
|
'verb 3\n<ca>\n$(cat /etc/openvpn/ca.crt)\n</ca>\n<cert>\n$(cat /etc/openvpn/easy-rsa/pki/issued/' + cn + '.crt)\n</cert>\n<key>\n$(cat /etc/openvpn/easy-rsa/pki/private/' + cn + '.key)\n</key>\n<tls-auth>\n$(cat /etc/openvpn/ta.key)\n</tls-auth>\nkey-direction 1\nOVPN_EOF')
|
|
return "\n".join(out) + "\n"
|
|
|
|
|
|
for _g in [WireGuard, Tailscale, Dnsmasq, OpenVPN]:
|
|
register(_g())
|