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