├── .github ├── workflows │ └── site.yml └── test_udp.py ├── Makefile ├── README.md ├── public └── index.html └── bpf.c /.github/workflows/site.yml: -------------------------------------------------------------------------------- 1 | name: udpquiz.com 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | site_check: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - run: | 16 | python3 -V 17 | python3 -m pip install pandas plotille 18 | python3 .github/test_udp.py udpquiz.com 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # If the DEVICE env variable is not set, choose first non-loopback/non-virt interface 2 | DEVICE ?= $(shell ip -br l | awk '$$1 !~ "lo|vir" { print $$1; exit }') 3 | 4 | all: qdisc 5 | 6 | bpf.o: bpf.c 7 | clang $(DEBUG) -O2 -target bpf $< -c -o $@ 8 | 9 | debug: DEBUG = -DDEBUG 10 | 11 | debug: all 12 | -sudo tc exec bpf dbg 13 | 14 | qdisc-del: 15 | -sudo tc qdisc del dev $(DEVICE) clsact 16 | 17 | qdisc: qdisc-del bpf.o 18 | sudo tc qdisc add dev $(DEVICE) clsact && \ 19 | sudo tc filter add dev $(DEVICE) ingress bpf direct-action obj bpf.o 20 | 21 | clean: qdisc-del 22 | -rm bpf.o 23 | 24 | .PHONY: all debug qdisc qdisc-del clean 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UDPQuiz  2 | 3 | UDPQuiz is an eBPF program that runs on Linux to send back all udp packets as is. This makes it a perfect tool to test which udp ports have been firewalled on your network. 4 | 5 | Visit https://udpquiz.com/ to try it out! 6 | 7 | ## Running locally 8 | 9 | ```sh 10 | # Replace eth0 with your interface name 11 | export DEVICE=eth0 12 | 13 | # Compile and load 14 | make 15 | 16 | # Once complete, the filter and qdisc can be removed 17 | make clean 18 | ``` 19 | 20 | ## References 21 | 22 | - [Send ICMP Echo Replies using eBPF](https://fnordig.de/2017/03/04/send-icmp-echo-replies-using-ebpf/) 23 | - [Bypassing Captive Portals](https://blog.chebro.dev/posts/bypassing-captive-portals) 24 | - [tc direct action mode for BPF](https://qmonnet.github.io/whirl-offload/2020/04/11/tc-bpf-direct-action/) 25 | -------------------------------------------------------------------------------- /.github/test_udp.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import random 3 | import sys 4 | from concurrent.futures import ThreadPoolExecutor, as_completed 5 | from socket import AF_INET, SOCK_DGRAM, gethostbyname, socket 6 | from time import perf_counter 7 | 8 | import pandas as pd 9 | import plotille 10 | 11 | # Configuration options 12 | PACKETS_TO_SEND = 1000 13 | WORKERS = 16 14 | FAILURE_THRESHOLD = 0.02 15 | SOCKET_TIMEOUT = 1 16 | 17 | 18 | def sendPacket(host): 19 | sock = socket(AF_INET, SOCK_DGRAM) 20 | sock.settimeout(SOCKET_TIMEOUT) 21 | 22 | start_time = perf_counter() 23 | sock.sendto(b"ping", (host, random.randrange(1, 65535))) 24 | try: 25 | data, _ = sock.recvfrom(1024) 26 | end_time = perf_counter() 27 | assert data == b"ping" 28 | except: 29 | return -1 30 | 31 | return (end_time - start_time) * 1000 32 | 33 | 34 | def main(): 35 | parser = argparse.ArgumentParser() 36 | parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true") 37 | parser.add_argument("host", help="The host to test") 38 | 39 | args = parser.parse_args() 40 | 41 | host = gethostbyname(args.host) 42 | 43 | rtt = [] 44 | failures = 0 45 | with ThreadPoolExecutor(max_workers=WORKERS) as executor: 46 | print(f"Sending {PACKETS_TO_SEND} packets to {host}") 47 | futures = [executor.submit(sendPacket, host) for _ in range(PACKETS_TO_SEND)] 48 | 49 | for future in as_completed(futures): 50 | result = future.result() 51 | if result == -1: 52 | failures += 1 53 | else: 54 | rtt.append(result) 55 | 56 | if args.verbose: 57 | print( 58 | f"\r{failures + len(rtt)}/{PACKETS_TO_SEND} packets sent (failures: {failures})", 59 | flush=True, 60 | end="", 61 | ) 62 | 63 | print("\r", end="") 64 | print(plotille.hist(rtt, bins=10)) 65 | 66 | print(f"{failures} failures") 67 | print(pd.Series(rtt).describe()) 68 | 69 | # Return non-zero exit code if failure% > FAILURE_THRESHOLD 70 | if failures > PACKETS_TO_SEND * FAILURE_THRESHOLD: 71 | sys.exit(1) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 11 |66 | This server listens on all udp ports, and sends back all udp packets 67 | sent to it. 68 |
69 |70 | With Nmap installed, you could use a 71 | command such as the following to check the open outgoing udp ports on 72 | your network: 73 |
74 |nmap -v -sU -p- --min-rate=1000 udpquiz.com
75 | 76 | Source 77 |
78 |