|
9 | 9 | left*, right*: IpTreeNode |
10 | 10 |
|
11 | 11 | IpTree* = ref object |
12 | | - root*: IpTreeNode |
| 12 | + root*: IpTreeNode ## IPv4 tree root: a 32-level binary tree. |
| 13 | + root6*: IpTreeNode ## IPv6 tree root: a 128-level binary tree. |
13 | 14 |
|
14 | 15 | proc new*(T: typedesc[IpTree]): T = |
15 | | - T(root: IpTreeNode(counter: 0)) |
| 16 | + T(root: IpTreeNode(counter: 0), root6: IpTreeNode(counter: 0)) |
| 17 | + |
| 18 | +proc ipBytes(ip: IpAddress): seq[uint8] {.raises: [].} = |
| 19 | + case ip.family |
| 20 | + of IpAddressFamily.IPv4: |
| 21 | + @(ip.address_v4) |
| 22 | + of IpAddressFamily.IPv6: |
| 23 | + @(ip.address_v6) |
| 24 | + |
| 25 | +proc treeRoot(ipTree: IpTree, ip: IpAddress): IpTreeNode = |
| 26 | + case ip.family |
| 27 | + of IpAddressFamily.IPv4: ipTree.root |
| 28 | + of IpAddressFamily.IPv6: ipTree.root6 |
| 29 | + |
| 30 | +proc step(v: IpTreeNode, goRight: bool, create: bool): IpTreeNode = |
| 31 | + ## Returns the child of `v` in the direction given by `goRight`. With |
| 32 | + ## `create`, a missing child is allocated and linked in, so the result is |
| 33 | + ## never nil; otherwise a missing child yields nil. |
| 34 | + var nxt = if goRight: v.right else: v.left |
| 35 | + if nxt.isNil and create: |
| 36 | + nxt = IpTreeNode(counter: 0) |
| 37 | + if goRight: |
| 38 | + v.right = nxt |
| 39 | + else: |
| 40 | + v.left = nxt |
| 41 | + nxt |
16 | 42 |
|
17 | 43 | proc insertIp*(ipTree: IpTree, ip: IpAddress) {.raises: [].} = |
18 | | - doAssert ip.family == IpAddressFamily.IPv4 |
19 | | - |
20 | | - var v = ipTree.root |
| 44 | + ## Inserts an IP address into the IP tree, following its binary |
| 45 | + ## representation and incrementing counters at each visited node. |
| 46 | + ## IPv4 addresses walk the 32-level tree, |
| 47 | + ## IPv6 addresses walk the 128-level tree. |
| 48 | + var v = ipTree.treeRoot(ip) |
21 | 49 | v.counter += 1 |
22 | 50 |
|
23 | | - let bytes = ip.address_v4 |
| 51 | + let bytes = ip.ipBytes() |
24 | 52 |
|
25 | | - for i in 0 ..< 4: |
26 | | - let b = bytes[i] |
| 53 | + for b in bytes: |
27 | 54 | for bit in countdown(7, 0): |
28 | 55 | let goRight = (b and (1'u8 shl bit)) != 0 |
29 | | - |
30 | | - if goRight: |
31 | | - if v.right.isNil: |
32 | | - v.right = IpTreeNode(counter: 0) |
33 | | - v = v.right |
34 | | - else: |
35 | | - if v.left.isNil: |
36 | | - v.left = IpTreeNode(counter: 0) |
37 | | - v = v.left |
38 | | - |
| 56 | + v = step(v, goRight, create = true) |
39 | 57 | v.counter += 1 |
40 | 58 |
|
41 | 59 | proc removeIp*(ipTree: IpTree, ip: IpAddress) {.raises: [].} = |
42 | | - ## Removes an IPv4 address from the IP tree by decrementing counters along |
43 | | - ## the 32-bit path. Counters never go below zero. Only IPv4 is supported. |
44 | | - doAssert ip.family == IpAddressFamily.IPv4 |
45 | | - |
46 | | - if ipTree.root.counter == 0: |
| 60 | + ## Removes an IP address from the IP tree by decrementing counters along |
| 61 | + ## its binary-representation path, from the root through the leaf. |
| 62 | + ## Counters never go below zero. Any trailing run of nodes left at counter |
| 63 | + ## 0 with no children is unlinked, so a fully-removed address doesn't leave |
| 64 | + ## dead nodes behind. |
| 65 | + let root = ipTree.treeRoot(ip) |
| 66 | + if root.counter == 0: |
47 | 67 | return |
48 | 68 |
|
49 | | - var v = ipTree.root |
50 | | - let bytes = ip.address_v4 |
| 69 | + let bytes = ip.ipBytes() |
51 | 70 |
|
52 | | - var path: array[32, IpTreeNode] |
53 | | - var pathLen = 0 |
| 71 | + # path[0] is the root; path[i] for i > 0 is the node reached after the |
| 72 | + # i-th bit. Sized for the deepest case (IPv6: root + 128 levels). |
| 73 | + var path: array[129, IpTreeNode] |
| 74 | + path[0] = root |
| 75 | + var pathLen = 1 |
54 | 76 |
|
55 | | - for i in 0 ..< 4: |
56 | | - let b = bytes[i] |
| 77 | + var v = root |
| 78 | + for b in bytes: |
57 | 79 | for bit in countdown(7, 0): |
| 80 | + let goRight = (b and (1'u8 shl bit)) != 0 |
| 81 | + v = step(v, goRight, create = false) |
58 | 82 | if v.isNil or v.counter == 0: |
59 | 83 | return |
60 | 84 |
|
61 | 85 | path[pathLen] = v |
62 | 86 | inc pathLen |
63 | 87 |
|
64 | | - let goLeft = (b and (1'u8 shl bit)) == 0 |
65 | | - let nxt = if goLeft: v.left else: v.right |
66 | | - if nxt.isNil: |
67 | | - return |
68 | | - v = nxt |
69 | | - |
| 88 | + # Every node visited above has counter > 0 (checked as it was added to |
| 89 | + # path), so this can't underflow. |
70 | 90 | for j in 0 ..< pathLen: |
71 | | - let n = path[j] |
72 | | - if n.counter > 0: |
73 | | - dec n.counter |
| 91 | + dec path[j].counter |
| 92 | + |
| 93 | + # Prune the trailing run of now-empty leaves: walk from the leaf back |
| 94 | + # toward the root, unlinking any node left with counter == 0 and no |
| 95 | + # children, stopping at the first node that still has either. |
| 96 | + for i in countdown(pathLen - 1, 1): |
| 97 | + let n = path[i] |
| 98 | + if n.counter > 0 or not n.left.isNil or not n.right.isNil: |
| 99 | + break |
| 100 | + let parent = path[i - 1] |
| 101 | + if parent.left == n: |
| 102 | + parent.left = nil |
| 103 | + else: |
| 104 | + parent.right = nil |
74 | 105 |
|
75 | 106 | proc ipScore*(ipTree: IpTree, ip: IpAddress): float64 {.raises: [].} = |
76 | | - ## Returns an IP similarity score in [0.0, 1.0] for the given IPv4 address. |
77 | | - ## Asserts that `ip` is an IPv4 address. |
| 107 | + ## Returns an IP similarity score in [0.0, 1.0] for the given IP address. |
| 108 | + ## Supports both IPv4 (32-level tree) and IPv6 (128-level tree). |
78 | 109 | ## |
79 | | - ## The score counts how many of the 32 prefix nodes along the IP's path have |
| 110 | + ## The score counts how many of the prefix nodes along the IP's path have |
80 | 111 | ## a counter exceeding the expected threshold (root.counter / 2^(depth+1)), |
81 | 112 | ## where depth+1 is the tree level of the child node being evaluated. |
82 | 113 | ## A high score means many existing IPs share the same subnet — a signal of |
83 | 114 | ## Sybil-style clustering. |
84 | | - doAssert ip.family == IpAddressFamily.IPv4 |
85 | | - |
86 | | - if ipTree.root.counter == 0: |
| 115 | + let root = ipTree.treeRoot(ip) |
| 116 | + if root.counter == 0: |
87 | 117 | return 0.0 |
88 | 118 |
|
89 | | - var v = ipTree.root |
| 119 | + var v = root |
90 | 120 | var score = 0 |
91 | | - let total = float64(ipTree.root.counter) |
92 | | - let bytes = ip.address_v4 |
| 121 | + let total = float64(root.counter) |
| 122 | + let bytes = ip.ipBytes() |
| 123 | + let nBits = bytes.len * 8 |
93 | 124 |
|
94 | | - for i in 0 ..< 4: |
95 | | - let b = bytes[i] |
| 125 | + var threshold = total * 0.5 |
| 126 | + for b in bytes: |
96 | 127 | for bit in countdown(7, 0): |
97 | | - let depth = i * 8 + (7 - bit) # 0 .. 31; child node sits at tree level depth+1 |
98 | | - let threshold = total / float64(1'u64 shl (depth + 1)) |
99 | | - |
100 | | - v = if (b and (1'u8 shl bit)) == 0: v.left else: v.right |
| 128 | + let goRight = (b and (1'u8 shl bit)) != 0 |
| 129 | + v = step(v, goRight, create = false) |
101 | 130 |
|
102 | 131 | if v.isNil: |
103 | | - return (float64(score) / 32.0) |
| 132 | + return (float64(score) / float64(nBits)) |
104 | 133 |
|
105 | 134 | if float64(v.counter) > threshold: |
106 | 135 | score += 1 |
107 | 136 |
|
108 | | - (float64(score) / 32.0) |
| 137 | + threshold *= 0.5 |
| 138 | + |
| 139 | + (float64(score) / float64(nBits)) |
0 commit comments