Compare commits

..

11 Commits

Author SHA1 Message Date
mercury 8398ecf736 edit announce 2025-11-08 19:24:39 +04:00
mercury 0d14061edb Merge branch 'dev' of github.com:mercurykd/vpnbot into dev 2025-11-08 19:23:05 +04:00
mercury 03218b974d download vless button 2025-11-08 19:20:50 +04:00
Konstantin 84ac1200dc Merge pull request #41 from legiz-ru/dev
Add HWID device limit support for sub page
2025-09-25 11:00:13 +04:00
legiz-ru aa64a48db2 delete overhead mkdir0777 2025-09-25 09:52:17 +03:00
legiz-ru 70c83da66a add hwid to backup 2025-09-23 20:44:42 +03:00
legiz-ru 52ff056474 fix: hwid ignore for browsers 2025-09-23 14:21:41 +03:00
legiz-ru 225260044a Add HWID device limit support for sub page
based on standart by remnawave: https://remna.st/docs/features/hwid-device-limit
2025-09-23 13:50:03 +03:00
mercury 050d1ee3eb fix start_io 2025-09-11 16:46:12 +04:00
mercury 6979cca602 iodine 2025-09-11 16:22:27 +04:00
mercury 7fc24ea4f4 add suburl, expire to subs page 2025-08-31 00:18:00 +04:00
10 changed files with 753 additions and 11 deletions
+1
View File
@@ -10,6 +10,7 @@
/sftp-config.json
/tg.pyr
/config/wg0.conf
/config/hwid.json
/todo.todo
/app/zapretlists/*
!/app/zapretlists/.gitkeep
+676 -9
View File
@@ -14,6 +14,7 @@ class Bot
public $logs;
public $reg;
public $pool;
public $hwid;
public function __construct($key, $i18n)
{
@@ -31,6 +32,7 @@ class Bot
$this->limit = $this->getPacConf()['limitpage'] ?: 5;
$this->adguard = '/config/AdGuardHome.yaml';
$this->update = '/update/json';
$this->hwid = '/config/hwid.json';
$this->logs = [
'nginx_default_access',
'nginx_domain_access',
@@ -168,6 +170,30 @@ class Bot
case preg_match('~^/setIpLimit$~', $this->input['callback'], $m):
$this->setIpLimit();
break;
case preg_match('~^/hwidLimit$~', $this->input['callback'], $m):
$this->hwidLimit();
break;
case preg_match('~^/toggleHwidLimit(?: (\w+))?$~', $this->input['callback'], $m):
$this->toggleHwidLimit($m[1] ?? null);
break;
case preg_match('~^/setHwidDevices(?: (\w+))?$~', $this->input['callback'], $m):
$this->setHwidDevices($m[1] ?? null);
break;
case preg_match('~^/hwidUser (\d+)(?:_(\d+))?$~', $this->input['callback'], $m):
$this->hwidUser($m[1], $m[2] ?? 0);
break;
case preg_match('~^/hwidUserToggle (\d+)$~', $this->input['callback'], $m):
$this->hwidUserToggle($m[1]);
break;
case preg_match('~^/hwidUserDefault (\d+)$~', $this->input['callback'], $m):
$this->hwidUserDefault($m[1]);
break;
case preg_match('~^/setHwidUserLimit (\d+)$~', $this->input['callback'], $m):
$this->setHwidUserLimit($m[1]);
break;
case preg_match('~^/hwidUserDel (\d+)_(\d+) (.+)$~', $this->input['callback'], $m):
$this->hwidUserDel($m[1], $m[2], $m[3]);
break;
case preg_match('~^/searchLogs (.+)$~', $this->input['message'], $m):
$this->searchLogs($m[1]);
break;
@@ -291,6 +317,21 @@ class Bot
case preg_match('~^/logs$~', $this->input['callback'], $m):
$this->logs();
break;
case preg_match('~^/iodine$~', $this->input['callback'], $m):
$this->iodine();
break;
case preg_match('~^/iodineDomain$~', $this->input['callback'], $m):
$this->iodineDomain();
break;
case preg_match('~^/iodinePassword$~', $this->input['callback'], $m):
$this->iodinePassword();
break;
case preg_match('~^/setIodineDomain (\w+)$~', $this->input['callback'], $m):
$this->setIodineDomain($m[1]);
break;
case preg_match('~^/setIodinePassword (\w+)$~', $this->input['callback'], $m):
$this->setIodinePassword($m[1]);
break;
case preg_match('~^/getLog (?P<arg>\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m):
$this->getLog(...explode('_', $m['arg']));
break;
@@ -432,6 +473,9 @@ class Bot
case preg_match('~^/download (\d+)$~', $this->input['callback'], $m):
$this->downloadPeer($m[1]);
break;
case preg_match('~^/dw (\w+) (\w+)$~', $this->input['callback'], $m):
$this->dw($m[1], $m[2]);
break;
case preg_match('~^/deloc (\d+)$~', $this->input['callback'], $m):
$this->deloc($m[1]);
break;
@@ -1691,6 +1735,7 @@ class Bot
'wg1' => $wg1,
'ad' => yaml_parse_file($this->adguard),
'pac' => $this->getPacConf(),
'hwid' => file_exists($this->hwid) ? (json_decode(file_get_contents($this->hwid), true) ?: []) : [],
'ssl' => file_exists('/certs/cert_private') && preg_match('~BEGIN PRIVATE KEY~', file_get_contents('/certs/cert_private')) ? [
'private' => file_get_contents('/certs/cert_private'),
'public' => file_get_contents('/certs/cert_public'),
@@ -1807,6 +1852,13 @@ class Bot
file_put_contents('/config/mtprotodomain', $json['mtprotodomain'] ?: '');
$this->restartTG();
}
// hwid
if (array_key_exists('hwid', $json)) {
$out[] = 'update hwid devices';
$this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out));
$data = is_array($json['hwid']) ? $json['hwid'] : [];
file_put_contents($this->hwid, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
// xray
if (!empty($json['xray'])) {
$out[] = 'update xray';
@@ -1849,6 +1901,15 @@ class Bot
}
}
public function dw($u, $t)
{
$c = $this->getXray()['inbounds'][0]['settings']['clients'][$u];
$_GET['s'] = $c['id'];
$_GET['t'] = $t;
$conf = $this->subscription(1);
$this->sendFile($this->input['from'], new CURLStringFile($conf, $c['email'] . ($t == 'cl' ? '_mihomo.yaml' :($t == 'si' ? '_singbox.json' : '_v2ray.json'))));
}
public function downloadPeer($client)
{
$cl = $client;
@@ -4347,6 +4408,104 @@ DNS-over-HTTPS with IP:
return implode("\n", $result);
}
public function iodineDomain()
{
$r = $this->send(
$this->input['chat'],
"@{$this->input['username']} enter domain",
$this->input['message_id'],
reply: 'enter domain',
);
$_SESSION['reply'][$r['result']['message_id']] = [
'start_message' => $this->input['message_id'],
'callback' => 'setIodineDomain',
'args' => [],
];
}
public function iodinePassword()
{
$r = $this->send(
$this->input['chat'],
"@{$this->input['username']} enter password",
$this->input['message_id'],
reply: 'enter password',
);
$_SESSION['reply'][$r['result']['message_id']] = [
'start_message' => $this->input['message_id'],
'callback' => 'setIodinePassword',
'args' => [],
];
}
public function setIodinePassword($text)
{
$c = $this->getPacConf();
if ($text) {
$c['iodinePassword'] = $text;
} else {
unset($c['iodinePassword']);
}
$this->setPacConf($c);
$this->iodineRestart();
$this->iodine();
}
public function setIodineDomain($text)
{
$c = $this->getPacConf();
if ($text) {
$c['iodineDomain'] = $text;
} else {
unset($c['iodineDomain']);
}
$this->setPacConf($c);
$this->iodineRestart();
$this->iodine();
}
public function iodineRestart()
{
$c = $this->getPacConf();
$this->ssh('pkill iodine', 'io');
if (!empty($c['iodineDomain']) && !empty($c['iodinePassword'])) {
$this->ssh("iodined -c -P {$c['iodinePassword']} 10.0.0.1 {$c['iodineDomain']}", 'io');
}
}
public function iodine()
{
$c = $this->getPacConf();
$text[] = 'Iodine';
$text[] = "domain: {$c['iodineDomain']}";
$text[] = "password: {$c['iodinePassword']}";
$data[] = [
[
'text' => $this->i18n('set domain'),
'callback_data' => "/iodineDomain",
],
];
$data[] = [
[
'text' => $this->i18n('set password'),
'callback_data' => "/iodinePassword",
],
];
$data[] = [
[
'text' => $this->i18n('back'),
'callback_data' => "/menu",
],
];
$this->update(
$this->input['chat'],
$this->input['message_id'],
implode("\n", $text),
$data ?: false,
);
}
public function menu($type = false, $arg = false, $return = false)
{
$conf = $this->getPacConf();
@@ -4402,6 +4561,7 @@ DNS-over-HTTPS with IP:
$this->i18n($this->ssh('pgrep mtproto-proxy', 'tg') ? 'on' : 'off') . ' ' . $this->i18n('mtproto'),
$this->i18n(exec("JSON=1 timeout 2 dnslookup google.com ad") ? 'on' : 'off') . ' ' . $this->i18n('ad_title'),
$this->i18n($this->ssh('pgrep ssserver', 'ss') ? 'on' : 'off') . ' ' . $this->i18n('sh_title'),
$this->i18n($this->ssh('pgrep iodine', 'io') ? 'on' : 'off') . ' ' . $this->i18n('Iodine'),
$this->i18n($this->warpStatus()) . ' ' . $this->i18n('warp'),
],
[
@@ -4413,6 +4573,7 @@ DNS-over-HTTPS with IP:
$this->i18n($c['tg'] ? 'on' : 'off') . ' ' . getenv('TGPORT'),
$this->i18n($c['ad'] ? 'on' : 'off') . ' 853',
$this->i18n($c['ss'] ? 'on' : 'off') . ' ' . getenv('SSPORT'),
$this->i18n($c['io'] ? 'on' : 'off') . ' 53',
'',
],
]);
@@ -4486,6 +4647,12 @@ DNS-over-HTTPS with IP:
'callback_data' => "/pacMenu 0",
],
],
[
[
'text' => $this->i18n('Iodine'),
'callback_data' => "/iodine",
],
],
[
[
'text' => $this->i18n('config'),
@@ -4597,6 +4764,263 @@ DNS-over-HTTPS with IP:
$this->xray();
}
public function hwidLimit()
{
$pac = $this->getPacConf();
$enabled = !empty($pac['hwid_limit_enabled']);
$count = max(1, (int) ($pac['hwid_device_count'] ?: 1));
$text[] = 'Settings -> ' . $this->i18n('hwid limit');
$text[] = $this->i18n('hwid notice');
$text[] = $this->i18n('hwid limit') . ': ' . ($enabled ? $count : $this->i18n('off'));
$data[] = [
[
'text' => $this->i18n($enabled ? 'on' : 'off'),
'callback_data' => '/toggleHwidLimit',
],
];
$data[] = [
[
'text' => $this->i18n('set hwid devices count') . ': ' . $count,
'callback_data' => '/setHwidDevices',
],
];
$data[] = [
[
'text' => $this->i18n('back'),
'callback_data' => '/xray',
],
];
$this->update(
$this->input['chat'],
$this->input['message_id'],
implode("\n", $text ?: ['...']),
$data ?: false,
);
}
public function toggleHwidLimit($context = null)
{
$pac = $this->getPacConf();
$pac['hwid_limit_enabled'] = $pac['hwid_limit_enabled'] ? 0 : 1;
if (!empty($pac['hwid_limit_enabled']) && empty($pac['hwid_device_count'])) {
$pac['hwid_device_count'] = 1;
}
$this->setPacConf($pac);
$this->answer($this->input['callback_id'], $this->i18n('hwid notice'), true);
if ($context === 'xray') {
$this->xray();
} else {
$this->hwidLimit();
}
}
public function setHwidDevices($context = null)
{
$r = $this->send(
$this->input['chat'],
"@{$this->input['username']} enter hwid devices count",
$this->input['message_id'],
reply: 'enter hwid devices count',
);
$_SESSION['reply'][$r['result']['message_id']] = [
'start_message' => $this->input['message_id'],
'callback' => 'saveHwidDevices',
'args' => [$context],
];
}
public function saveHwidDevices($count, $context = null)
{
$count = (int) $count;
if ($count <= 0) {
$count = 1;
}
$pac = $this->getPacConf();
$pac['hwid_device_count'] = $count;
$this->setPacConf($pac);
$this->send($this->input['chat'], $this->i18n('hwid notice'), $this->input['message_id']);
if ($context === 'xray') {
$this->xray();
} else {
$this->hwidLimit();
}
}
public function getHwidStorage()
{
if (!file_exists($this->hwid)) {
return [];
}
$data = json_decode(file_get_contents($this->hwid), true);
return is_array($data) ? $data : [];
}
public function setHwidStorage(array $storage)
{
file_put_contents($this->hwid, json_encode($storage, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
public function getHwidDevicesByUser($uid)
{
$storage = $this->getHwidStorage();
return $storage[$uid] ?? [];
}
public function setHwidDevice($uid, $hwid, array $info)
{
$storage = $this->getHwidStorage();
$storage[$uid][$hwid] = $info;
$this->setHwidStorage($storage);
}
public function deleteHwidDevice($uid, $hwid)
{
$storage = $this->getHwidStorage();
if (isset($storage[$uid][$hwid])) {
unset($storage[$uid][$hwid]);
if (empty($storage[$uid])) {
unset($storage[$uid]);
}
$this->setHwidStorage($storage);
}
}
public function deleteHwidUser($uid)
{
$storage = $this->getHwidStorage();
if (isset($storage[$uid])) {
unset($storage[$uid]);
$this->setHwidStorage($storage);
}
}
protected function getHwidTokenScope($index)
{
return ($this->input['chat'] ?? 'global') . ':' . $index;
}
protected function rememberHwidToken($scope, $hwid)
{
if (!isset($_SESSION['hwidTokens'])) {
$_SESSION['hwidTokens'] = [];
}
if (!isset($_SESSION['hwidTokens'][$scope])) {
$_SESSION['hwidTokens'][$scope] = [];
}
do {
try {
$token = bin2hex(random_bytes(5));
} catch (\Throwable $e) {
$token = substr(hash('sha256', $hwid . microtime(true)), 0, 10);
}
} while (isset($_SESSION['hwidTokens'][$scope][$token]));
$_SESSION['hwidTokens'][$scope][$token] = $hwid;
return $token;
}
protected function resolveHwidToken($scope, $token)
{
if (isset($_SESSION['hwidTokens'][$scope][$token])) {
$hwid = $_SESSION['hwidTokens'][$scope][$token];
unset($_SESSION['hwidTokens'][$scope][$token]);
return $hwid;
}
$decoded = base64_decode($token, true);
return $decoded !== false ? $decoded : '';
}
public function processHwidRequest(array $client)
{
$pac = $this->getPacConf();
if (empty($pac['hwid_limit_enabled']) || !empty($client['hwid_disabled'])) {
return true;
}
$limit = (int) ($client['hwid_limit'] ?: ($pac['hwid_device_count'] ?: 0));
if ($limit <= 0) {
return true;
}
$devices = $this->getHwidDevicesByUser($client['id']);
$hwid = trim($_SERVER['HTTP_X_HWID'] ?? '');
$isBrowser = $this->isBrowserRequest();
if ($hwid === '') {
if ($isBrowser) {
return true;
}
$message = 'HWID device limit exceeded';
header('announce: base64:' . base64_encode($message));
header('X-HWID-Status: ' . $message);
header('HTTP/1.1 429 Too Many Requests', true, 429);
return false;
}
$isNew = !isset($devices[$hwid]);
if ($isNew && count($devices) >= $limit) {
$message = 'HWID device limit exceeded';
header('announce: base64:' . base64_encode($message));
header('X-HWID-Status: ' . $message);
header('HTTP/1.1 429 Too Many Requests', true, 429);
return false;
}
$this->setHwidDevice($client['id'], $hwid, [
'time' => time(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'device_os' => $_SERVER['HTTP_X_DEVICE_OS'] ?? '',
'os_version' => $_SERVER['HTTP_X_VER_OS'] ?? '',
'device_model' => $_SERVER['HTTP_X_DEVICE_MODEL'] ?? '',
]);
return true;
}
protected function isBrowserRequest()
{
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
if ($userAgent === '' && $accept === '') {
return false;
}
$browserPatterns = [
'Mozilla/',
'Chrome/',
'Safari/',
'Firefox/',
'Edge/',
'Edg/',
'MSIE ',
'Trident/',
'Opera/',
'OPR/',
];
foreach ($browserPatterns as $pattern) {
if (stripos($userAgent, $pattern) !== false) {
return true;
}
}
if (stripos($accept, 'text/html') !== false) {
return true;
}
return false;
}
public function switchSilence()
{
$c = $this->getPacConf();
@@ -5432,6 +5856,7 @@ DNS-over-HTTPS with IP:
$st = $this->getXrayStats();
foreach ($r['inbounds'][0]['settings']['clients'] as $k => $v) {
if ($i == $k) {
$this->deleteHwidUser($r['inbounds'][0]['settings']['clients'][$k]['id']);
unset($r['inbounds'][0]['settings']['clients'][$k]);
unset($st['users'][$k]);
$this->setXrayStats($st);
@@ -5903,13 +6328,25 @@ DNS-over-HTTPS with IP:
'callback_data' => "/changeTransport 1",
],
];
$ip_count = $p['ip_count'] ?: 1;
$ip_count = $p['ip_count'] ?: 1;
$hwidEnabled = !empty($p['hwid_limit_enabled']);
$defaultHwids = max(1, (int) ($p['hwid_device_count'] ?: 1));
$data[] = [
[
'text' => $this->i18n('ip limit') . ' ' . ($p['ip_limit'] ? ": {$p['ip_limit']} sec & $ip_count" : $this->i18n('off')),
'callback_data' => "/setIpLimit",
],
];
$data[] = [
[
'text' => $this->i18n('hwid limit') . ': ' . $this->i18n($hwidEnabled ? 'on' : 'off') . " ({$defaultHwids})",
'callback_data' => '/toggleHwidLimit xray',
],
[
'text' => $this->i18n('set hwid devices count'),
'callback_data' => '/setHwidDevices xray',
],
];
if ($p['transport'] == 'Reality') {
$data[] = [
[
@@ -6295,12 +6732,18 @@ DNS-over-HTTPS with IP:
public function userXr($i)
{
$c = $this->getXray()['inbounds'][0]['settings']['clients'][$i];
$xray = $this->getXray();
$c = $xray['inbounds'][0]['settings']['clients'][$i];
$pac = $this->getPacConf();
$domain = $this->getDomain($pac['transport'] != 'Reality');
$scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https';
$hash = $this->getHashBot();
$devices = $this->getHwidDevicesByUser($c['id']);
$hwidEnabled = !empty($pac['hwid_limit_enabled']) && empty($c['hwid_disabled']);
$defaultHwid = max(1, (int) ($pac['hwid_device_count'] ?: 1));
$hwidLimit = $c['hwid_limit'] ? (int) $c['hwid_limit'] : $defaultHwid;
$text[] = "Menu -> " . $this->i18n('xray') . " -> {$c['email']}\n";
if (file_exists(__DIR__ . '/subscription.php')) {
$text[] = "<a href='$scheme://{$domain}/pac$hash/sub?id={$c['id']}'>subscription</a>";
@@ -6347,15 +6790,29 @@ DNS-over-HTTPS with IP:
$data[] = [
[
'text' => $this->i18n('v2ray'),
'web_app' => ['url' => "https://{$domain}/pac$hash?t=s&s={$c['id']}"],
'web_app' => ['url' => "https://{$domain}/pac$hash?t=s&s={$c['id']}"]
],
[
'text' => $this->i18n('singbox'),
'web_app' => ['url' => "https://{$domain}/pac$hash?t=si&s={$c['id']}"],
'web_app' => ['url' => "https://{$domain}/pac$hash?t=si&s={$c['id']}"]
],
[
'text' => $this->i18n('mihomo'),
'web_app' => ['url' => "https://{$domain}/pac$hash?t=cl&s={$c['id']}"],
'web_app' => ['url' => "https://{$domain}/pac$hash?t=cl&s={$c['id']}"]
],
];
$data[] = [
[
'text' => $this->i18n('v2ray ⬇️'),
'callback_data' => "/dw {$i} s",
],
[
'text' => $this->i18n('singbox ⬇️'),
'callback_data' => "/dw {$i} si",
],
[
'text' => $this->i18n('mihomo ⬇️'),
'callback_data' => "/dw {$i} cl",
],
];
$data[] = [
@@ -6399,6 +6856,12 @@ DNS-over-HTTPS with IP:
'callback_data' => "/qrXray {$i}_2",
],
];
$data[] = [
[
'text' => $this->i18n('hwid limit') . ': ' . ($hwidEnabled ? $hwidLimit : $this->i18n('off')) . ' (' . count($devices) . ')',
'callback_data' => "/hwidUser $i",
],
];
$data[] = [
[
'text' => $this->i18n('rename'),
@@ -6423,6 +6886,188 @@ DNS-over-HTTPS with IP:
);
}
public function hwidUser($i, $page = 0)
{
$xray = $this->getXray();
$client = $xray['inbounds'][0]['settings']['clients'][$i];
$pac = $this->getPacConf();
$devices = $this->getHwidDevicesByUser($client['id']);
$scope = $this->getHwidTokenScope($i);
if (!isset($_SESSION['hwidTokens'])) {
$_SESSION['hwidTokens'] = [];
}
$_SESSION['hwidTokens'][$scope] = [];
uasort($devices, fn($a, $b) => ($b['time'] ?? 0) <=> ($a['time'] ?? 0));
$hwids = array_keys($devices);
$perPage = max(1, $this->limit ?: 5);
$total = count($hwids);
$pages = max(1, (int) ceil($total / $perPage));
$page = min(max((int) $page, 0), $pages - 1);
$hwidsPage = array_slice($hwids, $page * $perPage, $perPage);
$defaultHwid = max(1, (int) ($pac['hwid_device_count'] ?: 1));
$text[] = "Menu -> " . $this->i18n('xray') . " -> {$client['email']} -> " . $this->i18n('hwid devices');
$text[] = $this->i18n('hwid notice');
if (empty($pac['hwid_limit_enabled'])) {
$status = $this->i18n('off');
} elseif (!empty($client['hwid_disabled'])) {
$status = $this->i18n('off');
} elseif (!empty($client['hwid_limit'])) {
$status = (int) $client['hwid_limit'];
} else {
$status = "default($defaultHwid)";
}
$text[] = $this->i18n('hwid limit') . ': ' . $status;
$text[] = $this->i18n('hwid devices') . ': ' . $total;
$data[] = [
[
'text' => $this->i18n(!empty($client['hwid_disabled']) ? 'off' : 'on'),
'callback_data' => "/hwidUserToggle $i",
],
];
$data[] = [
[
'text' => $this->i18n('set hwid devices count'),
'callback_data' => "/setHwidUserLimit $i",
],
];
if (!empty($client['hwid_limit'])) {
$data[] = [
[
'text' => $this->i18n('use default hwid limit'),
'callback_data' => "/hwidUserDefault $i",
],
];
}
if ($total == 0) {
$text[] = $this->i18n('no devices');
}
foreach ($hwidsPage as $index => $hwid) {
$info = $devices[$hwid];
$number = $page * $perPage + $index + 1;
$details = array_filter([
$info['device_os'] ?? '',
$info['os_version'] ?? '',
$info['device_model'] ?? '',
], fn($v) => $v !== '');
$line = $number . '. <code>' . htmlspecialchars($hwid, ENT_HTML5, 'UTF-8') . '</code>';
if (!empty($details)) {
$line .= ' - ' . htmlspecialchars(implode(' ', $details), ENT_HTML5, 'UTF-8');
}
if (!empty($info['time'])) {
$line .= ' (' . date('d.m.Y H:i', $info['time']) . ')';
}
$text[] = $line;
if (!empty($info['user_agent'])) {
$text[] = 'UA: ' . htmlspecialchars($info['user_agent'], ENT_HTML5, 'UTF-8');
}
$token = $this->rememberHwidToken($scope, $hwid);
$data[] = [
[
'text' => '🗑 ' . $number,
'callback_data' => "/hwidUserDel {$i}_{$page} $token",
],
];
}
if ($pages > 1) {
$data[] = [
[
'text' => '<<',
'callback_data' => "/hwidUser {$i}_" . ($page - 1 >= 0 ? $page - 1 : $pages - 1),
],
[
'text' => ($page + 1) . '/' . $pages,
'callback_data' => "/hwidUser {$i}_$page",
],
[
'text' => '>>',
'callback_data' => "/hwidUser {$i}_" . (($page + 1) % $pages),
],
];
}
$data[] = [
[
'text' => $this->i18n('back'),
'callback_data' => "/userXr $i",
],
];
$this->update(
$this->input['chat'],
$this->input['message_id'],
implode("\n", $text ?: ['...']),
$data ?: false,
);
}
public function hwidUserToggle($i)
{
$xray = $this->getXray();
if (!empty($xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled'])) {
unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled']);
} else {
$xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled'] = 1;
}
file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$this->answer($this->input['callback_id'], $this->i18n('hwid notice'), true);
$this->hwidUser($i);
}
public function hwidUserDefault($i)
{
$xray = $this->getXray();
unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit']);
file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$this->hwidUser($i);
}
public function setHwidUserLimit($i)
{
$r = $this->send(
$this->input['chat'],
"@{$this->input['username']} enter hwid devices count",
$this->input['message_id'],
reply: 'enter hwid devices count',
);
$_SESSION['reply'][$r['result']['message_id']] = [
'start_message' => $this->input['message_id'],
'callback' => 'saveHwidUserLimit',
'args' => [$i],
];
}
public function saveHwidUserLimit($count, $i)
{
$xray = $this->getXray();
$count = (int) $count;
if ($count > 0) {
$xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit'] = $count;
} else {
unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit']);
}
file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$this->send($this->input['chat'], $this->i18n('hwid notice'), $this->input['message_id']);
$this->hwidUser($i);
}
public function hwidUserDel($i, $page, $hwid)
{
$xray = $this->getXray();
$uid = $xray['inbounds'][0]['settings']['clients'][$i]['id'];
$scope = $this->getHwidTokenScope($i);
$decoded = $this->resolveHwidToken($scope, $hwid);
if ($decoded !== '') {
$this->deleteHwidDevice($uid, $decoded);
}
$this->hwidUser($i, $page);
}
public function getDomain($cdn = false)
{
$c = $this->getPacConf();
@@ -6441,16 +7086,23 @@ DNS-over-HTTPS with IP:
$scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https';
$hash = $this->getHashBot();
$flag = true;
$client = null;
foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) {
if ($v['id'] == $_GET['id']) {
if (empty($v['off'])) {
$flag = false;
}
$uid = $v['id'];
$email = $v['email'];
$uid = $v['id'];
$email = $v['email'];
$expire = $v['time'];
$client = $v;
break;
}
}
if (!$flag && !$this->processHwidRequest($client)) {
exit;
}
$suburl = "<a href='$scheme://{$domain}/pac$hash/sub?id={$uid}'>subscription</a>";
$download = $this->getBytes($st['users'][$k]['global']['download'] + $st['users'][$k]['session']['download']);
$upload = $this->getBytes($st['users'][$k]['global']['upload'] + $st['users'][$k]['session']['upload']);
$singbox = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([
@@ -6502,6 +7154,7 @@ DNS-over-HTTPS with IP:
$hash = $this->getHashBot();
$flag = true;
$client = null;
foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) {
if ($v['id'] == $_GET['s']) {
if (empty($v['off'])) {
@@ -6510,6 +7163,7 @@ DNS-over-HTTPS with IP:
$template = base64_decode($v["{$type}template"]);
$uid = $v['id'];
$email = $v['email'];
$client = $v;
break;
}
}
@@ -6518,6 +7172,10 @@ DNS-over-HTTPS with IP:
exit;
}
if (!$return && !$this->processHwidRequest($client)) {
exit;
}
if (!empty($_GET['r'])) {
$si = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([
'h' => $hash,
@@ -6642,7 +7300,6 @@ DNS-over-HTTPS with IP:
}
break;
case 'si':
$c['outbounds'][$index]['server'] = '~domain~';
$c['outbounds'][$index]['uuid'] = '~uid~';
if ($pac['transport'] != 'Reality') {
unset($c['outbounds'][$index]['tls']['reality']);
@@ -6725,6 +7382,9 @@ DNS-over-HTTPS with IP:
}
$c['route']['rules'] = array_values($c['route']['rules']);
}
if (empty($c['route'])) {
unset($c['route']);
}
break;
case 'cl':
$c = $this->addClashRuleSet($c);
@@ -6948,7 +7608,9 @@ DNS-over-HTTPS with IP:
}
}
}
$route['rules'] = array_values($route['rules']);
if (!empty($route['rules'])) {
$route['rules'] = array_values($route['rules']);
}
$route['rule_set'] = array_merge($route['rule_set'] ?: [], $ruleset ?: []);
if (empty($route['rule_set'])) {
unset($route['rule_set']);
@@ -7542,6 +8204,10 @@ DNS-over-HTTPS with IP:
'text' => $this->i18n($c['ss'] ? 'on' : 'off') . ' 8388 Shadowsocks',
'callback_data' => "/hidePort ss",
]],
[[
'text' => $this->i18n($c['io'] ? 'on' : 'off') . ' 53 Iodine',
'callback_data' => "/hidePort io",
]],
];
if (!empty($pac['restart'])) {
$data[] = [
@@ -7573,6 +8239,7 @@ DNS-over-HTTPS with IP:
'tg' => getenv('TGPORT') . ':' . getenv('TGPORT'),
'ad' => '853:853',
'ss' => '8388:8388',
'io' => '53:53/udp',
];
$f = '/docker/compose';
$c = yaml_parse_file($f);
+24
View File
@@ -193,6 +193,30 @@ $i = [
'en' => 'delete internal dns',
'ru' => 'удалить внутренний dns',
],
'hwid limit' => [
'en' => 'HWID limit',
'ru' => 'HWID лимит',
],
'hwid devices' => [
'en' => 'HWID devices',
'ru' => 'Устройства HWID',
],
'set hwid devices count' => [
'en' => 'set HWID devices count',
'ru' => 'установить количество HWID устройств',
],
'use default hwid limit' => [
'en' => 'use default limit',
'ru' => 'использовать лимит по умолчанию',
],
'no devices' => [
'en' => 'no devices',
'ru' => 'нет устройств',
],
'hwid notice' => [
'en' => 'HWID limit works only when subscription is refreshed or added',
'ru' => 'HWID лимит срабатывает только в момент обновления и добавления подписки',
],
'on' => [
'en' => '🟢',
'ru' => '🟢',
+1
View File
@@ -24,3 +24,4 @@ $bot->adguardSync();
$bot->cloakNginx();
$bot->syncDeny();
$bot->cleanDocker();
$bot->iodineRestart();
+1
View File
@@ -0,0 +1 @@
{}
+31 -2
View File
@@ -283,7 +283,7 @@ services:
ipv4_address: 10.10.0.14
logging: *default-logging
ad:
image: mercurykd/vpnbot-ad:1.3
image: mercurykd/vpnbot-ad:1.4
build:
dockerfile: dockerfile/adguard.dockerfile
args:
@@ -346,7 +346,7 @@ services:
ipv4_address: 10.10.0.8
logging: *default-logging
xr:
image: mercurykd/vpnbot-xr:1.4
image: mercurykd/vpnbot-xr:1.5
build:
dockerfile: dockerfile/xray.dockerfile
args:
@@ -532,3 +532,32 @@ services:
default:
ipv4_address: 10.10.0.6
logging: *default-logging
io:
image: mercurykd/vpnbot-io:1.0
build:
dockerfile: dockerfile/iodine.dockerfile
volumes:
- ./config/.profile:/root/.ashrc:ro
- ./ssh:/ssh
- ./config/sshd_config:/etc/ssh/sshd_config
- ./scripts/start_io.sh:/start_io.sh
hostname: iodine
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
container_name: iodine-${VER}
depends_on:
php:
condition: service_healthy
env_file:
- path: ./.env
required: true # default
- path: ./override.env
required: false
stop_grace_period: 1s
command: ["/bin/sh", "/start_io.sh"]
networks:
default:
ipv4_address: 10.10.0.16
logging: *default-logging
+4
View File
@@ -0,0 +1,4 @@
FROM alpine:3.22
RUN apk add iodine openssh iptables\
&& mkdir /root/.ssh
ENV ENV="/root/.ashrc"
+2
View File
@@ -37,6 +37,8 @@ proxy: # консоль сервиса
docker compose exec proxy /bin/sh
tg: # консоль сервиса
docker compose exec tg /bin/sh
io: # консоль сервиса
docker compose exec io /bin/sh
xr: # консоль сервиса
docker compose exec xr /bin/sh
oc: # консоль сервиса
+7
View File
@@ -0,0 +1,7 @@
cat /ssh/key.pub > /root/.ssh/authorized_keys
ssh-keygen -A
exec /usr/sbin/sshd -D -e "$@" &
iptables -A FORWARD -i dns0 -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o dns0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
tail -f /dev/null
+6
View File
@@ -1,3 +1,9 @@
08.11.2025 v2.26
- обновлено ядро xray
- обновлен adguardHome
- добавлен iodine(dnstt)
- добавлены кнопки скачивания vless-конфига
- hwid для подписки orion от legiz
19.07.2025 v2.25
- перед запуском обязательно запустить:
<pre>touch ./override.env ./docker-compose.override.yml ./config/location.conf ./config/override.conf</pre>