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


5 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";
    };
};