2020年6月18日 星期四

Wildcard Mask 用Python計算去簡化輸入

Repl.it (線上執行Python) ※支援手機(Android  IOS)
OneWildcardMask: 將輸入的IP位置用一組Network和WildcardMask表示。
MultiWildcardMask: 用一或多組Network和WildcardMask去表示所有輸入的IP的。

昨天教到 wildcard mask,打開我對 wildcard 只是二進位下該位 wildcard mask 為1就任意,為零就只能是 network 對應的值,這種淺的見解。

wildcard本身有萬用字元的意思,也就是通配,這也能與 wildcard mask 二進位為1就任意(01)起到關聯。

就計算結果上來看,wildcard mask 不是 subnet mask 的相反,而是比 subnet mask 計算更複雜的值。比起翻作「反遮罩」,我認為稱之「通配遮罩」更適合。

wildcard mask 用意是為了減少路由的輸入,也就是可以 match 到多網段,如果設定的好,可以用一行就涵蓋多個網段,可以減少路由器的負荷。

「0.0.0.0 255.255.255.255」(Network, WildcardMask) 代表全部network皆匹配。在設OSPF時,也不用一個個網段輸,只用一行就能解決。
但這是有風險的,也代表該路由器是可以跟所有IP位置形成夥伴關係,一個惡意就能透過路由交換去塞滿垃圾路由或是搶走路由做 Man in Middle。

最好就只開設定內且可信的進ACL,別偷懶!但是 wildcard 計算並不是那麼直觀的,常有不同的需求,如果要手算,就怕數量一大,會很累。

如我要只要開 192.168.0.7 跟 192.168.0.15 進來,只能用一個 network 跟一個 wildcard mask 就要能表示只有該兩個位置會被挑中。

二IP位置前方三部分 192.168.0 都一樣,所以可知 wildcard mask 的前三部分是 0.0.0。

到了第四位就會有些不同,這裡得換成二進位去看其運作。

二進位      十進位
00000111 <= 7 (IP 1)
00001111 <= 15 (IP 2)
 
在左方數來的第五位,同時出現 0 和 1,那代表該位應該要通配。其餘位置上只有 0 或 1 單獨出現,所有是固定的。

0000*000 => 00001000 => 8 (wildcard)

於是得到 wildcard mask 應該是設為 「0.0.0.8」。

這套算法是最基礎的,照這套路能很輕易就手算出各種要求。

那今天有10個VLAN,也就是10個不同網段,甚至是20個以上呢?手算光寫出二進位就很花時間了,更別說可能會看錯。

那交給電腦算吧。

在 wildcard 這種通配規則在該位同時有 01 的情況下要為 1,所以拿兩兩相比來看,運行的是 XOR 計算。

也就是說如果計算結果二進位下的某位為 1,那代表該位需要通配。

因此再把兩兩XOR的結果做一次總體的OR運算,就能算出只用一行指令的限制,最小限度的wildcard mask。但這不能保證只有目標只被挑出,可能會摻一些不在設定內的。

先只考慮濃縮成一個wildcard mask 去包含所有想要在內的IP位置,寫成Python來看


"""
ips間互相做XOR得到結果一
再把結果一做OR,得到單一最低限度的wildcard mask
"""

ips = [[192,168,0,7], [192,168,0,15]]  
result = [set(), set(), set(), set()]
wc = []
# 兩兩 XOR => 計算結果1
for i1 in range(len(ips)-1):
    for i2 in range(i1+1, len(ips)):
        for i3 in range(4):
            result[i3].add(ips[i1][i3] ^ ips[i2][i3]) 
# 計算結果1 做 OR 總計算
for i in range(4):
    num  = 0
    for res in result[i]:
        num |= res
    wc.append(num)
print(wc)
"""
再次簡化
"""
ips = [[192,168,0,7], [192,168,0,15]]  
wc = [0, 0, 0, 0]
for i1 in range(len(ips)-1):
    for i2 in range(i1+1, len(ips)):
        for i3 in range(4):
            wc[i3] |= (ips[i1][i3] ^ ips[i2][i3]) 
print(wc)
雖然算得出來,但是因為一開始要算兩兩成對XOR,所以會至少要算 n(n-1)/2 次,如果要把一個範圍納入,如最後二位可[0, 255]間,會計算(255*255)*((255*255)-1)/1次。
那會運算相當久,但對人來說,那可能是一秒就能算完的。

換個想法,既然在IP位置清單內的都要能符合,不如從一個 (Network, Wildcard) 的組合,對應每一個IP位置去做一些修改,讓每一個都能符合條件。
ips = []
for l1 in range(256):
    for l2 in range(256):
        ips.append([192, 168, l1, l2])

wc = [[[0,0,0,0], [255,255,255,255]]]
if ips:
    wc = [[ips.pop(0), [0,0,0,0]]]
    for ip in ips:
        for i in range(4):
            wc[0][1][i] |= wc[0][0][i] ^ ip[i]
print(wc)
=> [[[192, 168, 0, 0], [0, 0, 255, 255]]] 

這只要計算 (255*255) 次,明顯快很多,原來兩兩成對的XOR是可以拔除的,同時最後的輸出也已經是指令要打的格式了。

這十分接近理想,但不夠,目前仍未解決沒輸入的IP位置也可能被選中的情況。
要解決這個情況,就可能會用上多個 (Network, Wildcard) 的組合,即使用多個,也要是輸入最少次數就能處理掉。

所幸這是可以被計算出來的,最近看到 Software-Defined(SD),如果自動用上這樣的算法,人就只要去挑出範圍或對應的處理,剩下的計算或輸入指令,就可以交給程式碼代勞。
"""
給一串可以預期整理成下方結果的IP
Network: 192.168.0.0, Wildcard Mask: 0.0.0.3
Network: 192.168.0.4, Wildcard Mask: 0.0.0.8
"""
ips =  [[192,168,0,0],[192,168,0,1],[192,168,0,2],[192,168,0,3],[192,168,0,4],[192,168,0,12]]
from collections import defaultdict
wc = defaultdict(list)
"""
將 IP位置 做 summary
"""
def check(strIP, strIP2):
    """
    只能有一個位置01同時出現
    但如果是 * 符,二者沒有對上,就表示無法整合。
    """
    dismatch = 0
    pos = -1
    for i in range(32):
        if strIP[i] != strIP2[i]:
            if strIP[i] == "*" or strIP2[i] == "*": return -1
            dismatch += 1
            pos = i
            if dismatch == 2: return -1
    return pos

for ip in ips:
    strIP = "{:0>8}{:0>8}{:0>8}{:0>8}".format(*[bin(num)[2:] for num in ip]) # 換成 連續二進位 string
    level = 0
    pos = 0
    while pos != -1 and wc[level]:        
        for j in range(len(wc[level])):
            pos = check(strIP, wc[level][j][1])
            if not pos == -1:
                ip = wc[level].pop(j)[0]
                strIP = strIP[:pos] + "*" + strIP[pos+1:]
                level += 1
                break         
    wc[level].append([ip, strIP])

"""
將 Summary 後的結果轉換回十進位格式
"""
result = []
for key in wc:
    for NW in wc[key]:
        NW[1] = NW[1].replace("1", "0").replace("*", "1")
        result.append([NW[0], [int(NW[1][0:8],2), int(NW[1][8:16],2),int(NW[1][16:24],2),int(NW[1][24:32],2)]])

"""
將十進位的資料型態換成字串
"""
for each in sorted(result):
    Network = ".".join([str(num) for num in each[0]]) 
    WildcardMask = ".".join([str(num) for num in each[1]]) 
    print("Network: {:}, Wildcard Mask: {:}".format(Network, WildcardMask))

現在人只要決定哪些IP位置在要裡面,也能確保不會有預期之外的IP會被選中。
不過這對於 Range 沒有很好的支持,如果要 0 到 255 就得一一個打。但這是前處理的範疇,本篇就不做深做探討。