C#でIPアドレスからPC名を調べる|DNSの逆引き

C#・.NET

操作ログに「どのPCから操作したか」を残したい

業務システムの操作ログには、「誰が・いつ・何をしたか」に加えて、「どのPCから操作したか」を残しておくと、あとで調べるときに役立ちます。

Webアプリでは、接続してきたPCのIPアドレスは分かります。ただ、IPアドレスだけでは、どの席のPCなのかが分かりにくいです。

操作ログの「192.168.0.15」が、誰のPCなのか分からない

そこで、IPアドレスからPCの名前(コンピューター名)を調べて、IPアドレスと一緒に記録します。IPアドレスから名前を調べることを、DNSの逆引きといいます。

本記事では、C#でIPアドレスからPC名を調べる方法を紹介します。自作の立替精算システムで、操作ログの「端末名」に使っている部品です。

立替精算システムで使ったところ

立替精算システムの操作ログ照会の画面。操作日時・社員・操作・結果・IP・端末名の列がある

操作ログ照会の画面です。IPの列の右に、端末名(PCの名前)を出しています。

私が自作した立替精算システムでは、サーバと同じPCのブラウザから操作したときは、IPアドレスが「::1」や「127.0.0.1」になります。この場合はDNSを引かずに、サーバ自身のコンピューター名を記録しています。社内のほかのPCからの操作は、IPアドレスからDNSで名前を引いています。

考え方

  1. 「::1」「127.0.0.1」(同じPC)なら、このPCの名前を返す
  2. 一度引いた結果があれば、それを返す(1時間覚えておく)
  3. DNSで逆引きする。待つのは短い時間(0.3秒)だけ
  4. 引けなかったら空にする(推測で埋めない)

操作のたびにDNSの応答を待っていると、画面の動きが遅くなります。PCの名前はめったに変わらないので、「覚えておく」「待つ時間を決める」の2つで、画面を遅くしないようにしています。

C#でIPアドレスからPC名を調べるサンプルコード

動作確認は Windows 11・.NET 10 です(.NET 8 以降で動きます)。Visual Studio でコンソールアプリを作り、Program.cs に貼り付けて実行してください。.NET 10 なら、ファイルに保存して dotnet run ファイル名.cs でも実行できます。

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;

// 調べる IP アドレス
var ipList = new List<IPAddress> { IPAddress.IPv6Loopback, IPAddress.Loopback };

// この PC の社内ネットワークの IP アドレス(IPv4)も加える
ipList.AddRange((await Dns.GetHostAddressesAsync(Dns.GetHostName()))
    .Where(x => x.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(x))
    .Take(1));

// 社内に無い IP アドレス(名前は引けない)
ipList.Add(IPAddress.Parse("192.168.250.250"));

var tanmatsu = new TanmatsuNm();
foreach (var ip in ipList)
{
    var sw = Stopwatch.StartNew();
    var nm = await tanmatsu.GetAsync(ip);
    Console.WriteLine($"{ip,-16} → {nm ?? "(引けない)",-20} {sw.ElapsedMilliseconds,4} ミリ秒");
}

// 2 回目は覚えておいた結果を返すので、すぐに終わる
var sw2 = Stopwatch.StartNew();
await tanmatsu.GetAsync(IPAddress.Parse("192.168.250.250"));
Console.WriteLine($"2 回目(192.168.250.250):{sw2.ElapsedMilliseconds} ミリ秒");


/// <summary>IP アドレスから PC の名前(コンピューター名)を引く。</summary>
public sealed class TanmatsuNm
{
    /// <summary>逆引きを待つ時間。これを過ぎたら名前は空にする。</summary>
    public static readonly TimeSpan Machijikan = TimeSpan.FromMilliseconds(300);

    /// <summary>引いた結果を覚えておく時間。</summary>
    public static readonly TimeSpan Oboeru = TimeSpan.FromHours(1);

    private readonly ConcurrentDictionary<string, (string? Nm, DateTime Kigen)> kioku = new();

    public async Task<string?> GetAsync(IPAddress? ip, CancellationToken cancellationToken = default)
    {
        if (ip is null)
        {
            return null;
        }

        // 同じ PC からの接続(::1・127.0.0.1)は、この PC の名前
        if (IPAddress.IsLoopback(ip))
        {
            return Environment.MachineName;
        }

        // 覚えている結果があれば、それを返す
        var key = ip.ToString();
        var ima = DateTime.UtcNow;
        if (kioku.TryGetValue(key, out var oboe) && oboe.Kigen > ima)
        {
            return oboe.Nm;
        }

        string? nm;
        try
        {
            // 待つのは短い時間だけ。逆引きは Windows では途中で止められないので、
            // CancellationToken ではなく WaitAsync で「待つのをやめる」
            var entry = await Dns.GetHostEntryAsync(key).WaitAsync(Machijikan, cancellationToken);

            // 逆引きできないと、IP アドレスがそのまま返ることがある。名前ではないので空にする
            nm = string.IsNullOrWhiteSpace(entry.HostName) || IPAddress.TryParse(entry.HostName, out _)
                ? null
                : entry.HostName;
        }
        catch (Exception)
        {
            // 引けない・待ちすぎた
            nm = null;
        }

        kioku[key] = (nm, ima + Oboeru);
        return nm;
    }
}

実行結果

::1              → SAMPLE-PC               3 ミリ秒
127.0.0.1        → SAMPLE-PC               0 ミリ秒
192.168.0.10     → SAMPLE-PC               4 ミリ秒
192.168.250.250  → (引けない)                306 ミリ秒
2 回目(192.168.250.250):0 ミリ秒

社内に無いIPアドレス(192.168.250.250)は名前が引けず、0.3秒で待つのをやめています。2回目は覚えておいた結果を返すので、待ち時間はありません。

コードの説明

  • IPAddress.IsLoopback で、「::1」「127.0.0.1」かどうかを調べています。同じPCなら Environment.MachineName でコンピューター名を返します。
  • Dns.GetHostEntryAsync(IPアドレスの文字列) で逆引きします。結果の HostName がPCの名前です。
  • 逆引きできないと、HostName にIPアドレスがそのまま入ってくることがあります。名前ではないので、IPAddress.TryParse で読めるものは空にしています。
  • WaitAsync(Machijikan) は、指定した時間を過ぎると TimeoutException を出して待つのをやめます。
  • Webアプリでは同時に何人も操作するので、覚えておく辞書には、複数の処理から同時に使っても壊れない ConcurrentDictionary を使っています。

CancellationTokenでは止まらなかった

待つ時間を決める書き方として、CancellationTokenSource の CancelAfter で取り消す方法もよく見かけます。

using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMilliseconds(300));
var entry = await Dns.GetHostEntryAsync(ip.ToString(), cts.Token);

ところが、この書き方で計ってみると、名前の引けないIPアドレスでは0.3秒で止まらず、約5秒待っていました。Windowsでは、IPアドレスからの逆引きは途中で取り消しが効かないようです。

そこでサンプルコードでは、WaitAsync で「待つのをやめる」形にしています。問い合わせ自体は裏で続きますが、画面の処理は0.3秒で先に進めます。

ASP.NET Coreで使うとき

// Program.cs:覚えた結果を持ち続けるので、1 つを使い回す
builder.Services.AddSingleton<TanmatsuNm>();

// 画面の処理の中:接続してきた PC の IP アドレスから名前を引く
var ip = HttpContext.Connection.RemoteIpAddress;
var tanmatsuNm = await tanmatsu.GetAsync(ip, HttpContext.RequestAborted);

接続してきたPCのIPアドレスは、HttpContext.Connection.RemoteIpAddress で取れます。TanmatsuNm は引いた結果を覚えておくので、AddSingleton で登録して1つを使い回します。

注意点

  • 名前が引けるかどうかは、社内のネットワークの作りしだいです。社内のDNSにPCの名前が登録されていないと、引けないことがあります。
  • VPNやルーターを通った接続では、PCではなくルーターのIPアドレスに見えることがあります。
  • PCの名前は利用者が変えられるので、本人の確認には使えません。あくまで、あとで調べるときの手がかりとして記録してください。

最後に

以上が、C#でIPアドレスからPC名を調べる方法です。

逆引きは Dns.GetHostEntryAsync の1行でできますが、操作ログのように画面の操作のたびに呼ぶ処理では、「待つ時間を決める」「覚えておく」の2つを入れておくと、画面を遅くせずに使えます。

この部品を使っている立替精算システムは、こちらで紹介しています。

Pythonでホスト名とIPアドレスを調べる方法は、こちらで紹介しています。

コメント

タイトルとURLをコピーしました