package main import ( "fmt" "net" "regexp" ) var hostnameRe = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) func validateHostname(name string) error { if !hostnameRe.MatchString(name) { return fmt.Errorf("invalid hostname %q (letters, digits, hyphen only; no leading/trailing hyphen; no domain)", name) } return nil } func ip4ToUint32(ip net.IP) uint32 { b := ip.To4() return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) } func uint32ToIP4(v uint32) net.IP { return net.IPv4(byte(v>>24), byte(v>>16), byte(v>>8), byte(v)) } // validateIP checks that ip is a valid IPv4 address inside cidr, is neither // the network nor the broadcast address of cidr, and is not one of the // explicitly excluded addresses (e.g. the router's own gateway IP). func validateIP(ip, cidr string, excluded ...string) error { parsed := net.ParseIP(ip) if parsed == nil || parsed.To4() == nil { return fmt.Errorf("invalid IPv4 address: %q", ip) } _, network, err := net.ParseCIDR(cidr) if err != nil { return fmt.Errorf("internal error: invalid network %q: %w", cidr, err) } if !network.Contains(parsed) { return fmt.Errorf("IP %s is not inside network %s", ip, cidr) } if isNetworkOrBroadcast(parsed, network) { return fmt.Errorf("IP %s is the network or broadcast address of %s and cannot be assigned", ip, cidr) } for _, ex := range excluded { if parsed.Equal(net.ParseIP(ex)) { return fmt.Errorf("IP %s is reserved (router/gateway address) and cannot be assigned", ip) } } return nil } func isNetworkOrBroadcast(ip net.IP, network *net.IPNet) bool { ip4 := ip.To4() mask := network.Mask netAddr := ip4.Mask(mask) broadcast := make(net.IP, len(netAddr)) for i := range netAddr { broadcast[i] = netAddr[i] | ^mask[i] } return ip4.Equal(netAddr) || ip4.Equal(broadcast) } // nextFreeIP returns the first address in cidr (ascending order) that is not // the network or broadcast address, not in excluded, and not already used by // a host other than exceptIdx (-1 to not exempt any host). func nextFreeIP(hosts []Host, cidr string, exceptIdx int, excluded ...string) (string, error) { _, network, err := net.ParseCIDR(cidr) if err != nil { return "", fmt.Errorf("internal error: invalid network %q: %w", cidr, err) } ones, bits := network.Mask.Size() size := uint32(1) << uint32(bits-ones) if size < 2 { return "", fmt.Errorf("internal error: network %q too small", cidr) } start := ip4ToUint32(network.IP.To4()) for i := uint32(1); i < size-1; i++ { candidate := uint32ToIP4(start + i).String() if containsIP(excluded, candidate) { continue } if findByIP(hosts, candidate, exceptIdx) >= 0 { continue } return candidate, nil } return "", fmt.Errorf("no free IP address left in %s", cidr) } func containsIP(ips []string, ip string) bool { for _, v := range ips { if v == ip { return true } } return false } // normalizeMAC validates a MAC address and returns it in canonical lower-case // colon-separated form. Empty input is accepted and returned as-is. func normalizeMAC(mac string) (string, error) { if mac == "" { return "", nil } hw, err := net.ParseMAC(mac) if err != nil { return "", fmt.Errorf("invalid MAC address %q: %w", mac, err) } return hw.String(), nil }