378 lines
14 KiB
Kotlin
378 lines
14 KiB
Kotlin
package dorkbox.netUtil
|
|
|
|
import com.sun.jna.Memory
|
|
import com.sun.jna.Pointer.*
|
|
import com.sun.jna.platform.win32.WinError
|
|
import dorkbox.executor.Executor
|
|
import dorkbox.netUtil.jna.windows.IPHlpAPI
|
|
import dorkbox.netUtil.jna.windows.structs.IP_ADAPTER_ADDRESSES_LH
|
|
import dorkbox.netUtil.jna.windows.structs.IP_ADAPTER_DNS_SERVER_ADDRESS_XP
|
|
import java.io.*
|
|
import java.net.*
|
|
import java.nio.file.Files
|
|
import java.nio.file.Paths
|
|
import java.util.*
|
|
import javax.naming.Context
|
|
import javax.naming.NamingException
|
|
import javax.naming.directory.DirContext
|
|
import javax.naming.directory.InitialDirContext
|
|
|
|
|
|
object Dns {
|
|
/**
|
|
* @throws IOException if the DNS resolve.conf file cannot be read
|
|
*/
|
|
fun setDNSServers(dnsServersString: String) {
|
|
if (Common.OS_LINUX) {
|
|
val dnsServers = dnsServersString.split(",")
|
|
val dnsFile = File("/etc/resolvconf/resolv.conf.d/head")
|
|
|
|
if (!dnsFile.canRead()) {
|
|
throw IOException("Unable to initialize dns server file. Something is SERIOUSLY wrong")
|
|
}
|
|
|
|
BufferedWriter(FileWriter(dnsFile)).use {
|
|
it.write("# File location: /etc/resolvconf/resolv.conf.d/head\n")
|
|
it.write("# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)\n")
|
|
|
|
dnsServers.forEach { dns ->
|
|
it.write("nameserver $dns\n")
|
|
}
|
|
|
|
it.flush();
|
|
}
|
|
|
|
Executor().command("resolvconf", "-u").startBlocking()
|
|
} else {
|
|
throw RuntimeException("NOT IMPL.")
|
|
}
|
|
}
|
|
|
|
|
|
/** Returns all located name servers, which may be empty. */
|
|
val defaultNameServers: List<InetSocketAddress> by lazy {
|
|
val nameServers = getUnsortedDefaultNameServers()
|
|
|
|
if (IPv6.isPreferred) {
|
|
// prefer IPv6: return IPv6 first, then IPv4 (each in the order added)
|
|
nameServers.filter { it.address is Inet6Address } + nameServers.filter { it.address is Inet4Address }
|
|
} else if (IPv4.isPreferred) {
|
|
// skip IPv6 addresses
|
|
nameServers.filter { it.address is Inet4Address }
|
|
}
|
|
|
|
// neither is specified, return in the order added
|
|
nameServers
|
|
}
|
|
|
|
// largely from:
|
|
// https://github.com/dnsjava/dnsjava/blob/fb4889ee7a73f391f43bf6dc78b019d87ae15f15/src/main/java/org/xbill/DNS/config/BaseResolverConfigProvider.java#L22
|
|
private fun getUnsortedDefaultNameServers() : List<InetSocketAddress> {
|
|
val defaultNameServers = mutableListOf<InetSocketAddress>()
|
|
|
|
// Using jndi-dns to obtain the default name servers.
|
|
//
|
|
// See:
|
|
// - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html
|
|
// - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html
|
|
|
|
// Using jndi-dns to obtain the default name servers.
|
|
//
|
|
// See:
|
|
// - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html
|
|
// - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html
|
|
val env = Hashtable<String, String>()
|
|
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.dns.DnsContextFactory"
|
|
env["java.naming.provider.url"] = "dns://"
|
|
try {
|
|
val ctx: DirContext = InitialDirContext(env)
|
|
val dnsUrls = ctx.environment["java.naming.provider.url"] as String?
|
|
val servers = dnsUrls!!.split(" ".toRegex()).toTypedArray()
|
|
|
|
for (server in servers) {
|
|
try {
|
|
defaultNameServers.add(Common.socketAddress(URI(server).host, 53))
|
|
} catch (e: URISyntaxException) {
|
|
Common.logger.debug("Skipping a malformed nameserver URI: {}", server, e)
|
|
}
|
|
}
|
|
} catch (ignore: NamingException) {
|
|
// Will also try JNA/etc if this fails.
|
|
}
|
|
|
|
if (defaultNameServers.isNotEmpty()) {
|
|
return defaultNameServers
|
|
}
|
|
|
|
if (Common.OS_WINDOWS) {
|
|
// have to use JNA to access the WINDOWS resolver info
|
|
|
|
// modified from: https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/config/WindowsResolverConfigProvider.java
|
|
// The recommended method of calling the GetAdaptersAddresses function is to pre-allocate a 15KB working buffer
|
|
var buffer: Memory? = Memory(15 * 1024)
|
|
val size: com.sun.jna.ptr.IntByReference = com.sun.jna.ptr.IntByReference(0)
|
|
|
|
val flags: Int = IPHlpAPI.GAA_FLAG_SKIP_UNICAST or
|
|
IPHlpAPI.GAA_FLAG_SKIP_ANYCAST or
|
|
IPHlpAPI.GAA_FLAG_SKIP_MULTICAST or
|
|
IPHlpAPI.GAA_FLAG_SKIP_FRIENDLY_NAME
|
|
|
|
var error: Int = IPHlpAPI.GetAdaptersAddresses(
|
|
IPHlpAPI.AF_UNSPEC,
|
|
flags,
|
|
NULL,
|
|
buffer,
|
|
size
|
|
)
|
|
|
|
if (error == WinError.ERROR_BUFFER_OVERFLOW) {
|
|
buffer = Memory(size.value.toLong())
|
|
error = IPHlpAPI.GetAdaptersAddresses(
|
|
IPHlpAPI.AF_UNSPEC,
|
|
flags,
|
|
NULL,
|
|
buffer,
|
|
size
|
|
)
|
|
}
|
|
|
|
if (error == WinError.ERROR_SUCCESS) {
|
|
var result: IP_ADAPTER_ADDRESSES_LH? = IP_ADAPTER_ADDRESSES_LH(buffer)
|
|
while (result != null) {
|
|
|
|
// only interfaces with IfOperStatusUp
|
|
if (result.OperStatus == 1) {
|
|
var dns: IP_ADAPTER_DNS_SERVER_ADDRESS_XP? = result.FirstDnsServerAddress
|
|
while (dns != null) {
|
|
|
|
var address: InetAddress
|
|
try {
|
|
address = dns.Address.toAddress()
|
|
if (address is Inet4Address || !address.isSiteLocalAddress) {
|
|
defaultNameServers.add(InetSocketAddress(address, 53))
|
|
} else {
|
|
Common.logger.debug("Skipped site-local IPv6 server address {} on adapter index {}", address, result.IfIndex)
|
|
}
|
|
} catch (e: UnknownHostException) {
|
|
Common.logger.warn("Invalid nameserver address on adapter index {}", result.IfIndex, e)
|
|
}
|
|
dns = dns.Next
|
|
}
|
|
}
|
|
|
|
result = result.Next
|
|
}
|
|
}
|
|
} else {
|
|
// try resolve.conf
|
|
|
|
// first try the default unix config path
|
|
var tryParse = tryParseResolvConfNameservers("/etc/resolv.conf")
|
|
if (!tryParse.first) {
|
|
// then fallback to netware
|
|
tryParse = tryParseResolvConfNameservers("sys:/etc/resolv.cfg")
|
|
}
|
|
|
|
defaultNameServers.addAll(tryParse.second)
|
|
}
|
|
|
|
return defaultNameServers
|
|
}
|
|
|
|
private fun tryParseResolvConfNameservers(path: String): Pair<Boolean, MutableList<InetSocketAddress>> {
|
|
val p = Paths.get(path)
|
|
if (Files.exists(p)) {
|
|
try {
|
|
Files.newInputStream(p).use { `in` ->
|
|
return Pair(true, parseResolvConfNameservers(`in`))
|
|
}
|
|
} catch (e: IOException) {
|
|
// ignore
|
|
}
|
|
}
|
|
return Pair(false, mutableListOf())
|
|
}
|
|
|
|
private fun parseResolvConfNameservers(`in`: InputStream): MutableList<InetSocketAddress> {
|
|
val defaultNameServers = mutableListOf<InetSocketAddress>()
|
|
|
|
InputStreamReader(`in`).use { isr ->
|
|
BufferedReader(isr).use { br ->
|
|
var line: String?
|
|
while (br.readLine().also { line = it } != null) {
|
|
val st = StringTokenizer(line)
|
|
if (!st.hasMoreTokens()) {
|
|
continue
|
|
}
|
|
when (st.nextToken()) {
|
|
"nameserver" -> defaultNameServers.add(InetSocketAddress(st.nextToken(), 53))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return defaultNameServers
|
|
}
|
|
|
|
/**
|
|
* Gets the threshold for the number of dots which must appear in a name before it is considered
|
|
* absolute. The default is 1.
|
|
*/
|
|
val numberDots: Int by lazy {
|
|
if (Common.OS_WINDOWS) {
|
|
1
|
|
} else {
|
|
// first try the default unix config path
|
|
var tryParse = tryParseResolvConfNDots("/etc/resolv.conf")
|
|
if (!tryParse.first) {
|
|
// then fallback to netware
|
|
tryParse = tryParseResolvConfNDots("sys:/etc/resolv.cfg")
|
|
}
|
|
tryParse.second
|
|
}
|
|
}
|
|
|
|
private fun tryParseResolvConfNDots(path: String): Pair<Boolean, Int> {
|
|
val p = Paths.get(path)
|
|
if (Files.exists(p)) {
|
|
try {
|
|
Files.newInputStream(p).use { `in` ->
|
|
return Pair(true, getNdots(`in`))
|
|
}
|
|
} catch (e: IOException) {
|
|
// ignore
|
|
}
|
|
}
|
|
return Pair(false, 1)
|
|
}
|
|
|
|
|
|
// @kotlin.jvm.Throws(IOException::class)
|
|
// private fun parseResolvConf(`in`: InputStream) {
|
|
// InputStreamReader(`in`).use { isr ->
|
|
// BufferedReader(isr).use { br ->
|
|
//
|
|
// var line: String?
|
|
// loop@ while (br.readLine().also { line = it } != null) {
|
|
// val st = StringTokenizer(line)
|
|
// if (!st.hasMoreTokens()) {
|
|
// continue
|
|
// }
|
|
// when (st.nextToken()) {
|
|
// "domain" -> {
|
|
// // man resolv.conf:
|
|
// // The domain and search keywords are mutually exclusive. If more than one instance of
|
|
// // these keywords is present, the last instance wins.
|
|
// searchlist.clear()
|
|
// if (!st.hasMoreTokens()) {
|
|
// continue@loop
|
|
// }
|
|
// addSearchPath(st.nextToken())
|
|
// }
|
|
// "search" -> {
|
|
// // man resolv.conf:
|
|
// // The domain and search keywords are mutually exclusive. If more than one instance of
|
|
// // these keywords is present, the last instance wins.
|
|
// searchlist.clear()
|
|
// while (st.hasMoreTokens()) {
|
|
// addSearchPath(st.nextToken())
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// // man resolv.conf:
|
|
// // The search keyword of a system's resolv.conf file can be overridden on a per-process basis by
|
|
// // setting the environment variable LOCALDOMAIN to a space-separated list of search domains.
|
|
// val localdomain = System.getenv("LOCALDOMAIN")
|
|
// if (localdomain != null && !localdomain.isEmpty()) {
|
|
// searchlist.clear()
|
|
// parseSearchPathList(localdomain, " ")
|
|
// }
|
|
// }
|
|
|
|
// @kotlin.jvm.Throws(IOException::class)
|
|
private fun getNdots(`in`: InputStream): Int {
|
|
var ndots = 1
|
|
|
|
InputStreamReader(`in`).use { isr ->
|
|
BufferedReader(isr).use { br ->
|
|
var line: String?
|
|
loop@ while (br.readLine().also { line = it } != null) {
|
|
val st = StringTokenizer(line)
|
|
if (!st.hasMoreTokens()) {
|
|
continue
|
|
}
|
|
|
|
when (st.nextToken()) {
|
|
"domain" -> {
|
|
// man resolv.conf:
|
|
// The domain and search keywords are mutually exclusive. If more than one instance of
|
|
// these keywords is present, the last instance wins.
|
|
if (!st.hasMoreTokens()) {
|
|
continue@loop
|
|
}
|
|
}
|
|
"search" -> {
|
|
// man resolv.conf:
|
|
// The domain and search keywords are mutually exclusive. If more than one instance of
|
|
// these keywords is present, the last instance wins.
|
|
while (st.hasMoreTokens()) {
|
|
val token = st.nextToken()
|
|
if (token.startsWith("ndots:")) {
|
|
ndots = parseNdots(token.substring(6))
|
|
}
|
|
}
|
|
}
|
|
"options" -> while (st.hasMoreTokens()) {
|
|
val token = st.nextToken()
|
|
if (token.startsWith("ndots:")) {
|
|
ndots = parseNdots(token.substring(6))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// man resolv.conf:
|
|
// The options keyword of a system's resolv.conf file can be amended on a per-process basis by
|
|
// setting the environment variable RES_OPTIONS to a space-separated list of resolver options as
|
|
// explained above under options.
|
|
val resOptions = System.getenv("RES_OPTIONS")
|
|
if (resOptions != null && !resOptions.isEmpty()) {
|
|
val st = StringTokenizer(resOptions, " ")
|
|
while (st.hasMoreTokens()) {
|
|
val token = st.nextToken()
|
|
if (token.startsWith("ndots:")) {
|
|
ndots = parseNdots(token.substring(6))
|
|
}
|
|
}
|
|
}
|
|
|
|
return ndots
|
|
}
|
|
|
|
private fun parseNdots(token: String): Int {
|
|
if (token.isNotEmpty()) {
|
|
try {
|
|
var ndots = token.toInt()
|
|
if (ndots >= 0) {
|
|
if (ndots > 15) {
|
|
// man resolv.conf:
|
|
// The value for this option is silently capped to 15
|
|
ndots = 15
|
|
}
|
|
return ndots
|
|
}
|
|
} catch (e: NumberFormatException) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
return 1
|
|
}
|
|
}
|