github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/network/utils.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package network 5 6 import ( 7 "bufio" 8 "io/ioutil" 9 "net" 10 "os" 11 "path/filepath" 12 "strings" 13 14 "github.com/juju/errors" 15 ) 16 17 // DNSConfig holds a list of DNS nameserver addresses and default search 18 // domains. 19 type DNSConfig struct { 20 Nameservers []Address 21 SearchDomains []string 22 } 23 24 // ParseResolvConf parses a resolv.conf(5) file at the given path (usually 25 // "/etc/resolv.conf"), if present. Returns the values of any 'nameserver' 26 // stanzas, and the last 'search' stanza found. Values in the result will appear 27 // in the order found, including duplicates. Parsing errors will be returned in 28 // these cases: 29 // 30 // 1. if a 'nameserver' or 'search' without a value is found; 31 // 2. 'nameserver' with more than one value (trailing comments starting with '#' 32 // or ';' after the value are allowed). 33 // 3. if any value containing '#' or ';' (e.g. 'nameserver 8.8.8.8#bad'), because 34 // values and comments following them must be separated by whitespace. 35 // 36 // No error is returned if the file is missing. See resolv.conf(5) man page for 37 // details. 38 func ParseResolvConf(path string) (*DNSConfig, error) { 39 file, err := os.Open(path) 40 if os.IsNotExist(err) { 41 logger.Debugf("%q does not exist - not parsing", path) 42 return nil, nil 43 } else if err != nil { 44 return nil, errors.Trace(err) 45 } 46 defer file.Close() 47 48 var ( 49 nameservers []string 50 searchDomains []string 51 ) 52 scanner := bufio.NewScanner(file) 53 lineNum := 0 54 for scanner.Scan() { 55 line := scanner.Text() 56 lineNum++ 57 58 values, err := parseResolvStanza(line, "nameserver") 59 if err != nil { 60 return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum) 61 } 62 63 if numValues := len(values); numValues > 1 { 64 return nil, errors.Errorf( 65 "parsing %q, line %d: one value expected for \"nameserver\", got %d", 66 path, lineNum, numValues, 67 ) 68 } else if numValues == 1 { 69 nameservers = append(nameservers, values[0]) 70 continue 71 } 72 73 values, err = parseResolvStanza(line, "search") 74 if err != nil { 75 return nil, errors.Annotatef(err, "parsing %q, line %d", path, lineNum) 76 } 77 78 if len(values) > 0 { 79 // Last 'search' found wins. 80 searchDomains = values 81 } 82 } 83 84 if err := scanner.Err(); err != nil { 85 return nil, errors.Annotatef(err, "reading %q", path) 86 } 87 88 return &DNSConfig{ 89 Nameservers: NewAddresses(nameservers...), 90 SearchDomains: searchDomains, 91 }, nil 92 } 93 94 // parseResolvStanza parses a single line from a resolv.conf(5) file, beginning 95 // with the given stanza ('nameserver' or 'search' ). If the line does not 96 // contain the stanza, no results and no error is returned. Leading and trailing 97 // whitespace is removed first, then lines starting with ";" or "#" are treated 98 // as comments. 99 // 100 // Examples: 101 // parseResolvStanza(` # nothing ;to see here`, "doesn't matter") 102 // will return (nil, nil) - comments and whitespace are ignored, nothing left. 103 // 104 // parseResolvStanza(` nameserver ns1.example.com # preferred`, "nameserver") 105 // will return ([]string{"ns1.example.com"}, nil). 106 // 107 // parseResolvStanza(`search ;; bad: no value`, "search") 108 // will return (nil, err: `"search": required value(s) missing`) 109 // 110 // parseResolvStanza(`search foo bar foo foo.bar bar.foo ;; try all`, "search") 111 // will return ([]string("foo", "bar", "foo", "foo.bar", "bar.foo"}, nil) 112 // 113 // parseResolvStanza(`search foo#bad comment`, "nameserver") 114 // will return (nil, nil) - line does not start with "nameserver". 115 // 116 // parseResolvStanza(`search foo#bad comment`, "search") 117 // will return (nil, err: `"search": invalid value "foo#bad"`) - no whitespace 118 // between the value "foo" and the following comment "#bad comment". 119 func parseResolvStanza(line, stanza string) ([]string, error) { 120 const commentChars = ";#" 121 isComment := func(s string) bool { 122 return strings.IndexAny(s, commentChars) == 0 123 } 124 125 line = strings.TrimSpace(line) 126 fields := strings.Fields(line) 127 noFields := len(fields) == 0 // line contains only whitespace 128 129 if isComment(line) || noFields || fields[0] != stanza { 130 // Lines starting with ';' or '#' are comments and are ignored. Empty 131 // lines and those not starting with stanza are ignored. 132 return nil, nil 133 } 134 135 // Mostly for convenience, comments starting with ';' or '#' after a value 136 // are allowed and ignored, assuming there's whitespace between the value 137 // and the comment (e.g. 'search foo #bar' is OK, but 'search foo#bar' 138 // isn't). 139 var parsedValues []string 140 rawValues := fields[1:] // skip the stanza itself 141 for _, value := range rawValues { 142 if isComment(value) { 143 // We're done parsing as the rest of the line is still part of the 144 // same comment. 145 break 146 } 147 148 if strings.ContainsAny(value, commentChars) { 149 // This will catch cases like 'nameserver 8.8.8.8#foo', because 150 // fields[1] will be '8.8.8.8#foo'. 151 return nil, errors.Errorf("%q: invalid value %q", stanza, value) 152 } 153 154 parsedValues = append(parsedValues, value) 155 } 156 157 // resolv.conf(5) states that to be recognized as valid, the line must begin 158 // with the stanza, followed by whitespace, then at least one value (for 159 // 'nameserver', more values separated by whitespace are allowed for 160 // 'search'). 161 if len(parsedValues) == 0 { 162 return nil, errors.Errorf("%q: required value(s) missing", stanza) 163 } 164 165 return parsedValues, nil 166 } 167 168 var netListen = net.Listen 169 170 // SupportsIPv6 reports whether the platform supports IPv6 networking 171 // functionality. 172 // 173 // Source: https://github.com/golang/net/blob/master/internal/nettest/stack.go 174 func SupportsIPv6() bool { 175 ln, err := netListen("tcp6", "[::1]:0") 176 if err != nil { 177 return false 178 } 179 ln.Close() 180 return true 181 } 182 183 // SysClassNetRoot is the full Linux SYSFS path containing information about 184 // each network interface on the system. Used as argument to 185 // ParseInterfaceType(). 186 const SysClassNetPath = "/sys/class/net" 187 188 // ParseInterfaceType parses the DEVTYPE attribute from the Linux kernel 189 // userspace SYSFS location "<sysPath/<interfaceName>/uevent" and returns it as 190 // InterfaceType. SysClassNetPath should be passed as sysPath. Returns 191 // UnknownInterface if the type cannot be reliably determined for any reason. 192 // 193 // Example call: network.ParseInterfaceType(network.SysClassNetPath, "br-eth1") 194 func ParseInterfaceType(sysPath, interfaceName string) InterfaceType { 195 const deviceType = "DEVTYPE=" 196 location := filepath.Join(sysPath, interfaceName, "uevent") 197 198 data, err := ioutil.ReadFile(location) 199 if err != nil { 200 logger.Debugf("ignoring error reading %q: %v", location, err) 201 return UnknownInterface 202 } 203 204 devtype := "" 205 lines := strings.Fields(string(data)) 206 for _, line := range lines { 207 if !strings.HasPrefix(line, deviceType) { 208 continue 209 } 210 211 devtype = strings.TrimPrefix(line, deviceType) 212 switch devtype { 213 case "bridge": 214 return BridgeInterface 215 case "vlan": 216 return VLAN_8021QInterface 217 case "bond": 218 return BondInterface 219 case "": 220 // DEVTYPE is not present for some types, like Ethernet and loopback 221 // interfaces, so if missing do not try to guess. 222 break 223 } 224 } 225 226 return UnknownInterface 227 } 228 229 // GetBridgePorts extracts and returns the names of all interfaces configured as 230 // ports of the given bridgeName from the Linux kernel userspace SYSFS location 231 // "<sysPath/<bridgeName>/brif/*". SysClassNetPath should be passed as sysPath. 232 // Returns an empty result if the ports cannot be determined reliably for any 233 // reason, or if there are no configured ports for the bridge. 234 // 235 // Example call: network.GetBridgePorts(network.SysClassNetPath, "br-eth1") 236 func GetBridgePorts(sysPath, bridgeName string) []string { 237 portsGlobPath := filepath.Join(sysPath, bridgeName, "brif", "*") 238 // Glob ignores I/O errors and can only return ErrBadPattern, which we treat 239 // as no results, but for debugging we're still logging the error. 240 paths, err := filepath.Glob(portsGlobPath) 241 if err != nil { 242 logger.Debugf("ignoring error traversing path %q: %v", portsGlobPath, err) 243 } 244 245 if len(paths) == 0 { 246 return nil 247 } 248 249 // We need to convert full paths like /sys/class/net/br-eth0/brif/eth0 to 250 // just names. 251 names := make([]string, len(paths)) 252 for i := range paths { 253 names[i] = filepath.Base(paths[i]) 254 } 255 return names 256 }