Build a well-formatted iptables rule based on kwargs. A `table` and `chain`
    are not required, unless `full` is True.

    If `full` is `True`, then `table`, `chain` and `command` are required.
    `command` may be specified as either a short option ('I') or a long option
    (`--insert`). This will return the iptables command, exactly as it would
    be used from the command line.

    If a position is required (as with `-I` or `-D`), it may be specified as
    `position`. This will only be useful if `full` is True.

    If `state` is passed, it will be ignored, use `connstate`.
    If `connstate` is passed in, it will automatically be changed to `state`.

    To pass in jump options that doesn't take arguments, pass in an empty
    string.

    .. note::

        Whereas iptables will accept ``-p``, ``--proto[c[o[l]]]`` as synonyms
        of ``--protocol``, if ``--proto`` appears in an iptables command after
        the appearance of ``-m policy``, it is interpreted as the ``--proto``
        option of the policy extension (see the iptables-extensions(8) man
        page).

    CLI Examples:

    .. code-block:: bash

        salt '*' iptables.build_rule match=state \\
            connstate=RELATED,ESTABLISHED jump=ACCEPT

        salt '*' iptables.build_rule filter INPUT command=I position=3 \\
            full=True match=state connstate=RELATED,ESTABLISHED jump=ACCEPT

        salt '*' iptables.build_rule filter INPUT command=A \\
            full=True match=state connstate=RELATED,ESTABLISHED \\
            source='127.0.0.1' jump=ACCEPT

        .. Invert Rules
        salt '*' iptables.build_rule filter INPUT command=A \\
            full=True match=state connstate=RELATED,ESTABLISHED \\
            source='!127.0.0.1' jump=ACCEPT

        salt '*' iptables.build_rule filter INPUT command=A \\
            full=True match=state connstate=RELATED,ESTABLISHED \\
            destination='not 127.0.0.1' jump=ACCEPT

        IPv6:
        salt '*' iptables.build_rule match=state \\
            connstate=RELATED,ESTABLISHED jump=ACCEPT \\
            family=ipv6
        salt '*' iptables.build_rule filter INPUT command=I position=3 \\
            full=True match=state connstate=RELATED,ESTABLISHED jump=ACCEPT \\
            family=ipv6

def build_rule(table='filter', chain=None, command=None, position='', full=None, family='ipv4',
               **kwargs):
    '''
    Build a well-formatted iptables rule based on kwargs. A `table` and `chain`
    are not required, unless `full` is True.

    If `full` is `True`, then `table`, `chain` and `command` are required.
    `command` may be specified as either a short option ('I') or a long option
    (`--insert`). This will return the iptables command, exactly as it would
    be used from the command line.

    If a position is required (as with `-I` or `-D`), it may be specified as
    `position`. This will only be useful if `full` is True.

    If `state` is passed, it will be ignored, use `connstate`.
    If `connstate` is passed in, it will automatically be changed to `state`.

    To pass in jump options that doesn't take arguments, pass in an empty
    string.

    .. note::

        Whereas iptables will accept ``-p``, ``--proto[c[o[l]]]`` as synonyms
        of ``--protocol``, if ``--proto`` appears in an iptables command after
        the appearance of ``-m policy``, it is interpreted as the ``--proto``
        option of the policy extension (see the iptables-extensions(8) man
        page).

    CLI Examples:

    .. code-block:: bash

        salt '*' iptables.build_rule match=state \\
            connstate=RELATED,ESTABLISHED jump=ACCEPT

        salt '*' iptables.build_rule filter INPUT command=I position=3 \\
            full=True match=state connstate=RELATED,ESTABLISHED jump=ACCEPT

        salt '*' iptables.build_rule filter INPUT command=A \\
            full=True match=state connstate=RELATED,ESTABLISHED \\
            source='127.0.0.1' jump=ACCEPT

        .. Invert Rules
        salt '*' iptables.build_rule filter INPUT command=A \\
            full=True match=state connstate=RELATED,ESTABLISHED \\
            source='!127.0.0.1' jump=ACCEPT

        salt '*' iptables.build_rule filter INPUT command=A \\
            full=True match=state connstate=RELATED,ESTABLISHED \\
            destination='not 127.0.0.1' jump=ACCEPT

        IPv6:
        salt '*' iptables.build_rule match=state \\
            connstate=RELATED,ESTABLISHED jump=ACCEPT \\
            family=ipv6
        salt '*' iptables.build_rule filter INPUT command=I position=3 \\
            full=True match=state connstate=RELATED,ESTABLISHED jump=ACCEPT \\
            family=ipv6
    '''
    if 'target' in kwargs:
        kwargs['jump'] = kwargs.pop('target')

    # Ignore name and state for this function
    kwargs.pop('name', None)
    kwargs.pop('state', None)

    for ignore in list(_STATE_INTERNAL_KEYWORDS) + ['chain', 'save', 'table']:
        if ignore in kwargs:
            del kwargs[ignore]

    rule = []
    protocol = False
    bang_not_pat = re.compile(r'(!|not)\s?')

    def maybe_add_negation(arg):
        '''
        Will check if the defined argument is intended to be negated,
        (i.e. prefixed with '!' or 'not'), and add a '! ' to the rule.

        The prefix will be removed from the value in the kwargs dict.
        '''
        value = str(kwargs[arg])
        if value.startswith('!') or value.startswith('not'):
            kwargs[arg] = re.sub(bang_not_pat, '', value)
            return '! '
        return ''

    if 'if' in kwargs:
        rule.append('{0}-i {1}'.format(maybe_add_negation('if'), kwargs['if']))
        del kwargs['if']

    if 'of' in kwargs:
        rule.append('{0}-o {1}'.format(maybe_add_negation('of'), kwargs['of']))
        del kwargs['of']

    if 'proto' in kwargs and kwargs.get('match') != 'policy':
        kwargs['protocol'] = kwargs['proto']
        del kwargs['proto']
        # Handle the case 'proto' in kwargs and kwargs.get('match') == 'policy' below
    if 'protocol' in kwargs:
        if not protocol:
            rule.append('{0}-p {1}'.format(maybe_add_negation('protocol'), kwargs['protocol']))
            protocol = True
        del kwargs['protocol']

    if 'match' in kwargs:
        match_value = kwargs['match']
        if not isinstance(match_value, list):
            match_value = match_value.split(',')
        for match in match_value:
            rule.append('-m {0}'.format(match))
            if 'name_' in kwargs and match.strip() in ('pknock', 'quota2', 'recent'):
                rule.append('--name {0}'.format(kwargs['name_']))
                del kwargs['name_']
        if 'proto' in kwargs and kwargs.get('match') == 'policy':
            rule.append('{0}--proto {1}'.format(maybe_add_negation('proto'), kwargs['proto']))
            del kwargs['proto']
        del kwargs['match']

    if 'match-set' in kwargs:
        if isinstance(kwargs['match-set'], six.string_types):
            kwargs['match-set'] = [kwargs['match-set']]
        for match_set in kwargs['match-set']:
            negative_match_set = ''
            if match_set.startswith('!') or match_set.startswith('not'):
                negative_match_set = '! '
                match_set = re.sub(bang_not_pat, '', match_set)
            rule.append('-m set {0}--match-set {1}'.format(negative_match_set, match_set))
        del kwargs['match-set']

    if 'connstate' in kwargs:
        if '-m state' not in rule:
            rule.append('-m state')

        rule.append('{0}--state {1}'.format(maybe_add_negation('connstate'), kwargs['connstate']))

        del kwargs['connstate']

    if 'dport' in kwargs:
        rule.append('{0}--dport {1}'.format(maybe_add_negation('dport'), kwargs['dport']))
        del kwargs['dport']

    if 'sport' in kwargs:
        rule.append('{0}--sport {1}'.format(maybe_add_negation('sport'), kwargs['sport']))
        del kwargs['sport']

    for multiport_arg in ('dports', 'sports'):
        if multiport_arg in kwargs:
            if '-m multiport' not in rule:
                rule.append('-m multiport')
                if not protocol:
                    return 'Error: protocol must be specified'

            mp_value = kwargs[multiport_arg]
            if isinstance(mp_value, list):
                if any(i for i in mp_value if str(i).startswith('!') or str(i).startswith('not')):
                    mp_value = [re.sub(bang_not_pat, '', str(item)) for item in mp_value]
                    rule.append('!')
                dports = ','.join(str(i) for i in mp_value)
            else:
                if str(mp_value).startswith('!') or str(mp_value).startswith('not'):
                    dports = re.sub(bang_not_pat, '', mp_value)
                    rule.append('!')
                else:
                    dports = mp_value

            rule.append('--{0} {1}'.format(multiport_arg, dports))
            del kwargs[multiport_arg]

    if 'comment' in kwargs:
        if '-m comment' not in rule:
            rule.append('-m comment')

        rule.append('--comment "{0}"'.format(kwargs['comment']))
        del kwargs['comment']

    # --set in ipset is deprecated, works but returns error.
    # rewrite to --match-set if not empty, otherwise treat as recent option
    if 'set' in kwargs and kwargs['set']:
        rule.append('{0}--match-set {1}'.format(maybe_add_negation('set'), kwargs['set']))
        del kwargs['set']

    # Jumps should appear last, except for any arguments that are passed to
    # jumps, which of course need to follow.
    after_jump = []
    # All jump arguments as extracted from man iptables-extensions, man iptables,
    # man xtables-addons and http://www.iptables.info/en/iptables-targets-and-jumps.html
    after_jump_arguments = (
        'j',  # j and jump needs to be first
        'jump',

        # IPTABLES
        'add-set',
        'and-mark',
        'and-tos',
        'checksum-fill',
        'clamp-mss-to-pmtu',
        'clustermac',
        'ctevents',
        'ctmask',
        'del-set',
        'ecn-tcp-remove',
        'exist',
        'expevents',
        'gateway',
        'hash-init',
        'hashmode',
        'helper',
        'label',
        'local-node',
        'log-ip-options',
        'log-level',
        'log-prefix',
        'log-tcp-options',
        'log-tcp-sequence',
        'log-uid',
        'mask',
        'new',
        'nfmask',
        'nflog-group',
        'nflog-prefix',
        'nflog-range',
        'nflog-threshold',
        'nodst',
        'notrack',
        'on-ip',
        'on-port',
        'or-mark',
        'or-tos',
        'persistent',
        'queue-balance',
        'queue-bypass',
        'queue-num',
        'random',
        'rateest-ewmalog',
        'rateest-interval',
        'rateest-name',
        'reject-with',
        'restore',
        'restore-mark',
        #'save',  # no arg, problematic name: How do we avoid collision with this?
        'save-mark',
        'selctx',
        'set-class',
        'set-dscp',
        'set-dscp-class',
        'set-mark',
        'set-mss',
        'set-tos',
        'set-xmark',
        'strip-options',
        'timeout',
        'to',
        'to-destination',
        'to-ports',
        'to-source',
        'total-nodes',
        'tproxy-mark',
        'ttl-dec',
        'ttl-inc',
        'ttl-set',
        'type',
        'ulog-cprange',
        'ulog-nlgroup',
        'ulog-prefix',
        'ulog-qthreshold',
        'xor-mark',
        'xor-tos',
        'zone',

        # IPTABLES-EXTENSIONS
        'dst-pfx',
        'hl-dec',
        'hl-inc',
        'hl-set',
        'hmark-dport-mask',
        'hmark-dst-prefix',
        'hmark-mod',
        'hmark-offset',
        'hmark-proto-mask',
        'hmark-rnd',
        'hmark-spi-mask',
        'hmark-sport-mask',
        'hmark-src-prefix',
        'hmark-tuple',
        'led-always-blink',
        'led-delay',
        'led-trigger-id',
        'queue-cpu-fanout',
        'src-pfx',

        # WEB
        'to-port',

        # XTABLES
        'addr',
        'and-mask',
        'delude',
        'honeypot',
        'or-mask',
        'prefix',
        'reset',
        'reuse',
        'set-mac',
        'shift',
        'static',
        'tarpit',
        'tname',
        'ttl',
    )
    for after_jump_argument in after_jump_arguments:
        if after_jump_argument in kwargs:
            value = kwargs[after_jump_argument]
            if value in (None, ''):  # options without arguments
                after_jump.append('--{0}'.format(after_jump_argument))
            elif any(ws_char in str(value) for ws_char in string.whitespace):
                after_jump.append('--{0} "{1}"'.format(after_jump_argument, value))
            else:
                after_jump.append('--{0} {1}'.format(after_jump_argument, value))
            del kwargs[after_jump_argument]

    for key in kwargs:
        negation = maybe_add_negation(key)
        # don't use .items() since maybe_add_negation removes the prefix from
        # the value in the kwargs, thus we need to fetch it after that has run
        value = kwargs[key]
        flag = '-' if len(key) == 1 else '--'
        value = '' if value in (None, '') else ' {0}'.format(value)
        rule.append('{0}{1}{2}{3}'.format(negation, flag, key, value))

    rule += after_jump

    if full:
        if not table:
            return 'Error: Table needs to be specified'
        if not chain:
            return 'Error: Chain needs to be specified'
        if not command:
            return 'Error: Command needs to be specified'

        if command in 'ACDIRLSFZNXPE':
            flag = '-'
        else:
            flag = '--'

        wait = '--wait' if _has_option('--wait', family) else ''

        return '{0} {1} -t {2} {3}{4} {5} {6} {7}'.format(_iptables_cmd(family),
               wait, table, flag, command, chain, position, ' '.join(rule))

    return ' '.join(rule)