Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
646 lines (550 sloc) 17.7 KB
<?php
/**
* Dynamic Proxy Node Reconfiguration
*
* Reconfigure and manage redundant proxy processes with no connection interruptions
* with an unlimited number of unique ACLs for individual proxy nodes using
* multiple destination IPs on a single server.
*
* @author Will Parsons
* @copyright 2019 Will Parsons
* @license https://github.com/parsonsbots/dynamic-proxy-node-reconfiguration/blob/master/LICENSE MIT License
* @link https://parsonsbots.com
* @link https://eightomic.com
*/
class DynamicProxyReconfiguration {
public $api;
public $processes;
public $shell;
public $sshPorts;
public function __construct($api, $processes, $shell, $sshPorts) {
$this->api = $api;
$this->processes = $processes;
$this->shell = $shell;
$this->sshPorts = $sshPorts;
}
/**
* Apply firewall
*
* @return boolean
*/
protected function _applyFirewall() {
if (file_exists('/scripts/pid/reconfigure.pid')) {
return false;
}
$this->gatewaysData = json_decode(file_get_contents('/scripts/cache/gatewaysData'), true);
if (empty($this->gatewaysData['data']['proxy_ips'])) {
return false;
}
$overridePorts = array(
'http' => $this->processes['http']
);
$firewallRules = $this->_configureFirewallRules(false, $overridePorts);
$this->_applyFirewallRules($firewallRules, 'elastic');
}
/**
* Apply firewall rules
*
* @param array $firewallRules Firewall rules
* @param string $ruleSet Firewall rule set
*
* @return
*/
protected function _applyFirewallRules($firewallRules, $ruleSet) {
unlink('/scripts/iptables/iptables-' . $ruleSet);
touch('/scripts/iptables/iptables-' . $ruleSet);
foreach ($firewallRules as $ruleChunk) {
$saveRules = implode("\n", $ruleChunk);
shell_exec('echo "' . $saveRules . '" >> /scripts/iptables/iptables-' . $ruleSet);
}
shell_exec('iptables-restore < /scripts/iptables/iptables-' . $ruleSet);
return;
}
/**
* Apply seamless processes reconfiguration
*
* @return boolean
*/
protected function _applyReconfiguration() {
$this->_createDirectories();
if (file_exists('/scripts/pid/reconfigure.pid')) {
$lastRan = file_get_contents('/scripts/pid/reconfigure.pid');
if ($lastRan < strtotime('-15 minutes')) {
unlink('/scripts/pid/reconfigure.pid');
} else {
return false;
}
}
$gatewaysJsonData = shell_exec("curl " . $this->api . " --connect-timeout 30");
$this->gatewaysData = json_decode($gatewaysJsonData, true);
if (
empty($this->gatewaysData['data']) ||
!is_dir('/etc/squid3')
) {
file_put_contents('/scripts/errors/api-error-' . time(), $this->gatewaysData);
unlink('/scripts/pid/reconfigure.pid');
return false;
}
file_put_contents('/scripts/cache/gatewaysData', $gatewaysJsonData);
file_put_contents('/scripts/pid/reconfigure.pid', time());
if (!empty($this->gatewaysData['data']['server'])) {
file_put_contents('/etc/sysctl.conf', implode("\n", $this->gatewaysData['data']['server']));
shell_exec('sysctl -p');
}
$proxyIps = $this->gatewaysData['data']['proxy_ips'];
if (empty($proxyIps[0])) {
unlink('/scripts/pid/reconfigure.pid');
return false;
}
shell_exec('rm -rf /etc/squid3/users/');
shell_exec('mkdir -m 777 /etc/squid3/users/');
if (!empty($this->gatewaysData['data']['http']['files'])) {
foreach ($this->gatewaysData['data']['http']['files'] as $file) {
shell_exec('mkdir -m 777 ' . str_replace(array('s.txt', 'd.txt'), '', $file['path']));
shell_exec('touch ' . $file['path']);
file_put_contents($file['path'], $file['contents']);
}
}
$firewallRules = $this->_configureFirewallRules(true);
shell_exec('rm /etc/squid3/proxy_ip_acl.conf');
shell_exec('touch /etc/squid3/proxy_ip_acl.conf');
file_put_contents('/etc/squid3/proxy_ip_acl.conf', $this->gatewaysData['data']['http']['acls']);
shell_exec('htpasswd -cb /etc/squid3/passwords default default');
shell_exec('htpasswd -D /etc/squid3/passwords default');
if (!empty($this->gatewaysData['data']['http']['users'])) {
foreach ($this->gatewaysData['data']['http']['users'] as $username => $password) {
shell_exec('htpasswd -b /etc/squid3/passwords ' . $username . ' ' . $password);
}
}
$this->_applyFirewallRules($firewallRules, 'redundant');
if (
!empty($this->gatewaysData['data']['socks']) &&
!empty($this->processes['socks'][0])
) {
$this->_reconfigure(
'socks',
'service 3proxy start',
'3proxy.cfg',
'/usr/local/etc/3proxy/3proxy.pid',
0,
20,
'/usr/local/etc/3proxy/3proxy.cfg',
$this->gatewaysData['data']['socks']
);
}
$this->_reconfigure(
'http',
'squid3 start',
'squid3',
'/var/run/squid3.pid',
0,
75,
'/etc/squid3/squid.conf',
str_replace('[pid]', 'pid_filename /var/run/squid3.pid', str_replace('[ports]', 'http_port ' . implode("\n" . 'http_port ', $this->processes['http'][0]), $this->gatewaysData['data']['http']['configuration']))
);
$redundantProcesses = $this->processes['http'];
unset($redundantProcesses[0]);
$redundantProcessChunks = array_chunk($redundantProcesses, round(count($redundantProcesses) / 2), true);
foreach ($redundantProcessChunks as $redundantProcessChunk) {
$activeRedundantProcesses = array_keys($redundantProcesses);
reset($redundantProcessChunk);
$redundantProcessStart = key($redundantProcessChunk);
end($redundantProcessChunk);
$redundantProcessEnd = key($redundantProcessChunk);
$redundantProcessRange = range($redundantProcessStart, $redundantProcessEnd);
foreach ($redundantProcessRange as $redundantProcess) {
unset($activeRedundantProcesses[$redundantProcess - 1]);
}
$activeRedundantPorts = array();
foreach ($redundantProcesses as $key => $redundantProcess) {
if (in_array($key, $activeRedundantProcesses)) {
$activeRedundantPorts = array_merge($activeRedundantPorts, $redundantProcesses[$key]);
}
}
$overridePorts = array(
'http' => array(array_merge(array(
'80',
'8888',
'55555'
)), $activeRedundantPorts),
'socks' => array(
array(
'1090'
)
)
);
$firewallRules = $this->_configureFirewallRules(false, $overridePorts);
$this->_applyFirewallRules($firewallRules, 'elastic');
foreach ($redundantProcessRange as $redundantProcess) {
$this->_reconfigure(
'http',
'squid3-redundant' . $redundantProcess . ' start -f /etc/squid3/squid-redundant' . $redundantProcess . '.conf',
'squid3-redundant' . $redundantProcess,
'/var/run/squid-redundant' . $redundantProcess . '.pid',
0,
0,
'/etc/squid3/squid-redundant' . $redundantProcess . '.conf',
str_replace('[pid]', 'pid_filename /var/run/squid-redundant' . $redundantProcess . '.pid', str_replace('[ports]', 'http_port ' . implode("\n" . 'http_port ', $this->processes['http'][$redundantProcess]), $this->gatewaysData['data']['http']['configuration']))
);
}
sleep(75);
}
$overridePorts = array(
'http' => $this->processes['http']
);
$firewallRules = $this->_configureFirewallRules(false, $overridePorts);
$this->_applyFirewallRules($firewallRules, 'elastic');
if (!empty($this->gatewaysData['data']['socks-redundant'])) {
$this->_reconfigure(
'socks',
'service 3proxy-redundant start',
'3proxy-redundant.cfg',
'/usr/local/etc/3proxy/3proxy-redundant.pid',
0,
40,
'/usr/local/etc/3proxy/3proxy-redundant.cfg',
$this->gatewaysData['data']['socks-redundant']
);
}
$this->_applyFirewallRules($firewallRules, 'elastic');
unlink('/scripts/pid/reconfigure.pid');
return true;
}
/**
* DNS redundancy health checks and process recovery
*
* @return boolean
*/
protected function _checkDNS() {
if (file_exists('/scripts/pid/dns.pid')) {
$lastRan = file_get_contents('/scripts/pid/dns.pid');
if ($lastRan < strtotime('-2 minutes')) {
unlink('/scripts/pid/dns.pid');
} else {
return false;
}
}
file_put_contents('/scripts/pid/dns.pid', time());
$this->gatewaysData = json_decode(file_get_contents('/scripts/cache/gatewaysData'), true);
array_shift($this->gatewaysData['data']['dns_ips']);
if (empty($this->gatewaysData['data']['dns_ips'])) {
return false;
}
$dnsIps = array_values($this->gatewaysData['data']['dns_ips']);
foreach ($dnsIps as $key => $dnsIp) {
$processName = $key == 0 ? 'named' : 'named-redundant' . $key;
$dnsResponse = array();
exec('dig +time=2 +tries=1 proxies @' . $dnsIp . ' 2>&1', $dnsResponse);
if (
!empty($dnsResponse[3]) &&
strpos(strtolower($dnsResponse[3]), 'got answer') === false
) {
$dnsProcesses = array();
exec('ps $(pgrep named) 2>&1', $dnsProcesses);
if (!empty($dnsProcesses)) {
foreach ($dnsProcesses as $dnsProcess) {
$dnsProcess = array_map('strtolower', array_map('trim', array_values(array_filter(explode(' ', $dnsProcess)))));
if (
!empty($dnsProcess[0]) &&
is_numeric($dnsProcess[0]) &&
in_array('/usr/sbin/' . $processName, $dnsProcess)
) {
$killProcesses = array();
$shellCommands = array(
'#!' . $this->shell,
'kill -9 ' . trim($dnsProcess[0])
);
unlink('/scripts/dns.sh');
file_put_contents('/scripts/dns.sh', implode("\n", $shellCommands));
shell_exec('chmod +x /scripts/dns.sh');
shell_exec('/scripts/./dns.sh');
}
}
}
shell_exec('service ' . str_replace('named', 'bind9', $processName) . ' start');
sleep(1);
unlink('/scripts/pid/dns.pid');
$this->_checkDNS();
}
}
unlink('/scripts/pid/dns.pid');
return true;
}
/**
* Check HTTP and SOCKS ports
*
* @param string $ip Proxy IP
* @param string $port Proxy port
* @param string $port Proxy protocol (http or socks)
* @param integer $integer Request timeout
*
* @return boolean $alive True if port is active, false if refusing connections
*/
protected function _checkPort($ip, $port, $protocol, $timeout = 5) {
$alive = false;
switch ($protocol) {
case 'http':
$response = shell_exec('curl -I -s -x ' . $ip . ':' . $port . ' http://squid -v --connect-timeout ' . $timeout . ' --max-time ' . $timeout);
if ($this->_strposa(strtolower($response), array(
'407 proxy',
'403 forbidden',
' 503 ',
' timed out '
)) !== false) {
$alive = true;
}
break;
case 'socks':
exec('curl --socks5-hostname ' . $ip . ':' . $port . ' http://socks/ -v --connect-timeout ' . $timeout . ' --max-time ' . $timeout . ' 2>&1', $socksResponse);
$socksResponse = end($socksResponse);
$alive = (strpos(strtolower($socksResponse), 'empty reply ') !== false);
break;
}
return $alive;
}
/**
* Configure firewall rules
*
* @param boolean $redundant Route to redundant process ports only if true, all ports if false
* @param array $overridePorts Override default base ports
*
* @return array $rules Firewall rules
*/
protected function _configureFirewallRules($redundant = false, $overridePorts = array()) {
$rules = array(
'*filter',
':INPUT ACCEPT [0:0]',
':FORWARD ACCEPT [0:0]',
':OUTPUT ACCEPT [0:0]',
'-A INPUT -p icmp -m hashlimit --hashlimit-name icmp --hashlimit-mode srcip --hashlimit 1/second --hashlimit-burst 2 -j ACCEPT'
);
if (
!empty($this->sshPorts) &&
is_array($this->sshPorts)
) {
foreach ($this->sshPorts as $sshPort) {
if (is_numeric($sshPort)) {
$rules[] = '-A INPUT -p tcp -m tcp --dport ' . $sshPort . ' -m connlimit --connlimit-above 4 --connlimit-mask 32 --connlimit-saddr -j DROP';
$rules[] = '-A INPUT -p tcp -m tcp --dport ' . $sshPort . ' -m hashlimit --hashlimit-upto 15/hour --hashlimit-burst 3 --hashlimit-mode srcip --hashlimit-name ssh --hashlimit-htable-expire 500000 -j ACCEPT';
}
}
}
if (
!empty($this->gatewaysData['data']['firewall_filter']) &&
is_array($this->gatewaysData['data']['firewall_filter'])
) {
foreach ($this->gatewaysData['data']['firewall_filter'] as $rule) {
$rules[] = $rule;
}
}
if (
!empty($this->gatewaysData['data']['dns_ips']) &&
is_array($this->gatewaysData['data']['dns_ips'])
) {
$rules[] = '-A OUTPUT -d ' . implode(',', $this->gatewaysData['data']['dns_ips']) . ' -p udp -m udp -j ACCEPT';
}
$rules[] = '-A OUTPUT -s 127.0.0.0/24 -p udp -j DROP';
$rules[] = 'COMMIT';
$rules[] = '*nat';
$rules[] = ':PREROUTING ACCEPT [0:0]';
$rules[] = ':INPUT ACCEPT [0:0]';
$rules[] = ':OUTPUT ACCEPT [0:0]';
$rules[] = ':POSTROUTING ACCEPT [0:0]';
$dnsIps = array_values($this->gatewaysData['data']['dns_ips']);
unset($dnsIps[0]);
krsort($dnsIps);
foreach ($dnsIps as $key => $dnsIp) {
$loadBalancer = '';
if ($key > 1) {
$loadBalancer = '-m statistic --mode nth --every ' . $key . ' --packet 0 ';
}
$rules[] = '-A OUTPUT -d 127.0.0.1/32 -p udp -m udp --dport 53 ' . $loadBalancer . '-j DNAT --to-destination ' . $dnsIp;
}
if (
!empty($this->processes) &&
is_array($this->processes)
) {
foreach ($this->processes as $protocol => $processes) {
$basePorts = array();
if ($redundant) {
unset($processes[0]);
$processes = array_values($processes);
}
if (!empty($overridePorts)) {
if (empty($overridePorts[$protocol])) {
continue;
}
$processes = $overridePorts[$protocol];
}
foreach ($processes as $ports) {
$basePorts = array_merge($ports, $basePorts);
}
$ports = array_unique($basePorts);
foreach ($ports as $key => $port) {
if ($this->_checkPort($this->gatewaysData['data']['proxy_ips'][0], $port, $protocol) === false) {
unset($ports[$key]);
}
}
shuffle($ports);
$ports = array_values($ports);
krsort($ports);
$dports = array_merge($this->processes[$protocol][0], $basePorts);
$dportsSplit = array_chunk($dports, '5');
if (!empty($ports)) {
foreach ($dportsSplit as $chunk => $dports) {
$dports = implode(',', $dports);
foreach ($ports as $key => $port) {
$loadBalancer = '';
if ($key > 0) {
$loadBalancer = '-m statistic --mode nth --every ' . ($key + 1) . ' --packet 0 ';
}
$rules[] = '-A PREROUTING -p tcp -m multiport --dports ' . $dports . ' ' . $loadBalancer . '-j DNAT --to-destination :' . $port . ' --persistent';
}
}
}
}
}
$rules[] = 'COMMIT';
$rules = array_chunk($rules, 100);
return $rules;
}
/**
* Create writable log and cache directories
*
* @return
*/
protected function _createDirectories() {
$directories = array(
'/scripts',
'/scripts/cache',
'/scripts/errors',
'/scripts/iptables',
'/scripts/pid'
);
foreach ($directories as $directory) {
if (!is_dir($directory)) {
shell_exec('mkdir -m 777 ' . $directory);
}
}
return;
}
/**
* Get process IDs from process name
*
* @param string $processName Process name
* @param string $configurationFile Full path to process configuration file
*
* @return array $processIds Process IDs
*/
protected function _getProcessIds($processName, $configurationFile) {
$processIds = array();
exec('ps -fC ' . $processName . ' 2>&1', $processes);
if (!empty($processes[0])) {
unset($processes[0]);
foreach ($processes as $process) {
$processColumns = array_values(array_filter(explode(' ', $process)));
if (
!empty($processColumns[1]) &&
(
empty($processes[2]) ||
strpos($configurationFile, '-redundant') === false ||
$this->_strposa($process, array(
$processName . ' ',
$configurationFile
)) !== false
)
) {
$processIds[] = $processColumns[1];
}
}
}
return $processIds;
}
/**
* Reconfigure specific process
*
* @param string $protocol Proxy protocol (http or socks)
* @param string $startCommand Command to start process
* @param string $processName Process name
* @param string $processId Full path to process ID
* @param integer $delayStart Delay at beginning of reconfiguration
* @param integer $delayEnd Delay at end of reconfiguration
* @param string $configurationFile Full path to process configuration file
* @param string $configurationData Process configuration data
*
* @return
*/
protected function _reconfigure($protocol, $startCommand, $processName, $processId, $delayStart = 0, $delayEnd = 0, $configurationFile, $configurationData) {
if ($delayStart) {
sleep($delayStart);
}
$killProcesses = array();
$shellCommands = array(
'#!' . $this->shell
);
$killProcesses = $this->_getProcessIds($processName, $configurationFile);
foreach ($killProcesses as $killProcess) {
$shellCommands[] = 'kill -9 ' . trim($killProcess);
}
if (count($shellCommands > 1)) {
unlink('/scripts/' . $protocol . '.sh');
file_put_contents('/scripts/' . $protocol . '.sh', implode("\n", $shellCommands));
shell_exec('chmod +x /scripts/' . $protocol . '.sh');
shell_exec('/scripts/./' . $protocol . '.sh');
}
if (
!empty($configurationFile) &&
!empty($configurationData)
) {
file_put_contents($configurationFile, $configurationData);
}
unlink($processId);
sleep(2);
shell_exec($startCommand);
if ($delayEnd) {
sleep($delayEnd);
}
}
/**
* Initiate processes
*
* @param string $processName Process name
*
* @return boolean $status
*/
public function start($processName) {
switch ($processName) {
case 'apply_firewall':
$status = $this->_applyFirewall();
break;
case 'apply_reconfiguration':
$status = $this->_applyReconfiguration();
break;
case 'check_dns':
$status = $this->_checkDNS();
break;
}
return $status;
}
/**
* Format strpos to use array as needle
*
* @param array $haystack
* @param array $needles
* @param integer $offset
*
* @return boolean True if match is found, false if no match
*/
protected function _strposa($haystack, $needles, $offset = 0) {
if (!is_array($needles)) {
$needles = array($needles);
};
foreach ($needles as $needle) {
if (strpos($haystack, $needle, $offset) !== false) {
return true;
}
}
return false;
}
}
?>
You can’t perform that action at this time.