Skip to content

Commit 71d835c

Browse files
committed
BIND Zone Provider, tests, documentation and task updates
1 parent f47ea13 commit 71d835c

11 files changed

Lines changed: 1272 additions & 20 deletions

File tree

Dns/ZoneProvider/Bind/BindZoneProvider.cs

Lines changed: 854 additions & 18 deletions
Large diffs are not rendered by default.

Dns/ZoneProvider/FileWatcherZoneProvider.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,22 @@ private void Stop()
107107
private void OnTimer(object state)
108108
{
109109
this._timer.Change(Timeout.Infinite, Timeout.Infinite);
110-
Task.Run(() => this.GenerateZone()).ContinueWith(t => this.Notify(t.Result));
110+
Task.Run(() => this.GenerateZone()).ContinueWith(t =>
111+
{
112+
if (t.Status == TaskStatus.RanToCompletion)
113+
{
114+
Zone generatedZone = t.Result;
115+
if (generatedZone != null)
116+
{
117+
this.Notify(generatedZone);
118+
}
119+
}
120+
else if (t.IsFaulted)
121+
{
122+
Exception ex = t.Exception.GetBaseException();
123+
Console.WriteLine("Zone generation failed: {0}", ex.Message);
124+
}
125+
}, TaskScheduler.Default);
111126
}
112127

113128

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,33 @@ The DNS server has a built-in Web Server providing operational insight into the
4848
- counters
4949
- zone information
5050

51+
## Zone Providers
52+
The server ships with several pluggable providers that publish authoritative data into `SmartZoneResolver`:
53+
54+
- **CSV/AP provider** – watches a simple CSV file (`MachineFunction`, `StaticIP`) and publishes grouped A records for each function. See `docs/providers/AP_provider.md` for schema details.
55+
- **IPProbe provider** – continuously probes configured endpoints (ping/noop today) and only emits healthy addresses. Configuration and behavior live in `docs/providers/IPProbe_provider.md`.
56+
- **BIND zone provider** – watches a BIND-style forward zone file, parses `$ORIGIN`, `$TTL`, SOA/NS/A/AAAA/CNAME/MX/TXT records, and emits address records once the zone validates successfully. Any lexical or semantic validation error (missing SOA/NS, malformed TTLs, unsupported record types, duplicate CNAMEs, etc.) is surfaced with line numbers and the previous zone continues serving traffic.
57+
- See `docs/providers/BIND_provider.md` for configuration details, validation rules, and troubleshooting tips.
58+
59+
### BIND Provider Configuration
60+
Add the provider via `appsettings.json` (both `Dns` and `dns-cli` hosts read the same shape):
61+
62+
```json
63+
{
64+
"server": {
65+
"zone": {
66+
"name": ".example.com",
67+
"provider": "Dns.ZoneProvider.Bind.BindZoneProvider"
68+
}
69+
},
70+
"zoneprovider": {
71+
"FileName": "C:/zones/example.com.zone"
72+
}
73+
}
74+
```
75+
76+
The provider reads the file whenever it changes (a 10-second settlement window avoids partial writes), validates the directives/records, and only publishes `A`/`AAAA` data to SmartZoneResolver when the parse succeeds. All other record types are parsed/validated so that zone files failing to meet RFC expectations never poison the active zone.
77+
5178
## Documentation
5279
- [Product requirements](docs/product_requirements.md) describe the current roadmap, observability goals, and .NET maintenance plans.
5380
- [Project priorities & plan](docs/priorities.md) outline the P0/P1/P2 focus areas plus execution notes (DI migration, OpenTelemetry instrumentation).

dnstest/BindZoneProviderTests.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// // //-------------------------------------------------------------------------------------------------
2+
// // // <copyright file="BindZoneProviderTests.cs" company="stephbu">
3+
// // // Copyright (c) Steve Butler. All rights reserved.
4+
// // // </copyright>
5+
// // //-------------------------------------------------------------------------------------------------
6+
7+
namespace DnsTest
8+
{
9+
using System.Collections.Generic;
10+
using System.IO;
11+
using System.Linq;
12+
using System.Net;
13+
using Dns;
14+
using Dns.ZoneProvider.Bind;
15+
using DnsTest.Integration;
16+
using Microsoft.Extensions.Configuration;
17+
using Xunit;
18+
19+
public class BindZoneProviderTests
20+
{
21+
[Fact]
22+
public void GenerateZone_ReturnsZoneRecordsFromBindFile()
23+
{
24+
string zoneFile = Path.Combine(TestProjectPaths.TestDataDirectory, "Bind", "simple.zone");
25+
26+
using var provider = this.CreateProvider(zoneFile);
27+
Zone zone = provider.GenerateZone();
28+
29+
Assert.NotNull(zone);
30+
Assert.Equal(".example.com", zone.Suffix);
31+
Assert.Equal(0u, zone.Serial);
32+
33+
ZoneRecord wwwA = Assert.Single(zone.Where(record => record.Host == "www.example.com" && record.Type == ResourceType.A));
34+
Assert.Equal(IPAddress.Parse("192.0.2.10"), Assert.Single(wwwA.Addresses));
35+
36+
ZoneRecord wwwAaaa = Assert.Single(zone.Where(record => record.Host == "www.example.com" && record.Type == ResourceType.AAAA));
37+
Assert.Equal(IPAddress.Parse("2001:db8::10"), Assert.Single(wwwAaaa.Addresses));
38+
39+
ZoneRecord apex = Assert.Single(zone.Where(record => record.Host == "example.com" && record.Type == ResourceType.A));
40+
Assert.Contains(IPAddress.Parse("192.0.2.20"), apex.Addresses);
41+
42+
ZoneRecord api = Assert.Single(zone.Where(record => record.Host == "api.example.com"));
43+
Assert.Equal(IPAddress.Parse("192.0.2.30"), Assert.Single(api.Addresses));
44+
}
45+
46+
[Fact]
47+
public void GenerateZone_InvalidZoneReturnsNull()
48+
{
49+
string zoneFile = Path.Combine(TestProjectPaths.TestDataDirectory, "Bind", "invalid_missing_ttl.zone");
50+
51+
using var provider = this.CreateProvider(zoneFile);
52+
Zone zone = provider.GenerateZone();
53+
54+
Assert.Null(zone);
55+
}
56+
57+
[Fact]
58+
public void GenerateZone_ReturnsNullWhenCNameConflictsWithAddress()
59+
{
60+
string tempZone = this.WriteTempZoneFile(new[]
61+
{
62+
"$TTL 1h",
63+
"$ORIGIN example.com.",
64+
"@ IN SOA ns1.example.com. hostmaster.example.com. (",
65+
" 2024010101",
66+
" 7200",
67+
" 3600",
68+
" 1209600",
69+
" 3600 )",
70+
"@ IN NS ns1.example.com.",
71+
"www IN CNAME api",
72+
"www IN A 192.0.2.40",
73+
"api IN A 192.0.2.50"
74+
});
75+
76+
try
77+
{
78+
using var provider = this.CreateProvider(tempZone);
79+
Zone zone = provider.GenerateZone();
80+
81+
Assert.Null(zone);
82+
}
83+
finally
84+
{
85+
File.Delete(tempZone);
86+
}
87+
}
88+
89+
private BindZoneProvider CreateProvider(string zoneFile)
90+
{
91+
var config = new ConfigurationBuilder()
92+
.AddInMemoryCollection(new Dictionary<string, string>
93+
{
94+
{ "FileName", zoneFile }
95+
})
96+
.Build();
97+
98+
var provider = new BindZoneProvider();
99+
provider.Initialize(config, ".example.com");
100+
return provider;
101+
}
102+
103+
private string WriteTempZoneFile(IEnumerable<string> lines)
104+
{
105+
string path = Path.GetTempFileName();
106+
File.WriteAllLines(path, lines);
107+
return path;
108+
}
109+
}
110+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
$ORIGIN example.com.
2+
@ IN SOA ns1.example.com. hostmaster.example.com. (
3+
2024010101
4+
7200
5+
3600
6+
1209600
7+
3600 )
8+
@ IN NS ns1.example.com.
9+
www IN A 10.0.0.1

dnstest/TestData/Bind/simple.zone

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
$TTL 1h
2+
$ORIGIN example.com.
3+
@ IN SOA ns1.example.com. hostmaster.example.com. (
4+
2024010101 ; serial
5+
7200 ; refresh
6+
3600 ; retry
7+
1209600 ; expire
8+
3600 ) ; minimum
9+
IN NS ns1.example.com.
10+
IN NS ns2.example.com.
11+
www 600 IN A 192.0.2.10
12+
www 600 IN AAAA 2001:db8::10
13+
@ IN A 192.0.2.20
14+
api IN A 192.0.2.30
15+
alias IN CNAME www

docs/providers/AP_provider.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# CSV/AP Zone Provider
2+
3+
The historical CSV/AP provider (`Dns.ZoneProvider.AP.APZoneProvider`) is the simplest way to preload static IPv4 answers. It watches a CSV file, groups rows by machine function, and emits one `ZoneRecord` per function with every configured address so SmartZoneResolver can round-robin them.
4+
5+
## Configuration
6+
7+
Point the DNS host (or `dns-cli`) at the provider and supply a CSV path via `zoneprovider.FileName`:
8+
9+
```json
10+
{
11+
"server": {
12+
"zone": {
13+
"name": ".example.com",
14+
"provider": "Dns.ZoneProvider.AP.APZoneProvider"
15+
}
16+
},
17+
"zoneprovider": {
18+
"FileName": "C:/zones/machineinfo.csv"
19+
}
20+
}
21+
```
22+
23+
`FileWatcherZoneProvider` handles the reload mechanics: any file change restarts a 10-second settlement timer and the CSV is re-parsed after the timer expires. If parsing succeeds a brand-new zone replaces the previous one atomically.
24+
25+
## CSV Schema
26+
27+
The provider only reads three columns—`MachineFunction`, `StaticIP`, and `MachineName`. All other columns in the CSV are ignored. The parser expects a header declaration in the first non-comment line (mirroring both `Dns/Data/machineinfo.csv` and `dnstest/TestData/Zones/integration_machineinfo.csv`):
28+
29+
```
30+
#Fields:MachineName,MachineFunction,StaticIP
31+
myhost01,www,192.0.2.10
32+
myhost02,www,192.0.2.11
33+
api01,api,192.0.2.20
34+
```
35+
36+
- The hostname served to DNS clients is `<MachineFunction><ZoneName>`, so with the example above and `ZoneName=".example.com"` the provider emits `www.example.com` and `api.example.com` records.
37+
- Duplicate `MachineFunction` values are grouped and all IPv4 addresses are returned to SmartZoneResolver, enabling round-robin responses.
38+
- The parser ignores blank lines and comment lines beginning with `#` or `;`.
39+
40+
## Behavior & Limitations
41+
42+
- Records are always `A`/`IN` entries; IPv6 is not supported.
43+
- No TTL metadata exists in the CSV, so the DNS server continues using its default per-answer TTL (10 seconds today).
44+
- The provider trusts the CSV contents—malformed IP addresses throw at parse time and block publication, logging the exception to the console.
45+
46+
## Samples & Tests
47+
48+
- `Dns/Data/machineinfo.csv` – legacy data used for local experiments.
49+
- `dnstest/TestData/Zones/integration_machineinfo.csv` – trimmed-down fixture consumed by the integration tests. Update this file (and the tests that reference it) if you change the CSV schema.
50+
51+
Run `dotnet test csharp-dns-server.sln` after editing either the provider or its CSV assets to ensure the integration suite still passes.

docs/providers/BIND_provider.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# BIND Zone Provider
2+
3+
The `Dns.ZoneProvider.Bind.BindZoneProvider` watcher ingests a forward zone file written in standard BIND syntax, validates it aggressively, and publishes the resulting address records into `SmartZoneResolver`. This note captures the supported directives, configuration, validation rules, and troubleshooting steps so operators can confidently run static zone files alongside the existing CSV/IPProbe providers.
4+
5+
## Configuration
6+
7+
Add the provider to either the `Dns` or `dns-cli` host configuration. Only the zone name and the provider type change from the default template:
8+
9+
```json
10+
{
11+
"server": {
12+
"zone": {
13+
"name": ".example.com",
14+
"provider": "Dns.ZoneProvider.Bind.BindZoneProvider"
15+
}
16+
},
17+
"zoneprovider": {
18+
"FileName": "C:/zones/example.com.zone"
19+
}
20+
}
21+
```
22+
23+
The provider watches the specified file (after expanding environment variables and resolving to an absolute path). Any file system notification resets a 10-second settlement timer; once the timer expires, the provider re-parses the zone. This protects against partial writes and ensures the resolver only sees complete zones.
24+
25+
## Supported Syntax & Records
26+
27+
- **Directives**: `$ORIGIN`, `$TTL` are honored; `$INCLUDE` currently returns a validation error so you know the file is unsupported.
28+
- **Records**: SOA, NS, A, AAAA, CNAME, MX, and TXT. Additional RR types incur an `unsupported record type` error.
29+
- **Fields**: Owner name, TTL, class, and type tokens are parsed in the same order BIND allows (owner optional when indented; TTL/Class optional before the type). TTLs accept numeric suffixes (`s`, `m`, `h`, `d`, `w`).
30+
- **Comments & multi-line records**: Semicolons outside quoted strings begin a comment. Parentheses join multi-line records, including SOA definitions.
31+
32+
Only `A` and `AAAA` data become `ZoneRecord` entries today—the resolver still emits IPv4 answers exclusively, but caching the IPv6 data keeps us ready for future SmartZoneResolver updates.
33+
34+
## Validation Guarantees
35+
36+
Before replacing the active zone the provider enforces:
37+
38+
1. **Lexical/syntactic**: balanced parentheses, terminated quotes, escaped characters, and valid TTL literals.
39+
2. **Directive integrity**: `$ORIGIN` cannot move records outside the configured zone; missing `$TTL` values cause per-record failures unless the record specifies its own TTL.
40+
3. **SOA/NS requirements**: exactly one SOA at the apex and at least one NS record for the zone root.
41+
4. **Record semantics**:
42+
- A/AAAA addresses must match their IP family; duplicates are suppressed.
43+
- MX preference is a valid `ushort`; target names are canonicalized.
44+
- CNAME exclusivity—once a name is a CNAME it cannot host other record types, and conflicting targets are rejected.
45+
- Owner names must stay within the configured zone.
46+
5. **Zone completeness**: at least one address record must be produced; otherwise the zone is considered unusable.
47+
48+
If any validation fails the generated zone is discarded, the previous zone remains live, and an actionable error (with line number) is written to the console.
49+
50+
## Unsupported Features
51+
52+
The following are explicitly out of scope for this iteration, but the parser surfaces intentional errors so you know why a reload failed:
53+
54+
- `$INCLUDE`, `$GENERATE`, DNSSEC record types, and all RR classes besides `IN`.
55+
- Cross-record dependency checks (e.g., verifying MX targets exist) beyond the per-record rules listed above.
56+
- Serving SOA/NS/MX/CNAME/TXT answers—these records are validated but not yet surfaced in `DnsServer` responses.
57+
58+
## Troubleshooting
59+
60+
1. **Console errors**: the provider logs `BIND zone parse error (<file>:<line>): <message>`; fix the offending line and save the file to trigger a reload.
61+
2. **No reload after saving**: ensure file events fire for the resolved path. For temporary editors that save via rename, keep the file in place so the watcher can see `Created`/`Changed` events.
62+
3. **Zone not updating**: confirm the new zone actually produces at least one address record; otherwise the provider logs “did not produce any address records” and skips publication.
63+
64+
## Testing & Samples
65+
66+
Unit tests live under `dnstest/BindZoneProviderTests.cs`, driving sample zones stored in `dnstest/TestData/Bind/`. To add new regression cases, drop another `.zone` file in that directory and reference it from the tests. Running `dotnet test csharp-dns-server.sln` exercises these fixtures automatically.
67+
68+
For a ready-made example, `dnstest/TestData/Bind/simple.zone` demonstrates the accepted SOA, NS, A, AAAA, and CNAME records with mixed TTL declarations.

docs/providers/IPProbe_provider.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# IPProbe Zone Provider
2+
3+
`Dns.ZoneProvider.IPProbe.IPProbeZoneProvider` continuously probes configured endpoints and only advertises addresses that are currently healthy. It is the preferred choice when you want DNS round-robin coupled with basic liveness detection.
4+
5+
## Configuration
6+
7+
The provider is enabled when `server.zone.provider` points to `Dns.ZoneProvider.IPProbe.IPProbeZoneProvider`. All other settings live under `zoneprovider`:
8+
9+
```json
10+
{
11+
"server": {
12+
"zone": {
13+
"name": ".example.com",
14+
"provider": "Dns.ZoneProvider.IPProbe.IPProbeZoneProvider"
15+
}
16+
},
17+
"zoneprovider": {
18+
"PollingIntervalSeconds": 15,
19+
"Hosts": [
20+
{
21+
"Name": "www",
22+
"Probe": "ping",
23+
"Timeout": 30,
24+
"AvailabilityMode": "all",
25+
"Ip": [
26+
"192.0.2.10",
27+
"192.0.2.11"
28+
]
29+
},
30+
{
31+
"Name": "api",
32+
"Probe": "noop",
33+
"Timeout": 100,
34+
"AvailabilityMode": "first",
35+
"Ip": [
36+
"192.0.2.20",
37+
"192.0.2.21"
38+
]
39+
}
40+
]
41+
}
42+
}
43+
```
44+
45+
### Host settings
46+
47+
- `Name`: the left-most label served to clients. The provider appends the configured zone name, so `"Name": "www"` plus `"zone": ".example.com"` becomes `www.example.com`.
48+
- `Probe`: strategy label. Built-in options are `ping` (ICMP echo), and `noop` (always healthy, helpful for lab testing). Unknown values fall back to `noop`.
49+
- `Timeout`: milliseconds passed to the strategy implementation.
50+
- `AvailabilityMode`:
51+
- `all` – advertise every healthy IP.
52+
- `first` – advertise only the first healthy IP (useful when you want to fail over to a single target).
53+
- `Ip`: list of IPv4 or IPv6 addresses. Each entry is monitored independently but deduplicated if multiple hosts reference the same target.
54+
55+
`PollingIntervalSeconds` controls how long the provider sleeps between probe batches. Each batch records status, updates the rolling window, emits a new zone (if the provider is still running), and then waits out the remaining interval.
56+
57+
## Health Evaluation
58+
59+
- Every `Target` keeps a ring buffer of up to 10 recent `ProbeResult` entries.
60+
- `Target.IsAvailable` returns true only if the last three results were successful. This smooths out occasional probe failures.
61+
- When a probe function throws (e.g., ping exceptions) the provider treats the result as unavailable for the cycle.
62+
- Hosts marked `AvailabilityMode.First` return the first healthy address in ascending order from the configuration list; otherwise all healthy addresses are used. SmartZoneResolver still applies its own round-robin logic to the resulting ZoneRecord.
63+
64+
## Behavior & Limitations
65+
66+
- Records are emitted as `A` records today; the provider accepts IPv6 addresses but the resolver currently serves only IPv4 responses.
67+
- There is no persistent storage—restarts lose probe history, so it may take a few cycles before `IsAvailable` returns true.
68+
- Probe strategies run in parallel (up to four at a time). Ensure your environment allows outbound ICMP if you rely on `ping`.
69+
70+
## Observability
71+
72+
The provider logs probe loop start/end plus any exception raised during probing or zone publication. Future instrumentation (see docs/product_requirements.md §4) will hang metrics off this loop.
73+
74+
## Tests & Assets
75+
76+
- `Dns/appsettings.json` ships with an example IPProbe configuration you can tweak for local smoke tests.
77+
- `dnstest/Integration` wiring spins up `dns-cli` with probe data; add or update those assets whenever you change the provider surface.
78+
79+
Always run `dotnet test csharp-dns-server.sln` before submitting changes so the integration harness exercises your updates end-to-end.

0 commit comments

Comments
 (0)