From e5d0ec63ae3533876c6dd48c78df41659a6da7a7 Mon Sep 17 00:00:00 2001 From: Babloo Singh Date: Tue, 5 Mar 2024 09:49:33 +0530 Subject: [PATCH 01/31] HttpListener start listening before GetContextAsync --- Dns/HttpServer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dns/HttpServer.cs b/Dns/HttpServer.cs index 17b2555..f4d32c6 100644 --- a/Dns/HttpServer.cs +++ b/Dns/HttpServer.cs @@ -48,6 +48,8 @@ public void Initialize(params string[] prefixes) /// Start listening public async void Start(CancellationToken ct) { + _listener.Start(); + _running = true; ct.Register(this.Stop); while (true) @@ -79,7 +81,7 @@ public async void Start(CancellationToken ct) /// Stop listening public void Stop() { - if (_running == true) + if (_running) { _running = false; } @@ -134,7 +136,7 @@ private void ProcessRequest(HttpListenerContext context) /// Process health probe request private void HealthProbe(HttpListenerContext context) { - + if (this.OnHealthProbe != null) { this.OnHealthProbe(context); From 63a26b06267f215c9f93ca14f2b797606aa5d9b3 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 08:31:03 -0800 Subject: [PATCH 02/31] Documentation updates, prep for .NET SDK 8 --- .editorconfig | 525 +++++++++++++++++++++++++++++++++++ AGENTS.md | 83 ++++++ README.md | 9 +- docs/priorities.md | 17 ++ docs/product_requirements.md | 95 +++++++ docs/references.md | 22 ++ exclusion.dic | 1 + 7 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 AGENTS.md create mode 100644 docs/priorities.md create mode 100644 docs/product_requirements.md create mode 100644 docs/references.md create mode 100644 exclusion.dic diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b463fef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,525 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +spelling_exclusion_path = ./exclusion.dic + +[*.json] +indent_size = 2 + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,*/obj/*/External/**/*,*/obj/dotnet-new.IntegrationTests/*/TemplatePackagesPaths.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion + +# Code quality +dotnet_style_readonly_field = true:suggestion +dotnet_code_quality_unused_parameters = non_public:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Default analyzed API surface = 'all' (public APIs + non-public APIs) +dotnet_code_quality.api_surface = all + +# License header +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +# Code files +[*.{cs,vb}] +# Analyzers +dotnet_code_quality.ca1802.api_surface = private, internal +dotnet_code_quality.ca1822.api_surface = private, internal +dotnet_code_quality.ca2208.api_surface = public +# Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = warning +# Properties should not be write only +dotnet_diagnostic.CA1044.severity = warning +# Do not declare protected member in sealed type +dotnet_diagnostic.CA1047.severity = warning +# Declare types in namespaces +dotnet_diagnostic.CA1050.severity = warning +# Avoid using cref tags with a prefix +dotnet_diagnostic.CA1200.severity = suggestion +# P/Invokes should not be visible +dotnet_diagnostic.CA1401.severity = warning +# Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = suggestion +# Remove empty Finalizers +dotnet_diagnostic.CA1821.severity = warning +# Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_diagnostic.CA1824.severity = warning +# Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +dotnet_diagnostic.CA1828.severity = warning +# Prefer strongly-typed Append and Insert method overloads on StringBuilder. +dotnet_diagnostic.CA1830.severity = warning +# Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1832.severity = warning +# Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1833.severity = warning +# Prefer IsEmpty over Count +dotnet_diagnostic.CA1836.severity = warning +# Use 'Environment.ProcessPath' +dotnet_diagnostic.CA1839.severity = warning +# Do not call ToImmutableCollection on an ImmutableCollection value +# Temporarily disable to avoid regression in preview 1, revert back to warning when start using preview 2 +dotnet_diagnostic.CA2009.severity = none +# Avoid infinite recursion +dotnet_diagnostic.CA2011.severity = warning +# Initialize value type static fields inline +dotnet_diagnostic.CA2207.severity = warning +# Implement serialization constructors +dotnet_diagnostic.CA2229.severity = warning +# Provide correct arguments to formatting methods +dotnet_diagnostic.CA2241.severity = warning +# Test for NaN correctly +dotnet_diagnostic.CA2242.severity = warning +# Do not assign a property to itself. +dotnet_diagnostic.CA2245.severity = warning +# Provide correct 'enum' argument to 'Enum.HasFlag' +dotnet_diagnostic.CA2248.severity = warning +# Do Not Add Schema By URL +dotnet_diagnostic.CA3061.severity = warning +# Insecure DTD processing in XML +dotnet_diagnostic.CA3075.severity = warning +# Insecure XSLT script processing. +dotnet_diagnostic.CA3076.severity = warning +# Insecure Processing in API Design, XmlDocument and XmlTextReader +dotnet_diagnostic.CA3077.severity = warning +# Mark Verb Handlers With Validate Antiforgery Token +dotnet_diagnostic.CA3147.severity = warning +# Do Not Use Broken Cryptographic Algorithms +dotnet_diagnostic.CA5351.severity = warning +# Do Not Disable Certificate Validation +dotnet_diagnostic.CA5359.severity = warning +# Do Not Call Dangerous Methods In Deserialization +dotnet_diagnostic.CA5360.severity = warning +# Do Not Disable SChannel Use of Strong Crypto +dotnet_diagnostic.CA5361.severity = warning +# Do Not Disable Request Validation +dotnet_diagnostic.CA5363.severity = warning +# Do Not Use Deprecated Security Protocols +dotnet_diagnostic.CA5364.severity = warning +# Do Not Disable HTTP Header Checking +dotnet_diagnostic.CA5365.severity = warning +# Set ViewStateUserKey For Classes Derived From Page +dotnet_diagnostic.CA5368.severity = warning +# Use XmlReader For Validating Reader +dotnet_diagnostic.CA5370.severity = warning +# Do not use obsolete key derivation function +dotnet_diagnostic.CA5373.severity = warning +# Do Not Use XslTransform +dotnet_diagnostic.CA5374.severity = warning +# Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5376.severity = warning +# Use Container Level Access Policy +dotnet_diagnostic.CA5377.severity = warning +# Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5378.severity = warning +# Do Not Use Weak Key Derivation Function Algorithm +dotnet_diagnostic.CA5379.severity = warning +# Do Not Add Certificates To Root Store +dotnet_diagnostic.CA5380.severity = warning +# Ensure Certificates Are Not Added To Root Store +dotnet_diagnostic.CA5381.severity = warning +# Do Not Use Digital Signature Algorithm (DSA) +dotnet_diagnostic.CA5384.severity = warning +# Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size +dotnet_diagnostic.CA5385.severity = warning +dotnet_diagnostic.CS1591.severity = suggestion +# UseIsNullCheck +dotnet_diagnostic.IDE0041.severity = warning +# ValidateFormatString +dotnet_diagnostic.IDE0043.severity = warning +# MakeLocalFunctionStatic +dotnet_diagnostic.IDE0062.severity = warning +# ConvertTypeOfToNameOf +dotnet_diagnostic.IDE0082.severity = warning +# Remove unnecessary lambda expression +dotnet_diagnostic.IDE0200.severity = none +# Remove redundant nullable directive +dotnet_diagnostic.IDE0240.severity = warning + +# Additional rules for template engine source code + +# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.SpacingRules' +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = none + +[{src,test}/**{Microsoft.TemplateEngine.*,dotnet-new?*}/**.cs] +# Default analyzed API surface = 'public' (public APIs) +dotnet_code_quality.api_surface = public +# Provide ObsoleteAttribute message +dotnet_diagnostic.CA1041.severity = warning +# Static holder types should be Static or NotInheritable +dotnet_diagnostic.CA1052.severity = warning +# Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = warning +# Use literals where appropriate +dotnet_diagnostic.CA1802.severity = warning +# Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = warning +# Initialize reference type static fields inline +dotnet_diagnostic.CA1810.severity = warning +# Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning +# Avoid zero-length array allocations. +dotnet_diagnostic.CA1825.severity = warning +# Do not use Enumerable methods on indexable collections. Instead use the collection directly +dotnet_diagnostic.CA1826.severity = warning +# Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = warning +# Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = warning +# Consider using 'StringBuilder.Append(char)' when applicable. +dotnet_diagnostic.CA1834.severity = warning +# Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_diagnostic.CA1835.severity = warning +# Use 'Environment.ProcessId' +dotnet_diagnostic.CA1837.severity = warning +# Avoid 'StringBuilder' parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = warning +# Use 'Environment.CurrentManagedThreadId' +dotnet_diagnostic.CA1840.severity = warning +# Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = none +# Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = warning +# Use ValueTasks correctly +dotnet_diagnostic.CA2012.severity = warning +# Forward the 'CancellationToken' parameter to methods that take one +dotnet_diagnostic.CA2016.severity = warning +# Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = warning +# Consider using 'string.Contains' instead of 'string.IndexOf' +dotnet_diagnostic.CA2249.severity = warning +# Do Not Use Weak Cryptographic Algorithms +dotnet_diagnostic.CA5350.severity = warning +# Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = warning +# Remove unnecessary using directives +dotnet_diagnostic.IDE0005.severity = warning +# Fix formating +dotnet_diagnostic.IDE0055.severity = warning +# FileHeaderMismatch +dotnet_diagnostic.IDE0073.severity = warning +# Single line comment should begin with a space +dotnet_diagnostic.SA1005.severity = none +# Opening parenthesis should not be preceded by a space +dotnet_diagnostic.SA1008.severity = none +# Closing parenthesis should not be followed by a space +dotnet_diagnostic.SA1009.severity = none +# Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none +# Block statements should not contain embedded comments +dotnet_diagnostic.SA1108.severity = none +# Closing parenthesis should be on line of last parameter +dotnet_diagnostic.SA1111.severity = none +# Parameter should not span multiple lines +dotnet_diagnostic.SA1118.severity = none +# Statement should not use unnecessary parenthesis +dotnet_diagnostic.SA1119.severity = none +# Comments should contain text +dotnet_diagnostic.SA1120.severity = none +# Use string.Empty for empty strings +dotnet_diagnostic.SA1122.severity = none +# Region should not be located within a code element +dotnet_diagnostic.SA1123.severity = none +# Do not use regions +dotnet_diagnostic.SA1124.severity = none +# Generic type constraints should be on their own line +dotnet_diagnostic.SA1127.severity = none +# Put constructor initializers on their own line +dotnet_diagnostic.SA1128.severity = none +# Constant values should appear on the right-hand side of comparisons +dotnet_diagnostic.SA1131.severity = none +# Elements should have the same indentation +dotnet_diagnostic.SA1137.severity = none +# Use literal suffix notation instead of casting +dotnet_diagnostic.SA1139.severity = none +# Use tuple syntax +dotnet_diagnostic.SA1141.severity = warning +# Refer to tuple elements by name +dotnet_diagnostic.SA1142.severity = warning +# Using directive should appear within a namespace declaration +dotnet_diagnostic.SA1200.severity = none +# Field names should not begin with underscore +dotnet_diagnostic.SA1309.severity = none +# Type parameter names should begin with T +dotnet_diagnostic.SA1314.severity = none +# Tuple element names should use correct casing +dotnet_diagnostic.SA1316.severity = warning +# File may only contain a single type +dotnet_diagnostic.SA1402.severity = none +# Debug.Assert should provide message text +dotnet_diagnostic.SA1405.severity = none +# Arithmetic expressions should declare precedence +dotnet_diagnostic.SA1407.severity = none +# Conditional expressions should declare precedence +dotnet_diagnostic.SA1408.severity = none +# Use trailing comma in multi-line initializers +dotnet_diagnostic.SA1413.severity = none +# Tuple types in signatures should have element names +dotnet_diagnostic.SA1414.severity = none +# Statement should not be on a single line +dotnet_diagnostic.SA1501.severity = none +# Element should not be on a single line +dotnet_diagnostic.SA1502.severity = none +# All accessors should be single-line or multi-line +dotnet_diagnostic.SA1504.severity = none +# A closing brace should not be preceded by a blank line +dotnet_diagnostic.SA1508.severity = none +# Opening braces should not be preceded by blank line +dotnet_diagnostic.SA1509.severity = none +# Single-line comments should not be followed by blank line +dotnet_diagnostic.SA1512.severity = none +# Closing brace should be followed by blank line +dotnet_diagnostic.SA1513.severity = none +# Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1515.severity = none +# Code should not contain blank lines at the end of the file +dotnet_diagnostic.SA1518.severity = none +# Elements should be documented +dotnet_diagnostic.SA1600.severity = none +# Partial elements should be documented +dotnet_diagnostic.SA1601.severity = none +# Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = none +# The documentation for parameter 'message' is missing +dotnet_diagnostic.SA1611.severity = none +# Element parameter documentation should have text +dotnet_diagnostic.SA1614.severity = none +# Element return value should be documented +dotnet_diagnostic.SA1615.severity = none +# Element return value documentation should have text +dotnet_diagnostic.SA1616.severity = none +# The documentation for type parameter is missing +dotnet_diagnostic.SA1618.severity = none +# Generic type parameter documentation should have text +dotnet_diagnostic.SA1622.severity = none +# Property documentation text +dotnet_diagnostic.SA1623.severity = none +# Element documentation should not be copied and pasted +dotnet_diagnostic.SA1625.severity = none +# The documentation text within the \'exception\' tag should not be empty +dotnet_diagnostic.SA1627.severity = none +# File should have header +dotnet_diagnostic.SA1633.severity = none +# Constructor summary documentation should begin with standard text +dotnet_diagnostic.SA1642.severity = none +# File name should match first type name +dotnet_diagnostic.SA1649.severity = none + +# Disable some StyleCop rules for test common Program.cs that is linked to test project of template engine +[test/Common/Program.cs] +# Declare types in namespaces +dotnet_diagnostic.CA1050.severity = none +# Elements should be documented +dotnet_diagnostic.SA1600.severity = none +# Partial elements should be documented +dotnet_diagnostic.SA1601.severity = none +# File should have header: The file header XML is invalid. +dotnet_diagnostic.SA1633.severity = none + +# Additional rules for test source code for template engine +[test/{Microsoft.TemplateEngine.*,dotnet-new.IntegrationTests}/**.cs] +# Test methods should not be skipped +dotnet_diagnostic.xUnit1004.severity = warning +# Elements should appear in the correct order +dotnet_diagnostic.SA1201.severity = none +# Elements should be ordered by access +dotnet_diagnostic.SA1202.severity = none +# Static elements should appear before instance elements +dotnet_diagnostic.SA1204.severity = none + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# WiX files +[*.{wixproj,wxs,wxi,wxl,thm}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8-bom + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd, bat}] +end_of_line = crlf + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +[*.txt] +insert_final_newline = false + +# Verify settings +[*.{received,verified}.{txt,xml,json,sh,zsh,nu,fish,ps1}] +charset = "utf-8-bom" +end_of_line = lf +indent_size = unset +indent_style = unset +insert_final_newline = false +tab_width = unset +trim_trailing_whitespace = false \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..abfdb30 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,83 @@ +# AGENTS GUIDE + +> Scope: assistants may edit **code, tests, and documentation** in this repository. Infrastructure/deployment assets remain off-limits unless explicitly approved. + +## 1. Mission +- Maintain and extend the `csharp-dns-server` so it becomes a production-ready DNS service with rich testing, observability, and zone-provider capabilities. +- Follow the roadmap in `docs/product_requirements.md`, respect the priority tiers in `docs/priorities.md` (P0 reliability/protocol accuracy, P1 security & maintenance, P2 features), and focus on open GitHub issues aligned with those tiers. + +## 2. Repository Orientation +| Project | Purpose | Notes | +|---------|---------|-------| +| `Dns/` | Core library implementing DNS protocol, server loop, zone providers, HTTP status surface. | Entry point: `Dns/Program.cs`. Zone providers under `Dns/ZoneProvider`. | +| `dns-cli/` | Console host that runs the DNS server for local testing. | Mirrors `Dns/appsettings.json`. | +| `dnstest/` | xUnit suite covering protocol/utility components. | Expand here before adding new test assemblies. | +| `docs/` | Specs, PRD and future design docs. | `docs/product_requirements.md` drives priorities. | + +Key classes & files: +- `Dns/Program.cs`: wiring for DI/config/servers. +- `Dns/DnsServer.cs`: UDP DNS loop and upstream forwarding. +- `Dns/SmartZoneResolver.cs`: in-memory zone cache & round-robin dispenser. +- `Dns/HttpServer.cs`: embedded status/diagnostics surface. +- `Dns/ZoneProvider/**`: implementations (CSV, IP probes, BIND placeholder). + +## 3. Getting Started +```bash +# restore & build +dotnet build csharp-dns-server.sln + +# run tests +dotnet test csharp-dns-server.sln + +# run server (localhost) +cd dns-cli +dotnet run -- ./appsettings.json +``` +Gotchas: +- UDP port 53 may be taken by Docker/ICS on Windows; change listener port in `appsettings.json`. +- `Dns/appsettings.json` is copied to output; edit with care when adding samples. +- Zone providers may depend on local files (CSV) or ping-able IPs—mock or isolate tests accordingly. + +## 4. Coding Standards +- C# 8 / .NET 3.1 currently, migrating to .NET 8 (see PRD). Prefer idiomatic C# and existing project style. +- Keep ASCII unless file already uses Unicode. +- Windows (/r/n) line delimiters +- Prefer spaces not tabs +- Add comments only where logic is non-obvious. +- ```dotnet format``` all code before submission +- MIT license headers already present — preserve them. + +## 5. Allowed / Disallowed Work +- ✅ Modify C# source, tests, sample configs, docs within `docs/` and root (`AGENTS.md`, README). +- ✅ Add new tests or scripts that live in-repo (delete temporary tooling before submitting). +- 🚫 Do **not** edit deployment/infrastructure assets (Dockerfiles, systemd service files, external config stores) unless explicitly authorized by a maintainer. +- 🚫 No secret management or external network calls without approval. + +## 6. Workflow +1. **Plan**: understand issue context (link to PRD sections). If multiple files touched, outline steps before coding. +2. **Implement**: keep changes scoped; ensure zone providers/tests stay deterministic. +3. **Validate**: run `dotnet build` and relevant `dotnet test` subsets. Document skipped tests or environment assumptions. +4. **Document**: update `docs/` where appropriate. Update README when adding features, config switches and any other project-wide relevant information. +5. **Submit Pull Request**: run `dotnet format`. Follow the contribution workflow in README (squash commits, include rationale). + +## 7. Testing Expectations +- Minimum: `dotnet test csharp-dns-server.sln`. +- For networking changes, add/extend unit tests in `dnstest` or new integration fixtures. +- Capture repro cases for fixed bugs (#26 compressed pointers, #11 BitPacker write) and ensure tests fail before fixes. + +## 8. Observability & Diagnostics +- Prefer structured logging (use `Console.WriteLine` only as placeholder). +- When adding metrics or tracing, integrate with future Prometheus/OTel plan (see PRD §4). + +## 9. Communication & Review +- DO STOP and ask questions if there is missing, ambiguous, or inconsistent information. +- Document assumptions and remaining risks in PR descriptions. +- If blocked by environmental constraints (e.g., network access), leave instructions for a maintainer. +- Keep PRs focused; split unrelated fixes. + +## 10. Safety & Guardrails +- Treat `appsettings.json` samples as templates—do not embed secrets. +- Respect the agent scope limit: no infrastructure edits. +- When unsure, create an issue/comment instead of guessing. + +Thank you for helping build a reliable C# DNS server! diff --git a/README.md b/README.md index 4c916cf..ab5198f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ The DNS server has a built-in Web Server providing operational insight into the - counters - zone information +## Documentation +- [Product requirements](docs/product_requirements.md) describe the current roadmap, observability goals, and .NET maintenance plans. +- [Project priorities](docs/priorities.md) outline the P0/P1/P2 focus areas (reliability, security/maintenance, features). +- [Protocol references](docs/references.md) list the RFCs and supporting standards that guide implementation. +- [AGENTS guide](AGENTS.md) explains how automation/AI contributors should work within this repository. + ## Interesting Possible Uses Time-based constraints such as parental controls to block a site, e.g. Facebook. Logging of site usage e.g. company notifications @@ -71,6 +77,7 @@ Suggested workflow for PRs is 4. Squash your commits into a single change [(Find out how to squash here)](http://stackoverflow.com/questions/616556/how-do-you-squash-commits-into-one-patch-with-git-format-patch) 5. Submit a PR, and put in comments anything that you think I'll need to help merge and evaluate the changes +If you are using automated tooling or AI agents, please review [AGENTS.md](AGENTS.md) to ensure you follow the approved scope and workflow. + ### Licence Reminder All contributions must be licenced under the same MIT terms, do include a header file to that effect. - diff --git a/docs/priorities.md b/docs/priorities.md new file mode 100644 index 0000000..4116230 --- /dev/null +++ b/docs/priorities.md @@ -0,0 +1,17 @@ +# Project Priorities + +## P0 – Security & Maintenance +- Upgrade runtime/dependencies (target .NET 8), monitor CVEs, and enforce regular patch cadence. +- Implement observability guardrails (metrics, logging, tracing) to detect anomalies quickly. +- Plan authentication/authorization for admin surfaces (HTTP endpoints) and adopt secure defaults (TLS, restricted ports). +- Document operational runbooks and incident response procedures. + +## P1 – Reliability & Protocol Accuracy +- Ensure the DNS server produces RFC-compliant responses (1034/1035, 2181, 2308, etc.) and handles compressed pointers, caching semantics, and upstream delegation correctly. +- Maintain deterministic behavior under load: thread safety in `DnsServer`, zone reload consistency, and fault tolerance for zone-provider errors. +- Expand automated tests (unit + integration) to catch regressions in parsing, message serialization, and health probes. Failing tests block merges. + +## P2 – Feature Growth +- Deliver new zone providers (BIND, dynamic sources), parental-control/time-based policies, MAC-scoped records, and richer health-probe strategies. +- Enhance management surfaces (API/UI) and AI-assist documentation (AGENTS.md) to streamline contributions. +- Iterate on HTTP dashboards/metrics export per roadmap once P0/P1 goals are satisfied. diff --git a/docs/product_requirements.md b/docs/product_requirements.md new file mode 100644 index 0000000..e14682c --- /dev/null +++ b/docs/product_requirements.md @@ -0,0 +1,95 @@ +# csharp-dns-server – Product Requirements + +## 1. Overview +- **Purpose**: capture the feature, testing, and operational requirements needed to evolve the C# DNS server into a production-ready, multi-platform service and seed long-term maintenance/AI-assisted development. +- **Current State**: single `.NET Core 3.1` solution (`Dns`, `dns-cli`, `dnstest`) providing an authoritative UDP DNS server with pluggable zone providers (CSV file, IP-health probes) and a minimal HTML status endpoint. No production deployments exist yet. +- **Primary Goals** + - Ship a reliable DNS service with extensible zone data sources, configurable health probes, and first-class observability. + - Establish a comprehensive automated testing strategy. + - Plan the .NET runtime upgrade path (targeting .NET 8 LTS) across Windows/Linux targets. + - Enable AI-accelerated development via clear contributor guidelines (`AGENTS.md`). + +## 2. Functional Requirements +### 2.1 DNS Resolution & Protocol Support +- Maintain authoritative responses for configured zones with round-robin rotation and delegation to upstream DNS when needed. +- Expand record coverage beyond A/PTR: + - Support AAAA, CNAME, and MX records emitted by zone providers. + - Provide an extension point for future record types (SRV, TXT). +- Implement RFC-compliant caching with configurable TTL respect (`Issue #15`). +- Add DNSSEC parsing/response stubs with a roadmap to full signing/validation (`Issue #2`). +- Fix compressed-pointer parsing defects and add regression tests (`Issue #26`). + +### 2.2 Zone Providers & Configuration +- **BIND Zone Provider**: implement forward-zone parsing, $ORIGIN/$TTL handling, and change detection to replace the current `NotImplementedException`. +- **Dynamic Configuration**: + - Support multiple configuration providers (file watcher, REST, database) with a standard schema (`Issues #19, #9, #8`). + - Enable hot reload with validation, rollback, and observable serial increments. +- **Parental Control/Time-Based Rules**: ingest blocklists or schedules from configuration or services (`Issues #3, #9`). +- **MAC Scoped Records**: extend zone syntax to emit responses based on client identity when available (`Issue #4`). +- **Health-Probe Enhancements**: + - IPProbe provider must support HTTP/TCP/Synthetic probes, retries, and weighted routing. + - Record probe latency/availability for monitoring. + +## 3. Testing & Quality Requirements +- **Unit Tests**: expand beyond bit packing and protocol parsing to cover zone provider logic, DNS caching, HTTP handlers, and SmartZoneResolver behaviors. +- **Integration Tests**: + - Spin up the DNS server with test zone providers and assert full query/response flows. + - Simulate upstream delegation and caching behavior. + - Exercise IPProbe pathways with mocked probes/timeouts. +- **Regression Suites**: include fixtures for the compressed-pointer bug (#26) and BitPacker.Write implementation (#11). +- **Performance/Load**: define baseline throughput/latency targets and create repeatable load tests. +- **CI Gates**: `dotnet build` + `dotnet test` required on every PR, with optional fuzz testing for DNS message parsing. + +## 4. Observability & Monitoring +- Replace HTML dumps with structured JSON and/or metrics endpoints (Prometheus/OpenTelemetry). +- Track DNS/HTTP request counts, latencies, cache hit/miss rates, probe health, and zone reload stats (`Issue #16`). +- Implement structured logging with trace correlation (`Issue #10`). +- Provide `/health` endpoints for liveness/readiness plus synthetic probes for upstream dependencies. +- Document alerting thresholds and dashboards for initial production rollout. + +## 5. Deployment & Operations +- **Targets**: support Windows and Linux deployments (console, Windows Service per `Issue #5`, and container/systemd scenarios). +- **Configuration Management**: document secrets handling, validation pipelines, and rollback procedures. +- **Networking**: handle UDP port conflicts gracefully (e.g., Docker ICS note from README) and expose configurable listener ports. +- **Scalability**: specify requirements for running multiple instances (state sharing, consistent hashing, or health-probe coordination). +- **Security**: define TLS requirements for HTTP endpoints, access control for admin APIs, and logging of configuration changes. + +## 6. .NET Maintenance & Upgrades +- **Target Runtime**: move `Dns`, `dns-cli`, and `dnstest` to `.NET 8` (LTS) for multi-platform support. +- **Dependency Audit**: verify Ninject and Microsoft.Extensions packages for .NET 8 compatibility or plan replacements (e.g., Microsoft.Extensions.DependencyInjection). +- **Upgrade Plan**: + - Update SDK/TFM, resolve API changes, and ensure build pipelines install the correct .NET 8 SDK. + - Run full regression + performance suite pre/post-upgrade. + - Document rollout steps and rollback strategy. +- **Ongoing Maintenance**: establish a quarterly review for SDK patches, dependency updates, and security advisories; codify required validation steps (unit, integration, smoke tests). + +## 7. AI Agent Enablement +- Deliver an `AGENTS.md` modeled after [OpenAI’s reference](https://raw.githubusercontent.com/openai/agents.md/refs/heads/main/AGENTS.md) with: + - Repository layout, key projects, and entry points. + - Build/test commands, sample run instructions, and common gotchas. + - Coding standards, review policies, and MIT license reminders. + - Scope limitations: agents may modify code/tests only (no infrastructure or deployment assets). + - Validation checklist before submitting PRs (run tests, lint, documentation updates). +- Provide guidance for prioritizing issues (testing, monitoring, zone providers) so automated contributors align with roadmap. + +## 8. Roadmap Seeds (from open issues) +- `#1 Static Zone declaration file` +- `#2 DNS-Sec support` +- `#3 Time-based DNS resolver` +- `#4 MAC-address scoped zones` +- `#5 Install/run as NT Service` +- `#7/#8/#9/#19` configuration providers & dynamic updates +- `#10` trace logging tools +- `#11` BitPacker.Write +- `#15` DNS caching +- `#16` Metrics support +- `#25` HTTP server improvements +- `#26` Compressed string pointer parsing bug + +## 9. Success Criteria +- All code runs on .NET 8 across Windows/Linux, with published deployment artifacts. +- Automated tests cover DNS parsing, caching, zone providers, and health probes; CI enforced. +- Metrics/logging are exported in structured formats and consumed by dashboards. +- At least one additional zone provider (BIND or equivalent) and enhanced health probes are production-ready. +- `AGENTS.md` is published and successfully guides AI/automation contributions within the defined scope. + diff --git a/docs/references.md b/docs/references.md new file mode 100644 index 0000000..f2d8666 --- /dev/null +++ b/docs/references.md @@ -0,0 +1,22 @@ +# References +The DNS protocol is specified and built on a raft of IETF RFCs. These links serve as the canonical references for features implemented (or planned) in this repository. + +## Core DNS RFCs +- **[RFC 1034](https://datatracker.ietf.org/doc/html/rfc1034)** — Domain names: concepts and facilities; foundational DNS architecture. +- **[RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035)** — Domain names: implementation and specification; wire format, message structures, and resource records. + +## Other key DNS RFCs +- **[RFC 1123](https://datatracker.ietf.org/doc/html/rfc1123)** — Requirements for Internet Hosts; DNS-related operational requirements. +- **[RFC 2181](https://datatracker.ietf.org/doc/html/rfc2181)** — Clarifications to the DNS specification; authoritative guidance for modern resolvers. +- **[RFC 2308](https://datatracker.ietf.org/doc/html/rfc2308)** — Negative Caching of DNS Queries; defines TTL behavior for NXDOMAIN responses (relevant to caching work). +- **[RFC 3596](https://datatracker.ietf.org/doc/html/rfc3596)** — DNS Extensions to Support IP Version 6; specifies AAAA records handled by this server. +- **[RFC 4033](https://datatracker.ietf.org/doc/html/rfc4033)** — DNS Security Introduction and Requirements; foundation for DNSSEC features. +- **[RFC 4034](https://datatracker.ietf.org/doc/html/rfc4034)** — Resource Records for the DNS Security Extensions; details DNSSEC record types. +- **[RFC 4035](https://datatracker.ietf.org/doc/html/rfc4035)** — Protocol Modifications for DNSSEC; describes resolver behavior for signed responses. + +## Supporting Standards & Transports +- **[RFC 768](https://datatracker.ietf.org/doc/html/rfc768)** — User Datagram Protocol; the primary transport for DNS queries handled by `DnsServer`. +- **[RFC 9293](https://datatracker.ietf.org/doc/html/rfc9293)** — Transmission Control Protocol; DNS fallbacks to TCP must conform when implementing large responses or zone transfers. +- **[RFC 6891](https://datatracker.ietf.org/doc/html/rfc6891)** — Extension Mechanisms for DNS (EDNS(0)); governs modern DNS extensions and message size negotiation. +- **[RFC 7766](https://datatracker.ietf.org/doc/html/rfc7766)** — DNS Transport over TCP: Requirements; clarifies persistent TCP usage for DNS. +- **[RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110)** — HTTP Semantics; referenced by the embedded HTTP server providing health and diagnostics. diff --git a/exclusion.dic b/exclusion.dic new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/exclusion.dic @@ -0,0 +1 @@ + From 99d0c62e8546d6edb8bfb0a133a253599ed64f12 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 08:44:26 -0800 Subject: [PATCH 03/31] Planning updates --- AGENTS.md | 1 + README.md | 3 ++- docs/priorities.md | 5 +++++ docs/task_list.md | 16 ++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/task_list.md diff --git a/AGENTS.md b/AGENTS.md index abfdb30..0e2d0ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ ## 1. Mission - Maintain and extend the `csharp-dns-server` so it becomes a production-ready DNS service with rich testing, observability, and zone-provider capabilities. - Follow the roadmap in `docs/product_requirements.md`, respect the priority tiers in `docs/priorities.md` (P0 reliability/protocol accuracy, P1 security & maintenance, P2 features), and focus on open GitHub issues aligned with those tiers. +- Reference the prioritized backlog in `docs/task_list.md` when picking up work to stay aligned with near-term goals. ## 2. Repository Orientation | Project | Purpose | Notes | diff --git a/README.md b/README.md index ab5198f..919fe71 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ The DNS server has a built-in Web Server providing operational insight into the ## Documentation - [Product requirements](docs/product_requirements.md) describe the current roadmap, observability goals, and .NET maintenance plans. -- [Project priorities](docs/priorities.md) outline the P0/P1/P2 focus areas (reliability, security/maintenance, features). +- [Project priorities & plan](docs/priorities.md) outline the P0/P1/P2 focus areas plus execution notes (DI migration, OpenTelemetry instrumentation). +- [Task list](docs/task_list.md) captures the prioritized backlog that tracks to those priorities. - [Protocol references](docs/references.md) list the RFCs and supporting standards that guide implementation. - [AGENTS guide](AGENTS.md) explains how automation/AI contributors should work within this repository. diff --git a/docs/priorities.md b/docs/priorities.md index 4116230..5913b31 100644 --- a/docs/priorities.md +++ b/docs/priorities.md @@ -15,3 +15,8 @@ - Deliver new zone providers (BIND, dynamic sources), parental-control/time-based policies, MAC-scoped records, and richer health-probe strategies. - Enhance management surfaces (API/UI) and AI-assist documentation (AGENTS.md) to streamline contributions. - Iterate on HTTP dashboards/metrics export per roadmap once P0/P1 goals are satisfied. + +## Execution Plan Highlights +- **Dependency Injection**: migrate from Ninject to the built-in `Microsoft.Extensions.DependencyInjection` container across `Dns`, `dns-cli`, and supporting libraries as part of the P1 maintenance track. +- **Telemetry Direction**: instrument the DNS server, zone providers, and HTTP endpoints with OpenTelemetry-compatible metrics/traces. External operators are expected to supply collectors/exporters; the codebase will emit OTLP-compatible data but will not bundle collector infrastructure. +- **Roadmap Sync**: keep `docs/product_requirements.md`, this priorities doc, and `AGENTS.md` in sync whenever workstreams change so contributors understand current focus. diff --git a/docs/task_list.md b/docs/task_list.md new file mode 100644 index 0000000..2a32cb0 --- /dev/null +++ b/docs/task_list.md @@ -0,0 +1,16 @@ +# Task List (Prioritized) + +1. **Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. +2. **Authoritative response verification suite** — Create integration tests running `dns-cli` with sample zones to validate AA/RA/SOA behavior and caching semantics (P0 reliability). +3. **Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests. +4. **Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. +5. **Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. +6. **Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. +7. **Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. +8. **Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP. +9. **Secure HTTP admin surface** — Provide configuration for bindings/authz and document operational guidance to avoid exposing diagnostic endpoints unintentionally. +10. **Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types. +11. **Add dynamic configuration providers** — Introduce REST/service-backed zone/configuration sources with validation and hot reload pipelines. +12. **Implement parental/time-based/MAC policies** — Deliver requested zone behaviors (issues #3/#4/#9) leveraging the SmartZoneResolver framework. +13. **Extend health probes (HTTP/TCP)** — Add richer probe strategies with retries/weights within the IPProbe provider. +14. **Enhance HTTP operational UX** — Replace HTML dumps with JSON/metrics endpoints and basic dashboards aligned with the observability plan. From 84a3e420d278923cc2904d488e27684f1337cd92 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 09:00:06 -0800 Subject: [PATCH 04/31] .NET 8 SDK update, any CPU. --- Dns/Dns.csproj | 5 +++-- dns-cli/dns-cli.csproj | 3 ++- dnstest/dnstest.csproj | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Dns/Dns.csproj b/Dns/Dns.csproj index 661aa4c..ecb94cd 100644 --- a/Dns/Dns.csproj +++ b/Dns/Dns.csproj @@ -2,9 +2,10 @@ Library - netcoreapp3.1 + net8.0 Dns 65001 + AnyCPU @@ -31,4 +32,4 @@ Always - \ No newline at end of file + diff --git a/dns-cli/dns-cli.csproj b/dns-cli/dns-cli.csproj index 50cbe65..70b868d 100644 --- a/dns-cli/dns-cli.csproj +++ b/dns-cli/dns-cli.csproj @@ -2,9 +2,10 @@ Exe - netcoreapp3.1 + net8.0 DnsCli DnsCli.Program + AnyCPU diff --git a/dnstest/dnstest.csproj b/dnstest/dnstest.csproj index c4a2e52..ab01a5c 100644 --- a/dnstest/dnstest.csproj +++ b/dnstest/dnstest.csproj @@ -1,9 +1,10 @@  - netcoreapp3.1 + net8.0 false DnsTest + AnyCPU From cc41c6ab04f16504340a5bab9d57fa4e20a9ea4e Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 09:34:15 -0800 Subject: [PATCH 05/31] Fixed CA2241 warning --- Dns/DnsServer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dns/DnsServer.cs b/Dns/DnsServer.cs index cf06d1e..912ee7c 100644 --- a/Dns/DnsServer.cs +++ b/Dns/DnsServer.cs @@ -247,7 +247,7 @@ private IEnumerable GetDefaultDNS() foreach (IPAddress dns in dnsServers) { - Console.WriteLine("Discovered DNS: ", dns); + Console.WriteLine("Discovered DNS: {0}", dns); yield return dns; } @@ -266,4 +266,4 @@ public void DumpHtml(TextWriter writer) writer.WriteLine("DNS Server Status
"); } } -} \ No newline at end of file +} From c0d67462b0391cffdbd6075dceab174ad49e192a Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 09:47:08 -0800 Subject: [PATCH 06/31] Fix issue #26, added improved test suite for circular loops --- Dns/DnsProtocol.cs | 84 ++++++++++++++++++++++++-------- dnstest/DnsProtocolTest.cs | 98 +++++++++++++++++++++++++++++++++++++- docs/task_list.md | 29 +++++------ docs/tasks/task_01_plan.md | 32 +++++++++++++ 4 files changed, 207 insertions(+), 36 deletions(-) create mode 100644 docs/tasks/task_01_plan.md diff --git a/Dns/DnsProtocol.cs b/Dns/DnsProtocol.cs index 7660763..44d05b5 100644 --- a/Dns/DnsProtocol.cs +++ b/Dns/DnsProtocol.cs @@ -7,6 +7,8 @@ namespace Dns { using System; + using System.Collections.Generic; + using System.IO; using System.Text; public class DnsProtocol @@ -44,43 +46,83 @@ public static string ReadString(byte[] bytes, ref int currentOffset) { StringBuilder resourceName = new StringBuilder(); int compressionOffset = -1; + int readOffset = currentOffset; + HashSet pointerVisitedOffsets = null; + while (true) { - // get segment length or detect termination of segments - int segmentLength = bytes[currentOffset]; + if (readOffset >= bytes.Length) + { + throw new IndexOutOfRangeException("DNS label offset exceeded buffer length."); + } + + int segmentLength = bytes[readOffset]; - // compressed name + // compressed name pointer if ((segmentLength & 0xC0) == 0xC0) { - currentOffset++; + if (readOffset + 1 >= bytes.Length) + { + throw new IndexOutOfRangeException("DNS compression pointer exceeds buffer length."); + } + + pointerVisitedOffsets ??= new HashSet(); + if (!pointerVisitedOffsets.Add(readOffset)) + { + throw new InvalidDataException("DNS compression pointer cycle detected."); + } + + int pointer = ((segmentLength & 0x3F) << 8) | bytes[readOffset + 1]; if (compressionOffset == -1) { - // only record origin, and follow all pointers thereafter - compressionOffset = currentOffset; + // remember where to resume after following the pointer + compressionOffset = readOffset + 2; + } + + if (pointer >= bytes.Length) + { + throw new IndexOutOfRangeException("DNS compression pointer targets invalid offset."); } + // RFC 1035 §4.1.4: Pointers must reference a prior occurrence of the same name, + // must point to the start of a label, and forward references are prohibited. - // move pointer to compression segment - currentOffset = bytes[currentOffset]; - segmentLength = bytes[currentOffset]; + readOffset = pointer; + continue; } if (segmentLength == 0x00) { - if (compressionOffset != -1) - { - currentOffset = compressionOffset; - } - // move past end of name \0 - currentOffset++; + readOffset++; break; } - // move pass length and get segment text - currentOffset++; - resourceName.AppendFormat("{0}.", Encoding.Default.GetString(bytes, currentOffset, segmentLength)); - currentOffset += segmentLength; + readOffset++; + if (segmentLength > 63) + { + throw new InvalidDataException("DNS label length exceeds maximum of 63 bytes."); + } + if (readOffset + segmentLength > bytes.Length) + { + throw new IndexOutOfRangeException("DNS label exceeds buffer length."); + } + // RFC 1035: DNS labels must be ASCII. + // This is an intentional breaking change; validate against existing usage if upgrading. + // Check for non-ASCII bytes before decoding + for (int i = 0; i < segmentLength; i++) + { + if (bytes[readOffset + i] > 0x7F) + { + throw new InvalidDataException("DNS label contains non-ASCII characters, which are not allowed per RFC 1035."); + } + } + string label = Encoding.ASCII.GetString(bytes, readOffset, segmentLength); + resourceName.Append(label).Append('.'); + readOffset += segmentLength; } - return resourceName.ToString().TrimEnd(new[] {'.'}); + + currentOffset = compressionOffset == -1 ? readOffset : compressionOffset; + + return resourceName.ToString().TrimEnd('.'); } } -} \ No newline at end of file +} diff --git a/dnstest/DnsProtocolTest.cs b/dnstest/DnsProtocolTest.cs index 3ae4507..c36190f 100644 --- a/dnstest/DnsProtocolTest.cs +++ b/dnstest/DnsProtocolTest.cs @@ -6,6 +6,7 @@ namespace DnsTest { + using System; using System.IO; using System.Linq; using System.Net; @@ -190,6 +191,101 @@ public void DnsQuery3() query.Dump(); } + [Fact] + public void CompressedPointerBeyond255ParsesCorrectly() + { + byte[] buffer = new byte[400]; + + // create label at offset 300 ("bar") + int labelOffset = 300; + buffer[labelOffset] = 3; + buffer[labelOffset + 1] = (byte)'b'; + buffer[labelOffset + 2] = (byte)'a'; + buffer[labelOffset + 3] = (byte)'r'; + buffer[labelOffset + 4] = 0x00; + + // pointer referencing that label positioned later in the buffer + int pointerOffset = 360; + buffer[pointerOffset] = (byte)(0xC0 | ((labelOffset >> 8) & 0x3F)); + buffer[pointerOffset + 1] = (byte)(labelOffset & 0xFF); + + int offset = pointerOffset; + string result = DnsProtocol.ReadString(buffer, ref offset); + + Assert.Equal("bar", result); + Assert.Equal(pointerOffset + 2, offset); + } + + [Fact] + public void CompressedPointerCircularReferenceThrows() + { + byte[] buffer = new byte[100]; + int pointerOffset = 50; + buffer[pointerOffset] = (byte)(0xC0 | ((pointerOffset >> 8) & 0x3F)); + buffer[pointerOffset + 1] = (byte)(pointerOffset & 0xFF); + + int offset = pointerOffset; + Assert.Throws(() => DnsProtocol.ReadString(buffer, ref offset)); + } + + [Fact] + public void CompressedPointerBeyondBufferThrows() + { + byte[] buffer = new byte[100]; + int pointerOffset = 60; + int invalidTarget = buffer.Length + 10; + buffer[pointerOffset] = (byte)(0xC0 | ((invalidTarget >> 8) & 0x3F)); + buffer[pointerOffset + 1] = (byte)(invalidTarget & 0xFF); + + int offset = pointerOffset; + var ex = Assert.Throws(() => DnsProtocol.ReadString(buffer, ref offset)); + Assert.Equal("DNS compression pointer targets invalid offset.", ex.Message); + } + + [Fact] + public void CompressedPointerCanReferenceAnotherPointer() + { + byte[] buffer = new byte[200]; + + // base label at offset 10: "foo" + buffer[10] = 3; + buffer[11] = (byte)'f'; + buffer[12] = (byte)'o'; + buffer[13] = (byte)'o'; + buffer[14] = 0x00; + + // pointer B at offset 30 pointing to label at offset 10 + buffer[30] = (byte)(0xC0 | ((10 >> 8) & 0x3F)); + buffer[31] = (byte)(10 & 0xFF); + + // pointer A at offset 60 pointing to pointer B at offset 30 + buffer[60] = (byte)(0xC0 | ((30 >> 8) & 0x3F)); + buffer[61] = (byte)(30 & 0xFF); + + int offset = 60; + string name = DnsProtocol.ReadString(buffer, ref offset); + + Assert.Equal("foo", name); + Assert.Equal(62, offset); + } + + [Fact] + public void CompressedPointerTwoHopCycleThrows() + { + byte[] buffer = new byte[100]; + int first = 10; + int second = 30; + + buffer[first] = (byte)(0xC0 | ((second >> 8) & 0x3F)); + buffer[first + 1] = (byte)(second & 0xFF); + + buffer[second] = (byte)(0xC0 | ((first >> 8) & 0x3F)); + buffer[second + 1] = (byte)(first & 0xFF); + + int offset = first; + Assert.Throws(() => DnsProtocol.ReadString(buffer, ref offset)); + } + [Fact] public void TransitiveQueryTest() { @@ -317,4 +413,4 @@ public void Opcode() Assert.Equal(0xa800, message.Flags); } } -} \ No newline at end of file +} diff --git a/docs/task_list.md b/docs/task_list.md index 2a32cb0..05a6f99 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -1,16 +1,17 @@ # Task List (Prioritized) -1. **Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. -2. **Authoritative response verification suite** — Create integration tests running `dns-cli` with sample zones to validate AA/RA/SOA behavior and caching semantics (P0 reliability). -3. **Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests. -4. **Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. -5. **Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. -6. **Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. -7. **Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. -8. **Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP. -9. **Secure HTTP admin surface** — Provide configuration for bindings/authz and document operational guidance to avoid exposing diagnostic endpoints unintentionally. -10. **Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types. -11. **Add dynamic configuration providers** — Introduce REST/service-backed zone/configuration sources with validation and hot reload pipelines. -12. **Implement parental/time-based/MAC policies** — Deliver requested zone behaviors (issues #3/#4/#9) leveraging the SmartZoneResolver framework. -13. **Extend health probes (HTTP/TCP)** — Add richer probe strategies with retries/weights within the IPProbe provider. -14. **Enhance HTTP operational UX** — Replace HTML dumps with JSON/metrics endpoints and basic dashboards aligned with the observability plan. +1. [ ] **T01 – Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. +2. [ ] **T02 – Authoritative response verification suite** — Create integration tests running `dns-cli` with sample zones to validate AA/RA/SOA behavior and caching semantics (P0 reliability). +3. [ ] **T03 – Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests. +4. [ ] **T04 – Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. +5. [ ] **T05 – Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. +6. [ ] **T06 – Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. +7. [ ] **T07 – Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. +8. [ ] **T08 – Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP. +9. [ ] **T09 – Fix CA2241 format warning** — Update the logging call in `Dns/DnsServer.cs` (line 250) to use the correct string-format arguments so builds are warning-free. +10. [ ] **T10 – Secure HTTP admin surface** — Provide configuration for bindings/authz and document operational guidance to avoid exposing diagnostic endpoints unintentionally. +11. [ ] **T11 – Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types. +12. [ ] **T12 – Add dynamic configuration providers** — Introduce REST/service-backed zone/configuration sources with validation and hot reload pipelines. +13. [ ] **T13 – Implement parental/time-based/MAC policies** — Deliver requested zone behaviors (issues #3/#4/#9) leveraging the SmartZoneResolver framework. +14. [ ] **T14 – Extend health probes (HTTP/TCP)** — Add richer probe strategies with retries/weights within the IPProbe provider. +15. [ ] **T15 – Enhance HTTP operational UX** — Replace HTML dumps with JSON/metrics endpoints and basic dashboards aligned with the observability plan. diff --git a/docs/tasks/task_01_plan.md b/docs/tasks/task_01_plan.md new file mode 100644 index 0000000..973c9c1 --- /dev/null +++ b/docs/tasks/task_01_plan.md @@ -0,0 +1,32 @@ +# Task 1 Plan – Fix DNS Compressed-Name Parsing Regression + +## Goal +Resolve issue #26 (“Error in the compressed string pointer parsing”) by repairing the DNS message parser and adding regression coverage so malformed compressed names can’t slip through again. + +## Scope +- Code: `Dns/DnsProtocol.cs`, `Dns/DnsMessage.cs`, and any helper classes that read domain-name labels. +- Tests: `dnstest/DnsProtocolTest.cs` (unit), plus optional integration validation if needed. + +## Steps +1. **Reproduce the bug** + - Capture the failing packet(s) from issue #26 or craft equivalents. + - Add a failing test in `dnstest/DnsProtocolTest.cs` that exposes the incorrect compressed-pointer behavior. +2. **Inspect parser logic** + - Review `DnsProtocol.ReadString` and downstream usage in `DnsMessage.TryParse`. + - Verify pointer offset handling, pointer loops, and name termination per RFC 1035 §4.1.4. +3. **Implement fix** + - Adjust parsing logic to correctly handle offsets, prevent infinite loops, and ensure the buffer cursor is restored after following compression pointers. + - Consider additional validation (max label length, recursion limits). +4. **Extend regression tests** + - Add positive/negative cases covering compressed names at different positions (questions, answers, authority, additional). + - Include edge cases (nested pointers, zero-length labels). +5. **Optional integration test** + - Use `dns-cli` with a crafted response to ensure end-to-end decoding works. +6. **Documentation / notes** + - Update `docs/task_list.md` checkbox when complete. + - Reference this fix in release notes or issues if needed. + +## Acceptance Criteria +- All new tests pass and demonstrate correct compressed-name parsing. +- No regressions in existing protocol tests. +- The issue #26 reproduction no longer fails. From 9d70f8afacae63c4db4aa67cbb9aa0399a662eca Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 09:48:32 -0800 Subject: [PATCH 07/31] Tree cleanup --- .vs/csharp-dns-server/xs/UserPrefs.xml | 47 ------------------- .../xs/project-cache/dns-Debug.json | 1 - .../xs/project-cache/dnstest-Debug.json | 1 - 3 files changed, 49 deletions(-) delete mode 100644 .vs/csharp-dns-server/xs/UserPrefs.xml delete mode 100644 .vs/csharp-dns-server/xs/project-cache/dns-Debug.json delete mode 100644 .vs/csharp-dns-server/xs/project-cache/dnstest-Debug.json diff --git a/.vs/csharp-dns-server/xs/UserPrefs.xml b/.vs/csharp-dns-server/xs/UserPrefs.xml deleted file mode 100644 index 0bbb52c..0000000 --- a/.vs/csharp-dns-server/xs/UserPrefs.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.vs/csharp-dns-server/xs/project-cache/dns-Debug.json b/.vs/csharp-dns-server/xs/project-cache/dns-Debug.json deleted file mode 100644 index 4f5c5e9..0000000 --- a/.vs/csharp-dns-server/xs/project-cache/dns-Debug.json +++ /dev/null @@ -1 +0,0 @@ -{"Format":1,"ProjectReferences":[],"MetadataReferences":[{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.CSharp.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.caching.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Caching.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.caching.memory/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Caching.Memory.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.binder/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.Binder.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.commandline/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.CommandLine.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.environmentvariables/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.EnvironmentVariables.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.fileextensions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.FileExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.json/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.usersecrets/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.UserSecrets.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/3.1.9/lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.dependencyinjection/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.DependencyInjection.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.fileproviders.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.FileProviders.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.fileproviders.physical/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.FileProviders.Physical.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.filesystemglobbing/3.1.9/lib/netstandard2.0/Microsoft.Extensions.FileSystemGlobbing.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.hosting.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Hosting.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.hosting/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Hosting.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.abstractions/3.1.9/lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.configuration/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.console/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.Console.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.debug/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.Debug.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.eventlog/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.EventLog.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.eventsource/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.EventSource.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.options.configurationextensions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Options.ConfigurationExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.options/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Options.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.primitives/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.VisualBasic.Core.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.VisualBasic.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.Win32.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/mscorlib.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/netstandard.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/ninject/3.3.4/lib/netstandard2.0/Ninject.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.AppContext.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Buffers.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.Concurrent.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.Immutable.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.NonGeneric.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.Specialized.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.Annotations.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.DataAnnotations.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.EventBasedAsync.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.TypeConverter.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Console.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Core.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Data.Common.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Data.DataSetExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Data.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Contracts.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Debug.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.DiagnosticSource.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/system.diagnostics.eventlog/4.7.0/ref/netstandard2.0/System.Diagnostics.EventLog.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.FileVersionInfo.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Process.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.StackTrace.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.TextWriterTraceListener.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Tools.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.TraceSource.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Tracing.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Drawing.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Drawing.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Dynamic.Runtime.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Globalization.Calendars.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Globalization.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Globalization.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.Brotli.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.FileSystem.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.ZipFile.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.DriveInfo.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.Watcher.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.IsolatedStorage.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.MemoryMappedFiles.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Pipes.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.UnmanagedMemoryStream.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.Expressions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.Parallel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.Queryable.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Memory.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Http.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.HttpListener.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Mail.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.NameResolution.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.NetworkInformation.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Ping.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Requests.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Security.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.ServicePoint.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Sockets.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebClient.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebHeaderCollection.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebProxy.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebSockets.Client.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebSockets.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Numerics.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Numerics.Vectors.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ObjectModel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.DispatchProxy.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Emit.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Emit.ILGeneration.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Emit.Lightweight.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Metadata.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.TypeExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Resources.Reader.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Resources.ResourceManager.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Resources.Writer.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.CompilerServices.Unsafe.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.CompilerServices.VisualC.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Handles.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.InteropServices.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.InteropServices.RuntimeInformation.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.InteropServices.WindowsRuntime.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Intrinsics.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Loader.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Numerics.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Formatters.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Xml.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Claims.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Algorithms.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Csp.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Encoding.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.X509Certificates.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Principal.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/system.security.principal.windows/4.7.0/ref/netcoreapp3.0/System.Security.Principal.Windows.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.SecureString.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ServiceModel.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ServiceProcess.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encoding.CodePages.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encoding.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encoding.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encodings.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.RegularExpressions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Channels.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Overlapped.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.Dataflow.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.Parallel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Thread.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.ThreadPool.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Timer.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Transactions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Transactions.Local.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ValueTuple.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Web.HttpUtility.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Windows.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.Linq.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.ReaderWriter.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.Serialization.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XmlDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XmlSerializer.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XPath.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XPath.XDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/WindowsBase.dll","Aliases":[],"Framework":null}],"Files":["/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Config/AppConfig.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Contracts/IAddressDispenser.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Contracts/IDnsCache.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Contracts/IDnsResolver.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Contracts/IHtmlDump.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/DnsCache.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/DnsMessage.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/DnsProtocol.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/DnsServer.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Extensions.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/HttpServer.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/OpCode.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Program.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Question.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/QuestionList.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/RCode.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/RData.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ResourceClass.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ResourceList.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ResourceRecord.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ResourceType.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/SmartAddressDispenser.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/SmartZoneResolver.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/SocketExtensions.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/UdpListener.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Utility/BitPacker.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Utility/CsvParser.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Utility/CsvRow.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Zone.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/AP/APZoneProvider.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/BaseZoneProvider.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/Bind/BindZoneProvider.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/FileWatcherProviderOptions.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/FileWatcherZoneProvider.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/AddressProbe.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/Host.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/IPProbeProviderOptions.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/IPProbeZoneProvider.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/ProbeResult.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/State.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneProvider/IPProbe/Strategy.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/ZoneRecord.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/.DS_Store","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/Data/machineinfo.csv","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/appsettings.json","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/obj/Debug/netcoreapp3.1/dns.AssemblyInfo.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/obj/Debug/netcoreapp3.1/dns.AssemblyInfo.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/obj/Debug/netcoreapp3.1/dns.AssemblyInfo.cs"],"BuildActions":["Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","Compile","None","None","Content","Compile","Compile","Compile"],"Analyzers":[],"AdditionalFiles":[],"EditorConfigFiles":[]} \ No newline at end of file diff --git a/.vs/csharp-dns-server/xs/project-cache/dnstest-Debug.json b/.vs/csharp-dns-server/xs/project-cache/dnstest-Debug.json deleted file mode 100644 index 395e095..0000000 --- a/.vs/csharp-dns-server/xs/project-cache/dnstest-Debug.json +++ /dev/null @@ -1 +0,0 @@ -{"Format":1,"ProjectReferences":[{"FilePath":"/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dns/dns.csproj","Aliases":[],"Framework":null}],"MetadataReferences":[{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.CSharp.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.dotnet.platformabstractions/1.0.3/lib/netstandard1.3/Microsoft.DotNet.PlatformAbstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.caching.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Caching.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.caching.memory/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Caching.Memory.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.binder/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.Binder.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.commandline/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.CommandLine.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.environmentvariables/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.EnvironmentVariables.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.fileextensions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.FileExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.json/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.configuration.usersecrets/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Configuration.UserSecrets.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/3.1.9/lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.dependencyinjection/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.DependencyInjection.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.dependencymodel/1.0.3/lib/netstandard1.6/Microsoft.Extensions.DependencyModel.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.fileproviders.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.FileProviders.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.fileproviders.physical/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.FileProviders.Physical.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.filesystemglobbing/3.1.9/lib/netstandard2.0/Microsoft.Extensions.FileSystemGlobbing.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.hosting.abstractions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Hosting.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.hosting/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Hosting.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.abstractions/3.1.9/lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.configuration/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.console/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.Console.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.debug/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.Debug.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.eventlog/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.EventLog.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.logging.eventsource/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Logging.EventSource.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.options.configurationextensions/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Options.ConfigurationExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.options/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Options.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.extensions.primitives/3.1.9/lib/netcoreapp3.1/Microsoft.Extensions.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.testhost/15.9.0/lib/netstandard1.5/Microsoft.TestPlatform.CommunicationUtilities.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.objectmodel/15.9.0/lib/netstandard1.5/Microsoft.TestPlatform.CoreUtilities.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.testhost/15.9.0/lib/netstandard1.5/Microsoft.TestPlatform.CrossPlatEngine.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.objectmodel/15.9.0/lib/netstandard1.5/Microsoft.TestPlatform.PlatformAbstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.VisualBasic.Core.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.VisualBasic.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.codecoverage/15.9.0/lib/netcoreapp1.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.testhost/15.9.0/lib/netstandard1.5/Microsoft.VisualStudio.TestPlatform.Common.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.objectmodel/15.9.0/lib/netstandard1.5/Microsoft.VisualStudio.TestPlatform.ObjectModel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/Microsoft.Win32.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/mscorlib.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/netstandard.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/newtonsoft.json/9.0.1/lib/netstandard1.0/Newtonsoft.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/ninject/3.3.4/lib/netstandard2.0/Ninject.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.AppContext.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Buffers.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.Concurrent.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.Immutable.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.NonGeneric.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Collections.Specialized.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.Annotations.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.DataAnnotations.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.EventBasedAsync.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ComponentModel.TypeConverter.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Configuration.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Console.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Core.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Data.Common.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Data.DataSetExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Data.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Contracts.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Debug.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.DiagnosticSource.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/system.diagnostics.eventlog/4.7.0/ref/netstandard2.0/System.Diagnostics.EventLog.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.FileVersionInfo.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Process.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.StackTrace.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.TextWriterTraceListener.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Tools.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.TraceSource.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Diagnostics.Tracing.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Drawing.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Drawing.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Dynamic.Runtime.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Globalization.Calendars.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Globalization.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Globalization.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.Brotli.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.FileSystem.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Compression.ZipFile.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.DriveInfo.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.FileSystem.Watcher.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.IsolatedStorage.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.MemoryMappedFiles.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.Pipes.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.IO.UnmanagedMemoryStream.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.Expressions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.Parallel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Linq.Queryable.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Memory.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Http.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.HttpListener.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Mail.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.NameResolution.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.NetworkInformation.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Ping.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Requests.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Security.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.ServicePoint.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.Sockets.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebClient.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebHeaderCollection.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebProxy.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebSockets.Client.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Net.WebSockets.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Numerics.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Numerics.Vectors.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ObjectModel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.DispatchProxy.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Emit.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Emit.ILGeneration.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Emit.Lightweight.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Metadata.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Reflection.TypeExtensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Resources.Reader.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Resources.ResourceManager.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Resources.Writer.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.CompilerServices.Unsafe.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.CompilerServices.VisualC.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Handles.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.InteropServices.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.InteropServices.RuntimeInformation.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.InteropServices.WindowsRuntime.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Intrinsics.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Loader.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Numerics.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Formatters.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Runtime.Serialization.Xml.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Claims.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Algorithms.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Csp.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Encoding.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.Primitives.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Cryptography.X509Certificates.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.Principal.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/system.security.principal.windows/4.7.0/ref/netcoreapp3.0/System.Security.Principal.Windows.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Security.SecureString.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ServiceModel.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ServiceProcess.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encoding.CodePages.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encoding.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encoding.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Encodings.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.Json.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Text.RegularExpressions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Channels.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Overlapped.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.Dataflow.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.Extensions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Tasks.Parallel.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Thread.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.ThreadPool.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Threading.Timer.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Transactions.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Transactions.Local.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.ValueTuple.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Web.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Web.HttpUtility.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Windows.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.Linq.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.ReaderWriter.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.Serialization.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XmlDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XmlSerializer.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XPath.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/System.Xml.XPath.XDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/system.xml.xpath.xmldocument/4.0.1/ref/netstandard1.3/System.Xml.XPath.XmlDocument.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/microsoft.testplatform.testhost/15.9.0/lib/netstandard1.5/testhost.dll","Aliases":[],"Framework":null},{"FilePath":"/usr/local/share/dotnet/packs/Microsoft.NETCore.App.Ref/3.1.0/ref/netcoreapp3.1/WindowsBase.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/xunit.abstractions/2.0.3/lib/netstandard2.0/xunit.abstractions.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/xunit.assert/2.4.1/lib/netstandard1.1/xunit.assert.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/xunit.extensibility.core/2.4.1/lib/netstandard1.1/xunit.core.dll","Aliases":[],"Framework":null},{"FilePath":"/Users/sbutler/.nuget/packages/xunit.extensibility.execution/2.4.1/lib/netstandard1.1/xunit.execution.dotnet.dll","Aliases":[],"Framework":null}],"Files":["/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/BitPackerTests.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/ConfigTests.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/DnsCacheTests.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/DnsProtocolTest.cs","/Users/sbutler/.nuget/packages/xunit.runner.visualstudio/2.4.0/build/netcoreapp1.0/xunit.runner.visualstudio.dotnetcore.testadapter.dll","/Users/sbutler/.nuget/packages/xunit.runner.visualstudio/2.4.0/build/netcoreapp1.0/xunit.runner.reporters.netcoreapp10.dll","/Users/sbutler/.nuget/packages/xunit.runner.visualstudio/2.4.0/build/netcoreapp1.0/xunit.runner.utility.netcoreapp10.dll","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/mono_crash.11a6748113.0.json","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/Data/BindZoneFiles/bindzonetest1.txt","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/Data/Config/appsettings.json","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/obj/Debug/netcoreapp3.1/dnstest.AssemblyInfo.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/obj/Debug/netcoreapp3.1/dnstest.AssemblyInfo.cs","/Users/sbutler/dev/go/src/github.com/stephbu/csharp-dns-server/dnstest/obj/Debug/netcoreapp3.1/dnstest.AssemblyInfo.cs"],"BuildActions":["Compile","Compile","Compile","Compile","None","None","None","None","None","None","Compile","Compile","Compile"],"Analyzers":["/Users/sbutler/.nuget/packages/xunit.analyzers/0.10.0/analyzers/dotnet/cs/xunit.analyzers.dll"],"AdditionalFiles":[],"EditorConfigFiles":[]} \ No newline at end of file From 3010037ce4309380427e5a8c3b3147182da04290 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 09:57:32 -0800 Subject: [PATCH 08/31] Task list updates --- docs/task_list.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/task_list.md b/docs/task_list.md index 05a6f99..9ca875e 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -1,14 +1,14 @@ # Task List (Prioritized) -1. [ ] **T01 – Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. +1. [x] **T01 – Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. 2. [ ] **T02 – Authoritative response verification suite** — Create integration tests running `dns-cli` with sample zones to validate AA/RA/SOA behavior and caching semantics (P0 reliability). 3. [ ] **T03 – Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests. 4. [ ] **T04 – Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. 5. [ ] **T05 – Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. 6. [ ] **T06 – Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. -7. [ ] **T07 – Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. +7. [x] **T07 – Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. 8. [ ] **T08 – Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP. -9. [ ] **T09 – Fix CA2241 format warning** — Update the logging call in `Dns/DnsServer.cs` (line 250) to use the correct string-format arguments so builds are warning-free. +9. [x] **T09 – Fix CA2241 format warning** — Update the logging call in `Dns/DnsServer.cs` (line 250) to use the correct string-format arguments so builds are warning-free. 10. [ ] **T10 – Secure HTTP admin surface** — Provide configuration for bindings/authz and document operational guidance to avoid exposing diagnostic endpoints unintentionally. 11. [ ] **T11 – Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types. 12. [ ] **T12 – Add dynamic configuration providers** — Introduce REST/service-backed zone/configuration sources with validation and hot reload pipelines. From a97b7d8f14dcce41cbe79b119b44c42cc2727175 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 10:17:24 -0800 Subject: [PATCH 09/31] Task 2 planning --- docs/task_list.md | 15 +++++++---- docs/tasks/task_02_plan.md | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 docs/tasks/task_02_plan.md diff --git a/docs/task_list.md b/docs/task_list.md index 9ca875e..b9cdd80 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -2,16 +2,21 @@ 1. [x] **T01 – Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. 2. [ ] **T02 – Authoritative response verification suite** — Create integration tests running `dns-cli` with sample zones to validate AA/RA/SOA behavior and caching semantics (P0 reliability). -3. [ ] **T03 – Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests. +3. [ ] **T03 – Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests (issue #15). 4. [ ] **T04 – Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. 5. [ ] **T05 – Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. 6. [ ] **T06 – Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. 7. [x] **T07 – Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. -8. [ ] **T08 – Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP. +8. [ ] **T08 – Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP (issue #16). 9. [x] **T09 – Fix CA2241 format warning** — Update the logging call in `Dns/DnsServer.cs` (line 250) to use the correct string-format arguments so builds are warning-free. 10. [ ] **T10 – Secure HTTP admin surface** — Provide configuration for bindings/authz and document operational guidance to avoid exposing diagnostic endpoints unintentionally. -11. [ ] **T11 – Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types. -12. [ ] **T12 – Add dynamic configuration providers** — Introduce REST/service-backed zone/configuration sources with validation and hot reload pipelines. +11. [ ] **T11 – Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types (addresses “Static Zone declaration file” issue #1). +12. [ ] **T12 – Add dynamic configuration providers** — Introduce REST/service-backed configuration sources with validation and hot reload pipelines (issues #7/#8/#19). 13. [ ] **T13 – Implement parental/time-based/MAC policies** — Deliver requested zone behaviors (issues #3/#4/#9) leveraging the SmartZoneResolver framework. 14. [ ] **T14 – Extend health probes (HTTP/TCP)** — Add richer probe strategies with retries/weights within the IPProbe provider. -15. [ ] **T15 – Enhance HTTP operational UX** — Replace HTML dumps with JSON/metrics endpoints and basic dashboards aligned with the observability plan. +15. [ ] **T15 – Enhance HTTP operational UX** — Replace HTML dumps with JSON/metrics endpoints and improvements requested in issue #25. +16. [ ] **T16 – Trace logging tools** — Implement structured trace logging and tooling per issue #10. +17. [ ] **T17 – Implement BitPacker.Write** — Complete the BitPacker.Write implementation and accompanying tests (issue #11). +18. [ ] **T18 – Windows/NT service packaging** — Add installers/scripts so the server can run as a Windows service (issue #5). +19. [ ] **T19 – DNSSEC support** — Add foundational DNSSEC record handling and validation paths (issue #2). +20. [ ] **T20 – Documented static zone workflow** — Provide a simple static zone declaration option (issue #1) for setups that don’t rely on the BIND parser. diff --git a/docs/tasks/task_02_plan.md b/docs/tasks/task_02_plan.md new file mode 100644 index 0000000..567ce49 --- /dev/null +++ b/docs/tasks/task_02_plan.md @@ -0,0 +1,51 @@ +# Task 2 Plan – Authoritative Response Verification Suite + +## Goal +Build a deterministic integration test suite that executes the shipping `dns-cli` host against sample zones so we can assert the DNS protocol surface (AA/RA flags, SOA authority section, caching-related TTL fields) behaves as expected end-to-end. + +## Scope +- **Code/Test targets**: `dns-cli` (process runner), `Dns/Program.cs`, `Dns/DnsServer.cs`, `Dns/SmartZoneResolver.cs`, and new xUnit integration fixtures that live under `dnstest`. +- **Assets**: reproducible sample zone definitions/configuration files that live in-repo (likely under `dnstest/TestData/`). +- **Out of scope**: modifying server behavior or introducing RFC 2308 caching logic (that is T03). T02 only codifies the current semantics via integration coverage. + +## Steps +1. **Define behavior checklist** + - Re-read RFC 1034/1035 + existing implementation to document what “correct” looks like for AA, RA, NXDOMAIN, SOA authority counts, TTL/minimum TTL, and round-robin ordering. + - Capture these expectations in the test plan so every assertion has a justification (e.g., `AA=1` for in-zone answers, `RA=0` because recursion is not provided, SOA present for NXDOMAIN with `MinimumTTL` acting as the negative-cache TTL). + +2. **Create deterministic zone + config assets** + - Place a CSV/AP-zone file with a handful of hosts (single-address, multi-address for rotation, and an empty gap for NXDOMAIN) under `dnstest/TestData/Zones`. + - Add an integration `appsettings` template that points the zone provider at this CSV and exposes tokens for DNS/HTTP ports so tests can substitute an available port at runtime. + - Keep assets self-contained so the suite never depends on developer-specific paths or live IP probes. + +3. **Spin up `dns-cli` from tests** + - Build a reusable `DnsCliHostFixture` that: + - Chooses free UDP/TCP ports (using `Socket`/`TcpListener`) to avoid conflicts with system services. + - Writes the tokenized config (step 2) to a temp file with the resolved ports and zone path. + - Launches `dotnet /dns-cli.dll ` with redirected stdout/stderr and a cancellation token; wait until the server is ready by probing the HTTP `/dump/dnsresolver` endpoint or by polling the UDP port with a health query. + - Implements `IDisposable` to cancel/kill the process after the test collection completes and to surface logs when startup fails. + +4. **Author request helper** + - Within the test project, create a `DnsQueryClient` utility that uses `DnsMessage`/`DnsProtocol` to craft queries (A + NXDOMAIN) and parse responses. + - Support toggling RD flag, capturing round-trips, verifying TTLs, and exposing raw `DnsMessage` for assertions. + - Consider adding simple retry/timeout handling so the integration tests are resilient to transient startup delays. + +5. **AA/RA/SOA assertions** + - Add tests that query an in-zone A record and assert: `QR=1`, `AA=1`, `RA=0`, `RCode=NOERROR`, and that the answer payload matches the CSV data (including TTL=10 and round-robin order). + - Add a test that sets the RD flag on the query to confirm the server still responds with `RA=0` for authoritative answers (baseline recursion semantics). + - Add an NXDOMAIN test that validates the authority section contains a single SOA record populated with the resolver’s current serial and minimum TTL, proving negative answers include the caching hints mandated by RFC 2308. + +6. **Caching-semantic coverage** + - Positive caching: issue the same query multiple times and assert TTL remains at 10 seconds (current behavior) and that responses stay authoritative; this guards future TTL changes. + - Negative caching: query a nonexistent record twice and ensure the SOA `MinimumTTL` mirrors the configured 300 seconds value each time. + - Lay groundwork for future RFC 2308 work by encapsulating “wait for TTL expiry” helpers (even if currently skipped) so T03 can plug in actual caching checks without rewriting the harness. + +7. **Document and wire CI** + - Update `docs/task_list.md`/`AGENTS.md` with instructions on running the new integration suite (e.g., `dotnet test` now launches `dns-cli`, required ports, how to tweak sample config). + - Ensure the suite is part of `dotnet test csharp-dns-server.sln` locally and add notes on troubleshooting (port collisions, residual processes). + +## Acceptance Criteria +- Integration tests spin up `dns-cli` automatically and tear it down reliably across Windows/Linux. +- Tests assert AA, RA, SOA (authority section), and TTL/minimum TTL semantics for both successful and NXDOMAIN responses. +- Repeated queries verify current caching-related TTL behavior so future caching work has a safety net. +- Running `dotnet test csharp-dns-server.sln` executes the suite without manual steps, and documentation reflects the new coverage. From 9236ce6a177cd2f1869d53090f15c548da19625d Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 10:32:34 -0800 Subject: [PATCH 10/31] Bug planning --- docs/task_list.md | 6 ++++++ docs/tasks/task_23_plan.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 docs/tasks/task_23_plan.md diff --git a/docs/task_list.md b/docs/task_list.md index b9cdd80..ae7c496 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -20,3 +20,9 @@ 18. [ ] **T18 – Windows/NT service packaging** — Add installers/scripts so the server can run as a Windows service (issue #5). 19. [ ] **T19 – DNSSEC support** — Add foundational DNSSEC record handling and validation paths (issue #2). 20. [ ] **T20 – Documented static zone workflow** — Provide a simple static zone declaration option (issue #1) for setups that don’t rely on the BIND parser. +21. [ ] **T21 – Fix AppVeyor build configuration** — Repair `appveyor.yml` so CI restores/builds/tests the .NET solution using the current SDK/runtime matrix. +22. [ ] **T22 – Add GitHub Actions CI pipeline** — Introduce a workflow under `.github/workflows/` that restores, builds, and tests the solution on Windows/Linux runners aligned with PR gating guidance. +23. [ ] **T23 – Correct IPv4 RDATA endianness (Critical)** — Fix `ANameRData.Parse` so addresses parsed from wire format are not byte-swapped before being forwarded to clients; add regression tests. +24. [ ] **T24 – Stabilize UDP listener shutdown & endpoint capture (High)** — Ensure `UdpListener.Start` exits cleanly after `Stop()` and capture the remote endpoint per receive so responses aren’t misrouted. +25. [ ] **T25 – Support larger UDP payloads (Medium)** — Increase `UdpListener` buffer sizing and/or detect truncated packets so EDNS-sized responses don’t silently corrupt parsing. +26. [ ] **T26 – Allow full 8-bit DNS labels (Medium)** — Relax `DnsProtocol.ReadString` ASCII enforcement in line with RFC 2181 so internationalized/underscored names don’t throw. diff --git a/docs/tasks/task_23_plan.md b/docs/tasks/task_23_plan.md new file mode 100644 index 0000000..9d8560d --- /dev/null +++ b/docs/tasks/task_23_plan.md @@ -0,0 +1,35 @@ +# Task 23 Plan – Correct IPv4 RDATA Endianness + +## Goal +Fix `ANameRData.Parse` so IPv4 addresses extracted from DNS responses retain the network-order byte layout, preventing byte-swapped answers from being relayed to clients, and ensure the regression never returns. + +## Scope +- **Code**: `Dns/RData.cs` (specifically `ANameRData.Parse` and any related serialization helpers), plus any ancillary utilities that assume host-endian IPv4 storage. +- **Tests**: Extend `dnstest` with unit coverage that parses raw DNS response buffers containing known A records and asserts the resulting `IPAddress` matches the on-wire address. Add an integration-style test that exercises the forwarder path in `DnsServer` to confirm responses are emitted correctly. + +## Steps +1. **Reproduce the issue** + - Craft a byte array that represents a DNS answer with an A record (e.g., `127.0.0.1`) and parse it through the current `ANameRData.Parse` to observe the reversed address (`1.0.0.127`). + - Add a failing unit test in `dnstest` capturing this scenario to guard against regressions. + +2. **Inspect serialization assumptions** + - Review everywhere `IPAddress` instances are read/written (e.g., `ResourceRecord.WriteToStream`, `SmartZoneResolver` address handling) to understand whether we rely on `IPAddress.GetAddressBytes()` (network order) or host-endian integers. + - Confirm the bug is isolated to `ANameRData.Parse` rather than a broader serialization mismatch. + +3. **Implement the fix** + - Update `ANameRData.Parse` to either call `DnsProtocol.ReadUint` and `SwapEndian()` before constructing `IPAddress`, or, preferably, slice the original four bytes (`byte[] address = new byte[4]; Buffer.BlockCopy(...)`) and pass them to `new IPAddress(byte[])`. + - Ensure the change handles both IPv4 and potential future IPv6 extensions gracefully (guarding against `DataLength != 4`). + +4. **Add regression tests** + - Unit test: feed a minimal DNS message containing a single A record into `ResourceList.LoadFrom` and assert the resulting `Address` property equals the source IP. + - End-to-end test: simulate `DnsServer` receiving an upstream response with a known A record and verify the serialized bytes sent to the original client preserve the correct order. + +5. **Documentation and task tracking** + - Update `docs/task_list.md` to mark T23 complete once merged, and reference the new tests in `docs/task_list.md` or release notes if needed. + - Note any follow-on cleanup (e.g., IPv6 handling) discovered during the fix. + +## Acceptance Criteria +- Parsing an IPv4 RDATA blob yields an `IPAddress` matching the on-wire order. +- Forwarded responses from `DnsServer` contain correct byte ordering, validated by tests. +- New unit/integration tests fail prior to the fix and pass afterward. +- No regressions in existing DNS parsing tests.*** From 6c46c20b6353df84502da17c8a033ce11df78797 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 10:40:58 -0800 Subject: [PATCH 11/31] Fixes an endian bug when IPv4 addresses are reconstructed from the wire --- Dns/RData.cs | 10 ++++++++-- dnstest/DnsProtocolTest.cs | 28 ++++++++++++++++++++++++++++ docs/task_list.md | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Dns/RData.cs b/Dns/RData.cs index 2702b4b..81ed5f6 100644 --- a/Dns/RData.cs +++ b/Dns/RData.cs @@ -30,7 +30,13 @@ public IPAddress Address public static ANameRData Parse(byte[] bytes, int offset, int size) { ANameRData aname = new ANameRData(); - uint addressBytes = BitConverter.ToUInt32(bytes, offset); + if (size != 4) + { + throw new InvalidDataException("IPv4 RDATA must be 4 bytes long."); + } + + byte[] addressBytes = new byte[size]; + Buffer.BlockCopy(bytes, offset, addressBytes, 0, size); aname.Address = new IPAddress(addressBytes); return aname; } @@ -198,4 +204,4 @@ public override void Dump() } } -} \ No newline at end of file +} diff --git a/dnstest/DnsProtocolTest.cs b/dnstest/DnsProtocolTest.cs index c36190f..90713f4 100644 --- a/dnstest/DnsProtocolTest.cs +++ b/dnstest/DnsProtocolTest.cs @@ -402,6 +402,34 @@ public void TransitiveQueryTest2() Assert.Equal("8.8.8.9", outMessage.Answers[1].Name); } + [Fact] + public void ARecordParsingKeepsWireByteOrder() + { + byte[] response = + { + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, + 0x00, 0x01, 0x00, 0x01, + 0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x04, + 0xC0, 0x00, 0x02, 0x0A + }; + + Assert.True(DnsMessage.TryParse(response, out var message)); + Assert.Equal(1, message.AnswerCount); + Assert.Single(message.Answers); + + var aRecord = Assert.IsType(message.Answers[0].RData); + Assert.Equal(IPAddress.Parse("192.0.2.10"), aRecord.Address); + + using MemoryStream stream = new MemoryStream(); + message.WriteToStream(stream); + byte[] roundTrip = stream.ToArray(); + Assert.True(roundTrip.Length >= 4); + byte[] emittedAddress = new byte[4]; + Array.Copy(roundTrip, roundTrip.Length - 4, emittedAddress, 0, 4); + Assert.Equal(new byte[] { 0xC0, 0x00, 0x02, 0x0A }, emittedAddress); + } + [Fact] public void Opcode() { diff --git a/docs/task_list.md b/docs/task_list.md index ae7c496..651c5bd 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -22,7 +22,7 @@ 20. [ ] **T20 – Documented static zone workflow** — Provide a simple static zone declaration option (issue #1) for setups that don’t rely on the BIND parser. 21. [ ] **T21 – Fix AppVeyor build configuration** — Repair `appveyor.yml` so CI restores/builds/tests the .NET solution using the current SDK/runtime matrix. 22. [ ] **T22 – Add GitHub Actions CI pipeline** — Introduce a workflow under `.github/workflows/` that restores, builds, and tests the solution on Windows/Linux runners aligned with PR gating guidance. -23. [ ] **T23 – Correct IPv4 RDATA endianness (Critical)** — Fix `ANameRData.Parse` so addresses parsed from wire format are not byte-swapped before being forwarded to clients; add regression tests. +23. [x] **T23 – Correct IPv4 RDATA endianness (Critical)** — Fix `ANameRData.Parse` so addresses parsed from wire format are not byte-swapped before being forwarded to clients; add regression tests. 24. [ ] **T24 – Stabilize UDP listener shutdown & endpoint capture (High)** — Ensure `UdpListener.Start` exits cleanly after `Stop()` and capture the remote endpoint per receive so responses aren’t misrouted. 25. [ ] **T25 – Support larger UDP payloads (Medium)** — Increase `UdpListener` buffer sizing and/or detect truncated packets so EDNS-sized responses don’t silently corrupt parsing. 26. [ ] **T26 – Allow full 8-bit DNS labels (Medium)** — Relax `DnsProtocol.ReadString` ASCII enforcement in line with RFC 2181 so internationalized/underscored names don’t throw. From dce4121967f7808e3f9950b6c79bb2a740f0db27 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 19:07:45 -0800 Subject: [PATCH 12/31] Code Reformatting --- Dns/Config/AppConfig.cs | 46 +-- Dns/Contracts/IAddressDispenser.cs | 2 +- Dns/Contracts/IDnsCache.cs | 2 +- Dns/Contracts/IDnsResolver.cs | 2 +- Dns/Contracts/IHtmlDump.cs | 2 +- Dns/DnsCache.cs | 7 +- Dns/DnsMessage.cs | 16 +- Dns/DnsProtocol.cs | 4 +- Dns/DnsServer.cs | 32 +-- Dns/Extensions.cs | 18 +- Dns/HttpServer.cs | 2 +- Dns/OpCode.cs | 2 +- Dns/Program.cs | 9 +- Dns/Question.cs | 6 +- Dns/QuestionList.cs | 6 +- Dns/RCode.cs | 2 +- Dns/RData.cs | 6 +- Dns/ResourceClass.cs | 2 +- Dns/ResourceList.cs | 18 +- Dns/ResourceRecord.cs | 6 +- Dns/ResourceType.cs | 2 +- Dns/SmartAddressDispenser.cs | 8 +- Dns/SmartZoneResolver.cs | 6 +- Dns/SocketExtensions.cs | 2 +- Dns/UdpListener.cs | 4 +- Dns/Utility/BitPacker.cs | 264 +++++++++--------- Dns/Utility/CsvParser.cs | 68 ++--- Dns/Utility/CsvRow.cs | 8 +- Dns/Zone.cs | 2 +- Dns/ZoneProvider/AP/APZoneProvider.cs | 9 +- Dns/ZoneProvider/BaseZoneProvider.cs | 9 +- Dns/ZoneProvider/Bind/BindZoneProvider.cs | 2 +- Dns/ZoneProvider/FileWatcherZoneProvider.cs | 8 +- Dns/ZoneProvider/IPProbe/Host.cs | 2 +- .../IPProbe/IPProbeProviderOptions.cs | 4 +- .../IPProbe/IPProbeZoneProvider.cs | 22 +- Dns/ZoneProvider/IPProbe/ProbeResult.cs | 2 +- Dns/ZoneProvider/IPProbe/State.cs | 4 +- Dns/ZoneProvider/IPProbe/Target.cs | 2 +- Dns/ZoneRecord.cs | 2 +- dns-cli/Program.cs | 2 +- dnstest/BitPackerTests.cs | 6 +- dnstest/ConfigTests.cs | 5 +- dnstest/DnsCacheTests.cs | 10 +- dnstest/DnsCliAuthoritativeBehaviorTests.cs | 124 ++++++++ dnstest/DnsProtocolTest.cs | 36 +-- 46 files changed, 465 insertions(+), 338 deletions(-) create mode 100644 dnstest/DnsCliAuthoritativeBehaviorTests.cs diff --git a/Dns/Config/AppConfig.cs b/Dns/Config/AppConfig.cs index 7bd2d2c..c5f468e 100644 --- a/Dns/Config/AppConfig.cs +++ b/Dns/Config/AppConfig.cs @@ -1,32 +1,32 @@ -namespace Dns.Config +namespace Dns.Config { - public class AppConfig - { - public ServerOptions Server { get; set; } - } + public class AppConfig + { + public ServerOptions Server { get; set; } + } - public class ServerOptions - { + public class ServerOptions + { public ZoneOptions Zone { get; set; } - public DnsListenerOptions DnsListener { get; set;} + public DnsListenerOptions DnsListener { get; set; } public WebServerOptions WebServer { get; set; } - } + } - public class ZoneOptions - { - public string Name { get; set; } - public string Provider { get; set; } - } + public class ZoneOptions + { + public string Name { get; set; } + public string Provider { get; set; } + } - public class DnsListenerOptions + public class DnsListenerOptions { - public ushort Port { get; set; } - } + public ushort Port { get; set; } + } - public class WebServerOptions - { - public bool Enabled { get; set; } - public int Port { get; set; } - } + public class WebServerOptions + { + public bool Enabled { get; set; } + public int Port { get; set; } + } -} \ No newline at end of file +} diff --git a/Dns/Contracts/IAddressDispenser.cs b/Dns/Contracts/IAddressDispenser.cs index 28329dd..efe19b4 100644 --- a/Dns/Contracts/IAddressDispenser.cs +++ b/Dns/Contracts/IAddressDispenser.cs @@ -15,4 +15,4 @@ public interface IAddressDispenser : IHtmlDump IEnumerable GetAddresses(); } -} \ No newline at end of file +} diff --git a/Dns/Contracts/IDnsCache.cs b/Dns/Contracts/IDnsCache.cs index 5e40a26..6cc91a0 100644 --- a/Dns/Contracts/IDnsCache.cs +++ b/Dns/Contracts/IDnsCache.cs @@ -12,4 +12,4 @@ public interface IDnsCache void Set(string key, byte[] bytes, int ttlSeconds); } -} \ No newline at end of file +} diff --git a/Dns/Contracts/IDnsResolver.cs b/Dns/Contracts/IDnsResolver.cs index 17963b8..519ac5b 100644 --- a/Dns/Contracts/IDnsResolver.cs +++ b/Dns/Contracts/IDnsResolver.cs @@ -17,4 +17,4 @@ internal interface IDnsResolver : IHtmlDump bool TryGetHostEntry(string hostname, ResourceClass resClass, ResourceType resType, out IPHostEntry entry); } -} \ No newline at end of file +} diff --git a/Dns/Contracts/IHtmlDump.cs b/Dns/Contracts/IHtmlDump.cs index 705eb40..e5637c0 100644 --- a/Dns/Contracts/IHtmlDump.cs +++ b/Dns/Contracts/IHtmlDump.cs @@ -12,4 +12,4 @@ public interface IHtmlDump { void DumpHtml(TextWriter writer); } -} \ No newline at end of file +} diff --git a/Dns/DnsCache.cs b/Dns/DnsCache.cs index ebf151c..f549ff7 100644 --- a/Dns/DnsCache.cs +++ b/Dns/DnsCache.cs @@ -7,8 +7,8 @@ namespace Dns { using System; - using Microsoft.Extensions.Caching.Memory; using Dns.Contracts; + using Microsoft.Extensions.Caching.Memory; public class DnsCache : IDnsCache { @@ -17,7 +17,8 @@ public class DnsCache : IDnsCache byte[] IDnsCache.Get(string key) { byte[] entry; - if (_cache.TryGetValue(key, out entry)) { + if (_cache.TryGetValue(key, out entry)) + { return entry; } @@ -30,4 +31,4 @@ void IDnsCache.Set(string key, byte[] bytes, int ttlSeconds) _cache.Set(key, bytes, cacheEntryOptions); } } -} \ No newline at end of file +} diff --git a/Dns/DnsMessage.cs b/Dns/DnsMessage.cs index 0e5b8cd..f10f6c0 100644 --- a/Dns/DnsMessage.cs +++ b/Dns/DnsMessage.cs @@ -56,8 +56,8 @@ public bool QR /// Opcode public byte Opcode { - get { return (byte) ((this.Flags & 0x7800) >> 11); } - set { this.Flags = (ushort) ((this.Flags & ~0x7800) | (value << 11)); } + get { return (byte)((this.Flags & 0x7800) >> 11); } + set { this.Flags = (ushort)((this.Flags & ~0x7800) | (value << 11)); } } /// Is Authorative Answer @@ -179,8 +179,8 @@ public bool CheckingDisabled public byte RCode { - get { return (byte) (this.Flags & 0x000F); } - set { this.Flags = (ushort) ((this.Flags & ~0x000F) | value); } + get { return (byte)(this.Flags & 0x000F); } + set { this.Flags = (ushort)((this.Flags & ~0x000F) | value); } } public ushort AdditionalCount @@ -288,7 +288,7 @@ public void Dump() { Console.WriteLine("QueryIdentifier: 0x{0:X4}", this.QueryIdentifier); Console.WriteLine("QR: ({0}... .... .... ....) {1}", this.QR ? 1 : 0, this.QR ? "Response" : "Query"); - Console.WriteLine("Opcode: (.{0}{1}{2} {3}... .... ....) {4}", (this.Opcode & 1) > 1 ? 1 : 0, (this.Opcode & 2) > 1 ? 1 : 0, (this.Opcode & 4) > 1 ? 1 : 0, (this.Opcode & 8) > 1 ? 1 : 0, (OpCode) (this.Opcode)); + Console.WriteLine("Opcode: (.{0}{1}{2} {3}... .... ....) {4}", (this.Opcode & 1) > 1 ? 1 : 0, (this.Opcode & 2) > 1 ? 1 : 0, (this.Opcode & 4) > 1 ? 1 : 0, (this.Opcode & 8) > 1 ? 1 : 0, (OpCode)(this.Opcode)); Console.WriteLine("AA: (.... .{0}.. .... ....) {1}", this.AA ? 1 : 0, this.AA ? "Authoritative" : "Not Authoritative"); Console.WriteLine("TC: (.... ..{0}. .... ....) {1}", this.TC ? 1 : 0, this.TC ? "Truncated" : "Not Truncated"); Console.WriteLine("RD: (.... ...{0} .... ....) {1}", this.RD ? 1 : 0, this.RD ? "Recursion Desired" : "Recursion not desired"); @@ -296,7 +296,7 @@ public void Dump() Console.WriteLine("Zero: (.... .... .0.. ....) 0"); Console.WriteLine("AuthenticatedData: (.... .... ..{0}. ....) {1}", this.AuthenticatingData ? 1 : 0, this.AuthenticatingData ? "AuthenticatingData" : "Not AuthenticatingData"); Console.WriteLine("CheckingDisabled: (.... .... ...{0} ....) {1}", this.CheckingDisabled ? 1 : 0, this.CheckingDisabled ? "Checking Disabled" : "Not CheckingEnabled"); - Console.WriteLine("RCode: (.... .... .... {0}{1}{2}{3}) {4}", (this.RCode & 1) > 1 ? 1 : 0, (this.RCode & 2) > 1 ? 1 : 0, (this.RCode & 4) > 1 ? 1 : 0, (this.RCode & 8) > 1 ? 1 : 0, (RCode) (this.RCode)); + Console.WriteLine("RCode: (.... .... .... {0}{1}{2}{3}) {4}", (this.RCode & 1) > 1 ? 1 : 0, (this.RCode & 2) > 1 ? 1 : 0, (this.RCode & 4) > 1 ? 1 : 0, (this.RCode & 8) > 1 ? 1 : 0, (RCode)(this.RCode)); Console.WriteLine("QuestionCount: 0x{0:X4}", this.QuestionCount); Console.WriteLine("AnswerCount: 0x{0:X4}", this.AnswerCount); Console.WriteLine("NameServerCount: 0x{0:X4}", this.NameServerCount); @@ -316,7 +316,7 @@ public void Dump() { foreach (ResourceRecord resource in this.Answers) { - Console.WriteLine("Record: {0} of type {1} on class {2}", resource.Name, (ResourceType) resource.Type, (ResourceClass)resource.Class); + Console.WriteLine("Record: {0} of type {1} on class {2}", resource.Name, (ResourceType)resource.Type, (ResourceClass)resource.Class); resource.Dump(); Console.WriteLine(); } @@ -368,4 +368,4 @@ public static bool TryParse(byte[] bytes, out DnsMessage query) } } } -} \ No newline at end of file +} diff --git a/Dns/DnsProtocol.cs b/Dns/DnsProtocol.cs index 44d05b5..053ac0a 100644 --- a/Dns/DnsProtocol.cs +++ b/Dns/DnsProtocol.cs @@ -30,14 +30,14 @@ public static bool TryParse(byte[] bytes, out DnsMessage dnsMessage) public static ushort ReadUshort(byte[] bytes, ref int offset) { ushort ret = BitConverter.ToUInt16(bytes, offset); - offset += sizeof (ushort); + offset += sizeof(ushort); return ret; } public static uint ReadUint(byte[] bytes, ref int offset) { uint ret = BitConverter.ToUInt32(bytes, offset); - offset += sizeof (uint); + offset += sizeof(uint); return ret; } diff --git a/Dns/DnsServer.cs b/Dns/DnsServer.cs index 912ee7c..9130c96 100644 --- a/Dns/DnsServer.cs +++ b/Dns/DnsServer.cs @@ -9,14 +9,14 @@ namespace Dns { using System; + using System.Collections.Generic; using System.IO; + using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading; using Dns.Contracts; using Microsoft.Win32; - using System.Linq; - using System.Collections.Generic; internal class DnsServer : IHtmlDump { @@ -45,7 +45,7 @@ public void Initialize(IDnsResolver resolver) _resolver = resolver; _udpListener = new UdpListener(); - + _udpListener.Initialize(this.port); _udpListener.OnRequest += ProcessUdpRequest; @@ -79,7 +79,7 @@ private void ProcessUdpRequest(byte[] buffer, EndPoint remoteEndPoint) { foreach (Question question in message.Questions) { - Console.WriteLine("{0} asked for {1} {2} {3}", remoteEndPoint.ToString(),question.Name, question.Class, question.Type); + Console.WriteLine("{0} asked for {1} {2} {3}", remoteEndPoint.ToString(), question.Name, question.Class, question.Type); IPHostEntry entry; if (question.Type == ResourceType.PTR) { @@ -89,7 +89,7 @@ private void ProcessUdpRequest(byte[] buffer, EndPoint remoteEndPoint) message.AA = true; message.RA = false; message.AnswerCount++; - message.Answers.Add(new ResourceRecord {Name = question.Name, Class = ResourceClass.IN, Type = ResourceType.PTR, TTL = 3600, DataLength = 0xB, RData = new DomainNamePointRData() {Name = "localhost"}}); + message.Answers.Add(new ResourceRecord { Name = question.Name, Class = ResourceClass.IN, Type = ResourceType.PTR, TTL = 3600, DataLength = 0xB, RData = new DomainNamePointRData() { Name = "localhost" } }); } } else if (_resolver.TryGetHostEntry(question.Name, question.Class, question.Type, out entry)) // Right zone, hostname/machine function does exist @@ -97,11 +97,11 @@ private void ProcessUdpRequest(byte[] buffer, EndPoint remoteEndPoint) message.QR = true; message.AA = true; message.RA = false; - message.RCode = (byte) RCode.NOERROR; + message.RCode = (byte)RCode.NOERROR; foreach (IPAddress address in entry.AddressList) { message.AnswerCount++; - message.Answers.Add(new ResourceRecord {Name = question.Name, Class = ResourceClass.IN, Type = ResourceType.A, TTL = 10, RData = new ANameRData {Address = address}}); + message.Answers.Add(new ResourceRecord { Name = question.Name, Class = ResourceClass.IN, Type = ResourceType.A, TTL = 10, RData = new ANameRData { Address = address } }); } } else if (question.Name.EndsWith(_resolver.GetZoneName())) // Right zone, but the hostname/machine function doesn't exist @@ -109,16 +109,16 @@ private void ProcessUdpRequest(byte[] buffer, EndPoint remoteEndPoint) message.QR = true; message.AA = true; message.RA = false; - message.RCode = (byte) RCode.NXDOMAIN; + message.RCode = (byte)RCode.NXDOMAIN; message.AnswerCount = 0; message.Answers.Clear(); - var soaResourceData = new StatementOfAuthorityRData() {PrimaryNameServer = Environment.MachineName, ResponsibleAuthoritativeMailbox = "stephbu." + Environment.MachineName, Serial = _resolver.GetZoneSerial(), ExpirationLimit = 86400, RetryInterval = 300, RefreshInterval = 300, MinimumTTL = 300}; - var soaResourceRecord = new ResourceRecord {Class = ResourceClass.IN, Type = ResourceType.SOA, TTL = 300, RData = soaResourceData}; + var soaResourceData = new StatementOfAuthorityRData() { PrimaryNameServer = Environment.MachineName, ResponsibleAuthoritativeMailbox = "stephbu." + Environment.MachineName, Serial = _resolver.GetZoneSerial(), ExpirationLimit = 86400, RetryInterval = 300, RefreshInterval = 300, MinimumTTL = 300 }; + var soaResourceRecord = new ResourceRecord { Class = ResourceClass.IN, Type = ResourceType.SOA, TTL = 300, RData = soaResourceData }; message.NameServerCount++; message.Authorities.Add(soaResourceRecord); } - // + // else // Referral to regular DC DNS servers { // store current IP address and Query ID. @@ -142,13 +142,13 @@ private void ProcessUdpRequest(byte[] buffer, EndPoint remoteEndPoint) // send to upstream DNS servers foreach (IPAddress dnsServer in _defaultDns) { - SendUdp(responseStream.GetBuffer(), 0, (int) responseStream.Position, new IPEndPoint(dnsServer, 53)); + SendUdp(responseStream.GetBuffer(), 0, (int)responseStream.Position, new IPEndPoint(dnsServer, 53)); } } else { Interlocked.Increment(ref _responses); - SendUdp(responseStream.GetBuffer(), 0, (int) responseStream.Position, remoteEndPoint); + SendUdp(responseStream.GetBuffer(), 0, (int)responseStream.Position, remoteEndPoint); } } } @@ -238,7 +238,7 @@ private void SendUdp(byte[] bytes, int offset, int count, EndPoint remoteEndpoin /// List of configured DNS names private IEnumerable GetDefaultDNS() { - NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); + NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces(); foreach (NetworkInterface adapter in adapters) { @@ -248,10 +248,10 @@ private IEnumerable GetDefaultDNS() foreach (IPAddress dns in dnsServers) { Console.WriteLine("Discovered DNS: {0}", dns); - + yield return dns; } - + } } diff --git a/Dns/Extensions.cs b/Dns/Extensions.cs index c4ad175..07c6333 100644 --- a/Dns/Extensions.cs +++ b/Dns/Extensions.cs @@ -21,7 +21,7 @@ public static TextWriter CreateWriter(this Stream stream, Encoding encoding = nu public static ushort SwapEndian(this ushort val) { - ushort value = (ushort) ((val << 8) | (val >> 8)); + ushort value = (ushort)((val << 8) | (val >> 8)); return value; } @@ -42,7 +42,7 @@ public static byte[] GetResourceBytes(this string str, char delimiter = '.') using (MemoryStream stream = new MemoryStream(str.Length + 2)) { - string[] segments = str.Split(new char[] {'.'}); + string[] segments = str.Split(new char[] { '.' }); foreach (string segment in segments) { stream.WriteByte((byte)segment.Length); @@ -89,14 +89,14 @@ public static string IP(long ipLong) long tempLong, temp; tempLong = ipLong; - temp = tempLong/(256*256*256); - tempLong = tempLong - (temp*256*256*256); + temp = tempLong / (256 * 256 * 256); + tempLong = tempLong - (temp * 256 * 256 * 256); b.Append(Convert.ToString(temp)).Append("."); - temp = tempLong/(256*256); - tempLong = tempLong - (temp*256*256); + temp = tempLong / (256 * 256); + tempLong = tempLong - (temp * 256 * 256); b.Append(Convert.ToString(temp)).Append("."); - temp = tempLong/256; - tempLong = tempLong - (temp*256); + temp = tempLong / 256; + tempLong = tempLong - (temp * 256); b.Append(Convert.ToString(temp)).Append("."); temp = tempLong; tempLong = tempLong - temp; @@ -120,4 +120,4 @@ public static void WriteToStream(this uint value, Stream stream) stream.WriteByte((byte)((value >> 24) & 0xFF)); } } -} \ No newline at end of file +} diff --git a/Dns/HttpServer.cs b/Dns/HttpServer.cs index f4d32c6..d77aa42 100644 --- a/Dns/HttpServer.cs +++ b/Dns/HttpServer.cs @@ -165,4 +165,4 @@ public void DumpHtml(TextWriter writer) writer.WriteLine("600: {0}
", this._request600); } } -} \ No newline at end of file +} diff --git a/Dns/OpCode.cs b/Dns/OpCode.cs index f51f155..f993f53 100644 --- a/Dns/OpCode.cs +++ b/Dns/OpCode.cs @@ -14,4 +14,4 @@ public enum OpCode NOTIFY = 4, UPDATE = 5, } -} \ No newline at end of file +} diff --git a/Dns/Program.cs b/Dns/Program.cs index 79b7cbe..6fba9c8 100644 --- a/Dns/Program.cs +++ b/Dns/Program.cs @@ -12,9 +12,8 @@ namespace Dns using System.Net; using System.Threading; using Dns.ZoneProvider.AP; - - using Ninject; using Microsoft.Extensions.Configuration; + using Ninject; public class Program { @@ -62,7 +61,7 @@ public static void Run(string configFile, CancellationToken ct) _zoneProvider.Start(ct); _dnsServer.Start(ct); - if(appConfig.Server.WebServer.Enabled) + if (appConfig.Server.WebServer.Enabled) { _httpServer.Initialize(string.Format("http://+:{0}/", appConfig.Server.WebServer.Port)); _httpServer.OnProcessRequest += _httpServer_OnProcessRequest; @@ -83,7 +82,7 @@ private static void _httpServer_OnProcessRequest(HttpListenerContext context) string rawUrl = context.Request.RawUrl; if (rawUrl == "/dump/dnsresolver") { - context.Response.Headers.Add("Content-Type","text/html"); + context.Response.Headers.Add("Content-Type", "text/html"); using (TextWriter writer = context.Response.OutputStream.CreateWriter()) { _zoneResolver.DumpHtml(writer); @@ -129,4 +128,4 @@ private static Type ByName(string name) return null; } } -} \ No newline at end of file +} diff --git a/Dns/Question.cs b/Dns/Question.cs index ac678de..b118b71 100644 --- a/Dns/Question.cs +++ b/Dns/Question.cs @@ -21,10 +21,10 @@ public void WriteToStream(Stream stream) stream.Write(name, 0, name.Length); // Type - stream.Write(BitConverter.GetBytes(((ushort) (this.Type)).SwapEndian()), 0, 2); + stream.Write(BitConverter.GetBytes(((ushort)(this.Type)).SwapEndian()), 0, 2); // Class - stream.Write(BitConverter.GetBytes(((ushort) this.Class).SwapEndian()), 0, 2); + stream.Write(BitConverter.GetBytes(((ushort)this.Class).SwapEndian()), 0, 2); } } -} \ No newline at end of file +} diff --git a/Dns/QuestionList.cs b/Dns/QuestionList.cs index 960aa11..c1a5531 100644 --- a/Dns/QuestionList.cs +++ b/Dns/QuestionList.cs @@ -24,10 +24,10 @@ public int LoadFrom(byte[] bytes, int offset, ushort count) question.Name = DnsProtocol.ReadString(bytes, ref currentOffset); - question.Type = (ResourceType) (BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); + question.Type = (ResourceType)(BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); currentOffset += 2; - question.Class = (ResourceClass) (BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); + question.Class = (ResourceClass)(BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); currentOffset += 2; this.Add(question); @@ -48,4 +48,4 @@ public long WriteToStream(Stream stream) return end - start; } } -} \ No newline at end of file +} diff --git a/Dns/RCode.cs b/Dns/RCode.cs index e4b4c66..6dea0d2 100644 --- a/Dns/RCode.cs +++ b/Dns/RCode.cs @@ -21,4 +21,4 @@ public enum RCode NOTZONE = 10, BADVERS = 16, } -} \ No newline at end of file +} diff --git a/Dns/RData.cs b/Dns/RData.cs index 81ed5f6..98383f3 100644 --- a/Dns/RData.cs +++ b/Dns/RData.cs @@ -23,7 +23,7 @@ public class ANameRData : RData { public IPAddress Address { - get; + get; set; } @@ -67,7 +67,7 @@ public override ushort Length // dots replaced by bytes // + 1 segment prefix // + 1 null terminator - get { return (ushort) (Name.Length + 2); } + get { return (ushort)(Name.Length + 2); } } public static CNameRData Parse(byte[] bytes, int offset, int size) @@ -178,7 +178,7 @@ public override ushort Length // dots replaced by bytes // + 1 segment prefix // + 1 null terminator - get { return (ushort) (PrimaryNameServer.Length + 2 + ResponsibleAuthoritativeMailbox.Length + 2 + 20); } + get { return (ushort)(PrimaryNameServer.Length + 2 + ResponsibleAuthoritativeMailbox.Length + 2 + 20); } } public override void WriteToStream(Stream stream) diff --git a/Dns/ResourceClass.cs b/Dns/ResourceClass.cs index abeb76c..6565eeb 100644 --- a/Dns/ResourceClass.cs +++ b/Dns/ResourceClass.cs @@ -14,4 +14,4 @@ public enum ResourceClass : ushort CH = 3, HS = 4 } -} \ No newline at end of file +} diff --git a/Dns/ResourceList.cs b/Dns/ResourceList.cs index 96c4fa4..6c48f0c 100644 --- a/Dns/ResourceList.cs +++ b/Dns/ResourceList.cs @@ -25,17 +25,17 @@ public int LoadFrom(byte[] bytes, int offset, ushort count) resourceRecord.Name = DnsProtocol.ReadString(bytes, ref currentOffset); - resourceRecord.Type = (ResourceType) (BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); - currentOffset += sizeof (ushort); + resourceRecord.Type = (ResourceType)(BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); + currentOffset += sizeof(ushort); - resourceRecord.Class = (ResourceClass) (BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); - currentOffset += sizeof (ushort); + resourceRecord.Class = (ResourceClass)(BitConverter.ToUInt16(bytes, currentOffset).SwapEndian()); + currentOffset += sizeof(ushort); resourceRecord.TTL = BitConverter.ToUInt32(bytes, currentOffset).SwapEndian(); - currentOffset += sizeof (uint); + currentOffset += sizeof(uint); resourceRecord.DataLength = BitConverter.ToUInt16(bytes, currentOffset).SwapEndian(); - currentOffset += sizeof (ushort); + currentOffset += sizeof(ushort); if (resourceRecord.Class == ResourceClass.IN && resourceRecord.Type == ResourceType.A) { @@ -45,6 +45,10 @@ public int LoadFrom(byte[] bytes, int offset, ushort count) { resourceRecord.RData = CNameRData.Parse(bytes, currentOffset, resourceRecord.DataLength); } + else if (resourceRecord.Type == ResourceType.SOA) + { + resourceRecord.RData = StatementOfAuthorityRData.Parse(bytes, currentOffset, resourceRecord.DataLength); + } // move past resource data record currentOffset = currentOffset + resourceRecord.DataLength; @@ -64,4 +68,4 @@ public void WriteToStream(Stream stream) } } } -} \ No newline at end of file +} diff --git a/Dns/ResourceRecord.cs b/Dns/ResourceRecord.cs index 4e4fbb6..99762a7 100644 --- a/Dns/ResourceRecord.cs +++ b/Dns/ResourceRecord.cs @@ -15,7 +15,7 @@ public class ResourceRecord public uint TTL { get; set; } public ResourceClass Class { get; set; } public ResourceType Type { get; set; } - public RData RData { get; set;} + public RData RData { get; set; } public ushort DataLength { get; set; } /// Serialize resource to stream according to RFC1034 format @@ -27,7 +27,7 @@ public void WriteToStream(Stream stream) ((ushort)(this.Class)).SwapEndian().WriteToStream(stream); this.TTL.SwapEndian().WriteToStream(stream); - if(this.RData != null) + if (this.RData != null) { this.RData.Length.SwapEndian().WriteToStream(stream); this.RData.WriteToStream(stream); @@ -55,4 +55,4 @@ public void Dump() } } -} \ No newline at end of file +} diff --git a/Dns/ResourceType.cs b/Dns/ResourceType.cs index c7d5448..436584b 100644 --- a/Dns/ResourceType.cs +++ b/Dns/ResourceType.cs @@ -71,4 +71,4 @@ public enum ResourceType : ushort WINSR = 0xff02, NBSTAT = WINSR } -} \ No newline at end of file +} diff --git a/Dns/SmartAddressDispenser.cs b/Dns/SmartAddressDispenser.cs index cfb95d0..6428fd1 100644 --- a/Dns/SmartAddressDispenser.cs +++ b/Dns/SmartAddressDispenser.cs @@ -37,15 +37,15 @@ public IEnumerable GetAddresses() { IPAddress[] addresses = _zoneRecord.Addresses; - if(addresses.Length == 0) + if (addresses.Length == 0) { yield break; } // starting position in rollover list - int start = (int) (_sequence % (ulong) addresses.Length); + int start = (int)(_sequence % (ulong)addresses.Length); int offset = start; - + uint count = 0; while (true) { @@ -85,4 +85,4 @@ public void DumpHtml(TextWriter writer) } } } -} \ No newline at end of file +} diff --git a/Dns/SmartZoneResolver.cs b/Dns/SmartZoneResolver.cs index 1bac477..e0d7c66 100644 --- a/Dns/SmartZoneResolver.cs +++ b/Dns/SmartZoneResolver.cs @@ -39,7 +39,7 @@ public Zone Zone get { return this._zone; } set { - if(value == null) throw new ArgumentNullException("value"); + if (value == null) throw new ArgumentNullException("value"); this._zone = value; this._zoneReload = DateTime.Now; @@ -107,7 +107,7 @@ public bool TryGetHostEntry(string hostName, ResourceClass resClass, ResourceTyp if (_zoneMap.TryGetValue(key, out dispenser)) { Interlocked.Increment(ref this._hits); - entry = new IPHostEntry {AddressList = dispenser.GetAddresses().ToArray(), Aliases = new string[] {}, HostName = hostName}; + entry = new IPHostEntry { AddressList = dispenser.GetAddresses().ToArray(), Aliases = new string[] { }, HostName = hostName }; return true; } @@ -144,4 +144,4 @@ private string GenerateKey(string host, ResourceClass resClass, ResourceType res return string.Format("{0}|{1}|{2}", host, resClass, resType); } } -} \ No newline at end of file +} diff --git a/Dns/SocketExtensions.cs b/Dns/SocketExtensions.cs index 2447a78..101a6ba 100644 --- a/Dns/SocketExtensions.cs +++ b/Dns/SocketExtensions.cs @@ -30,4 +30,4 @@ public static SocketAwaitable SendToAsync(this Socket socket, SocketAwaitable aw return awaitable; } } -} \ No newline at end of file +} diff --git a/Dns/UdpListener.cs b/Dns/UdpListener.cs index a531d8c..bee3fe0 100644 --- a/Dns/UdpListener.cs +++ b/Dns/UdpListener.cs @@ -142,8 +142,8 @@ public void GetResult() { if (m_eventArgs.SocketError != SocketError.Success) { - throw new SocketException((int) m_eventArgs.SocketError); + throw new SocketException((int)m_eventArgs.SocketError); } } } -} \ No newline at end of file +} diff --git a/Dns/Utility/BitPacker.cs b/Dns/Utility/BitPacker.cs index fd8ea7e..0a096c0 100644 --- a/Dns/Utility/BitPacker.cs +++ b/Dns/Utility/BitPacker.cs @@ -1,4 +1,4 @@ -// // //------------------------------------------------------------------------------------------------- +// // //------------------------------------------------------------------------------------------------- // // // // // // Copyright (c) Steve Butler. All rights reserved. // // // @@ -9,134 +9,134 @@ namespace Dns.Utility using System; public class BitPacker - { - private readonly byte[] _buffer; - private int _bitOffset = 0; - private readonly int[] _mask = new[] {1, 2, 4, 8, 16, 32, 64, 128}; - - public BitPacker(byte[] buffer) - { - this._buffer = buffer; - } - - /// - /// - public byte GetByte(int count) - { - if (count > 8) throw new ArgumentOutOfRangeException("count"); - - int bit = this._bitOffset; - - if (((bit + count) / 8) > this._buffer.Length) throw new ArgumentOutOfRangeException("count"); - - int byteNumber; - ushort bitOffset; - - this.GenerateInitialOffset(out byteNumber, out bitOffset); - - ushort span = BitConverter.ToUInt16(this._buffer, byteNumber); - - int mask = GetMask(count, bitOffset); - int value = (span & mask) >> bitOffset; - - this._bitOffset += count; - return (byte) value; - } - - public enum Endian - { - HiLo, - LoHi - } - - public ushort GetUshort(int count = 16, Endian endian = Endian.LoHi) - { - if (count > 16) throw new ArgumentOutOfRangeException("count"); - - int bit = this._bitOffset; - - if (((bit + count) / 8) > this._buffer.Length) throw new ArgumentOutOfRangeException("count"); - - int byteNumber; - ushort bitOffset; - - this.GenerateInitialOffset(out byteNumber, out bitOffset); - - uint span; - if (byteNumber + 4 <= this._buffer.Length) - { - span = BitConverter.ToUInt32(this._buffer, byteNumber); - } - else - { - // buffer too small - clone bytes into an empty buffer - byte[] copy = new byte[8]; - Array.Copy(this._buffer, byteNumber, copy, byteNumber, this._buffer.Length - byteNumber); - span = BitConverter.ToUInt32(copy, byteNumber); - } - - int mask = GetMask(count, bitOffset); - ushort value = (ushort) ((span & mask) >> bitOffset); - - if (endian == Endian.HiLo) - { - SwapEndian(ref value); - } - - this._bitOffset += count; - return value; - } - - private static int GetMask(int count, ushort bitOffset) - { - int mask = 0; - for (int index = bitOffset; index < bitOffset + count; index++) - { - mask = mask + (int) Math.Pow(2, index); - } - return mask; - } - - public bool GetBoolean() - { - int byteNumber; - ushort bitOffset; - - this.GenerateInitialOffset(out byteNumber, out bitOffset); - int bitMask = this._mask[bitOffset]; - - bool value = (this._buffer[byteNumber] & bitMask) > 0; - - this._bitOffset++; - return value; - } - - public void Reset() - { - this._bitOffset = 0; - } - - public int Write(byte value, uint count) - { - - // generate bit - return 0; - } - - private void GenerateInitialOffset(out int index, out ushort offset) - { - index = this._bitOffset == 0 ? 0 : (this._bitOffset / 8); - offset = (ushort)(this._bitOffset - (index * 8)); - } - - public static void SwapEndian(ref ushort val) - { - val = (ushort)((val << 8) | (val >> 8)); - } - - public static void SwapEndian(ref uint val) - { - val = (val<<24) | ((val<<8) & 0x00ff0000) | ((val>>8) & 0x0000ff00) | (val>>24); - } - } -} \ No newline at end of file + { + private readonly byte[] _buffer; + private int _bitOffset = 0; + private readonly int[] _mask = new[] { 1, 2, 4, 8, 16, 32, 64, 128 }; + + public BitPacker(byte[] buffer) + { + this._buffer = buffer; + } + + /// + /// + public byte GetByte(int count) + { + if (count > 8) throw new ArgumentOutOfRangeException("count"); + + int bit = this._bitOffset; + + if (((bit + count) / 8) > this._buffer.Length) throw new ArgumentOutOfRangeException("count"); + + int byteNumber; + ushort bitOffset; + + this.GenerateInitialOffset(out byteNumber, out bitOffset); + + ushort span = BitConverter.ToUInt16(this._buffer, byteNumber); + + int mask = GetMask(count, bitOffset); + int value = (span & mask) >> bitOffset; + + this._bitOffset += count; + return (byte)value; + } + + public enum Endian + { + HiLo, + LoHi + } + + public ushort GetUshort(int count = 16, Endian endian = Endian.LoHi) + { + if (count > 16) throw new ArgumentOutOfRangeException("count"); + + int bit = this._bitOffset; + + if (((bit + count) / 8) > this._buffer.Length) throw new ArgumentOutOfRangeException("count"); + + int byteNumber; + ushort bitOffset; + + this.GenerateInitialOffset(out byteNumber, out bitOffset); + + uint span; + if (byteNumber + 4 <= this._buffer.Length) + { + span = BitConverter.ToUInt32(this._buffer, byteNumber); + } + else + { + // buffer too small - clone bytes into an empty buffer + byte[] copy = new byte[8]; + Array.Copy(this._buffer, byteNumber, copy, byteNumber, this._buffer.Length - byteNumber); + span = BitConverter.ToUInt32(copy, byteNumber); + } + + int mask = GetMask(count, bitOffset); + ushort value = (ushort)((span & mask) >> bitOffset); + + if (endian == Endian.HiLo) + { + SwapEndian(ref value); + } + + this._bitOffset += count; + return value; + } + + private static int GetMask(int count, ushort bitOffset) + { + int mask = 0; + for (int index = bitOffset; index < bitOffset + count; index++) + { + mask = mask + (int)Math.Pow(2, index); + } + return mask; + } + + public bool GetBoolean() + { + int byteNumber; + ushort bitOffset; + + this.GenerateInitialOffset(out byteNumber, out bitOffset); + int bitMask = this._mask[bitOffset]; + + bool value = (this._buffer[byteNumber] & bitMask) > 0; + + this._bitOffset++; + return value; + } + + public void Reset() + { + this._bitOffset = 0; + } + + public int Write(byte value, uint count) + { + + // generate bit + return 0; + } + + private void GenerateInitialOffset(out int index, out ushort offset) + { + index = this._bitOffset == 0 ? 0 : (this._bitOffset / 8); + offset = (ushort)(this._bitOffset - (index * 8)); + } + + public static void SwapEndian(ref ushort val) + { + val = (ushort)((val << 8) | (val >> 8)); + } + + public static void SwapEndian(ref uint val) + { + val = (val << 24) | ((val << 8) & 0x00ff0000) | ((val >> 8) & 0x0000ff00) | (val >> 24); + } + } +} diff --git a/Dns/Utility/CsvParser.cs b/Dns/Utility/CsvParser.cs index be3f7e4..3760664 100644 --- a/Dns/Utility/CsvParser.cs +++ b/Dns/Utility/CsvParser.cs @@ -1,4 +1,4 @@ -// //------------------------------------------------------------------------------------------------- +// //------------------------------------------------------------------------------------------------- // // // // Copyright (c) Steve Butler. All rights reserved. // // @@ -14,8 +14,8 @@ namespace Dns.Utility /// Parses CSV files public class CsvParser { - private static readonly char[] CSVDELIMITER = new[] {','}; - private static readonly char[] COLONDELIMITER = new[] {':'}; + private static readonly char[] CSVDELIMITER = new[] { ',' }; + private static readonly char[] COLONDELIMITER = new[] { ':' }; private readonly string _filePath; @@ -46,43 +46,43 @@ public IEnumerable Rows { using (FileStream stream = new FileStream(this._filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) using (StreamReader csvReader = new StreamReader(stream)) - while (true) - { - if (csvReader.Peek() < 0) + while (true) { - yield break; - } + if (csvReader.Peek() < 0) + { + yield break; + } - this._currentLine = csvReader.ReadLine(); - if (this._currentLine == null) - { - yield break; - } - if(this._currentLine.Trim() == string.Empty) - { - continue; - } - if ("#;".Contains(this._currentLine[0])) - { - // is a comment - if (this._currentLine.Length > 1 && this._currentLine.Substring(1).StartsWith("Fields")) + this._currentLine = csvReader.ReadLine(); + if (this._currentLine == null) { - string[] fieldDeclaration = this._currentLine.Split(COLONDELIMITER); - if (fieldDeclaration.Length != 2) - { - this._fields = null; - } - else + yield break; + } + if (this._currentLine.Trim() == string.Empty) + { + continue; + } + if ("#;".Contains(this._currentLine[0])) + { + // is a comment + if (this._currentLine.Length > 1 && this._currentLine.Substring(1).StartsWith("Fields")) { - this._fields = fieldDeclaration[1].Trim().Split(CSVDELIMITER); + string[] fieldDeclaration = this._currentLine.Split(COLONDELIMITER); + if (fieldDeclaration.Length != 2) + { + this._fields = null; + } + else + { + this._fields = fieldDeclaration[1].Trim().Split(CSVDELIMITER); + } } } + else + { + yield return new CsvRow(this._fields, this._currentLine.Split(CSVDELIMITER)); + } } - else - { - yield return new CsvRow(this._fields, this._currentLine.Split(CSVDELIMITER)); - } - } } } @@ -107,4 +107,4 @@ public static CsvParser Create(string filePath) return result; } } -} \ No newline at end of file +} diff --git a/Dns/Utility/CsvRow.cs b/Dns/Utility/CsvRow.cs index c558eca..63b0d68 100644 --- a/Dns/Utility/CsvRow.cs +++ b/Dns/Utility/CsvRow.cs @@ -1,4 +1,4 @@ -// //------------------------------------------------------------------------------------------------- +// //------------------------------------------------------------------------------------------------- // // // // Copyright (c) Steve Butler. All rights reserved. // // @@ -19,8 +19,8 @@ internal CsvRow(string[] fields, string[] fieldValues) this._fieldValues = fieldValues; if ((fields != null) && (fields.Length == fieldValues.Length)) { - - for(int index = 0; index < fields.Length; index++) + + for (int index = 0; index < fields.Length; index++) { this._fieldsByName[fields[index]] = fieldValues[index]; } @@ -51,4 +51,4 @@ public string this[string name] } } } -} \ No newline at end of file +} diff --git a/Dns/Zone.cs b/Dns/Zone.cs index 9de4f30..dc0df5b 100644 --- a/Dns/Zone.cs +++ b/Dns/Zone.cs @@ -20,4 +20,4 @@ public void Initialize(IEnumerable nameRecords) this.AddRange(nameRecords); } } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/AP/APZoneProvider.cs b/Dns/ZoneProvider/AP/APZoneProvider.cs index bf007e4..cd6409e 100644 --- a/Dns/ZoneProvider/AP/APZoneProvider.cs +++ b/Dns/ZoneProvider/AP/APZoneProvider.cs @@ -7,11 +7,10 @@ namespace Dns.ZoneProvider.AP { using System.IO; - using System.Net; using System.Linq; + using System.Net; using Dns.Utility; using Dns.ZoneProvider; - using Microsoft.Extensions.Configuration; /// Source of Zone records @@ -26,11 +25,11 @@ public override Zone GenerateZone() } CsvParser parser = CsvParser.Create(this.Filename); - var machines = parser.Rows.Select(row => new {MachineFunction = row["MachineFunction"], StaticIP = row["StaticIP"], MachineName = row["MachineName"]}).ToArray(); + var machines = parser.Rows.Select(row => new { MachineFunction = row["MachineFunction"], StaticIP = row["StaticIP"], MachineName = row["MachineName"] }).ToArray(); var zoneRecords = machines .GroupBy(machine => machine.MachineFunction + this.Zone, machine => IPAddress.Parse(machine.StaticIP)) - .Select(group => new ZoneRecord {Host = group.Key, Count = group.Count(), Addresses = group.Select(address => address).ToArray()}) + .Select(group => new ZoneRecord { Host = group.Key, Count = group.Count(), Addresses = group.Select(address => address).ToArray() }) .ToArray(); Zone zone = new Zone(); @@ -43,4 +42,4 @@ public override Zone GenerateZone() return zone; } } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/BaseZoneProvider.cs b/Dns/ZoneProvider/BaseZoneProvider.cs index b2c06ab..fc159ff 100644 --- a/Dns/ZoneProvider/BaseZoneProvider.cs +++ b/Dns/ZoneProvider/BaseZoneProvider.cs @@ -1,10 +1,9 @@ -namespace Dns.ZoneProvider +namespace Dns.ZoneProvider { using System; using System.Collections.Generic; - using System.Threading.Tasks; using System.Threading; - + using System.Threading.Tasks; using Microsoft.Extensions.Configuration; public abstract class BaseZoneProvider : IObservable, IDisposable @@ -70,6 +69,6 @@ public void Notify(Zone zone) } public abstract void Start(CancellationToken ct); - + } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/Bind/BindZoneProvider.cs b/Dns/ZoneProvider/Bind/BindZoneProvider.cs index 5a04b7d..29f1a90 100644 --- a/Dns/ZoneProvider/Bind/BindZoneProvider.cs +++ b/Dns/ZoneProvider/Bind/BindZoneProvider.cs @@ -36,4 +36,4 @@ public override void Dispose() throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/FileWatcherZoneProvider.cs b/Dns/ZoneProvider/FileWatcherZoneProvider.cs index 2d9d31b..363db82 100644 --- a/Dns/ZoneProvider/FileWatcherZoneProvider.cs +++ b/Dns/ZoneProvider/FileWatcherZoneProvider.cs @@ -20,7 +20,7 @@ public abstract class FileWatcherZoneProvider : BaseZoneProvider public event FileWatcherDelegate OnDeleted = delegate { }; public event FileWatcherDelegate OnRenamed = delegate { }; public event FileWatcherDelegate OnChanged = delegate { }; - public event FileWatcherDelegate OnSettlement = delegate {}; + public event FileWatcherDelegate OnSettlement = delegate { }; private FileSystemWatcher _fileWatcher; private TimeSpan _settlement = TimeSpan.FromSeconds(10); @@ -57,11 +57,11 @@ public override void Initialize(IConfiguration config, string zoneName) } - string directory = Path.GetDirectoryName(filename); + string directory = Path.GetDirectoryName(filename); string fileNameFilter = Path.GetFileName(filename); this.Filename = filename; - this._fileWatcher = new FileSystemWatcher(directory, fileNameFilter); + this._fileWatcher = new FileSystemWatcher(directory, fileNameFilter); this._fileWatcher.Created += (s, e) => this.OnCreated(s, e); this._fileWatcher.Changed += (s, e) => this.OnChanged(s, e); @@ -126,4 +126,4 @@ public override void Dispose() } } } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/IPProbe/Host.cs b/Dns/ZoneProvider/IPProbe/Host.cs index bac3ca8..97124d8 100644 --- a/Dns/ZoneProvider/IPProbe/Host.cs +++ b/Dns/ZoneProvider/IPProbe/Host.cs @@ -8,4 +8,4 @@ internal class Host internal AvailabilityMode AvailabilityMode { get; set; } internal List AddressProbes = new List(); } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/IPProbe/IPProbeProviderOptions.cs b/Dns/ZoneProvider/IPProbe/IPProbeProviderOptions.cs index 92b30b3..df59892 100644 --- a/Dns/ZoneProvider/IPProbe/IPProbeProviderOptions.cs +++ b/Dns/ZoneProvider/IPProbe/IPProbeProviderOptions.cs @@ -1,4 +1,4 @@ -namespace Dns.ZoneProvider.IPProbe +namespace Dns.ZoneProvider.IPProbe { public class IPProbeProviderOptions { @@ -27,4 +27,4 @@ public enum AvailabilityMode All, First, } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/IPProbe/IPProbeZoneProvider.cs b/Dns/ZoneProvider/IPProbe/IPProbeZoneProvider.cs index c2ea7c2..03e42c8 100644 --- a/Dns/ZoneProvider/IPProbe/IPProbeZoneProvider.cs +++ b/Dns/ZoneProvider/IPProbe/IPProbeZoneProvider.cs @@ -1,11 +1,11 @@ -namespace Dns.ZoneProvider.IPProbe +namespace Dns.ZoneProvider.IPProbe { using System; - using System.Threading; - using System.Threading.Tasks; - using System.Net; using System.Collections.Generic; using System.Linq; + using System.Net; + using System.Threading; + using System.Threading.Tasks; using Microsoft.Extensions.Configuration; @@ -66,8 +66,8 @@ public void ProbeLoop(CancellationToken ct) Console.WriteLine("Probe batch duration {0}", batchDuration); // wait remainder of Polling Interval - var remainingWaitTimeout = (this.options.PollingIntervalSeconds * 1000) -(int)batchDuration.TotalMilliseconds; - if(remainingWaitTimeout > 0) + var remainingWaitTimeout = (this.options.PollingIntervalSeconds * 1000) - (int)batchDuration.TotalMilliseconds; + if (remainingWaitTimeout > 0) { ct.WaitHandle.WaitOne(remainingWaitTimeout); } @@ -82,7 +82,7 @@ public override void Dispose() public override void Start(CancellationToken ct) { ct.Register(this.Stop); - this.runningTask = Task.Run(()=>ProbeLoop(ct)); + this.runningTask = Task.Run(() => ProbeLoop(ct)); } private void Stop() @@ -90,15 +90,15 @@ private void Stop() this.runningTask.Wait(); } - internal IEnumerableGetZoneRecords(State state) + internal IEnumerable GetZoneRecords(State state) { - foreach(var host in state.Hosts) + foreach (var host in state.Hosts) { var availableAddresses = host.AddressProbes .Where(addr => addr.IsAvailable) .Select(addr => addr.Address); - if(host.AvailabilityMode == AvailabilityMode.First) + if (host.AvailabilityMode == AvailabilityMode.First) { availableAddresses = availableAddresses.Take(1); } @@ -138,4 +138,4 @@ internal Zone GetZone(State state) return zone; } } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/IPProbe/ProbeResult.cs b/Dns/ZoneProvider/IPProbe/ProbeResult.cs index 5102104..5cd0662 100644 --- a/Dns/ZoneProvider/IPProbe/ProbeResult.cs +++ b/Dns/ZoneProvider/IPProbe/ProbeResult.cs @@ -8,4 +8,4 @@ internal class ProbeResult internal TimeSpan Duration; internal bool Available; } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/IPProbe/State.cs b/Dns/ZoneProvider/IPProbe/State.cs index 6ac2d49..cdde474 100644 --- a/Dns/ZoneProvider/IPProbe/State.cs +++ b/Dns/ZoneProvider/IPProbe/State.cs @@ -1,7 +1,7 @@ namespace Dns.ZoneProvider.IPProbe { - using System.Net; using System.Collections.Generic; + using System.Net; internal class State { @@ -43,4 +43,4 @@ internal State(IPProbeProviderOptions options) } } } -} \ No newline at end of file +} diff --git a/Dns/ZoneProvider/IPProbe/Target.cs b/Dns/ZoneProvider/IPProbe/Target.cs index 266bcee..0864aad 100644 --- a/Dns/ZoneProvider/IPProbe/Target.cs +++ b/Dns/ZoneProvider/IPProbe/Target.cs @@ -52,4 +52,4 @@ public int GetHashCode(Target obj) } } } -} \ No newline at end of file +} diff --git a/Dns/ZoneRecord.cs b/Dns/ZoneRecord.cs index 12b940a..9e937ae 100644 --- a/Dns/ZoneRecord.cs +++ b/Dns/ZoneRecord.cs @@ -16,4 +16,4 @@ public class ZoneRecord public IPAddress[] Addresses; public int Count; } -} \ No newline at end of file +} diff --git a/dns-cli/Program.cs b/dns-cli/Program.cs index 3f23167..2862d1d 100644 --- a/dns-cli/Program.cs +++ b/dns-cli/Program.cs @@ -16,7 +16,7 @@ public static void Main(string[] args) Console.WriteLine("DNS Server - Console Mode"); - if(args.Length == 0) + if (args.Length == 0) { args = new string[] { "./appsettings.json" }; } diff --git a/dnstest/BitPackerTests.cs b/dnstest/BitPackerTests.cs index 3cc2a73..391d887 100644 --- a/dnstest/BitPackerTests.cs +++ b/dnstest/BitPackerTests.cs @@ -42,7 +42,7 @@ public void Test2() bytes = BitConverter.GetBytes(0xAFFF); packer = new BitPacker(bytes); - + Assert.True(packer.GetBoolean()); Assert.True(packer.GetBoolean()); Assert.Equal(15, packer.GetByte(4)); @@ -63,7 +63,7 @@ public void Test3() Assert.Equal(15, packer.GetByte(4)); Assert.Equal(15, packer.GetByte(4)); - Assert.Equal(0xAF,packer.GetUshort(8)); + Assert.Equal(0xAF, packer.GetUshort(8)); bytes = BitConverter.GetBytes(0x0CD000); packer = new BitPacker(bytes); @@ -71,7 +71,7 @@ public void Test3() Assert.Equal(0x00, packer.GetByte(8)); Assert.Equal(0x0CD0, packer.GetUshort(16)); - bytes = BitConverter.GetBytes(0x000F << 1) ; + bytes = BitConverter.GetBytes(0x000F << 1); packer = new BitPacker(bytes); Assert.False(packer.GetBoolean()); Assert.Equal(0xF, packer.GetUshort(8)); diff --git a/dnstest/ConfigTests.cs b/dnstest/ConfigTests.cs index 51b2ba3..412960c 100644 --- a/dnstest/ConfigTests.cs +++ b/dnstest/ConfigTests.cs @@ -1,9 +1,8 @@ using System; -using Xunit; +using Dns.Config; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; - -using Dns.Config; +using Xunit; namespace DnsTest { diff --git a/dnstest/DnsCacheTests.cs b/dnstest/DnsCacheTests.cs index 4e1b4f4..3e8c3fd 100644 --- a/dnstest/DnsCacheTests.cs +++ b/dnstest/DnsCacheTests.cs @@ -1,6 +1,6 @@ using System; -using System.Text; using System.Linq; +using System.Text; using Xunit; namespace DnsTest @@ -10,14 +10,16 @@ namespace DnsTest public class DnsCacheTests { [Fact] - public void Test1() { + public void Test1() + { Dns.Contracts.IDnsCache cache = new Dns.DnsCache(); var invalidKeyResult = cache.Get("invalidTestKey"); Xunit.Assert.Null(invalidKeyResult); } [Fact] - public void Test2() { + public void Test2() + { Dns.Contracts.IDnsCache cache = new Dns.DnsCache(); string key = "sampleCacheKey"; @@ -33,4 +35,4 @@ public void Test2() { Xunit.Assert.Null(invalidKeyResult); } } -} \ No newline at end of file +} diff --git a/dnstest/DnsCliAuthoritativeBehaviorTests.cs b/dnstest/DnsCliAuthoritativeBehaviorTests.cs new file mode 100644 index 0000000..a788f01 --- /dev/null +++ b/dnstest/DnsCliAuthoritativeBehaviorTests.cs @@ -0,0 +1,124 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +namespace DnsTest +{ + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Threading.Tasks; + + using Dns; + using DnsTest.Integration; + using Xunit; + + [Collection(DnsCliIntegrationCollection.Name)] + public sealed class DnsCliAuthoritativeBehaviorTests + { + private static readonly IPAddress PrimaryHostAddress = IPAddress.Parse("192.0.2.10"); + private static readonly IPAddress[] RoundRobinAddresses = + { + IPAddress.Parse("192.0.2.11"), + IPAddress.Parse("192.0.2.12"), + IPAddress.Parse("192.0.2.13") + }; + + private readonly DnsCliHostFixture _fixture; + + public DnsCliAuthoritativeBehaviorTests(DnsCliHostFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task InZoneQueriesReturnAuthoritativeAnswers() + { + string hostName = _fixture.BuildHostName("alpha"); + DnsMessage response = await _fixture.Client.QueryAsync(hostName); + + Assert.True(response.QR); + Assert.True(response.AA); + Assert.False(response.RA); + Assert.Equal(RCode.NOERROR, (RCode)response.RCode); + Assert.Equal((ushort)1, response.AnswerCount); + + ResourceRecord answer = Assert.Single(response.Answers); + Assert.Equal(hostName, answer.Name); + Assert.Equal(ResourceType.A, answer.Type); + Assert.Equal(ResourceClass.IN, answer.Class); + Assert.Equal((uint)10, answer.TTL); + ANameRData address = Assert.IsType(answer.RData); + Assert.Equal(PrimaryHostAddress, address.Address); + } + + [Fact] + public async Task RecursionDesiredFlagDoesNotGrantRecursionAvailability() + { + string hostName = _fixture.BuildHostName("alpha"); + DnsMessage response = await _fixture.Client.QueryAsync(hostName, recursionDesired: true); + + Assert.True(response.RD); + Assert.False(response.RA); + Assert.True(response.AA); + } + + [Fact] + public async Task RoundRobinHostsRotateAddressesAcrossQueries() + { + string hostName = _fixture.BuildHostName("round"); + List firstAnswers = new List(); + + for (int iteration = 0; iteration < RoundRobinAddresses.Length; iteration++) + { + DnsMessage response = await _fixture.Client.QueryAsync(hostName); + ResourceRecord firstRecord = response.Answers.First(); + firstAnswers.Add(Assert.IsType(firstRecord.RData).Address); + } + + Assert.Equal(RoundRobinAddresses, firstAnswers); + } + + [Fact] + public async Task PositiveResponsesKeepConfiguredTtl() + { + string hostName = _fixture.BuildHostName("alpha"); + + DnsMessage firstResponse = await _fixture.Client.QueryAsync(hostName); + DnsMessage secondResponse = await _fixture.Client.QueryAsync(hostName); + + Assert.Equal((uint)10, Assert.Single(firstResponse.Answers).TTL); + Assert.Equal((uint)10, Assert.Single(secondResponse.Answers).TTL); + Assert.True(firstResponse.AA); + Assert.True(secondResponse.AA); + } + + [Fact] + public async Task NonexistentHostsReturnSoaAuthorityWithMinimumTtl() + { + string missingHost = _fixture.BuildHostName("missing"); + + DnsMessage firstResponse = await _fixture.Client.QueryAsync(missingHost); + DnsMessage secondResponse = await _fixture.Client.QueryAsync(missingHost); + + Assert.Equal(RCode.NXDOMAIN, (RCode)firstResponse.RCode); + Assert.Equal((ushort)0, firstResponse.AnswerCount); + Assert.Equal((ushort)1, firstResponse.NameServerCount); + Assert.True(firstResponse.AA); + Assert.False(firstResponse.RA); + + ResourceRecord soaRecord = Assert.Single(firstResponse.Authorities); + Assert.Equal(ResourceType.SOA, soaRecord.Type); + Assert.Equal((uint)300, soaRecord.TTL); + StatementOfAuthorityRData soaData = Assert.IsType(soaRecord.RData); + Assert.Equal((uint)300, soaData.MinimumTTL); + + ResourceRecord secondSoaRecord = Assert.Single(secondResponse.Authorities); + Assert.Equal((uint)300, secondSoaRecord.TTL); + StatementOfAuthorityRData secondSoaData = Assert.IsType(secondSoaRecord.RData); + Assert.Equal((uint)300, secondSoaData.MinimumTTL); + } + } +} diff --git a/dnstest/DnsProtocolTest.cs b/dnstest/DnsProtocolTest.cs index 90713f4..dcebfbc 100644 --- a/dnstest/DnsProtocolTest.cs +++ b/dnstest/DnsProtocolTest.cs @@ -18,7 +18,7 @@ public class DnsProtocolTest [Fact] public void DnsQuery() { - byte[] sampleQuery = new byte[] {0xD3, 0x03, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x64, 0x64, 0x63, 0x64, 0x73, 0x30, 0x31, 0x07, 0x72, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x04, 0x63, 0x6F, 0x72, 0x70, 0x09, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01}; + byte[] sampleQuery = new byte[] { 0xD3, 0x03, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x64, 0x64, 0x63, 0x64, 0x73, 0x30, 0x31, 0x07, 0x72, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x04, 0x63, 0x6F, 0x72, 0x70, 0x09, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01 }; DnsMessage query; Assert.True(DnsMessage.TryParse(sampleQuery, out query)); query.Dump(); @@ -27,7 +27,7 @@ public void DnsQuery() [Fact] public void DnsQuery2() { - byte[] sampleQuery = new byte[] {0x00, 0x03, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x03, 0x6D, 0x73, 0x6E, 0x03, 0x63, 0x6F, 0x6D, 0x07, 0x72, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x04, 0x63, 0x6F, 0x72, 0x70, 0x09, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x1C, 0x00, 0x01}; + byte[] sampleQuery = new byte[] { 0x00, 0x03, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x03, 0x6D, 0x73, 0x6E, 0x03, 0x63, 0x6F, 0x6D, 0x07, 0x72, 0x65, 0x64, 0x6D, 0x6F, 0x6E, 0x64, 0x04, 0x63, 0x6F, 0x72, 0x70, 0x09, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x1C, 0x00, 0x01 }; DnsMessage query; Assert.True(DnsMessage.TryParse(sampleQuery, out query)); @@ -63,7 +63,7 @@ public void DnsQuery2() [Fact] public void DnsResponse1() { - byte[] sampleQuery = new byte[] {0x44, 0xFD, 0x81, 0x80, 0x00, 0x01, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x10, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x2D, 0x61, 0x6E, 0x61, 0x6C, 0x79, 0x74, 0x69, 0x63, 0x73, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x89, 0x89, 0x00, 0x20, 0x14, 0x77, 0x77, 0x77, 0x2D, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x2D, 0x61, 0x6E, 0x61, 0x6C, 0x79, 0x74, 0x69, 0x63, 0x73, 0x01, 0x6C, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0xC0, 0x21, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x25, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x21, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x28, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x29, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x20, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x2E, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x26, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x24, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x27, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x22, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x23}; + byte[] sampleQuery = new byte[] { 0x44, 0xFD, 0x81, 0x80, 0x00, 0x01, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x10, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x2D, 0x61, 0x6E, 0x61, 0x6C, 0x79, 0x74, 0x69, 0x63, 0x73, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x89, 0x89, 0x00, 0x20, 0x14, 0x77, 0x77, 0x77, 0x2D, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0x2D, 0x61, 0x6E, 0x61, 0x6C, 0x79, 0x74, 0x69, 0x63, 0x73, 0x01, 0x6C, 0x06, 0x67, 0x6F, 0x6F, 0x67, 0x6C, 0x65, 0xC0, 0x21, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x25, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x21, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x28, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x29, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x20, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x2E, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x26, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x24, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x27, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x22, 0xC0, 0x36, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x19, 0x00, 0x04, 0xAD, 0xC2, 0x21, 0x23 }; DnsMessage query; Assert.True(DnsMessage.TryParse(sampleQuery, out query)); query.Dump(); @@ -73,7 +73,7 @@ public void DnsResponse1() [Fact] public void DnsResponse2() { - byte[] sampleQuery = new byte[] {0x00, 0x04, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x03, 0x6D, 0x73, 0x6E, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x02, 0x35, 0x00, 0x1E, 0x02, 0x75, 0x73, 0x03, 0x63, 0x6F, 0x31, 0x03, 0x63, 0x62, 0x33, 0x06, 0x67, 0x6C, 0x62, 0x64, 0x6E, 0x73, 0x09, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0xC0, 0x14, 0xC0, 0x29, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x53, 0x00, 0x04, 0x83, 0xFD, 0x0D, 0x8C}; + byte[] sampleQuery = new byte[] { 0x00, 0x04, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77, 0x03, 0x6D, 0x73, 0x6E, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x02, 0x35, 0x00, 0x1E, 0x02, 0x75, 0x73, 0x03, 0x63, 0x6F, 0x31, 0x03, 0x63, 0x62, 0x33, 0x06, 0x67, 0x6C, 0x62, 0x64, 0x6E, 0x73, 0x09, 0x6D, 0x69, 0x63, 0x72, 0x6F, 0x73, 0x6F, 0x66, 0x74, 0xC0, 0x14, 0xC0, 0x29, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x53, 0x00, 0x04, 0x83, 0xFD, 0x0D, 0x8C }; DnsMessage query; Assert.True(DnsMessage.TryParse(sampleQuery, out query)); @@ -110,7 +110,7 @@ public void DnsResponse2() [Fact] public void DnsResponse3() { - byte[] sampleQuery = new byte[] {0xDD, 0x15, 0x81, 0x80, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x61, 0x70, 0x69, 0x04, 0x62, 0x69, 0x6E, 0x67, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x83, 0x00, 0x14, 0x04, 0x61, 0x31, 0x33, 0x34, 0x02, 0x6C, 0x6D, 0x06, 0x61, 0x6B, 0x61, 0x6D, 0x61, 0x69, 0x03, 0x6E, 0x65, 0x74, 0x00, 0xC0, 0x2A, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x04, 0xCF, 0x6D, 0x49, 0x91, 0xC0, 0x2A, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x04, 0xCF, 0x6D, 0x49, 0x51}; + byte[] sampleQuery = new byte[] { 0xDD, 0x15, 0x81, 0x80, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x61, 0x70, 0x69, 0x04, 0x62, 0x69, 0x6E, 0x67, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, 0x0C, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x83, 0x00, 0x14, 0x04, 0x61, 0x31, 0x33, 0x34, 0x02, 0x6C, 0x6D, 0x06, 0x61, 0x6B, 0x61, 0x6D, 0x61, 0x69, 0x03, 0x6E, 0x65, 0x74, 0x00, 0xC0, 0x2A, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x04, 0xCF, 0x6D, 0x49, 0x91, 0xC0, 0x2A, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x04, 0xCF, 0x6D, 0x49, 0x51 }; DnsMessage query; Assert.True(DnsMessage.TryParse(sampleQuery, out query)); @@ -148,7 +148,7 @@ public void DnsResponse3() Assert.Equal(ResourceClass.IN, query.Answers[0].Class); Assert.True(query.Answers[0].TTL == 0x83); Assert.Equal(0x14, query.Answers[0].DataLength); - Assert.Equal(typeof (CNameRData), query.Answers[0].RData.GetType()); + Assert.Equal(typeof(CNameRData), query.Answers[0].RData.GetType()); // dump results query.Dump(); @@ -158,7 +158,7 @@ public void DnsResponse3() [Fact] public void DnsQuery3() { - byte[] sampleQuery = new byte[] {0xFB, 0x65, 0x84, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x2D, 0x75, 0x73, 0x0C, 0x69, 0x6D, 0x72, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x77, 0x69, 0x64, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x1C, 0x00, 0x01}; + byte[] sampleQuery = new byte[] { 0xFB, 0x65, 0x84, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x2D, 0x75, 0x73, 0x0C, 0x69, 0x6D, 0x72, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x77, 0x69, 0x64, 0x65, 0x03, 0x63, 0x6F, 0x6D, 0x00, 0x00, 0x1C, 0x00, 0x01 }; DnsMessage query; Assert.True(DnsMessage.TryParse(sampleQuery, out query)); @@ -292,7 +292,7 @@ public void TransitiveQueryTest() DnsMessage message = new DnsMessage(); message.QueryIdentifier = 0xFEED; message.QR = false; - message.Opcode = (byte) OpCode.QUERY; + message.Opcode = (byte)OpCode.QUERY; message.AA = false; message.TC = false; message.RD = true; @@ -306,7 +306,7 @@ public void TransitiveQueryTest() message.NameServerCount = 0; message.AdditionalCount = 0; message.Questions = new QuestionList(); - message.Questions.Add(new Question {Name = "www.msn.com", Class = ResourceClass.IN, Type = ResourceType.A}); + message.Questions.Add(new Question { Name = "www.msn.com", Class = ResourceClass.IN, Type = ResourceType.A }); DnsMessage outMessage; using (MemoryStream stream = new MemoryStream()) @@ -317,7 +317,7 @@ public void TransitiveQueryTest() Assert.Equal(0xFEED, outMessage.QueryIdentifier); Assert.False(outMessage.QR); - Assert.Equal((byte) OpCode.QUERY, outMessage.Opcode); + Assert.Equal((byte)OpCode.QUERY, outMessage.Opcode); Assert.False(outMessage.AA); Assert.False(outMessage.TC); Assert.True(outMessage.RD); @@ -346,7 +346,7 @@ public void TransitiveQueryTest2() DnsMessage message = new DnsMessage(); message.QueryIdentifier = 0xFEED; message.QR = false; - message.Opcode = (byte) OpCode.QUERY; + message.Opcode = (byte)OpCode.QUERY; message.AA = false; message.TC = false; message.RD = true; @@ -360,10 +360,10 @@ public void TransitiveQueryTest2() message.NameServerCount = 0; message.AdditionalCount = 0; message.Questions = new QuestionList(); - message.Questions.Add(new Question {Name = "www.msn.com", Class = ResourceClass.IN, Type = ResourceType.A}); - message.Answers.Add(new ResourceRecord {Name = "8.8.8.8", Class = ResourceClass.IN, Type = ResourceType.NS, TTL = 468, DataLength = 0, RData = null}); - RData data = new ANameRData {Address = IPAddress.Parse("8.8.8.9")}; - message.Answers.Add(new ResourceRecord {Name = "8.8.8.9", Class = ResourceClass.IN, Type = ResourceType.NS, TTL = 468, RData = data, DataLength = (ushort) data.Length}); + message.Questions.Add(new Question { Name = "www.msn.com", Class = ResourceClass.IN, Type = ResourceType.A }); + message.Answers.Add(new ResourceRecord { Name = "8.8.8.8", Class = ResourceClass.IN, Type = ResourceType.NS, TTL = 468, DataLength = 0, RData = null }); + RData data = new ANameRData { Address = IPAddress.Parse("8.8.8.9") }; + message.Answers.Add(new ResourceRecord { Name = "8.8.8.9", Class = ResourceClass.IN, Type = ResourceType.NS, TTL = 468, RData = data, DataLength = (ushort)data.Length }); DnsMessage outMessage; using (MemoryStream stream = new MemoryStream()) @@ -374,7 +374,7 @@ public void TransitiveQueryTest2() Assert.Equal(0xFEED, outMessage.QueryIdentifier); Assert.False(outMessage.QR); - Assert.Equal((byte) OpCode.QUERY, outMessage.Opcode); + Assert.Equal((byte)OpCode.QUERY, outMessage.Opcode); Assert.False(outMessage.AA); Assert.False(outMessage.TC); Assert.True(outMessage.RD); @@ -436,8 +436,8 @@ public void Opcode() DnsMessage message = new DnsMessage(); message.QR = true; Assert.Equal(0x8000, message.Flags); - message.Opcode = (byte) OpCode.UPDATE; - Assert.Equal((byte) OpCode.UPDATE, message.Opcode); + message.Opcode = (byte)OpCode.UPDATE; + Assert.Equal((byte)OpCode.UPDATE, message.Opcode); Assert.Equal(0xa800, message.Flags); } } From c4412da018fc8773d42d719202b29dbcdff1caa3 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 19:08:24 -0800 Subject: [PATCH 13/31] T02 - Authoritative response verification suite --- AGENTS.md | 1 + dnstest/Integration/DnsCliHostFixture.cs | 292 ++++++++++++++++++ .../DnsCliIntegrationCollection.cs | 16 + dnstest/Integration/DnsQueryClient.cs | 94 ++++++ dnstest/Integration/TestProjectPaths.cs | 49 +++ .../Zones/integration_machineinfo.csv | 6 + dnstest/TestData/appsettings.template.json | 18 ++ docs/task_list.md | 2 +- 8 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 dnstest/Integration/DnsCliHostFixture.cs create mode 100644 dnstest/Integration/DnsCliIntegrationCollection.cs create mode 100644 dnstest/Integration/DnsQueryClient.cs create mode 100644 dnstest/Integration/TestProjectPaths.cs create mode 100644 dnstest/TestData/Zones/integration_machineinfo.csv create mode 100644 dnstest/TestData/appsettings.template.json diff --git a/AGENTS.md b/AGENTS.md index 0e2d0ca..ecc8093 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,7 @@ Gotchas: ## 7. Testing Expectations - Minimum: `dotnet test csharp-dns-server.sln`. +- The dns-cli integration harness (`dnstest/Integration` + `DnsCliAuthoritativeBehaviorTests`) runs automatically with `dotnet test`, spins up `dns-cli` using the sample assets in `dnstest/TestData`, and needs free TCP/UDP ports; keep configs deterministic when extending it. - For networking changes, add/extend unit tests in `dnstest` or new integration fixtures. - Capture repro cases for fixed bugs (#26 compressed pointers, #11 BitPacker write) and ensure tests fail before fixes. diff --git a/dnstest/Integration/DnsCliHostFixture.cs b/dnstest/Integration/DnsCliHostFixture.cs new file mode 100644 index 0000000..122f836 --- /dev/null +++ b/dnstest/Integration/DnsCliHostFixture.cs @@ -0,0 +1,292 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +namespace DnsTest.Integration +{ + using System; + using System.Collections.Concurrent; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using Xunit; + + public sealed class DnsCliHostFixture : IAsyncLifetime, IDisposable + { + private const string ZoneSuffix = ".integration.test"; + + private readonly ConcurrentQueue _logLines = new ConcurrentQueue(); + private readonly TaskCompletionSource _readyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + private Process _process; + private Task _stdoutTask; + private Task _stderrTask; + private string _configPath; + private DirectoryInfo _workingDirectory; + + public IPEndPoint DnsEndpoint { get; private set; } + + internal DnsQueryClient Client { get; private set; } + + public string[] Logs => _logLines.ToArray(); + + public string BuildHostName(string hostPrefix) + { + if (string.IsNullOrWhiteSpace(hostPrefix)) + { + throw new ArgumentException("Host prefix is required.", nameof(hostPrefix)); + } + + if (hostPrefix.EndsWith(ZoneSuffix, StringComparison.OrdinalIgnoreCase)) + { + return hostPrefix; + } + + return $"{hostPrefix}{ZoneSuffix}"; + } + + public async Task InitializeAsync() + { + ValidateArtifacts(); + + int dnsPort = GetAvailableUdpPort(); + int httpPort = GetAvailableTcpPort(); + + DnsEndpoint = new IPEndPoint(IPAddress.Loopback, dnsPort); + + PrepareWorkingDirectory(); + CopyZoneFile(); + WriteConfigFile(dnsPort, httpPort); + + StartProcess(); + + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await WaitForReadyAsync(timeoutCts.Token).ConfigureAwait(false); + + Client = new DnsQueryClient(DnsEndpoint); + } + + public async Task DisposeAsync() + { + await StopProcessAsync().ConfigureAwait(false); + CleanupWorkingDirectory(); + } + + public void Dispose() + { + StopProcessAsync().GetAwaiter().GetResult(); + CleanupWorkingDirectory(); + } + + private void ValidateArtifacts() + { + if (!File.Exists(TestProjectPaths.DnsCliDllPath)) + { + throw new FileNotFoundException("dns-cli binary not found. Run dotnet build before executing the integration tests.", TestProjectPaths.DnsCliDllPath); + } + + if (!File.Exists(GetTemplatePath())) + { + throw new FileNotFoundException("Integration configuration template is missing.", GetTemplatePath()); + } + + if (!File.Exists(GetZoneSourcePath())) + { + throw new FileNotFoundException("Integration zone data file is missing.", GetZoneSourcePath()); + } + } + + private void PrepareWorkingDirectory() + { + string tempDirectory = Path.Combine(Path.GetTempPath(), $"dns-cli-tests-{Guid.NewGuid():N}"); + _workingDirectory = Directory.CreateDirectory(tempDirectory); + } + + private void CopyZoneFile() + { + string destination = Path.Combine(_workingDirectory.FullName, "machineinfo.csv"); + File.Copy(GetZoneSourcePath(), destination, overwrite: true); + } + + private void WriteConfigFile(int dnsPort, int httpPort) + { + string template = File.ReadAllText(GetTemplatePath()); + string zoneFilePath = Path.Combine(_workingDirectory.FullName, "machineinfo.csv"); + + template = template.Replace("{{DNS_PORT}}", dnsPort.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); + template = template.Replace("{{HTTP_PORT}}", httpPort.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); + template = template.Replace("{{ZONE_SUFFIX}}", ZoneSuffix, StringComparison.Ordinal); + template = template.Replace("{{ZONE_FILE}}", JsonEncodedText.Encode(zoneFilePath).ToString(), StringComparison.Ordinal); + + _configPath = Path.Combine(_workingDirectory.FullName, "appsettings.json"); + File.WriteAllText(_configPath, template, Encoding.UTF8); + } + + private void StartProcess() + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"\"{TestProjectPaths.DnsCliDllPath}\" \"{_configPath}\"", + WorkingDirectory = Path.GetDirectoryName(TestProjectPaths.DnsCliDllPath), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + _process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start dns-cli."); + + if (_process.HasExited) + { + throw new InvalidOperationException("dns-cli exited immediately after start."); + } + + _process.EnableRaisingEvents = true; + _process.Exited += (_, __) => + { + if (!_readyTcs.Task.IsCompleted) + { + _readyTcs.TrySetException(new InvalidOperationException("dns-cli exited before it signaled readiness.")); + } + }; + + _stdoutTask = Task.Run(() => PumpStreamAsync(_process.StandardOutput, "[out]")); + _stderrTask = Task.Run(() => PumpStreamAsync(_process.StandardError, "[err]")); + } + + private async Task PumpStreamAsync(StreamReader reader, string prefix) + { + while (!reader.EndOfStream) + { + string line = await reader.ReadLineAsync().ConfigureAwait(false); + if (line == null) + { + break; + } + + string formatted = $"{prefix} {line}"; + _logLines.Enqueue(formatted); + + if (line.IndexOf("Zone reloaded", StringComparison.OrdinalIgnoreCase) >= 0) + { + _readyTcs.TrySetResult(true); + } + } + } + + private async Task WaitForReadyAsync(CancellationToken cancellationToken) + { + Task completed = await Task.WhenAny(_readyTcs.Task, Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken)).ConfigureAwait(false); + if (completed != _readyTcs.Task) + { + cancellationToken.ThrowIfCancellationRequested(); + throw new TimeoutException("dns-cli did not emit a readiness signal."); + } + + await _readyTcs.Task.ConfigureAwait(false); + } + + private async Task StopProcessAsync() + { + if (_process == null) + { + return; + } + + try + { + if (!_process.HasExited) + { + _process.Kill(entireProcessTree: true); + } + } + catch (InvalidOperationException) + { + } + + try + { + await _process.WaitForExitAsync().ConfigureAwait(false); + } + catch (InvalidOperationException) + { + } + + if (_stdoutTask != null) + { + try + { + await _stdoutTask.ConfigureAwait(false); + } + catch + { + } + } + + if (_stderrTask != null) + { + try + { + await _stderrTask.ConfigureAwait(false); + } + catch + { + } + } + + _process.Dispose(); + _process = null; + _stdoutTask = null; + _stderrTask = null; + } + + private void CleanupWorkingDirectory() + { + try + { + if (_workingDirectory != null && _workingDirectory.Exists) + { + _workingDirectory.Delete(recursive: true); + } + } + catch + { + } + } + + private static int GetAvailableUdpPort() + { + using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + + private static int GetAvailableTcpPort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + private static string GetTemplatePath() + { + return Path.Combine(TestProjectPaths.TestDataDirectory, "appsettings.template.json"); + } + + private static string GetZoneSourcePath() + { + return Path.Combine(TestProjectPaths.TestDataDirectory, "Zones", "integration_machineinfo.csv"); + } + } +} diff --git a/dnstest/Integration/DnsCliIntegrationCollection.cs b/dnstest/Integration/DnsCliIntegrationCollection.cs new file mode 100644 index 0000000..5fe58d6 --- /dev/null +++ b/dnstest/Integration/DnsCliIntegrationCollection.cs @@ -0,0 +1,16 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +namespace DnsTest.Integration +{ + using Xunit; + + [CollectionDefinition(Name)] + public sealed class DnsCliIntegrationCollection : ICollectionFixture + { + public const string Name = "DnsCliIntegration"; + } +} diff --git a/dnstest/Integration/DnsQueryClient.cs b/dnstest/Integration/DnsQueryClient.cs new file mode 100644 index 0000000..a898107 --- /dev/null +++ b/dnstest/Integration/DnsQueryClient.cs @@ -0,0 +1,94 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +namespace DnsTest.Integration +{ + using System; + using System.IO; + using System.Net; + using System.Net.Sockets; + using System.Threading; + using System.Threading.Tasks; + + using Dns; + + internal sealed class DnsQueryClient + { + private readonly IPEndPoint _endpoint; + private readonly TimeSpan _timeout; + private int _messageId; + + public DnsQueryClient(IPEndPoint endpoint, TimeSpan? timeout = null) + { + _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + _timeout = timeout ?? TimeSpan.FromSeconds(5); + _messageId = Environment.TickCount; + } + + public async Task QueryAsync(string hostName, ResourceType resourceType = ResourceType.A, bool recursionDesired = false, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(hostName)) + { + throw new ArgumentException("A host name is required.", nameof(hostName)); + } + + var queryMessage = CreateQuery(hostName, resourceType, recursionDesired); + byte[] payload = SerializeMessage(queryMessage); + + using UdpClient udpClient = new UdpClient(AddressFamily.InterNetwork); + await udpClient.SendAsync(payload, payload.Length, _endpoint).ConfigureAwait(false); + + Task receiveTask = udpClient.ReceiveAsync(); + Task timeoutTask = Task.Delay(_timeout, cancellationToken); + + Task completed = await Task.WhenAny(receiveTask, timeoutTask).ConfigureAwait(false); + if (completed != receiveTask) + { + if (timeoutTask.IsCanceled) + { + throw new OperationCanceledException("DNS query was cancelled.", cancellationToken); + } + + throw new TimeoutException($"Timed out waiting for DNS response for {hostName}."); + } + + UdpReceiveResult receiveResult = await receiveTask.ConfigureAwait(false); + if (!DnsMessage.TryParse(receiveResult.Buffer, out DnsMessage response)) + { + throw new InvalidDataException("Unable to parse DNS response."); + } + + if (response.QueryIdentifier != queryMessage.QueryIdentifier) + { + throw new InvalidOperationException("Received DNS response with mismatched identifier."); + } + + return response; + } + + private DnsMessage CreateQuery(string hostName, ResourceType resourceType, bool recursionDesired) + { + var message = new DnsMessage(); + message.QueryIdentifier = (ushort)Interlocked.Increment(ref _messageId); + message.QuestionCount = 1; + message.RD = recursionDesired; + message.Questions.Add(new Question + { + Name = hostName, + Class = ResourceClass.IN, + Type = resourceType + }); + return message; + } + + private static byte[] SerializeMessage(DnsMessage message) + { + using MemoryStream stream = new MemoryStream(); + message.WriteToStream(stream); + return stream.ToArray(); + } + } +} diff --git a/dnstest/Integration/TestProjectPaths.cs b/dnstest/Integration/TestProjectPaths.cs new file mode 100644 index 0000000..ea093c0 --- /dev/null +++ b/dnstest/Integration/TestProjectPaths.cs @@ -0,0 +1,49 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +namespace DnsTest.Integration +{ + using System; + using System.IO; + + internal static class TestProjectPaths + { + static TestProjectPaths() + { + var tfmDirectory = new DirectoryInfo(AppContext.BaseDirectory); + TargetFramework = tfmDirectory.Name; + + var configurationDirectory = tfmDirectory.Parent ?? throw new InvalidOperationException("Unable to determine configuration directory for the test assembly output."); + Configuration = configurationDirectory.Name; + + var binDirectory = configurationDirectory.Parent ?? throw new InvalidOperationException("Unable to determine bin directory for the test assembly output."); + var projectDirectory = binDirectory.Parent ?? throw new InvalidOperationException("Unable to determine test project directory."); + TestProjectDirectory = projectDirectory.FullName; + + var solutionDirectory = projectDirectory.Parent ?? throw new InvalidOperationException("Unable to determine solution root."); + SolutionRoot = solutionDirectory.FullName; + + TestDataDirectory = Path.Combine(TestProjectDirectory, "TestData"); + + DnsCliOutputDirectory = Path.Combine(SolutionRoot, "dns-cli", "bin", Configuration, TargetFramework); + DnsCliDllPath = Path.Combine(DnsCliOutputDirectory, "dns-cli.dll"); + } + + public static string Configuration { get; } + + public static string TargetFramework { get; } + + public static string TestProjectDirectory { get; } + + public static string TestDataDirectory { get; } + + public static string SolutionRoot { get; } + + public static string DnsCliOutputDirectory { get; } + + public static string DnsCliDllPath { get; } + } +} diff --git a/dnstest/TestData/Zones/integration_machineinfo.csv b/dnstest/TestData/Zones/integration_machineinfo.csv new file mode 100644 index 0000000..19943b3 --- /dev/null +++ b/dnstest/TestData/Zones/integration_machineinfo.csv @@ -0,0 +1,6 @@ +#Version:1.0 +#Fields:MachineName,MachineFunction,StaticIP +alpha-node,alpha,192.0.2.10 +rr-node-1,round,192.0.2.11 +rr-node-2,round,192.0.2.12 +rr-node-3,round,192.0.2.13 diff --git a/dnstest/TestData/appsettings.template.json b/dnstest/TestData/appsettings.template.json new file mode 100644 index 0000000..85ff3ed --- /dev/null +++ b/dnstest/TestData/appsettings.template.json @@ -0,0 +1,18 @@ +{ + "server": { + "zone": { + "name": "{{ZONE_SUFFIX}}", + "provider": "Dns.ZoneProvider.AP.APZoneProvider" + }, + "dnslistener": { + "port": {{DNS_PORT}} + }, + "webserver": { + "port": {{HTTP_PORT}}, + "enabled": true + } + }, + "zoneprovider": { + "FileName": "{{ZONE_FILE}}" + } +} diff --git a/docs/task_list.md b/docs/task_list.md index 651c5bd..5ba331a 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -1,7 +1,7 @@ # Task List (Prioritized) 1. [x] **T01 – Fix DNS compressed-name parsing regression** — Address issue #26 in `Dns/DnsProtocol`/`DnsMessage` and add regression tests to ensure compliant decoding/encoding under RFC 1035. -2. [ ] **T02 – Authoritative response verification suite** — Create integration tests running `dns-cli` with sample zones to validate AA/RA/SOA behavior and caching semantics (P0 reliability). +2. [x] **T02 – Authoritative response verification suite** — Added `dnstest` integration coverage that boots `dns-cli` with deterministic zone/config assets to assert AA/RA/SOA flags, TTL stability, and NXDOMAIN authority responses (run via `dotnet test csharp-dns-server.sln`). 3. [ ] **T03 – Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests (issue #15). 4. [ ] **T04 – Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. 5. [ ] **T05 – Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. From b10c222ad94cc3391d669342f7ce567931dadee0 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 19:22:58 -0800 Subject: [PATCH 14/31] Updated VSCode tasks for .NET 8 --- .vscode/launch.json | 21 +++++++++++---------- .vscode/tasks.json | 43 +++++++++++++++++++++++++++++++------------ docs/task_list.md | 1 + 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d29b972..e5da1f8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,24 +4,25 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": ".NET Core Launch (console)", + "name": "dns-cli: Launch DNS server", "type": "coreclr", "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/dns/bin/Debug/netcoreapp2.2/dns.dll", - "args": [], - "cwd": "${workspaceFolder}/dns", - "console": "internalConsole", + "preLaunchTask": "dotnet: build solution", + "program": "${workspaceFolder}/dns-cli/bin/Debug/net8.0/dns-cli.dll", + "args": [ + "./appsettings.json" + ], + "cwd": "${workspaceFolder}/dns-cli", + "console": "integratedTerminal", "stopAtEntry": false }, { - "name": ".NET Core Attach", + "name": ".NET Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}", - "cwd": "${workspaceFolder}/dns" + "cwd": "${workspaceFolder}" } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 88bd04b..90ff6de 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,41 +2,60 @@ "version": "2.0.0", "tasks": [ { - "label": "build", + "label": "dotnet: build solution", "command": "dotnet", "type": "process", "args": [ "build", - "${workspaceFolder}/dns/dns.csproj", + "${workspaceFolder}/csharp-dns-server.sln", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], - "problemMatcher": "$msCompile" + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } }, { - "label": "publish", + "label": "dotnet: test solution", "command": "dotnet", "type": "process", "args": [ - "publish", - "${workspaceFolder}/dns/dns.csproj", + "test", + "${workspaceFolder}/csharp-dns-server.sln", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], - "problemMatcher": "$msCompile" + "problemMatcher": "$msCompile", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "dotnet: format solution", + "command": "dotnet", + "type": "process", + "args": [ + "format", + "${workspaceFolder}/csharp-dns-server.sln" + ], + "problemMatcher": [] }, { - "label": "watch", + "label": "dns-cli: watch run", "command": "dotnet", "type": "process", "args": [ "watch", "run", - "${workspaceFolder}/dns/dns.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" + "--project", + "${workspaceFolder}/dns-cli/dns-cli.csproj", + "--", + "./appsettings.json" ], "problemMatcher": "$msCompile" } ] -} \ No newline at end of file +} diff --git a/docs/task_list.md b/docs/task_list.md index 5ba331a..10a5d24 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -26,3 +26,4 @@ 24. [ ] **T24 – Stabilize UDP listener shutdown & endpoint capture (High)** — Ensure `UdpListener.Start` exits cleanly after `Stop()` and capture the remote endpoint per receive so responses aren’t misrouted. 25. [ ] **T25 – Support larger UDP payloads (Medium)** — Increase `UdpListener` buffer sizing and/or detect truncated packets so EDNS-sized responses don’t silently corrupt parsing. 26. [ ] **T26 – Allow full 8-bit DNS labels (Medium)** — Relax `DnsProtocol.ReadString` ASCII enforcement in line with RFC 2181 so internationalized/underscored names don’t throw. +27. [x] **T27 – Refresh VS Code launch/tasks configs** — Update `.vscode/launch.json` and `tasks.json` to mirror the current build/test/debug workflow so contributors get accurate defaults. From 5c8a621b019cb32608772499ceaf07a258b9bc10 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 19:33:40 -0800 Subject: [PATCH 15/31] T22 Github Action - Continuous Integration --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 3 +++ docs/task_list.md | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3774330 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build-and-test: + name: build-test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore + run: dotnet restore csharp-dns-server.sln + + - name: Build + run: dotnet build csharp-dns-server.sln --configuration Release --no-restore + + - name: Test + run: dotnet test csharp-dns-server.sln --configuration Release --no-build diff --git a/README.md b/README.md index 919fe71..cd2774f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ This software is licenced under MIT terms that permits reuse within proprietary ## Gotchas - if you're running on Windows 10 with Docker Tools installed, Docker uses the ICS SharedAccess service to provide DNS resolution for Docker containers - this listens on UDP:53, and will conflict with the DNS project. Either turn off the the service (```net stop SharedAccess```), or change the UDP port. +## Continuous Integration +All pushes and pull requests against `main` run through `.github/workflows/ci.yml`, a GitHub Actions pipeline that restores, builds, and tests the full `csharp-dns-server.sln` on both Ubuntu and Windows runners using the .NET 8 SDK. + ## Features As written, the server has the following features: diff --git a/docs/task_list.md b/docs/task_list.md index 10a5d24..1e3f18c 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -21,7 +21,7 @@ 19. [ ] **T19 – DNSSEC support** — Add foundational DNSSEC record handling and validation paths (issue #2). 20. [ ] **T20 – Documented static zone workflow** — Provide a simple static zone declaration option (issue #1) for setups that don’t rely on the BIND parser. 21. [ ] **T21 – Fix AppVeyor build configuration** — Repair `appveyor.yml` so CI restores/builds/tests the .NET solution using the current SDK/runtime matrix. -22. [ ] **T22 – Add GitHub Actions CI pipeline** — Introduce a workflow under `.github/workflows/` that restores, builds, and tests the solution on Windows/Linux runners aligned with PR gating guidance. +22. [x] **T22 – Add GitHub Actions CI pipeline** — Introduce a workflow under `.github/workflows/` that restores, builds, and tests the solution on Windows/Linux runners aligned with PR gating guidance. 23. [x] **T23 – Correct IPv4 RDATA endianness (Critical)** — Fix `ANameRData.Parse` so addresses parsed from wire format are not byte-swapped before being forwarded to clients; add regression tests. 24. [ ] **T24 – Stabilize UDP listener shutdown & endpoint capture (High)** — Ensure `UdpListener.Start` exits cleanly after `Stop()` and capture the remote endpoint per receive so responses aren’t misrouted. 25. [ ] **T25 – Support larger UDP payloads (Medium)** — Increase `UdpListener` buffer sizing and/or detect truncated packets so EDNS-sized responses don’t silently corrupt parsing. From c7cda39c4b7c422153bce4263d86d4ee7811e2b4 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 19:44:52 -0800 Subject: [PATCH 16/31] Github CI Badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cd2774f..41e8919 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # csharp-dns-server +[![GitHub Actions Status](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml) + Fully functional DNS server written in C#. The project was conceived while working to reduce the cost of datacentre "stamps" while providing robust services within a datacentre, specifically to remove the need for an expensive load-balancer device by providing round-robin DNS services, and retrying connectivity instead. From 687e73d9b20723f31c8c3d0c2ad504a079d21b33 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 19:48:39 -0800 Subject: [PATCH 17/31] Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3774330..79903df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,8 @@ on: branches: - main +permissions: + contents: read jobs: build-and-test: name: build-test (${{ matrix.os }}) From 364e41d5ea5bc16eda16dc7877f29e06a2ec0878 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 20:24:51 -0800 Subject: [PATCH 18/31] Log configured port --- Dns/DnsServer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dns/DnsServer.cs b/Dns/DnsServer.cs index 9130c96..e86e54a 100644 --- a/Dns/DnsServer.cs +++ b/Dns/DnsServer.cs @@ -57,6 +57,7 @@ public void Start(CancellationToken ct) { _udpListener.Start(); ct.Register(_udpListener.Stop); + Console.WriteLine("DNS server listening on port {0}", this.port); } /// Process UDP Request From 755fc542e665450f0ce67a6f9791697a8740dd8a Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 20:29:23 -0800 Subject: [PATCH 19/31] T24 UDP Listener shutdown stability --- Dns/UdpListener.cs | 175 +++++++++++++++++++++++++++++------- dnstest/UdpListenerTests.cs | 124 +++++++++++++++++++++++++ docs/task_list.md | 2 +- docs/tasks/task_24_plan.md | 33 +++++++ 4 files changed, 301 insertions(+), 33 deletions(-) create mode 100644 dnstest/UdpListenerTests.cs create mode 100644 docs/tasks/task_24_plan.md diff --git a/Dns/UdpListener.cs b/Dns/UdpListener.cs index bee3fe0..e25566b 100644 --- a/Dns/UdpListener.cs +++ b/Dns/UdpListener.cs @@ -18,73 +18,184 @@ namespace Dns public class UdpListener { public OnRequestHandler OnRequest; + + private readonly object _syncRoot = new object(); private Socket _listener; + private CancellationTokenSource _cts; + private Task _receiveLoopTask; + + public EndPoint LocalEndPoint => _listener?.LocalEndPoint; public void Initialize(ushort port = 53) { + if (_listener != null) + { + throw new InvalidOperationException("Listener already initialized."); + } + _listener = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint ep = new IPEndPoint(IPAddress.Any, port); _listener.Bind(ep); } - public async void Start() + public void Start() { - while (true) + if (_listener == null) + { + throw new InvalidOperationException("Call Initialize before Start."); + } + + lock (_syncRoot) + { + if (_cts != null) + { + throw new InvalidOperationException("UDP listener already started."); + } + + _cts = new CancellationTokenSource(); + _receiveLoopTask = ReceiveLoopAsync(_cts.Token); + } + } + + public void Stop() + { + CancellationTokenSource cts; + Task receiveLoop; + + lock (_syncRoot) + { + if (_cts == null) + { + return; + } + + cts = _cts; + receiveLoop = _receiveLoopTask; + _cts = null; + _receiveLoopTask = null; + } + + cts.Cancel(); + + _listener?.Close(); + + if (receiveLoop != null) { try { - // Reusable SocketAsyncEventArgs and awaitable wrapper - SocketAsyncEventArgs args = new SocketAsyncEventArgs(); - args.SetBuffer(new byte[0x1000], 0, 0x1000); - args.RemoteEndPoint = _listener.LocalEndPoint; - SocketAwaitable awaitable = new SocketAwaitable(args); - - // Do processing, continually receiving from the socket - while (true) + receiveLoop.Wait(); + } + catch (AggregateException ex) + { + ex.Handle(inner => inner is OperationCanceledException || inner is ObjectDisposedException); + } + } + + cts.Dispose(); + } + + public async void SendToAsync(SocketAsyncEventArgs args) + { + if (_listener == null) + { + throw new InvalidOperationException("Listener is not initialized."); + } + + SocketAwaitable awaitable = new SocketAwaitable(args); + await _listener.SendToAsync(awaitable); + } + + private async Task ReceiveLoopAsync(CancellationToken ct) + { + Socket listener = _listener; + if (listener == null) + { + return; + } + + SocketAsyncEventArgs args = new SocketAsyncEventArgs(); + args.SetBuffer(new byte[0x1000], 0, 0x1000); + SocketAwaitable awaitable = new SocketAwaitable(args); + + try + { + while (!ct.IsCancellationRequested) + { + args.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + + try { - await _listener.ReceiveFromAsync(awaitable); + await listener.ReceiveFromAsync(awaitable); int bytesRead = args.BytesTransferred; if (bytesRead <= 0) { - break; + continue; } + byte[] payload = new byte[bytesRead]; + Buffer.BlockCopy(args.Buffer, 0, payload, 0, bytesRead); + + EndPoint remoteClone = CloneEndPoint(args.RemoteEndPoint); + if (OnRequest != null) { - var buffer = new byte[bytesRead]; - Buffer.BlockCopy(args.Buffer, 0, buffer, 0, buffer.Length); - var process = Task.Run(() => OnRequest(buffer, args.RemoteEndPoint)); + _ = Task.Run(() => OnRequest(payload, remoteClone)); } else { - // defaults to console dump if no listener is bound - var dump = Task.Run(() => ProcessReceiveFrom(args)); + _ = Task.Run(() => ProcessReceiveFrom(remoteClone, payload.Length)); } } + catch (ObjectDisposedException) + { + if (ct.IsCancellationRequested) + { + break; + } + + throw; + } + catch (SocketException ex) + { + if (ct.IsCancellationRequested && (ex.SocketErrorCode == SocketError.OperationAborted || ex.SocketErrorCode == SocketError.Interrupted)) + { + break; + } + + Console.WriteLine(ex.ToString()); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - } - // listener restarts if an exception occurs + } + finally + { + args.Dispose(); } } - public void Stop() + private static EndPoint CloneEndPoint(EndPoint endpoint) { - _listener.Close(); - } + if (endpoint == null) + { + return null; + } - public async void SendToAsync(SocketAsyncEventArgs args) - { - SocketAwaitable awaitable = new SocketAwaitable(args); - await _listener.SendToAsync(awaitable); + if (endpoint is IPEndPoint ip) + { + return new IPEndPoint(ip.Address, ip.Port); + } + + SocketAddress address = endpoint.Serialize(); + return endpoint.Create(address); } - public void ProcessReceiveFrom(SocketAsyncEventArgs args) + public void ProcessReceiveFrom(EndPoint remoteEndPoint, int bytesTransferred) { - Console.WriteLine(args.RemoteEndPoint.ToString()); - Console.WriteLine(args.BytesTransferred); + Console.WriteLine(remoteEndPoint?.ToString()); + Console.WriteLine(bytesTransferred); } } diff --git a/dnstest/UdpListenerTests.cs b/dnstest/UdpListenerTests.cs new file mode 100644 index 0000000..fa559bf --- /dev/null +++ b/dnstest/UdpListenerTests.cs @@ -0,0 +1,124 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Dns; +using Xunit; + +namespace DnsTest +{ + public class UdpListenerTests + { + + [Fact] + public async Task Stop_ReleasesPortAndHaltsProcessing() + { + var listener = new UdpListener(); + listener.Initialize(0); + + try + { + var firstPacket = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + int invocations = 0; + + listener.OnRequest += (buffer, remote) => + { + if (Interlocked.Increment(ref invocations) == 1) + { + firstPacket.TrySetResult(true); + } + }; + + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndPoint).Port; + + using (var client = new UdpClient(new IPEndPoint(IPAddress.Loopback, 0))) + { + await client.SendAsync(new byte[] { 0x1 }, 1, new IPEndPoint(IPAddress.Loopback, port)); + } + + await firstPacket.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + listener.Stop(); + + int received = Volatile.Read(ref invocations); + Assert.Equal(1, received); + + using (var probe = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) + { + probe.Bind(new IPEndPoint(IPAddress.Loopback, port)); + } + } + finally + { + listener.Stop(); + } + } + + [Fact] + public async Task CapturesRemoteEndpointPerPacket() + { + var listener = new UdpListener(); + listener.Initialize(0); + + try + { + var captured = new List(); + var gate = new object(); + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + listener.OnRequest += (buffer, remote) => + { + lock (gate) + { + if (remote is IPEndPoint ip) + { + captured.Add(new IPEndPoint(ip.Address, ip.Port)); + if (captured.Count == 2) + { + completion.TrySetResult(true); + } + } + } + }; + + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndPoint).Port; + + using (var client1 = new UdpClient(new IPEndPoint(IPAddress.Loopback, 0))) + using (var client2 = new UdpClient(new IPEndPoint(IPAddress.Loopback, 0))) + { + var target = new IPEndPoint(IPAddress.Loopback, port); + + await Task.WhenAll( + client1.SendAsync(new byte[] { 0x1 }, 1, target), + client2.SendAsync(new byte[] { 0x2 }, 1, target)); + + await completion.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + var expectedPorts = new[] + { + ((IPEndPoint)client1.Client.LocalEndPoint).Port, + ((IPEndPoint)client2.Client.LocalEndPoint).Port + }.OrderBy(p => p).ToArray(); + + var actualPorts = captured.Select(ep => ep.Port).OrderBy(p => p).ToArray(); + Assert.Equal(expectedPorts, actualPorts); + } + } + finally + { + listener.Stop(); + } + } + } +} diff --git a/docs/task_list.md b/docs/task_list.md index 1e3f18c..9fd984f 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -23,7 +23,7 @@ 21. [ ] **T21 – Fix AppVeyor build configuration** — Repair `appveyor.yml` so CI restores/builds/tests the .NET solution using the current SDK/runtime matrix. 22. [x] **T22 – Add GitHub Actions CI pipeline** — Introduce a workflow under `.github/workflows/` that restores, builds, and tests the solution on Windows/Linux runners aligned with PR gating guidance. 23. [x] **T23 – Correct IPv4 RDATA endianness (Critical)** — Fix `ANameRData.Parse` so addresses parsed from wire format are not byte-swapped before being forwarded to clients; add regression tests. -24. [ ] **T24 – Stabilize UDP listener shutdown & endpoint capture (High)** — Ensure `UdpListener.Start` exits cleanly after `Stop()` and capture the remote endpoint per receive so responses aren’t misrouted. +24. [x] **T24 – Stabilize UDP listener shutdown & endpoint capture (High)** — Refactored `Dns/UdpListener` with cancellation-aware start/stop behavior, per-packet endpoint cloning, and new tests ensuring clean shutdown plus correct response routing. 25. [ ] **T25 – Support larger UDP payloads (Medium)** — Increase `UdpListener` buffer sizing and/or detect truncated packets so EDNS-sized responses don’t silently corrupt parsing. 26. [ ] **T26 – Allow full 8-bit DNS labels (Medium)** — Relax `DnsProtocol.ReadString` ASCII enforcement in line with RFC 2181 so internationalized/underscored names don’t throw. 27. [x] **T27 – Refresh VS Code launch/tasks configs** — Update `.vscode/launch.json` and `tasks.json` to mirror the current build/test/debug workflow so contributors get accurate defaults. diff --git a/docs/tasks/task_24_plan.md b/docs/tasks/task_24_plan.md new file mode 100644 index 0000000..e54227e --- /dev/null +++ b/docs/tasks/task_24_plan.md @@ -0,0 +1,33 @@ +# Task T24 – Stabilize UDP Listener Shutdown & Endpoint Capture + +## Goal +Ensure `UdpListener.Start` terminates cleanly after `Stop()` is invoked and that each received packet preserves its source endpoint so responses never get misrouted. + +## Plan + +1. **Assess Current Behavior** + - Inspect `Dns/UdpListener.cs` to map out how `Start`, `Stop`, and the receive loop interact (socket lifetime, cancellation tokens, exception handling). + - Trace how the listener hands off messages to `Dns/DnsServer.cs` (or other consumers) to see whether remote endpoints are currently cached/shared. + +2. **Design Improvements** + - Introduce deterministic shutdown semantics (e.g., cancellation token source + awaited receive loop task) so `Stop` closes sockets exactly once and `Start` returns promptly. + - Ensure each `ReceiveFromAsync` (or equivalent) call allocates/captures an `IPEndPoint` per packet rather than mutating shared instances. + - Propagate the captured endpoint through the processing pipeline so replies target the correct remote peer even with concurrent sends. + - Handle race conditions: guard shared state, tolerate socket disposal exceptions, and prevent `Start` re-entry mishaps. + +3. **Implementation Steps** + - Update `UdpListener` members (fields, constructor) to store cancellation/disposal state. + - Refactor the receive loop to honor cancellation and to package `(byte[] buffer, IPEndPoint remote)` results. + - Modify consumers (likely `DnsServer`) to accept and reuse per-packet endpoints when forming responses. + - Add logging where necessary to aid future diagnostics without spamming hot paths. + +4. **Testing & Validation** + - Unit tests: add focused tests around new helper methods or endpoint forwarding logic. + - Integration tests (e.g., expand `dnstest/DnsCliAuthoritativeBehaviorTests`) to simulate multiple senders, verify responses go to the correct addresses, and ensure `Stop` fully releases the port. + - Run `dotnet test csharp-dns-server.sln` locally; capture any flakes/regressions. + - Concurrency confidence: stress scenarios (multiple clients + repeated `Start`/`Stop`) can be scripted, but race bugs remain timing-dependent, so pair tests with code review and optional manual stress loops to boost assurance. + +5. **Documentation & Tracking** + - Update inline comments to explain shutdown behavior. + - If necessary, note operational changes (e.g., new logging) in README/docs. + - Mark T24 complete in `docs/task_list.md` once fixes and tests land, recording any follow-up issues discovered during testing. From 3904e34d1efe8692b9d8b40b04e6b00ed51a4e70 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 20:30:32 -0800 Subject: [PATCH 20/31] Github action branch names --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79903df..64aaf4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,10 @@ name: CI on: push: branches: - - main + - master pull_request: branches: - - main + - master permissions: contents: read From d34741a3435d426ba1f67e6fc9ca501c10a3e489 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 20:38:44 -0800 Subject: [PATCH 21/31] Linux Case-Sensitivity --- csharp-dns-server.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp-dns-server.sln b/csharp-dns-server.sln index 8f6c776..993eda0 100644 --- a/csharp-dns-server.sln +++ b/csharp-dns-server.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dns", "dns\dns.csproj", "{6804468B-333A-4A7C-BFC1-FD4E498AB4A6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dns", "Dns\Dns.csproj", "{6804468B-333A-4A7C-BFC1-FD4E498AB4A6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dnstest", "dnstest\dnstest.csproj", "{126A266A-FF89-4516-BF36-37774EA35CD6}" EndProject From d03f4adbd9fe63dc02d47bcd1d6b4bafb89e9d46 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 20:42:59 -0800 Subject: [PATCH 22/31] Action fix --- csharp-dns-server.sln | 2 +- dns-cli/dns-cli.csproj | 2 +- dnstest/dnstest.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/csharp-dns-server.sln b/csharp-dns-server.sln index 993eda0..a8330b9 100644 --- a/csharp-dns-server.sln +++ b/csharp-dns-server.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dns", "Dns\Dns.csproj", "{6804468B-333A-4A7C-BFC1-FD4E498AB4A6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dns", "Dns\Dns.csproj", "{6804468B-333A-4A7C-BFC1-FD4E498AB4A6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dnstest", "dnstest\dnstest.csproj", "{126A266A-FF89-4516-BF36-37774EA35CD6}" EndProject diff --git a/dns-cli/dns-cli.csproj b/dns-cli/dns-cli.csproj index 70b868d..f492641 100644 --- a/dns-cli/dns-cli.csproj +++ b/dns-cli/dns-cli.csproj @@ -9,7 +9,7 @@ - + diff --git a/dnstest/dnstest.csproj b/dnstest/dnstest.csproj index ab01a5c..2e58d21 100644 --- a/dnstest/dnstest.csproj +++ b/dnstest/dnstest.csproj @@ -19,7 +19,7 @@ - + From bfa44b80f3d5bac06ad3fc5a7b1c52249dfa2958 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 21:11:11 -0800 Subject: [PATCH 23/31] README CI badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41e8919..895fe81 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # csharp-dns-server -[![GitHub Actions Status](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml) +[![GitHub Actions Status](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml) Fully functional DNS server written in C#. From d050ba752a83057b93760f0a1aec911b815f0f16 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 21:16:07 -0800 Subject: [PATCH 24/31] DIG example in README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 895fe81..4602a11 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ This software is licenced under MIT terms that permits reuse within proprietary // check that the tests run >> dotnet test +// use DIG query appconfig'd local server +dig -p 5335 @127.0.0.1 www.google.com A + ``` ## Gotchas From 32b739fdc75194370caf78553e007643ce962284 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 21:17:46 -0800 Subject: [PATCH 25/31] DIG example in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4602a11..61c4f0f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This software is licenced under MIT terms that permits reuse within proprietary >> dotnet test // use DIG query appconfig'd local server -dig -p 5335 @127.0.0.1 www.google.com A +>> dig -p 5335 @127.0.0.1 www.google.com A ``` From 63a5e6fa88d2df9c8feca45b3341983ae2034091 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 21:28:53 -0800 Subject: [PATCH 26/31] Task T06 - Ninject to MS.DI migration --- AGENTS.md | 2 +- Dns/Dns.csproj | 2 +- Dns/Program.cs | 37 ++++++++++++++++++++++-------- docs/task_list.md | 2 +- docs/tasks/task_06_plan.md | 46 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 docs/tasks/task_06_plan.md diff --git a/AGENTS.md b/AGENTS.md index ecc8093..2f1e46c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ | `docs/` | Specs, PRD and future design docs. | `docs/product_requirements.md` drives priorities. | Key classes & files: -- `Dns/Program.cs`: wiring for DI/config/servers. +- `Dns/Program.cs`: wiring for DI/config/servers via `Microsoft.Extensions.DependencyInjection`. - `Dns/DnsServer.cs`: UDP DNS loop and upstream forwarding. - `Dns/SmartZoneResolver.cs`: in-memory zone cache & round-robin dispenser. - `Dns/HttpServer.cs`: embedded status/diagnostics surface. diff --git a/Dns/Dns.csproj b/Dns/Dns.csproj index ecb94cd..c86a822 100644 --- a/Dns/Dns.csproj +++ b/Dns/Dns.csproj @@ -17,7 +17,7 @@ - + diff --git a/Dns/Program.cs b/Dns/Program.cs index 6fba9c8..1e692a6 100644 --- a/Dns/Program.cs +++ b/Dns/Program.cs @@ -11,15 +11,12 @@ namespace Dns using System.Linq; using System.Net; using System.Threading; - using Dns.ZoneProvider.AP; using Microsoft.Extensions.Configuration; - using Ninject; + using Microsoft.Extensions.DependencyInjection; public class Program { - private static IKernel container = new StandardKernel(); - private static ZoneProvider.BaseZoneProvider _zoneProvider; // reloads Zones from machineinfo.csv changes private static SmartZoneResolver _zoneResolver; // resolver and delegated lookup for unsupported zones; private static DnsServer _dnsServer; // resolver and delegated lookup for unsupported zones; @@ -43,18 +40,18 @@ public static void Run(string configFile, CancellationToken ct) .Build(); var appConfig = configuration.Get(); + using var serviceProvider = BuildServiceProvider(configuration, appConfig); - container.Bind().To(ByName(appConfig.Server.Zone.Provider)); var zoneProviderConfig = configuration.GetSection("zoneprovider"); - _zoneProvider = container.Get(); + _zoneProvider = serviceProvider.GetRequiredService(); _zoneProvider.Initialize(zoneProviderConfig, appConfig.Server.Zone.Name); - _zoneResolver = new SmartZoneResolver(); + _zoneResolver = serviceProvider.GetRequiredService(); _zoneResolver.SubscribeTo(_zoneProvider); - _dnsServer = new DnsServer(appConfig.Server.DnsListener.Port); + _dnsServer = serviceProvider.GetRequiredService(); - _httpServer = new HttpServer(); + _httpServer = serviceProvider.GetRequiredService(); _dnsServer.Initialize(_zoneResolver); @@ -114,6 +111,28 @@ private static void _httpServer_OnProcessRequest(HttpListenerContext context) } } + private static ServiceProvider BuildServiceProvider(IConfiguration configuration, Config.AppConfig appConfig) + { + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddSingleton(appConfig); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(provider => new DnsServer(appConfig.Server.DnsListener.Port)); + services.AddSingleton(provider => + { + var zoneProviderType = ByName(appConfig.Server.Zone.Provider); + if (zoneProviderType == null || !typeof(ZoneProvider.BaseZoneProvider).IsAssignableFrom(zoneProviderType)) + { + throw new InvalidOperationException(string.Format("Unable to locate zone provider type '{0}'.", appConfig.Server.Zone.Provider)); + } + + return (ZoneProvider.BaseZoneProvider)ActivatorUtilities.CreateInstance(provider, zoneProviderType); + }); + + return services.BuildServiceProvider(); + } + private static Type ByName(string name) { foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Reverse()) diff --git a/docs/task_list.md b/docs/task_list.md index 9fd984f..a217ba9 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -5,7 +5,7 @@ 3. [ ] **T03 – Implement RFC 2308-compliant caching** — Extend `DnsServer`/`DnsCache` to honor positive/negative TTLs, flush stale entries, and cover with tests (issue #15). 4. [ ] **T04 – Harden SmartZoneResolver concurrency** — Ensure zone reloads and address dispensers are thread-safe and resilient to null/empty provider updates. 5. [ ] **T05 – Health-probe simulation tests** — Build deterministic tests for `Dns/ZoneProvider/IPProbe` strategies to guarantee consistent handling of latency/timeouts. -6. [ ] **T06 – Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. +6. [x] **T06 – Migrate to Microsoft.Extensions.DependencyInjection** — Replace Ninject usage in `Dns/Program.cs` and related projects with built-in DI, updating configuration wiring accordingly. 7. [x] **T07 – Upgrade solution to .NET 8** — Move all projects to `net8.0`, update dependencies, and validate builds/tests across Windows/Linux. 8. [ ] **T08 – Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP (issue #16). 9. [x] **T09 – Fix CA2241 format warning** — Update the logging call in `Dns/DnsServer.cs` (line 250) to use the correct string-format arguments so builds are warning-free. diff --git a/docs/tasks/task_06_plan.md b/docs/tasks/task_06_plan.md new file mode 100644 index 0000000..34ac973 --- /dev/null +++ b/docs/tasks/task_06_plan.md @@ -0,0 +1,46 @@ +# Task 6 Plan – Migrate to Microsoft.Extensions.DependencyInjection + +## Goal +Replace the ad-hoc Ninject usage across the solution with the built-in `Microsoft.Extensions.DependencyInjection` (MS.DI) stack so the DNS server, CLI host, and tests share a consistent, modern dependency injection pipeline compatible with .NET 8. + +## Scope +- **Projects**: `Dns`, `dns-cli`, and `dnstest` (any code instantiating `Program.Run` or depending on the service provider). +- **Files**: `Dns/Program.cs`, `dns-cli/Program.cs` (if they configure DI directly), any zone providers or resolvers that currently rely on `IKernel`. +- **Out of Scope**: Introducing new services or refactoring unrelated runtime logic; focus strictly on the container swap while keeping behavior identical. + +## Steps +1. **Inventory current bindings** + - Examine `Dns/Program.cs` to list every type registered via `container.Bind<>().To(...)`. + - Identify transient vs singleton semantics and how configuration sections are passed into zone providers. + +2. **Design MS.DI composition root** + - Decide where to build `IHost`/`ServiceProvider` (likely in `Dns/Program.Run`). + - Map Ninject lifetimes to MS.DI lifetimes (`Singleton`, `Scoped`, `Transient`). + - Determine how to register configuration (`IConfiguration`, options classes) so zone providers receive required settings. + +3. **Implement the container swap** + - Remove Ninject references/packages from the solution and add `Microsoft.Extensions.DependencyInjection` (and possibly `Microsoft.Extensions.Hosting`). + - Introduce a `ServiceCollection` setup in `Program.Run`, registering zone providers via reflection (mirroring `ByName`) or using configuration-driven type lookup. + - Replace `_zoneProvider = container.Get<...>()` with `provider.GetRequiredService<...>()`. + +4. **Update entry points and consumers** + - Ensure `dns-cli` and any tests constructing `Program.Run` use the new DI pipeline (e.g., pass an optional `IServiceProvider` or factory if needed). + - Confirm `SmartZoneResolver`, `DnsServer`, and `HttpServer` dependencies are resolved through the new provider instead of manual `new`. + +5. **Clean up configuration wiring** + - Register strongly typed options via `services.Configure(configuration)` or equivalent so components can consume options via `IOptions`. + - Remove remaining Ninject-specific code paths and update `using` statements. + +6. **Validation** + - Run `dotnet build` and `dotnet test csharp-dns-server.sln`. + - Exercise `dns-cli` locally with `dotnet run -- ./appsettings.json` to ensure the server still starts, loads zones, and answers queries. + +7. **Documentation** + - Update `README.md`/`AGENTS.md` build notes to mention MS.DI usage and remove references to Ninject. + - Mark T06 done in `docs/task_list.md` when merged. + +## Acceptance Criteria +- All projects build without Ninject dependencies and instead rely on MS.DI. +- Runtime behavior (zone loading, DNS/HTTP serving) matches the pre-migration behavior. +- Tests and CLI runs succeed without manual container wiring. +- Documentation reflects the new dependency injection approach. From f47ea13f712f1d35ce0f83f8b267f3129a31848f Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Sun, 16 Nov 2025 21:33:06 -0800 Subject: [PATCH 27/31] Ignore documentation updates --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64aaf4d..06de74f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,17 @@ on: push: branches: - master + paths-ignore: + - 'docs/**' + - 'AGENTS.md' + - 'README.md' pull_request: branches: - master + paths-ignore: + - 'docs/**' + - 'AGENTS.md' + - 'README.md' permissions: contents: read From 71d835cd6c6ca96766b78494a1dd2e285e968868 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Mon, 17 Nov 2025 00:03:50 -0800 Subject: [PATCH 28/31] BIND Zone Provider, tests, documentation and task updates --- Dns/ZoneProvider/Bind/BindZoneProvider.cs | 872 +++++++++++++++++- Dns/ZoneProvider/FileWatcherZoneProvider.cs | 17 +- README.md | 27 + dnstest/BindZoneProviderTests.cs | 110 +++ .../TestData/Bind/invalid_missing_ttl.zone | 9 + dnstest/TestData/Bind/simple.zone | 15 + docs/providers/AP_provider.md | 51 + docs/providers/BIND_provider.md | 68 ++ docs/providers/IPProbe_provider.md | 79 ++ docs/task_list.md | 3 +- docs/tasks/task_11_plan.md | 41 + 11 files changed, 1272 insertions(+), 20 deletions(-) create mode 100644 dnstest/BindZoneProviderTests.cs create mode 100644 dnstest/TestData/Bind/invalid_missing_ttl.zone create mode 100644 dnstest/TestData/Bind/simple.zone create mode 100644 docs/providers/AP_provider.md create mode 100644 docs/providers/BIND_provider.md create mode 100644 docs/providers/IPProbe_provider.md create mode 100644 docs/tasks/task_11_plan.md diff --git a/Dns/ZoneProvider/Bind/BindZoneProvider.cs b/Dns/ZoneProvider/Bind/BindZoneProvider.cs index 29f1a90..810bf5a 100644 --- a/Dns/ZoneProvider/Bind/BindZoneProvider.cs +++ b/Dns/ZoneProvider/Bind/BindZoneProvider.cs @@ -7,33 +7,869 @@ namespace Dns.ZoneProvider.Bind { using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; using Dns.ZoneProvider; + /// + /// Zone provider that parses BIND-style forward zone files and publishes address records to SmartZoneResolver. + /// public class BindZoneProvider : FileWatcherZoneProvider { public override Zone GenerateZone() { - // RFC 1035 - https://tools.ietf.org/html/rfc1035 - // Forward scanning parser - // while(not EOF) - // State is in record - // General Field list : Name Class Type [(Data 0..*)] EOR - // $ORIGIN [name] - // $TTL Timespan - // [Name|@] IN SOA Name - // [Name|@] IN NS Name - // [Name|@] IN MX Priority Name - // [Name|@] IN A IPv4 - // [Name|@] IN AAAA IPv6 - // [Name|@] IN CNAME name - // endwhile - - throw new NotImplementedException(); + try + { + var parser = new ZoneFileParser(this.Filename, this.Zone); + IReadOnlyList records = parser.Parse(); + + Zone zone = new Zone(); + zone.Suffix = this.Zone; + zone.Serial = this._serial; + zone.Initialize(records); + + this._serial++; + return zone; + } + catch (BindZoneFileException ex) + { + Console.WriteLine("BIND zone parse error ({0}:{1}): {2}", this.Filename, ex.LineNumber, ex.Message); + } + catch (IOException ex) + { + Console.WriteLine("Unable to read BIND zone file {0}: {1}", this.Filename, ex.Message); + } + catch (UnauthorizedAccessException ex) + { + Console.WriteLine("Unable to access BIND zone file {0}: {1}", this.Filename, ex.Message); + } + catch (Exception ex) + { + Console.WriteLine("Unexpected error while parsing {0}: {1}", this.Filename, ex.Message); + } + + return null; } - public override void Dispose() + private sealed class ZoneFileParser { - throw new NotImplementedException(); + private readonly string _filename; + private readonly string _zoneRoot; + private readonly string _zoneRootSuffix; + private readonly Dictionary _records = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly string _defaultOrigin; + + private string _currentOrigin; + private string _lastOwner; + private bool _sawSoa; + private int _apexNsCount; + private uint? _defaultTtl; + private int _lastLineNumber; + + public ZoneFileParser(string filename, string zoneSuffix) + { + if (string.IsNullOrWhiteSpace(filename)) + { + throw new ArgumentException("filename"); + } + + this._filename = filename; + this._zoneRoot = NormalizeZoneSuffix(zoneSuffix); + this._zoneRootSuffix = "." + this._zoneRoot; + this._defaultOrigin = this._zoneRoot + "."; + this._currentOrigin = this._defaultOrigin; + } + + public IReadOnlyList Parse() + { + using (var reader = new StreamReader(this._filename)) + { + foreach (LogicalLine line in this.ReadLogicalLines(reader)) + { + this._lastLineNumber = line.LineNumber; + + if (string.IsNullOrWhiteSpace(line.Text)) + { + continue; + } + + if (line.Text.StartsWith("$", StringComparison.Ordinal)) + { + this.ProcessDirective(line); + } + else + { + this.ProcessRecord(line); + } + } + } + + if (!this._sawSoa) + { + throw new BindZoneFileException(this._lastLineNumber, "Zone file must contain exactly one SOA record."); + } + + if (this._apexNsCount == 0) + { + throw new BindZoneFileException(this._lastLineNumber, "Zone file must declare at least one NS record for the zone apex."); + } + + var zoneRecords = new List(); + + foreach (NameRecord record in this._records.Values) + { + if (record.Ipv4Addresses.Count > 0) + { + zoneRecords.Add(new ZoneRecord + { + Host = record.Name, + Addresses = record.Ipv4Addresses.ToArray(), + Count = record.Ipv4Addresses.Count, + Class = ResourceClass.IN, + Type = ResourceType.A, + }); + } + + if (record.Ipv6Addresses.Count > 0) + { + zoneRecords.Add(new ZoneRecord + { + Host = record.Name, + Addresses = record.Ipv6Addresses.ToArray(), + Count = record.Ipv6Addresses.Count, + Class = ResourceClass.IN, + Type = ResourceType.AAAA, + }); + } + } + + if (zoneRecords.Count == 0) + { + throw new BindZoneFileException(this._lastLineNumber, "Zone file did not produce any address records."); + } + + return zoneRecords; + } + + private void ProcessDirective(LogicalLine line) + { + List tokens = Tokenize(line.Text, line.LineNumber); + string directive = tokens[0].ToUpperInvariant(); + + switch (directive) + { + case "$ORIGIN": + if (tokens.Count != 2) + { + throw new BindZoneFileException(line.LineNumber, "$ORIGIN expects a single domain name argument."); + } + this.ApplyOrigin(tokens[1], line.LineNumber); + break; + case "$TTL": + if (tokens.Count != 2) + { + throw new BindZoneFileException(line.LineNumber, "$TTL expects a single value."); + } + this._defaultTtl = this.ParseTtl(tokens[1], line.LineNumber); + break; + case "$INCLUDE": + throw new BindZoneFileException(line.LineNumber, "$INCLUDE is not supported in this build."); + default: + throw new BindZoneFileException(line.LineNumber, string.Format(CultureInfo.InvariantCulture, "Unsupported directive '{0}'.", directive)); + } + } + + private void ProcessRecord(LogicalLine line) + { + List tokens = Tokenize(line.Text, line.LineNumber); + if (tokens.Count == 0) + { + return; + } + + int index = 0; + string owner; + + if (line.OwnerImplicit) + { + if (string.IsNullOrEmpty(this._lastOwner)) + { + throw new BindZoneFileException(line.LineNumber, "Record omitted owner but no previous owner exists."); + } + owner = this._lastOwner; + } + else + { + owner = this.CanonicalizeOwner(tokens[index++], line.LineNumber); + this._lastOwner = owner; + } + + string recordClass = "IN"; + uint? recordTtl = null; + + while (index < tokens.Count) + { + string token = tokens[index]; + if (IsClassToken(token)) + { + recordClass = token.ToUpperInvariant(); + index++; + continue; + } + + if (this.TryParseTtlToken(token, line.LineNumber, out uint ttl)) + { + recordTtl = ttl; + index++; + continue; + } + + break; + } + + if (!string.Equals(recordClass, "IN", StringComparison.OrdinalIgnoreCase)) + { + throw new BindZoneFileException(line.LineNumber, string.Format(CultureInfo.InvariantCulture, "Unsupported class '{0}'.", recordClass)); + } + + if (index >= tokens.Count) + { + throw new BindZoneFileException(line.LineNumber, "Record is missing a type token."); + } + + string typeToken = tokens[index++].ToUpperInvariant(); + List rdata = tokens.Skip(index).ToList(); + + NameRecord record = this.GetOrCreateRecord(owner); + + switch (typeToken) + { + case "SOA": + this.ParseSoa(owner, rdata, line.LineNumber); + break; + case "NS": + this.ParseNs(record, rdata, line.LineNumber); + break; + case "A": + this.ParseAddressRecord(record, rdata, line.LineNumber, ResourceType.A); + break; + case "AAAA": + this.ParseAddressRecord(record, rdata, line.LineNumber, ResourceType.AAAA); + break; + case "CNAME": + this.ParseCName(record, rdata, line.LineNumber); + break; + case "MX": + this.ParseMx(record, rdata, line.LineNumber); + break; + case "TXT": + this.ParseTxt(record, rdata, line.LineNumber); + break; + default: + throw new BindZoneFileException(line.LineNumber, string.Format(CultureInfo.InvariantCulture, "Record type '{0}' is not supported.", typeToken)); + } + + // TTL currently unused but parsing keeps validation pathways ready. + if (!recordTtl.HasValue && !this._defaultTtl.HasValue) + { + throw new BindZoneFileException(line.LineNumber, "Record does not specify a TTL and no default $TTL directive exists."); + } + } + + private void ParseSoa(string owner, List rdata, int lineNumber) + { + if (this._sawSoa) + { + throw new BindZoneFileException(lineNumber, "Multiple SOA records detected."); + } + + if (!string.Equals(owner, this._zoneRoot, StringComparison.OrdinalIgnoreCase)) + { + throw new BindZoneFileException(lineNumber, "SOA record must belong to the zone apex."); + } + + if (rdata.Count < 7) + { + throw new BindZoneFileException(lineNumber, "SOA record must include MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, and MINIMUM fields."); + } + + this.CanonicalizeName(rdata[0], lineNumber); // primary name server + this.CanonicalizeName(rdata[1], lineNumber); // responsible mailbox + + for (int i = 2; i < 7; i++) + { + if (!ulong.TryParse(rdata[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong _)) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "Invalid SOA numeric field '{0}'.", rdata[i])); + } + } + + this._sawSoa = true; + } + + private void ParseNs(NameRecord record, List rdata, int lineNumber) + { + if (rdata.Count != 1) + { + throw new BindZoneFileException(lineNumber, "NS record expects a single target name."); + } + + this.CanonicalizeName(rdata[0], lineNumber); + record.RegisterGenericRecord("NS", lineNumber); + + if (string.Equals(record.Name, this._zoneRoot, StringComparison.OrdinalIgnoreCase)) + { + this._apexNsCount++; + } + } + + private void ParseMx(NameRecord record, List rdata, int lineNumber) + { + if (rdata.Count < 2) + { + throw new BindZoneFileException(lineNumber, "MX record expects preference and target host."); + } + + if (!ushort.TryParse(rdata[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ushort _)) + { + throw new BindZoneFileException(lineNumber, "MX preference must be between 0 and 65535."); + } + + this.CanonicalizeName(rdata[1], lineNumber); + record.RegisterGenericRecord("MX", lineNumber); + } + + private void ParseTxt(NameRecord record, List rdata, int lineNumber) + { + if (rdata.Count == 0) + { + throw new BindZoneFileException(lineNumber, "TXT record must include at least one string literal."); + } + + record.RegisterGenericRecord("TXT", lineNumber); + } + + private void ParseCName(NameRecord record, List rdata, int lineNumber) + { + if (rdata.Count != 1) + { + throw new BindZoneFileException(lineNumber, "CNAME record expects a single target."); + } + + string target = this.CanonicalizeName(rdata[0], lineNumber); + record.SetCName(target, lineNumber); + } + + private void ParseAddressRecord(NameRecord record, List rdata, int lineNumber, ResourceType resourceType) + { + if (rdata.Count != 1) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "{0} record expects a single address.", resourceType)); + } + + if (!IPAddress.TryParse(rdata[0], out IPAddress address)) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "'{0}' is not a valid IP address.", rdata[0])); + } + + if (resourceType == ResourceType.A && address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + { + throw new BindZoneFileException(lineNumber, "A record data must be an IPv4 address."); + } + + if (resourceType == ResourceType.AAAA && address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + { + throw new BindZoneFileException(lineNumber, "AAAA record data must be an IPv6 address."); + } + + record.AddAddress(resourceType, address, lineNumber); + } + + private void ApplyOrigin(string value, int lineNumber) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new BindZoneFileException(lineNumber, "$ORIGIN directive requires a domain name."); + } + + string newOrigin; + if (value == "@") + { + newOrigin = this._defaultOrigin; + } + else if (value.EndsWith(".", StringComparison.Ordinal)) + { + newOrigin = value; + } + else + { + newOrigin = value + "." + TrimTrailingDot(this._currentOrigin); + } + + string normalized = TrimTrailingDot(newOrigin); + this.EnsureWithinZone(normalized, lineNumber); + this._currentOrigin = normalized + "."; + } + + private NameRecord GetOrCreateRecord(string owner) + { + NameRecord record; + if (!this._records.TryGetValue(owner, out record)) + { + record = new NameRecord(owner); + this._records.Add(owner, record); + } + + return record; + } + + private string CanonicalizeOwner(string token, int lineNumber) + { + string canonical = this.CanonicalizeName(token, lineNumber); + this.EnsureWithinZone(canonical, lineNumber); + return canonical; + } + + private string CanonicalizeName(string token, int lineNumber) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new BindZoneFileException(lineNumber, "Name token cannot be empty."); + } + + string input = token.Trim(); + if (input == "@") + { + return TrimTrailingDot(this._currentOrigin); + } + + if (input == ".") + { + throw new BindZoneFileException(lineNumber, "Root label '.' is not supported in this context."); + } + + if (input.EndsWith(".", StringComparison.Ordinal)) + { + return TrimTrailingDot(input); + } + + return TrimTrailingDot(input + "." + this._currentOrigin); + } + + private void EnsureWithinZone(string fqdn, int lineNumber) + { + if (fqdn.Equals(this._zoneRoot, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (!fqdn.EndsWith(this._zoneRootSuffix, StringComparison.OrdinalIgnoreCase)) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "Owner '{0}' falls outside of zone '{1}'.", fqdn, this._zoneRoot)); + } + } + + private uint ParseTtl(string token, int lineNumber) + { + if (!this.TryParseTtlToken(token, lineNumber, out uint ttl)) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "'{0}' is not a valid TTL.", token)); + } + + return ttl; + } + + private bool TryParseTtlToken(string token, int lineNumber, out uint value) + { + value = 0; + if (string.IsNullOrEmpty(token)) + { + return false; + } + + int index = 0; + while (index < token.Length && char.IsDigit(token[index])) + { + index++; + } + + if (index == 0) + { + return false; + } + + string numberPart = token.Substring(0, index); + if (!uint.TryParse(numberPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint magnitude)) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "Unable to parse TTL value '{0}'.", token)); + } + + if (index == token.Length) + { + value = magnitude; + return true; + } + + if (index != token.Length - 1) + { + return false; + } + + char suffix = char.ToLowerInvariant(token[index]); + uint multiplier; + switch (suffix) + { + case 's': + multiplier = 1; + break; + case 'm': + multiplier = 60; + break; + case 'h': + multiplier = 3600; + break; + case 'd': + multiplier = 86400; + break; + case 'w': + multiplier = 604800; + break; + default: + return false; + } + + ulong total = (ulong)magnitude * multiplier; + if (total > uint.MaxValue) + { + throw new BindZoneFileException(lineNumber, "TTL value is too large."); + } + + value = (uint)total; + return true; + } + + private static List Tokenize(string text, int lineNumber) + { + var tokens = new List(); + var builder = new StringBuilder(); + bool inQuotes = false; + bool escape = false; + + foreach (char current in text) + { + if (escape) + { + builder.Append(current); + escape = false; + continue; + } + + if (current == '\\') + { + escape = true; + continue; + } + + if (current == '"') + { + inQuotes = !inQuotes; + continue; + } + + if (!inQuotes && char.IsWhiteSpace(current)) + { + if (builder.Length > 0) + { + tokens.Add(builder.ToString()); + builder.Clear(); + } + continue; + } + + builder.Append(current); + } + + if (escape) + { + throw new BindZoneFileException(lineNumber, "Dangling escape sequence in record."); + } + + if (inQuotes) + { + throw new BindZoneFileException(lineNumber, "Unterminated quote detected."); + } + + if (builder.Length > 0) + { + tokens.Add(builder.ToString()); + } + + return tokens; + } + + private IEnumerable ReadLogicalLines(TextReader reader) + { + string line; + int lineNumber = 0; + int recordStartLine = 0; + bool recordHasContent = false; + bool ownerImplicit = false; + int parenDepth = 0; + var builder = new StringBuilder(); + + while ((line = reader.ReadLine()) != null) + { + lineNumber++; + string sanitized = this.StripComments(line, lineNumber, out int parenDelta, out bool startsWithWhitespace, out bool hasContent); + + if (hasContent && !recordHasContent) + { + recordHasContent = true; + recordStartLine = lineNumber; + ownerImplicit = startsWithWhitespace; + } + + if (hasContent) + { + if (builder.Length > 0) + { + builder.Append(' '); + } + + builder.Append(sanitized.Trim()); + } + + parenDepth += parenDelta; + if (parenDepth < 0) + { + throw new BindZoneFileException(lineNumber, "Unmatched ')' detected."); + } + + if (recordHasContent && parenDepth == 0) + { + yield return new LogicalLine(recordStartLine, builder.ToString(), ownerImplicit); + builder.Clear(); + recordHasContent = false; + ownerImplicit = false; + } + } + + if (parenDepth != 0) + { + throw new BindZoneFileException(lineNumber, "Unterminated multi-line record detected."); + } + } + + private string StripComments(string line, int lineNumber, out int parenDelta, out bool startsWithWhitespace, out bool hasContent) + { + var builder = new StringBuilder(); + bool inQuotes = false; + bool escape = false; + parenDelta = 0; + + for (int i = 0; i < line.Length; i++) + { + char current = line[i]; + + if (escape) + { + builder.Append(current); + escape = false; + continue; + } + + if (current == '\\') + { + escape = true; + builder.Append(current); + continue; + } + + if (!inQuotes && current == ';') + { + break; + } + + if (current == '"') + { + inQuotes = !inQuotes; + builder.Append(current); + continue; + } + + if (!inQuotes && (current == '(' || current == ')')) + { + parenDelta += current == '(' ? 1 : -1; + builder.Append(' '); + continue; + } + + builder.Append(current); + } + + if (escape) + { + throw new BindZoneFileException(lineNumber, "Dangling escape sequence inside line."); + } + + if (inQuotes) + { + throw new BindZoneFileException(lineNumber, "Unterminated quote inside line."); + } + + string sanitized = builder.ToString(); + int firstNonWhitespaceIndex = -1; + for (int i = 0; i < sanitized.Length; i++) + { + if (!char.IsWhiteSpace(sanitized[i])) + { + firstNonWhitespaceIndex = i; + break; + } + } + + hasContent = firstNonWhitespaceIndex >= 0; + startsWithWhitespace = hasContent && firstNonWhitespaceIndex > 0; + + return sanitized; + } + + private static string NormalizeZoneSuffix(string zone) + { + if (string.IsNullOrWhiteSpace(zone)) + { + throw new ArgumentException("zone"); + } + + string trimmed = zone.Trim(); + if (trimmed.StartsWith(".", StringComparison.Ordinal)) + { + trimmed = trimmed.Substring(1); + } + + if (trimmed.EndsWith(".", StringComparison.Ordinal)) + { + trimmed = trimmed.Substring(0, trimmed.Length - 1); + } + + if (string.IsNullOrEmpty(trimmed)) + { + throw new ArgumentException("zone"); + } + + return trimmed; + } + + private static string TrimTrailingDot(string value) + { + if (value.EndsWith(".", StringComparison.Ordinal)) + { + return value.Substring(0, value.Length - 1); + } + + return value; + } + + private static bool IsClassToken(string token) + { + return token.Equals("IN", StringComparison.OrdinalIgnoreCase) || + token.Equals("CH", StringComparison.OrdinalIgnoreCase) || + token.Equals("HS", StringComparison.OrdinalIgnoreCase); + } + + private sealed class LogicalLine + { + public LogicalLine(int lineNumber, string text, bool ownerImplicit) + { + this.LineNumber = lineNumber; + this.Text = text; + this.OwnerImplicit = ownerImplicit; + } + + public int LineNumber { get; } + + public string Text { get; } + + public bool OwnerImplicit { get; } + } + + private sealed class NameRecord + { + public NameRecord(string name) + { + this.Name = name; + } + + public string Name { get; } + + public HashSet Ipv4Addresses { get; } = new HashSet(); + + public HashSet Ipv6Addresses { get; } = new HashSet(); + + public string CNameTarget { get; private set; } + + private bool HasOtherRecords { get; set; } + + public void AddAddress(ResourceType resourceType, IPAddress address, int lineNumber) + { + this.EnsureNotCName(resourceType.ToString(), lineNumber); + + if (resourceType == ResourceType.A) + { + this.Ipv4Addresses.Add(address); + } + else + { + this.Ipv6Addresses.Add(address); + } + + this.HasOtherRecords = true; + } + + public void RegisterGenericRecord(string recordType, int lineNumber) + { + this.EnsureNotCName(recordType, lineNumber); + this.HasOtherRecords = true; + } + + public void SetCName(string target, int lineNumber) + { + if (this.HasOtherRecords) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "'{0}' already hosts other records and cannot also be a CNAME.", this.Name)); + } + + if (this.CNameTarget != null && !this.CNameTarget.Equals(target, StringComparison.OrdinalIgnoreCase)) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "Conflicting CNAME definition for '{0}'.", this.Name)); + } + + this.CNameTarget = target; + } + + private void EnsureNotCName(string recordType, int lineNumber) + { + if (this.CNameTarget != null) + { + throw new BindZoneFileException(lineNumber, string.Format(CultureInfo.InvariantCulture, "'{0}' is a CNAME and cannot host {1} records.", this.Name, recordType)); + } + } + } + } + + private sealed class BindZoneFileException : Exception + { + public BindZoneFileException(int lineNumber, string message) + : base(message) + { + this.LineNumber = lineNumber; + } + + public int LineNumber { get; } } } } diff --git a/Dns/ZoneProvider/FileWatcherZoneProvider.cs b/Dns/ZoneProvider/FileWatcherZoneProvider.cs index 363db82..6805768 100644 --- a/Dns/ZoneProvider/FileWatcherZoneProvider.cs +++ b/Dns/ZoneProvider/FileWatcherZoneProvider.cs @@ -107,7 +107,22 @@ private void Stop() private void OnTimer(object state) { this._timer.Change(Timeout.Infinite, Timeout.Infinite); - Task.Run(() => this.GenerateZone()).ContinueWith(t => this.Notify(t.Result)); + Task.Run(() => this.GenerateZone()).ContinueWith(t => + { + if (t.Status == TaskStatus.RanToCompletion) + { + Zone generatedZone = t.Result; + if (generatedZone != null) + { + this.Notify(generatedZone); + } + } + else if (t.IsFaulted) + { + Exception ex = t.Exception.GetBaseException(); + Console.WriteLine("Zone generation failed: {0}", ex.Message); + } + }, TaskScheduler.Default); } diff --git a/README.md b/README.md index 61c4f0f..48219c7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,33 @@ The DNS server has a built-in Web Server providing operational insight into the - counters - zone information +## Zone Providers +The server ships with several pluggable providers that publish authoritative data into `SmartZoneResolver`: + +- **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. +- **IPProbe provider** – continuously probes configured endpoints (ping/noop today) and only emits healthy addresses. Configuration and behavior live in `docs/providers/IPProbe_provider.md`. +- **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. + - See `docs/providers/BIND_provider.md` for configuration details, validation rules, and troubleshooting tips. + +### BIND Provider Configuration +Add the provider via `appsettings.json` (both `Dns` and `dns-cli` hosts read the same shape): + +```json +{ + "server": { + "zone": { + "name": ".example.com", + "provider": "Dns.ZoneProvider.Bind.BindZoneProvider" + } + }, + "zoneprovider": { + "FileName": "C:/zones/example.com.zone" + } +} +``` + +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. + ## Documentation - [Product requirements](docs/product_requirements.md) describe the current roadmap, observability goals, and .NET maintenance plans. - [Project priorities & plan](docs/priorities.md) outline the P0/P1/P2 focus areas plus execution notes (DI migration, OpenTelemetry instrumentation). diff --git a/dnstest/BindZoneProviderTests.cs b/dnstest/BindZoneProviderTests.cs new file mode 100644 index 0000000..265330c --- /dev/null +++ b/dnstest/BindZoneProviderTests.cs @@ -0,0 +1,110 @@ +// // //------------------------------------------------------------------------------------------------- +// // // +// // // Copyright (c) Steve Butler. All rights reserved. +// // // +// // //------------------------------------------------------------------------------------------------- + +namespace DnsTest +{ + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using Dns; + using Dns.ZoneProvider.Bind; + using DnsTest.Integration; + using Microsoft.Extensions.Configuration; + using Xunit; + + public class BindZoneProviderTests + { + [Fact] + public void GenerateZone_ReturnsZoneRecordsFromBindFile() + { + string zoneFile = Path.Combine(TestProjectPaths.TestDataDirectory, "Bind", "simple.zone"); + + using var provider = this.CreateProvider(zoneFile); + Zone zone = provider.GenerateZone(); + + Assert.NotNull(zone); + Assert.Equal(".example.com", zone.Suffix); + Assert.Equal(0u, zone.Serial); + + ZoneRecord wwwA = Assert.Single(zone.Where(record => record.Host == "www.example.com" && record.Type == ResourceType.A)); + Assert.Equal(IPAddress.Parse("192.0.2.10"), Assert.Single(wwwA.Addresses)); + + ZoneRecord wwwAaaa = Assert.Single(zone.Where(record => record.Host == "www.example.com" && record.Type == ResourceType.AAAA)); + Assert.Equal(IPAddress.Parse("2001:db8::10"), Assert.Single(wwwAaaa.Addresses)); + + ZoneRecord apex = Assert.Single(zone.Where(record => record.Host == "example.com" && record.Type == ResourceType.A)); + Assert.Contains(IPAddress.Parse("192.0.2.20"), apex.Addresses); + + ZoneRecord api = Assert.Single(zone.Where(record => record.Host == "api.example.com")); + Assert.Equal(IPAddress.Parse("192.0.2.30"), Assert.Single(api.Addresses)); + } + + [Fact] + public void GenerateZone_InvalidZoneReturnsNull() + { + string zoneFile = Path.Combine(TestProjectPaths.TestDataDirectory, "Bind", "invalid_missing_ttl.zone"); + + using var provider = this.CreateProvider(zoneFile); + Zone zone = provider.GenerateZone(); + + Assert.Null(zone); + } + + [Fact] + public void GenerateZone_ReturnsNullWhenCNameConflictsWithAddress() + { + string tempZone = this.WriteTempZoneFile(new[] + { + "$TTL 1h", + "$ORIGIN example.com.", + "@ IN SOA ns1.example.com. hostmaster.example.com. (", + " 2024010101", + " 7200", + " 3600", + " 1209600", + " 3600 )", + "@ IN NS ns1.example.com.", + "www IN CNAME api", + "www IN A 192.0.2.40", + "api IN A 192.0.2.50" + }); + + try + { + using var provider = this.CreateProvider(tempZone); + Zone zone = provider.GenerateZone(); + + Assert.Null(zone); + } + finally + { + File.Delete(tempZone); + } + } + + private BindZoneProvider CreateProvider(string zoneFile) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "FileName", zoneFile } + }) + .Build(); + + var provider = new BindZoneProvider(); + provider.Initialize(config, ".example.com"); + return provider; + } + + private string WriteTempZoneFile(IEnumerable lines) + { + string path = Path.GetTempFileName(); + File.WriteAllLines(path, lines); + return path; + } + } +} diff --git a/dnstest/TestData/Bind/invalid_missing_ttl.zone b/dnstest/TestData/Bind/invalid_missing_ttl.zone new file mode 100644 index 0000000..33950c4 --- /dev/null +++ b/dnstest/TestData/Bind/invalid_missing_ttl.zone @@ -0,0 +1,9 @@ +$ORIGIN example.com. +@ IN SOA ns1.example.com. hostmaster.example.com. ( + 2024010101 + 7200 + 3600 + 1209600 + 3600 ) +@ IN NS ns1.example.com. +www IN A 10.0.0.1 diff --git a/dnstest/TestData/Bind/simple.zone b/dnstest/TestData/Bind/simple.zone new file mode 100644 index 0000000..43ed357 --- /dev/null +++ b/dnstest/TestData/Bind/simple.zone @@ -0,0 +1,15 @@ +$TTL 1h +$ORIGIN example.com. +@ IN SOA ns1.example.com. hostmaster.example.com. ( + 2024010101 ; serial + 7200 ; refresh + 3600 ; retry + 1209600 ; expire + 3600 ) ; minimum + IN NS ns1.example.com. + IN NS ns2.example.com. +www 600 IN A 192.0.2.10 +www 600 IN AAAA 2001:db8::10 +@ IN A 192.0.2.20 +api IN A 192.0.2.30 +alias IN CNAME www diff --git a/docs/providers/AP_provider.md b/docs/providers/AP_provider.md new file mode 100644 index 0000000..f92025d --- /dev/null +++ b/docs/providers/AP_provider.md @@ -0,0 +1,51 @@ +# CSV/AP Zone Provider + +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. + +## Configuration + +Point the DNS host (or `dns-cli`) at the provider and supply a CSV path via `zoneprovider.FileName`: + +```json +{ + "server": { + "zone": { + "name": ".example.com", + "provider": "Dns.ZoneProvider.AP.APZoneProvider" + } + }, + "zoneprovider": { + "FileName": "C:/zones/machineinfo.csv" + } +} +``` + +`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. + +## CSV Schema + +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`): + +``` +#Fields:MachineName,MachineFunction,StaticIP +myhost01,www,192.0.2.10 +myhost02,www,192.0.2.11 +api01,api,192.0.2.20 +``` + +- The hostname served to DNS clients is ``, so with the example above and `ZoneName=".example.com"` the provider emits `www.example.com` and `api.example.com` records. +- Duplicate `MachineFunction` values are grouped and all IPv4 addresses are returned to SmartZoneResolver, enabling round-robin responses. +- The parser ignores blank lines and comment lines beginning with `#` or `;`. + +## Behavior & Limitations + +- Records are always `A`/`IN` entries; IPv6 is not supported. +- No TTL metadata exists in the CSV, so the DNS server continues using its default per-answer TTL (10 seconds today). +- The provider trusts the CSV contents—malformed IP addresses throw at parse time and block publication, logging the exception to the console. + +## Samples & Tests + +- `Dns/Data/machineinfo.csv` – legacy data used for local experiments. +- `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. + +Run `dotnet test csharp-dns-server.sln` after editing either the provider or its CSV assets to ensure the integration suite still passes. diff --git a/docs/providers/BIND_provider.md b/docs/providers/BIND_provider.md new file mode 100644 index 0000000..4091914 --- /dev/null +++ b/docs/providers/BIND_provider.md @@ -0,0 +1,68 @@ +# BIND Zone Provider + +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. + +## Configuration + +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: + +```json +{ + "server": { + "zone": { + "name": ".example.com", + "provider": "Dns.ZoneProvider.Bind.BindZoneProvider" + } + }, + "zoneprovider": { + "FileName": "C:/zones/example.com.zone" + } +} +``` + +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. + +## Supported Syntax & Records + +- **Directives**: `$ORIGIN`, `$TTL` are honored; `$INCLUDE` currently returns a validation error so you know the file is unsupported. +- **Records**: SOA, NS, A, AAAA, CNAME, MX, and TXT. Additional RR types incur an `unsupported record type` error. +- **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`). +- **Comments & multi-line records**: Semicolons outside quoted strings begin a comment. Parentheses join multi-line records, including SOA definitions. + +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. + +## Validation Guarantees + +Before replacing the active zone the provider enforces: + +1. **Lexical/syntactic**: balanced parentheses, terminated quotes, escaped characters, and valid TTL literals. +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. +3. **SOA/NS requirements**: exactly one SOA at the apex and at least one NS record for the zone root. +4. **Record semantics**: + - A/AAAA addresses must match their IP family; duplicates are suppressed. + - MX preference is a valid `ushort`; target names are canonicalized. + - CNAME exclusivity—once a name is a CNAME it cannot host other record types, and conflicting targets are rejected. + - Owner names must stay within the configured zone. +5. **Zone completeness**: at least one address record must be produced; otherwise the zone is considered unusable. + +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. + +## Unsupported Features + +The following are explicitly out of scope for this iteration, but the parser surfaces intentional errors so you know why a reload failed: + +- `$INCLUDE`, `$GENERATE`, DNSSEC record types, and all RR classes besides `IN`. +- Cross-record dependency checks (e.g., verifying MX targets exist) beyond the per-record rules listed above. +- Serving SOA/NS/MX/CNAME/TXT answers—these records are validated but not yet surfaced in `DnsServer` responses. + +## Troubleshooting + +1. **Console errors**: the provider logs `BIND zone parse error (:): `; fix the offending line and save the file to trigger a reload. +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. +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. + +## Testing & Samples + +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. + +For a ready-made example, `dnstest/TestData/Bind/simple.zone` demonstrates the accepted SOA, NS, A, AAAA, and CNAME records with mixed TTL declarations. diff --git a/docs/providers/IPProbe_provider.md b/docs/providers/IPProbe_provider.md new file mode 100644 index 0000000..314d2bd --- /dev/null +++ b/docs/providers/IPProbe_provider.md @@ -0,0 +1,79 @@ +# IPProbe Zone Provider + +`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. + +## Configuration + +The provider is enabled when `server.zone.provider` points to `Dns.ZoneProvider.IPProbe.IPProbeZoneProvider`. All other settings live under `zoneprovider`: + +```json +{ + "server": { + "zone": { + "name": ".example.com", + "provider": "Dns.ZoneProvider.IPProbe.IPProbeZoneProvider" + } + }, + "zoneprovider": { + "PollingIntervalSeconds": 15, + "Hosts": [ + { + "Name": "www", + "Probe": "ping", + "Timeout": 30, + "AvailabilityMode": "all", + "Ip": [ + "192.0.2.10", + "192.0.2.11" + ] + }, + { + "Name": "api", + "Probe": "noop", + "Timeout": 100, + "AvailabilityMode": "first", + "Ip": [ + "192.0.2.20", + "192.0.2.21" + ] + } + ] + } +} +``` + +### Host settings + +- `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`. +- `Probe`: strategy label. Built-in options are `ping` (ICMP echo), and `noop` (always healthy, helpful for lab testing). Unknown values fall back to `noop`. +- `Timeout`: milliseconds passed to the strategy implementation. +- `AvailabilityMode`: + - `all` – advertise every healthy IP. + - `first` – advertise only the first healthy IP (useful when you want to fail over to a single target). +- `Ip`: list of IPv4 or IPv6 addresses. Each entry is monitored independently but deduplicated if multiple hosts reference the same target. + +`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. + +## Health Evaluation + +- Every `Target` keeps a ring buffer of up to 10 recent `ProbeResult` entries. +- `Target.IsAvailable` returns true only if the last three results were successful. This smooths out occasional probe failures. +- When a probe function throws (e.g., ping exceptions) the provider treats the result as unavailable for the cycle. +- 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. + +## Behavior & Limitations + +- Records are emitted as `A` records today; the provider accepts IPv6 addresses but the resolver currently serves only IPv4 responses. +- There is no persistent storage—restarts lose probe history, so it may take a few cycles before `IsAvailable` returns true. +- Probe strategies run in parallel (up to four at a time). Ensure your environment allows outbound ICMP if you rely on `ping`. + +## Observability + +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. + +## Tests & Assets + +- `Dns/appsettings.json` ships with an example IPProbe configuration you can tweak for local smoke tests. +- `dnstest/Integration` wiring spins up `dns-cli` with probe data; add or update those assets whenever you change the provider surface. + +Always run `dotnet test csharp-dns-server.sln` before submitting changes so the integration harness exercises your updates end-to-end. diff --git a/docs/task_list.md b/docs/task_list.md index a217ba9..8db6856 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -10,7 +10,7 @@ 8. [ ] **T08 – Instrument DNS & HTTP surfaces (OpenTelemetry-ready)** — Add metrics/tracing hooks (without bundling collectors) so operators can export via OTLP (issue #16). 9. [x] **T09 – Fix CA2241 format warning** — Update the logging call in `Dns/DnsServer.cs` (line 250) to use the correct string-format arguments so builds are warning-free. 10. [ ] **T10 – Secure HTTP admin surface** — Provide configuration for bindings/authz and document operational guidance to avoid exposing diagnostic endpoints unintentionally. -11. [ ] **T11 – Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types (addresses “Static Zone declaration file” issue #1). +11. [x] **T11 – Complete BIND zone provider** — Implement parsing logic for `Dns/ZoneProvider/Bind`, supporting `$ORIGIN`, `$TTL`, and core record types (addresses “Static Zone declaration file” issue #1). 12. [ ] **T12 – Add dynamic configuration providers** — Introduce REST/service-backed configuration sources with validation and hot reload pipelines (issues #7/#8/#19). 13. [ ] **T13 – Implement parental/time-based/MAC policies** — Deliver requested zone behaviors (issues #3/#4/#9) leveraging the SmartZoneResolver framework. 14. [ ] **T14 – Extend health probes (HTTP/TCP)** — Add richer probe strategies with retries/weights within the IPProbe provider. @@ -27,3 +27,4 @@ 25. [ ] **T25 – Support larger UDP payloads (Medium)** — Increase `UdpListener` buffer sizing and/or detect truncated packets so EDNS-sized responses don’t silently corrupt parsing. 26. [ ] **T26 – Allow full 8-bit DNS labels (Medium)** — Relax `DnsProtocol.ReadString` ASCII enforcement in line with RFC 2181 so internationalized/underscored names don’t throw. 27. [x] **T27 – Refresh VS Code launch/tasks configs** — Update `.vscode/launch.json` and `tasks.json` to mirror the current build/test/debug workflow so contributors get accurate defaults. +28. [ ] **T28 – Evaluate grammar-based BIND parsing** — Prototype a BIND zone grammar in BNF/EBNF and assess tooling like Irony (lexer/parser generators) to simplify maintenance versus the current handwritten parser; document findings and recommended next steps. diff --git a/docs/tasks/task_11_plan.md b/docs/tasks/task_11_plan.md new file mode 100644 index 0000000..7323827 --- /dev/null +++ b/docs/tasks/task_11_plan.md @@ -0,0 +1,41 @@ +# Task T11 – Complete BIND Zone Provider + +## Goal +Deliver a production-ready `Dns/ZoneProvider/Bind` plugin that can read BIND-style forward-zone files, emit the records SmartZoneResolver expects, and fail fast with actionable diagnostics when zone files are invalid. + +## Feasibility +Feasible with current architecture: the zone provider abstraction already allows pluggable data sources, SmartZoneResolver caches zone sets with TTL/round-robin behavior, and docs/product_requirements.md explicitly calls for a BIND provider with `$ORIGIN`/`$TTL` handling and change detection. Work primarily involves parser/validator implementation plus deterministic tests and assets under `dnstest`. + +## Plan + +1. **Understand Inputs & Expectations** + - Review `Dns/ZoneProvider` interfaces (how providers publish `ZoneRecord` collections, reload cadence, logging hooks). + - Capture requirements from `docs/product_requirements.md` §2.2 and open issue #1 (“Static Zone declaration file”) to ensure priority record types (NS/A/AAAA/CNAME/MX/TXT) are in scope. Supporting SOA is a non-goal. + - Inventory how SmartZoneResolver currently consumes CSV/IPProbe output so the BIND provider returns consistent object models (zones keyed by fqdn + record set). + +2. **Design the Parser** + - Implement a streaming tokenizer that handles whitespace, comments (`;`), quoted strings, escaped characters, and parentheses for multi-line records. + - Support directives: `$ORIGIN`, `$TTL`, `$INCLUDE` (optional; stub/not-supported errors are acceptable if documented), ensuring defaults cascade per RFC 1035/2308. + - Parse owner name, TTL, class (`IN`), type, and RDATA for SOA/NS/A/AAAA/CNAME/MX/TXT to start; emit “unsupported RR type” diagnostics for others to keep validation strict. While SOA may be included in the file. It is a NON-GOAL to support SOA in the resolver. + - Treat zone-file reloads as atomic: stage parsed data in-memory before publishing to SmartZoneResolver so partial failures do not corrupt active zones. + +3. **Implement Comprehensive Validation** + - **Lexical/Syntactic**: detect malformed directives, unterminated quotes/parentheses, numeric bounds (TTL fits `uint`, MX preference range, IPv4/IPv6 shape). + - **Semantic**: enforce one SOA per zone file, at least one NS, A/AAAA data matches family, CNAME exclusivity, duplicate record suppression, and TTL min/max constraints. + - **Cross-record**: verify references (e.g., MX targets exist), ensure `$ORIGIN` changes do not leak records outside the zone, and optionally ensure serial monotonicity when reloading. + - Surface rich error messages with line/column indicators; fail fast before updating SmartZoneResolver if any validation errors exist. + +4. **Wire Provider Into the System** + - Create `Dns/ZoneProvider/Bind/BindZoneProvider.cs` (or similar) implementing the existing provider interface with configuration (path, reload interval, optional file watcher). + - Integrate configuration binding through `appsettings.json`/DI so dns-cli can enable the provider (mirroring CSV provider wiring). + - Record metrics/logging for reload success/failure counts to align with observability goals. + +5. **Testing & Assets** + - Unit tests under `dnstest` covering: directive handling, per-record parsing (SOA/NS/A/AAAA/CNAME/MX/TXT), validation failures (duplicate SOA, invalid TTLs, bad MX target, etc.), and error messaging. + - Integration tests leveraging existing `DnsCliAuthoritativeBehaviorTests`: drop sample `.zone` files under `dnstest/TestData/Bind` and boot dns-cli with the new provider to assert full query/response flows, TTL application, and failure modes (invalid file should prevent startup or log errors without modifying live zones). + - Add regression fixtures for edge cases: multi-line SOA records, records inheriting owner names, default TTL changes, comments interleaved with data. + +6. **Documentation & Follow-up** + - Document supported directives/types, configuration knobs, validation guarantees, and troubleshooting tips in `docs/task_list.md` (mark complete later) and README/docs as appropriate. + - Note any unsupported BIND features (e.g., `$GENERATE`, DNSSEC records) plus follow-up issues if needed. + - After implementation, run `dotnet format`, `dotnet build`, and `dotnet test csharp-dns-server.sln` to validate before submitting. From 9bad90ddca7758c88f94b34e0a9005b4989a1027 Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Mon, 17 Nov 2025 00:22:45 -0800 Subject: [PATCH 29/31] Documentation update --- README.md | 6 ++++-- docs/product_requirements.md | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 48219c7..d2a1852 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GitHub Actions Status](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml) -Fully functional DNS server written in C#. +Fully functional DNS server written in C# targeting .NET 8 exclusively. Ensure the .NET 8 SDK is installed before building or testing. The project was conceived while working to reduce the cost of datacentre "stamps" while providing robust services within a datacentre, specifically to remove the need for an expensive load-balancer device by providing round-robin DNS services, and retrying connectivity instead. @@ -28,8 +28,10 @@ This software is licenced under MIT terms that permits reuse within proprietary ``` +> **Note:** The solution targets `net8.0`; all commands above assume the .NET 8 SDK is available on your PATH. + ## Gotchas -- if you're running on Windows 10 with Docker Tools installed, Docker uses the ICS SharedAccess service to provide DNS resolution for Docker containers - this listens on UDP:53, and will conflict with the DNS project. Either turn off the the service (```net stop SharedAccess```), or change the UDP port. +- if you're running on Windows with Docker Tools installed, Docker uses the ICS SharedAccess service to provide DNS resolution for Docker containers - this listens on UDP:53, and will conflict with the DNS project. Either turn off the the service (```net stop SharedAccess```), or change the UDP port. ## Continuous Integration All pushes and pull requests against `main` run through `.github/workflows/ci.yml`, a GitHub Actions pipeline that restores, builds, and tests the full `csharp-dns-server.sln` on both Ubuntu and Windows runners using the .NET 8 SDK. diff --git a/docs/product_requirements.md b/docs/product_requirements.md index e14682c..7fdc8ef 100644 --- a/docs/product_requirements.md +++ b/docs/product_requirements.md @@ -2,7 +2,7 @@ ## 1. Overview - **Purpose**: capture the feature, testing, and operational requirements needed to evolve the C# DNS server into a production-ready, multi-platform service and seed long-term maintenance/AI-assisted development. -- **Current State**: single `.NET Core 3.1` solution (`Dns`, `dns-cli`, `dnstest`) providing an authoritative UDP DNS server with pluggable zone providers (CSV file, IP-health probes) and a minimal HTML status endpoint. No production deployments exist yet. +- **Current State**: unified `.NET 8` solution (`Dns`, `dns-cli`, `dnstest`) providing an authoritative UDP DNS server with pluggable zone providers (CSV file, IP-health probes) and a minimal HTML status endpoint. No production deployments exist yet. - **Primary Goals** - Ship a reliable DNS service with extensible zone data sources, configurable health probes, and first-class observability. - Establish a comprehensive automated testing strategy. @@ -92,4 +92,3 @@ - Metrics/logging are exported in structured formats and consumed by dashboards. - At least one additional zone provider (BIND or equivalent) and enhanced health probes are production-ready. - `AGENTS.md` is published and successfully guides AI/automation contributions within the defined scope. - From 71061f4215e90f4bda61d20a79550584f8e5b21b Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Mon, 17 Nov 2025 00:34:39 -0800 Subject: [PATCH 30/31] README update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2a1852..c96af06 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GitHub Actions Status](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stephbu/csharp-dns-server/actions/workflows/ci.yml) -Fully functional DNS server written in C# targeting .NET 8 exclusively. Ensure the .NET 8 SDK is installed before building or testing. +Fully functional DNS server written in C# targeting .NET 8. Ensure the .NET 8 SDK is installed before building or testing. The project was conceived while working to reduce the cost of datacentre "stamps" while providing robust services within a datacentre, specifically to remove the need for an expensive load-balancer device by providing round-robin DNS services, and retrying connectivity instead. From 498434f4fb97442a5c49f7a7381d19d18865c76e Mon Sep 17 00:00:00 2001 From: Steve Butler Date: Mon, 17 Nov 2025 01:12:36 -0800 Subject: [PATCH 31/31] Add IPv6 Tasks --- docs/task_list.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/task_list.md b/docs/task_list.md index 8db6856..f713b32 100644 --- a/docs/task_list.md +++ b/docs/task_list.md @@ -28,3 +28,4 @@ 26. [ ] **T26 – Allow full 8-bit DNS labels (Medium)** — Relax `DnsProtocol.ReadString` ASCII enforcement in line with RFC 2181 so internationalized/underscored names don’t throw. 27. [x] **T27 – Refresh VS Code launch/tasks configs** — Update `.vscode/launch.json` and `tasks.json` to mirror the current build/test/debug workflow so contributors get accurate defaults. 28. [ ] **T28 – Evaluate grammar-based BIND parsing** — Prototype a BIND zone grammar in BNF/EBNF and assess tooling like Irony (lexer/parser generators) to simplify maintenance versus the current handwritten parser; document findings and recommended next steps. +29. [ ] **T29 – IPv6 resolution support** — Extend SmartZoneResolver/DnsServer so AAAA records flow end-to-end (zone providers, dispensers, response writer) with regression tests proving dual-stack answers work across providers.