NetworkDNS/src/dorkbox/dns/DnsServer.kt

275 lines
12 KiB
Kotlin

/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.dns
import dorkbox.dns.dns.DnsQuestion
import dorkbox.dns.dns.constants.DnsRecordType
import dorkbox.dns.dns.records.ARecord
import dorkbox.dns.dns.serverHandlers.DnsServerHandler
import dorkbox.dns.util.NativeLibrary
import dorkbox.dns.util.Shutdownable
import dorkbox.netUtil.IP.toBytes
import dorkbox.os.OS.isLinux
import dorkbox.os.OS.isMacOsX
import dorkbox.updates.Updates.add
import dorkbox.util.NamedThreadFactory
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.PooledByteBufAllocator
import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelOption
import io.netty.channel.DefaultEventLoopGroup
import io.netty.channel.EventLoopGroup
import io.netty.channel.WriteBufferWaterMark
import io.netty.channel.epoll.EpollDatagramChannel
import io.netty.channel.epoll.EpollEventLoopGroup
import io.netty.channel.kqueue.KQueueDatagramChannel
import io.netty.channel.kqueue.KQueueEventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioDatagramChannel
/**
* from: https://blog.cloudflare.com/how-the-consumer-product-safety-commission-is-inadvertently-behind-the-internets-largest-ddos-attacks/
*
* NOTE: CloudFlare has anti-DNS reflection protections in place. Specifically, we automatically upgrade from UDP to TCP when a DNS response
* is particularly large (generally, over 512 bytes). Since TCP requires a handshake, it prevents source IP address spoofing which is
* necessary for a DNS amplification attack.
*
* In addition, we rate limit unknown resolvers. Again, this helps ensure that our infrastructure can't be abused to amplify attacks.
*
* Finally, across our DNS infrastructure we have deprecated ANY queries and have proposed to the IETF to restrict ANY queries to only
* authorized parties. By neutering ANY, we've significantly reduced the maximum size of responses even for zone files that need to be
* large due to a large number of records.
*
*
*
* ALSO: see LINK-LOCAL MULTICAST NAME RESOLUTION
* https://en.wikipedia.org/wiki/Link-Local_Multicast_Name_Resolution
*
* In responding to queries, responders listen on UDP port 5355 on the following link-scope Multicast address:
*
* IPv4 - 224.0.0.252, MAC address of 01-00-5E-00-00-FC
* IPv6 - FF02:0:0:0:0:0:1:3 (this notation can be abbreviated as FF02::1:3), MAC address of 33-33-00-01-00-03
* The responders also listen on TCP port 5355 on the unicast address that the host uses to respond to queries.
*/
class DnsServer(host: String?, tcpPort: Int) : Shutdownable(DnsServer::class.java) {
companion object {
/**
* Gets the version number.
*/
val version = "2.7.2"
var workerThreadPoolSize = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
init {
// Add this project to the updates system, which verifies this class + UUID + version information
add(DnsServer::class.java, "3aaf262a500147daa340f7274a481a2b", version)
}
}
// private final ServerBootstrap tcpBootstrap;
private val udpBootstrap: Bootstrap
private val udpPort: Int
private var hostName: String? = null
private val dnsServerHandler: DnsServerHandler
init {
udpPort = tcpPort
hostName = host ?: "0.0.0.0"
dnsServerHandler = DnsServerHandler(logger)
val threadName = DnsServer::class.java.simpleName
val threadFactory = NamedThreadFactory(threadName, threadGroup)
val boss: EventLoopGroup
val work: EventLoopGroup
val namedThreadFactory = NamedThreadFactory("$threadName-boss", threadGroup)
if (isLinux && NativeLibrary.isAvailable) {
// epoll network stack is MUCH faster (but only on linux)
boss = EpollEventLoopGroup(1, namedThreadFactory)
work = EpollEventLoopGroup(workerThreadPoolSize, threadFactory)
} else if (isMacOsX && NativeLibrary.isAvailable) {
// KQueue network stack is MUCH faster (but only on macosx)
boss = KQueueEventLoopGroup(1, namedThreadFactory)
work = KQueueEventLoopGroup(workerThreadPoolSize, threadFactory)
} else {
// sometimes the native libraries cannot be loaded, so fall back to NIO
boss = NioEventLoopGroup(1, namedThreadFactory)
work = DefaultEventLoopGroup(workerThreadPoolSize, threadFactory)
}
manageForShutdown(boss)
manageForShutdown(work)
// tcpBootstrap = new ServerBootstrap();
udpBootstrap = Bootstrap()
// if (OS.isAndroid()) {
// // android ONLY supports OIO (not NIO)
// tcpBootstrap.channel(OioServerSocketChannel.class);
// }
// else if (OS.isLinux() && NativeLibrary.isAvailable()) {
// // epoll network stack is MUCH faster (but only on linux)
// tcpBootstrap.channel(EpollServerSocketChannel.class);
// }
// else if (OS.isMacOsX() && NativeLibrary.isAvailable()) {
// // KQueue network stack is MUCH faster (but only on macosx)
// tcpBootstrap.channel(KQueueServerSocketChannel.class);
// }
// else {
// tcpBootstrap.channel(NioServerSocketChannel.class);
// }
//
// tcpBootstrap.group(boss, work)
// .option(ChannelOption.SO_BACKLOG, backlogConnectionCount)
// .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
// .childOption(ChannelOption.SO_KEEPALIVE, true)
// .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH))
// .childHandler(dnsServerHandler);
//
// // have to check options.host for "0.0.0.0". we don't bind to "0.0.0.0", we bind to "null" to get the "any" address!
// if (hostName.equals("0.0.0.0")) {
// tcpBootstrap.localAddress(tcpPort);
// }
// else {
// tcpBootstrap.localAddress(hostName, tcpPort);
// }
//
//
// // android screws up on this!!
// tcpBootstrap.option(ChannelOption.TCP_NODELAY, !OS.isAndroid())
// .childOption(ChannelOption.TCP_NODELAY, !OS.isAndroid());
if (isLinux && NativeLibrary.isAvailable) {
// epoll network stack is MUCH faster (but only on linux)
udpBootstrap.channel(EpollDatagramChannel::class.java)
} else if (isMacOsX && NativeLibrary.isAvailable) {
// KQueue network stack is MUCH faster (but only on macosx)
udpBootstrap.channel(KQueueDatagramChannel::class.java)
} else {
udpBootstrap.channel(NioDatagramChannel::class.java)
}
udpBootstrap.group(work).option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT).option(
ChannelOption.WRITE_BUFFER_WATER_MARK,
WriteBufferWaterMark(WRITE_BUFF_LOW, WRITE_BUFF_HIGH)
) // not binding to specific address, since it's driven by TCP, and that can be bound to a specific address
.localAddress(udpPort) // if you bind to a specific interface, Linux will be unable to receive broadcast packets!
.handler(dnsServerHandler)
}
override fun stopExtraActions() {
dnsServerHandler.stop()
}
/**
* Binds the server to the configured, underlying protocols.
*
*
* This is a more advanced method, and you should consider calling `bind()` instead.
*
* @param blockUntilTerminate will BLOCK until the server stop method is called, and if you want to continue running code after this method
* invocation, bind should be called in a separate, non-daemon thread - or with false as the parameter.
*/
/**
* Binds the server to the configured, underlying protocols.
*
*
* This method will also BLOCK until the stop method is called, and if you want to continue running code after this method invocation,
* bind should be called in a separate, non-daemon thread.
*/
fun bind(blockUntilTerminate: Boolean = true) {
// make sure we are not trying to connect during a close or stop event.
// This will wait until we have finished starting up/shutting down.
synchronized(shutdownInProgress) {}
// The bootstraps will be accessed ONE AT A TIME, in this order!
val future: ChannelFuture
val logger2 = logger
// TCP
// Wait until the connection attempt succeeds or fails.
// try {
// future = tcpBootstrap.bind();
// future.await();
// } catch (Exception e) {
// // String errorMessage = stopWithErrorMessage(logger2,
// // "Could not bind to address " + hostName + " TCP port " + tcpPort +
// // " on the server.",
// // e);
// // throw new IllegalArgumentException(errorMessage);
// throw new RuntimeException();
// }
//
// if (!future.isSuccess()) {
// // String errorMessage = stopWithErrorMessage(logger2,
// // "Could not bind to address " + hostName + " TCP port " + tcpPort +
// // " on the server.",
// // future.cause());
// // throw new IllegalArgumentException(errorMessage);
// throw new RuntimeException();
// }
//
// // logger2.info("Listening on address {} at TCP port: {}", hostName, tcpPort);
//
// manageForShutdown(future);
// UDP
// Wait until the connection attempt succeeds or fails.
try {
future = udpBootstrap.bind()
future.await()
} catch (e: Exception) {
val errorMessage = stopWithErrorMessage(
logger2, "Could not bind to address $hostName UDP port $udpPort on the server.", e
)
throw IllegalArgumentException(errorMessage)
}
if (!future.isSuccess) {
val errorMessage = stopWithErrorMessage(
logger2, "Could not bind to address $hostName UDP port $udpPort on the server.", future.cause()
)
throw IllegalArgumentException(errorMessage)
}
// logger2.info("Listening on address {} at UDP port: {}", hostName, udpPort);
manageForShutdown(future)
// we now BLOCK until the stop method is called.
// if we want to continue running code in the server, bind should be called in a separate, non-daemon thread.
if (blockUntilTerminate) {
waitForShutdown()
}
}
/**
* Adds a domain name query result, so clients that request the domain name will get the ipAddress
*
* @param domainName the domain name to have results for
* @param ipAddresses the ip addresses (can be multiple) to return for the requested domain name
*/
fun aRecord(domainName: String, dClass: Int, ttl: Int, vararg ipAddresses: String) {
val name = DnsQuestion.createName(domainName, DnsRecordType.A)
val length = ipAddresses.size
val records = mutableListOf<ARecord>()
for (i in 0 until length) {
val address = toBytes(ipAddresses[i])
records.add(ARecord(name, dClass, ttl.toLong(), address))
}
dnsServerHandler.addARecord(name, records)
}
}