github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/pkg/dhclient/dhclient.go (about) 1 // Copyright 2017-2019 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package dhclient allows for getting both DHCPv4 and DHCPv6 leases on 6 // multiple network interfaces in parallel. 7 package dhclient 8 9 import ( 10 "bytes" 11 "context" 12 "errors" 13 "fmt" 14 "io/ioutil" 15 "log" 16 "net" 17 "net/url" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/insomniacslk/dhcp/dhcpv4" 23 "github.com/insomniacslk/dhcp/dhcpv4/nclient4" 24 "github.com/insomniacslk/dhcp/dhcpv6" 25 "github.com/insomniacslk/dhcp/dhcpv6/nclient6" 26 "github.com/vishvananda/netlink" 27 "golang.org/x/sys/unix" 28 ) 29 30 const linkUpAttempt = 30 * time.Second 31 32 // isIpv6LinkReady returns true if the interface has a link-local address 33 // which is not tentative. 34 func isIpv6LinkReady(l netlink.Link) (bool, error) { 35 addrs, err := netlink.AddrList(l, netlink.FAMILY_V6) 36 if err != nil { 37 return false, err 38 } 39 for _, addr := range addrs { 40 if addr.IP.IsLinkLocalUnicast() && (addr.Flags&unix.IFA_F_TENTATIVE == 0) { 41 if addr.Flags&unix.IFA_F_DADFAILED != 0 { 42 log.Printf("DADFAILED for %v, continuing anyhow", addr.IP) 43 } 44 return true, nil 45 } 46 } 47 return false, nil 48 } 49 50 // IfUp ensures the given network interface is up and returns the link object. 51 func IfUp(ifname string) (netlink.Link, error) { 52 start := time.Now() 53 for time.Since(start) < linkUpAttempt { 54 // Note that it may seem odd to keep trying the LinkByName 55 // operation, but consider that a hotplug device such as USB 56 // ethernet can just vanish. 57 iface, err := netlink.LinkByName(ifname) 58 if err != nil { 59 return nil, fmt.Errorf("cannot get interface %q by name: %v", ifname, err) 60 } 61 62 if iface.Attrs().Flags&net.FlagUp == net.FlagUp { 63 return iface, nil 64 } 65 66 if err := netlink.LinkSetUp(iface); err != nil { 67 return nil, fmt.Errorf("interface %q: %v can't make it up: %v", ifname, iface, err) 68 } 69 time.Sleep(100 * time.Millisecond) 70 } 71 72 return nil, fmt.Errorf("link %q still down after %d seconds", ifname, linkUpAttempt) 73 } 74 75 // WriteDNSSettings writes the given nameservers, search list, and domain to resolv.conf. 76 func WriteDNSSettings(ns []net.IP, sl []string, domain string) error { 77 rc := &bytes.Buffer{} 78 if domain != "" { 79 rc.WriteString(fmt.Sprintf("domain %s\n", domain)) 80 } 81 for _, ip := range ns { 82 rc.WriteString(fmt.Sprintf("nameserver %s\n", ip)) 83 } 84 if sl != nil { 85 rc.WriteString("search ") 86 rc.WriteString(strings.Join(sl, " ")) 87 rc.WriteString("\n") 88 } 89 return ioutil.WriteFile("/etc/resolv.conf", rc.Bytes(), 0644) 90 } 91 92 // Lease is a network configuration obtained by DHCP. 93 type Lease interface { 94 fmt.Stringer 95 96 // Configure configures the associated interface with the network 97 // configuration. 98 Configure() error 99 100 // Boot is a URL to obtain booting information from that was part of 101 // the network config. 102 Boot() (*url.URL, error) 103 104 // ISCSIBoot returns the target address and volume name to boot from if 105 // they were part of the DHCP message. 106 ISCSIBoot() (*net.TCPAddr, string, error) 107 108 // Link is the interface the configuration is for. 109 Link() netlink.Link 110 111 // Return the full DHCP response, this is either a *dhcpv4.DHCPv4 or a 112 // *dhcpv6.Message. 113 Message() (*dhcpv4.DHCPv4, *dhcpv6.Message) 114 } 115 116 // LogLevel is the amount of information to log. 117 type LogLevel uint8 118 119 // LogLevel are the levels. 120 const ( 121 LogInfo LogLevel = 0 122 LogSummary LogLevel = 1 123 LogDebug LogLevel = 2 124 ) 125 126 // Config is a DHCP client configuration. 127 type Config struct { 128 // Timeout is the timeout for one DHCP request attempt. 129 Timeout time.Duration 130 131 // Retries is how many times to retry DHCP attempts. 132 Retries int 133 134 // LogLevel determines the amount of information printed for each 135 // attempt. The highest log level should print each entire packet sent 136 // and received. 137 LogLevel LogLevel 138 139 // Modifiers4 allows modifications to the IPv4 DHCP request. 140 Modifiers4 []dhcpv4.Modifier 141 142 // Modifiers6 allows modifications to the IPv6 DHCP request. 143 Modifiers6 []dhcpv6.Modifier 144 145 // V6ServerAddr can be a unicast or broadcast destination for DHCPv6 146 // messages. 147 // 148 // If not set, it will default to nclient6's default (all servers & 149 // relay agents). 150 V6ServerAddr *net.UDPAddr 151 152 // V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP 153 // messages. 154 // 155 // If not set, it will default to nclient4's default (DHCP broadcast 156 // address). 157 V4ServerAddr *net.UDPAddr 158 } 159 160 func lease4(ctx context.Context, iface netlink.Link, c Config) (Lease, error) { 161 mods := []nclient4.ClientOpt{ 162 nclient4.WithTimeout(c.Timeout), 163 nclient4.WithRetry(c.Retries), 164 } 165 switch c.LogLevel { 166 case LogSummary: 167 mods = append(mods, nclient4.WithSummaryLogger()) 168 case LogDebug: 169 mods = append(mods, nclient4.WithDebugLogger()) 170 } 171 if c.V4ServerAddr != nil { 172 mods = append(mods, nclient4.WithServerAddr(c.V4ServerAddr)) 173 } 174 client, err := nclient4.New(iface.Attrs().Name, mods...) 175 if err != nil { 176 return nil, err 177 } 178 179 // Prepend modifiers with default options, so they can be overriden. 180 reqmods := append( 181 []dhcpv4.Modifier{ 182 dhcpv4.WithOption(dhcpv4.OptClassIdentifier("PXE UROOT")), 183 dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask), 184 dhcpv4.WithNetboot, 185 }, 186 c.Modifiers4...) 187 188 log.Printf("Attempting to get DHCPv4 lease on %s", iface.Attrs().Name) 189 _, p, err := client.Request(ctx, reqmods...) 190 if err != nil { 191 return nil, err 192 } 193 194 packet := NewPacket4(iface, p) 195 log.Printf("Got DHCPv4 lease on %s: %v", iface.Attrs().Name, p.Summary()) 196 return packet, nil 197 } 198 199 func lease6(ctx context.Context, iface netlink.Link, c Config) (Lease, error) { 200 // For ipv6, we cannot bind to the port until Duplicate Address 201 // Detection (DAD) is complete which is indicated by the link being no 202 // longer marked as "tentative". This usually takes about a second. 203 204 // If the link is never going to be ready, don't wait forever. 205 // (The user may not have configured a ctx with a timeout.) 206 // 207 // Hardcode the timeout to 30s for now. 208 linkTimeout := time.After(linkUpAttempt) 209 for { 210 if ready, err := isIpv6LinkReady(iface); err != nil { 211 return nil, err 212 } else if ready { 213 break 214 } 215 select { 216 case <-time.After(100 * time.Millisecond): 217 continue 218 case <-linkTimeout: 219 return nil, errors.New("timeout after waiting for a non-tentative IPv6 address") 220 case <-ctx.Done(): 221 return nil, errors.New("timeout after waiting for a non-tentative IPv6 address") 222 } 223 } 224 225 mods := []nclient6.ClientOpt{ 226 nclient6.WithTimeout(c.Timeout), 227 nclient6.WithRetry(c.Retries), 228 } 229 switch c.LogLevel { 230 case LogSummary: 231 mods = append(mods, nclient6.WithSummaryLogger()) 232 case LogDebug: 233 mods = append(mods, nclient6.WithDebugLogger()) 234 } 235 if c.V6ServerAddr != nil { 236 mods = append(mods, nclient6.WithBroadcastAddr(c.V6ServerAddr)) 237 } 238 client, err := nclient6.New(iface.Attrs().Name, mods...) 239 if err != nil { 240 return nil, err 241 } 242 243 // Prepend modifiers with default options, so they can be overriden. 244 reqmods := append( 245 []dhcpv6.Modifier{ 246 dhcpv6.WithNetboot, 247 }, 248 c.Modifiers6...) 249 250 log.Printf("Attempting to get DHCPv6 lease on %s", iface.Attrs().Name) 251 p, err := client.RapidSolicit(ctx, reqmods...) 252 if err != nil { 253 return nil, err 254 } 255 256 packet := NewPacket6(iface, p) 257 log.Printf("Got DHCPv6 lease on %s: %v", iface.Attrs().Name, p.Summary()) 258 return packet, nil 259 } 260 261 // NetworkProtocol is either IPv4 or IPv6. 262 type NetworkProtocol int 263 264 // Possible network protocols; either IPv4, IPv6, or both. 265 const ( 266 NetIPv4 NetworkProtocol = 1 267 NetIPv6 NetworkProtocol = 2 268 NetBoth NetworkProtocol = 3 269 ) 270 271 func (n NetworkProtocol) String() string { 272 switch n { 273 case NetIPv4: 274 return "IPv4" 275 case NetIPv6: 276 return "IPv6" 277 case NetBoth: 278 return "IPv4+IPv6" 279 } 280 return fmt.Sprintf("unknown network protocol (%#x)", n) 281 } 282 283 // Result is the result of a particular DHCP attempt. 284 type Result struct { 285 // Protocol is the IP protocol that we tried to configure. 286 Protocol NetworkProtocol 287 288 // Interface is the network interface the attempt was sent on. 289 Interface netlink.Link 290 291 // Lease is the DHCP configuration returned. 292 // 293 // If Lease is set, Err is nil. 294 Lease Lease 295 296 // Err is an error that occured during the DHCP attempt. 297 Err error 298 } 299 300 // SendRequests coordinates soliciting DHCP configuration on all ifs. 301 // 302 // ipv4 and ipv6 determine whether to send DHCPv4 and DHCPv6 requests, 303 // respectively. 304 // 305 // The *Result channel will be closed when all requests have completed. 306 func SendRequests(ctx context.Context, ifs []netlink.Link, ipv4, ipv6 bool, c Config) chan *Result { 307 // Yeah, this is a hack, until we can cancel all leases in progress. 308 r := make(chan *Result, 3*len(ifs)) 309 310 var wg sync.WaitGroup 311 for _, iface := range ifs { 312 wg.Add(1) 313 go func(iface netlink.Link) { 314 defer wg.Done() 315 316 log.Printf("Bringing up interface %s...", iface.Attrs().Name) 317 if _, err := IfUp(iface.Attrs().Name); err != nil { 318 log.Printf("Could not bring up interface %s: %v", iface.Attrs().Name, err) 319 return 320 } 321 322 if ipv4 { 323 wg.Add(1) 324 go func(iface netlink.Link) { 325 defer wg.Done() 326 lease, err := lease4(ctx, iface, c) 327 r <- &Result{NetIPv4, iface, lease, err} 328 }(iface) 329 } 330 331 if ipv6 { 332 wg.Add(1) 333 go func(iface netlink.Link) { 334 defer wg.Done() 335 lease, err := lease6(ctx, iface, c) 336 r <- &Result{NetIPv6, iface, lease, err} 337 }(iface) 338 } 339 }(iface) 340 } 341 342 go func() { 343 wg.Wait() 344 close(r) 345 }() 346 return r 347 }