mirror of
https://github.com/unraid/webgui.git
synced 2026-05-19 04:39:52 -05:00
14ba46a4a5
1. Make import config file of VPN providers more robust. 2. Add tunnel routing for docker containers 3. Automatically make the WG tunnel available to containers (custom network)
465 lines
17 KiB
PHP
465 lines
17 KiB
PHP
<?PHP
|
|
/* Copyright 2005-2022, Lime Technology
|
|
* Copyright 2012-2022, Bergware International.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License version 2,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*/
|
|
?>
|
|
<?
|
|
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
|
// add translations
|
|
$_SERVER['REQUEST_URI'] = 'settings';
|
|
// special case when script is called on form-submit and processed by update.php
|
|
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})?)";
|
|
$normalize = ['address'=>'Address', 'dns'=>'DNS', 'privatekey'=>'PrivateKey', 'publickey'=>'PublicKey', 'allowedips'=>'AllowedIPs', 'endpoint'=>'Endpoint'];
|
|
|
|
$t1 = '6'; // 6 sec timeout
|
|
$t2 = '12'; // 12 sec timeout
|
|
|
|
function mask2cidr($mask) {
|
|
$long = ip2long($mask);
|
|
$base = ip2long('255.255.255.255');
|
|
return 32-log(($long ^ $base)+1,2);
|
|
}
|
|
function ipv4($ip) {
|
|
return strpos($ip,'.')!==false;
|
|
}
|
|
function ipv6($ip) {
|
|
return strpos($ip,':')!==false;
|
|
}
|
|
function ipset($ip) {
|
|
return ipv4($ip) ? $ip : "[$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 status($vtun) {
|
|
return strpos(exec("wg show interfaces|tr '\n' ' '"),"$vtun ")===false;
|
|
}
|
|
function vtun() {
|
|
global $etc;
|
|
$x = 0; while (file_exists("$etc/wg{$x}.conf")) $x++;
|
|
return "wg{$x}";
|
|
}
|
|
function normalize($id) {
|
|
// ensure correct capitalization of keywords, some VPN providers use the wrong case
|
|
global $normalize;
|
|
return $normalize[strtolower($id)];
|
|
}
|
|
function addDocker($vtun) {
|
|
// create a docker network for the WG tunnel. Containers can select to this network for communication
|
|
if (!exec("docker network ls --filter name='$vtun' --format='{{.Name}}'")) {
|
|
$index = substr($vtun,2)+200;
|
|
$network = "172.31.$index.0/24";
|
|
exec("docker network create $vtun --subnet=$network 2>/dev/null");
|
|
}
|
|
}
|
|
function delDocker($vtun) {
|
|
// delete the docker network, containers using this network need to be reconfigured
|
|
if (exec("docker network ls --filter name='$vtun' --format='{{.Name}}'")) {
|
|
$index = substr($vtun,2)+200;
|
|
$network = "172.31.$index.0/24";
|
|
exec("docker network rm $vtun 2>/dev/null");
|
|
}
|
|
}
|
|
function delPeer($vtun, $id='') {
|
|
global $etc,$name;
|
|
$dir = "$etc/peers";
|
|
foreach (glob("$dir/peer-$name-$vtun-$id*",GLOB_NOSORT) as $peer) unlink($peer);
|
|
}
|
|
function addPeer(&$x) {
|
|
global $peers,$var;
|
|
$peers[$x] = ['[Interface]']; // [Interface]
|
|
if ($var['client']) $peers[$x][] = $var['client']; // #name
|
|
if ($var['privateKey']) $peers[$x][] = $var['privateKey']; // PrivateKey
|
|
$peers[$x][] = $var['address']; // Address
|
|
if ($var['listenport']) $peers[$x][] = $var['listenport']; // ListenPort
|
|
if ($var['dns']) $peers[$x][] = $var['dns']; // DNS server
|
|
if ($var['mtu']) $peers[$x][] = $var['mtu']; // MTU
|
|
$peers[$x][] = '';
|
|
$peers[$x][] = "[Peer]"; // [Peer]
|
|
if ($var['server']) $peers[$x][] = $var['server']; // #name
|
|
if ($var['handshake']) $peers[$x][] = $var['handshake']; // PersistentKeepalive
|
|
if ($var['presharedKey']) $peers[$x][] = $var['presharedKey']; // PresharedKey
|
|
$peers[$x][] = $var['publicKey']; // PublicKey
|
|
if ($var['tunnel']) $peers[$x][] = $var['tunnel']; // Tunnel address
|
|
$peers[$x][] = $var['endpoint'] ?: $var['internet']; // Endpoint
|
|
$peers[$x][] = $var['allowedIPs']; // AllowedIPs
|
|
$x++;
|
|
}
|
|
function autostart($cmd,$vtun) {
|
|
global $etc;
|
|
$autostart = "$etc/autostart";
|
|
$list = @file_get_contents($autostart) ?: '';
|
|
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;
|
|
}
|
|
}
|
|
function createPeerFiles($vtun) {
|
|
global $etc,$peers,$name,$gone,$vpn;
|
|
$dir = "$etc/peers";
|
|
$tmp = "/tmp/list.tmp";
|
|
if (is_dir($dir)) {
|
|
if (count($gone)) {
|
|
foreach ($gone as $peer) {
|
|
// one or more peers are removed, delete the associated files
|
|
[$n,$i] = my_explode('-',$peer);
|
|
delPeer($n,$i);
|
|
}
|
|
$new = 1;
|
|
$peer = "$dir/peer-$name-$vtun";
|
|
$files = glob("$peer-*.conf",GLOB_NOSORT);
|
|
natsort($files);
|
|
foreach ($files as $file) {
|
|
$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");
|
|
}
|
|
$new++;
|
|
}
|
|
}
|
|
} else {
|
|
mkdir($dir);
|
|
}
|
|
$list = [];
|
|
foreach ($peers as $id => $peer) {
|
|
if (empty($peer[1])) break; // tunnel without any peers
|
|
$cfg = "$dir/peer-$name-$vtun-$id.conf";
|
|
$cfgold = @file_get_contents($cfg) ?: '';
|
|
$cfgnew = implode("\n",$peer)."\n";
|
|
if ($cfgnew !== $cfgold && !$vpn) {
|
|
$list[] = "$vtun: peer $id (".($peer[1][0]=='#' ? substr($peer[1],1) : _('no name')).')';
|
|
file_put_contents($cfg,$cfgnew);
|
|
$png = str_replace('.conf','.png',$cfg);
|
|
exec("qrencode -t PNG -r $cfg -o $png");
|
|
}
|
|
}
|
|
// store the peer names which are updated
|
|
if (count($list)) file_put_contents($tmp,implode("<br>",$list)); else @unlink($tmp);
|
|
}
|
|
function parseInput(&$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
|
|
extract(parse_ini_file('state/network.ini',true));
|
|
$index = $x+199;
|
|
$network = "172.31.$index.0/24";
|
|
$thisnet = long2ip(ip2long($eth0['IPADDR:0']) & ip2long($eth0['NETMASK:0'])).'/'.mask2cidr($eth0['NETMASK:0']);
|
|
$gateway = $eth0['GATEWAY:0'];
|
|
$conf[] = "PostUp=ip -4 rule add from $network 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 rule del from $network table $index";
|
|
}
|
|
$conf[] = "\n[Peer]";
|
|
// add peers, this is only used for peer sections
|
|
$addPeer ? addPeer($x) : $addPeer = true;
|
|
$section = $i;
|
|
}
|
|
switch ($id) {
|
|
case 'Name':
|
|
if ($value) $conf[] = "#$value";
|
|
if ($i==0) {
|
|
$var['server'] = $value ? "#$value" : false;
|
|
} else {
|
|
$var['client'] = $value ? "#$value" : false;
|
|
}
|
|
break;
|
|
case 'PrivateKey':
|
|
if ($i==0) {
|
|
$conf[] = "$id=$value";
|
|
} else {
|
|
if ($value) $user[] = "$id:$x=\"$value\"";
|
|
$var['privateKey'] = $value ? "$id=$value" : false;
|
|
}
|
|
break;
|
|
case 'PublicKey':
|
|
if ($i==0) {
|
|
$user[] = "$id:0=\"$value\"";
|
|
$var['publicKey'] = "$id=$value";
|
|
} else {
|
|
$conf[] = "$id=$value";
|
|
}
|
|
break;
|
|
case 'DNS':
|
|
if ($i>0 && $value) {
|
|
$user[] = "$id:$x=\"$value\"";
|
|
$var['dns'] = "$id=$value";
|
|
} else $var['dns'] = false;
|
|
break;
|
|
case 'PROT':
|
|
$protocol = $value;
|
|
$user[] = "$id:0=\"$value\"";
|
|
switch ($protocol) {
|
|
case '46': $var['default'] = "AllowedIPs=$default, $default6"; break;
|
|
case '6' : $var['default'] = "AllowedIPs=$default6"; break;
|
|
default : $var['default'] = "AllowedIPs=$default"; break;
|
|
}
|
|
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)));
|
|
$var['tunnel'] = ($value==2||$value==3) ? $tunnel : false;
|
|
$user[] = "$id:$x=\"$value\"";
|
|
if ($value==7) $vpn = true;
|
|
break;
|
|
case 'Network':
|
|
case 'Network6':
|
|
case 'UPNP':
|
|
case 'DROP':
|
|
case 'RULE':
|
|
case 'NAT':
|
|
$user[] = "$id:0=\"$value\"";
|
|
break;
|
|
case 'Address':
|
|
$hosts = implode(', ',array_map('host',array_filter(explode(', ',$value))));
|
|
if ($i==0) {
|
|
$conf[] = "$id=$value";
|
|
$tunnel = "$id=$hosts";
|
|
$tunip = $value;
|
|
} else {
|
|
$user[] = "$id:$x=\"$value\"";
|
|
$var['address'] = "$id=$hosts";
|
|
}
|
|
break;
|
|
case 'MTU':
|
|
if ($value) $conf[] = "$id=$value";
|
|
$var['mtu'] = $value ? "$id=$value" : false;
|
|
break;
|
|
case 'Endpoint':
|
|
if ($i==0) {
|
|
$user[] = "$id:0=\"$value\"";
|
|
$var['endpoint'] = $value ? "Endpoint=".ipset($value) : false;
|
|
} else {
|
|
if ($value) $conf[] = "$id=$value";
|
|
$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':
|
|
if ($value) $conf[] = "$id=$value";
|
|
$var['handshake'] = $value ? "$id=$value" : false;
|
|
break;
|
|
case 'PresharedKey':
|
|
if ($value) $conf[] = "$id=$value";
|
|
$var['presharedKey'] = $value ? "$id=$value" : false;
|
|
break;
|
|
default:
|
|
if ($value) $conf[] = "$id=$value";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$default = '0.0.0.0/0';
|
|
$default6 = '::/0';
|
|
switch ($_POST['#cmd']) {
|
|
case 'keypair':
|
|
$private = exec("wg genkey");
|
|
$public = exec("wg pubkey <<<'$private'");
|
|
echo $private."\0".$public;
|
|
break;
|
|
case 'presharedkey':
|
|
echo exec("wg genpsk");
|
|
break;
|
|
case 'update':
|
|
if (!exec("iptables -S|grep -om1 'WIREGUARD$'")) {
|
|
exec("iptables -N WIREGUARD;iptables -A FORWARD -j WIREGUARD");
|
|
}
|
|
if (!exec("ip6tables -S|grep -om1 'WIREGUARD$'")) {
|
|
exec("ip6tables -N WIREGUARD;ip6tables -A FORWARD -j WIREGUARD");
|
|
}
|
|
$cfg = $_POST['#cfg'];
|
|
$wg = $_POST['#wg'];
|
|
$name = $_POST['#name'];
|
|
$vtun = $_POST['#vtun'];
|
|
$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);
|
|
addPeer($x);
|
|
exec("wg-quick down $vtun 2>/dev/null");
|
|
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");
|
|
$save = false;
|
|
break;
|
|
case 'toggle':
|
|
$vtun = $_POST['#vtun'];
|
|
switch ($_POST['#wg']) {
|
|
case 'stop':
|
|
exec("timeout $t1 wg-quick down $vtun 2>/dev/null");
|
|
echo status($vtun) ? 1 : 0;
|
|
break;
|
|
case 'start':
|
|
exec("timeout $t1 wg-quick up $vtun 2>/dev/null");
|
|
echo status($vtun) ? 0 : 1;
|
|
break;
|
|
}
|
|
break;
|
|
case 'ping':
|
|
$addr = $_POST['#addr'];
|
|
echo exec("ping -qc1 -W4 $addr|grep -Po '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'] ?: '')) : '';
|
|
$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'] ?: '')) : '';
|
|
$ext_ipv6 = $v6 ? (@file_get_contents('https://wanip6.unraid.net',false,$context) ?: '') : '';
|
|
echo "$int_ipv4;$ext_ipv4;$int_ipv6;$ext_ipv6";
|
|
break;
|
|
case 'addtunnel':
|
|
$vtun = vtun();
|
|
$name = $_POST['#name'];
|
|
touch("$etc/$vtun.conf");
|
|
exec("wg-quick down $vtun 2>/dev/null");
|
|
@unlink("$etc/$vtun.cfg");
|
|
delPeer($vtun);
|
|
addDocker($vtun);
|
|
autostart('off',$vtun);
|
|
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);
|
|
delDocker($vtun);
|
|
autostart('off',$vtun);
|
|
break;
|
|
case 'import':
|
|
$name = $_POST['#name'];
|
|
$user = $peers = $var = $import = $sort = [];
|
|
$entries = array_filter(array_map('trim',preg_split('/\[(Interface|Peer)\]/',$_POST['#data'])));
|
|
foreach($entries as $key => $entry) {
|
|
$i = $key-1;
|
|
foreach (explode("\n",$entry) as $row) {
|
|
if (ltrim($row)[0]!='#') {
|
|
[$id,$data] = array_map('trim',my_explode('=',$row));
|
|
$id = normalize($id);
|
|
$import["$id:$i"] = $data;
|
|
} elseif ($i>=0) {
|
|
$import["Name:$i"] = substr(trim($row),1);
|
|
}
|
|
}
|
|
}
|
|
if ($import['PrivateKey:0'] && !$import['PublicKey:0']) $import['PublicKey:0'] = exec("wg pubkey <<<'{$import['PrivateKey:0']}'");
|
|
$import['UPNP:0'] = 'no';
|
|
$import['NAT:0'] = 'no';
|
|
[$subnet,$mask] = my_explode('/',$import['Address:0']);
|
|
if (ipv4($subnet)) {
|
|
$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;
|
|
$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;
|
|
ipfilter($import["AllowedIPs:$n"]);
|
|
if ($import["TYPE:$n"]==0) $var['subnets1'] = "AllowedIPs=".$import["AllowedIPs:$n"];
|
|
}
|
|
foreach ($import as $key => $val) $sort[] = explode(':',$key)[1];
|
|
array_multisort($sort,$import);
|
|
$x = 1;
|
|
$conf = ['[Interface]'];
|
|
$var['default'] = $import['PROT:0']=='' ? "AllowedIPs=$default" : "AllowedIPs=$default6";
|
|
$var['internet'] = "Endpoint=unknown";
|
|
parseInput($import,$x);
|
|
addPeer($x);
|
|
$vtun = vtun();
|
|
file_put_contents("$etc/$vtun.conf",implode("\n",$conf)."\n");
|
|
file_put_contents("$etc/$vtun.cfg",implode("\n",$user)."\n");
|
|
delPeer($vtun);
|
|
addDocker($vtun);
|
|
autostart('off',$vtun);
|
|
echo $vtun;
|
|
break;
|
|
case 'autostart':
|
|
autostart($_POST['#start'],$_POST['#vtun']);
|
|
break;
|
|
case 'upnp':
|
|
$upnp = '/var/tmp/upnp';
|
|
if (is_executable('/usr/bin/upnpc')) {
|
|
$gw = $_POST['#gw'].':';
|
|
$link = $_POST['#link'];
|
|
$xml = @file_get_contents($upnp) ?: '';
|
|
if ($xml) {
|
|
exec("timeout $t1 stdbuf -o0 upnpc -u $xml -m $link -l 2>&1|grep -qm1 'refused'",$output,$code);
|
|
if ($code!=1) $xml = '';
|
|
}
|
|
if (!$xml) {
|
|
exec("timeout $t2 stdbuf -o0 upnpc -m $link -l 2>/dev/null|grep -Po 'desc: \K.+'",$desc);
|
|
foreach ($desc as $url) if ($url && strpos($url,$gw)!==false) {$xml = $url; break;}
|
|
}
|
|
} else $xml = "";
|
|
file_put_contents($upnp,$xml);
|
|
echo $xml;
|
|
break;
|
|
case 'upnpc':
|
|
if (!is_executable('/usr/bin/upnpc')) break;
|
|
$xml = $_POST['#xml'];
|
|
$vtun = $_POST['#vtun'];
|
|
$link = $_POST['#link'];
|
|
$ip = $_POST['#ip'];
|
|
if ($_POST['#wg']=='active') {
|
|
exec("timeout $t1 stdbuf -o0 upnpc -u $xml -m $link -l 2>/dev/null|grep -Po \"^(ExternalIPAddress = \K.+|.+\KUDP.+>$ip:[0-9]+ 'WireGuard-$vtun')\"",$upnp);
|
|
[$addr,$upnp] = $upnp;
|
|
[$type,$rule] = my_explode(' ',$upnp);
|
|
echo $rule ? "UPnP: $addr:$rule/$type" : _("UPnP: forwarding not set");
|
|
} else {
|
|
echo _("UPnP: tunnel is inactive");
|
|
}
|
|
break;
|
|
}
|
|
?>
|