<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sakhi Hussain</title>
    <description>The latest articles on DEV Community by Sakhi Hussain (@sakhi383).</description>
    <link>https://dev.to/sakhi383</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4242621%2F079f698d-e341-486a-844b-36bc3dc4b01a.png</url>
      <title>DEV Community: Sakhi Hussain</title>
      <link>https://dev.to/sakhi383</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sakhi383"/>
    <language>en</language>
    <item>
      <title>How I Fixed an RFC 2385 TCP-MD5 Protocol Bug in Scapy: An Open-Source Deep Dive</title>
      <dc:creator>Sakhi Hussain</dc:creator>
      <pubDate>Fri, 13 Nov 2026 15:20:21 +0000</pubDate>
      <link>https://dev.to/sakhi383/how-i-fixed-an-rfc-2385-tcp-md5-protocol-bug-in-scapy-an-open-source-deep-dive-2ejh</link>
      <guid>https://dev.to/sakhi383/how-i-fixed-an-rfc-2385-tcp-md5-protocol-bug-in-scapy-an-open-source-deep-dive-2ejh</guid>
      <description>&lt;p&gt;Scapy issue &lt;a href="https://github.com/secdev/scapy/issues/5229" rel="noopener noreferrer"&gt;#5229&lt;/a&gt; came down to one line: &lt;code&gt;tcp_bytes[12] &amp;gt;&amp;gt; 4&lt;/code&gt;. That line reads the TCP header length. Give it a UDP layer with no payload, though, and the byte it needs isn't there. The caller gets an &lt;code&gt;IndexError&lt;/code&gt; without a hint that they passed the wrong layer.&lt;/p&gt;

&lt;p&gt;I fixed that in &lt;a href="https://github.com/secdev/scapy/pull/5268" rel="noopener noreferrer"&gt;Scapy PR #5268&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the bug
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scapy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="no"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UDP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc_tcp_md5_hash&lt;/span&gt;

&lt;span class="n"&gt;packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="no"&gt;UDP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;calc_tcp_md5_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;UDP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;"example-key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;packet[UDP]&lt;/code&gt; part matters here. It pulls out the UDP layer, which serializes to just eight bytes in this example. The old code reached for byte index 12 to read the high four bits, where TCP stores its header length in 32-bit words. That's where the call failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I added two checks before that byte access. Here's the relevant part of the &lt;a href="https://github.com/secdev/scapy/pull/5268/files" rel="noopener noreferrer"&gt;merged diff&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;TCP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"calc_tcp_md5_hash requires a TCP layer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tcp_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tcp_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Truncated TCP header"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;doff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tcp_bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first check catches the wrong layer before serialization. If the caller does pass a TCP layer, the second check makes sure it serializes to at least 20 bytes, the size of a TCP header without options. Run the UDP example with the fix and the error tells you exactly what to pass: &lt;code&gt;TypeError: calc_tcp_md5_hash requires a TCP layer&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tested
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;test/scapy/layers/inet.uts&lt;/code&gt;, I added three calls that should raise &lt;code&gt;TypeError&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IP() / UDP()&lt;/code&gt;, which passes the whole IPv4 packet to the helper.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UDP()&lt;/code&gt;, which passes a standalone UDP layer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b"invalid_layer"&lt;/code&gt;, which passes a Python bytes object.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three exercise the type check. The test file already had an IPv4/TCP case that compares the generated MD5 option with an expected value. &lt;a href="https://github.com/secdev/scapy/pull/5268/files" rel="noopener noreferrer"&gt;Test diff&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it merged
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gpotter2&lt;/code&gt; approved and merged the fix into Scapy's &lt;code&gt;master&lt;/code&gt; branch on November 4, 2026. Another PR addressed the same issue, and &lt;code&gt;gpotter2&lt;/code&gt; noted there that the project had decided to go with #5268. &lt;a href="https://github.com/secdev/scapy/pull/5268" rel="noopener noreferrer"&gt;Merge record&lt;/a&gt; · &lt;a href="https://github.com/secdev/scapy/pull/5236" rel="noopener noreferrer"&gt;Related PR #5236&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>networking</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
