'Address', 'dns'=>'DNS', 'privatekey'=>'PrivateKey', 'publickey'=>'PublicKey', 'allowedips'=>'AllowedIPs', 'endpoint'=>'Endpoint','listenport'=>'ListenPort','mtu'=>'MTU','persistentkeepalive'=>'PersistentKeepalive']; $dockernet = "172.31"; $t1 = '10'; // 10 sec timeout $t2 = '15'; // 15 sec timeout function isPort($dev) { return file_exists("/sys/class/net/$dev"); } function carrier($dev, $loop=3) { if (!isPort($dev)) return false; try { for ($n=0; $n<$loop; $n++) { if (@file_get_contents("/sys/class/net/$dev/carrier") == 1) return true; if ($loop > 1) sleep(1); } } catch (Exception $e) { return false; } return false; } function thisNet() { $dev = isPort('br0') ? 'br0' : (isPort('bond0') ? 'bond0' : 'eth0'); if (!carrier($dev) && carrier('wlan0', 1)) $dev = 'wlan0'; $ip4 = exec("ip -4 -br addr show dev $dev | awk '{print \$3;exit}'"); $net = exec("ip -4 route show $ip4 dev $dev | awk '{print \$1;exit}'"); $gw = exec("ip -4 route show default dev $dev | awk '{print \$3;exit}'"); return [$dev, $net, $gw]; } 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 ipv4Addr($value) { return array_filter(array_map('trim', explode(',', $value)), 'ipv4'); } function ipv6Addr($value) { return array_filter(array_map('trim', explode(',', $value)), 'ipv6'); } function ipfilter(&$list) { // we only import IPv4 addresses, strip any IPv6 addresses $list = implode(', ', ipv4Addr($list)); } 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'; $log = '/var/log/wg-quick.log'; exec("timeout $t1 wg-quick $state $vtun 2>$tmp"); file_put_contents($log, "wg-quick $state $vtun\n".file_get_contents($tmp)."\n", FILE_APPEND); if ($type == 8) { // make VPN tunneled access for Docker containers only $table = exec("grep -Pom1 'fwmark \K[\d]+' $tmp"); $route = implode(ipv4Addr(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 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 normalize(&$id) { // ensure correct capitalization of keywords, some VPN providers use the wrong case global $normalize; // allow fallback for non-included keywords $id = $normalize[strtolower($id)] ?? $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 -o 'com.docker.network.driver.mtu'='1420' $vtun --subnet=$network 2>/dev/null"); $error = dockerNet($vtun); } if (!$error && !isNet($network)) { [$device, $thisnet, $gateway] = thisNet(); if (!empty($device) && !empty($thisnet) && !empty($gateway)) { 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 dev $device 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) delete_file($peer); } function addPeer(&$x) { global $peers, $var; $peers[$x] = ['[Interface]']; // [Interface] if (isset($var['client'])) $peers[$x][] = $var['client']; // #name if (isset($var['privateKey'])) $peers[$x][] = $var['privateKey']; // PrivateKey $peers[$x][] = _var($var,'address'); // Address if (isset($var['listenport'])) $peers[$x][] = $var['listenport']; // ListenPort if (isset($var['dns'])) $peers[$x][] = $var['dns']; // DNS server if (isset($var['mtu'])) $peers[$x][] = $var['mtu']; // MTU $peers[$x][] = ''; $peers[$x][] = "[Peer]"; // [Peer] if (isset($var['server'])) $peers[$x][] = $var['server']; // #name if (isset($var['handshake'])) $peers[$x][] = $var['handshake']; // PersistentKeepalive if (isset($var['presharedKey'])) $peers[$x][] = $var['presharedKey']; // PresharedKey $peers[$x][] = _var($var,'publicKey'); // PublicKey if (isset($var['tunnel'])) $peers[$x][] = $var['tunnel']; // Tunnel address $peers[$x][] = _var($var,'endpoint') ?: _var($var,'internet'); // Endpoint $peers[$x][] = _var($var,'allowedIPs'); // AllowedIPs $x++; } function autostart($vtun, $cmd) { global $etc; $autostart = "$etc/autostart"; $list = file_exists($autostart) ? array_filter(explode(' ', file_get_contents($autostart))) : []; $key = array_search($vtun, $list); switch ($cmd) { 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; $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 == 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); exec("qrencode -t PNG -r $cfg -o $png"); } } // store the peer names which are updated if (count($list)) file_put_contents($tmp, implode("
", $list)); else delete_file($tmp); } 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) { // assign values to parameters, be aware that certain parameters are assigned by parseInput itself // this is based on the sequence of processing global $conf, $user, $var, $default4, $default6, $vpn, $tunip; $tunnel = $protocol = null; // satisfy code checkers $section = 0; $addPeer = false; foreach ($input as $key => $value) { if ($key[0] == '#') continue; [$id, $i] = array_pad(explode(':', $key), 2, 0); if ($i != $section) { if ($section == 0) { // add WG routing for docker containers. Only IPv4 supported [$index, $network] = newNet($vtun); [$device, $thisnet, $gateway] = thisNet(); if (!empty($device) && !empty($thisnet) && !empty($gateway)) { $conf[] = "PostUp=ip -4 route flush table $index"; $conf[] = "PostUp=ip -4 route add default via $tunip dev $vtun table $index"; $conf[] = "PostUp=ip -4 route add $thisnet via $gateway dev $device 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 dev $device 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=$default4, $default6"; break; case '6' : $var['default'] = "AllowedIPs=$default6"; break; default : $var['default'] = "AllowedIPs=$default4"; break; } break; case 'TYPE': $list = $value < 4 ? ($value%2 == 1 ? _var($var,'subnets1') : _var($var,'subnets2')) : ($value < 6 ? ($value%2 == 1 ? _var($var,'shared1') : _var($var,'shared2')) : _var($var,'default')); $var['allowedIPs'] = createIPs($list); $var['tunnel'] = ($value == 2 || $value == 3) ? $tunnel : false; $user[] = "$id:$x=\"$value\""; if ($value >= 7) $vpn = $value; break; case 'Network6': if (!$protocol) break; case 'Network': case 'UPNP': case 'DROP': case 'RULE': case 'NAT': $user[] = "$id:0=\"$value\""; break; case 'Address': $hosts = createIPs($value); if ($i == 0) { $conf[] = "$id=$value"; $tunnel = "$id=$hosts"; $tunip = implode(ipv4Addr($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($var,'endpoint'),ipsplit(_var($var,'endpoint'))) === false) $var['endpoint'] .= ":".(explode(ipsplit(_var($var,'internet')),_var($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; case 'AllowedIPs': $conf[] = "$id=".createList($value); break; default: if ($value) $conf[] = "$id=$value"; break; } } } $default4 = '0.0.0.0/0'; $default6 = '::/0'; switch (_var($_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 = _var($_POST,'#cfg'); $wg = _var($_POST,'#wg'); $name = _var($_POST,'#name'); $vtun = _var($_POST,'#vtun'); $gone = explode(',',_var($_POST,'#deleted')); $conf = ['[Interface]']; $user = $peers = $var = []; $tunip = ""; $var['subnets1'] = "AllowedIPs=".createList(_var($_POST,'#subnets1')); $var['subnets2'] = "AllowedIPs=".createList(_var($_POST,'#subnets2')); $var['shared1'] = "AllowedIPs=".createList(_var($_POST,'#shared1')); $var['shared2'] = "AllowedIPs=".createList(_var($_POST,'#shared2')); $var['internet'] = "Endpoint=".createList(_var($_POST,'#internet')); $x = 1; $vpn = 0; parseInput($vtun, $_POST, $x); addPeer($x); 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 ($upstate) wgState($vtun, 'up', _var($_POST,'#type')); // if $tunip (with dots to slashes) not found in nginx config, then reload nginx to add it $nginx = parse_ini_file('/var/local/emhttp/nginx.ini'); if (stripos($nginx['NGINX_CERTNAME'],'.myunraid.net') !== false) { $key = 'NGINX_'.strtoupper($vtun).'FQDN'; if (!isset($nginx[$key]) || stripos($nginx[$key], str_replace('.', '-', $tunip)) === false) { exec("/etc/rc.d/rc.nginx reload"); } } $save = false; break; case 'toggle': $vtun = _var($_POST,'#vtun'); switch (_var($_POST,'#wg')) { case 'stop': wgState($vtun, 'down'); echo status($vtun) ? 1 : 0; break; case 'start': [$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', _var($_POST,'#type')); echo status($vtun) ? 0 : 1; break; } break; case 'ping': $addr = $_POST['#addr']; echo exec("ping -qc1 -W4 $addr | grep -Pom1 '1 received'"); break; case 'public': $ip = _var($_POST,'#ip'); $v4 = _var($_POST,'#prot') != '6'; $v6 = _var($_POST,'#prot') != ''; $int_ipv4 = $v4 ? (preg_match("/^$validIP4$/",$ip) ? $ip : (@dns_get_record($ip,DNS_A)[0]['ip'] ?: '')) : ''; $ext_ipv4 = $v4 ? (http_get_contents('https://wanip4.unraid.net') ?: '') : ''; $int_ipv6 = $v6 ? (preg_match("/^$validIP6$/",$ip) ? $ip : (@dns_get_record($ip,DNS_AAAA)[0]['ipv6'] ?: '')) : ''; $ext_ipv6 = $v6 ? (http_get_contents('https://wanip6.unraid.net') ?: '') : ''; echo "$int_ipv4;$ext_ipv4;$int_ipv6;$ext_ipv6"; break; case 'addtunnel': $vtun = vtun(); $name = _var($_POST,'#name'); touch("$etc/$vtun.conf"); wgState($vtun, 'down'); delete_file("$etc/$vtun.cfg"); delPeer($vtun); autostart($vtun, 'off'); break; case 'deltunnel': $vtun = _var($_POST,'#vtun'); $name = _var($_POST,'#name'); $error = delDocker($vtun); if (!$error) { wgState($vtun, 'down'); delete_file("$etc/$vtun.conf", "$etc/$vtun.cfg"); delPeer($vtun); autostart($vtun, 'off'); // remove tunnel url from nginx config $nginx = parse_ini_file('/var/local/emhttp/nginx.ini'); $key = 'NGINX_'.strtoupper($vtun).'FQDN'; if (isset($nginx[$key])) { exec("/etc/rc.d/rc.nginx reload"); } } echo $error ? 1 : 0; break; case 'import': $name = _var($_POST,'#name'); $user = $peers = $var = $import = $sort = []; $entries = array_filter(array_map('trim',preg_split('/\[(Interface|Peer)\]/', _var($_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)); normalize($id); $import["$id:$i"] = $data; } elseif ($i >= 0) { $import["Name:$i"] = substr(trim($row), 1); } } } if (_var($import,'PrivateKey:0') && !_var($import,'PublicKey:0')) $import['PublicKey:0'] = exec("wg pubkey <<<'"._var($import,'PrivateKey:0')."'"); // delete ListenPort and let WG generate a random local port unset($import['ListenPort:0']); $import['UPNP:0'] = 'no'; $import['NAT:0'] = 'no'; [$subnet, $mask] = my_explode('/', _var($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 = array_map('trim',explode(',', _var($import,"AllowedIPs:$n"))); $vpn = (in_array($default4,$vpn) || in_array($default6,$vpn)) ? 8 : 0; if ($vpn==8) $import["Address:$n"] = ''; $import["TYPE:$n"] = $vpn; ipfilter(_var($import,"AllowedIPs:$n")); if (_var($import,"TYPE:$n") == 0) $var['subnets1'] = "AllowedIPs="._var($import,"AllowedIPs:$n"); } foreach ($import as $key => $val) $sort[] = explode(':',$key)[1]; array_multisort($sort, $import); $x = 1; $conf = ['[Interface]']; $var['default'] = _var($import,'PROT:0') == '' ? "AllowedIPs=$default4" : "AllowedIPs=$default6"; $var['internet'] = "Endpoint=unknown"; $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); addDocker($vtun); autostart($vtun, 'off'); echo $vtun; break; case 'autostart': autostart(_var($_POST,'#vtun'), _var($_POST,'#start')); break; case 'upnp': $upnp = '/var/tmp/upnp'; if (is_executable('/usr/bin/upnpc')) { $gw = _var($_POST,'#gw').':'; $link = _var($_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 = _var($_POST,'#xml'); $vtun = _var($_POST,'#vtun'); $link = _var($_POST,'#link'); $ip = _var($_POST,'#ip'); if (_var($_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] = array_pad($upnp, 2, ''); [$type, $rule] = my_explode(' ', $upnp); echo $rule ? "UPnP: $addr:$rule/$type" : _("UPnP: forwarding not set"); } else { echo _("UPnP: tunnel is inactive"); } break; } ?>