├── .github └── workflows │ ├── backend.yml │ └── frontend.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── Caddyfile ├── acme │ ├── acme_post_hook.sh │ ├── acme_pre_hook.sh │ ├── deploy_cert.sh │ ├── proxy │ │ ├── auth.py │ │ ├── deploy-all.sh │ │ └── request.sh │ └── request_cert.sh ├── api.py ├── backup.sh ├── cdnctl.sh ├── check_ns.py ├── config.example.yml ├── config.py ├── exporter │ ├── go.mod │ ├── go.sum │ └── main.go ├── import-zone.py ├── orchestrator.py ├── packetframe-api.service ├── packetframe-orchestrator.service ├── packetframectl.py ├── provisioning │ ├── bird.j2 │ ├── install.yml │ ├── named.conf.options.j2 │ ├── network.sh.j2 │ ├── status.yml │ └── varnish.service.j2 ├── requirements.txt ├── templates │ ├── Caddyfile.j2 │ ├── collector_bird.j2 │ ├── default.vcl.j2 │ ├── local.j2 │ ├── nameserver_issue.j2 │ ├── new_domain.j2 │ ├── proxied_record.j2 │ ├── welcome.j2 │ └── zone.j2 ├── update-soas.py └── utils.py ├── eca ├── Makefile ├── delivr-eca.service └── eca.py ├── frontend ├── .gitignore ├── .idea │ ├── .gitignore │ ├── cdn-frontend.iml │ ├── dictionaries │ │ └── nate.xml │ ├── misc.xml │ ├── modules.xml │ └── vcs.xml ├── package-lock.json ├── package.json ├── public │ ├── global.css │ ├── index.html │ └── static │ │ └── img │ │ ├── arrow.png │ │ ├── check.png │ │ ├── cross.png │ │ ├── full.png │ │ ├── icon.png │ │ ├── laptop.png │ │ ├── logo.png │ │ ├── main │ │ ├── cache.svg │ │ ├── code.svg │ │ ├── code2.svg │ │ ├── code3.svg │ │ ├── community.svg │ │ ├── dashboard.svg │ │ ├── docs.svg │ │ ├── hacker.svg │ │ ├── privacy.svg │ │ ├── server.svg │ │ ├── together.svg │ │ └── world.svg │ │ └── server.png ├── rollup.config.js ├── scripts │ └── setupTypeScript.js ├── src │ ├── App.svelte │ ├── blog │ │ ├── 2020-10-22-hello-world.md │ │ └── 2020-11-22-outage.md │ ├── components │ │ ├── Banner.svelte │ │ ├── Button.svelte │ │ ├── ButtonBar.svelte │ │ ├── CheckBoxes.svelte │ │ ├── Dropdown.svelte │ │ ├── Email.svelte │ │ ├── EmailSignupForm.svelte │ │ ├── EntryForm.svelte │ │ ├── Navbar.svelte │ │ ├── NetworkMap.svelte │ │ ├── NetworkTable.svelte │ │ ├── NumberInput.svelte │ │ ├── RecordTable.svelte │ │ ├── Settings.svelte │ │ ├── Snackbar.svelte │ │ ├── SnackbarGroup.svelte │ │ ├── Table.svelte │ │ ├── TextInput.svelte │ │ ├── ToggleButton.svelte │ │ └── UserTable.svelte │ ├── docs │ │ ├── api.md │ │ ├── cachingProxy.md │ │ ├── cli.md │ │ ├── dashboard.md │ │ ├── eca.md │ │ ├── privacyPolicy.md │ │ └── security.md │ ├── main.js │ ├── mdp.config.json │ ├── pages │ │ ├── Community.svelte │ │ ├── Dashboard.svelte │ │ ├── Docs.svelte │ │ ├── Index.svelte │ │ ├── Login.svelte │ │ ├── NotFound.svelte │ │ └── Signup.svelte │ ├── stores.js │ └── utils.js └── yarn.lock └── util ├── pfctl.py └── requirements.txt /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: Deploy backend 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - "backend/**" 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Copy files to server 18 | uses: appleboy/scp-action@master 19 | with: 20 | host: ${{ secrets.HOST }} 21 | username: ${{ secrets.USERNAME }} 22 | password: ${{ secrets.PASSWORD }} 23 | port: ${{ secrets.PORT }} 24 | source: "backend" 25 | target: "/home/nate/" 26 | 27 | - name: Reload Caddy config 28 | uses: appleboy/ssh-action@master 29 | with: 30 | host: ${{ secrets.HOST }} 31 | username: ${{ secrets.USERNAME }} 32 | password: ${{ secrets.PASSWORD }} 33 | port: ${{ secrets.PORT }} 34 | script: sudo caddy reload -config /etc/caddy/Caddyfile 35 | 36 | - name: Restart backend services 37 | uses: appleboy/ssh-action@master 38 | with: 39 | host: ${{ secrets.HOST }} 40 | username: ${{ secrets.USERNAME }} 41 | password: ${{ secrets.PASSWORD }} 42 | port: ${{ secrets.PORT }} 43 | script: sudo systemctl daemon-reload ; sudo systemctl restart packetframe-orchestrator ; sudo systemctl restart packetframe-api ; sudo caddy reload -config /etc/caddy/Caddyfile 44 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy frontend 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - "frontend/**" 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Set up NodeJS 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: "12.x" 21 | 22 | - name: Build frontend 23 | run: cd frontend/ ; yarn install ; yarn build 24 | 25 | - name: Copy files to server 26 | uses: appleboy/scp-action@master 27 | with: 28 | host: ${{ secrets.HOST }} 29 | username: ${{ secrets.USERNAME }} 30 | password: ${{ secrets.PASSWORD }} 31 | port: ${{ secrets.PORT }} 32 | source: "frontend/public/" 33 | target: "/home/nate/" 34 | strip_components: 1 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | ssh-key* 3 | provisioning/hosts.yml 4 | provisioning/*.retry 5 | provisioning/vars.yml 6 | eca/debian/ 7 | eca/packetframe-eca.deb 8 | backend/exporter/exporter 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
7 | 8 | ## Packetframe 9 | 10 | *This repo stores the first production release of Packetframe, and is now considered legacy software. See https://github.com/packetframe/api and https://github.com/packetframe/web for the current production release.* 11 | 12 | The Packetframe CDN is an open source (AGPL-3.0) anycast CDN platform, currently in beta. If you're interested in an account, send an email to the address listed on the front page of https://packetframe.com or reach out in #packetframe on the [Libera.Chat](https://libera.chat/guides/connect) IRC network. 13 | 14 | #### Project Structure 15 | 16 | ``` 17 | . 18 | ├── backend # All code that runs server side 19 | │ ├── acme # LetsEncrypt automation for ACME challenges 20 | │ ├── provisioning # Ansible playbooks for node deployment 21 | │ └── templates # Per-node config templates used by the orchestrator 22 | └── frontend # Svelte dashboard (https://packetframe.com) 23 | ├── public 24 | │ └── static # Public static assets 25 | ├── scripts 26 | └── src 27 | └── components # Individual Svelte components 28 | ``` 29 | 30 | #### Development 31 | The repo is designed to be tested at the service level. Each daemon or service should be able to be ran and debugged independently of the rest of the network. 32 | 33 | #### Author 34 | [Nate Sales](https://natesales.net) and [friends](https://github.com/packetframe/cdn/graphs/contributors). 35 | 36 | #### License 37 | [AGPLv3](https://github.com/packetframe/cdn/blob/main/LICENSE) 38 | -------------------------------------------------------------------------------- /backend/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | email info@packetframe.com 3 | } 4 | 5 | www.packetframe.com { 6 | redir https://packetframe.com{uri} 7 | } 8 | 9 | packetframe.com { 10 | root * /home/nate/public/ 11 | file_server 12 | encode gzip 13 | 14 | @hashredir not { 15 | path / 16 | path /api/* 17 | path /static/* 18 | path /build/* 19 | path /global.css 20 | } 21 | redir @hashredir /#{path} 22 | 23 | route /api/* { 24 | uri strip_prefix /api 25 | reverse_proxy localhost:5000 26 | } 27 | 28 | log { 29 | output file /var/log/access-packetframe.log { 30 | roll_size 1gb 31 | roll_keep 1 32 | roll_keep_for 7d 33 | } 34 | } 35 | 36 | handle_errors { 37 | @404 { 38 | expression {http.error.status_code} == 404 39 | } 40 | redir @404 /#/404 41 | file_server 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/acme/acme_post_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nodes=$(mongo --quiet --eval 'db.nodes.find().forEach(function(node) {print(node.management_ip);});' cdn) 4 | 5 | for line in $nodes ; do 6 | echo "Removing file from $line" 7 | ssh -p 34553 -i /home/nate/ssh-key root@$line "rm -rf /usr/share/caddy/*" 8 | done 9 | -------------------------------------------------------------------------------- /backend/acme/acme_pre_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nodes=$(mongo --quiet --eval 'db.nodes.find().forEach(function(node) {print(node.management_ip);});' cdn) 4 | 5 | echo $CERTBOT_VALIDATION > /tmp/acme-challenge.txt 6 | 7 | for line in $nodes ; do 8 | echo "Sending validation to $line" 9 | scp -P 34553 -i /home/nate/backend/ssh-key /tmp/acme-challenge.txt root@$line:/usr/share/caddy/$CERTBOT_TOKEN 10 | done 11 | 12 | rm -rf /tmp/acme-challenge.txt 13 | -------------------------------------------------------------------------------- /backend/acme/deploy_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nodes=$(mongo --quiet --eval 'db.nodes.find().forEach(function(node) {print(node.management_ip);});' cdn) 4 | 5 | for line in $nodes ; do 6 | echo "Copying certificates to $line" 7 | scp -P 34553 -i /home/nate/ssh-key /etc/letsencrypt/live/local.packetframe.com/fullchain.pem root@$line:/caddy/fullchain.pem 8 | scp -P 34553 -i /home/nate/ssh-key /etc/letsencrypt/live/local.packetframe.com/privkey.pem root@$line:/caddy/privkey.pem 9 | ssh -p 34553 -i /home/nate/ssh-key root@$line "chown -R caddy:caddy /caddy/* ; caddy reload -config /etc/caddy/Caddyfile" 10 | done 11 | -------------------------------------------------------------------------------- /backend/acme/proxy/auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import requests 5 | import time 6 | 7 | API_KEY = "ADMIN_API_KEY" 8 | 9 | zone = "" 10 | cdn_zone = "" 11 | 12 | r = requests.post("https://packetframe.com/api/zone/" + cdn_zone + "/add", headers={"X-API-Key": API_KEY}, json={ 13 | "type": "TXT", 14 | "ttl": 300, 15 | "label": "_acme-challenge." + zone + ".", 16 | "value": os.environ["CERTBOT_VALIDATION"] 17 | }) 18 | 19 | print(r.text) 20 | print(r.json()) 21 | 22 | time.sleep(60) 23 | -------------------------------------------------------------------------------- /backend/acme/proxy/deploy-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nodes=$(mongo --quiet --eval 'db.nodes.find({"http": true}).forEach(function(node) {print(node.management_ip);});' cdn) 4 | 5 | rm -rf /tmp/certs/ certs.tar.gz 6 | mkdir /tmp/certs/ 7 | 8 | for zone in $(ls /etc/letsencrypt/live/); do 9 | cp /etc/letsencrypt/live/$zone/privkey.pem /tmp/certs/$zone-key.pem 10 | cp /etc/letsencrypt/live/$zone/fullchain.pem /tmp/certs/$zone-chain.pem 11 | done 12 | 13 | tar -cvzf certs.tar.gz /tmp/certs/* 14 | 15 | for node in $nodes ; do 16 | echo "Updating $node" 17 | scp -P 34553 -i /home/nate/ssh-key certs.tar.gz root@$node:/etc/caddy/certs.tar.gz 18 | ssh -p 34553 -i /home/nate/ssh-key root@$node "bash /root/update-certs.sh" 19 | done 20 | -------------------------------------------------------------------------------- /backend/acme/proxy/request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo certbot certonly \ 4 | -d "$1" \ 5 | --manual \ 6 | --preferred-challenges=dns \ 7 | --text \ 8 | --agree-tos \ 9 | --manual-public-ip-logging-ok \ 10 | --keep-until-expiring \ 11 | --email info@packetframe.com \ 12 | --manual-auth-hook $(pwd)/auth.py 13 | # --manual-cleanup-hook $(pwd)/deploy.sh 14 | -------------------------------------------------------------------------------- /backend/acme/request_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo certbot certonly \ 3 | --manual \ 4 | --preferred-challenges http-01 \ 5 | -m info@packetframe.com \ 6 | --agree-tos \ 7 | --manual-public-ip-logging-ok \ 8 | --manual-auth-hook /home/nate/backend/acme/acme_pre_hook.sh \ 9 | --manual-cleanup-hook /home/nate/backend/acme/acme_post_hook.sh \ 10 | -d local.packetframe.com -------------------------------------------------------------------------------- /backend/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BACKUP_FILE="/var/log/packetframe-backup.txt" 4 | 5 | echo "Backup at $(date)" > $BACKUP_FILE 6 | mongo --quiet --eval 'db.nodes.find();' cdn >> $BACKUP_FILE 7 | mongo --quiet --eval 'db.zones.find();' cdn >> $BACKUP_FILE 8 | mongo --quiet --eval 'db.users.find();' cdn >> $BACKUP_FILE 9 | -------------------------------------------------------------------------------- /backend/cdnctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SSH_KEY="/home/nate/ssh-key" 4 | 5 | echo -n Jumping to node $1 6 | node_ip=$(mongo --quiet --eval "db.nodes.findOne({name: \"${1}\"})[\"management_ip\"]" cdn) 7 | echo " - $node_ip" $(mongo --quiet --eval "db.nodes.findOne({name: \"${1}\"})[\"provider\"]" cdn) 8 | 9 | ssh -i $SSH_KEY -p 34553 root@$node_ip 10 | -------------------------------------------------------------------------------- /backend/check_ns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import sys 3 | 4 | from jinja2 import Template 5 | from pymongo import MongoClient 6 | import dns.resolver 7 | import time 8 | from config import configuration 9 | from api import add_queue_message 10 | 11 | try: 12 | send_emails = (sys.argv[1] == "send-emails") 13 | except IndexError: 14 | send_emails = False 15 | 16 | print("Send emails:", send_emails) 17 | 18 | correct_nameservers = [(ns + ".") for ns in configuration["dns"]["nameservers"]] 19 | 20 | with open("templates/nameserver_issue.j2", "r") as nameserver_issue_template_file: 21 | # noinspection JinjaAutoinspect 22 | nameserver_issue_template = Template(nameserver_issue_template_file.read()) 23 | 24 | 25 | def valid_nameservers(arr1): 26 | if len(arr1) != len(correct_nameservers): 27 | return False 28 | 29 | arr1.sort() 30 | correct_nameservers.sort() 31 | 32 | for i in range(0, len(arr1) - 1): 33 | if arr1[i] != correct_nameservers[i]: 34 | return False 35 | 36 | return True 37 | 38 | 39 | db = MongoClient("mongodb://localhost:27017")["cdn"] 40 | 41 | 42 | def ns_query(label): 43 | try: 44 | return None, dns.resolver.resolve(label, "NS") 45 | except Exception as e: 46 | return str(e), None 47 | 48 | 49 | for zone in db["zones"].find(): 50 | error_message = None 51 | print(f"Checking {zone['zone']}...", end="", flush=True) 52 | err, answers = ns_query(zone["zone"]) 53 | if not err: 54 | nameservers = [str(ns) for ns in answers.rrset] 55 | 56 | if valid_nameservers(nameservers): 57 | print("\033[92mOK\033[0m") 58 | else: 59 | error_message = ("Incorrect nameservers: " + ", ".join(nameservers)) 60 | else: 61 | error_message = str(err) 62 | 63 | if error_message: 64 | print("\033[91m" + error_message + "\033[0m") 65 | 66 | if send_emails: 67 | template = nameserver_issue_template.render( 68 | nameservers=configuration["dns"]["nameservers"], 69 | domain=zone["zone"], 70 | error=error_message 71 | ) 72 | 73 | print("Sending email to", zone["users"]) 74 | add_queue_message("send_email", args={"recipients": zone["users"], "subject": "[PacketFrame] Attention Needed: nameserver update", "body": template}) 75 | 76 | time.sleep(0.1) 77 | -------------------------------------------------------------------------------- /backend/config.example.yml: -------------------------------------------------------------------------------- 1 | # config.yml 2 | 3 | development: true 4 | ssh-key: /home/nate/ssh-key 5 | 6 | dns: 7 | rname: info.example.com 8 | nameservers: 9 | - ns1.example.dev 10 | - ns2.example.dev 11 | prefix4: 203.0.113.0/24 12 | prefix6: 2001:db8:2::/48 13 | bind_addresses4: 14 | - 203.0.113.2 15 | - 203.0.113.3 16 | bind_addresses6: 17 | - 2001:db8:2::2 18 | - 2001:db8:2::3 19 | network_config: | 20 | #!/bin/bash 21 | ip link del local 22 | ip link add local type dummy 23 | ip addr add 203.0.113.2/24 dev local 24 | ip addr add 203.0.113.3/24 dev local 25 | ip addr add 2001:db8:2::2/48 dev local 26 | ip addr add 2001:db8:2::3/48 dev local 27 | ip addr add 2001:db8:2::4/48 dev local 28 | ip link set dev local up 29 | 30 | email: 31 | username: noreply@example.com 32 | password: email-password 33 | server: mail.example.com 34 | 35 | proxy: 36 | server4: 192.0.2.100 37 | server6: 2001:db8::100 38 | 39 | prefix4: 192.0.2.0/23 40 | prefix6: 2001:db8::/47 41 | 42 | network_config: | 43 | #!/bin/bash 44 | ip link del local 45 | ip link add local type dummy 46 | ip addr add 192.0.2.100/23 dev local 47 | ip addr add 2001:db8::100/47 dev local 48 | ip link set dev local up 49 | 50 | # Hosts allowed to access caches (controller and monitoring IPs) 51 | allowed_hosts: 52 | - 192.0.2.2 53 | - 192.0.2.5 54 | 55 | monitoring_host: user@monitoring.example.com 56 | 57 | collector: 58 | host: 192.0.2.100 59 | asn: 65530 60 | -------------------------------------------------------------------------------- /backend/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import yaml 4 | 5 | CONFIG_FILE = "/home/nate/backend/config.yml" 6 | 7 | if not os.path.exists(CONFIG_FILE): 8 | print("No config.yml file found.") 9 | exit(1) 10 | 11 | with open(CONFIG_FILE, "r") as config_file: 12 | configuration = yaml.safe_load(config_file.read()) 13 | -------------------------------------------------------------------------------- /backend/exporter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/packetframe/cdn/backend/exporter 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/prometheus/client_golang v1.9.0 7 | github.com/sirupsen/logrus v1.7.0 8 | go.mongodb.org/mongo-driver v1.4.5 9 | ) 10 | -------------------------------------------------------------------------------- /backend/exporter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/promauto" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | log "github.com/sirupsen/logrus" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | "net/http" 14 | "os" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | const updateDelay = 10 * time.Second 20 | 21 | var ( 22 | listenAddr = flag.String("web.listen-addr", ":9786", "Listening Address") 23 | mongoAddr = flag.String("db", "mongodb://localhost:27017", "MongoDB host") 24 | ) 25 | 26 | var ( 27 | lastUpdate = promauto.NewGauge(prometheus.GaugeOpts{ 28 | Name: "packetframe_last_update", 29 | Help: "PacketFrame last update", 30 | }) 31 | 32 | nodes = promauto.NewGaugeVec( 33 | prometheus.GaugeOpts{ 34 | Name: "packetframe_node_status", 35 | Help: "PacketFrame Node Status", 36 | }, 37 | []string{"node", "latitude", "longitude"}, 38 | ) 39 | ) 40 | 41 | func main() { 42 | flag.Parse() 43 | 44 | if *listenAddr == "" { 45 | flag.Usage() 46 | os.Exit(1) 47 | } 48 | 49 | log.Println("connecting to database") 50 | client, err := mongo.NewClient(options.Client().ApplyURI(*mongoAddr)) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 55 | err = client.Connect(ctx) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | defer client.Disconnect(ctx) 60 | 61 | nodesCollection := client.Database("cdn").Collection("nodes") 62 | 63 | go func() { 64 | for { 65 | cursor, err := nodesCollection.Find(context.TODO(), bson.D{}) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | for cursor.Next(context.TODO()) { 71 | var node map[string]interface{} 72 | err := cursor.Decode(&node) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | //// ping node management IP 78 | //log.Infof("pinging %s\n", node["management_ip"].(string)) 79 | //pinger, err := ping.NewPinger(node["management_ip"].(string)) 80 | //if err != nil { 81 | // log.Warnln(err) 82 | //} 83 | //pinger.Count = 1 84 | //pinger.SetPrivileged(true) 85 | // 86 | //err = pinger.Run() 87 | //if err != nil { 88 | // log.Warnf("ping run: %v\n", err) 89 | //} 90 | // 91 | //statusCode := 0 // 0 for failure 92 | //pinger.OnFinish = func(stats *ping.Statistics) { 93 | // log.Printf("%s done\n", node["name"]) 94 | // if stats.PacketsSent == stats.PacketsRecv { 95 | // log.Printf("%s working\n", node["name"]) 96 | // statusCode = 1 97 | // } 98 | //} 99 | 100 | coords := strings.Split(node["geoloc"].(string), ", ") 101 | 102 | // set labels and status code 103 | nodes.With( 104 | prometheus.Labels{ 105 | "node": node["name"].(string), 106 | "latitude": coords[0], 107 | "longitude": coords[1], 108 | }, 109 | ).Set(1) 110 | } 111 | 112 | if err := cursor.Err(); err != nil { 113 | log.Warnln(err) 114 | } 115 | 116 | // close cursor connection 117 | cursor.Close(context.TODO()) 118 | 119 | // set last updated time 120 | lastUpdate.SetToCurrentTime() 121 | time.Sleep(updateDelay) 122 | } 123 | }() 124 | 125 | http.Handle("/metrics", promhttp.Handler()) 126 | log.Infof("Starting exporter: http://%s/metrics", *listenAddr) 127 | log.Fatal(http.ListenAndServe(*listenAddr, nil)) 128 | } 129 | -------------------------------------------------------------------------------- /backend/import-zone.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import requests 3 | from dns.rdatatype import RdataType 4 | import dns.zone 5 | import sys 6 | 7 | domain = sys.argv[1] 8 | dryrun = 0 9 | 10 | apikey = "ADMIN_API_KEY" 11 | 12 | 13 | def _post_record(domain, data): 14 | data["proxied"] = False 15 | if dryrun: 16 | print(data) 17 | else: 18 | r = requests.post("https://packetframe.com/api/zone/" + domain + "/add", json=data, headers={"X-API-Key": apikey}) 19 | if not r.json()["success"]: 20 | print(data, r.json()) 21 | 22 | 23 | zone = dns.zone.from_file(domain) 24 | 25 | for name, node in zone.items(): 26 | if str(name) == "@": 27 | name = domain + "." 28 | else: 29 | name = str(name) + "." + domain + "." 30 | 31 | for rdataset in node.rdatasets: 32 | for rdata in rdataset: 33 | if rdataset.rdtype == RdataType.A: 34 | _post_record(domain, { 35 | "label": name, 36 | "ttl": 3600, 37 | "type": "A", 38 | "value": str(rdata.address) 39 | }) 40 | elif rdataset.rdtype == RdataType.AAAA: 41 | _post_record(domain, { 42 | "label": name, 43 | "ttl": 3600, 44 | "type": "AAAA", 45 | "value": str(rdata.address) 46 | }) 47 | elif rdataset.rdtype == RdataType.MX: 48 | _post_record(domain, { 49 | "label": name, 50 | "ttl": 3600, 51 | "type": "MX", 52 | "value": str(rdata.exchange), 53 | "priority": int(rdata.preference) 54 | }) 55 | elif rdataset.rdtype == RdataType.CNAME: 56 | _post_record(domain, { 57 | "label": name, 58 | "ttl": 3600, 59 | "type": "CNAME", 60 | "value": str(rdata.target) 61 | }) 62 | elif rdataset.rdtype == RdataType.SRV: 63 | _post_record(domain, { 64 | "label": name, 65 | "ttl": 3600, 66 | "type": "SRV", 67 | "priority": str(rdata.priority), 68 | "weight": str(rdata.weight), 69 | "port": str(rdata.port), 70 | "value": str(rdata.target) 71 | }) 72 | elif rdataset.rdtype == RdataType.TXT: 73 | _post_record(domain, { 74 | "label": name, 75 | "ttl": 3600, 76 | "type": "TXT", 77 | "value": str(rdata) 78 | }) 79 | else: 80 | print("Skipped", rdataset.rdtype) 81 | -------------------------------------------------------------------------------- /backend/packetframe-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=PacketFrame CDN API 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | User=nate 9 | WorkingDirectory=/home/nate/backend/ 10 | ExecStart=python3 /home/nate/backend/api.py 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /backend/packetframe-orchestrator.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=PacketFrame CDN Orchestrator 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | User=nate 9 | WorkingDirectory=/home/nate/backend/ 10 | ExecStart=python3 /home/nate/backend/orchestrator.py 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /backend/packetframectl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # CLI utility for the PacketFrame CDN 3 | 4 | import os 5 | import sys 6 | import requests 7 | from terminaltables import SingleTable 8 | 9 | help_text = """Usage: packetframectl command category [args] 10 | 11 | Commands: 12 | get Retrieve data 13 | 14 | Categories: 15 | zones Get list of zones 16 | records zone Get records for zone 17 | acl Get user ACL 18 | users zone Get users for zone 19 | """ 20 | 21 | API_KEY = os.environ.get("PACKETFRAME_API_KEY") 22 | if not API_KEY: 23 | print("PACKETFRAME_API_KEY environment variable not found.") 24 | exit(1) 25 | 26 | 27 | def _get(endpoint, body): 28 | return requests.get("https://packetframe.com/api/" + endpoint, headers={"X-API-Key": API_KEY}, json=body) 29 | 30 | 31 | def _truncate(string): 32 | return string if len(string) < 50 else string[:50] + "..." 33 | 34 | 35 | if len(sys.argv) == 1: 36 | print(help_text) 37 | exit(1) 38 | 39 | if sys.argv[1] == "get": 40 | if sys.argv[2] == "zones": 41 | table = [("\033[4mZone\033[0m", "\033[4mRecords\033[0m", "\033[4mUsers\033[0m")] 42 | for zone in _get("zones/list", None).json()["message"]: 43 | table.append((zone["zone"], len(zone["records"]), len(zone["users"]))) 44 | print(SingleTable(table).table) 45 | 46 | elif sys.argv[2] == "records": 47 | zone = sys.argv[3] 48 | table = [("\033[4mLabel\033[0m", "\033[4mTTL\033[0m", "\033[4mProxied\033[0m", "\033[4mValue\033[0m")] 49 | for record in _get("zone/" + zone + "/records", None).json()["message"]: 50 | table.append((_truncate(record["label"]), record["ttl"], "✓" if record.get("proxied") else "x", _truncate(record["value"]))) 51 | print(SingleTable(table).table) 52 | 53 | elif sys.argv[2] == "acl": 54 | print("ACL:") 55 | for address in _get("user/acl", None).json()["message"]: 56 | print("- " + address) 57 | 58 | elif sys.argv[2] == "users": 59 | zone = sys.argv[3] 60 | print(f"Users for {zone}") 61 | for user in _get("zone/" + zone + "/users", None).json()["message"]: 62 | print("- " + user) 63 | 64 | else: 65 | print(help_text) 66 | -------------------------------------------------------------------------------- /backend/provisioning/bird.j2: -------------------------------------------------------------------------------- 1 | # bird.conf 2 | 3 | define ASN = {{ asn }}; 4 | 5 | define LOCAL_v4 = [ 6 | {% if http %} 7 | {{ dns["prefix4"] }}, 8 | {{ proxy["prefix4"] }} 9 | {% else %} 10 | {{ dns["prefix4"] }} 11 | {% endif %} 12 | ]; 13 | define LOCAL_v6 = [ 14 | {% if http %} 15 | {{ dns["prefix6"] }}, 16 | {{ proxy["prefix6"] }} 17 | {% else %} 18 | {{ dns["prefix6"] }} 19 | {% endif %} 20 | ]; 21 | 22 | protocol static { 23 | ipv4; 24 | route {{ dns["prefix4"] }} reject; 25 | {% if http %}route {{ proxy["prefix4"] }} reject;{% endif %} 26 | 27 | } 28 | 29 | protocol static { 30 | ipv6; 31 | route {{ dns["prefix6"] }} reject; 32 | {% if http %}route {{ proxy["prefix6"] }} reject;{% endif %} 33 | 34 | } 35 | 36 | # ---- Parameters ---- 37 | 38 | timeformat base iso long; 39 | timeformat log iso long; 40 | timeformat protocol iso long; 41 | timeformat route iso long; 42 | 43 | log syslog all; 44 | 45 | router id {{ ansible_host }}; 46 | 47 | protocol device {}; 48 | 49 | protocol direct { 50 | ipv4; 51 | ipv6; 52 | } 53 | 54 | protocol kernel { 55 | scan time 10; 56 | ipv4 { 57 | export all; 58 | }; 59 | } 60 | 61 | protocol kernel { 62 | scan time 10; 63 | ipv6 { 64 | export all; 65 | }; 66 | } 67 | 68 | # ---- RPKI ---- 69 | 70 | roa4 table rpki4; 71 | roa6 table rpki6; 72 | 73 | protocol rpki { 74 | roa4 { table rpki4; }; 75 | roa6 { table rpki6; }; 76 | 77 | transport tcp; 78 | remote "127.0.0.1" port 8282; 79 | 80 | retry keep 90; 81 | refresh keep 900; 82 | expire keep 172800; 83 | } 84 | 85 | # ---- Filter Lists ---- 86 | 87 | define TRANSIT_ASNS = [ 88 | 174, # Cogent 89 | # 209, # Qwest (HE carries this on IXPs IPv6 (Jul 12 2018)) 90 | 701, # UUNET 91 | 702, # UUNET 92 | 1239, # Sprint 93 | 1299, # Telia 94 | 2914, # NTT Communications 95 | 3257, # GTT Backbone 96 | 3320, # Deutsche Telekom AG (DTAG) 97 | 3356, # Level3 / Lumen 98 | 3491, # PCCW 99 | 3549, # Level3 100 | 3561, # Savvis / CenturyLink 101 | 4134, # Chinanet 102 | 5511, # Orange opentransit 103 | 6453, # Tata Communications 104 | 6461, # Zayo Bandwidth 105 | 6762, # Seabone / Telecom Italia 106 | 6830, # Liberty Global 107 | 7018 # AT&T 108 | ]; 109 | 110 | define BOGONS_v4 = [ 111 | # 0.0.0.0/0, # Default route 112 | 0.0.0.0/8{8,32}, # IANA - Local Identification 113 | 10.0.0.0/8{8,32}, # RFC 1918 - Private Use 114 | 100.64.0.0/10{10,32}, # RFC 6598 - Shared Address Space 115 | 127.0.0.0/8{8,32}, # IANA - Loopback 116 | 169.254.0.0/16{16,32}, # RFC 3927 - Link Local 117 | 172.16.0.0/12{12,32}, # RFC 1918 - Private Use 118 | 192.0.2.0/24{24,32}, # RFC 5737 - TEST-NET-1 119 | 192.88.99.0/24{24,32}, # RFC 3068 - 6to4 prefix 120 | 192.168.0.0/16{16,32}, # RFC 1918 - Private Use 121 | 198.18.0.0/15{15,32}, # RFC 2544 - Network Interconnect Device Benchmark Testing 122 | 198.51.100.0/24{24,32}, # RFC 5737 - TEST-NET-2 123 | 203.0.113.0/24{24,32}, # RFC 5737 - TEST-NET-3 124 | 224.0.0.0/3{3,32}, # RFC 5771 - Multcast (formerly Class D) 125 | 192.34.27.0/24 # UnmeteredExchange 126 | ]; 127 | 128 | define BOGONS_v6 = [ 129 | # ::/0, # Default route 130 | ::/8{8,128}, # loopback, unspecified, v4-mapped 131 | 64:ff9b::/96{96,128}, # RFC 6052 - IPv4-IPv6 Translation 132 | 100::/8{8,128}, # RFC 6666 - reserved for Discard-Only Address Block 133 | 200::/7{7,128}, # RFC 4048 - Reserved by IETF 134 | 400::/6{6,128}, # RFC 4291 - Reserved by IETF 135 | 800::/5{5,128}, # RFC 4291 - Reserved by IETF 136 | 1000::/4{4,128}, # RFC 4291 - Reserved by IETF 137 | 2001::/33{33,128}, # RFC 4380 - Teredo prefix 138 | 2001:0:8000::/33{33,128}, # RFC 4380 - Teredo prefix 139 | 2001:2::/48{48,128}, # RFC 5180 - Benchmarking 140 | 2001:3::/32{32,128}, # RFC 7450 - Automatic Multicast Tunneling 141 | 2001:10::/28{28,128}, # RFC 4843 - Deprecated ORCHID 142 | 2001:20::/28{28,128}, # RFC 7343 - ORCHIDv2 143 | 2001:db8::/32{32,128}, # RFC 3849 - NON-ROUTABLE range to be used for documentation purpose 144 | 2002::/16{16,128}, # RFC 3068 - 6to4 prefix 145 | 3ffe::/16{16,128}, # RFC 5156 - used for the 6bone but was returned 146 | 4000::/3{3,128}, # RFC 4291 - Reserved by IETF 147 | 5f00::/8{8,128}, # RFC 5156 - used for the 6bone but was returned 148 | 6000::/3{3,128}, # RFC 4291 - Reserved by IETF 149 | 8000::/3{3,128}, # RFC 4291 - Reserved by IETF 150 | a000::/3{3,128}, # RFC 4291 - Reserved by IETF 151 | c000::/3{3,128}, # RFC 4291 - Reserved by IETF 152 | e000::/4{4,128}, # RFC 4291 - Reserved by IETF 153 | f000::/5{5,128}, # RFC 4291 - Reserved by IETF 154 | f800::/6{6,128}, # RFC 4291 - Reserved by IETF 155 | fc00::/7{7,128}, # RFC 4193 - Unique Local Unicast 156 | fe80::/10{10,128}, # RFC 4291 - Link Local Unicast 157 | fec0::/10{10,128}, # RFC 4291 - Reserved by IETF 158 | ff00::/8{8,128}, # RFC 4291 - Multicast 159 | 2602:ffb1:200::/48 # Unmetered Exchange 160 | ]; 161 | 162 | define BOGON_ASNS = [ 163 | 0, # Reserved. RFC7607 164 | 23456, # AS_TRANS. RFC6793 165 | 64496..64511, # Reserved for use in documentation and sample code. RFC5398 166 | 64512..65534, # Reserved for Private Use. RFC6996 167 | 65535, # Reserved. RFC7300 168 | 65536..65551, # Reserved for use in documentation and sample code. RFC5398 169 | 65552..131071, # Reserved. 170 | 4200000000..4294967294, # Reserved for Private Use. [RFC6996] 171 | 4294967295 # Reserved. RFC7300 172 | ]; 173 | 174 | # ---- Functions ---- 175 | 176 | function _reject(string reason) { 177 | reject "REJECTED [", reason, "] pfx ", net, " session ", proto, " path ", bgp_path, " pathlen ", bgp_path.len, " origin ", bgp_path.last; 178 | } 179 | 180 | function honor_graceful_shutdown() { 181 | if (65535, 0) ~ bgp_community then bgp_local_pref = 0; 182 | } 183 | 184 | function reject_transit_paths() { 185 | if (bgp_path ~ TRANSIT_ASNS) then _reject("transit path"); 186 | } 187 | 188 | function reject_bad() { 189 | if (net ~ LOCAL_v4 || net ~ LOCAL_v6) then _reject("own prefix"); 190 | 191 | if (net.type = NET_IP4) then { 192 | if (net.len > 24 || net.len < 8) then _reject("out of bounds (24 > len > 8)"); 193 | if (net ~ BOGONS_v4) then _reject("bogon prefix"); 194 | if (roa_check(rpki6, net, bgp_path.last_nonaggregated) = ROA_INVALID) then _reject("RPKI invalid"); 195 | } 196 | 197 | if (net.type = NET_IP6) then { 198 | if (net.len > 48 || net.len < 12) then _reject("out of bounds (48 > len > 12)"); 199 | if (net ~ BOGONS_v6) then _reject("bogon prefix"); 200 | if (roa_check(rpki4, net, bgp_path.last_nonaggregated) = ROA_INVALID) then _reject("RPKI invalid"); 201 | } 202 | 203 | if (bgp_path ~ BOGON_ASNS) then _reject("bogon ASN in path"); 204 | if (bgp_path.len > 100) then _reject("long AS_PATH"); 205 | } 206 | 207 | # ---- Templates ---- 208 | 209 | function export_filter() { 210 | if (net ~ LOCAL_v4 || net ~ LOCAL_v6) then { 211 | bgp_community.add((ASN,110)); 212 | accept; 213 | } 214 | reject; 215 | } 216 | 217 | template bgp peer4 { 218 | local as ASN; 219 | 220 | ipv4 { 221 | import filter { 222 | reject_bad(); 223 | reject_transit_paths(); 224 | accept; 225 | }; 226 | 227 | export where export_filter(); 228 | }; 229 | } 230 | 231 | template bgp peer6 { 232 | local as ASN; 233 | 234 | ipv6 { 235 | import filter { 236 | reject_bad(); 237 | reject_transit_paths(); 238 | accept; 239 | }; 240 | 241 | export where export_filter(); 242 | }; 243 | } 244 | 245 | template bgp upstream4 { 246 | local as ASN; 247 | 248 | ipv4 { 249 | import filter { 250 | reject_bad(); 251 | accept; 252 | }; 253 | 254 | export where export_filter(); 255 | }; 256 | } 257 | 258 | template bgp upstream6 { 259 | local as ASN; 260 | 261 | ipv6 { 262 | import filter { 263 | reject_bad(); 264 | accept; 265 | }; 266 | 267 | export where export_filter(); 268 | }; 269 | } 270 | 271 | template bgp ibgp_upstream4 { 272 | local as ASN; 273 | 274 | ipv4 { 275 | next hop self; 276 | import filter { 277 | reject_bad(); 278 | accept; 279 | }; 280 | 281 | export where export_filter(); 282 | }; 283 | } 284 | 285 | template bgp ibgp_upstream6 { 286 | local as ASN; 287 | 288 | ipv6 { 289 | next hop self; 290 | import filter { 291 | reject_bad(); 292 | accept; 293 | }; 294 | 295 | export where export_filter(); 296 | }; 297 | } 298 | 299 | template bgp default_route_upstream4 { 300 | local as ASN; 301 | ipv4 { 302 | import where (net = 0.0.0.0/0); 303 | export where export_filter(); 304 | }; 305 | } 306 | 307 | template bgp default_route_upstream6 { 308 | local as ASN; 309 | ipv6 { 310 | import where (net = ::/0); 311 | export where export_filter(); 312 | }; 313 | } 314 | 315 | template bgp ibgp_default_route_upstream4 { 316 | local as ASN; 317 | ipv4 { 318 | next hop self; 319 | import where (net = 0.0.0.0/0); 320 | export where export_filter(); 321 | }; 322 | } 323 | 324 | template bgp ibgp_default_route_upstream6 { 325 | local as ASN; 326 | ipv6 { 327 | next hop self; 328 | import where (net = ::/0); 329 | export where export_filter(); 330 | }; 331 | } 332 | 333 | template bgp no_import_upstream4 { 334 | local as ASN; 335 | ipv4 { 336 | import none; 337 | export where export_filter(); 338 | }; 339 | } 340 | 341 | template bgp no_import_upstream6 { 342 | local as ASN; 343 | ipv6 { 344 | import none; 345 | export where export_filter(); 346 | }; 347 | } 348 | 349 | # ---- Peers ---- 350 | 351 | protocol bgp COLLECTOR { 352 | local as ASN; 353 | neighbor {{ collector["host"] }} as ASN; 354 | rr client; 355 | 356 | ipv4 { 357 | add paths; 358 | import none; 359 | export where source = RTS_BGP; 360 | }; 361 | 362 | ipv6 { 363 | add paths; 364 | import none; 365 | export where source = RTS_BGP; 366 | }; 367 | } 368 | 369 | include "peers.conf"; 370 | include "manual*.conf"; -------------------------------------------------------------------------------- /backend/provisioning/named.conf.options.j2: -------------------------------------------------------------------------------- 1 | options { 2 | directory "/var/cache/bind"; 3 | statistics-file "/var/run/named/stats"; 4 | recursion no; 5 | allow-transfer { none; }; 6 | version "not currently available"; 7 | server-id hostname; 8 | 9 | listen-on { 10 | {% for address in dns['bind_addresses4'] %} 11 | {{ address }}; 12 | {% endfor -%} 13 | {% if http %} 14 | {% for address in dns['secondary_addresses4'] %} 15 | {{ address }}; 16 | {% endfor -%} 17 | {% endif %} 18 | }; 19 | 20 | listen-on-v6 { 21 | {% for address in dns['bind_addresses6'] %} 22 | {{ address }}; 23 | {% endfor -%} 24 | {% if http %} 25 | {% for address in dns['secondary_addresses6'] %} 26 | {{ address }}; 27 | {% endfor -%} 28 | {% endif %} 29 | }; 30 | }; 31 | 32 | statistics-channels { 33 | inet 127.0.0.1 port 8053 allow { 127.0.0.1; }; 34 | }; 35 | -------------------------------------------------------------------------------- /backend/provisioning/network.sh.j2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ip link del local 3 | ip link add local type dummy 4 | {% for addr in dns["bind_addresses4"] %} 5 | ip addr add {{ addr }}/24 dev local 6 | {% endfor %} 7 | {% for addr in dns["bind_addresses6"] %} 8 | ip addr add {{ addr }}/48 dev local 9 | {% endfor %} 10 | {% if http %} 11 | {% for addr in dns["secondary_addresses4"] %} 12 | ip addr add {{ addr }}/24 dev local 13 | {% endfor %} 14 | {% for addr in dns["secondary_addresses6"] %} 15 | ip addr add {{ addr }}/48 dev local 16 | {% endfor %} 17 | {% endif %} 18 | {% if http %} 19 | ip addr add {{ proxy["server4"] }}/24 dev local 20 | ip addr add {{ proxy["server6"] }}/48 dev local 21 | {% endif %} 22 | ip link set dev local up 23 | -------------------------------------------------------------------------------- /backend/provisioning/status.yml: -------------------------------------------------------------------------------- 1 | - name: Status 2 | hosts: nodes 3 | tasks: 4 | - name: Check service status 5 | command: systemctl status "{{ item }}" 6 | with_items: 7 | - bind9 8 | - bird 9 | register: result 10 | ignore_errors: yes 11 | 12 | - name: Check http service status 13 | command: systemctl status "{{ item }}" 14 | with_items: 15 | - caddy 16 | - varnish 17 | register: http_result 18 | ignore_errors: yes 19 | when: http 20 | 21 | # - name: Check HTTP query 22 | # uri: 23 | # url: https://local.example.com/ 24 | # return_content: yes 25 | # register: this 26 | # failed_when: this.x_pop_tag != inventory_hostname 27 | 28 | - name: Check DNS query 29 | shell: dig +short TXT id.server chaos @ns1.packetframe.com | tr -d '"' 30 | register: this 31 | failed_when: this.stdout != inventory_hostname 32 | -------------------------------------------------------------------------------- /backend/provisioning/varnish.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Varnish HTTP accelerator 3 | Documentation=https://www.varnish-cache.org/docs/6.1/ man:varnishd 4 | 5 | [Service] 6 | Type=simple 7 | LimitNOFILE=131072 8 | LimitMEMLOCK=82000 9 | ExecStart=/usr/sbin/varnishd -j unix,user=vcache -F -a localhost:8000 -T localhost:6082 -f /etc/varnish/default.vcl -S /etc/varnish/secret -s malloc,256m 10 | ExecReload=/usr/share/varnish/varnishreload 11 | ProtectSystem=full 12 | ProtectHome=true 13 | PrivateTmp=true 14 | PrivateDevices=true 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask~=1.1.2 2 | pymongo~=3.11.0 3 | requests~=2.24.0 4 | Jinja2~=2.11.2 5 | PyYAML~=5.3.1 6 | paramiko>=2.7.2 7 | pystalk~=0.6.1 8 | scp~=0.13.2 9 | dnspython~=2.0.0 10 | argon2-cffi 11 | ipaddress~=1.0.23 -------------------------------------------------------------------------------- /backend/templates/Caddyfile.j2: -------------------------------------------------------------------------------- 1 | { 2 | experimental_http3 3 | admin {{ host }}:2019 4 | } 5 | 6 | (primary_site) { 7 | header PoP {{ hostname }} 8 | header / -Server 9 | } 10 | 11 | # https://local.packetframe.com { 12 | # import primary_site 13 | # 14 | # tls /etc/caddy/local.packetframe.com-chain.pem /etc/caddy/local.packetframe.com-key.pem 15 | # 16 | # respond / `{ 17 | # "pop": "{{ inventory_hostname }}", 18 | # "http": "{{ http }}" 19 | # }` 20 | # } 21 | 22 | {% for domain in domains %} 23 | https://{{ domain }} { 24 | import primary_site 25 | tls /etc/caddy/{{ domain }}-chain.pem /etc/caddy/{{ domain }}-key.pem 26 | reverse_proxy http://localhost:8000 27 | } 28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /backend/templates/collector_bird.j2: -------------------------------------------------------------------------------- 1 | define ASN = {{ asn }}; 2 | 3 | log syslog all; 4 | router id 192.0.2.1; 5 | protocol device {}; 6 | 7 | protocol direct { 8 | ipv4; 9 | ipv6; 10 | } 11 | 12 | protocol kernel { 13 | ipv4 { 14 | import none; 15 | export none; 16 | }; 17 | } 18 | 19 | protocol kernel { 20 | ipv6 { 21 | import none; 22 | export none; 23 | }; 24 | } 25 | 26 | define BOGONS_v4 = [ 27 | 0.0.0.0/0, # Default route 28 | 0.0.0.0/8{8,32}, # IANA - Local Identification 29 | 10.0.0.0/8{8,32}, # RFC 1918 - Private Use 30 | 100.64.0.0/10{10,32}, # RFC 6598 - Shared Address Space 31 | 127.0.0.0/8{8,32}, # IANA - Loopback 32 | 169.254.0.0/16{16,32}, # RFC 3927 - Link Local 33 | 172.16.0.0/12{12,32}, # RFC 1918 - Private Use 34 | 192.0.2.0/24{24,32}, # RFC 5737 - TEST-NET-1 35 | 192.88.99.0/24{24,32}, # RFC 3068 - 6to4 prefix 36 | 192.168.0.0/16{16,32}, # RFC 1918 - Private Use 37 | 198.18.0.0/15{15,32}, # RFC 2544 - Network Interconnect Device Benchmark Testing 38 | 198.51.100.0/24{24,32}, # RFC 5737 - TEST-NET-2 39 | 203.0.113.0/24{24,32}, # RFC 5737 - TEST-NET-3 40 | 224.0.0.0/3{3,32} # RFC 5771 - Multicast (formerly Class D) 41 | ]; 42 | 43 | define BOGONS_v6 = [ 44 | ::/0, # Default route 45 | ::/8{8,128}, # loopback, unspecified, v4-mapped 46 | 64:ff9b::/96{96,128}, # RFC 6052 - IPv4-IPv6 Translation 47 | 100::/8{8,128}, # RFC 6666 - reserved for Discard-Only Address Block 48 | 200::/7{7,128}, # RFC 4048 - Reserved by IETF 49 | 400::/6{6,128}, # RFC 4291 - Reserved by IETF 50 | 800::/5{5,128}, # RFC 4291 - Reserved by IETF 51 | 1000::/4{4,128}, # RFC 4291 - Reserved by IETF 52 | 2001::/33{33,128}, # RFC 4380 - Teredo prefix 53 | 2001:0:8000::/33{33,128}, # RFC 4380 - Teredo prefix 54 | 2001:2::/48{48,128}, # RFC 5180 - Benchmarking 55 | 2001:3::/32{32,128}, # RFC 7450 - Automatic Multicast Tunneling 56 | 2001:10::/28{28,128}, # RFC 4843 - Deprecated ORCHID 57 | 2001:20::/28{28,128}, # RFC 7343 - ORCHIDv2 58 | 2001:db8::/32{32,128}, # RFC 3849 - NON-ROUTABLE range to be used for documentation purpose 59 | 2002::/16{16,128}, # RFC 3068 - 6to4 prefix 60 | 3ffe::/16{16,128}, # RFC 5156 - used for the 6bone but was returned 61 | 4000::/3{3,128}, # RFC 4291 - Reserved by IETF 62 | 5f00::/8{8,128}, # RFC 5156 - used for the 6bone but was returned 63 | 6000::/3{3,128}, # RFC 4291 - Reserved by IETF 64 | 8000::/3{3,128}, # RFC 4291 - Reserved by IETF 65 | a000::/3{3,128}, # RFC 4291 - Reserved by IETF 66 | c000::/3{3,128}, # RFC 4291 - Reserved by IETF 67 | e000::/4{4,128}, # RFC 4291 - Reserved by IETF 68 | f000::/5{5,128}, # RFC 4291 - Reserved by IETF 69 | f800::/6{6,128}, # RFC 4291 - Reserved by IETF 70 | fc00::/7{7,128}, # RFC 4193 - Unique Local Unicast 71 | fe80::/10{10,128}, # RFC 4291 - Link Local Unicast 72 | fec0::/10{10,128}, # RFC 4291 - Reserved by IETF 73 | ff00::/8{8,128} # RFC 4291 - Multicast 74 | ]; 75 | 76 | define BOGON_ASNS = [ 77 | 0, # Reserved. RFC7607 78 | 23456, # AS_TRANS. RFC6793 79 | 64496..64511, # Reserved for use in documentation and sample code. RFC5398 80 | 64512..65534, # Reserved for Private Use. RFC6996 81 | 65535, # Reserved. RFC7300 82 | 65536..65551, # Reserved for use in documentation and sample code. RFC5398 83 | 65552..131071, # Reserved. 84 | 4200000000..4294967294, # Reserved for Private Use. [RFC6996] 85 | 4294967295 # Reserved. RFC7300 86 | ]; 87 | 88 | function _reject(string reason) { 89 | reject "REJECTED [", reason, "] pfx ", net, " session ", proto, " path ", bgp_path, " pathlen ", bgp_path.len, " origin ", bgp_path.last; 90 | } 91 | 92 | function external_import() { 93 | if (bgp_path ~ BOGON_ASNS) then _reject("bogon ASN in path"); 94 | if (bgp_path.len > 100) then _reject("long AS_PATH"); 95 | 96 | if (net.type = NET_IP4) then { 97 | if (net.len > 24 || net.len < 8) then _reject("out of bounds (24 > len > 8)"); 98 | if (net ~ BOGONS_v4) then _reject("bogon prefix"); 99 | } 100 | 101 | if (net.type = NET_IP6) then { 102 | if (net.len > 48 || net.len < 12) then _reject("out of bounds (48 > len > 12)"); 103 | if (net ~ BOGONS_v6) then _reject("bogon prefix"); 104 | } 105 | 106 | bgp_community.add((ASN,800)); 107 | accept; 108 | } 109 | 110 | protocol bgp RIPE_RISv4 { 111 | local as ASN; 112 | neighbor 193.0.4.28 as 12654; 113 | multihop 255; 114 | ipv4 { 115 | import none; 116 | export where (ASN,800) ~ bgp_community; 117 | }; 118 | } 119 | 120 | protocol bgp RIPE_RISv6 { 121 | local as ASN; 122 | neighbor 2001:67c:2e8:2:ffff:0:4:28 as 12654; 123 | multihop 255; 124 | ipv6 { 125 | import none; 126 | export where (ASN,800) ~ bgp_community; 127 | }; 128 | } 129 | 130 | protocol bgp NLNOG_LGv4 { 131 | local as ASN; 132 | neighbor 82.94.230.130 as 199036; 133 | multihop 255; 134 | ipv4 { 135 | import none; 136 | export where (ASN,800) ~ bgp_community; 137 | }; 138 | } 139 | 140 | protocol bgp NLNOG_LGv6 { 141 | local as ASN; 142 | neighbor 2001:888:2001::130 as 199036; 143 | multihop 255; 144 | ipv6 { 145 | import none; 146 | export where (ASN,800) ~ bgp_community; 147 | }; 148 | } 149 | 150 | protocol bgp HE_RTBGPv4 { 151 | local as ASN; 152 | neighbor 64.62.153.100 as 393338; 153 | multihop 255; 154 | ipv4 { 155 | import none; 156 | export where (ASN,800) ~ bgp_community; 157 | }; 158 | } 159 | 160 | protocol bgp HE_RTBGPv6 { 161 | local as ASN; 162 | neighbor 2001:470:0:441::50 as 393338; 163 | multihop 255; 164 | ipv6 { 165 | import none; 166 | export where (ASN,800) ~ bgp_community; 167 | }; 168 | } 169 | 170 | protocol bgp FMT_CORE { 171 | local as ASN; 172 | neighbor 23.141.112.1 as 34553; 173 | multihop 255; 174 | ipv4 { 175 | add paths; 176 | import all; 177 | export none; 178 | }; 179 | ipv6 { 180 | add paths; 181 | import all; 182 | export none; 183 | }; 184 | } 185 | 186 | protocol bgp PDX_CORE { 187 | local as ASN; 188 | neighbor 44.26.163.1 as 34553; 189 | multihop 255; 190 | ipv4 { 191 | add paths; 192 | import all; 193 | export none; 194 | }; 195 | ipv6 { 196 | add paths; 197 | import all; 198 | export none; 199 | }; 200 | } 201 | 202 | protocol bgp EXT_JEFFCOLO { 203 | local as ASN; 204 | neighbor 23.175.1.50 as 395502; 205 | multihop 255; 206 | ipv4 { 207 | import where external_import(); 208 | export none; 209 | }; 210 | ipv6 { 211 | import where external_import(); 212 | export none; 213 | }; 214 | } 215 | 216 | template bgp collector_peer { 217 | local as ASN; 218 | 219 | ipv4 { 220 | add paths; 221 | import filter { 222 | bgp_community.add((ASN,800)); 223 | accept; 224 | }; 225 | export none; 226 | }; 227 | 228 | ipv6 { 229 | add paths; 230 | import filter { 231 | bgp_community.add((ASN,800)); 232 | accept; 233 | }; 234 | export none; 235 | }; 236 | } 237 | 238 | 239 | # Nodes 240 | 241 | {% for node in nodes %} 242 | protocol bgp {{ node.upper() }} from collector_peer { 243 | neighbor {{ nodes[node] }} as ASN; 244 | } 245 | {% endfor %} 246 | -------------------------------------------------------------------------------- /backend/templates/local.j2: -------------------------------------------------------------------------------- 1 | zone "{{ zone }}" { 2 | type master; 3 | file "/etc/bind/db.{{ zone }}"; 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /backend/templates/nameserver_issue.j2: -------------------------------------------------------------------------------- 1 | Hello! 2 | 3 | {{ domain }} is on the PacketFrame CDN platform but it's nameservers haven't been properly configured on your end. We're seeing the following error: 4 | 5 | {{ error }} 6 | 7 | Make sure to update your nameservers to the following: 8 | {% for nameserver in nameservers -%} 9 | {{ nameserver }} 10 | {% endfor %} 11 | Cheers, 12 | PacketFrame 13 | -------------------------------------------------------------------------------- /backend/templates/new_domain.j2: -------------------------------------------------------------------------------- 1 | Hello! 2 | 3 | {{ domain }} has been added to the PacketFrame CDN platform. In case you haven't already, make sure to update your nameservers to the following: 4 | 5 | {% for nameserver in nameservers -%} 6 | {{ nameserver }} 7 | {% endfor %} 8 | Cheers, 9 | PacketFrame 10 | -------------------------------------------------------------------------------- /backend/templates/proxied_record.j2: -------------------------------------------------------------------------------- 1 | Hello! 2 | 3 | A proxied record has been added to your domain, {{ domain }}. The PacketFrame caching proxy is a highly experimental feature and therefore requires manual verification by an administrator. Please allow up to 48 hours for an admin to take a look. 4 | 5 | In case you haven't already, make sure at least one of your domain's authorized users has created an ACL for protected HTTP operations. 6 | 7 | As always, feel free to send an email to info@packetframe.com if you need anything! 8 | 9 | Cheers, 10 | PacketFrame 11 | -------------------------------------------------------------------------------- /backend/templates/welcome.j2: -------------------------------------------------------------------------------- 1 | Hello! 2 | 3 | Welcome to the PacketFrame CDN platform {{ email }}! While the service is in beta, accounts are activated manually - usually within 48 hours if you've already been verified. In the mean time, feel free to check out the docs at https://packetframe.com/docs or source code at https://github.com/packetframe/cdn 4 | 5 | How did you hear about PacketFrame? 6 | {{ message }} 7 | 8 | Feel free to send an email to info@packetframe.com if you need anything! 9 | 10 | Cheers, 11 | PacketFrame 12 | -------------------------------------------------------------------------------- /backend/templates/zone.j2: -------------------------------------------------------------------------------- 1 | $TTL 86400 2 | @ IN SOA {{ nameservers[0] }}. {{ rname }}. ( 3 | {{ serial }} ; Serial 4 | 604800 ; Refresh 5 | 86400 ; Retry 6 | 2419200 ; Expire 7 | 86400 ) ; Negative Cache TTL 8 | {% for nameserver in nameservers -%} 9 | @ IN NS {{ nameserver }}. 10 | {% endfor %} 11 | 12 | {%- for record in records -%} 13 | {% if not record["pinned_nodes"] -%} 14 | {%- if record["proxied"] %} 15 | {{ record["label"] }} 60 IN A {{ proxy4 }} 16 | {{ record["label"] }} 60 IN AAAA {{ proxy6 }} 17 | {%- else %} 18 | {{ record["label"] }} {{ record["ttl"] }} IN {{ record["type"] }} {{ record["value"] }} 19 | {%- endif %} 20 | {%- elif node in record["pinned_nodes"] -%} 21 | {%- if record["proxied"] %} 22 | {{ record["label"] }} 60 IN A {{ proxy4 }} 23 | {{ record["label"] }} 60 IN AAAA {{ proxy6 }} 24 | {%- else %} 25 | {{ record["label"] }} {{ record["ttl"] }} IN {{ record["type"] }} {{ record["value"] }} 26 | {% endif %} 27 | {% endif %} 28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /backend/update-soas.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from pymongo import MongoClient 3 | 4 | db = MongoClient("mongodb://localhost:27017")["cdn"] 5 | 6 | db["zones"].update_many({}, {"$set": {"serial": str(int(time()))}}) 7 | -------------------------------------------------------------------------------- /backend/utils.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Template 2 | from config import configuration 3 | 4 | with open("templates/local.j2", "r") as local_template_file: 5 | local_template = Template(local_template_file.read()) 6 | 7 | with open("templates/zone.j2") as zone_template_file: 8 | zone_template = Template(zone_template_file.read()) 9 | 10 | with open("templates/default.vcl.j2", "r") as vcl_template_file: 11 | vcl_template = Template(vcl_template_file.read()) 12 | 13 | with open("templates/Caddyfile.j2", "r") as caddy_template_file: 14 | caddy_template = Template(caddy_template_file.read()) 15 | 16 | 17 | def render_local(zone): 18 | return local_template.render(zone=zone) 19 | 20 | 21 | def render_zone(zone, node): 22 | return zone_template.render( 23 | nameservers=configuration["dns"]["nameservers"], 24 | rname=configuration["dns"]["rname"], 25 | records=zone.get("records"), 26 | serial=zone["serial"], 27 | proxy4=configuration["proxy"]["server4"], 28 | proxy6=configuration["proxy"]["server6"], 29 | node=node["name"] 30 | ) 31 | 32 | 33 | def render_vcl(backends, domains, acls, normalize): 34 | return vcl_template.render(backends=backends, domains=domains, acls=acls, normalize=normalize) 35 | 36 | 37 | def render_caddy(domains, node): 38 | return caddy_template.render(domains=domains, host=node["management_ip"], hostname=node["name"]) 39 | -------------------------------------------------------------------------------- /eca/Makefile: -------------------------------------------------------------------------------- 1 | all: clean build 2 | 3 | define CONTROL_FILE 4 | Package: packetframe-eca 5 | Version: 0.0.1 6 | Depends: bind9, python3 7 | Section: utils 8 | Priority: optional 9 | Architecture: amd64 10 | Maintainer: PacketFrameName ({nodeNum}) | 28 |Location | 29 |Datacenter | 30 |Provider | 31 | {#if admin} 32 |IP | 33 | {/if} 34 |
---|---|---|---|---|
{node["name"]} | 41 |{node["location"]} | 42 |{node["datacenter"]} | 43 |{node["provider"]} | 44 | {#if admin} 45 |{node["management_ip"]} | 46 | {/if} 47 |
No ACL rules defined
91 | {/if} 92 | 93 |{status}
41 |{message}
42 | 45 |40 | | 41 | | |
---|---|---|
{user["username"]} | 47 |48 | {#if user["enabled"]} 49 | 50 | {:else} 51 | 52 | {/if} 53 | | 54 | 55 |56 | {#if user["admin"]} 57 | 58 | {/if} 59 | | 60 |
PacketFrame is fueled by the community. If you're interested in helping out or supporting the project in any way, contact
The fosshost project is a non-profit organisation that exists to serve the hosting needs of the global open source community.
18 |Heficed provides full-range infrastructure services. Heficed serves around 60 multi-billion industries starting from hosting to automotive or healthcare. With more than a decade of industry experience and operations globally, Heficed can meet any demand needs.
25 |Our vision is to offer solutions of high quality at an affordable price. Our services are delivered from our own datacenter in Sandefjord. In that way we can ensure high stability and safety. We work around the clock to give you the best price and service on the market.
32 |We offer cPanel Web Hosting, Reseller Hosting, OpenVZ VPS, KVM VPS and Managed VPS in 10 datacenter locations across 4 continents. All backed by our amazing 24/7 support team.
39 |Mythic Beasts is a privately owned hosting ISP with a focus on a no-nonsense service backed by excellent technical support.
46 |We offer highly available dedicated servers, colocation, DDoS protection and managed services 53 | based on fail-safe and certified infrastructure in Germany. We are also your reliable partner for server and network management.
54 |iFog GmbH offers IT services. This includes Web Hosting, vServers, IXP Access as well as IP Transit and LIR Services.
61 |Virtual Private Servers and Colocation in the ♥ of Frankfurt am Main, Germany.
68 |The main goal of the company has always been to provide the best possible service and product at a price that is good value for money. We will continue to strive to meet this goal in the future as we expand our services and locations.
75 |Garanntor is a privately owned company hosting and infrastructure-as-a-service company. Since our founding in 2016, Garanntor has continually innovated new ways to deliver on our mission: Making it Easy So You Can Focus on Growing Your Business. Based in Lagos, Nigeria. we pride ourselves in providing high quality services and support for each and every one of our clients.
82 |Chicago based VPS and Web Hosting provider. Offering custom infrastructure solutions with exceptional support
89 |Hivane offers DNS / MX / WWW services to non-for-profit projects. Check out https://www.hivane.net/ for more information.
96 |Superb cloud infrastructure with low latency. All our services are carefully designed to provide high performance, reliable and scalable at a reasonably price.
103 |Virtua.Cloud is a cloud hosting company, providing affordable Linux VPS and Windows VPS in Europe with instant setup and hourly billing.
110 |BrowserStack is a cloud web and mobile testing platform that provides developers the ability to test their websites and mobile applications across on-demand browsers, operating systems, and real mobile devices. They have four primary products- Live, App Live, Automate, and App Automate
117 |Continuous testing is a key enabler of digital confidence—the knowledge that you’re delivering the best possible user experience to your customers. Digitally confident organizations know that their web and mobile applications look, function and perform exactly as intended, every single time they’re used. That’s the value of Sauce Labs.
124 |Hosting powerful servers in Spain. Service stability, high availability network and priority technical support.
131 |No zones
232 | {:else} 233 |Loading...
234 | {/if} 235 | {/if} 236 |Welcome to Packetframe, the Open Source CDN for technology enthusiasts. The platform is currently in private beta, contact
Packetframe was built with developers in mind. With the Packetframe CDN, the only thing kept private is your data and keys to the infrastructure. Everything else is open and accessible. All functionality of the platform is exposed via the API and the entire codebase is open source.
46 |There are currently 41 PoPs across 38 cities and with a presence in all 6 consumer-inhabited continents. (If you know of a datacenter in Antarctica, let me know!)
66 |While the code is written by one person (me!), the open source community plays a huge role in the CDN infrastructure. Special thanks to fosshost for their support and partnership in the project. Want to get involved? Feel free to send an email to
Browser
9 |PacketFrame
17 |