Skip to content

Commit

Permalink
Added dry run mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Aldaviva committed Jun 19, 2024
1 parent 118e0dd commit f6785a0
Show file tree
Hide file tree
Showing 11 changed files with 37 additions and 66 deletions.
2 changes: 2 additions & 0 deletions GandiDynamicDns.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=GandiDynamicDns_003B_002A_003BGandiDynamicDns_002EUnfucked_002E_002A_003B_002A/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
18 changes: 11 additions & 7 deletions GandiDynamicDns/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";

}
12 changes: 6 additions & 6 deletions GandiDynamicDns/DynamicDnsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class DynamicDnsServiceImpl(DnsManager dns, SelfWanAddressClient stun, IO
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);
Expand All @@ -42,19 +42,19 @@ public class DynamicDnsServiceImpl(DnsManager dns, SelfWanAddressClient stun, IO
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);
}
}

}
2 changes: 1 addition & 1 deletion GandiDynamicDns/GandiDynamicDns.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<RollForward>latestMajor</RollForward>
<LangVersion>latest</LangVersion>
<NoWarn>$(NoWarn);8524;VSTHRD200</NoWarn>
<Version>0.0.1</Version>
<Version>0.1.0</Version>
<Authors>Ben Hutchison</Authors>
<Copyright>© 2024 $(Authors)</Copyright>
<Company>$(Authors)</Company>
Expand Down
4 changes: 2 additions & 2 deletions GandiDynamicDns/Net/Dns/GandiDnsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default) =>
from record in await gandi.GetDomainRecords(domain, ct)
Expand Down
2 changes: 1 addition & 1 deletion GandiDynamicDns/Net/Stun/MultiServerStunClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class MultiServerStunClient(HttpClient http, StunClientFactory stunClient
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;
Expand Down
1 change: 0 additions & 1 deletion GandiDynamicDns/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
.AddSystemd()
.AddWindowsService(WindowsService.configure)
.Configure<Configuration>(appConfig.Configuration)
.PostConfigure<Configuration>(configuration => configuration.fix())
.AddSingleton(_ => new HttpClient(new SocketsHttpHandler {
AllowAutoRedirect = true,
ConnectTimeout = TimeSpan.FromSeconds(10),
Expand Down
16 changes: 3 additions & 13 deletions GandiDynamicDns/Unfucked/Dns/GandiLiveDns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion GandiDynamicDns/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"domain": "example.com",
"subdomain": "www",
"updateInterval": "0:05:00",
"dnsRecordTimeToLive": "0:05:00"
"dnsRecordTimeToLive": "0:05:00",
"dryRun": false
}
17 changes: 9 additions & 8 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:
Expand All @@ -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`<br>`example.co.uk`|The second-level domain name that you registered|
|`subdomain`|`string`|`www`<br>`@`<br>`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`<br>`example.co.uk`|The second-level domain name that you registered, including the TLD.|
|`subdomain`|`string`|`www`<br>`@`<br>`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`.<br>**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`<br>`true`|Set to `false` to run normally, or `true` to avoid changing any DNS records.|

## Execution
- **Manually**: `./GandiDynamicDns`
Expand Down
26 changes: 0 additions & 26 deletions Tests/ConfigurationTest.cs

This file was deleted.

0 comments on commit f6785a0

Please sign in to comment.