Compare commits

..

No commits in common. "master" and "networks-upgrade" have entirely different histories.

74 changed files with 1696 additions and 302 deletions

View File

@ -1,5 +1,18 @@
# Protected Resources
Список ресурсов для настройки родительского контроля чтобы заблокировать доступ детям до запрещённых в РФ вражеских сервисов.
## resources
## networks
Каталог с файлами сервисов и их подсетей
### Установка
```shell
apk add php php-session php-curl jq git
cd /opt
git clone https://git.kirillius.ru/kirillius/protected-resources-list.git
cd /opt/protected-resources-list/
chmod -R +x ./bin
ln -s /opt/protected-resources-list/bin/webui /etc/init.d/webui
cp config.json.example config.json
rc-update add webui
/etc/init.d/webui start
```

79
assets/App.js Normal file
View File

@ -0,0 +1,79 @@
import {JSONRPC} from "./jrpc.js";
const App = {
config: {},
networks: {},
RPC: JSONRPC
};
App.auth = async function () {
let authorized = await JSONRPC.__invoke("auth");
if (!authorized) {
do {
let pass = prompt("Password");
authorized = await JSONRPC.__invoke("auth", {
"password": pass
});
if (!authorized) {
alert("Wrong password");
}
} while (!authorized);
}
this.config = await JSONRPC.__invoke("getConfig");
this.networks = await JSONRPC.__invoke("getNetworks");
for (const key of this.config.plugins) {
$("body").append("<" + "script type='module' src='/plugins/" + key + "/plugin.js'><" + "/script>");
}
$("#loading").hide();
this.fillNetworks();
$("#panel").show();
let invalidNetworks = await JSONRPC.__invoke("getInvalidNetworks");
if (invalidNetworks.length > 0) {
$("body").append(`<span>There are invalid networks <BR>` + invalidNetworks.join("<BR>") + ` <BR></span>`);
}
}
App.fillNetworks = function () {
const that = this;
let proto = $("#net-table tr");
proto.detach();
for (const net in this.networks) {
let item = proto.clone();
item.find("input").prop('checked', this.config.networks.indexOf(net) !== -1).change(function () {
if ($(this).prop('checked')) {
that.config.networks.push(net);
} else {
that.config.networks = that.config.networks.filter(e => e !== net);
}
});
item.find("span").text(net);
$("#net-table").append(item);
}
}
App.render = async function () {
await this.auth();
$("#save").click(function () {
const self = $(this);
self.prop("disabled", true);
(async function () {
await JSONRPC.__invoke("setConfig", App.config);
alert("Config saved!");
self.prop("disabled", false);
})();
});
}
export {App};

36
assets/index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Routing config</title>
<script src="jquery-3.7.1.min.js" type="text/javascript"></script>
</head>
<body>
<span id="loading">Loading...</span>
<div style="display: none" id="panel">
Selected networks:
<table id="net-table">
<tr>
<td><input type="checkbox"></td>
<td><span>Network name</span></td>
</tr>
</table>
<div id="buttons">
<button id="save">Save</button>
</div>
</div>
<script type="module">
import {App} from "./App.js";
$(document).ready(function () {
App.render();
});
</script>
</body>
</html>

2
assets/jquery-3.7.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

59
assets/jrpc.js Normal file
View File

@ -0,0 +1,59 @@
export const JSONRPC = {
url: "/rpc",
__id: 1,
/**
*
* @param method
* @param params
* @returns Object
* @private
*/
__invoke: async function (method, params) {
if(params === undefined){
params = {};
}
const request = await JSONRPC.__performRequest(method, params);
if (!request.success) {
console.error(request.result);
throw new Error("Failed to invoke method " + method + " with params " + JSON.stringify(params));
}
return request.result;
},
__performRequest: async function (method, params) {
const __this = this;
const resp = await fetch(
__this.url,
{
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "include",
headers: {
"Content-Type": "application/json"
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: JSON.stringify({
jsonrpc: '2.0',
method: method,
params: params,
id: __this.__id++
})
}
);
const success = resp.status === 200;
const result = (success ? (await resp.json()).result : {
"error": true,
"code": resp.status,
"status": resp.statusText,
"body": await resp.text()
});
return {
"result": result,
"success": success
};
}
};

29
bin/ovpn-connect Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/php
<?php
require_once dirname(__DIR__) . "/loader.php";
require_once dirname(__DIR__) . "/plugins/openvpn/Openvpn.php";
try {
if (!isset($argv[1])) {
throw new RuntimeException("Output config is not set");
}
$rpc = new StaticRPC();
$instance = $rpc->getPlugins()["openvpn"] ?? null;
if ($instance === null) {
throw new RuntimeException("Plugin is not enabled");
}
/**
* @var Custom $instance
*/
$config = $instance->getRoutingConfig();
$outfile = $argv[1];
file_put_contents($outfile, implode("\n", $config));
} catch (Exception $e) {
echo "\nError:" . $e->getMessage() . "\n";
exit(1);
}
exit(0);

27
bin/sync-networks Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/php
<?php
require_once dirname(__DIR__) . "/loader.php";
require_once dirname(__DIR__) . "/plugins/netsync/Netsync.php";
try {
$rpc = new StaticRPC();
$instance = $rpc->getPlugins()["netsync"] ?? null;
if ($instance === null) {
throw new RuntimeException("Plugin is not enabled");
}
/**
* @var Netsync $instance
*/
$config = $instance->sync();
var_dump($config);
} catch (Exception $e) {
echo "\nError:" . $e->getMessage() . "\n";
exit(1);
}
exit(0);

4
bin/webui Normal file
View File

@ -0,0 +1,4 @@
#!/sbin/openrc-run
command="/opt/protected-resources-list/bin/webui-server"
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"

17
bin/webui-server Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
SELFDIR=`dirname $0`
ROOT=`realpath $SELFDIR/..`
CFGFILE=$ROOT/config.json
if ! test -f "$CFGFILE"; then
echo Config file $CFGFILE not found
exit 1
fi
HOST=`jq -r .web.host $CFGFILE`
PORT=`jq -r .web.port $CFGFILE`
#TODO FIXME !!!!
killall php
cd $ROOT
php $ROOT/loader.php --init
php -S $HOST:$PORT $ROOT/server.php

57
classes/Config.php Normal file
View File

@ -0,0 +1,57 @@
<?php
class Config implements ArrayAccess
{
private string $path;
public function asArray(): array
{
return $this->data;
}
public function fromArray($a)
{
$this->data = $a;
}
public function __construct()
{
$this->path = dirname(__DIR__) . "/config.json";
}
public function read(): void
{
$this->data = @json_decode(@file_get_contents($this->path), true);
if ($this->data == null) {
throw new RuntimeException("Failed to read or parse config file");
}
}
public function save(): void
{
file_put_contents($this->path, json_encode($this->data,JSON_PRETTY_PRINT));
}
private mixed $data = [];
public function offsetExists(mixed $offset): bool
{
return isset($this->data[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return $this->data[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->data[$offset] = $value;
}
public function offsetUnset(mixed $offset): void
{
unset($this->data[$offset]);
}
}

8
classes/IPluggable.php Normal file
View File

@ -0,0 +1,8 @@
<?php
interface IPluggable
{
public function onServerStarted();
public function onInit(PluginContext $context);
public function onSync(array $remote_config);
}

46
classes/IPv4Subnet.php Normal file
View File

@ -0,0 +1,46 @@
<?php
class IPv4Subnet
{
private string $address;
private int $prefix;
/**
* @param string $subnetAddress
* @param int $prefix
*/
public function __construct(string $subnetAddress, int $prefix)
{
$this->address = $subnetAddress;
if (ip2long($subnetAddress) === false) {
throw new RuntimeException("Invalid subnet address: " . $subnetAddress);
}
$this->prefix = $prefix;
if ($prefix < 0 or $prefix > 32) {
throw new RuntimeException("Invalid subnet prefix: " . $prefix);
}
}
public function getFirstAddress()
{
$a = ip2long($this->address);
$mask = ip2long($this->getNetMask());
return long2ip($a & $mask);
}
public function getLastAddress()
{
return long2ip(ip2long($this->getFirstAddress()) + $this->getAddressCount() - 1);
}
public function getAddressCount()
{
return pow(2, 32 - $this->prefix);
}
public function getNetMask()
{
return long2ip(-1 << (32 - $this->prefix));
}
}

View File

@ -0,0 +1,33 @@
<?php
class NetworkConfigReader
{
private array $configs = [];
public function __construct()
{
$path = dirname(__DIR__) . "/networks";
foreach (new IteratorIterator(new DirectoryIterator($path)) as $file) {
/**
* @var SplFileInfo $file
*/
if ($file->getExtension() === "json") {
$key = $file->getBasename(".json");
$value = @json_decode(@file_get_contents($file->getPathname()), true);
if ($value === null) {
throw new RuntimeException("Network file " . $file->getBasename() . " is invalid or cannot be read");
}
$this->configs[$key] = $value;
}
}
}
public function getConfigs(): array
{
return $this->configs;
}
}

49
classes/Plugin.php Normal file
View File

@ -0,0 +1,49 @@
<?php
abstract class Plugin implements IPluggable
{
protected PluginContext $context;
protected array $config;
public function onServerStarted()
{
}
public function onSync($remote_config)
{
}
public function onInit(PluginContext $context): void
{
$this->context = $context;
$this->checkConfig();
$this->config = $this->context->getConfig()[$context->getName()];
}
protected function checkConfig(): void
{
$config = $this->context->getConfig();
$defaults = $this->context->getMetadata()["config"];
$name = $this->context->getName();
if (!isset($config[$name])) {
$config[$name] = $defaults;
return;
}
foreach ($defaults as $key => $value) {
if (!isset($config[$name][$key])) {
$config[$name][$key] = $value;
}
}
}
protected function saveConfig()
{
$wrapper = $this->context->getConfig();
$wrapper[$this->context->getName()] = $this->config;
$wrapper->save();
}
}

43
classes/PluginContext.php Normal file
View File

@ -0,0 +1,43 @@
<?php
class PluginContext
{
private RPC $RPC;
private Config $config;
private array $metadata;
private string $name;
public function getName(): string
{
return $this->name;
}
/**
* @param RPC $RPC
* @param Config $config
* @param array $metadata
* @param string $name
*/
public function __construct(RPC $RPC, Config $config, array $metadata, string $name)
{
$this->name = $name;
$this->RPC = $RPC;
$this->config = $config;
$this->metadata = $metadata;
}
public function getRPC(): RPC
{
return $this->RPC;
}
public function getConfig(): Config
{
return $this->config;
}
public function getMetadata(): array
{
return $this->metadata;
}
}

167
classes/RPC.php Normal file
View File

@ -0,0 +1,167 @@
<?php
class RPC
{
protected Config $config;
protected array $plugins = [];
public function getPlugins(): array
{
return $this->plugins;
}
public function __construct()
{
$this->config = new Config();
$this->config->read();
foreach ($this->config["plugins"] as $plugin) {
try {
$meta = $this->getPluginMetadata($plugin);
$inst = $this->loadPlugin($plugin, $meta["class"]);
$inst->onInit(new PluginContext($this, $this->config, $meta, $plugin));
} catch (Error $e) {
continue;
}
}
}
private function getPluginMetadata($name): array
{
$root = dirname(__DIR__) . "/plugins/" . $name;
if (!file_exists($root)) {
throw new RuntimeException("Plugin $name dir not found");
}
$metafile = $root . "/metadata.json";
if (!file_exists($metafile)) {
throw new RuntimeException("Plugin $name metadata not found");
}
$meta = @json_decode(@file_get_contents($metafile), true);
if ($meta === null) {
throw new RuntimeException("Unable to parse $name plugin metadata");
}
return $meta;
}
private function loadPlugin($name, $classname): IPluggable
{
$file = dirname(__DIR__) . "/plugins/" . $name . "/" . $classname . ".php";
if (!file_exists($file)) {
throw new RuntimeException("Plugin $name class $classname not found");
}
require_once $file;
$instance = new $classname();
if (!($instance instanceof IPluggable)) {
throw new RuntimeException("Class $classname have to implement IPluggable");
}
return $this->plugins[$name] = $instance;
}
public function getConfig(): array
{
$this->checkAuth();
return $this->config->asArray();
}
public function setConfig($config): bool
{
$this->config->fromArray($config);
$this->config->save();
return true;
}
private function checkAuth(): void
{
$auth = $_SESSION["auth"] ?? false;
if (!$auth) {
throw new RuntimeException("Unauthorized");
}
}
public function getNetworks(): array
{
$this->checkAuth();
return (new NetworkConfigReader())->getConfigs();
}
public function getInvalidNetworks(): array
{
$this->checkAuth();
$invalid = [];
foreach ((new NetworkConfigReader())->getConfigs() as $config) {
foreach ($config["networks"] as $network) {
if (!RouteUtil::validateSubnet($network)) {
$invalid[] = $network;
}
}
}
return $invalid;
}
public function logout(): void
{
$_SESSION["auth"] = false;
}
public function auth($params): bool
{
if (isset($params["password"])) {
if ($this->comparePassword($params["password"])) {
return $_SESSION["auth"] = true;
} else {
return false;
}
}
return $_SESSION["auth"] ?? false;
}
private function comparePassword($passwd): bool
{
$pass = $this->config["password"];
if ($pass["type"] == "plaintext") {
return $pass["data"] == $passwd;
} else if ($pass["type"] == "hash") {
return $this->hash($passwd) == $pass["data"];
}
return false;
}
private function hash($what): string
{
return md5(sha1($what) . md5($what));
}
private function isInternalMethod($name)
{
$cls = new ReflectionClass(IPluggable::class);
return $cls->hasMethod($name);
}
public function __invoke($method, $args)
{
$parts = explode("::", $method);
$isPlugin = count($parts) > 1;
if ($isPlugin) {
$this->checkAuth();
$plugin = $this->plugins[$parts[0]];
$methodname = $parts[1];
if ($this->isInternalMethod($methodname)) {
throw new RuntimeException("Unable to invoke internal methods");
}
return call_user_func([$plugin, $methodname], $args);
} else {
$cls = new ReflectionClass(__CLASS__);
$method = $cls->getMethod($method);
if (!$method or !$method->isPublic()) {
throw new RuntimeException("Unable to find method");
}
return $method->invoke($this, $args);
}
}
}

18
classes/RouteUtil.php Normal file
View File

@ -0,0 +1,18 @@
<?php
class RouteUtil
{
public static function validateSubnet($subnet)
{
$parts = explode("/", $subnet);
if (count($parts) != 2) {
return false;
}
try {
$ipv4subnet = new IPv4Subnet($parts[0], $parts[1]);
return $parts[0] == $ipv4subnet->getFirstAddress();
} catch (Exception $e) {
return false;
}
}
}

View File

@ -0,0 +1,39 @@
<?php
class RoutingTableReader
{
private $routes = [];
/**
* @return array
*/
public function getRoutes(): array
{
return $this->routes;
}
public function __construct()
{
$result = @shell_exec("ip --json route show");
if (!$result) {
throw new RuntimeException("Failed to read routing table");
}
$this->routes = @json_decode($result, true);
if ($this->routes === null) {
throw new RuntimeException("Failed to parse json output");
}
foreach ($this->routes as $key => &$route) {
if ($route["dst"] === "default") {
$route["dst"] = "0.0.0.0/0";
} elseif (!str_contains($route["dst"], "/")) {
$route["dst"] .= "/32";
}
}
}
}

15
classes/StaticRPC.php Normal file
View File

@ -0,0 +1,15 @@
<?php
class StaticRPC extends RPC
{
public function __construct()
{
parent::__construct();
}
public function getPlugins(): array
{
return $this->plugins;
}
}

85
classes/WebRouter.php Normal file
View File

@ -0,0 +1,85 @@
<?php
class WebRouter
{
private string $requestBody;
private string $requestedFile;
private string $URI;
public function __construct()
{
$this->requestBody = @file_get_contents('php://input');
$this->requestedFile = dirname(__DIR__) . "/" . $_SERVER["REQUEST_URI"];
$this->URI = $_SERVER["REQUEST_URI"];
}
public function handleRequest(): void
{
@session_start();
try {
if (str_starts_with($this->URI, "/assets") or str_starts_with($this->URI, "/plugins") and str_ends_with($this->URI, ".js")) {
$this->handleAsset();
} elseif (!str_starts_with($this->URI, "/rpc")) {
$this->redirect("/assets/index.html");
} else {
$this->handleJRPC();
}
} finally {
session_write_close();
}
}
private function handleJRPC(): void
{
try {
$request = @json_decode($this->requestBody, true);
if ($request === null) {
throw new RuntimeException("Failed to parse JRPC");
}
foreach (["id", "jsonrpc", "method", "params"] as $param) {
if (!isset($request[$param])) {
throw new RuntimeException("Bad JRPC structure");
}
}
$rpc = new RPC();
$response = $rpc($request["method"], $request["params"]);
header("content-type: application/json");
echo json_encode([
"jsonrpc" => "2.0",
"id" => $request["id"] ?? 0,
"result" => $response
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
"jsonrpc" => "2.0",
"id" => $request["id"],
"error" => $e->getMessage()
]);
}
}
private function handleAsset(): void
{
if (!file_exists($this->requestedFile)) {
http_response_code(404);
echo "File not found: " . $this->URI;
} else {
header("content-type: " . mime_content_type($this->requestedFile));
echo file_get_contents($this->requestedFile);
}
}
private function redirect($where): void
{
http_response_code(302);
header("location: " . $where);
}
}

74
common.inc.php Normal file
View File

@ -0,0 +1,74 @@
<?php
if (!function_exists('mime_content_type')) {
function mime_content_type($filename): bool|string
{
$mime_types = array(
'txt' => 'text/plain',
'htm' => 'text/html',
'html' => 'text/html',
'php' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'xml' => 'application/xml',
'swf' => 'application/x-shockwave-flash',
'flv' => 'video/x-flv',
// images
'png' => 'image/png',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
'ico' => 'image/vnd.microsoft.icon',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
// archives
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
'exe' => 'application/x-msdownload',
'msi' => 'application/x-msdownload',
'cab' => 'application/vnd.ms-cab-compressed',
// audio/video
'mp3' => 'audio/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
// adobe
'pdf' => 'application/pdf',
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript',
// ms office
'doc' => 'application/msword',
'rtf' => 'application/rtf',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
// open office
'odt' => 'application/vnd.oasis.opendocument.text',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
);
$parts = explode('.', $filename);
$ext = strtolower(array_pop($parts));
if (array_key_exists($ext, $mime_types)) {
return $mime_types[$ext];
} elseif (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $filename);
finfo_close($finfo);
return $mimetype;
} else {
return 'application/octet-stream';
}
}
}

14
config.json.example Normal file
View File

@ -0,0 +1,14 @@
{
"password": {
"type": "plaintext",
"data": "admin"
},
"networks": [
"google"
],
"web": {
"port":8000,
"host":"0.0.0.0"
},
"plugins":["updates"]
}

15
loader.php Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/php
<?php
require_once __DIR__ . "/common.inc.php";
spl_autoload_register(function ($classname) {
require_once __DIR__ . "/classes/" . $classname . ".php";
});
if (isset($argv) and in_array("--init", $argv)) {
$rpc = new StaticRPC();
foreach ($rpc->getPlugins() as $plugin => $instance) {
/**
* @var IPluggable $instance
*/
$instance->onServerStarted();
}
}

35
networks/META.json Normal file
View File

@ -0,0 +1,35 @@
{
"description": "facebook, instagram, oculus",
"domains": [],
"networks": [
"213.102.128.0/24",
"204.15.20.0/22",
"199.201.0.0/16",
"185.89.0.0/16",
"185.60.216.0/21",
"179.60.0.0/16",
"173.252.0.0/16",
"173.194.10.0/24",
"164.163.191.64/26",
"163.70.0.0/16",
"157.240.0.0/16",
"147.75.0.0/16",
"142.250.0.0/15",
"129.134.0.0/16",
"103.4.0.0/16",
"102.221.0.0/16",
"102.132.0.0/16",
"99.84.0.0/16",
"87.245.208.0/24",
"77.240.43.0/24",
"74.119.0.0/16",
"69.171.0.0/16",
"69.63.0.0/16",
"66.220.0.0/16",
"57.141.0.0/16",
"57.144.222.0/24",
"45.64.0.0/16",
"45.130.4.0/24",
"31.13.0.0/16"
]
}

16
networks/broadcom.json Normal file
View File

@ -0,0 +1,16 @@
{
"description": "broadcom.com",
"domains": ["broadcom.com","omnissa.com"],
"networks": [
"172.66.0.165/32",
"162.159.140.167/32",
"54.68.22.26/32",
"50.112.202.115/32",
"52.13.171.212/32",
"2.19.183.16/32",
"2.19.183.47/32",
"129.153.117.201/32",
"23.73.4.0/24",
"184.50.200.7/32"
]
}

21
networks/cloudflare.json Normal file
View File

@ -0,0 +1,21 @@
{
"description": "cloudflare",
"domains": [],
"networks": [
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22"
]
}

View File

@ -1,8 +1,7 @@
{
"description": "amazon cloudfront",
"domains": [],
"ASN": [],
"subnets": [
"networks": [
"108.138.0.0/15",
"108.156.0.0/14",
"111.13.0.0/16",

View File

@ -3,8 +3,7 @@
"domains": [
"ultimaker.com"
],
"ASN": [],
"subnets": [
"networks": [
"188.114.98.0/23"
]
}

View File

@ -1,8 +1,7 @@
{
"description": "discord",
"domains": [],
"ASN": [],
"subnets": [
"networks": [
"104.16.0.0/12",
"108.177.14.207/32",
"138.128.140.240/28",

View File

@ -3,8 +3,7 @@
"domains": [
"flibusta.is"
],
"ASN": [],
"subnets": [
"networks": [
"179.43.150.83/32"
]
}

7
networks/github.json Normal file
View File

@ -0,0 +1,7 @@
{
"description": "facebook, instagram, oculus",
"domains": ["githubusercontent.com", "github.com"],
"networks": [
"185.199.0.0/16"
]
}

78
networks/google.json Normal file
View File

@ -0,0 +1,78 @@
{
"description": "google services (youtube, mail)",
"domains": [
"youtube.com",
"googlevideo.com",
"ytimg.com",
"youtu.be",
"ggpht.com",
"nhacmp3youtube.com",
"googleusercontent.com",
"googleapis.com",
"gstatic.com"
],
"networks": [
"188.43.61.0/24",
"157.240.252.0/23",
"87.245.216.0/24",
"85.249.244.0/23",
"213.221.56.0/27",
"104.154.0.0/15",
"104.196.0.0/14",
"104.237.160.0/19",
"104.237.160.0/19",
"107.167.160.0/19",
"107.178.192.0/18",
"108.170.192.0/18",
"108.177.0.0/17",
"108.59.80.0/20",
"130.211.0.0/16",
"136.124.0.0/15",
"136.22.0.0/16",
"142.250.0.0/15",
"146.148.0.0/17",
"152.65.0.0/16",
"162.120.128.0/17",
"162.216.148.0/22",
"162.222.176.0/21",
"172.110.32.0/21",
"172.217.0.0/16",
"172.253.0.0/16",
"173.194.0.0/16",
"173.255.112.0/20",
"178.66.83.0/24",
"192.158.28.0/22",
"192.178.0.0/15",
"193.186.4.0/24",
"195.95.178.0/24",
"199.192.112.0/22",
"199.223.232.0/21",
"199.36.154.0/23",
"199.36.156.0/24",
"207.223.160.0/20",
"208.117.224.0/19",
"208.65.152.0/22",
"208.68.108.0/22",
"208.81.188.0/22",
"209.85.0.0/16",
"216.239.32.0/19",
"216.58.192.0/19",
"216.73.80.0/20",
"23.236.48.0/20",
"23.251.128.0/19",
"34.0.0.0/7",
"57.140.192.0/18",
"64.15.112.0/20",
"64.233.0.0/16",
"66.102.0.0/20",
"66.22.228.0/23",
"66.249.64.0/19",
"70.32.128.0/19",
"72.14.192.0/18",
"74.125.0.0/16",
"8.34.208.0/20",
"8.35.192.0/20",
"8.8.4.0/24",
"8.8.8.0/24"
]
}

9
networks/habr.json Normal file
View File

@ -0,0 +1,9 @@
{
"description": "habrahabr",
"domains": [
"habr.com"
],
"networks": [
"178.248.237.68/32"
]
}

9
networks/intel.json Normal file
View File

@ -0,0 +1,9 @@
{
"description": "intel website",
"domains": [
"intel.com"
],
"networks": [
"23.42.171.108/32"
]
}

View File

@ -1,8 +1,7 @@
{
"description": "jetbrains market",
"domains": ["jetbrains.com"],
"ASN": [],
"subnets": [
"networks": [
"108.157.229.0/24",
"18.245.46.0/24",
"18.238.243.0/24",

7
networks/lostfilm.json Normal file
View File

@ -0,0 +1,7 @@
{
"description": "lostfilm",
"domains": [],
"networks": [
"104.21.0.0/17"
]
}

View File

@ -3,8 +3,7 @@
"domains": [
"notion.so"
],
"ASN": [],
"subnets": [
"networks": [
"104.18.39.102/32",
"172.64.148.154/32",
"208.103.161.0/30"

10
networks/public dns.json Normal file
View File

@ -0,0 +1,10 @@
{
"description": "google & cloudflare public dns",
"domains": [],
"networks": [
"1.1.1.1/32",
"1.0.0.1/32",
"8.8.8.8/32",
"8.8.4.4/32"
]
}

View File

@ -1,8 +1,7 @@
{
"description": "rutor",
"domains": ["rutor.info"],
"ASN": [],
"subnets": [
"networks": [
"193.46.255.29/32"
]
}

16
networks/rutracker.json Normal file
View File

@ -0,0 +1,16 @@
{
"description": "rutracker site & trackers",
"domains": [
"rutracker.org",
"rutracker.cc",
"t-ru.org"
],
"networks": [
"104.21.32.39/32",
"172.67.182.196/32",
"188.114.97.0/24",
"188.114.96.0/24",
"104.21.50.150/32",
"172.67.163.237/32"
]
}

9
networks/snapeda.json Normal file
View File

@ -0,0 +1,9 @@
{
"description": "www.snapeda.com",
"domains": ["www.snapeda.com"],
"networks": [
"104.26.0.159/32",
"104.26.1.159/32",
"172.67.71.169/32"
]
}

32
plugins/api/API.php Normal file
View File

@ -0,0 +1,32 @@
<?php
class API extends Plugin
{
public function onInit(PluginContext $context): void
{
parent::onInit($context);
if (!isset($this->config["key"])) {
$this->generateNewKey();
}
$headers = getallheaders();
if ($headers and isset($headers["X-Auth"]) and $headers["X-Auth"] == md5($this->config["key"])) {
$_SESSION["auth"] = true;
}
}
public function generateNewKey(): string
{
$this->config["key"] = sha1(rand() . uniqid());
$this->saveConfig();
return $this->config["key"];
}
public function getKey(): string
{
return $this->config["key"];
}
}

View File

@ -0,0 +1,5 @@
{
"class": "API",
"config": {
}
}

7
plugins/api/plugin.js Normal file
View File

@ -0,0 +1,7 @@
import {App} from "/assets/App.js";
(async function () {
let key = await App.RPC.__invoke("api::getKey");
$("body").append("<span>API Key: " + key + "</span>")
})();

35
plugins/custom/Custom.php Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/php
<?php
class Custom extends Plugin
{
public function onSync($remote_config)
{
if (!isset($remote_config["custom"])) {
return;
}
$this->config = $remote_config["custom"];
$this->saveConfig();
$this->updateConfigFile();
}
public function updateConfigFile()
{
$path = dirname(__DIR__, 2) . "/networks/custom.json";
$current_config = @file_get_contents($path);
$new_config = json_encode($this->config);
if($current_config !== $new_config) {
file_put_contents($path, $new_config);
}
}
public function onInit(PluginContext $context): void
{
parent::onInit($context);
$this->updateConfigFile();
}
}

View File

@ -0,0 +1,10 @@
{
"class": "Custom",
"config": {
"description": "Custom routes",
"domains": [
],
"networks": [
]
}
}

5
plugins/custom/plugin.js Normal file
View File

@ -0,0 +1,5 @@
import {App} from "/assets/App.js";
(async function(){
await App.RPC.__invoke("custom::updateConfigFile")
})();

View File

@ -0,0 +1,57 @@
<?php
class BindPlugin extends Plugin
{
public function restart(): string|bool|null
{
$selectedDomains = [];
$networks = (new NetworkConfigReader())->getConfigs();
//add new routes
foreach ($this->context->getConfig()["networks"] as $key) {
if (isset($networks[$key])) {
foreach ($networks[$key]["domains"] as $domain) {
$selectedDomains[] = $domain;
}
}
}
$selectedDomains = array_unique($selectedDomains);
$data = [];
foreach ($selectedDomains as $domain) {
$data[] = $this->createForwardRecord($domain);
}
file_put_contents($this->config["file"], implode("\n", $data));
return shell_exec($this->config["restart_cmd"]);
}
private function createForwardRecord($domain)
{
$fwd = implode(";", $this->config["forwarders"]) . ";";
return <<<TXT
zone "{$domain}" IN {
type forward;
forward only;
forwarders{{$fwd}};
};
TXT;
}
public function onServerStarted()
{
$this->restart();
}
public function onSync($remote_config)
{
$this->restart();
}
}

View File

@ -0,0 +1,8 @@
{
"class": "BindPlugin",
"config": {
"restart_cmd": "/etc/init.d/named restart",
"file": "/var/bind/forward.dns",
"forwarders": ["8.8.8.8", "1.1.1.1"]
}
}

21
plugins/named/plugin.js Normal file
View File

@ -0,0 +1,21 @@
import {App} from "/assets/App.js";
(async function () {
$("#buttons").append(`<button id="restart-bind">Restart named</button>`);
$("#restart-bind").click(function () {
const self = $(this);
self.prop("disabled", true);
(async function () {
try {
alert(await App.RPC.__invoke("named::restart"));
} finally {
setTimeout(() => {
self.prop("disabled", false);
}, 5000);
}
})();
});
})();

View File

@ -0,0 +1,79 @@
<?php
class Netsync extends Plugin
{
public function sync()
{
$host = $this->config["master"];
$key = $this->config["key"];
if (empty($key)) {
throw new RuntimeException("API key is empty");
}
$ch = curl_init("http://" . $host . "/rpc");
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"jsonrpc" => "2.0",
"id" => "1",
"method" => "getConfig",
"params" => []
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-Auth: " . md5($key),
"Content-type: application/json"
]);
$output = curl_exec($ch);
try {
if ($err = curl_error($ch)) {
throw new RuntimeException("Failed to fetch remote API: " . $err);
}
} finally {
curl_close($ch);
}
$header = @json_decode($output, true);
$remote_config = $header["result"] ?? null;
$networks = $remote_config["networks"] ?? null;
if ($remote_config === null or $networks === null) {
throw new RuntimeException("Response has invalid data");
}
$available = array_keys((new NetworkConfigReader())->getConfigs());
$remote_enabled = array_filter($networks, function ($e) use ($available) {
return in_array($e, $available);
});
$wrapper = $this->context->getConfig();
$local_enabled = $wrapper["networks"];
$diff = array_diff($remote_enabled, $local_enabled);
if (count($diff) > 0) {
$wrapper["networks"] = array_values($remote_enabled);
$wrapper->save();
}
$last_hash = $this->config["last_hash"] ?? "";
$current_hash = md5(json_encode($remote_config));
if ($last_hash != $current_hash) {
foreach ($this->context->getRPC()->getPlugins() as $plugin) {
/**
* @var IPluggable $plugin
*/
$plugin->onSync($remote_config);
}
$this->config["last_hash"] = $current_hash;
$this->saveConfig();
}
return $diff;
}
}

View File

@ -0,0 +1,7 @@
{
"class": "Netsync",
"config": {
"master": "127.0.0.1:8001",
"key": ""
}
}

21
plugins/netsync/plugin.js Normal file
View File

@ -0,0 +1,21 @@
import {App} from "/assets/App.js";
(async function () {
$("#buttons").append(`<button id="sync">Force sync</button>`);
$("#sync").click(function () {
const self = $(this);
self.prop("disabled", true);
(async function () {
try {
alert(await App.RPC.__invoke("netsync::sync"));
} finally {
setTimeout(() => {
location.reload();
}, 5000);
}
})();
});
})();

44
plugins/openvpn/Openvpn.php Executable file
View File

@ -0,0 +1,44 @@
#!/usr/bin/php
<?php
class Openvpn extends Plugin
{
public function restart()
{
//restart ovpn
return shell_exec($this->config["restart_cmd"]);
}
public function getRoutingConfig(): array
{
$networks = (new NetworkConfigReader())->getConfigs();
$data = [];
//add new routes
foreach ($this->context->getConfig() ["networks"] as $key) {
if (isset($networks[$key])) {
foreach ($networks[$key]["networks"] as $route) {
$parts = explode("/", $route);
$mask = long2ip(-1 << (32 - (int)$parts[1]));
$dst = $parts[0];
$data[] = "push \"route {$dst} {$mask}\"";
}
}
}
return $data;
}
public function onServerStarted()
{
$this->restart();
}
public function onSync($remote_config)
{
$this->restart();
}
}

View File

@ -0,0 +1,6 @@
{
"class": "Openvpn",
"config": {
"restart_cmd": "/etc/init.d/openvpn restart"
}
}

20
plugins/openvpn/plugin.js Normal file
View File

@ -0,0 +1,20 @@
import {App} from "/assets/App.js";
(async function () {
$("#buttons").append(`<button id="restart-ovpn">Restart openvpn server</button>`);
$("#restart-ovpn").click(function () {
if (confirm("Are you sure?")) {
const self = $(this);
self.prop("disabled", true);
(async function () {
try {
alert(await App.RPC.__invoke("openvpn::restart"));
} finally {
setTimeout(() => {
self.prop("disabled", false);
}, 5000);
}
})();
}
});
})();

View File

@ -0,0 +1,84 @@
<?php
class QuaggaPlugin extends Plugin
{
const REM_PREFIX = "! routes from file ";
public function restart(): string
{
$configfile = $this->config["file"];
if (!file_exists($configfile)) {
throw new RuntimeException("Quagga config file not found");
}
$networks = (new NetworkConfigReader())->getConfigs();
$routeParser = new RoutingTableReader();
$routes = $routeParser->getRoutes();
$defGatewayInterface = "";
$defGateway = "";
foreach ($routes as $route) {
if ($route["dst"] === "0.0.0.0/0") {
$defGatewayInterface = $route["dev"];
$defGateway = $route["gateway"];
break;
}
}
if (!$defGatewayInterface) {
throw new RuntimeException("Failed to detect default gateway interface");
}
$contents = file_get_contents($configfile);
$lines = explode("\n", $contents);
//remove existing routes
foreach ($lines as $key => $line) {
if (str_starts_with($line, self::REM_PREFIX) or str_starts_with($line, "ip route ") and str_contains($line . " ", $defGateway . " ")) {
unset($lines[$key]);
}
}
//add new routes
foreach ($this->context->getConfig()["networks"] as $key) {
$lines[] = self::REM_PREFIX . $key;
if (isset($networks[$key])) {
foreach ($networks[$key]["networks"] as $route) {
$lines[] = "ip route " . $route . " " . $defGateway;
}
}
}
foreach ($lines as $key => $line) {
if (trim($line) === "") {
unset($lines[$key]);
}
}
$backupFile = $configfile . ".sav";
unlink($backupFile);
rename($configfile, $backupFile);
file_put_contents($configfile, implode("\n", $lines));
sleep(10);
//restart zebra
return shell_exec($this->config["restart_cmd"]);
}
public function onServerStarted()
{
$this->restart();
}
public function onSync($remote_config)
{
$this->restart();
}
}

View File

@ -0,0 +1,7 @@
{
"class": "QuaggaPlugin",
"config": {
"restart_cmd": "/etc/init.d/zebra restart",
"file": "/etc/quagga/zebra.conf"
}
}

20
plugins/quagga/plugin.js Normal file
View File

@ -0,0 +1,20 @@
import {App} from "/assets/App.js";
(async function () {
$("#buttons").append(`<button id="restart-quagga">Restart quagga</button>`);
$("#restart-quagga").click(function () {
if (confirm("Are you sure?")) {
const self = $(this);
self.prop("disabled", true);
(async function () {
try {
alert(await App.RPC.__invoke("quagga::restart"));
} finally {
setTimeout(() => {
self.prop("disabled", false);
}, 5000);
}
})();
}
});
})();

View File

@ -0,0 +1,24 @@
<?php
class Updates extends Plugin
{
public function check(): ?bool
{
$data = str_replace("=", "", @shell_exec("git --no-pager fetch --dry-run --porcelain --verbose 2>&1 | grep refs"));
$parts = explode(" ", trim($data));
if (count($parts) < 3) {
return null;
}
return $parts[0] != $parts[1];
}
public function install(): string|bool|null
{
return @shell_exec("git --no-pager pull --verbose 2>&1");
}
}

View File

@ -0,0 +1,5 @@
{
"class": "Updates",
"config": {
}
}

22
plugins/updates/plugin.js Normal file
View File

@ -0,0 +1,22 @@
import {JSONRPC} from "/assets/jrpc.js";
(async function () {
$("#panel").prepend(`<div id="update-panel">Checking for updates...</div><hr>`);
let state = (await JSONRPC.__invoke("updates::check"));
if (state === null) {
$("#update-panel").html(`<span style="color:red;">Error checking updates</span>`);
} else if (state === false) {
$("#update-panel").html(`<span style="color:black;">There is no updates</span>`);
} else if (state === true) {
$("#update-panel").html(`<span style="color:green;">Some updates are available</span>&nbsp;<button>Update</button>`);
$("#update-panel button").click(async function () {
$("#panel").hide();
$("#loading").show().text("Installing updates...");
try {
alert(await JSONRPC.__invoke("updates::install"));
} finally {
setTimeout(() => location.reload(), 1000);
}
});
}
})();

View File

@ -1,15 +0,0 @@
{
"description": "MEGA.nz",
"domains": [
"mega.nz",
"mega.co.nz",
"mega.io"
],
"ASN": [
205809,
203055
],
"subnets": [
]
}

View File

@ -22,7 +22,7 @@
32934,
149642
],
"subnets": [
"networks": [
]
}

View File

@ -1,8 +0,0 @@
{
"description": "cloudflare",
"domains": [],
"ASN": [395747, 394536, 209242, 203898, 202623, 14789, 139242, 133877, 13335, 132892],
"subnets": [
]
}

View File

@ -1,12 +0,0 @@
{
"description": "factory AI",
"domains": [
"factory.ai"
],
"ASN": [],
"subnets": [
"76.76.21.0/24",
"66.33.60.0/24",
"216.150.1.1/32"
]
}

View File

@ -1,8 +0,0 @@
{
"description": "github",
"domains": ["githubusercontent.com", "github.com"],
"ASN": [36459],
"subnets": [
]
}

View File

@ -13,202 +13,13 @@
"gmail.com",
"doubleclick.net",
"google-analytics.com",
"docs.google.com",
"drive.google.com",
"blogger.com",
"android.com",
"firebaseapp.com",
"appspot.com",
"g.co",
"google.ac",
"google.ad",
"google.ae",
"google.al",
"google.am",
"google.as",
"google.at",
"google.az",
"google.ba",
"google.be",
"google.bf",
"google.bg",
"google.bi",
"google.bj",
"google.bs",
"google.bt",
"google.by",
"google.ca",
"google.cat",
"google.cd",
"google.cf",
"google.cg",
"google.ch",
"google.ci",
"google.cl",
"google.cm",
"google.cn",
"google.co.ao",
"google.co.bw",
"google.co.ck",
"google.co.cr",
"google.co.id",
"google.co.il",
"google.co.in",
"google.co.jp",
"google.co.ke",
"google.co.kr",
"google.co.ls",
"google.co.ma",
"google.co.mz",
"google.co.nz",
"google.co.th",
"google.co.tz",
"google.co.ug",
"google.co.uk",
"google.co.uz",
"google.co.ve",
"google.co.vi",
"google.co.za",
"google.co.zm",
"google.co.zw",
"google.com.af",
"google.com.ag",
"google.com.ai",
"google.com.ar",
"google.com.au",
"google.com.bd",
"google.com.bh",
"google.com.bn",
"google.com.bo",
"google.com.br",
"google.com.bz",
"google.com.co",
"google.com.cu",
"google.com.cy",
"google.com.do",
"google.com.ec",
"google.com.eg",
"google.com.et",
"google.com.fj",
"google.com.gh",
"google.com.gi",
"google.com.gt",
"google.com.hk",
"google.com.jm",
"google.com.jo",
"google.com.kh",
"google.com.kw",
"google.com.lb",
"google.com.ly",
"google.com.mm",
"google.com.mt",
"google.com.mx",
"google.com.my",
"google.com.na",
"google.com.ng",
"google.com.ni",
"google.com.np",
"google.com.om",
"google.com.pa",
"google.com.pe",
"google.com.pg",
"google.com.ph",
"google.com.pk",
"google.com.pr",
"google.com.py",
"google.com.qa",
"google.com.sa",
"google.com.sb",
"google.com.sg",
"google.com.sl",
"google.com.sv",
"google.com.tj",
"google.com.tr",
"google.com.tw",
"google.com.ua",
"google.com.uy",
"google.com.vc",
"google.com.vn",
"google.cv",
"google.cz",
"google.de",
"google.dj",
"google.dk",
"google.dm",
"google.dz",
"google.ee",
"google.es",
"google.fi",
"google.fm",
"google.fr",
"google.ga",
"google.ge",
"google.gg",
"google.gl",
"google.gm",
"google.gp",
"google.gr",
"google.gy",
"google.hn",
"google.hr",
"google.ht",
"google.hu",
"google.ie",
"google.im",
"google.iq",
"google.is",
"google.it",
"google.je",
"google.jo",
"google.kg",
"google.ki",
"google.kz",
"google.la",
"google.li",
"google.lk",
"google.lt",
"google.lu",
"google.lv",
"google.md",
"google.me",
"google.mg",
"google.mk",
"google.ml",
"google.mn",
"google.ms",
"google.mu",
"google.mv",
"google.mw",
"google.ne",
"google.nl",
"google.no",
"google.nr",
"google.nu",
"google.pl",
"google.pn",
"google.ps",
"google.pt",
"google.ro",
"google.rs",
"google.ru",
"google.rw",
"google.sc",
"google.se",
"google.sh",
"google.si",
"google.sk",
"google.sn",
"google.so",
"google.sr",
"google.st",
"google.td",
"google.tg",
"google.tl",
"google.tm",
"google.tn",
"google.to",
"google.tt",
"google.vg",
"google.vu",
"google.ws"
"g.co"
],
"ASN": [
6432,
@ -259,4 +70,4 @@
"74.221.128.0/20",
"151.193.0.0/16"
]
}
}

View File

@ -1,8 +0,0 @@
{
"description": "lostfilm",
"domains": ["lostfilm.tv", "lostfilm.one"],
"ASN": [],
"subnets": [
"104.21.0.0/17"
]
}

View File

@ -6,13 +6,12 @@
"t-ru.org"
],
"ASN": [],
"subnets": [
"networks": [
"104.21.32.39/32",
"172.67.182.196/32",
"188.114.97.0/24",
"188.114.96.0/24",
"104.21.50.150/32",
"172.67.163.237/32",
"188.186.154.0/24"
"172.67.163.237/32"
]
}

View File

@ -1,9 +0,0 @@
{
"description": "snapeda",
"domains": ["snapeda.com"],
"ASN": [],
"subnets": [
"104.20.16.0/20",
"172.66.144.0/20"
]
}

View File

@ -1,30 +0,0 @@
{
"description": "telegram",
"domains": [
"t.me",
"telegram.me",
"telegram.org",
"nicegram.app",
"telesco.pe",
"tg.dev"
],
"ASN": [
62041,
62014,
59930,
44907,
211157
],
"subnets": [
"91.108.4.0/22",
"91.108.8.0/22",
"91.108.12.0/22",
"91.108.16.0/22",
"91.108.20.0/22",
"91.108.56.0/22",
"91.105.192.0/23",
"95.161.64.0/20",
"149.154.160.0/20",
"185.76.151.0/24"
]
}

12
restart.php Normal file
View File

@ -0,0 +1,12 @@
#!/usr/bin/php
<?php
$memory = trim(shell_exec("free -b | grep Mem:"));
$memory = str_replace(" ", " ", $memory);
while (str_contains($memory, " ")) $memory = str_replace(" ", " ", $memory);
$items = explode(" ", $memory);
$available = intval($items[count($items) - 1]) / 1024 / 1024;
if ($available < 25) {
shell_exec("systemctl restart grass");
}

4
server.php Normal file
View File

@ -0,0 +1,4 @@
<?php
require_once __DIR__ . "/loader.php";
$router = new WebRouter();
$router->handleRequest();