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
next prev 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