#!/bin/bash # End-to-end test: builds the image, starts target + 2 clients, patches # services through PatchBay and checks TCP, UDP, client-to-client, integrity, # throughput, Services reporting and the web login. # Usage: docker/run-tests.sh [--keep] (--keep leaves the containers running) set -u cd "$(dirname "$0")" DC="docker compose -f compose.yml" KEEP=0 [ "${1:-}" = "--keep" ] && KEEP=1 PASS=0 FAIL=0 ok() { echo "PASS $*"; PASS=$((PASS + 1)); } bad() { echo "FAIL $*"; FAIL=$((FAIL + 1)); } on() { local c=$1; shift; $DC exec -T "$c" bash -c "$*"; } cleanup() { if [ $KEEP = 0 ]; then $DC down -v >/dev/null 2>&1 else echo "containers kept; web UI at https://127.0.0.1:18443 (admin / patchbay-test-pw, code in 'docker compose -f docker/compose.yml logs target')" fi } trap cleanup EXIT $DC down -v >/dev/null 2>&1 $DC up -d --build || exit 1 echo "waiting for client keys..." for i in $(seq 60); do on target 'ls /shared/client1.pub /shared/client2.pub' >/dev/null 2>&1 && break sleep 1 done on target 'cd /usr/local/lib/patchbay/web && python3 -m patchbay_web add-client client1 < /shared/client1.pub && python3 -m patchbay_web add-client client2 < /shared/client2.pub' >/dev/null # Patch: echo service on client1 exposed publicly (via splitter) and on # client2's loopback, a UDP echo, and a discard sink for throughput. on target "sqlite3 /etc/patchbay/patchbay.db \" INSERT INTO nodes (id, type, client_id, host, port, proto) VALUES (1, 'client_source', 1, '127.0.0.1', 9000, 'tcp'), (2, 'public_sink', NULL, '0.0.0.0', 2200, 'tcp'), (3, 'client_source', 1, '127.0.0.1', 9001, 'udp'), (4, 'public_sink', NULL, '0.0.0.0', 2201, 'udp'), (5, 'splitter', NULL, '', 0, 'tcp'), (6, 'client_sink', 2, '127.0.0.1', 7000, 'tcp'), (7, 'client_source', 1, '127.0.0.1', 9002, 'tcp'), (8, 'public_sink', NULL, '0.0.0.0', 2202, 'tcp'); INSERT INTO links (from_node, to_node) VALUES (1, 5), (5, 2), (5, 6), (3, 4), (7, 8);\"" on target 'echo RELOAD | socat - UNIX-CONNECT:/run/patchbay/api.sock' >/dev/null echo "waiting for clients to connect..." for i in $(seq 60); do st=$(on target 'echo STATUS | socat - UNIX-CONNECT:/run/patchbay/api.sock') echo "$st" | grep -q '"1":{"since"' && echo "$st" | grep -q '"2":{"since"' && break sleep 1 done echo "$st" | grep -q '"2":{"since"' && ok "both clients connected" || { bad "clients did not connect: $st"; $DC logs --tail 40; exit 1; } sleep 2 r=$(on client2 'echo hello-public | socat -t3 - TCP:target:2200') [ "$r" = hello-public ] && ok "TCP via public sink" || bad "TCP via public sink: got '$r'" r=$(on client2 'echo hello-client | socat -t3 - TCP:127.0.0.1:7000') [ "$r" = hello-client ] && ok "TCP client sink (client2 -> client1)" || bad "client sink: got '$r'" r=$(on client2 'echo hello-udp | socat -T3 - UDP:target:2201') [ "$r" = hello-udp ] && ok "UDP via public sink" || bad "UDP via public sink: got '$r'" r=$(on client2 'head -c 8000000 /dev/urandom > /tmp/blob && socat -t10 - TCP:target:2200 < /tmp/blob | sha256sum | cut -c1-64; sha256sum < /tmp/blob | cut -c1-64') [ "$(echo "$r" | sed -n 1p)" = "$(echo "$r" | sed -n 2p)" ] && ok "8 MB echo integrity" || bad "integrity: $r" r=$(on client2 'for i in $(seq 20); do (echo "par$i" | socat -t5 - TCP:target:2200) & done; wait' | sort | uniq | wc -l) [ "$r" = 20 ] && ok "20 parallel connections" || bad "parallel connections: $r distinct answers" r=$(on client2 'start=$(date +%s.%N); head -c 500000000 /dev/zero | socat -u - TCP:target:2202; end=$(date +%s.%N); echo "$start $end" | awk "{printf \"%.0f\", 500/(\$2-\$1)}"') [ -n "$r" ] && [ "$r" -gt 0 ] 2>/dev/null && ok "throughput 500 MB: ${r} MB/s" || bad "throughput: $r" sleep 1 r=$(on target 'echo LIVE | socat - UNIX-CONNECT:/run/patchbay/api.sock') echo "$r" | grep -Eq '"8":\{[^}]*"total_in":[0-9]{9}' && ok "stats count bytes" || bad "stats: $r" r=$(on target "sqlite3 /etc/patchbay/patchbay.db \"SELECT COUNT(*) FROM services WHERE client_id = 1 AND port = 9000\"") [ "$r" = 1 ] && ok "Services report from client1" || bad "Services: $r rows" # Tunnel nodes: a veth pair named tunN stands in for a VPN interface, its peer # end lives in network namespace "peer" with echo services on 10.77.0.2. mktun() { on "$1" "ip netns add peer && ip link add $2 type veth peer name p0 && ip link set p0 netns peer && ip addr add 10.77.0.1/24 dev $2 && ip link set $2 up && ip netns exec peer ip addr add 10.77.0.2/24 dev p0 && ip netns exec peer ip link set p0 up && ip netns exec peer ip link set lo up && (ip netns exec peer socat TCP-LISTEN:9100,fork,reuseaddr EXEC:cat >/dev/null 2>&1 &) && (ip netns exec peer socat UDP-RECVFROM:9101,fork EXEC:cat >/dev/null 2>&1 &)" } mktun target tun7 mktun client1 tun8 mktun client2 tun9 on target "sqlite3 /etc/patchbay/patchbay.db \" INSERT INTO nodes (id, type, client_id, host, port, proto, iface) VALUES (20, 'tunnel_source', NULL, '10.77.0.2', 9100, 'tcp', 'tun7'), (21, 'public_sink', NULL, '0.0.0.0', 2210, 'tcp', ''), (22, 'tunnel_source', NULL, '10.77.0.2', 9101, 'udp', 'tun7'), (23, 'public_sink', NULL, '0.0.0.0', 2211, 'udp', ''), (24, 'tunnel_sink', NULL, '', 9200, 'tcp', 'tun7'), (25, 'client_source', 1, '127.0.0.1', 9000, 'tcp', ''), (26, 'tunnel_source', 1, '10.77.0.2', 9100, 'tcp', 'tun8'), (27, 'public_sink', NULL, '0.0.0.0', 2212, 'tcp', ''), (28, 'tunnel_sink', 2, '', 9300, 'tcp', 'tun9'), (29, 'client_source', 1, '127.0.0.1', 9000, 'tcp', ''), (30, 'tunnel_sink', 2, '', 9301, 'udp', 'tun9'), (31, 'tunnel_source', NULL, '10.77.0.2', 9101, 'udp', 'tun7'); INSERT INTO links (from_node, to_node) VALUES (20, 21), (22, 23), (25, 24), (26, 27), (29, 28), (31, 30);\"" on target 'echo RELOAD | socat - UNIX-CONNECT:/run/patchbay/api.sock' >/dev/null sleep 3 r=$(on client2 'echo t-src-target | socat -t3 - TCP:target:2210') [ "$r" = t-src-target ] && ok "tunnel source on target (TCP)" || bad "tunnel source on target: got '$r'" r=$(on client2 'echo t-src-udp | socat -T3 - UDP:target:2211') [ "$r" = t-src-udp ] && ok "tunnel source on target (UDP)" || bad "tunnel source UDP: got '$r'" r=$(on target 'echo t-sink-target | ip netns exec peer socat -t3 - TCP:10.77.0.1:9200') [ "$r" = t-sink-target ] && ok "tunnel sink on target" || bad "tunnel sink on target: got '$r'" r=$(on target 'echo not-via-tun | socat -t2 - TCP:127.0.0.1:9200 2>&1') [ "$r" != not-via-tun ] && ok "tunnel sink only accepts traffic from its interface" || bad "tunnel sink reachable via lo" r=$(on client2 'echo t-src-client | socat -t3 - TCP:target:2212') [ "$r" = t-src-client ] && ok "tunnel source on client1" || bad "tunnel source on client: got '$r'" r=$(on client2 'echo t-sink-client | ip netns exec peer socat -t3 - TCP:10.77.0.1:9300') [ "$r" = t-sink-client ] && ok "tunnel sink on client2 (TCP)" || bad "tunnel sink on client: got '$r'" r=$(on client2 'echo t-sink-udp | ip netns exec peer socat -T3 - UDP:10.77.0.1:9301') [ "$r" = t-sink-udp ] && ok "tunnel sink on client2 -> target tunnel source (UDP)" || bad "client tunnel sink UDP: got '$r'" on target 'echo SERVICES | socat - UNIX-CONNECT:/run/patchbay/api.sock' >/dev/null sleep 2 r=$(on target "sqlite3 /etc/patchbay/patchbay.db \"SELECT COUNT(*) FROM interfaces WHERE (client_id = 0 AND name = 'tun7') OR (client_id = 1 AND name = 'tun8')\"") [ "$r" = 2 ] && ok "tun interfaces reported" || bad "interfaces: $r rows" # Origin address: PROXY v2 headers (services print the first 28 bytes as hex, # an IPv4 header is exactly 28) and transparent source spoofing (services # print the peer address they see). HEXDUMP="head -c 28 | od -An -tx1 | tr -dc 0-9a-f" # no quotes or colons: socat parses these on client1 "(socat TCP-LISTEN:9400,fork,reuseaddr SYSTEM:'$HEXDUMP' >/dev/null 2>&1 &) && (socat UDP-RECVFROM:9401,bind=127.0.0.1,fork SYSTEM:'$HEXDUMP' >/dev/null 2>&1 &) && (socat TCP-LISTEN:9410,fork,reuseaddr SYSTEM:'echo \$SOCAT_PEERADDR' >/dev/null 2>&1 &) && (socat UDP-RECVFROM:9411,bind=127.0.0.1,fork SYSTEM:'echo \$SOCAT_PEERADDR' >/dev/null 2>&1 &)" on target "(ip netns exec peer socat TCP-LISTEN:9402,fork,reuseaddr SYSTEM:'$HEXDUMP' >/dev/null 2>&1 &)" on target "sqlite3 /etc/patchbay/patchbay.db \" INSERT INTO nodes (id, type, client_id, host, port, proto, iface, origin) VALUES (40, 'client_source', 1, '127.0.0.1', 9400, 'tcp', '', ''), (41, 'splitter', NULL, '', 0, 'tcp', '', ''), (42, 'public_sink', NULL, '0.0.0.0', 2220, 'tcp', '', 'proxy_v2'), (43, 'client_sink', 2, '127.0.0.1', 7400, 'tcp', '', 'proxy_v2'), (44, 'client_source', 1, '127.0.0.1', 9401, 'udp', '', ''), (45, 'public_sink', NULL, '0.0.0.0', 2221, 'udp', '', 'proxy_v2'), (46, 'tunnel_source', NULL, '10.77.0.2', 9402, 'tcp', 'tun7', ''), (47, 'public_sink', NULL, '0.0.0.0', 2224, 'tcp', '', 'proxy_v2'), (48, 'client_source', 1, '127.0.0.1', 9410, 'tcp', '', ''), (49, 'public_sink', NULL, '0.0.0.0', 2230, 'tcp', '', 'transparent'), (50, 'client_source', 1, '127.0.0.1', 9411, 'udp', '', ''), (51, 'public_sink', NULL, '0.0.0.0', 2231, 'udp', '', 'transparent'), (52, 'tunnel_source', NULL, '10.77.0.2', 9402, 'tcp', 'tun7', ''), (53, 'public_sink', NULL, '0.0.0.0', 2232, 'tcp', '', 'transparent'); INSERT INTO links (from_node, to_node) VALUES (40, 41), (41, 42), (41, 43), (44, 45), (46, 47), (48, 49), (50, 51), (52, 53);\"" on target 'echo RELOAD | socat - UNIX-CONNECT:/run/patchbay/api.sock' >/dev/null sleep 3 c2ip=$(on target 'getent ahostsv4 client2 | head -1 | cut -d" " -f1') c2hex=$(printf '%02x' $(echo "$c2ip" | tr . ' ')) sig=0d0a0d0a000d0a515549540a r=$(on client2 'echo x | socat -t3 - TCP:target:2220') [ "${r:0:32}" = "${sig}2111000c" ] && [ "${r:32:8}" = "$c2hex" ] && ok "PROXY v2 via public sink (TCP)" || bad "PROXY v2 public TCP: got '$r', want src $c2hex" r=$(on client2 'echo x | socat -t3 - TCP:127.0.0.1:7400') [ "${r:0:32}" = "${sig}2111000c" ] && [ "${r:32:8}" = 7f000001 ] && ok "PROXY v2 via client sink" || bad "PROXY v2 client sink: got '$r'" r=$(on client2 'echo x | socat -T3 - UDP:target:2221') [ "${r:0:32}" = "${sig}2112000c" ] && [ "${r:32:8}" = "$c2hex" ] && ok "PROXY v2 via public sink (UDP)" || bad "PROXY v2 public UDP: got '$r'" r=$(on client2 'echo x | socat -t3 - TCP:target:2224') [ "${r:0:32}" = "${sig}2111000c" ] && [ "${r:32:8}" = "$c2hex" ] && ok "PROXY v2 from target tunnel source" || bad "PROXY v2 target source: got '$r'" r=$(on client2 'echo x | socat -t3 - TCP:target:2230') [ "${r%%:*}" = "$c2ip" ] && ok "transparent spoofing (TCP): service sees $r" || bad "spoofing TCP: service saw '$r', want $c2ip" r=$(on client2 'echo x | socat -T3 - UDP:target:2231') [ "${r%%:*}" = "$c2ip" ] && ok "transparent spoofing (UDP): service sees $r" || bad "spoofing UDP: service saw '$r', want $c2ip" r=$(on target 'echo STATUS | socat - UNIX-CONNECT:/run/patchbay/api.sock') echo "$r" | grep -q '"53":{"ok":false,[^}]*needs a source on a client' && ok "spoofing refused for a target source" || bad "spoofing on target source: $r" # Web login over HTTPS with the emailed (logged) code. r=$(on target 'set -e J=/tmp/jar; rm -f $J; U=https://127.0.0.1:8443 tok=$(curl -sk -c $J -b $J $U/login | sed -n "s/.*csrf-token\" content=\"\([^\"]*\)\".*/\1/p") curl -sk -c $J -b $J -o /dev/null --data-urlencode "csrf_token=$tok" -d username=admin -d password=patchbay-test-pw $U/login echo $tok' 2>&1) sleep 1 code=$($DC logs target 2>&1 | sed -n 's/.*login code is: \([0-9]\{6\}\).*/\1/p' | tail -1) r=$(on target "set -e J=/tmp/jar; U=https://127.0.0.1:8443 tok=\$(curl -sk -c \$J -b \$J \$U/verify | sed -n 's/.*csrf-token\" content=\"\([^\"]*\)\".*/\1/p') curl -sk -c \$J -b \$J -o /dev/null -d csrf_token=\$tok -d code=$code \$U/verify curl -sk -c \$J -b \$J \$U/api/graph") echo "$r" | grep -q '"client_source"' && ok "web login with email code + API" || bad "web login: code='$code' $r" echo echo "$PASS passed, $FAIL failed" [ $FAIL = 0 ]