.NET实现一个监控 IP 的 windows 服务

Intro

我们公司的 VPN 用自己的电脑连公司的台式机的时候需要用 IP 地址,有一次尝试去连的时候发现连不上,第二天到公司发现 IP 变掉了,不是之前连的 IP 了,于是就想写一个简单 Windows 服务来监控台式机的 IP 变化。.

Overview

在 C# 里我们可以使用 Dns.GetHostAddresses() 方法来获取 IP 地址,我们可以每隔一段时间就判断一下当前的 IP 地址,为了方便测试,可以把这个时间定义在配置里,这样本地开发的时候比较方便

为了避免消息太多,我们可以做一个简单的检查,如果 IP 地址不变,就不发消息了,只有当 IP 信息变化的时候再发消息

我们办公使用的是 Google Chat, 所以打算使用 Google Chat 来发消息,也可以根据需要改成自己想用的通知方式

Implement

首先我们可以新建一个 worker 服务,使用 dotnet cli 新建即可

dotnet new worker -n IpMonitor

如果不习惯没有解决方案文件,也可以新建一个解决方案文件并将项目添加到解决方案文件中

cd IpMonitor
dotnet new sln
dotnet sln add ./IpMonitor.csproj

然后我们来改造我们的 WorkerWorker 其实就是一个后台服务,我们的服务比较简单就直接在上面改了

public sealed class Worker : BackgroundService
{
    private readonly TimeSpan _period;
    private readonly INotification _notification;
    private readonly ILogger<Worker> _logger;

    private volatile string _previousIpInfo = string.Empty;

    public Worker(IConfiguration configuration, INotification notification, ILogger<Worker> logger)
    {
        _notification = notification;
        _logger = logger;
        _period = configuration.GetAppSetting<TimeSpan>("MonitorPeriod");
        if (_period <= TimeSpan.Zero)
        {
            _period = TimeSpan.FromMinutes(10);
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(_period);
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                var host = Dns.GetHostName();
                var ips = await Dns.GetHostAddressesAsync(host, stoppingToken);
                var ipInfo = $"{Environment.MachineName} - {host}\n {ips.Order(new IpAddressComparer()).Select(x => x.MapToIPv4().ToString()).StringJoin(", ")}";
                if (_previousIpInfo == ipInfo)
                {
                    _logger.LogDebug("IpInfo not changed");
                    continue;
                }

                _logger.LogInformation("Ip info: {IpInfo}", ipInfo);
                await _notification.SendNotification(ipInfo);
                _previousIpInfo = ipInfo;
            }
            catch (Exception e)
            {
                _logger.LogError(e, "GetIp exception");
            }
        }
    }
}

这里我们使用了 .NET 6 引入的 PeriodicTimer 来实现定时任务,自定义了一个 IpAddressComparer 来对 IP 地址做一个排序,实现如下:

public sealed class IpAddressComparer: IComparer<IPAddress>
{
    public int Compare(IPAddress? x, IPAddress? y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (ReferenceEquals(null, y)) return 1;
        if (ReferenceEquals(null, x)) return -1;

        var bytes1 = x.MapToIPv4().ToString().SplitArray<byte>(new []{ '.' });
        var bytes2 = y.MapToIPv4().ToString().SplitArray<byte>(new []{ '.' });
        for (var i = 0; i < bytes1.Length; i++)
        {
            if (bytes1[i] != bytes2[i])
            {
                return bytes1[i].CompareTo(bytes2[i]);
            }
        }
        
        return 0;
    }
}

通知使用了 Google Chat 的 webhook API,可以自定义一个 Space,添加一个 webhook 即可,添加成功即可获取一个 webhook URL, 发送消息 API 可以参考文档:https://developers.google.com/chat/api/guides/message-formats/basic

.NET实现一个监控 IP 的 windows 服务

.NET实现一个监控 IP 的 windows 服务

实现如下:

public sealed class GoogleChatNotification: INotification
{
    private readonly HttpClient _httpClient;
    private readonly string _webhookUrl;

    public GoogleChatNotification(HttpClient httpClient, IConfiguration configuration)
    {
        _httpClient = httpClient;
        _webhookUrl = Guard.NotNullOrEmpty(configuration.GetAppSetting("GChatWebHookUrl"));
    }
    
    public async Task<bool> SendNotification(string text)
    {
        using var response = await _httpClient.PostAsJsonAsync(_webhookUrl, new { text });
        return response.IsSuccessStatusCode;
    }
}

在 Program 文件中注册我们新加的服务就可以了

然后我们进行一些改造来发布和部署 Windows 服务,可以按照文档的提示将项目发布为单文件,部署我比较喜欢 powershell,写了两个简单的 powershell script 来安装和卸载 Windows 服务

首先我们可以在项目里添加 Microsoft.Extensions.Hosting.WindowsServices 的引用,并添加一些发布属性

<PropertyGroup>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
</PropertyGroup>

在 Program 中注册 windows 服务相关配置

using IpMonitor;

Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
        services.AddSingleton<HttpClient>();
        services.AddSingleton<INotification, GoogleChatNotification>();
    })
#if !DEBUG
    // https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service
    .UseWindowsService(options =>
    {
        options.ServiceName = "IpMonitor";
    })
#endif
    .Build()
    .Run();

安装服务 powershell 脚本:

$serviceName = "IpMonitor"
Write-Output "serviceName: $serviceName"

dotnet publish -c Release -o out
$destDir = Resolve-Path ".\out"
$ipMonitorPath = "$destDir\IpMonitor.exe"

Write-Output "Installing service... $ipMonitorPath $destDir"
New-Service $serviceName -BinaryPathName $ipMonitorPath
Start-Service $serviceName
Write-Output "Service $serviceName started"

卸载服务 powershell 脚本:

$serviceName = "IpMonitor"
Stop-Service $serviceName
Write-Output "Service $serviceName stopped"
Remove-Service $serviceName
Write-Output "Service $serviceName removed"

运行效果如下(脚本运行需要以管理员权限运行):

我们可以使用 Get-Service IpMonitor 来查看服务状态

.NET实现一个监控 IP 的 windows 服务

install

也可以在任务管理器和服务中查看

.NET实现一个监控 IP 的 windows 服务

.NET实现一个监控 IP 的 windows 服务

最后再把我们的服务卸载掉

.NET实现一个监控 IP 的 windows 服务

uninstall

More

发布为 Windows 服务时如果有问题可以通过 event log 来排查,在 event log 里可以看到我们服务的日志

.NET实现一个监控 IP 的 windows 服务