Skip to content

Commit e6c6e90

Browse files
committed
private key
1 parent 63e83e3 commit e6c6e90

File tree

3 files changed

+79
-89
lines changed

3 files changed

+79
-89
lines changed

README.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,23 @@ d2ray is a single Docker container that provides easy 5-minute setups and braind
66
## Quickstart
77
1. You can start with the example `docker-compose.yml` from this repo.
88
2. Adjust environment variables:
9-
- `PORT`: the port Xray listens on.
10-
- `TARGET_HOST`: the target host to redirect non proxy connections.
11-
- `TARGET_PORT`: the target port to redirect non proxy connections.
12-
- `TARGET_SNI`: comma separated list of the target website's SNIs.
13-
- `USERS`: comma separated list of usernames that can access Xray.
14-
- `LOG_LEVEL`: the verbosity of Xray logs. Default: `warn`.
9+
- `PORT`: the port Xray listens on. `Optional, default = 443`.
10+
- `TARGET_HOST`: the target host to redirect non proxy connections. `Required`.
11+
- `TARGET_PORT`: the target port to redirect non proxy connections. `Optional, default = 443`.
12+
- `TARGET_SNI`: comma separated list of the target website's SNIs. `Required`.
13+
- `PRIVATE_KEY` : server's private key. `Optional`.
14+
- `USERS`: comma separated list of usernames that can access Xray. `Required`.
15+
- `LOG_LEVEL`: the verbosity of Xray logs. `Optional, default = warn`.
1516
3. `docker compose up -d`
1617
4. Test your connection.
1718

1819
## Docker Volume
19-
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`.
20+
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.
2021

2122
## Key Generation
22-
d2ray checks whether a key file exists at path `/etc/xray/certs/keys` and generates a new key pair if not found.
23+
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.
2324

24-
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.
25-
26-
If you are generating the keys yourself, the key file must contain exactly the output of `xray x25519`.
25+
To make d2ray regenerate a new key pair, manually delete the key file `/etc/xray/certs/keys` from the mounted volume.
2726

2827
## How To Update?
2928
- `docker compose down`

docker-compose.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ services:
1010
- 8443:8443
1111
environment:
1212
- PORT=8443
13-
- TARGET_HOST=example.com
13+
- TARGET_HOST=www.apple.com
1414
- TARGET_PORT=443
15-
- TARGET_SNI=www.example.com,example.com
16-
- USERS=exampleuser1,exampleuser2
15+
- TARGET_SNI=www.apple.com,apple.com
16+
- USERS=alice,bob,eve
17+
- PRIVATE_KEY=KE5MCI5e395fub55O1lsNPzvWw9nNAyCaecRSp3BvHg # Do NOT use this random key
1718
- LOG_LEVEL=warn
1819
restart: unless-stopped
1920
networks:

opt/init.py

Lines changed: 65 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -16,51 +16,76 @@ class d2args:
1616
target_host : str
1717
target_sni : str
1818
log_level : str
19+
private_key : str
20+
public_key : str
1921
users : list[str]
2022
def __init__(self) -> None:
21-
self.port = 443
22-
self.target_host = "localhost"
23-
self.target_port = 443
24-
self.target_sni = "localhost"
25-
self.log_level = "warn"
26-
self.users = [''.join(random.choices(string.ascii_uppercase + string.digits, k=24))]
27-
28-
def from_env(self) -> None:
29-
env = os.getenv("PORT")
30-
if env != None:
31-
self.port = int(env)
32-
33-
env = os.getenv("TARGET_PORT")
34-
if env != None:
35-
self.target_port = int(env)
36-
37-
env = os.getenv("TARGET_SNI")
38-
if env != None:
39-
self.target_sni = env.split(",")
23+
self._from_env()
24+
25+
@staticmethod
26+
def _get_env(name : str, default : str = None, required : bool = True) -> str:
27+
env = os.getenv(name)
28+
if env == None:
29+
if required:
30+
raise Exception(f"Missing environment variable \"{name}\".")
31+
else:
32+
return default
33+
return env
34+
35+
@staticmethod
36+
def _parse_xray_x25519_output(stdout : str) -> tuple[str, str]:
37+
skey = None
38+
pkey = None
39+
lines = stdout.split("\n")
40+
if len(lines) < 2:
41+
raise Exception(f"Unknown Xray output format:\n\"{stdout}\"\n")
4042

41-
env = os.getenv("TARGET_HOST")
42-
if env != None:
43-
self.target_host = env
44-
45-
env = os.getenv("LOG_LEVEL")
46-
if env != None:
47-
self.log_level = env
48-
49-
env = os.getenv("USERS")
50-
if env != None:
51-
self.users = env.split(",")
52-
53-
def __str__(self):
43+
priv_key_hdr = "Private key: "
44+
pub_key_hdr = "Public key: "
45+
for line in lines:
46+
if line.startswith(priv_key_hdr):
47+
skey = line[len(priv_key_hdr):]
48+
elif line.startswith(pub_key_hdr):
49+
pkey = line[len(pub_key_hdr):]
50+
if (skey == None) or (pkey == None):
51+
raise Exception(f"Unable to extract private or public key from Xray output:\n\"{stdout}\"\n")
52+
return (skey.strip(), pkey.strip())
53+
54+
def _from_env(self) -> None:
55+
self.target_host = self._get_env("TARGET_HOST")
56+
self.target_sni = self._get_env("TARGET_SNI").split(",")
57+
self.users = self._get_env("USERS").split(",")
58+
59+
self.port = int(self._get_env("PORT", default="443", required=False))
60+
self.target_port = int(self._get_env("TARGET_PORT", default="443", required=False))
61+
self.log_level = self._get_env("LOG_LEVEL", default="warn", required=False)
62+
63+
self.private_key = self._get_env("PRIVATE_KEY", default=None, required=False)
64+
if (self.private_key == None):
65+
print(f"Private key not provided.", flush=True)
66+
if not KEY_FILE.exists():
67+
print(f"Key file {KEY_FILE} not found. Generating new keys...")
68+
self.private_key, _ = self._parse_xray_x25519_output(subprocess.check_output(f"{XRAY_BIN} x25519", shell = True).decode())
69+
with open(KEY_FILE, "w") as f:
70+
f.write(self.private_key)
71+
else:
72+
print(f"Reading from key file {KEY_FILE} ...")
73+
with open(KEY_FILE, "r") as f:
74+
self.private_key = f.read().strip()
75+
76+
_ , self.public_key = self._parse_xray_x25519_output(subprocess.check_output(f"{XRAY_BIN} x25519 -i {self.private_key}", shell = True).decode())
77+
78+
def __str__(self) -> str:
5479
ret = (f"Port: {self.port}\n"
5580
f"Target Port: {self.target_port}\n"
5681
f"Target Host: {self.target_host}\n"
5782
f"Target SNI: {', '.join(self.target_sni)}\n"
5883
f"Log Level: {self.log_level}\n"
59-
f"Users: {', '.join(self.users)}"
84+
f"Users: {', '.join(self.users)}\n"
85+
f"Public Key: {self.public_key}"
6086
)
6187
return ret
6288

63-
6489
def process_directory(path : str, vars : dict[str, str], delete_template : bool = True) -> None:
6590
for f in os.listdir(path):
6691
full_path = os.path.join(path, f)
@@ -81,7 +106,7 @@ def build_target_snis(snis : list[str]) -> str:
81106
def build_users_json(users: list[str]) -> str:
82107
return ', '.join(["{\"id\": \"" + item + "\", \"flow\": \"xtls-rprx-vision\"}" for item in users])
83108

84-
def build_jinja_dict(args : d2args, skey : str) -> dict[str, str]:
109+
def build_jinja_dict(args : d2args) -> dict[str, str]:
85110
jinja_dict : dict[str,str] = dict()
86111
jinja_dict["PORT"] = str(args.port)
87112

@@ -93,56 +118,21 @@ def build_jinja_dict(args : d2args, skey : str) -> dict[str, str]:
93118
jinja_dict["LOG_LEVEL"] = args.log_level
94119

95120
jinja_dict["USERS"] = build_users_json(args.users)
96-
jinja_dict["PRIVATE_KEY"] = skey
97-
121+
jinja_dict["PRIVATE_KEY"] = args.private_key
98122
return jinja_dict
99123

100-
def parse_xray_x25519_output(stdout : str) -> tuple[str, str]:
101-
skey = None
102-
pkey = None
103-
lines = stdout.split("\n")
104-
if len(lines) < 2:
105-
raise Exception(f"Unknown Xray output format:\n\"{stdout}\"\n")
106-
107-
priv_key_hdr = "Private key: "
108-
pub_key_hdr = "Public key: "
109-
for line in lines:
110-
if line.startswith(priv_key_hdr):
111-
skey = line[len(priv_key_hdr):]
112-
elif line.startswith(pub_key_hdr):
113-
pkey = line[len(pub_key_hdr):]
114-
if (skey == None) or (pkey == None):
115-
raise Exception(f"Unable to extract private or public key from Xray output:\n\"{stdout}\"\n")
116-
return (skey.strip(), pkey.strip())
117124

118125
def main():
119126
print(f"Initializing d2ray...", flush=True)
120127
args = d2args()
121-
args.from_env()
122-
123-
print(f"Checking key file...", flush=True)
124-
if not KEY_FILE.exists():
125-
print(f"Key file not found at {KEY_FILE}. Generating...")
126-
out = subprocess.check_output(f"{XRAY_BIN} x25519", shell = True).decode()
127-
with open(KEY_FILE, "w") as f:
128-
f.write(out)
129-
130-
with open(KEY_FILE, "r") as f:
131-
out = f.read()
132-
133-
print(f"Reading keys...", flush=True)
134-
skey, pkey = parse_xray_x25519_output(out)
135128

136-
print(f"Verifying public key...", flush=True)
137-
_, _pkey = parse_xray_x25519_output(subprocess.check_output(f"{XRAY_BIN} x25519 -i {skey}", shell = True).decode())
138-
if (_pkey != pkey):
139-
print(f"Unmatching public key: expected \"{_pkey}\" but key file provided \"{pkey}\". Please verify the key file.", flush=True)
129+
print(f"\nConfiguration:\n{str(args)}\n", flush=True)
140130

141-
print(f"\nConfigurations:\n{str(args)}\nPublic key: {pkey}\n", flush=True)
142-
143-
template = build_jinja_dict(args, skey)
131+
template = build_jinja_dict(args)
144132

145133
print(f"Processing config files...", flush=True)
146134
process_directory("/opt/xray", template)
147135

136+
print(f"Initialization completed.\n", flush=True)
137+
148138
main()

0 commit comments

Comments
 (0)