Files
Mark D Horn 13b28fd349 yaml: switch to strict unmarshal
Enable Strict Unmarshal to ensure that we fail on unknown
tokens and duplicate tokens.

Signed-off-by: Mark D Horn <mark.d.horn@intel.com>
2020-05-08 15:03:31 -07:00

923 lines
21 KiB
Go

// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only
package network
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
"github.com/coreos/go-systemd/v22/dbus"
"gopkg.in/yaml.v2"
"github.com/clearlinux/clr-installer/cmd"
"github.com/clearlinux/clr-installer/errors"
"github.com/clearlinux/clr-installer/log"
"github.com/clearlinux/clr-installer/utils"
)
// Interface is a network interface representation and wraps the net' package Interface struct
type Interface struct {
Name string
Addrs []*Addr
DHCP bool
Gateway string `json:"gateway,omitempty"`
DNSServer string
DNSDomain string
UserDefined bool
Metric uint32 `json:"metric,omitempty"`
}
// Version used for reading and writing YAML
type interfaceYAMLMarshal struct {
Name string `yaml:"name,omitempty"`
Addrs []*Addr `yaml:"addrs,omitempty"`
DHCP string `yaml:"dhcp,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
DNSServer string `yaml:"dns,omitempty"`
DNSDomain string `yaml:"domain,omitempty"`
}
// Addr wraps the net' package Addr struct
type Addr struct {
IP string
NetMask string
Version int
}
// LangString hold strings for each translated language
// and one for default if the current language is unavailable
type LangString struct {
Default string `yaml:"default,omitempty"`
EnUs string `yaml:"en_US,omitempty"`
}
// Messenger is an array of language string used to create
// an end user message for display
type Messenger struct {
Messages []*LangString `yaml:"install-msg,omitempty"`
}
const (
// RequiredBundle the bundle needed to use NetworkManager
RequiredBundle = "NetworkManager"
// IPv4 identifies the addr version as ipv4
IPv4 = iota
// IPv6 identifies the addr version as ipv6
IPv6
systemdNetworkdDir = "/etc/systemd/network"
networkManagerDir = "/etc/NetworkManager/system-connections"
versionURLPath = "/usr/share/defaults/swupd/contenturl"
// PreInstallConf is the name of the pre-installation message file
PreInstallConf = "pre-install-msg.yaml"
// PostInstallConf is the name of the pre-installation message file
PostInstallConf = "post-install-msg.yaml"
// PreGuiInstallConf is the name of the pre-installation message file
PreGuiInstallConf = "pre-gui-install-msg.yaml"
// PostGuiInstallConf is the name of the pre-installation message file
PostGuiInstallConf = "post-gui-install-msg.yaml"
)
var (
startsWithExp = regexp.MustCompile(`^[0-9A-Za-z]`)
endsWithExp = regexp.MustCompile(`[0-9A-Za-z]$`)
domainNameExp = regexp.MustCompile(`^[0-9A-Za-z]+[0-9A-Za-z-]*$`)
numericOnlyExp = regexp.MustCompile(`^[0-9]+[0-9]*$`)
validIPExp = regexp.MustCompile(`^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` +
`(\.{1})){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?){1}$`)
dnsExp = regexp.MustCompile(`Current DNS Server:(.*)`)
domainExp = regexp.MustCompile(`DNS Domain:(.*)`)
needPacDiscover = false
installDataURLBase = "https://cdn.download.clearlinux.org/releases/%s/clear/config/image/.data/%s"
)
// IsValidDomainName returns error message or nil if is valid
func IsValidDomainName(domain string) string {
// https://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax,_internationalization
if len(domain) < 1 {
return "Required field"
}
if len(domain) > 253 {
return "Domain too long (> 253)"
}
labels := strings.Split(domain, ".")
// This might be 127?
if len(labels) > 126 {
return "Domain has too many sub-domains"
}
top := labels[len(labels)-1]
// never end on a dot
if top == "" {
return "Dot not allowed as last character"
}
// top level domain may not be all-numeric
if numericOnlyExp.MatchString(top) {
return "Top level domain can not be numeric"
}
for _, label := range labels {
if len(label) > 63 {
return fmt.Sprintf("too long (> 63) '%s'", label)
}
if !startsWithExp.MatchString(label) {
return fmt.Sprintf("'%s' may only start with alphanumeric", label)
}
if !endsWithExp.MatchString(label) {
return fmt.Sprintf("'%s' may only end with alphanumeric", label)
}
if !domainNameExp.MatchString(label) {
return fmt.Sprintf("sub-domain '%s' may only contain alphanumeric hyphen", label)
}
}
return ""
}
// IsValidURI checks for valid URIs that use the HTTPS or FILE protocol
func IsValidURI(uri string, allowInsecureHTTP bool) bool {
_, err := url.ParseRequestURI(uri)
if err != nil {
return false
}
httpsPrefix := strings.HasPrefix(strings.ToLower(uri), "https:")
if httpsPrefix {
return true
}
filePrefix := strings.HasPrefix(strings.ToLower(uri), "file:")
if filePrefix {
return true
}
httpPrefix := strings.HasPrefix(strings.ToLower(uri), "http:")
if httpPrefix {
if allowInsecureHTTP {
return true
}
msg := "HTTP is disabled, pass --allow-insecure-http to enable HTTP"
fmt.Println(msg)
log.Info(msg)
}
return false
}
// IsUserDefined returns true if the configuration was interactively
// defined by the user
func (i *Interface) IsUserDefined() bool {
return i.UserDefined
}
// MarshalYAML marshals Interface into YAML format
func (i *Interface) MarshalYAML() (interface{}, error) {
var im interfaceYAMLMarshal
im.Name = i.Name
im.Addrs = i.Addrs
im.DHCP = strconv.FormatBool(i.DHCP)
im.Gateway = i.Gateway
im.DNSServer = i.DNSServer
im.DNSDomain = i.DNSDomain
return im, nil
}
// UnmarshalYAML unmarshals Interface from YAML format
func (i *Interface) UnmarshalYAML(unmarshal func(interface{}) error) error {
var im interfaceYAMLMarshal
if err := unmarshal(&im); err != nil {
return err
}
i.Name = im.Name
i.Addrs = im.Addrs
i.Gateway = im.Gateway
i.DNSServer = im.DNSServer
i.DNSDomain = im.DNSDomain
i.UserDefined = false
if im.DHCP != "" {
dhcp, err := strconv.ParseBool(im.DHCP)
if err != nil {
return err
}
i.DHCP = dhcp
}
return nil
}
// AddAddr adds a new interface set with the provided arguments to a given Interface
func (i *Interface) AddAddr(IP string, NetMask string, Version int) {
i.Addrs = append(i.Addrs, &Addr{IP: IP, NetMask: NetMask, Version: Version})
}
// HasIPv4Addr will lookup an addr with Version set to ipv4
func (i *Interface) HasIPv4Addr() bool {
for _, curr := range i.Addrs {
if curr.Version == IPv4 {
return true
}
}
return false
}
// GetGateway returns the best gateway for the interface
func (i *Interface) GetGateway() (string, error) {
const (
maxUint32 = 1<<32 - 1
)
var gateway string
var metric uint32 = maxUint32
w := bytes.NewBuffer(nil)
// TODO: Should we remove the absolute path? Absolute file path is used to ensure pkexec doesn't mess up PATH.
err := cmd.Run(w, "/usr/bin/ip", "-j", "route", "show", "dev", i.Name)
if err != nil {
return "", errors.Wrap(err)
}
var interfaces []*Interface //`json:"interfaces"`
err = json.Unmarshal(w.Bytes(), &interfaces)
if err != nil {
return "", errors.Wrap(err)
}
for _, inet := range interfaces {
if inet.Gateway != "" {
if inet.Metric <= metric {
gateway = inet.Gateway
metric = inet.Metric
}
}
}
return gateway, nil
}
// GetDNSInfo returns the DNS Server and Domain
func (i *Interface) GetDNSInfo() (string, string, error) {
var dns string
var domain string
w := bytes.NewBuffer(nil)
err := cmd.Run(w, "resolvectl", "--no-pager", "status", i.Name)
if err != nil {
return dns, domain, errors.Wrap(err)
}
lines := strings.Split(w.String(), "\n")
for _, curr := range lines {
if curr == "" {
continue
}
if dnsExp.MatchString(curr) {
dns = strings.TrimSpace(dnsExp.ReplaceAllString(curr, `$1`))
continue
}
if domainExp.MatchString(curr) {
domain = strings.TrimSpace(domainExp.ReplaceAllString(curr, `$1`))
continue
}
}
if dns == "" && domain == "" {
log.Debug("Could not parse DNS Server nor Domain for %s", i.Name)
} else {
if domain == "" {
log.Debug("Could not parse DNS Server for %s", i.Name)
}
if dns == "" {
log.Debug("Could not parse DNS Domain for %s", i.Name)
}
}
return dns, domain, err
}
// VersionString returns a string representation for a given addr version (ipv4/ipv6)
func (a *Addr) VersionString() string {
if a.Version == IPv4 {
return "ipv4"
}
return "ipv6"
}
func isDHCP(iface string) (bool, error) {
w := bytes.NewBuffer(nil)
err := cmd.Run(w, "/usr/bin/ip", "route", "show")
if err != nil {
return false, errors.Wrap(err)
}
for _, curr := range strings.Split(w.String(), "\n") {
if strings.Contains(curr, iface) && strings.Contains(curr, "dhcp") {
return true, nil
}
}
return false, nil
}
// Interfaces lists all available network interfaces
func Interfaces() ([]*Interface, error) {
result := []*Interface{}
var err error
ifaces, err := net.Interfaces()
if err != nil {
return nil, errors.Wrap(err)
}
for _, curr := range ifaces {
if curr.Flags&net.FlagLoopback == net.FlagLoopback {
continue
}
iface := &Interface{Name: curr.Name, Addrs: []*Addr{}}
result = append(result, iface)
addrs, err := curr.Addrs()
if err != nil {
return nil, errors.Wrap(err)
}
for _, cAddr := range addrs {
var ip net.IP
var ipNet *net.IPNet
ip, ipNet, err = net.ParseCIDR(cAddr.String())
if err != nil {
return nil, errors.Wrap(err)
}
addr := &Addr{IP: ip.String(), NetMask: net.IP(ipNet.Mask).String(), Version: IPv4}
if ip.To4() == nil {
addr.Version = IPv6
}
iface.Addrs = append(iface.Addrs, addr)
}
iface.DHCP, err = isDHCP(curr.Name)
if err != nil {
return nil, err
}
iface.Gateway, err = iface.GetGateway()
if err != nil {
return nil, err
}
iface.DNSServer, iface.DNSDomain, err = iface.GetDNSInfo()
if err != nil {
return nil, err
}
}
return result, nil
}
func netMaskToCIDR(mask string) (num int, err error) {
var tks = strings.Split(mask, ".")
if len(tks) != 4 {
return 0, errors.Errorf("Invalid mask: %s", mask)
}
var result uint32
for _, octet := range tks {
bt, err := strconv.ParseInt(octet, 10, 16)
if err != nil {
return 0, errors.Wrap(err)
}
result = result << 8
result += uint32(bt)
}
bits := 0
for result > 0 {
rem := result & 1
bits += int(rem)
result = result >> 1
}
return bits, nil
}
// IsNetworkManagerActive is used to
// check if we are using systemd.networkd or NetworkManager to
// manage the wired connections.
// Clear Linux OS change from system.networkd to NetworkManager
// in April 2019 build ?????
func IsNetworkManagerActive() bool {
const (
sysNetStr = "systemd-networkd.service"
netMgrStr = "NetworkManager.service"
)
// Assume we are new school
networkManager := true
nm := false
systemd := false
// Make the new dbus connection
conn, err := dbus.New()
defer conn.Close()
if err != nil {
log.Warning("Failed to connect to dbus")
return networkManager
}
// Get the list of Units
units, err := conn.ListUnits()
if err != nil {
log.Warning("Failed to get to dbus units")
return networkManager
}
// Look for Network Manager and systemd.networkd
for _, unit := range units {
if strings.Contains(unit.Name, sysNetStr) {
log.Debug("%s service is %s", unit.Name, unit.ActiveState)
if unit.ActiveState == "active" {
systemd = true
}
}
if strings.Contains(unit.Name, netMgrStr) {
log.Debug("%s service is %s", unit.Name, unit.ActiveState)
if unit.ActiveState == "active" {
nm = true
}
}
}
// If systemd.networkd is active, we use it for wired connections
if systemd {
networkManager = false
log.Info("Wired networking managed by " + sysNetStr)
} else if nm {
networkManager = true
log.Info("Wired networking managed by " + netMgrStr)
}
return networkManager
}
func (i *Interface) applyNetworkDStatic(root string, file *os.File) error {
needPacDiscover = true
config := `[Match]
Name={{.Name}}
[Network]
DNS={{.DNSServer}}
Address={{.Address}}
Gateway={{.Gateway}}
Domains={{.DNSDomain}}
`
var address string
for _, curr := range i.Addrs {
if curr.Version != IPv4 {
continue
}
cidrd, err := netMaskToCIDR(curr.NetMask)
if err != nil {
return err
}
address = fmt.Sprintf("%s/%d", curr.IP, cidrd)
}
template := template.Must(template.New("").Parse(config))
err := template.Execute(file, struct {
Name string
DNSServer string
Address string
Gateway string
DNSDomain string
}{
Name: i.Name,
DNSServer: i.DNSServer,
Gateway: i.Gateway,
DNSDomain: i.DNSDomain,
Address: address,
})
if err != nil {
return errors.Wrap(err)
}
return nil
}
// ApplyNetworkD does apply the interface configuration to the running system
// using systemd.networkd
func (i *Interface) ApplyNetworkD(root string) error {
fileName := fmt.Sprintf("10-%s.network", i.Name)
filePath := filepath.Join(root, systemdNetworkdDir, fileName)
if i.DHCP {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil
}
if err := os.Remove(filePath); err != nil {
return err
}
return nil
}
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.Wrap(err)
}
return i.applyNetworkDStatic(root, f)
}
func (i *Interface) applyNetworkManagerStatic(root string, file *os.File) error {
needPacDiscover = true
var address string
for _, curr := range i.Addrs {
if curr.Version != IPv4 {
continue
}
cidrd, err := netMaskToCIDR(curr.NetMask)
if err != nil {
return err
}
address = fmt.Sprintf("%s/%d", curr.IP, cidrd)
}
args := []string{
"nmcli",
"connection",
"add",
"type",
"ethernet",
"ifname",
i.Name,
"con-name",
fmt.Sprintf("Wired-%s", i.Name),
"ip4",
address,
"gw4",
i.Gateway,
"ipv4.method",
"manual",
"ipv4.dns",
i.DNSServer,
"ipv4.dns-search",
i.DNSDomain,
}
err := cmd.RunAndLog(args...)
if err != nil {
return errors.Wrap(err)
}
return nil
}
// ApplyNetworkManager does apply the interface configuration to the running system
// using Network Manager
func (i *Interface) ApplyNetworkManager(root string) error {
fileName := fmt.Sprintf("Wired-%s.nmconnection", i.Name)
filePath := filepath.Join(root, networkManagerDir, fileName)
if i.DHCP {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil
}
if err := os.Remove(filePath); err != nil {
return err
}
return nil
}
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errors.Wrap(err)
}
return i.applyNetworkManagerStatic(root, f)
}
// Apply apply the configurations of a set of interfaces to the running system
// Determines the network manage type to generate the correct files
func Apply(root string, ifaces []*Interface) error {
if root == "" {
return errors.Errorf("Could not apply network settings, Invalid root directory: %s", root)
}
netMgr := IsNetworkManagerActive()
if netMgr {
if _, err := os.Stat(networkManagerDir); os.IsNotExist(err) {
if err = os.MkdirAll(networkManagerDir, 0755); err != nil {
return errors.Wrap(err)
}
}
} else {
if _, err := os.Stat(systemdNetworkdDir); os.IsNotExist(err) {
if err = os.MkdirAll(systemdNetworkdDir, 0755); err != nil {
return errors.Wrap(err)
}
}
}
for _, curr := range ifaces {
if !curr.IsUserDefined() {
log.Info("Interface %s was not changed, skipping config apply.", curr.Name)
continue
}
if netMgr {
err := curr.ApplyNetworkManager(root)
if err != nil {
return err
}
} else {
err := curr.ApplyNetworkD(root)
if err != nil {
return err
}
}
}
if needPacDiscover {
err := EnablePacDiscovery(root)
if err != nil {
return err
}
}
return nil
}
// Restart restarts the network services
func Restart() error {
netMgr := IsNetworkManagerActive()
// TODO: pkexec might require the absolute path in GUI mode to ensure pkexec doesn't mess up PATH.
if netMgr {
err := cmd.RunAndLog("systemctl", "restart", "NetworkManager")
if err != nil {
return errors.Wrap(err)
}
} else {
err := cmd.RunAndLog("systemctl", "restart", "systemd-networkd")
if err != nil {
return errors.Wrap(err)
}
}
err := cmd.RunAndLog("systemctl", "restart", "systemd-resolved")
if err != nil {
return errors.Wrap(err)
}
err = cmd.RunAndLog("systemctl", "restart", "pacdiscovery")
if err != nil {
return errors.Wrap(err)
}
return nil
}
// VerifyConnectivity tests if the network configuration is working
func VerifyConnectivity() error {
var versionURL []byte
var err error
if versionURL, err = ioutil.ReadFile(versionURLPath); err != nil {
return errors.Errorf("Read version file %s: %v", versionURLPath, err)
}
return CheckURL(string(versionURL))
}
// CheckURL tests if the given URL is accessible
func CheckURL(url string) error {
args := []string{
"/usr/bin/timeout",
"--kill-after=10s",
"10s",
"/usr/bin/curl",
"--no-sessionid",
"-o",
"/dev/null",
"-s",
"-f",
url,
}
if err := cmd.Run(nil, args...); err != nil {
log.Debug("curl failed : %q", err)
return errors.Wrap(err)
}
return nil
}
// FetchRemoteConfigFile given an config url fetches it from the network. This function
// currently supports only http/https protocol. After success return the local file path.
func FetchRemoteConfigFile(url string) (string, error) {
// Get a temp filename to download to
out, err := ioutil.TempFile("", "clr-installer-yaml-")
if err != nil {
return "", err
}
_ = out.Close()
// Since Clear Linux automatically proxy are not support in golang via
// the patch to libcurl, we need to use a system call to curl for now.
// TODO: Change this back to native http.Get(url) once a better
// proxy solution is deployed in the OS
args := []string{
"timeout",
"--kill-after=30s",
"30s",
"curl",
"--no-sessionid",
"-o",
out.Name(),
"-s",
"-f",
url,
}
if err := cmd.Run(nil, args...); err != nil {
log.Debug("FetchRemoteConfigFile failed : %q", err)
defer func() { _ = os.Remove(out.Name()) }()
return "", err
}
return out.Name(), nil
}
// DownloadInstallerMessage pulls down a message from a URL
// Intended for getting a message to display before or after
// the installation process
func DownloadInstallerMessage(header string, installConf string) string {
var result Messenger
downloadURL := fmt.Sprintf(installDataURLBase, utils.ClearVersion, installConf)
msgFile, err := FetchRemoteConfigFile(downloadURL)
if err != nil {
log.Debug("Failed to download the %s message: %s", header, err)
return ""
}
defer func() { _ = os.Remove(msgFile) }()
configStr, err := ioutil.ReadFile(msgFile)
if err != nil {
log.Debug("Failed to read the %s file: %s", header, err)
return ""
}
if err := yaml.UnmarshalStrict(configStr, &result); err != nil {
log.Debug("Failed to parse the %s YAML file: %s", header, err)
return ""
}
if result.Messages == nil {
log.Debug("%s has no message content", header)
return ""
}
var lines []string
found := false
for _, message := range result.Messages {
line := strings.TrimSpace(message.Default)
if len(line) > 0 {
// We found at least one line with non-whitespace
found = true
}
lines = append(lines, strings.TrimSuffix(message.Default, "\n"))
}
msg := ""
if found {
msg = strings.Join(lines, "\n")
log.Debug("%s message: %s", header, msg)
}
return msg
}
// IsValidIP returns empty string if IP address is valid
func IsValidIP(str string) string {
if !validIPExp.MatchString(str) {
return "Invalid IP Addr"
}
return ""
}
// EnablePacDiscovery turns on the pacdiscovery service
// Normally this service is enabled by a DHCP lease path, but
// it must be manually enabled if we set a static IP
func EnablePacDiscovery(rootDir string) error {
args := []string{
"chroot",
rootDir,
"systemctl",
"enable",
"pacdiscovery",
}
// Make sure we have an installation environment
// and not a test environment
systemctl := filepath.Join(rootDir, "/usr/sbin/systemctl")
if _, err := os.Stat(systemctl); os.IsNotExist(err) {
return nil
}
if err := cmd.RunAndLog(args...); err != nil {
return errors.Wrap(err)
}
return nil
}
// CopyNetworkInterfaces transfer the user defined network interface files
// to the target installation media
func CopyNetworkInterfaces(rootDir string) error {
systemNetworkPaths := []string{systemdNetworkdDir, networkManagerDir}
for _, systemNetworkPath := range systemNetworkPaths {
log.Debug("Checking network interfaces in %q to install to target", systemNetworkPath)
if _, err := os.Stat(systemNetworkPath); err != nil {
if os.IsNotExist(err) {
log.Debug("No updated network interfaces in %q to install to target",
systemNetworkPath)
continue
}
log.Warning("Issue check interface %q: %v", systemNetworkPath, errors.Wrap(err))
continue
}
if err := utils.CopyAllFiles(systemNetworkPath, rootDir); err != nil {
log.Warning("Failed to copy image Network configuration data to %s", systemNetworkPath)
} else {
log.Info("Copied image Network configuration data to %s",
filepath.Join(rootDir, systemNetworkPath))
}
}
// We likely copied a static IP, so enable PacDiscovery
err := EnablePacDiscovery(rootDir)
if err != nil {
return err
}
return nil
}