專案上的小需求,公司內網依實體網路架構區分了多個網段,系統有網段清單,已知不同 CIDR (Classless Inter-Domain Routing) 格式(例如:192.168.1.0/24、10.0.0.0/8) 對映的代碼及說明。系統在接收到任一 IP 地址時,需識別出其隸屬哪一個網段。有個小眉角是子網段範圍有大小之分,例如同時定義了 10.0.0.0/8 以及 10.10.0.0/16,要能識別除了 10.10.* 以外的 10.0-255.* 是前者,10.10.* 為後者。

這邏輯說來不難,但我想用物件導向一點的方式來寫。

傳統平鋪直述寫法應該會寫一個迴圈,用 IP 跟每個網段 CIDR 相比,檢查套用遮罩後的結果是否相符,然後要注意兩個範圍不同網段重疊時以範圍小的優先... 等細節,程式寫起來類似這樣:

var netZones = new string[] {
    "10.0.0.0/8\tSvr-ALL\t10 網段",
    "10.10.0.0/16\tSvr-10\t10.10 網段",
    "172.16.0.8/12\tOffice-ALL\t172.16-31 網段",
    "172.17.0.0/16\tOffice-17\t172.17 網段",
    "192.168.0.0/16\tNetDevice\t192.168 網段"
};

Action<string> test = (ip) =>
{
    var maxMaskBits = 0;
    var matchNetZone = "Undefined";
    var ipUint = IPToUint(ip);
    foreach (var netZone in netZones)
    {
        var p = netZone.Split('\t');
        var cidr = p[0];
        var zoneCode = p[1];
        var comment = p[2];
        p = cidr.Split('/');
        var maskBits = int.Parse(p[1]);
        var netUint = IPToUint(p[0]) & (uint.MaxValue << (32 - maskBits));
        if ((ipUint & (uint.MaxValue << (32 - maskBits))) == netUint)
        {
            if (maskBits > maxMaskBits) {
                maxMaskBits = maskBits;
                matchNetZone = netZone;
            }
        }
    }
    Console.WriteLine($"{ip,-15} => {matchNetZone}");
};

test("10.10.123.123");
test("10.123.123.123");
test("192.168.1.1");
test("172.28.1.1");
test("172.16.1.1");
test("172.17.1.1");
test("8.8.8.8");

static uint IPToUint(string ip)
{
    var segments = ip.Split('.');
    if (segments.Length != 4) return 0;
    try
    {
        return (uint.Parse(segments[0]) << 24)
             | (uint.Parse(segments[1]) << 16)
             | (uint.Parse(segments[2]) << 8)
             | uint.Parse(segments[3]);
    }
    catch (Exception)
    {
        return 0;
    }
}

如果用物件導向寫法,我們可讓每個網段是一個 NetworkZone 物件,用 bool Match(string ip) 判斷是否屬於該網段;而所有 NetworkZone 集合也寫成物件 - NetworkZoneTable,提供一個 NetworkZone(string ip) 方法,傳入任意 IP 可判斷其屬於何網段。

使用起來會像這樣:

var netZones = new NetworkZoneTable
{
    new NetworkZone("10.0.0.0/8", "Svr-ALL", "10 網段"),
    new NetworkZone("10.10.0.0/16", "Svr-10", "10.10 網段"),
    new NetworkZone("172.16.0.8/12", "Office-ALL", "172.16-31 網段"),
    new NetworkZone("172.17.0.0/16", "Office-17", "172.17 網段"),
    new NetworkZone("192.168.0.0/16", "NetDevice", "192.168 網段")
};

Action<string> test = (ip) =>
{
    var netZone = netZones.Match(ip);
    Console.WriteLine($"{ip,-15} => {netZone}");
};

test("10.10.123.123");
test("10.123.123.123");
test("192.168.1.1");
test("172.28.1.1");
test("172.16.1.1");
test("172.17.1.1");
test("8.8.8.8");

NetworkZone 及 NetworkZoneTable 寫法如下:

using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace netmask_lab
{
    /// <summary>
    /// 網段定義
    /// </summary>
    public class NetworkZone
    {
        [JsonIgnore]
        /// <summary>
        /// IP 位址
        /// </summary>
        public string IP = "0.0.0.0";
        [JsonIgnore]
        /// <summary>
        /// 子網路遮罩位元數
        /// </summary>
        public int MaskBits = 32;
        [JsonIgnore]
        /// <summary>
        /// IP 位址轉為 uint
        /// </summary>
        public uint UintVal = 0;
       /// <summary>
        /// 是否為未定義網段
        /// </summary>
        public bool IsUndefined => UintVal == 0 && MaskBits == 32;
        /// <summary>
        /// 子網段識別(CIDR)
        /// </summary>
        public string Cidr { get; set; } = "0.0.0.0/32";
        /// <summary>
        /// 網段代碼
        /// </summary>
        public string ZoneCode { get; set; } = "NA";
        /// <summary>
        /// 網段說明
        /// </summary>
        public string Comment { get; set; } = "Undefined";

        /// <summary>
        /// 將 IP 轉為 uint,方便 Mask 計算
        /// </summary>
        /// <param name="ip"></param>
        /// <returns></returns>
        public static uint IPToUint(string ip)
        {
            var segments = ip.Split('.');
            if (segments.Length != 4) return 0;
            try
            {
                return (uint.Parse(segments[0]) << 24)
                     | (uint.Parse(segments[1]) << 16)
                     | (uint.Parse(segments[2]) << 8)
                     | uint.Parse(segments[3]);
            }
            catch (Exception)
            {
                return 0;
            }
        }

        void Init()
        {
            if (!Regex.IsMatch(Cidr, @"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$"))
                throw new Exception("CIDR format error.");
            IP = Cidr.Split('/').First();
            MaskBits = int.Parse(Cidr.Split('/').Last());
            UintVal = IPToUint(IP) & (uint.MaxValue << (32 - MaskBits));
        }

        public NetworkZone()
        {
            Init();
        }

        public NetworkZone(string cidr, string zoneCode, string comment)
        {
            this.Cidr = cidr;
            this.ZoneCode = zoneCode;
            this.Comment = comment;
            Init();
        }

        public bool Match(uint ip) => (ip & (uint.MaxValue << (32 - MaskBits))) == UintVal;
        
        public bool Match(string ip) => Match(IPToUint(ip));
 
        override public string ToString()
        {
            if (IsUndefined) return Comment;
            return $"{Cidr} ({ZoneCode} {Comment})";
        }
    }

    public class NetworkZoneTable : List<NetworkZone>
    {
        public NetworkZone Match(string ip)
        {
            var ipUint = NetworkZone.IPToUint(ip);
            return this.OrderByDescending(o => o.MaskBits)
                .ThenBy(o => o.IP).FirstOrDefault(z => z.Match(ipUint)) ?? new NetworkZone();
        }
    }
}

補充幾個小地方:

  1. 網段判別主要靠兩個 IP 套用網段遮罩後比對是否一致。我採用的做法是將 IP 的四個 0-255 轉成 Unsigned Integer 的四個 Bytes,用 Uint_Value_of_IP & uint.MaxValue << (32 - MaskBits) 套用遮罩。
  2. 每個 NetworkZone 有個 public bool Match(uint ip),依序比對遇到 true 時代表該 IP 屬於此網段。由於要先比範圍小的再比範圍大的,故比對順序會依 .OrderByDescending(o => o.MaskBits).ThenBy(o => o.IP) 排序。
  3. NetworkZoneTable 骨子裡是個 IList<NetworkZone>,只差在多了一個 public NetworkZone Match(string ip)。public class NetworkZoneTable : List<NetworkZone> 繼承 List<T> 可直接獲得 List<NetworkZone> 的所有屬性、方法,如開始的程式碼,能用 new NetworkZoneTable { new NetworkZone(...), new NetworkZone(...) } 以集合初始設定式指定內容。

A requirement to compare CIDR and find out the subnet it belongs to, implemented using C# object-oriented approach.


Comments

Be the first to post a comment

Post a comment