code: def protocol(self):
          
          if self.address_family == socket.AF_INET:
              proto = i(self.raw[9])
              start = (i(self.raw[0]) & 0b1111) * 4
          elif self.address_family == socket.AF_INET6:
              proto = i(self.raw[6])
  
              # skip over well-known ipv6 headers
              start = 40
              while proto in IPV6_EXT_HEADERS:
                  if start >= len(self.raw):
                      # less than two bytes left
                      start = None
                      proto = None
                      break
                  if proto == Protocol.FRAGMENT:
                      hdrlen = 8
                  elif proto == Protocol.AH:
                      hdrlen = (i(self.raw[start + 1]) + 2) * 4
                  else:
                      # Protocol.HOPOPT, Protocol.DSTOPTS, Protocol.ROUTING
                      hdrlen = (i(self.raw[start + 1]) + 1) * 8
                  proto = i(self.raw[start])
                  start += hdrlen
          else:
              start = None
              proto = None
  
          out_of_bounds = (
              (proto == Protocol.TCP and start + 20 > len(self.raw)) or
              (proto == Protocol.UDP and start + 8 > len(self.raw)) or
              (proto in {Protocol.ICMP, Protocol.ICMPV6} and start + 4 > len(self.raw))
          )
          if out_of_bounds:
              # special-case tcp/udp so that we can rely on .protocol for the port properties.
              start = None
              proto = None
  
          return proto, start
docs: - | A (ipproto, proto_start) tuple.
            | ``ipproto`` is the IP protocol in use, e.g. Protocol.TCP or Protocol.UDP.
            | ``proto_start`` denotes the beginning of the protocol data.
            | If the packet does not match our expectations, both ipproto and proto_start are None.