diff --git a/examples/live_safe_dns_record/README.md b/examples/live_safe_dns_record/README.md index 2546e9f..8db8848 100644 --- a/examples/live_safe_dns_record/README.md +++ b/examples/live_safe_dns_record/README.md @@ -32,7 +32,7 @@ Edit `terraform.tfvars` and set at least: - `dynu_root_domain` - `test_suffix` (use a unique value per run) -Optional overrides include `test_ipv4`, `test_ipv6`, and `test_cname_target`. +Optional overrides include `test_ipv4`, `test_ipv6`, and `test_cname_target`. Use real routable IPs for `test_ipv4`/`test_ipv6`; Dynu rejects documentation ranges such as `192.0.2.0/24` and `2001:db8::/32`. ## Run diff --git a/examples/live_safe_dns_record/terraform.tfvars.example b/examples/live_safe_dns_record/terraform.tfvars.example index 1158cca..2c75fac 100644 --- a/examples/live_safe_dns_record/terraform.tfvars.example +++ b/examples/live_safe_dns_record/terraform.tfvars.example @@ -8,7 +8,7 @@ dynu_root_domain = "example.com" # Set to a unique value per run so records are clearly disposable. test_suffix = "manual" -# Safe documentation values for live test scenarios. +# Replace with real routable values for your environment (Dynu rejects documentation-only ranges). test_ipv4 = "192.0.2.123" test_ipv6 = "2001:db8::123" test_cname_target = "example.com" diff --git a/internal/provider/resource_dns_record.go b/internal/provider/resource_dns_record.go index b0790ff..b064d38 100644 --- a/internal/provider/resource_dns_record.go +++ b/internal/provider/resource_dns_record.go @@ -478,6 +478,13 @@ func validateDNSRecordContentForTypeWithKnowledge(recordType string, content *st diagnostics.AddError("Invalid DNS record content", fmt.Sprintf("Record type %q requires an IPv6 address, got %q.", normalizedType, trimmedContent)) return false } + if isDocumentationAddress(addr) { + diagnostics.AddError( + "Unsupported documentation IP address", + fmt.Sprintf("Dynu rejects documentation-only address ranges for live DNS records. Replace %q with a real routable address under your control.", trimmedContent), + ) + return false + } return true } @@ -504,6 +511,22 @@ func validateDNSRecordContentForTypeWithKnowledge(recordType string, content *st return true } +func isDocumentationAddress(addr netip.Addr) bool { + if addr.Is4() { + v4 := addr.As4() + return (v4[0] == 192 && v4[1] == 0 && v4[2] == 2) || + (v4[0] == 198 && v4[1] == 51 && v4[2] == 100) || + (v4[0] == 203 && v4[1] == 0 && v4[2] == 113) + } + + if addr.Is6() { + v6 := addr.As16() + return v6[0] == 0x20 && v6[1] == 0x01 && v6[2] == 0x0d && v6[3] == 0xb8 + } + + return false +} + func resolveDynamicIntent(recordType string, content types.String, dynamic types.Bool, diagnostics *diag.Diagnostics) (bool, bool) { normalizedType := strings.ToUpper(strings.TrimSpace(recordType)) contentPtr := stringPointerFromOptionalContent(content) diff --git a/internal/provider/resource_dns_record_integration_test.go b/internal/provider/resource_dns_record_integration_test.go index 7cf7efb..05504c9 100644 --- a/internal/provider/resource_dns_record_integration_test.go +++ b/internal/provider/resource_dns_record_integration_test.go @@ -195,7 +195,7 @@ func TestIntegrationResourceDNSRecordDynamicAStateStableAndTransitionToStatic(t } updatePlan := state - updatePlan.Content = types.StringValue("192.0.2.42") + updatePlan.Content = types.StringValue("1.1.1.1") updatePlan.Dynamic = types.BoolValue(false) plan = tfsdk.Plan{Schema: schemaResp.Schema} @@ -214,7 +214,7 @@ func TestIntegrationResourceDNSRecordDynamicAStateStableAndTransitionToStatic(t if state.Dynamic.ValueBool() { t.Fatalf("expected static A after setting content") } - if state.Content.ValueString() != "192.0.2.42" { + if state.Content.ValueString() != "1.1.1.1" { t.Fatalf("expected static content after update, got %q", state.Content.ValueString()) } } diff --git a/internal/provider/resource_dns_record_test.go b/internal/provider/resource_dns_record_test.go index eb9f702..7fc59c4 100644 --- a/internal/provider/resource_dns_record_test.go +++ b/internal/provider/resource_dns_record_test.go @@ -26,8 +26,8 @@ func TestParseDNSRecordIDInvalid(t *testing.T) { } func TestValidateDNSRecordContentForType(t *testing.T) { - ipv4 := "192.0.2.123" - ipv6 := "2001:db8::123" + ipv4 := "8.8.8.8" + ipv6 := "2606:4700:4700::1111" nonEmpty := "hello" blank := "" @@ -75,7 +75,7 @@ func TestResolveDynamicIntent(t *testing.T) { } diags = diag.Diagnostics{} - dynamic, ok = resolveDynamicIntent("A", types.StringValue("192.0.2.10"), types.BoolNull(), &diags) + dynamic, ok = resolveDynamicIntent("A", types.StringValue("8.8.4.4"), types.BoolNull(), &diags) if !ok || dynamic || diags.HasError() { t.Fatalf("expected static A content to resolve to dynamic=false, got dynamic=%v ok=%v diags=%v", dynamic, ok, diags) }