release Hex.pm Documentation

A pure Elixir DNS library that provides comprehensive DNS protocol message parsing, zone management, and resource record handling according to DNS RFC standards.

Features

  • Complete DNS Protocol Implementation: Full DNS message parsing and serialization
  • 20+ Resource Record Types: Support for A, AAAA, CNAME, MX, TXT, DNSSEC records, and more
  • Zone Management: Authoritative, stub, forward, and cache zone support
  • DNSSEC Support: Basic DNSSEC signing and validation capabilities
  • Binary Protocol Handling: Efficient binary parsing with domain name compression
  • Type Safety: Comprehensive type specifications throughout the codebase
  • Protocol-Based Architecture: Consistent behavior via DNS.Parameter and String.Chars protocols

Installation

Add ex_dns to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_dns, "~> 0.4.0"}
  ]
end

Quick Start

DNS Message Parsing

# Parse a DNS message from binary data
message = DNS.Message.from_iodata(binary_data)

# Create a new DNS query
message = DNS.Message.new()
|> DNS.Message.add_question(%DNS.Message.Question{
  name: DNS.Message.Domain.new("example.com"),
  type: DNS.ResourceRecordType.new(:a),
  class: DNS.Class.new(:in)
})

# Convert to binary for network transmission
binary_data = DNS.Parameter.to_iodata(message)

# Display as human-readable string
IO.puts(to_string(message))

Zone Management

# Create a new authoritative zone
zone = DNS.Zone.new("example.com", :authoritative)

# Add records interactively
{:ok, zone} = DNS.Zone.Editor.create_zone_interactive("example.com",
  type: :authoritative,
  soa: [
    mname: "ns1.example.com",
    rname: "admin.example.com",
    serial: 2024010101,
    refresh: 3600,
    retry: 1800,
    expire: 604800,
    minimum: 300
  ],
  ns: ["ns1.example.com", "ns2.example.com"],
  a: [{"@", "192.168.1.10"}, {"www", "192.168.1.10"}]
)

# Export zone to BIND format
{:ok, zone_file} = DNS.Zone.Editor.export_zone("example.com", format: :bind)

Architecture

Core Components

DNS.Message Protocol System

The library uses a protocol-based architecture where all DNS entities implement:

  • DNS.Parameter: Binary serialization/deserialization for network transmission
  • String.Chars: Human-readable string representations

DNS.Message Hierarchy

DNS.Message
 DNS.Message.Header        # Message header (ID, flags, counts)
 DNS.Message.Question      # Query section (QNAME, QTYPE, QCLASS)
 DNS.Message.Record        # Resource records (name, type, class, TTL, data)
 DNS.Message.Record.Data/* # 20+ specific record type implementations
     A, AAAA, CNAME, MX, TXT
     DNSSEC records (DNSKEY, RRSIG, DS, NSEC, NSEC3)
     Modern records (HTTPS, SVCB, TLSA, CAA)

DNS.Zone Management

DNS.Zone
 DNS.Zone.Manager         # CRUD operations and lifecycle management
 DNS.Zone.Store           # ETS-based persistent storage
 DNS.Zone.Cache           # TTL-based caching with expiration
 DNS.Zone.Loader          # Zone file loading from various sources
 DNS.Zone.FileParser      # BIND format zone file parsing
 DNS.Zone.Validator       # Zone validation and diagnostics
 DNS.Zone.DNSSEC          # DNSSEC signing and validation

Supported Record Types

The library supports over 20 DNS record types including:

  • Basic Records: A, AAAA, CNAME, MX, NS, PTR, TXT, SOA
  • Service Records: SRV, SSHFP, TLSA
  • DNSSEC Records: DNSKEY, RRSIG, DS, NSEC, NSEC3, NSEC3PARAM
  • Modern Records: HTTPS, SVCB, CAA
  • Experimental/Deprecated: Various historical and experimental types

Usage Examples

Working with DNS Messages

# Create a DNS query for an A record
query = DNS.Message.new()
|> DNS.Message.add_question(%DNS.Message.Question{
  name: DNS.Message.Domain.new("example.com"),
  type: DNS.ResourceRecordType.new(:a),
  class: DNS.Class.new(:in)
})

# Parse response
response = DNS.Message.from_iodata(response_binary)

# Extract answer records
answers = response.anlist
# => [%DNS.Message.Record{name: "example.com", type: :a, data: %{ip: {93, 184, 216, 34}}}]

Zone File Operations

# Load zone from BIND format file
{:ok, zone} = DNS.Zone.Loader.load_zone_from_file("example.com", "example.com.zone")

# Parse zone from string
zone_content = """
$TTL 3600
$ORIGIN example.com.
@ IN SOA ns1.example.com. admin.example.com. (
    2024010101 ; serial
    3600       ; refresh
    1800       ; retry
    604800     ; expire
    300        ; minimum
)
@ IN NS ns1.example.com.
www IN A 192.168.1.100
"""

{:ok, zone} = DNS.Zone.Loader.load_zone_from_string(zone_content)

Record Management

# Add various record types
{:ok, zone} = DNS.Zone.Editor.add_record("example.com", :a,
  name: "www.example.com",
  ip: {192, 168, 1, 100},
  ttl: 300
)

{:ok, zone} = DNS.Zone.Editor.add_record("example.com", :mx,
  name: "example.com",
  preference: 10,
  exchange: "mail.example.com"
)

{:ok, zone} = DNS.Zone.Editor.add_record("example.com", :txt,
  name: "example.com",
  text: "v=spf1 mx ~all"
)

# Search records
{:ok, a_records} = DNS.Zone.Editor.search_records("example.com", type: :a)
{:ok, www_records} = DNS.Zone.Editor.search_records("example.com", name: "www.example.com")

DNSSEC Operations

# Enable DNSSEC for a zone
case DNS.Zone.Editor.enable_dnssec("example.com") do
  {:ok, signed_zone} ->
    IO.puts("DNSSEC enabled successfully")
  {:error, reason} ->
    IO.puts("DNSSEC setup failed: #{reason}")
end

# Manual zone signing
case DNS.Zone.DNSSEC.sign_zone(zone,
  algorithm: :rsasha256,
  key_size: 2048,
  nsec3_enabled: true
) do
  {:ok, signed_zone} ->
    IO.puts("Zone signed successfully")
  {:error, reason} ->
    IO.puts("Signing failed: #{reason}")
end

Zone Validation

# Validate zone configuration
case DNS.Zone.Validator.validate_zone(zone) do
  {:ok, result} ->
    IO.puts("Zone is valid: #{result.zone_name}")
  {:error, result} ->
    IO.puts("Zone has errors: #{inspect(result.errors)}")
end

# Generate comprehensive diagnostics
diagnostics = DNS.Zone.Validator.generate_diagnostics(zone)
IO.inspect(diagnostics.statistics)
IO.inspect(diagnostics.security_assessment)
IO.inspect(diagnostics.recommendations)

Development

Testing

# Run all tests
mix test

# Run specific test file
mix test test/dns/message_test.exs

# Run tests including WIP tagged tests
mix test --include wip

# Run tests with detailed output
mix test --trace

Code Quality

# Format code
mix format

# Check if code is formatted
mix format --check-formatted

# Run static code analysis
mix credo

# Run type checking
mix dialyzer

Manual Testing Scripts

# Test all String.Chars implementations
elixir test_all_string_chars.exs

# Test zone system functionality
elixir test_zone_system.exs

Performance Considerations

Domain Name Compression

Domain name compression is implemented in DNS.Message.Domain.parse_domain_from_message/2 with security measures to prevent compression loop attacks.

Binary Pattern Matching

The library heavily utilizes Elixir's pattern matching on binaries for efficient DNS protocol parsing, particularly in performance-critical paths.

ETS-Based Storage

Zone management uses ETS tables for high-concurrency in-memory storage with separate tables for zone data and metadata.

Security Notes

Current Limitations

  • DNSSEC implementation uses placeholder cryptographic functions (production use requires proper crypto implementations)
  • Domain compression depth limits should be enforced in production
  • Record length fields (rdlength) require validation to prevent memory exhaustion attacks

Recommendations

  • Validate all binary input data before processing
  • Implement rate limiting for DNS message processing
  • Use proper cryptographic libraries for DNSSEC operations
  • Monitor for compression loop attacks in domain name parsing

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

References