diff --git a/cmd/cloudinitsave/cloudinitsave.go b/cmd/cloudinitsave/cloudinitsave.go index fd2af739..4e4b6bc0 100644 --- a/cmd/cloudinitsave/cloudinitsave.go +++ b/cmd/cloudinitsave/cloudinitsave.go @@ -66,6 +66,8 @@ func Main() { log.Errorf("Failed to save cloud-config: %v", err) } + // exit wpa_supplicant + netconf.StopWpaSupplicant() // exit dhcpcd netconf.StopDhcpcd() } diff --git a/cmd/network/network.go b/cmd/network/network.go index 47a5294b..f4d637fd 100644 --- a/cmd/network/network.go +++ b/cmd/network/network.go @@ -1,10 +1,14 @@ package network import ( + "fmt" "io/ioutil" "os" "os/signal" + "path/filepath" + "strconv" "syscall" + "text/template" "github.com/rancher/os/config" "github.com/rancher/os/pkg/docker" @@ -16,6 +20,12 @@ import ( "golang.org/x/net/context" ) +var funcMap = template.FuncMap{ + "addFunc": func(a, b int) string { + return strconv.Itoa(a + b) + }, +} + func Main() { log.InitLogger() @@ -36,6 +46,7 @@ func Main() { signal.Notify(signalChan, syscall.SIGTERM) <-signalChan log.Info("Received SIGTERM, shutting down") + netconf.StopWpaSupplicant() netconf.StopDhcpcd() } @@ -51,6 +62,14 @@ func ApplyNetworkConfig(cfg *config.CloudConfig) { if cfg.Rancher.Network.DHCPTimeout <= 0 { cfg.Rancher.Network.DHCPTimeout = cfg.Rancher.Defaults.Network.DHCPTimeout } + + // In order to handle the STATIC mode in Wi-Fi network, we have to update the dhcpcd.conf file. + // https://wiki.archlinux.org/index.php/dhcpcd#Static_profile + if len(cfg.Rancher.Network.WifiNetworks) > 0 { + generateDhcpcdFiles(cfg) + generateWpaFiles(cfg) + } + dhcpSetDNS, err := netconf.ApplyNetworkConfigs(&cfg.Rancher.Network, userSetHostname, userSetDNS) if err != nil { log.Errorf("Failed to apply network configs(by netconf): %v", err) @@ -86,3 +105,50 @@ func ApplyNetworkConfig(cfg *config.CloudConfig) { log.Errorf("Failed to sync hostname: %v", err) } } + +func generateDhcpcdFiles(cfg *config.CloudConfig) { + networks := cfg.Rancher.Network.WifiNetworks + interfaces := cfg.Rancher.Network.Interfaces + configs := make(map[string]netconf.WifiNetworkConfig) + for k, v := range interfaces { + if c, ok := networks[v.WifiNetwork]; ok && c.Address != "" { + configs[k] = c + } + } + f, err := os.Create(config.DHCPCDConfigFile) + defer f.Close() + if err != nil { + log.Errorf("Failed to open file: %s err: %v", config.DHCPCDConfigFile, err) + } + templateFiles := []string{config.DHCPCDTemplateFile} + templateName := filepath.Base(templateFiles[0]) + p := template.Must(template.New(templateName).ParseFiles(templateFiles...)) + if err = p.Execute(f, configs); err != nil { + log.Errorf("Failed to wrote wpa configuration to %s: %v", config.DHCPCDConfigFile, err) + } +} + +func generateWpaFiles(cfg *config.CloudConfig) { + networks := cfg.Rancher.Network.WifiNetworks + interfaces := cfg.Rancher.Network.Interfaces + for k, v := range interfaces { + if v.WifiNetwork != "" { + configs := make(map[string]netconf.WifiNetworkConfig) + filename := fmt.Sprintf(config.WPAConfigFile, k) + f, err := os.Create(filename) + if err != nil { + log.Errorf("Failed to open file: %s err: %v", filename, err) + } + if c, ok := networks[v.WifiNetwork]; ok { + configs[v.WifiNetwork] = c + } + templateFiles := []string{config.WPATemplateFile} + templateName := filepath.Base(templateFiles[0]) + p := template.Must(template.New(templateName).Funcs(funcMap).ParseFiles(templateFiles...)) + if err = p.Execute(f, configs); err != nil { + log.Errorf("Failed to wrote wpa configuration to %s: %v", filename, err) + } + f.Close() + } + } +} diff --git a/config/schema.go b/config/schema.go index 9dd7859c..6bd46c4d 100644 --- a/config/schema.go +++ b/config/schema.go @@ -87,7 +87,8 @@ var schema = `{ "post_cmds": {"$ref": "#/definitions/list_of_strings"}, "http_proxy": {"type": "string"}, "https_proxy": {"type": "string"}, - "no_proxy": {"type": "string"} + "no_proxy": {"type": "string"}, + "wifi_networks": {"type": "object"} } }, diff --git a/config/types.go b/config/types.go index 4a839909..62389cf5 100644 --- a/config/types.go +++ b/config/types.go @@ -47,6 +47,10 @@ const ( MetaDataFile = "/var/lib/rancher/conf/metadata" CloudConfigFile = "/var/lib/rancher/conf/cloud-config.yml" EtcResolvConfFile = "/etc/resolv.conf" + WPAConfigFile = "/etc/wpa_supplicant-%s.conf" + WPATemplateFile = "/etc/wpa_supplicant.conf.tpl" + DHCPCDConfigFile = "/etc/dhcpcd.conf" + DHCPCDTemplateFile = "/etc/dhcpcd.conf.tpl" MultiDockerConfFile = "/var/lib/rancher/conf.d/m-user-docker.yml" MultiDockerDataDir = "/var/lib/m-user-docker" ) diff --git a/images/01-base/Dockerfile b/images/01-base/Dockerfile index c67597e7..0c316835 100644 --- a/images/01-base/Dockerfile +++ b/images/01-base/Dockerfile @@ -37,6 +37,8 @@ COPY start_ntp.sh /bin/start_ntp.sh COPY dhcpcd/dhcpcd.enter-hook /etc/dhcpcd.enter-hook COPY dhcpcd/10-mtu /lib/dhcpcd/dhcpcd-hooks/ COPY dhcpcd/dhcpcd.debug /usr/share/logrotate/logrotate.d/ +COPY templates/dhcpcd.conf.tpl /etc/dhcpcd.conf.tpl +COPY templates/wpa_supplicant.conf.tpl /etc/wpa_supplicant.conf.tpl RUN sed -i s/"partx --update \"\$part\" \"\$dev\""/"partx --update --nr \"\$part\" \"\$dev\""/g /usr/bin/growpart && \ sed -i -e 's/duid/clientid/g' /etc/dhcpcd.conf && \ rm -f /etc/wpa_supplicant.conf && \ diff --git a/images/01-base/templates/dhcpcd.conf.tpl b/images/01-base/templates/dhcpcd.conf.tpl new file mode 100644 index 00000000..7ffdf9e1 --- /dev/null +++ b/images/01-base/templates/dhcpcd.conf.tpl @@ -0,0 +1,46 @@ +# A sample configuration for dhcpcd. +# See dhcpcd.conf(5) for details. + +# Allow users of this group to interact with dhcpcd via the control socket. +#controlgroup wheel + +# Inform the DHCP server of our hostname for DDNS. +hostname + +# Use the hardware address of the interface for the Client ID. +#clientid +# or +# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361. +# Some non-RFC compliant DHCP servers do not reply with this set. +# In this case, comment out duid and enable clientid above. +clientid + +# Persist interface configuration when dhcpcd exits. +persistent + +# Rapid commit support. +# Safe to enable by default because it requires the equivalent option set +# on the server to actually work. +option rapid_commit + +# A list of options to request from the DHCP server. +option domain_name_servers, domain_name, domain_search, host_name +option classless_static_routes +# Most distributions have NTP support. +option ntp_servers +# Respect the network MTU. This is applied to DHCP routes. +option interface_mtu + +# A ServerID is required by RFC2131. +require dhcp_server_identifier + +# Generate SLAAC address using the Hardware Address of the interface +#slaac hwaddr +# OR generate Stable Private IPv6 Addresses based from the DUID +slaac private + +{{- range $key, $value := .}} +interface {{$key}} +static ip_address={{$value.Address}} +static routers={{$value.Gateway}} +{{- end}} \ No newline at end of file diff --git a/images/01-base/templates/wpa_supplicant.conf.tpl b/images/01-base/templates/wpa_supplicant.conf.tpl new file mode 100644 index 00000000..81c0d19e --- /dev/null +++ b/images/01-base/templates/wpa_supplicant.conf.tpl @@ -0,0 +1,73 @@ +ctrl_interface=/var/run/wpa_supplicant +ap_scan=1 +update_config=1 + +{{- range $key, $value := .}} +network={ + ssid="{{$value.SSID}}" + {{- if gt (len $value.PSK) 0}} + psk="{{$value.PSK}}" + {{- end}} + {{- if gt (len $value.KeyMgmt) 0}} + key_mgmt={{$value.KeyMgmt}} + {{- end}} + {{- if $value.ScanSSID}} + scan_ssid={{$value.ScanSSID}} + {{- end}} + {{- if $value.Priority}} + priority={{$value.Priority}} + {{- end}} + {{- if gt (len $value.Pairwise) 0}} + pairwise={{$value.Pairwise}} + {{- end}} + {{- if gt (len $value.Group) 0}} + group={{$value.Group}} + {{- end}} + {{- if gt (len $value.Eap) 0}} + eap={{$value.Eap}} + {{- end}} + {{- if gt (len $value.Identity) 0}} + identity="{{$value.Identity}}" + {{- end}} + {{- if gt (len $value.AnonymousIdentity) 0}} + anonymous_identity="{{$value.AnonymousIdentity}}" + {{- end}} + {{- if $value.EapolFlags}} + eapol_flags={{$value.EapolFlags}} + {{- end}} + {{- if gt (len $value.Password) 0}} + password="{{$value.Password}}" + {{- end}} + {{- range $i, $v := $value.Phases}} + phase{{addFunc $i 1}}="{{$v}}" + {{- end}} + {{- range $i, $v := $value.CaCerts}} + {{- if eq $i 0}} + ca_cert="{{$v}}" + {{- else}} + ca_cert{{addFunc $i 1}}="{{$v}}" + {{- end}} + {{- end}} + {{- range $i, $v := $value.ClientCerts}} + {{- if eq $i 0}} + client_cert="{{$v}}" + {{- else}} + client_cert{{addFunc $i 1}}="{{$v}}" + {{- end}} + {{- end}} + {{- range $i, $v := $value.PrivateKeys}} + {{- if eq $i 0}} + private_key="{{$v}}" + {{- else}} + private_key{{addFunc $i 1}}="{{$v}}" + {{- end}} + {{- end}} + {{- range $i, $v := $value.PrivateKeyPasswds}} + {{- if eq $i 0}} + private_key_passwd="{{$v}}" + {{- else}} + private_key{{addFunc $i 1}}_passwd="{{$v}}" + {{- end}} + {{- end}} +} +{{- end}} \ No newline at end of file diff --git a/pkg/init/cloudinit/cloudinit.go b/pkg/init/cloudinit/cloudinit.go index 6f6f9f49..06caadf0 100644 --- a/pkg/init/cloudinit/cloudinit.go +++ b/pkg/init/cloudinit/cloudinit.go @@ -20,6 +20,13 @@ func CloudInit(cfg *config.CloudConfig) (*config.CloudConfig, error) { } } + if len(stateConfig.Rancher.Network.WifiNetworks) > 0 { + cfg.Rancher.Network.WifiNetworks = stateConfig.Rancher.Network.WifiNetworks + if err := config.Set("rancher.network.wifi_networks", stateConfig.Rancher.Network.WifiNetworks); err != nil { + log.Error(err) + } + } + if len(stateConfig.Rancher.Network.Interfaces) > 0 { cfg.Rancher.Network = stateConfig.Rancher.Network if err := config.Set("rancher.network", stateConfig.Rancher.Network); err != nil { diff --git a/pkg/netconf/netconf_linux.go b/pkg/netconf/netconf_linux.go index 795b85b1..51efa9f9 100644 --- a/pkg/netconf/netconf_linux.go +++ b/pkg/netconf/netconf_linux.go @@ -26,6 +26,7 @@ const ( var ( defaultDhcpArgs = []string{"dhcpcd", "-MA4"} exitDhcpArgs = []string{"dhcpcd", "-x"} + exitWpaArgs = []string{"wpa_cli", "terminate"} dhcpReleaseCmd = "dhcpcd --release" ) @@ -208,6 +209,13 @@ func applyOuter(link netlink.Link, netCfg *NetworkConfig, wg *sync.WaitGroup, us } 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) @@ -223,16 +231,19 @@ func applyOuter(link netlink.Link, netCfg *NetworkConfig, wg *sync.WaitGroup, us } wg.Add(1) - go func(iface string, match InterfaceConfig) { + go func(link netlink.Link, match InterfaceConfig) { if match.DHCP { - // retrigger, perhaps we're running this to get the new address - runDhcp(netCfg, iface, match.DHCPArgs, !userSetHostname, !userSetDNS) + 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", iface) - runDhcp(netCfg, iface, dhcpReleaseCmd, false, true) + log.Infof("dhcp release %s", link.Attrs().Name) + runDhcp(netCfg, link.Attrs().Name, dhcpReleaseCmd, false, true) } wg.Done() - }(linkName, match) + }(link, match) } func GetDhcpLease(iface string) (lease map[string]string) { @@ -310,6 +321,25 @@ func runDhcp(netCfg *NetworkConfig, iface string, argstr string, setHostname, se } } +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, false) + } + 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) @@ -538,3 +568,20 @@ func StopDhcpcd() { 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) + } + } +} diff --git a/pkg/netconf/types.go b/pkg/netconf/types.go index 1e7f809c..3655d83a 100644 --- a/pkg/netconf/types.go +++ b/pkg/netconf/types.go @@ -1,14 +1,15 @@ package netconf type NetworkConfig struct { - PreCmds []string `yaml:"pre_cmds,omitempty"` - DHCPTimeout int `yaml:"dhcp_timeout,omitempty"` - DNS DNSConfig `yaml:"dns,omitempty"` - Interfaces map[string]InterfaceConfig `yaml:"interfaces,omitempty"` - PostCmds []string `yaml:"post_cmds,omitempty"` - HTTPProxy string `yaml:"http_proxy,omitempty"` - HTTPSProxy string `yaml:"https_proxy,omitempty"` - NoProxy string `yaml:"no_proxy,omitempty"` + PreCmds []string `yaml:"pre_cmds,omitempty"` + DHCPTimeout int `yaml:"dhcp_timeout,omitempty"` + DNS DNSConfig `yaml:"dns,omitempty"` + Interfaces map[string]InterfaceConfig `yaml:"interfaces,omitempty"` + PostCmds []string `yaml:"post_cmds,omitempty"` + HTTPProxy string `yaml:"http_proxy,omitempty"` + HTTPSProxy string `yaml:"https_proxy,omitempty"` + NoProxy string `yaml:"no_proxy,omitempty"` + WifiNetworks map[string]WifiNetworkConfig `yaml:"wifi_networks,omitempty"` } type InterfaceConfig struct { @@ -27,9 +28,32 @@ type InterfaceConfig struct { PostUp []string `yaml:"post_up,omitempty"` PreUp []string `yaml:"pre_up,omitempty"` Vlans string `yaml:"vlans,omitempty"` + WifiNetwork string `yaml:"wifi_network,omitempty"` } type DNSConfig struct { Nameservers []string `yaml:"nameservers,flow,omitempty"` Search []string `yaml:"search,flow,omitempty"` } + +type WifiNetworkConfig struct { + Address string `yaml:"address,omitempty"` + Gateway string `yaml:"gateway,omitempty"` + ScanSSID int `yaml:"scan_ssid,omitempty"` + SSID string `yaml:"ssid,omitempty"` + PSK string `yaml:"psk,omitempty"` + Priority int `yaml:"priority,omitempty"` + Pairwise string `yaml:"pairwise,omitempty"` + Group string `yaml:"group,omitempty"` + Eap string `yaml:"eap,omitempty"` + Identity string `yaml:"identity,omitempty"` + AnonymousIdentity string `yaml:"anonymous_identity,omitempty"` + CaCerts []string `yaml:"ca_certs,omitempty"` + ClientCerts []string `yaml:"client_certs,omitempty"` + PrivateKeys []string `yaml:"private_keys,omitempty"` + PrivateKeyPasswds []string `yaml:"private_key_passwds,omitempty"` + Phases []string `yaml:"phases,omitempty"` + EapolFlags int `yaml:"eapol_flags,omitempty"` + KeyMgmt string `yaml:"key_mgmt,omitempty"` + Password string `yaml:"password,omitempty"` +}