Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to configure DNS server for bridge and OVN networks #1739

Merged
merged 5 commits into from
Mar 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ qgroups
RADOS
RBAC
RBD
RDNSS
README
reconfiguring
requestor
Expand Down
5 changes: 5 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2735,3 +2735,8 @@ This allows specifying pairs of CIDR networks and gateway address to be announce
Adds a new `LogicalSwitch` field to the `NetworkStateOVN` struct which is part of the `GET /1.0/networks/NAME/state` API.

This is used to get the OVN logical switch name.

## `network_dns_nameservers`

Introduces the `dns.nameservers` configuration option on bridged and OVN networks.
This allows specifying IPv4 and IPv6 DNS server addresses to be announced by the DHCP server and via Router Advertisements.
1 change: 1 addition & 0 deletions doc/reference/network_bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Key | Type | Condition | Defau
`bridge.external_interfaces` | string | - | - | Comma-separated list of unconfigured network interfaces to include in the bridge
`bridge.hwaddr` | string | - | - | MAC address for the bridge
`bridge.mtu` | integer | - | `1500` | Bridge MTU (default varies if tunnel in use)
`dns.nameservers` | string | - | IPv4 and IPv6 address | DNS server IPs to advertise to DHCP clients and via Router Advertisements. Both IPv4 and IPv6 addresses get pushed via DHCP, and IPv6 addresses are also advertised as RDNSS via RA.
`dns.domain` | string | - | `incus` | Domain to advertise to DHCP clients and use for DNS resolution
`dns.mode` | string | - | `managed` | DNS registration mode: `none` for no DNS record, `managed` for Incus-generated static records or `dynamic` for client-generated records
`dns.search` | string | - | - | Full comma-separated domain search list, defaulting to `dns.domain` value
Expand Down
1 change: 1 addition & 0 deletions doc/reference/network_ovn.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Key | Type | Condition | Defau
`bridge.external_interfaces` | string | - | - | Comma-separated list of unconfigured network interfaces to include in the bridge
`bridge.hwaddr` | string | - | - | MAC address for the bridge
`bridge.mtu` | integer | - | `1442` | Bridge MTU (default allows host to host Geneve tunnels)
`dns.nameservers` | string | - | Uplink DNS servers (IPv4 and IPv6 address if no uplink is configured) | DNS server IPs to advertise to DHCP clients and via Router Advertisements. Both IPv4 and IPv6 addresses get pushed via DHCP, and the first IPv6 address is also advertised as RDNSS via RA.
`dns.domain` | string | - | `incus` | Domain to advertise to DHCP clients and use for DNS resolution
`dns.search` | string | - | - | Full comma-separated domain search list, defaulting to `dns.domain` value
`dns.zone.forward` | string | - | - | Comma-separated list of DNS zone names for forward DNS records
Expand Down
27 changes: 27 additions & 0 deletions internal/server/network/driver_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ func (n *bridge) Validate(config map[string]string) error {
"ipv6.routes": validate.Optional(validate.IsListOf(validate.IsNetworkV6)),
"ipv6.routing": validate.Optional(validate.IsBool),
"ipv6.ovn.ranges": validate.Optional(validate.IsListOf(validate.IsNetworkRangeV6)),
"dns.nameservers": validate.Optional(validate.IsListOf(validate.IsNetworkAddress)),
"dns.domain": validate.IsAny,
"dns.mode": validate.Optional(validate.IsOneOf("dynamic", "managed", "none")),
"dns.search": validate.IsAny,
Expand Down Expand Up @@ -901,6 +902,16 @@ func (n *bridge) setup(oldConfig map[string]string) error {
}
}

var dnsIPv4 []string
var dnsIPv6 []string
for _, s := range util.SplitNTrimSpace(n.config["dns.nameservers"], ",", -1, false) {
if net.ParseIP(s).To4() != nil {
dnsIPv4 = append(dnsIPv4, s)
} else {
dnsIPv6 = append(dnsIPv6, s)
}
}

// Configure IPv4.
if !util.IsNoneOrEmpty(n.config["ipv4.address"]) {
// Parse the subnet.
Expand All @@ -920,6 +931,14 @@ func (n *bridge) setup(oldConfig map[string]string) error {
dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--dhcp-option-force=3,%s", n.config["ipv4.dhcp.gateway"]))
}

if n.config["dns.nameservers"] != "" {
if len(dnsIPv4) == 0 {
dnsmasqCmd = append(dnsmasqCmd, "--dhcp-option-force=6")
} else {
dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--dhcp-option-force=6,%s", strings.Join(dnsIPv4, ",")))
}
}

if bridge.MTU != bridgeMTUDefault {
dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--dhcp-option-force=26,%d", bridge.MTU))
}
Expand Down Expand Up @@ -1095,6 +1114,14 @@ func (n *bridge) setup(oldConfig map[string]string) error {
dnsmasqCmd = append(dnsmasqCmd, []string{"--dhcp-range", fmt.Sprintf("::,constructor:%s,ra-only", n.name)}...)
}

if n.config["dns.nameservers"] != "" {
if len(dnsIPv6) == 0 {
dnsmasqCmd = append(dnsmasqCmd, "--dhcp-option-force=option6:dns-server")
} else {
dnsmasqCmd = append(dnsmasqCmd, fmt.Sprintf("--dhcp-option-force=option6:dns-server,[%s]", strings.Join(dnsIPv6, ",")))
}
}

// Allow forwarding.
if util.IsTrueOrEmpty(n.config["ipv6.routing"]) {
// Get a list of proc entries.
Expand Down
68 changes: 39 additions & 29 deletions internal/server/network/driver_ovn.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ func (n *ovn) Validate(config map[string]string) error {
"ipv6.nat.address": validate.Optional(validate.IsNetworkAddressV6),
"ipv4.l3only": validate.Optional(validate.IsBool),
"ipv6.l3only": validate.Optional(validate.IsBool),
"dns.nameservers": validate.Optional(validate.IsListOf(validate.IsNetworkAddress)),
"dns.domain": validate.IsAny,
"dns.search": validate.IsAny,
"dns.zone.forward": validate.IsAny,
Expand Down Expand Up @@ -2646,6 +2647,26 @@ func (n *ovn) setup(update bool) error {
}
}

var dnsIPv4 []net.IP
var dnsIPv6 []net.IP

if n.config["dns.nameservers"] != "" {
for _, s := range util.SplitNTrimSpace(n.config["dns.nameservers"], ",", -1, false) {
nsIP := net.ParseIP(s)
if nsIP.To4() != nil {
dnsIPv4 = append(dnsIPv4, nsIP)
} else {
dnsIPv6 = append(dnsIPv6, nsIP)
}
}
} else if uplinkNet != nil {
dnsIPv4 = uplinkNet.dnsIPv6
dnsIPv6 = uplinkNet.dnsIPv6
} else {
dnsIPv4 = []net.IP{routerIntPortIPv4}
dnsIPv6 = []net.IP{routerIntPortIPv6}
}

// Create DHCPv4 options for internal switch.
if dhcpV4Subnet != nil {
// In l3only mode we configure the DHCPv4 server to request the instances use a /32 subnet mask.
Expand All @@ -2655,21 +2676,16 @@ func (n *ovn) setup(update bool) error {
}

opts := &networkOVN.OVNDHCPv4Opts{
ServerID: routerIntPortIPv4,
ServerMAC: routerMAC,
Router: routerIntPortIPv4,
DomainName: n.getDomainName(),
LeaseTime: time.Duration(time.Hour * 1),
MTU: bridgeMTU,
Netmask: dhcpV4Netmask,
DNSSearchList: n.getDNSSearchList(),
StaticRoutes: n.config["ipv4.dhcp.routes"],
}

if uplinkNet != nil {
opts.RecursiveDNSServer = uplinkNet.dnsIPv4
} else {
opts.RecursiveDNSServer = []net.IP{routerIntPortIPv4}
ServerID: routerIntPortIPv4,
ServerMAC: routerMAC,
Router: routerIntPortIPv4,
DomainName: n.getDomainName(),
LeaseTime: time.Duration(time.Hour * 1),
MTU: bridgeMTU,
Netmask: dhcpV4Netmask,
DNSSearchList: n.getDNSSearchList(),
StaticRoutes: n.config["ipv4.dhcp.routes"],
RecursiveDNSServer: dnsIPv4,
}

err = n.ovnnb.UpdateLogicalSwitchDHCPv4Options(context.TODO(), n.getIntSwitchName(), dhcpv4UUID, dhcpV4Subnet, opts)
Expand All @@ -2681,14 +2697,9 @@ func (n *ovn) setup(update bool) error {
// Create DHCPv6 options for internal switch.
if dhcpV6Subnet != nil {
opts := &networkOVN.OVNDHCPv6Opts{
ServerID: routerMAC,
DNSSearchList: n.getDNSSearchList(),
}

if uplinkNet != nil {
opts.RecursiveDNSServer = uplinkNet.dnsIPv6
} else {
opts.RecursiveDNSServer = []net.IP{routerIntPortIPv6}
ServerID: routerMAC,
DNSSearchList: n.getDNSSearchList(),
RecursiveDNSServer: dnsIPv6,
}

err = n.ovnnb.UpdateLogicalSwitchDHCPv6Options(context.TODO(), n.getIntSwitchName(), dhcpv6UUID, dhcpV6Subnet, opts)
Expand All @@ -2708,10 +2719,9 @@ func (n *ovn) setup(update bool) error {
}

var recursiveDNSServer net.IP
if uplinkNet != nil && len(uplinkNet.dnsIPv6) > 0 {
recursiveDNSServer = uplinkNet.dnsIPv6[0] // OVN only supports 1 RA DNS server.
} else {
recursiveDNSServer = routerIntPortIPv6

if len(dnsIPv6) > 0 {
recursiveDNSServer = dnsIPv6[0] // OVN only supports 1 RA DNS server.
}

err = n.ovnnb.UpdateLogicalRouterPort(context.TODO(), n.getRouterIntPortName(), &networkOVN.OVNIPv6RAOpts{
Expand Down Expand Up @@ -2763,8 +2773,8 @@ func (n *ovn) setup(update bool) error {
// Apply baseline ACL rules to internal logical switch.
dnsServers := []net.IP{}
if uplinkNet != nil {
dnsServers = append(dnsServers, uplinkNet.dnsIPv4...)
dnsServers = append(dnsServers, uplinkNet.dnsIPv6...)
dnsServers = append(dnsServers, dnsIPv4...)
dnsServers = append(dnsServers, dnsIPv6...)
}

err = acl.OVNApplyNetworkBaselineRules(n.ovnnb, n.getIntSwitchName(), n.getIntSwitchRouterPortName(), intRouterIPs, dnsServers)
Expand Down
18 changes: 17 additions & 1 deletion internal/server/network/ovn/ovn_nb_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1262,14 +1262,18 @@ func (o *NB) UpdateLogicalSwitchDHCPv4Options(ctx context.Context, switchName OV

if opts.Router != nil {
dhcpOption.Options["router"] = opts.Router.String()
} else {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these else branches for all the options for consistency, but they're probably not strictly in scope for this PR, I can move them to a new one if desired.

The delete is only really required for the dns servers (and the routes, to which I already added it in the PR for those) because otherwise they would never get set to empty if so configured.

The other options are always set anyways, but in case that ever changes it's probably nice if they all behave in the same way.

delete(dhcpOption.Options, "router")
}

if len(opts.DNSSearchList) > 0 {
// Special quoting to allow domain names.
dhcpOption.Options["domain_search_list"] = fmt.Sprintf(`"%s"`, strings.Join(opts.DNSSearchList, ","))
} else {
delete(dhcpOption.Options, "domain_search_list")
}

if opts.RecursiveDNSServer != nil {
if len(opts.RecursiveDNSServer) > 0 {
nsIPs := make([]string, 0, len(opts.RecursiveDNSServer))
for _, nsIP := range opts.RecursiveDNSServer {
if nsIP.To4() == nil {
Expand All @@ -1280,19 +1284,27 @@ func (o *NB) UpdateLogicalSwitchDHCPv4Options(ctx context.Context, switchName OV
}

dhcpOption.Options["dns_server"] = fmt.Sprintf("{%s}", strings.Join(nsIPs, ","))
} else {
delete(dhcpOption.Options, "dns_server")
}

if opts.DomainName != "" {
// Special quoting to allow domain names.
dhcpOption.Options["domain_name"] = fmt.Sprintf(`"%s"`, opts.DomainName)
} else {
delete(dhcpOption.Options, "domain_name")
}

if opts.MTU > 0 {
dhcpOption.Options["mtu"] = fmt.Sprintf("%d", opts.MTU)
} else {
delete(dhcpOption.Options, "mtu")
}

if opts.Netmask != "" {
dhcpOption.Options["netmask"] = opts.Netmask
} else {
delete(dhcpOption.Options, "netmask")
}

if opts.StaticRoutes != "" {
Expand Down Expand Up @@ -1364,6 +1376,8 @@ func (o *NB) UpdateLogicalSwitchDHCPv6Options(ctx context.Context, switchName OV
if len(opts.DNSSearchList) > 0 {
// Special quoting to allow domain names.
dhcpOption.Options["domain_search"] = fmt.Sprintf(`"%s"`, strings.Join(opts.DNSSearchList, ","))
} else {
delete(dhcpOption.Options, "domain_search")
}

if opts.RecursiveDNSServer != nil {
Expand All @@ -1377,6 +1391,8 @@ func (o *NB) UpdateLogicalSwitchDHCPv6Options(ctx context.Context, switchName OV
}

dhcpOption.Options["dns_server"] = fmt.Sprintf("{%s}", strings.Join(nsIPs, ","))
} else {
delete(dhcpOption.Options, "dns_server")
}

// Prepare the changes.
Expand Down
1 change: 1 addition & 0 deletions internal/version/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ var APIExtensions = []string{
"security_iommu",
"network_ipv4_dhcp_routes",
"network_state_ovn_ls",
"network_dns_nameservers",
}

// APIExtensionsCount returns the number of available API extensions.
Expand Down