From ce90234bfc8aa09d5aead5992af2e07667603d07 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Tue, 13 Mar 2018 18:54:52 +0100 Subject: More configuration options for networks --- libvirt/resource_libvirt_domain.go | 2 +- libvirt/resource_libvirt_network.go | 274 ++++++++++++++++++++++++------- libvirt/resource_libvirt_network_test.go | 118 +++++++++++++ libvirt/utils_net.go | 10 +- libvirt/utils_net_test.go | 2 +- libvirt/utils_test.go | 2 +- website/docs/r/network.markdown | 85 +++++++--- 7 files changed, 403 insertions(+), 90 deletions(-) diff --git a/libvirt/resource_libvirt_domain.go b/libvirt/resource_libvirt_domain.go index f7d7a074..526f5a91 100644 --- a/libvirt/resource_libvirt_domain.go +++ b/libvirt/resource_libvirt_domain.go @@ -1238,7 +1238,7 @@ func setNetworkInterfaces(d *schema.ResourceData, domainDef *libvirtxml.Domain, mac = strings.ToUpper(macI.(string)) } else { var err error - mac, err = RandomMACAddress() + mac, err = randomMACAddress() if err != nil { return fmt.Errorf("Error generating mac address: %s", err) } diff --git a/libvirt/resource_libvirt_network.go b/libvirt/resource_libvirt_network.go index 4f984e72..773e9b29 100644 --- a/libvirt/resource_libvirt_network.go +++ b/libvirt/resource_libvirt_network.go @@ -78,27 +78,83 @@ func resourceLibvirtNetwork() *schema.Resource { Optional: true, Required: false, }, - "dns_forwarder": { + "dhcp": { Type: schema.TypeList, Optional: true, ForceNew: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "address": { - Type: schema.TypeString, + "enabled": { + Type: schema.TypeBool, + Default: true, Optional: true, Required: false, - ForceNew: true, }, - "domain": { - Type: schema.TypeString, + }, + }, + }, + "dns": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Default: true, Optional: true, Required: false, + }, + "local_only": { + Type: schema.TypeBool, + Default: false, + Optional: true, + Required: false, + }, + "forwarders": { + Type: schema.TypeList, + Optional: true, ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "host": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address": { + Type: schema.TypeString, + Optional: true, + Required: false, + ForceNew: true, + }, + "name": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, }, }, }, }, + "routes": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, }, } } @@ -167,8 +223,17 @@ func resourceLibvirtNetworkCreate(d *schema.ResourceData, meta interface{}) erro networkDef := newNetworkDef() networkDef.Name = d.Get("name").(string) - networkDef.Domain = &libvirtxml.NetworkDomain{ - Name: d.Get("domain").(string), + + if domain, ok := d.GetOk("domain"); ok { + networkDef.Domain = &libvirtxml.NetworkDomain{ + Name: domain.(string), + } + + if dnsLocalOnly, ok := d.GetOk("dns.0.local_only"); ok { + if dnsLocalOnly.(bool) { + networkDef.Domain.LocalOnly = "yes" // this "boolean" must be "yes"|"no" + } + } } // use a bridge provided by the user, or create one otherwise (libvirt will assign on automatically when empty) @@ -195,77 +260,128 @@ func resourceLibvirtNetworkCreate(d *schema.ResourceData, meta interface{}) erro networkDef.Forward.NAT = nil } - // some network modes require a DHCP/DNS server - // set the addresses for DHCP + // set the addresses if addresses, ok := d.GetOk("addresses"); ok { ipsPtrsLst := []libvirtxml.NetworkIP{} for _, addressI := range addresses.([]interface{}) { - address := addressI.(string) + // get the IP address entry for this subnet (with a guessed DHCP range) + dni, dhcp, err := setNetworkIP(addressI.(string)) + if err != nil { + return err + } + if d.Get("dhcp.0.enabled").(bool) { + dni.DHCP = dhcp + } + + ipsPtrsLst = append(ipsPtrsLst, *dni) + } + networkDef.IPs = ipsPtrsLst + } + + // set static routes + if routes, ok := d.GetOk("routes"); ok { + for _, routeI := range routes.([]interface{}) { + route := libvirtxml.NetworkRoute{} + + routeComponents := strings.Split(routeI.(string), "->") + if len(routeComponents) != 2 { + return fmt.Errorf("Error parsing address '%s'", routeI.(string)) + } + + address := strings.TrimSpace(routeComponents[0]) + gateway := strings.TrimSpace(routeComponents[1]) + + // parse the address _, ipNet, err := net.ParseCIDR(address) if err != nil { - return fmt.Errorf("Error parsing addresses definition '%s': %s", address, err) + return fmt.Errorf("Error parsing address '%s': %s", address, err) } ones, bits := ipNet.Mask.Size() family := "ipv4" if bits == (net.IPv6len * 8) { family = "ipv6" } - ipsRange := 2 ^ bits - 2 ^ ones - if ipsRange < 4 { - return fmt.Errorf("Netmask seems to be too strict: only %d IPs available (%s)", ipsRange-3, family) + route.Address = ipNet.IP.String() + route.Prefix = strconv.Itoa(ones) + route.Family = family + + // parse and check the gateway + parsedGateway := net.ParseIP(gateway) + if parsedGateway == nil { + return fmt.Errorf("Could not parse IP address '%s'", parsedGateway) } + route.Gateway = parsedGateway.String() - // we should calculate the range served by DHCP. For example, for - // 192.168.121.0/24 we will serve 192.168.121.2 - 192.168.121.254 - start, end := NetworkRange(ipNet) + networkDef.Routes = append(networkDef.Routes, route) + } + } - // skip the .0, (for the network), - start[len(start)-1]++ + if _, ok := d.GetOk("dns.0"); ok { + dnsPrefix := "dns.0" + dns := libvirtxml.NetworkDNS{} - // assign the .1 to the host interface - dni := libvirtxml.NetworkIP{ - Address: start.String(), - Prefix: strconv.Itoa(ones), - Family: family, - } + if d.Get(dnsPrefix + ".enabled").(bool) { + dns.Enable = "yes" + } - start[len(start)-1]++ // then skip the .1 - end[len(end)-1]-- // and skip the .255 (for broadcast) + if forwarders, ok := d.GetOk(dnsPrefix + ".forwarders"); ok { + dns.Forwarders = []libvirtxml.NetworkDNSForwarder{} + + for _, forwardersI := range forwarders.([]interface{}) { + forwarderSpec := forwardersI.(string) + ip := "" + domain := "" + + forwarderComponents := strings.Split(forwarderSpec, "->") + if len(forwarderComponents) == 1 { + // the first element can be an IP or a domain: we must identify the class + target := strings.TrimSpace(forwarderComponents[0]) + parsedIP := net.ParseIP(target) + if parsedIP != nil { + ip = parsedIP.String() + } else { + domain = target + } + } else if len(forwarderComponents) == 2 { + domain = strings.TrimSpace(forwarderComponents[0]) + ip = strings.TrimSpace(forwarderComponents[1]) + } else { + return fmt.Errorf("Error parsing forwarder '%s'", forwarders.(string)) + } - dni.DHCP = &libvirtxml.NetworkDHCP{ - Ranges: []libvirtxml.NetworkDHCPRange{ - { - Start: start.String(), - End: end.String(), - }, - }, + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + return fmt.Errorf("Could not parse address in forwarder specification '%s'", forwarderSpec) + } + + dns.Forwarders = append(dns.Forwarders, libvirtxml.NetworkDNSForwarder{Addr: parsedIP.String(), Domain: domain}) } - ipsPtrsLst = append(ipsPtrsLst, dni) } - networkDef.IPs = ipsPtrsLst - } - if dnsForwardCount, ok := d.GetOk("dns_forwarder.#"); ok { - dns := libvirtxml.NetworkDNS{ - Forwarders: []libvirtxml.NetworkDNSForwarder{}, - } + if _, ok := d.GetOk(dnsPrefix + ".host"); ok { + dns.Host = &libvirtxml.NetworkDNSHost{} + hostPrefix := dnsPrefix + ".host" - for i := 0; i < dnsForwardCount.(int); i++ { - forward := libvirtxml.NetworkDNSForwarder{} - forwardPrefix := fmt.Sprintf("dns_forwarder.%d", i) - if address, ok := d.GetOk(forwardPrefix + ".address"); ok { - ip := net.ParseIP(address.(string)) - if ip == nil { - return fmt.Errorf("Could not parse address '%s'", address) + if address, ok := d.GetOk(hostPrefix + ".address"); ok { + parsedIP := net.ParseIP(address.(string)) + if parsedIP == nil { + return fmt.Errorf("Could not parse IP address '%s'", parsedIP) } - forward.Addr = ip.String() + dns.Host.IP = parsedIP.String() } - if domain, ok := d.GetOk(forwardPrefix + ".domain"); ok { - forward.Domain = domain.(string) + + if dnsHostCount, ok := d.GetOk(hostPrefix + ".name.#"); ok { + dns.Host.Hostnames = []libvirtxml.NetworkDNSHostHostname{} + + for i := 0; i < dnsHostCount.(int); i++ { + dnsHostNamePrefix := fmt.Sprintf(dnsPrefix+".name.%d", i) + if name, ok := d.GetOk(dnsHostNamePrefix); ok { + hostname := libvirtxml.NetworkDNSHostHostname{Hostname: name.(string)} + dns.Host.Hostnames = append(dns.Host.Hostnames, hostname) + } + } } - dns.Forwarders = append(dns.Forwarders, forward) } - networkDef.DNS = &dns } } else if networkDef.Forward.Mode == netModeBridge { @@ -368,6 +484,7 @@ func resourceLibvirtNetworkRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error reading network autostart setting: %s", err) } d.Set("autostart", autostart) + addresses := []string{} for _, address := range networkDef.IPs { // we get the host interface IP (ie, 10.10.8.1) but we want the network CIDR (ie, 10.10.8.0/24) @@ -390,7 +507,10 @@ func resourceLibvirtNetworkRead(d *schema.ResourceData, meta interface{}) error d.Set("addresses", addresses) } - // TODO: get any other parameters from the network and save them + // TODO: get any other parameters from the network and save them (ie, DNS forwarders...) + + d.Set("dns.0.local_only", networkDef.Domain != nil && strings.ToLower(networkDef.Domain.LocalOnly) == "yes") + d.Set("dns.0.enabled", networkDef.DNS != nil && strings.ToLower(networkDef.DNS.Enable) == "yes") log.Printf("[DEBUG] Network ID %s successfully read", d.Id()) return nil @@ -444,6 +564,50 @@ func resourceLibvirtNetworkDelete(d *schema.ResourceData, meta interface{}) erro return nil } +func setNetworkIP(address string) (*libvirtxml.NetworkIP, *libvirtxml.NetworkDHCP, error) { + _, ipNet, err := net.ParseCIDR(address) + if err != nil { + return nil, nil, fmt.Errorf("Error parsing addresses definition '%s': %s", address, err) + } + ones, bits := ipNet.Mask.Size() + family := "ipv4" + if bits == (net.IPv6len * 8) { + family = "ipv6" + } + ipsRange := 2 ^ bits - 2 ^ ones + if ipsRange < 4 { + return nil, nil, fmt.Errorf("Netmask seems to be too strict: only %d IPs available (%s)", ipsRange-3, family) + } + + // we should calculate the range served by DHCP. For example, for + // 192.168.121.0/24 we will serve 192.168.121.2 - 192.168.121.254 + start, end := networkRange(ipNet) + + // skip the .0, (for the network), + start[len(start)-1]++ + + // assign the .1 to the host interface + dni := &libvirtxml.NetworkIP{ + Address: start.String(), + Prefix: strconv.Itoa(ones), + Family: family, + } + + start[len(start)-1]++ // then skip the .1 + end[len(end)-1]-- // and skip the .255 (for broadcast) + + dhcp := &libvirtxml.NetworkDHCP{ + Ranges: []libvirtxml.NetworkDHCPRange{ + { + Start: start.String(), + End: end.String(), + }, + }, + } + + return dni, dhcp, nil +} + func waitForNetworkActive(network libvirt.Network) resource.StateRefreshFunc { return func() (interface{}, string, error) { active, err := network.IsActive() diff --git a/libvirt/resource_libvirt_network_test.go b/libvirt/resource_libvirt_network_test.go index f3c0d211..eafbc76f 100644 --- a/libvirt/resource_libvirt_network_test.go +++ b/libvirt/resource_libvirt_network_test.go @@ -48,6 +48,124 @@ func TestNetworkAutostart(t *testing.T) { }) } +func TestNetworkDNS(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtNetworkDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_network" "test_net" { + name = "networktest" + mode = "nat" + domain = "k8s.local" + addresses = ["10.17.3.0/24"] + dns { + enabled = true + local_only = true + + forwarders = [ + "8.8.8.8", + "my.domain.com -> 10.10.0.67", + "hello.com" + ] + + host { + address = "10.17.3.2" + name = ["server1.com", "server2.com"] + } + } + }`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.enabled", "true"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.local_only", "true"), + + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.forwarders.#", "3"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.forwarders.0", "8.8.8.8"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.forwarders.1", "my.domain.com -> 10.10.0.67"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.forwarders.2", "hello.com"), + + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.host.0.address", "10.17.3.2"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.host.0.name.#", "2"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.host.0.name.0", "server1.com"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "dns.0.host.0.name.1", "server2.com"), + ), + }, + }, + }) +} + +func TestNetworkDHCP(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtNetworkDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_network" "test_net" { + name = "networktest" + mode = "nat" + domain = "k8s.local" + addresses = ["10.17.3.0/24"] + dhcp { + enabled = false + } + }`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("libvirt_network.test_net", "dhcp.0.enabled", "false"), + ), + }, + }, + }) +} + +func TestNetworkStaticRoutes(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLibvirtNetworkDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "libvirt_network" "test_net" { + name = "networktest" + mode = "nat" + domain = "k8s.local" + addresses = ["10.17.3.0/24"] + routes = [ + "192.168.7.0/24 -> 127.0.0.1", + "192.168.9.1/24 -> 127.0.0.1", + "192.168.17.1/32 -> 127.0.0.1", + "2001:db9:4:1::/64 -> ::1" + ] + }`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.0.address", "192.168.7.0"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.0.prefix", "24"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.0.family", "ipv4"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.0.gateway", "127.0.0.1"), + + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.1.address", "192.168.9.0"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.1.prefix", "24"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.1.family", "ipv4"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.1.gateway", "127.0.0.1"), + + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.2.address", "192.168.17.1"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.2.prefix", "32"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.2.family", "ipv4"), + resource.TestCheckResourceAttr("libvirt_network.test_net", "route.2.gateway", "127.0.0.1"), + ), + }, + }, + }) +} + +/************************* + * tests helpers + ************************/ + func networkExists(n string, network *libvirt.Network) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] diff --git a/libvirt/utils_net.go b/libvirt/utils_net.go index 140c4a8d..0f57a3fa 100644 --- a/libvirt/utils_net.go +++ b/libvirt/utils_net.go @@ -16,7 +16,7 @@ const ( ) // RandomMACAddress returns a randomized MAC address -func RandomMACAddress() (string, error) { +func randomMACAddress() (string, error) { buf := make([]byte, 6) _, err := rand.Read(buf) if err != nil { @@ -38,7 +38,7 @@ func RandomMACAddress() (string, error) { } // RandomPort returns a random port -func RandomPort() int { +func randomPort() int { const minPort = 1024 const maxPort = 65535 @@ -47,7 +47,7 @@ func RandomPort() int { } // FreeNetworkInterface returns a free network interface -func FreeNetworkInterface(basename string) (string, error) { +func freeNetworkInterface(basename string) (string, error) { for i := 0; i < maxIfaceNum; i++ { ifaceName := fmt.Sprintf("%s%d", basename, i) _, err := net.InterfaceByName(ifaceName) @@ -59,7 +59,7 @@ func FreeNetworkInterface(basename string) (string, error) { } // NetworkRange calculates the first and last IP addresses in an IPNet -func NetworkRange(network *net.IPNet) (net.IP, net.IP) { +func networkRange(network *net.IPNet) (net.IP, net.IP) { netIP := network.IP.To4() lastIP := net.IPv4(0, 0, 0, 0).To4() if netIP == nil { @@ -89,7 +89,7 @@ func (fws *fileWebServer) Start() error { } fws.Dir = dir - fws.Port = RandomPort() + fws.Port = randomPort() fws.URL = fmt.Sprintf("http://127.0.0.1:%d", fws.Port) handler := http.NewServeMux() diff --git a/libvirt/utils_net_test.go b/libvirt/utils_net_test.go index 036b3616..c84c94de 100644 --- a/libvirt/utils_net_test.go +++ b/libvirt/utils_net_test.go @@ -6,7 +6,7 @@ import ( ) func TestRandomMACAddress(t *testing.T) { - mac, err := RandomMACAddress() + mac, err := randomMACAddress() if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/libvirt/utils_test.go b/libvirt/utils_test.go index f2cd9aaf..ad0781ec 100644 --- a/libvirt/utils_test.go +++ b/libvirt/utils_test.go @@ -29,7 +29,7 @@ func TestIPsRange(t *testing.T) { t.Errorf("When parsing network: %s", err) } - start, end := NetworkRange(net) + start, end := networkRange(net) if start.String() != "192.168.18.0" { t.Errorf("unexpected range start for '%s': %s", net, start) } diff --git a/website/docs/r/network.markdown b/website/docs/r/network.markdown index 1f4698ad..0415016b 100644 --- a/website/docs/r/network.markdown +++ b/website/docs/r/network.markdown @@ -32,12 +32,17 @@ resource "libvirt_network" "kube_network" { # (only necessary in "bridge" mode) # bridge = "br7" - # (Optional) one or more DNS forwarder entries. One or both of - # "address" and "domain" must be specified. The format is: - # dns_forwarder { - # address = "my address" - # domain = "my domain" - # } + # (Optional) DNS configuration + dns { + local_only = true + + # (Optional) one or more DNS forwarder entries. One or both of + # "address" and "domain" must be specified. The format is: + # forwarder { + # address = "my address" + # domain = "my domain" + # } + } } ``` @@ -47,9 +52,10 @@ The following arguments are supported: * `name` - (Required) A unique name for the resource, required by libvirt. Changing this forces a new resource to be created. -* `domain` - The domain used by the DNS server. -* `addresses` - A list of (0 or 1) ipv4 and (0 or 1) ipv6 subnets in CIDR notation +* `domain` - (Optional) The domain used by the DNS server. +* `addresses` - (Optional) A list of (0 or 1) IPv4 and (0 or 1) IPv6 subnets in CIDR notation format for being served by the DHCP server. Address of subnet should be used. + No DHCP server will be started if this attributed is omitted. * `mode` - One of: - `none`: the guests can talk to each other and the host OS, but cannot reach any other machines on the LAN. @@ -70,41 +76,66 @@ The following arguments are supported: * `bridge` - (Optional) The bridge device defines the name of a bridge device which will be used to construct the virtual network (when not provided, it will be automatically obtained by libvirt in `none`, `nat` and `route` modes). -* `dns_forwarder` - (Optional) a DNS forwarder entry block. You can have - one or mode of these blocks in your network definition. You must specify one or - both of `address` and `domain`. You can use either of the forms below to - specify dns_forwarders: * `autostart` - (Optional) Set to `true` to start the network on host boot up. If not specified `false` is assumed. - +* `dns` - (Optional) DNS configuration + * `enabled` - (Optional) when false, disable the DNS server + * `local_only` - (Optional) when set, then DNS requests for this domain will + only be resolved by the virtual network's own DNS server (they will not be + forwarded to the host's upstream DNS server) + * `host` - (Optional) the host element within DNS is the definition of DNS hosts + to be passed to the DNS service. The IP address is identified by the `address` attribute + and the names for that IP address are identified in the `name` sub-elements of + the host element. ```hcl resource "libvirt_network" "my_network" { ... - dns_forwarder { - address = "my address" + dns { + host { + address = "10.17.3.2" + name = ["server1.com", "server2.com"] + } } - dns_forwarder { - address = "my address 1" - domain = "my domain" +} +``` + * `forwarder` - (Optional) a list of DNS forwarders, with entries following + the `[Domain ->] Domain|IP` format. Each forwarder element defines an alternate DNS + server to use for some, or all, DNS requests sent to this network's DNS server. + There are two attributes: a `Domain` and/or an `IP` (at least one of these must be specified). + - If both `Domain` and `IP` are specified, then all requests that match the given `Domain` will + be forwarded to the DNS server at `IP`. + - If only `Domain` is specified, then all matching + domains will be resolved locally (or via the host's standard DNS forwarding if they can't + be resolved locally) + - If an `IP` is specified by itself, then all DNS requests to the + network's DNS server will be forwarded to the DNS server at that address with no + exceptions. + For example: +: +```hcl +resource "libvirt_network" "my_network" { + ... + dns { + forwarders = ["8.8.8.8", "my.domain.com -> 10.10.0.67"] } } ``` - +* `dhcp` - (Optional) DHCP configuration + * `enabled` - (Optional) when false, disable the DHCP server +* `routes` - (Optional) List of static routes, as a list of `CIDR -> gateway`. For example: ```hcl resource "libvirt_network" "my_network" { ... - dns_forwarder = [ - { - address = "my address" - }, - { - address = "my address 1" - domain = "my domain - } + routes = [ + "192.168.7.0/24 -> 127.0.0.1", + "192.168.9.1/24 -> 127.0.0.1", + "192.168.17.1/32 -> 127.0.0.1", + "2001:db9:4:1::/64 -> 2001:db8:ca2:2::3" ] } ``` + ## Attributes Reference * `id` - a unique identifier for the resource -- cgit v1.2.3