1 -- | The CIDR modules contains most of the functions used for working
21 import Data.List (nubBy)
22 import Data.List.Split (splitOneOf)
23 import Data.Maybe (catMaybes, mapMaybe)
25 import Test.Tasty ( TestTree, testGroup )
26 import Test.Tasty.HUnit ( (@?=), testCase )
27 import Test.Tasty.QuickCheck (
28 Arbitrary( arbitrary ),
33 import Text.Read (readMaybe)
35 import qualified Bit as B (Bit(..))
37 IPv4Address( IPv4Address, octet1, octet2, octet3, octet4 ),
38 most_sig_bit_different )
39 import Maskable (Maskable(apply_mask))
40 import Maskbits ( Maskbits(Zero) )
41 import Octet (Octet())
44 data Cidr = Cidr { ipv4address :: IPv4Address,
45 maskbits :: Maskbits }
48 instance Show Cidr where
49 show cidr = (show (ipv4address cidr)) ++ "/" ++ (show (maskbits cidr))
52 instance Arbitrary Cidr where
54 ipv4 <- arbitrary :: Gen IPv4Address
55 mask <- arbitrary :: Gen Maskbits
56 return (Cidr ipv4 mask)
59 instance Eq Cidr where
60 cidr1 == cidr2 = (cidr1 `equivalent` cidr2)
63 -- | Two CIDR ranges are equivalent if they have the same network bits
64 -- and the masks are the same.
65 equivalent :: Cidr -> Cidr -> Bool
66 equivalent (Cidr addr1 mbits1) (Cidr addr2 mbits2) =
67 (mbits1 == mbits2) && ((apply_mask addr1 mbits1 B.Zero) == (apply_mask addr2 mbits2 B.Zero))
69 -- | Returns the mask portion of a CIDR address. That is, everything
70 -- after the trailing slash.
71 maskbits_from_cidr_string :: String -> Maybe Maskbits
72 maskbits_from_cidr_string s
73 | length partlist == 2 = readMaybe (partlist !! 1)
76 partlist = splitOneOf "/" s
79 -- | Takes an IP address String in CIDR notation, and returns a list
80 -- of its octets (as Ints).
81 octets_from_cidr_string :: String -> [Octet]
82 octets_from_cidr_string s =
84 (p1:p2:p3:p4:_) -> mapMaybe readMaybe [p1,p2,p3,p4]
87 parts = splitOneOf "./" s
89 instance Read Cidr where
90 -- | Parse everything or nothing.
92 case (octets_from_cidr_string s) of
93 [oct1, oct2, oct3, oct4] ->
94 case (maskbits_from_cidr_string s) of
96 [(Cidr (IPv4Address oct1 oct2 oct3 oct4) mbits, "")]
101 -- | Given a CIDR, return the minimum valid IPv4 address contained
103 min_host :: Cidr -> IPv4Address
104 min_host (Cidr addr mask) = apply_mask addr mask B.Zero
106 -- | Given a CIDR, return the maximum valid IPv4 address contained
108 max_host :: Cidr -> IPv4Address
109 max_host (Cidr addr mask) = apply_mask addr mask B.One
111 -- | Given a CIDR, return the first octet of the minimum valid IPv4
112 -- address contained within it.
113 min_octet1 :: Cidr -> Octet
114 min_octet1 cidr = octet1 (min_host cidr)
116 -- | Given a CIDR, return the second octet of the minimum valid IPv4
117 -- address contained within it.
118 min_octet2 :: Cidr -> Octet
119 min_octet2 cidr = octet2 (min_host cidr)
121 -- | Given a CIDR, return the third octet of the minimum valid IPv4
122 -- address contained within it.
123 min_octet3 :: Cidr -> Octet
124 min_octet3 cidr = octet3 (min_host cidr)
126 -- | Given a CIDR, return the fourth octet of the minimum valid IPv4
127 -- address contained within it.
128 min_octet4 :: Cidr -> Octet
129 min_octet4 cidr = octet4 (min_host cidr)
131 -- | Given a CIDR, return the first octet of the maximum valid IPv4
132 -- address contained within it.
133 max_octet1 :: Cidr -> Octet
134 max_octet1 cidr = octet1 (max_host cidr)
136 -- | Given a CIDR, return the second octet of the maximum valid IPv4
137 -- address contained within it.
138 max_octet2 :: Cidr -> Octet
139 max_octet2 cidr = octet2 (max_host cidr)
141 -- | Given a CIDR, return the third octet of the maximum valid IPv4
142 -- address contained within it.
143 max_octet3 :: Cidr -> Octet
144 max_octet3 cidr = octet3 (max_host cidr)
146 -- | Given a CIDR, return the fourth octet of the maximum valid IPv4
147 -- address contained within it.
148 max_octet4 :: Cidr -> Octet
149 max_octet4 cidr = octet4 (max_host cidr)
153 -- | Return true if the first argument (a CIDR range) contains the
154 -- second (another CIDR range). There are a lot of ways we can be
155 -- fed junk here. For lack of a better alternative, just return
156 -- False when we are given nonsense.
158 -- If the number of bits in the network part of the first address is
159 -- larger than the number of bits in the second, there is no way
160 -- that the first range can contain the second. For, if the number
161 -- of network bits is larger, then the number of host bits must be
162 -- smaller, and if cidr1 has fewer hosts than cidr2, cidr1 most
163 -- certainly does not contain cidr2.
165 -- On the other hand, if the first argument (cidr1) has fewer (or
166 -- the same number of) network bits as the second, it can contain
167 -- the second. In this case, we need to check that every host in
168 -- cidr2 is contained in cidr1. If a host in cidr2 is contained in
169 -- cidr1, then at least mbits1 of an address in cidr2 will match
170 -- cidr1. For example,
172 -- cidr1 = 192.168.1.0\/23, cidr2 = 192.168.1.100\/24
174 -- Here, cidr2 contains all of 192.168.1.0 through
175 -- 192.168.1.255. However, cidr1 contains BOTH 192.168.0.0 through
176 -- 192.168.0.255 and 192.168.1.0 through 192.168.1.255. In essence,
177 -- what we want to check is that cidr2 "begins with" something that
178 -- cidr1 CAN begin with. Since cidr1 can begin with 192.168.1, and
179 -- cidr2 DOES, cidr1 contains cidr2..
181 -- The way that we check this is to apply cidr1's mask to cidr2's
182 -- address and see if the result is the same as cidr1's mask applied
183 -- to cidr1's address.
185 contains :: Cidr -> Cidr -> Bool
186 contains (Cidr addr1 mbits1) (Cidr addr2 mbits2)
187 | mbits1 > mbits2 = False
188 | otherwise = addr1masked == addr2masked
190 addr1masked = apply_mask addr1 mbits1 B.Zero
191 addr2masked = apply_mask addr2 mbits1 B.Zero
194 -- | Contains but is not equal to.
195 contains_proper :: Cidr -> Cidr -> Bool
196 contains_proper cidr1 cidr2 =
197 (cidr1 `contains` cidr2) && (not (cidr2 `contains` cidr1))
200 -- | A CIDR range is redundant (with respect to the given list) if
201 -- another CIDR range in that list properly contains it.
202 redundant :: [Cidr] -> Cidr -> Bool
203 redundant cidrlist cidr = any ((flip contains_proper) cidr) cidrlist
206 -- | First, we look at all possible pairs of cidrs, and combine the
207 -- adjacent ones in to a new list. Then, we concatenate that list
208 -- with the original one, and filter out all of the redundancies. If
209 -- two adjacent Cidrs are combined into a larger one, they will be
210 -- removed in the second step since the larger Cidr must contain the
213 -- Once this is done, we see whether or not the result is different
214 -- than the argument that was passed in. If nothing changed, we're
215 -- done and return the list that was passed to us. However, if
216 -- something changed, we recurse and try to combine the list again.
217 combine_all :: [Cidr] -> [Cidr]
219 | cidrs == (combine_contained unique_cidrs) = cidrs
220 | otherwise = combine_all (combine_contained unique_cidrs)
222 unique_cidrs = nubBy equivalent cidr_combinations
224 cidrs ++ (catMaybes [ (combine_adjacent x y) | x <- cidrs, y <- cidrs ])
227 -- | Take a list of CIDR ranges and filter out all of the ones that
228 -- are contained entirelt within some other range in the list.
229 combine_contained :: [Cidr] -> [Cidr]
230 combine_contained cidrs =
231 filter (not . (redundant cidrs)) cidrs
234 -- | If the two Cidrs are not adjacent, return Cidr.None. Otherwise,
235 -- decrement the maskbits of cidr1 and return that; it will contain
236 -- both cidr1 and cidr2.
237 combine_adjacent :: Cidr -> Cidr -> Maybe Cidr
238 combine_adjacent cidr1 cidr2
239 | not (adjacent cidr1 cidr2) = Nothing
240 | (maskbits cidr1 == Zero) = Nothing
241 | otherwise = Just $ cidr1 { maskbits = pred (maskbits cidr1) }
245 -- | Determine whether or not two CIDR ranges are adjacent. If two
246 -- ranges lie consecutively within the IP space, they can be
247 -- combined. For example, 10.1.0.0/24 and 10.0.1.0/24 are adjacent,
248 -- and can be combined in to 10.1.0.0/23.
249 adjacent :: Cidr -> Cidr -> Bool
251 | mbits1 /= mbits2 = False
252 | mbits1 == Maskbits.Zero = False -- They're equal.
253 | otherwise = (mbits1 == (most_sig_bit_different addr1 addr2))
255 addr1 = ipv4address cidr1
256 addr2 = ipv4address cidr2
257 mbits1 = maskbits cidr1
258 mbits2 = maskbits cidr2
261 enumerate :: Cidr -> [IPv4Address]
262 enumerate cidr = [(min_host cidr)..(max_host cidr)]
265 cidr_tests :: TestTree
267 testGroup "CIDR Tests" [
274 test_contains_proper1,
275 test_contains_proper2,
280 test_combine_contained1,
281 test_combine_contained2,
286 cidr_properties :: TestTree
288 testGroup "CIDR Properties" [
289 prop_all_cidrs_contain_themselves,
290 prop_contains_proper_antisymmetric ]
294 test_enumerate :: TestTree
296 testCase desc $ actual @?= expected
298 desc = "192.168.0.240/30 is enumerated correctly"
299 oct1 = toEnum 192 :: Octet
300 oct2 = toEnum 168 :: Octet
301 oct3 = minBound :: Octet
302 mk_ip = IPv4Address oct1 oct2 oct3
303 addr1 = mk_ip $ toEnum 240
304 addr2 = mk_ip $ toEnum 241
305 addr3 = mk_ip $ toEnum 242
306 addr4 = mk_ip $ toEnum 243
307 expected = [addr1, addr2, addr3, addr4]
308 actual = enumerate (read "192.168.0.240/30" :: Cidr)
310 test_min_host1 :: TestTree
312 testCase desc $ actual @?= expected
314 desc = "The minimum host in 10.0.0.0/24 is 10.0.0.0"
315 actual = show $ min_host (read "10.0.0.0/24" :: Cidr)
316 expected = "10.0.0.0"
319 test_max_host1 :: TestTree
321 testCase desc $ actual @?= expected
323 desc = "The maximum host in 10.0.0.0/24 is 10.0.0.255"
324 actual = show $ max_host (read "10.0.0.0/24" :: Cidr)
325 expected = "10.0.0.255"
328 test_equality1 :: TestTree
330 testCase desc $ actual @?= expected
332 desc = "10.1.1.0/23 equals itself"
333 actual = read "10.1.1.0/23" :: Cidr
334 expected = read "10.1.1.0/23" :: Cidr
337 test_contains1 :: TestTree
339 testCase desc $ actual @?= expected
341 desc = "10.1.1.0/23 contains 10.1.1.0/24"
342 cidr1 = read "10.1.1.0/23" :: Cidr
343 cidr2 = read "10.1.1.0/24" :: Cidr
345 actual = cidr1 `contains` cidr2
348 test_contains2 :: TestTree
350 testCase desc $ actual @?= expected
352 desc = "10.1.1.0/23 contains itself"
353 cidr1 = read "10.1.1.0/23" :: Cidr
355 actual = cidr1 `contains` cidr1
358 test_contains_proper1 :: TestTree
359 test_contains_proper1 =
360 testCase desc $ actual @?= expected
362 desc = "10.1.1.0/23 contains 10.1.1.0/24 properly"
363 cidr1 = read "10.1.1.0/23" :: Cidr
364 cidr2 = read "10.1.1.0/24" :: Cidr
366 actual = cidr1 `contains_proper` cidr2
369 test_contains_proper2 :: TestTree
370 test_contains_proper2 =
371 testCase desc $ actual @?= expected
373 desc = "10.1.1.0/23 does not contain itself properly"
374 cidr1 = read "10.1.1.0/23" :: Cidr
376 actual = cidr1 `contains_proper` cidr1
379 test_adjacent1 :: TestTree
381 testCase desc $ actual @?= expected
383 desc = "10.1.0.0/24 is adjacent to 10.1.1.0/24"
384 cidr1 = read "10.1.0.0/24" :: Cidr
385 cidr2 = read "10.1.1.0/24" :: Cidr
387 actual = cidr1 `adjacent` cidr2
390 test_adjacent2 :: TestTree
392 testCase desc $ actual @?= expected
394 desc = "10.1.0.0/23 is not adjacent to 10.1.0.0/24"
395 cidr1 = read "10.1.0.0/23" :: Cidr
396 cidr2 = read "10.1.0.0/24" :: Cidr
398 actual = cidr1 `adjacent` cidr2
401 test_adjacent3 :: TestTree
403 testCase desc $ actual @?= expected
405 desc = "10.1.0.0/24 is not adjacent to 10.2.5.0/24"
406 cidr1 = read "10.1.0.0/24" :: Cidr
407 cidr2 = read "10.2.5.0/24" :: Cidr
409 actual = cidr1 `adjacent` cidr2
412 test_adjacent4 :: TestTree
414 testCase desc $ actual @?= expected
416 desc = "10.1.1.0/24 is not adjacent to 10.1.2.0/24"
417 cidr1 = read "10.1.1.0/24" :: Cidr
418 cidr2 = read "10.1.2.0/24" :: Cidr
420 actual = cidr1 `adjacent` cidr2
422 test_combine_contained1 :: TestTree
423 test_combine_contained1 =
424 testCase desc $ actual @?= expected
426 desc = "10.0.0.0/8, 10.1.0.0/16, and 10.1.1.0/24 combine to 10.0.0.0/8"
427 cidr1 = read "10.0.0.0/8" :: Cidr
428 cidr2 = read "10.1.0.0/16" :: Cidr
429 cidr3 = read "10.1.1.0/24" :: Cidr
430 test_cidrs = [cidr1, cidr2, cidr3]
432 actual = combine_contained test_cidrs
434 test_combine_contained2 :: TestTree
435 test_combine_contained2 =
436 testCase desc $ actual @?= expected
438 desc = "192.168.3.0/23 does not contain 192.168.1.0/24"
439 cidr1 = read "192.168.3.0/23" :: Cidr
440 cidr2 = read "192.168.1.0/24" :: Cidr
441 expected = [cidr1, cidr2]
442 actual = combine_contained [cidr1, cidr2]
445 test_combine_all1 :: TestTree
447 testCase desc $ actual @?= expected
449 desc = "10.0.0.0/24 is adjacent to 10.0.1.0/24 "
450 ++ "and 10.0.3.0/23 contains 10.0.2.0/24"
451 cidr1 = read "10.0.0.0/24" :: Cidr
452 cidr2 = read "10.0.1.0/24" :: Cidr
453 cidr3 = read "10.0.2.0/24" :: Cidr
454 cidr4 = read "10.0.3.0/23" :: Cidr
455 cidr5 = read "10.0.0.0/23" :: Cidr
456 test_cidrs = [cidr1, cidr2, cidr3, cidr4, cidr5]
457 expected = [read "10.0.0.0/22" :: Cidr]
458 actual = combine_all test_cidrs
461 test_combine_all2 :: TestTree
463 testCase desc $ actual @?= expected
465 desc = "127.0.0.1/32 combines with itself recursively"
466 cidr1 = read "127.0.0.1/32" :: Cidr
467 test_cidrs = [cidr1, cidr1, cidr1, cidr1, cidr1]
469 actual = combine_all test_cidrs
472 test_combine_all3 :: TestTree
474 testCase desc $ actual @?= expected
476 desc = "10.0.0.16, 10.0.0.17, 10.0.0.18, and "
477 ++ "10.0.0.19 get combined into 10.0.0.16/30"
478 cidr1 = read "10.0.0.16/32" :: Cidr
479 cidr2 = read "10.0.0.17/32" :: Cidr
480 cidr3 = read "10.0.0.18/32" :: Cidr
481 cidr4 = read "10.0.0.19/32" :: Cidr
482 test_cidrs = [cidr1, cidr2, cidr3, cidr4]
483 expected = [read "10.0.0.16/30" :: Cidr]
484 actual = combine_all test_cidrs
487 prop_all_cidrs_contain_themselves :: TestTree
488 prop_all_cidrs_contain_themselves =
489 testProperty "All CIDRs contain themselves" prop
492 prop cidr1 = cidr1 `contains` cidr1
495 -- If cidr1 properly contains cidr2, then by definition cidr2
496 -- does not properly contain cidr1.
497 prop_contains_proper_antisymmetric :: TestTree
498 prop_contains_proper_antisymmetric =
499 testProperty "CIDR proper containment is an antisymmetric relation" prop
501 prop :: Cidr -> Cidr -> Property
503 (cidr1 `contains_proper` cidr2) ==>
504 (not (cidr2 `contains_proper` cidr1))