Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conveniences for testing ip addresses for known characteristics #562

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.Multicast.ordinal"),
ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.Multicast$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.SourceSpecificMulticast.ordinal"),
ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.SourceSpecificMulticast$")
ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.SourceSpecificMulticast$"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isPrivate"), // #562
ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isLoopback"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isLinkLocal")
)

lazy val root = tlCrossRootProject.aggregate(core, testKit)
Expand Down
78 changes: 78 additions & 0 deletions shared/src/main/scala/com/comcast/ip4s/Host.scala
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@ sealed abstract class IpAddress extends IpAddressPlatform with Host with Seriali
def asSourceSpecificMulticastLenient: Option[SourceSpecificMulticast[this.type]] =
SourceSpecificMulticast.fromIpAddressLenient(this)

/** Returns true if this address is a loopback address. */
def isLoopback: Boolean

/** Returns true if this address is a link local address. */
def isLinkLocal: Boolean

/** Returns true if this address is in a private range. */
def isPrivate: Boolean

/** Narrows this address to an Ipv4Address if that is the underlying type. */
def asIpv4: Option[Ipv4Address] = collapseMappedV4.fold(Some(_), _ => None)

Expand Down Expand Up @@ -356,6 +365,17 @@ final class Ipv4Address private (protected val bytes: Array[Byte]) extends IpAdd
override def isSourceSpecificMulticast: Boolean =
this >= Ipv4Address.SourceSpecificMulticastRangeStart && this <= Ipv4Address.SourceSpecificMulticastRangeEnd

override def isLoopback: Boolean =
Ipv4Address.Classes.Loopback.contains(this)

override def isLinkLocal: Boolean =
Ipv4Address.Classes.LinkLocal.contains(this)

override def isPrivate: Boolean =
Ipv4Address.Classes.Private.A.contains(this) ||
Ipv4Address.Classes.Private.B.contains(this) ||
Ipv4Address.Classes.Private.C.contains(this)

/** Converts this V4 address to a compat V6 address, where the first 12 bytes are all zero and the last 4 bytes
* contain the bytes of this V4 address.
*/
Expand Down Expand Up @@ -418,6 +438,44 @@ object Ipv4Address extends Ipv4AddressCompanionPlatform {
val SourceSpecificMulticastRangeEnd: Ipv4Address =
fromBytes(232, 255, 255, 255)

/** IPv4 address classes represented as CIDRs. */
object Classes {

/** Class A: 0.0.0.0 - 127.255.255.255 */
val A: Cidr[Ipv4Address] = Cidr(fromBytes(0, 0, 0, 0), 1)

/** Class B: 128.0.0.0 - 191.255.255.255 */
val B: Cidr[Ipv4Address] = Cidr(fromBytes(128, 0, 0, 0), 2)

/** Class C: 192.0.0.0 - 223.255.255.255 */
val C: Cidr[Ipv4Address] = Cidr(fromBytes(192, 0, 0, 0), 3)

/** Class D: 224.0.0.0 - 239.255.255.255 */
val D: Cidr[Ipv4Address] = Cidr(fromBytes(224, 0, 0, 0), 4)

/** Class E: 240.0.0.0 - 255.255.255.255 */
val E: Cidr[Ipv4Address] = Cidr(fromBytes(240, 0, 0, 0), 5)

/** Private address ranges. */
object Private {

/** Class A: 10.0.0.0 - 10.255.255.255 */
val A: Cidr[Ipv4Address] = Cidr(fromBytes(10, 0, 0, 0), 8)

/** Class B: 172.16.0.0 - 172.31.255.255 */
val B: Cidr[Ipv4Address] = Cidr(fromBytes(172, 16, 0, 0), 12)

/** Class A: 192.168.0.0 - 192.168.255.255 */
val C: Cidr[Ipv4Address] = Cidr(fromBytes(192, 168, 0, 0), 16)
}

/** Loopback: 127.0.0.0 - 127.255.255.255. */
val Loopback: Cidr[Ipv4Address] = Cidr(fromBytes(127, 0, 0, 0), 8)

/** Link local: 169.254.0.0 - 169.254.255.255. */
val LinkLocal: Cidr[Ipv4Address] = Cidr(fromBytes(169, 254, 0, 0), 16)
}

/** Parses an IPv4 address from a dotted-decimal string, returning `None` if the string is not a valid IPv4 address.
*/
def fromString(value: String): Option[Ipv4Address] = {
Expand Down Expand Up @@ -609,6 +667,15 @@ final class Ipv6Address private (protected val bytes: Array[Byte]) extends IpAdd
override def isSourceSpecificMulticast: Boolean =
this >= Ipv6Address.SourceSpecificMulticastRangeStart && this <= Ipv6Address.SourceSpecificMulticastRangeEnd

override def isLoopback: Boolean =
this == Ipv6Address.Loopback || (isMappedV4 && collapseMappedV4.isLoopback)

override def isLinkLocal: Boolean =
Ipv6Address.LinkLocalBlock.contains(this) || (isMappedV4 && collapseMappedV4.isLinkLocal)

override def isPrivate: Boolean =
Ipv6Address.UniqueLocalBlock.contains(this) || (isMappedV4 && collapseMappedV4.isPrivate)

/** Applies the supplied mask to this address.
*
* @example {{{
Expand Down Expand Up @@ -653,6 +720,17 @@ object Ipv6Address extends Ipv6AddressCompanionPlatform {
val MappedV4Block: Cidr[Ipv6Address] =
Cidr(Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0), 96)

/** Alias for ::1. */
val Loopback: Ipv6Address = Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)

/** CIDR which defines the unique local address block. */
val UniqueLocalBlock: Cidr[Ipv6Address] =
Cidr(Ipv6Address.fromBytes(0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 7)

/** CIDR which defines the linked scope unicast address block. */
val LinkLocalBlock: Cidr[Ipv6Address] =
Cidr(Ipv6Address.fromBytes(0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 10)

/** Parses an IPv6 address from a string in RFC4291 notation, returning `None` if the string is not a valid IPv6
* address.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,34 @@ class Ipv4AddressTest extends BaseTestSuite {
assertEquals(Ipv4Address.fromString("0.0.0.0").map(_.previous), Ipv4Address.fromString("255.255.255.255"))
forAll { (ip: Ipv4Address) => assertEquals(ip.previous, Ipv4Address.fromLong(ip.toLong - 1)) }
}

test("isPrivate") {
assert(!ipv4"10.0.0.0".previous.isPrivate)
assert(ipv4"10.0.0.0".isPrivate)
assert(ipv4"10.255.255.255".isPrivate)
assert(!ipv4"10.255.255.255".next.isPrivate)

assert(!ipv4"172.16.0.0".previous.isPrivate)
assert(ipv4"172.16.0.0".isPrivate)
assert(ipv4"172.31.255.255".isPrivate)
assert(!ipv4"172.31.255.255".next.isPrivate)

assert(!ipv4"192.168.0.0".previous.isPrivate)
assert(ipv4"192.168.0.0".isPrivate)
assert(ipv4"192.168.255.255".isPrivate)
assert(!ipv4"192.168.255.255".next.isPrivate)
}

test("isLoopback") {
assert(ipv4"127.0.0.1".isLoopback)
assert(ipv4"127.255.255.255".isLoopback)
assert(!ipv4"128.0.0.0".isLoopback)
}

test("isLinkLocal") {
assert(!ipv4"127.0.0.1".isLinkLocal)
assert(ipv4"169.254.0.0".isLinkLocal)
assert(ipv4"169.254.255.255".isLinkLocal)
assert(!ipv4"169.254.255.255".next.isLinkLocal)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,23 @@ class Ipv6AddressTest extends BaseTestSuite {
assertEquals[Any, Any](addr.asIpv6, Some(addr))
assertEquals[Any, Any](addr.asIpv4, Some(ip"0.15.0.15"))
}

test("isPrivate") {
assert(!ipv6"fc00::".previous.isPrivate)
assert(ipv6"fc00::".isPrivate)
assert(ipv6"fe00::".previous.isPrivate)
assert(!ipv6"fe00::".isPrivate)
// mapped v4
assert(ipv6"::ffff:10.1.1.1".isPrivate)
}

test("isLoopback") {
assert(ipv6"::1".isLoopback)
assert(ipv6"::ffff:127.0.0.1".isLoopback)
}

test("isLinkLocal") {
assert(ipv6"fe80::1".isLinkLocal)
assert(ipv6"::ffff:169.254.0.0".isLinkLocal)
}
}