From ce79c4cf111e35546d041f632530d9b0a414ae6f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Fri, 10 Nov 2023 10:30:27 -0500 Subject: [PATCH] Draft example of an alternative of setting filename in initial pass In at least one ARM UEFI, the PXE support is non-compliant, and absolutely requires the filename field to be populated. That said, the same firmware fails to let iPXE function, so we have other issues with this firmware. Very preliminary testing indicates that PXE compliant firmware ignores the filename in the offer and proceeds to proxyDHCP. The non-compliant firmware ignores the PXEClient guidance and just consumes the filename. In theory, this just means that the compliant firmware inflicts a bit of redundant effort on our part, as we must sort out the filename to offer twice (once for DHCP, then again for proxyDHCP). It may aid non compliant firmware to proceed in more limited circumstances. --- .../confluent/discovery/protocols/pxe.py | 70 +++++++++++-------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/confluent_server/confluent/discovery/protocols/pxe.py b/confluent_server/confluent/discovery/protocols/pxe.py index a9a07963..1705bdb7 100644 --- a/confluent_server/confluent/discovery/protocols/pxe.py +++ b/confluent_server/confluent/discovery/protocols/pxe.py @@ -273,6 +273,29 @@ def opts_to_dict(rq, optidx, expectype=1): def ipfromint(numb): return socket.inet_ntoa(struct.pack('I', numb)) +def get_pxe_bootfile(node, opts, disco, profile, cfg, myip): + if opts.get(77, None) == b'iPXE': + if not profile: + profile = get_deployment_profile(node, cfg) + if not profile: + log.log({'info': 'No pending profile for {0}, skipping proxyDHCP reply'.format(node)}) + return None + bootfile = 'http://{0}/confluent-public/os/{1}/boot.ipxe'.format(myip, profile).encode('utf8') + elif disco['arch'] == 'uefi-x64': + bootfile = b'confluent/x86_64/ipxe.efi' + elif disco['arch'] == 'bios-x86': + bootfile = b'confluent/x86_64/ipxe.kkpxe' + elif disco['arch'] == 'uefi-aarch64': + bootfile = b'confluent/aarch64/ipxe.efi' + if len(bootfile) > 127: + log.log( + {'info': 'Boot offer cannot be made to {0} as the ' + 'profile name "{1}" is {2} characters longer than is supported ' + 'for this boot method.'.format( + node, profile, len(bootfile) - 127)}) + return None + return bootfile + def proxydhcp(handler, nodeguess): net4011 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) net4011.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -346,6 +369,7 @@ def proxydhcp(handler, nodeguess): profile = None if not myipn: myipn = socket.inet_aton(recv) + myip = socket.inet_ntoa(myipn) profile = get_deployment_profile(node, cfg) if profile: log.log({ @@ -354,26 +378,9 @@ def proxydhcp(handler, nodeguess): if not skiplogging: log.log({'info': 'No pending profile for {0}, skipping proxyDHCP reply'.format(node)}) continue - if opts.get(77, None) == b'iPXE': - if not profile: - profile = get_deployment_profile(node, cfg) - if not profile: - log.log({'info': 'No pending profile for {0}, skipping proxyDHCP reply'.format(node)}) - continue - myip = socket.inet_ntoa(myipn) - bootfile = 'http://{0}/confluent-public/os/{1}/boot.ipxe'.format(myip, profile).encode('utf8') - elif disco['arch'] == 'uefi-x64': - bootfile = b'confluent/x86_64/ipxe.efi' - elif disco['arch'] == 'bios-x86': - bootfile = b'confluent/x86_64/ipxe.kkpxe' - elif disco['arch'] == 'uefi-aarch64': - bootfile = b'confluent/aarch64/ipxe.efi' - if len(bootfile) > 127: - log.log( - {'info': 'Boot offer cannot be made to {0} as the ' - 'profile name "{1}" is {2} characters longer than is supported ' - 'for this boot method.'.format( - node, profile, len(bootfile) - 127)}) + myip = socket.inet_ntoa(myipn) + bootfile = get_pxe_bootfile(node, opts, disco, profile, cfg, myip) + if not bootfile: continue rpv[:240] = rqv[:240].tobytes() rpv[0:1] = b'\x02' @@ -398,6 +405,7 @@ def snoop(handler, protocol=None, nodeguess=None): #prominent #TODO(jjohnson2): enable unicast replies. This would suggest either # injection into the neigh table before OFFER or using SOCK_RAW. + global tracelog start_proxydhcp(handler, nodeguess) tracelog = log.Logger('trace') global attribwatcher @@ -499,7 +507,7 @@ def process_dhcp6req(handler, rqv, addr, net, cfg, nodeguess): if ignoredisco.get(mac, 0) + 90 < time.time(): ignoredisco[mac] = time.time() handler(info) - consider_discover(info, req, net, cfg, None, nodeguess, addr) + consider_discover(info, req, net, cfg, None, nodeguess, disco, addr) def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv): rq = bytearray(rqv) @@ -539,7 +547,7 @@ def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv): and time.time() > ignoredisco.get(netaddr, 0) + 90): ignoredisco[netaddr] = time.time() handler(info) - consider_discover(info, rqinfo, net4, cfg, rqv, nodeguess) + consider_discover(info, rqinfo, net4, cfg, rqv, nodeguess, disco) @@ -594,7 +602,7 @@ def get_deployment_profile(node, cfg, cfd=None): staticassigns = {} myipbypeer = {} -def check_reply(node, info, packet, sock, cfg, reqview, addr): +def check_reply(node, info, packet, sock, cfg, reqview, addr, disco): httpboot = info['architecture'] == 'uefi-httpboot' cfd = cfg.get_node_attributes(node, ('deployment.*', 'collective.managercandidates')) profile = get_deployment_profile(node, cfg, cfd) @@ -611,7 +619,7 @@ def check_reply(node, info, packet, sock, cfg, reqview, addr): return return reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock) else: - return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile) + return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, disco) def reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock): myaddrs = netutil.get_my_addresses(addr[-1], socket.AF_INET6) @@ -695,7 +703,7 @@ def get_my_duid(): return _myuuid -def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): +def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, disco): replen = 275 # default is going to be 286 # while myipn is describing presumed destination, it's really # vague in the face of aliases, need to convert to ifidx and evaluate @@ -733,7 +741,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): log.log({'error': nicerr}) if niccfg.get('ipv4_broken', False): # Received a request over a nic with no ipv4 configured, ignore it - log.log({'error': 'Skipping boot reply to {0} due to no viable IPv4 configuration on deployment system'.format(node)}) + log.log({'error': 'Skipping boot reply to {0} due to no viable IPv4 configuration on deployment system, over nic "{}"'.format(node, info['netinfo']['ifidx'])}) return clipn = None if niccfg['ipv4_method'] == 'firmwarenone': @@ -786,6 +794,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): repview[249:255] = b'\x33\x04\x00\x00\x00\xf0' # fixed short lease time repview[255:257] = b'\x61\x11' repview[257:274] = packet[97] + # Note that sending PXEClient kicks off the proxyDHCP procedure, ignoring # boot filename and such in the DHCP packet # we will simply always do it to provide the boot payload in a consistent @@ -794,6 +803,9 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile): repview[replen - 1:replen + 11] = b'\x3c\x0aHTTPClient' replen += 12 else: + bootfile = get_pxe_bootfile(node, packet, disco, profile, cfg, myip) + if bootfile: + repview[108:108 + len(bootfile)] = bootfile repview[replen - 1:replen + 10] = b'\x3c\x09PXEClient' replen += 11 hwlen = bytearray(reqview[2:3].tobytes())[0] @@ -885,11 +897,11 @@ def ack_request(pkt, rq, info): repview[26:28] = struct.pack('!H', datasum) send_raw_packet(repview, len(rply), rq, info) -def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None): +def consider_discover(info, packet, sock, cfg, reqview, nodeguess, disco, addr=None): if info.get('hwaddr', None) in macmap and info.get('uuid', None): - check_reply(macmap[info['hwaddr']], info, packet, sock, cfg, reqview, addr) + check_reply(macmap[info['hwaddr']], info, packet, sock, cfg, reqview, addr, disco) elif info.get('uuid', None) in uuidmap: - check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr) + check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr, disco) elif packet.get(53, None) == b'\x03': ack_request(packet, reqview, info) elif info.get('uuid', None) and info.get('hwaddr', None):