Linux Netfilter development
 help / color / mirror / Atom feed
From: Florian Westphal <fw@strlen.de>
To: <netfilter-devel@vger.kernel.org>
Cc: kadlec@netfilter.org, Florian Westphal <fw@strlen.de>
Subject: [PATCH ipset 7/7] tests: runtest.sh: add sendip emulation via scapy
Date: Thu,  9 Jul 2026 22:03:58 +0200	[thread overview]
Message-ID: <20260709200358.15504-8-fw@strlen.de> (raw)
In-Reply-To: <20260709200358.15504-1-fw@strlen.de>

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>
---
 README                     |  13 ++--
 tests/bitmap:ip.t          |   2 +-
 tests/check_sendip_packets |   2 +-
 tests/hash:ip.t            |   2 +-
 tests/runtest.sh           |  18 ++++-
 tests/sendip.py            | 154 +++++++++++++++++++++++++++++++++++++
 tests/sendip.sh            |   7 +-
 tests/setlist.t            |   2 +-
 8 files changed, 188 insertions(+), 12 deletions(-)
 create mode 100755 tests/sendip.py

diff --git a/README b/README
index 08b3598b6c0e..075ffd4ba991 100644
--- 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
 
diff --git a/tests/bitmap:ip.t b/tests/bitmap:ip.t
index 53034bbbae89..f97266450423 100644
--- a/tests/bitmap:ip.t
+++ b/tests/bitmap:ip.t
@@ -209,7 +209,7 @@
 # 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
diff --git a/tests/check_sendip_packets b/tests/check_sendip_packets
index 0dad3d0944d2..758f31d59dad 100755
--- a/tests/check_sendip_packets
+++ b/tests/check_sendip_packets
@@ -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
 
diff --git a/tests/hash:ip.t b/tests/hash:ip.t
index 529e6b815069..88b36873b41d 100644
--- a/tests/hash:ip.t
+++ b/tests/hash:ip.t
@@ -195,7 +195,7 @@
 # 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
diff --git a/tests/runtest.sh b/tests/runtest.sh
index ba4683f59e5d..238175440cc4 100755
--- a/tests/runtest.sh
+++ b/tests/runtest.sh
@@ -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
index 000000000000..61191f485b26
--- /dev/null
+++ b/tests/sendip.py
@@ -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()
diff --git a/tests/sendip.sh b/tests/sendip.sh
index d1f8ebe4d75b..cd65227f2ae6 100755
--- a/tests/sendip.sh
+++ b/tests/sendip.sh
@@ -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 "$@"
diff --git a/tests/setlist.t b/tests/setlist.t
index c1f1d3b44816..0edebfced72d 100644
--- a/tests/setlist.t
+++ b/tests/setlist.t
@@ -174,7 +174,7 @@
 # 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
-- 
2.54.0


  parent reply	other threads:[~2026-07-09 20:04 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-09 20:03 [PATCH ipset 0/7] test updates Florian Westphal
2026-07-09 20:03 ` [PATCH ipset 1/7] tests: make runtest.sh work with readonly-cwd Florian Westphal
2026-07-09 20:03 ` [PATCH ipset 2/7] tests: runtest.sh: run inside namespace Florian Westphal
2026-07-09 20:03 ` [PATCH ipset 3/7] tests: diff.sh: preserve file name Florian Westphal
2026-07-09 20:03 ` [PATCH ipset 4/7] tests: check_klog.sh: unclutter stderr Florian Westphal
2026-07-09 20:03 ` [PATCH ipset 5/7] tests: setlist_ns.sh: use local ipset binary and don't rely on userns Florian Westphal
2026-07-09 20:03 ` [PATCH ipset 6/7] tests: make setlist_resize.sh more verbose on error Florian Westphal
2026-07-09 20:03 ` Florian Westphal [this message]
2026-07-13 13:35 ` [PATCH ipset 0/7] test updates Jozsef Kadlecsik
2026-07-13 14:11   ` Pablo Neira Ayuso
2026-07-13 14:25     ` Jozsef Kadlecsik

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260709200358.15504-8-fw@strlen.de \
    --to=fw@strlen.de \
    --cc=kadlec@netfilter.org \
    --cc=netfilter-devel@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox