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