package netconf import ( "bytes" "errors" "net" "os" "os/exec" "strconv" "strings" "sync" "syscall" "github.com/rancher/os/pkg/log" shlex "github.com/flynn/go-shlex" glob "github.com/ryanuber/go-glob" "github.com/vishvananda/netlink" ) const ( CONF = "/var/lib/rancher/conf" MODE = "mode" ) var ( defaultDhcpArgs = []string{"dhcpcd", "-MA4"} exitDhcpArgs = []string{"dhcpcd", "-x"} exitWpaArgs = []string{"wpa_cli", "terminate"} dhcpReleaseCmd = "dhcpcd --release" ) func createInterfaces(netCfg *NetworkConfig) { configured := map[string]bool{} for name, iface := range netCfg.Interfaces { if iface.Bridge == "true" { if _, err := NewBridge(name); err != nil { log.Errorf("Failed to create bridge %s: %v", name, err) } } else if iface.Bridge != "" { if _, err := NewBridge(iface.Bridge); err != nil { log.Errorf("Failed to create bridge %s: %v", iface.Bridge, err) } } else if iface.Bond != "" { bond, err := Bond(iface.Bond) if err != nil { log.Errorf("Failed to create bond %s: %v", iface.Bond, err) continue } if !configured[iface.Bond] { if bondIface, ok := netCfg.Interfaces[iface.Bond]; ok { // Other settings depends on mode, so set it first if v, ok := bondIface.BondOpts[MODE]; ok { bond.Opt(MODE, v) } for k, v := range bondIface.BondOpts { if k != MODE { bond.Opt(k, v) } } configured[iface.Bond] = true } } } } } func createSlaveInterfaces(netCfg *NetworkConfig) { links, err := GetValidLinkList() if err != nil { log.Errorf("Failed to list links: %v", err) return } for _, link := range links { match, ok := findMatch(link, netCfg) if !ok { continue } vlanDefs, err := ParseVlanDefinitions(match.Vlans) if err != nil { log.Errorf("Failed to create vlans on device %s: %v", link.Attrs().Name, err) continue } for _, vlanDef := range vlanDefs { if _, err = NewVlan(link, vlanDef.Name, vlanDef.ID); err != nil { log.Errorf("Failed to create vlans on device %s, id %d: %v", link.Attrs().Name, vlanDef.ID, err) } } } } func findMatch(link netlink.Link, netCfg *NetworkConfig) (InterfaceConfig, bool) { linkName := link.Attrs().Name var match InterfaceConfig exactMatch := false found := false for key, netConf := range netCfg.Interfaces { if netConf.Match == "" { netConf.Match = key } if netConf.Match == "" { continue } if strings.HasPrefix(netConf.Match, "mac") { haAddr, err := net.ParseMAC(netConf.Match[4:]) if err != nil { log.Errorf("Failed to parse mac %s: %v", netConf.Match[4:], err) continue } // Don't match mac address of a bond or VLAN interface because it is the same address as the slave or parent. if bytes.Compare(haAddr, link.Attrs().HardwareAddr) == 0 && link.Attrs().Name != netConf.Bond && link.Type() != "vlan" { // MAC address match is used over all other matches return netConf, true } } if !exactMatch && glob.Glob(netConf.Match, linkName) { match = netConf found = true } if netConf.Match == linkName { // Found exact match, use it over wildcard match match = netConf exactMatch = true } } return match, exactMatch || found } func populateDefault(netCfg *NetworkConfig) { if netCfg.Interfaces == nil { netCfg.Interfaces = map[string]InterfaceConfig{} } if len(netCfg.Interfaces) == 0 { netCfg.Interfaces["eth*"] = InterfaceConfig{ DHCP: true, } } if _, ok := netCfg.Interfaces["lo"]; !ok { netCfg.Interfaces["lo"] = InterfaceConfig{ Addresses: []string{ "127.0.0.1/8", "::1/128", }, } } } func ApplyNetworkConfigs(netCfg *NetworkConfig, userSetHostname, userSetDNS bool) (bool, error) { populateDefault(netCfg) log.Debugf("Config: %#v", netCfg) runCmds(netCfg.PreCmds, "") defer runCmds(netCfg.PostCmds, "") createInterfaces(netCfg) createSlaveInterfaces(netCfg) links, err := GetValidLinkList() if err != nil { log.Errorf("error getting LinkList: %s", err) return false, err } wg := sync.WaitGroup{} //apply network config for _, link := range links { if !strings.Contains(link.Attrs().Name, "wlan") { applyOuter(link, netCfg, &wg, userSetHostname, userSetDNS) } } wg.Wait() // apply wifi network config for _, link := range links { if strings.Contains(link.Attrs().Name, "wlan") { applyOuter(link, netCfg, &wg, userSetHostname, userSetDNS) } } wg.Wait() // make sure there was a DHCP set dns - or tell ros to write 8.8.8.8,8.8.8.4 log.Infof("Checking to see if DNS was set by DHCP") dnsSet := false for _, link := range links { linkName := link.Attrs().Name log.Infof("dns testing %s", linkName) lease := GetDhcpLease(linkName) if _, ok := lease["domain_name_servers"]; ok { log.Infof("dns was dhcp set for %s", linkName) dnsSet = true } } return dnsSet, nil } func applyOuter(link netlink.Link, netCfg *NetworkConfig, wg *sync.WaitGroup, userSetHostname, userSetDNS bool) { linkName := link.Attrs().Name log.Debugf("applyOuter(%v, %v), link: %s", userSetHostname, userSetDNS, linkName) match, ok := findMatch(link, netCfg) if !ok { return } log.Debugf("Config(%s): %#v", linkName, match) // We plan to use the dhcpcd hook to control the wpa_supplicant, Whether the Wi-Fi network uses DHCP or Static // https://wiki.archlinux.org/index.php/Dhcpcd#Hooks. if match.WifiNetwork != "" { match.DHCP = true } runCmds(match.PreUp, linkName) defer runCmds(match.PostUp, linkName) if !match.DHCP { if err := applyInterfaceConfig(link, match); err != nil { log.Errorf("Failed to apply settings to %s : %v", linkName, err) } } if !match.DHCP && !hasDhcp(linkName) { log.Debugf("Skipping(%s): DHCP=false && no DHCP lease yet", linkName) return } wg.Add(1) go func(link netlink.Link, match InterfaceConfig) { if match.DHCP { if match.WifiNetwork != "" { runWifiDhcp(netCfg, link, match.WifiNetwork, !userSetHostname, !userSetDNS) } else { runDhcp(netCfg, link.Attrs().Name, match.DHCPArgs, !userSetHostname, !userSetDNS) } } else { log.Infof("dhcp release %s", link.Attrs().Name) runDhcp(netCfg, link.Attrs().Name, dhcpReleaseCmd, false, true) } wg.Done() }(link, match) } func GetDhcpLease(iface string) (lease map[string]string) { lease = make(map[string]string) out := getDhcpLeaseString(iface) log.Debugf("getDhcpLease %s: %s", iface, out) lines := strings.Split(string(out), "\n") for _, line := range lines { l := strings.SplitN(line, "=", 2) log.Debugf("line: %v", l) if len(l) > 1 { lease[l[0]] = l[1] } } return lease } func getDhcpLeaseString(iface string) []byte { args := defaultDhcpArgs args = append(args, "-U", iface) cmd := exec.Command(args[0], args[1:]...) //cmd.Stderr = os.Stderr out, err := cmd.Output() log.Debugf("Running cmd: %s, output: %s", args, string(out)) if err != nil { // dhcpcd works fine, but gets an error: exit status 1 log.Warnf("Failed to run cmd: %s, error: %v", args, err) } return out } func hasDhcp(iface string) bool { out := getDhcpLeaseString(iface) return len(out) > 0 } func runDhcp(netCfg *NetworkConfig, iface string, argstr string, setHostname, setDNS bool) { args := []string{} if argstr != "" { var err error args, err = shlex.Split(argstr) if err != nil { log.Errorf("Failed to parse [%s]: %v", argstr, err) } } if len(args) == 0 { args = defaultDhcpArgs } if setHostname { args = append(args, "-e", "force_hostname=true") } if !setDNS { args = append(args, "--nohook", "resolv.conf") } if netCfg.DHCPTimeout > 0 { args = append(args, "--timeout", strconv.Itoa(netCfg.DHCPTimeout)) } // Wait for lease // TODO: this should be optional - based on kernel arg? args = append(args, "-w", "--debug") args = append(args, iface) cmd := exec.Command(args[0], args[1:]...) log.Infof("Running DHCP on %s: %s", iface, strings.Join(args, " ")) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Failed to run dhcpcd for %s: %v", iface, err) } } func runWifiDhcp(netCfg *NetworkConfig, link netlink.Link, network string, setHostname, setDNS bool) { iface := link.Attrs().Name if _, ok := netCfg.WifiNetworks[network]; !ok { return } // Remove DHCP lease IP and static IP if hasDhcp(iface) { runDhcp(netCfg, iface, dhcpReleaseCmd, false, true) } existAddress, _ := getLinkAddrs(link) for _, addr := range existAddress { log.Infof("removing %s from %s", addr.String(), link.Attrs().Name) removeAddress(addr, link) } runDhcp(netCfg, iface, "", setHostname, setDNS) } func linkUp(link netlink.Link, netConf InterfaceConfig) error { if err := netlink.LinkSetUp(link); err != nil { log.Errorf("failed to setup link: %v", err) return err } return nil } func applyAddress(address string, link netlink.Link, netConf InterfaceConfig) error { addr, err := netlink.ParseAddr(address) if err != nil { return err } if err := netlink.AddrAdd(link, addr); err == syscall.EEXIST { //Ignore this error } else if err != nil { log.Errorf("addr add failed: %v", err) } else { log.Infof("Set %s on %s", netConf.Address, link.Attrs().Name) } return nil } func removeAddress(addr netlink.Addr, link netlink.Link) error { if err := netlink.AddrDel(link, &addr); err == syscall.EEXIST { //Ignore this error } else if err != nil { log.Errorf("addr del failed: %v", err) } else { log.Infof("Removed %s from %s", addr.String(), link.Attrs().Name) } return nil } // setGateway(add=false) will set _one_ gateway on an interface (ie, replace an existing one) // setGateway(add=true) will add another gateway to an interface func setGateway(gateway string, add bool) error { if gateway == "" { return nil } gatewayIP := net.ParseIP(gateway) if gatewayIP == nil { return errors.New("Invalid gateway address " + gateway) } route := netlink.Route{ Scope: netlink.SCOPE_UNIVERSE, Gw: gatewayIP, } if add { if err := netlink.RouteAdd(&route); err == syscall.EEXIST { //Ignore this error } else if err != nil { log.Errorf("gateway add failed: %v", err) return err } log.Infof("Added default gateway %s", gateway) } else { if err := netlink.RouteReplace(&route); err == syscall.EEXIST { //Ignore this error } else if err != nil { log.Errorf("gateway replace failed: %v", err) return err } log.Infof("Replaced default gateway %s", gateway) } return nil } func applyInterfaceConfig(link netlink.Link, netConf InterfaceConfig) error { //TODO: skip doing anything if the settings are "default"? //TODO: how do you undo a non-default with a default? // ATM, this removes // TODO: undo if netConf.Bond != "" { if err := netlink.LinkSetDown(link); err != nil { return err } b, err := Bond(netConf.Bond) if err != nil { return err } return b.AddSlave(link.Attrs().Name) } //TODO: undo if netConf.Bridge != "" && netConf.Bridge != "true" { b, err := NewBridge(netConf.Bridge) if err != nil { return err } if err := b.AddLink(link); err != nil { return err } return linkUp(link, netConf) } if netConf.IPV4LL { if err := AssignLinkLocalIP(link); err != nil { log.Errorf("IPV4LL set failed: %v", err) return err } } else { if err := RemoveLinkLocalIP(link); err != nil { log.Errorf("IPV4LL del failed: %v", err) return err } } addresses := []string{} if netConf.Address != "" { addresses = append(addresses, netConf.Address) } if len(netConf.Addresses) > 0 { addresses = append(addresses, netConf.Addresses...) } existingAddrs, _ := getLinkAddrs(link) addrMap := make(map[string]bool) for _, address := range addresses { addrMap[address] = true } for _, addr := range existingAddrs { if _, ok := addrMap[addr.IPNet.String()]; !ok { if netConf.DHCP || netConf.IPV4LL { // let the dhcpcd take care of it log.Infof("leaving %s from %s", addr.String(), link.Attrs().Name) } else { log.Infof("removing %s from %s", addr.String(), link.Attrs().Name) removeAddress(addr, link) } } } for _, address := range addresses { log.Infof("Applying %s to %s", address, link.Attrs().Name) err := applyAddress(address, link, netConf) if err != nil { log.Errorf("Failed to apply address %s to %s: %v", address, link.Attrs().Name, err) } } // TODO: can we set to default? if netConf.MTU > 0 { if err := netlink.LinkSetMTU(link, netConf.MTU); err != nil { log.Errorf("set MTU Failed: %v", err) return err } } if err := linkUp(link, netConf); err != nil { return err } // replace the existing gw with the main ipv4 one if err := setGateway(netConf.Gateway, true); err != nil { log.Errorf("Fail to set gateway %s", netConf.Gateway) } //and then add the ipv6 one if it exists if err := setGateway(netConf.GatewayIpv6, true); err != nil { log.Errorf("Fail to set gateway %s", netConf.GatewayIpv6) } // TODO: how to remove a GW? (on aws it seems to be hard to find out what the gw is :/) return nil } func runCmds(cmds []string, iface string) { log.Debugf("runCmds(on %s): %v", iface, cmds) for _, cmd := range cmds { log.Debugf("runCmd(on %s): %v", iface, cmd) cmd = strings.TrimSpace(cmd) if cmd == "" { continue } args, err := shlex.Split(strings.Replace(cmd, "$iface", iface, -1)) if err != nil { log.Errorf("Failed to parse command [%s]: %v", cmd, err) continue } log.Infof("Running command %s %v", args[0], args[1:]) cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Failed to run command [%v]: %v", cmd, err) continue } } } func GetValidLinkList() ([]netlink.Link, error) { var validLinkList []netlink.Link links, err := netlink.LinkList() if err != nil { return validLinkList, err } for _, l := range links { linkName := l.Attrs().Name if linkName == "lo" || linkName == "docker-sys" || linkName == "docker0" { continue } validLinkList = append(validLinkList, l) } return validLinkList, nil } func StopDhcpcd() { cmd := exec.Command(exitDhcpArgs[0], exitDhcpArgs[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Failed to run command [%v]: %v", cmd, err) } } func StopWpaSupplicant() { links, err := GetValidLinkList() if err != nil { log.Errorf("error getting LinkList: %s", err) return } // need terminate all ifname for _, link := range links { cmd := exec.Command(exitWpaArgs[0], exitWpaArgs[1], "-i", link.Attrs().Name) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Errorf("Failed to run command %v: %v", cmd.Args, err) } } }