wg-ops/generate.py

938 lines
40 KiB
Python
Raw Normal View History

import os
2022-02-15 21:16:35 +08:00
import copy
import sys
import time
2022-01-27 11:12:58 +08:00
import getopt
2022-02-06 05:31:18 +08:00
import uuid
import json
import base64
2022-02-15 15:53:04 +08:00
import traceback
import subprocess
from hashlib import sha256
import requests
from cryptography import x509
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
2022-01-27 11:12:58 +08:00
2022-02-06 16:29:19 +08:00
wgop_basepath = os.path.dirname(os.path.realpath(sys.argv[0]))
path_get_gateway = os.path.join(wgop_basepath, 'tools/get-gateway.py')
path_get_ip = os.path.join(wgop_basepath, 'tools/get-ip.py')
path_get_lan_ip = os.path.join(wgop_basepath, 'tools/get-lan-ip.py')
path_bin_dir = os.path.join(wgop_basepath, 'bin')
path_app_dir = os.path.join(wgop_basepath, 'app')
2022-02-02 02:52:53 +08:00
2022-02-15 16:47:25 +08:00
def errprint(msg):
sys.stderr.write("{}\n".format(msg))
2022-02-15 15:53:04 +08:00
def get_subject_name_from_cert(cert_path):
try:
with open(cert_path, 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read())
return cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value
except Exception:
2022-02-15 16:47:25 +08:00
errprint(traceback.format_exc())
2022-02-15 15:53:04 +08:00
return ""
def generate_rsa_keypair():
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
return private_key, public_key
def get_pem_from_rsa_keypair(private_key, public_key):
if private_key:
pripem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
2022-02-15 19:06:47 +08:00
encryption_algorithm=serialization.NoEncryption()).decode()
2022-02-15 15:53:04 +08:00
else:
pripem = None
if public_key:
pubpem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
2022-02-15 19:06:47 +08:00
).decode()
2022-02-15 15:53:04 +08:00
else:
pubpem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
2022-02-15 19:06:47 +08:00
).decode()
2022-02-15 15:53:04 +08:00
return pripem, pubpem
2022-02-15 21:16:35 +08:00
def get_rsa_keypair_from_private_pem(private_pem):
2022-02-15 19:06:47 +08:00
private_key = serialization.load_pem_private_key(private_pem.encode(), password=None)
2022-02-15 15:53:04 +08:00
public_key = private_key.public_key()
return private_key, public_key
2022-02-15 21:16:35 +08:00
def get_rsa_pubkey_from_public_pem(public_pem):
public_key = serialization.load_pem_public_key(public_pem.encode())
return public_key
2022-02-15 15:53:04 +08:00
def rsa_sign_base64(private_key, bytes_data):
signature = private_key.sign(bytes_data, padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
), hashes.SHA256())
2022-02-15 16:47:25 +08:00
2022-02-15 15:53:04 +08:00
return base64.b64encode(signature).decode()
def rsa_encrypt_base64(public_key, bytes_data):
return base64.b64encode(public_key.encrypt(bytes_data, padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
2022-02-15 21:16:35 +08:00
))).decode()
2022-02-15 15:53:04 +08:00
2022-02-15 21:16:35 +08:00
def rsa_decrypt_base64(private_key, str_data):
return private_key.decrypt(base64.b64decode(str_data), padding.OAEP(
2022-02-15 15:53:04 +08:00
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
))
2022-02-02 02:52:53 +08:00
class Parser:
def __init__(self):
# input parts
self.input_interface = []
self.input_peer = []
# output
self.result_interface = []
self.result_postup = []
self.result_postdown = []
self.result_peers = []
2022-02-06 05:31:18 +08:00
# container related output
self.result_container_prebootstrap = []
self.result_container_postbootstrap = []
2022-02-02 02:52:53 +08:00
# flags
self.flag_is_route_forward = False
self.flag_is_route_lookup = False
2022-02-06 09:18:43 +08:00
self.flag_container_must_host = False
2022-02-15 15:56:57 +08:00
self.flag_require_registry = False
self.flag_allow_modify = False
# opts
self.opt_source_path = ''
2022-02-02 02:52:53 +08:00
# vars
self.wg_name = '%i'
self.wg_port = 0
self.wg_mtu = 0
2022-02-15 15:56:57 +08:00
self.wg_pubkey = ''
self.wg_hash = ''
self.registry_domain = ''
self.registry_client_name = ''
self.local_private_key = None
self.local_public_key = None
self.local_autogen_nextport = 29100
self.pending_accepts = []
self.tunnel_local_endpoint = {}
self.tunnel_server_reports = {}
self.lookup_table = ''
2022-02-06 05:31:18 +08:00
self.container_expose_port = []
self.container_bootstrap = []
self.podman_user = ''
2022-02-15 15:56:57 +08:00
2022-02-06 05:31:18 +08:00
def get_container_network_name(self):
2022-02-06 09:18:43 +08:00
if self.flag_container_must_host:
return "host"
else:
return "wgop-net-{}".format(self.wg_name)
2022-02-06 05:31:18 +08:00
def get_container_name(self):
return "wgop-runner-{}".format(self.wg_name)
def get_podman_cmd_with(self, command):
if self.podman_user:
return "su - {} -c '{}'".format(self.podman_user, command)
else:
return command
2022-02-15 16:47:25 +08:00
2022-02-15 15:56:57 +08:00
def registry_resolve(self, client_name):
if not self.registry_domain:
2022-02-15 16:47:25 +08:00
errprint('[ERROR] Cannot query from registry, domain not specified')
2022-02-15 15:56:57 +08:00
exit(1)
if not self.registry_client_name:
2022-02-15 16:47:25 +08:00
errprint('[ERROR] No registry client name found.')
2022-02-15 15:56:57 +08:00
exit(1)
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY] Resolving client {} from registry ({})...'.format(client_name, self.registry_domain))
2022-02-15 15:56:57 +08:00
try:
res = requests.get('{}/query'.format(self.registry_domain), params={
"name": client_name,
})
2022-02-15 21:16:35 +08:00
res = res.json()
errprint('[REGISTRY-SERVER] {}'.format(res['message']))
if res['code'] < 0:
return {}
2022-02-15 15:56:57 +08:00
2022-02-15 21:16:35 +08:00
remote_result = res['data']
2022-02-15 15:56:57 +08:00
remote_peers = remote_result['peers']
if self.registry_client_name not in remote_peers:
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY-REMOTE] This client ({}) is not accepted by {}'.format(self.registry_client_name, client_name))
return {}
remote_config = rsa_decrypt_base64(self.local_private_key, remote_peers[self.registry_client_name])
remote_config = json.loads(remote_config)
remote_peers[self.registry_client_name] = remote_config
return remote_result
except Exception:
errprint(traceback.format_exc())
errprint('[REGISTRY] Exception happened during registry client resolve')
return {}
def registry_query(self, client_name):
if not self.registry_domain:
errprint('[ERROR] Cannot query from registry, domain not specified')
exit(1)
if not self.registry_client_name:
errprint('[ERROR] No registry client name found.')
exit(1)
errprint('[REGISTRY] Querying client {}...'.format(client_name))
try:
res = requests.get('{}/query'.format(self.registry_domain), params={
"name": client_name,
})
res = res.json()
errprint('[REGISTRY-SERVER] {}'.format(res['message']))
if res['code'] < 0:
2022-02-15 15:56:57 +08:00
return {}
2022-02-15 21:16:35 +08:00
remote_result = res['data']
return remote_result
2022-02-15 15:56:57 +08:00
except Exception:
2022-02-15 16:47:25 +08:00
errprint(traceback.format_exc())
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY] Exception happened during registry client query')
2022-02-15 15:56:57 +08:00
return {}
def registry_upload(self, content):
if not self.registry_domain:
2022-02-15 16:47:25 +08:00
errprint('[ERROR] Cannot query from registry, domain not specified')
2022-02-15 15:56:57 +08:00
exit(1)
if not self.registry_client_name:
2022-02-15 16:47:25 +08:00
errprint('[ERROR] No registry client name found.')
2022-02-15 15:56:57 +08:00
exit(1)
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY] Registering this client ({}) with registry ({})...'.format(self.registry_client_name, self.registry_domain))
2022-02-15 15:56:57 +08:00
try:
res = requests.post('{}/register'.format(self.registry_domain), json=content)
res = res.json()
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY-SERVER] {}'.format(res['message']))
2022-02-15 15:56:57 +08:00
if res['code'] < 0:
return False
else:
return True
except Exception:
2022-02-15 16:47:25 +08:00
errprint(traceback.format_exc())
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY] Exception happened during registry register')
2022-02-15 15:56:57 +08:00
return False
2022-02-15 16:47:25 +08:00
2022-02-15 21:16:35 +08:00
def registry_ensure(self, peers=None):
2022-02-15 15:56:57 +08:00
private_pem, public_pem = get_pem_from_rsa_keypair(None, self.local_public_key)
2022-02-15 16:47:25 +08:00
can_ensure = self.registry_upload({
2022-02-15 15:56:57 +08:00
"name": self.registry_client_name,
"pubkey": public_pem,
"wgkey": self.wg_pubkey,
2022-02-15 21:16:35 +08:00
"peers": peers or {},
2022-02-15 15:56:57 +08:00
"sig": rsa_sign_base64(self.local_private_key, self.wg_pubkey.encode())
})
2022-02-15 16:47:25 +08:00
if not can_ensure:
2022-02-15 19:06:47 +08:00
errprint('[ERROR] registry ensure failed, please check your network.')
2022-02-15 16:47:25 +08:00
exit(1)
2022-02-06 05:31:18 +08:00
def add_expose(self, expose_port, mode='udp'):
self.container_expose_port.append({
2022-02-06 17:01:25 +08:00
"port": int(expose_port),
2022-02-06 05:31:18 +08:00
"mode": mode,
})
2022-02-15 21:16:35 +08:00
def append_input_peer_clientside(self, peer_wgkey, allowed_ip, tunnel_name):
this_peer = []
this_peer.append("PublicKey = {}".format(peer_wgkey))
this_peer.append("AllowedIPs = {}".format(allowed_ip))
this_peer.append("PersistentKeepalive = 5")
this_peer.append("#use-tunnel {}".format(tunnel_name))
self.input_peer.append(this_peer)
def append_input_peer_serverside(self, peer_wgkey, allowed_ip):
this_peer = []
this_peer.append("PublicKey = {}".format(peer_wgkey))
this_peer.append("AllowedIPs = {}".format(allowed_ip))
self.input_peer.append(this_peer)
2022-02-06 05:31:18 +08:00
def add_muxer(self, listen_port, forward_start, forward_size):
self.container_bootstrap.append({
"type": "mux",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
"forward": int(forward_start),
"size": int(forward_size),
2022-02-06 05:31:18 +08:00
})
2022-02-15 15:55:25 +08:00
def add_gost_server(self, tunnel_name, listen_port):
2022-02-06 05:31:18 +08:00
self.container_bootstrap.append({
"type": "gost-server",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
2022-02-06 05:31:18 +08:00
})
2022-02-15 15:55:25 +08:00
self.tunnel_server_reports[tunnel_name] = {
"type": "gost",
"listen": int(listen_port),
}
2022-02-15 16:47:25 +08:00
2022-02-15 21:16:35 +08:00
def add_gost_client_with(self, remote_config, remote_peer_config):
2022-02-15 15:55:25 +08:00
self.local_autogen_nextport += 1
tunnel_name = "gen{}{}".format(self.wg_hash[:8], self.local_autogen_nextport)
2022-02-15 21:16:35 +08:00
self.add_gost_client(tunnel_name, self.local_autogen_nextport, "{}:{}".format(remote_config['ip'], remote_peer_config['listen']))
self.append_input_peer_clientside(remote_config["wgkey"], remote_peer_config["allowed"], tunnel_name)
2022-02-15 15:55:25 +08:00
def add_gost_client_mux(self, tunnel_name, mux_size, listen_port, tunnel_remote):
if self.podman_user:
self.add_expose(listen_port)
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(listen_port)
else:
self.tunnel_local_endpoint[tunnel_name] = "gateway:{}".format(listen_port)
self.add_muxer(listen_port, listen_port+1, mux_size)
for mux_idx in range(mux_size):
self._do_add_gost_client(listen_port + 1 + mux_idx, tunnel_remote)
2022-02-06 05:31:18 +08:00
2022-02-15 15:55:25 +08:00
def add_gost_client(self, tunnel_name, listen_port, tunnel_remote):
if self.podman_user:
self.add_expose(listen_port)
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(listen_port)
else:
self.tunnel_local_endpoint[tunnel_name] = "gateway:{}".format(listen_port)
self._do_add_gost_client(listen_port, tunnel_remote)
def _do_add_gost_client(self, listen_port, tunnel_remote):
2022-02-06 05:31:18 +08:00
self.container_bootstrap.append({
"type": "gost-client",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
2022-02-06 05:31:18 +08:00
"remote": tunnel_remote,
})
2022-02-15 15:55:25 +08:00
def add_udp2raw_server(self, tunnel_name, listen_port, tunnel_password):
2022-02-06 05:31:18 +08:00
conf_uuid = str(uuid.uuid4())
self.container_bootstrap.append({
"type": "udp2raw-server",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
2022-02-06 05:31:18 +08:00
"password": tunnel_password,
"id": conf_uuid,
})
2022-02-15 15:55:25 +08:00
self.tunnel_server_reports[tunnel_name] = {
"type": "udp2raw",
"listen": int(listen_port),
"password": tunnel_password,
}
2022-02-06 05:31:18 +08:00
ipt_filename_inside = "/root/conf/{}-ipt.conf".format(conf_uuid)
self.result_container_postbootstrap.append('PostUp=IPT_COMMANDS=$({}); echo $IPT_COMMANDS; $IPT_COMMANDS'.format(
2022-02-06 07:03:55 +08:00
self.get_podman_cmd_with("podman exec {} /root/bin/udp2raw_amd64 --conf-file {} | grep ^iptables".format(self.get_container_name(), ipt_filename_inside))
2022-02-06 05:31:18 +08:00
))
self.result_postdown.append("PostDown=IPT_COMMANDS=$({}); IPT_COMMANDS=$(echo $IPT_COMMANDS | sed -e 's/-I /-D /g'); echo $IPT_COMMANDS; $IPT_COMMANDS".format(
2022-02-06 07:03:55 +08:00
self.get_podman_cmd_with("podman exec {} /root/bin/udp2raw_amd64 --conf-file {} | grep ^iptables".format(self.get_container_name(), ipt_filename_inside))
2022-02-06 05:31:18 +08:00
))
2022-02-15 15:55:25 +08:00
2022-02-15 21:16:35 +08:00
def add_udp2raw_client_with(self, remote_config, remote_peer_config):
2022-02-15 15:55:25 +08:00
self.local_autogen_nextport += 1
tunnel_name = "gen{}{}".format(self.wg_hash[:8], self.local_autogen_nextport)
2022-02-15 21:16:35 +08:00
self.add_udp2raw_client(tunnel_name, self.local_autogen_nextport, remote_peer_config["password"], "{}:{}".format(remote_config['ip'], remote_peer_config['listen']))
self.append_input_peer_clientside(remote_config["wgkey"], remote_peer_config["allowed"], tunnel_name)
2022-02-15 15:55:25 +08:00
def add_udp2raw_client_mux(self, tunnel_name, mux_size, listen_port, tunnel_password, remote_addr):
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(listen_port)
self.flag_container_must_host = True
self.add_muxer(listen_port, listen_port+1, mux_size)
for mux_idx in range(mux_size):
self._do_add_udp2raw_client(listen_port + 1 + mux_idx, tunnel_password, remote_addr)
def add_udp2raw_client(self, tunnel_name, listen_port, tunnel_password, remote_addr):
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(listen_port)
self.flag_container_must_host = True
self._do_add_udp2raw_client(listen_port, tunnel_password, remote_addr)
def _do_add_udp2raw_client(self, listen_port, tunnel_password, remote_addr):
2022-02-06 05:31:18 +08:00
conf_uuid = str(uuid.uuid4())
self.container_bootstrap.append({
"type": "udp2raw-client",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
2022-02-06 05:31:18 +08:00
"password": tunnel_password,
"remote": remote_addr,
"id": conf_uuid,
})
ipt_filename_inside = "/root/conf/{}-ipt.conf".format(conf_uuid)
self.result_container_postbootstrap.append('PostUp=IPT_COMMANDS=$({}); echo $IPT_COMMANDS; $IPT_COMMANDS'.format(
2022-02-06 07:03:55 +08:00
self.get_podman_cmd_with("podman exec {} /root/bin/udp2raw_amd64 --conf-file {} | grep ^iptables".format(self.get_container_name(), ipt_filename_inside))
2022-02-06 05:31:18 +08:00
))
self.result_postdown.append("PostDown=IPT_COMMANDS=$({}); IPT_COMMANDS=$(echo $IPT_COMMANDS | sed -e 's/-I /-D /g'); echo $IPT_COMMANDS; $IPT_COMMANDS".format(
2022-02-06 07:03:55 +08:00
self.get_podman_cmd_with("podman exec {} /root/bin/udp2raw_amd64 --conf-file {} | grep ^iptables".format(self.get_container_name(), ipt_filename_inside))
2022-02-06 05:31:18 +08:00
))
2022-02-15 15:55:25 +08:00
def add_trojan_server(self, tunnel_name, listen_port, tunnel_password, ssl_cert_path, ssl_key_path):
2022-02-06 05:31:18 +08:00
cert_uuid = str(uuid.uuid4())
cert_filepath = "/root/ssl/{}.cert".format(cert_uuid)
key_filepath = "/root/ssl/{}.key".format(cert_uuid)
self.result_container_prebootstrap.append('PostUp={}'.format(
self.get_podman_cmd_with('podman cp {} {}:{}'.format(ssl_cert_path, self.get_container_name(), cert_filepath))
))
self.result_container_prebootstrap.append('PostUp={}'.format(
self.get_podman_cmd_with('podman cp {} {}:{}'.format(ssl_key_path, self.get_container_name(), key_filepath))
))
self.container_bootstrap.append({
"type": "trojan-server",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
2022-02-06 05:31:18 +08:00
"password": tunnel_password,
"cert": cert_uuid,
})
2022-02-15 15:55:25 +08:00
self.tunnel_server_reports[tunnel_name] = {
"type": "trojan",
"listen": int(listen_port),
"password": tunnel_password,
"target": int(self.wg_port),
"sni": get_subject_name_from_cert(ssl_cert_path),
}
2022-02-15 21:16:35 +08:00
def add_trojan_client_with(self, remote_config, remote_peer_config):
2022-02-15 15:55:25 +08:00
self.local_autogen_nextport += 1
tunnel_name = "gen{}{}".format(self.wg_hash[:8], self.local_autogen_nextport)
2022-02-15 21:16:35 +08:00
self.add_trojan_client(tunnel_name, self.local_autogen_nextport, remote_peer_config["password"],
"{}:{}".format(remote_config["ip"], remote_peer_config["listen"]), remote_peer_config["target"], ssl_sni=remote_peer_config["sni"])
self.append_input_peer_clientside(remote_config["wgkey"], remote_peer_config["allowed"], tunnel_name)
2022-02-15 15:55:25 +08:00
def add_trojan_client_mux(self, tunnel_name, mux_size, listen_port, tunnel_password, remote_addr, target_port, ssl_sni=None):
if self.podman_user:
self.add_expose(listen_port)
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(listen_port)
else:
self.tunnel_local_endpoint[tunnel_name] = "gateway:{}".format(listen_port)
self.add_muxer(listen_port, listen_port+1, mux_size)
for mux_idx in range(mux_size):
self._do_add_trojan_client(listen_port + 1 + mux_idx, tunnel_password, remote_addr, target_port, ssl_sni)
2022-02-06 05:31:18 +08:00
2022-02-15 15:55:25 +08:00
def add_trojan_client(self, tunnel_name, listen_port, tunnel_password, remote_addr, target_port, ssl_sni=None):
if self.podman_user:
self.add_expose(listen_port)
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(listen_port)
else:
self.tunnel_local_endpoint[tunnel_name] = "gateway:{}".format(listen_port)
self._do_add_trojan_client(listen_port, tunnel_password, remote_addr, target_port, ssl_sni)
def _do_add_trojan_client(self, listen_port, tunnel_password, remote_addr, target_port, ssl_sni):
2022-02-06 05:31:18 +08:00
self.container_bootstrap.append({
"type": "trojan-client",
2022-02-06 17:01:25 +08:00
"listen": int(listen_port),
2022-02-06 05:31:18 +08:00
"password": tunnel_password,
"remote": remote_addr,
2022-02-06 17:01:25 +08:00
"target": int(target_port),
2022-02-06 05:31:18 +08:00
"sni": ssl_sni,
})
2022-02-02 02:52:53 +08:00
def parse(self, content):
2022-02-15 15:56:57 +08:00
self.wg_hash = sha256(content.encode()).hexdigest()
2022-02-15 16:47:25 +08:00
errprint('[INFO] config hash: {}'.format(self.wg_hash))
2022-02-15 15:56:57 +08:00
2022-02-02 02:52:53 +08:00
# parse input
input_mode = ''
current_peer = []
2022-02-15 15:56:57 +08:00
for line in content.split('\n'):
# tags to filter out (never enter compile module)
if line.startswith('#store:key'):
parts = line.split(' ')[1:]
private_pem = parts[0]
2022-02-15 16:47:25 +08:00
private_pem = base64.b64decode(private_pem).decode()
2022-02-15 15:56:57 +08:00
2022-02-15 21:16:35 +08:00
self.local_private_key, self.local_public_key = get_rsa_keypair_from_private_pem(private_pem)
2022-02-15 16:47:25 +08:00
errprint('Loaded 1 PEM private key')
2022-02-15 15:56:57 +08:00
continue
2022-02-02 02:52:53 +08:00
if line.startswith('[Interface]'):
input_mode = 'interface'
continue
2022-02-02 02:52:53 +08:00
if line.startswith('[Peer]'):
input_mode = 'peer'
if current_peer:
self.input_peer.append(current_peer)
current_peer = []
continue
if input_mode == 'interface':
self.input_interface.append(line)
elif input_mode == 'peer':
current_peer.append(line)
else:
2022-02-15 16:47:25 +08:00
errprint('[WARN] Unexpected line: {}'.format(line))
2022-02-02 02:52:53 +08:00
if current_peer:
self.input_peer.append(current_peer)
2022-02-15 16:47:25 +08:00
2022-02-02 02:52:53 +08:00
def compile_interface(self):
2022-02-02 04:10:06 +08:00
self.result_interface.append('[Interface]')
2022-02-15 19:06:47 +08:00
filted_input_interface = []
2022-02-15 15:56:57 +08:00
unresolved_peers = []
# pre-compile registry-related
2022-02-02 02:52:53 +08:00
for line in self.input_interface:
if line.startswith('ListenPort'):
self.wg_port = int(line.split('=')[1])
if line.startswith('MTU'):
self.wg_mtu = int(line.split('=')[1])
2022-02-15 15:56:57 +08:00
if line.startswith('PrivateKey'):
wg_private_key = '='.join(line.split('=')[1:]).strip()
2022-02-15 16:47:25 +08:00
self.wg_pubkey = subprocess.check_output(["wg", "pubkey"], input=wg_private_key.encode()).decode().strip()
2022-02-02 02:52:53 +08:00
2022-02-15 16:47:25 +08:00
if line.startswith('#registry '):
2022-02-15 15:56:57 +08:00
parts = line.split(' ')[1:]
reg_name = parts[0]
self.registry_domain = "https://{}".format(reg_name)
elif line.startswith('#registry-insecure'):
parts = line.split(' ')[1:]
reg_name = parts[0]
self.registry_domain = "http://{}".format(reg_name)
2022-02-15 16:47:25 +08:00
errprint('[WARN] Insecure registry may have potential danger, only use for test purpose.')
2022-02-15 15:56:57 +08:00
elif line.startswith('#name'):
parts = line.split(' ')[1:]
client_name = parts[0]
self.registry_client_name = client_name
self.flag_require_registry = True
elif line.startswith('#connect-to'):
parts = line.split(' ')[1:]
target_name = parts[0]
unresolved_peers.append(target_name)
self.flag_require_registry = True
elif line.startswith('#accept-client'):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
client_name = parts[1]
client_ip = parts[2]
client_allowed = parts[3]
2022-02-15 21:16:35 +08:00
peer_allowed = parts[4]
2022-02-15 15:56:57 +08:00
self.pending_accepts.append({
"tunnel": tunnel_name,
"client": client_name,
2022-02-15 21:16:35 +08:00
"client_ip": client_ip,
2022-02-15 15:56:57 +08:00
"allowed": client_allowed,
2022-02-15 21:16:35 +08:00
"peer_allowed": peer_allowed,
2022-02-15 15:56:57 +08:00
})
self.flag_require_registry = True
2022-02-15 19:06:47 +08:00
else:
filted_input_interface.append(line)
2022-02-15 15:56:57 +08:00
# registry init
if self.flag_require_registry:
if not self.local_private_key:
2022-02-15 16:47:25 +08:00
errprint('registry required but no existing private key found, generating new...')
2022-02-15 15:56:57 +08:00
self.local_private_key, self.local_public_key = generate_rsa_keypair()
private_pem, public_pem = get_pem_from_rsa_keypair(self.local_private_key, self.local_public_key)
if self.flag_allow_modify:
2022-02-15 16:47:25 +08:00
errprint('[MODIFY] appending to {}...'.format(self.opt_source_path))
2022-02-15 15:56:57 +08:00
with open(self.opt_source_path, 'a') as f:
2022-02-15 16:47:25 +08:00
f.write('\n#store:key {}\n'.format(base64.b64encode(private_pem.encode()).decode()))
errprint('[MODIFY] source file modifed, please re-run to continue.')
2022-02-15 15:56:57 +08:00
else:
2022-02-15 16:47:25 +08:00
errprint('[ERROR] cannot modify source file, please re-run with -i option.')
2022-02-15 15:56:57 +08:00
exit(1)
2022-02-15 16:47:25 +08:00
2022-02-15 15:56:57 +08:00
self.registry_ensure()
# registry fetch connect-to
for peer_client_name in unresolved_peers:
2022-02-15 21:16:35 +08:00
errprint('[REGISTRY-RESOLVE] Resolving connect-to {}...'.format(peer_client_name))
peer_client_config = self.registry_resolve(peer_client_name)
if not peer_client_config:
errprint('[WARN] Unable to resolve client: {}'.format(peer_client_name))
continue
peer_config = peer_client_config["peers"][self.registry_client_name]
2022-02-15 15:56:57 +08:00
{
"udp2raw": self.add_udp2raw_client_with,
"gost": self.add_gost_client_with,
"trojan": self.add_trojan_client_with,
2022-02-15 21:16:35 +08:00
}.get(peer_config["type"], lambda x, y: False)(peer_client_config, peer_config)
2022-02-15 15:56:57 +08:00
# compile interface
2022-02-15 19:06:47 +08:00
for line in filted_input_interface:
2022-02-02 02:52:53 +08:00
if not line.startswith('#'):
self.result_interface.append(line)
continue
2022-02-15 15:56:57 +08:00
elif line.startswith('#enable-bbr'):
2022-02-02 02:52:53 +08:00
self.result_postup.append('PostUp=sysctl net.core.default_qdisc=fq\nPostUp=sysctl net.ipv4.tcp_congestion_control=bbr')
elif line.startswith('#enable-forward'):
self.result_postup.append('PostUp=sysctl net.ipv4.ip_forward=1')
elif line.startswith('#iptables-forward'):
self.result_postup.append('PostUp=iptables -A FORWARD -i {} -j ACCEPT'.format(self.wg_name))
self.result_postdown.append('PostDown=iptables -D FORWARD -i {} -j ACCEPT'.format(self.wg_name))
elif line.startswith('#route-to'):
2022-02-02 02:52:53 +08:00
self.flag_is_route_forward = True
parts = line.split(' ')[1:]
table_name = parts[0]
self.result_postup.append('PostUp=ip route add 0.0.0.0/0 dev {} table {}'.format(self.wg_name, table_name))
2022-02-15 16:47:25 +08:00
errprint('[WARN] Please ensure custom route table {} exists.'.format(table_name))
elif line.startswith('#route-from'):
self.flag_is_route_lookup = True
parts = line.split(' ')[1:]
table_name = parts[0]
self.lookup_table = table_name
2022-02-15 16:47:25 +08:00
errprint('[WARN] Please ensure custom route table {} exists.'.format(table_name))
2022-02-06 05:31:18 +08:00
elif line.startswith('#podman-user'):
parts = line.split(' ')[1:]
user_name = parts[0]
self.podman_user = user_name
2022-02-02 02:52:53 +08:00
elif line.startswith('#udp2raw-server'):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_port = int(parts[1])
2022-02-02 02:52:53 +08:00
tunnel_passwd = parts[2]
2022-02-06 09:18:43 +08:00
if self.podman_user:
2022-02-15 16:47:25 +08:00
errprint('[Error] udp2raw tunnel need root as podman user, got {}'.format(self.podman_user))
2022-02-06 09:18:43 +08:00
exit(1)
2022-02-15 15:55:25 +08:00
self.add_udp2raw_server(tunnel_name, tunnel_port, tunnel_passwd)
2022-02-06 09:18:43 +08:00
self.flag_container_must_host = True
2022-02-02 02:52:53 +08:00
elif line.startswith('#udp2raw-client '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_port = int(parts[1])
2022-02-02 02:52:53 +08:00
tunnel_remote = parts[2]
tunnel_passwd = parts[3]
2022-02-06 09:18:43 +08:00
if self.podman_user:
2022-02-15 16:47:25 +08:00
errprint('[Error] udp2raw tunnel need root as podman user, got {}'.format(self.podman_user))
2022-02-06 09:18:43 +08:00
exit(1)
2022-02-15 15:55:25 +08:00
self.add_udp2raw_client(tunnel_name, tunnel_port, tunnel_passwd, tunnel_remote)
2022-02-02 02:52:53 +08:00
elif line.startswith('#udp2raw-client-mux '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
tunnel_mux = int(parts[1])
tunnel_port = int(parts[2])
tunnel_remote = parts[3]
tunnel_passwd = parts[4]
2022-02-06 09:18:43 +08:00
if self.podman_user:
2022-02-15 16:47:25 +08:00
errprint('[Error] udp2raw tunnel need root as podman user, got {}'.format(self.podman_user))
2022-02-06 09:18:43 +08:00
exit(1)
2022-02-15 15:55:25 +08:00
self.add_udp2raw_client_mux(tunnel_name, tunnel_mux, tunnel_port + 1 + mux_idx, tunnel_passwd, tunnel_remote)
2022-02-02 02:52:53 +08:00
elif line.startswith('#gost-server '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_port = int(parts[1])
2022-02-02 02:52:53 +08:00
2022-02-15 15:55:25 +08:00
self.add_gost_server(tunnel_name, tunnel_port)
2022-02-06 16:32:01 +08:00
self.add_expose(tunnel_port, mode='tcp')
2022-02-02 02:52:53 +08:00
elif line.startswith('#gost-client '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_port = int(parts[1])
2022-02-02 02:52:53 +08:00
tunnel_remote = parts[2]
2022-02-15 15:55:25 +08:00
self.add_gost_client(tunnel_name, tunnel_port, tunnel_remote)
2022-02-02 02:52:53 +08:00
elif line.startswith('#gost-client-mux '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
tunnel_mux = int(parts[1])
tunnel_port = int(parts[2])
tunnel_remote = parts[3]
2022-02-15 15:55:25 +08:00
self.add_gost_client_mux(tunnel_name, tunnel_mux, tunnel_port, tunnel_remote)
2022-02-06 05:31:18 +08:00
elif line.startswith('#trojan-server'):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_port = int(parts[1])
2022-02-06 05:31:18 +08:00
tunnel_passwd = parts[2]
tunnel_cert = parts[3]
tunnel_key = parts[4]
2022-02-15 15:55:25 +08:00
self.add_trojan_server(tunnel_name, tunnel_port, tunnel_passwd, tunnel_cert, tunnel_key)
2022-02-06 09:39:30 +08:00
self.add_expose(tunnel_port, mode='tcp')
2022-02-06 05:31:18 +08:00
elif line.startswith('#trojan-client '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_port = int(parts[1])
2022-02-06 05:31:18 +08:00
tunnel_passwd = parts[2]
tunnel_remote = parts[3]
2022-02-06 17:01:25 +08:00
tunnel_target = int(parts[4])
2022-02-02 02:52:53 +08:00
2022-02-15 15:55:25 +08:00
self.add_trojan_client(tunnel_name, tunnel_port, tunnel_passwd, tunnel_remote, tunnel_target)
2022-02-06 05:31:18 +08:00
elif line.startswith('#trojan-client-mux '):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-06 17:01:25 +08:00
tunnel_mux = int(parts[1])
tunnel_port = int(parts[2])
2022-02-06 05:31:18 +08:00
tunnel_passwd = parts[3]
tunnel_remote = parts[4]
2022-02-06 17:01:25 +08:00
tunnel_target = int(parts[5])
2022-02-06 05:31:18 +08:00
2022-02-15 15:55:25 +08:00
self.tunnel_local_endpoint[tunnel_name] = "gateway:{}".format(tunnel_port)
2022-02-06 05:31:18 +08:00
self.add_muxer(tunnel_port, tunnel_port+1, tunnel_mux)
2022-02-02 02:52:53 +08:00
for mux_idx in range(tunnel_mux):
2022-02-06 05:31:18 +08:00
self.add_trojan_client(tunnel_port + 1 + mux_idx, tunnel_passwd, tunnel_remote, tunnel_target)
2022-02-15 16:47:25 +08:00
2022-02-06 10:04:45 +08:00
if self.podman_user:
self.add_expose(tunnel_port)
2022-02-15 15:55:25 +08:00
self.tunnel_local_endpoint[tunnel_name] = "127.0.0.1:{}".format(tunnel_port)
2022-02-02 02:52:53 +08:00
else:
2022-02-15 16:47:25 +08:00
errprint('[WARN] comment or unknown hint: {}'.format(line))
2022-02-06 05:31:18 +08:00
2022-02-02 02:52:53 +08:00
if not self.wg_mtu:
2022-02-15 16:47:25 +08:00
errprint('[WARN] MTU not detected, using suggested mtu value (1280).')
2022-02-02 02:52:53 +08:00
self.result_interface.append('MTU=1280')
2022-02-15 15:55:25 +08:00
2022-02-06 05:31:18 +08:00
if self.container_bootstrap:
2022-02-15 15:55:25 +08:00
config_str = json.dumps(self.container_bootstrap, ensure_ascii=False)
2022-02-06 05:31:18 +08:00
config_gen = base64.b64encode(config_str.encode()).decode()
2022-02-15 15:55:25 +08:00
2022-02-06 05:31:18 +08:00
config_parts = []
while len(config_gen) > 1024:
config_parts.append(config_gen[:1024])
config_gen = config_gen[1024:]
2022-02-06 07:03:55 +08:00
config_parts.append(config_gen)
2022-02-06 05:31:18 +08:00
tmp_base64_filepath = "/tmp/wg-op-container-bootstrap-{}.data".format(self.wg_name)
tmp_filepath = "/tmp/wg-op-container-bootstrap-{}.json".format(self.wg_name)
self.result_postup.append('PostUp=rm -f {}'.format(tmp_base64_filepath))
for this_config_line in config_parts:
self.result_postup.append('PostUp=echo {} >> {}'.format(this_config_line, tmp_base64_filepath))
self.result_postup.append('PostUp=base64 -d {} > {}'.format(tmp_base64_filepath, tmp_filepath))
self.result_postup.append('PostUp=rm {}'.format(tmp_base64_filepath))
self.result_container_prebootstrap.append('PostUp={}'.format(
self.get_podman_cmd_with('podman cp {} {}:/root/conf/bootstrap.json'.format(tmp_filepath, self.get_container_name()))
))
self.result_container_prebootstrap.append('PostUp=rm {}'.format(tmp_filepath))
if self.result_container_prebootstrap or self.result_container_postbootstrap:
2022-02-06 09:18:43 +08:00
if not self.flag_container_must_host:
self.result_postup.append('PostUp={}'.format(
self.get_podman_cmd_with('podman network create {}'.format(self.get_container_network_name()))
))
if not self.flag_container_must_host and self.container_expose_port:
2022-02-06 07:03:55 +08:00
cmd_ports = ["-p {}:{}/{}".format(this_port['port'], this_port['port'], this_port['mode']) for this_port in self.container_expose_port]
cmd_ports = ' '.join(cmd_ports)
else:
cmd_ports = ''
2022-02-06 05:31:18 +08:00
self.result_postup.append('PostUp={}'.format(
2022-02-06 07:37:27 +08:00
self.get_podman_cmd_with('podman run --rm --cap-add NET_RAW -v {}:/root/bin -v {}:/root/app {} --name {} --network {} -d wg-ops-runenv'.format(
2022-02-06 07:03:55 +08:00
path_bin_dir, path_app_dir, cmd_ports, self.get_container_name(), self.get_container_network_name()))
2022-02-06 05:31:18 +08:00
))
self.result_postup.append('PostUp={}'.format(
self.get_podman_cmd_with('podman exec {} mkdir -p /root/ssl /root/runner /root/conf'.format(
self.get_container_name()))
))
self.result_postdown.append('PostDown={}'.format(
self.get_podman_cmd_with('podman stop {}'.format(self.get_container_name()))
))
2022-02-06 09:18:43 +08:00
if not self.flag_container_must_host:
self.result_postdown.append('PostDown={}'.format(
self.get_podman_cmd_with('podman network rm {}'.format(self.get_container_network_name()))
))
2022-02-06 05:31:18 +08:00
self.result_postup.extend(self.result_container_prebootstrap)
if self.flag_container_must_host:
2022-02-06 09:18:43 +08:00
self.result_postup.append('PostUp={}'.format(
self.get_podman_cmd_with('podman exec -t -e GATEWAY_IP=127.0.0.1 -e WG_PORT={} {} /usr/bin/python3 /root/app/bootstrap.py'.format(
self.wg_port, self.get_container_name()))
))
elif self.podman_user:
self.result_postup.append('PostUp={}'.format(
self.get_podman_cmd_with('CT_GATEWAY=$(/usr/bin/python3 {}); podman exec -t -e GATEWAY_IP=$CT_GATEWAY -e WG_PORT={} {} /usr/bin/python3 /root/app/bootstrap.py'.format(
path_get_lan_ip, self.wg_port, self.get_container_name()))
2022-02-06 09:18:43 +08:00
))
else:
self.result_postup.append('PostUp={}'.format(
self.get_podman_cmd_with('CT_GATEWAY=$(/usr/bin/python3 {} {}); podman exec -t -e GATEWAY_IP=$CT_GATEWAY -e WG_PORT={} {} /usr/bin/python3 /root/app/bootstrap.py'.format(
path_get_gateway, self.get_container_network_name(), self.wg_port, self.get_container_name()))
2022-02-06 09:18:43 +08:00
))
2022-02-06 05:31:18 +08:00
self.result_postup.extend(self.result_container_postbootstrap)
2022-02-15 21:16:35 +08:00
# registry fetch accept-client
if self.flag_require_registry and self.pending_accepts:
resolved_upload_arr = {}
for accept_info in self.pending_accepts:
peer_client_name = accept_info["client"]
errprint('[REGISTRY-RESOLVE] Resolving accept-client {}...'.format(peer_client_name))
peer_client_config = self.registry_query(peer_client_name)
if not peer_client_config:
errprint('[WARN] Unable to resolve client: {}'.format(peer_client_name))
continue
self.append_input_peer_serverside(peer_client_config["wgkey"], accept_info["allowed"])
peer_tunnel_info = copy.copy(self.tunnel_server_reports[accept_info['tunnel']])
peer_tunnel_info["allowed"] = accept_info["peer_allowed"]
public_key = get_rsa_pubkey_from_public_pem(peer_client_config["pubkey"])
resolved_upload_arr[peer_client_name] = rsa_encrypt_base64(public_key, json.dumps(peer_tunnel_info, ensure_ascii=False).encode())
if resolved_upload_arr:
self.registry_ensure(peers=resolved_upload_arr)
2022-02-02 02:52:53 +08:00
def compile_peers(self):
if self.flag_is_route_forward and len(self.input_peer) > 1:
2022-02-15 16:47:25 +08:00
errprint('[WARN] route-forward should used with ONLY one peer.')
2022-02-02 02:52:53 +08:00
for this_peer_idx, this_peer_lines in enumerate(self.input_peer):
current_pubkey = ''
current_allowed = ''
2022-02-02 03:51:21 +08:00
if self.flag_is_route_lookup:
current_lookup = self.lookup_table
else:
current_lookup = ''
2022-02-06 17:01:25 +08:00
# pre-scan
2022-02-02 02:52:53 +08:00
for line in this_peer_lines:
if line.startswith('PublicKey'):
2022-02-02 04:40:14 +08:00
current_pubkey = '='.join(line.split('=')[1:])
if line.startswith('AllowedIPs'):
current_allowed = line.split('=')[1].strip().split(',')
2022-02-02 02:52:53 +08:00
2022-02-06 17:01:25 +08:00
self.result_peers.append('[Peer]')
for line in this_peer_lines:
2022-02-02 02:52:53 +08:00
if not line.startswith('#'):
self.result_peers.append(line)
continue
if line.startswith('#use-tunnel'):
parts = line.split(' ')[1:]
tunnel_name = parts[0]
2022-02-15 15:55:25 +08:00
tunnel_addr = self.tunnel_local_endpoint[tunnel_name]
2022-02-06 05:31:18 +08:00
if ":" in tunnel_addr:
addr_parts = tunnel_addr.split(':')
addr_host = addr_parts[0]
2022-02-06 17:01:25 +08:00
addr_port = int(addr_parts[1])
2022-02-06 05:31:18 +08:00
if addr_host == "gateway":
tunnel_addr = ""
if self.flag_container_must_host or self.podman_user:
self.result_postup.append("PostUp=wg set {} peer {} endpoint 127.0.0.1:{}".format(
2022-02-06 09:18:43 +08:00
self.wg_name, current_pubkey, addr_port))
else:
self.result_postup.append("PostUp=CT_IP=$({}); wg set {} peer {} endpoint $CT_IP:{}".format(
self.get_podman_cmd_with('/usr/bin/python3 {} {} {}'.format(path_get_ip, self.get_container_network_name(), self.get_container_name())),
2022-02-06 09:18:43 +08:00
self.wg_name, current_pubkey, addr_port))
2022-02-06 05:31:18 +08:00
elif tunnel_addr:
tunnel_addr = "127.0.0.1:{}".format(tunnel_addr)
if tunnel_addr:
self.result_peers.append('Endpoint={}'.format(tunnel_addr))
2022-02-02 03:51:21 +08:00
elif line.startswith('#route-from'):
parts = line.split(' ')[1:]
table_name = parts[0]
if table_name != self.lookup_table:
current_lookup = table_name
2022-02-15 16:47:25 +08:00
errprint('[WARN] Please ensure custom route table {} exists.'.format(table_name))
2022-02-02 02:52:53 +08:00
else:
2022-02-15 16:47:25 +08:00
errprint('[WARN] comment or unknown hint: {}'.format(line))
2022-02-02 02:52:53 +08:00
if self.flag_is_route_forward and this_peer_idx == 0:
self.result_postup.insert(0, 'PostUp=wg set {} peer {} allowed-ips 0.0.0.0/0'.format(self.wg_name, current_pubkey))
2022-02-02 03:51:21 +08:00
if current_lookup:
for ip_cidr in current_allowed:
2022-02-02 03:51:21 +08:00
self.result_postup.append('PostUp=ip rule add from {} lookup {}'.format(ip_cidr, current_lookup))
2022-02-02 04:14:08 +08:00
self.result_postdown.append('PostDown=ip rule del from {} lookup {}'.format(ip_cidr, current_lookup))
2022-02-15 21:16:35 +08:00
2022-02-02 02:52:53 +08:00
def get_result(self):
current_time = time.strftime("%Y-%m-%d %H:%M:%S")
return '''# Generated by wg-ops at {}. DO NOT EDIT.
2022-02-02 02:52:53 +08:00
{}
{}
{}
{}
'''.format(current_time, '\n'.join(self.result_interface), '\n'.join(self.result_postup), '\n'.join(self.result_postdown), '\n'.join(self.result_peers))
if __name__ == "__main__":
2022-02-15 15:56:57 +08:00
opts, args = getopt.getopt(sys.argv[1:], 'hiko:')
2022-02-02 03:51:21 +08:00
opts = {p[0]: p[1] for p in opts}
if '-h' in opts:
print('''wg-ops: WireGuard configuration extended generator
OPTIONS
-h Display this help and quit.
-k Output generated config to standard output
-o <filename> Output generated config to file. Default is {source_filename}.gen
2022-02-07 08:24:17 +08:00
HELP
For latest help please view https://github.com/Kiritow/wg-ops
''')
exit(0)
2022-02-02 02:52:53 +08:00
filepath = args[0]
filename = os.path.basename(filepath)
2022-02-02 03:51:21 +08:00
with open(filepath, 'r') as f:
2022-02-15 15:56:57 +08:00
content = f.read()
2022-02-02 02:52:53 +08:00
parser = Parser()
2022-02-15 15:56:57 +08:00
if '-i' in opts:
parser.flag_allow_modify = True
parser.opt_source_path = filepath
2022-02-02 02:52:53 +08:00
parser.parse(content)
parser.compile_interface()
parser.compile_peers()
if '-k' in opts or ('-o' in opts and opts['-o'] == '-'):
print(parser.get_result())
elif '-o' in opts:
2022-02-15 16:47:25 +08:00
errprint('Saving to {}...'.format(opts['-o']))
with open(opts['-o'], 'w') as f:
2022-02-02 02:52:53 +08:00
f.write(parser.get_result())
else:
2022-02-15 16:47:25 +08:00
errprint('Saving to {}.gen...'.format(filename))
with open('{}.gen'.format(filename), 'w') as f:
f.write(parser.get_result())