diff --git a/app/bot.php b/app/bot.php index 413c532..faba5a5 100644 --- a/app/bot.php +++ b/app/bot.php @@ -11,6 +11,7 @@ class Bot public $file; public $dns; public $mtu; + public $logs; public function __construct($key, $i18n) { @@ -28,6 +29,12 @@ class Bot $this->limit = $this->getPacConf()['limitpage'] ?: 5; $this->adguard = '/config/AdGuardHome.yaml'; $this->update = '/update/json'; + $this->logs = [ + 'nginx_default_access', + 'nginx_domain_access', + 'upstream_access', + 'xray', + ]; } public function input() @@ -139,12 +146,40 @@ class Bot case preg_match('~^/mirror$~', $this->input['message'], $m): $this->menu('mirror'); break; + case preg_match('~^/switchBanIp$~', $this->input['callback'], $m): + $this->switchBanIp(); + break; + case preg_match('~^/switchScanIp$~', $this->input['callback'], $m): + $this->switchScanIp(); + break; case preg_match('~^/autoupdate$~', $this->input['message'], $m): $this->autoupdate(); break; case preg_match('~^/ports$~', $this->input['callback'], $m): $this->ports(); break; + case preg_match('~^/ip$~', $this->input['message'], $m): + case preg_match('~^/analysisIp$~', $this->input['callback'], $m): + $this->analysisIp(); + break; + case preg_match('~^/ipMenu$~', $this->input['callback'], $m): + $this->ipMenu(); + break; + case preg_match('~^/cleanDeny$~', $this->input['callback'], $m): + $this->cleanDeny(); + break; + case preg_match('~^/denyList (\d+)$~', $this->input['callback'], $m): + $this->denyList($m[1]); + break; + case preg_match('~^/allowIp (.+?) (\d+)$~', $this->input['callback'], $m): + $this->allowIp($m[1], $m[2]); + break; + case preg_match('~^/searchIp (.+)$~', $this->input['callback'], $m): + $this->searchIp($m[1]); + break; + case preg_match('~^/denyIp (.+)$~', $this->input['callback'], $m): + $this->denyIp($m[1]); + break; case preg_match('~^/adgFillAllowedClients(?: (\d+))?$~', $this->input['callback'], $m): $this->adgFillAllowedClients($m[1] ?: false); break; @@ -1097,10 +1132,50 @@ class Bot $this->checkVersion(); $this->checkBackup($period); $this->checkCert(); + $this->autoAnalyzeLogs(); sleep($period); } } + public function autoAnalyzeLogs() + { + try { + $pac = $this->getPacConf(); + if (!empty($pac['autoscan'])) { + $r = $this->analysisIp(1); + require __DIR__ . '/config.php'; + if (!empty($c['admin']) && (empty($this->time3) || ((time() - $this->time3) > 60 * 60))) { + $this->time3 = time(); + if (!empty($r)) { + foreach ($r as $k => $v) { + $tmp = array_unique($v); + foreach ($tmp as $i) { + $t[$i]++; + } + } + foreach ($t as $k => $v) { + $text .= "\n$v $k"; + } + if (!empty($pac['autodeny'])) { + $this->denyIp(array_keys($r)); + $ban = count(array_keys($r)); + $ips = implode("\n", array_keys($r)); + } + foreach ($c['admin'] as $k => $v) { + $this->send($v, "suspicious ips found: $text" . ($ban ? "\nbanned:$ban\n$ips" : ''), button: $pac['autodeny'] ? false : [[ + [ + 'text' => $this->i18n('analyze'), + 'callback_data' => '/analysisIp', + ], + ]]); + } + } + } + } + } catch (Exception $e) { + } + } + public function checkBackup($delta) { $c = $this->getPacConf(); @@ -3945,7 +4020,7 @@ DNS-over-HTTPS with IP: public function menu($type = false, $arg = false, $return = false) { $domain = $this->getPacConf()['domain'] ?: $this->ip; - $cron = exec('pgrep -f cron.php'); + $cron = $this->ssh('pgrep -f cron.php', 'service'); $menu = [ 'main' => [ 'text' => 'v' . getenv('VER') . ($this->dontshowcron ? '' : "\ncron: " . $this->i18n($cron ? 'on' : 'off') . ($cron ? '' : ' show logs/php_error')), @@ -4000,6 +4075,12 @@ DNS-over-HTTPS with IP: 'callback_data' => "/pacMenu 0", ], ], + [ + [ + 'text' => $this->i18n('IP'), + 'callback_data' => "/ipMenu", + ], + ], [ [ 'text' => $this->i18n('config'), @@ -4020,18 +4101,18 @@ DNS-over-HTTPS with IP: ], ], ], - 'wg' => $type == 'wg' ? $this->statusWg($arg) : false, + 'wg' => $type == 'wg' ? $this->statusWg($arg) : false, 'client' => $type == 'client' ? $this->getClient(...explode('_', $arg)) : false, - 'addpeer' => $type == 'addpeer' ? $this->addWg(...explode('_', $arg)) : false, - 'pac' => $type == 'pac' ? $this->pacMenu($arg) : false, - 'adguard' => $type == 'adguard' ? $this->adguardMenu() : false, - 'config' => $type == 'config' ? $this->configMenu() : false, - 'ss' => $type == 'ss' ? $this->menuSS() : false, - 'lang' => $type == 'lang' ? $this->menuLang() : false, - 'oc' => $type == 'oc' ? $this->ocMenu() : false, - 'naive' => $type == 'naive' ? $this->naiveMenu() : false, - 'mirror' => $type == 'mirror' ? $this->mirrorMenu() : false, - 'update' => $type == 'update' ? $this->updatebot() : false, + 'addpeer' => $type == 'addpeer' ? $this->addWg(...explode('_', $arg)) : false, + 'pac' => $type == 'pac' ? $this->pacMenu($arg) : false, + 'adguard' => $type == 'adguard' ? $this->adguardMenu() : false, + 'config' => $type == 'config' ? $this->configMenu() : false, + 'ss' => $type == 'ss' ? $this->menuSS() : false, + 'lang' => $type == 'lang' ? $this->menuLang() : false, + 'oc' => $type == 'oc' ? $this->ocMenu() : false, + 'naive' => $type == 'naive' ? $this->naiveMenu() : false, + 'mirror' => $type == 'mirror' ? $this->mirrorMenu() : false, + 'update' => $type == 'update' ? $this->updatebot() : false, ]; $text = $menu[$type ?: 'main' ]['text']; @@ -4058,6 +4139,311 @@ DNS-over-HTTPS with IP: } } + public function switchScanIp() + { + $c = $this->getPacConf(); + $c['autoscan'] = $c['autoscan'] ? 0 : 1; + $this->setPacConf($c); + $this->ipMenu(); + } + + public function switchBanIp() + { + $c = $this->getPacConf(); + $c['autodeny'] = $c['autodeny'] ? 0 : 1; + $this->setPacConf($c); + $this->ipMenu(); + } + + public function ipMenu() + { + $text = 'Menu -> IP'; + $pac = $this->getPacConf(); + $data[] = [ + [ + 'text' => $this->i18n('autoscan') . ': ' . $this->i18n($pac['autoscan'] ? 'on' : 'off'), + 'callback_data' => '/switchScanIp', + ], + [ + 'text' => $this->i18n('autodeny') . ': ' . $this->i18n($pac['autodeny'] ? 'on' : 'off'), + 'callback_data' => '/switchBanIp', + ], + ]; + $data[] = [ + [ + 'text' => $this->i18n('deny list'), + 'callback_data' => '/denyList 0', + ], + ]; + $data[] = [ + [ + 'text' => $this->i18n('analyze'), + 'callback_data' => '/analysisIp', + ], + ]; + $data[] = [ + [ + 'text' => $this->i18n('back'), + 'callback_data' => "/menu", + ], + ]; + $this->update( + $this->input['chat'], + $this->input['message_id'], + $text, + $data ?: false, + ); + } + + public function analysisIp($return = false) + { + if ($r = fopen('/logs/nginx_tlgrm_access', 'r')) { + while (feof($r) === false) { + $l = fgets($r); + if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { + $xr[$m[1]] = true; + } + } + fclose($r); + } + if ($r = fopen('/logs/xray', 'r')) { + while (feof($r) === false) { + $l = fgets($r); + if (preg_match('~(\d+\.\d+\.\d+\.\d+)(?=.+accepted)~', $l, $m)) { + $xr[$m[1]] = true; + } + } + fclose($r); + } + $xr = array_merge(['10.10.0.10' => true], $xr ?: []); + + if ($r = fopen('/logs/upstream_access', 'r')) { + while (feof($r) === false) { + $l = fgets($r); + if (preg_match('~(\d+\.\d+\.\d+\.\d+).+200\s\d+\s0$~', $l, $m)) { + if (empty($xr[$m[1]])) { + $ip[$m[1]][] = 'possibly a Reality Degenerate'; + } + } + } + fclose($r); + } + + $reg = [ + 'GET /ws.+ HTTP', + 'GET /adguard/.+ HTTP', + 'GET /webapp.+ HTTP', + 'GET /pac.+ HTTP', + 'GET \.well-known.+ HTTP', + 'GET /v2ray.+ HTTP', + 'GET /dns-query.+ HTTP', + 'GET / HTTP', + 'GET /tlgrm.+ HTTP', + 'GET /jsoneditor.min.css HTTP', + 'GET /jsoneditor.min.js HTTP', + 'GET /jquery-3.7.1.min.js HTTP', + 'GET /img/jsoneditor-icons.svg HTTP', + 'GET /favicon.ico HTTP', + ]; + + if ($r = fopen('/logs/nginx_default_access', 'r')) { + while (feof($r) === false) { + $l = fgets($r); + if (!preg_match('~' . implode('|', $reg) . '~', $l)) { + if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { + if (empty($xr[$m[1]])) { + $ip[$m[1]][] = 'possibly a scanner'; + } + } + } + } + fclose($r); + } + + if ($r = fopen('/logs/nginx_domain_access', 'r')) { + while (feof($r) === false) { + $l = fgets($r); + if (!preg_match('~' . implode('|', $reg) . '~', $l)) { + if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { + if (empty($xr[$m[1]])) { + $ip[$m[1]][] = 'possibly a scanner'; + } + } + } + } + fclose($r); + } + + $pac = $this->getPacConf(); + foreach ($ip as $k => $v) { + if (!in_array($k, $pac['deny'] ?: [])) { + $ips[$k] = $v; + } + } + if (!empty($ips)) { + foreach ($ips as $k => $v) { + if (!in_array($k, $pac['deny'] ?: [])) { + $comment = implode(', ', array_unique($v)); + if (!empty($return)) { + $ret[$k] = $v; + } else { + $this->send($this->input['from'], "$k $comment\n", button: [[ + [ + 'text' => $this->i18n('search'), + 'callback_data' => "/searchIp $k", + ], + [ + 'text' => $this->i18n('disallow'), + 'callback_data' => "/denyIp $k", + ], + ]]); + } + } + } + if (!empty($return)) { + return $ret; + } + } else { + $this->answer($this->input['callback_id'], 'empty'); + } + + } + + public function searchIp($ip) + { + foreach ($this->logs as $v) { + if ($r = fopen("/logs/$v", 'r')) { + while (feof($r) === false) { + $l = fgets($r); + if (preg_match('~' . preg_quote($ip) . '~', $l)) { + $res[$v][] = $l; + } + } + fclose($r); + } + } + foreach ($res as $k => $v) { + $head= "$k:\n"; + $t = array_chunk($v, 10); + foreach ($t as $j) { + $text = "$head
";
+                foreach ($j as $i) {
+                    $text .= htmlspecialchars($i, ENT_HTML5, 'UTF-8');
+                }
+                $text .= '
'; + $this->send($this->input['from'], $text, $this->input['message_id']); + } + } + } + + public function denyList($page = 0) + { + $text = 'Menu -> IP -> deny list'; + $domains = $this->getPacConf()['deny'] ?: []; + $all = (int) ceil(count($domains) / $this->limit); + $page = min($page, $all - 1); + $page = $page < 0 ? $all - 1 : $page; + + if (!empty($domains)) { + foreach (array_slice($domains, $page * $this->limit, $this->limit) as $v) { + $data[] = [ + [ + 'text' => $this->i18n('delete') . " $v", + 'callback_data' => "/allowIp $v $page", + ], + [ + 'text' => $this->i18n('search'), + 'callback_data' => "/searchIp $v", + ], + ]; + } + if ($all > 1) { + $data[] = [ + [ + 'text' => '<<', + 'callback_data' => "/denyList " . ($page - 1 >= 0 ? $page - 1 : $all - 1), + ], + [ + 'text' => '>>', + 'callback_data' => "/denyList " . ($page < $all - 1 ? $page + 1 : 0), + ] + ]; + } + $data[] = [ + [ + 'text' => $this->i18n('delete all'), + 'callback_data' => "/cleanDeny", + ], + ]; + } + + $data[] = [ + [ + 'text' => $this->i18n('back'), + 'callback_data' => "/ipMenu", + ], + ]; + $this->update( + $this->input['chat'], + $this->input['message_id'], + $text, + $data ?: false, + ); + } + + public function cleanDeny() + { + $pac = $this->getPacConf(); + unset($pac['deny']); + $this->setPacConf($pac); + $this->syncDeny(); + $this->denyList(0); + } + + public function denyIp($ip) + { + $pac = $this->getPacConf(); + if (is_array($ip)) { + foreach ($ip as $v) { + $pac['deny'][] = $v; + } + } else { + $pac['deny'][] = $ip; + } + $this->setPacConf($pac); + $this->delete($this->input['from'], $this->input['message_id']); + $this->syncDeny(); + } + + public function allowIp($ip, $page) + { + $pac = $this->getPacConf(); + unset($pac['deny'][array_search($ip, $pac['deny'])]); + $this->setPacConf($pac); + $this->syncDeny(); + $this->denyList($page); + } + + public function deleteFromLogs($ip) + { + foreach ($this->logs as $v) { + exec("sed '/$ip/d' $v"); + } + } + + public function syncDeny() + { + $pac = $this->getPacConf(); + if (!empty($pac['deny'])) { + foreach (array_unique($pac['deny']) as $v) { + // $this->deleteFromLogs($v); + $text .= "deny $v;\n"; + } + } + file_put_contents('/config/deny', $text ?: ''); + $this->ssh('nginx -s reload', 'up'); + } + public function linkXray($i, $s = false) { $c = $this->getXray(); diff --git a/app/service.php b/app/service.php index ea184e2..6f962ca 100644 --- a/app/service.php +++ b/app/service.php @@ -20,4 +20,5 @@ if (!empty($bot->selfupdate)) { $bot->dontshowcron = 1; $bot->adguardSync(); $bot->sslip(); +$bot->syncDeny(); $bot->cleanDocker(); diff --git a/config/deny b/config/deny new file mode 100644 index 0000000..e69de29 diff --git a/config/upstream.conf b/config/upstream.conf index d5f9a4a..895482a 100644 --- a/config/upstream.conf +++ b/config/upstream.conf @@ -11,6 +11,7 @@ events { } stream { + include /etc/nginx/deny; log_format basic '$remote_addr [$time_local] ' '$protocol $status $bytes_sent $bytes_received'; access_log /logs/upstream_access basic; diff --git a/docker-compose.yml b/docker-compose.yml index 6fe859e..84c6172 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: volumes: - ./config/.profile:/root/.ashrc:ro - ./config/upstream.conf:/etc/nginx/nginx.conf + - ./config/deny:/etc/nginx/deny - ./scripts/start_upstream.sh:/start_upstream.sh - ./ssh:/ssh - ./config/sshd_config:/etc/ssh/sshd_config @@ -95,7 +96,7 @@ services: ipv4_address: 10.10.1.2 logging: *default-logging php: - image: mercurykd/vpnbot-php:1.5 + image: mercurykd/vpnbot-php:1.6 build: dockerfile: dockerfile/php.dockerfile args: @@ -146,7 +147,7 @@ services: timeout: 5s retries: 5 service: - image: mercurykd/vpnbot-php:1.5 + image: mercurykd/vpnbot-php:1.6 build: dockerfile: dockerfile/php.dockerfile args: diff --git a/dockerfile/php.dockerfile b/dockerfile/php.dockerfile index 711aa43..895c2d1 100644 --- a/dockerfile/php.dockerfile +++ b/dockerfile/php.dockerfile @@ -32,4 +32,6 @@ RUN apk add --no-cache --update php81 \ && mv sing-box-1.8.11-linux-amd64/sing-box /usr/bin \ && rm sing-box-1.8.11-linux-amd64.tar.gz \ && rm -rf /sing-box-1.8.11-linux-amd64 -ENV ENV="/root/.ashrc" +RUN apk add openssh \ + && mkdir /root/.ssh + ENV ENV="/root/.ashrc" diff --git a/scripts/start_php.sh b/scripts/start_php.sh index 8526ce0..9e65edf 100755 --- a/scripts/start_php.sh +++ b/scripts/start_php.sh @@ -3,7 +3,6 @@ ssh-keygen -m PEM -t rsa -f /ssh/key -N '' openssl req -newkey rsa:2048 -sha256 -nodes -x509 -days 365 -keyout /certs/self_private -out /certs/self_public -subj "/C=NN/ST=N/L=N/O=N/CN=$IP" php init.php if [[ -f "/start" && -f "/ssh/key.pub" && -s "/ssh/key.pub" ]]; then - php cron.php & unitd --log /logs/unit_error curl -X PUT --data-binary @/config/unit.json --unix-socket /var/run/control.unit.sock http://localhost/config pkill unitd diff --git a/scripts/start_service.sh b/scripts/start_service.sh index 90016cd..f3ccd78 100755 --- a/scripts/start_service.sh +++ b/scripts/start_service.sh @@ -1 +1,5 @@ -php service.php \ No newline at end of file +cat /ssh/key.pub > /root/.ssh/authorized_keys +ssh-keygen -A +exec /usr/sbin/sshd -D -e "$@" & +php service.php +php cron.php \ No newline at end of file