package netconfig import ( "context" "encoding/json" "fmt" "net" "os" "path" "sort" "strings" "time" "dario.cat/mergo" "github.com/vishvananda/netlink" "gopkg.in/yaml.v3" ) const ( cloudInitDefault = "/etc/cloud/cloud.cfg" cloudInitDir = "/etc/cloud/cloud.cfg.d" cloudBaseInit = "/curtin/network.json" ) // Name server cvonfigurations. type ciNameservers struct { Search []string `yaml:"search,omitempty,flow" json:"search,omitempty"` Addresses []string `yaml:"addresses,omitempty,flow" json:"addresses,omitempty"` } // Common route configs. type ciRoute struct { To string `yaml:"to,omitempty" json:"to,omitempty"` Via string `yaml:"via,omitempty" json:"via,omitempty"` Metric *int `yaml:"metric,omitempty" json:"metric,omitempty"` } // Match options. type ciMatch struct { Name string `yaml:"name,omitempty" json:"name,omitempty"` MACAddress string `yaml:"macaddress,omitempty" json:"macaddress,omitempty"` Driver string `yaml:"driver,omitempty" json:"driver,omitempty"` } // Common configurations on physical interfaces. type ciPhysical struct { Match ciMatch `yaml:"match,omitempty" json:"match,omitempty"` SetName string `yaml:"set-name,omitempty" json:"set-name,omitempty"` Wakeonlan bool `yaml:"wakeonlan,omitempty" json:"wakeonlan,omitempty"` } // Common cloudinit interface configurations. type ciInterface struct { Addresses []string `yaml:"addresses,omitempty" json:"addresses,omitempty"` DHCP4 *bool `yaml:"dhcp4,omitempty" json:"dhcp4,omitempty"` DHCP6 *bool `yaml:"dhcp6,omitempty" json:"dhcp6,omitempty"` DHCP4Overrides map[string]any `yaml:"dhcp4-overrides,omitempty" json:"dhcp4-overrides,omitempty"` DHCP6Overrides map[string]any `yaml:"dhcp6-overrides,omitempty" json:"dhcp6-overrides,omitempty"` Gateway4 string `yaml:"gateway4,omitempty" json:"gateway4,omitempty"` Gateway6 string `yaml:"gateway6,omitempty" json:"gateway6,omitempty"` Nameservers ciNameservers `yaml:"nameservers,omitempty" json:"nameservers,omitempty"` MTU int `yaml:"mtu,omitempty" json:"mtu,omitempty"` Renderer string `yaml:"renderer,omitempty" json:"renderer,omitempty"` Routes []ciRoute `yaml:"routes,omitempty" json:"routes,omitempty"` Optional bool `yaml:"optional,omitempty" json:"optional,omitempty"` } // dhcpState reports whether each family's DHCP client is enabled. An absent key // means the schema default, which is off. func (c *ciInterface) dhcpState() (dhcp4, dhcp6 bool) { return c.DHCP4 != nil && *c.DHCP4, c.DHCP6 != nil && *c.DHCP6 } // setDHCP records the requested DHCP client state for both families, dropping // the overrides of a family whose client is being turned off. A family being // disabled that was already absent stays absent rather than being written out // as false. func (c *ciInterface) setDHCP(dhcp4, dhcp6 bool) { if dhcp4 || c.DHCP4 != nil { c.DHCP4 = boolPtr(dhcp4) } if !dhcp4 { c.DHCP4Overrides = nil } if dhcp6 || c.DHCP6 != nil { c.DHCP6 = boolPtr(dhcp6) } if !dhcp6 { c.DHCP6Overrides = nil } } // Physical ethernet interface. type ciEthernet struct { ciPhysical `yaml:",inline" json:",inline"` ciInterface `yaml:",inline" json:",inline"` } // Bridge interface. type ciBridgeParameters struct { AgeingTime *int `yaml:"ageing-time,omitempty" json:"ageing-time,omitempty"` ForwardDelay intString `yaml:"forward-delay,omitempty" json:"forward-delay,omitempty"` HelloTime *int `yaml:"hello-time,omitempty" json:"hello-time,omitempty"` MaxAge *int `yaml:"max-age,omitempty" json:"max-age,omitempty"` PathCost *int `yaml:"path-cost,omitempty" json:"path-cost,omitempty"` Priority *int `yaml:"priority,omitempty" json:"priority,omitempty"` STP *bool `yaml:"stp,omitempty" json:"stp,omitempty"` } type ciBridge struct { Interfaces []string `yaml:"interfaces,omitempty,flow" json:"interfaces,omitempty"` ciInterface `yaml:",inline" json:",inline"` Parameters ciBridgeParameters `yaml:"parameters,omitempty" json:"parameters,omitempty"` } // Bond interfaces. type ciBondParameters struct { Mode intString `yaml:"mode,omitempty" json:"mode,omitempty"` LACPRate intString `yaml:"lacp-rate,omitempty" json:"lacp-rate,omitempty"` MIIMonitorInterval intString `yaml:"mii-monitor-interval,omitempty" json:"mii-monitor-interval,omitempty"` MinLinks *int `yaml:"min-links,omitempty" json:"min-links,omitempty"` TransmitHashPolicy string `yaml:"transmit-hash-policy,omitempty" json:"transmit-hash-policy,omitempty"` ADSelect intString `yaml:"ad-select,omitempty" json:"ad-select,omitempty"` AllSlavesActive *bool `yaml:"all-slaves-active,omitempty" json:"all-slaves-active,omitempty"` ARPInterval *int `yaml:"arp-interval,omitempty" json:"arp-interval,omitempty"` ARPIPTargets []string `yaml:"arp-ip-targets,omitempty" json:"arp-ip-targets,omitempty"` ARPValidate intString `yaml:"arp-validate,omitempty" json:"arp-validate,omitempty"` ARPAllTargets intString `yaml:"arp-all-targets,omitempty" json:"arp-all-targets,omitempty"` UpDelay intString `yaml:"up-delay,omitempty" json:"up-delay,omitempty"` DownDelay intString `yaml:"down-delay,omitempty" json:"down-delay,omitempty"` FailOverMACPolicy intString `yaml:"fail-over-mac-policy,omitempty" json:"fail-over-mac-policy,omitempty"` GratuitousARP *int `yaml:"gratuitous-arp,omitempty" json:"gratuitous-arp,omitempty"` PacketsPerSlave *int `yaml:"packets-per-slave,omitempty" json:"packets-per-slave,omitempty"` PrimaryReselectPolicy intString `yaml:"primary-reselect-policy,omitempty" json:"primary-reselect-policy,omitempty"` LearnPacketInterval *int `yaml:"learn-packet-interval,omitempty" json:"learn-packet-interval,omitempty"` } type ciBond struct { Interfaces []string `yaml:"interfaces,omitempty,flow" json:"interfaces,omitempty"` ciInterface `yaml:",inline" json:",inline"` Parameters ciBondParameters `yaml:"parameters,omitempty" json:"parameters,omitempty"` } // VLAN interface. type ciVLAN struct { Id *int `yaml:"id,omitempty" json:"id,omitempty"` Link string `yaml:"link,omitempty" json:"link,omitempty"` ciInterface `yaml:",inline" json:",inline"` } // Cloud-init network version 1 config. type ciSubnetRoute struct { Destination string `yaml:"destination,omitempty" json:"destination,omitempty"` Netmask string `yaml:"netmask,omitempty" json:"netmask,omitempty"` Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"` Metric int `yaml:"metric,omitempty" json:"metric,omitempty"` } type ciSubnet struct { Type string `yaml:"type" json:"type"` Control intString `yaml:"control,omitempty" json:"control,omitempty"` Address string `yaml:"address,omitempty" json:"address,omitempty"` Netmask string `yaml:"netmask,omitempty" json:"netmask,omitempty"` Broadcast string `yaml:"broadcast,omitempty" json:"broadcast,omitempty"` Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"` DNSNameservers []string `yaml:"dns_nameservers,omitempty" json:"dns_nameservers,omitempty"` DNSSearch []string `yaml:"dns_search,omitempty" json:"dns_search,omitempty"` Routes []ciSubnetRoute `yaml:"routes,omitempty" json:"routes,omitempty"` } type ciConfig struct { // Common options. Type string `yaml:"type" json:"type"` Name string `yaml:"name,omitempty" json:"name,omitempty"` MACAddress string `yaml:"mac_address,omitempty" json:"mac_address,omitempty"` MTU int `yaml:"mtu,omitempty" json:"mtu,omitempty"` AcceptRA *bool `yaml:"accept-ra,omitempty" json:"accept-ra,omitempty"` Subnets []ciSubnet `yaml:"subnets,omitempty" json:"subnets,omitempty"` // Bridge and bond options. BondInterfaces []string `yaml:"bond_interfaces,omitempty,flow" json:"bond_interfaces,omitempty"` BridgeInterfaces []string `yaml:"bridge_interfaces,omitempty,flow" json:"bridge_interfaces,omitempty"` Params map[string]string `yaml:"params,omitempty" json:"params,omitempty"` // VLAN options. VLANLink string `yaml:"vlan_link,omitempty" json:"vlan_link,omitempty"` VLANID int `yaml:"vlan_id,omitempty" json:"vlan_id,omitempty"` // Nameserver options. Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` Addresses []string `yaml:"address,omitempty" json:"address,omitempty"` Search []string `yaml:"search,omitempty" json:"search,omitempty"` // Route options. Destination string `yaml:"destination,omitempty" json:"destination,omitempty"` Netmask string `yaml:"netmask,omitempty" json:"netmask,omitempty"` Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"` Metric int `yaml:"metric,omitempty" json:"metric,omitempty"` } // Common network structure for both v1 and v2. type ciNetwork struct { Version int `yaml:"version" json:"version"` Renderer string `yaml:"renderer,omitempty" json:"renderer,omitempty"` Ethernets map[string]ciEthernet `yaml:"ethernets,omitempty" json:"ethernets,omitempty"` Bridges map[string]ciBridge `yaml:"bridges,omitempty" json:"bridges,omitempty"` Bonds map[string]ciBond `yaml:"bonds,omitempty" json:"bonds,omitempty"` VLANs map[string]ciVLAN `yaml:"vlans,omitempty" json:"vlans,omitempty"` Configs []ciConfig `yaml:"config,omitempty" json:"config,omitempty"` } // The default netplan configuration structure and interface to configuring netplan. type ciFile struct { Path string OnlyNetwork bool NetworkEncap bool } type cloudInit struct { Network ciNetwork `yaml:"network" json:"network"` configFiles []ciFile `yaml:"-" json:"-"` backupRetention int `yaml:"-" json:"-"` } // Find cloud-init and cloudbase-init network configurations. func newCloudInit(backupRetention int) (ci *cloudInit, err error) { ci, err = newCloudInitWith(cloudInitDefault, cloudBaseInit, cloudInitDir) if ci != nil { ci.backupRetention = backupRetention } return } func newCloudInitWith(cloudinitFile, cloudbaseFile, cloudinitDir string) (ci *cloudInit, err error) { // Read data. var mergedConfig map[string]any var configFiles []ciFile var files []string // Check which base file exists. if _, serr := os.Stat(cloudinitFile); serr == nil { files = append(files, cloudinitFile) } else if _, serr := os.Stat(cloudbaseFile); serr == nil { files = append(files, cloudbaseFile) } // No files, end. if len(files) == 0 { return nil, fmt.Errorf("unable to find cloudinit config") } // Try to read the directory files. unsortedEntries, _ := os.ReadDir(cloudinitDir) entries := sortableDirEntries(unsortedEntries) sort.Sort(entries) // Add found files to list. for _, entry := range entries { // If not an cfg file, skip. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".cfg") { continue } files = append(files, path.Join(cloudinitDir, entry.Name())) } // Parse config files. for _, configFile := range files { var data []byte data, err = os.ReadFile(configFile) if err != nil { return } // Setup cloud-init file info. var file ciFile file.Path = configFile // Parse the yaml to an map. var config map[string]any if strings.HasSuffix(configFile, ".json") { err = json.Unmarshal(data, &config) configS, ok := config["config"] if ok { version, ok := config["version"] if ok { network := make(map[string]any) network["config"] = configS network["version"] = version config = make(map[string]any) config["network"] = network file.NetworkEncap = true } } } else { err = yaml.Unmarshal(data, &config) } if err != nil { return } // Determine if this config only contains network configs. foundNetwork := false foundNonNetwork := false for key, value := range config { if key == "network" { // This is a network config, unless the network config // only contains the config disabled option. foundNetwork = true configS, ok := value.(map[string]any) if ok { // We found disabled if the config key is a string // with disabled. foundDisable := false configV, ok := configS["config"].(string) if ok && configV == "disabled" { foundDisable = true } // If there are other configs that are not // the config key, there are other configs to // parse. for keyV := range configS { if keyV != "config" { foundDisable = false } } // If we only found a disabled config, no network // config exists here. if foundDisable { foundNetwork = false } } } else { foundNonNetwork = true } } // If there are no network configs in this file, skip it. if !foundNetwork { continue } // If there are network configs, determine if its only network // and add to list of config files. file.OnlyNetwork = !foundNonNetwork configFiles = append(configFiles, file) // If this is the first config file read, don't merge. if mergedConfig == nil { mergedConfig = config continue } // Merge the configurations. err = mergo.Merge(&mergedConfig, config) if err != nil { return } } // Convert merged configs back into yaml so we can re-parse into a struct. mergedData, err := yaml.Marshal(mergedConfig) if err != nil { return } // Parse the cloud-init config. ci = new(cloudInit) err = yaml.Unmarshal(mergedData, ci) if err != nil { return nil, err } // If there are no ethernet configurations or configs in cloud-init, we consider the cloud-init configurations to not exist. if len(ci.Network.Ethernets) == 0 && len(ci.Network.Configs) == 0 { return nil, fmt.Errorf("no ethernet configurations in cloud-init") } // Successfully parsed, we should return. ci.configFiles = configFiles return } // Convert cloud-init interface to a netconfig Interface. func (*cloudInit) ConvertInterface(name string, MAC string, config ciInterface, links []netlink.Link) *Interface { // Find the MAC address, starting with the list of interfaces // on the machine. var mac net.HardwareAddr var foundLink netlink.Link for _, link := range links { if link.Attrs().Name == name { mac = link.Attrs().HardwareAddr foundLink = link } } // If the MAC was not found, try parsing the config. if mac == nil && MAC != "" { mac, _ = net.ParseMAC(MAC) } // Setup interface with the name and MAC. i := new(Interface) i.Name = name i.MAC = mac i.Link = foundLink i.DHCP4, i.DHCP6 = config.dhcpState() // Parse addresses. for _, addr := range config.Addresses { ip, ipnet, err := net.ParseCIDR(addr) if err != nil { continue } ipnet.IP = ip i.Addresses = append(i.Addresses, ipnet) } // Add gateways. if config.Gateway4 != "" { i.Gateway4 = net.ParseIP(config.Gateway4) } if config.Gateway6 != "" { i.Gateway6 = net.ParseIP(config.Gateway6) } // Parse nameservers. for _, addr := range config.Nameservers.Addresses { if ip := net.ParseIP(addr); ip != nil { i.DNS = append(i.DNS, ip) } } i.SearchDomains = config.Nameservers.Search // Parse routes. for _, route := range config.Routes { dstIP, dst, err := net.ParseCIDR(route.To) if err != nil { continue } dst.IP = dstIP ip := net.ParseIP(route.Via) r := new(Route) r.Destination = dst r.Gateway = ip if route.Metric != nil { r.Metric = *route.Metric } else { r.Metric = 256 } i.Routes = append(i.Routes, r) } return i } // Get interfaces configured. func (ci *cloudInit) GetInterfaces() (interfaces []*Interface, err error) { // Connect to netlink. var h *netlink.Handle h, err = netlink.NewHandle() if err != nil { return } defer h.Close() // Get list of interfaces to match with MAC address. var links []netlink.Link links, err = h.LinkList() if err != nil { return } // Add configs. for _, config := range ci.Network.Configs { // Skip types we cannot parse. switch config.Type { case "physical", "bond", "bridge", "vlan": default: continue } // Find the MAC address, starting with the list of interfaces // on the machine. var mac net.HardwareAddr var foundLink netlink.Link for _, link := range links { if link.Attrs().Name == config.Name { mac = link.Attrs().HardwareAddr foundLink = link } } // If we did not find the MAC address, but one is defined. Parse it. if mac == nil && config.MACAddress != "" { mac, _ = net.ParseMAC(config.MACAddress) } // Setup interface with the name and MAC. i := new(Interface) i.Name = config.Name i.MAC = mac i.Link = foundLink // Parse subnet configs. for _, subnet := range config.Subnets { // A v1 subnet encodes the DHCP client in its type rather than in a // dhcp4/dhcp6 key. "dhcp" with no suffix is the IPv4 client. switch subnet.Type { case "dhcp", "dhcp4": i.DHCP4 = true case "dhcp6", "ipv6_dhcpv6-stateful", "ipv6_dhcpv6-stateless": i.DHCP6 = true } // Add DNS servers and search domains. for _, dns := range subnet.DNSNameservers { if ip := net.ParseIP(dns); ip != nil { i.DNS = append(i.DNS, ip) } } i.SearchDomains = append(i.SearchDomains, subnet.DNSSearch...) // Add routes. for _, route := range subnet.Routes { r := new(Route) // Try and parse the destination. var ipnet *net.IPNet var ip net.IP if strings.Contains(route.Destination, "/") { ip, ipnet, err = net.ParseCIDR(route.Destination) if err != nil { continue } ipnet.IP = ip } else { ipnet = &net.IPNet{ IP: net.ParseIP(route.Destination), } if ipnet.IP.To4() == nil { ipnet.Mask = net.CIDRMask(128, 128) } else { ipnet.Mask = net.CIDRMask(32, 32) } } // If the IP wasn't parsed, skip this route. if ipnet.IP == nil { continue } // If the netmask exists, parse and add to the destination. if route.Netmask != "" { ipMask := net.ParseIP(route.Netmask).To4() if ipMask == nil { continue } ipnet.Mask = net.IPMask(ipMask) } r.Destination = ipnet // Parse the gateway, requiring it. gateway := net.ParseIP(route.Gateway) if gateway == nil { continue } r.Gateway = gateway // Add the route. r.Metric = route.Metric i.Routes = append(i.Routes, r) } // Only add subnet as address if static type. if subnet.Type != "static" { continue } // Parse the address field. var ipnet *net.IPNet var ip net.IP if strings.Contains(subnet.Address, "/") { ip, ipnet, err = net.ParseCIDR(subnet.Address) if err != nil { continue } ipnet.IP = ip } else { ipnet = &net.IPNet{ IP: net.ParseIP(subnet.Address), } if ipnet.IP.To4() == nil { ipnet.Mask = net.CIDRMask(128, 128) } else { ipnet.Mask = net.CIDRMask(32, 32) } } // If IP not parsed, skip. if ipnet.IP == nil { continue } // If subnet assigned, parse and add to address. if subnet.Netmask != "" { ipMask := net.ParseIP(subnet.Netmask).To4() if ipMask == nil { continue } ipnet.Mask = net.IPMask(ipMask) } // Add address. i.Addresses = append(i.Addresses, ipnet) // Parse the gateway. gateway := net.ParseIP(subnet.Gateway) if gateway != nil { if gateway.To4() == nil { i.Gateway6 = gateway } else { i.Gateway4 = gateway } } } // Add interface to list. interfaces = append(interfaces, i) } // Add ethernet interfaces to the list. var keys []string for key := range ci.Network.Ethernets { keys = append(keys, key) } sort.Strings(keys) for _, name := range keys { config := ci.Network.Ethernets[name] // If the name is being changed in the config, use that // as the name. if config.SetName != "" { name = config.SetName } // Get interface. i := ci.ConvertInterface(name, config.Match.MACAddress, config.ciInterface, links) // Add the interface. interfaces = append(interfaces, i) } // Add Bridge interfaces to the list. keys = nil for key := range ci.Network.Bridges { keys = append(keys, key) } sort.Strings(keys) for _, name := range keys { config := ci.Network.Bridges[name] // Get interface. i := ci.ConvertInterface(name, "", config.ciInterface, links) // Add the interface. interfaces = append(interfaces, i) } // Add Bond interfaces to the list. keys = nil for key := range ci.Network.Bonds { keys = append(keys, key) } sort.Strings(keys) for _, name := range keys { config := ci.Network.Bonds[name] // Get interface. i := ci.ConvertInterface(name, "", config.ciInterface, links) // Add the interface. interfaces = append(interfaces, i) } // Add VLAN interfaces to the list. keys = nil for key := range ci.Network.VLANs { keys = append(keys, key) } sort.Strings(keys) for _, name := range keys { config := ci.Network.VLANs[name] // Get interface. i := ci.ConvertInterface(name, "", config.ciInterface, links) // Add the interface. interfaces = append(interfaces, i) } return } // Save configurations. func (ci *cloudInit) Save() error { // First we are to determine which configuration we will store the network // configurations to. We will call this the main config, and will consider // the first network only config file as the main config. mainConfig := "" for _, file := range ci.configFiles { if file.OnlyNetwork { mainConfig = file.Path break } } // If there is no network only configs, choose the first config file. if mainConfig == "" { mainConfig = ci.configFiles[0].Path } // Update all files with new configurations. tmpSuffix := fmt.Sprintf(".tmp.%d", time.Now().UnixNano()) bakSuffix := fmt.Sprintf(".bak.%d", time.Now().UnixNano()) for _, file := range ci.configFiles { configPath := file.Path configDir, configName := path.Split(configPath) tmpName := configName + tmpSuffix tmpPath := path.Join(configDir, tmpName) bakName := configName + bakSuffix bakPath := path.Join(configDir, bakName) // Try to encode/save the config, start by encoding to yaml, // then decode to a map. data, err := yaml.Marshal(ci) if err != nil { return err } var config map[string]any err = yaml.Unmarshal(data, &config) if err != nil { return err } // Decode the current config. data, err = os.ReadFile(configPath) if err != nil { return err } var origConfig map[string]any if strings.HasSuffix(configPath, ".json") { err = json.Unmarshal(data, &origConfig) } else { err = yaml.Unmarshal(data, &origConfig) } if err != nil { return err } // Update the original config with the new config. // On files which were not determined as the main config, // we will delete the network config. if configPath == mainConfig { if file.NetworkEncap { network := config["network"].(map[string]any) origConfig["config"] = network["config"] } else { origConfig["network"] = config["network"] } } else { if file.NetworkEncap { delete(origConfig, "config") } else { delete(origConfig, "network") } } // Save to temporary file. if strings.HasSuffix(configPath, ".json") { data, err = json.MarshalIndent(origConfig, "", " ") data = append(data, '\n') } else { data, err = yaml.Marshal(origConfig) } if err != nil { return err } err = os.WriteFile(tmpPath, data, 0644) if err != nil { return err } // Backup original file. err = fileMove(configPath, bakPath) if err != nil { return err } // Move new config in place. err = fileMove(tmpPath, configPath) if err != nil { return err } // Prune old backups of this config file beyond the retention count. pruneBackups(configDir, suffixBackupMatcher(configName), ci.backupRetention) } return nil } // Set addresses to interface. func (ci *cloudInit) SetIfaceAddresses(_ context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) (err error) { // Apply changes to v1 configs. for c, config := range ci.Network.Configs { // Skip types we cannot parse. switch config.Type { case "physical", "bond", "bridge", "vlan": default: continue } // Skip interfaces that are not the one we're updating. if config.Name != iface { continue } // Parse all routes and DNS config so we can re-add as needed. var routes []Route var dns []string var domains []string for _, subnet := range config.Subnets { for _, route := range subnet.Routes { var r Route // Try and parse the destination. var ipnet *net.IPNet var ip net.IP if strings.Contains(route.Destination, "/") { ip, ipnet, err = net.ParseCIDR(route.Destination) if err != nil { continue } ipnet.IP = ip } else { ipnet = &net.IPNet{ IP: net.ParseIP(route.Destination), } if ipnet.IP.To4() == nil { ipnet.Mask = net.CIDRMask(128, 128) } else { ipnet.Mask = net.CIDRMask(32, 32) } } // If the IP wasn't parsed, skip this route. if ipnet.IP == nil { continue } // If the netmask exists, parse and add to the destination. if route.Netmask != "" { ipMask := net.ParseIP(route.Netmask).To4() if ipMask == nil { continue } ipnet.Mask = net.IPMask(ipMask) } r.Destination = ipnet // Parse the gateway, requiring it. gateway := net.ParseIP(route.Gateway) if gateway == nil { continue } r.Gateway = gateway // Add the route. r.Metric = route.Metric routes = append(routes, r) } for _, d := range subnet.DNSNameservers { found := false for _, d2 := range dns { if d2 == d { found = true break } } if !found { dns = append(dns, d) } } for _, d := range subnet.DNSSearch { found := false for _, d2 := range domains { if d2 == d { found = true break } } if !found { domains = append(domains, d) } } } // Clear existing subnets. config.Subnets = nil // Add subnets. for _, addr := range addrs { var subnet ciSubnet subnet.Type = "static" subnet.Address = addr.String() if addr.Contains(gateway4) { subnet.Gateway = gateway4.String() } else if addr.Contains(gateway6) { subnet.Gateway = gateway6.String() } // Add back DNS config. subnet.DNSNameservers = dns subnet.DNSSearch = domains // Add all pertaining routes. for { foundRoute := false for r, route := range routes { if addr.Contains(route.Gateway) { foundRoute = true subnet.Routes = append(subnet.Routes, ciSubnetRoute{ Destination: route.Destination.String(), Gateway: route.Gateway.String(), Metric: route.Metric, }) routes = append(routes[:r], routes[r+1:]...) break } } if !foundRoute { break } } // Add subnet. config.Subnets = append(config.Subnets, subnet) } // Add stray routes to the first subnet. if len(routes) != 0 && len(config.Subnets) != 0 { for _, route := range routes { config.Subnets[0].Routes = append(config.Subnets[0].Routes, ciSubnetRoute{ Destination: route.Destination.String(), Gateway: route.Gateway.String(), Metric: route.Metric, }) } } // Update the config. ci.Network.Configs[c] = config } // Check ethernet interfaces. for name, config := range ci.Network.Ethernets { // If the name is being changed in the config, use that // as the name. ifName := name if config.SetName != "" { ifName = config.SetName } // If this is not the interface we're updating addresses on. if ifName != iface { continue } // Re-configure with specified addresses. config.Addresses = nil for _, addr := range addrs { config.Addresses = append(config.Addresses, addr.String()) } if gateway4 == nil { config.Gateway4 = "" } else { config.Gateway4 = gateway4.String() } if gateway6 == nil { config.Gateway6 = "" } else { config.Gateway6 = gateway6.String() } ci.Network.Ethernets[name] = config } // Check bridges. { config, ok := ci.Network.Bridges[iface] if ok { // Re-configure with specified addresses. config.Addresses = nil for _, addr := range addrs { config.Addresses = append(config.Addresses, addr.String()) } if gateway4 == nil { config.Gateway4 = "" } else { config.Gateway4 = gateway4.String() } if gateway6 == nil { config.Gateway6 = "" } else { config.Gateway6 = gateway6.String() } ci.Network.Bridges[iface] = config } } // Check bonds. { config, ok := ci.Network.Bonds[iface] if ok { // Re-configure with specified addresses. config.Addresses = nil for _, addr := range addrs { config.Addresses = append(config.Addresses, addr.String()) } if gateway4 == nil { config.Gateway4 = "" } else { config.Gateway4 = gateway4.String() } if gateway6 == nil { config.Gateway6 = "" } else { config.Gateway6 = gateway6.String() } ci.Network.Bonds[iface] = config } } // Check VLAN interfaces. { config, ok := ci.Network.VLANs[iface] if ok { // Re-configure with specified addresses. config.Addresses = nil for _, addr := range addrs { config.Addresses = append(config.Addresses, addr.String()) } if gateway4 == nil { config.Gateway4 = "" } else { config.Gateway4 = gateway4.String() } if gateway6 == nil { config.Gateway6 = "" } else { config.Gateway6 = gateway6.String() } ci.Network.VLANs[iface] = config } } // Try to save changes. err = ci.Save() return } // ciDHCPSubnetTypes are the v1 subnet types that describe a DHCP client rather // than a static address, keyed by whether they are removed when that family's // client is disabled. var ciDHCPSubnetTypes = map[string]bool{ "dhcp": true, // IPv4, the unsuffixed spelling. "dhcp4": true, "dhcp6": false, "ipv6_dhcpv6-stateful": false, "ipv6_dhcpv6-stateless": false, } // Set the DHCP client state on an interface. func (ci *cloudInit) SetIfaceDHCP(_ context.Context, iface string, dhcp4, dhcp6 bool) (err error) { // Apply changes to v1 configs, where the DHCP client is a subnet whose type // names it rather than a key. Static subnets are left in place: cloud-init // accepts a dhcp subnet and a static subnet on the same interface. for c, config := range ci.Network.Configs { switch config.Type { case "physical", "bond", "bridge", "vlan": default: continue } if config.Name != iface { continue } // Drop the existing DHCP subnets of both families, then re-add the ones // that were asked for. Rebuilding rather than editing in place keeps a // config that listed several spellings of the same client from ending up // with a stale one alongside the new one. subnets := make([]ciSubnet, 0, len(config.Subnets)+2) if dhcp4 { subnets = append(subnets, ciSubnet{Type: "dhcp4"}) } if dhcp6 { subnets = append(subnets, ciSubnet{Type: "dhcp6"}) } for _, subnet := range config.Subnets { if _, isDHCP := ciDHCPSubnetTypes[subnet.Type]; isDHCP { continue } subnets = append(subnets, subnet) } config.Subnets = subnets ci.Network.Configs[c] = config } // Apply changes to v2 ethernets. for name, config := range ci.Network.Ethernets { ifName := name if config.SetName != "" { ifName = config.SetName } if ifName != iface { continue } config.setDHCP(dhcp4, dhcp6) ci.Network.Ethernets[name] = config } // Apply changes to v2 bridges. if config, ok := ci.Network.Bridges[iface]; ok { config.setDHCP(dhcp4, dhcp6) ci.Network.Bridges[iface] = config } // Apply changes to v2 bonds. if config, ok := ci.Network.Bonds[iface]; ok { config.setDHCP(dhcp4, dhcp6) ci.Network.Bonds[iface] = config } // Apply changes to v2 VLAN interfaces. if config, ok := ci.Network.VLANs[iface]; ok { config.setDHCP(dhcp4, dhcp6) ci.Network.VLANs[iface] = config } // Try to save changes. err = ci.Save() return } // Set static routes to interface. func (ci *cloudInit) SetIfaceRoutes(_ context.Context, iface string, routes []*Route) (err error) { // Apply changes to v1 configs. for c, config := range ci.Network.Configs { // Skip types we cannot parse. switch config.Type { case "physical", "bond", "bridge", "vlan": default: continue } // Skip interfaces that are not the one we're updating. if config.Name != iface { continue } if len(config.Subnets) == 0 { return fmt.Errorf("no subnets on interface") } // Remove existing routes, and parse destination. subnetMap := make(map[int]*net.IPNet) for s, subnet := range config.Subnets { config.Subnets[s].Routes = nil // Only add subnet as address if static type. if subnet.Type != "static" { continue } // Parse the address field. var ipnet *net.IPNet var ip net.IP if strings.Contains(subnet.Address, "/") { ip, ipnet, err = net.ParseCIDR(subnet.Address) if err != nil { continue } ipnet.IP = ip } else { ipnet = &net.IPNet{ IP: net.ParseIP(subnet.Address), } if ipnet.IP.To4() == nil { ipnet.Mask = net.CIDRMask(128, 128) } else { ipnet.Mask = net.CIDRMask(32, 32) } } // If IP not parsed, skip. if ipnet.IP == nil { continue } // If subnet assigned, parse and add to address. if subnet.Netmask != "" { ipMask := net.ParseIP(subnet.Netmask).To4() if ipMask == nil { continue } ipnet.Mask = net.IPMask(ipMask) } // Add to map. subnetMap[s] = ipnet } // Add each route accordingly. for _, route := range routes { // Find subnet to assign route, defaulting to 0. idx := 0 for s, ipnet := range subnetMap { if ipnet.Contains(route.Gateway) { idx = s break } } // Add route. var r ciSubnetRoute r.Destination = route.Destination.String() r.Gateway = route.Gateway.String() r.Metric = route.Metric config.Subnets[idx].Routes = append(config.Subnets[idx].Routes, r) } // Update configs. ci.Network.Configs[c] = config } // Check ethernet interfaces. for name, config := range ci.Network.Ethernets { // If the name is being changed in the config, use that // as the name. ifName := name if config.SetName != "" { ifName = config.SetName } // If this is not the interface we're updating routes on. if ifName != iface { continue } // Re-configure with specified routes. config.Routes = nil for _, route := range routes { config.Routes = append(config.Routes, ciRoute{ To: route.Destination.String(), Via: route.Gateway.String(), Metric: &route.Metric, }) } ci.Network.Ethernets[name] = config } // Check bridges. { config, ok := ci.Network.Bridges[iface] if ok { // Re-configure with specified routes. config.Routes = nil for _, route := range routes { config.Routes = append(config.Routes, ciRoute{ To: route.Destination.String(), Via: route.Gateway.String(), Metric: &route.Metric, }) } ci.Network.Bridges[iface] = config } } // Check bonds. { config, ok := ci.Network.Bonds[iface] if ok { // Re-configure with specified routes. config.Routes = nil for _, route := range routes { config.Routes = append(config.Routes, ciRoute{ To: route.Destination.String(), Via: route.Gateway.String(), Metric: &route.Metric, }) } ci.Network.Bonds[iface] = config } } // Check VLAN interfaces. { config, ok := ci.Network.VLANs[iface] if ok { // Re-configure with specified routes. config.Routes = nil for _, route := range routes { config.Routes = append(config.Routes, ciRoute{ To: route.Destination.String(), Via: route.Gateway.String(), Metric: &route.Metric, }) } ci.Network.VLANs[iface] = config } } // Try to save changes. err = ci.Save() return } // Set DNS servers and search domains on interface. func (ci *cloudInit) SetIfaceDNS(_ context.Context, iface string, servers []net.IP, searchDomains []string) (err error) { dnsStrings := ipStrings(servers) nameservers := ciNameservers{ Search: searchDomains, Addresses: dnsStrings, } // Apply changes to v1 configs, where DNS lives on each subnet. for c, config := range ci.Network.Configs { switch config.Type { case "physical", "bond", "bridge", "vlan": default: continue } if config.Name != iface { continue } for s := range config.Subnets { config.Subnets[s].DNSNameservers = dnsStrings config.Subnets[s].DNSSearch = searchDomains } ci.Network.Configs[c] = config } // Apply changes to v2 ethernet interfaces. for name, config := range ci.Network.Ethernets { ifName := name if config.SetName != "" { ifName = config.SetName } if ifName != iface { continue } config.Nameservers = nameservers ci.Network.Ethernets[name] = config } // Apply changes to v2 bridges, bonds, and VLANs. if config, ok := ci.Network.Bridges[iface]; ok { config.Nameservers = nameservers ci.Network.Bridges[iface] = config } if config, ok := ci.Network.Bonds[iface]; ok { config.Nameservers = nameservers ci.Network.Bonds[iface] = config } if config, ok := ci.Network.VLANs[iface]; ok { config.Nameservers = nameservers ci.Network.VLANs[iface] = config } // Try to save changes. err = ci.Save() return }