├── .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 ![Site status](https://github.com/radiantly/udpquiz/actions/workflows/site.yml/badge.svg) 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 | UDP Quiz 12 | 57 | 58 | 59 |
60 |

Outgoing udp port tester

61 | Site status 65 |

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 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /bpf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "bpf/bpf_helpers.h" 11 | 12 | #ifdef DEBUG 13 | #define trace_printk(fmt, ...) \ 14 | do { \ 15 | char _fmt[] = fmt; \ 16 | bpf_trace_printk(_fmt, sizeof(_fmt), ##__VA_ARGS__); \ 17 | } while (0) 18 | #else 19 | #define trace_printk(fmt, ...) 20 | #endif 21 | 22 | SEC("classifier") 23 | int cls_main(struct __sk_buff *skb) { 24 | /* packet data is stored in skb->data */ 25 | void *data = (void *)(long)skb->data; 26 | 27 | /* first we check that the packet has enough data to contain the eth, ip and udp headers */ 28 | if (sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + skb->data > skb->data_end) 29 | return TC_ACT_UNSPEC; 30 | 31 | struct ethhdr *eth = data; 32 | struct iphdr *ip = (data + sizeof(struct ethhdr)); 33 | struct udphdr *udp = (data + sizeof(struct ethhdr) + sizeof(struct iphdr)); 34 | 35 | /* Check if ip packet */ 36 | if (eth->h_proto != __constant_htons(ETH_P_IP)) 37 | return TC_ACT_UNSPEC; 38 | 39 | /* Check if udp packet */ 40 | if (ip->protocol != IPPROTO_UDP) 41 | return TC_ACT_UNSPEC; 42 | 43 | /* We'll store the mac addresses (L2) */ 44 | __u8 src_mac[ETH_ALEN]; 45 | __u8 dst_mac[ETH_ALEN]; 46 | 47 | memcpy(src_mac, eth->h_source, ETH_ALEN); 48 | memcpy(dst_mac, eth->h_dest, ETH_ALEN); 49 | 50 | /* ip addresses (L3) */ 51 | __be32 src_ip = ip->saddr; 52 | __be32 dst_ip = ip->daddr; 53 | 54 | trace_printk("FROM: %d...%d", src_ip & 0xff, src_ip >> 24); 55 | 56 | // ignore private ip ranges 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 57 | if ((src_ip & 0xff) == 10 || ((src_ip & 0xff) == 127) || (src_ip & 0xf0ff) == 0x10ac || (src_ip & 0xffff) == 0xa8c0 || 58 | (dst_ip & 0xff) == 10 || ((dst_ip & 0xff) == 127) || (dst_ip & 0xf0ff) == 0x10ac || (dst_ip & 0xffff) == 0xa8c0) 59 | return TC_ACT_UNSPEC; 60 | 61 | /* and source/destination ports (L4) */ 62 | __be16 dest_port = udp->dest; 63 | __be16 src_port = udp->source; 64 | 65 | /* and then swap them all */ 66 | 67 | /* Swap the mac addresses */ 68 | bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_source), dst_mac, ETH_ALEN, 0); 69 | bpf_skb_store_bytes(skb, offsetof(struct ethhdr, h_dest), src_mac, ETH_ALEN, 0); 70 | 71 | /* Swap the ip addresses 72 | * swapping the ips does not require checksum recalculation, 73 | * but we might want to reduce the TTL to prevent packets infinitely looping between us and another device that does not reduce the TTL */ 74 | bpf_skb_store_bytes(skb, sizeof(struct ethhdr) + offsetof(struct iphdr, saddr), &dst_ip, sizeof(dst_ip), 0); 75 | bpf_skb_store_bytes(skb, sizeof(struct ethhdr) + offsetof(struct iphdr, daddr), &src_ip, sizeof(src_ip), 0); 76 | 77 | /* Swap the source and destination ports in the udp packet */ 78 | bpf_skb_store_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr) + offsetof(struct udphdr, source), &dest_port, sizeof(dest_port), 0); 79 | bpf_skb_store_bytes(skb, sizeof(struct ethhdr) + sizeof(struct iphdr) + offsetof(struct udphdr, dest), &src_port, sizeof(src_port), 0); 80 | 81 | /* And then send it back from wherever it's come from */ 82 | bpf_clone_redirect(skb, skb->ifindex, 0); 83 | 84 | /* Since we've handled the packet, drop it */ 85 | return TC_ACT_SHOT; 86 | } 87 | 88 | char __license[] SEC("license") = "GPL"; 89 | --------------------------------------------------------------------------------