diff --git a/plugins/dns/dns_plugin.js b/plugins/dns/dns_plugin.js index 39007465..e168dd94 100644 --- a/plugins/dns/dns_plugin.js +++ b/plugins/dns/dns_plugin.js @@ -15,6 +15,8 @@ 'use strict'; +const _ = require('lodash'); +const {Address4, Address6} = require('ip-address'); const Plugin = require('../plugin.js'); const pl = require('../plugin_loader.js'); const event = require('../../core/event.js'); @@ -92,8 +94,17 @@ class DNSPlugin extends Plugin { await fs.writeFileAsync(this._getConfFilePath(), content); await fs.unlinkAsync(this._getResolvFilePath()).catch((err) => {}); - if (this.networkConfig.nameservers) { - const nameservers = this.networkConfig.nameservers.map((n) => `nameserver ${n}`).join("\n"); + if (this.networkConfig.nameservers || this.networkConfig.dns6Servers) { + let dnsservers = this.networkConfig.nameservers || []; + dnsservers = dnsservers.filter(i => new Address4(i).isValid()); + if (this.networkConfig.dns6Servers && _.isArray(this.networkConfig.dns6Servers)) + dnsservers = dnsservers.concat(this.networkConfig.dns6Servers.filter(i => new Address6(i).isValid())); + if (dnsservers.length == 0) { + this.fatal("Cannot find valid dns options for", this.name); + return; + } + this.log.info("write dns conf file", this.name, dnsservers); + const nameservers = _.uniq(dnsservers).map((n) => `nameserver ${n}`).join("\n") + "\n"; await fs.writeFileAsync(this._getResolvFilePath(), nameservers); } else { if (this.networkConfig.useNameserversFromWAN) { @@ -131,8 +142,17 @@ class DNSPlugin extends Plugin { async applyDefaultResolvConf() { await fs.unlinkAsync(this._getResolvFilePath()).catch((err) => {}); - if (this.networkConfig.nameservers) { - const nameservers = this.networkConfig.nameservers.map((n) => `nameserver ${n}`).join("\n"); + if (this.networkConfig.nameservers || this.networkConfig.dns6Servers) { + let dnsservers = this.networkConfig.nameservers || []; + dnsservers = dnsservers.filter(i => new Address4(i).isValid()); + if (this.networkConfig.dns6Servers && _.isArray(this.networkConfig.dns6Servers)) + dnsservers = dnsservers.concat(this.networkConfig.dns6Servers.filter(i => new Address6(i).isValid())); + if (dnsservers.length == 0) { + this.fatal("Cannot find valid dns options for", this.name); + return; + } + this.log.info("write dns conf file", this.name, dnsservers); + const nameservers = _.uniq(dnsservers).map((n) => `nameserver ${n}`).join("\n") + "\n"; await fs.writeFileAsync(this._getResolvFilePath(), nameservers); } else { if (this.networkConfig.useNameserversFromWAN) { @@ -140,7 +160,7 @@ class DNSPlugin extends Plugin { if (routingPlugin) { this.subscribeChangeFrom(routingPlugin); const wanIntfPlugins = routingPlugin.getActiveWANPlugins(); - if (wanIntfPlugins.length > 0) { + if (wanIntfPlugins && wanIntfPlugins.length > 0) { const wanIntf = wanIntfPlugins[0].name; await fs.symlinkAsync(r.getInterfaceResolvConfPath(wanIntf), this._getResolvFilePath()); } else { @@ -204,8 +224,8 @@ class DNSPlugin extends Plugin { return; } const state = await intfPlugin.state(); - if (!state || !state.ip4) { - this.log.warn(`Interface ${this.name} does not have IPv4 address`); + if (!state || (!state.ip4 && !state.ip6)) { + this.log.warn(`Interface ${this.name} does not have IPv4 or IPv6 address`); return; } await this.prepareEnvironment(); diff --git a/plugins/interface/intf_base_plugin.js b/plugins/interface/intf_base_plugin.js index ab3d9034..3ec81dc9 100644 --- a/plugins/interface/intf_base_plugin.js +++ b/plugins/interface/intf_base_plugin.js @@ -201,6 +201,10 @@ class InterfaceBasePlugin extends Plugin { return `/run/resolvconf/interface/${this.name}.dhclient`; } + _getDhcpcdFilePath() { + return `/run/resolvconf/interface/${this.name}.dhcpcd`; + } + async _getDHCPCDLease6Filename() { const version = await exec(`dhcpcd --version | head -n 1 | awk '{print $2}'`).then(result => result.stdout.trim()).catch((err) => { this.log.error(`Failed to get dhcpcd version`, err.message); @@ -389,7 +393,6 @@ class InterfaceBasePlugin extends Plugin { await exec(`sudo systemctl restart firerouter_dhcpcd6@${this.name}`).catch((err) => { this.fatal(`Failed to enable dhcpv6 client on interface ${this.name}: ${err.message}`); }); - // TODO: do not support dns nameservers from DHCPv6 currently } else { if (this.networkConfig.ipv6 && (_.isString(this.networkConfig.ipv6) || _.isArray(this.networkConfig.ipv6))) { // add link local route to interface local and default routing table @@ -631,17 +634,29 @@ class InterfaceBasePlugin extends Plugin { } async applyDnsSettings() { - if (this.networkConfig.dhcp || (this.networkConfig.nameservers && this.networkConfig.nameservers.length > 0)) { + if (this.isDHCP() || (this.networkConfig.nameservers && this.networkConfig.nameservers.length > 0) || (this.networkConfig.dns6Servers && this.networkConfig.dns6Servers.length > 0)) { await fs.accessAsync(r.getInterfaceResolvConfPath(this.name), fs.constants.F_OK).then(() => { this.log.info(`Remove old resolv conf for ${this.name}`); return fs.unlinkAsync(r.getInterfaceResolvConfPath(this.name)); }).catch((err) => {}); // specified DNS nameservers supersedes those assigned by DHCP + let dnsservers = []; if (this.networkConfig.nameservers && this.networkConfig.nameservers.length > 0 && this.networkConfig.nameservers.some(s => new Address4(s).isValid())) { - const nameservers = this.networkConfig.nameservers.filter(s => new Address4(s).isValid()).map((nameserver) => `nameserver ${nameserver}`).join("\n"); + dnsservers = this.networkConfig.nameservers.filter(s => new Address4(s).isValid()); + } + + if (this.networkConfig.dns6Servers && this.networkConfig.dns6Servers.some(s => new Address6(s).isValid())) { + dnsservers = dnsservers.concat(this.networkConfig.dns6Servers.filter(s => new Address6(s).isValid())); + } + if (dnsservers.length > 0) { + let nameservers = dnsservers.map((nameserver) => `nameserver ${nameserver}`).join("\n") + "\n"; await fs.writeFileAsync(r.getInterfaceResolvConfPath(this.name), nameservers); } else { - await fs.symlinkAsync(this._getResolvConfFilePath(), r.getInterfaceResolvConfPath(this.name)).catch((err) => {}); + let content = await fs.readFileAsync(this._getResolvConfFilePath(), {encoding: "utf8"}); + const dns6 = await this.getOrigDNS6Nameservers(); + if (!content.endsWith("\n")) content += "\n" + content += dns6.map((nameserver) => `nameserver ${nameserver}`).join("\n") + "\n"; + await fs.writeFileAsync(r.getInterfaceResolvConfPath(this.name), content); } } } @@ -781,13 +796,16 @@ class InterfaceBasePlugin extends Plugin { } async updateRouteForDNS() { - // TODO: there is no IPv6 DNS currently const dns = await this.getDNSNameservers(); const gateway = await routing.getInterfaceGWIP(this.name, 4); + const gateway6 = await routing.getInterfaceGWIP(this.name, 6); if (!_.isArray(dns) || dns.length === 0 || !gateway) return; for (const dnsIP of dns) { - await routing.addRouteToTable(dnsIP, gateway, this.name, `${this.name}_default`, null, 4, true).catch((err) => {}); + if (new Address4(dnsIP).isValid()) + await routing.addRouteToTable(dnsIP, gateway, this.name, `${this.name}_default`, null, 4, true).catch((err) => {}); + else + await routing.addRouteToTable(dnsIP, gateway6, this.name, `${this.name}_default`, null, 6, true).catch((err) => {}); } } @@ -961,6 +979,21 @@ class InterfaceBasePlugin extends Plugin { return dns; } + async getDns4Nameservers() { + const dns = await this.getDNSNameservers() || []; + return dns.filter(i => new Address4(i).isValid()); + } + + async getDns6Nameservers() { + const dns = await this.getDNSNameservers() || []; + return dns.filter(i => new Address6(i).isValid()); + } + + async getOrigDNS6Nameservers() { + const dns6 = await fs.readFileAsync(this._getDhcpcdFilePath(), {encoding: "utf8"}).then(content => content.trim().split("\n").filter(line => line.startsWith("nameserver")).map(line => line.replace("nameserver", "").trim())).catch((err) => null); + return dns6 || []; + } + async getPrefixDelegations() { const pds = await fs.readFileAsync(r.getInterfaceDelegatedPrefixPath(this.name), {encoding: "utf8"}).then(content => content.trim().split("\n").filter(line => line.length > 0)).catch((err) => null); return pds; @@ -1262,8 +1295,9 @@ class InterfaceBasePlugin extends Plugin { } // use throw error for Promise.any - async _getDNSResult(dnsTestDomain, srcIP, nameserver, sendEvent = false) { - const cmd = `dig -4 -b ${srcIP} +time=3 +short +tries=2 @${nameserver} ${dnsTestDomain}`; + async _getDNSResult(dnsTestDomain, srcIP, nameserver, sendEvent = false, af = 4) { + const cmd = `dig -${af} -b ${srcIP} +time=3 +short +tries=2 @${nameserver} ${dnsTestDomain}`; + this.log.debug("dig dns command:", cmd); const result = await exec(cmd).catch((err) => null); let dnsResult = null; @@ -1278,7 +1312,6 @@ class InterfaceBasePlugin extends Plugin { } } - const wanName = this.networkConfig && this.networkConfig.meta && this.networkConfig.meta.name; const wanUUID = this.networkConfig && this.networkConfig.meta && this.networkConfig.meta.uuid; @@ -1286,6 +1319,7 @@ class InterfaceBasePlugin extends Plugin { era.addStateEvent(EventConstants.EVENT_DNS_STATE, nameserver, dnsResult ? 0 : 1, { "wan_intf_name":wanName, "wan_intf_uuid":wanUUID, + "wan_intf_address":srcIP, "name_server":nameserver, "dns_test_domain":dnsTestDomain }); @@ -1300,22 +1334,43 @@ class InterfaceBasePlugin extends Plugin { async getDNSResult(dnsTestDomain, sendEvent = false) { const nameservers = await this.getDNSNameservers(); const ip4s = await this.getIPv4Addresses(); + const ip6s = await this.getIPv6Addresses(); + let dnsResult = []; if (_.isArray(nameservers) && nameservers.length !== 0 && _.isArray(ip4s) && ip4s.length !== 0) { const srcIP = ip4s[0].split('/')[0]; - const promises = []; for(const nameserver of nameservers) { + if (!new Address4(nameserver).isValid()) continue; promises.push(this._getDNSResult(dnsTestDomain, srcIP, nameserver, sendEvent)); } + const result = await Promise.any(promises).catch((err) => { + this.log.warn("no valid ipv4 dns nameservers on", this.name, err.message); + }); + if (result) dnsResult.push(result); + } + if (_.isArray(nameservers) && nameservers.length !== 0 && _.isArray(ip6s) && ip6s.length !== 0) { + const srcIPs = ip6s.map(i => i.split('/')[0]); + const promises = []; + for(const nameserver of nameservers) { + for (const srcIP of srcIPs) { + const ipaddr = new Address6(srcIP); + if (!ipaddr.isValid() || ipaddr.getScope().toLowerCase() != "global") continue; + if (!new Address6(nameserver).isValid()) continue; + promises.push(this._getDNSResult(dnsTestDomain, srcIP, nameserver, sendEvent, 6)); + } + } const result = await Promise.any(promises).catch((err) => { - this.log.error("no valid dns from any nameservers on", this.name, err.message); - return null; + this.log.warn("no valid ipv6 dns nameservers on", this.name, err.message); }); - return result; + if (result) dnsResult.push(result); } + if (dnsResult.length > 0) { + return dnsResult; + } + this.log.error("no valid dns from any nameservers on", this.name, err.message); return null; } @@ -1411,7 +1466,7 @@ class InterfaceBasePlugin extends Plugin { } async state() { - let [mac, mtu, carrier, duplex, speed, operstate, txBytes, rxBytes, rtid, ip4, ip4s, routableSubnets, ip6, gateway, gateway6, dns, origDns, pds, present] = await Promise.all([ + let [mac, mtu, carrier, duplex, speed, operstate, txBytes, rxBytes, rtid, ip4, ip4s, routableSubnets, ip6, gateway, gateway6, dns, origDns, dns6, pds, present] = await Promise.all([ this._getSysFSClassNetValue("address"), this._getSysFSClassNetValue("mtu"), this._getSysFSClassNetValue("carrier"), @@ -1427,8 +1482,9 @@ class InterfaceBasePlugin extends Plugin { exec(`ip addr show dev ${this.name} | awk '/inet6 /' | awk '{print $2}'`, {encoding: "utf8"}).then((result) => result.stdout.trim() || null).catch((err) => null), routing.getInterfaceGWIP(this.name) || null, routing.getInterfaceGWIP(this.name, 6) || null, - this.getDNSNameservers(), + this.getDns4Nameservers(), this.getOrigDNSNameservers(), + this.getOrigDNS6Nameservers(), this.getPrefixDelegations(), this.isInterfacePresent() ]); @@ -1442,7 +1498,7 @@ class InterfaceBasePlugin extends Plugin { wanConnState = this.getWANConnState() || {}; wanTestResult = this._wanStatus; // use a different name to differentiate from existing wanConnState } - return {mac, mtu, carrier, duplex, speed, operstate, txBytes, rxBytes, ip4, ip4s, routableSubnets, ip6, gateway, gateway6, dns, origDns, pds, rtid, wanConnState, wanTestResult, present}; + return {mac, mtu, carrier, duplex, speed, operstate, txBytes, rxBytes, ip4, ip4s, routableSubnets, ip6, gateway, gateway6, dns, origDns, dns6, pds, rtid, wanConnState, wanTestResult, present}; } onEvent(e) { diff --git a/plugins/interface/phy_intf_plugin.js b/plugins/interface/phy_intf_plugin.js index e7024295..aafe3079 100644 --- a/plugins/interface/phy_intf_plugin.js +++ b/plugins/interface/phy_intf_plugin.js @@ -40,6 +40,7 @@ class PhyInterfacePlugin extends InterfaceBasePlugin { await exec(`sudo cp ${r.getFireRouterHome()}/scripts/firerouter_dhcpcd_update_rt /lib/dhcpcd/dhcpcd-hooks/`); await exec(`sudo cp ${r.getFireRouterHome()}/scripts/firerouter_dhcpcd_record_pd /lib/dhcpcd/dhcpcd-hooks/`); await exec(`sudo cp ${r.getFireRouterHome()}/scripts/firerouter_dhcpcd_record_lease /lib/dhcpcd/dhcpcd-hooks/`); + await exec(`sudo cp ${r.getFireRouterHome()}/scripts/firerouter_dhcpcd_resolv_dns6 /lib/dhcpcd/dhcpcd-hooks/`); // copy firerouter_dhclient@.service await exec(`sudo cp ${r.getFireRouterHome()}/scripts/firerouter_dhclient@.service /etc/systemd/system/`); // copy firerouter_dhcpcd6@.service diff --git a/scripts/firerouter_dhcpcd_resolv_dns6 b/scripts/firerouter_dhcpcd_resolv_dns6 new file mode 100644 index 00000000..483c39d4 --- /dev/null +++ b/scripts/firerouter_dhcpcd_resolv_dns6 @@ -0,0 +1,138 @@ +# Generate /run/resolvconf/resolv.conf +# Support resolvconf(8) if available +# We can merge other dhcpcd resolv.conf files into one like resolvconf, +# but resolvconf is preferred as other applications like VPN clients +# can readily hook into it. +# Also, resolvconf can configure local nameservers such as bind +# or dnsmasq. This is important as the libc resolver isn't that powerful. + +resolv_conf_file="/run/resolvconf/interface/$interface.dhcpcd" +NL=" +" +: ${resolvconf:=resolvconf} + +syslog info "dhcpcd resolv dns6 $interface" +# Extract any ND DNS options from the RA +# For now, we ignore the lifetime of the DNS options unless they +# are absent or zero. +# In this case they are removed from consideration. +# See draft-gont-6man-slaac-dns-config-issues-01 for issues +# regarding DNS option lifetime in ND messages. +eval_nd_dns() +{ + eval ltime=\$nd${i}_rdnss${j}_lifetime + if [ -z "$ltime" ] || [ "$ltime" = 0 ]; then + rdnss= + else + eval rdnss=\$nd${i}_rdnss${j}_servers + fi + eval ltime=\$nd${i}_dnssl${j}_lifetime + if [ -z "$ltime" ] || [ "$ltime" = 0 ]; then + dnssl= + else + eval dnssl=\$nd${i}_dnssl${j}_search + fi + + [ -z "${rdnss}${dnssl}" ] && return 1 + + [ -n "$rdnss" ] && new_rdnss="$new_rdnss${new_rdnss:+ }$rdnss" + [ -n "$dnssl" ] && new_dnssl="$new_dnssl${new_dnssl:+ }$dnssl" + j=$(($j + 1)) + return 0 +} + +add_resolv_conf() +{ + conf="$signature$NL" + warn=true + + # Loop to extract the ND DNS options using our indexed shell values + i=1 + j=1 + while true; do + while true; do + eval_nd_dns || break + done + i=$(($i + 1)) + j=1 + eval_nd_dns || break + done + [ -n "$new_rdnss" ] && \ + new_domain_name_servers="$new_domain_name_servers${new_domain_name_servers:+ }$new_rdnss" + [ -n "$new_dnssl" ] && \ + new_domain_search="$new_domain_search${new_domain_search:+ }$new_dnssl" + + # Derive a new domain from our various hostname options + if [ -z "$new_domain_name" ]; then + if [ "$new_dhcp6_fqdn" != "${new_dhcp6_fqdn#*.}" ]; then + new_domain_name="${new_dhcp6_fqdn#*.}" + elif [ "$new_fqdn" != "${new_fqdn#*.}" ]; then + new_domain_name="${new_fqdn#*.}" + elif [ "$new_host_name" != "${new_host_name#*.}" ]; then + new_domain_name="${new_host_name#*.}" + fi + fi + + # If we don't have any configuration, remove it + if [ -z "$new_domain_name_servers" ] && + [ -z "$new_domain_name" ] && + [ -z "$new_domain_search" ]; then + remove_resolv_conf + return $? + fi + + if [ -n "$new_domain_name" ]; then + set -- $new_domain_name + if valid_domainname "$1"; then + conf="${conf}domain $1$NL" + else + syslog err "Invalid domain name: $1" + fi + # If there is no search this, make this one + if [ -z "$new_domain_search" ]; then + new_domain_search="$new_domain_name" + [ "$new_domain_name" = "$1" ] && warn=true + fi + fi + if [ -n "$new_domain_search" ]; then + if valid_domainname_list $new_domain_search; then + conf="${conf}search $new_domain_search$NL" + elif ! $warn; then + syslog err "Invalid domain name in list:" \ + "$new_domain_search" + fi + fi + for x in ${new_domain_name_servers}; do + conf="${conf}nameserver $x$NL" + done + + echo "${conf}" > $resolv_conf_file + + if type "$resolvconf" >/dev/null 2>&1; then + [ -n "$ifmetric" ] && export IF_METRIC="$ifmetric" + printf %s "$conf" | "$resolvconf" -a "$ifname" + return $? + fi +} + +remove_resolv_conf() +{ + if type "$resolvconf" >/dev/null 2>&1; then + "$resolvconf" -d "$ifname" -f + fi + rm $resolv_conf_file +} + +# For ease of use, map DHCP6 names onto our DHCP4 names +case "$reason" in +BOUND6|RENEW6|REBIND6|REBOOT6|INFORM6) + new_domain_name_servers="$new_dhcp6_name_servers" + new_domain_search="$new_dhcp6_domain_search" + ;; +esac + +if $if_up || [ "$reason" = ROUTERADVERT ]; then + add_resolv_conf +elif $if_down; then + remove_resolv_conf +fi \ No newline at end of file diff --git a/tests/plugins/test_dns.js b/tests/plugins/test_dns.js new file mode 100644 index 00000000..6a7a31f1 --- /dev/null +++ b/tests/plugins/test_dns.js @@ -0,0 +1,50 @@ +/* Copyright 2016-2024 Firewalla Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict' + +let chai = require('chai'); +let expect = chai.expect; + +const exec = require('child-process-promise').exec; +let log = require('../../util/logger.js')(__filename, 'info'); + +let DNSPlugin = require('../../plugins/dns/dns_plugin.js'); + +describe('Test interface base dhcp6', function(){ + this.timeout(30000); + + before((done) => ( + async() => { + this.plugin = new DNSPlugin("eth5"); + this.plugin.configure({useNameserversFromWAN: false, dns6Servers: ["2606:4700:4700::1111", "2001:4860:4860::8888"]}); + done(); + })() + ); + + after((done) => ( + async() => { + await exec(`rm ${this.plugin._getResolvFilePath()}`).catch(err=>null); + await exec(`rm ${this.plugin._getConfFilePath()}`).catch(err=>null); + done(); + })() + ); + + it('should dns6', async() => { + this._intfUuid = "fake-uuid"; + await this.plugin.writeDNSConfFile(); + log.debug(`dns resolv ${this.plugin._getResolvFilePath()}\n`, await exec(`cat ${this.plugin._getResolvFilePath()}`).then(r => r.stdout.trim()).catch(err => null)); + }); + }); diff --git a/tests/plugins/test_intf_base.js b/tests/plugins/test_intf_base.js index 81d83252..38536451 100644 --- a/tests/plugins/test_intf_base.js +++ b/tests/plugins/test_intf_base.js @@ -94,3 +94,54 @@ describe('Test interface base dhcp6', function(){ expect(this.plugin._getDuidType('00:04:7e:89:20:22:89:15:45:b8:ac:05:3c:68:2b:08:04:8f')).to.be.equal('DUID-UUID'); }); }); + + + describe('Test interface base dns', function(){ + this.timeout(30000); + + before((done) => ( + async() => { + this.plugin = new InterfaceBasePlugin("eth0"); + this.plugin.configure({dhcp6:{}, dhcp:true}); + done(); + })() + ); + + after((done) => ( + async() => { + done(); + })() + ); + + it('should config dns6', async() => { + await this.plugin.configure({dns6Servers: ["2606:4700:4700::1111", "2001:4860:4860::8888"], dhcp:false}); + await this.plugin.applyDnsSettings(); + log.debug("dns6", await this.plugin.getOrigDNS6Nameservers()); + log.debug("resolv.conf\n", await exec("cat /etc/resolv.conf").then(ret => ret.stdout.trim()).catch( (err) => {log.error(err.message)})); + }); + + it('should dhcp dns6', async() => { + await this.plugin.configure({dhcp:true}); + await this.plugin.applyDnsSettings(); + log.debug("dns6", await this.plugin.getOrigDNS6Nameservers()); + log.debug("resolv.conf\n", await exec("cat /etc/resolv.conf").then(ret => ret.stdout.trim()).catch( (err) => {log.error(err.message)})); + }); + + it.skip('should get dns result', async() => { + const config = await exec('redis-cli -n 1 get sysdb:networkConfig | jq -c .interface.phy.eth0').then(r => r.stdout.trim()).catch((err) => {return '{dhcp:true, extra:{}}'}) ; + await this.plugin.configure(JSON.parse(config)); + const ip6s = await this.plugin.getIPv6Addresses(); + const dns6 = await this.plugin.getOrigDNS6Nameservers(); + + const result = await this.plugin._getDNSResult("archlinux.org", ip6s.pop().split('/')[0], dns6[0], false, 6); + log.debug("dig dns result:", result); + // expect(result).to.be.equal("95.217.163.246"); + }); + + it('should run dns test', async() => { + const results = await this.plugin.getDNSResult("archlinux.org", false); + log.debug("dig dns result:", results); + // expect(result).to.be.equal(["95.217.163.246","95.217.163.246"]); + }); + }); +