Error while handling UDP stream (Bind9)

Hi All,

I was wondering if anyone would be able to help me solve an error I get in Traefik. I set up a Bind9 container and linked Traefik port 53 to it. However, I get the following error message in the traefik.log.

2025-06-03T14:47:34+02:00 ERR Error while handling UDP stream error="read udp 172.18.0.2:35735->172.18.0.3:53: read: connection refused"

Using dig @192.168.2.10 google.com or dig @192.168.2.10 test.example.home results in a connection refused error message.

I have tested this both on bare metal and on a VM running in Proxmox (OS Debian 12). In both cases I get the same outcome. When I do not use Traefik & Docker - meaning install Bind9 via apt - the same dig command works as expected giving me the correct ip addresses.

I created a runnable demo using the files below for you to reproduce this error. Would anyone have an idea how to fix this?


Assums Docker is installed and a network traefik_network is created

Docker compose

services:
  traefik:
    container_name: 'traefik'
    image: 'docker.io/library/traefik:v3.3.6'
    networks:
      - traefik_network
    ports:
      - 53:53/tcp
      - 53:53/udp
      - 8080:8080 # for dashboard
    privileged: false
    restart: 'unless-stopped'
    volumes:
      - /etc/traefik/traefik.yml:/etc/traefik/traefik.yaml:ro
      - /etc/traefik/conf/:/etc/traefik/conf/
      - /etc/traefik/log/:/var/log/traefik/
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro

  bind9:
    container_name: 'bind9'
    image: 'internetsystemsconsortium/bind9:9.21'
    labels:
      - "traefik.enable=true" # enable traefik
      - "traefik.docker.network=traefik_network"

      - "traefik.tcp.routers.bind9-rtr.entrypoints=dns-tcp"
      - "traefik.tcp.routers.bind9-rtr.rule=HostSNI(`*`)"
      - "traefik.tcp.routers.bind9-rtr.service=bind9-svc"
      - "traefik.tcp.services.bind9-svc.loadbalancer.server.port=53"

      - "traefik.udp.routers.bind9-udp-rtr.entrypoints=dns-udp"
      - "traefik.udp.routers.bind9-udp-rtr.service=bind9-udp-svc"
      - "traefik.udp.services.bind9-udp-svc.loadbalancer.server.port=53"
    networks:
      - 'traefik_network'
    privileged: false
    restart: 'unless-stopped'
    volumes:
      - '/etc/bind:/etc/bind/'
      - '/var/cache/bind:/var/cache/bind'
      - '/var/lib/bind:/var/lib/bind'
      - '/var/log/bind:/var/log/bind'

networks:
  traefik_network:
    external: true

/etc/traefik/traefik.yml

global:
  checkNewVersion: false
  sendAnonymousUsage: false

log:
 filePath: '/var/log/traefik/traefik.log'
 format: 'common'
 level: 'ERROR'

api:
  dashboard: true

entryPoints:
  dns-tcp:
    address: ':53'
  dns-udp:
    address: ':53/udp'
  web:
    address: :80

providers:
  docker:
    exposedByDefault: false
    watch: true

/etc/bind/db.192.168.1

$TTL    604800
@       IN      SOA     ns1.example.home.           info.example.home. (
                                      0000000001    ; Serial
                                          604800    ; Refresh
                                           86400    ; Retry
                                         2419200    ; Expire
                                          604800    ; Negative Cache TTL
                                               )
; name servers
        IN      NS     ns1.example.home.

; PTR records
10      IN      PTR     ns1.example.home.

11      IN      PTR     test.example.home.

/etc/bind/db.example.home

$TTL    604800
@               IN  SOA     ns1.example.home. info.example.home. (
                    0000000001      ; Serial
                        604800      ; Refresh
                         86400      ; Retry
                       2419200      ; Expire
                        604800 )    ; Negative Cache TTL
;
; name servers - NS records
        IN  NS  ns1.example.home.

; name servers - A records
ns1     IN  A   192.168.1.10

; A records
test    IN  A   192.168.1.11

/etc/bind/named.conf

include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";

logging {
    channel default_file {
        file "/var/log/bind/default.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel general_file {
        file "/var/log/bind/general.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel database_file {
        file "/var/log/bind/database.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel security_file {
        file "/var/log/bind/security.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel config_file {
        file "/var/log/bind/config.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel resolver_file {
        file "/var/log/bind/resolver.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel xfer-in_file {
        file "/var/log/bind/xfer-in.log" versions 3 size 5m;
        severity dynamic;
    };
    channel xfer-out_file {
        file "/var/log/bind/xfer-out.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel notify_file {
        file "/var/log/bind/notify.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel client_file {
        file "/var/log/bind/client.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel unmatched_file {
        file "/var/log/bind/unmatched.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel queries_file {
        file "/var/log/bind/queries.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel network_file {
        file "/var/log/bind/network.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel update_file {
        file "/var/log/bind/update.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel dispatch_file {
        file "/var/log/bind/dispatch.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel dnssec_file {
        file "/var/log/bind/dnssec.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };
    channel lame-servers_file {
        file "/var/log/bind/lame-servers.log" versions 3 size 5m;
        severity dynamic;
        print-time yes;
    };

    category default { default_file; };
    category general { general_file; };
    category database { database_file; };
    category security { security_file; };
    category config { config_file; };
    category resolver { resolver_file; };
    category xfer-in { xfer-in_file; };
    category xfer-out { xfer-out_file; };
    category notify { notify_file; };
    category client { client_file; };
    category unmatched { unmatched_file; };
    category queries { queries_file; };
    category network { network_file; };
    category update { update_file; };
    category dispatch { dispatch_file; };
    category dnssec { dnssec_file; };
    category lame-servers { lame-servers_file; };
};

/etc/bind/named.conf.local

zone "example.home" {
    type primary;
    file "/etc/bind/db.example.home"; # Zone path file
};

zone "1.168.192.in-addr.arpa" {
    type primary;
    file "/etc/bind/db.192.168.1";    # Zone path file
};

/etc/bind/named.conf.options

acl example {
    localhost;
    localnets;
    192.168.1.0/24;
};

options {
    directory "/var/cache/bind";

    recursion yes;
    allow-recursion { example; };

    listen-on port 53 { 192.168.1.10; };
};

Did some deep digging (imo) and I found the issue. The problem does not lie with Traefik.

In the configuration of Bind9 I set it to listen on the ip address of the host machine. However, Bind9 is running within Docker which has another IP (range) than the host machine. So, Bind9 was not listening on the correct ip.
As Docker is setting the ip of containers at runtime and as it therefore will vary, it is not possible to set a fixed ip in the configuration file. This can be solved by letting it listen on the localhost.

Also, in the acl I allowed the ip range of my network. However, due to Docker making use of its own range, the acl specified will not allow my traffic. So, I fixed this by allowing the ip range (172.x.x.x) that Docker might be using.
Sidenote, according to security-by-design, you'd probably want to specify a more narrow range within the /etc/docker/daemon.json so you do not have to expose the whole ip range within 172. But I consider that outside of the scope of this thread.


So, the correct configuration is

/etc/bind/named.conf.options

acl example {
    localhost;
    localnets;
    192.168.1.0/24; // NO USE
    172.0.0.0/8;
};

options {
    directory "/var/cache/bind";

    recursion yes;
    allow-recursion { example; };

    // listen-on port 53 { 192.168.1.10; }; // NO USE
    listen-on port 53 { localhost; };
};