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