├── tests ├── run_tests.sh └── test1.lua ├── image.png ├── image-1.png ├── examples ├── access.lua ├── example.lua └── init.lua ├── Readme.md └── lib └── IPInSubnet.lua /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | lua5.2 test1.lua -v -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnchdan/IpInSubnet/main/image.png -------------------------------------------------------------------------------- /image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnchdan/IpInSubnet/main/image-1.png -------------------------------------------------------------------------------- /examples/access.lua: -------------------------------------------------------------------------------- 1 | 2 | ngx.header.content_type = "text/plain" 3 | 4 | ngx.say("ip : ", ngx.var.remote_addr) 5 | ngx.say("is allowed : ",allowedIPs:isInSubnets(ngx.var.remote_addr)) 6 | ngx.say("is blocked : ",blockedIPs:isInSubnets(ngx.var.remote_addr)) 7 | 8 | -------------------------------------------------------------------------------- /examples/example.lua: -------------------------------------------------------------------------------- 1 | package.path = "../lib/?.lua;"..package.path 2 | IPInSubnet = require("IPInSubnet") 3 | mIPInSubnet = IPInSubnet:new() 4 | 5 | 6 | mIPInSubnet:addSubnet("192.168.1.0/24") 7 | 8 | print("192.168.1.1 : ",mIPInSubnet:isInSubnets("192.168.1.1")) -------------------------------------------------------------------------------- /examples/init.lua: -------------------------------------------------------------------------------- 1 | --load module 2 | info = debug.getinfo(1, "S") 3 | path = info.source:sub(2):match("(.*/)") 4 | package.path = path.."../lib/?.lua;" ..package.path 5 | -- or append to package.path where the lib is saved 6 | -- package.path = package.path .. ";/usr/local/share/lua/IPInSubnet/lib/?.lua" 7 | 8 | 9 | 10 | IPInSubnet = require("IPInSubnet") 11 | blockedIPs = IPInSubnet:new() 12 | allowedIPs = IPInSubnet:new() 13 | 14 | 15 | allowedIPs:addSubnet("192.168.1.0/24") 16 | 17 | 18 | blockedIPs:addSubnet("192.168.2.0/24") 19 | blockedIPs:addSubnet("11:0db8::/32") 20 | 21 | 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # IPInSubnet - Fast Binary Tree IP Subnet Matching 2 | 3 | ## Overview 4 | **IPInSubnet** is a Lua library for efficient IPv4 and IPv6 subnet searches using a binary tree. It is designed for scenarios involving a large number of subnets, enabling quick lookups to determine whether an IP address belongs to a specific subnet. 5 | 6 | 7 | ### Features 8 | - Supports both **IPv4** and **IPv6** addresses. 9 | - Efficient binary tree implementation for quick lookups. 10 | - Handles **CIDR notation** for subnet masks. 11 | - Ideal for **firewalls, access control, and Nginx/OpenResty** use cases. 12 | 13 | ### Requirements 14 | lua-bitop - for bitwise operations, for Nginx, Openrestry is preinstalled 15 | apt install lua-bitop 16 | 17 | luaunit - for unit testing 18 | apt install lua-unit 19 | 20 | --- 21 | ## Usage Example 22 | Below is a complete example in Nginx 23 | 24 | ### nginx.conf 25 | ```nginx 26 | ... 27 | init_by_lua_file "/{YOUR_PATH}/init.lua"; 28 | ... 29 | server { 30 | ... 31 | access_by_lua_file "/{YOUR_PATH}/access.lua"; 32 | ... 33 | } 34 | ... 35 | ``` 36 | 37 | ### init.lua 38 | ```lua 39 | -- Load module dynamically 40 | local info = debug.getinfo(1, "S") 41 | local path = info.source:sub(2):match("(.*/)") 42 | package.path = path.."../lib/?.lua;" .. package.path 43 | -- or append to package.path where the lib is saved 44 | -- package.path = package.path .. ";/usr/local/share/lua/IPInSubnet/lib/?.lua" 45 | 46 | -- Require the module 47 | IPInSubnet = require("IPInSubnet") 48 | 49 | -- Create subnet lists 50 | blockedIPs = IPInSubnet:new() 51 | allowedIPs = IPInSubnet:new() 52 | 53 | -- Define allowed and blocked subnets 54 | allowedIPs:addSubnet("192.168.1.0/24") 55 | allowedIPs:addSubnet("192.168.4.0/24") 56 | allowedIPs:addSubnet("192.1.4.0/24") 57 | allowedIPs:addSubnet("192.1.1.0/24") 58 | 59 | 60 | blockedIPs:addSubnet("192.168.2.0/24") 61 | blockedIPs:addSubnet("11:0db8::/32") 62 | ``` 63 | 64 | ### access.lua 65 | ```lua 66 | ngx.header.content_type = "text/plain" 67 | 68 | ngx.say("IP: ", ngx.var.remote_addr) 69 | ngx.say("Is allowed: ", allowedIPs:isInSubnets(ngx.var.remote_addr)) 70 | ngx.say("Is blocked: ", blockedIPs:isInSubnets(ngx.var.remote_addr)) 71 | ``` 72 | 73 | ### response 74 | ![alt text](image-1.png) 75 | 76 | #### Show IPv4 tree for allowedIP 77 | ```lua 78 | --if you add the subnets 79 | allowedIPs:addSubnet("192.1.1.0/24") 80 | allowedIPs:addSubnet("192.1.4.0/24") 81 | allowedIPs:addSubnet("192.168.1.0/24") 82 | allowedIPs:addSubnet("192.168.4.0/24") 83 | 84 | --the tree will have the values from the image. 85 | -- where *8bits delimiter is a separator for visual efects 86 | ngx.say("Allowed IPs Tree IPv4") 87 | allowedIPs.treeIPv4.printTree(allowedIPs.treeIPv4.root) 88 | 89 | ``` 90 | ``` 91 | 192.1.1.0/24 => 11000000 00000001 00000001 /24 92 | 192.1.4.0/24 => 11000000 00000001 00000100 /24 93 | 192.168.1.0/24 => 11000000 10101000 00000001 /24 94 | 192.168.4.0/24 => 11000000 10101000 00000100 /24 95 | 96 | 97 | ``` 98 | ![alt text](image.png) 99 | 100 | -------------------------------------------------------------------------------- /tests/test1.lua: -------------------------------------------------------------------------------- 1 | -- Unit testing starts - for Lu5.2 2 | --[[ 3 | library required 4 | lua-bitop - for bitwise operations 5 | apt install lua-bitop, or from lua-jit on Nginx 6 | 7 | luaunit - for unit testing 8 | apt install lua-unit 9 | 10 | --]] 11 | 12 | 13 | package.path = "/usr/share/lua/5.1/?.lua;"..package.path 14 | 15 | 16 | local lu = require('luaunit') 17 | 18 | --define ngx.say 19 | ngx = {} 20 | function ngx.say(msg1, msg2) 21 | local msg="" 22 | if msg2 then 23 | msg = msg1..msg2 24 | else 25 | msg = msg1 26 | end 27 | print(msg) 28 | end 29 | 30 | --import the module 31 | function getIPInSubnet() 32 | -- os.execute("sleep 0.2") 33 | 34 | package.path = "../lib/?.lua;"..package.path 35 | IPInSubnet = require("IPInSubnet") 36 | mIPInSubnet = IPInSubnet:new() 37 | return mIPInSubnet 38 | end 39 | 40 | --test cases 41 | function testSubnetFormat() 42 | mIPInSubnet = getIPInSubnet() 43 | lu.assertEquals( mIPInSubnet:addSubnet("127.0.0.0/8"), true) 44 | lu.assertEquals( mIPInSubnet:addSubnet("127.0.0.0/64"), false) 45 | lu.assertEquals( mIPInSubnet:addSubnet("127.0.0.0/a"), false) 46 | lu.assertEquals( mIPInSubnet:addSubnet("300.330.0.0/8"), false) 47 | lu.assertEquals( mIPInSubnet:addSubnet("192.168.0.0"), false) 48 | lu.assertEquals( mIPInSubnet:addSubnet("127.0.0/8"), false) 49 | 50 | lu.assertEquals( mIPInSubnet:addSubnet("3001:0db2::/24"), true) 51 | lu.assertEquals( mIPInSubnet:addSubnet("3001:0db2::"), false) 52 | lu.assertEquals( mIPInSubnet:addSubnet("gggg:0db2::/24"), false) 53 | 54 | lu.assertEquals( mIPInSubnet:addSubnet("ip"), false) 55 | end 56 | 57 | 58 | function testIsInSubnet() 59 | mIPInSubnet = getIPInSubnet() 60 | 61 | lu.assertEquals( mIPInSubnet:isInSubnets("192.168.2.0"), false) 62 | lu.assertEquals( mIPInSubnet:isInSubnets("2001:0db8::1"), false) 63 | 64 | lu.assertEquals( mIPInSubnet:addSubnet("192.168.1.0/24"), true) 65 | lu.assertEquals( mIPInSubnet:addSubnet("2001:0db3::/64"), true) 66 | 67 | lu.assertEquals( mIPInSubnet:isInSubnets("192.168.2.0"), false) 68 | lu.assertEquals( mIPInSubnet:isInSubnets("2001:0db8::1"), false) 69 | 70 | lu.assertEquals( mIPInSubnet:addSubnet("192.168.2.0/24"), true) 71 | lu.assertEquals( mIPInSubnet:isInSubnets("192.168.2.0"), true) 72 | lu.assertEquals( mIPInSubnet:isInSubnets("192.168.2.2"), true) 73 | 74 | lu.assertEquals( mIPInSubnet:addSubnet("192.150.2.0/23"), true) 75 | lu.assertEquals( mIPInSubnet:isInSubnets("192.150.3.254"), true) 76 | lu.assertEquals( mIPInSubnet:isInSubnets("192.150.2.0"), true) 77 | lu.assertEquals( mIPInSubnet:isInSubnets("192.150.1.0"), false) 78 | 79 | lu.assertEquals( mIPInSubnet:addSubnet("2001:0db8::/64"), true) 80 | lu.assertEquals( mIPInSubnet:isInSubnets("2001:0db8::1"), true) 81 | lu.assertEquals( mIPInSubnet:isInSubnets("2001:0db9::1"), false) 82 | lu.assertEquals( mIPInSubnet:isInSubnets("2001:0db7::1"), false) 83 | 84 | lu.assertEquals( mIPInSubnet:addSubnet("3001:0db2::/31"), true) 85 | lu.assertEquals( mIPInSubnet:isInSubnets("3001:0db1::"), false) 86 | 87 | lu.assertEquals( mIPInSubnet:isInSubnets("3001:0db2::"), true) 88 | lu.assertEquals( mIPInSubnet:isInSubnets("3001:0db3::"), true) 89 | lu.assertEquals( mIPInSubnet:isInSubnets("3001:0db5::"), false) 90 | lu.assertEquals( mIPInSubnet:isInSubnets("4001:0db5::"), false) 91 | lu.assertEquals( mIPInSubnet:isInSubnets("4001:b5::"), false) 92 | 93 | end 94 | 95 | 96 | function testIPv4() 97 | mIPInSubnet = getIPInSubnet() 98 | mIPInSubnet.treeIPv4.insertIPv4("192.168.1.0", mIPInSubnet.treeIPv4.root, 24) 99 | lu.assertEquals( mIPInSubnet.treeIPv4.searchIPv4("192.168.2.0", mIPInSubnet.treeIPv4.root), false) 100 | mIPInSubnet.treeIPv4.insertIPv4("192.168.2.0", mIPInSubnet.treeIPv4.root, 24) 101 | lu.assertEquals( mIPInSubnet.treeIPv4.searchIPv4("192.168.2.0", mIPInSubnet.treeIPv4.root), true) 102 | mIPInSubnet.treeIPv4.insertIPv4("192.150.2.0", mIPInSubnet.treeIPv4.root, 23) 103 | lu.assertEquals( mIPInSubnet.treeIPv4.searchIPv4("192.150.3.254", mIPInSubnet.treeIPv4.root), true) 104 | lu.assertEquals( mIPInSubnet.treeIPv4.searchIPv4("192.150.2.0", mIPInSubnet.treeIPv4.root), true) 105 | lu.assertEquals( mIPInSubnet.treeIPv4.searchIPv4("192.150.1.0", mIPInSubnet.treeIPv4.root), false) 106 | 107 | 108 | end 109 | 110 | function testIPv6() 111 | 112 | mIPInSubnet = getIPInSubnet() 113 | mIPInSubnet.treeIPv6.insertIPv6("2001:0db8::", mIPInSubnet.treeIPv6.root, 64) 114 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("2001:0db8::1", mIPInSubnet.treeIPv6.root), true) 115 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("2001:0db9::1", mIPInSubnet.treeIPv6.root), false) 116 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("2001:0db7::1", mIPInSubnet.treeIPv6.root), false) 117 | 118 | -- .0010: 119 | mIPInSubnet.treeIPv6.insertIPv6("3001:0db2::", mIPInSubnet.treeIPv6.root, 31) 120 | -- .0001: -false 121 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("3001:0db1::", mIPInSubnet.treeIPv6.root), false) 122 | -- .0010: -true 123 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("3001:0db2::", mIPInSubnet.treeIPv6.root), true) 124 | -- .0011: -true 125 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("3001:0db3::", mIPInSubnet.treeIPv6.root), true) 126 | 127 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("3001:0db5::", mIPInSubnet.treeIPv6.root), false) 128 | 129 | lu.assertEquals( mIPInSubnet.treeIPv6.searchIPv6("4001:0db5::", mIPInSubnet.treeIPv6.root), false) 130 | end 131 | 132 | 133 | 134 | function testDecompressedIPv6() 135 | mIPInSubnet = getIPInSubnet() 136 | lu.assertEquals( 137 | mIPInSubnet.treeIPv6.decompressedIPv6("21:db8::1"), 138 | "0021:0db8:0000:0000:0000:0000:0000:0001" 139 | ) 140 | lu.assertEquals( 141 | mIPInSubnet.treeIPv6.decompressedIPv6("21:db8::800:0:0:12"), 142 | "0021:0db8:0000:0000:0800:0000:0000:0012" 143 | ) 144 | lu.assertEquals( 145 | mIPInSubnet.treeIPv6.decompressedIPv6("21:0db8::0000:0800:0000:0000:0001"), 146 | "0021:0db8:0000:0000:0800:0000:0000:0001" 147 | ) 148 | 149 | lu.assertEquals( 150 | mIPInSubnet.treeIPv6.decompressedIPv6("1234:5678:9abc:def0:1234:5678:9abc:def0"), 151 | "1234:5678:9abc:def0:1234:5678:9abc:def0" 152 | ) 153 | 154 | lu.assertEquals( 155 | mIPInSubnet.treeIPv6.decompressedIPv6("::1"), 156 | "0000:0000:0000:0000:0000:0000:0000:0001" 157 | ) 158 | 159 | lu.assertEquals( 160 | mIPInSubnet.treeIPv6.decompressedIPv6("::"), 161 | "0000:0000:0000:0000:0000:0000:0000:0000" 162 | ) 163 | 164 | lu.assertEquals( 165 | mIPInSubnet.treeIPv6.decompressedIPv6("0:0:0:0:0:0:0:1"), 166 | "0000:0000:0000:0000:0000:0000:0000:0001" 167 | ) 168 | 169 | lu.assertEquals( 170 | mIPInSubnet.treeIPv6.decompressedIPv6("1::"), 171 | "0001:0000:0000:0000:0000:0000:0000:0000" 172 | ) 173 | 174 | lu.assertEquals( 175 | mIPInSubnet.treeIPv6.decompressedIPv6("0:2:1::"), 176 | "0000:0002:0001:0000:0000:0000:0000:0000" 177 | ) 178 | 179 | lu.assertEquals( 180 | mIPInSubnet.treeIPv6.decompressedIPv6("0:2:1::23"), 181 | "0000:0002:0001:0000:0000:0000:0000:0023" 182 | ) 183 | 184 | lu.assertEquals( 185 | mIPInSubnet.treeIPv6.decompressedIPv6("2001:db8::ff00:42:8329"), 186 | "2001:0db8:0000:0000:0000:ff00:0042:8329" 187 | ) 188 | 189 | lu.assertEquals( 190 | mIPInSubnet.treeIPv6.decompressedIPv6("fe80::1"), 191 | "fe80:0000:0000:0000:0000:0000:0000:0001" 192 | ) 193 | 194 | lu.assertEquals( 195 | mIPInSubnet.treeIPv6.decompressedIPv6("fe80::"), 196 | "fe80:0000:0000:0000:0000:0000:0000:0000" 197 | ) 198 | 199 | lu.assertEquals( 200 | mIPInSubnet.treeIPv6.decompressedIPv6("0000:0002:0001:0000:0000:0000:0000:0023"), 201 | "0000:0002:0001:0000:0000:0000:0000:0023" 202 | ) 203 | 204 | lu.assertEquals( 205 | mIPInSubnet.treeIPv6.decompressedIPv6("1:2::3:4:5:6"), 206 | "0001:0002:0000:0000:0003:0004:0005:0006" 207 | ) 208 | 209 | lu.assertEquals( 210 | mIPInSubnet.treeIPv6.decompressedIPv6("1:2:3:4:5:6::"), 211 | "0001:0002:0003:0004:0005:0006:0000:0000" 212 | ) 213 | 214 | lu.assertEquals( 215 | mIPInSubnet.treeIPv6.decompressedIPv6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), 216 | "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" 217 | ) 218 | 219 | end 220 | 221 | 222 | 223 | os.exit(lu.LuaUnit.run()) 224 | 225 | 226 | -------------------------------------------------------------------------------- /lib/IPInSubnet.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Binary tree implementation for efficient IPv4 and IPv6 subnet search. 3 | Designed for scenarios with a large number of subnets, enabling quick checks 4 | to determine if an IP address belongs to a specific subnet. 5 | 6 | Features: 7 | - Supports both IPv4 and IPv6 addresses. 8 | - Efficient insertion and search operations using binary trees. 9 | - Handles CIDR notation for subnet masks. 10 | 11 | Created by: bnch.dan 12 | Created on: 2025-03-21 13 | GitHub: https://github.com/bnchdan 14 | ]] 15 | 16 | bit = require("bit") 17 | 18 | 19 | LookUp = {} 20 | function LookUp.getDec() 21 | return { 22 | ["0"]=0, 23 | ["1"]=1, 24 | ["2"]=2, 25 | ["3"]=3, 26 | ["4"]=4, 27 | ["5"]=5, 28 | ["6"]=6, 29 | ["7"]=7, 30 | ["8"]=8, 31 | ["9"]=9, 32 | ["a"]=10, 33 | ["b"]=11, 34 | ["c"]=12, 35 | ["d"]=13, 36 | ["e"]=14, 37 | ["f"]=15 38 | } 39 | end 40 | 41 | 42 | 43 | 44 | 45 | BinaryTreeNode = {} 46 | BinaryTreeNode.__index = BinaryTreeNode 47 | 48 | function BinaryTreeNode:new(value) 49 | local node = { 50 | value = value, 51 | left = nil, 52 | right = nil 53 | } 54 | setmetatable(node, BinaryTreeNode) 55 | return node 56 | end 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | BinaryTree = {} 66 | BinaryTree.__index = BinaryTree 67 | BinaryTree.lookupIPv6Dec = LookUp.getDec() 68 | 69 | function BinaryTree:new() 70 | local tree = { 71 | root = BinaryTreeNode:new('*') --root node will have the value * 72 | } 73 | setmetatable(tree, BinaryTree) 74 | return tree 75 | end 76 | 77 | 78 | --[[ 79 | Insert IPv4 address into the tree 80 | @param value string IPv4 address 81 | @param node root 82 | @param num_bits -mask 83 | ]]-- 84 | function BinaryTree.insertIPv4(value, node, num_bits) 85 | local dec=0 86 | local curent_num_bits = 0 87 | local j=1 88 | local size_value = #value 89 | for i=1, size_value do 90 | c = value:sub(i,i) 91 | 92 | --new byte 93 | if c == "." then 94 | dec = value:sub(j,i-1) 95 | j=i+1 96 | goto process_byte 97 | end 98 | 99 | --end of IP 100 | if i == size_value then 101 | dec = value:sub(j,i) 102 | goto process_byte 103 | 104 | end 105 | 106 | --go to the next character 107 | goto continue_insert_IPv4 108 | 109 | ::process_byte:: 110 | -- ngx.say("dec : ", dec) 111 | for k=7,0, -1 do 112 | 113 | --get most significant bit 114 | mbit = bit.band( bit.rshift(dec, k), 1) 115 | -- ngx.say(" bin : ", mbit) 116 | 117 | if mbit == 0 then 118 | if node.left == nil then 119 | node.left = BinaryTreeNode:new(mbit) 120 | end 121 | -- ngx.say(" bin : ", mbit) 122 | -- go to the next node 123 | node = node.left 124 | else 125 | if node.right == nil then 126 | node.right = BinaryTreeNode:new(mbit) 127 | end 128 | -- ngx.say(" bin : ", mbit) 129 | -- go to the next node 130 | node = node.right 131 | end 132 | 133 | curent_num_bits = curent_num_bits + 1 134 | --stop if we reach the mask 135 | if curent_num_bits == num_bits then 136 | -- node.left = BinaryTreeNode:new("*") 137 | -- ngx.say("curent_num_bits : ", curent_num_bits) 138 | return 139 | end 140 | end 141 | 142 | 143 | ::continue_insert_IPv4:: 144 | end 145 | end 146 | 147 | 148 | --[[ 149 | Search IPv4 address in the tree 150 | @param value string IPv4 address 151 | @param node root 152 | return boolean true if found, false otherwise 153 | ]]-- 154 | function BinaryTree.searchIPv4(value, node) 155 | if node.left == nil and node.right == nil then 156 | --tree is empty 157 | return false 158 | end 159 | local dec 160 | local j=1 161 | local size_value = #value 162 | for i=1, size_value do 163 | c = value:sub(i,i) 164 | 165 | --new byte 166 | if c == "." then 167 | dec = value:sub(j,i-1) 168 | j=i+1 169 | goto search_byte 170 | end 171 | 172 | --end of IP 173 | if i == size_value then 174 | dec = value:sub(j,i) 175 | goto search_byte 176 | 177 | end 178 | 179 | --go to the next character 180 | goto continue_search_IPv4 181 | 182 | ::search_byte:: 183 | -- ngx.say("dec : ", dec) 184 | for k=7,0, -1 do 185 | 186 | --get most significant bit 187 | mbit = bit.band( bit.rshift(dec, k), 1) 188 | 189 | if node.left == nil and node.right == nil then 190 | -- ngx.say("is in tree") 191 | return true 192 | end 193 | 194 | if mbit == 0 then 195 | if node.left == nil then 196 | -- ngx.say("is not in tree 0") 197 | return false 198 | end 199 | node = node.left 200 | else 201 | if node.right == nil then 202 | -- ngx.say("is not in tree 1") 203 | return false 204 | end 205 | node = node.right 206 | end 207 | 208 | end 209 | 210 | 211 | ::continue_search_IPv4:: 212 | end 213 | end 214 | 215 | 216 | 217 | 218 | --[[ 219 | Insert IPv6 address into the tree 220 | @param value string IPv6 address 221 | @param node root 222 | @param num_bits -mask 223 | ]] 224 | function BinaryTree.insertIPv6(value, node, num_bits) 225 | 226 | local dec, mbit 227 | local num4_hex = 0 228 | local j=1 229 | local current_num_of_groups = 0 230 | local current_num_of_bits = 0 231 | local END=0 232 | local decompressed = "" 233 | -- ngx.say("value : ", value) 234 | for i=1, #value do 235 | --stop if we reach the mask 236 | -- if current_num_of_bits > num_bits then 237 | -- ngx.say("curent_num_bits : ", current_num_of_bits) 238 | -- return 239 | -- end 240 | mhex = value:sub(i,i) 241 | 242 | if i == #value then 243 | END = i 244 | if mhex == ":" then 245 | goto process_insert_group 246 | end 247 | num4_hex = num4_hex + 1 248 | goto process_insert_group 249 | end 250 | 251 | if mhex == ":" then 252 | if i == 1 then -- skip if starts with :, eg. ::1 253 | goto continue_insert_IPv6 254 | end 255 | END = i-1 256 | goto process_insert_group 257 | end 258 | 259 | num4_hex = num4_hex + 1 260 | goto continue_insert_IPv6 261 | 262 | ::process_insert_group:: 263 | current_num_of_groups = current_num_of_groups + 1 264 | 265 | if current_num_of_groups > 1 then 266 | decompressed = decompressed .. ":" 267 | end 268 | 269 | 270 | --case 1 - not compressed 271 | if num4_hex == 4 then 272 | for k=j, END do 273 | 274 | mhex = value:sub(k,k) 275 | -- append mhex 276 | dec = BinaryTree.lookupIPv6Dec[mhex] 277 | node, current_num_of_bits = BinaryTree.append4Bits(node, dec, num_bits, current_num_of_bits) 278 | if current_num_of_bits == num_bits then --stop if we reach the mask 279 | return 280 | end 281 | end 282 | 283 | end 284 | 285 | --case 2 - on :: 286 | if num4_hex == 0 then 287 | local num_of_groups = 0 288 | --search number of : after :: 289 | for k=i, #value do 290 | mhex = value:sub(k,k) 291 | if mhex == ":" then 292 | if k == #value then -- if ends with : 293 | break 294 | end 295 | 296 | num_of_groups = num_of_groups + 1 297 | end 298 | end 299 | local num_of_groups_to_add = 8 - current_num_of_groups - num_of_groups +1 300 | --apend zeros 301 | for k=1, num_of_groups_to_add do 302 | for i=1, 4 do 303 | node, current_num_of_bits = BinaryTree.append4Bits(node, 0, num_bits, current_num_of_bits) 304 | if current_num_of_bits == num_bits then --stop if we reach the mask 305 | return 306 | end 307 | end 308 | end 309 | 310 | --case 3 - not 4 hex group 311 | elseif num4_hex < 4 then 312 | --append 0 313 | for i=num4_hex, 4-1 do 314 | -- apend 0 315 | 316 | node, current_num_of_bits = BinaryTree.append4Bits(node, 0, num_bits, current_num_of_bits) 317 | if current_num_of_bits == num_bits then --stop if we reach the mask 318 | return 319 | end 320 | end 321 | for k=j, END do 322 | mhex = value:sub(k,k) 323 | --append mhex 324 | dec = BinaryTree.lookupIPv6Dec[mhex] 325 | node, current_num_of_bits = BinaryTree.append4Bits(node, dec, num_bits, current_num_of_bits) 326 | if current_num_of_bits == num_bits then --stop if we reach the mask 327 | return 328 | end 329 | end 330 | end 331 | num4_hex = 0 332 | j=i+1 333 | 334 | ::continue_insert_IPv6:: 335 | end 336 | 337 | end 338 | 339 | --[[ 340 | Append 4 bits to the tree. Called from insertIPv6 341 | @param node 342 | @param value int value of the hex, max 4 bits 343 | @param num_bits int 344 | @param current_num_of_bits int 345 | return next node and the current number of bits 346 | ]] 347 | function BinaryTree.append4Bits(node,value, num_bits, current_num_of_bits) 348 | for i=3,0,-1 do 349 | 350 | --append 0 351 | mbit = bit.band( bit.rshift(value, i), 1) 352 | -- ngx.say(" bin : ", mbit) 353 | 354 | 355 | if mbit == 0 then 356 | if node.left == nil then 357 | node.left = BinaryTreeNode:new(mbit) 358 | end 359 | -- ngx.say(" bin : ", mbit) 360 | -- go to the next node 361 | node = node.left 362 | else 363 | if node.right == nil then 364 | node.right = BinaryTreeNode:new(mbit) 365 | end 366 | -- ngx.say(" bin : ", mbit) 367 | -- go to the next node 368 | node = node.right 369 | end 370 | 371 | current_num_of_bits = current_num_of_bits + 1 372 | 373 | --stop if we reach the mask 374 | if current_num_of_bits == num_bits then 375 | break 376 | end 377 | end 378 | return node, current_num_of_bits 379 | end 380 | 381 | 382 | --[[ 383 | Search IPv6 address in the tree 384 | @param value string IPv6 address 385 | @param node root 386 | return boolean true if found, false otherwise 387 | ]] 388 | function BinaryTree.searchIPv6(value, node) 389 | if node.left == nil and node.right == nil then 390 | --tree is empty 391 | return false 392 | end 393 | 394 | 395 | local dec, mbit 396 | local num4_hex = 0 397 | local j=1 398 | local current_num_of_groups = 0 399 | local current_num_of_bits = 0 400 | local END=0 401 | local decompressed = "" 402 | -- ngx.say("value : ", value) 403 | for i=1, #value do 404 | --stop if we reach the mask 405 | -- if current_num_of_bits > num_bits then 406 | -- ngx.say("curent_num_bits : ", current_num_of_bits) 407 | -- return 408 | -- end 409 | mhex = value:sub(i,i) 410 | 411 | if i == #value then 412 | END = i 413 | if mhex == ":" then 414 | goto process_search_group 415 | end 416 | num4_hex = num4_hex + 1 417 | goto process_search_group 418 | end 419 | 420 | if mhex == ":" then 421 | if i == 1 then -- skip if starts with :, eg. ::1 422 | goto continue_search_IPv6 423 | end 424 | END = i-1 425 | goto process_search_group 426 | end 427 | 428 | num4_hex = num4_hex + 1 429 | goto continue_search_IPv6 430 | 431 | ::process_search_group:: 432 | current_num_of_groups = current_num_of_groups + 1 433 | 434 | if current_num_of_groups > 1 then 435 | decompressed = decompressed .. ":" 436 | end 437 | 438 | 439 | --case 1 - not compressed 440 | if num4_hex == 4 then 441 | for k=j, END do 442 | 443 | mhex = value:sub(k,k) 444 | -- append mhex 445 | dec = BinaryTree.lookupIPv6Dec[mhex] 446 | node, res = BinaryTree.search4Bits(node, dec) 447 | 448 | if res ~= nil then 449 | return res 450 | end 451 | end 452 | 453 | end 454 | 455 | --case 2 - :: 456 | if num4_hex == 0 then 457 | local num_of_groups = 0 458 | --search number of : after :: 459 | for k=i, #value do 460 | mhex = value:sub(k,k) 461 | if mhex == ":" then 462 | if k == #value then -- if ends with : 463 | break 464 | end 465 | 466 | num_of_groups = num_of_groups + 1 467 | end 468 | end 469 | local num_of_groups_to_add = 8 - current_num_of_groups - num_of_groups +1 470 | --apend zeros 471 | for k=1, num_of_groups_to_add do 472 | for i=1, 4 do 473 | 474 | --apend 0 475 | node, res = BinaryTree.search4Bits(node, 0) 476 | 477 | if res ~= nil then 478 | return res 479 | end 480 | end 481 | end 482 | 483 | --case 3 - not 4 hex 484 | elseif num4_hex < 4 then 485 | --append 0 486 | for i=num4_hex, 4-1 do 487 | -- apend 0 488 | 489 | node, res = BinaryTree.search4Bits(node, 0) 490 | if res ~= nil then 491 | return res 492 | end 493 | end 494 | for k=j, END do 495 | 496 | mhex = value:sub(k,k) 497 | --append mhex 498 | node, res = BinaryTree.search4Bits(node, dec) 499 | if res ~= nil then 500 | return res 501 | end 502 | end 503 | end 504 | num4_hex = 0 505 | 506 | j=i+1 507 | 508 | ::continue_search_IPv6:: 509 | end 510 | end 511 | 512 | 513 | --[[ 514 | Search 4 bits in the tree. Called from searchIPv6 515 | @param value int value of the hex, max 4 bits 516 | @param node 517 | return next node and the result if found 518 | ]] 519 | function BinaryTree.search4Bits(node,value) 520 | for i=3,0,-1 do 521 | --append 0 522 | mbit = bit.band( bit.rshift(value, i), 1) 523 | if node.left == nil and node.right == nil then 524 | -- ngx.say("is in tree") 525 | return node, true 526 | end 527 | 528 | if mbit == 0 then 529 | if node.left == nil then 530 | -- ngx.say("is not in tree") 531 | return node, false 532 | end 533 | node = node.left 534 | else 535 | if node.right == nil then 536 | -- ngx.say("is not in tree") 537 | return node, false 538 | end 539 | node = node.right 540 | end 541 | end 542 | 543 | return node, nil 544 | end 545 | 546 | 547 | 548 | --[[ 549 | Decompress IPv6 address alghortihm ussed in the insertIPv6 and searchIPv6 550 | @param value string IPv6 address 551 | @return string decompressed IPv6 address 552 | e.g. 2001:0db8::0001 -> 2001:0db8:0000:0000:0000:0000:0000:0001 553 | ]] 554 | function BinaryTree.decompressedIPv6(value) 555 | -- ngx.say(value) 556 | -- ngx.say(num_bits) 557 | local dec, mbit 558 | local num4_hex = 0 559 | local j=1 560 | local current_num_of_groups = 0 561 | local END=0 562 | local decompressed = "" 563 | -- ngx.say("value : ", value) 564 | for i=1, #value do 565 | mhex = value:sub(i,i) 566 | 567 | if i == #value then 568 | END = i 569 | if mhex == ":" then 570 | -- current_num_of_groups = current_num_of_groups - 1 -- for :: 571 | goto process_decompressed_group 572 | end 573 | num4_hex = num4_hex + 1 574 | goto process_decompressed_group 575 | end 576 | 577 | if mhex == ":" then 578 | if i == 1 then -- skip if starts with :, eg. ::1 579 | goto continue_decompressed_IPv6 580 | end 581 | END = i-1 582 | goto process_decompressed_group 583 | end 584 | 585 | num4_hex = num4_hex + 1 586 | goto continue_decompressed_IPv6 587 | 588 | ::process_decompressed_group:: 589 | current_num_of_groups = current_num_of_groups + 1 590 | 591 | if current_num_of_groups > 1 then 592 | decompressed = decompressed .. ":" 593 | end 594 | --not compressed 595 | if num4_hex == 4 then 596 | for k=j, END do 597 | mhex = value:sub(k,k) 598 | -- ngx.say("mhex : ", mhex) 599 | decompressed = decompressed .. mhex 600 | end 601 | end 602 | 603 | -- found :: 604 | if num4_hex == 0 then 605 | -- ngx.say("found ::") 606 | local num_of_groups = 0 607 | --search number of : after :: 608 | for k=i, #value do 609 | mhex = value:sub(k,k) 610 | if mhex == ":" then 611 | if k == #value then -- if ends with : 612 | break 613 | end 614 | 615 | num_of_groups = num_of_groups + 1 616 | end 617 | end 618 | -- ngx.say("num_of_groups : ", num_of_groups) 619 | -- ngx.say("current_num_of_groups : ", current_num_of_groups) 620 | local num_of_groups_to_add = 8 - current_num_of_groups - num_of_groups +1 621 | 622 | -- ngx.say("num_of_groups_to_add : ", num_of_groups_to_add) 623 | 624 | --apend zeros 625 | for k=1, num_of_groups_to_add do 626 | for i=1, 4 do 627 | -- ngx.say("i : ", 0) 628 | decompressed = decompressed .. "0" 629 | end 630 | if k < num_of_groups_to_add then 631 | decompressed = decompressed .. ":" 632 | end 633 | end 634 | 635 | elseif num4_hex < 4 then 636 | --append 0 637 | for i=num4_hex, 4-1 do 638 | -- ngx.say("i : ", 0) 639 | decompressed = decompressed .. "0" 640 | end 641 | for k=j, END do 642 | mhex = value:sub(k,k) 643 | -- ngx.say("mhex : ", mhex) 644 | decompressed = decompressed .. mhex 645 | end 646 | end 647 | num4_hex = 0 648 | 649 | j=i+1 650 | 651 | ::continue_decompressed_IPv6:: 652 | end 653 | -- ngx.say("compresed: ", value) 654 | -- ngx.say("decompressed : ", decompressed) 655 | return decompressed 656 | end 657 | 658 | 659 | 660 | --[[ 661 | Print the tree 662 | @param node root 663 | @param delimiter string 664 | @param space string 665 | @param index int 666 | ]] 667 | function BinaryTree.printTree(node, delimiter, space, index) 668 | if node == nil then 669 | return 670 | end 671 | 672 | if (space == nil) then 673 | space = "" 674 | end 675 | 676 | if (delimiter == nil) then 677 | delimiter = "..." 678 | end 679 | 680 | if (index == nil) then 681 | index = 0 682 | end 683 | 684 | ngx.say(space..node.value) 685 | if index % 8 == 0 and index ~= 0 then 686 | ngx.say(space.."............8bits delimiter............") 687 | end 688 | 689 | BinaryTree.printTree(node.left, delimiter, space .. delimiter, index +1) 690 | BinaryTree.printTree(node.right, delimiter, space.. delimiter, index +1) 691 | end 692 | 693 | 694 | 695 | 696 | 697 | IPInSubnet = { 698 | treeIPv4 = nil, 699 | treeIPv6 = nil 700 | } 701 | IPInSubnet.__index = IPInSubnet 702 | --Constructor 703 | function IPInSubnet:new() 704 | local o ={ 705 | treeIPv4 = BinaryTree:new(), 706 | treeIPv6 = BinaryTree:new() 707 | } 708 | setmetatable(o, self) 709 | return o 710 | 711 | end 712 | 713 | function IPInSubnet:isValidIPv4(ip) 714 | -- Check if the IP matches the IPv4 pattern 715 | local octets = { ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") } 716 | if #octets ~= 4 then 717 | return false 718 | end 719 | 720 | -- Ensure each octet is within the valid range (0-255) 721 | for _, octet in ipairs(octets) do 722 | local num = tonumber(octet) 723 | if not num or num < 0 or num > 255 then 724 | return false 725 | end 726 | end 727 | 728 | return true 729 | end 730 | 731 | function IPInSubnet:isValidIPv6(ip) 732 | ip = self.treeIPv6.decompressedIPv6(ip) 733 | -- Check if the IP matches the IPv6 pattern 734 | local segments = { ip:match("([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):([^:]*):([^:]*)") } 735 | if #segments ~= 8 then 736 | return false 737 | end 738 | 739 | -- Ensure each segment is a valid hexadecimal number 740 | for _, segment in ipairs(segments) do 741 | if not tonumber(segment, 16) then 742 | return false 743 | end 744 | end 745 | 746 | return true 747 | end 748 | 749 | 750 | function IPInSubnet:addSubnet(cidr) 751 | -- --split the subnet and mask 752 | local subnet, maskBits = cidr:match("([^/]+)/(%d+)") 753 | 754 | if not subnet or not maskBits then 755 | return false, "Invalid CIDR" 756 | end 757 | 758 | maskBits = tonumber(maskBits) 759 | 760 | if self:isValidIPv4(subnet) then 761 | -- ngx.say("IPv4 subnet ", subnet) 762 | if maskBits > 32 or maskBits<1 then 763 | return false, "Invalid mask" 764 | end 765 | 766 | self.treeIPv4.insertIPv4( 767 | subnet, self.treeIPv4.root, 768 | maskBits 769 | ) 770 | 771 | return true 772 | end 773 | 774 | if self:isValidIPv6(subnet) then 775 | -- ngx.say("IPv6 subnet") 776 | 777 | if maskBits > 128 or maskBits<1 then 778 | return false, "Invalid mask" 779 | end 780 | 781 | self.treeIPv6.insertIPv6( 782 | subnet, 783 | self.treeIPv6.root, 784 | maskBits 785 | ) 786 | return true 787 | end 788 | 789 | return false 790 | end 791 | 792 | 793 | function IPInSubnet:isInSubnets(ip) 794 | if ip:find(".", 1, true) then --is IPv4 795 | -- ngx.say("IPv4#", ip) 796 | return self.treeIPv4.searchIPv4(ip, self.treeIPv4.root) 797 | end 798 | -- ngx.say("IPv6") 799 | return self.treeIPv6.searchIPv6(ip, self.treeIPv6.root) 800 | end 801 | 802 | 803 | function IPInSubnet:isInSubnetsStrict(ip) 804 | if self:isValidIPv4(ip) then 805 | -- ngx.say("IPv4") 806 | return self.treeIPv4.searchIPv4(ip, self.treeIPv4.root) 807 | end 808 | 809 | if self:isValidIPv6(ip) then 810 | -- ngx.say("IPv6") 811 | return self.treeIPv6.searchIPv6(ip, self.treeIPv6.root) 812 | end 813 | 814 | --invalid format 815 | return false 816 | end 817 | 818 | 819 | return IPInSubnet 820 | 821 | 822 | 823 | 824 | --------------------------------------------------------------------------------