diff --git a/README.md b/README.md index d944515..3037781 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,23 @@ d2ray is a single Docker container that provides easy 5-minute setups and braind ## Quickstart 1. You can start with the example `docker-compose.yml` from this repo. 2. Adjust environment variables: - - `PORT`: the port Xray listens on. - - `TARGET_HOST`: the target host to redirect non proxy connections. - - `TARGET_PORT`: the target port to redirect non proxy connections. - - `TARGET_SNI`: comma separated list of the target website's SNIs. - - `USERS`: comma separated list of usernames that can access Xray. - - `LOG_LEVEL`: the verbosity of Xray logs. Default: `warn`. + - `PORT`: the port Xray listens on. `Optional, default = 443`. + - `TARGET_HOST`: the target host to redirect non proxy connections. `Required`. + - `TARGET_PORT`: the target port to redirect non proxy connections. `Optional, default = 443`. + - `TARGET_SNI`: comma separated list of the target website's SNIs. `Required`. + - `PRIVATE_KEY` : server's private key. `Optional`. + - `USERS`: comma separated list of usernames that can access Xray. `Required`. + - `LOG_LEVEL`: the verbosity of Xray logs. `Optional, default = warn`. 3. `docker compose up -d` 4. Test your connection. ## Docker Volume -All d2ray logs and private/public key pairs are stored in `/etc/d2ray` in the container. You can mount an external folder to that location to persist settings. See the example `docker-compose.yml`. +The logs and private key are stored in `/etc/d2ray` in the container. You can mount an external folder to that location to persist settings. Otherwise d2ray creates an anonymous Docker volume. ## Key Generation -d2ray checks whether a key file exists at path `/etc/xray/certs/keys` and generates a new key pair if not found. +If `PRIVATE_KEY` is provided, d2ray uses that key. Otherwise, d2ray generates a new key pair and persists it in `/etc/xray/certs/keys`. The corresponding public key is always printed to the container log (`docker logs`), which clients use to connect. -You can either supply a pre-generated private key using `xray x25519` or let d2ray generate one. The corresponding public key is printed to the container log (`docker logs`), which clients use to connect. - -If you are generating the keys yourself, the key file must contain exactly the output of `xray x25519`. +To make d2ray regenerate a new key pair, manually delete the key file `/etc/xray/certs/keys` from the mounted volume. ## How To Update? - `docker compose down` diff --git a/docker-compose.yml b/docker-compose.yml index 4379bf8..b439870 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,10 +10,11 @@ services: - 8443:8443 environment: - PORT=8443 - - TARGET_HOST=example.com + - TARGET_HOST=www.apple.com - TARGET_PORT=443 - - TARGET_SNI=www.example.com,example.com - - USERS=exampleuser1,exampleuser2 + - TARGET_SNI=www.apple.com,apple.com + - USERS=alice,bob,eve + - PRIVATE_KEY=KE5MCI5e395fub55O1lsNPzvWw9nNAyCaecRSp3BvHg # Do NOT use this random key - LOG_LEVEL=warn restart: unless-stopped networks: diff --git a/opt/init.py b/opt/init.py index 0b232c8..d1b07f0 100644 --- a/opt/init.py +++ b/opt/init.py @@ -16,51 +16,76 @@ class d2args: target_host : str target_sni : str log_level : str + private_key : str + public_key : str users : list[str] def __init__(self) -> None: - self.port = 443 - self.target_host = "localhost" - self.target_port = 443 - self.target_sni = "localhost" - self.log_level = "warn" - self.users = [''.join(random.choices(string.ascii_uppercase + string.digits, k=24))] + self._from_env() - def from_env(self) -> None: - env = os.getenv("PORT") - if env != None: - self.port = int(env) + @staticmethod + def _get_env(name : str, default : str = None, required : bool = True) -> str: + env = os.getenv(name) + if env == None: + if required: + raise Exception(f"Missing environment variable \"{name}\".") + else: + return default + return env - env = os.getenv("TARGET_PORT") - if env != None: - self.target_port = int(env) - - env = os.getenv("TARGET_SNI") - if env != None: - self.target_sni = env.split(",") + @staticmethod + def _parse_xray_x25519_output(stdout : str) -> tuple[str, str]: + skey = None + pkey = None + lines = stdout.split("\n") + if len(lines) < 2: + raise Exception(f"Unknown Xray output format:\n\"{stdout}\"\n") - env = os.getenv("TARGET_HOST") - if env != None: - self.target_host = env + priv_key_hdr = "Private key: " + pub_key_hdr = "Public key: " + for line in lines: + if line.startswith(priv_key_hdr): + skey = line[len(priv_key_hdr):] + elif line.startswith(pub_key_hdr): + pkey = line[len(pub_key_hdr):] + if (skey == None) or (pkey == None): + raise Exception(f"Unable to extract private or public key from Xray output:\n\"{stdout}\"\n") + return (skey.strip(), pkey.strip()) - env = os.getenv("LOG_LEVEL") - if env != None: - self.log_level = env + def _from_env(self) -> None: + self.target_host = self._get_env("TARGET_HOST") + self.target_sni = self._get_env("TARGET_SNI").split(",") + self.users = self._get_env("USERS").split(",") - env = os.getenv("USERS") - if env != None: - self.users = env.split(",") + self.port = int(self._get_env("PORT", default="443", required=False)) + self.target_port = int(self._get_env("TARGET_PORT", default="443", required=False)) + self.log_level = self._get_env("LOG_LEVEL", default="warn", required=False) - def __str__(self): + self.private_key = self._get_env("PRIVATE_KEY", default=None, required=False) + if (self.private_key == None): + print(f"Private key not provided.", flush=True) + if not KEY_FILE.exists(): + print(f"Key file {KEY_FILE} not found. Generating new keys...") + self.private_key, _ = self._parse_xray_x25519_output(subprocess.check_output(f"{XRAY_BIN} x25519", shell = True).decode()) + with open(KEY_FILE, "w") as f: + f.write(self.private_key) + else: + print(f"Reading from key file {KEY_FILE} ...") + with open(KEY_FILE, "r") as f: + self.private_key = f.read().strip() + + _ , self.public_key = self._parse_xray_x25519_output(subprocess.check_output(f"{XRAY_BIN} x25519 -i {self.private_key}", shell = True).decode()) + + def __str__(self) -> str: ret = (f"Port: {self.port}\n" f"Target Port: {self.target_port}\n" f"Target Host: {self.target_host}\n" f"Target SNI: {', '.join(self.target_sni)}\n" f"Log Level: {self.log_level}\n" - f"Users: {', '.join(self.users)}" + f"Users: {', '.join(self.users)}\n" + f"Public Key: {self.public_key}" ) return ret - def process_directory(path : str, vars : dict[str, str], delete_template : bool = True) -> None: for f in os.listdir(path): full_path = os.path.join(path, f) @@ -81,7 +106,7 @@ def build_target_snis(snis : list[str]) -> str: def build_users_json(users: list[str]) -> str: return ', '.join(["{\"id\": \"" + item + "\", \"flow\": \"xtls-rprx-vision\"}" for item in users]) -def build_jinja_dict(args : d2args, skey : str) -> dict[str, str]: +def build_jinja_dict(args : d2args) -> dict[str, str]: jinja_dict : dict[str,str] = dict() jinja_dict["PORT"] = str(args.port) @@ -93,56 +118,21 @@ def build_jinja_dict(args : d2args, skey : str) -> dict[str, str]: jinja_dict["LOG_LEVEL"] = args.log_level jinja_dict["USERS"] = build_users_json(args.users) - jinja_dict["PRIVATE_KEY"] = skey - + jinja_dict["PRIVATE_KEY"] = args.private_key return jinja_dict -def parse_xray_x25519_output(stdout : str) -> tuple[str, str]: - skey = None - pkey = None - lines = stdout.split("\n") - if len(lines) < 2: - raise Exception(f"Unknown Xray output format:\n\"{stdout}\"\n") - - priv_key_hdr = "Private key: " - pub_key_hdr = "Public key: " - for line in lines: - if line.startswith(priv_key_hdr): - skey = line[len(priv_key_hdr):] - elif line.startswith(pub_key_hdr): - pkey = line[len(pub_key_hdr):] - if (skey == None) or (pkey == None): - raise Exception(f"Unable to extract private or public key from Xray output:\n\"{stdout}\"\n") - return (skey.strip(), pkey.strip()) def main(): print(f"Initializing d2ray...", flush=True) args = d2args() - args.from_env() - print(f"Checking key file...", flush=True) - if not KEY_FILE.exists(): - print(f"Key file not found at {KEY_FILE}. Generating...") - out = subprocess.check_output(f"{XRAY_BIN} x25519", shell = True).decode() - with open(KEY_FILE, "w") as f: - f.write(out) - - with open(KEY_FILE, "r") as f: - out = f.read() + print(f"\nConfiguration:\n{str(args)}\n", flush=True) - print(f"Reading keys...", flush=True) - skey, pkey = parse_xray_x25519_output(out) - - print(f"Verifying public key...", flush=True) - _, _pkey = parse_xray_x25519_output(subprocess.check_output(f"{XRAY_BIN} x25519 -i {skey}", shell = True).decode()) - if (_pkey != pkey): - print(f"Unmatching public key: expected \"{_pkey}\" but key file provided \"{pkey}\". Please verify the key file.", flush=True) - - print(f"\nConfigurations:\n{str(args)}\nPublic key: {pkey}\n", flush=True) - - template = build_jinja_dict(args, skey) + template = build_jinja_dict(args) print(f"Processing config files...", flush=True) process_directory("/opt/xray", template) + print(f"Initialization completed.\n", flush=True) + main()