private key
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
quackerd 2023-08-22 12:36:05 -04:00
parent 63e83e3275
commit e6c6e90619
3 changed files with 74 additions and 84 deletions

View File

@ -6,24 +6,23 @@ d2ray is a single Docker container that provides easy 5-minute setups and braind
## Quickstart ## Quickstart
1. You can start with the example `docker-compose.yml` from this repo. 1. You can start with the example `docker-compose.yml` from this repo.
2. Adjust environment variables: 2. Adjust environment variables:
- `PORT`: the port Xray listens on. - `PORT`: the port Xray listens on. `Optional, default = 443`.
- `TARGET_HOST`: the target host to redirect non proxy connections. - `TARGET_HOST`: the target host to redirect non proxy connections. `Required`.
- `TARGET_PORT`: the target port to redirect non proxy connections. - `TARGET_PORT`: the target port to redirect non proxy connections. `Optional, default = 443`.
- `TARGET_SNI`: comma separated list of the target website's SNIs. - `TARGET_SNI`: comma separated list of the target website's SNIs. `Required`.
- `USERS`: comma separated list of usernames that can access Xray. - `PRIVATE_KEY` : server's private key. `Optional`.
- `LOG_LEVEL`: the verbosity of Xray logs. Default: `warn`. - `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` 3. `docker compose up -d`
4. Test your connection. 4. Test your connection.
## Docker Volume ## 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 ## 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. To make d2ray regenerate a new key pair, manually delete the key file `/etc/xray/certs/keys` from the mounted volume.
If you are generating the keys yourself, the key file must contain exactly the output of `xray x25519`.
## How To Update? ## How To Update?
- `docker compose down` - `docker compose down`

View File

@ -10,10 +10,11 @@ services:
- 8443:8443 - 8443:8443
environment: environment:
- PORT=8443 - PORT=8443
- TARGET_HOST=example.com - TARGET_HOST=www.apple.com
- TARGET_PORT=443 - TARGET_PORT=443
- TARGET_SNI=www.example.com,example.com - TARGET_SNI=www.apple.com,apple.com
- USERS=exampleuser1,exampleuser2 - USERS=alice,bob,eve
- PRIVATE_KEY=KE5MCI5e395fub55O1lsNPzvWw9nNAyCaecRSp3BvHg # Do NOT use this random key
- LOG_LEVEL=warn - LOG_LEVEL=warn
restart: unless-stopped restart: unless-stopped
networks: networks:

View File

@ -16,51 +16,76 @@ class d2args:
target_host : str target_host : str
target_sni : str target_sni : str
log_level : str log_level : str
private_key : str
public_key : str
users : list[str] users : list[str]
def __init__(self) -> None: def __init__(self) -> None:
self.port = 443 self._from_env()
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))]
def from_env(self) -> None: @staticmethod
env = os.getenv("PORT") def _get_env(name : str, default : str = None, required : bool = True) -> str:
if env != None: env = os.getenv(name)
self.port = int(env) if env == None:
if required:
raise Exception(f"Missing environment variable \"{name}\".")
else:
return default
return env
env = os.getenv("TARGET_PORT") @staticmethod
if env != None: def _parse_xray_x25519_output(stdout : str) -> tuple[str, str]:
self.target_port = int(env) skey = None
pkey = None
env = os.getenv("TARGET_SNI") lines = stdout.split("\n")
if env != None: if len(lines) < 2:
self.target_sni = env.split(",") raise Exception(f"Unknown Xray output format:\n\"{stdout}\"\n")
env = os.getenv("TARGET_HOST") priv_key_hdr = "Private key: "
if env != None: pub_key_hdr = "Public key: "
self.target_host = env 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") def _from_env(self) -> None:
if env != None: self.target_host = self._get_env("TARGET_HOST")
self.log_level = env self.target_sni = self._get_env("TARGET_SNI").split(",")
self.users = self._get_env("USERS").split(",")
env = os.getenv("USERS") self.port = int(self._get_env("PORT", default="443", required=False))
if env != None: self.target_port = int(self._get_env("TARGET_PORT", default="443", required=False))
self.users = env.split(",") 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" ret = (f"Port: {self.port}\n"
f"Target Port: {self.target_port}\n" f"Target Port: {self.target_port}\n"
f"Target Host: {self.target_host}\n" f"Target Host: {self.target_host}\n"
f"Target SNI: {', '.join(self.target_sni)}\n" f"Target SNI: {', '.join(self.target_sni)}\n"
f"Log Level: {self.log_level}\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 return ret
def process_directory(path : str, vars : dict[str, str], delete_template : bool = True) -> None: def process_directory(path : str, vars : dict[str, str], delete_template : bool = True) -> None:
for f in os.listdir(path): for f in os.listdir(path):
full_path = os.path.join(path, f) 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: def build_users_json(users: list[str]) -> str:
return ', '.join(["{\"id\": \"" + item + "\", \"flow\": \"xtls-rprx-vision\"}" for item in users]) 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 : dict[str,str] = dict()
jinja_dict["PORT"] = str(args.port) 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["LOG_LEVEL"] = args.log_level
jinja_dict["USERS"] = build_users_json(args.users) jinja_dict["USERS"] = build_users_json(args.users)
jinja_dict["PRIVATE_KEY"] = skey jinja_dict["PRIVATE_KEY"] = args.private_key
return jinja_dict 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(): def main():
print(f"Initializing d2ray...", flush=True) print(f"Initializing d2ray...", flush=True)
args = d2args() args = d2args()
args.from_env()
print(f"Checking key file...", flush=True) print(f"\nConfiguration:\n{str(args)}\n", 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"Reading keys...", flush=True) template = build_jinja_dict(args)
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)
print(f"Processing config files...", flush=True) print(f"Processing config files...", flush=True)
process_directory("/opt/xray", template) process_directory("/opt/xray", template)
print(f"Initialization completed.\n", flush=True)
main() main()