-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use Unfucked shared libraries after extracting a lot of logic to them.
- Loading branch information
Showing
18 changed files
with
499 additions
and
1,688 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,86 @@ | ||
using GandiDynamicDns.Net.Dns; | ||
using GandiDynamicDns.Net.Stun; | ||
using GandiDynamicDns.Unfucked.Tasks; | ||
using Microsoft.Extensions.Options; | ||
using System.Diagnostics; | ||
using System.Net; | ||
|
||
namespace GandiDynamicDns; | ||
|
||
public interface DynamicDnsService: IDisposable { | ||
|
||
IPAddress? selfWanAddress { get; } | ||
|
||
} | ||
|
||
public class DynamicDnsServiceImpl(DnsManager dns, SelfWanAddressClient stun, IOptions<Configuration> configuration, ILogger<DynamicDnsServiceImpl> logger, IHostApplicationLifetime lifetime) | ||
: BackgroundService, DynamicDnsService { | ||
|
||
private const string DNS_A_RECORD = "A"; | ||
|
||
public IPAddress? selfWanAddress { get; private set; } | ||
|
||
private readonly EventLog? eventLog = | ||
#if WINDOWS | ||
new("Application") { Source = "GandiDynamicDns" }; | ||
#else | ||
null; | ||
#endif | ||
|
||
protected override async Task ExecuteAsync(CancellationToken ct) { | ||
if ((await dns.fetchDnsRecords(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, ct)).FirstOrDefault() is { } existingIpAddress) { | ||
try { | ||
selfWanAddress = IPAddress.Parse(existingIpAddress); | ||
} catch (FormatException) { } | ||
} | ||
logger.LogInformation("On startup, the {fqdn} DNS A record was pointing to {address}", configuration.Value.fqdn, selfWanAddress?.ToString() ?? "(nothing)"); | ||
|
||
while (!ct.IsCancellationRequested) { | ||
await updateDnsRecordIfNecessary(ct); | ||
|
||
if (configuration.Value.updateInterval > TimeSpan.Zero) { | ||
await Task2.Delay(configuration.Value.updateInterval, ct); | ||
} else { | ||
lifetime.StopApplication(); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private async Task updateDnsRecordIfNecessary(CancellationToken ct = default) { | ||
SelfWanAddressResponse stunResponse = await stun.getSelfWanAddress(ct); | ||
if (stunResponse.selfWanAddress != null && !stunResponse.selfWanAddress.Equals(selfWanAddress)) { | ||
logger.LogInformation("This computer's public IP address changed from {old} to {new} according to {server} ({serverAddr}), updating {fqdn} A record in DNS server", selfWanAddress, | ||
stunResponse.selfWanAddress, stunResponse.server.Host, stunResponse.serverAddress.ToString(), configuration.Value.fqdn); | ||
#if WINDOWS | ||
eventLog?.WriteEntry( | ||
$"This computer's public IP address changed from {selfWanAddress} to {stunResponse.selfWanAddress}, according to {stunResponse.server.Host} ({stunResponse.serverAddress}), updating {configuration.Value.fqdn} A record in DNS server", | ||
EventLogEntryType.Information, 1); | ||
#endif | ||
|
||
selfWanAddress = stunResponse.selfWanAddress; | ||
await updateDnsRecord(stunResponse.selfWanAddress, ct); | ||
} else { | ||
logger.LogDebug("Not updating DNS {type} record for {fqdn} because it is already set to {value}", DNS_A_RECORD, configuration.Value.fqdn, selfWanAddress); | ||
} | ||
} | ||
|
||
private async Task updateDnsRecord(IPAddress currentIPAddress, CancellationToken ct = default) { | ||
if (!configuration.Value.dryRun) { | ||
await dns.setDnsRecord(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, configuration.Value.dnsRecordTimeToLive, [currentIPAddress.ToString()], ct); | ||
} | ||
} | ||
|
||
protected virtual void Dispose(bool disposing) { | ||
if (disposing) { | ||
eventLog?.Dispose(); | ||
} | ||
} | ||
|
||
public sealed override void Dispose() { | ||
Dispose(true); | ||
base.Dispose(); | ||
GC.SuppressFinalize(this); | ||
} | ||
|
||
using GandiDynamicDns.Net.Dns; | ||
using Microsoft.Extensions.Options; | ||
using System.Diagnostics; | ||
using System.Net; | ||
using Unfucked; | ||
using Unfucked.STUN; | ||
|
||
namespace GandiDynamicDns; | ||
|
||
public interface DynamicDnsService: IDisposable { | ||
|
||
IPAddress? selfWanAddress { get; } | ||
|
||
} | ||
|
||
public class DynamicDnsServiceImpl(DnsManager dns, ISelfWanAddressClient stun, IOptions<Configuration> configuration, ILogger<DynamicDnsServiceImpl> logger, IHostApplicationLifetime lifetime) | ||
: BackgroundService, DynamicDnsService { | ||
|
||
private const string DNS_A_RECORD = "A"; | ||
|
||
public IPAddress? selfWanAddress { get; private set; } | ||
|
||
private readonly EventLog? eventLog = | ||
#if WINDOWS | ||
new("Application") { Source = "GandiDynamicDns" }; | ||
#else | ||
null; | ||
#endif | ||
|
||
protected override async Task ExecuteAsync(CancellationToken ct) { | ||
if ((await dns.fetchDnsRecords(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, ct)).FirstOrDefault() is { } existingIpAddress) { | ||
try { | ||
selfWanAddress = IPAddress.Parse(existingIpAddress); | ||
} catch (FormatException) { } | ||
} | ||
logger.LogInformation("On startup, the {fqdn} DNS A record was pointing to {address}", configuration.Value.fqdn, selfWanAddress?.ToString() ?? "(nothing)"); | ||
|
||
while (!ct.IsCancellationRequested) { | ||
await updateDnsRecordIfNecessary(ct); | ||
|
||
if (configuration.Value.updateInterval > TimeSpan.Zero) { | ||
await Tasks.Delay(configuration.Value.updateInterval, ct); | ||
} else { | ||
lifetime.StopApplication(); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private async Task updateDnsRecordIfNecessary(CancellationToken ct = default) { | ||
SelfWanAddressResponse stunResponse = await stun.GetSelfWanAddress(ct); | ||
if (stunResponse.SelfWanAddress != null && !stunResponse.SelfWanAddress.Equals(selfWanAddress)) { | ||
logger.LogInformation("This computer's public IP address changed from {old} to {new} according to {server} ({serverAddr}), updating {fqdn} A record in DNS server", selfWanAddress, | ||
stunResponse.SelfWanAddress, stunResponse.Server.Host, stunResponse.ServerAddress.ToString(), configuration.Value.fqdn); | ||
#if WINDOWS | ||
eventLog?.WriteEntry( | ||
$"This computer's public IP address changed from {selfWanAddress} to {stunResponse.SelfWanAddress}, according to {stunResponse.Server.Host} ({stunResponse.ServerAddress}), updating {configuration.Value.fqdn} A record in DNS server", | ||
EventLogEntryType.Information, 1); | ||
#endif | ||
|
||
selfWanAddress = stunResponse.SelfWanAddress; | ||
await updateDnsRecord(stunResponse.SelfWanAddress, ct); | ||
} else { | ||
logger.LogDebug("Not updating DNS {type} record for {fqdn} because it is already set to {value}", DNS_A_RECORD, configuration.Value.fqdn, selfWanAddress); | ||
} | ||
} | ||
|
||
private async Task updateDnsRecord(IPAddress currentIPAddress, CancellationToken ct = default) { | ||
if (!configuration.Value.dryRun) { | ||
await dns.setDnsRecord(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, configuration.Value.dnsRecordTimeToLive, [currentIPAddress.ToString()], ct); | ||
} | ||
} | ||
|
||
protected virtual void Dispose(bool disposing) { | ||
if (disposing) { | ||
eventLog?.Dispose(); | ||
} | ||
} | ||
|
||
public sealed override void Dispose() { | ||
Dispose(true); | ||
base.Dispose(); | ||
GC.SuppressFinalize(this); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,69 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Worker"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFrameworks>net8.0;net8.0-windows</TargetFrameworks> | ||
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm;linux-arm64</RuntimeIdentifiers> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<RollForward>latestMajor</RollForward> | ||
<LangVersion>latest</LangVersion> | ||
<NoWarn>$(NoWarn);8524;VSTHRD200</NoWarn> | ||
<Version>0.2.0</Version> | ||
<Authors>Ben Hutchison</Authors> | ||
<Copyright>© 2024 $(Authors)</Copyright> | ||
<Company>$(Authors)</Company> | ||
<AssemblyTitle>Gandi Dynamic DNS</AssemblyTitle> | ||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> | ||
<ApplicationIcon>favicon.ico</ApplicationIcon> | ||
<ApplicationManifest>app.manifest</ApplicationManifest> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<AdditionalFiles Include="ExceptionAdjustments.txt" /> | ||
<Content Include="favicon.ico" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="G6.GandiLiveDns" Version="1.0.0" /> | ||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" /> | ||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" /> | ||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.0" /> | ||
<PackageReference Include="Stun.Net" Version="8.0.2" /> | ||
<PackageReference Include="System.Runtime.Caching" Version="8.0.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('win'))"> | ||
<None Update="Install service.ps1"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))"> | ||
<None Update="gandidynamicdns.service"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<InternalsVisibleTo Include="Tests" /> | ||
</ItemGroup> | ||
|
||
<Project Sdk="Microsoft.NET.Sdk.Worker"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFrameworks>net8.0;net8.0-windows</TargetFrameworks> | ||
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm;linux-arm64</RuntimeIdentifiers> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<RollForward>latestMajor</RollForward> | ||
<LangVersion>latest</LangVersion> | ||
<NoWarn>$(NoWarn);8524;VSTHRD200</NoWarn> | ||
<Version>0.2.1</Version> | ||
<Authors>Ben Hutchison</Authors> | ||
<Copyright>© 2024 $(Authors)</Copyright> | ||
<Company>$(Authors)</Company> | ||
<AssemblyTitle>Gandi Dynamic DNS</AssemblyTitle> | ||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> | ||
<ApplicationIcon>favicon.ico</ApplicationIcon> | ||
<ApplicationManifest>app.manifest</ApplicationManifest> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<AdditionalFiles Include="ExceptionAdjustments.txt" /> | ||
<Content Include="favicon.ico" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="G6.GandiLiveDns" Version="1.0.0" /> | ||
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" /> | ||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" /> | ||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" /> | ||
<PackageReference Include="Stun.Net" Version="8.0.2" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('win'))"> | ||
<None Update="Install service.ps1"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))"> | ||
<None Update="gandidynamicdns.service"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<InternalsVisibleTo Include="Tests" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Unfucked"> | ||
<HintPath>..\..\Unfucked\Unfucked\bin\Debug\net8.0\Unfucked.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Unfucked.Caching"> | ||
<HintPath>..\..\Unfucked\Caching\bin\Debug\netstandard2.0\Unfucked.Caching.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Unfucked.DI"> | ||
<HintPath>..\..\Unfucked\DI\bin\Debug\net6.0\Unfucked.DI.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Unfucked.DNS"> | ||
<HintPath>..\..\Unfucked\DNS\bin\Debug\net6.0\Unfucked.DNS.dll</HintPath> | ||
</Reference> | ||
<Reference Include="Unfucked.STUN"> | ||
<HintPath>..\..\Unfucked\STUN\bin\Debug\net8.0\Unfucked.STUN.dll</HintPath> | ||
</Reference> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,38 @@ | ||
using GandiDynamicDns.Unfucked.Dns; | ||
|
||
namespace GandiDynamicDns.Net.Dns; | ||
|
||
public interface DnsManager { | ||
|
||
Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default); | ||
|
||
Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default); | ||
|
||
} | ||
|
||
public class GandiDnsManager(IGandiLiveDns gandi): DnsManager { | ||
|
||
public static readonly TimeSpan MINIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(300); // 5 minutes | ||
private static readonly TimeSpan MAXIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(2_592_000); // 30 days | ||
|
||
public async Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default) => | ||
from record in await gandi.GetDomainRecords(domain, ct) | ||
where record.rrset_type == type.ToString() && record.rrset_name == subdomain && record.rrset_values.LongLength != 0 | ||
select record.rrset_values[0]; | ||
|
||
public async Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default) => | ||
await gandi.PutDomainRecord(domain: domain, | ||
name: subdomain, | ||
type: type.ToString(), | ||
values: values.ToArray(), | ||
ttl: (int) (timeToLive > MINIMUM_TIME_TO_LIVE ? timeToLive < MAXIMUM_TIME_TO_LIVE ? timeToLive : MAXIMUM_TIME_TO_LIVE : MINIMUM_TIME_TO_LIVE).TotalSeconds, | ||
cancellationToken: ct); | ||
|
||
using Unfucked.DNS; | ||
|
||
namespace GandiDynamicDns.Net.Dns; | ||
|
||
public interface DnsManager { | ||
|
||
Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default); | ||
|
||
Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default); | ||
|
||
} | ||
|
||
public class GandiDnsManager(IGandiLiveDns gandi): DnsManager { | ||
|
||
/// <summary> | ||
/// 5 minutes | ||
/// </summary> | ||
public static readonly TimeSpan MINIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(300); | ||
|
||
/// <summary> | ||
/// 30 days | ||
/// </summary> | ||
private static readonly TimeSpan MAXIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(2_592_000); | ||
|
||
public async Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default) => | ||
from record in await gandi.GetDomainRecords(domain, ct) | ||
where record.rrset_type == type.ToString() && record.rrset_name == subdomain && record.rrset_values.LongLength != 0 | ||
select record.rrset_values[0]; | ||
|
||
public async Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default) => | ||
await gandi.PutDomainRecord(domain: domain, | ||
name: subdomain, | ||
type: type.ToString(), | ||
values: values.ToArray(), | ||
ttl: (int) (timeToLive > MINIMUM_TIME_TO_LIVE ? timeToLive < MAXIMUM_TIME_TO_LIVE ? timeToLive : MAXIMUM_TIME_TO_LIVE : MINIMUM_TIME_TO_LIVE).TotalSeconds, | ||
cancellationToken: ct); | ||
|
||
} |
Oops, something went wrong.