diff --git a/GandiDynamicDns.sln.DotSettings b/GandiDynamicDns.sln.DotSettings new file mode 100644 index 0000000..5f99287 --- /dev/null +++ b/GandiDynamicDns.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/GandiDynamicDns/Configuration.cs b/GandiDynamicDns/Configuration.cs index ff91b79..33213f6 100644 --- a/GandiDynamicDns/Configuration.cs +++ b/GandiDynamicDns/Configuration.cs @@ -7,17 +7,21 @@ namespace GandiDynamicDns; public record Configuration { + private const string DOMAIN_ROOT = "@"; + + private readonly string _subdomain = DOMAIN_ROOT; + public required string gandiApiKey { get; init; } public required string domain { get; init; } - public string subdomain { get; set; } = string.Empty; public TimeSpan updateInterval { get; init; } = GandiDnsManager.MINIMUM_TIME_TO_LIVE; - public TimeSpan dnsRecordTimeToLive { get; set; } = GandiDnsManager.MINIMUM_TIME_TO_LIVE; + public TimeSpan dnsRecordTimeToLive { get; init; } = GandiDnsManager.MINIMUM_TIME_TO_LIVE; + public bool dryRun { get; init; } - public void fix() { - subdomain = subdomain.TrimEnd('.'); - if (subdomain.Length == 0) { - subdomain = "@"; - } + public string subdomain { + get => _subdomain; + init => _subdomain = value.TrimEnd('.') is { Length: not 0 } s ? s : DOMAIN_ROOT; } + public string fqdn => subdomain == DOMAIN_ROOT ? domain : $"{subdomain}.{domain}"; + } \ No newline at end of file diff --git a/GandiDynamicDns/DynamicDnsService.cs b/GandiDynamicDns/DynamicDnsService.cs index 7689559..609e211 100644 --- a/GandiDynamicDns/DynamicDnsService.cs +++ b/GandiDynamicDns/DynamicDnsService.cs @@ -25,7 +25,7 @@ protected override async Task ExecuteAsync(CancellationToken ct) { selfWanAddress = IPAddress.Parse(existingIpAddress); } catch (FormatException) { } } - logger.LogInformation("On startup, {subdomain}.{domain} was set to {address}", configuration.Value.subdomain, configuration.Value.domain, selfWanAddress?.ToString() ?? "(nothing)"); + 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); @@ -42,19 +42,19 @@ protected override async Task ExecuteAsync(CancellationToken ct) { private async Task updateDnsRecordIfNecessary(CancellationToken ct = default) { IPAddress? newAddress = await stun.getSelfWanAddress(ct); if (newAddress != null && !newAddress.Equals(selfWanAddress)) { - logger.LogInformation("IP address changed from {old} to {new}, updating {subdomain}.{domain} DNS {type} record", selfWanAddress, newAddress, configuration.Value.subdomain, - configuration.Value.domain, DNS_A_RECORD); + logger.LogInformation("IP address changed from {old} to {new}, updating {fqdn} DNS {type} record", selfWanAddress, newAddress, configuration.Value.fqdn, DNS_A_RECORD); selfWanAddress = newAddress; await updateDnsRecord(newAddress, ct); } else { - logger.LogDebug("Not updating DNS {type} record for {subdomain}.{domain} because it is already set to {value}", DNS_A_RECORD, configuration.Value.subdomain, configuration.Value.domain, - selfWanAddress); + 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) { - await dns.setDnsRecord(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, configuration.Value.dnsRecordTimeToLive, [currentIPAddress.ToString()], ct); + if (!configuration.Value.dryRun) { + await dns.setDnsRecord(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, configuration.Value.dnsRecordTimeToLive, [currentIPAddress.ToString()], ct); + } } } \ No newline at end of file diff --git a/GandiDynamicDns/GandiDynamicDns.csproj b/GandiDynamicDns/GandiDynamicDns.csproj index af509ce..8e64dec 100644 --- a/GandiDynamicDns/GandiDynamicDns.csproj +++ b/GandiDynamicDns/GandiDynamicDns.csproj @@ -9,7 +9,7 @@ latestMajor latest $(NoWarn);8524;VSTHRD200 - 0.0.1 + 0.1.0 Ben Hutchison © 2024 $(Authors) $(Authors) diff --git a/GandiDynamicDns/Net/Dns/GandiDnsManager.cs b/GandiDynamicDns/Net/Dns/GandiDnsManager.cs index 37ba657..01f829d 100644 --- a/GandiDynamicDns/Net/Dns/GandiDnsManager.cs +++ b/GandiDynamicDns/Net/Dns/GandiDnsManager.cs @@ -12,8 +12,8 @@ public interface DnsManager { public class GandiDnsManager(IGandiLiveDns gandi): DnsManager { - public static readonly TimeSpan MINIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(300); - private static readonly TimeSpan MAXIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(2_592_000); + 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> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default) => from record in await gandi.GetDomainRecords(domain, ct) diff --git a/GandiDynamicDns/Net/Stun/MultiServerStunClient.cs b/GandiDynamicDns/Net/Stun/MultiServerStunClient.cs index ad07880..f46a750 100644 --- a/GandiDynamicDns/Net/Stun/MultiServerStunClient.cs +++ b/GandiDynamicDns/Net/Stun/MultiServerStunClient.cs @@ -86,7 +86,7 @@ public async ValueTask BindingTestAsync(CancellationToken cancel foreach (IStunClient5389 stun in await getStunClients(cancellationToken)) { using (stun) { Server = stun.Server; - logger.LogDebug("Sending STUN request to {host}", stun.Server.ToString()); + logger.LogDebug("Sending UDP STUN request to {host}", stun.Server.ToString()); State = await stun.BindingTestAsync(cancellationToken); if (isSuccessfulResponse(State)) { break; diff --git a/GandiDynamicDns/Program.cs b/GandiDynamicDns/Program.cs index c747c16..4d485ff 100644 --- a/GandiDynamicDns/Program.cs +++ b/GandiDynamicDns/Program.cs @@ -22,7 +22,6 @@ .AddSystemd() .AddWindowsService(WindowsService.configure) .Configure(appConfig.Configuration) - .PostConfigure(configuration => configuration.fix()) .AddSingleton(_ => new HttpClient(new SocketsHttpHandler { AllowAutoRedirect = true, ConnectTimeout = TimeSpan.FromSeconds(10), diff --git a/GandiDynamicDns/Unfucked/Dns/GandiLiveDns.cs b/GandiDynamicDns/Unfucked/Dns/GandiLiveDns.cs index 1a6cb3f..6f34423 100644 --- a/GandiDynamicDns/Unfucked/Dns/GandiLiveDns.cs +++ b/GandiDynamicDns/Unfucked/Dns/GandiLiveDns.cs @@ -10,7 +10,7 @@ namespace GandiDynamicDns.Unfucked.Dns; [GeneratedCode("G6.GandiLiveDns", "1.0.0")] public class GandiLiveDns: IGandiLiveDns { - #region Custom + #region New private readonly G6.GandiLiveDns.GandiLiveDns gandi; @@ -19,31 +19,21 @@ public class GandiLiveDns: IGandiLiveDns { private GandiLiveDns(G6.GandiLiveDns.GandiLiveDns gandi, bool shouldDisposeHttpClient) { this.gandi = gandi; this.shouldDisposeHttpClient = shouldDisposeHttpClient; - - // MethodInfo prepareRequestMethod = typeof(G6.GandiLiveDns.GandiLiveDns).GetMethod("PrepareRequest", BindingFlags.NonPublic, [typeof(HttpClient), typeof(HttpRequestMessage), typeof(string)])!; - // prepareRequestMethod.GetMethodBody().GetILAsByteArray() } public GandiLiveDns(): this(new G6.GandiLiveDns.GandiLiveDns(), true) { } public GandiLiveDns(string baseUrl, HttpClient httpClient): this(new G6.GandiLiveDns.GandiLiveDns(baseUrl, httpClient), false) { } - protected void PrepareRequest2(HttpClient client, HttpRequestMessage request, string url) { } - - /* - * I added this myself, don't replace it when generating a new version of this file - */ public void Dispose() { if (shouldDisposeHttpClient) { - HttpClient httpClient = (HttpClient) typeof(G6.GandiLiveDns.GandiLiveDns).GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(info => info.FieldType == typeof(HttpClient)) - .GetValue(gandi)!; - httpClient.Dispose(); + ((HttpClient) typeof(G6.GandiLiveDns.GandiLiveDns).GetFields(BindingFlags.NonPublic | BindingFlags.Instance).First(f => f.FieldType == typeof(HttpClient)).GetValue(gandi)!).Dispose(); } } #endregion - #region Third-party + #region Delegated public string BaseUrl { [DebuggerStepThrough] get => gandi.BaseUrl; diff --git a/GandiDynamicDns/appsettings.json b/GandiDynamicDns/appsettings.json index eeea316..547af26 100644 --- a/GandiDynamicDns/appsettings.json +++ b/GandiDynamicDns/appsettings.json @@ -3,5 +3,6 @@ "domain": "example.com", "subdomain": "www", "updateInterval": "0:05:00", - "dnsRecordTimeToLive": "0:05:00" + "dnsRecordTimeToLive": "0:05:00", + "dryRun": false } \ No newline at end of file diff --git a/Readme.md b/Readme.md index 9db09e8..c995439 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/Aldaviva/GandiDynamicDns/dotnet.yml?branch=master&logo=github)](https://github.com/Aldaviva/GandiDynamicDns/actions/workflows/dotnet.yml) [![Testspace](https://img.shields.io/testspace/tests/Aldaviva/Aldaviva:GandiDynamicDns/master?passed_label=passing&failed_label=failing&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4NTkgODYxIj48cGF0aCBkPSJtNTk4IDUxMy05NCA5NCAyOCAyNyA5NC05NC0yOC0yN3pNMzA2IDIyNmwtOTQgOTQgMjggMjggOTQtOTQtMjgtMjh6bS00NiAyODctMjcgMjcgOTQgOTQgMjctMjctOTQtOTR6bTI5My0yODctMjcgMjggOTQgOTQgMjctMjgtOTQtOTR6TTQzMiA4NjFjNDEuMzMgMCA3Ni44My0xNC42NyAxMDYuNS00NFM1ODMgNzUyIDU4MyA3MTBjMC00MS4zMy0xNC44My03Ni44My00NC41LTEwNi41UzQ3My4zMyA1NTkgNDMyIDU1OWMtNDIgMC03Ny42NyAxNC44My0xMDcgNDQuNXMtNDQgNjUuMTctNDQgMTA2LjVjMCA0MiAxNC42NyA3Ny42NyA0NCAxMDdzNjUgNDQgMTA3IDQ0em0wLTU1OWM0MS4zMyAwIDc2LjgzLTE0LjgzIDEwNi41LTQ0LjVTNTgzIDE5Mi4zMyA1ODMgMTUxYzAtNDItMTQuODMtNzcuNjctNDQuNS0xMDdTNDczLjMzIDAgNDMyIDBjLTQyIDAtNzcuNjcgMTQuNjctMTA3IDQ0cy00NCA2NS00NCAxMDdjMCA0MS4zMyAxNC42NyA3Ni44MyA0NCAxMDYuNVMzOTAgMzAyIDQzMiAzMDJ6bTI3NiAyODJjNDIgMCA3Ny42Ny0xNC44MyAxMDctNDQuNXM0NC02NS4xNyA0NC0xMDYuNWMwLTQyLTE0LjY3LTc3LjY3LTQ0LTEwN3MtNjUtNDQtMTA3LTQ0Yy00MS4zMyAwLTc2LjY3IDE0LjY3LTEwNiA0NHMtNDQgNjUtNDQgMTA3YzAgNDEuMzMgMTQuNjcgNzYuODMgNDQgMTA2LjVTNjY2LjY3IDU4NCA3MDggNTg0em0tNTU3IDBjNDIgMCA3Ny42Ny0xNC44MyAxMDctNDQuNXM0NC02NS4xNyA0NC0xMDYuNWMwLTQyLTE0LjY3LTc3LjY3LTQ0LTEwN3MtNjUtNDQtMTA3LTQ0Yy00MS4zMyAwLTc2LjgzIDE0LjY3LTEwNi41IDQ0UzAgMzkxIDAgNDMzYzAgNDEuMzMgMTQuODMgNzYuODMgNDQuNSAxMDYuNVMxMDkuNjcgNTg0IDE1MSA1ODR6IiBmaWxsPSIjZmZmIi8%2BPC9zdmc%2B)](https://aldaviva.testspace.com/spaces/277280) [![Coveralls](https://img.shields.io/coveralls/github/Aldaviva/GandiDynamicDns?logo=coveralls)](https://coveralls.io/github/Aldaviva/GandiDynamicDns?branch=master) -Automatically update a DNS A record in Gandi LiveDNS whenever your computer's public IP address changes, detected automatically using a [large, auto-updating pool](https://github.com/pradt2/always-online-stun) of public [STUN](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#stun) servers. +Automatically update a DNS `A` record in [Gandi](https://www.gandi.net) [LiveDNS](https://www.gandi.net/en-US/domain/dns) whenever your computer's public IP address changes, detected automatically using a [large, auto-updating pool](https://github.com/pradt2/always-online-stun) of public [STUN](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols#stun) servers. This is an alternative to filling out monthly CAPTCHAs for [No-IP](https://www.noip.com) or paying for [DynDNS](https://account.dyn.com), if you happen to already be paying for a domain name from the world's greatest domain registrar. @@ -23,12 +23,12 @@ This is an alternative to filling out monthly CAPTCHAs for [No-IP](https://www.n - ❌ Classic DNS (`*.dns.gandi.net`) is incompatible; you will need to [migrate to LiveDNS](https://docs.gandi.net/en/domain_names/common_operations/changing_nameservers.html#switching-to-livedns) - ❌ External nameservers (with glue records) are incompatible; you will need to update the record on the external nameserver instead of on Gandi's nameservers - Your computer must have a public WAN IPv4 address - - IPv6 is not supported because router NATs don't support IPv6 port forwarding + - IPv6 is not supported because router NATs don't support IPv6 port forwarding, so an `AAAA` record wouldn't be useful ## Installation 1. Download the [latest release](https://github.com/Aldaviva/GandiDynamicDns/releases/latest) ZIP archive for your operating system and CPU architecture 1. Extract the ZIP archive to a directory, such as `C:\Program Files\GandiDynamicDns\` or `/opt/gandidynamicdns/` - - Only extract `appsettings.json` during a new installation, not when upgrading an existing installation + - Extract `appsettings.json` during a new installation, but not when upgrading an existing installation 1. Install the service - Windows: `& '.\Install service.ps1'` - Linux with systemd: @@ -43,11 +43,12 @@ Open `appsettings.json` in a text editor. |Key|Type|Examples|Description| |-|-|-|-| -|`gandiApiKey`|`string`|`abcdefg`|Generate an API key under [Developer access](https://account.gandi.net/en/users/_/security) in your Gandi [Account](https://account.gandi.net/en)| -|`domain`|`string`|`example.com`
`example.co.uk`|The second-level domain name that you registered| -|`subdomain`|`string`|`www`
`@`
`en.www`|The subdomain whose DNS record you want to update, not including `domain` or a trailing period. To update `domain` itself, set `subdomain` to `@`. Can also be a multi-level subdomain like `en.www`.| -|`updateInterval`|`TimeSpan`|`0.00:05:00`|How frequently this program will check if your public IP address has changed and update DNS. Format is `d.hh:mm:ss`.| -|`dnsRecordTimeToLive`|`TimeSpan`|`0.00:05:00`|How long DNS resolvers can cache your record before they must look it up again. Gandi's minimum is 5 minutes.| +|`gandiApiKey`|`string`|`abcdefg`|Generate an API key under [Developer access](https://account.gandi.net/en/users/_/security) in your [Gandi Account](https://account.gandi.net/en). Personal Access Tokens are unfortunately not supported by [G6.GandiLiveDns](https://github.com/gaylord-roger/G6.GandiLiveDns).| +|`domain`|`string`|`example.com`
`example.co.uk`|The second-level domain name that you registered, including the TLD.| +|`subdomain`|`string`|`www`
`@`
`api.stage`|The subdomain whose DNS record you want to update, not including `domain` or a trailing period. To update `domain` itself, set this to `@`. Can also be a multi-level subdomain.| +|`updateInterval`|`TimeSpan`|`0.00:05:00`|How frequently this program will check if your public IP address has changed and update DNS. Format is `d.hh:mm:ss`.
**One-shot mode:** if set to `0:0:0` or negative, this program will exit after the first update attempt, instead of remaining running and updating periodically; useful if you want to trigger it yourself, like with `cron`.| +|`dnsRecordTimeToLive`|`TimeSpan`|`0.00:05:00`|How long DNS resolvers can cache your record before they must look it up again. Gandi requires this to be between 5 minutes and 30 days, inclusive.| +|`dryRun`|`bool`|`false`
`true`|Set to `false` to run normally, or `true` to avoid changing any DNS records.| ## Execution - **Manually**: `./GandiDynamicDns` diff --git a/Tests/ConfigurationTest.cs b/Tests/ConfigurationTest.cs deleted file mode 100644 index cd5ac06..0000000 --- a/Tests/ConfigurationTest.cs +++ /dev/null @@ -1,26 +0,0 @@ -using GandiDynamicDns; - -namespace Tests; - -public class ConfigurationTest { - - [Fact] - public void fix() { - Configuration conf = new() { subdomain = "www.", domain = "example.com", gandiApiKey = "abcdef" }; - conf.fix(); - conf.subdomain.Should().Be("www"); - - conf.subdomain = "."; - conf.fix(); - conf.subdomain.Should().Be("@"); - - conf.subdomain = string.Empty; - conf.fix(); - conf.subdomain.Should().Be("@"); - - conf.subdomain = "www"; - conf.fix(); - conf.subdomain.Should().Be("www"); - } - -} \ No newline at end of file