]> git.ipfire.org Git - thirdparty/ipset.git/commitdiff
tests: runtest.sh: add sendip emulation via scapy master
authorFlorian Westphal <fw@strlen.de>
Thu, 9 Jul 2026 20:03:58 +0000 (22:03 +0200)
committerJozsef Kadlecsik <kadlec@netfilter.org>
Mon, 13 Jul 2026 13:25:54 +0000 (15:25 +0200)
sendip tool is ancient and not packaged on fedora (or i'm
blind).  Add a scapy-based replacement.

Assisted-by: Claude:claude-sonnet-4-6
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Jozsef Kadlecsik <kadlec@netfilter.org>
README
tests/bitmap:ip.t
tests/check_sendip_packets
tests/hash:ip.t
tests/runtest.sh
tests/sendip.py [new file with mode: 0755]
tests/sendip.sh
tests/setlist.t

diff --git a/README b/README
index 08b3598b6c0e3f5d91796371b0700f39957e11ad..075ffd4ba9913683f4f972265dadd69aacc7502b 100644 (file)
--- a/README
+++ b/README
@@ -41,12 +41,13 @@ instructions too.
    After installing the modules, you can run the testsuite as well.
    Please note, several assumptions must be met for the testsuite:
 
-       - no sets defined
-       - iptables/ip6tables rules are not set up
-       - the destination for kernel logs is /var/log/kern.log
-       - the networks 10.255.255.0/24 and 1002:1002:1002:1002::/64
-         are not in use
-       - sendip utility is installed
+       - the destination for kernel logs is /var/log/kern.log, OR,
+         if running from a network namespace, iptables -j LOG can
+         write to the kernel ring buffer (dmesg).
+       - by default tests run in a extra namespace to avoid
+         conflicts with the networks used by the tests:
+         10.255.255.0/24 and 1002:1002:1002:1002::/64.
+       - sendip utility OR scapy is installed
 
    # make tests
 
index 53034bbbae8941c4077eaef08b097c15a46fc327..f972664504232f367e577c72e3f786a2843d40dd 100644 (file)
 # Counters and timeout: destroy set
 0 ipset x test
 # Counters: require sendip
-skip which sendip
+require_sendip
 # Counters: create set
 0 ipset n test bitmap:ip range 10.255.0.0/16 counters
 # Counters: add elemet with zero counters
index 0dad3d0944d23141d61f37528d286dc6fa806942..758f31d59dadfbaea60d83291d82e5cdd096e1f0 100755 (executable)
@@ -18,7 +18,7 @@ fi
 
 $cmd -A INPUT -m set --match-set test $2 -j DROP
 for x in `seq 1 $3`; do
-    sendip -p $proto -id $dst -is $src -p tcp -td 80 -ts 1025 $dst
+    ./sendip.sh -p $proto -id $dst -is $src -p tcp -td 80 -ts 1025 $dst
 done
 $cmd -D INPUT -m set --match-set test $2 -j DROP
 
index 529e6b81506951bce551bf75f6c27e2c119c10cd..88b36873b41dc0e4552b26043f6ce020cca592c1 100644 (file)
 # Counters and timeout: destroy set
 0 ipset x test
 # Counters: require sendip
-skip which sendip
+require_sendip
 # Counters: create set
 0 ipset n test hash:ip counters
 # Counters: add elemet with zero counters
index ba4683f59e5d8c1e0a91f5790eec69b3f900905c..238175440cc48f86b24b7a04189ec15aa5b8aa9f 100755 (executable)
@@ -51,6 +51,14 @@ LC_ALL=C
 export LC_ALL
 export IPSET_TMP="$tmpdir"
 
+HAVE_SENDIP=n
+[ -z "`which sendip`" ] && HAVE_SENDIP=y
+if [ $HAVE_SENDIP = "n" ]; then
+       if python -c 'from scapy.all import ( IPv6 )'; then
+               HAVE_SENDIP=y
+       fi
+fi
+
 add_tests() {
        # inet|inet6 network
        if [ $1 = "inet" ]; then
@@ -72,7 +80,7 @@ add_tests() {
        fi
        if [ `$cmd -t filter | wc -l` -eq 7 -a \
             `$cmd -t filter | grep ACCEPT | wc -l` -eq 3 ]; then
-               if [ -z "`which sendip`" ]; then
+               if [ "$HAVE_SENDIP" = "n" ]; then
                        echo "sendip utility is missig: skipping $1 match and target tests"
                        return
                elif [ -n "`which ss`" ]; then
@@ -130,6 +138,14 @@ for types in $tests; do
                fi
                continue
                ;;
+           require_sendip)
+               if [ "$HAVE_SENDIP" = "y" ]; then
+                       continue
+               else
+                       echo "Skipping, sendip (scapy) not available"
+                       break
+               fi
+               ;;
            *)
                ;;
        esac
diff --git a/tests/sendip.py b/tests/sendip.py
new file mode 100755 (executable)
index 0000000..61191f4
--- /dev/null
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+"""Minimal replacement for sendip via scapy. sends one packet."""
+
+import errno
+import os
+import socket
+import sys
+from scapy.all import (
+    send,
+    IP, IPv6,
+    TCP, UDP, ICMP,
+    ICMPv6DestUnreach, ICMPv6TimeExceeded, ICMPv6ParamProblem,
+    Raw,
+)
+from scapy.layers.inet6 import ICMPv6Unknown
+
+_LOOPBACK = {'127.0.0.1', '::1'}
+
+def parse_args(argv):
+    p = {
+        'ip_version': None,
+        'transport': None,
+        'src': None,
+        'dst': None,
+        'sport': None,
+        'dport': None,
+        'icmp_type': None,
+        'icmp_code': None,
+        'payload_size': 0,
+    }
+
+    i = 0
+    while i < len(argv):
+        a = argv[i]
+        if a == '-p':
+            proto = argv[i + 1]; i += 2
+            if proto in ('ipv4', 'ipv6'):
+                p['ip_version'] = proto
+            elif proto in ('tcp', 'udp', 'icmp'):
+                p['transport'] = proto
+            else:
+                sys.exit(f'unknown protocol: {proto}')
+        elif a == '-is':
+            p['src'] = argv[i + 1]; i += 2
+        elif a == '-id':
+            p['dst'] = argv[i + 1]; i += 2
+        elif a == '-6s':
+            p['src'] = argv[i + 1]; i += 2
+        elif a == '-6d':
+            p['dst'] = argv[i + 1]; i += 2
+        elif a == '-ts':
+            p['sport'] = int(argv[i + 1]); i += 2
+        elif a == '-td':
+            p['dport'] = int(argv[i + 1]); i += 2
+        elif a == '-us':
+            p['sport'] = int(argv[i + 1]); i += 2
+        elif a == '-ud':
+            p['dport'] = int(argv[i + 1]); i += 2
+        elif a == '-ct':
+            p['icmp_type'] = int(argv[i + 1]); i += 2
+        elif a == '-cd':
+            p['icmp_code'] = int(argv[i + 1]); i += 2
+        elif a == '-d':
+            # e.g. "r10" = 10 random bytes of payload
+            spec = argv[i + 1]; i += 2
+            if spec.startswith('r'):
+                p['payload_size'] = int(spec[1:])
+        elif not a.startswith('-'):
+            i += 1  # positional: routing dest, scapy routes by dst in header
+        else:
+            i += 1
+
+    return p
+
+
+_ICMPV6_CLASS = {
+    1: ICMPv6DestUnreach,
+    3: ICMPv6TimeExceeded,
+    4: ICMPv6ParamProblem,
+}
+
+
+def make_icmpv6(typ, code):
+    cls = _ICMPV6_CLASS.get(typ, ICMPv6Unknown)
+    pkt = cls(code=code)
+    if cls is ICMPv6Unknown:
+        pkt.type = typ
+    return pkt
+
+
+def build_packet(p):
+    if p['ip_version'] == 'ipv4':
+        ip = IP(src=p['src'], dst=p['dst'])
+    elif p['ip_version'] == 'ipv6':
+        ip = IPv6(src=p['src'], dst=p['dst'])
+    else:
+        sys.exit('no IP version specified')
+
+    t = p['transport']
+    if t == 'tcp':
+        l4 = TCP(sport=p['sport'], dport=p['dport'])
+    elif t == 'udp':
+        l4 = UDP(sport=p['sport'], dport=p['dport'])
+    elif t == 'icmp':
+        typ, code = p['icmp_type'], p['icmp_code']
+        if p['ip_version'] == 'ipv4':
+            l4 = ICMP(type=typ, code=code)
+        else:
+            l4 = make_icmpv6(typ, code)
+    else:
+        sys.exit('no transport protocol specified')
+
+    pkt = ip / l4
+    if p['payload_size'] > 0:
+        pkt = pkt / Raw(os.urandom(p['payload_size']))
+    return pkt
+
+
+def send_on_iface(pkt, dst, iface):
+    """Send via AF_INET(6) SOCK_RAW with SO_BINDTODEVICE.
+
+    The packet originates locally and traverses the OUTPUT netfilter chain,
+    matching the iptables rules under test.  No ARP on dummy interfaces.
+    """
+    is_ipv6 = IPv6 in pkt
+    if is_ipv6:
+        sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
+                             socket.IPPROTO_RAW)
+        dst_addr = (dst, 0, 0, 0)
+    else:
+        sock = socket.socket(socket.AF_INET, socket.SOCK_RAW,
+                             socket.IPPROTO_RAW)
+        dst_addr = (dst, 0)
+    try:
+        sock.sendto(bytes(pkt), dst_addr)
+    except OSError as e:
+        if e.errno != errno.EPERM:
+            raise
+    finally:
+        sock.close()
+
+
+def main():
+    p = parse_args(sys.argv[1:])
+    iface = None if p['dst'] in _LOOPBACK else 'eth0'
+    pkt = build_packet(p)
+    if p['dst'] in _LOOPBACK:
+        send(pkt, verbose=False)
+    else:
+        send_on_iface(pkt, p['dst'], 'eth0')
+
+
+if __name__ == '__main__':
+    main()
index d1f8ebe4d75b1a9aac1703e459d2a7395775dce8..cd65227f2ae67810b279d683eb3a0c148ebacbe3 100755 (executable)
@@ -2,4 +2,9 @@
 
 # Save lineno for checking
 wc -l /var/log/kern.log 2>/dev/null | cut -d ' ' -f 1 > "$IPSET_TMP/.loglines"
-sendip "$@"
+
+if [ -n "`which sendip 2>/dev/null`" ]; then
+       exec sendip "$@"
+fi
+
+exec ./sendip.py "$@"
index c1f1d3b4481642162833f446c1052cf5ee202a6f..0edebfced72d79bfa9546c7e92ce5d5bbffd6f64 100644 (file)
 # Counters and timeout: destroy sets
 0 ipset x
 # Counters: require sendip
-skip which sendip >/dev/null
+require_sendip
 # Counters: create set
 0 ipset n a hash:ip counters
 # Counters: create list set