This commit is contained in:
parent
63e83e3275
commit
e6c6e90619
21
README.md
21
README.md
@ -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`
|
||||||
|
@ -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:
|
||||||
|
128
opt/init.py
128
opt/init.py
@ -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
|
||||||
|
lines = stdout.split("\n")
|
||||||
|
if len(lines) < 2:
|
||||||
|
raise Exception(f"Unknown Xray output format:\n\"{stdout}\"\n")
|
||||||
|
|
||||||
env = os.getenv("TARGET_SNI")
|
priv_key_hdr = "Private key: "
|
||||||
if env != None:
|
pub_key_hdr = "Public key: "
|
||||||
self.target_sni = env.split(",")
|
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("TARGET_HOST")
|
def _from_env(self) -> None:
|
||||||
if env != None:
|
self.target_host = self._get_env("TARGET_HOST")
|
||||||
self.target_host = env
|
self.target_sni = self._get_env("TARGET_SNI").split(",")
|
||||||
|
self.users = self._get_env("USERS").split(",")
|
||||||
|
|
||||||
env = os.getenv("LOG_LEVEL")
|
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.log_level = env
|
self.log_level = self._get_env("LOG_LEVEL", default="warn", required=False)
|
||||||
|
|
||||||
env = os.getenv("USERS")
|
self.private_key = self._get_env("PRIVATE_KEY", default=None, required=False)
|
||||||
if env != None:
|
if (self.private_key == None):
|
||||||
self.users = env.split(",")
|
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()
|
||||||
|
|
||||||
def __str__(self):
|
_ , 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:
|
template = build_jinja_dict(args)
|
||||||
out = f.read()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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()
|
||||||
|
Loading…
Reference in New Issue
Block a user