├── .github └── workflows │ └── gotest.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── bind9 ├── Makefile ├── README.md ├── ansible │ ├── Makefile │ ├── ansible.cfg │ ├── hosts.yml │ ├── roles │ │ └── sig0namectl │ │ │ └── tasks │ │ │ ├── install.yml │ │ │ └── main.yml │ └── sig0namectl.yml ├── start.sh └── terraform │ ├── Makefile │ ├── ansible-ssh-keygen.sh │ ├── cloud_init.cfg.tmpl │ ├── main.tf │ └── terraform.tfvars ├── demo ├── .gitignore ├── DNS-SD browser notes.md ├── JsUI.md ├── map │ ├── README.md │ ├── dohjs_helpers.js │ ├── map.html │ └── map.js └── playground │ ├── Makefile │ ├── README.md │ ├── dns.js │ ├── domain_manager.css │ ├── domain_manager.html │ ├── domain_manager.js │ ├── domains.js │ ├── fontawesome │ ├── LICENSE.txt │ ├── css │ │ ├── fontawesome.min.css │ │ ├── regular.min.css │ │ └── solid.min.css │ └── webfonts │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.ttf │ │ └── fa-solid-900.woff2 │ ├── go.work │ ├── keys.js │ ├── play.css │ ├── play.html │ ├── sd.css │ ├── sd_inspector.html │ ├── sd_inspector.js │ ├── services.js │ ├── sig0.css │ ├── sig0.js │ ├── sig0_wasm.js │ └── wasm_exec.html ├── dnssd-domain ├── dnssd-service ├── docs ├── CNAME ├── DNS-SD Introduction.md ├── index.md ├── sig0namectl Dynamic DNSSEC Server Zone Configuration.md └── sig0namectl Key Management.md ├── dyn_ip ├── dyn_key ├── dyn_loc ├── dyn_txt ├── env ├── env.dnssd-service ├── functions ├── 10-set_vars.sh ├── get_loc.sh ├── get_sig0_keyid.sh ├── get_soa.sh ├── send_nsupdate.sh ├── validateIP.sh └── validateKEY.sh ├── golang ├── .gitignore ├── Makefile ├── cmd │ └── sig0namectl │ │ ├── cli.go │ │ ├── keys.go │ │ ├── query.go │ │ └── update.go ├── env ├── go.mod ├── go.sum ├── notes ├── sig0 │ ├── answers.go │ ├── doh.go │ ├── doh_test.go │ ├── keys.go │ ├── keys_nowasm.go │ ├── keys_test.go │ ├── keys_wasm.go │ ├── query.go │ ├── query_test.go │ ├── request_key.go │ ├── request_key_test.go │ ├── update.go │ └── update_test.go └── wasm │ ├── Makefile │ ├── README.md │ ├── pure.js │ ├── wasm_exec.html │ ├── wasm_exec.js │ └── wrapper_js.go ├── keystore └── .keep ├── presentations ├── 20230815 CCCamp 2023 To Name is to Own.odp ├── 20230815 CCCamp 2023 To Name is to Own.pdf ├── 20230824 Draft Improved Presentation.odp ├── 20230824 Draft Improved Presentation.pdf ├── 20240501 Freifunk Berlin May meeting Presentation.pdf ├── 20240525 P4P Unconference at offline-space.odp └── 20241211 splintercon sig0namectl 16-9 v3.7.pdf ├── process_requests ├── request_key ├── start_loc_loop.sh └── start_process_requests_loop.sh /.github/workflows/gotest.yml: -------------------------------------------------------------------------------- 1 | name: "test" 2 | 3 | on: [push] 4 | 5 | jobs: 6 | gotest: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Set up Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: 1.22 15 | - name: update apt pkgs 16 | run: sudo apt-get update 17 | - name: install bind9 utilities 18 | run: sudo apt-get install bind9utils 19 | - name: Run tests 20 | run: go test -v ./... 21 | working-directory: ./golang 22 | - name: Check CLI compilation 23 | run: go build 24 | working-directory: ./golang/cmd/sig0namectl 25 | - name: Check WASM compilation 26 | run: GOOS=js GOARCH=wasm go build -o test.wasm 27 | working-directory: ./golang/wasm 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | flake.nix 2 | flake.lock 3 | # exclude everything 4 | keystore/* 5 | # terraform 6 | bind9/terraform/cloud_init.cfg 7 | bind9/terraform/terraform.tfstate* 8 | bind9/terraform/.terraform.lock.hcl 9 | bind9/terraform/.terraform/ 10 | # ansible 11 | bind9/ansible/ansible_ssh_key* 12 | # exceptions to the rule 13 | !keystore/.keep 14 | !zones/.keep 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bind9/ansible/roles/bind9"] 2 | path = bind9/ansible/roles/bind9 3 | url = https://github.com/adam-burns/ansible-role-bind.git 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "standard.vscode-standard" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "dnssec", 4 | "keypair", 5 | "Nameserver", 6 | "rrsig", 7 | "sig0namectl", 8 | "wireformat" 9 | ], 10 | 11 | "javascript.format.enable": false, 12 | "javascript.validate.enable": false, 13 | "standard.enable": true, 14 | "standard.autoFixOnSave": true, 15 | "[javascript]": { 16 | "editor.formatOnSave": true, 17 | }, 18 | 19 | "[css]": { 20 | "editor.formatOnSave": true 21 | } 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | sig0namectl
3 | 4 |

5 | 6 |

7 | to name is to own 8 |

9 | 10 |

11 | 12 |

13 | 📝 Prepare 14 | 15 | 💾 Install 16 | 17 | 🎮 Quick start 18 | 19 | 🌐 Links 20 | 21 | 👤 Contributing 22 | 23 | 💼 License 24 |

25 | 26 | 27 | sig0namectl is a set of tools that allow secure dynamic DNS updates, allowing for further delegation of update rights to others using standards-based, secure SIG(0) key based DNS update authentication. 28 | 29 |
30 | 🚩 Table of Contents (click to expand) 31 | 32 | * [Prepare](#-prepare) 33 | * [Install](#-install) 34 | * [Quick start](#-quick-start) 35 | * [Links](#-links) 36 | * [Contributing](#-contributing) 37 | * [License](#-license) 38 |
39 | 40 | ## 📝 Prepare 41 | 42 | Install dependencies. 43 | 44 | For Debian and derivatives: 45 | 46 | `apt install bind9-dnsutils python golang` 47 | 48 | For Fedora and related distributions and derivates 49 | 50 | `dnf install bind-utils python golang` 51 | 52 | 53 | ## ⛭ Build 54 | 55 | To build the golang executable utility `sig0namectl`, 56 | 57 | ``` 58 | cd golang 59 | make sig0namectl 60 | ``` 61 | 62 | To build the browser based GUIs, use `make` to build the target within each directory under /demo. 63 | 64 | Each browser GUI application can be locally run by executing `make start` in their respective directories under the repo directory demo/ 65 | 66 | ## 💾 Install 67 | 68 | The Bash tools can be copied to a directory in your in your current `$PATH`. Environment variable `$NSUPDATE_SIG0_KEYPATH` defines location of the keystore directory. 69 | 70 | Once built, the golang command line tool `sig0namectl` can simply be copied to a directory in your current `$PATH`. 71 | 72 | Each browser GUI application can be installed by simply by copying across the application directory files to the desired location served by a web server. 73 | 74 | ## 🎮 Quick start 75 | 76 | ### Registering a named key 77 | 78 | ```mermaid 79 | sequenceDiagram 80 | autonumber 81 | participant R as Requester

(DNSSEC client) 82 | participant P as Provider

(DNSSEC server) 83 | 84 | R->>R: Generate named keypair 85 | R->>P: Request registration of named public key 86 | P->>P: Apply registration policy 87 | break when named public key fails policy 88 | P->>R: Show unsuccessful registration 89 | end 90 | P->>+P: Publish & sign named public KEY record 91 | P->>R: Show successful named key registration 92 | ``` 93 | 94 | By default, DNS key labels beneath a compatible domain can be claimed on a "First Come, First Served" (FCFS) basis. 95 | 96 | To request a key registration within a compatible domain (*zenr.io* is an example domain for a public playground), use the `request_key` tool, specifying the fully qualified domain name (FQDN) of the new domain you wish to control. For example, under the *zenr.io* domain, issuing: 97 | 98 | `request_key mysubdomain.zenr.io` 99 | 100 | will create a new ED25519 keypair in your local keystore (where '*my_subdomain*' is unclaimed on a FCFS basis). 101 | 102 | 103 | The successful registration can be verified by 104 | 105 | `dig mysubdomain.zenr.io KEY` 106 | 107 | returning the listed public key for the specific FQDN. 108 | 109 | the keypair is enabled to add, modify or delete any DNS resource record at or under [*.]*mysubdomain.zenr.io*. 110 | 111 | Note: It may take a minute or so for your local DNS resolver to update its cache with the new key. 112 | 113 | ### Updating resource records with a named key 114 | 115 | ```mermaid 116 | sequenceDiagram 117 | autonumber 118 | participant R as Requester

(DNSSEC client) 119 | participant P as Provider

(DNSSEC server) 120 | R->>R: Create resource record updates & sign with named private key 121 | R->>P: Request publication of updated records 122 | P->>P: Verify update request signature against registered named key 123 | break when update request signature does not match registered named public key 124 | P-->>R: Show unsuccessful update 125 | end 126 | P->>P: Publish & sign updated resource records 127 | P->>R: Show successful update 128 | ``` 129 | 130 | To manage a fully qualified domain name, you will need the keypair for that FQDN in your local keystore directory (./keystore). Advanced users can use -k and -s flags to specify other keys when needed. 131 | 132 | #### `dyn_ip fqdn [ip4]|[ip6] ...` 133 | 134 | Manages A & AAAA records for the specified fully qualified domain name, fqdn. 135 | 136 | #### `dyn_loc fqdn` 137 | 138 | Updates LOC records for fqdn from GPS source (currently compatible with Android mobile devices using termux-location). 139 | 140 | #### `dnssd-domain fqdn` 141 | 142 | Manages DNS records necessary to activate wide area DNS Service Discovery browsing. 143 | 144 | #### `dnssd-service fqdn` 145 | 146 | Gives an *example* of how to register browsable wide area DNS-SD services. 147 | 148 | #### `nsupdate -k path_to_keypair_prefix` 149 | 150 | Successfully registered keypairs are stored in your local keystore and also can be used with the standard DNS tool, `nsupdate` (using -k option to specify the keypair prefix filepath). See `man nsupdate` for further details. 151 | 152 | *** 153 | **[🔝 back to top](#toc)** 154 | 155 | 156 | ## 😍 Acknowledgements 157 | 158 | Copyleft (ɔ) 2022 Adam Burns, [free2air limited](https://free2air.net) & the [Dyne.org](https://www.dyne.org) foundation, Amsterdam 159 | 160 | Designed, written and maintained by Adam Burns. 161 | 162 | **[🔝 back to top](#toc)** 163 | 164 | *** 165 | ## 🌐 Links 166 | 167 | 168 | 169 | **[🔝 back to top](#toc)** 170 | 171 | *** 172 | ## 👤 Contributing 173 | 174 | 1. 🔀 [FORK IT](../../fork) 175 | 2. Create your feature branch `git checkout -b feature/branch` 176 | 3. Commit your changes `git commit -am 'Add some fooBar'` 177 | 4. Push to the branch `git push origin feature/branch` 178 | 5. Create a new Pull Request 179 | 6. 🙏 Thank you 180 | 181 | 182 | **[🔝 back to top](#toc)** 183 | 184 | *** 185 | ## 💼 License 186 | sig0namectl - 187 | Copyright (c) 2023 Adam Burns, free2air limited 188 | 189 | This program is free software: you can redistribute it and/or modify 190 | it under the terms of the GNU Affero General Public License as 191 | published by the Free Software Foundation, either version 3 of the 192 | License, or (at your option) any later version. 193 | 194 | This program is distributed in the hope that it will be useful, 195 | but WITHOUT ANY WARRANTY; without even the implied warranty of 196 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 197 | GNU Affero General Public License for more details. 198 | 199 | You should have received a copy of the GNU Affero General Public License 200 | along with this program. If not, see . 201 | 202 | **[🔝 back to top](#toc)** 203 | -------------------------------------------------------------------------------- /bind9/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := sig0namectl 2 | 3 | init: 4 | make -C terraform init 5 | 6 | build: 7 | make -C terraform build 8 | 9 | destroy: 10 | make -C terraform destroy 11 | 12 | clean: 13 | make -C terraform clean 14 | make -C ansible clean_ssh_key 15 | play: 16 | ANSIBLE_HOST_KEY_CHECKING=False make -C ansible play 17 | 18 | create-zone: 19 | make -C ansible create-zone 20 | 21 | so: 22 | make -C ansible so 23 | 24 | 25 | all: destroy init build play 26 | -------------------------------------------------------------------------------- /bind9/README.md: -------------------------------------------------------------------------------- 1 | # sig0namectl: BIND9 DNS server utilities 2 | 3 | ## dependencies 4 | - terraform 5 | - ansible 6 | - libvirt-nss (optional to access to VM by hostname from host) 7 | - virt-manager (optional gui for local VM management) 8 | 9 | ## system configuration 10 | - ensure libvirt permissions for host user 11 | - configure nsswitch.conf for libvirt-nss (optional) 12 | - issue `make init` to configure terraform & ansible 13 | 14 | ## quick start 15 | - issue `./start.sh` (note pause to allow terraform time to reboot host) 16 | 17 | ## access to VM 18 | - default VM hostname is sig0namectl 19 | - ssh access to DNS VM via ansible user key (`ansible/ansible_ssh_key`) ie `ssh -i ansible/ansible_ssh_key ansible@sig0namectl` (with libvirt-nss configured) 20 | 21 | ## TODO 22 | - [ ] ansible scripting for 23 | - [ ] dynamic zone 24 | - [ ] dynamic \_signal zone 25 | - [ ] DNSSEC 26 | 27 | 28 | see Makefile for detailed actions 29 | 30 | # Further Research & Notes 31 | 32 | ## Terraform 33 | 34 | ### Background 35 | 36 | Using terraform with a libvirt adapter, a local VM for DNS testing can be created or destroyed. 37 | 38 | - [How To Install Terraform on Fedora ...](https://computingforgeeks.com/how-to-install-terraform-on-fedora/) 39 | 40 | - Initialise [Terraform libvirt provider](https://registry.terraform.io/providers/dmacvicar/libvirt/latest) 41 | - in the local directory where main.tf & terraform.tfvars are located, 42 | 43 | ### Terraform and ENV variables 44 | 45 | As a fallback for the other ways of defining variables, Terraform searches the environment of its own process for environment variables named `TF_VAR_` followed by the name of a declared variable. 46 | 47 | This can be useful when running Terraform in automation, or when running a sequence of Terraform commands in succession with the same variables. For example, at a bash prompt on a Unix system: 48 | 49 | ``` 50 | $ export TF_VAR_image_id=ami-abc123 51 | $ terraform plan... 52 | ``` 53 | 54 | ### Optional: Bridged instead NAT Network 55 | 56 | In the example above, we will create a virtual machine using libvirt’s default NAT network. Sometimes it is useful or necessary to run the VM on the same subnet as the KVM host. For this purpose we have to run the VM over a so called Bridge Network Device. 57 | 58 | For this we first have to create a network bridge under Linux. 59 | 60 | ``` 61 |  sudo brctl addbr br1 62 | 63 |  sudo brctl show 64 | bridge name bridge id STP enabled interfaces 65 | br1 8000.f6e545eb05ba no 66 | docker0 8000.02420865003e no 67 | virbr0 8000.525400b83e08 yes virbr0-nic 68 | vnet0 69 | 70 |  sudo brctl addif br1 enp0s31f6 71 | 72 |  brctl show 73 | bridge name bridge id STP enabled interfaces 74 | br1 8000.f6e545eb05ba no enp0s31f6 75 | vnet0 76 | docker0 8000.02420865003e no 77 | virbr0 8000.525400b83e08 yes virbr0-nic 78 | ``` 79 | 80 | We can now use this virtual Bridge Device (br1) with the libvirt provider in our Terraform module to create a libvirt network. Examples of libvirt_network can be found here: https://github.com/dmacvicar/terraform-provider-libvirt/blob/master/website/docs/r/network.markdown 81 | 82 | ``` 83 | resource "libvirt_network" "vmbridge" { 84 | # the name used by libvirt 85 | name = "vmbridge" 86 | 87 | # mode can be: "nat" (default), "none", "route", "bridge" 88 | mode = "bridge" 89 | 90 | # (optional) the bridge device defines the name of a bridge device 91 | # which will be used to construct the virtual network. 92 | # (only necessary in "bridge" mode) 93 | bridge = "br1" 94 | autostart = true 95 | } 96 | ``` 97 | ### Initial local DNS resolution 98 | 99 | In order to allow ansible to initially access KVM VM hosts locally by name, install [libvirt-nss](https://libvirt.org/nss.html) and configure `/etc/nsswitch.conf` 100 | 101 | ## Ansible 102 | 103 | Using ansible, the DNS VM host can be prepared for sig0namectl by: 104 | - installing all package dependencies for DNSSEC enabled DNS server 105 | - configuring BIND9 & authoritative zones for: 106 | - DNSSEC 107 | - dynamic updates with SIG(0) 108 | - \_signal open update subzone for KEY requests 109 | 110 | 111 | -------------------------------------------------------------------------------- /bind9/ansible/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := sig0namectl 2 | 3 | ${PROJECT}: 4 | ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.yml ${PROJECT}.yml 5 | 6 | create-zone: 7 | ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.yml -vvv create_zone.yml 8 | 9 | so: 10 | ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.yml -vvv so_playbook.yml 11 | 12 | clean_ssh_key: 13 | rm ansible/ansible_ssh_key 14 | rm ansible/ansible_ssh_key.pub 15 | 16 | -------------------------------------------------------------------------------- /bind9/ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | hash_behaviour=merge 3 | host_key_checking=false 4 | 5 | [ssh_connection] 6 | pipelining = True 7 | timeout=30 8 | -------------------------------------------------------------------------------- /bind9/ansible/hosts.yml: -------------------------------------------------------------------------------- 1 | dns_servers: 2 | vars: 3 | ansible_connection: ssh 4 | ansible_user: ansible 5 | ansible_become: true 6 | ansible_ssh_private_key_file: ansible_ssh_key 7 | ansible_python_interpreter: /usr/bin/python3 8 | 9 | hosts: 10 | vmtest: 11 | -------------------------------------------------------------------------------- /bind9/ansible/roles/sig0namectl/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install some packages 3 | hosts: dns_servers 4 | tasks: 5 | 6 | # Wait until host is ready 7 | - name: Wait for host {{inventory_hostname}} connection 8 | wait_for_connection: 9 | 10 | # Upgrade all packages 11 | - name: upgrade all packages on CentOS/Fedora 12 | yum: 13 | name: "*" 14 | state: latest 15 | when: ansible_os_family == 'RedHat' 16 | register: result 17 | tags: 18 | - upgrade 19 | - debug: 20 | var: result.changed 21 | tags: 22 | - upgrade 23 | 24 | # Upgrade all packages 25 | - name: update apt cache on Debian derivative 26 | apt: 27 | update_cache: yes 28 | - name: upgrade all packages on Debian derivative 29 | apt: 30 | name: "*" 31 | state: latest 32 | when: ansible_os_family == 'Debian' 33 | register: result 34 | tags: 35 | - upgrade 36 | - debug: 37 | var: result.changed 38 | tags: 39 | - upgrade 40 | 41 | # Some useful packages for Debian 42 | - name: install bind9 for Debian derivative 43 | package: 44 | name: bind9 45 | state: present 46 | when: ansible_os_family == 'Debian' 47 | 48 | # Some useful packages for Debian 49 | - name: install bind-utils for Debian derivative 50 | package: 51 | name: bind9utils 52 | state: present 53 | when: ansible_os_family == 'Debian' 54 | 55 | 56 | # Install for all distributions Debian/Redhat 57 | - name: install bash-completion 58 | package: 59 | name: bash-completion 60 | state: present 61 | 62 | - name: install wireguard-tools 63 | package: 64 | name: wireguard-tools 65 | state: present 66 | 67 | # Set timezone 68 | - name: set timezone 69 | timezone: 70 | name: Europe/Berlin 71 | 72 | # Reboot when all things done 73 | # - reboot: 74 | # msg: "Reboot to finish setup." 75 | # reboot_timeout: 60 76 | # when: result.changed == "true" 77 | 78 | -------------------------------------------------------------------------------- /bind9/ansible/roles/sig0namectl/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Wait until host is ready 4 | - name: Wait for host {{inventory_hostname}} connection 5 | wait_for_connection: 6 | 7 | - name: checkout sig0namectl 8 | ansible.builtin.git: 9 | repo: 'https://github.com/NetworkCommons/sig0namectl.git' 10 | dest: src/sig0namectl 11 | become: true 12 | become_user: ansible 13 | 14 | # if keypair for host exists in local keystore, copy .key & .private files to host keystore 15 | # 16 | - name: does host .key exist in local keystore 17 | delegate_to: localhost 18 | find: 19 | paths: "{{ sig0namectl_local_keystore }}" 20 | patterns: "K{{ sig0namectl_key }}*.key" 21 | register: sig0_keyfile 22 | 23 | - name: does host .private exist in local keystore 24 | delegate_to: localhost 25 | find: 26 | paths: "{{ sig0namectl_local_keystore }}" 27 | patterns: "K{{ sig0namectl_key }}*.private" 28 | register: sig0_privatefile 29 | 30 | - name: copy keyfile if key exists in local keystore 31 | ansible.builtin.copy: 32 | src: "{{ item }}" 33 | dest: "{{ sig0namectl_remote_keystore }}" 34 | mode: preserve 35 | loop: "{{ sig0_keyfile.files|map(attribute='path')|list }}" 36 | become: true 37 | become_user: ansible 38 | when: sig0_keyfile.matched|int == 1 39 | 40 | - name: copy privatefile if key exists in local keystore 41 | ansible.builtin.copy: 42 | src: "{{ item }}" 43 | dest: "{{ sig0namectl_remote_keystore }}" 44 | mode: preserve 45 | loop: "{{ sig0_privatefile.files|map(attribute='path')|list }}" 46 | become: true 47 | become_user: ansible 48 | when: sig0_privatefile.matched|int == 1 49 | 50 | # - name: debug getting KEY RR from host .key file 51 | # ansible.builtin.slurp: 52 | # src: "{{ sig0_keyfile.files|map(attribute='path') }}" 53 | # register: sig0_keyfile_RR 54 | 55 | #- name: testing 56 | # debug: 57 | # # msg: "{{ lookup('file', sig0_keyfile.files['path']|string) }}" 58 | # msg: "{{ lookup('file', sig0_keyfile.files|map(attribute='path')|string) }}" 59 | # # msg: "{{ sig0_keyfile.files|map(attribute='path')|string }}" 60 | # 61 | - name: get KEY RR 62 | command: "cat {{ sig0namectl_remote_keystore }}/{{ sig0_keyfile.files[0].path|basename }}" 63 | become: true 64 | become_user: ansible 65 | register: sig0_keyRR 66 | when: sig0_keyfile.matched|int == 1 67 | -------------------------------------------------------------------------------- /bind9/ansible/sig0namectl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Configure zones for sig0namectl 3 | hosts: dns_servers 4 | vars: 5 | # allow_new_zones: true # allow zone management through rndc 6 | # sig0namectl_local_keystore: "/home/vortex/src/sig0namectl/keystore" 7 | sig0namectl_local_keystore: "~/src/sig0namectl/keystore" 8 | sig0namectl_remote_keystore: "~/src/sig0namectl/keystore" 9 | sig0namectl_key: vmtest.zenr.io 10 | bind_zone_ttl: "30" # wind down cacheing TTL to 30 seconds 11 | bind_zone_subdirs: true 12 | bind_zones: 13 | - name: "{{ sig0namectl_key }}" 14 | add_sig0_key: true 15 | # auto_dnssec: maintain 16 | dnssec_policy_default: true 17 | create_reverse_zones: false 18 | primaries: 19 | - "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}" 20 | - "{{ ansible_default_ipv6.address|default(ansible_all_ipv6_addresses[0]) }}" 21 | name_servers: 22 | - ns1.example.com. 23 | - ns2.example.com. 24 | update_policy_sig0: 25 | - "TODO: POLICY IS STILL HARDCODED IN TEMPLATE" 26 | - "grant \"vortex.zenr.io\" name zenr.io. ANY" 27 | - "grant \"vortex.zenr.io\" subdomain zenr.io. ANY" 28 | - "grant * selfsub . ANY" 29 | hosts: 30 | - name: '@' 31 | ip: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}" 32 | ipv6: "{{ ansible_default_ipv6.address|default(ansible_all_ipv6_addresses[0]) }}" 33 | - name: "_signal.{{ sig0namectl_key }}" 34 | # auto_dnssec: maintain 35 | dnssec_policy_default: true 36 | create_reverse_zones: false 37 | primaries: 38 | - "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}" 39 | - "{{ ansible_default_ipv6.address|default(ansible_all_ipv6_addresses[0]) }}" 40 | check_names: ignore 41 | name_servers: 42 | - ns1.example.com. 43 | - ns2.example.com. 44 | allow_update: ['any'] 45 | hosts: 46 | - name: '@' 47 | ip: "{{ ansible_default_ipv4.address|default(ansible_all_ipv4_addresses[0]) }}" 48 | ipv6: "{{ ansible_default_ipv6.address|default(ansible_all_ipv6_addresses[0]) }}" 49 | roles: 50 | - sig0namectl 51 | - bind9 52 | -------------------------------------------------------------------------------- /bind9/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export ANSIBLE_HOST_KEY_CHECKING=False 3 | make destroy 4 | sleep 10 5 | sed -i.bak '/sig0namectl ssh-ed25519 /d' ~/.ssh/known_hosts 6 | make build 7 | sleep 100 8 | make -C ansible sig0namectl 9 | -------------------------------------------------------------------------------- /bind9/terraform/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := sig0namectl 2 | 3 | init: 4 | cp cloud_init.cfg.tmpl cloud_init.cfg 5 | @./ansible-ssh-keygen.sh 6 | terraform init 7 | 8 | build: 9 | terraform apply -auto-approve 10 | 11 | destroy: 12 | terraform destroy -auto-approve 13 | 14 | clean: 15 | terraform destroy -auto-approve 16 | rm -fr .terraform 17 | rm .terraform.lock.hcl 18 | 19 | rebuild: destroy init apply 20 | -------------------------------------------------------------------------------- /bind9/terraform/ansible-ssh-keygen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | export ANSIBLE_SSH_KEY="../ansible/ansible_ssh_key" 5 | [ ! -f ${ANSIBLE_SSH_KEY} ] && ssh-keygen -f ${ANSIBLE_SSH_KEY} -t ed25519 -q -N "" -C "ansible-ssh-key" || echo "${ANSIBLE_SSH_KEY} already exists ... updating cloud_init.cfg" 6 | sed -i 's/\(.*\)ssh-ed25519\(.*\)/echo '\''\1'\''$(cat ${ANSIBLE_SSH_KEY}.pub)/e' cloud_init.cfg 7 | # cat cloud_init.cfg 8 | -------------------------------------------------------------------------------- /bind9/terraform/cloud_init.cfg.tmpl: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | # vim: syntax=yaml 3 | # 4 | # *********************** 5 | # ---- for more examples look at: ------ 6 | # ---> https://cloudinit.readthedocs.io/en/latest/topics/examples.html 7 | # ****************************** 8 | # 9 | # This is the configuration syntax that the write_files module 10 | # will know how to understand. encoding can be given b64 or gzip or (gz+b64). 11 | # The content will be decoded accordingly and then written to the path that is 12 | # provided. 13 | # 14 | # Note: Content strings here are truncated for example purposes. 15 | ssh_pwauth: false 16 | chpasswd: 17 | list: | 18 | root:Geheim1234 19 | expire: false 20 | 21 | # User 'ansible' is used for ansible 22 | users: 23 | - name: ansible 24 | ssh_authorized_keys: 25 | - ssh-ed25519 not yet configured 26 | sudo: ['ALL=(ALL) NOPASSWD:ALL'] 27 | shell: /bin/bash 28 | groups: wheel 29 | - name: root 30 | 31 | # Set hostname based on main.tf variables 32 | preserve_hostname: false 33 | fqdn: ${hostname}.${domainname} 34 | hostname: ${hostname} 35 | 36 | # Initiate a reboot after setting the fqdn. It's necessary to update the DNS/DHCP information in libwirt dnsmasq 37 | power_state: 38 | delay: "+1" 39 | mode: reboot 40 | condition: true 41 | 42 | # Install python for ansible 43 | packages: 44 | - python36 45 | - wireguard-tools 46 | #package_update: true 47 | #package_upgrade: true 48 | #package_reboot_if_required: true 49 | 50 | -------------------------------------------------------------------------------- /bind9/terraform/main.tf: -------------------------------------------------------------------------------- 1 | # Declare libvirt provider for this project 2 | terraform { 3 | required_version = ">= 0.13" 4 | required_providers { 5 | libvirt = { 6 | source = "dmacvicar/libvirt" 7 | } 8 | } 9 | } 10 | # Provider URI for libvirt 11 | provider "libvirt" { 12 | uri = "qemu:///system" 13 | } 14 | 15 | # Use terraform.tfvars to define the settings of your servers 16 | # the variables here are the defaults if no terraform.tfvars setting is found 17 | variable "projectname" { 18 | type = string 19 | default = "sig0namectl" 20 | } 21 | variable "hosts" { 22 | default = { 23 | "srv1" = { 24 | name = "srv1", 25 | vcpu = 1, 26 | memory = "1536", 27 | diskpool = "default", 28 | disksize = "4000000000", 29 | mac = "52:54:00:11:11:11", 30 | }, 31 | } 32 | } 33 | variable "baseimagediskpool" { 34 | type = string 35 | default = "default" 36 | } 37 | variable "domainname" { 38 | type = string 39 | default = "domain.local" 40 | } 41 | variable "networkname" { 42 | type = string 43 | default = "default" 44 | } 45 | variable "sourceimage" { 46 | type = string 47 | default = "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2" 48 | } 49 | 50 | # Base OS image 51 | resource "libvirt_volume" "baseosimage" { 52 | name = "baseosimage_${var.projectname}" 53 | source = var.sourceimage 54 | pool = var.baseimagediskpool 55 | } 56 | 57 | # Create a virtual disk per host based on the Base OS Image 58 | resource "libvirt_volume" "qcow2_volume" { 59 | for_each = var.hosts 60 | name = "${each.value.name}.qcow2" 61 | base_volume_id = libvirt_volume.baseosimage.id 62 | pool = each.value.diskpool 63 | format = "qcow2" 64 | size = each.value.disksize 65 | } 66 | 67 | # Use cloudinit config file and forward some variables to cloud_init.cfg 68 | data "template_file" "user_data" { 69 | template = file("${path.module}/cloud_init.cfg") 70 | for_each = var.hosts 71 | vars = { 72 | hostname = each.value.name 73 | domainname = var.domainname 74 | } 75 | } 76 | 77 | # Use CloudInit to add the instance 78 | resource "libvirt_cloudinit_disk" "commoninit" { 79 | for_each = var.hosts 80 | name = "commoninit_${each.value.name}.iso" 81 | user_data = data.template_file.user_data[each.key].rendered 82 | } 83 | 84 | # Define KVM-Guest/Domain 85 | resource "libvirt_domain" "newvm" { 86 | for_each = var.hosts 87 | name = each.value.name 88 | memory = each.value.memory 89 | vcpu = each.value.vcpu 90 | 91 | network_interface { 92 | network_name = var.networkname 93 | # mac = each.value.mac 94 | # If networkname is host-bridge do not wait for a lease 95 | wait_for_lease = var.networkname == "host-bridge" ? false : true 96 | } 97 | 98 | disk { 99 | volume_id = element(libvirt_volume.qcow2_volume[each.key].*.id, 1 ) 100 | } 101 | 102 | cloudinit = libvirt_cloudinit_disk.commoninit[each.key].id 103 | 104 | } 105 | ## END OF KVM DOMAIN CONFIG 106 | 107 | # Output results to console 108 | output "hostnames" { 109 | value = [libvirt_domain.newvm.*] 110 | } 111 | 112 | -------------------------------------------------------------------------------- /bind9/terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # Projectname 2 | projectname = "sig0namectl" 3 | 4 | # OS Image 5 | #sourceimage = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img" 6 | #sourceimage = "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64.img" 7 | sourceimage = "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img" 8 | #sourceimage = "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.2.2004-20200611.2.x86_64.qcow2" 9 | #sourceimage = "/home/juergen/images/CentOS-8-GenericCloud-8.2.2004-20200611.2.x86_64.qcow2" 10 | 11 | # The baseimage is the source diskimage for all VMs created from the sourceimage 12 | baseimagediskpool = "default" 13 | 14 | # Domain and network settings 15 | #domainname = "mydomain.vm" 16 | domainname = "zenr.io" 17 | networkname = "default" # Virtual Networks: default (=NAT) 18 | 19 | # Host specific settings 20 | # RAM size in bytes 21 | # Disksize in bytes (disksize must be bigger than sourceimage virtual size) 22 | # Example: 23 | # qemu-img info debian-10.3.4-20200429-openstack-amd64.qcow2 24 | # virtual size: 2 GiB (2147483648 bytes) 25 | hosts = { 26 | "vmtest" = { 27 | name = "vmtest", 28 | vcpu = 8 29 | memory = "4096", 30 | diskpool = "default", 31 | disksize = 12000000000, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | *.wasm 2 | nohup.out 3 | -------------------------------------------------------------------------------- /demo/DNS-SD browser notes.md: -------------------------------------------------------------------------------- 1 | #### DNSSD browsing demo app 2 | (see [RFC 6763: DNS-Based Service Discovery](https://www.rfc-editor.org/rfc/rfc6763)) 3 | 4 | 5 | 6 | No domains defined 7 | 8 | ``` 9 | 🞣 10 | ``` 11 | --- 12 | Select/click 🞣 to type & add main domain 13 | 14 | ``` 15 | 🞣 zembla.zenr.io 16 | ``` 17 | --- 18 | If new valid domain is entered and has DNS-SD browsing domain PTRs, show the SD domain in retracted state. 19 | 20 | ``` 21 | ▶ zembla.zenr.io 22 | 🞣 23 | ``` 24 | --- 25 | Expanding the SD domain displays a list of browsing domains, defined as the combined list of unique PTR record values of db.\_dns-sd.\_udp (single RR), and b.\_dns-sd.\_udp (one or more RRs) of the SD domain. 26 | - Expanding the SD domain triggers regular updates of browsing domain PTR enumeration. 27 | - Retracting the SD domain stops regular updates of browsing domain enumeration of the main SD domain. 28 | 29 | ``` 30 | 31 | ▼ zembla.zenr.io 32 | ▶ zembla.zenr.io 33 | 🞣 34 | ``` 35 | --- 36 | If the browser has access to a private KEY capable of updating the SD domain, this could be signified in rendering, eg. something like ⨯ delete, 🞣 add 37 | 38 | 39 | ``` 40 | ▼ zembla.zenr.io 41 | ⨯ ▶ zembla.zenr.io 42 | 🞣 43 | 🞣 44 | ``` 45 | --- 46 | 47 | Extra SD browsing domain dns-sd.org (e.g. added manually as above or by outside process) dynamically appears (only whilst expanded) 48 | 49 | ``` 50 | ▼ zembla.zenr.io 51 | ⨯ ▶ zembla.zenr.io 52 | ⨯ ▶ dns-sd.org 53 | 🞣 54 | 🞣 55 | ``` 56 | 57 | --- 58 | Expanding a browsing domain displays the service instance types available under the browsing domain, defined as the list of PTR record values of \_services.\_dns-sd.\_udp under the browsing domain. 59 | 60 | - Expanding a browsing domain triggers regular service instance type enumeration updates for the browsing domain. 61 | - Retracting a browsing domain the stops the regular updates of the service instance type enumeration for the browsing domain. 62 | 63 | Note that the UI may want to use an internal friendly name for service types (displayed below) in addition to or instead of e.g. _loc._udp. 64 | 65 | 66 | ``` 67 | ▼ zembla.zenr.io 68 | ▼ zembla.zenr.io 69 | ▶ 🌐 location (_loc._udp) 70 | ▶ 🕸 web resource (_http._tcp) 71 | ▶ 🖨 printer (_lpr._tcp) 72 | 🞣 73 | ``` 74 | --- 75 | 76 | Expanding a service type of a browsing domain enumerates & displays a list of service instances of a service type, defined by resolving PTR records under the service type label of the browsing domain (eg _loc._udp.zembla.zenr.io) 77 | 78 | - Expanding a service type triggers regular service instance enumeration updates for the service type. 79 | - Retracting a service type stops the regular service instance enumeration updates for the service type. 80 | 81 | Note that for enumeration, the friendly-named "Schwarze Pumpe" example is enumerated from a PTR record: 82 | 83 | `Schwarze\032Pumpe._loc._udp.zembla.zenr.io. IN PTR Schwarze\032Pumpe.zembla.zenr.io.` 84 | 85 | 86 | ``` 87 | ▼ zembla.zenr.io 88 | ▼ zembla.zenr.io 89 | ▼ 🌐 location (_loc._udp) 90 | 🌐 redb.zenr.io 91 | 🌐 bluebox.zenr.io 92 | 🌐 zembla.zenr.io 93 | 🌐 op6.zenr.io 94 | 🌐 Schwarze Pumpe 95 | ▶ 🕸 web resource (_http._tcp) 96 | ▶ 🖨 printer (_lpr._tcp) 97 | 🞣 98 | ``` 99 | 100 | --- 101 | Selecting/Clicking on Service Instances (resolution via SRV, TXT & LOC records) provides service resolution ie enough information to connect to the service. 102 | Within most current browsers this requires small helper functions. 103 | 104 | For instance, "sig0namectl Documentation" web resources can be resolved with RR records at sig0namectl\032Documentation.zembla.zenr.io of SRV 0 0 80 test.zembla.zenr.io and TXT page=/doc, where a URL can then be constructed from SRV domain and port with path from TXT. 105 | 106 | Note that "Schwarze Pumpe" example is presented as a label under the active browsing domain and the service resolves to a LOC (SRV & TXT) records at Schwarze\032Pumpe.zembla.zenr.io. The map application could also resolve the LOC record directly. 107 | 108 | For example service instance resolution of web resource `sig0namectl Documentation` can result in 109 | 110 | SRV 0 0 80 sig0namectl.networkcommons.org 111 | TXT path=/docs 112 | 113 | Which gives the appliction enough information (domain, port and URL path) to construct the URL to the resource (and connect to the resource, perhaps in a new tab). 114 | 115 | 116 | ``` 117 | ▼ zembla.zenr.io 118 | ▼ zembla.zenr.io 119 | ▶ 🌐 location (_loc._udp) 120 | ▼ 🕸 web resource (_http._tcp) 121 | 🕸 sig0namectl Documentation 122 | ▶ 🖨 printer (_lpr._tcp) 123 | 🞣 124 | ``` 125 | 126 | 127 | -------------------------------------------------------------------------------- /demo/JsUI.md: -------------------------------------------------------------------------------- 1 | # sig0namectl Javascript UI documentation 2 | 3 | ## Javascript Event System 4 | 5 | The UI can listen to events in order to execute tasks 6 | related to these events. 7 | The following events exist. 8 | 9 | ### WASM Events 10 | 11 | - `wasm_ready`: This event occurs, when the WASM is fully loaded and ready to use. All functions using WASM should only be started after this event occurred. 12 | 13 | ### KEY Store Events 14 | 15 | The Javascript key store representation signals changes via the following events: 16 | 17 | - `keys_ready` 18 | - `keys_updated` 19 | -------------------------------------------------------------------------------- /demo/map/README.md: -------------------------------------------------------------------------------- 1 | Demo UI - map 2 | 3 | map.html is a sig0namectl demo that demonstrates the publication of decentralised voluntary publication of location information. 4 | 5 | Each keypair owner can publish and update their own location details via DNS LOC records under their keyname and choose to make this available to others to browse and publish over multiple DNS-SD domains. 6 | 7 | This means that each domain can select and maintain their own groups of continuously updated location points (both their own and those published by others). 8 | 9 | The markers on the map represents a curated group of GPS locations published by the DNS-SD domain keyname owner as a DNS-SD service (service type: \_loclist.\_udp). 10 | 11 | The markers resolve to DNS LOC records with each position published and updated by each of their keypair-holding owners through their unique respective sig0 keypairs. 12 | 13 | The end result is a curated decentralised group of location points collaboratively generated and maintained by their owners with no central web site or database. 14 | 15 | 16 | Current compatible location generators 17 | 18 | - Android phones: see sig0namectl Termux installation guide 19 | - Linux/Other: see sig0namectl gpsd installation guide 20 | 21 | How to create a location generator: 22 | - clone sig0namectl 23 | - create sig0namectl keypair for location generator using ./request_key script 24 | - ./dnssd-domain 25 | - ./dnssd-services 26 | - ./send\_loc 27 | 28 | How to curate a decentralised list of location markers 29 | - clone sig0namectl 30 | - create sig0namectl keypair for location generator using ./request_key script 31 | - ./dnssd-domain 32 | - ./dnssd-services 33 | - add service instance label PTR entries under .\_loc.\_udp. 34 | - add service instance SRV & TXT entries pointing to known DNS LOC resource record 35 | - (for SRV target, ensure the target resolves to at least one IPv4 or IPv6 address - avahi requires it) 36 | -------------------------------------------------------------------------------- /demo/map/dohjs_helpers.js: -------------------------------------------------------------------------------- 1 | /// DoHjs Helper Functions 2 | /// 3 | /// These functions help getting the values from binary wireformat 4 | /// which are not yet handled by DoHjs. 5 | 6 | /// Uint32 helper Class for byte conversion 7 | class Uint32 8 | { 9 | constructor(Value) 10 | { 11 | this.Number = new Uint32Array(1); 12 | this.Number[0] = Value; 13 | } 14 | get Get() 15 | { 16 | return this.Number[0]; 17 | } 18 | set Set(newValue) 19 | { 20 | this.Number[0] = newValue; 21 | } 22 | }; 23 | 24 | /// Convert u8 Byte array from network byte order (big-endian) to 32bit unsigned integer (little-endian) 25 | function ntoh_Uint32 (Source_Byte_Array, Start_Position) 26 | { 27 | var Uint32_Num = new Uint32(0); 28 | var Multiplier = 1; 29 | for (let i = 3; i >= 0; i--) 30 | { 31 | Uint32_Num.Set = Uint32_Num.Get + Source_Byte_Array[Start_Position + i] * Multiplier; 32 | Multiplier = Multiplier * 256; 33 | } 34 | return (Uint32_Num.Get); 35 | } 36 | 37 | /// LOC Wireformat Decoder 38 | class LocDecoder 39 | { 40 | /// constructor takes the raw wireformat bytes array 41 | constructor(wire_data) 42 | { 43 | this.data = wire_data; 44 | this.latitude = this.get_latitude(); 45 | this.longitude = this.get_longitude(); 46 | } 47 | 48 | /// get latitude 49 | get_latitude() { 50 | let latitude_raw = ntoh_Uint32(this.data, 4); 51 | let latitude = (latitude_raw - 2147483648.0) / 3600000; 52 | return latitude; 53 | } 54 | 55 | /// get longitude 56 | get_longitude() { 57 | let longitude_raw = ntoh_Uint32(this.data, 8); 58 | let longitude = (longitude_raw - 2147483648.0) / 3600000; 59 | return longitude; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /demo/map/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | sig0namectl: secure dynamic DNS resource location mapping 8 | 9 | 10 | 11 | 12 | 13 | 25 | 26 | 27 |
28 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/map/map.js: -------------------------------------------------------------------------------- 1 | /// Query Logic to show DNS LOC Records on a Map 2 | /// 3 | /// The MapLocQuery Object will 4 | 5 | /// Map LOC Query Object 6 | class MapLocQuery { 7 | /// construct this object with a domain to query 8 | constructor(domain) { 9 | this.domains = []; 10 | 11 | this.add_domain(domain); 12 | } 13 | 14 | /// add and query domain 15 | add_domain(domain) { 16 | let service_domain = new LocServiceDomain(domain); 17 | this.domains.push(service_domain); 18 | } 19 | } 20 | 21 | /// Collection of ServicePtr to query 22 | class LocServiceDomain { 23 | /// construct a new _loc service domain 24 | constructor(loc_service_domain) { 25 | // domain name 26 | this.domain = loc_service_domain; 27 | // array of ServicePtr objects 28 | this.ptr_entries = []; 29 | 30 | // query PTR records for domain 31 | this.query_PTR(); 32 | } 33 | 34 | /// query domain for service PTR records 35 | query_PTR() { 36 | // create _loc service FQDN 37 | var query_domain = "_loc._udp." + this.domain; 38 | 39 | console.log("query PTR records of " + query_domain); 40 | 41 | resolver.query(query_domain, 'PTR') 42 | .then(response => { 43 | response.answers.forEach(ans => { 44 | console.log("PTR " +ans.data); 45 | 46 | // Create a pointer object and add it to the 47 | // service PTR list. 48 | let ptr = new ServicePtr(ans.data); 49 | this.ptr_entries.push(ptr); 50 | }); 51 | }); 52 | } 53 | }; 54 | 55 | /// Service PTR 56 | class ServicePtr { 57 | constructor(ptr_domain) { 58 | this.domain = ptr_domain; 59 | this.txt = {}; 60 | this.srv_entries = []; 61 | 62 | // query TXT records 63 | this.query_TXT(); 64 | 65 | // query SRV records 66 | this.query_SRV(); 67 | } 68 | 69 | /// query TXT records of PTR domain 70 | query_TXT() { 71 | console.log("query_TXT()"); 72 | 73 | resolver.query(this.domain, 'TXT') 74 | .then(response => { 75 | var id = 0; 76 | response.answers.forEach(ans => { 77 | console.log("TXT: " +ans.data); 78 | // TODO: pars TXT answer into key value object 79 | 80 | // fill in txt object 81 | this.txt.comment = ans.data; 82 | 83 | id++; 84 | }); 85 | }) 86 | .catch(err => console.error(err)); 87 | } 88 | 89 | /// query SRV records 90 | query_SRV() { 91 | console.log("query_SRV()"); 92 | 93 | resolver.query(this.domain, 'SRV') 94 | .then(response => { 95 | var id = 0; 96 | response.answers.forEach(ans => { 97 | console.log("SRV: " +ans.data.target) 98 | 99 | // create an LOC records object for each record 100 | let loc_record = new LocRecords(ans.data.target, this.txt.comment); 101 | this.srv_entries.push(loc_record); 102 | id++; 103 | }); 104 | }) 105 | .catch(err => console.error(err)); 106 | } 107 | 108 | /// create LOC records object 109 | create_loc_records(domain, info) { 110 | let loc_record = new LocRecords(domain, info); 111 | this.srv_entries.push(loc_record); 112 | } 113 | } 114 | 115 | /// LOC SRV Records 116 | /// 117 | /// On construction, a LOC records does the following things: 118 | /// 119 | /// - query the FQDN for LOC records 120 | /// - place itself on the map 121 | /// - register a pop-up 122 | /// - set a timer to query the LOC record and update the location 123 | /// every 10 seconds. 124 | class LocRecords { 125 | constructor(domain, info) { 126 | this.domain = domain; 127 | this.info = info; 128 | this.entries = []; 129 | 130 | // query LOC records 131 | this.query_LOC(); 132 | 133 | // set timer to re-query LOC records 134 | setInterval(function() { this.query_LOC() }.bind(this), 1000); 135 | } 136 | 137 | /// construct a timeable function 138 | /// queries LOC records of a domain 139 | query_LOC() { 140 | console.log("query_LOC() " +this.domain); 141 | 142 | resolver.query(this.domain, 'LOC') 143 | .then(response => { 144 | var id = 0; 145 | response.answers.forEach(ans => { 146 | // decode LOC wireformat package 147 | let loc = new LocDecoder(ans.data); 148 | 149 | // update marker on map 150 | this.update_marker(loc.latitude, loc.longitude, id); 151 | id++; 152 | }); 153 | }) 154 | .catch(err => console.error(err)); 155 | } 156 | 157 | /// create popup text 158 | create_popup_text(latitude, longitude) { 159 | let popup_text = "" + this.domain + ""; 160 | popup_text += "
Latitude: " + latitude; 161 | popup_text += "
Longitude: " + longitude; 162 | popup_text += "
" +this.info; 163 | 164 | return popup_text; 165 | } 166 | 167 | /// update marker on map 168 | /// 169 | /// the map ID is the answer number 170 | update_marker(latitude, longitude, id) { 171 | // check if marker exists 172 | if (this.entries.length > id) { 173 | // update position 174 | let latLng = new L.LatLng(latitude, longitude); 175 | this.entries[id].setLatLng(latLng); 176 | 177 | // update popup text 178 | let popup_text = this.create_popup_text(latitude, longitude); 179 | this.entries[id].setPopupContent(popup_text); 180 | } else { 181 | // set new map point 182 | this.create_marker(latitude, longitude); 183 | } 184 | } 185 | 186 | /// create new map marker 187 | create_marker(latitude, longitude) { 188 | // set point of interest on map 189 | let marker = L.marker([latitude, longitude]).addTo(map); 190 | 191 | // set explanatory pop-up 192 | let popup_text = this.create_popup_text(latitude, longitude); 193 | marker.bindPopup(popup_text); 194 | 195 | // set tooltip 196 | marker.bindTooltip(this.domain, { 197 | permanent: true 198 | }).addTo(map); 199 | 200 | // add to entries 201 | this.entries.push(marker); 202 | } 203 | 204 | /// Trim location array to defined length 205 | trim_entries(length) { 206 | while(length > this.entries.length()) { 207 | this.entries.pop(); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /demo/playground/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := sig0 2 | 3 | PORT := 8822 4 | 5 | ${PROJECT}.wasm: ../../golang/wasm/*.go ../../golang/sig0/*.go 6 | GOOS=js GOARCH=wasm go build -o ${PROJECT}.wasm ../../golang/wasm/*.go 7 | 8 | start: ${PROJECT}.wasm 9 | nohup python3 -m http.server -d . ${PORT} & 10 | $(info open http://localhost:${PORT}/ in your browser) 11 | 12 | stop: 13 | pkill -f "python3 -m http.server -d . ${PORT}" 14 | -------------------------------------------------------------------------------- /demo/playground/README.md: -------------------------------------------------------------------------------- 1 | # Sig0namectl Web-UI Playground 2 | 3 | This playground contains experiments and building blocks for the 4 | sig0namectl web UI's. 5 | 6 | This folder is only here for reference and development and will be 7 | deleted in the future. 8 | -------------------------------------------------------------------------------- /demo/playground/domain_manager.css: -------------------------------------------------------------------------------- 1 | /* sig0namectl Domain Manager UI CSS */ 2 | 3 | h1 { 4 | margin-left: 3rem 5 | } 6 | 7 | #domain-list-section { 8 | max-width: 27rem; 9 | margin: 0 auto; 10 | padding: 3rem; 11 | } 12 | 13 | /* domain list */ 14 | ul#domain-list { 15 | list-style-type: none; 16 | margin: 0; 17 | margin-bottom: 2.5rem; 18 | padding: 0; 19 | width: 100%; 20 | 21 | } 22 | 23 | #domain-list li { 24 | display: flex; 25 | padding: 0; 26 | border-bottom: 1px solid #ddd; 27 | } 28 | 29 | #domain-list li div { 30 | padding: 0.2rem; 31 | } 32 | 33 | #domain-list li .domain { 34 | min-width: 15rem; 35 | } 36 | 37 | #domain-list li .status { 38 | font-size: smaller; 39 | color: grey; 40 | } 41 | 42 | /* advanced options */ 43 | #domain-advanced { 44 | margin-top: 4rem; 45 | padding-top: 0.7rem; 46 | padding-bottom: 0.7rem; 47 | border-top: 1px solid #ddd; 48 | border-bottom: 1px solid #ddd; 49 | } 50 | 51 | #domain-advanced #domain-advanced-toggle span.fa-solid { 52 | transition: .5s ease-in-out; 53 | } 54 | 55 | #domain-advanced.reduced #domain-advanced-toggle span.fa-solid { 56 | transform: rotate(-90deg); 57 | } 58 | 59 | #domain-advanced.reduced #domain-advanced-container { 60 | display: none; 61 | } 62 | 63 | #domain-advanced .import-export { 64 | margin-top: 2rem; 65 | margin-bottom: 1rem; 66 | } 67 | 68 | /* Domain Overview */ 69 | #domain-overview { 70 | z-index: 10; 71 | margin: 1rem; 72 | padding: 1rem; 73 | background-color: #ffffff; 74 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 75 | width: calc(100% - 4rem); 76 | min-height: calc(100vh - 4rem); 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | } 81 | 82 | #domain-overview button.page { 83 | font-size: x-large; 84 | width: 1.4em; 85 | height: 1.4em; 86 | border-radius: 0; 87 | float: right; 88 | border-width: 0; 89 | } 90 | 91 | #domain-overview-container { 92 | margin: 0 auto; 93 | max-width: 27rem; 94 | } 95 | 96 | #domain-overview label { 97 | display: inline-block; 98 | width: 10em; 99 | } 100 | 101 | #domain-overview input { 102 | margin-bottom: 0.5rem; 103 | } 104 | 105 | div.hidden, 106 | section.hidden { 107 | display: none; 108 | } -------------------------------------------------------------------------------- /demo/playground/domain_manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | sig0namectl Domain Manager 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 49 | 65 | 66 | 67 |
68 |
69 |

Your Domains

70 |
    71 |
    72 |
    73 |

    Request a new domain:

    74 |
    75 | 84 | 89 | 90 |
    91 |
    92 |
    93 | 100 | 101 |
    102 |
    103 |

    104 | Import all sig0namectl keys in a folder into this manager. 105 | Select the folder containing the keys on your device. 106 | The keys will be stored in the local cache of this browser. 107 |

    108 | 109 |
    110 |
    111 |

    112 | Export all your keys from this manager. 113 | This creates a ZIP file backup of all your keys. When unzipped, the keys can be imported for use in other browsers and devices. 114 |

    115 | 116 |
    117 |
    118 |
    119 |
    120 | 174 | 175 | 176 |
    177 |
      178 |
    • 179 |
      180 |
      181 |
    • 182 |
    183 |
    184 | 185 | 186 | -------------------------------------------------------------------------------- /demo/playground/domains.js: -------------------------------------------------------------------------------- 1 | /// Domains Class 2 | /// 3 | /// A Domains object contains a collection of Dns objects. 4 | /// they represent the domains that are of interest for further querying. 5 | /// The domains can have a key related to it or not. 6 | /// 7 | /// The class provides convenient functions for managing the collection 8 | /// of Dns objects. 9 | /// 10 | /// There are the following configuration options: 11 | /// 12 | /// - automatically add a domain when there is a key for it. 13 | /// - TODO: automatically remove domains when there is no key for it. 14 | /// 15 | /// The object sends the following events: 16 | /// `domains_ready`, `domains_updated` 17 | class Domains { 18 | /// constructor of the Domains object 19 | /// the constructor can be optionally provided with an array of domains 20 | /// and some configuration options. 21 | constructor(domain_array, options) { 22 | this.options = {'key_auto_add': true, 'key_auto_remove': false}; 23 | if (options) { 24 | this.options = options 25 | } 26 | this.domains = []; 27 | this.initialized = false; 28 | this.recheck_status = false; 29 | 30 | // add domains 31 | if (Array.isArray(domain_array)) { 32 | for (const domain_name of domain_array) { 33 | this.add_domain(domain_name) 34 | } 35 | } 36 | this.initialized = true; 37 | 38 | // send domains ready event 39 | const event = new CustomEvent('domains_ready') 40 | window.dispatchEvent(event) 41 | } 42 | 43 | /// listen for keys changes 44 | keys_updated(keys_array) { 45 | console.log('keys_updated') 46 | // auto add keys 47 | if (this.options.key_auto_add) { 48 | for (const key of keys_array) { 49 | const domain_item = this.get_domain(key.domain) 50 | 51 | // add domain if it does not exist 52 | if (!domain_item) { 53 | this.add_domain(key.domain, key) 54 | } 55 | else { 56 | // check if key exists in already existing domain 57 | let key_exists = false; 58 | for (const domain_key of domain_item.keys) { 59 | if (domain_key.filename === key.filename) { 60 | key_exists = true; 61 | break 62 | } 63 | } 64 | // add key if it doesn't exist 65 | if (key_exists === false) { 66 | domain_item.keys.push(key); 67 | domain_item.check_key_status(); 68 | this.recheck_status = true; 69 | } 70 | } 71 | } 72 | } 73 | 74 | // TODO: auto remove keys 75 | } 76 | 77 | /// add domain only if it does not yet exist 78 | /// 79 | /// returns true if it was added, false otherwise 80 | add_domain_if_inexistent(domain_name, key) { 81 | // check if domain exists 82 | let domain = this.get_domain(domain_name) 83 | if (domain) { 84 | return false 85 | } 86 | // add the domain 87 | this.add_domain(domain_name, key) 88 | return true 89 | } 90 | 91 | /// add domain 92 | add_domain(domain_name, key) { 93 | let dns_item = new Dns(domain_name, key); 94 | this.domains.push(dns_item) 95 | // send updated event 96 | if (this.initialized) { 97 | const event = new CustomEvent('domains_updated') 98 | window.dispatchEvent(event) 99 | } 100 | } 101 | 102 | /// Get Domain object 103 | /// 104 | /// This function searches for the domain name 105 | /// and returns the Dns object if found. 106 | /// The function returns `null` if no domain was found. 107 | get_domain(domain_name) { 108 | for (const dns of this.domains) { 109 | if (domain_name === dns.domain) { 110 | return dns 111 | } 112 | } 113 | return null 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /demo/playground/fontawesome/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Fonticons, Inc. (https://fontawesome.com) 2 | 3 | -------------------------------------------------------------------------------- 4 | 5 | Font Awesome Free License 6 | 7 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 8 | commercial projects, open source projects, or really almost whatever you want. 9 | Full Font Awesome Free license: https://fontawesome.com/license/free. 10 | 11 | -------------------------------------------------------------------------------- 12 | 13 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 14 | 15 | The Font Awesome Free download is licensed under a Creative Commons 16 | Attribution 4.0 International License and applies to all icons packaged 17 | as SVG and JS file types. 18 | 19 | -------------------------------------------------------------------------------- 20 | 21 | # Fonts: SIL OFL 1.1 License 22 | 23 | In the Font Awesome Free download, the SIL OFL license applies to all icons 24 | packaged as web and desktop font files. 25 | 26 | Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com) 27 | with Reserved Font Name: "Font Awesome". 28 | 29 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 30 | This license is copied below, and is also available with a FAQ at: 31 | http://scripts.sil.org/OFL 32 | 33 | SIL OPEN FONT LICENSE 34 | Version 1.1 - 26 February 2007 35 | 36 | PREAMBLE 37 | The goals of the Open Font License (OFL) are to stimulate worldwide 38 | development of collaborative font projects, to support the font creation 39 | efforts of academic and linguistic communities, and to provide a free and 40 | open framework in which fonts may be shared and improved in partnership 41 | with others. 42 | 43 | The OFL allows the licensed fonts to be used, studied, modified and 44 | redistributed freely as long as they are not sold by themselves. The 45 | fonts, including any derivative works, can be bundled, embedded, 46 | redistributed and/or sold with any software provided that any reserved 47 | names are not used by derivative works. The fonts and derivatives, 48 | however, cannot be released under any other type of license. The 49 | requirement for fonts to remain under this license does not apply 50 | to any document created using the fonts or their derivatives. 51 | 52 | DEFINITIONS 53 | "Font Software" refers to the set of files released by the Copyright 54 | Holder(s) under this license and clearly marked as such. This may 55 | include source files, build scripts and documentation. 56 | 57 | "Reserved Font Name" refers to any names specified as such after the 58 | copyright statement(s). 59 | 60 | "Original Version" refers to the collection of Font Software components as 61 | distributed by the Copyright Holder(s). 62 | 63 | "Modified Version" refers to any derivative made by adding to, deleting, 64 | or substituting — in part or in whole — any of the components of the 65 | Original Version, by changing formats or by porting the Font Software to a 66 | new environment. 67 | 68 | "Author" refers to any designer, engineer, programmer, technical 69 | writer or other person who contributed to the Font Software. 70 | 71 | PERMISSION & CONDITIONS 72 | Permission is hereby granted, free of charge, to any person obtaining 73 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 74 | redistribute, and sell modified and unmodified copies of the Font 75 | Software, subject to the following conditions: 76 | 77 | 1) Neither the Font Software nor any of its individual components, 78 | in Original or Modified Versions, may be sold by itself. 79 | 80 | 2) Original or Modified Versions of the Font Software may be bundled, 81 | redistributed and/or sold with any software, provided that each copy 82 | contains the above copyright notice and this license. These can be 83 | included either as stand-alone text files, human-readable headers or 84 | in the appropriate machine-readable metadata fields within text or 85 | binary files as long as those fields can be easily viewed by the user. 86 | 87 | 3) No Modified Version of the Font Software may use the Reserved Font 88 | Name(s) unless explicit written permission is granted by the corresponding 89 | Copyright Holder. This restriction only applies to the primary font name as 90 | presented to the users. 91 | 92 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 93 | Software shall not be used to promote, endorse or advertise any 94 | Modified Version, except to acknowledge the contribution(s) of the 95 | Copyright Holder(s) and the Author(s) or with their explicit written 96 | permission. 97 | 98 | 5) The Font Software, modified or unmodified, in part or in whole, 99 | must be distributed entirely under this license, and must not be 100 | distributed under any other license. The requirement for fonts to 101 | remain under this license does not apply to any document created 102 | using the Font Software. 103 | 104 | TERMINATION 105 | This license becomes null and void if any of the above conditions are 106 | not met. 107 | 108 | DISCLAIMER 109 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 110 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 111 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 112 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 113 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 114 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 115 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 116 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 117 | OTHER DEALINGS IN THE FONT SOFTWARE. 118 | 119 | -------------------------------------------------------------------------------- 120 | 121 | # Code: MIT License (https://opensource.org/licenses/MIT) 122 | 123 | In the Font Awesome Free download, the MIT license applies to all non-font and 124 | non-icon files. 125 | 126 | Copyright 2024 Fonticons, Inc. 127 | 128 | Permission is hereby granted, free of charge, to any person obtaining a copy of 129 | this software and associated documentation files (the "Software"), to deal in the 130 | Software without restriction, including without limitation the rights to use, copy, 131 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 132 | and to permit persons to whom the Software is furnished to do so, subject to the 133 | following conditions: 134 | 135 | The above copyright notice and this permission notice shall be included in all 136 | copies or substantial portions of the Software. 137 | 138 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 139 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 140 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 141 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 142 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 143 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 144 | 145 | -------------------------------------------------------------------------------- 146 | 147 | # Attribution 148 | 149 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 150 | Awesome Free files already contain embedded comments with sufficient 151 | attribution, so you shouldn't need to do anything additional when using these 152 | files normally. 153 | 154 | We've kept attribution comments terse, so we ask that you do not actively work 155 | to remove them from files, especially code. They're a great way for folks to 156 | learn about Font Awesome. 157 | 158 | -------------------------------------------------------------------------------- 159 | 160 | # Brand Icons 161 | 162 | All brand icons are trademarks of their respective owners. The use of these 163 | trademarks does not indicate endorsement of the trademark holder by Font 164 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 165 | to represent the company, product, or service to which they refer.** 166 | -------------------------------------------------------------------------------- /demo/playground/fontawesome/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2024 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400} -------------------------------------------------------------------------------- /demo/playground/fontawesome/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2024 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900} -------------------------------------------------------------------------------- /demo/playground/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/demo/playground/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /demo/playground/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/demo/playground/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /demo/playground/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/demo/playground/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /demo/playground/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/demo/playground/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /demo/playground/go.work: -------------------------------------------------------------------------------- 1 | go 1.21.0 2 | 3 | use ( 4 | ../../golang 5 | ) 6 | -------------------------------------------------------------------------------- /demo/playground/keys.js: -------------------------------------------------------------------------------- 1 | /// sig0namectl Key Management 2 | 3 | /// Key Management Class 4 | /// 5 | /// There can only be one single key management class, 6 | /// which should be globally accessible. 7 | /// 8 | /// Changes in the Key store are notified via the events: 9 | /// `keys_ready`, `keys_updated` 10 | class Keys { 11 | /// construct the Key object 12 | /// 13 | /// this will load all keys from the local key store 14 | /// via WASM 15 | constructor() { 16 | this.keys = []; 17 | this.init_keys() 18 | } 19 | 20 | /// initialize Keys 21 | async init_keys() { 22 | const keys = await this.get_keys().catch(error => { 23 | console.log(error); 24 | alert('error initializing key store') 25 | return Promise.reject(error) 26 | }) 27 | 28 | let promises = []; 29 | for (const key of keys) { 30 | promises.push(this.update_key(key)); 31 | } 32 | 33 | // wait for all promises to resolve 34 | await Promise.all(promises) 35 | 36 | // send keystore ready event 37 | const event = new CustomEvent('keys_ready') 38 | window.dispatchEvent(event) 39 | } 40 | 41 | /// update Keys 42 | async update_keys() { 43 | try { 44 | const keys = await this.get_keys() 45 | 46 | let promises = []; 47 | for (const key of keys) { 48 | promises.push(this.update_key(key)); 49 | } 50 | 51 | // wait for all promises to resolve 52 | await Promise.all(promises).then((values) => { 53 | let key_updated = false; 54 | for (const value of values) { 55 | if (value === true) { 56 | key_updated = true 57 | } 58 | 59 | // send keystore ready event 60 | if (key_updated === true) { 61 | const event = new CustomEvent('keys_updated') 62 | window.dispatchEvent(event) 63 | } 64 | } 65 | }) 66 | } catch (error) { 67 | console.error(error) 68 | } 69 | } 70 | 71 | /// Update a Single Key 72 | /// 73 | /// This method takes a keystore key object as input. 74 | /// 75 | /// - check status of the key 76 | /// - create a key object 77 | /// - fill in the key object into the keys object 78 | async update_key(key) { 79 | // check if key exists 80 | if (this.key_exists(key.Name) === false) { 81 | // create new key object 82 | const filename = key.Name; 83 | const domain = this.domain_from_filename(filename); 84 | const my_key = new Key(domain, filename); 85 | 86 | // push key to keys 87 | this.keys.push(my_key) 88 | 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | 95 | /// check if key already exists 96 | key_exists(filename) { 97 | for (const key of this.keys) { 98 | if (key.filename === filename) { 99 | return true 100 | } 101 | } 102 | return false 103 | } 104 | 105 | /// get keys from WASM keystore 106 | async get_keys() { 107 | const keys = await window.goFuncs.listKeys() 108 | 109 | if (!Array.isArray(keys)) { 110 | return Promise.resolve([keys]); 111 | } 112 | return Promise.resolve(keys); 113 | } 114 | 115 | /// Request a new Key for a new Domain 116 | /// 117 | /// @param {string} domain The domain name you would like to request 118 | /// @param {string} doh_server The DoH (DNS over Https) server where this 119 | /// should be requested. 120 | /// 121 | /// example: `this.request_key('mynewname.zenr.io','doh.zenr.io')` 122 | async request_key(domain, doh_server) { 123 | console.log('domain: ' + domain + ' doh_server: ' + doh_server) 124 | const result = 125 | await window.goFuncs.newKeyRequest(domain, doh_server).catch(error => { 126 | return Promise.reject(error); 127 | }) 128 | 129 | console.log( 130 | 'key request for ' + domain + ' at ' + doh_server + ' was successful'); 131 | 132 | // update keystore 133 | this.update_keys(); 134 | return Promise.resolve(true) 135 | } 136 | 137 | /// domain from key filename 138 | domain_from_filename(filename) { 139 | const regex = /K([A-Za-z0-9-\.]+)\.\+/; 140 | const result = filename.match(regex) 141 | if (result[1]) { 142 | return result[1] 143 | } 144 | return null 145 | } 146 | } 147 | 148 | /// sig0namectl Key class 149 | class Key { 150 | /// construct the key 151 | /// 152 | /// providing it a domain name and optionally a key filename 153 | constructor(domain, filename, public_key) { 154 | this.domain = domain; 155 | this.filename = filename; 156 | this.public_key = public_key; 157 | this.active = null; 158 | this.waiting = null; 159 | } 160 | 161 | /// TODO: check status of key 162 | /// 163 | /// this function requires the zone domain and the domain of the DoH (DNS over 164 | /// Https) 165 | async check_status(zone, doh_domain) { 166 | // check status of key 167 | console.log( 168 | 'key.check_status ' + this.domain + ' ' + zone + ' ' + doh_domain) 169 | 170 | try { 171 | const status = 172 | await window.goFuncs.checkKeyStatus(this.filename, zone, doh_domain) 173 | 174 | if (status.KeyRRExists === 'true') { 175 | this.active = true; 176 | } 177 | else { 178 | this.active = false; 179 | } 180 | if (status.QueuePTRExists === 'true') { 181 | this.waiting = true; 182 | } else { 183 | this.waiting = false; 184 | } 185 | 186 | console.log( 187 | 'status received: ' + this.domain + ' active: ' + this.active + 188 | ' waiting: ' + this.waiting) 189 | } catch (error) { 190 | console.log( 191 | 'key.check_status error ' + this.domain + ' ' + zone + ' ' + 192 | doh_domain) 193 | console.error(error); 194 | return 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /demo/playground/play.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin-left: 3rem 3 | } 4 | 5 | #domain-list-section, 6 | #domain-overview { 7 | margin: 3rem; 8 | } 9 | 10 | /* domain list */ 11 | ul#domain-list { 12 | list-style-type: none; 13 | margin: 0; 14 | margin-bottom: 2.5rem; 15 | padding: 0; 16 | width: 100%; 17 | 18 | } 19 | 20 | #domain-list li { 21 | display: flex; 22 | padding: 0; 23 | border-bottom: 1px solid #ddd; 24 | } 25 | 26 | #domain-list li div { 27 | padding: 0.2rem; 28 | } 29 | 30 | #domain-list li .domain { 31 | min-width: 15rem; 32 | } 33 | 34 | #domain-list li .domain:hover { 35 | text-decoration: underline; 36 | } 37 | 38 | #domain-list li .status { 39 | font-size: smaller; 40 | color: grey; 41 | } 42 | 43 | /* domain playground */ 44 | textarea { 45 | width: 50rem; 46 | max-width: 100%; 47 | height: 8rem; 48 | } -------------------------------------------------------------------------------- /demo/playground/sd.css: -------------------------------------------------------------------------------- 1 | /* sig0namectl SD-Browser UI CSS */ 2 | 3 | /* sd structural column layout */ 4 | #sd-structural { 5 | display: flex; 6 | flex-wrap: nowrap; 7 | height: 100vh; 8 | padding: 0; 9 | margin: 0; 10 | } 11 | 12 | #sd-structural div.column { 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | @media only screen and (max-width: 800px) { 18 | #sd-structural { 19 | flex-direction: column; 20 | margin-bottom: 2rem; 21 | } 22 | 23 | #sd-structural div.column { 24 | width: 100%; 25 | margin-bottom: 2rem; 26 | } 27 | 28 | #sd-structural h2 { 29 | border-top: 1px solid #000; 30 | } 31 | } 32 | 33 | @media only screen and (min-width: 800px) { 34 | #sd-structural { 35 | flex-direction: row; 36 | overflow-x: auto; 37 | } 38 | 39 | #sd-structural div.column { 40 | max-width: 30rem; 41 | min-width: 20rem; 42 | height: 100vh; 43 | border-right: 1px solid #000; 44 | } 45 | } 46 | 47 | /* Elements in all Columns */ 48 | #sd-structural h2 { 49 | font-size: 1rem; 50 | font-weight: normal; 51 | color: #666; 52 | border-bottom: 1px solid #666; 53 | padding: 0.25rem; 54 | margin: 0; 55 | } 56 | 57 | #sd-structural ul { 58 | list-style-type: none; 59 | margin: 0; 60 | padding: 0; 61 | width: 100%; 62 | } 63 | 64 | #sd-structural li.entry { 65 | display: flex; 66 | justify-content: space-between; 67 | border-bottom: 1px solid #ccc; 68 | padding: 0; 69 | } 70 | 71 | #sd-structural li.entry div { 72 | padding: 0.25rem; 73 | } 74 | 75 | #sd-structural li.entry div.name { 76 | flex: 5 auto; 77 | } 78 | 79 | #sd-structural li.entry div.buttons { 80 | flex: 1 auto; 81 | text-align: right; 82 | } 83 | 84 | #sd-structural li:hover { 85 | background-color: #efefef; 86 | } 87 | 88 | #sd-structural li.active { 89 | background-color: #eee; 90 | } 91 | 92 | #sd-structural div.column div.info h2 { 93 | padding-top: 1rem; 94 | } 95 | 96 | div.column div.info p { 97 | margin: 0; 98 | padding: 0.5rem 0.3rem; 99 | overflow-wrap: break-word; 100 | border-bottom: 1px solid #ddd; 101 | } 102 | 103 | div.add { 104 | display: flex; 105 | justify-content: space-between; 106 | border-bottom: 1px solid #ccc; 107 | padding: 0; 108 | } 109 | 110 | div.add div.domain { 111 | flex: 5 auto; 112 | } 113 | 114 | div.add div { 115 | padding: 0.25em; 116 | } 117 | 118 | div.add div.domain input { 119 | width: 100%; 120 | margin-top: 0.1em; 121 | } 122 | 123 | div.add .buttons { 124 | flex: 1 auto; 125 | text-align: right; 126 | 127 | } 128 | 129 | /* domain styles */ 130 | #domains li.entry div.name.dnssec::before { 131 | font: var(--fa-font-solid); 132 | content: '\f3ed '; 133 | padding-right: 0.25rem; 134 | } 135 | 136 | /* SRV styles */ 137 | #sd-structural li.entry.srv-entry { 138 | flex-direction: column; 139 | margin: 0; 140 | padding: 0.25rem; 141 | } 142 | 143 | #sd-structural li.entry.srv-entry:hover { 144 | background-color: initial; 145 | } 146 | 147 | #sd-structural li.entry.srv-entry .hidden { 148 | display: none; 149 | } 150 | 151 | #sd-structural li.entry.srv-entry div { 152 | padding: 0; 153 | margin: 0; 154 | } 155 | 156 | #sd-structural li.entry.srv-entry div.service-link { 157 | padding-bottom: 0.5rem; 158 | } 159 | 160 | #sd-structural li.entry.srv-entry .srv-domain { 161 | display: flex; 162 | flex-wrap: nowrap; 163 | justify-content: flex-start; 164 | gap: 0.25rem; 165 | font-size: 0.8rem; 166 | } 167 | 168 | .srv-entry .srv-domain div { 169 | padding: 0; 170 | } 171 | 172 | .srv-entry .srv-domain .field { 173 | width: 4em; 174 | color: #666; 175 | } 176 | 177 | /* loading animation */ 178 | .loading-spinner { 179 | width: 50px; 180 | aspect-ratio: 1; 181 | border-radius: 50%; 182 | border: 8px solid; 183 | border-color: #000 #0000; 184 | animation: l1 1s infinite; 185 | margin: 10vh auto; 186 | } 187 | 188 | @keyframes l1 { 189 | to { 190 | transform: rotate(.5turn) 191 | } 192 | } -------------------------------------------------------------------------------- /demo/playground/sd_inspector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DNS Service-Discovery Inspector 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 103 | 104 | 105 |
    106 | 115 |
    116 |
    117 |

    Domains

    118 |
      119 |
    120 |
    121 |
    122 |
    123 | 132 |
    133 |
    134 | 135 |
    136 |
    137 |
    138 |
    139 |
    140 | 141 |
    142 |
    143 |
    144 |
    145 |
    146 |
    147 |

    148 |
      149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 |
    156 |

    157 |
      158 |
    159 |
    160 |
    161 |
    162 |
    163 |
      164 |
    • 165 |
      166 |
      167 |
    • 168 |
    169 |
      170 |
    • 171 |
      172 |
      173 | 174 |
      175 |
    • 176 |
    177 |
      178 |
    • 179 | 185 |
      186 |
      Target
      187 |
      188 |
      189 |
      190 |
      Port
      191 |
      192 |
      193 |
      194 |
      Weight
      195 |
      196 |
      197 |
      198 |
      Priority
      199 |
      200 |
      201 |
    • 202 |
    203 |
    204 |
    205 |

    TXT Entry

    206 |
    207 |
    208 |
    209 |
    210 |
    211 | 212 | 213 | -------------------------------------------------------------------------------- /demo/playground/services.js: -------------------------------------------------------------------------------- 1 | /// DNS-SD Service Information 2 | /// 3 | /// Helper class that provides information and helper methods for services. 4 | class SdServiceInfo { 5 | /// list of service objects 6 | /// 7 | /// each service has the following info: 8 | /// - url protocol start string (e.g.: 'http://') 9 | /// - default port 10 | /// - TXT entry requirement: 'required', 'optional', 'none' 11 | /// - The fontawesome icon to use 12 | service_list = { 13 | '_http': { 14 | 'url': 'http://', 15 | 'default_port': 80, 16 | 'txt': 'required', 17 | 'icon': 'arrow-up-right-from-square' 18 | }, 19 | '_ssh': { 20 | 'url': 'ssh://', 21 | 'default_port': 22, 22 | 'txt': 'required', 23 | 'icon': 'terminal' 24 | }, 25 | '_telnet': { 26 | 'url': 'telnet://', 27 | 'default_port': 23, 28 | 'txt': 'required', 29 | 'icon': 'terminal' 30 | }, 31 | '_sftp': { 32 | 'url': 'sftp://', 33 | 'default_port': 22, 34 | 'txt': 'required', 35 | 'icon': 'folder-tree' 36 | }, 37 | '_ftp': { 38 | 'url': 'ftp://', 39 | 'default_port': 21, 40 | 'txt': 'required', 41 | 'icon': 'folder-tree' 42 | }, 43 | '_gopher': { 44 | 'url': 'gopher://', 45 | 'default_port': 70, 46 | 'txt': 'required', 47 | 'icon': 'shield-cat' 48 | }, 49 | } 50 | 51 | /// creates a service link and returns the link as string 52 | /// 53 | /// if the link can't be created, the function returns 'null' 54 | create_link(service, target, port, txt) { 55 | let link_user = ''; 56 | let link_port = ''; 57 | let link_path = ''; 58 | 59 | // check if service is known 60 | let service_object = this.service_list[service]; 61 | if (service_object === undefined) { 62 | return null 63 | } else if (service === '_http' && port === 443) { 64 | // set specific https settings 65 | service_object.default_port = 443; 66 | service_object.url = 'https://' 67 | } 68 | 69 | // set port, if it is not at default port 70 | if (service_object.default_port != port) { 71 | link_port = ':' + port 72 | } 73 | 74 | // check TXT 75 | if (txt === null || txt === undefined) { 76 | if (service_object.txt === 'required') { 77 | return null 78 | } 79 | } else { 80 | const txt_object = this.txt_2_object(txt) 81 | if (txt_object.path) { 82 | link_path = txt_object.path 83 | } 84 | if (txt_object.user) { 85 | link_user = txt_object.user + '@' 86 | } 87 | } 88 | 89 | // create link 90 | let link = service_object.url + link_user + target + link_port + link_path; 91 | 92 | return link 93 | } 94 | 95 | /// Parse binary TXT entries, to get an object of key: value pairs 96 | txt_2_object(txt_array) { 97 | let txt_result = {}; 98 | for (let i = 0; i < txt_array.length; i++) { 99 | for (let j = 0; j < txt_array[i].data.length; j++) { 100 | // convert the binary array to string 101 | let entry = String.fromCharCode.apply(String, txt_array[i].data[j]) 102 | 103 | // split string 104 | const key_value = entry.split('=') 105 | if (key_value.length == 1) { 106 | txt_result[key_value[0]] = true 107 | } 108 | else if (key_value.length == 2) { 109 | txt_result[key_value[0]] = key_value[1] 110 | } 111 | else { 112 | console.log('unexpected output: ' + key_value); 113 | txt_result[key_value[0]] = key_value[1] 114 | } 115 | } 116 | } 117 | return txt_result 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /demo/playground/sig0.css: -------------------------------------------------------------------------------- 1 | /* sig0namectl WASM admin CSS */ 2 | 3 | body { 4 | background-color: #ffffff; 5 | color: #000000; 6 | font-family: Arial, Helvetica, sans-serif; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | h1 { 12 | font-size: 2.5rem; 13 | } 14 | 15 | h2 { 16 | font-size: 2rem; 17 | } 18 | 19 | h3 { 20 | font-size: 1.75rem; 21 | } 22 | 23 | h4 { 24 | font-size: 1.5rem; 25 | } 26 | 27 | h5 { 28 | font-size: 1.3rem; 29 | } 30 | 31 | h6 { 32 | font-size: 1rem; 33 | } 34 | 35 | body, 36 | p, 37 | div { 38 | font-size: 1rem; 39 | } 40 | 41 | a { 42 | color: #000000; 43 | } 44 | 45 | /* form fields & buttons */ 46 | button, 47 | input[type=file], 48 | input[type=submit] { 49 | border-radius: 0.9em; 50 | border: solid 0.1rem #000; 51 | background-color: inherit; 52 | } 53 | 54 | button.icon { 55 | height: 1.8em; 56 | width: 1.8em; 57 | padding: 0; 58 | } 59 | 60 | /** Chrominum */ 61 | @media screen and (-webkit-min-device-pixel-ratio: 0) and (min-resolution: 0.001dpcm) { 62 | button.icon { 63 | padding-top: 0.15em; 64 | } 65 | } 66 | 67 | 68 | button:hover { 69 | background-color: #ccc; 70 | } -------------------------------------------------------------------------------- /demo/playground/sig0.js: -------------------------------------------------------------------------------- 1 | /// sig0namectl specific WASM functions 2 | /// 3 | /// This file does the following: 4 | /// 5 | /// - registers the go functions 6 | /// - provides needed helper functions 7 | 8 | window.goFuncs = {}; 9 | 10 | 11 | const go = new Go(); 12 | WebAssembly.instantiateStreaming(fetch('sig0.wasm'), go.importObject) 13 | .then((result) => { 14 | go.run(result.instance); 15 | 16 | // create and dispatch WASM ready event 17 | const evt = new CustomEvent('wasm_ready') 18 | window.dispatchEvent(evt) 19 | }); 20 | 21 | function concatArrayBuffers(chunks /*: Uint8Array[]*/) { 22 | const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0)); 23 | let offset = 0; 24 | for (const chunk of chunks) { 25 | result.set(chunk, offset); 26 | offset += chunk.length; 27 | } 28 | return result; 29 | } 30 | 31 | function _base64ToArrayBuffer(buffer) { 32 | var binary_string = window.atob(buffer); 33 | var len = binary_string.length; 34 | var bytes = new Uint8Array(len); 35 | for (var i = 0; i < len; i++) { 36 | bytes[i] = binary_string.charCodeAt(i); 37 | } 38 | return bytes; 39 | } 40 | 41 | function _arrayBufferToBase64(buffer) { 42 | var binary = ''; 43 | var bytes = new Uint8Array(buffer); 44 | var len = bytes.byteLength; 45 | for (var i = 0; i < len; i++) { 46 | binary += String.fromCharCode(bytes[i]); 47 | } 48 | return window.btoa(binary); 49 | } 50 | 51 | const input = Uint8Array.from('foo'.split('').map(c => c.charCodeAt(0))); 52 | const converted = _arrayBufferToBase64(input); 53 | const andBack = _base64ToArrayBuffer(converted) 54 | 55 | // make sure input and andBack are the same 56 | if (input.length != andBack.length) { 57 | console.error('input and andBack are different lengths') 58 | } 59 | for (let i = 0; i < input.length; i++) { 60 | if (input[i] != andBack[i]) { 61 | console.error('input and andBack differ at index', i) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /demo/playground/wasm_exec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 86 | 87 | 88 |

    DNS Keys

    89 | 90 |

    List

    91 | 92 |
    93 | 94 | 95 |

    Request

    96 |

    97 | 98 | 99 |

    100 | 101 | 102 | -------------------------------------------------------------------------------- /dnssd-domain: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Sends DNS-SD updates for domain browsing setup 4 | #------------------------------------------------------------------------------ 5 | 6 | 7 | # load helpful functions 8 | for i in functions/*.sh 9 | do 10 | . ${i} 11 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/$i ..." 12 | done 13 | 14 | set_vars $* 15 | 16 | #------------------------------------------------------------------------------ 17 | # test 18 | 19 | NEW_SUBZONE=${NEW_SUBZONE:-""} 20 | 21 | # select zone or subdomain within zone 22 | if [[ -n ${NEW_SUBZONE} ]]; then 23 | DNSSD_DOMAIN="${NEW_SUBZONE}.${ZONE}" 24 | else 25 | DNSSD_DOMAIN="${ZONE}" 26 | fi 27 | 28 | NSUPDATE_AUTH_SIG0_KEY_FQDN=${NSUPDATE_AUTH_SIG0_KEY_FQDN:-${DNSSD_DOMAIN}} 29 | 30 | # define default update add 31 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 32 | NSUPDATE_TTL="600" 33 | 34 | for word in lb b db r dr 35 | do 36 | # echo "***** DEBUG: ZONE = '${ZONE}' NEW_SUBZONE='${NEW_SUBZONE}' NEW_FQDN='${NEW_FQDN}'" 37 | case ${NSUPDATE_ACTION} in 38 | add) 39 | NSUPDATE_PRECONDITION_SET="nxrrset" 40 | NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 41 | NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 42 | send_nsupdate "${DNSSD_DOMAIN}" "$(echo ${NSUPDATE_PRECONDITION};echo ${NSUPDATE_ITEM_RR})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 43 | ;; 44 | delete) 45 | NSUPDATE_PRECONDITION_SET="yxrrset" 46 | NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 47 | NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 48 | send_nsupdate "${DNSSD_DOMAIN}" "$(echo ${NSUPDATE_PRECONDITION};echo ${NSUPDATE_ITEM_RR})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 49 | ;; 50 | *) 51 | # NSUPDATE_ACTION should default to "add" - should never get here 52 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 53 | exit 1 54 | ;; 55 | esac 56 | DIG_QUERY_PARAM="@${ZONE_SOA_MASTER} +short" 57 | [[ -n ${DEBUG} ]] && echo "dig @${ZONE_SOA_MASTER} PTR ${word}._dns-sd._udp.${DNSSD_DOMAIN}. '$( dig ${DIG_QUERY_PARAM} PTR ${word}._dns-sd._udp.${DNSSD_DOMAIN}. )'" 58 | done 59 | -------------------------------------------------------------------------------- /dnssd-service: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Sends DNS-DS updates for dns-ds service registration and browsing records 4 | #------------------------------------------------------------------------------ 5 | # 6 | # load helpful functions 7 | for i in functions/*.sh 8 | do 9 | . ${i} 10 | [[ -n ${DEBUG} ]] && echo "Sourced ${PWD}/functions/$i ..." 11 | done 12 | 13 | set_vars $* 14 | 15 | #------------------------------------------------------------------------------ 16 | # test 17 | 18 | NEW_SUBZONE=${NEW_SUBZONE:-""} 19 | 20 | # select zone or subdomain within zone 21 | if [[ -n ${NEW_SUBZONE} ]]; then 22 | DNSSD_DOMAIN="${NEW_SUBZONE}.${ZONE}" 23 | else 24 | DNSSD_DOMAIN="${ZONE}" 25 | fi 26 | 27 | DNSSD_SERVICES=${DNSSD_SERVICES:-""} 28 | if [[ ! -n ${DNSSD_SERVICES} ]]; then 29 | echo "Error: DNSSD SERVICES services \$DNSSD_SERVICES environment variable is not set. No services have been defined to browse in domain ${DNSSD_SERVICES}" 30 | exit 1 31 | fi 32 | 33 | NSUPDATE_AUTH_SIG0_KEY_FQDN=${NSUPDATE_AUTH_SIG0_KEY_FQDN:-${DNSSD_DOMAIN}} 34 | 35 | # define default update add & ttl 36 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 37 | NSUPDATE_TTL=${NSUPDATE_TTL:-"60"} 38 | 39 | for service in ${DNSSD_SERVICES} 40 | do 41 | case ${NSUPDATE_ACTION} in 42 | add) 43 | NSUPDATE_PRECONDITION_SET="nxrrset" 44 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}check-names off\n" 45 | # service type browsing instance pointer 46 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} _services._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${service}.${DNSSD_DOMAIN}.\n" 47 | # service instance of service to browse 48 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} ${service}.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${NEW_SUBZONE}.${service}.${DNSSD_DOMAIN}.\n" 49 | # service SRV record example 50 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${service}.${DNSSD_DOMAIN} ${NSUPDATE_TTL} SRV 0 0 80 ${DNSSD_DOMAIN}.\n" 51 | # service TXT record example 52 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${service}.${DNSSD_DOMAIN} ${NSUPDATE_TTL} TXT comment=Hello_this_is_a_${service}_service_instance\n" 53 | send_nsupdate "${DNSSD_DOMAIN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEMS})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 54 | ;; 55 | delete) 56 | NSUPDATE_PRECONDITION_SET="yxrrset" 57 | # service type browsing instance pointer 58 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} _services._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${service}.${DNSSD_DOMAIN}.\n" 59 | # service instance of service to browse 60 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} ${service}.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${NEW_SUBZONE}.${service}.${DNSSD_DOMAIN}.\n" 61 | # service SRV record example 62 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${service}.${DNSSD_DOMAIN} ${NSUPDATE_TTL} SRV 0 0 80 ${DNSSD_DOMAIN}.\n" 63 | # service TXT record example 64 | NSUPDATE_ITEMS="${NSUPDATE_ITEMS}update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${service}.${DNSSD_DOMAIN} ${NSUPDATE_TTL} TXT comment=Hello_this_is_a_${service}_service_instance\n" 65 | send_nsupdate "${DNSSD_DOMAIN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEMS})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 66 | ;; 67 | *) 68 | # NSUPDATE_ACTION should default to "add" - should never get here 69 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 70 | exit 1 71 | ;; 72 | esac 73 | done 74 | 75 | if [[ -n ${DEBUG} ]]; then 76 | echo "---DEBUG" 77 | echo "SCRIPT_NAME = ${SCRIPT_NAME}" 78 | echo "ZONE = ${ZONE}" 79 | echo "DIG_QUERY_PARAM = ${DIG_QUERY_PARAM}" 80 | echo "DNSSD_DOMAIN = ${DNSSD_DOMAIN}" 81 | echo "ZONE_SOA_MASTER = ${ZONE_SOA_MASTER}" 82 | echo "NSUPDATE_SIG0_KEYID = ${NSUPDATE_SIG0_KEYID}" 83 | echo 84 | echo "DEBUG: nsupdate settings" 85 | echo " NSUPDATE_PARAM = ${NSUPDATE_PARAM}" 86 | echo " NSUPDATE_SET_SERVER = ${NSUPDATE_SET_SERVER}" 87 | echo " NSUPDATE_ACTION = ${NSUPDATE_ACTION}" 88 | echo " NSUPDATE_PRECONDITION_SET = ${NSUPDATE_PRECONDITION_SET} (not implemented for ${SCRIPT_NAME})" 89 | echo 90 | echo "DEBUG: nsupdate commands to send" 91 | echo 92 | echo "NSUPDATE_SET_SERVER = ${NSUPDATE_SET_SERVER}" 93 | echo "-- NSUPDATE_ITEMS" 94 | echo -e "${NSUPDATE_ITEMS}" 95 | echo "--" 96 | fi 97 | 98 | if [[ -n ${DEBUG} ]]; then 99 | echo 100 | echo "Browsable services via dig ${DIG_QUERY_PARAM} +short _services._dns-sd._udp.${DNSSD_DOMAIN} PTR" 101 | dig ${DIG_QUERY_PARAM} +short _services._dns-sd._udp.${DNSSD_DOMAIN} PTR 102 | echo 103 | echo "Browsable services type instances via dig" 104 | for SERVICE in ${DNSSD_SERVICES} 105 | do 106 | echo "* ${SERVICE}: dig ${DIG_QUERY_PARAM} +short ${SERVICE}.${DNSSD_DOMAIN} PTR" 107 | dig ${DIG_QUERY_PARAM} +short ${SERVICE}.${DNSSD_DOMAIN} PTR 108 | done 109 | echo 110 | AVAHI_BROWSE=${AVAHI_BROWSE:-"avahi-browse"} 111 | echo "Browsable services via ${AVAHI_BROWSE} ${AVAHI_BROWSE_PARAM} -d ${DNSSD_DOMAIN}" 112 | ${AVAHI_BROWSE} ${AVAHI_BROWSE_PARAM} -d ${DNSSD_DOMAIN} 113 | fi 114 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | sig0namectl.networkcommons.org 2 | -------------------------------------------------------------------------------- /docs/DNS-SD Introduction.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Service Discovery Browsing and Registration Domains 4 | 5 | To enable clients to discover and browse services registered under a domain, the following PTR resource records are required. 6 | 7 | ``` 8 | $ORIGIN example.com 9 | ; 10 | ; Add PTR records to indicate browsing and registering domains 11 | b._dns-sd._udp IN PTR @ ; "b" = browse domain 12 | db._dns-sd._udp IN PTR @ ; "db" = default browse domain 13 | lb._dns-sd._udp IN PTR @ ; "lb" = legacy browse domain 14 | r._dns-sd._udp IN PTR @ ; "r" = registration domain 15 | dr._dns-sd._udp IN PTR @ ; "dr" = default registration domain 16 | ``` 17 | 18 | ## Browsing Service Types 19 | 20 | In order to browse the service types offered within a domain, PTR records are required. Service types are listed as PTR records in `_services._dns-sd._udp` traditionally of the form `_[service]._[protocol]`. 21 | 22 | 23 | ``` 24 | $ORIGIN example.com 25 | ; 26 | ; For each browse/register domain, add PTR records to indicate the available service types that can be browsed/registered. 27 | 28 | _services._dns-sd._udp IN PTR _http._tcp 29 | _services._dns-sd._udp IN PTR _ftp._tcp 30 | ... 31 | ``` 32 | 33 | ## Service Instance Lists 34 | 35 | For each service type, there multiple service instances can be registered, for instance: 36 | 37 | ``` 38 | $ORIGIN example.com 39 | ; 40 | ; For each service type, add PTR records to indicate a list of service instances of that service type. 41 | 42 | _http._tcp IN PTR zembla._http._tcp ; zembla's web server 43 | _http._tcp IN PTR math._http._tcp ; mathias' web server 44 | _http._tcp IN PTR \032*\032SlashDot,\032News\032for\032Nerds._http._tcp ; external web server 45 | _http._tcp IN PTR freifunk.net 46 | ... 47 | _ftp._tcp IN PTR zembla._ftp._tcp 48 | ... 49 | ``` 50 | 51 | ## Service Resolution & Connection Details 52 | 53 | Finally to allow clients to resolve and connect to each service instance SRV and TXT records are used to indicate host and port number (SRV) as well as other properties (TXT) required for machine connection configuration and human readable service entries: 54 | 55 | ``` 56 | zembla._http._tcp IN SRV 0 0 80 zembla.zenr.io. 57 | \032*\032SlashDot,\032News\032for\032Nerds._http._tcp IN SRV 0 0 80 slashdot.com. 58 | ... 59 | ``` 60 | Note SRV targets should resolve to at least one IPv4 or IPv6 address. 61 | 62 | ## Custom Service Types 63 | 64 | For custom service types other DNS RR may be added, but are not defined within current standards (such as LOC for GPS position points, etc). 65 | 66 | ## Compatibility with existing DNS-SD stacks 67 | 68 | For the avahi DNS-SD stack to successfully resolve a service, 69 | - at least one SRV record and one TXT record must be present for each service instance 70 | - service types must be defined as at least a pair of underscored labels (single underscored labels do not work) 71 | - the closest to root label must be either \_udp or \_tcp 72 | - the SRV target must resolve to a IPv4 or IPv6 address, if not, service resolution fails 73 | 74 | ## Further Resources 75 | 76 | - [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763) 77 | - [Service Discovery for IP Applications](https://datatracker.ietf.org/meeting/106/materials/slides-106-edu-sessf-service-discovery-for-ip-applications-00.pdf), Dr Stuart Cheshire, IETF 106, Singapore, Sunday 17th November 2019 78 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/sig0namectl Dynamic DNSSEC Server Zone Configuration.md: -------------------------------------------------------------------------------- 1 | # sig0namectl 2 | 3 | ## DNSSEC Zone configuration for primary DNS servers 4 | 5 | ### DNSSEC 6 | 7 | DNSSEC must be enabled in all parent zones. 8 | 9 | In a primary authorative BIND9 DNS server zone configuration, this can be enabled, for example, the following enables DNSSEC in the **`zenr.io`** zone : 10 | 11 | type master; 12 | file "dynamic/zenr.io/named.zenr.io"; 13 | key-directory "dynamic/zenr.io"; 14 | auto-dnssec maintain; 15 | 16 | 17 | ### Dynamic zone update policy configuration 18 | 19 | Each dynamic DNSSEC update zone to be managed by sig0namectl must have at least one (or more) KEY resource records that are granted update permissions for within that dynamic zone. 20 | 21 | 22 | In a primary authorative BIND9 DNS server zone configuration, this can be enabled. for example, the following allows updates for **`zenr.io`** zone using: 23 | 24 | Firstly, an initial zone management keypair with the label **`vortex.zenr.io`** is generated by invoking 25 | 26 | ./register_key vortex.zenr.io 27 | 28 | (or using dnssec-keygen directly) and the contents of the public key file is copied into a corresponding KEY RR into the `zenr.io` zonefile. 29 | 30 | Secondly, a dynamic DNSSEC update policy is applied to the `zenr.io` zone configuration 31 | 32 | update-policy { 33 | grant "vortex.zenr.io" name zenr.io. ANY; 34 | grant "vortex.zenr.io" subdomain zenr.io. ANY; 35 | grant * selfsub . ANY; 36 | }; 37 | 38 | A BIND DNS server authenticates signed update requests only for SIG(0) KEY RRs in zones for which it is authoritative. 39 | 40 | 41 | ### Dynamic Subzone for named key registration requests 42 | 43 | A subzone with dynamic DNS is configured to allow clients to submit named key requests below the zone. The default subzone registration policy is to allow all registration requests for any new label with **no existing resource records of any type**. This default registration policy is also known as "First Come, First Served". 44 | 45 | sig0namectl uses the environment variable `SIGNAL_SUBZONE` to define the subzone where name requests are managed. It can be set though configuration files or overriden on command line invocation. 46 | 47 | Default value in **`env`** configuation file: 48 | 49 | `SIGNAL_SUBZONE="_signal"` 50 | 51 | Zone keys allow for a scripted policy to be applied to name requests placed in this 'holding zone'. If the policy is successful, the zone key is used with `nsupdate` to update the parent zone of **`${SIGNAL_SUBZONE}`** with the requested named KEY resource record (the FQDN without the `${SIGNAL_SUBZONE}` label). 52 | 53 | -------------------------------------------------------------------------------- /docs/sig0namectl Key Management.md: -------------------------------------------------------------------------------- 1 | # sig0namectl 2 | 3 | ## Keypair Generation 4 | 5 | From man page of `dnssec-keygen`: 6 | 7 | > When `dnssec-keygen` completes successfully, it prints a string of the form `Knnnn.+aaa+iiiii` to the standard output. This is an identification string for the key it has generated. 8 | > * nnnn is the key name. 9 | > * aaa is the numeric representation of the algorithm. 10 | > * *iiiii is the key identifier (or footprint). 11 | > 12 | > `dnssec-keygen` creates two files, with names based on the printed string. `Knnnn.+aaa+iiiii.key` contains the public key, and `Knnnn.+aaa+iiiii.private` contains the private key. 13 | > 14 | > The `.key` file contains a DNS KEY record that can be inserted into a zone file (directly or with a $INCLUDE statement). 15 | > 16 | > The `.private` file contains algorithm-specific fields. For obvious security reasons, this file does not have general read permission. 17 | > 18 | 19 | ## Key Identifier clarification 20 | 21 | Key identifiers (key ids or footprints) for KEY & DNSKEY RR files referred to in the `dnssec-keygen` man page correspond to what is referred to as key 'tags' in DNSSEC terminology (RFC4034 see [3.3](https://www.rfc-editor.org/rfc/rfc4034#section-3.3)). For example, the filename id component of a zone's ZSK DNSKEY corresponds to the RRSIG key tag field in the zone it signs. So in theory, the C reference code example in [RFC4034 Appendix B](https://www.rfc-editor.org/rfc/rfc4034#appendix-B) should generate key identifiers. 22 | 23 | Note that `dig` also calculates this ID from the KEY RR itself. 24 | 25 | For example: 26 | ``` 27 | $ ls Kvortex* && dig +short +dnssec +rrcomments vortex.zenr.io KEY 28 | Kvortex.zenr.io.+015+56161.key Kvortex.zenr.io.+015+56161.private 29 | 512 3 15 2MK3KZkUgYQVumU9bhy1KzIZ2FhFQZ8yLP2nFMJRCEQ= ; alg = ED25519 ; key id = 56161 30 | ``` 31 | 32 | 33 | ## sig0 Key Usage 34 | 35 | ### nsupdate 36 | From the man page of `nsupdate`: 37 | 38 | `nsupdate` is used to submit Dynamic DNS Update requests, as defined in RFC 2136, to a name server. This allows resource records to be added or removed from a zone without manually editing the zone file. A single update request can contain requests to add or remove more than one resource record. 39 | 40 | Zones that are under dynamic control via nsupdate or a DHCP server should not be edited by hand. Manual edits could conflict with dynamic updates and cause data to be lost. 41 | 42 | The resource records that are dynamically added or removed with nsupdate must be in the same zone. Requests are sent to the zone's primary server, which is identified by the MNAME field of the zone's SOA record. 43 | 44 | Transaction signatures can be used to authenticate the Dynamic DNS updates. These use the TSIG resource record type described in RFC 2845, the SIG(0) record described in RFC 2535 and RFC 2931, or GSS-TSIG as described in RFC 3645. 45 | 46 | ... 47 | 48 | SIG(0) uses public key cryptography. To use a SIG(0) key, the public key must be stored in a KEY record in a zone served by the name server. 49 | 50 | ... 51 | 52 | **-k keyfile** 53 | 54 | This option indicates the file containing the TSIG authentication key. Keyfiles may be in two formats: 55 | * a single file containing a named.conf-format key statement, which may be generated automatically by ddns-confgen; or a pair of files whose names are of the format `K{name}.+157.+{random}.key` and `K{name}.+157.+{random}.private`, which can be generated by dnssec-keygen. The **-k** option can also be used to specify a SIG(0) key used to authenticate Dynamic DNS update requests. In this case, the key specified is not an HMAC-MD5 key. 56 | 57 | ## Local Keystore 58 | 59 | sig0namectl uses the environment variable `NSUPDATE_SIG0_KEYPATH` to be the directory it uses to manage all keypairs generated. It can be set though the configuration files or overriden by command line library option **-k**. 60 | 61 | Default value in **`env`** configuation file: 62 | 63 | `NSUPDATE_SIG0_KEYPATH="${PWD}/keystore"` 64 | 65 | Key generation through `request_key` writes newly generated key files in the directory path of `${NSUPDATE_SIG0_KEYPATH}`. 66 | 67 | Workflow scripts use this environment variable to prepend a default path to keypairs to invoke `nsupdate` with a **-k ${NSUPDATE_SIG0_KEYPATH}/keyfile** argument. 68 | 69 | ## Key Management 70 | 71 | sig0namectl uses the environment variable `SIGNAL_SUBZONE` to define the subzone where name requests are managed. 72 | 73 | Default value in **`env`** configuation file: 74 | 75 | `SIGNAL_SUBZONE="_signal"` 76 | -------------------------------------------------------------------------------- /dyn_ip: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Sends DNS-SD updates for domain browsing setup 4 | #------------------------------------------------------------------------------ 5 | 6 | 7 | # load helpful functions 8 | for i in functions/*.sh 9 | do 10 | . ${i} 11 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/$i ..." 12 | done 13 | 14 | set_vars $* 15 | 16 | #------------------------------------------------------------------------------ 17 | 18 | # define default update add 19 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 20 | NSUPDATE_TTL="60" 21 | 22 | NSUPDATE_AUTH_SIG0_KEY_FQDN=${NSUPDATE_AUTH_SIG0_KEY_FQDN:-${NEW_FQDN}} 23 | [[ -n ${DEBUG} ]] && echo "DEBUG: NSUPDATE_AUTH_SIG0_KEY_FQDN='${NSUPDATE_AUTH_SIG0_KEY_FQDN}'" 24 | 25 | # split NEW_FQDN into DNS ZONE & SUBLABEL 26 | ZONE=$(get_soa "${NEW_FQDN}") 27 | [[ ! -n ${ZONE} ]] && echo "Could not find SOA in FQDN '${NEW_FQDN}'" && exit 1 28 | NEW_SUBZONE=${NEW_FQDN%*${ZONE}} 29 | [[ -n ${NEW_SUBZONE} ]] && NEW_SUBZONE=${NEW_SUBZONE::-1} # if not null, remove trailing dot 30 | 31 | # recursively search keystore for most particular subdomain keypair under ZONE 32 | subdomain="${NSUPDATE_AUTH_SIG0_KEY_FQDN:-NEW_FQDN}" 33 | while [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && [[ "${subdomain}" == *"${ZONE}" ]] 34 | do 35 | # [[ -n ${DEBUG} ]] && echo "DEBUG: get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID '${subdomain}' '${NSUPDATE_SIG0_KEYPATH}'" 36 | get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID "${subdomain}" "${NSUPDATE_SIG0_KEYPATH}" 37 | [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && subdomain="${subdomain#*.}" 38 | done 39 | 40 | # loop over command line parameter (post getops()) for IPv[4,6] assignments 41 | for ip in ${CMDLINE_EXTRA_PARAMS}; do 42 | if validateIPv4 "${ip}";then 43 | NSUPDATE_RRTYPE="A" 44 | # [[ -n ${DEBUG} ]] && echo "$ip is parsed as an IPv4 Address. Set A Record" 45 | NSUPDATE_ITEM_RR="${NSUPDATE_ITEM_RR}update ${NSUPDATE_ACTION} ${NEW_FQDN} ${NSUPDATE_TTL} ${NSUPDATE_RRTYPE} ${ip}\n" 46 | else 47 | validateIPv6 "${ip}" 48 | isIPv6="$?" 49 | if [[ $isIPv6 -eq 0 ]];then 50 | NSUPDATE_RRTYPE="AAAA" 51 | # [[ -n ${DEBUG} ]] && echo "$ip is parsed as an IPv6 Address. Set ${NSUPDATE_RRTYPE} Record" 52 | NSUPDATE_ITEM_RR="${NSUPDATE_ITEM_RR}update ${NSUPDATE_ACTION} ${NEW_FQDN} ${NSUPDATE_TTL} ${NSUPDATE_RRTYPE} ${ip}\n" 53 | else 54 | echo "Warning: Skipping Invalid IP Address ($ip)" 55 | fi 56 | fi 57 | done 58 | 59 | # form nsupdate RR update statements 60 | case ${NSUPDATE_ACTION} in 61 | add) 62 | # NSUPDATE_PRECONDITION_SET="nxrrset" 63 | # NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 64 | # NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 65 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 66 | ;; 67 | delete) 68 | # NSUPDATE_PRECONDITION_SET="yxrrset" 69 | # NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 70 | # NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 71 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 72 | ;; 73 | *) 74 | # NSUPDATE_ACTION should default to "add" - should never get here 75 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 76 | exit 1 77 | ;; 78 | esac 79 | 80 | 81 | DIG_QUERY_PARAM="@${ZONE_SOA_MASTER} +noall +answer +dnssec" 82 | echo "$( dig ${DIG_QUERY_PARAM} ${NEW_FQDN} A )" 83 | echo "$( dig ${DIG_QUERY_PARAM} ${NEW_FQDN} AAAA )" 84 | -------------------------------------------------------------------------------- /dyn_key: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Manages KEY entries under a writable domain 4 | # usage 5 | # ./dyn_key fqdn pkey pkey .... 6 | # where 7 | # - fqdn is the fully qualified domain name to place the public keys 8 | # - pkey is the fqdn of the key(s) to add 9 | # 10 | # by default all key RRs of pkey are added or deleted from fqdn 11 | # if no pkey is specified, all exisiting KEY RRs at fqdn are listed 12 | #------------------------------------------------------------------------------ 13 | 14 | 15 | # load helpful functions 16 | for i in functions/*.sh 17 | do 18 | . ${i} 19 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/$i ..." 20 | done 21 | 22 | set_vars $* 23 | 24 | #------------------------------------------------------------------------------ 25 | 26 | # define default update add 27 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 28 | NSUPDATE_TTL="60" 29 | 30 | # For a given ${NEW_FQDN}, recursively search keystore for closest (most particular) keypair 31 | subdomain="${NEW_FQDN}" 32 | while [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && [[ "${subdomain}" == *"."* ]] 33 | do 34 | [[ -n ${DEBUG} ]] && echo "DEBUG: get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID '${subdomain}' '${NSUPDATE_SIG0_KEYPATH}'" 35 | get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID "${subdomain}" "${NSUPDATE_SIG0_KEYPATH}" 36 | [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && subdomain="${subdomain#*.}" || NSUPDATE_AUTH_SIG0_KEY_FQDN="${subdomain}" 37 | done 38 | 39 | if [[ -n ${DEBUG} ]]; then 40 | echo 41 | echo "NEW_FQDN='${NEW_FQDN}'" 42 | echo "subdomain='${subdomain}'" 43 | echo "NSUPDATE_AUTH_SIG0_KEYID='${NSUPDATE_AUTH_SIG0_KEYID}'" 44 | echo "NSUPDATE_AUTH_SIG0_KEY_FQDN='${NSUPDATE_AUTH_SIG0_KEY_FQDN}'" 45 | echo "ZONE='${ZONE}'" 46 | echo "---" 47 | #echo "cat ${NSUPDATE_SIG0_KEYPATH}/${NSUPDATE_AUTH_SIG0_KEYID}.key | cut -f4- -d' '" 48 | echo "keystore public key for ${NSUPDATE_AUTH_SIG0_KEY_FQDN}: $(cat ${NSUPDATE_SIG0_KEYPATH}/${NSUPDATE_AUTH_SIG0_KEYID}.key)" 49 | #echo "dig +short ${NSUPDATE_AUTH_SIG0_KEY_FQDN} KEY" 50 | echo "DNS named public key for ${NSUPDATE_AUTH_SIG0_KEY_FQDN}: $(dig +noall +nottlid +answer ${NSUPDATE_AUTH_SIG0_KEY_FQDN} KEY)" 51 | echo "---" 52 | echo "Processing named KEY parameters" 53 | fi 54 | 55 | # loop over command line parameter (post getops()) for IPv[4,6] assignments 56 | 57 | NSUPDATE_ITEM_RR="" 58 | for keyname in ${CMDLINE_EXTRA_PARAMS}; do 59 | if validateKEY "${keyname}";then 60 | NSUPDATE_RRTYPE="KEY" 61 | [[ -n ${DEBUG} ]] && echo "KEY '${keyname}' resolves, marked to ${NSUPDATE_ACTION}" 62 | NSUPDATE_ITEM_RR="${NSUPDATE_ITEM_RR}update ${NSUPDATE_ACTION} ${NEW_FQDN} ${NSUPDATE_TTL} ${NSUPDATE_RRTYPE} $(dig +short ${keyname} ${NSUPDATE_RRTYPE})\n" 63 | else 64 | echo "Warning: Skipping no KEY resolved with FQDN '${keyname}'" 65 | fi 66 | done 67 | 68 | if [[ -n ${DEBUG} ]]; then 69 | echo "---" 70 | echo NSUPDATE_ITEM_RR 71 | echo -e ${NSUPDATE_ITEM_RR} 72 | fi 73 | # form nsupdate RR update statements 74 | case ${NSUPDATE_ACTION} in 75 | add) 76 | # NSUPDATE_PRECONDITION_SET="nxrrset" 77 | # NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 78 | # NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 79 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 80 | ;; 81 | delete) 82 | # NSUPDATE_PRECONDITION_SET="yxrrset" 83 | # NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 84 | # NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 85 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 86 | ;; 87 | *) 88 | # NSUPDATE_ACTION should default to "add" - should never get here 89 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 90 | exit 1 91 | ;; 92 | esac 93 | 94 | 95 | DIG_QUERY_PARAM="@${ZONE_SOA_MASTER} +noall +answer +dnssec" 96 | echo "$( dig ${DIG_QUERY_PARAM} ${NEW_FQDN} KEY )" 97 | -------------------------------------------------------------------------------- /dyn_loc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Sends LOC updates of GPS co-ordinates 4 | # (currently only for Android with Termux using termux-location tool) 5 | # * added gpsd for *nix/*bsd in functions/get_loc.sh 6 | #------------------------------------------------------------------------------ 7 | 8 | 9 | # load helpful functions 10 | for i in functions/*.sh 11 | do 12 | . ${i} 13 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/$i ..." 14 | done 15 | 16 | set_vars $* 17 | 18 | #------------------------------------------------------------------------------ 19 | 20 | # define default update add 21 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 22 | NSUPDATE_TTL=${NSUPDATE_TTL:-"30"} 23 | 24 | NSUPDATE_AUTH_SIG0_KEY_FQDN=${NSUPDATE_AUTH_SIG0_KEY_FQDN:-${NEW_FQDN}} 25 | [[ -n ${DEBUG} ]] && echo "DEBUG: NSUPDATE_AUTH_SIG0_KEY_FQDN='${NSUPDATE_AUTH_SIG0_KEY_FQDN}'" 26 | 27 | # split NEW_FQDN into DNS ZONE & SUBLABEL 28 | ZONE=$(get_soa "${NEW_FQDN}") 29 | [[ ! -n ${ZONE} ]] && echo "Could not find SOA in FQDN '${NEW_FQDN}'" && exit 1 30 | NEW_SUBZONE=${NEW_FQDN%*${ZONE}} 31 | [[ -n ${NEW_SUBZONE} ]] && NEW_SUBZONE=${NEW_SUBZONE::-1} # if not null, remove trailing dot 32 | 33 | # recursively search keystore for most particular subdomain keypair under ZONE 34 | subdomain="${NSUPDATE_AUTH_SIG0_KEY_FQDN:-NEW_FQDN}" 35 | while [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && [[ "${subdomain}" == *"${ZONE}" ]] 36 | do 37 | # [[ -n ${DEBUG} ]] && echo "DEBUG: get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID '${subdomain}' '${NSUPDATE_SIG0_KEYPATH}'" 38 | get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID "${subdomain}" "${NSUPDATE_SIG0_KEYPATH}" 39 | [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && subdomain="${subdomain#*.}" 40 | done 41 | 42 | NSUPDATE_RRTYPE="LOC" 43 | 44 | # form nsupdate RR update statements 45 | case ${NSUPDATE_ACTION} in 46 | add) 47 | LOC_RR_DATA=$(get_loc "${CMDLINE_EXTRA_PARAMS}") 48 | NSUPDATE_ITEM_RR="${NSUPDATE_RRTYPE} ${LOC_RR_DATA}" 49 | # 50 | NSUPDATE_ITEM_RR="update delete ${NEW_FQDN} ${NSUPDATE_RRTYPE}\n" 51 | # 52 | #NSUPDATE_PRECONDITION_SET="nxrrset ${NEW_FQDN} ${NSUPDATE_RRTYPE}" 53 | #NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET}" 54 | NSUPDATE_ITEM_RR="${NSUPDATE_ITEM_RR}update ${NSUPDATE_ACTION} ${NEW_FQDN} ${NSUPDATE_TTL} ${NSUPDATE_RRTYPE} ${LOC_RR_DATA}\n" 55 | echo "send_nsupdate ${NEW_FQDN} $(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" 56 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 57 | ;; 58 | delete) 59 | LOC_RR_DATA="" 60 | NSUPDATE_ITEM_RR="${NSUPDATE_RRTYPE} ${LOC_RR_DATA}" 61 | # 62 | NSUPDATE_PRECONDITION_SET="yxrrset ${NEW_FQDN} ${NSUPDATE_RRTYPE}" 63 | NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET}" 64 | NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${NEW_FQDN} ${NSUPDATE_RRTYPE}" 65 | echo "send_nsupdate ${NEW_FQDN} $(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" 66 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 67 | ;; 68 | *) 69 | # NSUPDATE_ACTION should default to "add" - should never get here 70 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 71 | exit 1 72 | ;; 73 | esac 74 | 75 | 76 | DIG_QUERY_PARAM="@${ZONE_SOA_MASTER} +noall +answer +dnssec" 77 | echo "$( dig ${DIG_QUERY_PARAM} ${NEW_FQDN} LOC )" 78 | -------------------------------------------------------------------------------- /dyn_txt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # dyn_txt 4 | # Sends DNS updates for TXT Resource Records 5 | # where is the full dns name at which to place the record 6 | # where 7 | # 8 | # 9 | #------------------------------------------------------------------------------ 10 | 11 | 12 | # load helpful functions 13 | for i in functions/*.sh 14 | do 15 | . ${i} 16 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/$i ..." 17 | done 18 | 19 | set_vars $* 20 | 21 | #------------------------------------------------------------------------------ 22 | 23 | # define default update add 24 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 25 | NSUPDATE_TTL="60" 26 | 27 | NSUPDATE_AUTH_SIG0_KEY_FQDN=${NSUPDATE_AUTH_SIG0_KEY_FQDN:-${NEW_FQDN}} 28 | [[ -n ${DEBUG} ]] && echo "DEBUG: NSUPDATE_AUTH_SIG0_KEY_FQDN='${NSUPDATE_AUTH_SIG0_KEY_FQDN}'" 29 | 30 | # split NEW_FQDN into DNS ZONE & SUBLABEL 31 | ZONE=$(get_soa "${NEW_FQDN}") 32 | [[ ! -n ${ZONE} ]] && echo "Could not find SOA in FQDN '${NEW_FQDN}'" && exit 1 33 | NEW_SUBZONE=${NEW_FQDN%*${ZONE}} 34 | [[ -n ${NEW_SUBZONE} ]] && NEW_SUBZONE=${NEW_SUBZONE::-1} # if not null, remove trailing dot 35 | 36 | # recursively search keystore for most particular subdomain keypair under ZONE 37 | subdomain="${NSUPDATE_AUTH_SIG0_KEY_FQDN:-NEW_FQDN}" 38 | while [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && [[ "${subdomain}" == *"${ZONE}" ]] 39 | do 40 | # [[ -n ${DEBUG} ]] && echo "DEBUG: get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID '${subdomain}' '${NSUPDATE_SIG0_KEYPATH}'" 41 | get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID "${subdomain}" "${NSUPDATE_SIG0_KEYPATH}" 42 | [[ ! -n "${NSUPDATE_AUTH_SIG0_KEYID}" ]] && subdomain="${subdomain#*.}" 43 | done 44 | 45 | # loop over command line parameter (post getops()) for TXT records to add 46 | # for txt in ${CMDLINE_EXTRA_PARAMS}; do 47 | txt="${CMDLINE_EXTRA_PARAMS}" 48 | NSUPDATE_RRTYPE="TXT" 49 | NSUPDATE_ITEM_RR="${NSUPDATE_ITEM_RR}update ${NSUPDATE_ACTION} ${NEW_FQDN} ${NSUPDATE_TTL} ${NSUPDATE_RRTYPE} \"${txt}\"\n" 50 | # done 51 | 52 | # form nsupdate RR update statements 53 | case ${NSUPDATE_ACTION} in 54 | add) 55 | # NSUPDATE_PRECONDITION_SET="nxrrset" 56 | # NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 57 | # NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 58 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 59 | ;; 60 | delete) 61 | # NSUPDATE_PRECONDITION_SET="yxrrset" 62 | # NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${word}._dns-sd._udp.${DNSSD_DOMAIN}. IN PTR" 63 | # NSUPDATE_ITEM_RR="update ${NSUPDATE_ACTION} ${word}._dns-sd._udp.${DNSSD_DOMAIN} ${NSUPDATE_TTL} PTR ${DNSSD_DOMAIN}." 64 | send_nsupdate "${NEW_FQDN}" "$(echo ${NSUPDATE_PRECONDITION};echo -e ${NSUPDATE_ITEM_RR})" "${subdomain}" 65 | ;; 66 | *) 67 | # NSUPDATE_ACTION should default to "add" - should never get here 68 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 69 | exit 1 70 | ;; 71 | esac 72 | 73 | 74 | DIG_QUERY_PARAM="@${ZONE_SOA_MASTER} +noall +answer +dnssec" 75 | echo "$( dig ${DIG_QUERY_PARAM} ${NEW_FQDN} TXT )" 76 | -------------------------------------------------------------------------------- /env: -------------------------------------------------------------------------------- 1 | # NEW_ZONE_PATH="/var/named/dynamic" # Fedora/Red Hat 2 | # NEW_ZONE_PATH_OWNER="named:named" # Fedora/Red Hat 3 | # NEW_ZONE_PATH="/var/cache/bind" # Debian 4 | # NEW_ZONE_PATH_OWNER="named:named" # Debian 5 | # signalling subzone 6 | # - no SIG0 required by default 7 | # - may filter by policy to some RRs, ie KEY, PTR, TXT 8 | SIGNAL_SUBZONE="_signal" 9 | # for testing 10 | NEW_ZONE_PATH="${PWD}/zones" 11 | NEW_ZONE_PATH_OWNER="`id -un`:`id -gn`" 12 | NSUPDATE_SIG0_KEYPATH="${PWD}/keystore" 13 | -------------------------------------------------------------------------------- /env.dnssd-service: -------------------------------------------------------------------------------- 1 | #NSUPDATE_SIG0_KEYPATH="${HOME}/src/great-dane/test_go" 2 | #DEBUG="true" 3 | DNSSD_SERVICES=${DNSSD_SERVICES:-"_ssh._tcp _telnet._tcp _gopher._tcp _http._tcp _ftp._tcp _loc._udp _loclist._udp"} 4 | -------------------------------------------------------------------------------- /functions/10-set_vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set_vars() { 4 | # set_vars 5 | # 6 | # sets environment variables from env files 7 | # set_vars $* 8 | # function also handles invocation paramters & requires parameters passed 9 | SCRIPT_NAME="${0#*/}" 10 | 11 | # Source env file for project-wide variable default values 12 | ENV_FILE=${ENV_FILE:-"env"} 13 | if [ -e ${ENV_FILE} ]; then 14 | . ./${ENV_FILE} 15 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/${ENV_FILE} ..." 16 | fi 17 | 18 | # Source env file for script-wide default values 19 | if [ -e ${ENV_FILE}.${SCRIPT_NAME} ]; then 20 | source ${ENV_FILE}.${SCRIPT_NAME} 21 | [[ -n ${DEBUG_SET_VARS} ]] && echo "Sourced ${PWD}/${ENV_FILE}.${SCRIPT_NAME} ..." 22 | fi 23 | 24 | while getopts ":dhk:s:" ARG; 25 | do 26 | case "${ARG}" in 27 | d) 28 | NSUPDATE_ACTION="delete" 29 | [[ -n ${DEBUG_SET_VARS} ]] && echo "-d passed: delete action set" 30 | ;; 31 | s) 32 | NSUPDATE_SIG0_KEYPATH="${OPTARG}" 33 | [[ -n ${DEBUG_SET_VARS} ]] && echo "-s passed: explicit sig0 keystore path parameter '${OPTARG}' given" 34 | ;; 35 | k) 36 | NSUPDATE_AUTH_SIG0_KEY_FQDN="${OPTARG}" 37 | [[ -n ${DEBUG_SET_VARS} ]] && echo "-k passed: explicit sig0 key fqdn parameter '${OPTARG}' given" 38 | ;; 39 | h) 40 | [[ -n ${DEBUG_SET_VARS} ]] && echo "-h passed: print help & exit '${OPTARG}' given" 41 | echo -e "USAGE: ${SCRIPT_NAME} [-d] [-s keystore_path] [-k keypair_fqdn] fqdn" 42 | echo -e "\t -d deletion action request (default action request is add)" 43 | echo -e "\t -s specify explicit keystore path" 44 | echo -e "\t -k specify explicit keypair FQDN for server action authentication" 45 | echo -e "\t fqdn specifies the fully qualified domain name (FQDN) to act upon (or under)" 46 | echo -e "\n All options overide default environment variable values, set on command line or in:" 47 | echo -e "\t${PWD}/${ENV_FILE}: for project-wide scope" 48 | echo -e "\t${PWD}/${ENV_FILE}.${SCRIPT_NAME}: for script-wide scope" 49 | exit 1 50 | ;; 51 | esac 52 | done 53 | shift "$((OPTIND-1))" 54 | 55 | NEW_FQDN="${NEW_FQDN:-${1}}" 56 | if [[ ! -n ${NEW_FQDN} ]]; then 57 | if [[ -n ${ZONE} ]]; then 58 | if [[ -n ${NEW_SUBZONE} ]]; then 59 | NEW_FQDN="${NEW_SUBZONE}.${ZONE}" 60 | else 61 | NEW_FQDN="${ZONE}" 62 | fi 63 | else 64 | echo "No ZONE var set or FQDN argument given" && exit 1 65 | fi 66 | fi 67 | shift 1 68 | CMDLINE_EXTRA_PARAMS=$* 69 | 70 | # Last attempt to set default sig0 keystore path 71 | NSUPDATE_SIG0_KEYPATH=${NSUPDATE_SIG0_KEYPATH:-"${PWD}"} 72 | ZONE="${ZONE:-$(get_soa ${NEW_FQDN})}" # sanity check? 73 | NEW_SUBZONE=${NEW_SUBZONE:-${NEW_FQDN%*.${ZONE}}} 74 | [[ -n ${DEBUG_SET_VARS} ]] && echo "NEW_FQDN='${NEW_FQDN}'" && echo "ZONE='${ZONE}'" && echo "NEW_SUBZONE='${NEW_SUBZONE}'" && echo "NSUPDATE_SIG0_KEYPATH='${NSUPDATE_SIG0_KEYPATH}'" 75 | if (( ${#ZONE} < 2 )); then 76 | echo "Error: DNS zone ZONE='${ZONE}' environment variable is not set & could not be determined from \$DOMAINNAME or \$HOSTNAME" 77 | echo "DEBUG: NEW_FQDN='${NEW_FQDN}'" 78 | exit 1 79 | fi 80 | 81 | # Discover master (usually primary DNS server) of zone from master field of SOA record 82 | # 83 | ZONE_SOA_MASTER=$( get_soa_master ${ZONE} ) 84 | if [[ ! -n ${ZONE_SOA_MASTER} ]]; then 85 | echo "Error: Could not resolve SOA record for ZONE '${ZONE}'" 86 | exit 1 87 | fi 88 | 89 | # External tool defaults 90 | DIG_QUERY_PARAM=${DIG_QUERY_PARAM:-} 91 | AVAHI_BROWSE_PARAM=${AVAHI_BROWSE_PARAM:-"-brat"} 92 | } 93 | -------------------------------------------------------------------------------- /functions/get_loc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function get_loc { 3 | HOST_OS="`uname -o`" 4 | case "${HOST_OS}" in 5 | "Android") 6 | LOC_PROVIDER=${LOC_PROVIDER:-"termux"} 7 | ;; 8 | "GNU/Linux") 9 | LOC_PROVIDER=${LOC_PROVIDER:-"gpsd"} 10 | ;; 11 | *) 12 | LOC_PROVIDER="none" 13 | ;; 14 | esac 15 | 16 | case "${LOC_PROVIDER}" in 17 | "termux") 18 | LOC_JSON_PROVIDER=${LOC_JSON_PROVIDER:-"termux-location -p gps"} 19 | LOC_JSON=$($LOC_JSON_PROVIDER) 20 | # 21 | LAT=`echo $LOC_JSON | jq -r '.latitude'` 22 | LON=`echo $LOC_JSON | jq -r '.longitude'` 23 | ALT=`echo $LOC_JSON | jq -r '.altitude'` 24 | HACC=`echo $LOC_JSON | jq -r '.accuracy'` 25 | VACC=`echo $LOC_JSON | jq -r '.vertical_accuracy'` 26 | BEARING=`echo $LOC_JSON | jq -r '.bearing'` 27 | SPEED=`echo $LOC_JSON | jq -r '.speed'` 28 | # 29 | LATLON_DEG="${LAT} ${LON}" 30 | LATLON_DMS=`echo "${LATLON_DEG}" | GeoConvert -d -:` 31 | # echo "${LATLON_DMS}" 32 | RR_CONV="${LATLON_DMS//\:/\ }" 33 | RR_CONV="${RR_CONV/N/\ N}" 34 | RR_CONV="${RR_CONV/S/\ S}" 35 | RR_CONV="${RR_CONV/E/\ E}" 36 | RR_CONV="${RR_CONV/W/\ W}" 37 | 38 | # 2 decimal places for alt, hacc, vacc 39 | ALT2="$(printf "%8.2f" ${ALT})" 40 | HACC2="$(printf "%8.2f" ${HACC})" 41 | VACC2="$(printf "%8.2f" ${VACC})" 42 | # 43 | LOC_RR_DATA="${RR_CONV} 0.00m ${ALT2}m ${HACC2}m ${VACC2}m" 44 | ;; 45 | 46 | "gpsd") 47 | # pindrop --lon --lat --alt --error -v 48 | # 49 | LAT="$(pindrop --lat)" 50 | LON="$(pindrop --lon)" 51 | ALT="$(pindrop --alt)" 52 | HACC="$(pindrop --error | tr "\'" '\"' | jq -r '.s')" 53 | VACC="$(pindrop --error | tr "\'" '\"' | jq -r '.v')" 54 | # 55 | LATLON_DEG="${LAT} ${LON}" 56 | LATLON_DMS=`echo "${LATLON_DEG}" | GeoConvert -d -:` 57 | # echo "${LATLON_DMS}" 58 | RR_CONV="${LATLON_DMS//\:/\ }" 59 | RR_CONV="${RR_CONV/N/\ N}" 60 | RR_CONV="${RR_CONV/S/\ S}" 61 | RR_CONV="${RR_CONV/E/\ E}" 62 | RR_CONV="${RR_CONV/W/\ W}" 63 | 64 | # 2 decimal places for alt, hacc, vacc 65 | ALT2="$(printf "%8.2f" ${ALT})" 66 | HACC2="$(printf "%8.2f" ${HACC})" 67 | VACC2="$(printf "%8.2f" ${VACC})" 68 | # 69 | LOC_RR_DATA="${RR_CONV} 0.00m ${ALT2}m ${HACC2}m ${VACC2}m" 70 | ;; 71 | *) 72 | ;; 73 | esac 74 | 75 | echo ${LOC_RR_DATA} 76 | } 77 | -------------------------------------------------------------------------------- /functions/get_sig0_keyid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | 5 | # NSUPDATE_SIG0_KEYPATH=${NSUPDATE_SIG0_KEYPATH:-"${PWD}"} 6 | 7 | get_sig0_keyid() { 8 | # get_sig0_keyid( refvar, fqdn, search_path ) 9 | # 10 | # refvar: global env var passed by reference will be set to *.[key|private] keypair filename prefix if found, or "" if not. 11 | # 12 | # fqdn: FQDN portion of keypair filenames to search and return, eg host.exmple.com 13 | # 14 | # search_path: path to search for *.key and *.private files 15 | # 16 | declare -n refvar="${1}" 17 | local fqdn="${2}" 18 | local search_path="${3:-${NSUPDATE_SIG0_KEYPATH}}" 19 | 20 | [[ -n ${DEBUG_GET_SIG0_KEYID} ]] && printf "==================== calling ${FUNCNAME[0]} refvar='${1}' fqdn='${2}' search_path='${3}'\n" 21 | 22 | # get list of matching files 23 | # 24 | local FIND=$(find ${search_path:-${NSUPDATE_SIG0_KEYPATH}} -type f \( -iname K${fqdn}.\*.key -o -iname K${fqdn}.\*.private \)) 25 | # split list into array 26 | local ar=(${FIND}) 27 | local PREFIX="" 28 | 29 | if (( ${#ar[@]} < 2 )); then 30 | refvar="" 31 | [[ -n ${DEBUG_GET_SIG0_KEYID} ]] && printf "Warning: ${FUNCNAME[0]}(): no keypair files for '${fqdn}' found under ${search_path}.\n" 32 | return 1 33 | fi 34 | 35 | if (( ${#ar[@]} > 2 )); then 36 | refvar="" 37 | [[ -n ${DEBUG_GET_SIG0_KEYID} ]] && printf "Warning: ${FUNCNAME[0]}(): path var set to \"${refvar}\": multiple keypair files for ${fqdn} found under ${search_path}.\n" 38 | return 1 39 | fi 40 | 41 | if [ "${ar[0]%.*}" != "${ar[1]%.*}" ]; then 42 | refvar="" 43 | [[ -n ${DEBUG_GET_SIG0_KEYID} ]] && printf "Warning: ${FUNCNAME[0]}(): path var set to \"${refvar}\": no matching keypair files for ${fqdn} found under ${search_path}.\n" 44 | return 1 45 | fi 46 | 47 | PREFIX="${ar[0]%.*}" 48 | refvar="${PREFIX##*/}" 49 | [[ -n ${DEBUG_GET_SIG0_KEYID} ]] && printf "Info: ${FUNCNAME[0]}(): Unique keypair prefix \"${refvar}\" for domain \"${fqdn}\" found in path \"${search_path}\"\n" 50 | 51 | } 52 | 53 | 54 | if [[ -n ${TEST} ]]; then 55 | printf "** TEST get_sig0_keyid()\n" 56 | DEBUG_GET_SIG0_KEYID="true" 57 | get_sig0_keyid SIG0_KEYID zembla.zenr.io ${NSUPDATE_SIG0_KEYPATH} 58 | echo "SIG0_KEYID set to '${SIG0_KEYID}'" 59 | get_sig0_keyid SIG0_KEYID vortex.zenr.io 60 | echo "SIG0_KEYID set to '${SIG0_KEYID}'" 61 | get_sig0_keyid SIG0_KEYID no.such.name ${NSUPDATE_SIG0_KEYPATH} 62 | echo "SIG0_KEYID set to '${SIG0_KEYID}'" 63 | exit 64 | fi 65 | -------------------------------------------------------------------------------- /functions/get_soa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | get_soa() { 4 | # get_soa( fqdn ) 5 | # 6 | # fqdn: FQDN to search and return superdomain that has SOA, eg host.example.com 7 | # 8 | # returns string of most granular superdomain of fqdn parameter that contains a SOA resource record 9 | # 10 | local soa_fqdn="${1}" 11 | while [[ ! -n $(dig +short ${soa_fqdn} SOA) ]] 12 | do 13 | soa_fqdn=${soa_fqdn#*.} 14 | [[ ! "${soa_fqdn}" == *"."* ]] && soa_fqdn="" && break 15 | done 16 | 17 | # soa_fqdn="$(dig +noall +authority ${1} SOA | cut -f1)" 18 | # if [[ "${soa_fqdn}" == "" ]]; then 19 | # soa_fqdn="${1}" 20 | # fi 21 | echo ${soa_fqdn} 22 | } 23 | 24 | get_soa_master() { 25 | # get_soa_master( fqdn ) 26 | # 27 | # fqdn: FQDN portion of keypair filenames to search and return, eg host.exmple.com 28 | # 29 | # returns master DNS server FQDN of most granular superdomain $fqdn with SOA record 30 | # 31 | local soa="" 32 | local master="" 33 | soa_fqdn=$( get_soa "${1}" ) 34 | [[ ! "${soa_fqdn}" == "" ]] && master=$(dig ${DIG_QUERY_PARAM} +short ${soa_fqdn} SOA | cut -f1 -d' ') 35 | echo ${master} 36 | } 37 | 38 | if [[ -n ${TEST} ]]; then 39 | printf "** TEST get_soa()\n" 40 | DEBUG="true" 41 | for test in zenr.io _signal.zenr.io request._signal.zenr.io test.zembla.zenr.io test5.test4.test3.test2.test1.zembla.zenr.io test1.testzone.zenr.io test5.test4.test3.test2.test1.zembla.zenr.io.rubbish 42 | do 43 | test_ret=$(get_soa ${test}) 44 | echo "'${test_ret}' is most granular domain with an SOA resource record for ${test}" 45 | done 46 | fi 47 | 48 | if [[ -n ${TEST} ]]; then 49 | printf "** TEST get_soa_master()\n" 50 | DEBUG="true" 51 | for test in zenr.io _signal.zenr.io request._signal.zenr.io test5.test4.test3.test2.test1.zembla.zenr.io test1.testzone.zenr.io test5.test4.test3.test2.test1.zembla.zenr.io.rubbish 52 | do 53 | test_ret=$(get_soa_master ${test}) 54 | echo "'${test_ret}' is master DNS server for DNS name ${test}" 55 | done 56 | fi 57 | 58 | -------------------------------------------------------------------------------- /functions/send_nsupdate.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | # 3 | if [[ -n ${TEST_SEND_NSUPDATE} ]]; then 4 | source functions/get_soa.sh 5 | source functions/get_sig0_keyid.sh 6 | fi 7 | 8 | send_nsupdate() { 9 | # send_nsupdate zone sig0_auth_key_fqdn 10 | # sends updates via nsupdate, where 11 | # zone: required. the dns domain needed to send (reflected in the list of updates). this sets the server to send to via get_soa_master() 12 | # nsupdate_items: required. the nsupdate command lines to pass 13 | # sig0_auth_key_fqdn: optional. the FQDN of the sig0 auth key to use with -k option of nsupdate command. uses get_sig0_keyid() 14 | # sig0_auth_keypath: optional, needed only wih sig0_auth_keyi, with global fallback default. path to search for sig0_keyid with get_sig0_keyid() 15 | # 16 | # uses multiple environment variables as fallbacks 17 | # 18 | # local variables from function paramters 19 | local zone=${1:-"${ZONE}"} 20 | [[ ! -n ${zone} ]] && echo "Error: ${FUNCNAME[0]}(): requires a zone parmeter or ZONE environment variable to be set." && exit 1 21 | 22 | local nsupdate_items="${2}" 23 | 24 | local sig0_auth_fqdn=${3:-""} # set sig0 authentication key FQDN with global var fallback default 25 | 26 | local sig0_auth_keypath=${4:-"${NSUPDATE_SIG0_KEYPATH}"} # set keystore path with global var fallback default 27 | 28 | 29 | # local variables (derived) 30 | local zone_master="$( get_soa_master ${zone} )" # set zone_master to master field of most fine grained SOA record given for zone 31 | local sig0_auth_keyid 32 | get_sig0_keyid sig0_auth_keyid "${sig0_auth_fqdn}" "${sig0_auth_keypath}" 33 | # form SIG0 private auth key param 34 | local nsupdate_auth_param 35 | if [[ -n ${sig0_auth_keyid} ]]; then 36 | nsupdate_auth_param="-k ${sig0_auth_keypath}/${sig0_auth_keyid}" 37 | else 38 | nsupdate_auth_param="" 39 | fi 40 | 41 | local nsupdate_server="server ${zone_master}" 42 | [[ -n ${DEBUG_SEND_NSUPDATE} ]] && local nsupdate_preamble=$(echo "debug") 43 | local nsupdate_postamble=$(echo "send";echo "quit") 44 | 45 | 46 | if [[ -n ${DEBUG_SEND_NSUPDATE} ]]; then 47 | echo 48 | echo "${FUNCNAME[0]}( zone='${zone}' nsupdate_items= sig0_auth_fqdn='${sig0_auth_fqdn}' sig0_auth_keypath=${sig0_auth_keypath} )" 49 | echo 50 | echo "${FUNCNAME[0]}(): zone_master = ${zone_master}" 51 | echo "${FUNCNAME[0]}(): sig0_auth_keypath = ${sig0_auth_keypath}" 52 | echo "${FUNCNAME[0]}(): sig0_auth_fqdn = ${sig0_auth_fqdn}" 53 | echo "${FUNCNAME[0]}(): sig0_auth_keyid = ${sig0_auth_keyid}" 54 | echo "${FUNCNAME[0]}(): nsupdate_auth_param = '${nsupdate_auth_param}'" 55 | #echo "${FUNCNAME[0]}(): nsupdate commands to send" 56 | #(echo "${nsupdate_preamble}";echo "${nsupdate_server}"; echo "${nsupdate_items}";echo "${nsupdate_postamble}") | cat 57 | fi 58 | 59 | if [[ ! -n ${NSUPDATE_DISABLE} ]]; then 60 | [[ -n ${DEBUG_SEND_NSUPDATE} ]] && echo && echo "${FUNCNAME[0]}(): Sending zone SOA master server '${zone_master}' update requests via nsupdate..." 61 | (echo "${nsupdate_preamble}";echo "${nsupdate_server}"; echo -e "${nsupdate_items}";echo "${nsupdate_postamble}") | nsupdate ${nsupdate_auth_param} 62 | else 63 | echo "${FUNCNAME[0]}(): Warning: sending updates via nsupdate is disabled. NSUPDATE_DISABLE=\"${NSUPDATE_DISABLE}\"" 64 | fi 65 | } 66 | 67 | if [[ -n ${TEST_SEND_NSUPDATE} ]]; then 68 | # NSUPDATE_DISABLE="true" 69 | DEBUG_SEND_NSUPDATE="true" 70 | # NEW_SUBZONE="test" 71 | NSUPDATE_SIG0_KEYPATH="${PWD}/keystore" 72 | items=$(echo "update add test1 60 A 127.0.0.1";echo "update add test2 60 A 127.0.0.1") 73 | send_nsupdate "zenr.io" "${items}" "debug1.zenr.io" 74 | fi 75 | -------------------------------------------------------------------------------- /functions/validateIP.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function validateIPv4() 3 | { 4 | 5 | # validate( ip[6,4] ... ) 6 | # 7 | # ip[6,4]: 1 or more IPv6 or IPv4 addresses 8 | # 9 | # return value 0 = valid address 10 | # non zero value = invalid address 11 | local ip=$1 12 | ipcalc -s -4 -c ${ip} 13 | local stat=$? 14 | #local stat=1 15 | #if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 16 | # OIFS=$IFS 17 | # IFS='.' 18 | # ip=($ip) 19 | # IFS=$OIFS 20 | # [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ 21 | # && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] 22 | # stat=$? 23 | #fi 24 | #echo "$stat" 25 | return $stat 26 | } 27 | 28 | function validateIPv6() 29 | { 30 | local ip=$1 31 | ipcalc -s -6 -c ${ip} 32 | local stat=$? 33 | #TODO: doesn't handle arbitrary zero compression yet 34 | #local stat=1 35 | #if [[ $ip =~ ^[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:\:[0-9,a-f,A-F]{1,4}$ ]]; then 36 | # # OIFS=$IFS 37 | # # IFS=':' 38 | # # ip=($ip) 39 | # # IFS=$OIFS 40 | # # [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ 41 | # # && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] 42 | # stat=0 43 | #fi 44 | #echo "$stat" 45 | return $stat 46 | } 47 | 48 | if [[ -n ${TEST} ]]; then 49 | test_list=( 50 | 127.0.0.1 51 | 1.2.3.4 52 | 10.20.30.40 53 | 100.200.100.100 54 | 123.256.123.12 55 | 3.3.3. 56 | 3.3.3 57 | 5.5.5.5.5 58 | fe80::eeb4:b20e:7677:6144 59 | fe80::eeb4:b20e:7677:6144:8888:9999:AAAAA 60 | fe80:eeb4:b20e: 61 | 2a01:4f8:c17:3dd5:8000::10 62 | ) 63 | echo "-- TEST -- ipv4" 64 | for ip in "${test_list[@]}"; do 65 | res=$(validateIPv4 ${ip}) 66 | echo "${ip}= '${res}' : '$?' " 67 | done 68 | echo "-- TEST -- ipv6" 69 | for ip in "${test_list[@]}"; do 70 | res="$(validateIPv6 ${ip})" 71 | echo "${ip}= '${res}' : '$?' " 72 | done 73 | fi 74 | -------------------------------------------------------------------------------- /functions/validateKEY.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | function validateKEY() 3 | { 4 | 5 | # validate KEY RR presence at key_fqdn 6 | # 7 | # ip[6,4]: 1 or more IPv6 or IPv4 addresses 8 | # 9 | # return value 0 = valid address 10 | # non zero value = invalid address 11 | local key_fqdn=$1 12 | local query_answer=$(dig +short ${key_fqdn} KEY) 13 | local stat=$? 14 | # echo "validateKEY() DEBUG: resolve ${key_fqdn} KEY query_answer='${query_answer}'" 15 | # echo "length of query_answer=${#query_answer}" 16 | [[ ${#query_answer} > 0 ]] && stat=0 || stat=1 17 | return $stat 18 | } 19 | 20 | #function validateIPv6() 21 | #{ 22 | # local ip=$1 23 | # ipcalc -s -6 -c ${ip} 24 | # local stat=$? 25 | # #TODO: doesn't handle arbitrary zero compression yet 26 | # #local stat=1 27 | # #if [[ $ip =~ ^[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:[0-9,a-f,A-F]{1,4}\:\:[0-9,a-f,A-F]{1,4}$ ]]; then 28 | # # # OIFS=$IFS 29 | # # # IFS=':' 30 | # # # ip=($ip) 31 | # # # IFS=$OIFS 32 | # # # [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ 33 | # # # && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]] 34 | # # stat=0 35 | # #fi 36 | # #echo "$stat" 37 | # return $stat 38 | #} 39 | # 40 | #if [[ -n ${TEST} ]]; then 41 | # test_list=( 42 | # 127.0.0.1 43 | # 1.2.3.4 44 | # 10.20.30.40 45 | # 100.200.100.100 46 | # 123.256.123.12 47 | # 3.3.3. 48 | # 3.3.3 49 | # 5.5.5.5.5 50 | # fe80::eeb4:b20e:7677:6144 51 | # fe80::eeb4:b20e:7677:6144:8888:9999:AAAAA 52 | # fe80:eeb4:b20e: 53 | # 2a01:4f8:c17:3dd5:8000::10 54 | # ) 55 | # echo "-- TEST -- ipv4" 56 | # for ip in "${test_list[@]}"; do 57 | # res=$(validateIPv4 ${ip}) 58 | # echo "${ip}= '${res}' : '$?' " 59 | # done 60 | # echo "-- TEST -- ipv6" 61 | # for ip in "${test_list[@]}"; do 62 | # res="$(validateIPv6 ${ip})" 63 | # echo "${ip}= '${res}' : '$?' " 64 | # done 65 | #fi 66 | -------------------------------------------------------------------------------- /golang/.gitignore: -------------------------------------------------------------------------------- 1 | cmd/sig0namectl/sig0namectl 2 | wasm/demo.wasm 3 | wasm/nohup.out 4 | K*.private 5 | -------------------------------------------------------------------------------- /golang/Makefile: -------------------------------------------------------------------------------- 1 | sig0namectl: sig0/*.go cmd/sig0namectl/*.go 2 | go build ./cmd/sig0namectl 3 | make -C wasm demo.wasm 4 | -------------------------------------------------------------------------------- /golang/cmd/sig0namectl/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func main() { 11 | app := cli.App{ 12 | Name: os.Args[0], 13 | Usage: "secure dynamic DNS tool", 14 | Flags: []cli.Flag{ 15 | &cli.StringFlag{Name: "keystore", Aliases: []string{"ks"}, Value: "keystore", Usage: "path to keystore", EnvVars: []string{"SIG0_KEYSTORE"}}, 16 | }, 17 | Commands: []*cli.Command{ 18 | keysCmd, queryCmd, updateCmd, 19 | }, 20 | } 21 | 22 | err := app.Run(os.Args) 23 | if err != nil { 24 | fmt.Fprintf(os.Stderr, "Error: %s\n\n", err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /golang/cmd/sig0namectl/keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "text/tabwriter" 8 | 9 | "github.com/NetworkCommons/sig0namectl/sig0" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | var keysCmd = &cli.Command{ 14 | Name: "keys", 15 | Usage: "print sig0 signing key", 16 | Subcommands: []*cli.Command{ 17 | { 18 | Name: "list", 19 | Usage: "lists all keys in the keystore", 20 | Action: listKeysAction, 21 | }, 22 | { 23 | Name: "get", 24 | Usage: "search keystore for a key for ", 25 | Action: getKeyAction, 26 | }, 27 | { 28 | Name: "request", 29 | Usage: "request ", 30 | Action: requestKeyAction, 31 | }, 32 | }, 33 | } 34 | 35 | func requestKeyAction(cCtx *cli.Context) error { 36 | newName := cCtx.Args().Get(0) 37 | if newName == "" { 38 | return cli.Exit("newName required", 1) 39 | } 40 | dohServer := "doh.zenr.io" 41 | log.Println("Requesting key for", newName, "from", dohServer) 42 | 43 | err := sig0.RequestKey(newName) 44 | if err != nil { 45 | return fmt.Errorf("failed to create request key: %w", err) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func getKeyAction(cCtx *cli.Context) error { 52 | searchDomain := cCtx.Args().First() 53 | if searchDomain == "" { 54 | return cli.Exit("searchDomain required", 1) 55 | } 56 | keystore := cCtx.String("keystore") 57 | log.Printf("keystore: %s", keystore) 58 | log.Printf("searchDomain: %s", searchDomain) 59 | keys, err := sig0.ListKeysFiltered(keystore, searchDomain) 60 | if err != nil { 61 | return err 62 | } 63 | if len(keys) == 0 { 64 | return cli.Exit("no keys found", 1) 65 | } 66 | return printKeys(keys) 67 | } 68 | 69 | func listKeysAction(cCtx *cli.Context) error { 70 | keystore := cCtx.String("keystore") 71 | log.Printf("keystore: %s", keystore) 72 | keys, err := sig0.ListKeys(keystore) 73 | if err != nil { 74 | return err 75 | } 76 | if len(keys) == 0 { 77 | return cli.Exit("no keys found", 1) 78 | } 79 | return printKeys(keys) 80 | } 81 | 82 | func printKeys(keys []sig0.StoredKeyData) error { 83 | tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) 84 | fmt.Fprintf(tw, "Index\tName\tKey Name\tPublic Key\n") 85 | for i, k := range keys { 86 | parsed, err := k.ParseKey() 87 | if err != nil { 88 | return err 89 | } 90 | fmt.Fprintf(tw, "%d\t%s\t%s\t%s\n", i, k.Name, parsed.Hdr.Name, parsed.PublicKey) 91 | } 92 | err := tw.Flush() 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /golang/cmd/sig0namectl/query.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | // "strconv" 8 | // "strings" 9 | 10 | "github.com/NetworkCommons/sig0namectl/sig0" 11 | "github.com/davecgh/go-spew/spew" 12 | "github.com/miekg/dns" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | var queryCmd = &cli.Command{ 17 | Name: "query", 18 | Usage: "query ", 19 | Aliases: []string{"q"}, 20 | Flags: []cli.Flag{ 21 | &cli.GenericFlag{ 22 | Name: "type", 23 | Usage: "type of query to run", 24 | Value: &dnsRRTypeFlag{}, 25 | }, 26 | 27 | &cli.BoolFlag{ 28 | Name: "json", 29 | Usage: "output JSON", 30 | Value: false, 31 | }, 32 | }, 33 | Action: queryAction, 34 | } 35 | 36 | func queryAction(cCtx *cli.Context) error { 37 | name := cCtx.Args().First() 38 | 39 | tf := cCtx.Generic("type") 40 | typeFlag, ok := tf.(*dnsRRTypeFlag) 41 | if !ok { 42 | return fmt.Errorf("unexpected flag type: %T", tf) 43 | } 44 | // default 45 | if typeFlag.qtype == 0 { 46 | typeFlag.qtype = dns.TypeA 47 | } 48 | fmt.Fprintf(os.Stderr, "[Querying] %d:%v\n", typeFlag.qtype, name) 49 | 50 | q, err := sig0.QueryWithType(name, typeFlag.qtype) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | server := cCtx.String("server") 56 | answer, err := sig0.SendDOHQuery(server, q) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if cCtx.Bool("json") { 62 | out, err := json.Marshal(answer) 63 | if err != nil { 64 | return fmt.Errorf("failed to encode answer to %d:%q as JSON: %w", typeFlag.qtype, name, err) 65 | } 66 | _, _ = os.Stdout.Write(out) 67 | } else { 68 | spew.Dump(answer) 69 | } 70 | return nil 71 | } 72 | 73 | type dnsRRTypeFlag struct { 74 | qtype uint16 75 | } 76 | 77 | func (f *dnsRRTypeFlag) Set(value string) error { 78 | var err error 79 | f.qtype, err = sig0.QueryTypeFromString(value) 80 | if err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func (f *dnsRRTypeFlag) String() string { 87 | return fmt.Sprintf("dnsType:%d", f.qtype) 88 | } 89 | -------------------------------------------------------------------------------- /golang/cmd/sig0namectl/update.go: -------------------------------------------------------------------------------- 1 | // from https://miek.nl/2014/august/16/go-dns-package/ 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "log" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/miekg/dns" 15 | "github.com/urfave/cli/v2" 16 | 17 | "github.com/NetworkCommons/sig0namectl/sig0" 18 | ) 19 | 20 | var updateCmd = &cli.Command{ 21 | Name: "update", 22 | Aliases: []string{"u"}, 23 | UsageText: "See flags for usage", 24 | Subcommands: []*cli.Command{ 25 | { 26 | Name: "a", 27 | UsageText: "update a ", 28 | Usage: "update A record for in ", 29 | Action: updateAAction, 30 | Flags: []cli.Flag{ 31 | &cli.BoolFlag{Name: "unset", Value: true, Usage: "unset the A record, before adding one"}, 32 | &cli.BoolFlag{Name: "remove", Usage: "remove the A record"}, 33 | }, 34 | }, 35 | 36 | { 37 | Name: "rr", 38 | Usage: "update rr", 39 | Action: updateRRAction, 40 | Flags: []cli.Flag{ 41 | &cli.BoolFlag{Name: "remove", Usage: "remove RRs"}, 42 | }, 43 | }, 44 | }, 45 | } 46 | 47 | func updateAAction(cCtx *cli.Context) error { 48 | host := cCtx.Args().Get(0) 49 | if host == "" { 50 | return cli.Exit("host required", 1) 51 | } 52 | if !strings.HasSuffix(host, ".") { 53 | host = host + "." 54 | } 55 | ipAddrStr := cCtx.Args().Get(1) 56 | if ipAddrStr == "" { 57 | return cli.Exit("IP address required", 1) 58 | } 59 | if ipAddr := net.ParseIP(ipAddrStr); ipAddr == nil { 60 | return cli.Exit("invalid IP address: "+ipAddrStr, 1) 61 | } 62 | 63 | keystore := cCtx.String("keystore") 64 | 65 | keys, err := sig0.ListKeysFiltered(keystore, host) 66 | if err != nil { 67 | return err 68 | } 69 | if len(keys) == 0 { 70 | return cli.Exit("no key found for host", 1) 71 | } 72 | 73 | log.Println("-- Using key:", keys[0].Name) 74 | // ugh.. what? doubley .key.key 75 | keyPath := filepath.Join(keystore, keys[0].Name) 76 | keyPath = strings.TrimSuffix(keyPath, ".key") 77 | signer, err := sig0.LoadKeyFile(keyPath) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | soa, err := sig0.QuerySOA(host) 83 | if err != nil { 84 | return err 85 | } 86 | reply, err := sig0.SendDOHQuery(sig0.DefaultDOHResolver, soa) 87 | if err != nil { 88 | return err 89 | } 90 | soaReply, err := sig0.AnySOA(reply) 91 | if err != nil { 92 | return err 93 | } 94 | zone := soaReply.Hdr.Name 95 | log.Println("-- SOA lookup for", host, "found zone:", zone) 96 | 97 | dohServer, err := sig0.FindDOHEndpoint(zone) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | err = signer.StartUpdate(zone) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | parsedIP := net.ParseIP(ipAddrStr) 108 | if parsedIP.To4() == nil { 109 | return fmt.Errorf("invalid IPv4 address: %s", ipAddrStr) 110 | } 111 | 112 | rrStr := fmt.Sprintf("%s.%s %d IN A %s", strings.TrimSuffix(host, "."+zone), zone, sig0.DefaultTTL, ipAddrStr) 113 | rr, err := dns.NewRR(rrStr) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if cCtx.Bool("remove") { 119 | err = signer.RemoveRR(rr) 120 | } else { 121 | if cCtx.Bool("unset") { 122 | // query current ip 123 | query, err := sig0.QueryA(host) 124 | if err != nil { 125 | return err 126 | } 127 | reply, err := sig0.SendDOHQuery(dohServer.Host, query) 128 | if err != nil { 129 | return err 130 | } 131 | log.Println("unsetting A record for", host) 132 | for _, rr := range reply.Answer { 133 | rrA, ok := rr.(*dns.A) 134 | if !ok { 135 | continue 136 | } 137 | log.Println("current A record", rrA.A.String()) 138 | 139 | err = signer.RemoveRR(rrA) 140 | if err != nil { 141 | return fmt.Errorf("failed to unset A record: %w", err) 142 | } 143 | } 144 | } 145 | err = signer.UpdateRR(rr) 146 | } 147 | if err != nil { 148 | return err 149 | } 150 | 151 | m, err := signer.SignUpdate() 152 | if err != nil { 153 | return err 154 | } 155 | 156 | log.Println("-- Configure DoH client --") 157 | respMsg, err := sig0.SendDOHQuery(dohServer.Host, m) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | log.Println("-- Response from DNS server --") 163 | fmt.Println(respMsg) 164 | 165 | return nil 166 | } 167 | 168 | func updateRRAction(cCtx *cli.Context) error { 169 | host := cCtx.Args().Get(0) 170 | if host == "" { 171 | return cli.Exit("host required", 1) 172 | } 173 | if !strings.HasSuffix(host, ".") { 174 | host = host + "." 175 | } 176 | keystore := cCtx.String("keystore") 177 | 178 | keys, err := sig0.ListKeysFiltered(keystore, host) 179 | if err != nil { 180 | return err 181 | } 182 | if len(keys) == 0 { 183 | return cli.Exit("no key found for host", 1) 184 | } 185 | 186 | log.Println("-- Using key:", keys[0].Name) 187 | // ugh.. what? doubley .key.key 188 | keyPath := filepath.Join(keystore, keys[0].Name) 189 | keyPath = strings.TrimSuffix(keyPath, ".key") 190 | signer, err := sig0.LoadKeyFile(keyPath) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | soa, err := sig0.QuerySOA(host) 196 | if err != nil { 197 | return err 198 | } 199 | reply, err := sig0.SendDOHQuery(sig0.DefaultDOHResolver, soa) 200 | if err != nil { 201 | return err 202 | } 203 | soaReply, err := sig0.AnySOA(reply) 204 | if err != nil { 205 | return err 206 | } 207 | zone := soaReply.Hdr.Name 208 | log.Println("-- SOA lookup for", host, "found zone:", zone) 209 | 210 | dohServer, err := sig0.FindDOHEndpoint(zone) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | err = signer.StartUpdate(zone) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | // read RRs from stdin 221 | count := 0 222 | log.Println("-- Reading RRs from stdin --") 223 | scanner := bufio.NewScanner(os.Stdin) 224 | for scanner.Scan() { 225 | rrStr := scanner.Text() 226 | if rrStr == "" { 227 | log.Println("-- Empty line, stopping --") 228 | break 229 | } 230 | rr, err := dns.NewRR(rrStr) 231 | if err != nil { 232 | return err 233 | } 234 | if cCtx.Bool("remove") { 235 | log.Println("-- Removing RR --") 236 | err = signer.RemoveRR(rr) 237 | } else { 238 | log.Println("-- Updating RR --") 239 | err = signer.UpdateRR(rr) 240 | } 241 | log.Printf("-- %+v", rr) 242 | count++ 243 | if err != nil { 244 | return err 245 | } 246 | } 247 | 248 | if count == 0 { 249 | return cli.Exit("no RRs to update", 0) 250 | } 251 | log.Printf("-- Updated %d RRs --", count) 252 | 253 | m, err := signer.SignUpdate() 254 | if err != nil { 255 | return err 256 | } 257 | 258 | log.Println("-- Configure DoH client --") 259 | respMsg, err := sig0.SendDOHQuery(dohServer.Host, m) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | log.Println("-- Response from DNS server --") 265 | fmt.Println(respMsg) 266 | 267 | return nil 268 | } 269 | -------------------------------------------------------------------------------- /golang/env: -------------------------------------------------------------------------------- 1 | export SIG0_SERVER="abulafia.free2air.net" 2 | export SIG0_ZONE="zenr.io" 3 | export SIG0_HOST="godnstest" 4 | export SIG0_SIG0_KEYFILES="Kvortex.zenr.io.+015+56161" 5 | -------------------------------------------------------------------------------- /golang/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/NetworkCommons/sig0namectl 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.21.7 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/miekg/dns v1.1.58 10 | github.com/shynome/doh-client v1.2.0 11 | github.com/stretchr/testify v1.9.0 12 | github.com/urfave/cli/v2 v2.27.1 13 | ) 14 | 15 | require ( 16 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 19 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 20 | golang.org/x/mod v0.17.0 // indirect 21 | golang.org/x/net v0.24.0 // indirect 22 | golang.org/x/sync v0.7.0 // indirect 23 | golang.org/x/sys v0.19.0 // indirect 24 | golang.org/x/tools v0.20.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /golang/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= 6 | github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 10 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 11 | github.com/shynome/doh-client v1.1.0 h1:g42zg3cJjMEG7hZpoV0r8caxCB63hps+YhpN5mykxtU= 12 | github.com/shynome/doh-client v1.1.0/go.mod h1:fSKGg5Q8Mo2oYF2KsLxPkE8Hf0xbyw2PJXtYl5QAz1Y= 13 | github.com/shynome/doh-client v1.2.0 h1:ZtW0ztE0lPytvq6WdIIB2JdNkTlS4W7jTRzpc4qfC4I= 14 | github.com/shynome/doh-client v1.2.0/go.mod h1:fSKGg5Q8Mo2oYF2KsLxPkE8Hf0xbyw2PJXtYl5QAz1Y= 15 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 16 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= 18 | github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 19 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 20 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 21 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 22 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 23 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 24 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 25 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 26 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 27 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 28 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 29 | golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= 30 | golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /golang/notes: -------------------------------------------------------------------------------- 1 | # we would like to 2 | 3 | * import key pairs from dnssec-keygen files into golang DNS key structure 4 | 5 | 6 | * read/write arbitrary DNS RRs under ZONE using key pair authorized in ZONE 7 | 8 | - export public key to KEY RR in or under ZONE 9 | - in allows shared access 10 | - under creates independent HOST/ENTITY 11 | 12 | - manage DNS RRs for HOST/ENTITY under ZONE 13 | - A/AAAA, SRV, URI & LOC 14 | -------------------------------------------------------------------------------- /golang/sig0/answers.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | func ParseBase64Answer(answer string) (*dns.Msg, error) { 13 | data, err := base64.StdEncoding.DecodeString(answer) 14 | if err != nil { 15 | return nil, fmt.Errorf("parse: invalid base64: %w", err) 16 | } 17 | var resp = new(dns.Msg) 18 | err = resp.Unpack(data) 19 | if err != nil { 20 | return nil, fmt.Errorf("parse: invalid dns data: %w", err) 21 | } 22 | return resp, nil 23 | } 24 | 25 | func ExpectSOA(answer *dns.Msg) (*dns.SOA, error) { 26 | if len(answer.Answer) < 1 { 27 | return nil, fmt.Errorf("expected at least one answer section.") 28 | } 29 | firstNS := answer.Answer[0] 30 | soa, ok := firstNS.(*dns.SOA) 31 | if !ok { 32 | return nil, fmt.Errorf("expected SOA but got type of RR: %T: %+v", firstNS, firstNS) 33 | } 34 | return soa, nil 35 | } 36 | 37 | func ExpectAdditonalSOA(answer *dns.Msg) (*dns.SOA, error) { 38 | if len(answer.Ns) < 1 { 39 | return nil, fmt.Errorf("expected at least one authority section.") 40 | } 41 | firstNS := answer.Ns[0] 42 | soa, ok := firstNS.(*dns.SOA) 43 | if !ok { 44 | return nil, fmt.Errorf("expected SOA but got type of RR: %T: %+v", firstNS, firstNS) 45 | } 46 | return soa, nil 47 | } 48 | 49 | func AnySOA(answer *dns.Msg) (*dns.SOA, error) { 50 | if soa, err := ExpectSOA(answer); err == nil { 51 | return soa, nil 52 | } 53 | if soa, err := ExpectAdditonalSOA(answer); err == nil { 54 | return soa, nil 55 | } 56 | spew.Dump(answer) 57 | return nil, errors.New("no SOA in either answer or additional") 58 | } 59 | -------------------------------------------------------------------------------- /golang/sig0/doh.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strings" 7 | 8 | "os" 9 | 10 | "github.com/davecgh/go-spew/spew" 11 | "github.com/miekg/dns" 12 | "github.com/shynome/doh-client" 13 | ) 14 | 15 | func SendDOHQuery(server string, m *dns.Msg) (*dns.Msg, error) { 16 | co := &dns.Conn{Conn: doh.NewConn(nil, nil, server)} 17 | 18 | err := co.WriteMsg(m) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | answer, err := co.ReadMsg() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return answer, nil 29 | } 30 | 31 | func SendUDPQuery(server string, m *dns.Msg) (*dns.Msg, error) { 32 | co, err := dns.Dial("udp4", server+":53") 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | err = co.WriteMsg(m) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | answer, err := co.ReadMsg() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return answer, nil 48 | } 49 | 50 | func FindDOHEndpoint(name string) (*url.URL, error) { 51 | lookup := dns.Fqdn("_dns." + name) 52 | 53 | svcbQry, err := QueryWithType(lookup, dns.TypeSVCB) 54 | if err != nil { 55 | return nil, fmt.Errorf("findDOHEndpoint: failed to construct query for %s: %w", lookup, err) 56 | } 57 | 58 | answer, err := SendDOHQuery(DefaultDOHResolver, svcbQry) 59 | if err != nil { 60 | return nil, fmt.Errorf("findDOHEndpoint: no answer for svcb query for %s: %w", lookup, err) 61 | } 62 | if len(answer.Answer) < 1 { 63 | spew.Dump(answer) 64 | return nil, fmt.Errorf("findDOHEndpoint: no answer section for %s", lookup) 65 | } 66 | 67 | // TODO deal with more than one SVCB "_dns." + name in RRSet 68 | first := answer.Answer[0] 69 | svcb, ok := first.(*dns.SVCB) 70 | if !ok { 71 | return nil, fmt.Errorf("findDOHEndpoint: unable to cast %T answer for %s to SVCB", first, lookup) 72 | } 73 | 74 | if !dns.IsFqdn(svcb.Target) { 75 | return nil, fmt.Errorf("findDOHEndpoint: Expected SVCB target to be FQDN but got %q", svcb.Target) 76 | } 77 | host := svcb.Target[:len(svcb.Target)-1] 78 | 79 | var alpn *dns.SVCBAlpn 80 | var dohPath *dns.SVCBDoHPath 81 | 82 | for _, v := range svcb.Value { 83 | switch tv := v.(type) { 84 | case *dns.SVCBAlpn: 85 | alpn = tv 86 | case *dns.SVCBDoHPath: 87 | dohPath = tv 88 | default: 89 | fmt.Fprintf(os.Stderr, "findDOHEndpoint: unexpected keyValue pair type: %T\n", tv) 90 | } 91 | } 92 | 93 | if alpn == nil { 94 | return nil, fmt.Errorf("findDOHEndpoint: expected alpn in svcb.Value") 95 | } 96 | if dohPath == nil { 97 | return nil, fmt.Errorf("findDOHEndpoint: expected dohPath in svcb.Value") 98 | } 99 | if dohPath.Template == "" { 100 | return nil, fmt.Errorf("findDOHEndpoint: expected dohPath Template value") 101 | } 102 | 103 | // TODO: write this more genericly but striclty speaky we should check for IN HTTPS to check if we can use https 104 | var hasHttp = false 105 | for _, a := range alpn.Alpn { 106 | if strings.Contains(a, "h1") || strings.Contains(a, "h2") { 107 | hasHttp = true 108 | } 109 | } 110 | if !hasHttp { 111 | fmt.Printf("alpn: %#v\n", alpn.Alpn[0]) 112 | return nil, fmt.Errorf("findDOHEndpoint: expected http alpn in svcb.Value") 113 | } 114 | 115 | const placeholder = "{?dns}" 116 | if !strings.Contains(dohPath.Template, placeholder) { 117 | return nil, fmt.Errorf("findDOHEndpoint: dohPath does not contain {?dns}: %s", dohPath.Template) 118 | } 119 | 120 | var u url.URL 121 | u.Scheme = "https" 122 | u.Host = host 123 | u.Path = strings.Replace(dohPath.Template, placeholder, "", -1) 124 | return &u, nil 125 | } 126 | -------------------------------------------------------------------------------- /golang/sig0/doh_test.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestFindDOHEndpoint(t *testing.T) { 10 | endpoint, err := FindDOHEndpoint("zenr.io") 11 | require.NoError(t, err) 12 | 13 | want := "https://doh.zenr.io/dns-query" 14 | require.Equal(t, want, endpoint.String()) 15 | } 16 | -------------------------------------------------------------------------------- /golang/sig0/keys.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "crypto" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | var DefaultKeyTTL uint32 = 60 13 | 14 | type StoredKeyData struct { 15 | Name string 16 | Key string 17 | Private string 18 | } 19 | 20 | func (data StoredKeyData) ParseKey() (*dns.KEY, error) { 21 | rr, err := dns.NewRR(data.Key) 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to read RR from key data: %w", err) 24 | } 25 | 26 | dnsKey, ok := rr.(*dns.KEY) 27 | if !ok { 28 | return nil, fmt.Errorf("expected dns.KEY, instead: %T", rr) 29 | } 30 | return dnsKey, nil 31 | } 32 | 33 | // wasm<>js bridge helper 34 | func (data StoredKeyData) AsMap() map[string]any { 35 | return map[string]any{ 36 | "Key": data.Key, 37 | "Name": data.Name, 38 | // TODO: could call ParseKey() and add header fields as needed 39 | } 40 | } 41 | 42 | type Signer struct { 43 | Key *dns.KEY 44 | private crypto.PrivateKey 45 | 46 | update *dns.Msg 47 | } 48 | 49 | func (s Signer) KeyName() string { 50 | zone := s.Key.Hdr.Name 51 | return fmt.Sprintf("K%s+%03d+%d", zone, s.Key.Algorithm, s.Key.KeyTag()) 52 | } 53 | 54 | func LoadOrGenerateKey(zone string) (*Signer, error) { 55 | if !strings.HasSuffix(zone, ".") { 56 | zone += "." 57 | } 58 | 59 | keys, err := ListKeys(".") 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | for _, key := range keys { 65 | if strings.HasPrefix(key.Name, "K"+zone) { 66 | signer, err := LoadKeyFile(key.Name) 67 | if err == nil { 68 | return signer, nil 69 | } 70 | } 71 | } 72 | 73 | log.Println("generating new key for", zone) 74 | return GenerateKeyAndSave(zone) 75 | 76 | } 77 | 78 | // GenerateKey creates a new ED25519 key for the given zone 79 | func GenerateKey(zone string) (*Signer, error) { 80 | if !strings.HasSuffix(zone, ".") { 81 | zone += "." 82 | } 83 | 84 | var k = new(dns.KEY) 85 | k.Hdr.Name = zone 86 | k.Hdr.Class = dns.ClassINET 87 | k.Hdr.Rrtype = dns.TypeKEY 88 | k.Algorithm = dns.ED25519 89 | k.Hdr.Ttl = DefaultKeyTTL 90 | // TODO: find consts for these magic numbers 91 | k.Flags = 0x200 92 | k.Protocol = 3 93 | 94 | const keySize = 256 95 | pk, err := k.Generate(keySize) 96 | if err != nil { 97 | return nil, fmt.Errorf("failed to generate key: %w", err) 98 | } 99 | 100 | return &Signer{ 101 | Key: k, 102 | private: pk, 103 | }, nil 104 | } 105 | 106 | func ParseKeyData(key, private string) (*Signer, error) { 107 | rr, err := dns.NewRR(key) 108 | if err != nil { 109 | return nil, fmt.Errorf("failed to read RR from key data: %w", err) 110 | } 111 | 112 | dnsKey, ok := rr.(*dns.KEY) 113 | if !ok { 114 | return nil, fmt.Errorf("expected dns.KEY, instead: %T", rr) 115 | } 116 | 117 | hdr := rr.Header() 118 | log.Println("key import:", hdr.Name, hdr.Ttl, hdr.Class, hdr.Rrtype, dnsKey.Flags, dnsKey.Protocol, dnsKey.Algorithm, dnsKey.PublicKey) 119 | 120 | privkey, err := dnsKey.ReadPrivateKey(strings.NewReader(private), rr.Header().Name+":private") 121 | if err != nil { 122 | return nil, fmt.Errorf("failed to read private key material from private key data: %w", err) 123 | } 124 | 125 | return &Signer{Key: dnsKey, private: privkey}, nil 126 | } 127 | 128 | // Lists keys as JSON 129 | // Returns a filtered array of Keystore public keys and names as array of JSON objects 130 | // Keys returned possess a public key DNS RR name where searchDomain is contained. 131 | // where 132 | // 133 | // Name: 134 | // Key: 135 | func ListKeysFiltered(dir, searchDomain string) ([]StoredKeyData, error) { 136 | allKeys, err := ListKeys(dir) 137 | if err != nil { 138 | return nil, err 139 | } 140 | var keys []StoredKeyData 141 | for _, k := range allKeys { 142 | parsed, err := k.ParseKey() 143 | if err != nil { 144 | return nil, fmt.Errorf("failed to parse Key: %s: %w", k.Name, err) 145 | } 146 | if strings.HasSuffix(searchDomain, parsed.Hdr.Name) { 147 | keys = append(keys, k) 148 | } 149 | } 150 | return keys, nil 151 | } 152 | -------------------------------------------------------------------------------- /golang/sig0/keys_nowasm.go: -------------------------------------------------------------------------------- 1 | //go:build !wasm 2 | // +build !wasm 3 | 4 | package sig0 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | func GenerateKeyAndSave(zone string) (*Signer, error) { 16 | signer, err := GenerateKey(zone) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | keyName := signer.KeyName() 22 | 23 | keyData := []byte(signer.Key.String()) 24 | err = os.WriteFile(keyName+".key", keyData, 0644) 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to write %q: %w", keyName+".key", err) 27 | } 28 | 29 | privateData := []byte(signer.Key.PrivateKeyString(signer.private)) 30 | err = os.WriteFile(keyName+".private", privateData, 0600) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to write %q: %w", keyName+".private", err) 33 | } 34 | 35 | return signer, nil 36 | } 37 | 38 | func ListKeys(dir string) ([]StoredKeyData, error) { 39 | fh, err := os.Open(dir) 40 | if err != nil { 41 | return nil, fmt.Errorf("failed to open %q: %w", dir, err) 42 | } 43 | defer fh.Close() 44 | 45 | names, err := fh.Readdirnames(0) 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to read directory %q: %w", dir, err) 48 | } 49 | 50 | var keyfiles []StoredKeyData 51 | for _, name := range names { 52 | if !(strings.HasPrefix(name, "K") && strings.HasSuffix(name, ".key")) { 53 | continue 54 | } 55 | 56 | keyData, err := os.ReadFile(filepath.Join(dir, name)) 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to read %q: %w", name, err) 59 | } 60 | keyfiles = append(keyfiles, StoredKeyData{ 61 | Name: name, 62 | Key: string(keyData), 63 | }) 64 | } 65 | 66 | return keyfiles, nil 67 | } 68 | 69 | func LoadKeyFile(keyfile string) (*Signer, error) { 70 | var ( 71 | pubKeyName = keyfile + ".key" 72 | secretKeyName = keyfile + ".private" 73 | ) 74 | 75 | pubfh, err := os.Open(pubKeyName) 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to open %q: %w", pubKeyName, err) 78 | } 79 | defer pubfh.Close() 80 | 81 | rr, err := dns.ReadRR(pubfh, pubKeyName) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to read RR from %q: %w", pubKeyName, err) 84 | } 85 | 86 | dnsKey, ok := rr.(*dns.KEY) 87 | if !ok { 88 | return nil, fmt.Errorf("expected dns.KEY, instead: %T", rr) 89 | } 90 | 91 | privfh, err := os.Open(secretKeyName) 92 | if err != nil { 93 | return nil, fmt.Errorf("failed to open %q: %w", secretKeyName, err) 94 | } 95 | defer privfh.Close() 96 | 97 | privkey, err := dnsKey.ReadPrivateKey(privfh, secretKeyName) 98 | if err != nil { 99 | return nil, fmt.Errorf("failed to read private key material from %q: %w", secretKeyName, err) 100 | } 101 | 102 | return &Signer{Key: dnsKey, private: privkey}, nil 103 | } 104 | -------------------------------------------------------------------------------- /golang/sig0/keys_test.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestLoadKey(t *testing.T) { 16 | r := require.New(t) 17 | keyName := createKeyViaBind(t) 18 | 19 | signer, err := LoadKeyFile(keyName) 20 | r.NoError(err) 21 | r.NotNil(signer) 22 | } 23 | 24 | func TestParseKeyFile(t *testing.T) { 25 | r := require.New(t) 26 | a := assert.New(t) 27 | keyName := createKeyViaBind(t) 28 | 29 | keyContent, err := os.ReadFile(keyName + ".key") 30 | r.NoError(err) 31 | 32 | privateContent, err := os.ReadFile(keyName + ".private") 33 | r.NoError(err) 34 | 35 | signer, err := ParseKeyData(string(keyContent), string(privateContent)) 36 | r.NoError(err) 37 | r.NotNil(signer) 38 | 39 | // t.Logf("pk type: %T", signer.private) 40 | 41 | a.Equal(uint8(0xf), signer.Key.Algorithm, "Algorithm") 42 | a.Equal(uint16(0x200), signer.Key.Flags, "Flags") 43 | a.Equal(uint8(0x3), signer.Key.Protocol, "Protocol") 44 | a.Equal(DefaultKeyTTL, signer.Key.Hdr.Ttl, "TTL") 45 | 46 | // re-create the key string 47 | k := signer.Key 48 | out := k.String() 49 | out = strings.ReplaceAll(out, "\t", " ") 50 | out += "\n" 51 | a.Equal(string(keyContent), out) 52 | 53 | // TODO: recreate the private string 54 | // pk := signer.Key.PrivateKeyString(signer.private) 55 | // assert.Equal(t, string(privateContent), pk) 56 | } 57 | 58 | func TestWritingAndParsingInBind(t *testing.T) { 59 | r := require.New(t) 60 | 61 | signer, err := GenerateKeyAndSave("go.te.st") 62 | r.NoError(err) 63 | 64 | kn := signer.KeyName() 65 | t.Log("KeyName:", kn) 66 | t.Cleanup(func() { 67 | os.Remove(kn + ".key") 68 | os.Remove(kn + ".private") 69 | }) 70 | 71 | nsUpdateVersion, err := exec.Command("nsupdate", "-V").CombinedOutput() 72 | r.NoError(err) 73 | // TODO: add regexp for >= 9.18 74 | r.Contains(string(nsUpdateVersion), "nsupdate 9.18") 75 | 76 | nsUpdateInput := strings.NewReader(` 77 | update add go.te.st 60 A 1.1.1.1 78 | show 79 | `) 80 | cmd := exec.Command("nsupdate", "-k", kn) 81 | cmd.Stdin = nsUpdateInput 82 | cmd.Stderr = os.Stderr 83 | cmd.Stdout = os.Stderr 84 | err = cmd.Run() 85 | r.NoError(err) 86 | } 87 | 88 | func TestCompareFlags(t *testing.T) { 89 | a := assert.New(t) 90 | 91 | bindKey := createAndLoadKey(t) 92 | 93 | ourKey, err := GenerateKey("go.te.st") 94 | require.NoError(t, err) 95 | 96 | a.Equal(bindKey.Key.Algorithm, ourKey.Key.Algorithm, "Algorithm") 97 | a.Equal(bindKey.Key.Flags, ourKey.Key.Flags, "Flags") 98 | a.Equal(bindKey.Key.Protocol, ourKey.Key.Protocol, "Protocol") 99 | a.Equal(bindKey.Key.Hdr.Ttl, ourKey.Key.Hdr.Ttl, "TTL") 100 | } 101 | 102 | func createKeyViaBind(t *testing.T) string { 103 | var buf bytes.Buffer 104 | cmd := exec.Command("dnssec-keygen", "-K", "/tmp", "-a", "ED25519", "-n", "HOST", "-T", "KEY", "-L", "60", "go.te.st") 105 | cmd.Stderr = os.Stderr 106 | cmd.Stdout = &buf 107 | err := cmd.Run() 108 | if err != nil { 109 | t.Log(buf.String()) 110 | } 111 | require.NoError(t, err) 112 | 113 | keyName := filepath.Join("/tmp", strings.TrimSpace(buf.String())) 114 | 115 | t.Log("created key file:", keyName) 116 | 117 | t.Cleanup(func() { 118 | os.Remove(keyName + ".key") 119 | os.Remove(keyName + ".private") 120 | }) 121 | 122 | return keyName 123 | } 124 | 125 | func createAndLoadKey(t *testing.T) *Signer { 126 | keyName := createKeyViaBind(t) 127 | 128 | signer, err := LoadKeyFile(keyName) 129 | require.NoError(t, err) 130 | require.NotNil(t, signer) 131 | 132 | return signer 133 | } 134 | -------------------------------------------------------------------------------- /golang/sig0/keys_wasm.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "syscall/js" 8 | ) 9 | 10 | func GenerateKeyAndSave(zone string) (*Signer, error) { 11 | signer, err := GenerateKey(zone) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | var persisted StoredKeyData 17 | persisted.Key = signer.Key.String() 18 | persisted.Private = signer.Key.PrivateKeyString(signer.private) 19 | 20 | marshalled, err := json.Marshal(persisted) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to marshal key data: %w", err) 23 | } 24 | 25 | keyName := fmt.Sprintf("K%s+%03d+%d", zone, signer.Key.Algorithm, signer.Key.KeyTag()) 26 | js.Global().Get("localStorage").Call("setItem", keyName, string(marshalled)) 27 | 28 | return signer, nil 29 | } 30 | 31 | func LoadKeyFile(keyfile string) (*Signer, error) { 32 | keyDataJson := js.Global().Get("localStorage").Call("getItem", keyfile).String() 33 | if keyDataJson == "" { 34 | return nil, fmt.Errorf("key not found") 35 | } 36 | 37 | var data StoredKeyData 38 | err := json.Unmarshal([]byte(keyDataJson), &data) 39 | if err != nil { 40 | return nil, fmt.Errorf("failed to unmarshal key data for %q: %w", keyfile, err) 41 | } 42 | 43 | return ParseKeyData(data.Key, data.Private) 44 | } 45 | 46 | // Returns all Keystore public keys and names as array of JSON objects 47 | // where 48 | // 49 | // Name: 50 | // Key: 51 | func ListKeys(dir string) ([]StoredKeyData, error) { 52 | if dir != "." { 53 | return nil, fmt.Errorf("directories not supported in wasm - use '.'") 54 | } 55 | 56 | n := js.Global().Get("localStorage").Get("length").Int() 57 | 58 | var keys []StoredKeyData 59 | for i := 0; i < n; i++ { 60 | key := js.Global().Get("localStorage").Call("key", i) 61 | if key.IsNull() { 62 | break 63 | } 64 | 65 | keyName := key.String() 66 | if !strings.HasPrefix(keyName, "K") { 67 | continue 68 | } 69 | 70 | keyDataJson := js.Global().Get("localStorage").Call("getItem", keyName).String() 71 | if keyDataJson == "" { 72 | continue 73 | } 74 | 75 | var data StoredKeyData 76 | err := json.Unmarshal([]byte(keyDataJson), &data) 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to unmarshal key data for %q: %w", keyName, err) 79 | } 80 | data.Name = keyName 81 | 82 | // validate key data 83 | _, err = ParseKeyData(data.Key, data.Private) 84 | if err != nil { 85 | return nil, fmt.Errorf("failed to parse key data for %q: %w", keyName, err) 86 | } 87 | 88 | keys = append(keys, data) 89 | } 90 | 91 | return keys, nil 92 | } 93 | -------------------------------------------------------------------------------- /golang/sig0/query.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/davecgh/go-spew/spew" 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | // QueryA returns a base64 encoded string of a DNS Question for an A record of the passed domain name 14 | func QuerySOA(zone string) (*dns.Msg, error) { 15 | return QueryWithType(zone, dns.TypeSOA) 16 | } 17 | 18 | func QueryA(name string) (*dns.Msg, error) { 19 | return QueryWithType(name, dns.TypeA) 20 | } 21 | 22 | func QueryKEY(name string) (*dns.Msg, error) { 23 | return QueryWithType(name, dns.TypeKEY) 24 | } 25 | 26 | func QueryPTR(name string) (*dns.Msg, error) { 27 | return QueryWithType(name, dns.TypePTR) 28 | } 29 | 30 | // uses ANY query type 31 | func QueryAny(name string) (*dns.Msg, error) { 32 | return QueryWithType(name, dns.TypeANY) 33 | } 34 | 35 | func QueryNSEC(name string) (*dns.Msg, error) { 36 | return QueryWithType(name, dns.TypeNSEC) 37 | } 38 | 39 | func QueryRRSIG(name string) (*dns.Msg, error) { 40 | return QueryWithType(name, dns.TypeRRSIG) 41 | } 42 | 43 | func QueryWithType(name string, qtype uint16) (*dns.Msg, error) { 44 | q := dns.Question{ 45 | Name: dns.Fqdn(name), 46 | Qtype: qtype, 47 | Qclass: dns.ClassINET, 48 | } 49 | 50 | m := &dns.Msg{ 51 | MsgHdr: dns.MsgHdr{Id: dns.Id(), Opcode: dns.OpcodeQuery, RecursionDesired: true, AuthenticatedData: true}, 52 | Question: []dns.Question{q}, 53 | } 54 | m.SetEdns0(4096, true) 55 | 56 | if os.Getenv("DEBUG") != "" { 57 | fmt.Println("DNS Query:") 58 | spew.Dump(m) 59 | } 60 | 61 | return m, nil 62 | } 63 | 64 | func QueryWithStringType(name, qtype string) (*dns.Msg, error) { 65 | t, err := QueryTypeFromString(qtype) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return QueryWithType(name, t) 70 | } 71 | 72 | func QueryTypeFromString(value string) (uint16, error) { 73 | var t uint16 74 | switch strings.ToLower(value) { 75 | case "a": 76 | t = dns.TypeA 77 | case "aaaa": 78 | t = dns.TypeAAAA 79 | case "any": 80 | t = dns.TypeANY 81 | case "key": 82 | t = dns.TypeKEY 83 | case "ptr": 84 | t = dns.TypePTR 85 | case "loc": 86 | t = dns.TypeLOC 87 | case "txt": 88 | t = dns.TypeTXT 89 | case "svcb": 90 | t = dns.TypeSVCB 91 | case "srv": 92 | t = dns.TypeSRV 93 | case "soa": 94 | t = dns.TypeSOA 95 | case "nsec": 96 | t = dns.TypeNSEC 97 | case "rrsig": 98 | t = dns.TypeRRSIG 99 | default: 100 | asNum, err := strconv.ParseUint(value, 10, 16) 101 | if err != nil { 102 | return 0, fmt.Errorf("unhandled dns type: %q: %w", value, err) 103 | } 104 | t = uint16(asNum) 105 | } 106 | return t, nil 107 | } 108 | -------------------------------------------------------------------------------- /golang/sig0/query_test.go: -------------------------------------------------------------------------------- 1 | // -build test_network 2 | 3 | package sig0 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/miekg/dns" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestQuerySOA(t *testing.T) { 14 | r := require.New(t) 15 | a := assert.New(t) 16 | 17 | const srv = "9.9.9.9" 18 | 19 | zones := []struct { 20 | zone, soa string 21 | }{ 22 | 23 | {"cryptix.pizza", "ns1.dnsowl.com."}, 24 | {"_signal.zenr.io", "ns1.free2air.org."}, 25 | {"twitter.com", "a.u06.twtrdns.net."}, 26 | //{"github.com", "ns-1622.awsdns-10.co.uk."}, 27 | {"wikipedia.org", "ns0.wikimedia.org."}, 28 | } 29 | for _, testdata := range zones { 30 | qry, err := QuerySOA(testdata.zone) 31 | r.NoError(err, testdata) 32 | 33 | answer, err := SendDOHQuery(srv, qry) 34 | r.NoError(err, testdata) 35 | 36 | soa, err := ExpectSOA(answer) 37 | r.NoError(err, testdata) 38 | a.Equal(testdata.soa, soa.Ns) 39 | 40 | verifyication, err := QueryWithType(testdata.zone, dns.TypeSOA) 41 | r.NoError(err, testdata) 42 | 43 | verifyAnswer, err := SendUDPQuery(soa.Ns, verifyication) 44 | r.NoError(err, testdata) 45 | r.True(verifyAnswer.Authoritative, verifyAnswer) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /golang/sig0/request_key.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | var ( 13 | SignalSubzonePrefix = "_signal" 14 | DefaultTTL = 300 15 | //DefaultDOHResolver = "8.8.8.8" 16 | // DefaultDOHResolver = "1.1.1.1" 17 | // DefaultDOHResolver = "quad9.zenr.io" 18 | DefaultDOHResolver = "google.zenr.io" 19 | // DefaultDOHResolver = "doh.zenr.io" 20 | ) 21 | 22 | func RequestKey(newName string) error { 23 | log.Println("[requestKey] query SOA for", newName) 24 | query, err := QuerySOA(newName) 25 | if err != nil { 26 | return fmt.Errorf("ZONE %s SOA record does not resolve: %w", newName, err) 27 | } 28 | 29 | var answer *dns.Msg 30 | answer, err = SendDOHQuery(DefaultDOHResolver, query) 31 | if err != nil { 32 | // TODO: add context to err 33 | return err 34 | } 35 | 36 | soaForZone, err := AnySOA(answer) 37 | if err != nil { 38 | return fmt.Errorf("SOA record not found in response for %s: %w", newName, err) 39 | } 40 | 41 | zoneOfName := soaForZone.Hdr.Name 42 | log.Printf("[requestKey] Found zone for new name: %s", zoneOfName) 43 | 44 | newNameFQDN := newName 45 | if !strings.HasSuffix(newNameFQDN, ".") { 46 | newNameFQDN += "." 47 | } 48 | 49 | if !strings.HasSuffix(newNameFQDN, zoneOfName) { 50 | err = fmt.Errorf("expected new zone to be under it's SOA. Instead got SOA %q for %q", zoneOfName, newNameFQDN) 51 | return err 52 | } 53 | subDomain := strings.TrimSuffix(newNameFQDN, zoneOfName) 54 | 55 | // Determine the zone master using the provided sub zone and base zone 56 | signalZone := fmt.Sprintf("%s.%s", SignalSubzonePrefix, zoneOfName) 57 | querySOAForSignal, err := QuerySOA(signalZone) 58 | if err != nil { 59 | err = fmt.Errorf("ZONE %s SOA record does not resolve: %w", signalZone, err) 60 | return err 61 | } 62 | 63 | answer, err = SendDOHQuery(DefaultDOHResolver, querySOAForSignal) 64 | if err != nil { 65 | // TODO: add context to err 66 | return err 67 | } 68 | 69 | signalZoneSoa, err := ExpectSOA(answer) 70 | if err != nil { 71 | err = fmt.Errorf("SOA record not found in response for %s: %w", signalZone, err) 72 | return err 73 | } 74 | 75 | if !strings.HasSuffix(signalZoneSoa.Hdr.Name, soaForZone.Hdr.Name) { 76 | err = fmt.Errorf("expected signal zone to be under requested zonet got %q and %q", signalZoneSoa.Hdr.Name, soaForZone.Hdr.Name) 77 | return err 78 | } 79 | 80 | // get DoH endpoint for signal zone via SVCB 81 | dohEndpoint, err := FindDOHEndpoint(signalZone) 82 | if err != nil { 83 | err = fmt.Errorf("unable to lookup DOH endoint for signal zone: %w", err) 84 | return err 85 | } 86 | log.Printf("[requestKey] Found DOH endoint: %s", dohEndpoint) 87 | 88 | // Check if requested name already exists 89 | existQuery, err := QueryAny(newName) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | answer, err = SendDOHQuery(dohEndpoint.Host, existQuery) 95 | if err != nil { 96 | // TODO: add context to err 97 | return err 98 | } 99 | 100 | if answer.Rcode != dns.RcodeNameError { 101 | return fmt.Errorf("new zone %s already exists: %v", newName, answer) 102 | } 103 | 104 | log.Printf("[requestKey/debug] %s is not yet taken", newName) 105 | 106 | // check request doesnt exist 107 | zoneRequest := fmt.Sprintf("%s%s.%s", subDomain, SignalSubzonePrefix, zoneOfName) 108 | existQuery, err = QueryAny(zoneRequest) 109 | if err != nil { 110 | return fmt.Errorf("exists query for zoneRequest %q failed: %w", zoneRequest, err) 111 | } 112 | 113 | answer, err = SendDOHQuery(dohEndpoint.Host, existQuery) 114 | if err != nil { 115 | // TODO: add context to err 116 | return err 117 | } 118 | 119 | if answer.Rcode != dns.RcodeNameError { 120 | return fmt.Errorf("existing zoneRequest for %q already exists: %v", zoneRequest, answer) 121 | } 122 | 123 | // craft RRs and create signed update 124 | nameSigner, err := LoadOrGenerateKey(newName) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | log.Printf("[requestKey/debug] creating request with key %s", nameSigner.Key.String()) 130 | 131 | err = nameSigner.StartUpdate(zoneOfName) 132 | if err != nil { 133 | return fmt.Errorf("unable to start update for zone: %q: %w", zoneOfName, err) 134 | } 135 | 136 | // Here we split the key details 137 | // turn it into an RR and split of the first 3 fields 138 | // so that we can re-use the key for a different zone 139 | // TODO: there should be a way to get the keyData without full stringification 140 | keyDetails := strings.TrimSpace(nameSigner.Key.String()) 141 | keyFields := strings.Fields(keyDetails) 142 | if len(keyFields) < 6 { 143 | return errors.New("invalid key data") 144 | } 145 | keyData := strings.Join(keyFields[3:], " ") 146 | 147 | nsupdateItemSig0Key := fmt.Sprintf("%s %d %s", zoneRequest, DefaultTTL, keyData) 148 | err = nameSigner.UpdateParsedRR(nsupdateItemSig0Key) 149 | if err != nil { 150 | return fmt.Errorf("failed to add KEY RR: %w", err) 151 | } 152 | 153 | nsupdateItemPtr := fmt.Sprintf("%s %d IN PTR %s", signalZone, DefaultTTL, zoneRequest) 154 | err = nameSigner.UpdateParsedRR(nsupdateItemPtr) 155 | if err != nil { 156 | return fmt.Errorf("failed to add PTR RR: %w", err) 157 | } 158 | 159 | updateMsg, err := nameSigner.UnsignedUpdate(signalZone) 160 | if err != nil { 161 | return fmt.Errorf("unable to create update message: %w", err) 162 | } 163 | 164 | answer, err = SendDOHQuery(dohEndpoint.Host, updateMsg) 165 | if err != nil { 166 | return fmt.Errorf("unable to send update: %w", err) 167 | } 168 | 169 | if answer == nil { 170 | return fmt.Errorf("answer is nil") 171 | 172 | } 173 | 174 | if answer.Rcode != dns.RcodeSuccess { 175 | return fmt.Errorf("update failed: %v", answer) 176 | } 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /golang/sig0/request_key_test.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/miekg/dns" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestRequestKey(t *testing.T) { 16 | r := require.New(t) 17 | a := assert.New(t) 18 | 19 | // TODO: cleanup test keys 20 | buf := make([]byte, 5) 21 | _, _ = rand.Read(buf) 22 | testName := fmt.Sprintf("sig0namectl-test-%x.zenr.io", buf) 23 | 24 | err := RequestKey(testName) 25 | r.NoError(err) 26 | 27 | t.Log("registered key - checking registration") 28 | 29 | for i := 10; true; i-- { 30 | accepted, err := QueryAny(testName) 31 | r.NoError(err) 32 | 33 | answer, err := SendDOHQuery("doh.zenr.io", accepted) 34 | r.NoError(err) 35 | t.Log(answer) 36 | 37 | if answer.Rcode != dns.RcodeNameError { 38 | t.Log("name registered") 39 | break 40 | } 41 | 42 | if i == 0 { 43 | t.Fatal("name registration failed") 44 | } 45 | 46 | t.Log("waiting...") 47 | time.Sleep(15 * time.Second) 48 | } 49 | 50 | // TODO: move checkKey() from wrapper_js.go into sig0 51 | signer, err := LoadOrGenerateKey(testName) 52 | r.NoError(err) 53 | 54 | t.Cleanup(func() { 55 | kn := signer.KeyName() 56 | t.Log("deleting key:", kn) 57 | _ = os.Remove(kn + ".private") 58 | _ = os.Remove(kn + ".key") 59 | }) 60 | 61 | err = signer.StartUpdate("zenr.io") 62 | r.NoError(err) 63 | 64 | err = signer.UpdateA("foo", testName, "1.2.3.4") 65 | r.NoError(err) 66 | 67 | updateMsg, err := signer.SignUpdate() 68 | r.NoError(err) 69 | 70 | answer, err := SendDOHQuery("doh.zenr.io", updateMsg) 71 | r.NoError(err) 72 | if !a.True(answer.Rcode == dns.RcodeSuccess, "answer not successful") { 73 | t.Log(answer) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /golang/sig0/update.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "crypto" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strings" 9 | "time" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | func (signer *Signer) StartUpdate(zone string) error { 15 | if signer.update != nil { 16 | return fmt.Errorf("update already in progress") 17 | } 18 | log.Println("-- Set dns.Msg Structure --") 19 | m := new(dns.Msg) 20 | m.SetUpdate(dns.Fqdn(zone)) 21 | 22 | signer.update = m 23 | return nil 24 | } 25 | 26 | func (signer *Signer) UnsignedUpdate(zone string) (*dns.Msg, error) { 27 | if signer.update == nil { 28 | return nil, fmt.Errorf("no update in progress") 29 | } 30 | 31 | if !strings.HasSuffix(zone, ".") { 32 | zone += "." 33 | } 34 | 35 | m := signer.update 36 | m.SetUpdate(zone) 37 | signer.update = nil 38 | return m, nil 39 | } 40 | 41 | func (signer *Signer) SignUpdate() (*dns.Msg, error) { 42 | if signer.update == nil { 43 | return nil, fmt.Errorf("no update in progress") 44 | } 45 | 46 | // create & fill SIG structure (see sig0_test.go for guidance) 47 | log.Println("-- Create, fill & attach SIG RR to dns.Msg Structure --") 48 | now := uint32(time.Now().Unix()) 49 | sig0RR := new(dns.SIG) 50 | sig0RR.Hdr.Name = "." 51 | sig0RR.Hdr.Rrtype = dns.TypeSIG 52 | sig0RR.Hdr.Class = dns.ClassANY 53 | sig0RR.Algorithm = signer.Key.Algorithm 54 | sig0RR.Expiration = now + 300 55 | sig0RR.Inception = now - 300 56 | sig0RR.KeyTag = signer.Key.KeyTag() 57 | sig0RR.SignerName = signer.Key.Hdr.Name 58 | 59 | mb, err := sig0RR.Sign(signer.private.(crypto.Signer), signer.update) 60 | if err != nil { 61 | algstr := dns.AlgorithmToString[signer.Key.Algorithm] 62 | return nil, fmt.Errorf("failed to sign %v message: %w", algstr, err) 63 | } 64 | 65 | // unpack SIG into update message 66 | if err := signer.update.Unpack(mb); err != nil { 67 | return nil, fmt.Errorf("failed to unpack message: %w", err) 68 | } 69 | 70 | m := signer.update 71 | signer.update = nil 72 | return m, nil 73 | } 74 | 75 | func (signer *Signer) UpdateParsedRR(rr string) error { 76 | rrInsert, err := dns.NewRR(rr) 77 | if err != nil { 78 | return fmt.Errorf("sig0: failed to parse RR: %w", err) 79 | } 80 | 81 | return signer.UpdateRR(rrInsert) 82 | } 83 | 84 | func (signer *Signer) UpdateRR(rr dns.RR) error { 85 | if signer.update == nil { 86 | return fmt.Errorf("no update in progress") 87 | } 88 | 89 | signer.update.Insert([]dns.RR{rr}) 90 | return nil 91 | } 92 | 93 | func (signer *Signer) RemoveParsedRR(rr string) error { 94 | rrRemove, err := dns.NewRR(rr) 95 | if err != nil { 96 | return fmt.Errorf("sig0: failed to parse RR: %w", err) 97 | } 98 | 99 | return signer.RemoveRR(rrRemove) 100 | } 101 | 102 | func (signer *Signer) RemoveRR(rr dns.RR) error { 103 | if signer.update == nil { 104 | return fmt.Errorf("no update in progress") 105 | } 106 | 107 | signer.update.Remove([]dns.RR{rr}) 108 | return nil 109 | } 110 | 111 | func (signer *Signer) RemoveParsedRRset(rr string) error { 112 | rrRemove, err := dns.NewRR(rr) 113 | if err != nil { 114 | return fmt.Errorf("sig0: failed to parse RR: %w", err) 115 | } 116 | 117 | return signer.RemoveRRset(rrRemove) 118 | } 119 | 120 | func (signer *Signer) RemoveRRset(rr dns.RR) error { 121 | if signer.update == nil { 122 | return fmt.Errorf("no update in progress") 123 | } 124 | 125 | signer.update.RemoveRRset([]dns.RR{rr}) 126 | return nil 127 | } 128 | 129 | func (signer *Signer) RemoveParsedName(rr string) error { 130 | rrRemove, err := dns.NewRR(rr) 131 | if err != nil { 132 | return fmt.Errorf("sig0: failed to parse RR: %w", err) 133 | } 134 | 135 | return signer.RemoveName(rrRemove) 136 | } 137 | 138 | func (signer *Signer) RemoveName(rr dns.RR) error { 139 | if signer.update == nil { 140 | return fmt.Errorf("no update in progress") 141 | } 142 | 143 | signer.update.RemoveName([]dns.RR{rr}) 144 | return nil 145 | } 146 | 147 | // UpdateA is a convenience function to update an A record. 148 | // Need to call StartUpdate first, then UpdateA for each record to update, then SignUpdate. 149 | func (signer *Signer) UpdateA(subZone, zone, addr string) error { 150 | if signer.update == nil { 151 | return fmt.Errorf("no update in progress") 152 | } 153 | 154 | parsedIP := net.ParseIP(addr) 155 | if parsedIP.To4() == nil { 156 | return fmt.Errorf("invalid IPv4 address: %s", addr) 157 | } 158 | 159 | myRR := fmt.Sprintf("%s.%s %d IN A %s", subZone, zone, DefaultTTL, addr) 160 | return signer.UpdateParsedRR(myRR) 161 | } 162 | -------------------------------------------------------------------------------- /golang/sig0/update_test.go: -------------------------------------------------------------------------------- 1 | package sig0 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/miekg/dns" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestUpdate(t *testing.T) { 11 | r := require.New(t) 12 | signer := createAndLoadKey(t) 13 | 14 | err := signer.StartUpdate("zone") 15 | r.NoError(err) 16 | 17 | err = signer.UpdateA("host", "zone", "1.2.3.4") 18 | r.NoError(err) 19 | 20 | signedUpdate, err := signer.SignUpdate() 21 | r.NoError(err) 22 | 23 | // verify signing 24 | rr, ok := signedUpdate.Extra[0].(*dns.SIG) 25 | r.True(ok, "expected SIG RR, instead: %T", signedUpdate.Extra[0]) 26 | 27 | mb, err := signedUpdate.Pack() 28 | r.NoError(err) 29 | 30 | err = rr.Verify(signer.Key, mb) 31 | r.NoError(err) 32 | } 33 | -------------------------------------------------------------------------------- /golang/wasm/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT := demo 2 | 3 | PORT := 8800 4 | 5 | ${PROJECT}.wasm: *.go ../sig0/*.go 6 | GOOS=js GOARCH=wasm go build -o ${PROJECT}.wasm 7 | 8 | start: ${PROJECT}.wasm 9 | nohup python3 -m http.server -d . ${PORT} & 10 | 11 | stop: 12 | pkill -f "python3 -m http.server -d . ${PORT}" 13 | -------------------------------------------------------------------------------- /golang/wasm/README.md: -------------------------------------------------------------------------------- 1 | # WebAssembly DNS Demo 2 | 3 | ## Function access 4 | 5 | ```js 6 | const {query, parse} = window.goFuncs 7 | 8 | const q = query("test.somewhe.re") 9 | 10 | const resp = await fetch(`https://zembla.zenr.io/dns-query?dns=${q}`) 11 | // TODO: assert status == 200 12 | const data = await resp.arrayBuffer() 13 | 14 | const answer = parse(_arrayBufferToBase64(data)) 15 | console.log(answer) 16 | ``` 17 | 18 | 19 | ## Links 20 | 21 | * https://golangbot.com/webassembly-using-go/ 22 | 23 | -------------------------------------------------------------------------------- /golang/wasm/wasm_exec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Go<>JS Demo 7 | 8 | 9 | 10 | 11 | 12 |

    DNS

    13 |

    DOH Resolver

    14 |

    15 | 16 | 17 | 18 |

    
    19 |   

    20 | 21 |

    DNS Keys

    22 |

    List Keystore

    23 | 24 | 25 | 26 |
    27 | 28 |

    Request

    29 |

    30 | 31 | 32 |

    33 | 34 |

    Search Keystore for Key to sign update for a given Subdomain

    35 |

    36 | 37 | 38 |

    39 |

    40 | 41 |

    Find DOH Endpoint for a given Subdomain

    42 |

    43 | 44 | 45 |

    46 |

    47 | 48 |

    Import Key pair files from filesystem into Browser Keystore

    49 |

    50 | 51 |

      52 |
      53 |

      54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /keystore/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/keystore/.keep -------------------------------------------------------------------------------- /presentations/20230815 CCCamp 2023 To Name is to Own.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20230815 CCCamp 2023 To Name is to Own.odp -------------------------------------------------------------------------------- /presentations/20230815 CCCamp 2023 To Name is to Own.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20230815 CCCamp 2023 To Name is to Own.pdf -------------------------------------------------------------------------------- /presentations/20230824 Draft Improved Presentation.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20230824 Draft Improved Presentation.odp -------------------------------------------------------------------------------- /presentations/20230824 Draft Improved Presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20230824 Draft Improved Presentation.pdf -------------------------------------------------------------------------------- /presentations/20240501 Freifunk Berlin May meeting Presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20240501 Freifunk Berlin May meeting Presentation.pdf -------------------------------------------------------------------------------- /presentations/20240525 P4P Unconference at offline-space.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20240525 P4P Unconference at offline-space.odp -------------------------------------------------------------------------------- /presentations/20241211 splintercon sig0namectl 16-9 v3.7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetworkCommons/sig0namectl/3001d62401798923c97679d5b82c9b83411f0ed6/presentations/20241211 splintercon sig0namectl 16-9 v3.7.pdf -------------------------------------------------------------------------------- /process_requests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # process_key 4 | # Processes signal "requests" to _signal. ie CDS, CDNSKEY, and KEY records to zone master. 5 | #------------------------------------------------------------------------------ 6 | 7 | # load helpful functions 8 | for i in functions/*.sh 9 | do 10 | . ${i} 11 | [[ -n ${DEBUG_SOURCED} ]] && echo "Sourced ${PWD}/functions/$i ..." 12 | done 13 | 14 | #------------------------------------------------------------------------------ 15 | 16 | set_vars $* 17 | 18 | # find zone master for ${SIGNAL_SUBZONE}.${ZONE} which may be different from zone master of ${ZONE} 19 | SIGNAL_SOA_MASTER=$( get_soa_master "${SIGNAL_SUBZONE}.${ZONE}" ) 20 | if [[ ! -n ${SIGNAL_SOA_MASTER} ]]; then 21 | echo "Warning: ZONE ${SIGNAL_SUBZONE}.${ZONE} SOA record does not resolve" 22 | exit 1 23 | fi 24 | 25 | # if not set, set default update auth key to zonename 26 | if [[ ! -n ${NSUPDATE_AUTH_SIG0_KEY_FQDN} ]]; then 27 | [[ -n ${DEBUG_PROCESS_REQUESTS} ]] && echo "Warning: ZONE update KEY FQDN not set, setting default to zone FQDN '${ZONE}'" 28 | NSUPDATE_AUTH_SIG0_KEY_FQDN="${NSUPDATE_AUTH_SIG0_KEY_FQDN:-${ZONE}}" 29 | fi 30 | 31 | # find existing SIG0 auth keypair for nsupdate parameter 32 | [[ ! -n ${NSUPDATE_AUTH_SIG0_KEYID} ]] && get_sig0_keyid NSUPDATE_AUTH_SIG0_KEYID ${NSUPDATE_AUTH_SIG0_KEY_FQDN} ${NSUPDATE_SIG0_KEYPATH} 33 | if [[ ! -n ${NSUPDATE_AUTH_SIG0_KEYID} ]]; then 34 | echo "Warning: ZONE ${ZONE}: SIG(0) keypair for ${NSUPDATE_AUTH_SIG0_KEY_FQDN} not found in keystore ${NSUPDATE_AUTH_SIG0_KEYPATH}" 35 | exit 1 36 | fi 37 | 38 | 39 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} # ensures default action 40 | NSUPDATE_TTL=${NSUPDATE_TTL:-"60"} # ensures default TTLs for RRs 41 | 42 | REQUEST_QUEUE="`dig @${SIGNAL_SOA_MASTER} +short ${SIGNAL_SUBZONE}.${ZONE} PTR`" 43 | # [[ -n ${TEST_REQUEST} ]] && REQUEST_QUEUE="${REQUEST_QUEUE} testzone._signal.zenr.io _signal._signal.zenr.io below.test._signal.zenr.io" 44 | # TODO: handle timeout errors from dig (error lines start with ';;') 45 | for request_ptr in ${REQUEST_QUEUE}; do 46 | # get requested KEY from SIGNAL_SOA_MASTER 47 | request_key="`dig @${SIGNAL_SOA_MASTER} +noall +answer ${request_ptr} KEY`" 48 | # remove signal zone FQDN 49 | request_key="`echo ${request_key} | cut -f3- -d' '`" 50 | request_key_subzone="${request_ptr%%\.${SIGNAL_SUBZONE}*}" 51 | request_key_fqdn="${request_key_subzone}.${ZONE}" 52 | 53 | case ${NSUPDATE_ACTION} in 54 | add) 55 | # test for ANY usual DNS RR, as well as NS delegation RRs (which are not covered by ANY) 56 | request_key_fqdn_dig="`dig @${ZONE_SOA_MASTER} +noall +answer +nocrypto +dnssec ${request_key_fqdn} ANY``dig @${ZONE_SOA_MASTER} +noall +authority +nocrypto +nodnssec ${request_key_fqdn} NS | grep -v SOA`" 57 | # send add update iff no DNS RR or NS exists 58 | if [[ ! -n ${request_key_fqdn_dig} ]]; then 59 | # set SOA_MASTER & NEW_SUBZONE for send_update() 60 | # SOA_MASTER="${ZONE_SOA_MASTER}" 61 | NSUPDATE_PRECONDITION_SET="nxdomain" 62 | NEW_SUBZONE="${request_key_subzone}" 63 | NEW_SUBZONE_SIG0_KEY=${request_key##*.} 64 | NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${NEW_SUBZONE}.${ZONE}" 65 | NSUPDATE_ITEM_SIG0_KEY="update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${ZONE} ${NSUPDATE_TTL} ${NEW_SUBZONE_SIG0_KEY}" 66 | # add successful request 67 | send_nsupdate "${ZONE}" "$(echo ${NSUPDATE_PRECONDITION};echo ${NSUPDATE_ITEM_SIG0_KEY})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 68 | #send_update 69 | key_display="`dig @${ZONE_SOA_MASTER} +noall +answer +nottl +noclass +nocrypto +idnout ${request_key_fqdn} KEY`" 70 | request_key_fqdn_status="KEY '${request_key_fqdn}' submitted to ${NSUPDATE_ACTION} under zone '${ZONE}' with '[${key_display#*[}', IDN '${key_display%.*}' by KEY FQDN: ${NSUPDATE_AUTH_SIG0_KEY_FQDN} KEYID: ${NSUPDATE_AUTH_SIG0_KEYID} ." 71 | else 72 | request_key_fqdn_status="KEY '${request_key_fqdn}' IS NOT submitted to ${NSUPDATE_ACTION} under zone '${ZONE}', as DNS resource records for '${request_key_fqdn}' already exist." 73 | fi 74 | # now delete PTR & KEY record 75 | # optionally they could be archived somwhere else first ... 76 | # NSUPDATE_PRECONDITION_SET="yxdomain" 77 | # NSUPDATE_PRECONDITION="" 78 | NSUPDATE_ITEM_SIGNAL_PTR="update delete ${SIGNAL_SUBZONE}.${ZONE}. PTR ${request_ptr}" 79 | NSUPDATE_ITEM_SIGNAL_KEY="update delete ${request_ptr} ${request_key}" 80 | send_nsupdate "${ZONE}" "$(echo ${NSUPDATE_ITEM_SIGNAL_PTR};echo ${NSUPDATE_ITEM_SIGNAL_KEY})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 81 | 82 | ;; 83 | delete) 84 | # test for KEY DNS RR to delete (only deletes KEY & no other RR) 85 | request_key_fqdn_dig="`dig @${ZONE_SOA_MASTER} +noall +answer +nocrypto +dnssec ${request_key_fqdn} KEY`" 86 | # send delete update iff KEY FQDN exists in zone 87 | if [[ -n ${request_key_fqdn_dig} ]]; then 88 | # SOA_MASTER="${ZONE_SOA_MASTER}" 89 | NSUPDATE_PRECONDITION_SET="yxdomain" 90 | NEW_SUBZONE="${request_key_subzone}" 91 | NEW_SUBZONE_SIG0_KEY=${request_key##*.} 92 | NSUPDATE_PRECONDITION="prereq ${NSUPDATE_PRECONDITION_SET} ${NEW_SUBZONE}.${ZONE}" 93 | NSUPDATE_ITEM_SIG0_KEY="update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${ZONE} ${NSUPDATE_TTL} ${NEW_SUBZONE_SIG0_KEY}" 94 | # add successful request 95 | key_display="`dig @${ZONE_SOA_MASTER} +noall +answer +nottl +noclass +nocrypto +idnout ${request_key_fqdn} KEY`" 96 | send_nsupdate "${ZONE}" "$(echo ${NSUPDATE_PRECONDITION};echo ${NSUPDATE_ITEM_SIG0_KEY})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 97 | # send_update 98 | request_key_fqdn_status="KEY '${request_key_fqdn}' submitted to ${NSUPDATE_ACTION} under zone '${ZONE}', with '[${key_display#*[}', IDN '${key_display%.*}'." 99 | else 100 | request_key_fqdn_status="KEY '${request_key_fqdn}' IS NOT submitted to ${NSUPDATE_ACTION} under zone '${ZONE}', as no DNS KEY resource record exists." 101 | fi 102 | ;; 103 | *) 104 | # NSUPDATE_ACTION should default to "add" - should never get here 105 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 106 | exit 1 107 | ;; 108 | esac 109 | echo "${request_key_fqdn_status}" 110 | logger "${request_key_fqdn_status}" 111 | done 112 | -------------------------------------------------------------------------------- /request_key: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # register_key 4 | # Sends signal "requests" to ${SIGNAL_SUBZONE}. ie CDS, CDNSKEY, and KEY records to zone master. 5 | #------------------------------------------------------------------------------ 6 | 7 | # load helpful functions 8 | for i in functions/*.sh 9 | do 10 | . ${i} 11 | [[ -n ${DEBUG_SOURCED} ]] && echo "Sourced ${PWD}/$i ..." 12 | done 13 | 14 | #------------------------------------------------------------------------------ 15 | 16 | set_vars $* 17 | 18 | # Define NEW_SUBZONE as a name (subdomain or subzone) within ZONE for requested SIG0 key FQDN 19 | # 20 | if [[ ! -n ${NEW_SUBZONE} ]]; then 21 | echo 22 | echo "Error: NEW_SUBZONE ${NEW_SUBZONE} environment variable is undefined." 23 | exit 1 24 | fi 25 | 26 | # find zone master for ${SIGNAL_SUBZONE}.${ZONE} which may be different from zone master of ${ZONE} 27 | SIGNAL_SOA_MASTER=$( get_soa_master "${SIGNAL_SUBZONE}.${ZONE}" ) 28 | if [[ ! -n ${SIGNAL_SOA_MASTER} ]]; then 29 | echo "Error: ZONE ${SIGNAL_SUBZONE}.${ZONE} SOA record does not resolve" 30 | exit 1 31 | fi 32 | 33 | # set requested final FQDN of key to be submitted to zone 34 | SIG0_KEY_FQDN="${NEW_SUBZONE}.${ZONE}" 35 | 36 | # find any existing key from keystore 37 | # Note: NEW_SUBZONE_SIG0_KEYID value updated by reference in get_sig0_keyid() 38 | get_sig0_keyid NEW_SUBZONE_SIG0_KEYID ${SIG0_KEY_FQDN} ${NSUPDATE_SIG0_KEYPATH} 39 | 40 | # if no such key exists in keystore, create a new keypair in keystore 41 | if [[ ! -n ${NEW_SUBZONE_SIG0_KEYID} ]]; then 42 | [[ -n ${DEBUG} ]] && echo "No SIG0 keypair for ${SIG0_KEY_FQDN} found in ${NSUPDATE_SIG0_KEYPATH}" 43 | SIG0_KEY_ALGO=${SIG0_KEY_ALGO:-"ED25519"} 44 | dnssec-keygen -K ${NSUPDATE_SIG0_KEYPATH} -a ${SIG0_KEY_ALGO} -n HOST -T KEY ${SIG0_KEY_FQDN} || exit 1 45 | echo "New SIG0 keypair for ${SIG0_KEY_FQDN} generated in ${NSUPDATE_SIG0_KEYPATH}" 46 | get_sig0_keyid NEW_SUBZONE_SIG0_KEYID ${SIG0_KEY_FQDN} ${NSUPDATE_SIG0_KEYPATH} 47 | [[ ! -n ${NEW_SUBZONE_SIG0_KEYID} ]] && echo "Error creating new key for ${SIG0_KEY_FQDN} in keystore ${NSUPDATE_SIG0_KEYPATH}" && exit 1 48 | fi 49 | 50 | # form input for nsupdate 51 | 52 | NSUPDATE_TTL=${NSUPDATE_TTL:-"60"} 53 | 54 | NSUPDATE_ACTION=${NSUPDATE_ACTION:-"add"} 55 | 56 | NEW_SUBZONE_SIG0_KEY="`cat ${NSUPDATE_SIG0_KEYPATH}/${NEW_SUBZONE_SIG0_KEYID}.key`" 57 | # create item for *request* with KEY RR under ${SIGNAL_SUBZONE}.${ZONE} 58 | NSUPDATE_ITEM_SIG0_KEY="update ${NSUPDATE_ACTION} ${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE} ${NSUPDATE_TTL} ${NEW_SUBZONE_SIG0_KEY##*.}" 59 | 60 | # create pointer RR to request processing 61 | NSUPDATE_ITEM_PTR="update ${NSUPDATE_ACTION} ${SIGNAL_SUBZONE}.${ZONE} ${NSUPDATE_TTL} PTR ${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE}." 62 | 63 | if [[ -n ${DEBUG} ]]; then 64 | echo 65 | echo "ZONE_SOA_MASTER = ${ZONE_SOA_MASTER}" 66 | echo "SIGNAL_SOA_MASTER = ${ZONE_SOA_MASTER}" 67 | echo 68 | echo "SUBZONE KEY Request FQDN = ${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE}" 69 | echo " Requested SIG0 KEY FQDN = ${SIG0_KEY_FQDN}" 70 | echo " Requested SIG0 DNS Update KEY ID = ${NEW_SUBZONE_SIG0_KEYID}" 71 | echo " Requested SIG0 DNS Update KEY file = ${NEW_SUBZONE_SIG0_KEY}" 72 | fi 73 | 74 | DIG_SIG0_SIGNAL_FQDN="$( dig +short @${SIGNAL_SOA_MASTER} ${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE} KEY )" 75 | DIG_SIG0_FQDN="$( dig +short @${ZONE_SOA_MASTER} ${SIG0_KEY_FQDN} KEY )" 76 | 77 | case ${NSUPDATE_ACTION} in 78 | add) 79 | if [[ ! -n ${DIG_SIG0_FQDN} ]]; then 80 | # add request 81 | [[ -n ${DIG_SIG0_SIGNAL_FQDN} ]] && echo "Existing KEY request '${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE} ${DIG_SIG0_SIGNAL_FQDN}' in queue." && exit 1 82 | send_nsupdate "${ZONE}" "$(echo ${NSUPDATE_ITEM_SIG0_KEY};echo ${NSUPDATE_ITEM_PTR})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 83 | echo "KEY request '${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE} ${NEW_SUBZONE_SIG0_KEY##*.}' added" 84 | else 85 | echo "Cannot request new KEY with FQDN '${SIG0_KEY_FQDN}' as KEY record already exists in zone '${ZONE}'." 86 | echo "Please select a new key name under zone '${ZONE}'." 87 | exit 1 88 | fi 89 | ;; 90 | delete) 91 | if [[ -n ${DIG_SIG0_SIGNAL_FQDN} ]]; then 92 | # delete request 93 | [[ -n ${DIG_SIG0_FQDN} ]] && echo "KEY '${NEW_SUBZONE}.${ZONE} ${DIG_SIG0_FQDN}' is already deployed." && exit 1 94 | send_nsupdate "${ZONE}" "$(echo ${NSUPDATE_ITEM_SIG0_KEY};echo ${NSUPDATE_ITEM_PTR})" "${NSUPDATE_AUTH_SIG0_KEY_FQDN}" 95 | echo "KEY request '${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE}' deleted" 96 | else 97 | echo "Cannot request deletion of KEY with FQDN '${NEW_SUBZONE}.${SIGNAL_SUBZONE}.${ZONE}' as KEY record does not exist in zone '${SIGNAL_SUBZONE}.${ZONE}'." 98 | fi 99 | ;; 100 | 101 | *) 102 | # NSUPDATE_ACTION should default to "add" - should never get here 103 | echo "Error: NSUPDATE_ACTION is set to '${NSUPDATE_ACTION}', but must be set to 'add' or 'delete'." 104 | exit 1 105 | ;; 106 | esac 107 | 108 | 109 | # list pending requests for processor 110 | if [[ -n ${DEBUG_LIST} ]]; then 111 | echo 112 | echo "ZONE KEY REQUESTS for '${ZONE}'" 113 | echo 114 | dig @${SIGNAL_SOA_MASTER} +noall +answer +nottl +noclass PTR ${SIGNAL_SUBZONE}.${ZONE} 115 | echo 116 | echo "ZONE KEY STATUS for '${ZONE}'" 117 | echo 118 | REQUEST_QUEUE="`dig @${SIGNAL_SOA_MASTER} +short ${SIGNAL_SUBZONE}.${ZONE} PTR`" 119 | for request_ptr in ${REQUEST_QUEUE}; do 120 | signal_key="`dig @${SIGNAL_SOA_MASTER} +noall +answer +nottl +noclass +nocrypto +idnout ${request_ptr} KEY`" 121 | signal_key_fqdn="`echo ${signal_key} | cut -f1 -d' '`" 122 | requested_keyname="${signal_key_fqdn%%\.${SIGNAL_SUBZONE}*}" 123 | requested_key_fqdn="${requested_keyname}.${ZONE}" 124 | echo "-- REQUEST FQDN: ${signal_key_fqdn} requests KEY '${requested_key_fqdn}' under zone '${ZONE}':" 125 | # echo "${requested_key}" 126 | # echo "${signal_key}" 127 | deployed_key="`dig @${ZONE_SOA_MASTER} +noall +answer +idnin +idnout ${requested_key_fqdn} KEY`" 128 | # echo "deployed_key='${deployed_key}'" 129 | [[ -n ${deployed_key} ]] && echo "-- DEPLOYED KEY: ${deployed_key}" || echo "** ${requested_key_fqdn} is not deployed" 130 | done 131 | fi 132 | -------------------------------------------------------------------------------- /start_loc_loop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while true; do 3 | ./dyn_loc ${1} 4 | sleep 10 5 | done 6 | -------------------------------------------------------------------------------- /start_process_requests_loop.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | while true; do 3 | ./process_requests $1 4 | sleep 5 5 | done 6 | --------------------------------------------------------------------------------