github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/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 "log" 15 "net" 16 "net/url" 17 "os" 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 os.WriteFile("/etc/resolv.conf", rc.Bytes(), 0o644) 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 // V6ClientPort is the port that is used to send and receive DHCPv6 154 // messages. 155 // 156 // If not set, it will default to dhcpv6's default (546). 157 V6ClientPort *int 158 159 // V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP 160 // messages. 161 // 162 // If not set, it will default to nclient4's default (DHCP broadcast 163 // address). 164 V4ServerAddr *net.UDPAddr 165 166 // If true, add Client Identifier (61) option to the IPv4 request. 167 V4ClientIdentifier bool 168 } 169 170 func lease4(ctx context.Context, iface netlink.Link, c Config) (Lease, error) { 171 mods := []nclient4.ClientOpt{ 172 nclient4.WithTimeout(c.Timeout), 173 nclient4.WithRetry(c.Retries), 174 } 175 switch c.LogLevel { 176 case LogSummary: 177 mods = append(mods, nclient4.WithSummaryLogger()) 178 case LogDebug: 179 mods = append(mods, nclient4.WithDebugLogger()) 180 } 181 if c.V4ServerAddr != nil { 182 mods = append(mods, nclient4.WithServerAddr(c.V4ServerAddr)) 183 } 184 client, err := nclient4.New(iface.Attrs().Name, mods...) 185 if err != nil { 186 return nil, err 187 } 188 defer client.Close() 189 190 // Prepend modifiers with default options, so they can be overriden. 191 reqmods := append( 192 []dhcpv4.Modifier{ 193 dhcpv4.WithOption(dhcpv4.OptClassIdentifier("PXE UROOT")), 194 dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask), 195 dhcpv4.WithNetboot, 196 }, 197 c.Modifiers4...) 198 199 if c.V4ClientIdentifier { 200 // Client Id is hardware type + mac per RFC 2132 9.14. 201 ident := []byte{0x01} // Type ethernet 202 ident = append(ident, iface.Attrs().HardwareAddr...) 203 reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptClientIdentifier(ident))) 204 } 205 206 log.Printf("Attempting to get DHCPv4 lease on %s", iface.Attrs().Name) 207 lease, err := client.Request(ctx, reqmods...) 208 if err != nil { 209 return nil, err 210 } 211 212 packet := NewPacket4(iface, lease.ACK) 213 log.Printf("Got DHCPv4 lease on %s: %v", iface.Attrs().Name, lease.ACK.Summary()) 214 return packet, nil 215 } 216 217 func lease6(ctx context.Context, iface netlink.Link, c Config, linkUpTimeout time.Duration) (Lease, error) { 218 clientPort := dhcpv6.DefaultClientPort 219 if c.V6ClientPort != nil { 220 clientPort = *c.V6ClientPort 221 } 222 223 // For ipv6, we cannot bind to the port until Duplicate Address 224 // Detection (DAD) is complete which is indicated by the link being no 225 // longer marked as "tentative". This usually takes about a second. 226 227 // If the link is never going to be ready, don't wait forever. 228 // (The user may not have configured a ctx with a timeout.) 229 // 230 // Hardcode the timeout to 30s for now. 231 linkTimeout := time.After(linkUpTimeout) 232 for { 233 if ready, err := isIpv6LinkReady(iface); err != nil { 234 return nil, err 235 } else if ready { 236 break 237 } 238 select { 239 case <-time.After(100 * time.Millisecond): 240 continue 241 case <-linkTimeout: 242 return nil, errors.New("timeout after waiting for a non-tentative IPv6 address") 243 case <-ctx.Done(): 244 return nil, errors.New("timeout after waiting for a non-tentative IPv6 address") 245 } 246 } 247 248 mods := []nclient6.ClientOpt{ 249 nclient6.WithTimeout(c.Timeout), 250 nclient6.WithRetry(c.Retries), 251 } 252 switch c.LogLevel { 253 case LogSummary: 254 mods = append(mods, nclient6.WithSummaryLogger()) 255 case LogDebug: 256 mods = append(mods, nclient6.WithDebugLogger()) 257 } 258 if c.V6ServerAddr != nil { 259 mods = append(mods, nclient6.WithBroadcastAddr(c.V6ServerAddr)) 260 } 261 conn, err := nclient6.NewIPv6UDPConn(iface.Attrs().Name, clientPort) 262 if err != nil { 263 return nil, err 264 } 265 i, err := net.InterfaceByName(iface.Attrs().Name) 266 if err != nil { 267 return nil, err 268 } 269 client, err := nclient6.NewWithConn(conn, i.HardwareAddr, mods...) 270 if err != nil { 271 return nil, err 272 } 273 defer client.Close() 274 275 // Prepend modifiers with default options, so they can be overriden. 276 reqmods := append( 277 []dhcpv6.Modifier{ 278 dhcpv6.WithNetboot, 279 }, 280 c.Modifiers6...) 281 282 log.Printf("Attempting to get DHCPv6 lease on %s", iface.Attrs().Name) 283 p, err := client.RapidSolicit(ctx, reqmods...) 284 if err != nil { 285 return nil, err 286 } 287 288 packet := NewPacket6(iface, p) 289 log.Printf("Got DHCPv6 lease on %s: %v", iface.Attrs().Name, p.Summary()) 290 return packet, nil 291 } 292 293 // NetworkProtocol is either IPv4 or IPv6. 294 type NetworkProtocol int 295 296 // Possible network protocols; either IPv4, IPv6, or both. 297 const ( 298 NetIPv4 NetworkProtocol = 1 299 NetIPv6 NetworkProtocol = 2 300 NetBoth NetworkProtocol = 3 301 ) 302 303 func (n NetworkProtocol) String() string { 304 switch n { 305 case NetIPv4: 306 return "IPv4" 307 case NetIPv6: 308 return "IPv6" 309 case NetBoth: 310 return "IPv4+IPv6" 311 } 312 return fmt.Sprintf("unknown network protocol (%#x)", n) 313 } 314 315 // Result is the result of a particular DHCP attempt. 316 type Result struct { 317 // Protocol is the IP protocol that we tried to configure. 318 Protocol NetworkProtocol 319 320 // Interface is the network interface the attempt was sent on. 321 Interface netlink.Link 322 323 // Lease is the DHCP configuration returned. 324 // 325 // If Lease is set, Err is nil. 326 Lease Lease 327 328 // Err is an error that occured during the DHCP attempt. 329 Err error 330 } 331 332 // SendRequests coordinates soliciting DHCP configuration on all ifs. 333 // 334 // ipv4 and ipv6 determine whether to send DHCPv4 and DHCPv6 requests, 335 // respectively. 336 // 337 // The *Result channel will be closed when all requests have completed. 338 func SendRequests(ctx context.Context, ifs []netlink.Link, ipv4, ipv6 bool, c Config, linkUpTimeout time.Duration) chan *Result { 339 // Yeah, this is a hack, until we can cancel all leases in progress. 340 r := make(chan *Result, 3*len(ifs)) 341 342 var wg sync.WaitGroup 343 for _, iface := range ifs { 344 wg.Add(1) 345 go func(iface netlink.Link) { 346 defer wg.Done() 347 348 log.Printf("Bringing up interface %s...", iface.Attrs().Name) 349 if _, err := IfUp(iface.Attrs().Name, linkUpTimeout); err != nil { 350 log.Printf("Could not bring up interface %s: %v", iface.Attrs().Name, err) 351 return 352 } 353 354 if ipv4 { 355 wg.Add(1) 356 go func(iface netlink.Link) { 357 defer wg.Done() 358 lease, err := lease4(ctx, iface, c) 359 r <- &Result{NetIPv4, iface, lease, err} 360 }(iface) 361 } 362 363 if ipv6 { 364 wg.Add(1) 365 go func(iface netlink.Link) { 366 defer wg.Done() 367 lease, err := lease6(ctx, iface, c, linkUpTimeout) 368 r <- &Result{NetIPv6, iface, lease, err} 369 }(iface) 370 } 371 }(iface) 372 } 373 374 go func() { 375 wg.Wait() 376 close(r) 377 }() 378 return r 379 }