NetworkUtils/src/dorkbox/netUtil/IPv4.kt

679 lines
22 KiB
Kotlin

/*
* Copyright 2020 Dorkbox, llc
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you 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.netUtil
import java.io.IOException
import java.io.Writer
import java.net.Inet4Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.util.*
import kotlin.math.floor
import kotlin.math.ln
import kotlin.math.pow
/**
* A class that holds a number of network-related constants, also from:
* (Netty, apache 2.0 license)
* https://github.com/netty/netty/blob/4.1/common/src/main/java/io/netty/util/NetUtil.java
*
* This class borrowed some of its methods from a modified fork of the
* [Inet6Util class]
* (http://svn.apache.org/repos/asf/harmony/enhanced/java/branches/java6/classlib/modules/luni/src/main/java/org/apache/harmony/luni/util/Inet6Util.java) which was part of Apache Harmony.
*/
object IPv4 {
/**
* Returns `true` if IPv4 should be used even if the system supports both IPv4 and IPv6. Setting this
* property to `true` will disable IPv6 support. The default value of this property is `false`.
*
* @see [Java SE networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html)
*/
val isPreferred = Common.getBoolean("java.net.preferIPv4Stack", false)
/**
* The [Inet4Address] that represents the IPv4 loopback address '127.0.0.1'
*/
val LOCALHOST: Inet4Address by lazy {
// Create IPv4 loopback address.
// this will ALWAYS work
InetAddress.getByAddress("localhost", byteArrayOf(127, 0, 0, 1)) as Inet4Address
}
/**
* Windows is unable to work with 0.0.0.0 directly, and if you use LOOPBACK, you might not be able to access the server from another
* machine.
*
* What this does is open a connection to 1.1.1.1 and see get the interface this traffic was on, and use that interface IP address
*/
val WILDCARD: String by lazy {
if (Common.OS_WINDOWS) {
// silly windows can't work with 0.0.0.0, BUT we can't use loopback because we might need to reach this machine from a different host
// what we do is open a connection to 1.1.1.1 and see what interface this happened on, and this is used as the accessible
// interface
var ip = "127.0.0.1"
runCatching {
Socket().use {
it.connect(InetSocketAddress("1.1.1.1", 80))
ip = it.localAddress.hostAddress
}
}.onFailure {
Common.logger.error("Unable to determine outbound traffic local address. Using loopback instead.", it)
}
ip
} else {
// everyone else works correctly
"0.0.0.0"
}
}
private val SLASH_REGEX = "\\.".toRegex()
/**
* Determine whether a given string is a valid CIDR IP address. Accepts only 1.2.3.4/24
*
* @param ipAsString The string that will be checked.
*
* @return return true if the string is a valid IP address, false if it is not.
*/
fun isValidCidr(ipAsString: String): Boolean {
if (ipAsString.isEmpty()) {
return false
}
val slashIndex = ipAsString.indexOf('/')
if (slashIndex < 6) {
// something is malformed.
return false
}
val ipOnly = ipAsString.substring(0, slashIndex)
if (!isValid(ipOnly)) {
return false
}
try {
val cidr = ipAsString.substring(slashIndex + 1).toInt()
if (cidr in 0..32) {
return true
}
} catch (ignored: Exception) {
}
return false
}
/**
* Takes a [String] and parses it to see if it is a valid IPV4 address.
*
* @return true, if the string represents an IPV4 address in dotted notation, false otherwise
*/
fun isValid(ip: String): Boolean {
return isValidIpV4Address(ip, 0, ip.length)
}
internal fun isValidIpV4Address(ip: String, from: Int, toExcluded: Int): Boolean {
val len = toExcluded - from
if (len !in 7..15) {
return false
}
var from = from
var i = ip.indexOf('.', from)
if (i <= 0 || !isValidIpV4Word(ip, from, i)) {
return false
}
from = i + 1
i = ip.indexOf('.', from)
if (i <= 0 || !isValidIpV4Word(ip, from, i)) {
return false
}
from = i + 1
i = ip.indexOf('.', from)
if (i <= 0 || !isValidIpV4Word(ip, from, i)) {
return false
}
if (i <= 0 || !isValidIpV4Word(ip, i + 1, toExcluded)) {
return false
}
return true
}
private fun isValidIpV4Word(word: CharSequence, from: Int, toExclusive: Int): Boolean {
val len = toExclusive - from
var c0 = ' '
var c1 = ' '
var c2 = ' '
if (len < 1 || len > 3 || word[from].also { c0 = it } < '0') {
return false
}
return if (len == 3) {
word[from + 1].also { c1 = it } >= '0'
&& word[from + 2].also { c2 = it } >= '0'
&& (c0 <= '1' && c1 <= '9' && c2 <= '9' || c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9'))
}
else c0 <= '9' && (len == 1 || isValidNumericChar(word[from + 1]))
}
internal fun isValidNumericChar(c: Char): Boolean {
return c in '0'..'9'
}
internal fun isValidIPv4MappedChar(c: Char): Boolean {
return c == 'f' || c == 'F'
}
fun toBytesorNull(ip: String): ByteArray? {
if (!isValid(ip)) {
return null
}
var i = 0
return byteArrayOf(
ipv4WordToByte(ip, 0, ip.indexOf('.', 1).also { i = it }),
ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }),
ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }),
ipv4WordToByte(ip, i + 1, ip.length)
)
}
fun toBytes(ip: String): ByteArray {
var i = 0
return byteArrayOf(
ipv4WordToByte(ip, 0, ip.indexOf('.', 1).also { i = it }),
ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }),
ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }),
ipv4WordToByte(ip, i + 1, ip.length)
)
}
private fun decimalDigit(str: CharSequence, pos: Int): Int {
return str[pos] - '0'
}
private fun ipv4WordToByte(ip: CharSequence, from: Int, toExclusive: Int): Byte {
var newFrom = from
var ret = decimalDigit(ip, newFrom)
newFrom++
if (newFrom == toExclusive) {
return ret.toByte()
}
ret = ret * 10 + decimalDigit(ip, newFrom)
newFrom++
return if (newFrom == toExclusive) {
ret.toByte()
} else (ret * 10 + decimalDigit(ip, newFrom)).toByte()
}
fun findFreeSubnet24(): ByteArray? {
Common.logger.info("Scanning for available cidr...")
// have to find a free cidr
// start with 10.x.x.x /24 and just march through starting at 0 -> 200 for each, ping to see if there is a gateway (should be)
// and go from there.
// on linux, PING has the setuid bit set - so it runs "as root". isReachable() requires either java to have the setuid bit set
// (ie: sudo setcap cap_net_raw=ep /usr/lib/jvm/jdk/bin/java) or it requires to be run as root. We run as root in production, so it
// works.
val ip = byteArrayOf(10, 0, 0, 0)
var subnet24Counter = 0
while (true) {
ip[3]++
if (ip[3] > 255) {
ip[3] = 1
ip[2]++
subnet24Counter = 0
}
if (ip[2] > 255) {
ip[2] = 0
ip[1]++
}
if (ip[1] > 255) {
Common.logger.error("Exhausted all ip searches. FATAL ERROR.")
return null
}
try {
val address = InetAddress.getByAddress(ip)
val reachable = address.isReachable(100)
if (!reachable) {
subnet24Counter++
}
if (subnet24Counter == 250) {
// this means that we tried all /24 IPs, and ALL of them came back an "non-responsive". 100ms timeout is OK, because
// we are on a LAN, that should have MORE than one IP in the cidr, and it should be fairly responsive (ie: <10ms ping)
// we found an empty cidr
ip[3] = 1
return ip
}
} catch (e: IOException) {
e.printStackTrace()
return null
}
}
}
/**
* Scans for existing IP addresses on the network.
*
* @param startingIp the IP address to start scanning at
* @param numberOfHosts the number of hosts to scan for. A /28 is 14 hosts : 2^(32-28) - 2 = 14
*
* @return true if no hosts were reachable (pingable)
*/
fun scanHosts(startingIp: String, numberOfHosts: Int): Boolean {
Common.logger.info("Scanning {} hosts, starting at IP {}.", numberOfHosts, startingIp)
val split = startingIp.split(SLASH_REGEX).toTypedArray()
val a = split[0].toByte()
val b = split[1].toByte()
val c = split[2].toByte()
val d = split[3].toByte()
val ip = byteArrayOf(a, b, c, d)
var counter = numberOfHosts
while (counter >= 0) {
counter--
ip[3]++
if (ip[3] > 255) {
ip[3] = 1
ip[2]++
}
if (ip[2] > 255) {
ip[2] = 0
ip[1]++
}
if (ip[1] > 255) {
Common.logger.error("Exhausted all ip searches. FATAL ERROR.")
return false
}
try {
val address = InetAddress.getByAddress(ip)
val reachable = address.isReachable(100)
if (reachable) {
Common.logger.error("IP address {} is already reachable on the network. Unable to continue.", address.hostAddress)
return false
}
} catch (e: IOException) {
Common.logger.error("Error pinging the IP address", e)
return false
}
}
return true
}
/**
* @param cidr the CIDR notation, ie: 24, 16, etc. That we want to convert into a netmask, as a string
*
* @return the netmask or if there were errors, the default /0 netmask
*/
fun getCidrAsNetmask(cidr: Int): String {
return when (cidr) {
32 -> "255.255.255.255"
31 -> "255.255.255.254"
30 -> "255.255.255.252"
29 -> "255.255.255.248"
28 -> "255.255.255.240"
27 -> "255.255.255.224"
26 -> "255.255.255.192"
25 -> "255.255.255.128"
24 -> "255.255.255.0"
23 -> "255.255.254.0"
22 -> "255.255.252.0"
21 -> "255.255.248.0"
20 -> "255.255.240.0"
19 -> "255.255.224.0"
18 -> "255.255.192.0"
17 -> "255.255.128.0"
16 -> "255.255.0.0"
15 -> "255.254.0.0"
14 -> "255.252.0.0"
13 -> "255.248.0.0"
12 -> "255.240.0.0"
11 -> "255.224.0.0"
10 -> "255.192.0.0"
9 -> "255.128.0.0"
8 -> "255.0.0.0"
7 -> "254.0.0.0"
6 -> "252.0.0.0"
5 -> "248.0.0.0"
4 -> "240.0.0.0"
3 -> "224.0.0.0"
2 -> "192.0.0.0"
1 -> "128.0.0.0"
else -> "0.0.0.0"
}
}
/**
* @param cidr the CIDR notation, ie: 24, 16, etc. That we want to convert into a netmask, as a SIGNED INTEGER (the bits are still
* correct, but to see this "as unix would", you must convert to an unsigned integer.
*
* @return the netmask (as a signed int), or if there were errors, the default /0 netmask
*/
fun getCidrAsIntNetmask(cidr: Int): Int {
return when (cidr) {
32 -> -1
31 -> -2
30 -> -4
29 -> -8
28 -> -16
27 -> -32
26 -> -64
25 -> -128
24 -> -256
23 -> -512
22 -> -1024
21 -> -2048
20 -> -4096
19 -> -8192
18 -> -16384
17 -> -32768
16 -> -65536
15 -> -131072
14 -> -262144
13 -> -524288
12 -> -1048576
11 -> -2097152
10 -> -4194304
9 -> -8388608
8 -> -16777216
7 -> -33554432
6 -> -67108864
5 -> -134217728
4 -> -268435456
3 -> -536870912
2 -> -1073741824
1 -> -2147483648
else -> 0
}
}
fun getCidrFromMask(mask: String): Int {
return when (mask) {
"255.255.255.255" -> 32
"255.255.255.254" -> 31
"255.255.255.252" -> 30
"255.255.255.248" -> 29
"255.255.255.240" -> 28
"255.255.255.224" -> 27
"255.255.255.192" -> 26
"255.255.255.128" -> 25
"255.255.255.0" -> 24
"255.255.254.0" -> 23
"255.255.252.0" -> 22
"255.255.248.0" -> 21
"255.255.240.0" -> 20
"255.255.224.0" -> 19
"255.255.192.0" -> 18
"255.255.128.0" -> 17
"255.255.0.0" -> 16
"255.254.0.0" -> 15
"255.252.0.0" -> 14
"255.248.0.0" -> 13
"255.240.0.0" -> 12
"255.224.0.0" -> 11
"255.192.0.0" -> 10
"255.128.0.0" -> 9
"255.0.0.0" -> 8
"254.0.0.0" -> 7
"252.0.0.0" -> 6
"248.0.0.0" -> 5
"240.0.0.0" -> 4
"224.0.0.0" -> 3
"192.0.0.0" -> 2
"128.0.0.0" -> 1
else -> 0
}
}
private val CIDR2MASK = intArrayOf(
0x00000000,
-0x80000000,
-0x40000000,
-0x20000000,
-0x10000000,
-0x8000000,
-0x4000000,
-0x2000000,
-0x1000000,
-0x800000,
-0x400000,
-0x200000,
-0x100000,
-0x80000,
-0x40000,
-0x20000,
-0x10000,
-0x8000,
-0x4000,
-0x2000,
-0x1000,
-0x800,
-0x400,
-0x200,
-0x100,
-0x80,
-0x40,
-0x20,
-0x10,
-0x8,
-0x4,
-0x2,
-0x1)
fun range2Cidr(startIp: String, endIp: String): List<String> {
var start = toInt(startIp).toLong()
val end = toInt(endIp).toLong()
val pairs: MutableList<String> = ArrayList()
while (end >= start) {
var maxsize = 32.toByte()
while (maxsize > 0) {
val mask = CIDR2MASK[maxsize - 1].toLong()
val maskedBase = start and mask
if (maskedBase != start) {
break
}
maxsize--
}
val x = ln(end - start + 1.toDouble()) / ln(2.0)
val maxDiff = (32 - floor(x)).toByte()
if (maxsize < maxDiff) {
maxsize = maxDiff
}
val ip = toString(start)
pairs.add("$ip/$maxsize")
start += 2.0.pow(32 - maxsize.toDouble()).toLong()
}
return pairs
}
/* Mask to convert unsigned int to a long (i.e. keep 32 bits) */
private const val UNSIGNED_INT_MASK = 0x0FFFFFFFFL
/**
* Check if the IP address is in the range of a specific IP/CIDR
*
* a prefix of 0 will ALWAYS return true
*
* @param address the address to check
* @param networkAddress the network address that will have the other address checked against
* @param networkPrefix 0-32 the network prefix (subnet) to use for the network address
*
* @return true if it is in range
*/
fun isInRange(address: Int, networkAddress: Int, networkPrefix: Int): Boolean {
// System.err.println(" ip: " + IP.toString(address));
// System.err.println(" networkAddress: " + IP.toString(networkAddress));
// System.err.println(" networkSubnetPrefix: " + networkPrefix);
if (networkPrefix == 0) {
// a prefix of 0 means it is always true (even though the broadcast address is '-1'). So we short-cut it here
return true
}
val netmask = ((1 shl 32 - networkPrefix) - 1).inv()
// Calculate base network address
val network = (networkAddress and netmask and UNSIGNED_INT_MASK.toInt()).toLong()
// System.err.println(" network " + IP.toString(network));
// Calculate broadcast address
val broadcast = network or netmask.inv().toLong() and UNSIGNED_INT_MASK
// System.err.println(" broadcast " + IP.toString(broadcast));
val addressLong = (address and UNSIGNED_INT_MASK.toInt()).toLong()
return addressLong in network..broadcast
}
fun toInt(ipBytes: ByteArray): Int {
return ipBytes[0].toInt() shl 24 or
(ipBytes[1].toInt() shl 16) or
(ipBytes[2].toInt() shl 8) or
(ipBytes[3].toInt())
}
/**
* Converts a 32-bit integer into an IPv4 address.
*/
fun toString(ipAddress: Int): String {
val buf = StringBuilder(15)
buf.append(ipAddress shr 24 and 0xFF)
buf.append('.')
buf.append(ipAddress shr 16 and 0xFF)
buf.append('.')
buf.append(ipAddress shr 8 and 0xF)
buf.append('.')
buf.append(ipAddress and 0xFF)
return buf.toString()
}
fun toString(ipBytes: ByteArray): String {
val buf = StringBuilder(15)
buf.append(ipBytes[0].toUByte())
buf.append('.')
buf.append(ipBytes[1].toUByte())
buf.append('.')
buf.append(ipBytes[2].toUByte())
buf.append('.')
buf.append(ipBytes[3].toUByte())
return buf.toString()
}
/**
* Returns the [String] representation of an [InetAddress]. Results are identical to [InetAddress.getHostAddress]
*
* @param ip [InetAddress] to be converted to an address string
*
* @return `String` containing the text-formatted IP address
*/
fun toString(ip: InetAddress): String {
return (ip as Inet4Address).hostAddress
}
@Throws(Exception::class)
fun writeString(ipAddress: Int, writer: Writer) {
writer.write((ipAddress shr 24 and 0x000000FF).toString())
writer.write('.'.toInt())
writer.write((ipAddress shr 16 and 0x000000FF).toString())
writer.write('.'.toInt())
writer.write((ipAddress shr 8 and 0x000000FF).toString())
writer.write('.'.toInt())
writer.write((ipAddress and 0x000000FF).toString())
}
fun toString(ipAddress: Long): String {
val ipString = StringBuilder(15)
ipString.append(ipAddress shr 24 and 0x000000FF)
ipString.append('.')
ipString.append(ipAddress shr 16 and 0x000000FF)
ipString.append('.')
ipString.append(ipAddress shr 8 and 0x000000FF)
ipString.append('.')
ipString.append(ipAddress and 0x000000FF)
return ipString.toString()
}
fun toBytes(bytes: Int): ByteArray {
return byteArrayOf((bytes ushr 24 and 0xFF).toByte(),
(bytes ushr 16 and 0xFF).toByte(),
(bytes ushr 8 and 0xFF).toByte(),
(bytes and 0xFF).toByte())
}
fun toInt(ipAsString: String): Int {
return if (isValid(ipAsString)) {
val bytes = toBytes(ipAsString)
var address = 0
address = address or (bytes[0].toInt() shl 24)
address = address or (bytes[1].toInt() shl 16)
address = address or (bytes[2].toInt() shl 8)
address = address or bytes[3].toInt()
address
} else {
0
}
}
/**
* Returns the [Inet4Address] representation of a [String] IP address.
*
* This method will treat all IPv4 type addresses as "IPv4 mapped" (see [.getByName])
*
* @param ip [String] IP address to be converted to a [Inet4Address]
* @return [Inet4Address] representation of the `ip` or `null` if not a valid IP address.
*/
fun getByName(ip: String): Inet4Address? {
return if (isValid(ip)) {
val asBytes = toBytes(ip)
return Inet4Address.getByAddress(ip, asBytes) as Inet4Address
} else {
null
}
}
}