Merge branch 'master' into refactor/offsite-sign-in-base-os-requirement

This commit is contained in:
Zack Spear
2022-04-20 16:38:03 -07:00
24 changed files with 1057 additions and 628 deletions
+278 -87
View File
@@ -1,7 +1,21 @@
<?php
// incuded in login.php
// Included in login.php
function fileRead($file) {
// Only start a session to check if they have a cookie that looks like our session
$server_name = strtok($_SERVER['HTTP_HOST'],":");
if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
// Start the session so we can check if $_SESSION has data
session_start();
// Check if the user is already logged in
if ($_SESSION && !empty($_SESSION['unraid_user'])) {
// If so redirect them to the start page
header("Location: /".$var['START_PAGE']);
exit;
}
}
function readFromFile($file): string {
$text = "";
if (file_exists($file)) {
$fp = fopen($file,"r");
@@ -13,7 +27,8 @@ function fileRead($file) {
}
return $text;
}
function fileAppend($file, $text) {
function appendToFile($file, $text): void {
$fp = fopen($file,"a");
if (flock($fp, LOCK_EX)) {
fwrite($fp, $text);
@@ -22,7 +37,8 @@ function fileAppend($file, $text) {
fclose($fp);
}
}
function fileWrite($file, $text) {
function writeToFile($file, $text): void {
$fp = fopen($file,"w");
if (flock($fp, LOCK_EX)) {
fwrite($fp, $text);
@@ -32,74 +48,198 @@ function fileWrite($file, $text) {
}
}
$maxfails = 3;
$cooldown = 15*60;
$remote_addr = $_SERVER['REMOTE_ADDR'] ?? "unknown";
$failfile = "/var/log/pwfail/{$remote_addr}";
if (!empty($_POST['username']) && !empty($_POST['password'])) {
@mkdir("/var/log/pwfail/", 0755);
$failtext = fileRead($failfile);
$fails = explode("\n", trim($failtext));
$time = time();
// remove entries older than $cooldown minutes
$updatefails = false;
foreach ((array) $fails as $key => $value) {
if ($value && $time - $value > $cooldown) {
unset ($fails[$key]);
$updatefails = true;
}
}
if ($updatefails) {
$failtext = implode("\n", $fails);
fileWrite($failfile, $failtext);
}
if (count($fails) >= $maxfails) {
$error = _('Too many invalid login attempts');
if (count($fails) == $maxfails)
exec("logger -t webGUI ".escapeshellarg("Ignoring login attempts for {$_POST['username']} from {$remote_addr}"));
} else {
// User Login attempt, validate credentials
if (($_POST['username'] == "root")) {
// more: integrate with PAM to avoid direct access to /etc/shadow and validate other user names (future)
$output = exec("/usr/bin/grep root /etc/shadow");
if ($output !== false) {
$strCredentials = explode(":", $output);
if (password_verify($_POST['password'], $strCredentials[1])) {
// Successful login, start session
@unlink($failfile);
session_start();
$_SESSION['unraid_login'] = time();
$_SESSION['unraid_user'] = $_POST['username'];
session_regenerate_id(true);
session_write_close();
exec("logger -t webGUI ".escapeshellarg("Successful login user {$_POST['username']} from {$remote_addr}"));
header("Location: /".$var['START_PAGE']);
exit;
}
}
}
// Invalid login
$error = _('Invalid Username or Password');
exec("logger -t webGUI ".escapeshellarg("Unsuccessful login user {$_POST['username']} from {$remote_addr}"));
}
fileAppend($failfile, $time."\n");
// Source: https://stackoverflow.com/a/2524761
function isValidTimeStamp($timestamp)
{
return ((string) (int) $timestamp === $timestamp)
&& ($timestamp <= PHP_INT_MAX)
&& ($timestamp >= ~PHP_INT_MAX);
}
function cleanupFails(string $failFile, int $time): int {
global $cooldown;
// Read existing fails
@mkdir(dirname($failFile), 0755);
$failText = readFromFile($failFile);
$fails = explode("\n", trim($failText));
// Remove entries older than $cooldown minutes, and entries that are not timestamps
$updateFails = false;
foreach ((array) $fails as $key => $value) {
if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) {
unset ($fails[$key]);
$updateFails = true;
}
}
// Save fails to disk
if ($updateFails) {
$failText = implode("\n", $fails)."\n";
writeToFile($failFile, $failText);
}
return count($fails);
}
function verifyUsernamePassword(string $username, string $password): bool {
if ($username != "root") return false;
// @TODO: integrate with PAM to avoid direct access to /etc/shadow and validate other user names
$output = exec("/usr/bin/grep root /etc/shadow");
if ($output === false) return false;
$credentials = explode(":", $output);
return password_verify($password, $credentials[1]);
}
function verifyTwoFactorToken(string $username, string $token): bool {
try {
// Create curl client
$curlClient = curl_init();
curl_setopt($curlClient, CURLOPT_HEADER, true);
curl_setopt($curlClient, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlClient, CURLOPT_UNIX_SOCKET_PATH, '/var/run/unraid-api.sock');
curl_setopt($curlClient, CURLOPT_URL, 'http://unixsocket/verify');
curl_setopt($curlClient, CURLOPT_BUFFERSIZE, 256);
curl_setopt($curlClient, CURLOPT_TIMEOUT, 5);
curl_setopt($curlClient, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Origin: /var/run/unraid-notifications.sock'));
curl_setopt($curlClient, CURLOPT_POSTFIELDS, json_encode([
'username' => $username,
'token' => $token
]));
// Send the request
curl_exec($curlClient);
// Get the http status code
$httpCode = curl_getinfo($curlClient, CURLINFO_HTTP_CODE);
// Close the connection
curl_close($curlClient);
// Error
// This should accept 200 or 204 status codes
if ($httpCode !== 200 && $httpCode !== 204) {
// Log error to syslog
exec("logger -t webGUI ".escapeshellarg("2FA code for {$username} is invalid, blocking access!"));
return false;
}
// Log success to syslog
exec("logger -t webGUI ".escapeshellarg("2FA code for {$username} is valid, allowing login!"));
// Success
return true;
} catch (Exception $exception) {
// Error
return false;
}
}
// Check if a haystack ends in a needle
function endsWith($haystack, $needle): bool {
return substr_compare($haystack, $needle, -strlen($needle)) === 0;
}
// Check if we're accessing this via a wildcard cert
function isWildcardCert(): bool {
global $server_name;
return endsWith($server_name, '.myunraid.net');
}
// Check if we're accessing this locally via the expected myunraid.net url
function isLocalAccess(): bool {
global $nginx, $server_name;
return isWildcardCert() && $nginx['NGINX_LANFQDN'] === $server_name;
}
// Check if we're accessing this remotely via the expected myunraid.net url
function isRemoteAccess(): bool {
global $nginx, $server_name;
return isWildcardCert() && $nginx['NGINX_WANFQDN'] === $server_name;
}
// Check if 2fa is enabled for local (requires USE_SSL to be "auto" so no alternate urls can access the server)
function isLocalTwoFactorEnabled(): bool {
global $nginx, $my_servers;
return $nginx['NGINX_USESSL'] === "auto" && $my_servers['local']['2Fa'] === 'yes';
}
// Check if 2fa is enabled for remote
function isRemoteTwoFactorEnabled(): bool {
global $my_servers;
return $my_servers['remote']['2Fa'] === 'yes';
}
// Load configs into memory
$my_servers = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
$nginx = @parse_ini_file('/var/local/emhttp/nginx.ini');
// Vars
$maxFails = 3;
$cooldown = 15 * 60; // 15 mins
$remote_addr = $_SERVER['REMOTE_ADDR'] ?? "unknown";
$failFile = "/var/log/pwfail/{$remote_addr}";
// Get the credentials
$username = $_POST['username'];
$password = $_POST['password'];
$token = $_REQUEST['token'];
// Check if we need 2fa
$twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled());
// If we have a username + password combo attempt to login
if (!empty($username) && !empty($password)) {
try {
// Bail if we're missing the 2FA token and we expect one
if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected'));
// Read existing fails, cleanup expired ones
$time = time();
$failCount = cleanupFails($failFile, $time);
// Check if we're limited
if ($failCount >= $maxFails) {
if ($failCount == $maxFails) exec("logger -t webGUI ".escapeshellarg("Ignoring login attempts for {$username} from {$remote_addr}"));
throw new Exception(_('Too many invalid login attempts'));
}
// Bail if username + password combo doesn't work
if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password'));
// Bail if we need a token but it's invalid
if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
// Successful login, start session
@unlink($failFile);
session_start();
$_SESSION['unraid_login'] = time();
$_SESSION['unraid_user'] = $username;
session_regenerate_id(true);
session_write_close();
exec("logger -t webGUI ".escapeshellarg("Successful login user {$username} from {$remote_addr}"));
// Redirect the user to the start page
header("Location: /".$var['START_PAGE']);
exit;
} catch (Exception $exception) {
// Set error message
$error = $exception->getMessage();
// Log error to syslog
exec("logger -t webGUI ".escapeshellarg("Unsuccessful login user {$username} from {$remote_addr}"));
appendToFile($failFile, $time."\n");
}
}
$boot = "/boot/config/plugins/dynamix";
$myfile = "case-model.cfg";
$mycase = file_exists("$boot/$myfile") ? file_get_contents("$boot/$myfile") : false;
$myFile = "case-model.cfg";
$myCase = file_exists("$boot/$myFile") ? file_get_contents("$boot/$myFile") : false;
extract(parse_plugin_cfg('dynamix',true));
$theme_dark = in_array($display['theme'],['black','gray']);
extract(parse_plugin_cfg('dynamix', true));
$theme_dark = in_array($display['theme'], ['black', 'gray']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
@@ -137,8 +277,8 @@ $theme_dark = in_array($display['theme'],['black','gray']);
/
/************************/
body {
background: #<?=$theme_dark?'1C1B1B':'F2F2F2'?>;
color: #<?=$theme_dark?'fff':'1c1b1b'?>;
background: <?=$theme_dark?'#1C1B1B':'#F2F2F2'?>;
color: <?=$theme_dark?'#fff':'#1c1b1b'?>;
font-family: clear-sans, sans-serif;
font-size: .875rem;
padding: 0;
@@ -215,14 +355,19 @@ $theme_dark = in_array($display['theme'],['black','gray']);
/************************
/
/ Login spesific styling
/ Login specific styling
/
/************************/
#login {
width: 500px;
margin: 6rem auto;
border-radius: 10px;
background: #<?=$theme_dark?'2B2A29':'fff'?>;
background: <?=$theme_dark?'#2B2A29':'#fff'?>;
}
#login::after {
content: "";
clear: both;
display: table;
}
#login .logo {
position: relative;
@@ -281,7 +426,7 @@ $theme_dark = in_array($display['theme'],['black','gray']);
-webkit-box-shadow: 0 2px 8px 0 rgba(0,0,0,.12);
box-shadow: 0 2px 8px 0 rgba(0,0,0,.12);
}
.hidden { display: none; }
/************************
/
/ Cases
@@ -309,7 +454,7 @@ $theme_dark = in_array($display['theme'],['black','gray']);
/************************/
@media (max-width: 500px) {
body {
background: #<?=$theme_dark?'2B2A29':'fff'?>;
background: <?=$theme_dark?'#2B2A29':'#fff'?>;
}
[type=email], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], textarea {
font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
@@ -345,23 +490,41 @@ $theme_dark = in_array($display['theme'],['black','gray']);
</h2>
<div class="case">
<?if ($mycase):?>
<?if (substr($mycase,-4)!='.png'):?>
<span class='case-<?=$mycase?>'></span>
<?if ($myCase):?>
<?if (substr($myCase,-4)!='.png'):?>
<span class='case-<?=$myCase?>'></span>
<?else:?>
<img src='<?=autov("/webGui/images/$mycase")?>'>
<img src='<?=autov("/webGui/images/$myCase")?>'>
<?endif;?>
<?endif;?>
</div>
<div class="form">
<form action="/login" method="POST">
<p>
<input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
<input name="password" type="password" placeholder="<?=_('Password')?>" required>
</p>
<?if ($error) echo "<p class='error'>$error</p>";?>
<form class="js-removeTimeout" action="/login" method="POST">
<? if (($twoFactorRequired && $token) || !$twoFactorRequired) { ?>
<p>
<input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
<input name="password" type="password" placeholder="<?=_('Password')?>" required>
<input name="token" type="hidden" value="<?= $token ?>">
</p>
<? if ($error) echo "<p class='error'>$error</p>"; ?>
<p>
<button type="submit" class="button button--small"><?=_('Login')?></button>
</p>
<? } else { ?>
<? if ($error) { ?>
<div>
<p class="error" style="padding-top:10px;"><?= $error ?></p>
</div>
<? } else { ?>
<div>
<p class="error" style="padding-top:10px;" title="<?= _('Please access this server via the My Servers Dashboard') ?>"><?= _('No 2FA token detected') ?></p>
</div>
<? } ?>
<div>
<a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
</div>
<? } ?>
<script type="text/javascript">
document.cookie = "cookietest=1";
cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
@@ -370,14 +533,42 @@ $theme_dark = in_array($display['theme'],['black','gray']);
document.write("<p class='error'><?=_('Browser cookie support required for Unraid OS webgui')?></p>");
}
</script>
<p>
<button type="submit" class="button button--small"><?=_('Login')?></button>
</p>
</form>
<? if (($twoFactorRequired && $token) || !$twoFactorRequired) { ?>
<div class="js-addTimeout hidden">
<p class="error" style="padding-top:10px;"><?=_('Transparent 2FA Token timed out')?></p>
<a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
</div>
<? } ?>
</div>
<p><a href="https://wiki.unraid.net/Manual/Troubleshooting#Lost_root_Password" target="_blank"><?=_('Password recovery')?></a></p>
<? if (($twoFactorRequired && $token) || !$twoFactorRequired) { ?>
<p class="js-removeTimeout"><a href="https://wiki.unraid.net/Manual/Troubleshooting#Lost_root_Password" target="_blank"><?=_('Password recovery')?></a></p>
<? } ?>
</div>
</section>
<? if ($token) { ?>
<script type="text/javascript">
const $elsToRemove = document.querySelectorAll('.js-removeTimeout');
const $elsToShow = document.querySelectorAll('.js-addTimeout');
/**
* A user can manually refresh the page or submit with the wrong username/password
* the t2fa token will be re-used on these page refreshes. We need to keep track of the timeout across potential page
* loads rather than setting the timer with a fresh timeout each page load
*/
const tokenName = '<?=$token?>'.slice(-20);
const ts = Date.now();
const timeoutStarted = sessionStorage.getItem(tokenName) ? Number(sessionStorage.getItem(tokenName)) : ts;
const timeoutDiff = ts - timeoutStarted; // current timestamp minus timestamp when token first set
const timeoutMS = 297000 - timeoutDiff; // 5 minutes minus 3seconds or (5*60)*1000ms - 3000ms = 297000
sessionStorage.setItem(tokenName, timeoutStarted);
const tokenTimeout = setTimeout(() => {
$elsToRemove.forEach(z => z.remove()); // remove elements
$elsToShow.forEach(z => z.classList.remove('hidden')); // add elements
}, timeoutMS); // if timeoutMS is negative value the timeout will trigger immediately
</script>
<? } ?>
</body>
</html>
@@ -161,9 +161,10 @@ function refresh(top) {
location.reload();
}
}
function initab() {
function initab(page) {
$.removeCookie('one');
$.removeCookie('tab');
if (page != null) location.replace(page);
}
function settab(tab) {
<?switch ($myPage['name']):?>
@@ -421,7 +422,7 @@ foreach ($tasks as $button) {
$page = $button['name'];
echo "<div id='nav-item'";
echo $task==$page ? " class='active'>" : ">";
echo "<a href=\"/$page\" onclick='initab();window.location=\"/$page\"'>"._($button['Name'] ?? $page)."</a></div>";
echo "<a href=\"/$page\" onclick=\"initab('/$page')\">"._($button['Name'] ?? $page)."</a></div>";
// create list of nchan scripts to be started
if (isset($button['Nchan'])) nchan_merge($button['root'], $button['Nchan']);
}
+5 -4
View File
@@ -68,7 +68,7 @@ $myDisks = array_filter(array_diff(array_keys($disks), explode(',',$var['shareUs
// Share size per disk
$ssz2 = [];
if ($fill)
foreach (glob("state/*.ssz2", GLOB_NOSORT) as $entry) $ssz2[basename($entry, ".ssz2")] = parse_ini_file($entry);
foreach (glob("state/*.ssz2", GLOB_NOSORT) as $entry) $ssz2[basename($entry, ".ssz2")] = file($entry,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
else
exec("rm -f /var/local/emhttp/*.ssz2");
@@ -92,7 +92,7 @@ foreach ($disks as $name => $disk) {
echo "<td>{$disk['comment']}</td>";
echo "<td>".disk_share_settings($var['shareSMBEnabled'], $sec[$name])."</td>";
echo "<td>".disk_share_settings($var['shareNFSEnabled'], $sec_nfs[$name])."</td>";
$cmd="/webGui/scripts/disk_size"."&arg1=".urlencode($name)."&arg2=ssz2";
$cmd="/webGui/scripts/disk_size"."&arg1=$name&arg2=ssz2";
$type = $disk['rotational'] ? _('HDD') : _('SSD');
if (array_key_exists($name, $ssz2)) {
echo "<td>$type</td>";
@@ -100,7 +100,8 @@ foreach ($disks as $name => $disk) {
echo "<td>".my_scale($disk['fsFree']*1024, $unit)." $unit</td>";
echo "<td><a href=\"/$path/Browse?dir=/mnt/$name\"><i class=\"icon-u-tab\" title=\""._('Browse')." /mnt/$name\"></i></a></td>";
echo "</tr>";
foreach ($ssz2[$name] as $sharename => $sharesize) {
foreach ($ssz2[$name] as $entry) {
[$sharename,$sharesize] = my_explode('=',$entry);
if ($sharename=='share.total') continue;
$include = $shares[$sharename]['include'];
$inside = in_array($disk['name'], array_filter(array_diff($myDisks, explode(',',$shares[$sharename]['exclude'])), 'shareInclude'));
@@ -110,7 +111,7 @@ foreach ($disks as $name => $disk) {
echo "<td></td>";
echo "<td></td>";
echo "<td></td>";
echo "<td class='disk-$row-1'>".my_scale($sharesize*1024, $unit)." $unit</td>";
echo "<td class='disk-$row-1'>".my_scale($sharesize, $unit)." $unit</td>";
echo "<td class='disk-$row-2'>".my_scale($disk['fsFree']*1024, $unit)." $unit</td>";
echo "<td><a href=\"/update.htm?cmd=$cmd&csrf_token={$var['csrf_token']}\" target=\"progressFrame\" title=\""._('Recompute')."...\" onclick='$.cookie(\"ssz\",\"ssz\",{path:\"/\"});$(\".disk-$row-1\").html(\""._('Please wait')."...\");$(\".disk-$row-2\").html(\"\");'><i class='fa fa-refresh icon'></i></a></td>";
echo "</tr>";
+1 -1
View File
@@ -113,7 +113,7 @@ foreach ($shares as $name => $share) {
echo "<td></td>";
echo "<td></td>";
echo "<td></td>";
echo "<td class='share-$row-1'>".my_scale($disksize*1024, $unit)." $unit</td>";
echo "<td class='share-$row-1'>".my_scale($disksize, $unit)." $unit</td>";
echo "<td class='share-$row-2'>".my_scale($disks[$diskname]['fsFree']*1024, $unit)." $unit</td>";
echo "<td><a href=\"/update.htm?cmd=$cmd&csrf_token={$var['csrf_token']}\" target=\"progressFrame\" title=\""._('Recompute')."...\" onclick='$.cookie(\"ssz\",\"ssz\",{path:\"/\"});$(\".share-$row-1\").html(\""._('Please wait')."...\");$(\".share-$row-2\").html(\"\");'><i class='fa fa-refresh icon'></i></a></td>";
echo "</tr>";
+176 -63
View File
@@ -19,37 +19,117 @@ if (!isset($_SESSION['locale'])) $_SESSION['locale'] = $_POST['#locale'];
require_once "$docroot/webGui/include/Translations.php";
require_once "$docroot/webGui/include/Helpers.php";
$etc = '/etc/wireguard';
$validIP4 = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}";
$validIP6 = "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(:|([0-9a-fA-F]{1,4}:)+):(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)";
$dockerd = is_file('/var/run/dockerd.pid') && is_dir('/proc/'.file_get_contents('/var/run/dockerd.pid'));
$etc = '/etc/wireguard';
$validIP4 = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}";
$validIP6 = "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(:|([0-9a-fA-F]{1,4}:)+):(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)";
$normalize = ['address'=>'Address', 'dns'=>'DNS', 'privatekey'=>'PrivateKey', 'publickey'=>'PublicKey', 'allowedips'=>'AllowedIPs', 'endpoint'=>'Endpoint'];
$dockernet = "172.31";
$t1 = '6'; // 6 sec timeout
$t2 = '12'; // 12 sec timeout
$t1 = '10'; // 10 sec timeout
$t2 = '15'; // 15 sec timeout
function mask2cidr($mask) {
$long = ip2long($mask);
$base = ip2long('255.255.255.255');
return 32-log(($long ^ $base)+1,2);
}
function thisNet($ethX='eth0') {
extract(parse_ini_file('state/network.ini',true));
$net = long2ip(ip2long($$ethX['IPADDR:0']) & ip2long($$ethX['NETMASK:0'])).'/'.mask2cidr($$ethX['NETMASK:0']);
return [$net,$$ethX['GATEWAY:0']];
}
function ipv4($ip) {
return strpos($ip,'.')!==false;
}
function ipv6($ip) {
return strpos($ip,':')!==false;
}
function ipset($ip) {
return ipv4($ip) ? $ip : "[$ip]";
}
function ipv6($ip) {
function ipsplit($ip) {
return ipv4($ip) ? ':' : ']:';
}
function ipfilter(&$list) {
// we only import IPv4 addresses at this moment, strip any IPv6 addresses
$list = implode(', ',array_filter(array_map('trim',explode(',',$list)),'ipv4'));
}
function host($ip) {
return strpos($ip,'/')!==false ? $ip : (ipv4($ip) ? "$ip/32" : "$ip/128");
}
function isNet($network) {
return !empty(exec("ip rule|grep -Pom1 'from $network'"));
}
function newNet($vtun) {
global $dockernet;
$i = substr($vtun,2)+200;
return [$i,"$dockernet.$i.0/24"];
}
function wgState($vtun,$state,$type=0) {
global $t1,$etc;
$tmp = '/tmp/wg-quick.tmp';
exec("timeout $t1 wg-quick $state $vtun 2>$tmp");
if ($type==8) {
// make VPN tunneled access for Docker containers only
$table = exec("grep -Pom1 'fwmark \K[\d]+' $tmp");
$route = exec("grep -Pom1 '^Address=\K.+$' $etc/$vtun.conf");
sleep(1);
exec("ip -4 route flush table $table");
exec("ip -4 route add $route dev $vtun table $table");
}
delete_file($tmp);
}
function status($vtun) {
return strpos(exec("wg show interfaces|tr '\n' ' '"),"$vtun ")===false;
return in_array($vtun,explode(" ",exec("wg show interfaces")));
}
function vtun() {
global $etc;
$x = 0; while (file_exists("$etc/wg{$x}.conf")) $x++;
return "wg{$x}";
}
function delPeer($vtun, $id='') {
function normalize(&$id) {
// ensure correct capitalization of keywords, some VPN providers use the wrong case
global $normalize;
$id = $normalize[strtolower($id)];
}
function dockerNet($vtun) {
return empty(exec("docker network ls --filter name='$vtun' --format='{{.Name}}'"));
}
function addDocker($vtun) {
global $dockerd;
$error = false;
[$index,$network] = newNet($vtun);
if ($dockerd && dockerNet($vtun)) {
exec("docker network create $vtun --subnet=$network 2>/dev/null");
$error = dockerNet($vtun);
}
if (!$error && !isNet($network)) {
[$thisnet,$gateway] = thisNet();
exec("ip -4 rule add from $network table $index");
exec("ip -4 route add unreachable default table $index");
exec("ip -4 route add $thisnet via $gateway table $index");
}
return $error;
}
function delDocker($vtun) {
global $dockerd;
$error = false;
[$index,$network] = newNet($vtun);
if ($dockerd && !dockerNet($vtun)) {
exec("docker network rm $vtun 2>/dev/null");
$error = !dockerNet($vtun);
}
if (!$error && isNet($network)) {
exec("ip -4 route flush table $index");
exec("ip -4 rule del from $network table $index");
}
return $error;
}
function delPeer($vtun,$id='') {
global $etc,$name;
$dir = "$etc/peers";
foreach (glob("$dir/peer-$name-$vtun-$id*",GLOB_NOSORT) as $peer) unlink($peer);
foreach (glob("$dir/peer-$name-$vtun-$id*",GLOB_NOSORT) as $peer) delete_file($peer);
}
function addPeer(&$x) {
global $peers,$var;
@@ -71,18 +151,16 @@ function addPeer(&$x) {
$peers[$x][] = $var['allowedIPs']; // AllowedIPs
$x++;
}
function autostart($cmd,$vtun) {
function autostart($vtun,$cmd) {
global $etc;
$autostart = "$etc/autostart";
$list = @file_get_contents($autostart) ?: '';
$list = file_exists($autostart) ? array_filter(explode(' ',file_get_contents($autostart))) : [];
$key = array_search($vtun,$list);
switch ($cmd) {
case 'off':
if ($list && strpos($list,"$vtun ")!==false) file_put_contents($autostart,str_replace("$vtun ","",$list));
break;
case 'on':
if (!$list || strpos($list,"$vtun ")===false) file_put_contents($autostart,$list."$vtun ");
break;
case 'off': if ($key!==false) unset($list[$key]); break;
case 'on' : if ($key===false) $list[] = $vtun; break;
}
if (count($list)) file_put_contents($autostart,implode(' ',$list)); else delete_file($autostart);
}
function createPeerFiles($vtun) {
global $etc,$peers,$name,$gone,$vpn;
@@ -103,8 +181,8 @@ function createPeerFiles($vtun) {
$id = explode('-',basename($file,'.conf'))[3];
if ($id > $new) {
// rename files to match revised peers list
rename($file, "$peer-$new.conf");
rename(str_replace('.conf','.png',$file), "$peer-$new.png");
rename($file,"$peer-$new.conf");
rename(str_replace('.conf','.png',$file),"$peer-$new.png");
}
$new++;
}
@@ -118,7 +196,7 @@ function createPeerFiles($vtun) {
$cfg = "$dir/peer-$name-$vtun-$id.conf";
$cfgold = @file_get_contents($cfg) ?: '';
$cfgnew = implode("\n",$peer)."\n";
if ($cfgnew !== $cfgold && !$vpn) {
if ($cfgnew !== $cfgold && $vpn==0) {
$list[] = "$vtun: peer $id (".($peer[1][0]=='#' ? substr($peer[1],1) : _('no name')).')';
file_put_contents($cfg,$cfgnew);
$png = str_replace('.conf','.png',$cfg);
@@ -126,17 +204,34 @@ function createPeerFiles($vtun) {
}
}
// store the peer names which are updated
if (count($list)) file_put_contents($tmp,implode("<br>",$list)); else @unlink($tmp);
if (count($list)) file_put_contents($tmp,implode("<br>",$list)); else delete_file($tmp);
}
function parseInput(&$input,&$x) {
function createList($list) {
return implode(', ',array_unique(array_filter(array_map('trim',explode(',',$list)))));
}
function createIPs($list) {
return implode(', ',array_map('host',array_filter(array_map('trim',explode(',',$list)))));
}
function parseInput($vtun,&$input,&$x) {
global $conf,$user,$var,$default,$default6,$vpn;
$section = 0; $addPeer = false;
foreach ($input as $key => $value) {
if ($key[0]=='#') continue;
[$id,$i] = my_explode(':',$key);
if ($i != $section) {
if ($section==0) {
// add WG routing for docker containers. Only IPv4 supported
[$index,$network] = newNet($vtun);
[$thisnet,$gateway] = thisNet();
$conf[] = "PostUp=ip -4 route flush table $index";
$conf[] = "PostUp=ip -4 route add default via $tunip table $index";
$conf[] = "PostUp=ip -4 route add $thisnet via $gateway table $index";
$conf[] = "PostDown=ip -4 route flush table $index";
$conf[] = "PostDown=ip -4 route add unreachable default table $index";
$conf[] = "PostDown=ip -4 route add $thisnet via $gateway table $index";
}
$conf[] = "\n[Peer]";
// add peers only for peer sections
// add peers, this is only used for peer sections
$addPeer ? addPeer($x) : $addPeer = true;
$section = $i;
}
@@ -166,7 +261,7 @@ function parseInput(&$input,&$x) {
}
break;
case 'DNS':
if ($i>0 && $value) {
if ($i > 0 && $value) {
$user[] = "$id:$x=\"$value\"";
$var['dns'] = "$id=$value";
} else $var['dns'] = false;
@@ -181,14 +276,14 @@ function parseInput(&$input,&$x) {
}
break;
case 'TYPE':
$list = array_map('trim',explode(',',$value<4 ? ($value%2==1 ? $var['subnets1'] : $var['subnets2']) : ($value<6 ? ($value%2==1 ? $var['shared1'] : $var['shared2']) : $var['default'])));
$var['allowedIPs'] = implode(', ',array_map('host',array_filter($list)));
$list = $value<4 ? ($value%2==1 ? $var['subnets1'] : $var['subnets2']) : ($value<6 ? ($value%2==1 ? $var['shared1'] : $var['shared2']) : $var['default']);
$var['allowedIPs'] = createIPs($list);
$var['tunnel'] = ($value==2||$value==3) ? $tunnel : false;
$user[] = "$id:$x=\"$value\"";
if ($value==7) $vpn = true;
if ($value>=7) $vpn = $value;
break;
case 'Network6': if (!$protocol) break;
case 'Network':
case 'Network6':
case 'UPNP':
case 'DROP':
case 'RULE':
@@ -196,10 +291,11 @@ function parseInput(&$input,&$x) {
$user[] = "$id:0=\"$value\"";
break;
case 'Address':
$hosts = implode(', ',array_map('host',array_filter(explode(', ',$value))));
$hosts = createIPs($value);
if ($i==0) {
$conf[] = "$id=$value";
$tunnel = "$id=$hosts";
$tunip = $value;
} else {
$user[] = "$id:$x=\"$value\"";
$var['address'] = "$id=$hosts";
@@ -215,8 +311,8 @@ function parseInput(&$input,&$x) {
$var['endpoint'] = $value ? "Endpoint=".ipset($value) : false;
} else {
if ($value) $conf[] = "$id=$value";
$var['listenport'] = $value ? "ListenPort=".explode(ipv6($value),$value)[1] : false;
if ($var['endpoint'] && strpos($var['endpoint'],ipv6($var['endpoint']))===false) $var['endpoint'] .= ":".explode(ipv6($var['internet']),$var['internet'])[1];
$var['listenport'] = $value ? "ListenPort=".explode(ipsplit($value),$value)[1] : false;
if ($var['endpoint'] && strpos($var['endpoint'],ipsplit($var['endpoint']))===false) $var['endpoint'] .= ":".explode(ipsplit($var['internet']),$var['internet'])[1];
}
break;
case 'PersistentKeepalive':
@@ -227,6 +323,9 @@ function parseInput(&$input,&$x) {
if ($value) $conf[] = "$id=$value";
$var['presharedKey'] = $value ? "$id=$value" : false;
break;
case 'AllowedIPs':
$conf[] = "$id=".createList($value);
break;
default:
if ($value) $conf[] = "$id=$value";
break;
@@ -258,46 +357,53 @@ case 'update':
$gone = explode(',',$_POST['#deleted']);
$conf = ['[Interface]'];
$user = $peers = $var = [];
$var['subnets1'] = "AllowedIPs=".implode(', ',(array_unique(explode(', ',$_POST['#subnets1']))));
$var['subnets2'] = "AllowedIPs=".implode(', ',(array_unique(explode(', ',$_POST['#subnets2']))));
$var['shared1'] = "AllowedIPs=".implode(', ',(array_unique(explode(', ',$_POST['#shared1']))));
$var['shared2'] = "AllowedIPs=".implode(', ',(array_unique(explode(', ',$_POST['#shared2']))));
$var['internet'] = "Endpoint=".implode(', ',(array_unique(explode(', ',$_POST['#internet']))));
$x = 1; $vpn = false;
parseInput($_POST,$x);
$var['subnets1'] = "AllowedIPs=".createList($_POST['#subnets1']);
$var['subnets2'] = "AllowedIPs=".createList($_POST['#subnets2']);
$var['shared1'] = "AllowedIPs=".createList($_POST['#shared1']);
$var['shared2'] = "AllowedIPs=".createList($_POST['#shared2']);
$var['internet'] = "Endpoint=".createList($_POST['#internet']);
$x = 1; $vpn = 0;
parseInput($vtun,$_POST,$x);
addPeer($x);
exec("wg-quick down $vtun 2>/dev/null");
addDocker($vtun);
$upstate = status($vtun);
wgState($vtun,'down');
file_put_contents($file,implode("\n",$conf)."\n");
file_put_contents($cfg,implode("\n",$user)."\n");
createPeerFiles($vtun);
if ($wg) exec("wg-quick up $vtun >/dev/null");
if ($upstate) wgState($vtun,'up',$_POST['#type']);
$save = false;
break;
case 'toggle':
$vtun = $_POST['#vtun'];
switch ($_POST['#wg']) {
case 'stop':
exec("timeout $t1 wg-quick down $vtun 2>/dev/null");
wgState($vtun,'down');
echo status($vtun) ? 1 : 0;
break;
case 'start':
exec("timeout $t1 wg-quick up $vtun 2>/dev/null");
[$index,$network] = newNet($vtun);
if (!isNet($network)) {
exec("ip -4 rule add from $network table $index");
exec("ip -4 route add unreachable default table $index");
}
wgState($vtun,'up',$_POST['#type']);
echo status($vtun) ? 0 : 1;
break;
}
break;
case 'ping':
$addr = $_POST['#addr'];
echo exec("ping -qc1 -W4 $addr|grep -Po '1 received'");
echo exec("ping -qc1 -W4 $addr|grep -Pom1 '1 received'");
break;
case 'public':
$ip = $_POST['#ip'];
$v4 = $_POST['#prot']!='6';
$v6 = $_POST['#prot']!='';
$context = stream_context_create(['https'=>['timeout'=>12]]);
$int_ipv4 = $v4 ? (preg_match("/^$validIP4$/", $ip) ? $ip : (@dns_get_record($ip, DNS_A)[0]['ip'] ?: '')) : '';
$int_ipv4 = $v4 ? (preg_match("/^$validIP4$/",$ip) ? $ip : (@dns_get_record($ip,DNS_A)[0]['ip'] ?: '')) : '';
$ext_ipv4 = $v4 ? (@file_get_contents('https://wanip4.unraid.net',false,$context) ?: '') : '';
$int_ipv6 = $v6 ? (preg_match("/^$validIP6$/", $ip) ? $ip : (@dns_get_record($ip, DNS_AAAA)[0]['ipv6'] ?: '')) : '';
$int_ipv6 = $v6 ? (preg_match("/^$validIP6$/",$ip) ? $ip : (@dns_get_record($ip,DNS_AAAA)[0]['ipv6'] ?: '')) : '';
$ext_ipv6 = $v6 ? (@file_get_contents('https://wanip6.unraid.net',false,$context) ?: '') : '';
echo "$int_ipv4;$ext_ipv4;$int_ipv6;$ext_ipv6";
break;
@@ -305,19 +411,22 @@ case 'addtunnel':
$vtun = vtun();
$name = $_POST['#name'];
touch("$etc/$vtun.conf");
exec("wg-quick down $vtun 2>/dev/null");
@unlink("$etc/$vtun.cfg");
wgState($vtun,'down');
delete_file("$etc/$vtun.cfg");
delPeer($vtun);
autostart('off',$vtun);
autostart($vtun,'off');
break;
case 'deltunnel':
$vtun = $_POST['#vtun'];
$name = $_POST['#name'];
exec("wg-quick down $vtun 2>/dev/null");
@unlink("$etc/$vtun.conf");
@unlink("$etc/$vtun.cfg");
delPeer($vtun);
autostart('off',$vtun);
$error = delDocker($vtun);
if (!$error) {
wgState($vtun,'down');
delete_file("$etc/$vtun.conf","$etc/$vtun.cfg");
delPeer($vtun);
autostart($vtun,'off');
}
echo $error ? 1 : 0;
break;
case 'import':
$name = $_POST['#name'];
@@ -328,8 +437,9 @@ case 'import':
foreach (explode("\n",$entry) as $row) {
if (ltrim($row)[0]!='#') {
[$id,$data] = array_map('trim',my_explode('=',$row));
normalize($id);
$import["$id:$i"] = $data;
} elseif ($i>=0) {
} elseif ($i >= 0) {
$import["Name:$i"] = substr(trim($row),1);
}
}
@@ -339,21 +449,23 @@ case 'import':
$import['NAT:0'] = 'no';
[$subnet,$mask] = my_explode('/',$import['Address:0']);
if (ipv4($subnet)) {
$mask = ($mask>0 && $mask<32) ? $mask : 24;
$mask = ($mask > 0 && $mask < 32) ? $mask : 24;
$import['Network:0'] = long2ip(ip2long($subnet) & (0x100000000-2**(32-$mask))).'/'.$mask;
$import['Address:0'] = $subnet;
$import['PROT:0'] = '';
} else {
$mask = ($mask>0 && $mask<128) ? $mask : 64;
$mask = ($mask > 0 && $mask < 128) ? $mask : 64;
$import['Network6:0'] = strstr($subnet,'::',true).'::/'.$mask;
$import['Address:0'] = $subnet;
$import['PROT:0'] = '6';
}
$import['Endpoint:0'] = '';
for ($n = 1; $n <= $i; $n++) {
$vpn = strpos($import["AllowedIPs:$n"],$default)!==false || strpos($import["AllowedIPs:$n"],$default6)!==false;
if ($vpn) $import["Address:$n"] = '';
$import["TYPE:$n"] = $vpn ? 7 : 0;
$vpn = array_map('trim',explode(',',$import["AllowedIPs:$n"]));
$vpn = (in_array($default,$vpn) || in_array($default6,$vpn)) ? 8 : 0;
if ($vpn==8) $import["Address:$n"] = '';
$import["TYPE:$n"] = $vpn;
ipfilter($import["AllowedIPs:$n"]);
if ($import["TYPE:$n"]==0) $var['subnets1'] = "AllowedIPs=".$import["AllowedIPs:$n"];
}
foreach ($import as $key => $val) $sort[] = explode(':',$key)[1];
@@ -362,17 +474,18 @@ case 'import':
$conf = ['[Interface]'];
$var['default'] = $import['PROT:0']=='' ? "AllowedIPs=$default" : "AllowedIPs=$default6";
$var['internet'] = "Endpoint=unknown";
parseInput($import,$x);
addPeer($x);
$vtun = vtun();
parseInput($vtun,$import,$x);
addPeer($x);
file_put_contents("$etc/$vtun.conf",implode("\n",$conf)."\n");
file_put_contents("$etc/$vtun.cfg",implode("\n",$user)."\n");
delPeer($vtun);
autostart('off',$vtun);
addDocker($vtun);
autostart($vtun,'off');
echo $vtun;
break;
case 'autostart':
autostart($_POST['#start'],$_POST['#vtun']);
autostart($_POST['#vtun'],$_POST['#start']);
break;
case 'upnp':
$upnp = '/var/tmp/upnp';