3

C# 基本練習 - 識別 IP 所屬網段 (物件導向寫法)

 7 months ago
source link: https://blog.darkthread.net/blog/cidr-match/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

識別 IP 所屬網段 (物件導向寫法)-黑暗執行緒

專案上的小需求,公司內網依實體網路架構區分了多個網段,系統有網段清單,已知不同 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");

Fig1_638424866362503338.png

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(...) } 以集合初始設定式指定內容。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK