1010- rfc951 - BOOTSTRAP PROTOCOL (BOOTP)
1111- rfc1542 - Clarifications and Extensions for the Bootstrap Protocol
1212- rfc1533 - DHCP Options and BOOTP Vendor Extensions
13+ - rfc3396 - Encoding Long Options in DHCPv4
14+
15+ You can disable concatenation on DHCP options holding the same code
16+ while decoding DHCP packets with::
17+
18+ >>> conf.contribs["dhcp"]["rfc3396"] = False
19+
20+ (Defaults to True)
1321"""
1422
1523try :
6977)
7078
7179dhcpmagic = b"c\x82 Sc"
80+ if "dhcp" not in conf .contribs :
81+ conf .contribs ["dhcp" ] = {}
82+ conf .contribs ["dhcp" ]["rfc3396" ] = True
7283
7384
7485class _BOOTP_chaddr (StrFixedLenField ):
@@ -436,60 +447,145 @@ def i2repr(self, pkt, x):
436447 s .append (sane (v ))
437448 return "[%s]" % (" " .join (s ))
438449
439- def getfield (self , pkt , s ):
440- return b"" , self .m2i (pkt , s )
441-
442- def m2i (self , pkt , x ):
443- opt = []
450+ def _extract_raw_entries (self , x ):
451+ """
452+ Extract raw TLV entries from the options buffer without interpreting them.
453+ Returns a list where each entry is either:
454+ - A string ('pad'/'end').
455+ - A tuple (code, raw_bytes).
456+ - Raw bytes for malformed trailing data.
457+ """
458+ entries = []
444459 while x :
445460 o = orb (x [0 ])
446461 if o == 255 :
447- opt .append ("end" )
462+ entries .append ("end" )
448463 x = x [1 :]
449464 continue
450465 if o == 0 :
451- opt .append ("pad" )
466+ entries .append ("pad" )
452467 x = x [1 :]
453468 continue
454469 if len (x ) < 2 or len (x ) < orb (x [1 ]) + 2 :
455- opt .append (x )
470+ entries .append (x )
456471 break
457- elif o in DHCPOptions :
458- f = DHCPOptions [o ]
472+ olen = orb (x [1 ])
473+ entries .append ((o , x [2 :olen + 2 ]))
474+ x = x [olen + 2 :]
475+ return entries
459476
460- if isinstance (f , str ):
461- olen = orb (x [1 ])
462- opt .append ((f , x [2 :olen + 2 ]))
463- x = x [olen + 2 :]
477+ def _merge_entries (self , entries ):
478+ """
479+ RFC 3396: merge all entries sharing the same option code.
480+ Preserves order of first appearance.
481+ Pads, ends and malformed entries are kept in place.
482+ """
483+ merged = {}
484+ order = []
485+ for entry in entries :
486+ if isinstance (entry , tuple ) and len (entry ) == 2 :
487+ code , value = entry
488+ if code in merged :
489+ merged [code ] += value
464490 else :
465- olen = orb (x [1 ])
466- lval = [f .name ]
491+ merged [code ] = bytearray (value )
492+ order .append (('option' , code ))
493+ else :
494+ order .append (('special' , entry ))
495+ result = []
496+ for kind , data in order :
497+ if kind == 'special' :
498+ result .append (data )
499+ else :
500+ result .append ((data , bytes (merged [data ])))
501+ return result
467502
468- if olen == 0 :
503+ def _entries_to_raw (self , entries ):
504+ """
505+ Reconstruct raw bytes from a list of extracted entries.
506+ """
507+ s = b""
508+ for entry in entries :
509+ if isinstance (entry , tuple ) and len (entry ) == 2 :
510+ code , value = entry
511+ s += struct .pack ("!BB" , code , len (value )) + value
512+ elif entry == "end" :
513+ s += b'\xff '
514+ elif entry == "pad" :
515+ s += b'\x00 '
516+ elif isinstance (entry , bytes ):
517+ s += entry
518+ return s
519+
520+ def getfield (self , pkt , s ):
521+ return b"" , self .m2i (pkt , s )
522+
523+ def m2i (self , pkt , x ):
524+ rfc3396 = conf .contribs .get ("dhcp" , {}).get ("rfc3396" , True )
525+
526+ entries = self ._extract_raw_entries (x )
527+
528+ if rfc3396 :
529+ # RFC 3396 section 5: check for option overload
530+ overload = 0
531+ for entry in entries :
532+ if (isinstance (entry , tuple ) and len (entry ) == 2
533+ and entry [0 ] == 52 and len (entry [1 ]) == 1 ):
534+ overload = orb (entry [1 ][0 ])
535+ break
536+
537+ # Build aggregate entries from file/sname if needed
538+ if (overload
539+ and pkt .underlayer is not None
540+ and isinstance (pkt .underlayer , BOOTP )):
541+ if overload in (1 , 3 ):
542+ entries += self ._extract_raw_entries (
543+ pkt .underlayer .file
544+ )
545+ if overload in (2 , 3 ):
546+ entries += self ._extract_raw_entries (
547+ pkt .underlayer .sname
548+ )
549+
550+ # Merge all entries with same code
551+ entries = self ._merge_entries (entries )
552+
553+ # Interpret entries
554+ opt = []
555+ for i , entry in enumerate (entries ):
556+ if isinstance (entry , tuple ) and len (entry ) == 2 :
557+ code , raw_value = entry
558+ if code in DHCPOptions :
559+ f = DHCPOptions [code ]
560+ if isinstance (f , str ):
561+ opt .append ((f , raw_value ))
562+ continue
563+ lval = [f .name ]
564+ if len (raw_value ) == 0 :
469565 try :
470566 _ , val = f .getfield (pkt , b'' )
471567 except Exception :
472- opt .append (x )
568+ opt .append (
569+ self ._entries_to_raw (entries [i :])
570+ )
473571 break
474572 else :
475573 lval .append (val )
476-
477574 try :
478- left = x [ 2 : olen + 2 ]
575+ left = raw_value
479576 while left :
480577 left , val = f .getfield (pkt , left )
481578 lval .append (val )
482579 except Exception :
483- opt .append (x )
580+ opt .append (
581+ self ._entries_to_raw (entries [i :])
582+ )
484583 break
485- else :
486- otuple = tuple (lval )
487- opt .append (otuple )
488- x = x [olen + 2 :]
584+ opt .append (tuple (lval ))
585+ else :
586+ opt .append ((code , raw_value ))
489587 else :
490- olen = orb (x [1 ])
491- opt .append ((o , x [2 :olen + 2 ]))
492- x = x [olen + 2 :]
588+ opt .append (entry )
493589 return opt
494590
495591 def i2m (self , pkt , x ):
@@ -514,8 +610,15 @@ def i2m(self, pkt, x):
514610 warning ("Unknown field option %s" , name )
515611 continue
516612
517- s += struct .pack ("!BB" , onum , len (oval ))
518- s += oval
613+ # RFC 3396: split options longer than 255 bytes
614+ if not oval :
615+ s += struct .pack ("!BB" , onum , 0 )
616+ else :
617+ while oval :
618+ chunk = oval [:255 ]
619+ oval = oval [255 :]
620+ s += struct .pack ("!BB" , onum , len (chunk ))
621+ s += chunk
519622
520623 elif (isinstance (o , str ) and o in DHCPRevOptions and
521624 DHCPRevOptions [o ][1 ] is None ):
0 commit comments