"""Decode the Linux socket error queue (IP_RECVERR) for datagram scans.

When a UDP datagram to an unresponsive destination draws an ICMP error (port
unreachable, host/net unreachable, admin-prohibited, TTL exceeded) or fails
locally, the kernel -- with the IP_RECVERR socket option set -- queues a
sock_extended_err that recvmsg(MSG_ERRQUEUE) delivers as ancillary data. These
helpers classify that into a short status label and attribute it to a
destination IP, so any datagram-based scanner (SNMP, DNS, HTTP-over-UDP, ...)
can tell "no service / unreachable" from "still waiting". Nothing here is
protocol-specific. Linux-only.
"""
import errno
import socket
import struct

IP_RECVERR = getattr(socket, 'IP_RECVERR', 11)
MSG_ERRQUEUE = getattr(socket, 'MSG_ERRQUEUE', 0x2000)
SO_EE_ORIGIN_LOCAL = 1
SO_EE_ORIGIN_ICMP = 2
# ICMP type 3 (destination unreachable) codes -> status label.
_ICMP_UNREACH = {
    0: 'net-unreachable',
    1: 'host-unreachable',
    2: 'proto-unreachable',
    3: 'port-closed',  # ICMP_PORT_UNREACH: host up, no service on that port
    9: 'admin-prohibited',
    10: 'admin-prohibited',
    13: 'admin-prohibited',  # ICMP_PKT_FILTERED: firewall reject
}
# Any of these means the destination won't answer -- stop probing it. A local
# send error with an unmapped errno (classify_icmp -> 'local-error') is left
# OUT deliberately: it may be transient (e.g. ENOBUFS under buffer pressure),
# and treating it as terminal would cut a scan's retries short. Local errors we
# can attribute (EHOSTUNREACH / ENETUNREACH) map to the reachability statuses
# above and stay terminal.
CLOSED_STATUSES = frozenset(_ICMP_UNREACH.values())


def classify_icmp(ee_origin, ee_type, ee_code, ee_errno):
    """Map a sock_extended_err into a short status label."""
    if ee_origin == SO_EE_ORIGIN_ICMP:
        if ee_type == 3:  # ICMP destination unreachable
            return _ICMP_UNREACH.get(ee_code, 'unreachable')
        if ee_type == 11:  # time exceeded
            return 'ttl-exceeded'
        return 'icmp-type-%d' % ee_type
    if ee_origin == SO_EE_ORIGIN_LOCAL:
        return {
            errno.EHOSTUNREACH: 'host-unreachable',
            errno.ENETUNREACH: 'net-unreachable'
        }.get(ee_errno, 'local-error')
    return 'error'


def parse_icmp_error(ancdata, address):
    """Parse an error-queue recvmsg result into (target_ip, status).

    ``address`` is the recvmsg msg_name (the destination of the datagram that
    triggered the error) and is the preferred way to attribute the error; the
    SO_EE_OFFENDER address embedded after the sock_extended_err is a fallback.
    Returns (None, None) if no IP_RECVERR ancillary data is present.
    """
    ip = address[0] if address and address[0] else None
    status = None
    for level, ctype, cdata in ancdata:
        if level != socket.IPPROTO_IP or ctype != IP_RECVERR or len(cdata) < 16:
            continue
        ee_errno, ee_origin, ee_type, ee_code = struct.unpack('=IBBB', cdata[:7])
        status = classify_icmp(ee_origin, ee_type, ee_code, ee_errno)
        if ip is None and len(cdata) >= 24:
            # Fall back to the SO_EE_OFFENDER sockaddr_in that follows the
            # 16-byte sock_extended_err: family(2) port(2) addr(4).
            if struct.unpack('=H', cdata[16:18])[0] == socket.AF_INET:
                ip = '.'.join(str(b) for b in cdata[20:24])
    return ip, status
