Skip to content

RNDC Integration

bindcar integrates with BIND9 through the native RNDC (Remote Name Daemon Control) protocol to manage DNS zones dynamically.

Architecture Overview

graph TD A[HTTP API Request] --> B[bindcar Handler] B --> C[RNDC Executor] C --> D[rndc crate] D --> E[RNDC Protocol<br/>TCP Connection] E --> F[BIND9 Server<br/>:953] F --> G[Zone Files] E --> H{Response} H -->|Success| I[Parse Response Text] H -->|Error| J[Parse Error Message] I --> K[Success Response] J --> L[Error Response] style C fill:#e1f5ff style D fill:#c8e6c9 style F fill:#e1ffe1

RNDC Command Execution

Native Protocol Model

bindcar communicates with BIND9 using the native RNDC protocol via the rndc crate:

use rndc::RndcClient;

pub struct RndcExecutor {
    client: RndcClient,
}

impl RndcExecutor {
    pub fn new(server: String, algorithm: String, secret: String) -> Result<Self> {
        let client = RndcClient::new(&server, &algorithm, &secret);
        Ok(Self { client })
    }

    async fn execute(&self, command: &str) -> Result<String> {
        let result = tokio::task::spawn_blocking({
            let client = self.client.clone();
            let command = command.to_string();
            move || client.rndc_command(&command)
        }).await?;

        match result {
            Ok(rndc_result) => {
                if let Some(err) = &rndc_result.err {
                    return Err(anyhow::anyhow!("RNDC command failed: {}", err));
                }
                Ok(rndc_result.text.unwrap_or_default())
            }
            Err(e) => Err(anyhow::anyhow!("RNDC command failed: {}", e))
        }
    }
}

Key Characteristics

  • Native Protocol - Direct RNDC protocol communication, no subprocess overhead
  • Asynchronous - Non-blocking command execution using tokio spawn_blocking
  • Authenticated - HMAC-based authentication with configurable algorithms
  • Error Handling - Structured error responses from BIND9
  • Efficient - No process spawning, direct TCP communication
  • Configurable - Supports environment variables or rndc.conf parsing

Configuration

bindcar can be configured in two ways:

Option 1: Environment Variables

export RNDC_SERVER="127.0.0.1:953"
export RNDC_ALGORITHM="sha256"
export RNDC_SECRET="dGVzdC1zZWNyZXQtaGVyZQ=="

Supported algorithms (with or without hmac- prefix): - md5 / hmac-md5 - sha1 / hmac-sha1 - sha224 / hmac-sha224 - sha256 / hmac-sha256 - sha384 / hmac-sha384 - sha512 / hmac-sha512

Option 2: Automatic rndc.conf Parsing

If RNDC_SECRET is not set, bindcar automatically parses /etc/bind/rndc.conf or /etc/rndc.conf:

# /etc/bind/rndc.conf
include "/etc/bind/rndc.key";

options {
    default-server 127.0.0.1;
    default-key "rndc-key";
};

The parser supports include directives and will automatically load key files:

# /etc/bind/rndc.key
key "rndc-key" {
    algorithm hmac-sha256;
    secret "dGVzdC1zZWNyZXQtaGVyZQ==";
};

RNDC Commands Used

graph TD Root[RNDC Commands] --> Lifecycle[Zone Lifecycle] Root --> State[Zone State] Root --> Server[Server Operations] Lifecycle --> addzone[addzone] Lifecycle --> delzone[delzone] Lifecycle --> reload[reload] addzone --> adddesc["Create new zone<br/>Add to BIND9 config"] delzone --> deldesc["Remove zone<br/>Delete from BIND9"] reload --> reloaddesc["Reload zone data<br/>Update records"] State --> freeze[freeze] State --> thaw[thaw] freeze --> freezedesc["Pause updates<br/>Manual editing"] thaw --> thawdesc["Resume updates<br/>Re-enable dynamic"] Server --> status[status] Server --> notify[notify] status --> statusdesc["Server info<br/>Zone count"] notify --> notifydesc["Trigger transfer<br/>Update secondaries"] style Root fill:#e1f5ff style Lifecycle fill:#c8e6c9 style State fill:#fff3e0 style Server fill:#f3e5f5

addzone

Add a new zone to BIND9 dynamically:

rndc addzone example.com '{ type primary; file "/var/cache/bind/example.com.zone"; };'

When Used: POST /api/v1/zones

Error Scenarios: - Zone already exists - Invalid zone configuration - Permission denied - BIND9 not running

delzone

Remove a zone from BIND9:

rndc delzone example.com

When Used: DELETE /api/v1/zones/{name}

Error Scenarios: - Zone does not exist - Zone is a built-in zone - Permission denied

reload

Reload a specific zone:

rndc reload example.com

When Used: POST /api/v1/zones/{name}/reload

Error Scenarios: - Zone does not exist - Zone file syntax error - Permission denied

status

Get BIND9 server status:

rndc status

When Used: GET /api/v1/server/status

Returns: - BIND9 version - Number of zones - Server uptime - Resource usage

freeze/thaw

Freeze or thaw dynamic zone updates:

rndc freeze example.com
rndc thaw example.com

When Used: - POST /api/v1/zones/{name}/freeze - POST /api/v1/zones/{name}/thaw

Use Cases: - Manual zone file editing - Backup operations - Maintenance windows

notify

Trigger zone transfer to secondary servers:

rndc notify example.com

When Used: POST /api/v1/zones/{name}/notify

Triggers: - NOTIFY messages to secondaries - Zone transfer (AXFR/IXFR)

Zone File Management

File Creation Workflow

When a zone is created via API:

flowchart TD Start[POST /api/v1/zones] --> Validate{Validate Request} Validate -->|Invalid| Error400[400 Bad Request] Validate -->|Valid| GenFile[Generate Zone File] GenFile --> CheckExists{Check if<br/>file exists} CheckExists -->|Exists| Error409[409 Conflict] CheckExists -->|Not exists| WriteFile[Write Zone File<br/>to Disk] WriteFile --> WriteSuccess{Write<br/>Success?} WriteSuccess -->|Fail| Error500[500 Internal Error] WriteSuccess -->|Success| ExecRNDC[Execute RNDC<br/>addzone command] ExecRNDC --> RNDCSuccess{RNDC<br/>Success?} RNDCSuccess -->|Fail| Cleanup[Delete zone file] Cleanup --> Error500[500 Internal Server Error] RNDCSuccess -->|Success| Success201[201 Created] style Error400 fill:#ffe1e1 style Error409 fill:#ffe1e1 style Error500 fill:#ffe1e1 style Error500 fill:#ffe1e1 style Success201 fill:#e1ffe1

File Creation Steps

  1. Validate Request - Check zone name, SOA record, NS records, etc.
  2. Generate Zone File - Create BIND9 format zone file content
  3. Write to Disk - Save to BIND_ZONE_DIR/{zone_name}.zone
  4. Execute addzone - Register zone with BIND9 via RNDC
  5. Cleanup on Failure - Remove zone file if RNDC command fails

Example zone file generation:

$TTL 3600
@       IN      SOA     ns1.example.com. admin.example.com. (
                        2024010101 ; Serial
                        3600       ; Refresh
                        1800       ; Retry
                        604800     ; Expire
                        86400 )    ; Negative TTL

@       IN      NS      ns1.example.com.
@       IN      A       192.0.2.1

File Naming Convention

Zone files are named using the pattern:

{zone_name}.zone

Examples: - example.com.zone - sub.example.com.zone - 192.in-addr.arpa.zone

Shared Volume Requirements

In sidecar deployments, bindcar and BIND9 must share the zone directory:

volumes:
- name: zones
  emptyDir: {}

containers:
- name: bind9
  volumeMounts:
  - name: zones
    mountPath: /var/cache/bind

- name: bindcar
  volumeMounts:
  - name: zones
    mountPath: /var/cache/bind

Error Handling

RNDC Command Failures

bindcar maps RNDC errors to HTTP status codes:

RNDC Error HTTP Status Reason
zone already exists 409 Conflict Zone exists
not found 404 Not Found Zone doesn't exist
permission denied 500 Internal Server Error RNDC permission issue
syntax error 500 Internal Server Error Invalid zone file
Connection refused 500 Internal Server Error BIND9 not running

Error Response Format

{
  "error": "Failed to execute RNDC command",
  "details": "rndc: 'addzone' failed: zone already exists"
}

Logging

RNDC operations are logged at multiple levels:

{
  "level": "info",
  "message": "Executing RNDC command",
  "command": "addzone",
  "zone": "example.com"
}
{
  "level": "error",
  "message": "RNDC command failed",
  "command": "addzone",
  "zone": "example.com",
  "error": "zone already exists",
  "exit_code": 1
}

Security Considerations

RNDC Key Authentication

BIND9 uses rndc.key for authentication. In Kubernetes:

volumes:
- name: rndc-key
  secret:
    secretName: rndc-key

containers:
- name: bind9
  volumeMounts:
  - name: rndc-key
    mountPath: /etc/bind/rndc.key
    subPath: rndc.key
    readOnly: true

- name: bindcar
  volumeMounts:
  - name: rndc-key
    mountPath: /etc/bind/rndc.key
    subPath: rndc.key
    readOnly: true

File Permissions

Zone directory must be writable by bindcar:

securityContext:
  fsGroup: 101  # bind group
  runAsUser: 101
  runAsNonRoot: true

Command Injection Prevention

bindcar validates all zone names to prevent command injection:

  • Alphanumeric characters
  • Hyphens
  • Dots (for subdomains)
  • No shell metacharacters

Performance Characteristics

Command Execution Time

Typical RNDC command execution times using native protocol:

  • addzone: 5-30ms (improved from subprocess approach)
  • delzone: 5-20ms (improved from subprocess approach)
  • reload: 3-15ms (improved from subprocess approach)
  • status: 3-10ms (improved from subprocess approach)

Performance benefits of native protocol: - No subprocess spawning overhead - Direct TCP communication - Efficient binary protocol - Reduced system call overhead

Concurrency

bindcar handles multiple concurrent RNDC operations:

  • Async/await pattern for non-blocking execution
  • Native protocol allows multiple concurrent connections
  • No explicit locking required
  • BIND9 handles internal synchronization
  • spawn_blocking prevents blocking the async runtime

Resource Usage

Native RNDC protocol has minimal overhead:

  • No persistent connections (connects per command)
  • No subprocess spawning
  • Minimal memory footprint
  • Direct binary protocol (no stdout/stderr parsing)

Troubleshooting

RNDC Connection Issues

Symptom: 500 errors, "connection refused" or "Failed to execute RNDC command"

Causes: - BIND9 not running - BIND9 not listening on port 953 - RNDC key/secret mismatch - Network connectivity issues - Incorrect RNDC_SERVER address

Diagnosis:

# Check BIND9 is running
ps aux | grep named

# Check BIND9 is listening on port 953
netstat -tuln | grep 953
# or
ss -tuln | grep 953

# Verify RNDC configuration
cat /etc/bind/rndc.conf

# Test connectivity to RNDC port
nc -zv 127.0.0.1 953

# Check bindcar logs for RNDC errors
docker logs bindcar | grep -i rndc

Permission Errors

Symptom: 500 errors, "permission denied"

Causes: - Zone directory not writable - RNDC key not readable - SELinux/AppArmor restrictions

Diagnosis:

# Check directory permissions
ls -la /var/cache/bind

# Check RNDC key permissions
ls -la /etc/bind/rndc.key

# Test writing to zone directory
touch /var/cache/bind/test.txt

Zone File Syntax Errors

Symptom: Reload fails with syntax error

Causes: - Invalid DNS record format - Missing SOA record - Invalid TTL values

Diagnosis:

# Check zone file syntax
named-checkzone example.com /var/cache/bind/db.example.com

# View recent logs
tail -f /var/log/syslog | grep named

RNDC Output Parsing

bindcar includes comprehensive parsers for RNDC command outputs using the nom parser combinator library. This enables structured parsing of BIND9 responses for reliable zone configuration management.

Parser Architecture

graph LR A[RNDC Output] --> B[nom Parser] B --> C[ZoneConfig] C --> D[Modify Fields] D --> E[Serialize] E --> F[RNDC Input] style B fill:#e1f5ff style C fill:#c8e6c9

Supported Commands

showzone Parser

Parses rndc showzone <zone> output into structured ZoneConfig:

use bindcar::rndc_parser::parse_showzone;

let output = r#"zone "example.com" {
    type primary;
    file "/var/cache/bind/example.com.zone";
    allow-transfer { 10.0.0.1/32; 10.0.0.2/32; };
    also-notify { 10.0.0.1; 10.0.0.2; };
};"#;

let config = parse_showzone(output)?;
assert_eq!(config.zone_name, "example.com");
assert_eq!(config.zone_type, ZoneType::Primary);

Supported Fields: - zone_name - Zone domain name - zone_type - Primary, Secondary, Stub, Forward, Hint, Mirror, Delegation, Redirect - class - IN, CH, HS (default: IN) - file - Zone file path - primaries - Primary server IPs with optional ports (for secondary zones) - also-notify - IPs to notify on zone changes - allow-transfer - IPs allowed to transfer the zone - allow-update - IPs allowed to update the zone (key references ignored)

CIDR Notation Support

The parser automatically handles CIDR notation in IP address lists:

// Input from BIND9
let input = r#"zone "internal.local" {
    type primary;
    allow-transfer { 10.244.1.18/32; 10.244.1.21/32; };
};"#;

let config = parse_showzone(input)?;

// CIDR suffix is stripped, only IP addresses extracted
assert_eq!(config.allow_transfer.unwrap(), vec![
    "10.244.1.18".parse::<IpAddr>()?,
    "10.244.1.21".parse::<IpAddr>()?,
]);

Why CIDR Stripping? - BIND9 outputs CIDR notation (/32, /128) in ACLs - bindcar stores only IP addresses for simplicity - CIDR information is not needed for zone modification - Reduces complexity in zone configuration updates

Key-Based Access Control

The parser handles key-based allow-update directives:

// Input with TSIG key reference
let input = r#"zone "example.com" {
    type primary;
    allow-update { key "update-key"; };
};"#;

let config = parse_showzone(input)?;

// Key references are ignored, only IP addresses extracted
assert_eq!(config.allow_update, Some(vec![])); // Empty - no IPs

Rationale: - TSIG keys managed separately from zone configuration - bindcar focuses on IP-based ACLs for API operations - Key-based updates use BIND9's existing TSIG infrastructure - Prevents accidental modification of key-based permissions

Zone Type Support

Parser accepts both modern and legacy BIND9 terminology:

Modern Legacy ZoneType
primary master Primary
secondary slave Secondary
stub stub Stub
forward forward Forward
hint hint Hint
mirror mirror Mirror
delegation-only delegation-only Delegation
redirect redirect Redirect
// Both formats are accepted
parse_showzone(r#"zone "a.com" { type master; };"#)?; // Legacy
parse_showzone(r#"zone "b.com" { type primary; };"#)?; // Modern

Round-Trip Serialization

Zone configurations can be parsed, modified, and serialized back to RNDC format:

use bindcar::rndc_parser::parse_showzone;

// 1. Parse BIND9 output
let output = rndc_executor.showzone("example.com").await?;
let mut config = parse_showzone(&output)?;

// 2. Modify configuration
config.also_notify = Some(vec![
    "10.0.0.3".parse()?,
    "10.0.0.4".parse()?,
]);

// 3. Serialize back to RNDC format
let rndc_block = config.to_rndc_block();
// Output: "{ type primary; file "..."; also-notify { 10.0.0.3; 10.0.0.4; }; };"

// 4. Update zone in BIND9
rndc_executor.modzone("example.com", &rndc_block).await?;

Error Handling

Parser provides detailed error messages:

use bindcar::rndc_parser::{parse_showzone, RndcParseError};

match parse_showzone(invalid_input) {
    Ok(config) => { /* Use config */ },
    Err(RndcParseError::ParseError(msg)) => {
        eprintln!("Parse failed: {}", msg);
    },
    Err(RndcParseError::InvalidZoneType(type_str)) => {
        eprintln!("Unknown zone type: {}", type_str);
    },
    Err(RndcParseError::InvalidIpAddress(addr)) => {
        eprintln!("Invalid IP address: {}", addr);
    },
    Err(RndcParseError::MissingField(field)) => {
        eprintln!("Required field missing: {}", field);
    },
    Err(RndcParseError::Incomplete) => {
        eprintln!("Incomplete input");
    },
}

Parser Implementation

The parser uses nom combinators for robust parsing:

// Primitive parsers
fn quoted_string(input: &str) -> IResult<&str, String>
fn identifier(input: &str) -> IResult<&str, &str>
fn ip_addr(input: &str) -> IResult<&str, IpAddr>
fn ip_with_port(input: &str) -> IResult<&str, PrimarySpec>

// Zone statement parsers
fn parse_type_statement(input: &str) -> IResult<&str, ZoneStatement>
fn parse_file_statement(input: &str) -> IResult<&str, ZoneStatement>
fn parse_primaries_statement(input: &str) -> IResult<&str, ZoneStatement>
fn parse_also_notify_statement(input: &str) -> IResult<&str, ZoneStatement>
fn parse_allow_transfer_statement(input: &str) -> IResult<&str, ZoneStatement>
fn parse_allow_update_statement(input: &str) -> IResult<&str, ZoneStatement>

// Top-level parser
pub fn parse_showzone(input: &str) -> ParseResult<ZoneConfig>

Use Cases

Zone Modification (PATCH /api/v1/zones/{name}):

// Get current configuration
let output = state.rndc.showzone(&zone_name).await?;
let mut config = parse_showzone(&output)?;

// Update fields from API request
if let Some(also_notify) = request.also_notify {
    config.also_notify = Some(also_notify);
}

// Apply changes
let rndc_block = config.to_rndc_block();
state.rndc.modzone(&zone_name, &rndc_block).await?;

Zone Inspection:

// Parse zone configuration
let output = rndc_executor.showzone("example.com").await?;
let config = parse_showzone(&output)?;

// Inspect zone details
println!("Zone: {}", config.zone_name);
println!("Type: {}", config.zone_type.as_str());
println!("File: {:?}", config.file);
println!("Notify: {:?}", config.also_notify);

Testing

The parser includes comprehensive test coverage:

#[test]
fn test_parse_real_world_output() {
    let input = r#"zone "internal.local" {
        type primary;
        file "/var/cache/bind/internal.local.zone";
        allow-transfer { 10.244.1.18/32; 10.244.1.21/32; };
        allow-update { key "bindy-operator"; };
        also-notify { 10.244.1.18; 10.244.1.21; };
    };"#;

    let config = parse_showzone(input).unwrap();

    assert_eq!(config.zone_name, "internal.local");
    assert_eq!(config.zone_type, ZoneType::Primary);
    assert_eq!(config.allow_transfer.unwrap().len(), 2);
    assert_eq!(config.also_notify.unwrap().len(), 2);
}

#[test]
fn test_parse_ip_addr_with_cidr() {
    assert_eq!(
        ip_addr("192.168.1.1/32").unwrap().1,
        "192.168.1.1".parse::<IpAddr>().unwrap()
    );
}

Limitations

Not Currently Parsed: - Views and ACL names (e.g., allow-transfer { "trusted"; };) - Complex ACL expressions (e.g., { !10.0.0.1; any; }) - Key definitions (only references are skipped) - Custom zone options beyond documented fields

Future Enhancements: - Parser for rndc zonestatus output - Parser for rndc status output - Parser for rndc.conf files - Support for ACL names and expressions

Best Practices

  1. Validate Early - Validate zone data before executing RNDC commands
  2. Log Everything - Log all RNDC operations for audit trail
  3. Handle Errors Gracefully - Provide clear error messages to API clients
  4. Monitor RNDC Health - Use /api/v1/server/status to monitor BIND9
  5. Use Timeouts - Set reasonable timeouts for RNDC command execution
  6. Share Volumes Correctly - Ensure BIND9 and bindcar can both access zone files
  7. Secure RNDC Keys - Use Kubernetes secrets for rndc.key in production
  8. Parse Before Modify - Always parse showzone output before modifying zones
  9. Test Parser Changes - Validate parser against real BIND9 output

Next Steps