├── .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 |

2 | PacketFrame logo 3 |
4 | GitHub license 5 | Documentation 6 |

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: PacketFrame 11 | Description: Edge Cache Appliance for the PacketFrame CDN 12 | endef 13 | 14 | define VARS 15 | $(CONTROL_FILE) 16 | endef 17 | export VARS 18 | 19 | build: 20 | mkdir -p {debian/DEBIAN/,debian/etc/packetframe-eca/,debian/var/packetframe-eca/,debian/usr/bin/,/etc/systemd/system/} 21 | cp eca.py debian/usr/bin/packetframe-eca.py 22 | cp packetframe-eca.service /etc/systemd/system/ 23 | echo "$$VARS" > debian/DEBIAN/control 24 | echo "Copyright 2021 PacketFrame." > debian/DEBIAN/copyright 25 | dpkg-deb --build debian 26 | mv debian.deb packetframe-eca.deb 27 | 28 | clean: 29 | rm -rf packetframe-eca*.deb debian/ 30 | -------------------------------------------------------------------------------- /eca/delivr-eca.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=PacketFrame Edge Cache Appliance 3 | Documentation=https://packetframe.com 4 | After=network.target 5 | 6 | [Service] 7 | User=root 8 | Group=root 9 | ExecStart=/usr/bin/packetframe-eca.py 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /eca/eca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import glob 3 | import os 4 | import time 5 | 6 | import requests 7 | 8 | 9 | def log_error(err): 10 | print("Unable to connect to ECA control plane. Please contact info@packetframe.com for more information. Error: " + str(err)) 11 | exit(1) 12 | 13 | 14 | def query(route): 15 | return requests.get("https://packetframe.com/api/eca/" + route, headers={"X-Auth-Key": auth_key}) 16 | 17 | 18 | def check_response(response): 19 | if response.status_code != 200: 20 | log_error("HTTP status code == " + str(response.status_code)) 21 | 22 | if not response.json()["success"]: 23 | log_error("(PacketFrame) " + str(response.json()["message"])) 24 | 25 | 26 | print("Clearing zone files") 27 | files = glob.glob("/etc/bind/db.*") 28 | for file in files: 29 | os.remove(file) 30 | 31 | print("Checking credentials") 32 | auth_key = "" 33 | try: 34 | with open("/etc/packetframe-eca/auth") as auth_key_file: 35 | auth_key = auth_key_file.read().strip() 36 | except Exception as e: 37 | print("Unable to read auth file: " + str(e)) 38 | else: 39 | print("Loaded authentication file") 40 | 41 | print("Verifying connectivity to ECA control plane") 42 | r = query("check") 43 | check_response(r) 44 | print("Connected to ECA control plane successfully") 45 | 46 | with open("/var/packetframe-eca/info", "w") as info_file: 47 | info_file.write(str(r.json()["message"])) 48 | 49 | disabled = False 50 | local_manifest = {} 51 | server_manifest = {} 52 | while not disabled: 53 | print("Pulling manifest") 54 | r = query("manifest") 55 | check_response(r) 56 | server_manifest = r.json()["message"]["zones"] 57 | 58 | should_pull_registry = False 59 | 60 | for zone in server_manifest: 61 | server_serial = server_manifest[zone] 62 | local_serial = local_manifest.get(zone) 63 | 64 | if (not local_serial) or (server_serial > local_serial): 65 | print("Pulling " + zone) 66 | r = query("pull/" + zone) 67 | check_response(r) 68 | 69 | with open("/etc/bind/db." + zone, "w") as zone_file: 70 | zone_file.write(r.json()["message"]["file"]) 71 | 72 | local_manifest[zone] = r.json()["message"]["serial"] 73 | 74 | should_pull_registry = True 75 | 76 | if should_pull_registry: 77 | print("Pulling zone registry") 78 | r = query("registry") 79 | 80 | with open("/etc/bind/named.conf.local", "w") as local_file: 81 | local_file.write(r.json()["message"]) 82 | 83 | os.system("rndc reload") 84 | 85 | print("Waiting for next pull") 86 | time.sleep(60) 87 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /frontend/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /frontend/.idea/cdn-frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/.idea/dictionaries/nate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | aaaa 5 | delivr 6 | navbar 7 | sethjs 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "dev": "rollup -c -w", 7 | "start": "sirv public" 8 | }, 9 | "devDependencies": { 10 | "@rollup/plugin-commonjs": "^14.0.0", 11 | "@rollup/plugin-node-resolve": "^8.0.0", 12 | "rollup": "^2.3.4", 13 | "rollup-plugin-livereload": "^2.0.0", 14 | "rollup-plugin-mdsvex-pages": "^1.2.9", 15 | "rollup-plugin-svelte": "^6.0.0", 16 | "rollup-plugin-terser": "^7.0.0", 17 | "svelte": "^3.0.0" 18 | }, 19 | "dependencies": { 20 | "mdsvex": "^0.8.9", 21 | "sirv-cli": "^1.0.0", 22 | "svelte-spa-router": "^3.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #ffffff; 9 | background-color: #000; 10 | margin: 0; 11 | padding: 0; 12 | box-sizing: border-box; 13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 14 | } -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Packetframe CDN - The Open Source CDN for Technology Enthusiasts 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /frontend/public/static/img/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/arrow.png -------------------------------------------------------------------------------- /frontend/public/static/img/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/check.png -------------------------------------------------------------------------------- /frontend/public/static/img/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/cross.png -------------------------------------------------------------------------------- /frontend/public/static/img/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/full.png -------------------------------------------------------------------------------- /frontend/public/static/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/icon.png -------------------------------------------------------------------------------- /frontend/public/static/img/laptop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/laptop.png -------------------------------------------------------------------------------- /frontend/public/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/logo.png -------------------------------------------------------------------------------- /frontend/public/static/img/main/cache.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/img/main/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/img/main/code2.svg: -------------------------------------------------------------------------------- 1 | proud_coder -------------------------------------------------------------------------------- /frontend/public/static/img/main/docs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/img/main/server.svg: -------------------------------------------------------------------------------- 1 | server_cluster -------------------------------------------------------------------------------- /frontend/public/static/img/main/world.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/static/img/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetframe/legacy/80453b545f33dc4944b02d4faece37dedd5e8fe3/frontend/public/static/img/server.png -------------------------------------------------------------------------------- /frontend/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import mdsvexPages from 'rollup-plugin-mdsvex-pages'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | function serve() { 11 | let server; 12 | 13 | function toExit() { 14 | if (server) server.kill(0); 15 | } 16 | 17 | return { 18 | writeBundle() { 19 | if (server) return; 20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 21 | stdio: ['ignore', 'inherit', 'inherit'], 22 | shell: true 23 | }); 24 | 25 | process.on('SIGTERM', toExit); 26 | process.on('exit', toExit); 27 | } 28 | }; 29 | } 30 | 31 | export default { 32 | input: 'src/main.js', 33 | output: { 34 | sourcemap: true, 35 | format: 'iife', 36 | name: 'app', 37 | file: 'public/build/bundle.js' 38 | }, 39 | plugins: [ 40 | mdsvexPages({ 41 | paths: ["docs"], 42 | }), 43 | 44 | svelte({ 45 | extensions: [".svelte", ".md"], 46 | 47 | // enable run-time checks when not in production 48 | dev: !production, 49 | // we'll extract any component CSS out into 50 | // a separate file - better for performance 51 | css: css => { 52 | css.write('bundle.css'); 53 | }, 54 | }), 55 | 56 | // If you have external dependencies installed from 57 | // npm, you'll most likely need these plugins. In 58 | // some cases you'll need additional configuration - 59 | // consult the documentation for details: 60 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 61 | resolve({ 62 | browser: true, 63 | dedupe: ['svelte'] 64 | }), 65 | commonjs(), 66 | 67 | // In dev mode, call `npm run start` once 68 | // the bundle has been generated 69 | !production && serve(), 70 | 71 | // Watch the `public` directory and refresh the 72 | // browser on changes when not in production 73 | !production && livereload('public'), 74 | 75 | // If we're building for production (npm run build 76 | // instead of npm run dev), minify 77 | production && terser() 78 | ], 79 | watch: { 80 | clearScreen: false 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /frontend/scripts/setupTypeScript.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** This script modifies the project to support TS code in .svelte files like: 4 | 5 | 8 | 9 | As well as validating the code for CI. 10 | */ 11 | 12 | /** To work on this script: 13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template 14 | */ 15 | 16 | const fs = require("fs") 17 | const path = require("path") 18 | const { argv } = require("process") 19 | 20 | const projectRoot = argv[2] || path.join(__dirname, "..") 21 | 22 | // Add deps to pkg.json 23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) 24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { 25 | "svelte-check": "^1.0.0", 26 | "svelte-preprocess": "^4.0.0", 27 | "@rollup/plugin-typescript": "^6.0.0", 28 | "typescript": "^3.9.3", 29 | "tslib": "^2.0.0", 30 | "@tsconfig/svelte": "^1.0.0" 31 | }) 32 | 33 | // Add script for checking 34 | packageJSON.scripts = Object.assign(packageJSON.scripts, { 35 | "validate": "svelte-check" 36 | }) 37 | 38 | // Write the package JSON 39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) 40 | 41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too 42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js") 43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts") 44 | fs.renameSync(beforeMainJSPath, afterMainTSPath) 45 | 46 | // Switch the app.svelte file to use TS 47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte") 48 | let appFile = fs.readFileSync(appSveltePath, "utf8") 49 | appFile = appFile.replace(" 36 | 37 | 52 | 53 |
54 | 55 | 56 | 64 | 65 |
66 | 67 | 68 |
69 | 70 |
© Nate Sales 2021.
71 |
72 |
73 | -------------------------------------------------------------------------------- /frontend/src/blog/2020-10-22-hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: hello-world 3 | title: Hello World 4 | author: Nate Sales 5 | author_title: PacketFrame 6 | author_url: https://natesales.net/ 7 | author_image_url: https://avatars2.githubusercontent.com/u/20391440 8 | tags: ["Hello World"] 9 | --- 10 | 11 | Hello World! 12 | 13 | Welcome to PacketFrame, the open source CDN platform for technology enthusiasts - currently in private beta. If you would like an account, please contact moc.emarftekcap@ofni and your application will be considered! 14 | -------------------------------------------------------------------------------- /frontend/src/blog/2020-11-22-outage.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: november-22-outage 3 | title: DNS Outage of November 22, 2020 4 | author: Nate Sales 5 | author_title: PacketFrame 6 | author_url: https://natesales.net/ 7 | author_image_url: https://avatars2.githubusercontent.com/u/20391440 8 | --- 9 | 10 | PacketFrame DNS Outage of November 22, 2020 11 | 12 | On Sat Nov 21, a code change to the PacketFrame control plane was pushed that allowed for more flexibility in embedded origin nodes for use in IXPs. The change allowed a single machine to serve both DNS and HTTP traffic. As a result, the current HTTP nodes were drained and scheduled for removal from the network. As an anycast network, PacketFrame is designed to handle multiple node failures and ordinarily powering off multiple nodes would not cause a problem. However, after the nodes were drained, they did not properly power down likely due to a hypervisor operation lock that failed to acquire. There was a period of time where 3 nodes were announcing the anycast prefixes with incorrect data which effected certain sites on the platform. 13 | 14 | On Sun Nov 20, a user reported a partial DNS outage. Upon investigation, it appeared that one of the cache nodes had still been announcing the anycast prefixes without an operational authoritative DNS process. The failed node was immideately withdrawn from the cluster to allow for functional nodes to take over the load. A series of tests were ran to ensure the cluster was still operating normally, and all nodes were found to be operational. A DNS query was executed on 50 random NLNOG RING nodes and 49 returned the correct DNS response. The one incorrectly responding node is likely behind a caching resolver that is storing records longer than it should be. 15 | 16 | At this point the issue is marked as resolved on our end. Popular public DNS resolvers (Google, Cloudflare, Quad9, and OpenDNS) are all currently returning correct data, although ISP resolvers may be incorrectly configured to not honor DNS TTLs or are caching records for longer than they should be. If you are still having issues please try an alternate resolver. 17 | 18 | If you have any questions, feel free to send an email to info@packetframe.com 19 | -------------------------------------------------------------------------------- /frontend/src/components/Banner.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {#if shown} 9 |
10 | 11 |

12 | shown=false} class="material-icons">close 13 |
14 | {/if} 15 |
16 | 17 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/Button.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 32 |
33 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/ButtonBar.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | -------------------------------------------------------------------------------- /frontend/src/components/CheckBoxes.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {#each nodes as node} 7 |
8 | 12 |
13 | {/each} 14 |
15 | 16 | 53 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 16 |
17 | 18 | 39 | -------------------------------------------------------------------------------- /frontend/src/components/Email.svelte: -------------------------------------------------------------------------------- 1 | moc.emarftekcap@ofni 2 | 3 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/EmailSignupForm.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Want to keep up to date? Subscribe to the mailing list!

5 |

6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 37 | -------------------------------------------------------------------------------- /frontend/src/components/EntryForm.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 |
42 |
43 |
{type.charAt(0).toUpperCase() + type.slice(1)}
44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 | {#if type === "signup"} 54 |
55 | 56 |
57 | {/if} 58 | 59 |
60 | 61 |
62 |
63 |
64 |
65 | 66 | 91 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | Logo 38 |
39 | {#if screenWidth <= 820} 40 | 41 | {menuOpen ? 'close' : 'menu'} 42 | 43 | {/if} 44 |
45 |
46 | Dashboard 47 | Community 48 | Docs 49 | {#if $location === "/dashboard"} 50 | logout()}>Logout 51 | {:else} 52 | Signup 53 | Login 54 | {/if} 55 |
56 |
57 | 58 | 157 | -------------------------------------------------------------------------------- /frontend/src/components/NetworkMap.svelte: -------------------------------------------------------------------------------- 1 | 74 | 75 |
76 |
77 |
78 | 79 | 87 | 88 | 91 | -------------------------------------------------------------------------------- /frontend/src/components/NetworkTable.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | {#if admin} 32 | 33 | {/if} 34 | 35 | 36 | 37 | {#if nodes} 38 | {#each nodes as node, i } 39 | 40 | 41 | 42 | 43 | 44 | {#if admin} 45 | 46 | {/if} 47 | 48 | {/each} 49 | {:else} 50 |

Loading...

51 | {/if} 52 | 53 |
Name ({nodeNum})LocationDatacenterProviderIP
{node["name"]}{node["location"]}{node["datacenter"]}{node["provider"]}{node["management_ip"]}
54 |
55 |
56 | 57 | 113 | -------------------------------------------------------------------------------- /frontend/src/components/NumberInput.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 12 |
13 | 14 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/Settings.svelte: -------------------------------------------------------------------------------- 1 | 64 | 65 |
66 |
67 | {#if email} 68 |

User Settings for {email}

69 | {:else} 70 |

User Settings

71 | {/if} 72 | 73 |
74 | 75 | 76 | 77 |
78 |
79 | 80 |
81 |

ACL Settings

82 |
83 | {#if acl && acl.length > 0} 84 |
    85 | {#each acl as rule, i } 86 |
  • {rule}
  • 87 | {/each} 88 |
89 | {:else} 90 |

No ACL rules defined

91 | {/if} 92 | 93 | 94 | 95 |
96 |
97 |
98 | 99 | 121 | 122 | -------------------------------------------------------------------------------- /frontend/src/components/Snackbar.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | {#if open} 36 |
40 |

{status}

41 |

{message}

42 | 43 | close 44 | 45 |
46 | {/if} 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/components/SnackbarGroup.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | {#each Object.keys($SnackBars) as key, i} 25 | 33 | {/each} 34 |
35 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/Table.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |

{title}

10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 75 | -------------------------------------------------------------------------------- /frontend/src/components/TextInput.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 | {#if password} 13 | 14 | {:else} 15 | 16 | {/if} 17 |
18 | 19 | 39 | -------------------------------------------------------------------------------- /frontend/src/components/ToggleButton.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 33 |
34 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/UserTable.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {#if users} 44 | {#each users as user, i } 45 | 46 | 47 | 54 | 55 | 60 | 61 | {/each} 62 | {:else} 63 |

Loading...

64 | {/if} 65 |
Email
{user["username"]} 48 | {#if user["enabled"]} 49 | 50 | {:else} 51 | 52 | {/if} 53 | 56 | {#if user["admin"]} 57 | 58 | {/if} 59 |
66 |
67 |
68 | 69 | 127 | -------------------------------------------------------------------------------- /frontend/src/docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: api 3 | title: API 4 | --- 5 | 6 |
7 |
8 | 9 | # Docs / API 10 | 11 | Code 12 | 13 | PacketFrame's API exposes all functionality of the platform via a RESTful programmatic interface. 14 | 15 | All API routes are under `https://packetframe.com/api/` 16 | 17 | Authenticated routes require the `X-API-Key` request header to be set to your API key. 18 | 19 | ## Routes 20 | 21 | | Endpoint | Methods | Usage | 22 | | ---------------------------------- | -------- | -------------------------------------------------- | 23 | | /auth/signup | POST | Create an account | 24 | | /auth/login | POST | Get your API token | 25 | | /zones/add | POST | Add a zone | 26 | | /zones/list | GET | Get all zones under your account | 27 | | /zone/[ZONE]/delete | POST | Remove [ZONE] | 28 | | /zone/[ZONE]/add | POST | Add a record to [ZONE] | 29 | | /zone/[ZONE/records | GET | Get records for [ZONE] | 30 | | /zone/[ZONE/users | GET | Get users for [ZONE] | 31 | | /zone/[ZONE]/delete_record/[INDEX] | POST | Delete record at [INDEX] from [ZONE] | 32 | | /zone/[ZONE]/export | GET | Download RFC 1035 zone file for [ZONE] | 33 | | /admin | GET | Check if the authenticated user is an admin or not | 34 | | /user/acl | GET, PUT | Get or append to a user IP ACL | 35 | | /user/change_password | POST | Change a user's password | 36 | | /counters | GET | Get node and location counters | 37 | | /nodes/geoloc | GET | Get node locations | 38 | 39 | ## Admin Routes 40 | 41 | | Endpoint | Methods | Usage | 42 | | ------------------- | ------- | --------------------------------------------- | 43 | | /nodes/add | POST | Add a DNS node | 44 | | /nodes/list | GET | Get all DNS node | 45 | | /nodes/power | POST | Enable or disable the node's BGP daemon | 46 | | /stats | GET | Get system stats counters (nodes/zones/users) | 47 | | /users | GET | Get all users | 48 | | /user/[USER]/toggle | POST | Toggle [USER]'s enabled state | 49 | 50 | ## Debug Routes 51 | 52 | | Endpoint | Methods | Usage | 53 | | ---------------------------------- | ------- | ----------------------------------------- | 54 | | /debug/refresh_zones | GET | Refresh all zones | 55 | | /debug/clear_queue | GET | Clear opqueue | 56 | | /debug/queue_status | GET | Get number of running and ready tasks | 57 | | /debug/refresh_cache | GET | Refresh cache's varnish and caddy configs | 58 | | /debug/update_collector_monitoring | GET | Update route collector and prometheus configs | 59 | | /debug/healthcheck | GET | Run network-wide node healthcheck | 60 | 61 | ### Route Specifics 62 | 63 | #### /auth/signup 64 | 65 | Create a zone 66 | 67 | Methods: POST 68 | 69 | Request Body: 70 | 71 | | POST Field | Type | Description | 72 | | ---------- | ------ | ----------- | 73 | | username | string | Username | 74 | | password | string | Password | 75 | 76 | #### /auth/login 77 | 78 | Create a zone 79 | 80 | Methods: POST Returns: API Key 81 | 82 | Request Body: 83 | 84 | | POST Field | Type | Description | 85 | | ---------- | ------ | ----------- | 86 | | username | string | Username | 87 | | password | string | Password | 88 | 89 | #### /zones/add 90 | 91 | Create a zone 92 | 93 | Methods: POST 94 | 95 | Request Body: 96 | 97 | | POST Field | Type | Description | 98 | | ---------- | ------ | ----------------------------------------------------------------------------- | 99 | | zone | string | RFC 1035 DNS label of the zone (e.g. `example.com` or `2.0.192.in-addr.arpa`) | 100 | 101 | #### /zones/list 102 | 103 | Create a zone 104 | 105 | Methods: GET 106 | 107 | ##### /zone/[ZONE]/delete 108 | 109 | Delete a zone 110 | 111 | Methods: POST 112 | 113 | | URL Field | Type | Description | 114 | | --------- | ------ | ----------------------------------------------------------------------------- | 115 | | zone | string | RFC 1035 DNS label of the zone (e.g. `example.com` or `2.0.192.in-addr.arpa`) | 116 | 117 | ##### /zone/[ZONE]/add 118 | 119 | Add a record to zone 120 | 121 | Methods: POST 122 | 123 | Request Body: 124 | 125 | | POST Field | Type | Description | 126 | | ------------------ | ---------- | ------------------------------------------------------------------------------------- | 127 | | ttl | int | Record TTL (Time To Live) | 128 | | label | string | RFC 1035 DNS label of the zone (e.g. `example.com` or `2.0.192.in-addr.arpa`) | 129 | | type | string | DNS record type (Supported values are A, AAAA, MX, SRV, TXT, [PTR for reverse zones]) | 130 | | value | string | Value of record (IP address, name, etc) | 131 | | additional options | string/int | additional options for specific zone type | 132 | 133 | ##### /zone/[ZONE]/records 134 | 135 | Get records 136 | 137 | Methods: GET 138 | 139 | | URL Field | Type | Description | 140 | | --------- | ------ | ----------------------------------------------------------------------------- | 141 | | zone | string | RFC 1035 DNS label of the zone (e.g. `example.com` or `2.0.192.in-addr.arpa`) | 142 | 143 | ##### /zone/[ZONE]/delete_record/[INDEX] 144 | 145 | Delete a record 146 | 147 | Methods: POST 148 | 149 | Request Body: 150 | 151 | | URL Field | Type | Description | 152 | | --------- | ------ | ----------------------------------------------------------------------------- | 153 | | zone | string | RFC 1035 DNS label of the zone (e.g. `example.com` or `2.0.192.in-addr.arpa`) | 154 | | index | int | Index of the record (To retrieve records, see `/records/list`) | 155 | 156 |
157 |
158 | 159 | 215 | -------------------------------------------------------------------------------- /frontend/src/docs/cachingProxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: caching-proxy 3 | title: Caching Proxy 4 | --- 5 | 6 |
7 | 8 | # Docs / Caching Proxy 9 | 10 | Cache 11 | 12 | The PacketFrame caching proxy puts the PacketFrame network in front of your origin webserver to increase performance and security. 13 | 14 | Proxied records can be added though the dashboard by clicking the "Add Record" button and enabling the proxy by clicking the cloud icon. 15 | 16 | Once a proxied record is enabled, PacketFrame will request an SSL certificate from [LetsEncrypt](https://letsencrypt.org/) and configure the proxy. Origin pulls will come from unicast source IPs of the caching network. 17 | 18 | Backend errors (HTTP 500, 502, 503, 504) will be caught by the caching servers and a generic error page will be shown. 19 | 20 | ### ACL Configuration 21 | 22 | Every domain that has a proxied record must have at least one user with an ACL configured. IP blocks in the ACL are permitted to send the `PURGE` HTTP request method to any route under domains that user controls to purge the cache of that object. The ACL can be configured on a per-user basis by clicking the settings gear icon in the dashboard and adding the address in CIDR notation. 23 | 24 |
25 | 26 | 47 | -------------------------------------------------------------------------------- /frontend/src/docs/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cli 3 | title: CLI 4 | --- 5 | 6 |
7 | 8 | # Docs / CLI 9 | 10 | Code 11 | 12 | For users that prefer a CLI option, there's a `pfctl` tool that can wraps the API in a simple command line interface. 13 | 14 | The latest version of `pfctl` can be found on GitHub (https://raw.githubusercontent.com/packetframe/cdn/main/util/pfctl.py) 15 | 16 |
17 | 18 | 39 | -------------------------------------------------------------------------------- /frontend/src/docs/dashboard.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: dashboard 3 | title: Dashboard 4 | --- 5 | 6 |
7 | 8 | # Docs / Dashboard 9 | 10 | Dashboard 11 | 12 |

Dashboard

13 | The PacketFrame dashboard implements all the functionality of PacketFrame through the API. Records can be added though the dashboard by clicking the "Add Record" button. Each record type has specific required attributes which will be displayed as input fields after selecting the record type from the dropdown. 14 | 15 |

Adding Zones

16 | To add a zone, log in and click the "Add Zone" button located near the top right corner of the page, and entering your domain name. PacketFrame supports reverse zones as well, which can be added by specifying the IP address block in CIDR notation. 17 | 18 |

Record Management

19 | The dashboard implements all the functionality of PacketFrame through the API. Records can be added though the dashboard by clicking the "Add Record" button. Each record type has specific required attributes which will be displayed as input fields after selecting the record type from the dropdown. 20 | 21 |

Node Pinning

22 | DNS records are propagated to every edge node by default. You can control which nodes have an individual record with Node Pinning. By clicking the location icon when adding a record, you can choose which nodes that the record will be served from. 23 | 24 |
25 | 26 | 47 | -------------------------------------------------------------------------------- /frontend/src/docs/eca.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: eca 3 | title: Embedded Cache Appliance 4 | --- 5 | 6 |
7 | 8 | # Docs / Embedded Cache Appliance 9 | 10 | Server 11 | 12 | The Edge Cache Appliance (ECA) program brings PacketFrame content as close to your users as possible by deploying an anycast node directly in your network. ECAs are designed to run in a wide variety of environments, even those behind firewalls with limited external access. Requests are evaluated on a case-by-case basis and involve specific technical and policy requirements. Depending on the scenario, ECAs are available as dedicated hardware as well as VM images. Networks that wish to have PacketFrame content with the lowest possible latency can opt to host an EOS server for the full DNS or HTTP(S) traffic. EOS are fully managed by PacketFrame and can be run on a VM or dedicated server. EOS nodes to the host network via BGP and announce routes based on instructions from the PacketFrame control plane. 13 |
14 | 15 | 36 | -------------------------------------------------------------------------------- /frontend/src/docs/privacyPolicy.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: privacy-policy 3 | title: Privacy Policy 4 | --- 5 | 6 |
7 | 8 | # Docs / Privacy Policy 9 | 10 | Privacy 11 | 12 | Last updated January 3, 2021 13 | 14 | Webserver logs are stored for **7 days** (or less if the log file gets too big) and are in the [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format). 15 | 16 | We also use a self hosted instance of [Plausible](https://github.com/plausible/analytics) for analytics of the `packetframe.com` site. 17 | 18 | Anonymized counters from all DNS queries are stored for 360 days (or until they get too big). We use https://github.com/prometheus-community/bind_exporter if you would like to see the full list of metrics, but in short these logs include numbers only so nothing that's able to identify any particular user. 19 | 20 | Nothing else is *ever* logged other than what is listed above. 21 |
22 | 23 | 44 | -------------------------------------------------------------------------------- /frontend/src/docs/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | 9 | 10 |
11 | 12 | # Docs / Security 13 | 14 | Hacker 15 | 16 | Hackers are welcome! If you're interested in testing PacketFrame for vulns, please follow the rules below. When in doubt, ask! 17 | 18 | Scope: Source code at https://github.com/packetframe/cdn 19 | 20 | Security vulnerabilities should be reported to , for general bugs feel free to file an issue at https://github.com/packetframe/cdn/issues 21 | 22 | Don't test in production! Download the source at https://github.com/packetframe/cdn and spin up a local instance of the CDN. If there's something you want to try on the production network, let me know first and we can work something out. 23 | 24 |
25 | 26 | 47 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: {} 6 | }); 7 | 8 | export default app; -------------------------------------------------------------------------------- /frontend/src/mdp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sidebar": { 3 | "docs": { 4 | "Documentation": [ 5 | {"route": "", "label": "Home"}, 6 | {"route": "dashboard", "label": "Dashboard"}, 7 | {"route": "eca", "label": "ECA"}, 8 | {"route": "api", "label": "API"}, 9 | {"route": "cli", "label": "CLI"}, 10 | {"route": "caching-proxy", "label": "Caching Proxy"}, 11 | {"route": "privacy-policy", "label": "Privacy Policy"}, 12 | {"route": "security", "label": "Security"} 13 | ], 14 | "default-open": ["Documentation"] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /frontend/src/pages/Community.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |

Community

7 | Together 8 |

PacketFrame is fueled by the community. If you're interested in helping out or supporting the project in any way, contact . The supporters below have been instrumental in providing infrastructure or other services to the project - huge thanks to all of the amazing supporters!

9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 | Fosshost 17 |

The fosshost project is a non-profit organisation that exists to serve the hosting needs of the global open source community.

18 |
19 |
20 | 21 |
22 |
23 | Heficed 24 |

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 |
26 |
27 | 28 |
29 |
30 | Terrahost 31 |

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 |
33 |
34 | 35 |
36 |
37 | HostUS 38 |

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 |
40 |
41 | 42 |
43 |
44 | Mythic Beasts 45 |

Mythic Beasts is a privately owned hosting ISP with a focus on a no-nonsense service backed by excellent technical support.

46 |
47 |
48 | 49 |
50 |
51 | Combahton 52 |

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 |
55 |
56 | 57 |
58 |
59 | iFog 60 |

iFog GmbH offers IT services. This includes Web Hosting, vServers, IXP Access as well as IP Transit and LIR Services.

61 |
62 |
63 | 64 |
65 |
66 | VPS66 67 |

Virtual Private Servers and Colocation in the ♥ of Frankfurt am Main, Germany.

68 |
69 |
70 | 71 |
72 |
73 | SpartanHost 74 |

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 |
76 |
77 | 78 |
79 |
80 | Garanntor 81 |

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 |
83 |
84 | 85 |
86 |
87 | XenSpec 88 |

Chicago based VPS and Web Hosting provider. Offering custom infrastructure solutions with exceptional support

89 |
90 |
91 | 92 |
93 |
94 | Hivane 95 |

Hivane offers DNS / MX / WWW services to non-for-profit projects. Check out https://www.hivane.net/ for more information.

96 |
97 |
98 | 99 |
100 |
101 | Misaka Network 102 |

Superb cloud infrastructure with low latency. All our services are carefully designed to provide high performance, reliable and scalable at a reasonably price.

103 |
104 |
105 | 106 |
107 |
108 | Virtua Systems 109 |

Virtua.Cloud is a cloud hosting company, providing affordable Linux VPS and Windows VPS in Europe with instant setup and hourly billing.

110 |
111 |
112 | 113 |
114 |
115 | BrowserStack 116 |

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 |
118 |
119 | 120 |
121 |
122 | Sauce Labs 123 |

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 |
125 |
126 | 127 |
128 |
129 | GINERNET 130 |

Hosting powerful servers in Spain. Service stability, high availability network and priority technical support.​

131 |
132 |
133 | 134 |
135 |
136 | 137 | 197 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard.svelte: -------------------------------------------------------------------------------- 1 | 170 | 171 |
172 |
173 |

Dashboard

174 | 175 | {#if $IsAdmin} 176 | 177 | {/if} 178 | 179 | 181 | 182 | {#if zones} 183 | 184 | {#each zones as zone} 185 | 186 | {/each} 187 | 188 | {:else} 189 | {#if no_zones} 190 |

No zones

191 | {:else} 192 |

Loading...

193 | {/if} 194 | {/if} 195 |
196 | 197 | {#if showAdmin && $IsAdmin} 198 |
199 |
200 | 201 |
202 |
203 | 204 |
205 |
206 | 207 | {#if showUserTable} 208 | 209 | {/if} 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | {/if} 222 | 223 | {#if showSettings} 224 | 225 | {/if} 226 | 227 | {#if selected_zone !== ""} 228 | 229 | {:else} 230 | {#if no_zones} 231 |

No zones

232 | {:else} 233 |

Loading...

234 | {/if} 235 | {/if} 236 |
237 | 238 | 239 | 284 | -------------------------------------------------------------------------------- /frontend/src/pages/Docs.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Docs

4 | Docs 5 |
6 | 7 |

Pages

8 | 9 | 18 |
19 | 20 | 39 | -------------------------------------------------------------------------------- /frontend/src/pages/Index.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | PacketFrame Icon 31 | 32 |

Packetframe

33 |

Welcome to Packetframe, the Open Source CDN for technology enthusiasts. The platform is currently in private beta, contact for more information.

34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | code 42 |
43 |
44 |

Built for Developers

45 |

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 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 |
63 |
64 |

Globally Distributed

65 |

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 |
67 |
68 | world 69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 | community 88 |
89 |
90 |

Community Centric

91 |

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 or hop in #packetframe on Libera.Chat and ask away!

92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /frontend/src/pages/Login.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFound.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Error 404

3 |

Not Found

4 |
5 |
6 | User 7 |
8 |

Browser

9 | 10 |
11 |
12 | 13 |
14 | PacketFrame 15 |
16 |

PacketFrame

17 | 18 |
19 |
20 |
21 |
22 | 23 | 70 | -------------------------------------------------------------------------------- /frontend/src/pages/Signup.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 |
-------------------------------------------------------------------------------- /frontend/src/stores.js: -------------------------------------------------------------------------------- 1 | import {writable} from "svelte/store"; 2 | 3 | export const SnackBars = writable({}); 4 | export let IsAdmin = writable(false); 5 | export let Debug = writable(true); 6 | export let API = writable("https://packetframe.com/api/"); 7 | -------------------------------------------------------------------------------- /frontend/src/utils.js: -------------------------------------------------------------------------------- 1 | import {SnackBars, Debug} from "./stores"; 2 | 3 | export function addSnackbar(status, message, color, timeout) { 4 | let id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5); 5 | SnackBars.update(sb => {sb[id] = {status, message, color, timeout}; return sb}) 6 | } 7 | 8 | export function log(message) { 9 | if (Debug) { 10 | console.log(message) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /util/pfctl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """Usage: 3 | pfctl account [login|logout] 4 | pfctl records [add] 5 | pfctl records delete 6 | pfctl zones list 7 | """ 8 | 9 | from os import makedirs 10 | from os.path import exists 11 | from pathlib import Path 12 | from shutil import rmtree 13 | 14 | import requests 15 | from PyInquirer import style_from_dict, prompt, Token 16 | from docopt import docopt 17 | from rich import box 18 | from rich.console import Console 19 | from rich.table import Table 20 | from rich.traceback import install 21 | 22 | # Register rich traceback hook 23 | install() 24 | 25 | args = docopt(__doc__) 26 | 27 | console = Console() 28 | 29 | PARENT_ENDPOINT = "https://packetframe.com/api/" 30 | config_path = str(Path.home()) + "/.config/packetframe/" 31 | 32 | PYINQUIRER_STYLE = style_from_dict({ 33 | Token.Separator: "#6C6C6C", 34 | Token.QuestionMark: "#FFF", 35 | Token.Selected: "#dd00ff", 36 | Token.Pointer: "#FFF", 37 | Token.Instruction: "", 38 | Token.Answer: "#dd00ff", 39 | Token.Question: "", 40 | }) 41 | 42 | # Set API key if not trying to lotg in 43 | if args and not args["login"]: 44 | try: 45 | with open(config_path + "key", "r") as api_key_file: 46 | API_KEY = api_key_file.read() 47 | except FileNotFoundError: 48 | console.print(":x: Account not found: run [dim]pfctl login[/] to log into your account") 49 | exit(1) 50 | else: 51 | API_KEY = "" 52 | 53 | 54 | # Request helper 55 | 56 | 57 | def _request(message, route, method, body=None): 58 | with console.status(f"[bold green]{message}..."): 59 | r = requests.request(method, PARENT_ENDPOINT + route, json=body, headers={"X-API-Key": API_KEY}) 60 | if r.status_code != 200: 61 | console.log( 62 | f"[bold red]ERROR (request)[reset] code {r.status_code} body {r.text}") 63 | exit(1) 64 | elif not r.json()["success"]: 65 | console.log(f"[bold red]ERROR (api)[reset] {r.json()['message']}") 66 | exit(1) 67 | return r.json()["message"] 68 | 69 | 70 | def list_zones(): 71 | table = Table( 72 | title="Zones List", 73 | show_header=True, 74 | show_edge=False, 75 | expand=False, 76 | row_styles=["none", "dim"], 77 | box=box.SIMPLE 78 | ) 79 | 80 | table.add_column("Zone", style="cyan") 81 | table.add_column("Records", style="magenta") 82 | table.add_column("Users", style="green") 83 | 84 | for zone in _request("Getting zones", "zones/list", "GET"): 85 | table.add_row(zone["zone"], str( 86 | len(zone["records"])), str(len(zone["users"]))) 87 | 88 | console.print(table) 89 | 90 | 91 | def list_records(zone: str): 92 | table = Table( 93 | title=f"Records for {zone}", 94 | show_header=True, 95 | show_edge=False, 96 | expand=False, 97 | row_styles=["none", "dim"], 98 | box=box.SIMPLE 99 | ) 100 | 101 | table.add_column("Label", style="cyan") 102 | table.add_column("Index", style="magenta") 103 | table.add_column("TTL", style="magenta") 104 | table.add_column("Type", style="magenta") 105 | table.add_column("Value", style="green") 106 | 107 | for index, record in enumerate(_request(f"Getting records for {zone}", f"zone/{zone}/records", "GET")): 108 | table.add_row(record["label"], str(index), record["type"], str(record["ttl"]), record["value"]) 109 | 110 | console.print(table) 111 | 112 | 113 | def add_record(zone): 114 | console.print(f"[underline]Add a new record to {zone}") 115 | record = prompt([ 116 | { 117 | "type": "input", 118 | "name": "label", 119 | "message": "Label", 120 | }, 121 | { 122 | "type": "list", 123 | "name": "type", 124 | "message": "Type", 125 | "choices": ["A", "AAAA", "TXT", "MX", "CNAME", "PTR"] 126 | }, 127 | { 128 | "type": "input", 129 | "name": "ttl", 130 | "message": "TTL", 131 | "default": "86400" 132 | }, 133 | { 134 | "type": "input", 135 | "name": "value", 136 | "message": "Value", 137 | }, 138 | { 139 | "type": "confirm", 140 | "name": "proxied", 141 | "message": "Proxied" 142 | } 143 | ], style=PYINQUIRER_STYLE) 144 | 145 | if record: 146 | # Add domain suffix 147 | if not record["label"].endswith(zone): 148 | record["label"] += "." + zone + "." 149 | 150 | # Cast TTL to an int 151 | record["ttl"] = int(record["ttl"]) 152 | 153 | r = _request(f"Adding record {record['label']}", f"zone/{zone}/add", "POST", record) 154 | print(r) 155 | 156 | else: 157 | console.print("Exited") 158 | 159 | 160 | def delete_record(zone, index): 161 | print(_request("Deleting record", f"zone/{zone}/delete_record/{index}", "POST")) 162 | 163 | 164 | def login(): 165 | console.print("[underline]PacketFrame Login") 166 | account = prompt([ 167 | { 168 | "type": "input", 169 | "name": "username", 170 | "message": "Email:", 171 | }, 172 | { 173 | "type": "password", 174 | "message": "Password:", 175 | "name": "password" 176 | } 177 | ], style=PYINQUIRER_STYLE) 178 | 179 | if account: 180 | r = _request(f"Logging in as {account['username']}", "auth/login", "POST", account) 181 | console.print("[bold green]Login successful") 182 | if not exists(config_path): 183 | makedirs(config_path) 184 | with open(config_path + "key", "w") as api_key_file: 185 | api_key_file.write(r) 186 | with open(config_path + "email", "w") as email_file: 187 | email_file.write(account["username"]) 188 | 189 | 190 | def account(): 191 | with open(config_path + "email", "r") as email_file: 192 | console.print(f":lock: Logged in as [underline]{email_file.read()}") 193 | 194 | 195 | def logout(): 196 | rmtree(config_path) 197 | console.print("Logout complete") 198 | 199 | 200 | # Main 201 | if args["login"]: 202 | login() 203 | elif args["logout"]: 204 | logout() 205 | elif args["account"] and not args["login"]: 206 | account() 207 | elif args["zones"] and args["list"]: 208 | list_zones() 209 | elif args["records"] and not args["add"] and not args["delete"]: 210 | list_records(args[""]) 211 | elif args["records"] and args["add"]: 212 | add_record(args[""]) 213 | elif args["records"] and args["delete"]: 214 | delete_record(args[""], args[""]) 215 | -------------------------------------------------------------------------------- /util/requirements.txt: -------------------------------------------------------------------------------- 1 | PyInquirer~=1.0.3 2 | requests~=2.24.0 3 | docopt~=0.6.2 4 | rich~=9.5.1 --------------------------------------------------------------------------------