Dynamic DNS Response Rewriting in BIND Based on Client Subnet (Views vs RPZ Solutions)


17 views

Working with legacy infrastructure often presents unique DNS challenges. In healthcare environments where network changes require extensive coordination, we frequently encounter IP scheme conflicts with residential networks - particularly when 192.168.0.0/23 overlaps with common home router configurations.

The existing 1:1 NAT solution (192.168.0.0/23 ↔ 10.22.0.0/23) functions at the network layer but breaks at DNS resolution. The cron-based view implementation has several technical drawbacks:

# Current implementation (simplified)
*/5 * * * * sed -e 's/192.168.0./10.22.0./' -e 's/192.168.1./10.22.1./' \
/var/lib/bind/db.company.local > /var/lib/bind/db.company.local.ext \
&& rndc reload company.local in extView

Instead of maintaining separate zone files, we can implement response rewriting within BIND itself. Here's a more elegant solution using BIND's built-in functionality:

view "internal" {
    match-clients { !10.22.0.0/23; localhost; };
    zone "company.local" {
        type master;
        file "/var/lib/bind/db.company.local";
    };
};

view "vpn" {
    match-clients { 10.22.0.0/23; };
    zone "company.local" {
        type master;
        file "/var/lib/bind/db.company.local";
        response-policy {
            zone "rpz.company.local";
        };
    };
};

zone "rpz.company.local" {
    type master;
    file "/var/lib/bind/db.rpz.company.local";
    allow-query { none; };
};

Create a response policy zone (RPZ) file that handles the translation dynamically:

; db.rpz.company.local
$TTL 1H
@ SOA ns1.company.local. hostmaster.company.local. (
    2023081501 ; serial
    1h ; refresh
    15m ; retry
    1w ; expiry
    1h ; minimum
)
NS ns1.company.local.

; Rewrite rules
192.168.0.0/23 CNAME rewrite.company.local.
rewrite.company.local A 10.22.0.1

; Specific host rewrites
server1.company.local A 10.22.0.50 ; instead of 192.168.0.50

When implementing this solution:

  • Enable rndc trace to monitor RPZ processing
  • Consider adding rpz-passthru for non-matching queries
  • Test with dig +short @localhost server1.company.local from VPN clients

For BIND 9.16+, you can use the rewrite statement for more granular control:

view "vpn" {
    match-clients { 10.22.0.0/23; };
    rewrite {
        name prefix "192.168.0." "10.22.0.";
        name prefix "192.168.1." "10.22.1.";
    };
    // ... rest of view configuration
};

Many organizations face IP address conflicts when merging networks, especially when the original internal network uses common private IP ranges like 192.168.0.0/23. In healthcare environments with sensitive equipment, changing these addresses becomes particularly challenging due to:

  • Legacy medical devices with hardcoded IPs
  • Critical systems that can't tolerate downtime
  • Complex network topologies with distributed equipment

The interim solution using 1:1 NAT for VPN clients (mapping 192.168.0.0/23 to 10.22.0.0/23) creates a DNS challenge. While clients can access resources via the translated addresses, DNS continues to return the original internal IPs. The current implementation with cron and sed has several drawbacks:

# Current implementation (flawed due to delay)
sed -e 's/192.168.0./10.22.0./' -e 's/192.168.1./10.22.1./' \
    /var/lib/bind/db.company.local > /var/lib/bind/db.company.local.ext \
    && /usr/sbin/rndc reload company.local in extView

The correct approach involves using BIND's view functionality with client IP matching. Here's a complete configuration example:

// named.conf options
options {
    directory "/var/named";
    version "not currently available";
    allow-query { any; };
};

// Internal view for LAN clients
view "internal" {
    match-clients { 192.168.0.0/23; localhost; };
    recursion yes;
    
    zone "company.local" {
        type master;
        file "/var/named/db.company.local";
    };
};

// External view for VPN clients
view "external" {
    match-clients { 10.22.0.0/23; };
    recursion yes;
    
    zone "company.local" {
        type master;
        file "/var/named/db.company.local.vpn";
    };
};

For more dynamic translation without maintaining separate zone files, RPZ provides a powerful alternative:

// RPZ configuration in named.conf
options {
    response-policy { zone "rpz.company.local"; };
};

zone "rpz.company.local" {
    type master;
    file "/var/named/db.rpz.company.local";
    allow-query { none; };
};

// RPZ zone file content
$TTL 1H
@ IN SOA ns1.company.local. admin.company.local. (
    2023060101 ; serial
    1h ; refresh
    15m ; retry
    1w ; expiry
    1h ; minimum
)
    NS ns1.company.local.

; Rewrite rules for VPN clients
192.168.0.0/23.client-vpn CNAME 10.22.0.0/23
*.company.local.client-vpn CNAME rpz-passthru.

When implementing these solutions, consider:

  • View matching occurs before recursive resolution, adding minimal overhead
  • RPZ processing happens after resolution but before response
  • For large networks, maintain separate zone files to avoid runtime translations
  • Monitor query rates with rndc stats to detect performance impact

A more maintainable long-term solution involves restructuring your DNS architecture:

// named.conf split-horizon configuration
view "vpn-clients" {
    match-clients { 10.22.0.0/23; };
    zone "internal.company.local" {
        type forward;
        forwarders { 192.168.0.53; }; // Internal DNS
    };
    zone "company.local" {
        type master;
        file "/var/named/db.company.local.vpn";
    };
};