Improved Telemt

1) Improved Telemt request processing.
2) Telemt API support.
This commit is contained in:
Public relations & Virtual technology committed 2026-04-16 15:14:42 +03:00
1 parent 8353f3cad7
commit 082c4f398a
6 files changed
+276 -37

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
# syntax=docker/dockerfile:1
FROM python:3.14-slim
WORKDIR /app
# Копируем requirements.txt и устанавливаем зависимости
COPY requirements.txt requirements.txt
RUN pip3 install --no-cache-dir -r requirements.txt
# Копируем остальные файлы проекта
COPY . .
# Команда запуска приложения
CMD ["python3", "app.py"]
+45 -2
View File
@@ -47,7 +47,7 @@ else:
application_path = os.path.dirname(__file__)
DATA_FILE = os.path.join(application_path, 'data.json')
CURRENT_VERSION = "v1.4.0"
CURRENT_VERSION = "v1.4.1"
# ======================== Translations ========================
@@ -505,6 +505,9 @@ class AddConnectionRequest(BaseModel):
telemt_quota: Optional[str] = None
telemt_max_ips: Optional[int] = None
telemt_expiry: Optional[str] = None
telemt_secret: Optional[str] = None
telemt_ad_tag: Optional[str] = None
telemt_max_conns: Optional[int] = None
class EditConnectionRequest(BaseModel):
@@ -513,6 +516,9 @@ class EditConnectionRequest(BaseModel):
telemt_quota: Optional[str] = None
telemt_max_ips: Optional[int] = None
telemt_expiry: Optional[str] = None
telemt_secret: Optional[str] = None
telemt_ad_tag: Optional[str] = None
telemt_max_conns: Optional[int] = None
class ConnectionActionRequest(BaseModel):
@@ -539,6 +545,12 @@ class AddUserRequest(BaseModel):
protocol: Optional[str] = None
connection_name: Optional[str] = None
expiration_date: Optional[str] = None
telemt_quota: Optional[str] = None
telemt_max_ips: Optional[int] = None
telemt_expiry: Optional[str] = None
telemt_secret: Optional[str] = None
telemt_ad_tag: Optional[str] = None
telemt_max_conns: Optional[int] = None
@@ -613,6 +625,9 @@ class AddUserConnectionRequest(BaseModel):
telemt_quota: Optional[str] = None
telemt_max_ips: Optional[int] = None
telemt_expiry: Optional[str] = None
telemt_secret: Optional[str] = None
telemt_ad_tag: Optional[str] = None
telemt_max_conns: Optional[int] = None
class ShareSetupRequest(BaseModel):
@@ -1443,7 +1458,10 @@ async def api_add_connection(request: Request, server_id: int, req: AddConnectio
req.protocol, req.name, server['host'], port,
telemt_quota=req.telemt_quota,
telemt_max_ips=req.telemt_max_ips,
telemt_expiry=req.telemt_expiry
telemt_expiry=req.telemt_expiry,
secret=req.telemt_secret,
user_ad_tag=req.telemt_ad_tag,
max_tcp_conns=req.telemt_max_conns
)
elif req.protocol == 'wireguard':
result = manager.add_client(req.name, server['host'])
@@ -1521,6 +1539,9 @@ async def api_edit_connection(request: Request, server_id: int, req: EditConnect
edit_params['telemt_quota'] = req.telemt_quota
edit_params['telemt_max_ips'] = req.telemt_max_ips
edit_params['telemt_expiry'] = req.telemt_expiry
edit_params['secret'] = req.telemt_secret
edit_params['user_ad_tag'] = req.telemt_ad_tag
edit_params['max_tcp_conns'] = req.telemt_max_conns
result = manager.edit_client(req.protocol, req.client_id, edit_params)
ssh.disconnect()
@@ -1695,6 +1716,17 @@ async def api_add_user(request: Request, req: AddUserRequest):
ssh = get_ssh(server)
ssh.connect()
manager = get_protocol_manager(ssh, req.protocol)
if req.protocol == 'telemt':
conn_result = manager.add_client(
req.protocol, conn_name, server['host'], port,
telemt_quota=req.telemt_quota,
telemt_max_ips=req.telemt_max_ips,
telemt_expiry=req.telemt_expiry,
secret=req.telemt_secret,
user_ad_tag=req.telemt_ad_tag,
max_tcp_conns=req.telemt_max_conns
)
else:
conn_result = manager.add_client(req.protocol, conn_name, server['host'], port)
ssh.disconnect()
@@ -1825,6 +1857,17 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
result = {'client_id': target_client_id, 'config': config}
else:
# Create new client
if req.protocol == 'telemt':
result = await asyncio.to_thread(
manager.add_client, req.protocol, req.name, server['host'], port,
telemt_quota=req.telemt_quota,
telemt_max_ips=req.telemt_max_ips,
telemt_expiry=req.telemt_expiry,
secret=req.telemt_secret,
user_ad_tag=req.telemt_ad_tag,
max_tcp_conns=req.telemt_max_conns
)
else:
result = await asyncio.to_thread(manager.add_client, req.protocol, req.name, server['host'], port)
await asyncio.to_thread(ssh.disconnect)
+3 -3
View File
@@ -9,9 +9,9 @@ services:
- "127.0.0.1:9090:9090"
- "127.0.0.1:9091:9091"
# Allow caching 'proxy-secret' in read-only container
working_dir: /run/telemt
working_dir: /app/conf
volumes:
- ./config.toml:/run/telemt/config.toml:ro
- .:/app/conf:rw
tmpfs:
- /run/telemt:rw,mode=1777,size=1m
environment:
@@ -22,7 +22,7 @@ services:
- ALL
cap_add:
- NET_BIND_SERVICE # allow binding to port 443
read_only: true
read_only: false
security_opt:
- no-new-privileges:true
ulimits:
+109 -31
View File
@@ -125,9 +125,9 @@ class TelemtManager:
self.ssh.upload_file_sudo(dockerfile, f"{remote_dir}/Dockerfile")
results.append("Starting Telemt container...")
out, err, code = self.ssh.run_sudo_command(f"cd {remote_dir} && docker compose up -d --build", timeout=600)
out, err, code = self.ssh.run_sudo_command(f"sh -c 'cd {remote_dir} && docker compose up -d --build'", timeout=600)
if code != 0:
self.ssh.run_sudo_command(f"cd {remote_dir} && docker-compose up -d --build", timeout=600)
self.ssh.run_sudo_command(f"sh -c 'cd {remote_dir} && docker-compose up -d --build'", timeout=600)
return {
"status": "success",
@@ -246,7 +246,7 @@ class TelemtManager:
users[full_name] = secret
return users
def add_client(self, protocol_type, name, host='', port='', telemt_quota=None, telemt_max_ips=None, telemt_expiry=None):
def add_client(self, protocol_type, name, host='', port='', **kwargs):
username = re.sub(r'[^a-zA-Z0-9_.-]', '', name.replace(' ', '_'))
if not username: username = "user_" + uuid.uuid4().hex[:8]
@@ -258,19 +258,54 @@ class TelemtManager:
username = f"{base_username}_{idx}"
idx += 1
secret = secrets.token_hex(16)
secret = kwargs.get('secret') or secrets.token_hex(16)
# 1. Update config file for persistence (but don't restart yet)
config_text = self._insert_into_section(config_text, "access.users", f'{username} = "{secret}"')
if telemt_quota:
config_text = self._insert_into_section(config_text, "access.user_data_quota", f'{username} = {telemt_quota}')
if telemt_max_ips:
config_text = self._insert_into_section(config_text, "access.user_max_unique_ips", f'{username} = {telemt_max_ips}')
if telemt_expiry:
config_text = self._insert_into_section(config_text, "access.user_expirations", f'{username} = "{telemt_expiry}"')
self.save_server_config(protocol_type, config_text)
api_payload = {
"username": username,
"secret": secret
}
if kwargs.get('telemt_quota'):
val = int(kwargs['telemt_quota'])
config_text = self._insert_into_section(config_text, "access.user_data_quota", f'{username} = {val}')
api_payload['data_quota_bytes'] = val
if kwargs.get('telemt_max_ips'):
val = int(kwargs['telemt_max_ips'])
config_text = self._insert_into_section(config_text, "access.user_max_unique_ips", f'{username} = {val}')
api_payload['max_unique_ips'] = val
if kwargs.get('telemt_expiry'):
val = kwargs['telemt_expiry']
config_text = self._insert_into_section(config_text, "access.user_expirations", f'{username} = "{val}"')
api_payload['expiration_rfc3339'] = val
if kwargs.get('user_ad_tag'):
val = kwargs['user_ad_tag']
config_text = self._insert_into_section(config_text, "access.user_ad_tags", f'{username} = "{val}"')
api_payload['user_ad_tag'] = val
if kwargs.get('max_tcp_conns'):
val = int(kwargs['max_tcp_conns'])
config_text = self._insert_into_section(config_text, "access.user_max_tcp_conns", f'{username} = {val}')
api_payload['max_tcp_conns'] = val
# Save config to host
self.ssh.upload_file_sudo(config_text.replace('\r\n', '\n'), "/opt/amnezia/telemt/config.toml")
# 2. Call API for immediate effect
resp = self._api_request("POST", "/v1/users", data=api_payload)
link = f"tg://proxy?server={host}&port={port}&secret={secret}"
# If API returned specialized links, use them
if resp and resp.get('ok') and resp.get('data', {}).get('links'):
links = resp['data']['links']
if links.get('tls'): link = links['tls'][0]
elif links.get('secure'): link = links['secure'][0]
return {
"client_id": username,
"config": link,
@@ -278,19 +313,48 @@ class TelemtManager:
}
def edit_client(self, protocol_type, client_id, new_params):
"""Update existing client parameters in config."""
"""Update existing client parameters via API and in config."""
config_text = self._get_server_config()
api_payload = {}
if 'telemt_quota' in new_params:
config_text = self._update_line_in_section(config_text, "access.user_data_quota", client_id, new_params['telemt_quota'])
if 'telemt_max_ips' in new_params:
config_text = self._update_line_in_section(config_text, "access.user_max_unique_ips", client_id, new_params['telemt_max_ips'])
if 'telemt_expiry' in new_params:
# Expiry is a string with quotes
val = f'"{new_params["telemt_expiry"]}"' if new_params['telemt_expiry'] else None
config_text = self._update_line_in_section(config_text, "access.user_expirations", client_id, val)
val = int(new_params['telemt_quota']) if new_params['telemt_quota'] else None
config_text = self._update_line_in_section(config_text, "access.user_data_quota", client_id, val)
api_payload['data_quota_bytes'] = val
self.save_server_config(protocol_type, config_text)
if 'telemt_max_ips' in new_params:
val = int(new_params['telemt_max_ips']) if new_params['telemt_max_ips'] else None
config_text = self._update_line_in_section(config_text, "access.user_max_unique_ips", client_id, val)
api_payload['max_unique_ips'] = val
if 'telemt_expiry' in new_params:
val = new_params['telemt_expiry']
quoted_val = f'"{val}"' if val else None
config_text = self._update_line_in_section(config_text, "access.user_expirations", client_id, quoted_val)
api_payload['expiration_rfc3339'] = val
if 'secret' in new_params:
val = new_params['secret']
quoted_val = f'"{val}"' if val else None
config_text = self._update_line_in_section(config_text, "access.users", client_id, quoted_val)
api_payload['secret'] = val
if 'user_ad_tag' in new_params:
val = new_params['user_ad_tag']
quoted_val = f'"{val}"' if val else None
config_text = self._update_line_in_section(config_text, "access.user_ad_tags", client_id, quoted_val)
api_payload['user_ad_tag'] = val
if 'max_tcp_conns' in new_params:
val = int(new_params['max_tcp_conns']) if new_params['max_tcp_conns'] else None
config_text = self._update_line_in_section(config_text, "access.user_max_tcp_conns", client_id, val)
api_payload['max_tcp_conns'] = val
# Save config to host
self.ssh.upload_file_sudo(config_text.replace('\r\n', '\n'), "/opt/amnezia/telemt/config.toml")
# API call
self._api_request("PATCH", f"/v1/users/{client_id}", data=api_payload)
return {"status": "success"}
def _update_line_in_section(self, config_text, section_name, client_id, value):
@@ -306,23 +370,18 @@ class TelemtManager:
if section_end == -1: section_end = len(lines)
if section_start == -1:
# Section not found, add it
if value is not None:
lines.append(f"[{section_name}]")
lines.append(f'{client_id} = {value}')
lines.append("")
return '\n'.join(lines)
# Look for client in section
found = False
for i in range(section_start + 1, section_end):
line = lines[i].strip().lstrip('#').strip()
if line.startswith(f"{client_id} ") or line.startswith(f"{client_id}="):
if value is None:
# Remove line if value is None
lines.pop(i)
else:
lines[i] = f'{client_id} = {value}'
if value is None: lines.pop(i)
else: lines[i] = f'{client_id} = {value}'
found = True
break
@@ -347,6 +406,10 @@ class TelemtManager:
return '\n'.join(lines)
def remove_client(self, protocol_type, client_id):
# 1. API
self._api_request("DELETE", f"/v1/users/{client_id}")
# 2. Config
config_text = self._get_server_config()
lines = config_text.split('\n')
new_lines = []
@@ -355,9 +418,13 @@ class TelemtManager:
if stripped.startswith(f"{client_id} ") or stripped.startswith(f"{client_id}="):
continue
new_lines.append(line)
self.save_server_config(protocol_type, '\n'.join(new_lines))
self.ssh.upload_file_sudo('\n'.join(new_lines).replace('\r\n', '\n'), "/opt/amnezia/telemt/config.toml")
def toggle_client(self, protocol_type, client_id, enable, restart=True):
# API doesn't have a direct "toggle", so we either set a huge quota or remove/re-add
# But for Telemt, commenting out in config is the persistent way.
# We'll use HUP after toggling in config.
config_text = self._get_server_config()
lines = config_text.split('\n')
new_lines = []
@@ -373,11 +440,22 @@ class TelemtManager:
line = base_line if enable else f"# {base_line}"
new_lines.append(line)
if restart:
self.save_server_config(protocol_type, '\n'.join(new_lines))
else:
self.ssh.upload_file_sudo('\n'.join(new_lines).replace('\r\n', '\n'), "/opt/amnezia/telemt/config.toml")
if enable:
# If enabling, we re-add via API since it might have been deleted from memory
secret = ""
users = self._parse_users_from_config('\n'.join(new_lines))
secret = users.get(client_id, "")
if secret:
self._api_request("POST", "/v1/users", data={"username": client_id, "secret": secret})
else:
# If disabling, we just delete from memory
self._api_request("DELETE", f"/v1/users/{client_id}")
if restart:
self.ssh.run_sudo_command(f"docker kill -s HUP {self.CONTAINER_NAME} || docker restart {self.CONTAINER_NAME}")
def get_client_config(self, protocol_type, client_id, host='', port=''):
resp = self._api_request("GET", f"/v1/users/{client_id}")
if resp and resp.get('ok'):
+22 -1
View File
@@ -383,6 +383,18 @@
<div id="telemtConnOptions"
style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
<div class="form-group">
<label class="form-label">Secret (Hex 32 chars)</label>
<input class="form-input" type="text" id="telemtSecret" placeholder="Optional (auto-generated if empty)">
</div>
<div class="form-group">
<label class="form-label">AD Tag (Hex 32 chars)</label>
<input class="form-input" type="text" id="telemtAdTag" placeholder="Optional">
</div>
<div class="form-group">
<label class="form-label">Max TCP Conns</label>
<input class="form-input" type="number" id="telemtMaxConns" placeholder="Optional (e.g. 1000)" min="0">
</div>
<div class="form-group">
<label class="form-label">Quota (Bytes)</label>
<input class="form-input" type="number" id="telemtQuota" placeholder="Optional (e.g. 1073741824)"
@@ -1039,6 +1051,9 @@
params.telemt_quota = document.getElementById('telemtQuota').value || null;
params.telemt_max_ips = parseInt(document.getElementById('telemtMaxIps').value) || null;
params.telemt_expiry = document.getElementById('telemtExpiry').value || null;
params.telemt_secret = document.getElementById('telemtSecret').value || null;
params.telemt_ad_tag = document.getElementById('telemtAdTag').value || null;
params.telemt_max_conns = parseInt(document.getElementById('telemtMaxConns').value) || null;
if (isEditingConn) params.client_id = editingClientId;
}
const result = await apiCall(isEditingConn ? `/api/servers/${SERVER_ID}/connections/edit` : `/api/servers/${SERVER_ID}/connections/add`, 'POST', params);
@@ -1090,12 +1105,18 @@
if (isEdit && clientData) {
editingClientId = clientData.clientName;
document.getElementById('telemtQuota').value = clientData.quota || '';
document.getElementById('telemtMaxIps').value = clientData.active_ips || ''; // actually max_ips isn't stored in api data yet, assume it was active_ips for now or better - just empty
document.getElementById('telemtMaxIps').value = '';
document.getElementById('telemtExpiry').value = clientData.expiry || '';
document.getElementById('telemtSecret').value = clientData.token || '';
document.getElementById('telemtAdTag').value = clientData.user_ad_tag || '';
document.getElementById('telemtMaxConns').value = clientData.max_tcp_conns || '';
} else {
document.getElementById('telemtQuota').value = '';
document.getElementById('telemtMaxIps').value = '';
document.getElementById('telemtExpiry').value = '';
document.getElementById('telemtSecret').value = '';
document.getElementById('telemtAdTag').value = '';
document.getElementById('telemtMaxConns').value = '';
}
} else {
telemtOpts.style.display = 'none';
+81
View File
@@ -145,6 +145,33 @@
placeholder="{{ _('connection_name_placeholder') }}">
</div>
<div id="newUserTelemtOptions" style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
<div class="form-group">
<label class="form-label">Secret (Hex 32 chars)</label>
<input class="form-input" type="text" id="newUserTelemtSecret" placeholder="Optional">
</div>
<div class="form-group">
<label class="form-label">AD Tag (Hex 32 chars)</label>
<input class="form-input" type="text" id="newUserTelemtAdTag" placeholder="Optional">
</div>
<div class="form-group">
<label class="form-label">Max TCP Conns</label>
<input class="form-input" type="number" id="newUserTelemtMaxConns" placeholder="Optional" min="0">
</div>
<div class="form-group">
<label class="form-label">Quota (Bytes)</label>
<input class="form-input" type="number" id="newUserTelemtQuota" placeholder="Optional" min="0">
</div>
<div class="form-group">
<label class="form-label">Max Unique IPs</label>
<input class="form-input" type="number" id="newUserTelemtMaxIps" placeholder="Optional" min="1">
</div>
<div class="form-group">
<label class="form-label">Expiration Date (ISO 8601)</label>
<input class="form-input" type="text" id="newUserTelemtExpiry" placeholder="e.g. 2026-12-31T23:59:59Z">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('addUserModal')">{{ _('cancel')
}}</button>
@@ -261,6 +288,33 @@
<input class="form-input" type="text" id="ucName" placeholder="{{ _('connection_name_placeholder') }}">
</div>
<div id="ucTelemtOptions" style="display:none; padding: var(--space-md); background: rgba(0,0,0,0.03); border-radius: var(--radius-md); margin-bottom: var(--space-md);">
<div class="form-group">
<label class="form-label">Secret (Hex 32 chars)</label>
<input class="form-input" type="text" id="ucTelemtSecret" placeholder="Optional">
</div>
<div class="form-group">
<label class="form-label">AD Tag (Hex 32 chars)</label>
<input class="form-input" type="text" id="ucTelemtAdTag" placeholder="Optional">
</div>
<div class="form-group">
<label class="form-label">Max TCP Conns</label>
<input class="form-input" type="number" id="ucTelemtMaxConns" placeholder="Optional" min="0">
</div>
<div class="form-group">
<label class="form-label">Quota (Bytes)</label>
<input class="form-input" type="number" id="ucTelemtQuota" placeholder="Optional" min="0">
</div>
<div class="form-group">
<label class="form-label">Max Unique IPs</label>
<input class="form-input" type="number" id="ucTelemtMaxIps" placeholder="Optional" min="1">
</div>
<div class="form-group">
<label class="form-label">Expiration Date (ISO 8601)</label>
<input class="form-input" type="text" id="ucTelemtExpiry" placeholder="e.g. 2026-12-31T23:59:59Z">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('addUserConnModal')">{{ _('cancel') }}</button>
<button class="btn btn-primary" onclick="doAddUserConnection()" id="ucAddBtn">
@@ -509,8 +563,19 @@
const show = e.target.value !== '';
updateProtocols(e.target.value, 'newUserProtocol', 'newUserProtoGroup');
document.getElementById('newUserConnNameGroup').style.display = show ? '' : 'none';
updateTelemtOptions('newUserProtocol', 'newUserTelemtOptions');
});
document.getElementById('newUserProtocol').addEventListener('change', (e) => {
updateTelemtOptions('newUserProtocol', 'newUserTelemtOptions');
});
function updateTelemtOptions(protoSelectId, optionsId) {
const proto = document.getElementById(protoSelectId).value;
const options = document.getElementById(optionsId);
if (options) options.style.display = (proto === 'telemt') ? 'block' : 'none';
}
document.getElementById('ucServer').addEventListener('change', (e) => {
updateProtocols(e.target.value, 'ucProtocol');
if (document.getElementById('ucMode').value === 'existing') {
@@ -522,6 +587,7 @@
if (document.getElementById('ucMode').value === 'existing') {
fetchExistingClients();
}
updateTelemtOptions('ucProtocol', 'ucTelemtOptions');
});
async function addUser(e) {
@@ -549,6 +615,14 @@
body.server_id = parseInt(serverId);
body.protocol = document.getElementById('newUserProtocol').value;
body.connection_name = document.getElementById('newUserConnName').value || null;
if (body.protocol === 'telemt') {
body.telemt_quota = document.getElementById('newUserTelemtQuota').value || null;
body.telemt_max_ips = parseInt(document.getElementById('newUserTelemtMaxIps').value) || null;
body.telemt_expiry = document.getElementById('newUserTelemtExpiry').value || null;
body.telemt_secret = document.getElementById('newUserTelemtSecret').value || null;
body.telemt_ad_tag = document.getElementById('newUserTelemtAdTag').value || null;
body.telemt_max_conns = parseInt(document.getElementById('newUserTelemtMaxConns').value) || null;
}
}
const result = await apiCall('/api/users/add', 'POST', body);
showToast(_('success'), 'success');
@@ -669,6 +743,13 @@
const clientId = document.getElementById('ucExistingClient').value;
if (!clientId) throw new Error(_('select_connection'));
body.client_id = clientId;
} else if (body.protocol === 'telemt') {
body.telemt_quota = document.getElementById('ucTelemtQuota').value || null;
body.telemt_max_ips = parseInt(document.getElementById('ucTelemtMaxIps').value) || null;
body.telemt_expiry = document.getElementById('ucTelemtExpiry').value || null;
body.telemt_secret = document.getElementById('ucTelemtSecret').value || null;
body.telemt_ad_tag = document.getElementById('ucTelemtAdTag').value || null;
body.telemt_max_conns = parseInt(document.getElementById('ucTelemtMaxConns').value) || null;
}
const result = await apiCall(`/api/users/${currentUserId}/connections/add`, 'POST', body);