├── .gitignore ├── README.md ├── index.js ├── package.json └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Looking Glass 2 | 3 | This is a Workers script that calls the Enterprise Plan API which runs traceroute from 200+ Cloudflare data centers. 4 | 5 | ## Getting Started 6 | 7 | Edit `wrangler.toml` file, add your Cloudflare account ID, email, zone ID and the route you want the Workers script to be deployed to. 8 | 9 | Add Cloudflare API key as a secret variable: 10 | 11 | ``` 12 | wrangler secret put API_KEY 13 | ``` 14 | 15 | The script can be deployed by running: 16 | 17 | ``` 18 | wrangler publish 19 | ``` 20 | 21 | # How to Use 22 | 23 | Add the parameters in the query string. 24 | 25 | | Parameter | Definition | Example | 26 | | ------------- |:-------------:|:-------------:| 27 | | targets | hostname or IP address, separated by comma | 1.1.1.1,google.com | 28 | | colos | Cloudflare data center in IATA code of the nearest major international airport, separated by comma | sin02,nrt01 | 29 | | packets_per_ttl | number of packets sent at each TTL. min:0, max:10 | 5 | 30 | | packet_type | icmp, tcp, udp, gre, gre+icmp | icmp | 31 | | max_ttl | Maximum time to live. min:0, max:64 | 10 | 32 | | wait_time | time in seconds to wait for a response. min:1, max:5 | 1 | 33 | | port | For UDP and TCP, specifies the destination port. For ICMP, specifies the initial ICMP sequence value. min/default:0 (will choose the best value to use for each protocol), max:65535 | 443 | 34 | 35 | Example: 36 | 37 | ``` 38 | curl -s "https://faizazhar.com/projects/traceroute?targets=1.1.1.1%2Cgoogle.com&colos=sin02%2Cnrt01&packet_type=icmp" 39 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | addEventListener("fetch", event => { 2 | event.respondWith(eventHandler(event)) 3 | }) 4 | 5 | const account_id = ACCOUNT_ID 6 | const endpoint = 'https://api.cloudflare.com/client/v4/accounts/' + ACCOUNT_ID + '/diagnostics/traceroute' 7 | const email = EMAIL 8 | const api_key = API_KEY 9 | let targets = ['1.1.1.1'] 10 | let colos = [] 11 | let options = { 12 | "packets_per_ttl": 5, 13 | "packet_type": "tcp", 14 | "max_ttl": 15, 15 | "wait_time": 1, 16 | "port": 443 17 | } 18 | 19 | async function eventHandler(event) { 20 | try { 21 | //get params from query strings 22 | const params = {} 23 | const url = new URL(event.request.url) 24 | const queryString = url.search.slice(1).split('&') 25 | 26 | //format params 27 | queryString.forEach(item => { 28 | const kv = item.split('=') 29 | if (kv[0]) params[kv[0]] = kv[1] || true 30 | }) 31 | console.log(JSON.stringify(params)) 32 | 33 | //if targets is defined in query strings 34 | if (params.targets) { 35 | //remove the default targets value 36 | targets.pop() 37 | //split the param values into an array 38 | const target = decodeURIComponent(params.targets).split(',') 39 | //push each items in the array into targets arrray 40 | target.forEach(item => { 41 | targets.push(item) 42 | }) 43 | } 44 | 45 | const cf = await event.request.cf 46 | 47 | //if colos is defined in query strings 48 | if (params.colos) { 49 | //split the param values into an array 50 | const colo = decodeURIComponent(params.colos).split(',') 51 | //push each items in the array into colos arrray 52 | colo.forEach(item => { 53 | colos.push(item.toLowerCase()) 54 | }) 55 | //if colos is defined in query strings but no values set 56 | if (params.colos === true) { 57 | colos.pop() 58 | } 59 | } else { 60 | //get the eyeball colo from request.cf object and push to target colos 61 | colos.push(cf.colo.toLowerCase() + '01') 62 | } 63 | 64 | //if packets_per_ttl is defined in query strings 65 | if (params.packets_per_ttl) { 66 | options.packets_per_ttl = parseInt(params.packets_per_ttl) 67 | } 68 | 69 | //if max_ttl is defined in query strings 70 | if (params.max_ttl) { 71 | options.max_ttl = parseInt(params.max_ttl) 72 | } 73 | 74 | //if wait_time is defined in query strings 75 | if (params.wait_time) { 76 | options.wait_time = parseInt(params.wait_time) 77 | } 78 | 79 | //if packet_type is defined in query strings 80 | if (params.packet_type) { 81 | options.packet_type = params.packet_type.toLowerCase() 82 | } 83 | 84 | //if port is defined in query strings 85 | if (params.port) { 86 | options.port = parseInt(params.port) 87 | } 88 | 89 | const body = { 90 | "targets": targets, 91 | "colos": colos, 92 | "options": options 93 | } 94 | console.log(body) 95 | 96 | const init = { 97 | body: JSON.stringify(body), 98 | method: 'POST', 99 | headers: { 100 | 'content-type': 'application/json', 101 | 'X-Auth-Email': email, 102 | 'X-Auth-Key': api_key 103 | }, 104 | } 105 | const response = await fetch(endpoint, init) 106 | return response 107 | } catch (e) { 108 | console.error(e.stack) 109 | return `${e.message}` 110 | } 111 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "cf-looking-glass", 4 | "version": "1.0.0", 5 | "description": "Run traceroute from Cloudflare data centers, invoked from API via Workers.", 6 | "main": "index.js", 7 | "author": "Faiz Azhar ", 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cf-looking-glass" 2 | type = "javascript" 3 | account_id = "" 4 | zone_id = "" 5 | workers_dev = false 6 | route = "" 7 | vars = {ACCOUNT_ID = "", EMAIL = ""} --------------------------------------------------------------------------------