Compare commits

..

3 Commits

Author SHA1 Message Date
lordwelch
f147fb47bb Add JSON tags 2020-06-17 06:13:07 -07:00
lordwelch
b6dfdcd5ef DNS changes
go mod tidy
2020-06-17 06:13:07 -07:00
lordwelch
db53b259a6 Add the ability to run router7 on a normal Linux distribution 2020-06-17 06:13:07 -07:00
69 changed files with 751 additions and 3228 deletions

View File

@ -16,8 +16,8 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
# Run on the latest minor release of Go 1.18:
go-version: ^1.18
# Run on the latest minor release of Go 1.14:
go-version: ^1.14
id: go
- name: Check out code into the Go module directory
@ -33,7 +33,7 @@ jobs:
- name: Ensure all files were formatted as per gofmt
run: |
[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
gofmt -l $(find . -name '*.go') >/dev/null
- name: Go Vet
run: |
@ -51,8 +51,8 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
# Run on the latest minor release of Go 1.18:
go-version: ^1.18
# Run on the latest minor release of Go 1.14:
go-version: ^1.14
id: go
- name: Check out code into the Go module directory
@ -78,8 +78,8 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
# Run on the latest minor release of Go 1.18:
go-version: ^1.18
# Run on the latest minor release of Go 1.14:
go-version: ^1.14
id: go
- name: Check out code into the Go module directory

View File

@ -1,19 +1,10 @@
SUDO=GOPATH=$(shell go env GOPATH) sudo --preserve-env=GOPATH --preserve-env=PATH --preserve-env=HOME
SUDO=GOPATH=$(shell go env GOPATH) sudo --preserve-env=GOPATH
PKGS := github.com/gokrazy/breakglass \
PKGS := github.com/rtr7/router7/cmd/... \
github.com/gokrazy/breakglass \
github.com/gokrazy/timestamps \
github.com/gokrazy/gdns \
github.com/gokrazy/serial-busybox \
github.com/prometheus/node_exporter \
github.com/gokrazy/fbstatus \
github.com/gokrazy/iptables \
github.com/gokrazy/nsenter \
github.com/gokrazy/podman \
github.com/greenpau/cni-plugins/cmd/cni-nftables-portmap \
github.com/greenpau/cni-plugins/cmd/cni-nftables-firewall \
github.com/gokrazy/syslogd/cmd/gokr-syslogd \
github.com/gokrazy/stat/cmd/... \
github.com/rtr7/router7/cmd/...
github.com/stapelberg/zkj-nas-tools/wolgw \
github.com/gokrazy/gdns
build:
mkdir -p result
@ -29,45 +20,41 @@ ifndef DIR
@echo variable DIR unset
false
endif
go install github.com/gokrazy/tools/cmd/gokr-packer@latest
go install github.com/gokrazy/tools/cmd/gokr-packer
GOARCH=amd64 gokr-packer \
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
-kernel_package=github.com/rtr7/kernel \
-firmware_package=github.com/rtr7/kernel \
-eeprom_package= \
-overwrite_boot=${DIR}/boot.img \
-overwrite_root=${DIR}/root.img \
-overwrite_mbr=${DIR}/mbr.img \
-serial_console=ttyS0,115200 \
-serial_console=ttyS0,115200n8 \
-hostname=router7 \
${PKGS}
recover: #test
go install github.com/gokrazy/tools/cmd/gokr-packer@latest
go install github.com/rtr7/tools/cmd/rtr7-recover@latest
go install \
github.com/gokrazy/tools/cmd/gokr-packer \
github.com/rtr7/tools/cmd/rtr7-recover
GOARCH=amd64 gokr-packer \
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
-kernel_package=github.com/rtr7/kernel \
-firmware_package=github.com/rtr7/kernel \
-eeprom_package= \
-overwrite_boot=/tmp/recovery/boot.img \
-overwrite_root=/tmp/recovery/root.img \
-serial_console=ttyS0,115200n8 \
-hostname=router7 \
${PKGS}
${SUDO} $(which rtr7-recover) \
--boot /tmp/recovery/boot.img \
--root /tmp/recovery/root.img \
--mbr /tmp/recovery/mbr.img \
--hostname router7 \
--interface enp0s31f6
${SUDO} /home/michael/go/bin/rtr7-recover \
-boot=/tmp/recovery/boot.img \
-root=/tmp/recovery/root.img
test:
# simulate recover (quick, for early for feedback)
go build -mod=mod ${PKGS} github.com/rtr7/tools/cmd/...
go test -mod=mod -count=1 -v -race github.com/rtr7/router7/internal/...
go build ${PKGS} github.com/rtr7/tools/cmd/...
go test -count=1 -v -race github.com/rtr7/router7/internal/...
# integration tests
${SUDO} $(shell go env GOROOT)/bin/go test -buildvcs=false -count=1 -v -race github.com/rtr7/router7/...
${SUDO} $(shell go env GOROOT)/bin/go test -count=1 -v -race github.com/rtr7/router7/...
testdhcp:
go test -v -coverprofile=/tmp/cov github.com/rtr7/router7/internal/dhcp4d
@ -81,23 +68,20 @@ strace:
(cd /tmp && go test -c router7) && ${SUDO} strace -f -o /tmp/st -s 2048 /tmp/router7.test -test.v #-test.race
update:
rtr7-safe-update -build_command='make image DIR=$$GOKR_DIR'
rtr7-safe-update -build_command='make -C ~/router7 image DIR=$GOKR_DIR'
# sudo ip link add link enp3s0 name macvtap0 type macvtap
# sudo ip link add link enp0s31f6 name macvtap0 type macvtap
# sudo ip link set macvtap0 address 52:55:00:d1:55:03 up
# sudo chown $USER /dev/tap*
#
# TODO: use veth pairs for router7s lan0?
# e.g. get a network namespace to talk through router7
# ip link add dev veth1 type veth peer name veth2
qemu:
mkdir -p /tmp/router7-qemu
GOARCH=amd64 gokr-packer \
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
-hostname=qemu-router7 \
-kernel_package=github.com/rtr7/kernel \
-firmware_package=github.com/rtr7/kernel \
-eeprom_package= \
-overwrite=/tmp/router7-qemu/disk.img \
-target_storage_bytes=$$((2*1024*1024*1024)) \
-serial_console=ttyS0,115200 \
@ -111,7 +95,6 @@ qemu:
-device virtio-net-pci,netdev=uplink,mac=52:55:00:d1:55:03 \
-device virtio-net-pci,id=lan,mac=52:55:00:d1:55:04 \
-device i6300esb,id=watchdog0 -watchdog-action reset \
-bios /usr/share/edk2-ovmf/x64/OVMF_CODE.fd \
-smp 8 \
-machine accel=kvm \
-m 4096 \

163
README.md
View File

@ -1,6 +1,6 @@
# router7
[![GitHub Actions CI](https://github.com/rtr7/router7/actions/workflows/go.yml/badge.svg)](https://github.com/rtr7/router7/actions/workflows/go.yml)
[![Build Status](https://travis-ci.org/rtr7/router7.svg?branch=master)](https://travis-ci.org/rtr7/router7)
[![GoDoc](https://godoc.org/github.com/rtr7/router7/cmd?status.svg)](https://godoc.org/github.com/rtr7/router7/cmd)
[![Go Report Card](https://goreportcard.com/badge/github.com/rtr7/router7)](https://goreportcard.com/report/github.com/rtr7/router7)
@ -8,4 +8,163 @@ router7 is a pure-Go implementation of a small home internet router. It comes wi
Note that this project should be considered a (working!) tech demo. Feature requests will likely not be implemented, and see [CONTRIBUTING.md](CONTRIBUTING.md) for details about which contributions are welcome.
**For more details, please see [router7.org](https://router7.org/)**
## Motivation
Before starting router7, I was using the [Turris Omnia](https://omnia.turris.cz/en/) router running OpenWrt. That worked fine up until May 2018, when an automated update pulled in a new version of [odhcp6c](https://git.openwrt.org/?p=project/odhcp6c.git;a=shortlog), OpenWrts DHCPv6 client. That version is incompatible with fiber7s DHCP server setup (I think there are shortcomings on both sides).
It was not only quicker to develop my own router than to wait for either side to resolve the issue, but it was also a lot of fun and allowed me to really tailor my router to my needs, experimenting with a bunch of interesting ideas I had.
## Project goals
* Maximize internet connectivity: retain the most recent DHCP configuration across reboots and even after its expiration (chances are the DHCP server will be back before the configuration stops working).
* Unit/integration tests use fiber7 packet capture files to minimize the chance of software changes breaking my connectivity.
* Safe and quick updates
* Auto-rollback of updates which result in loss of connectivity: the diagnostics daemon assesses connectivity state, the update tool reads it and rolls back faulty updates.
* Thanks to kexec, updates translate into merely 13s of internet connectivity loss.
* Easy debugging
* Configuration-related network packets (e.g. DHCP, IPv6 neighbor/router advertisements) are stored in a ring buffer which can be streamed into [Wireshark](https://www.wireshark.org/), allowing for live and retro-active debugging.
* The diagnostics daemon performs common diagnostic steps (ping, traceroute, …) for you.
* All state in the system is stored as human-readable JSON within the `/perm` partition and can be modified.
## Hardware
The reference hardware platform is the [PC Engines™ apu2c4](https://pcengines.ch/apu2c4.htm) system board. It features a 1 GHz quad core amd64 CPU, 4 GB of RAM, 3 Ethernet ports and a DB9 serial port. It conveniently supports PXE boot, the schematics and bootloader sources are available. I recommend the [msata16g](https://pcengines.ch/msata16g.htm) SSD module for reliable persistent storage and the [usbcom1a](https://pcengines.ch/usbcom1a.htm) serial adapter if you dont have one already.
Other hardware might work, too, but is not tested.
### Teensy rebootor
The cheap and widely-available [Teensy++ USB development board](https://www.pjrc.com/store/teensypp.html) comes with a firmware called rebootor, which is used by the [`teensy_loader_cli`](https://www.pjrc.com/teensy/loader_cli.html) program to perform hard resets.
This setup can be used to programmatically reset the apu2c4 (from `rtr7-recover`) by connecting the Teensy++ to the [apu2c4s reset pins](http://pcengines.ch/pdf/apu2.pdf):
* connect the Teensy++s `GND` pin to the apu2c4 J2s pin 4 (`GND`)
* connect the Teensy++s `B7` pin to the apu2c4 J2s pin 5 (`3.3V`, resets when pulled to `GND`)
You can find a working rebootor firmware .hex file at https://github.com/PaulStoffregen/teensy_loader_cli/issues/38
## Architecture
router7 is based on [gokrazy](https://gokrazy.org/): it is an appliance which gets packed into a hard disk image, containing a FAT partition with the kernel, a read-only SquashFS partition for the root file system and an ext4 partition for permanent data.
The individual services can be found in [github.com/rtr7/router7/cmd](https://godoc.org/github.com/rtr7/router7/cmd).
* Each service runs in a separate process.
* Services communicate with each other by persisting state files. E.g., `cmd/dhcp4` writes `/perm/dhcp4/wire/lease.json`.
* A service notifies other services about state changes by sending them signal `SIGUSR1`.
### Configuration files
| File | Consumer(s) | Purpose |
|---|---|---|
| `/perm/interfaces.json` | `netconfigd` | Set IP/MAC addresses of `uplink0` and `lan0` |
| `/perm/portforwardings.json` | `netconfigd` | Configure nftables port forwarding rules |
| `/perm/dhcp6/duid` | `dhcp6` | Set DHCP Unique Identifier (DUID) for obtaining static leases |
### State files
| File | Producer | Consumer(s) | Purpose |
|---|---|---|---|
| `/perm/dhcp4/wire/ack` | `dhcp4` | `dhcp4` | last DHCPACK packet for renewals across restarts |
| `/perm/dhcp4/wire/lease.json` | `dhcp4` | `netconfigd` | Obtained DHCPv4 lease |
| `/perm/dhcp6/wire/lease.json` | `dhcp6` | `netconfigd`, `radvd` | Obtained DHCPv6 lease |
| `/perm/dhcp4d/leases.json` | `dhcp4d` | `dhcp4d`, `dnsd` | DHCPv4 leases handed out (including hostnames) |
### Available ports
| Port | Purpose |
|---|---|
| `<public>:8053` | `dnsd` metrics (forwarded requests)
| `<public>:8066` | `netconfigd` metrics (nftables counters)
| `<private>:80` | gokrazy web interface
| `<private>:67` | `dhcp4d`
| `<private>:58` | `radvd`
| `<private>:53` | `dnsd`
| `<private>:8077` | `backupd` (serve backup.tar.gz)
| `<private>:7733` | `diagd` (perform diagnostics)
| `<private>:5022` | `captured` (serve captured packets)
Heres an example of the diagd output:
<img src="https://github.com/rtr7/router7/raw/master/2018-07-14-diagd.png"
width="800" alt="diagd output">
Heres an example of the metrics when scraped with [Prometheus](https://prometheus.io/) and displayed in [Grafana](https://grafana.com/):
<img src="https://github.com/rtr7/router7/raw/master/2018-07-14-grafana.png"
width="800" alt="metrics in grafana">
## Installation
Connect your serial adapter ([usbcom1a](https://pcengines.ch/usbcom1a.htm) works well if you dont have one already) to the apu2c4 and start a program to use it, e.g. `screen /dev/ttyUSB0 115200`. Then, power on the apu2c4 and configure it to do PXE boot:
* Press `F10` to enter the boot menu
* Press `3` to enter setup
* Press `n` to enable network boot
* Press `c` to move mSATA to the top of the boot order
* Press `e` to move iPXE to the top of the boot order
* Press `s` to save configuration and exit
Connect a network cable on `net0`, the port closest to the serial console port:
<img src="https://github.com/rtr7/router7/raw/master/devsetup.jpg"
width="800" alt="router7 development setup">
Next, build a router7 image:
```
go get -u github.com/gokrazy/tools/cmd/gokr-packer github.com/rtr7/tools/cmd/...
go get -u -d github.com/rtr7/router7
mkdir /tmp/recovery
GOARCH=amd64 gokr-packer \
-hostname=router7 \
-overwrite_boot=/tmp/recovery/boot.img \
-overwrite_mbr=/tmp/recovery/mbr.img \
-overwrite_root=/tmp/recovery/root.img \
-kernel_package=github.com/rtr7/kernel \
-firmware_package=github.com/rtr7/kernel \
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp \
-serial_console=ttyS0,115200n8 \
github.com/rtr7/router7/cmd/...
```
Run `rtr7-recover -boot=/tmp/recovery/boot.img -mbr=/tmp/recovery/mbr.img -root=/tmp/recovery/root.img` to:
* trigger a reset if a Teensy with the rebootor firmware is attached
* serve a DHCP lease to all clients which request PXE boot (i.e., your apu2c4)
* serve via TFTP:
* the PXELINUX bootloader
* the router7 kernel
* an initrd archive containing the rtr7-recovery-init program and mke2fs
* serve via HTTP the boot and root images
* optionally serve via HTTP a backup.tar.gz image containing files for /perm (e.g. for moving to new hardware, rolling back corrupted state, or recovering from a disk failure)
* exit once the router successfully wrote the images to disk
### Updates
Run e.g. `rtr7-safe-update -updates_dir=$HOME/router7/updates` to:
* verify the router currently has connectivity, abort the update otherwise
* download a backup archive of `/perm`
* build a new image
* update the router
* wait until the router restored connectivity, roll back the update using `rtr7-recover` otherwise
The update step uses kexec to reduce the downtime to approximately 15 seconds.
### Manual Recovery
Given `rtr7-safe-update`s safeguards, manual recovery should rarely be required.
To manually roll back to an older image, invoke `rtr7-safe-update` via the
`recover.bash` script in the image directory underneath `-updates_dir`, e.g.:
```
% cd ~/router7/updates/2018-07-03T17:33:52+02:00
% ./recover.bash
```
### Prometheus
See https://github.com/rtr7/router7/tree/master/contrib/prometheus for example
configuration files, and install the [router7 Grafana
Dashboard](https://grafana.com/dashboards/8288).

View File

@ -51,7 +51,7 @@ func updateListeners() error {
func logic() error {
http.HandleFunc("/backup.tar.gz", func(w http.ResponseWriter, r *http.Request) {
if err := backup.Archive(w, *perm, flag.Args()); err != nil {
if err := backup.Archive(w, *perm); err != nil {
log.Printf("backup.tar.gz: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
@ -68,7 +68,6 @@ func logic() error {
}
func main() {
flag.Parse()
if err := logic(); err != nil {
log.Fatal(err)
}

View File

@ -1,4 +1,3 @@
//go:build ignore
// +build ignore
// Copyright 2018 Google Inc.

View File

@ -17,14 +17,11 @@
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/signal"
"path"
@ -37,7 +34,6 @@ import (
"github.com/google/gopacket/layers"
"github.com/google/renameio"
"github.com/jpillora/backoff"
rtr7dhcp4 "github.com/rtr7/dhcp4"
"github.com/rtr7/router7/internal/dhcp4"
"github.com/rtr7/router7/internal/netconfig"
"github.com/rtr7/router7/internal/notify"
@ -52,45 +48,6 @@ var (
perm = flag.String("perm", "/perm", "path to replace /perm")
)
func healthy() error {
req, err := http.NewRequest("GET", "http://localhost:7733/health.json", nil)
if err != nil {
return err
}
ctx, canc := context.WithTimeout(context.Background(), 30*time.Second)
defer canc()
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if got, want := resp.StatusCode, http.StatusOK; got != want {
b, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%v: got HTTP %v (%s), want HTTP status %v",
req.URL.String(),
resp.Status,
string(b),
want)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
var reply struct {
FirstError string `json:"first_error"`
}
if err := json.Unmarshal(b, &reply); err != nil {
return err
}
if reply.FirstError != "" {
return errors.New(reply.FirstError)
}
return nil
}
func logic() error {
leasePath := filepath.Join(*stateDir, "wire/lease.json")
if err := os.MkdirAll(filepath.Dir(leasePath), 0755); err != nil {
@ -137,36 +94,14 @@ func logic() error {
Min: 10 * time.Second,
Max: 1 * time.Minute,
}
var lastSuccess time.Time
if st, err := os.Stat(ackFn); err == nil {
lastSuccess = st.ModTime()
}
log.Printf("last success: %v", lastSuccess)
ObtainOrRenew:
for c.ObtainOrRenew() {
if err := c.Err(); err != nil {
dur := backoff.Duration()
// Drop the lease if we do not get a reply from the DHCP server.
// I observed this in practice where over a period of days,
// the dhcp4 client would hang like this:
//
// dhcp4.go:140: Temporary error: DHCP: read packet
// 42:66:f1:f1:bd:e7: i/o timeout (waiting 1m0s)
//
// For brief periods of time, we probably want to paper over such
// issues, but after the lease expired, we should start the DHCP
// exchange from scratch.
if c.Ack != nil && time.Since(lastSuccess) > rtr7dhcp4.LeaseFromACK(c.Ack).RenewalTime {
log.Printf("Temporary error: %v (dropping lease and retrying)", err)
c.Ack = nil
continue
}
log.Printf("Temporary error: %v (waiting %v)", err, dur)
time.Sleep(dur)
continue
}
backoff.Reset()
lastSuccess = time.Now()
log.Printf("lease: %+v", c.Config())
b, err := json.Marshal(c.Config())
if err != nil {
@ -189,43 +124,15 @@ ObtainOrRenew:
if err := notify.Process(path.Join(path.Dir(os.Args[0]), "/netconfigd"), syscall.SIGUSR1); err != nil {
log.Printf("notifying netconfig: %v", err)
}
unhealthyCycles := 0
for {
select {
case <-time.After(time.Until(c.Config().RenewAfter)):
// fallthrough and renew the DHCP lease
continue ObtainOrRenew
case <-time.After(1 * time.Minute):
if err := healthy(); err == nil {
unhealthyCycles = 0
continue // wait another minute
} else {
unhealthyCycles++
log.Printf("router unhealthy (cycle %d of 5): %v", unhealthyCycles, err)
if unhealthyCycles < 20 {
continue // wait until unhealthy for longer
}
// fallthrough
}
// Still not healthy? Drop DHCP lease and start from scratch.
log.Printf("unhealthy for 5 cycles, starting over without lease")
c.Ack = nil
continue ObtainOrRenew
case <-usr2:
log.Printf("SIGUSR2 received, sending DHCPRELEASE")
if err := c.Release(); err != nil {
return err
}
// Ensure dhcp4 does start from scratch next time
// by deleting the DHCPACK file:
if err := os.Remove(ackFn); err != nil && !os.IsNotExist(err) {
return err
}
os.Exit(125) // quit supervision by gokrazy
select {
case <-time.After(time.Until(c.Config().RenewAfter)):
// fallthrough and renew the DHCP lease
case <-usr2:
log.Printf("SIGUSR2 received, sending DHCPRELEASE")
if err := c.Release(); err != nil {
return err
}
os.Exit(125) // quit supervision by gokrazy
}
}
return c.Err() // permanent error

View File

@ -35,7 +35,6 @@ import (
"sync"
"syscall"
"time"
"unicode"
"github.com/gokrazy/gokrazy"
"github.com/google/renameio"
@ -47,7 +46,6 @@ import (
"github.com/rtr7/router7/internal/dhcp4d"
"github.com/rtr7/router7/internal/multilisten"
"github.com/rtr7/router7/internal/netconfig"
"github.com/rtr7/router7/internal/notify"
"github.com/rtr7/router7/internal/oui"
"github.com/rtr7/router7/internal/teelogger"
@ -60,9 +58,8 @@ var (
Help: "Number of non-expired DHCP leases",
})
iface = flag.String("interface", "lan0", "ethernet interface to listen for DHCPv4 requests on")
perm = flag.String("perm", "/perm", "path to replace /perm")
domain = flag.String("domain", "lan", "domain name for your network")
iface = flag.String("interface", "lan0", "ethernet interface to listen for DHCPv4 requests on")
perm = flag.String("perm", "/perm", "path to replace /perm")
)
func updateNonExpired(leases []*dhcp4d.Lease) {
@ -97,9 +94,6 @@ var (
}
return dur.Truncate(1 * time.Second).String()
},
"zero": func(t time.Time) bool {
return t.IsZero()
},
}).Parse(`<!DOCTYPE html>
<head>
<meta charset="utf-8">
@ -158,9 +152,7 @@ form {
<th>Hostname</th>
<th>MAC address</th>
<th>Vendor</th>
<th>VendorIdentifier</th>
<th>Expiry</th>
<th>Last ACK</th>
</tr>
{{ range $idx, $l := . }}
<tr>
@ -176,33 +168,27 @@ form {
</td>
<td class="hwaddr">{{$l.HardwareAddr}}</td>
<td>{{$l.Vendor}}</td>
<td>{{$l.VendorIdentifier}}</td>
<td title="{{ timefmt $l.Expiry }}">
{{ if (not (zero $l.LastACK)) }}
{{ timefmt $l.LastACK }}
{{ if $l.Active }}
{{ if $l.Expired }}
{{ since $l.Expiry }}
<span class="expired">expired</span>
{{ else }}
{{ if $l.Static }}
<span class="static">static</span>
{{ else }}
{{ timefmt $l.Expiry }}
<span class="active">active</span>
{{ end }}
{{ if $l.Expired }}
<span class="expired">expired</span>
{{ end }}
{{ end }}
</td>
</tr>
{{ end }}
{{ end }}
<h1>Static Leases</h1>
<table cellpadding="0" cellspacing="0">
{{ template "table" .StaticLeases }}
</table>
<h1>Dynamic Leases</h1>
<table cellpadding="0" cellspacing="0">
{{ template "table" .DynamicLeases }}
</table>
</body>
</html>
`))
@ -251,8 +237,6 @@ type srv struct {
}
func newSrv(permDir string) (*srv, error) {
mayqtt := MQTT()
http.Handle("/metrics", promhttp.Handler())
if err := updateListeners(); err != nil {
return nil, err
@ -275,27 +259,7 @@ func newSrv(permDir string) (*srv, error) {
if err != nil {
return nil, err
}
serverIP, err := netconfig.LinkAddress(permDir, *iface)
if err != nil {
return nil, err
}
serverIP = serverIP.To4()
var domainSearch []byte
domainSearch, err = dhcp4d.CompressNames("lan.", *domain)
if err != nil {
return nil, err
}
options := dhcp4.Options{
dhcp4.OptionSubnetMask: []byte{255, 255, 255, 0},
dhcp4.OptionRouter: []byte(serverIP),
dhcp4.OptionDomainNameServer: []byte(serverIP),
dhcp4.OptionNetworkTimeProtocolServers: []byte(serverIP),
dhcp4.OptionDomainName: []byte(*domain),
dhcp4.OptionDomainSearch: domainSearch,
}
handler, err := dhcp4d.NewHandler(permDir, ifc, *iface, nil, options)
handler, err := dhcp4d.NewHandler(permDir, ifc, *iface, nil)
if err != nil {
return nil, err
}
@ -318,10 +282,7 @@ func newSrv(permDir string) (*srv, error) {
http.Error(w, "missing hostname parameter", http.StatusBadRequest)
return
}
if err := handler.SetHostname(hwaddr, hostname); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
handler.SetHostname(hwaddr, hostname)
http.Redirect(w, r, "/", http.StatusFound)
})
@ -377,20 +338,17 @@ func newSrv(permDir string) (*srv, error) {
Vendor string
Expired bool
Static bool
Active bool
}
leasesMu.Lock()
defer leasesMu.Unlock()
static := make([]tmplLease, 0, len(leases))
dynamic := make([]tmplLease, 0, len(leases))
now := time.Now()
tl := func(l *dhcp4d.Lease) tmplLease {
return tmplLease{
Lease: *l,
Vendor: ouiDB.Lookup(l.HardwareAddr[:8]),
Expired: l.Expired(now),
Active: l.Active(now),
Expired: l.Expired(time.Now()),
Static: l.Expiry.IsZero(),
}
}
@ -441,51 +399,6 @@ func newSrv(permDir string) (*srv, error) {
if err := notify.Process(path.Join(path.Dir(os.Args[0]), "/dnsd"), syscall.SIGUSR1); err != nil {
log.Printf("notifying dnsd: %v", err)
}
// Publish the DHCP lease as JSON to MQTT, if configured:
leaseVal := struct {
Addr string `json:"addr"`
HardwareAddr string `json:"hardware_addr"`
Expiration time.Time `json:"expiration"`
Start time.Time `json:"start"`
VendorIdentifier string `json:"vendor_identifier"`
}{
Addr: latest.Addr.String(),
HardwareAddr: latest.HardwareAddr,
Expiration: latest.Expiry.In(time.UTC),
Start: latest.LastACK.In(time.UTC),
VendorIdentifier: latest.VendorIdentifier,
}
leaseJSON, err := json.Marshal(leaseVal)
if err != nil {
log.Fatal(err)
}
// MQTT requires valid UTF-8 and some brokers dont cope well with
// invalid UTF-8: https://github.com/fhmq/hmq/issues/104
identifier := strings.ToValidUTF8(latest.Hostname, "")
// Some MQTT clients (e.g. mosquitto_pub) dont cope well with topic
// names containing non-printable characters (see also
// https://twitter.com/zekjur/status/1347295676909158400):
identifier = strings.Map(func(r rune) rune {
if unicode.IsPrint(r) {
return r
}
return -1
}, identifier)
if identifier == "" {
identifier = latest.HardwareAddr
}
select {
case mayqtt <- PublishRequest{
Topic: "router7/dhcp4d/lease/" + identifier,
Retained: true,
Payload: leaseJSON,
}:
default:
// Channel not ready? skip publishing this lease (best-effort).
// This is an easy way of breaking circular dependencies between
// MQTT broker and DHCP server, and avoiding deadlocks.
}
}
conn, err := conn.NewUDP4BoundListener(*iface, ":67")
if err != nil {

View File

@ -1,70 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"path"
"strings"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
type PublishRequest struct {
Topic string
Qos byte
Retained bool
Payload interface{}
}
func publisherLoop(requests <-chan PublishRequest) error {
configFn := path.Join(*perm, "/dhcp4d/mqtt-broker.txt")
b, err := ioutil.ReadFile(configFn)
if err != nil {
// discard requests:
for range requests {
}
return nil
}
var (
broker string
username string
password string
)
cfg := strings.Split(string(b), "\n")
// e.g. tcp://10.0.0.54:1883, which is a static DHCP lease for the dr.lan
// Raspberry Pi, which is running an MQTT broker in my network.
broker = strings.TrimSpace(cfg[0])
if len(cfg) > 1 {
username = cfg[1]
}
if len(cfg) > 2 {
password = cfg[2]
}
log.Printf("Connecting to MQTT broker %q (configured in %s)", broker, configFn)
opts := mqtt.NewClientOptions().AddBroker(broker)
opts.SetUsername(username)
opts.SetPassword(password)
opts.SetClientID("dhcp4d")
opts.SetConnectRetry(true)
mqttClient := mqtt.NewClient(opts)
if token := mqttClient.Connect(); token.Wait() && token.Error() != nil {
return fmt.Errorf("MQTT connection failed: %v", token.Error())
}
for r := range requests {
// discard Token, MQTT publishing is best-effort
_ = mqttClient.Publish(r.Topic, r.Qos, r.Retained, r.Payload)
}
return nil
}
func MQTT() chan<- PublishRequest {
result := make(chan PublishRequest)
go func() {
if err := publisherLoop(result); err != nil {
log.Print(err)
}
}()
return result
}

View File

@ -28,7 +28,6 @@ import (
"time"
"github.com/google/renameio"
"github.com/jpillora/backoff"
"github.com/rtr7/router7/internal/dhcp6"
"github.com/rtr7/router7/internal/notify"
"github.com/rtr7/router7/internal/teelogger"
@ -58,21 +57,12 @@ func logic() error {
}
usr2 := make(chan os.Signal, 1)
signal.Notify(usr2, syscall.SIGUSR2)
backoff := backoff.Backoff{
Factor: 2,
Jitter: true,
Min: 10 * time.Second,
Max: 1 * time.Minute,
}
for c.ObtainOrRenew() {
if err := c.Err(); err != nil {
dur := backoff.Duration()
log.Printf("Temporary error: %v (waiting %v)", err, dur)
time.Sleep(dur)
log.Printf("Temporary error: %v", err)
time.Sleep(10 * time.Second)
continue
}
backoff.Reset()
log.Printf("lease: %+v", c.Config())
b, err := json.Marshal(c.Config())
if err != nil {

View File

@ -34,12 +34,12 @@ import (
"github.com/rtr7/router7/internal/diag"
"github.com/rtr7/router7/internal/multilisten"
_ "net/http/pprof"
)
var httpListeners = multilisten.NewPool()
var perm = flag.String("perm", "/perm", "path to replace /perm")
func updateListeners() error {
hosts, err := gokrazy.PrivateInterfaceAddrs()
if err != nil {
@ -80,53 +80,34 @@ func firstError(re *diag.EvalResult) string {
return ""
}
func graph(uplink string, ipv6 bool) *diag.Monitor {
const ip6allrouters = "ff02::2" // no /etc/hosts on gokrazy
graph := diag.Link(uplink).
func logic() error {
const (
uplink = "uplink0" /* enp0s31f6 */
ip6allrouters = "ff02::2" // no /etc/hosts on gokrazy
)
m := diag.NewMonitor(diag.Link(uplink).
Then(diag.DHCPv4().
Then(diag.Ping4Gateway().
Then(diag.TCP4("www.google.com:80"))))
if ipv6 {
graph = graph.
Then(diag.DHCPv6().
Then(diag.TCP6("lan0", "www.google.com:80"))).
Then(diag.RouterAdvertisments(uplink).
Then(diag.Ping6Gateway().
Then(diag.TCP6(uplink, "www.google.com:80")))).
Then(diag.Ping6("", ip6allrouters+"%"+uplink))
}
return diag.NewMonitor(graph)
}
func logic() error {
var (
ifname = flag.String("interface",
"uplink0",
"interface name to query")
ipv6 = flag.Bool("ipv6",
true,
"whether to expect IPv6 connectivity in health.json")
perm = flag.String("perm",
"/perm",
"path to replace /perm")
)
flag.Parse()
uplink := *ifname
diag.Perm = *perm
mHumanReadable := graph(uplink, true) // for display only
mJSON := graph(uplink, *ipv6) // for updates
Then(diag.Ping4("google.ch").
Then(diag.TCP4("www.google.ch:80"))))).
Then(diag.DHCPv6().
Then(diag.Ping6("lan0", "google.ch"))).
Then(diag.RouterAdvertisments(uplink).
Then(diag.Ping6Gateway().
Then(diag.Ping6(uplink, "google.ch").
Then(diag.TCP6("www.google.ch:80"))))).
Then(diag.Ping6("", ip6allrouters+"%"+uplink)))
var mu sync.Mutex
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
re := mHumanReadable.Evaluate()
re := m.Evaluate()
mu.Unlock()
fmt.Fprintf(w, `<!DOCTYPE html><style type="text/css">ul { list-style-type: none; }</style><ul>`)
dump(0, w, re)
})
http.HandleFunc("/health.json", func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
re := mJSON.Evaluate()
re := m.Evaluate()
mu.Unlock()
reply := struct {
FirstError string `json:"first_error"`
@ -154,6 +135,9 @@ func logic() error {
}
func main() {
flag.Parse()
diag.Perm = *perm
if err := logic(); err != nil {
log.Fatal(err)
}

View File

@ -39,21 +39,20 @@ import (
)
var (
httpListeners = multilisten.NewPool()
dnsUDPListeners = multilisten.NewPool()
dnsTCPListeners = multilisten.NewPool()
httpListeners = multilisten.NewPool()
dnsListeners = multilisten.NewPool()
perm = flag.String("perm", "/perm", "path to replace /perm")
domain = flag.String("domain", "lan", "domain name for your network")
)
func updateListeners(mux *miekgdns.ServeMux) error {
privateAddrs, err := gokrazy.PrivateInterfaceAddrs()
hosts, err := gokrazy.PrivateInterfaceAddrs()
if err != nil {
return err
}
dnsUDPListeners.ListenAndServe(privateAddrs, func(host string) multilisten.Listener {
dnsListeners.ListenAndServe(hosts, func(host string) multilisten.Listener {
return &listenerAdapter{&miekgdns.Server{
Addr: net.JoinHostPort(host, "53"),
Net: "udp",
@ -61,19 +60,11 @@ func updateListeners(mux *miekgdns.ServeMux) error {
}}
})
dnsTCPListeners.ListenAndServe(privateAddrs, func(host string) multilisten.Listener {
return &listenerAdapter{&miekgdns.Server{
Addr: net.JoinHostPort(host, "53"),
Net: "tcp",
Handler: mux,
}}
})
if net1, err := multilisten.IPv6Net1("/perm"); err == nil {
privateAddrs = append(privateAddrs, net1)
if net1, err := multilisten.IPv6Net1(*perm); err == nil {
hosts = append(hosts, net1)
}
httpListeners.ListenAndServe(privateAddrs, func(host string) multilisten.Listener {
httpListeners.ListenAndServe(hosts, func(host string) multilisten.Listener {
return &http.Server{Addr: net.JoinHostPort(host, "8053")}
})

View File

@ -47,8 +47,6 @@ var (
func init() {
var c nftables.Conn
filter4 := &nftables.Table{Family: nftables.TableFamilyIPv4, Name: "filter"}
filter6 := &nftables.Table{Family: nftables.TableFamilyIPv6, Name: "filter"}
for _, metric := range []struct {
name string
labels prometheus.Labels
@ -58,34 +56,18 @@ func init() {
{
name: "filter_forward",
labels: prometheus.Labels{"family": "ipv4"},
obj: &nftables.CounterObj{Table: filter4, Name: "fwded"},
obj: &nftables.CounterObj{
Table: &nftables.Table{Family: nftables.TableFamilyIPv4, Name: "filter"},
Name: "fwded",
},
},
{
name: "filter_forward",
labels: prometheus.Labels{"family": "ipv6"},
obj: &nftables.CounterObj{Table: filter6, Name: "fwded"},
},
{
name: "filter_input",
labels: prometheus.Labels{"family": "ipv4"},
obj: &nftables.CounterObj{Table: filter4, Name: "inputc"},
},
{
name: "filter_input",
labels: prometheus.Labels{"family": "ipv6"},
obj: &nftables.CounterObj{Table: filter6, Name: "inputc"},
},
{
name: "filter_output",
labels: prometheus.Labels{"family": "ipv4"},
obj: &nftables.CounterObj{Table: filter4, Name: "outputc"},
},
{
name: "filter_output",
labels: prometheus.Labels{"family": "ipv6"},
obj: &nftables.CounterObj{Table: filter6, Name: "outputc"},
obj: &nftables.CounterObj{
Table: &nftables.Table{Family: nftables.TableFamilyIPv6, Name: "filter"},
Name: "fwded",
},
},
} {
metric := metric // copy
@ -93,11 +75,12 @@ func init() {
updateCounter := func() {
mu.Lock()
defer mu.Unlock()
obj, err := c.ResetObject(metric.obj)
if err != nil {
objs, err := c.GetObjReset(metric.obj)
if err != nil ||
len(objs) != 1 {
return
}
if co, ok := obj.(*nftables.CounterObj); ok {
if co, ok := objs[0].(*nftables.CounterObj); ok {
metric.packets += co.Packets
metric.bytes += co.Bytes
}

View File

@ -1 +0,0 @@
router7.org

View File

@ -1,221 +0,0 @@
<!DOCTYPE html>
<html> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/bootstrap-4.4.1.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://router7.org/sass/sidebar.css">
<title>router7: architecture</title>
</head>
<body>
<div id="content">
<div class="container">
<div class="row">
<div class="col-md-10"><nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">router7</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav ml-auto">
<a class="nav-item nav-link " href="/">Home </a>
<a class="nav-item nav-link active" href="/architecture/">Architecture <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link " href="/installation/">Installation </a>
<a class="nav-item nav-link " href="https://github.com/rtr7/router7">GitHub </a>
</div>
</div>
</nav>
<h1 id="architecture">Architecture</h1>
<p>router7 is based on <a href="https://gokrazy.org/">gokrazy</a>: it is an appliance which gets packed into a hard disk image, containing a FAT partition with the kernel, a read-only SquashFS partition for the root file system and an ext4 partition for permanent data.</p>
<p>The individual services can be found in <a href="https://pkg.go.dev/github.com/rtr7/router7/cmd">github.com/rtr7/router7/cmd</a></p>
<ul>
<li>Each service runs in a separate process.</li>
<li>Services communicate with each other by persisting state files. E.g., <code>cmd/dhcp4</code> writes <code>/perm/dhcp4/wire/lease.json</code>.</li>
<li>A service notifies other services about state changes by sending them signal <code>SIGUSR1</code>.</li>
</ul>
<h2 id="configuration-files">Configuration files</h2>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>File</th>
<th>Consumer(s)</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/perm/interfaces.json</code></td>
<td><code>netconfigd</code></td>
<td>Set IP/MAC addresses of <code>uplink0</code> and <code>lan0</code></td>
</tr>
<tr>
<td><code>/perm/portforwardings.json</code></td>
<td><code>netconfigd</code></td>
<td>Configure nftables port forwarding rules</td>
</tr>
<tr>
<td><code>/perm/dhcp6/duid</code></td>
<td><code>dhcp6</code></td>
<td>Set DHCP Unique Identifier (DUID) for obtaining static leases</td>
</tr>
</tbody>
</table>
<h2 id="state-files">State files</h2>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>File</th>
<th>Producer</th>
<th>Consumer(s)</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/perm/dhcp4/wire/ack</code></td>
<td><code>dhcp4</code></td>
<td><code>dhcp4</code></td>
<td>last DHCPACK packet for renewals across restarts</td>
</tr>
<tr>
<td><code>/perm/dhcp4/wire/lease.json</code></td>
<td><code>dhcp4</code></td>
<td><code>netconfigd</code></td>
<td>Obtained DHCPv4 lease</td>
</tr>
<tr>
<td><code>/perm/dhcp6/wire/lease.json</code></td>
<td><code>dhcp6</code></td>
<td><code>netconfigd</code>, <code>radvd</code></td>
<td>Obtained DHCPv6 lease</td>
</tr>
<tr>
<td><code>/perm/dhcp4d/leases.json</code></td>
<td><code>dhcp4d</code></td>
<td><code>dhcp4d</code>, <code>dnsd</code></td>
<td>DHCPv4 leases handed out (including hostnames)</td>
</tr>
</tbody>
</table>
<h2 id="available-ports">Available ports</h2>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Port</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>&lt;public&gt;:8053</code></td>
<td><code>dnsd</code> metrics (forwarded requests)</td>
</tr>
<tr>
<td><code>&lt;public&gt;:8066</code></td>
<td><code>netconfigd</code> metrics (nftables counters)</td>
</tr>
<tr>
<td><code>&lt;private&gt;:80</code></td>
<td>gokrazy web interface</td>
</tr>
<tr>
<td><code>&lt;private&gt;:67</code></td>
<td><code>dhcp4d</code></td>
</tr>
<tr>
<td><code>&lt;private&gt;:58</code></td>
<td><code>radvd</code></td>
</tr>
<tr>
<td><code>&lt;private&gt;:53</code></td>
<td><code>dnsd</code></td>
</tr>
<tr>
<td><code>&lt;private&gt;:8077</code></td>
<td><code>backupd</code> (serve backup.tar.gz)</td>
</tr>
<tr>
<td><code>&lt;private&gt;:7733</code></td>
<td><code>diagd</code> (perform diagnostics)</td>
</tr>
<tr>
<td><code>&lt;private&gt;:5022</code></td>
<td><code>captured</code> (serve captured packets)</td>
</tr>
</tbody>
</table>
<p>Heres an example of <code>cmd/diagd</code> output:</p>
<p><img src="https://github.com/rtr7/router7/raw/master/2018-07-14-diagd.png"
width="800" alt="diagd output"></p>
<p>Heres an example of <code>cmd/netconfigd</code> metrics when scraped with <a href="https://prometheus.io/">Prometheus</a> and displayed in <a href="https://grafana.com/">Grafana</a>:</p>
<p><img src="https://github.com/rtr7/router7/raw/master/2018-07-14-grafana.png"
width="800" alt="metrics in grafana"></p>
<hr>
<p class="small">
© 2018 Michael Stapelberg and contributors
</p>
</div>
<div class="col-md-2">
<aside class="bd-toc">
<nav id="TableOfContents">
<ul>
<li><a href="#configuration-files">Configuration files</a></li>
<li><a href="#state-files">State files</a></li>
<li><a href="#available-ports">Available ports</a></li>
</ul>
</nav>
</aside>
</div>
</div>
</div>
</div>
<script src="/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="/popper-1.16.0.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="/bootstrap-4.4.1.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,94 +0,0 @@
<!DOCTYPE html>
<html> <head>
<meta name="generator" content="Hugo 0.106.0-DEV">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/bootstrap-4.4.1.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://router7.org/sass/sidebar.css">
<title>router7: a small home internet router completely written in Go</title>
</head>
<body>
<div id="content">
<div class="container">
<div class="row">
<div class="col-md-10"><nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">router7</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav ml-auto">
<a class="nav-item nav-link active" href="/">Home <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link " href="/architecture/">Architecture </a>
<a class="nav-item nav-link " href="/installation/">Installation </a>
<a class="nav-item nav-link " href="https://github.com/rtr7/router7">GitHub </a>
</div>
</div>
</nav>
<h1 id="router7">router7</h1>
<p>router7 is a pure-Go implementation of a small home internet router. It comes with all the services required to make a <a href="https://www.init7.net/en/internet/fiber7/">fiber7 internet connection</a> work (DHCPv4, DHCPv6, DNS, etc.).</p>
<p>Note that this project should be considered a (working!) tech demo. Feature requests will likely not be implemented, and see <a href="https://github.com/rtr7/router7/blob/master/CONTRIBUTING.md">CONTRIBUTING.md</a> for details about which contributions are welcome.</p>
<h2 id="motivation">Motivation</h2>
<p>Before starting router7, I was using the <a href="https://omnia.turris.cz/en/">Turris Omnia</a> router running OpenWrt. That worked fine up until May 2018, when an automated update pulled in a new version of <a href="https://git.openwrt.org/?p=project/odhcp6c.git;a=shortlog">odhcp6c</a>, OpenWrts DHCPv6 client. That version is incompatible with fiber7s DHCP server setup (I think there are shortcomings on both sides).</p>
<p>It was not only quicker to develop my own router than to wait for either side to resolve the issue, but it was also a lot of fun and allowed me to really tailor my router to my needs, experimenting with a bunch of interesting ideas I had.</p>
<h2 id="project-goals">Project goals</h2>
<ul>
<li>Maximize internet connectivity: retain the most recent DHCP configuration across reboots and even after its expiration (chances are the DHCP server will be back before the configuration stops working).</li>
<li>Unit/integration tests use fiber7 packet capture files to minimize the chance of software changes breaking my connectivity.</li>
<li>Safe and quick updates
<ul>
<li>Auto-rollback of updates which result in loss of connectivity: the diagnostics daemon assesses connectivity state, the update tool reads it and rolls back faulty updates.</li>
<li>Thanks to kexec, updates translate into merely 13s of internet connectivity loss.</li>
</ul>
</li>
<li>Easy debugging
<ul>
<li>Configuration-related network packets (e.g. DHCP, IPv6 neighbor/router advertisements) are stored in a ring buffer which can be streamed into <a href="https://www.wireshark.org/">Wireshark</a>, allowing for live and retro-active debugging.</li>
<li>The diagnostics daemon performs common diagnostic steps (ping, traceroute, …) for you.</li>
<li>All state in the system is stored as human-readable JSON within the <code>/perm</code> partition and can be modified.</li>
</ul>
</li>
</ul>
<h2 id="hardware">Hardware</h2>
<p>The reference hardware platform is the <a href="https://pcengines.ch/apu2c4.htm">PC Engines™ apu2c4</a> system board. It features a 1 GHz quad core amd64 CPU, 4 GB of RAM, 3 Ethernet ports and a DB9 serial port. It conveniently supports PXE boot, the schematics and bootloader sources are available. I recommend the <a href="https://pcengines.ch/msata16g.htm">msata16g</a> SSD module for reliable persistent storage and the <a href="https://pcengines.ch/usbcom1a.htm">usbcom1a</a> serial adapter if you dont have one already.</p>
<p>Other hardware might work, too, but is not tested.</p>
<hr>
<p class="small">
© 2018 Michael Stapelberg and contributors
</p>
</div>
<div class="col-md-2">
</div>
</div>
</div>
</div>
<script src="/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="/popper-1.16.0.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="/bootstrap-4.4.1.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>
</body>
</html>

View File

@ -1,212 +0,0 @@
<!DOCTYPE html>
<html> <head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/bootstrap-4.4.1.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://router7.org/sass/sidebar.css">
<title>router7: installation</title>
</head>
<body>
<div id="content">
<div class="container">
<div class="row">
<div class="col-md-10"><nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">router7</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav ml-auto">
<a class="nav-item nav-link " href="/">Home </a>
<a class="nav-item nav-link " href="/architecture/">Architecture </a>
<a class="nav-item nav-link active" href="/installation/">Installation <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link " href="https://github.com/rtr7/router7">GitHub </a>
</div>
</div>
</nav>
<h1 id="installation">Installation</h1>
<p>Connect your serial adapter (<a href="https://pcengines.ch/usbcom1a.htm">usbcom1a</a> works well if you dont have one already) to the apu2c4 and start a program to use it, e.g. <code>screen /dev/ttyUSB0 115200</code>. Then, power on the apu2c4 and configure it to do PXE boot:</p>
<ul>
<li>Press <code>F10</code> to enter the boot menu</li>
<li>Press <code>3</code> to enter setup</li>
<li>Press <code>n</code> to enable network boot</li>
<li>Press <code>c</code> to move mSATA to the top of the boot order</li>
<li>Press <code>e</code> to move iPXE to the top of the boot order</li>
<li>Press <code>s</code> to save configuration and exit</li>
</ul>
<p>Connect a network cable on <code>net0</code>, the port closest to the serial console port:</p>
<p><img src="https://raw.githubusercontent.com/rtr7/router7/master/devsetup.jpg"
width="800" alt="router7 development setup"></p>
<p>Next, create a router7 gokrazy instance (see <a href="https://gokrazy.org/quickstart/">gokrazy
quickstart</a> if youre unfamiliar with gokrazy):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>go install github.com/gokrazy/tools/cmd/gok@main
</span></span><span style="display:flex;"><span>go install github.com/rtr7/tools/cmd/...@latest
</span></span><span style="display:flex;"><span>mkdir /tmp/recovery
</span></span><span style="display:flex;"><span>gok -i router7 new
</span></span><span style="display:flex;"><span>gok -i router7 edit
</span></span></code></pre></div><p>Change the config until you have the following fields set:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Hostname&#34;</span>: <span style="color:#e6db74">&#34;router7&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;Packages&#34;</span>: [
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/gokrazy/fbstatus&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/gokrazy/hello&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/gokrazy/serial-busybox&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/gokrazy/breakglass&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/rtr7/router7/cmd/...&#34;</span>
</span></span><span style="display:flex;"><span> ],
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;SerialConsole&#34;</span>: <span style="color:#e6db74">&#34;ttyS0,115200&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;GokrazyPackages&#34;</span>: [
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/gokrazy/gokrazy/cmd/ntp&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;github.com/gokrazy/gokrazy/cmd/randomd&#34;</span>
</span></span><span style="display:flex;"><span> ],
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;KernelPackage&#34;</span>: <span style="color:#e6db74">&#34;github.com/rtr7/kernel&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;FirmwarePackage&#34;</span>: <span style="color:#e6db74">&#34;github.com/rtr7/kernel&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;EEPROMPackage&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then, build an image:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>GOARCH<span style="color:#f92672">=</span>amd64 gok -i router7 overwrite <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --boot /tmp/recovery/boot.img <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --mbr /tmp/recovery/mbr.img <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --root /tmp/recovery/root.img
</span></span></code></pre></div><p>And serve the image for netboot installation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>rtr7-recover <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --boot /tmp/recovery/boot.img <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --mbr /tmp/recovery/mbr.img <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> --root /tmp/recovery/root.img
</span></span></code></pre></div><p>Specifically, <code>rtr7-recover</code>:</p>
<ul>
<li>trigger a reset <a href="#rebootor">if a Teensy with the rebootor firmware is attached</a></li>
<li>serve a DHCP lease to all clients which request PXE boot (i.e., your apu2c4)</li>
<li>serve via TFTP:
<ul>
<li>the PXELINUX bootloader</li>
<li>the router7 kernel</li>
<li>an initrd archive containing the rtr7-recovery-init program and mke2fs</li>
</ul>
</li>
<li>serve via HTTP the boot and root images</li>
<li>optionally serve via HTTP a backup.tar.gz image containing files for <code>/perm</code> (e.g. for moving to new hardware, rolling back corrupted state, or recovering from a disk failure)</li>
<li>exit once the router successfully wrote the images to disk</li>
</ul>
<h2 id="configuration">Configuration</h2>
<h3 id="interfaces">Interfaces</h3>
<p>The <code>/perm/interfaces.json</code> configuration file will be <a href="https://github.com/rtr7/tools/blob/57c2cdc3b629d2fbd13564ae37f6282f6ee8427f/cmd/rtr7-recovery-init/recoveryinit.go#L320">automatically created</a> if it is not present when you run the first recovery.</p>
<p>Example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;interfaces&#34;</span>: [
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;hardware_addr&#34;</span>: <span style="color:#e6db74">&#34;12:34:56:78:9a:b0&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;lan0&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;addr&#34;</span>: <span style="color:#e6db74">&#34;192.168.0.1/24&#34;</span>
</span></span><span style="display:flex;"><span> },
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;hardware_addr&#34;</span>: <span style="color:#e6db74">&#34;12:34:56:78:9a:b2&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;uplink0&#34;</span>
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Schema: see <a href="https://github.com/rtr7/router7/blob/f86e20be5305fc0e7e77421e0f2abde98a84f2a7/internal/netconfig/netconfig.go#L183"><code>InterfaceConfig</code></a></p>
<h3 id="port-forwarding">Port Forwarding</h3>
<p>The <code>/perm/portforwardings.json</code> configuration file can be created to define port forwarding rules.</p>
<p>Example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;forwardings&#34;</span>: [
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;proto&#34;</span>: <span style="color:#e6db74">&#34;tcp&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#e6db74">&#34;22&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;dest_addr&#34;</span>: <span style="color:#e6db74">&#34;10.0.0.10&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;dest_port&#34;</span>: <span style="color:#e6db74">&#34;22&#34;</span>
</span></span><span style="display:flex;"><span> },
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;proto&#34;</span>: <span style="color:#e6db74">&#34;tcp&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;port&#34;</span>: <span style="color:#e6db74">&#34;80&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;dest_addr&#34;</span>: <span style="color:#e6db74">&#34;10.0.0.10&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;dest_port&#34;</span>: <span style="color:#e6db74">&#34;80&#34;</span>
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Schema: see <a href="https://github.com/rtr7/router7/blob/f86e20be5305fc0e7e77421e0f2abde98a84f2a7/internal/netconfig/netconfig.go#L431"><code>portForwardings</code></a></p>
<h2 id="updates">Updates</h2>
<p>Run e.g. <code>rtr7-safe-update -updates_dir=$HOME/router7/updates</code> to:</p>
<ul>
<li>verify the router currently has connectivity, abort the update otherwise</li>
<li>download a backup archive of <code>/perm</code></li>
<li>build a new image</li>
<li>update the router</li>
<li>wait until the router restored connectivity, roll back the update using <code>rtr7-recover</code> otherwise</li>
</ul>
<p>The update step uses kexec to reduce the downtime to approximately 15 seconds.</p>
<h2 id="manual-recovery">Manual Recovery</h2>
<p>Given <code>rtr7-safe-update</code>s safeguards, manual recovery should rarely be required.</p>
<p>To manually roll back to an older image, invoke <code>rtr7-safe-update</code> via the
<code>recover.bash</code> script in the image directory underneath <code>-updates_dir</code>, e.g.:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>% cd ~/router7/updates/2018-07-03T17:33:52+02:00
</span></span><span style="display:flex;"><span>% ./recover.bash
</span></span></code></pre></div><h2 id="rebootor">Teensy rebootor</h2>
<p>The cheap and widely-available <a href="https://www.pjrc.com/store/teensypp.html">Teensy++ USB development board</a> comes with a firmware called rebootor, which is used by the <a href="https://www.pjrc.com/teensy/loader_cli.html"><code>teensy_loader_cli</code></a> program to perform hard resets.</p>
<p>This setup can be used to programmatically reset the apu2c4 (from <code>rtr7-recover</code>) by connecting the Teensy++ to the <a href="http://pcengines.ch/pdf/apu2.pdf">apu2c4s reset pins</a>:</p>
<ul>
<li>connect the Teensy++s <code>GND</code> pin to the apu2c4 J2s pin 4 (<code>GND</code>)</li>
<li>connect the Teensy++s <code>B7</code> pin to the apu2c4 J2s pin 5 (<code>3.3V</code>, resets when pulled to <code>GND</code>)</li>
</ul>
<p>You can find a working rebootor firmware .hex file at <a href="https://github.com/PaulStoffregen/teensy_loader_cli/issues/38">https://github.com/PaulStoffregen/teensy_loader_cli/issues/38</a></p>
<h2 id="prometheus">Prometheus</h2>
<p>See <a href="https://github.com/rtr7/router7/tree/master/contrib/prometheus">https://github.com/rtr7/router7/tree/master/contrib/prometheus</a> for example
configuration files, and install the <a href="https://grafana.com/dashboards/8288">router7 Grafana
Dashboard</a>.</p>
<hr>
<p class="small">
© 2018 Michael Stapelberg and contributors
</p>
</div>
<div class="col-md-2">
<aside class="bd-toc">
<nav id="TableOfContents">
<ul>
<li><a href="#configuration">Configuration</a>
<ul>
<li><a href="#interfaces">Interfaces</a></li>
<li><a href="#port-forwarding">Port Forwarding</a></li>
</ul>
</li>
<li><a href="#updates">Updates</a></li>
<li><a href="#manual-recovery">Manual Recovery</a></li>
<li><a href="#rebootor">Teensy rebootor</a></li>
<li><a href="#prometheus">Prometheus</a></li>
</ul>
</nav>
</aside>
</div>
</div>
</div>
</div>
<script src="/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="/popper-1.16.0.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="/bootstrap-4.4.1.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
User-Agent: *
sitemap: https://router7.org/sitemap.xml

View File

@ -1,19 +0,0 @@
.bd-toc {
position: sticky;
top: 4rem;
height: calc(100vh - 4rem);
overflow-y: auto; }
.bd-toc ul {
list-style: none;
padding-left: 1em;
border-left: 1px solid #eee; }
.bd-toc li {
margin-top: 1em;
margin-bottom: 1em; }
/* TODO: move this to a separate style sheet */
.bigbutton {
margin-left: 1em;
margin-right: 1em; }

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://router7.org/</loc>
</url><url>
<loc>https://router7.org/architecture/</loc>
</url><url>
<loc>https://router7.org/installation/</loc>
</url>
</urlset>

86
go.mod
View File

@ -1,63 +1,39 @@
module github.com/rtr7/router7
go 1.21
toolchain go1.22.2
go 1.13
require (
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
github.com/digineo/go-ping v1.0.1
github.com/eclipse/paho.mqtt.golang v1.4.1
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83
github.com/google/go-cmp v0.6.0
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c
github.com/google/renameio v1.0.1
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
github.com/digineo/go-ping v1.0.0
github.com/gokrazy/gokrazy v0.0.0-20200501080617-f3445e01a904
github.com/gokrazy/internal v0.0.0-20200407080221-9da902858268 // indirect
github.com/golang/protobuf v1.4.1 // indirect
github.com/google/go-cmp v0.4.0
github.com/google/gopacket v1.1.17
github.com/google/nftables v0.0.0-20200316075819-7127d9d22474
github.com/google/renameio v0.1.0
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7
github.com/jpillora/backoff v1.0.0
github.com/kr/text v0.2.0 // indirect
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771
github.com/libdns/cloudflare v0.1.0
github.com/libdns/libdns v0.2.1
github.com/mdlayher/ethtool v0.1.0
github.com/mdlayher/ndp v0.10.0
github.com/mdlayher/packet v1.1.2
github.com/miekg/dns v1.1.50
github.com/prometheus/client_golang v1.19.0
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/vishvananda/netns v0.0.4
golang.org/x/crypto v0.31.0
golang.org/x/net v0.23.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.28.0
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 // indirect
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0 // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/kenshaw/evdev v0.1.0 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.14.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/tools v0.1.8 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/libdns/cloudflare v0.0.0-20200528144945-97886e7873b1
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821
github.com/mdlayher/ndp v0.0.0-20200509194142-8a50b5ef8b52
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065
github.com/miekg/dns v1.1.29
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/prometheus/client_golang v1.6.0
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5
github.com/sergi/go-diff v1.1.0 // indirect
github.com/u-root/u-root v6.0.0+incompatible // indirect
github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
golang.zx2c4.com/wireguard v0.0.20200320 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)

356
go.sum
View File

@ -1,221 +1,265 @@
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs=
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts=
github.com/digineo/go-ping v1.0.1 h1:Yn9hwM0RY4j4D3gcmLvRJf0d7MrbucfUhnOeVDvcVyk=
github.com/digineo/go-ping v1.0.1/go.mod h1:uCbFC0VUqGNBNiev44BGSxfOrEAmC73GjpRje1l40Zo=
github.com/eclipse/paho.mqtt.golang v1.4.1 h1:tUSpviiL5G3P9SZZJPC4ZULZJsxQKXxfENpMvdbAXAI=
github.com/eclipse/paho.mqtt.golang v1.4.1/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA=
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/digineo/go-ping v1.0.0 h1:gOuD3YzkIcW/0Y2IAe27bsMKtpfNZdoX1Rnc1RGYOSI=
github.com/digineo/go-ping v1.0.0/go.mod h1:YLDBnHoAygacawa2aubI4vXhZ4do5f62oJSvRiJVEjw=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83 h1:Y4sADvUYd/c0eqnqebipHHl0GMpAxOQeTzPnwI4ievM=
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83/go.mod h1:9q5Tg+q+YvRjC3VG0gfMFut46dhbhtAnvUEp4lPjc6c=
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0 h1:QTi0skQ/OM7he/5jEWA9k/DYgdwGAhw3hrUoiPGGZHM=
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0/go.mod h1:ddHcxXZ/VVQOSAWcRBbkYY58+QOw4L145ye6phyDmRA=
github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gokrazy/gokrazy v0.0.0-20200501080617-f3445e01a904 h1:eqfH4A/LLgxv5RvqEXwVoFvfmpRa8+TokRjB5g6xBkk=
github.com/gokrazy/gokrazy v0.0.0-20200501080617-f3445e01a904/go.mod h1:pq6rGHqxMRPSaTXaCMzIZy0wLDusAJyoVNyNo05RLs0=
github.com/gokrazy/internal v0.0.0-20200407075822-660ad467b7c9/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
github.com/gokrazy/internal v0.0.0-20200407080221-9da902858268 h1:Q0Z5vi1HjXMlwiIaC6nn04y0PwRjyG9h9S4hZVzFjTw=
github.com/gokrazy/internal v0.0.0-20200407080221-9da902858268/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c h1:XJHEjE/d9/F9Sp6hvRCfh6Sl4WtCoKx7JJI2z1trH/Y=
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c/go.mod h1:Fo/xFnOxWlRQtnHdNi46KbIjufTDzbKhtghpWrmsSUg=
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84 h1:MJTy6H+EpXLeAn0P5WAWeLk6dJA3V0ik6S3VJfUyQuI=
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.16/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw=
github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/google/nftables v0.0.0-20200316075819-7127d9d22474 h1:D6bN82zzK92ywYsE+Zjca7EHZCRZbcNTU3At7WdxQ+c=
github.com/google/nftables v0.0.0-20200316075819-7127d9d22474/go.mod h1:cfspEyr/Ap+JDIITA+N9a0ernqG0qZ4W1aqMRgDZa1g=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 h1:iaCm+9nZdYb8XCSU2TfIb0qYTcAlIv2XzyKR2d2xZ38=
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kenshaw/evdev v0.1.0 h1:wmtceEOFfilChgdNT+c/djPJ2JineVsQ0N14kGzFRUo=
github.com/kenshaw/evdev v0.1.0/go.mod h1:B/fErKCihUyEobz0mjn2qQbHgyJKFQAxkXSvkeeA/Wo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d h1:MFX8DxRnKMY/2M3H61iSsVbo/n3h0MWGmWNN1UViOU0=
github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d/go.mod h1:QHb4k4cr1fQikUahfcRVPcEXiUgFsdIstGqlurL0XL4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk=
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic=
github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/ethtool v0.1.0 h1:XAWHsmKhyPOo42qq/yTPb0eFBGUKKTR1rE0dVrWVQ0Y=
github.com/mdlayher/ethtool v0.1.0/go.mod h1:fBMLn2UhfRGtcH5ZFjr+6GUiHEjZsItFD7fSn7jbZVQ=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/ndp v0.10.0 h1:Zdwol2bq1EHY8xSnejIYkq6LEj7dLjLymJX0o/2tjGw=
github.com/mdlayher/ndp v0.10.0/go.mod h1:Uv6IWvgvqirNUu2N3ZXJEB86xu6foyUsG0NrClSSfek=
github.com/libdns/cloudflare v0.0.0-20200528144945-97886e7873b1 h1:Jx0AoxHtj2NMwxHByM8VmcqvGMa3lEu28xVDArhSi7E=
github.com/libdns/cloudflare v0.0.0-20200528144945-97886e7873b1/go.mod h1:A9MqNmkZcd81mY7JsNysmgmj5O9vlRjfDVaNw4j9pjU=
github.com/libdns/libdns v0.0.0-20200430163404-ee2c42449104/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821 h1:663opx/RKxiISi1ozf0WbvweQpYBgf34dx8hKSIau3w=
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/ndp v0.0.0-20200509194142-8a50b5ef8b52 h1:qWqNvHaKhGECNieU1gGusKRuoPeoR+rhlkaWdO1gyT8=
github.com/mdlayher/ndp v0.0.0-20200509194142-8a50b5ef8b52/go.mod h1:AXE3T2f7eg/MV02LS+DGHgH0c+ehknWViE4pgbHtZf8=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v0.0.0-20191009155606-de872b0d824b/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af/go.mod h1:rC/yE65s/DoHB6BzVOUBNYBGTg772JVytyAytffIZkY=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5 h1:80FAK3TW5lVymfHu3kvB1QvTZvy9Kmx1lx6sT5Ep16s=
github.com/mdlayher/watchdog v0.0.0-20221003142519-49be0df7b3b5/go.mod h1:z0QjVpjpK4jksEkffQwS3+abQ3XFTm1bnimyDzWyUk0=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
github.com/rivo/tview v0.0.0-20201204190810-5406288b8e4e/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46 h1:3psQveH4RUiv5yc3p7kRySilf1nSXLQhAvJFwg4fgnE=
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46/go.mod h1:Ng1F/s+z0zCMsbEFEneh+30LJa9DrTfmA+REbEqcTPk=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A=
github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rivo/tview v0.0.0-20181226202439-36893a669792/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5 h1:/kzTBQ20DbbhSNaBXiFEk2gPrGhY26kajwC1ro/Vlh8=
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5/go.mod h1:FwstIpm6vX98QgtR8KEwZcVjiRn2WP76LjXAHj84fK0=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA=
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE=
github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI=
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0=
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b h1:9JncmKXcUwE918my+H6xmjBdhK2jM/UTUNXxhRG1BAk=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b/go.mod h1:yp4gl6zOlnDGOZeWeDfMwQcsdOIQnMdhuPx9mwwWBL4=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
golang.zx2c4.com/wireguard v0.0.20200320 h1:1vE6zVeO7fix9cJX1Z9ZQ+ikPIIx7vIyU0o0tLDD88g=
golang.zx2c4.com/wireguard v0.0.20200320/go.mod h1:lDian4Sw4poJ04SgHh35nzMVwGSYlPumkdnHcucAQoY=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf h1:rWUZHukj3poXegPQMZOXgxjTGIBe3mLNHNVvL5DsHus=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf/go.mod h1:UdS9frhv65KTfwxME1xE8+rHYoFpbm36gOud1GhBe9c=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

51
init/init.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"flag"
"fmt"
"log"
"os/exec"
"path"
"github.com/gokrazy/gokrazy"
)
// buildTimestamp can be overridden by specifying e.g.
// -ldflags "-X main.buildTimestamp=foo" when building.
var (
buildTimestamp = "2020-06-08T19:45:52-07:00"
domain string
cmdRoot string
perm string
noFirewall bool
)
func main() {
flag.StringVar(&cmdRoot, "cmdroot", "/usr/bin", "path to rtr7 binaries")
flag.StringVar(&domain, "domain", "lan", "domain name for your network")
flag.StringVar(&perm, "perm", "/var/lib/rtr7/", "path to replace /perm")
flag.BoolVar(&noFirewall, "nofirewall", false, "disable the rtr7 firewall")
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lshortfile)
fmt.Printf("gokrazy build timestamp %s\n", buildTimestamp)
cmds := []*exec.Cmd{
// exec.Command(path.Join(cmdRoot, "/ntp")),
exec.Command(path.Join(cmdRoot, "backupd"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "captured"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "dhcp4"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "dhcp4d"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "dhcp6"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "diagd"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "dnsd"), fmt.Sprintf("-domain=%s", domain), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "dyndns"), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "netconfigd"), fmt.Sprintf("-nofirewall=%t", noFirewall), "-perm="+perm),
exec.Command(path.Join(cmdRoot, "radvd"), "-perm="+perm),
}
if err := gokrazy.Supervise(cmds); err != nil {
log.Fatal(err)
}
select {}
}

View File

@ -28,7 +28,6 @@ import (
"github.com/rtr7/router7/internal/netconfig"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
"github.com/andreyvit/diff"
"github.com/google/go-cmp/cmp"
@ -46,21 +45,11 @@ const goldenInterfaces = `
"hardware_addr": "02:73:53:00:b0:0c",
"spoof_hardware_addr": "02:73:53:00:b0:aa",
"name": "lan0",
"addr": "192.168.42.1/24",
"mtu": 1492
"addr": "192.168.42.1/24"
},
{
"name": "wg0",
"addr": "fe80::1/64",
"extra_addrs": [
"10.22.100.1/24"
],
"extra_routes": [
{
"destination": "2a02:168:4a00:22::/64",
"gateway": "fe80::2"
}
]
"addr": "fe80::1/64"
}
]
}
@ -148,24 +137,19 @@ func goldenNftablesRules(additionalForwarding bool) string {
add := ""
if additionalForwarding {
add = `
ip daddr != 127.0.0.0/8 ip daddr != 192.168.42.0/24 fib daddr type 2 tcp dport 8045 dnat to 192.168.42.22:8045`
iifname "uplink0" tcp dport 8045 dnat to 192.168.42.22:8045`
}
return `table ip nat {
chain router7-portforwardings {
ip daddr != 127.0.0.0/8 ip daddr != 192.168.42.0/24 fib daddr type 2 tcp dport 8080 dnat to 192.168.42.23:9999` + add + `
ip daddr != 127.0.0.0/8 ip daddr != 192.168.42.0/24 fib daddr type 2 tcp dport 8040-8060 dnat to 192.168.42.99:8040-8060
ip daddr != 127.0.0.0/8 ip daddr != 192.168.42.0/24 fib daddr type 2 udp dport 53 dnat to 192.168.42.99:53
}
chain prerouting {
type nat hook prerouting priority 0; policy accept;
jump router7-portforwardings
iifname "uplink0" tcp dport 8080 dnat to 192.168.42.23:9999` + add + `
iifname "uplink0" tcp dport 8040-8060 dnat to 192.168.42.99:8040-8060
iifname "uplink0" udp dport 53 dnat to 192.168.42.99:53
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
oifname "uplink0" masquerade
iifname "lan0" oifname "lan0" ct status 0x20 masquerade
}
}
table ip filter {
@ -173,58 +157,22 @@ table ip filter {
packets 23 bytes 42
}
counter inputc {
packets 23 bytes 42
}
counter outputc {
packets 23 bytes 42
}
chain forward {
type filter hook forward priority 0; policy accept;
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
counter name "fwded"
}
chain input {
type filter hook input priority 0; policy accept;
counter name "inputc"
}
chain output {
type filter hook output priority 0; policy accept;
counter name "outputc"
}
}
table ip6 filter {
counter fwded {
packets 23 bytes 42
}
counter inputc {
packets 23 bytes 42
}
counter outputc {
packets 23 bytes 42
}
chain forward {
type filter hook forward priority 0; policy accept;
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
counter name "fwded"
}
chain input {
type filter hook input priority 0; policy accept;
counter name "inputc"
}
chain output {
type filter hook output priority 0; policy accept;
counter name "outputc"
}
}`
}
@ -254,27 +202,17 @@ const goldenDhcp6 = `
}
`
type wgLink struct {
ns int
}
type wgLink struct{}
func (w *wgLink) Type() string { return "wireguard" }
func (w *wgLink) Attrs() *netlink.LinkAttrs {
attrs := netlink.NewLinkAttrs()
attrs.Name = "wg5"
if w.ns > 0 {
attrs.Namespace = netlink.NsFd(w.ns)
}
return &attrs
}
var wireGuardAvailable = func() bool {
// The wg tool must also be available for our test to succeed:
if _, err := exec.LookPath("wg"); err != nil {
return false
}
// ns must not collide with any namespace used in the test functions: this
// function will be called by the helper process, too.
const ns = "ns4"
@ -285,18 +223,7 @@ var wireGuardAvailable = func() bool {
}
defer exec.Command("ip", "netns", "delete", ns).Run()
nsHandle, err := netns.GetFromName(ns)
if err != nil {
log.Printf("GetFromName: %v", err)
return false
}
if err := netlink.LinkAdd(&wgLink{ns: int(nsHandle)}); err != nil {
log.Printf("netlink.LinkAdd: %v", err)
return false
}
return true
return netlink.LinkAdd(&wgLink{}) == nil
}()
func TestNetconfig(t *testing.T) {
@ -338,7 +265,7 @@ func TestNetconfig(t *testing.T) {
}
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42}
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root"), true); err != nil {
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
t.Fatalf("netconfig.Apply: %v", err)
}
@ -346,7 +273,7 @@ func TestNetconfig(t *testing.T) {
// already-configured interfaces, addresses, routes, … (and ensure
// nftables rules are replaced, not appendend to).
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0}
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root"), true); err != nil {
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
t.Fatalf("netconfig.Apply: %v", err)
}
@ -398,9 +325,6 @@ func TestNetconfig(t *testing.T) {
if !strings.Contains(string(link), "link/ether 02:73:53:00:b0:aa") {
t.Errorf("lan0 MAC address is not 02:73:53:00:b0:aa")
}
if !strings.Contains(string(link), " mtu 1492 ") {
t.Errorf("lan0 MTU is not 1492 (link: %q)", string(link))
}
addrs, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "uplink0").Output()
if err != nil {
@ -478,27 +402,11 @@ peer: AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=
if !upRe.MatchString(string(out)) {
t.Errorf("regexp %s does not match %s", upRe, string(out))
}
addr4Re := regexp.MustCompile(`(?m)^\s*inet 10.22.100.1/24 brd 10.22.100.255 scope global wg0\s*$`)
if !addr4Re.MatchString(string(out)) {
t.Errorf("regexp %s does not match %s", addr4Re, string(out))
}
addr6Re := regexp.MustCompile(`(?m)^\s*inet6 fe80::1/64 scope link\s*$`)
if !addr6Re.MatchString(string(out)) {
t.Errorf("regexp %s does not match %s", addr6Re, string(out))
}
out, err = exec.Command("ip", "-netns", ns, "-6", "route", "show", "dev", "wg0").Output()
if err != nil {
t.Fatal(err)
}
extraRouteRe := regexp.MustCompile(`(?m)^\s*2a02:168:4a00:22::/64 via fe80::2 metric 1024 pref medium\s*$`)
if !extraRouteRe.MatchString(string(out)) {
t.Errorf("regexp %s does not match %s", extraRouteRe, string(out))
}
})
opts := []cmp.Option{
@ -545,131 +453,6 @@ peer: AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=
})
}
const goldenInterfacesBridges = `
{
"bridges":[
{
"name": "lan0",
"interface_hardware_addrs": ["02:73:53:00:b0:0c"]
}
],
"interfaces":[
{
"hardware_addr": "02:73:53:00:ca:fe",
"name": "uplink0"
},
{
"spoof_hardware_addr": "02:73:53:00:b0:aa",
"name": "lan0",
"addr": "192.168.42.1/24"
}
]
}
`
func TestNetconfigBridges(t *testing.T) {
if os.Getenv("HELPER_PROCESS") == "1" {
tmp, err := ioutil.TempDir("", "router7")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
for _, golden := range []struct {
filename, content string
}{
{"interfaces.json", goldenInterfacesBridges},
} {
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(tmp, golden.filename), []byte(golden.content), 0600); err != nil {
t.Fatal(err)
}
}
if err := os.MkdirAll(filepath.Join(tmp, "root", "etc"), 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(tmp, "root", "tmp"), 0755); err != nil {
t.Fatal(err)
}
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42}
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
t.Fatalf("netconfig.Apply: %v", err)
}
// Apply twice to ensure the absence of errors when dealing with
// already-configured interfaces, addresses, routes, … (and ensure
// nftables rules are replaced, not appendend to).
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0}
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
t.Fatalf("netconfig.Apply: %v", err)
}
return
}
const ns = "ns6" // name of the network namespace to use for this test
add := exec.Command("ip", "netns", "add", ns)
add.Stderr = os.Stderr
if err := add.Run(); err != nil {
t.Fatalf("%v: %v", add.Args, err)
}
defer exec.Command("ip", "netns", "delete", ns).Run()
nsSetup := []*exec.Cmd{
exec.Command("ip", "-netns", ns, "link", "add", "dummy0", "type", "dummy"),
exec.Command("ip", "-netns", ns, "link", "add", "eth0", "type", "dummy"),
exec.Command("ip", "-netns", ns, "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"),
exec.Command("ip", "-netns", ns, "link", "set", "eth0", "address", "02:73:53:00:b0:0c"),
}
for _, cmd := range nsSetup {
if err := cmd.Run(); err != nil {
t.Fatalf("%v: %v", cmd.Args, err)
}
}
cmd := exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestNetconfigBridges")
cmd.Env = append(os.Environ(), "HELPER_PROCESS=1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
t.Run("VerifyAddresses", func(t *testing.T) {
link, err := exec.Command("ip", "-netns", ns, "link", "show", "dev", "lan0", "type", "bridge").Output()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(link), "link/ether 02:73:53:00:b0:aa") {
t.Errorf("lan0 MAC address is not 02:73:53:00:b0:aa")
}
addrs, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "lan0").Output()
if err != nil {
t.Fatal(err)
}
addrRe := regexp.MustCompile(`(?m)^\s*inet 192.168.42.1/24 brd 192.168.42.255 scope global lan0`)
if !addrRe.MatchString(string(addrs)) {
t.Fatalf("regexp %s does not match %s", addrRe, string(addrs))
}
bridgeLinks, err := exec.Command("ip", "-netns", ns, "link", "show", "master", "lan0").Output()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(bridgeLinks), ": eth0: ") {
t.Errorf("lan0 bridge does not contain eth0 interface")
}
})
}
func ipLines(args ...string) ([]string, error) {
cmd := exec.Command("ip", args...)
out, err := cmd.Output()
@ -683,124 +466,3 @@ func ipLines(args ...string) ([]string, error) {
return strings.Split(strings.TrimSpace(outstr), "\n"), nil
}
func TestDHCPv4OldAddressDeconfigured(t *testing.T) {
if os.Getenv("HELPER_PROCESS") == "1" {
tmp, err := ioutil.TempDir("", "router7")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
for _, golden := range []struct {
filename, content string
}{
{"dhcp4/wire/lease.json", goldenDhcp4},
{"interfaces.json", goldenInterfaces},
} {
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(filepath.Join(tmp, golden.filename), []byte(golden.content), 0600); err != nil {
t.Fatal(err)
}
}
if err := os.MkdirAll(filepath.Join(tmp, "root", "etc"), 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(tmp, "root", "tmp"), 0755); err != nil {
t.Fatal(err)
}
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
t.Fatalf("netconfig.Apply: %v", err)
}
const anotherDhcp4 = `
{
"valid_until":"2018-05-18T23:46:04.429895261+02:00",
"client_ip":"85.195.199.99",
"subnet_mask":"255.255.255.128",
"router":"85.195.199.1",
"dns":[
"77.109.128.2",
"213.144.129.20"
]
}
`
if err := ioutil.WriteFile(filepath.Join(tmp, "dhcp4/wire/lease.json"), []byte(anotherDhcp4), 0600); err != nil {
t.Fatal(err)
}
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
t.Fatalf("netconfig.Apply: %v", err)
}
return
}
const ns = "ns5" // name of the network namespace to use for this test
add := exec.Command("ip", "netns", "add", ns)
add.Stderr = os.Stderr
if err := add.Run(); err != nil {
t.Fatalf("%v: %v", add.Args, err)
}
defer exec.Command("ip", "netns", "delete", ns).Run()
nsSetup := []*exec.Cmd{
exec.Command("ip", "-netns", ns, "link", "add", "dummy0", "type", "dummy"),
exec.Command("ip", "-netns", ns, "link", "add", "lan0", "type", "dummy"),
exec.Command("ip", "-netns", ns, "link", "set", "dummy0", "address", "02:73:53:00:ca:fe"),
exec.Command("ip", "-netns", ns, "link", "set", "lan0", "address", "02:73:53:00:b0:0c"),
}
for _, cmd := range nsSetup {
if err := cmd.Run(); err != nil {
t.Fatalf("%v: %v", cmd.Args, err)
}
}
cmd := exec.Command("ip", "netns", "exec", ns, os.Args[0], "-test.run=^TestDHCPv4OldAddressDeconfigured$")
cmd.Env = append(os.Environ(), "HELPER_PROCESS=1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
t.Run("VerifyAddresses", func(t *testing.T) {
show := exec.Command("ip", "-netns", ns, "address", "show", "dev", "uplink0")
show.Stderr = os.Stderr
addrs, err := show.Output()
if err != nil {
t.Fatal(err)
}
oldAddrRe := regexp.MustCompile(`(?m)^\s*inet 85.195.207.62/25 brd 85.195.207.127 scope global uplink0$`)
if oldAddrRe.MatchString(string(addrs)) {
t.Fatalf("regexp %s unexpectedly still matches %s", oldAddrRe, string(addrs))
}
addrRe := regexp.MustCompile(`(?m)^\s*inet 85.195.199.99/25 brd 85.195.199.127 scope global uplink0$`)
if !addrRe.MatchString(string(addrs)) {
t.Fatalf("regexp %s does not match %s", addrRe, string(addrs))
}
wantRoutes := []string{
"default via 85.195.199.1 proto dhcp src 85.195.199.99 ",
"85.195.199.0/25 proto kernel scope link src 85.195.199.99 ",
"85.195.199.1 proto dhcp scope link src 85.195.199.99",
}
routes, err := ipLines("-netns", ns, "route", "show", "dev", "uplink0")
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(wantRoutes, routes); diff != "" {
t.Fatalf("routes: diff (-want +got):\n%s", diff)
}
})
}

View File

@ -23,10 +23,9 @@ import (
"io/ioutil"
"os"
"path/filepath"
"slices"
)
func Archive(w io.Writer, dir string, excludes []string) error {
func Archive(w io.Writer, dir string) error {
gw, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
if err != nil {
return err
@ -47,9 +46,6 @@ func Archive(w io.Writer, dir string, excludes []string) error {
if path == dir {
return nil // skip root
}
if last := filepath.Base(path); last == "nobackup" || last == "srv" || slices.Contains(excludes, path) {
return filepath.SkipDir // skip nobackup (and srv for legacy)
}
rel, err := filepath.Rel(dir, path)
if err != nil {
return err
@ -62,7 +58,7 @@ func Archive(w io.Writer, dir string, excludes []string) error {
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if !info.Mode().IsDir() && !slices.Contains(excludes, path) {
if !info.Mode().IsDir() {
b, err := ioutil.ReadFile(path)
if err != nil {
return err

View File

@ -25,7 +25,7 @@ import (
"time"
"github.com/google/gopacket/layers"
"github.com/mdlayher/packet"
"github.com/mdlayher/raw"
"github.com/rtr7/dhcp4"
"golang.org/x/sys/unix"
)
@ -44,7 +44,6 @@ type Client struct {
err error
once sync.Once
onceErr error
connection net.PacketConn
hardwareAddr net.HardwareAddr
hostname string
@ -52,8 +51,6 @@ type Client struct {
timeNow func() time.Time
generateXID func() uint32
timeoutCount int
// last DHCPACK packet for renewal/release
Ack *layers.DHCPv4
}
@ -87,20 +84,23 @@ var errNAK = errors.New("received DHCPNAK")
// ObtainOrRenew returns false when encountering a permanent error.
func (c *Client) ObtainOrRenew() bool {
var onceErr error
c.once.Do(func() {
if c.timeNow == nil {
c.timeNow = time.Now
}
if c.connection == nil && c.Interface != nil {
conn, err := packet.Listen(c.Interface, packet.Datagram, syscall.ETH_P_IP, nil)
conn, err := raw.ListenPacket(c.Interface, syscall.ETH_P_IP, &raw.Config{
LinuxSockDGRAM: true,
})
if err != nil {
c.onceErr = err
onceErr = err
return
}
c.connection = conn
}
if c.connection == nil && c.Interface == nil {
c.onceErr = fmt.Errorf("c.Interface is nil")
onceErr = fmt.Errorf("c.Interface is nil")
return
}
if c.hardwareAddr == nil && c.HWAddr != nil {
@ -115,32 +115,21 @@ func (c *Client) ObtainOrRenew() bool {
if c.hostname == "" {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
c.onceErr = err
onceErr = err
return
}
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
}
})
if c.onceErr != nil {
c.err = c.onceErr
if onceErr != nil {
c.err = onceErr
return false // permanent error
}
c.err = nil // clear previous error
ack, err := c.dhcpRequest()
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
var serverip net.IP
for _, opt := range c.Ack.Options {
if opt.Type == layers.DHCPOptServerID {
serverip = opt.Data
}
}
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable: %v)", serverip)
c.timeoutCount++
if c.timeoutCount > 3 {
c.timeoutCount = 0
c.Ack = nil // start over at DHCPDISCOVER it has failed 3 times
}
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable)")
return true // temporary error
}
if err == errNAK {
@ -165,7 +154,6 @@ func (c *Client) ObtainOrRenew() bool {
}
}
c.cfg.RenewAfter = c.timeNow().Add(lease.RenewalTime)
c.timeoutCount = 0
return true
}

View File

@ -18,7 +18,6 @@ package dhcp4d
import (
"bytes"
"encoding/hex"
"fmt"
"log"
"math/rand"
"net"
@ -33,28 +32,22 @@ import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/krolaw/dhcp4"
"github.com/mdlayher/packet"
"github.com/mdlayher/raw"
)
type Lease struct {
Num int `json:"num"` // relative to Handler.start
Addr net.IP `json:"addr"` // subnet.start+Num
Num int `json:"num"` // relative to Handler.start
Addr net.IP `json:"addr"`
HardwareAddr string `json:"hardware_addr"`
Hostname string `json:"hostname"`
HostnameOverride string `json:"hostname_override"`
Expiry time.Time `json:"expiry"`
VendorIdentifier string `json:"vendor"`
LastACK time.Time `json:"last_ack"`
}
func (l *Lease) Expired(at time.Time) bool {
return !l.Expiry.IsZero() && at.After(l.Expiry)
}
func (l *Lease) Active(at time.Time) bool {
return !l.LastACK.IsZero() && at.Before(l.LastACK.Add(leasePeriod))
}
type Handler struct {
serverIP net.IP
start net.IP // first IP address to hand out
@ -74,7 +67,7 @@ type Handler struct {
leasesIP map[int]*Lease
}
func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.PacketConn, options dhcp4.Options) (*Handler, error) {
func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.PacketConn) (*Handler, error) {
serverIP, err := netconfig.LinkAddress(dir, ifaceName)
if err != nil {
return nil, err
@ -86,29 +79,15 @@ func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.Pac
}
}
if conn == nil {
conn, err = packet.Listen(iface, packet.Raw, syscall.ETH_P_ALL, nil)
conn, err = raw.ListenPacket(iface, syscall.ETH_P_ALL, nil)
if err != nil {
return nil, err
}
}
if options == nil {
var domainSearch []byte
domainSearch, err = CompressNames("lan.")
if err != nil {
return nil, err
}
options = dhcp4.Options{
dhcp4.OptionSubnetMask: []byte{255, 255, 255, 0},
dhcp4.OptionRouter: []byte(serverIP),
dhcp4.OptionDomainNameServer: []byte(serverIP),
dhcp4.OptionDomainName: []byte("lan"),
dhcp4.OptionDomainSearch: domainSearch,
}
}
serverIP = serverIP.To4()
start := make(net.IP, len(serverIP))
copy(start, serverIP)
start[len(start)-1]++
start[len(start)-1] += 1
return &Handler{
rawConn: conn,
iface: iface,
@ -117,18 +96,18 @@ func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.Pac
serverIP: serverIP,
start: start,
leaseRange: 230,
LeasePeriod: leasePeriod,
options: options,
timeNow: time.Now,
LeasePeriod: 20 * time.Minute,
options: dhcp4.Options{
dhcp4.OptionSubnetMask: []byte{255, 255, 255, 0},
dhcp4.OptionRouter: []byte(serverIP),
dhcp4.OptionDomainNameServer: []byte(serverIP),
dhcp4.OptionDomainName: []byte("lan"),
dhcp4.OptionDomainSearch: []byte{0x03, 'l', 'a', 'n', 0x00},
},
timeNow: time.Now,
}, nil
}
// Apple recommends a DHCP lease time of 1 hour in
// https://support.apple.com/de-ch/HT202068,
// so if 20 minutes ever causes any trouble,
// we should try increasing it to 1 hour.
const leasePeriod = 20 * time.Minute
// SetLeases overwrites the leases database with the specified leases, typically
// loaded from persistent storage. There is no locking, so SetLeases must be
// called before Serve.
@ -138,9 +117,6 @@ func (h *Handler) SetLeases(leases []*Lease) {
h.leasesHW = make(map[string]int)
h.leasesIP = make(map[int]*Lease)
for _, l := range leases {
if l.LastACK.IsZero() {
l.LastACK = l.Expiry
}
h.leasesHW[l.HardwareAddr] = l.Num
h.leasesIP[l.Num] = l
}
@ -157,18 +133,14 @@ func (h *Handler) callLeasesLocked(lease *Lease) {
h.Leases(leases, lease)
}
func (h *Handler) SetHostname(hwaddr, hostname string) error {
func (h *Handler) SetHostname(hwaddr, hostname string) {
h.leasesMu.Lock()
defer h.leasesMu.Unlock()
leaseNum := h.leasesHW[hwaddr]
lease := h.leasesIP[leaseNum]
if lease.HardwareAddr != hwaddr || lease.Expired(h.timeNow()) {
return fmt.Errorf("hwaddr %v does not have a valid lease", hwaddr)
}
lease.Hostname = hostname
lease.HostnameOverride = hostname
h.callLeasesLocked(lease)
return nil
}
func (h *Handler) findLease() int {
@ -196,7 +168,7 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
}
leaseNum := dhcp4.IPRange(h.start, reqIP) - 1
if leaseNum < 0 {
if leaseNum < 0 || leaseNum >= h.leaseRange {
return -1
}
@ -204,10 +176,6 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
defer h.leasesMu.Unlock()
l, ok := h.leasesIP[leaseNum]
if !ok {
if leaseNum >= h.leaseRange {
return -1
}
return leaseNum // lease available
}
@ -215,10 +183,6 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
return leaseNum // lease already owned by requestor
}
if leaseNum >= h.leaseRange {
return -1
}
if l.Expired(h.timeNow()) {
return leaseNum // lease expired
}
@ -268,7 +232,7 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
udp,
gopacket.Payload(reply))
if _, err := h.rawConn.WriteTo(buf.Bytes(), &packet.Addr{HardwareAddr: destMAC}); err != nil {
if _, err := h.rawConn.WriteTo(buf.Bytes(), &raw.Addr{destMAC}); err != nil {
log.Printf("WriteTo: %v", err)
}
@ -320,18 +284,18 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
// try to offer the requested IP, if any and available
if !reqIP.To4().Equal(net.IPv4zero) {
free = h.canLease(reqIP, hwAddr)
// log.Printf("canLease(%v, %s) = %d", reqIP, hwAddr, free)
//log.Printf("canLease(%v, %s) = %d", reqIP, hwAddr, free)
}
// offer previous lease for this HardwareAddr, if any
if lease, ok := h.leaseHW(hwAddr); ok && !lease.Expired(h.timeNow()) {
free = lease.Num
// log.Printf("h.leasesHW[%s] = %d", hwAddr, free)
//log.Printf("h.leasesHW[%s] = %d", hwAddr, free)
}
if free == -1 {
free = h.findLease()
// log.Printf("findLease = %d", free)
//log.Printf("findLease = %d", free)
}
if free == -1 {
@ -354,15 +318,13 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
if leaseNum == -1 {
return dhcp4.ReplyPacket(p, dhcp4.NAK, h.serverIP, nil, 0, nil)
}
now := h.timeNow()
lease := &Lease{
Num: leaseNum,
Addr: make([]byte, 4),
HardwareAddr: hwAddr,
Expiry: now.Add(h.leasePeriodForDevice(hwAddr)),
Hostname: string(options[dhcp4.OptionHostName]),
VendorIdentifier: string(bytes.ToValidUTF8(bytes.ReplaceAll(options[dhcp4.OptionVendorClassIdentifier], []byte{0}, []byte{}), []byte{})),
LastACK: h.timeNow(),
Num: leaseNum,
Addr: make([]byte, 4),
HardwareAddr: hwAddr,
Expiry: h.timeNow().Add(h.leasePeriodForDevice(hwAddr)),
Hostname: string(options[dhcp4.OptionHostName]),
}
copy(lease.Addr, reqIP.To4())
@ -427,78 +389,3 @@ func (h *Handler) expireLease(hwAddr string) bool {
l.Expiry = time.Now()
return true
}
func CompressNames(names ...string) ([]byte, error) {
b := make([]byte, 0, 255)
m := make(map[string]int)
var err error
for _, name := range names {
if name[len(name)-1] != '.' {
name += "."
}
b, err = pack([]byte(name), b, m)
if err != nil {
return []byte{}, err
}
}
return b, nil
}
func pack(name []byte, msg []byte, compression map[string]int) ([]byte, error) {
oldMsg := msg
// Add a trailing dot to canonicalize name.
if len(name) == 0 || name[len(name)-1] != '.' {
return oldMsg, fmt.Errorf("%s", "errNonCanonicalName")
}
// Allow root domain.
if name[0] == '.' && len(name) == 1 {
return append(msg, 0), nil
}
// Emit sequence of counted strings, chopping at dots.
for i, begin := 0, 0; i < len(name); i++ {
// Check for the end of the segment.
if name[i] == '.' {
// The two most significant bits have special meaning.
// It isn't allowed for segments to be long enough to
// need them.
if i-begin >= 1<<6 {
return oldMsg, fmt.Errorf("%s", "errSegTooLong")
}
// Segments must have a non-zero length.
if i-begin == 0 {
return oldMsg, fmt.Errorf("%s", "errZeroSegLen")
}
msg = append(msg, byte(i-begin))
for j := begin; j < i; j++ {
msg = append(msg, name[j])
}
begin = i + 1
continue
}
// We can only compress domain suffixes starting with a new
// segment. A pointer is two bytes with the two most significant
// bits set to 1 to indicate that it is a pointer.
if (i == 0 || name[i-1] == '.') && compression != nil {
if ptr, ok := compression[string(name[i:])]; ok {
// Hit. Emit a pointer instead of the rest of
// the domain.
return append(msg, byte(ptr>>8|0xC0), byte(ptr)), nil
}
// Miss. Add the suffix to the compression table if the
// offset can be stored in the available 14 bytes.
if len(msg) <= int(^uint16(0)>>2) {
compression[string(name[i:])] = len(msg)
}
}
}
return append(msg, 0), nil
}

View File

@ -31,7 +31,7 @@ func messageType(p dhcp4.Packet) dhcp4.MessageType {
return dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0])
}
func newPacket(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []dhcp4.Option) dhcp4.Packet {
func packet(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []dhcp4.Option) dhcp4.Packet {
return dhcp4.RequestPacket(
mt,
hwaddr, // MAC address
@ -43,15 +43,15 @@ func newPacket(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts
}
func request(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
return newPacket(dhcp4.Request, addr, hwaddr, opts)
return packet(dhcp4.Request, addr, hwaddr, opts)
}
func discover(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
return newPacket(dhcp4.Discover, addr, hwaddr, opts)
return packet(dhcp4.Discover, addr, hwaddr, opts)
}
func decline(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
return newPacket(dhcp4.Decline, addr, hwaddr, opts)
return packet(dhcp4.Decline, addr, hwaddr, opts)
}
const goldenInterfaces = `
@ -95,7 +95,6 @@ func testHandler(t *testing.T) (_ *Handler, cleanup func()) {
},
"lan0",
&noopSink{},
nil,
)
if err != nil {
t.Fatal(err)
@ -174,7 +173,7 @@ func TestPreferredAddress(t *testing.T) {
})
t.Run("requested option", func(t *testing.T) {
// p := request(net.IPv4zero, hardwareAddr)
//p := request(net.IPv4zero, hardwareAddr)
p := dhcp4.RequestPacket(
dhcp4.Discover,
hardwareAddr, // MAC address
@ -216,6 +215,7 @@ func TestPoolBoundaries(t *testing.T) {
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
}
}
}
func TestPreviousLease(t *testing.T) {
@ -465,7 +465,7 @@ func TestMinimumLeaseTime(t *testing.T) {
handler, cleanup := testHandler(t)
defer cleanup()
addr := net.IP{192, 168, 42, 23}
var addr = net.IP{192, 168, 42, 23}
for _, tt := range []struct {
hwaddr net.HardwareAddr

View File

@ -266,14 +266,6 @@ func (c *Client) ObtainOrRenew() bool {
}
c.advertise = advertise
if iapd := advertise.Options.OneIAPD(); iapd != nil {
if status := iapd.Options.Status(); status != nil && status.StatusCode != iana.StatusSuccess {
c.err = fmt.Errorf("IAPD error: %v (%v)", status.StatusCode, status.StatusMessage)
return false
}
}
_, reply, err := c.request(advertise)
if err != nil {
c.err = err

View File

@ -73,7 +73,6 @@ func (d *ping4gw) Evaluate() (string, error) {
if err != nil {
return "", err
}
defer p.Close()
rtt, err := p.Ping(addr, timeout)
if err != nil {
return "", err
@ -116,7 +115,6 @@ func (d *ping4) Evaluate() (string, error) {
if err != nil {
return "", err
}
defer p.Close()
rtt, err := p.Ping(addr, timeout)
if err != nil {
return "", err
@ -179,7 +177,6 @@ func (d *ping6gw) Evaluate() (string, error) {
if err != nil {
return "", fmt.Errorf("ping.New(::): %v", err)
}
defer p.Close()
rtt, err := p.Ping(addr, timeout)
if err != nil {
return "", fmt.Errorf("ping6(%v, %v): %v", addr, timeout, err)
@ -254,7 +251,6 @@ func (d *ping6) Evaluate() (string, error) {
if err != nil {
return "", err
}
defer p.Close()
ctx, canc := context.WithTimeout(context.Background(), timeout)
defer canc()
if strings.HasPrefix(addr.String(), "ff02::") {
@ -278,11 +274,6 @@ func (d *ping6) Evaluate() (string, error) {
if localAddr[reply.Address.String()] {
continue
}
go func() {
for range replies {
// drain channel
}
}()
return formatRTT(reply.Duration) + " from " + reply.Address.String(), nil
}
return "", fmt.Errorf("no responses to %s within %v", addr, timeout)

View File

@ -53,7 +53,6 @@ func TCP4(addr string) Node {
type tcp6 struct {
children []Node
ifname string
addr string
}
@ -71,39 +70,7 @@ func (d *tcp6) Children() []Node {
}
func (d *tcp6) Evaluate() (string, error) {
var dialer net.Dialer
if d.ifname != "" {
iface, err := net.InterfaceByName(d.ifname)
if err != nil {
return "", err
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipnet.IP.To4() != nil {
continue // skip IPv4 addresses
}
if !global.Contains(ipnet.IP) {
continue // skip local IPv6 addresses
}
dialer.LocalAddr = &net.TCPAddr{
IP: ipnet.IP,
}
break
}
}
conn, err := dialer.Dial("tcp6", d.addr)
conn, err := net.Dial("tcp6", d.addr)
if err != nil {
return "", err
}
@ -113,9 +80,6 @@ func (d *tcp6) Evaluate() (string, error) {
// TCP6 returns a Node which succeeds when the specified address accepts a TCPv6
// connection.
func TCP6(ifname, addr string) Node {
return &tcp6{
ifname: ifname,
addr: addr,
}
func TCP6(addr string) Node {
return &tcp6{addr: addr}
}

View File

@ -90,16 +90,14 @@ func NewServer(addr, domain string) *Server {
domain: lcHostname(strings.ToLower(domain)),
upstream: []string{
// https://developers.google.com/speed/public-dns/docs/using#google_public_dns_ip_addresses
"45.90.28.26:53",
"45.90.30.26:53",
"[2a07:a8c0::54:f68e]:53",
"[2a07:a8c1::54:f68e]:53",
"194.242.2.4:53",
"[2a07:e340::4]:52",
"94.140.14.14:53",
"94.140.15.15:53",
"[2a10:50c0::ad1:ff]:53",
"[2a10:50c0::ad2:ff]:53",
"1.1.1.1:53",
"1.0.0.1:53",
"2606:4700:4700::1111:53",
"2606:4700:4700::1001:53",
"8.8.8.8:53",
"8.8.4.4:53",
"[2001:4860:4860::8888]:53",
"[2001:4860:4860::8844]:53",
},
sometimes: rate.NewLimiter(rate.Every(1*time.Second), 1), // at most once per second
hostname: hostname,
@ -145,11 +143,6 @@ func NewServer(addr, domain string) *Server {
}
func (s *Server) initHostsLocked() {
for k := range s.subnames {
if k != s.domain {
s.Mux.HandleRemove(string(k))
}
}
s.hostsByName = make(map[lcHostname]string)
s.hostsByIP = make(map[string]string)
s.subnames[s.domain] = make(map[lcHostname]IP)
@ -179,40 +172,43 @@ func (m measurement) String() string {
}
func (s *Server) probeUpstreamLatency() {
upstreams := s.upstreams()
results := make([]measurement, len(upstreams))
var wg sync.WaitGroup
for idx, u := range upstreams {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
// resolve a most-definitely cached record
m := new(dns.Msg)
m.SetQuestion("google.ch.", dns.TypeA)
start := time.Now()
_, _, err := s.client.Exchange(m, u)
rtt := time.Since(start)
if err != nil {
// including unresponsive upstreams in results makes the update
// code simpler:
results[idx] = measurement{u, time.Duration(math.MaxInt64)}
return
}
results[idx] = measurement{u, rtt}
}(idx, u)
if !s.once {
s.once = true
upstreams := s.upstreams()
results := make([]measurement, len(upstreams))
var wg sync.WaitGroup
for idx, u := range upstreams {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
// resolve a most-definitely cached record
m := new(dns.Msg)
m.SetQuestion("google.ch.", dns.TypeA)
start := time.Now()
_, _, err := s.client.Exchange(m, u)
rtt := time.Since(start)
if err != nil {
// including unresponsive upstreams in results makes the update
// code simpler:
results[idx] = measurement{u, time.Duration(math.MaxInt64)}
return
}
results[idx] = measurement{u, rtt}
}(idx, u)
}
wg.Wait()
// Re-order by resolving latency:
sort.Slice(results, func(i, j int) bool {
return results[i].rtt < results[j].rtt
})
log.Printf("probe results: %v %v", s.once, results)
for idx, result := range results {
upstreams[idx] = result.upstream
}
s.upstreamMu.Lock()
defer s.upstreamMu.Unlock()
s.upstream = upstreams
}
wg.Wait()
// Re-order by resolving latency:
sort.Slice(results, func(i, j int) bool {
return results[i].rtt < results[j].rtt
})
log.Printf("probe results: %v", results)
for idx, result := range results {
upstreams[idx] = result.upstream
}
s.upstreamMu.Lock()
defer s.upstreamMu.Unlock()
s.upstream = upstreams
}
func (s *Server) hostByName(n lcHostname) (string, bool) {
@ -229,10 +225,10 @@ func (s *Server) hostByIP(n string) (string, bool) {
return r, ok
}
func (s *Server) subname(domain, host string) (IP, bool) {
func (s *Server) subname(hostname, host string) (IP, bool) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.subnames[lcHostname(strings.ToLower(domain))][lcHostname(strings.ToLower(host))]
r, ok := s.subnames[lcHostname(strings.ToLower(hostname))][lcHostname(strings.ToLower(host))]
return r, ok
}
@ -348,15 +344,6 @@ func (s *Server) SetDNSEntries(dnsEntries []IP) {
entry.Host = lcHostname(strings.TrimSuffix(dn, "lan")) + s.domain
}
s.setSubname(entry)
hdnSlice := strings.SplitN(string(entry.Host), ".", 2)
domain := lcHostname("")
if len(hdnSlice) == 2 {
domain = lcHostname(hdnSlice[1])
}
if domain == "" || domain == s.domain {
continue
}
s.Mux.HandleFunc(string(domain), s.subnameHandler(domain))
}
}
@ -493,7 +480,6 @@ func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
if err == errEmpty {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
w.WriteMsg(m)
return
}
@ -502,7 +488,6 @@ func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
if rr != nil {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Answer = append(m.Answer, rr)
w.WriteMsg(m)
return
@ -510,7 +495,6 @@ func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
// Send an authoritative NXDOMAIN for local:
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.SetRcode(r, dns.RcodeNameError)
w.WriteMsg(m)
}
@ -538,85 +522,35 @@ func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
s.promInc("DNS", r)
if r.RecursionDesired {
for idx, u := range s.upstreams() {
in, _, err := s.client.Exchange(r, u)
if err != nil {
if s.sometimes.Allow() {
log.Printf("resolving %v failed: %v", r.Question, err)
}
continue // fall back to next-slower upstream
for idx, u := range s.upstreams() {
in, _, err := s.client.Exchange(r, u)
if err != nil {
if s.sometimes.Allow() {
log.Printf("resolving %v failed: %v", r.Question, err)
}
if len(in.Answer) > 1 {
if in.Answer[0].Header().Rrtype == dns.TypeCNAME {
for i, rr := range in.Answer {
if rr != nil && rr.Header() != nil && rr.Header().Rrtype == dns.TypeA {
newRR, err := s.resolveSubname(string(s.domain), dns.Question{strings.ToLower(rr.Header().Name), dns.TypeA, dns.ClassINET})
if err == nil && newRR != nil {
in.Answer[i] = newRR
}
}
}
}
}
w.WriteMsg(in)
if idx > 0 {
// re-order this upstream to the front of s.upstream.
s.upstreamMu.Lock()
// if the upstreams were reordered in the meantime leave them alone
if s.upstream[idx] == u {
s.upstream = append(append([]string{u}, s.upstream[:idx]...), s.upstream[idx+1:]...)
}
s.upstreamMu.Unlock()
}
return
continue // fall back to next-slower upstream
}
} else {
for _, u := range s.upstreams() {
nr := r.Copy()
nr.Question[0].Qtype = dns.TypeSOA
nr.RecursionDesired = true
soa, _, err := s.client.Exchange(nr, u)
fmt.Println(w.RemoteAddr(), err, soa)
fmt.Println()
fmt.Println(soa.Ns)
if len(soa.Ns) > 0 {
soa2 := soa.Ns[0].(*dns.SOA)
in, _, err := s.client.Exchange(r, strings.TrimRight(soa2.Ns, ".")+":53")
fmt.Println(err, in)
if err != nil {
if s.sometimes.Allow() {
log.Printf("resolving %v failed: %v", r.Question, err)
}
continue // fall back to next-slower upstream
}
w.WriteMsg(in)
return
}
w.WriteMsg(in)
if idx > 0 {
// re-order this upstream to the front of s.upstream.
s.upstreamMu.Lock()
s.upstream = append(append([]string{u}, s.upstream[:idx]...), s.upstream[idx+1:]...)
s.upstreamMu.Unlock()
}
return
}
// DNS has no reply for resolving errors
}
func (s *Server) getSubname(domain string, queryName string) (IP, bool) {
name := strings.TrimSuffix(queryName, ".")
name = strings.TrimSuffix(name, ".lan") // trim lan domain
name = strings.TrimSuffix(name, "."+string(s.domain)) // trim server domain
name = strings.TrimSuffix(name, "."+strings.TrimSuffix(domain, "."+string(s.domain))) // trim function domain
if ip, ok := s.subname(domain, name); ok {
return ip, true
}
return IP{}, false
}
func (s *Server) resolveSubname(domain string, q dns.Question) (dns.RR, error) {
if q.Qclass != dns.ClassINET {
return nil, nil
}
ip, ok := s.getSubname(domain, q.Name)
if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA /*|| q.Qtype == dns.TypeMX*/ {
if ok {
name := strings.TrimSuffix(q.Name, ".")
name = strings.TrimSuffix(name, "."+string(s.domain)) // trim server domain
name = strings.TrimSuffix(name, "."+strings.TrimSuffix(domain, "."+string(s.domain))) // trim function domain
if ip, ok := s.subname(domain, name); ok {
if q.Qtype == dns.TypeA && ip.IPv4.To4() != nil {
return dns.NewRR(q.Name + " 3600 IN A " + ip.IPv4.String())
}
@ -635,20 +569,19 @@ func (s *Server) promInc(label string, r *dns.Msg) {
s.prom.upstream.WithLabelValues(label).Inc()
}
func (s *Server) subnameHandler(domain lcHostname) func(w dns.ResponseWriter, r *dns.Msg) {
func (s *Server) subnameHandler(hostname lcHostname) func(w dns.ResponseWriter, r *dns.Msg) {
return func(w dns.ResponseWriter, r *dns.Msg) {
if len(r.Question) != 1 { // TODO: answer all questions we can answer
s.promInc("local", r)
return
}
rr, err := s.resolveSubname(string(domain), r.Question[0])
rr, err := s.resolveSubname(string(hostname), r.Question[0])
if err != nil {
s.promInc("local", r)
if err == errEmpty {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
w.WriteMsg(m)
return
}
@ -658,18 +591,16 @@ func (s *Server) subnameHandler(domain lcHostname) func(w dns.ResponseWriter, r
s.promInc("local", r)
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Answer = append(m.Answer, rr)
w.WriteMsg(m)
return
}
// Send an authoritative NXDOMAIN for local names:
if _, ok := s.getSubname(string(domain), r.Question[0].Name); r.Question[0].Qtype == dns.TypePTR || (r.Question[0].Qtype == dns.TypeCNAME && ok) || !strings.Contains(strings.TrimSuffix(r.Question[0].Name, "."), ".") || strings.HasSuffix(r.Question[0].Name, ".lan.") {
if r.Question[0].Qtype == dns.TypePTR || !strings.Contains(strings.TrimSuffix(r.Question[0].Name, "."), ".") || strings.HasSuffix(r.Question[0].Name, ".lan.") {
s.promInc("local", r)
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.SetRcode(r, dns.RcodeNameError)
w.WriteMsg(m)
return

View File

@ -158,28 +158,6 @@ func TestResolveLatencySteering(t *testing.T) {
}
}
func TestDHCPDomain(t *testing.T) {
s := NewServer("localhost:0", "example.org")
s.SetLeases([]dhcp4d.Lease{
{
Hostname: "testtarget",
Addr: net.IP{192, 168, 42, 23},
},
})
t.Run("testtarget.lan.", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.lan.", net.ParseIP("192.168.42.23")); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget.example.org.", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.lan.", net.ParseIP("192.168.42.23")); err != nil {
t.Fatal(err)
}
})
}
func TestDHCP(t *testing.T) {
r := &recorder{}
s := NewServer("localhost:0", "lan")
@ -642,93 +620,3 @@ func TestSubname(t *testing.T) {
}
})
}
func TestDNSEntries(t *testing.T) {
r := &recorder{}
s := NewServer("127.0.0.2:0", "lan")
s.SetLeases([]dhcp4d.Lease{
{
Hostname: "testtarget",
Addr: net.IP{192, 168, 42, 23},
},
{
Hostname: "testtarget-ipv6",
Addr: net.ParseIP("fe80:3::"),
},
})
s.SetDNSEntries([]IP{
IP{
Host: "testtarget",
IPv4: net.IP{7, 7, 7, 7},
IPv6: net.ParseIP("fe80:1::"),
},
IP{
Host: "testtarget.example.org",
IPv4: net.IP{8, 8, 8, 8},
IPv6: net.ParseIP("fe80:2::"),
},
{
Host: "testtarget-ipv6",
IPv4: net.IP{9, 9, 9, 9},
IPv6: net.ParseIP("fe80:9::"),
},
})
t.Run("testtarget.", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.", net.IP{192, 168, 42, 23}); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget.lan.", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.lan.", net.IP{192, 168, 42, 23}); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget-ipv6.lan. (IPv6)", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget-ipv6.lan.", net.ParseIP("fe80:3::")); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget-ipv6.lan. (no override???)", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget-ipv6.lan.", net.IP{9, 9, 9, 9}); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget.lan. (IPv6) (no override???)", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.lan.", net.ParseIP("fe80:1::")); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget.example.org.", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.example.org.", net.IP{8, 8, 8, 8}); err != nil {
t.Fatal(err)
}
})
t.Run("testtarget.example.org. (IPv6)", func(t *testing.T) {
if err := resolveTestTarget(s, "testtarget.example.org.", net.ParseIP("fe80:2::")); err != nil {
t.Fatal(err)
}
})
s.SetLeases([]dhcp4d.Lease{
{
Hostname: "testtarget",
Addr: net.IP{192, 168, 42, 23},
},
})
t.Run("testtarget.example.org. (deleted)", func(t *testing.T) {
m := new(dns.Msg)
m.SetQuestion("testtarget.example.org.", dns.TypeA)
s.Mux.ServeDNS(r, m)
if got, want := r.response.Rcode, dns.RcodeNameError; got != want {
t.Fatalf("unexpected rcode: got %v, want %v", got, want)
}
})
}

View File

@ -36,7 +36,7 @@ func Update(ctx context.Context, zone string, record libdns.Record, provider Rec
var updated []libdns.Record
for _, rec := range existing {
if rec.Name+"."+zone != record.Name || rec.Type != record.Type {
if rec.Name != record.Name || rec.Type != record.Type {
continue
}

View File

@ -21,7 +21,6 @@ import (
"io/ioutil"
"net"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
@ -32,7 +31,7 @@ import (
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
"github.com/mdlayher/ethtool"
"github.com/google/renameio"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
@ -63,7 +62,7 @@ func subnetMaskSize(mask string) (int, error) {
return ones, nil
}
func applyDhcp4(dir string, cfg InterfaceConfig) error {
func applyDhcp4(dir string) error {
b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp4/wire/lease.json"))
if err != nil {
if os.IsNotExist(err) {
@ -76,8 +75,7 @@ func applyDhcp4(dir string, cfg InterfaceConfig) error {
return err
}
const linkName = "uplink0"
link, err := netlink.LinkByName(linkName)
link, err := netlink.LinkByName("uplink0")
if err != nil {
return err
}
@ -91,8 +89,7 @@ func applyDhcp4(dir string, cfg InterfaceConfig) error {
return err
}
gotAddr := fmt.Sprintf("%s/%d", got.ClientIP, subnetSize)
addr, err := netlink.ParseAddr(gotAddr)
addr, err := netlink.ParseAddr(fmt.Sprintf("%s/%d", got.ClientIP, subnetSize))
if err != nil {
return err
}
@ -102,24 +99,8 @@ func applyDhcp4(dir string, cfg InterfaceConfig) error {
return fmt.Errorf("netlink.NewHandle: %v", err)
}
defer h.Delete()
log.Printf("replacing address %v on %v", addr, linkName)
if err := h.AddrReplace(link, addr); err != nil {
return fmt.Errorf("AddrReplace(%v, %v): %v", linkName, addr, err)
}
addrs, err := h.AddrList(link, netlink.FAMILY_V4)
if err != nil {
return fmt.Errorf("AddrList(%v): %v", linkName, err)
}
for _, addr := range addrs {
ipnet := addr.IPNet.String() // e.g. "85.195.199.99/25"
if ipnet == gotAddr {
continue
}
log.Printf("de-configuring old IP address %s from %v", ipnet, linkName)
if err := h.AddrDel(link, &addr); err != nil {
return fmt.Errorf("AddrDel(%v, %v): %v", linkName, addr, err)
}
return fmt.Errorf("AddrReplace(%v): %v", addr, err)
}
// from include/uapi/linux/rtnetlink.h
@ -141,87 +122,22 @@ func applyDhcp4(dir string, cfg InterfaceConfig) error {
return fmt.Errorf("RouteReplace(router): %v", err)
}
if defaultViaWireguard(cfg) {
// The default route is on a WireGuard interface, so do not install the
// default route from the DHCP reply. Instead, set up a host route for
// the WireGuard endpoint(s).
log.Printf("IPv4 traffic is routed via WireGuard, setting host route instead of default route")
b, err := ioutil.ReadFile(filepath.Join(dir, "wireguard.json"))
if err != nil {
return err
}
var wgcfg wireguardInterfaces
if err := json.Unmarshal(b, &wgcfg); err != nil {
return err
}
for _, iface := range wgcfg.Interfaces {
for _, p := range iface.Peers {
addr, err := net.ResolveUDPAddr("udp", p.Endpoint)
if err != nil {
return err
}
log.Printf(" WireGuard endpoint %s", addr.IP)
router := net.ParseIP(got.Router)
if addr.IP.Equal(router) {
continue // endpoint == router, no route required
}
if err := h.RouteReplace(&netlink.Route{
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{
IP: addr.IP,
Mask: net.CIDRMask(32, 32),
},
Gw: net.ParseIP(got.Router),
Src: net.ParseIP(got.ClientIP),
Protocol: RTPROT_DHCP,
}); err != nil {
return fmt.Errorf("RouteReplace(default): %v", err)
}
}
}
} else {
if err := h.RouteReplace(&netlink.Route{
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
Gw: net.ParseIP(got.Router),
Src: net.ParseIP(got.ClientIP),
Protocol: RTPROT_DHCP,
}); err != nil {
return fmt.Errorf("RouteReplace(default): %v", err)
}
if err := h.RouteReplace(&netlink.Route{
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{
IP: net.ParseIP("0.0.0.0"),
Mask: net.CIDRMask(0, 32),
},
Gw: net.ParseIP(got.Router),
Src: net.ParseIP(got.ClientIP),
Protocol: RTPROT_DHCP,
}); err != nil {
return fmt.Errorf("RouteReplace(default): %v", err)
}
return nil
}
func defaultViaWireguard(cfg InterfaceConfig) bool {
for _, iface := range cfg.Interfaces {
if !strings.HasPrefix(iface.Name, "wg") {
continue
}
for _, route := range iface.ExtraRoutes {
_, n, err := net.ParseCIDR(route.Destination)
if err != nil {
continue
}
ones, bits := n.Mask.Size()
if n.IP.Equal(net.IPv4zero) && ones == 0 && bits == 32 {
return true
}
}
}
return false
}
func applyDhcp6(dir string) error {
b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp6/wire/lease.json"))
if err != nil {
@ -260,36 +176,15 @@ func applyDhcp6(dir string) error {
return nil
}
type Route struct {
Destination string `json:"destination"` // e.g. 2a02:168:4a00:22::/64
Gateway string `json:"gateway"` // e.g. fe80::1
}
type InterfaceDetails struct {
HardwareAddr string `json:"hardware_addr"` // e.g. dc:9b:9c:ee:72:fd
SpoofHardwareAddr string `json:"spoof_hardware_addr"` // e.g. dc:9b:9c:ee:72:fd
Name string `json:"name"` // e.g. uplink0, or lan0
Addr string `json:"addr"` // e.g. 192.168.42.1/24
ExtraAddrs []string `json:"extra_addrs"` // e.g. ["192.168.23.1/24"]
ExtraRoutes []Route `json:"extra_routes"`
MTU int `json:"mtu"` // e.g. 1492 for PPPoE connections
// FEC optionally allows configuring forward error correction, e.g. RS for
// reed-solomon forward error correction, or Off to disable.
//
// Some network card and SFP module combinations (e.g. Mellanox ConnectX-4
// with a Flexoptix P.B1625G.10.AD) need to explicitly be configured to use
// RS forward error correction, otherwise they wont link.
FEC string `json:"fec"`
}
type BridgeDetails struct {
Name string `json:"name"` // e.g. br0 or lan0
InterfaceHardwareAddrs []string `json:"interface_hardware_addrs"`
HardwareAddr string `json:"hardware_addr"` // e.g. dc:9b:9c:ee:72:fd
SpoofHardwareAddr string `json:"spoof_hardware_addr"` // e.g. dc:9b:9c:ee:72:fd
Name string `json:"name"` // e.g. uplink0, or lan0
Addr string `json:"addr"` // e.g. 192.168.42.1/24
}
type InterfaceConfig struct {
Interfaces []InterfaceDetails `json:"interfaces"`
Bridges []BridgeDetails `json:"bridges"`
}
// Interface returns the InterfaceDetails configured for interface ifname in
@ -324,159 +219,18 @@ func LinkAddress(dir, ifname string) (net.IP, error) {
return ip, err
}
func applyBridges(cfg *InterfaceConfig) error {
for _, bridge := range cfg.Bridges {
if _, err := netlink.LinkByName(bridge.Name); err != nil {
log.Printf("creating bridge %s", bridge.Name)
link := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: bridge.Name}}
if err := netlink.LinkAdd(link); err != nil {
return fmt.Errorf("netlink.LinkAdd: %v", err)
}
}
interfaces := make(map[string]bool)
for _, hwaddr := range bridge.InterfaceHardwareAddrs {
interfaces[hwaddr] = true
}
bridgeLink, err := netlink.LinkByName(bridge.Name)
if err != nil {
return fmt.Errorf("LinkByName(%s): %v", bridge.Name, err)
}
links, err := netlink.LinkList()
if err != nil {
return err
}
for _, l := range links {
attr := l.Attrs()
addr := attr.HardwareAddr.String()
if addr == "" {
continue
}
if !interfaces[addr] {
continue
}
if attr.Name == bridge.Name {
// Dont try to add the bridge to itself: the bridge will take
// the MAC address of the first interface.
continue
}
log.Printf("adding interface %s to bridge %s", attr.Name, bridge.Name)
if err := netlink.LinkSetMaster(l, bridgeLink); err != nil {
return fmt.Errorf("LinkSetMaster(%s): %v", attr.Name, err)
}
if attr.OperState != netlink.OperUp {
log.Printf("setting interface %s up", attr.Name)
if err := netlink.LinkSetUp(l); err != nil {
return fmt.Errorf("LinkSetUp(%s): %v", attr.Name, err)
}
}
}
if attr := bridgeLink.Attrs(); attr.OperState != netlink.OperUp {
log.Printf("setting interface %s up", attr.Name)
if err := netlink.LinkSetUp(bridgeLink); err != nil {
return fmt.Errorf("LinkSetUp(%s): %v", attr.Name, err)
}
}
}
return nil
}
func applyInterfaceFEC(details InterfaceDetails) error {
if details.FEC == "" {
return nil // nothing to do
}
desired := ethtool.FECModes(unix.ETHTOOL_FEC_RS)
switch strings.ToLower(details.FEC) {
case "rs":
desired = unix.ETHTOOL_FEC_RS
case "baser":
desired = unix.ETHTOOL_FEC_BASER
case "off":
desired = unix.ETHTOOL_FEC_OFF
case "none":
desired = unix.ETHTOOL_FEC_NONE
case "llrs":
desired = unix.ETHTOOL_FEC_LLRS
case "auto":
desired = 0
default:
return fmt.Errorf("unknown FEC value %q, expected one of RS, BaseR, LLRS, Auto, None, Off", details.FEC)
}
cl, err := ethtool.New()
func applyInterfaces(dir, root string) error {
b, err := ioutil.ReadFile(filepath.Join(dir, "interfaces.json"))
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer cl.Close()
li, err := cl.LinkInfo(ethtool.Interface{Name: details.Name})
if err != nil {
return fmt.Errorf("LinkInfo(%s): %v", details.Name, err)
}
fec, err := cl.FEC(li.Interface)
if err != nil {
return fmt.Errorf("FEC(%s): %v", li.Interface.Name, err)
}
log.Printf("FEC supported/configured: [%v], active: %v", fec.Supported(), fec.Active)
// fec.Active is not set when there is no link, so we compare
// supported/configured instead.
if fec.Supported() == desired {
return nil // already matching the desired configuration
}
log.Printf("setting FEC to %v", desired)
if err := cl.SetFEC(ethtool.FEC{
Interface: li.Interface,
Modes: desired,
Auto: strings.ToLower(details.FEC) == "auto",
}); err != nil {
var cfg InterfaceConfig
if err := json.Unmarshal(b, &cfg); err != nil {
return err
}
return nil
}
func createResolvConfIfMissing(root, contents string) error {
fn := filepath.Join(root, "tmp", "resolv.conf")
// Explicitly check for the file's existance
// just so that we can avoid printing an error
// in the normal case (file exists).
st, err := os.Lstat(fn)
if err == nil {
if st.Mode()&os.ModeSymlink != 0 {
// File is a symbolic link (at boot, gokrazy links /tmp/resolv.conf to /proc/net/pnp).
// Delete the link and fallthrough to create the file.
if err := os.Remove(fn); err != nil {
return err
}
} else {
return nil // regular file already exists, do not overwrite
}
} else if !os.IsNotExist(err) {
return err // unexpected error
}
// /tmp/resolv.conf does not exist yet, create it.
// This is os.WriteFile, but with O_EXCL set
// so that we do not accidentally clobber the file
// in case another process (e.g. tailscaled) just wrote it.
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL, 0644)
if err != nil {
return err
}
_, err = f.Write([]byte(contents))
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
func applyInterfaces(dir, root string, cfg InterfaceConfig) error {
byName := make(map[string]InterfaceDetails)
byHardwareAddr := make(map[string]InterfaceDetails)
for _, details := range cfg.Interfaces {
@ -486,11 +240,6 @@ func applyInterfaces(dir, root string, cfg InterfaceConfig) error {
}
byName[details.Name] = details
}
if err := applyBridges(&cfg); err != nil {
log.Printf("applyBridges: %v", err)
}
links, err := netlink.LinkList()
if err != nil {
return err
@ -512,9 +261,6 @@ func applyInterfaces(dir, root string, cfg InterfaceConfig) error {
}
} else {
details, ok = byHardwareAddr[addr]
if !ok {
details, ok = byName[attr.Name]
}
}
if !ok {
log.Printf("no config for interface %s/%s", attr.Name, addr)
@ -528,12 +274,6 @@ func applyInterfaces(dir, root string, cfg InterfaceConfig) error {
attr.Name = details.Name
}
if details.MTU != 0 {
if err := netlink.LinkSetMTU(l, details.MTU); err != nil {
return fmt.Errorf("LinkSetMTU(%d): %v", details.MTU, err)
}
}
if spoof := details.SpoofHardwareAddr; spoof != "" {
hwaddr, err := net.ParseMAC(spoof)
if err != nil {
@ -544,11 +284,6 @@ func applyInterfaces(dir, root string, cfg InterfaceConfig) error {
}
}
if err := applyInterfaceFEC(details); err != nil {
// TODO: turn this into returning an error once proven stable
log.Printf("applyInterfaceFEC: %v", err)
}
if attr.OperState != netlink.OperUp {
// Set the interface to up, which is required by all other configuration.
if err := netlink.LinkSetUp(l); err != nil {
@ -567,41 +302,14 @@ func applyInterfaces(dir, root string, cfg InterfaceConfig) error {
}
if details.Name == "lan0" {
// Use dnsd for the system's own DNS resolution.
resolvConf := "nameserver " + addr.IP.String() + "\n"
if err := createResolvConfIfMissing(root, resolvConf); err != nil {
b := []byte("nameserver " + addr.IP.String() + "\n")
fn := filepath.Join(root, "tmp", "resolv.conf")
if err := os.Remove(fn); err != nil && !os.IsNotExist(err) {
return err
}
if err := renameio.WriteFile(fn, b, 0644); err != nil {
return err
}
}
}
for _, addr := range details.ExtraAddrs {
log.Printf("replacing extra address %v on %v", addr, attr.Name)
addr, err := netlink.ParseAddr(addr)
if err != nil {
return fmt.Errorf("ParseAddr(%q): %v", addr, err)
}
if err := netlink.AddrReplace(l, addr); err != nil {
return fmt.Errorf("AddrReplace(%s, %v): %v", attr.Name, addr, err)
}
}
for _, route := range details.ExtraRoutes {
_, dst, err := net.ParseCIDR(route.Destination)
if err != nil {
return fmt.Errorf("ParseCIDR(%q): %v", route.Destination, err)
}
r := &netlink.Route{Dst: dst}
if route.Gateway != "" {
r.Gw = net.ParseIP(route.Gateway)
}
r.LinkIndex = attr.Index
log.Printf("replacing extra route %v on %v", r, attr.Name)
if err := netlink.RouteReplace(r); err != nil {
return fmt.Errorf("RouteReplace(%v): %v", r, err)
}
}
}
@ -614,76 +322,7 @@ func nfifname(n string) []byte {
return b
}
// matchUplinkIP is conceptually equivalent to "ip daddr <uplink0-ip>", but
// without actually using the IP address of the uplink0 interface (which would
// mean that rules need to change when the IP address changes).
//
// Instead, it uses “fib daddr type local” to match all locally-configured IP
// addresses and then excludes the loopback and LAN IP addresses.
func matchUplinkIP(lan0ip net.IP) []expr.Any {
return []expr.Any{
// [ payload load 4b @ network header + 16 => reg 1 ]
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: 16, // TODO
Len: 4, // TODO
},
// [ bitwise reg 1 = (reg=1 & 0x000000ff ) ^ 0x00000000 ]
&expr.Bitwise{
DestRegister: 1,
SourceRegister: 1,
Len: 4,
Mask: []byte{0xff, 0x00, 0x00, 0x00}, // 255.0.0.0, i.e. /8
Xor: []byte{0x00, 0x00, 0x00, 0x00},
},
// [ cmp neq reg 1 0x0000007f ]
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: []byte{0x7f, 0x00, 0x00, 0x00},
},
// [ payload load 4b @ network header + 16 => reg 1 ]
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: 16, // TODO
Len: 4, // TODO
},
// [ bitwise reg 1 = (reg=1 & 0x00ffffff ) ^ 0x00000000 ]
&expr.Bitwise{
DestRegister: 1,
SourceRegister: 1,
Len: 4,
Mask: []byte{0xff, 0xff, 0xff, 0x00}, // 255.255.255.0, i.e. /24
Xor: []byte{0x00, 0x00, 0x00, 0x00},
},
// [ cmp neq reg 1 0x0000000a ]
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
// Turn the lan0 IP address (e.g. 192.168.42.1)
// into a netmask like 192.168.42.0/24.
Data: []byte{lan0ip[0], lan0ip[1], lan0ip[2], 0},
},
// [ fib daddr type => reg 1 ]
&expr.Fib{
Register: 1,
FlagDADDR: true,
ResultADDRTYPE: true,
},
// [ cmp eq reg 1 0x00000002 ]
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{0x02, 0x00, 0x00, 0x00},
},
}
}
func portForwardExpr(lan0ip net.IP, proto uint8, portMin, portMax uint16, dest net.IP, dportMin, dportMax uint16) []expr.Any {
func portForwardExpr(ifname string, proto uint8, portMin, portMax uint16, dest net.IP, dportMin, dportMax uint16) []expr.Any {
var cmp []expr.Any
if portMin == portMax {
cmp = []expr.Any{
@ -710,7 +349,16 @@ func portForwardExpr(lan0ip net.IP, proto uint8, portMin, portMax uint16, dest n
},
}
}
ex := append(matchUplinkIP(lan0ip),
ex := []expr.Any{
// [ meta load iifname => reg 1 ]
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
// [ cmp eq reg 1 0x696c7075 0x00306b6e 0x00000000 0x00000000 ]
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nfifname(ifname),
},
// [ meta load l4proto => reg 1 ]
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
// [ cmp eq reg 1 0x00000006 ]
@ -726,7 +374,8 @@ func portForwardExpr(lan0ip net.IP, proto uint8, portMin, portMax uint16, dest n
Base: expr.PayloadBaseTransportHeader,
Offset: 2, // TODO
Len: 2, // TODO
})
},
}
ex = append(ex, cmp...)
ex = append(ex,
// [ immediate reg 1 0x0217a8c0 ]
@ -820,15 +469,6 @@ func applyPortForwardings(dir, ifname string, c *nftables.Conn, nat *nftables.Ta
return err
}
lan0ip, err := LinkAddress(dir, "lan0")
if err != nil {
return err
}
lan0ip = lan0ip.To4()
if got, want := len(lan0ip), net.IPv4len; got != want {
return fmt.Errorf("lan0 does not have an IPv4 address configured: len %d != %d", got, want)
}
for _, fw := range cfg.Forwardings {
for _, proto := range strings.Split(fw.Proto, ",") {
var p uint8
@ -853,7 +493,7 @@ func applyPortForwardings(dir, ifname string, c *nftables.Conn, nat *nftables.Ta
c.AddRule(&nftables.Rule{
Table: nat,
Chain: prerouting,
Exprs: portForwardExpr(lan0ip, p, min, max, net.ParseIP(fw.DestAddr), dmin, dmax),
Exprs: portForwardExpr(ifname, p, min, max, net.ParseIP(fw.DestAddr), dmin, dmax),
})
}
}
@ -864,13 +504,35 @@ func applyPortForwardings(dir, ifname string, c *nftables.Conn, nat *nftables.Ta
var DefaultCounterObj = &nftables.CounterObj{}
func getCounterObj(c *nftables.Conn, o *nftables.CounterObj) *nftables.CounterObj {
obj, err := c.GetObject(o)
objs, err := c.GetObj(o)
if err != nil {
o.Bytes = DefaultCounterObj.Bytes
o.Packets = DefaultCounterObj.Packets
return o
}
if co, ok := obj.(*nftables.CounterObj); ok {
{
// TODO: remove this workaround once travis has workers with a newer kernel
// than its current Ubuntu trusty kernel (Linux 4.4.0):
var filtered []nftables.Obj
for _, obj := range objs {
co, ok := obj.(*nftables.CounterObj)
if !ok {
continue
}
if co.Table.Name != o.Table.Name {
continue
}
filtered = append(filtered, obj)
}
objs = filtered
}
if got, want := len(objs), 1; got != want {
log.Printf("could not carry counter values: unexpected number of objects in table %v: got %d, want %d", o.Table.Name, got, want)
o.Bytes = DefaultCounterObj.Bytes
o.Packets = DefaultCounterObj.Packets
return o
}
if co, ok := objs[0].(*nftables.CounterObj); ok {
return co
}
o.Bytes = DefaultCounterObj.Bytes
@ -878,99 +540,14 @@ func getCounterObj(c *nftables.Conn, o *nftables.CounterObj) *nftables.CounterOb
return o
}
func hairpinDNAT() []expr.Any {
return []expr.Any{
// [ meta load oifname => reg 1 ]
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
// [ cmp eq reg 1 0x306e616c 0x00000000 0x00000000 0x00000000 ]
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nfifname("lan0"),
},
// [ meta load oifname => reg 1 ]
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
// [ cmp eq reg 1 0x306e616c 0x00000000 0x00000000 0x00000000 ]
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: nfifname("lan0"),
},
// [ ct load status => reg 1 ]
&expr.Ct{
Register: 1,
SourceRegister: false,
Key: expr.CtKeySTATUS,
},
// [ bitwise reg 1 = (reg=1 & 0x00000020 ) ^ 0x00000000 ]
&expr.Bitwise{
DestRegister: 1,
SourceRegister: 1,
Len: 4,
Mask: []byte{0x20, 0x00, 0x00, 0x00},
Xor: []byte{0x00, 0x00, 0x00, 0x00},
},
// [ cmp neq reg 1 0x00000000 ]
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: []byte{0x00, 0x00, 0x00, 0x00},
},
// [ masq ]
&expr.Masq{},
}
}
const pfChain = "router7-portforwardings"
// Only update port forwarding if there are existing rules.
// This is required to not stomp over podman port forwarding, for example.
func updatePortforwardingsOnly(dir, ifname string) error {
c := &nftables.Conn{}
nat, err := c.ListTable("nat")
if err != nil {
return err
}
chain, err := c.ListChain(nat, pfChain)
if err != nil {
return err
}
log.Printf("rules already configured, only updating port forwardings")
c.FlushChain(chain)
if err := applyPortForwardings(dir, ifname, c, nat, chain); err != nil {
return err
}
return c.Flush()
}
func applyFirewall(dir, ifname string) error {
c := &nftables.Conn{}
if err := updatePortforwardingsOnly(dir, ifname); err != nil {
log.Printf("could not update port forwardings (%v), creating ruleset from scratch", err)
} else {
return nil // keep existing ruleset
}
c.FlushRuleset()
nat := c.AddTable(&nftables.Table{
Family: nftables.TableFamilyIPv4,
Name: "nat-gokrazy",
})
pf := c.AddChain(&nftables.Chain{
Name: pfChain,
Table: nat,
Type: nftables.ChainTypeNAT,
Name: "nat",
})
prerouting := c.AddChain(&nftables.Chain{
@ -981,17 +558,6 @@ func applyFirewall(dir, ifname string) error {
Type: nftables.ChainTypeNAT,
})
c.AddRule(&nftables.Rule{
Table: nat,
Chain: prerouting,
Exprs: []expr.Any{
&expr.Verdict{
Kind: expr.VerdictJump,
Chain: pfChain,
},
},
})
postrouting := c.AddChain(&nftables.Chain{
Name: "postrouting",
Hooknum: nftables.ChainHookPostrouting,
@ -1017,24 +583,18 @@ func applyFirewall(dir, ifname string) error {
},
})
c.AddRule(&nftables.Rule{
Table: nat,
Chain: postrouting,
Exprs: hairpinDNAT(),
})
if err := applyPortForwardings(dir, ifname, c, nat, pf); err != nil {
if err := applyPortForwardings(dir, ifname, c, nat, prerouting); err != nil {
return err
}
filter4 := c.AddTable(&nftables.Table{
Family: nftables.TableFamilyIPv4,
Name: "filter-gokrazy",
Name: "filter",
})
filter6 := c.AddTable(&nftables.Table{
Family: nftables.TableFamilyIPv6,
Name: "filter-gokrazy",
Name: "filter",
})
for _, filter := range []*nftables.Table{filter4, filter6} {
@ -1132,56 +692,6 @@ func applyFirewall(dir, ifname string) error {
},
},
})
input := c.AddChain(&nftables.Chain{
Name: "input",
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityFilter,
Table: filter,
Type: nftables.ChainTypeFilter,
})
counterObj = getCounterObj(c, &nftables.CounterObj{
Table: filter,
Name: "inputc",
})
counter = c.AddObj(counterObj).(*nftables.CounterObj)
c.AddRule(&nftables.Rule{
Table: filter,
Chain: input,
Exprs: []expr.Any{
// [ counter name input ]
&expr.Objref{
Type: NFT_OBJECT_COUNTER,
Name: counter.Name,
},
},
})
output := c.AddChain(&nftables.Chain{
Name: "output",
Hooknum: nftables.ChainHookOutput,
Priority: nftables.ChainPriorityFilter,
Table: filter,
Type: nftables.ChainTypeFilter,
})
counterObj = getCounterObj(c, &nftables.CounterObj{
Table: filter,
Name: "outputc",
})
counter = c.AddObj(counterObj).(*nftables.CounterObj)
c.AddRule(&nftables.Rule{
Table: filter,
Chain: output,
Exprs: []expr.Any{
// [ counter name output ]
&expr.Objref{
Type: NFT_OBJECT_COUNTER,
Name: counter.Name,
},
},
})
}
return c.Flush()
@ -1206,8 +716,6 @@ func applySysctl(ifname string) error {
sysctls := []string{
"net.ipv4.ip_forward=1",
"net.ipv6.conf.all.forwarding=1",
"net.ipv4.icmp_ratelimit=0",
"net.ipv6.icmp.ratelimit=0",
}
if ifname != "" {
sysctls = append(sysctls, "net.ipv6.conf."+ifname+".accept_ra=2")
@ -1225,20 +733,10 @@ func applySysctl(ifname string) error {
}
func Apply(dir, root string, firewall bool) error {
var cfg InterfaceConfig
b, err := ioutil.ReadFile(filepath.Join(dir, "interfaces.json"))
if err != nil && !os.IsNotExist(err) {
return err
}
if err == nil || os.IsNotExist(err) {
if err := json.Unmarshal(b, &cfg); err != nil {
return err
}
// TODO: split apply into two parts: delay the up until later
if err := applyInterfaces(dir, root, cfg); err != nil {
return fmt.Errorf("interfaces: %v", err)
}
// TODO: split into two parts: delay the up until later
if err := applyInterfaces(dir, root); err != nil {
return fmt.Errorf("interfaces: %v", err)
}
var errors []error
@ -1247,7 +745,7 @@ func Apply(dir, root string, firewall bool) error {
log.Println(err)
}
if err := applyDhcp4(dir, cfg); err != nil {
if err := applyDhcp4(dir); err != nil {
appendError(fmt.Errorf("dhcp4: %v", err))
}
@ -1280,24 +778,6 @@ func Apply(dir, root string, firewall bool) error {
if err := applyFirewall(dir, ifname); err != nil {
appendError(fmt.Errorf("firewall: %v", err))
}
} else {
if _, err := os.Stat("/user/nft"); err == nil {
log.Println("Applying custom firewall")
cmd := &exec.Cmd{
Path: "/user/nft",
Args: []string{"/user/nft", "-ef", "/etc/firewall.nft"},
Env: cleanEnviron(os.Environ()),
Stdout: os.Stdout,
Stderr: os.Stderr,
}
if err := cmd.Run(); err != nil {
appendError(fmt.Errorf("firewall: nft: %v", err))
} else {
log.Println("Custom firewall successfully applied:", cmd.ProcessState.ExitCode())
}
} else {
log.Println("Firewall Disabled")
}
}
if err := applyWireGuard(dir); err != nil {
@ -1309,12 +789,3 @@ func Apply(dir, root string, firewall bool) error {
}
return nil
}
func cleanEnviron(environ []string) []string {
for i, env := range environ {
if strings.Contains(env, "GOKRAZY") {
environ[i] = ""
}
}
return environ
}

View File

@ -45,7 +45,7 @@ func Process(name string, sig os.Signal) error {
}
return err
}
if !strings.Contains(string(b), name) {
if !strings.HasPrefix(string(b), name) {
continue
}
pid, _ := strconv.Atoi(fi.Name()) // already verified to be numeric

View File

@ -18,8 +18,6 @@ package radvd
import (
"log"
"net"
"net/netip"
"strings"
"sync"
"time"
@ -94,10 +92,6 @@ func (s *Server) Serve(ifname string, conn net.PacketConn) error {
if err != nil {
return err
}
if !strings.HasSuffix(addr.String(), "%"+ifname) {
log.Printf("ignoring off-interface request from %v", addr)
continue
}
// TODO: isnt this guaranteed by the filter above?
if n == 0 ||
ipv6.ICMPType(buf[0]) != ipv6.ICMPTypeRouterSolicitation {
@ -150,21 +144,21 @@ func (s *Server) sendAdvertisement(addr net.Addr) error {
if err != nil {
return err
}
var linkLocal netip.Addr
var linkLocal net.IP
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipv6LinkLocal.Contains(ipnet.IP) {
linkLocal, _ = netip.AddrFromSlice(ipnet.IP)
linkLocal = ipnet.IP
break
}
}
if linkLocal.IsValid() && !linkLocal.IsUnspecified() {
if !linkLocal.Equal(net.IPv6zero) {
options = append(options, &ndp.RecursiveDNSServer{
Lifetime: 30 * time.Minute,
Servers: []netip.Addr{linkLocal},
Servers: []net.IP{linkLocal},
})
}
}
@ -176,14 +170,13 @@ func (s *Server) sendAdvertisement(addr net.Addr) error {
ones = 64
}
addr, _ := netip.AddrFromSlice(prefix.IP)
options = append(options, &ndp.PrefixInformation{
PrefixLength: uint8(ones),
OnLink: true,
AutonomousAddressConfiguration: true,
ValidLifetime: 2 * time.Hour,
PreferredLifetime: 30 * time.Minute,
Prefix: addr,
Prefix: prefix.IP,
})
}

View File

@ -23,36 +23,12 @@ import (
"os"
)
type nonBlockingWriter struct {
W chan<- string
}
func (w *nonBlockingWriter) Write(p []byte) (n int, _ error) {
select {
// Intentionally convert from byte slice ([]byte) to string because sending
// a byte slice over a channel is not safe: it may point to new contents,
// resulting in duplicate log lines showing up.
case w.W <- string(p):
default:
// channel unavailable, ignore
}
return len(p), nil
}
// NewConsole returns a logger which returns to /dev/console and
// os.Stderr. Writes to /dev/console are non-blocking, i.e. messages will be
// discarded if /dev/console stalls (e.g. when enabling Scroll Lock on a HDMI
// console).
// NewConsole returns a logger which returns to /dev/console and os.Stderr.
func NewConsole() *log.Logger {
w := ioutil.Discard
if console, err := os.OpenFile("/dev/console", os.O_RDWR, 0600); err == nil {
ch := make(chan string, 1)
go func() {
for buf := range ch {
console.Write([]byte(buf))
}
}()
w = &nonBlockingWriter{W: ch}
var w io.Writer
w, err := os.OpenFile("/dev/console", os.O_RDWR, 0600)
if err != nil {
w = ioutil.Discard
}
return log.New(io.MultiWriter(os.Stderr, w), "", log.LstdFlags|log.Lshortfile)
}

View File

@ -12,7 +12,7 @@ RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
dnsmasq ndisc6 nftables dnsutils strace wireguard iproute2 && \
dnsmasq ndisc6 nftables dnsutils strace && \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src

View File

@ -1,6 +0,0 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

View File

@ -1,19 +0,0 @@
baseURL = "https://router7.org/"
languageCode = "en-us"
title = "router7"
theme = "router7"
disableKinds = ["RSS", "taxonomyTerm"]
publishDir = "../docs/"
enableRobotsTXT = true
[markup.goldmark.renderer]
# Required for e.g. <img> tags in markdown articles.
unsafe = true
[menu]
[[menu.main]]
name = "github"
title = "GitHub"
url = "https://github.com/rtr7/router7"
weight = 60

View File

@ -1,37 +0,0 @@
---
title: "router7: a small home internet router completely written in Go"
menu:
main:
title: "Home"
weight: 10
---
# router7
router7 is a pure-Go implementation of a small home internet router. It comes with all the services required to make a [fiber7 internet connection](https://www.init7.net/en/internet/fiber7/) work (DHCPv4, DHCPv6, DNS, etc.).
Note that this project should be considered a (working!) tech demo. Feature requests will likely not be implemented, and see [CONTRIBUTING.md](https://github.com/rtr7/router7/blob/master/CONTRIBUTING.md) for details about which contributions are welcome.
## Motivation
Before starting router7, I was using the [Turris Omnia](https://omnia.turris.cz/en/) router running OpenWrt. That worked fine up until May 2018, when an automated update pulled in a new version of [odhcp6c](https://git.openwrt.org/?p=project/odhcp6c.git;a=shortlog), OpenWrts DHCPv6 client. That version is incompatible with fiber7s DHCP server setup (I think there are shortcomings on both sides).
It was not only quicker to develop my own router than to wait for either side to resolve the issue, but it was also a lot of fun and allowed me to really tailor my router to my needs, experimenting with a bunch of interesting ideas I had.
## Project goals
* Maximize internet connectivity: retain the most recent DHCP configuration across reboots and even after its expiration (chances are the DHCP server will be back before the configuration stops working).
* Unit/integration tests use fiber7 packet capture files to minimize the chance of software changes breaking my connectivity.
* Safe and quick updates
* Auto-rollback of updates which result in loss of connectivity: the diagnostics daemon assesses connectivity state, the update tool reads it and rolls back faulty updates.
* Thanks to kexec, updates translate into merely 13s of internet connectivity loss.
* Easy debugging
* Configuration-related network packets (e.g. DHCP, IPv6 neighbor/router advertisements) are stored in a ring buffer which can be streamed into [Wireshark](https://www.wireshark.org/), allowing for live and retro-active debugging.
* The diagnostics daemon performs common diagnostic steps (ping, traceroute, …) for you.
* All state in the system is stored as human-readable JSON within the `/perm` partition and can be modified.
## Hardware
The reference hardware platform is the [PC Engines™ apu2c4](https://pcengines.ch/apu2c4.htm) system board. It features a 1 GHz quad core amd64 CPU, 4 GB of RAM, 3 Ethernet ports and a DB9 serial port. It conveniently supports PXE boot, the schematics and bootloader sources are available. I recommend the [msata16g](https://pcengines.ch/msata16g.htm) SSD module for reliable persistent storage and the [usbcom1a](https://pcengines.ch/usbcom1a.htm) serial adapter if you dont have one already.
Other hardware might work, too, but is not tested.

View File

@ -1,64 +0,0 @@
---
title: "router7: architecture"
menu:
main:
title: "Architecture"
weight: 20
---
# Architecture
router7 is based on [gokrazy](https://gokrazy.org/): it is an appliance which gets packed into a hard disk image, containing a FAT partition with the kernel, a read-only SquashFS partition for the root file system and an ext4 partition for permanent data.
The individual services can be found in [github.com/rtr7/router7/cmd](https://pkg.go.dev/github.com/rtr7/router7/cmd)
* Each service runs in a separate process.
* Services communicate with each other by persisting state files. E.g., `cmd/dhcp4` writes `/perm/dhcp4/wire/lease.json`.
* A service notifies other services about state changes by sending them signal `SIGUSR1`.
## Configuration files
{{<table "table table-striped table-bordered">}}
| File | Consumer(s) | Purpose |
|---|---|---|
| `/perm/interfaces.json` | `netconfigd` | Set IP/MAC addresses of `uplink0` and `lan0` |
| `/perm/portforwardings.json` | `netconfigd` | Configure nftables port forwarding rules |
| `/perm/dhcp6/duid` | `dhcp6` | Set DHCP Unique Identifier (DUID) for obtaining static leases |
{{</table>}}
## State files
{{<table "table table-striped table-bordered">}}
| File | Producer | Consumer(s) | Purpose |
|---|---|---|---|
| `/perm/dhcp4/wire/ack` | `dhcp4` | `dhcp4` | last DHCPACK packet for renewals across restarts |
| `/perm/dhcp4/wire/lease.json` | `dhcp4` | `netconfigd` | Obtained DHCPv4 lease |
| `/perm/dhcp6/wire/lease.json` | `dhcp6` | `netconfigd`, `radvd` | Obtained DHCPv6 lease |
| `/perm/dhcp4d/leases.json` | `dhcp4d` | `dhcp4d`, `dnsd` | DHCPv4 leases handed out (including hostnames) |
{{</table>}}
## Available ports
{{<table "table table-striped table-bordered">}}
| Port | Purpose |
|---|---|
| `<public>:8053` | `dnsd` metrics (forwarded requests)
| `<public>:8066` | `netconfigd` metrics (nftables counters)
| `<private>:80` | gokrazy web interface
| `<private>:67` | `dhcp4d`
| `<private>:58` | `radvd`
| `<private>:53` | `dnsd`
| `<private>:8077` | `backupd` (serve backup.tar.gz)
| `<private>:7733` | `diagd` (perform diagnostics)
| `<private>:5022` | `captured` (serve captured packets)
{{</table>}}
Heres an example of `cmd/diagd` output:
<img src="https://github.com/rtr7/router7/raw/master/2018-07-14-diagd.png"
width="800" alt="diagd output">
Heres an example of `cmd/netconfigd` metrics when scraped with [Prometheus](https://prometheus.io/) and displayed in [Grafana](https://grafana.com/):
<img src="https://github.com/rtr7/router7/raw/master/2018-07-14-grafana.png"
width="800" alt="metrics in grafana">

View File

@ -1,181 +0,0 @@
---
title: "router7: installation"
menu:
main:
title: "Installation"
weight: 30
---
# Installation
Connect your serial adapter ([usbcom1a](https://pcengines.ch/usbcom1a.htm) works well if you dont have one already) to the apu2c4 and start a program to use it, e.g. `screen /dev/ttyUSB0 115200`. Then, power on the apu2c4 and configure it to do PXE boot:
* Press `F10` to enter the boot menu
* Press `3` to enter setup
* Press `n` to enable network boot
* Press `c` to move mSATA to the top of the boot order
* Press `e` to move iPXE to the top of the boot order
* Press `s` to save configuration and exit
Connect a network cable on `net0`, the port closest to the serial console port:
<img src="https://raw.githubusercontent.com/rtr7/router7/master/devsetup.jpg"
width="800" alt="router7 development setup">
Next, create a router7 gokrazy instance (see [gokrazy
quickstart](https://gokrazy.org/quickstart/) if youre unfamiliar with gokrazy):
```bash
go install github.com/gokrazy/tools/cmd/gok@main
go install github.com/rtr7/tools/cmd/...@latest
mkdir /tmp/recovery
gok -i router7 new
gok -i router7 edit
```
Change the config until you have the following fields set:
```json
{
"Hostname": "router7",
"Packages": [
"github.com/gokrazy/fbstatus",
"github.com/gokrazy/hello",
"github.com/gokrazy/serial-busybox",
"github.com/gokrazy/breakglass"
"github.com/rtr7/router7/cmd/..."
],
"SerialConsole": "ttyS0,115200",
"GokrazyPackages": [
"github.com/gokrazy/gokrazy/cmd/ntp",
"github.com/gokrazy/gokrazy/cmd/randomd"
],
"KernelPackage": "github.com/rtr7/kernel",
"FirmwarePackage": "github.com/rtr7/kernel",
"EEPROMPackage": ""
}
```
Then, build an image:
```bash
GOARCH=amd64 gok -i router7 overwrite \
--boot /tmp/recovery/boot.img \
--mbr /tmp/recovery/mbr.img \
--root /tmp/recovery/root.img
```
And serve the image for netboot installation:
```bash
rtr7-recover \
--boot /tmp/recovery/boot.img \
--mbr /tmp/recovery/mbr.img \
--root /tmp/recovery/root.img
```
Specifically, `rtr7-recover`:
* trigger a reset [if a Teensy with the rebootor firmware is attached](#rebootor)
* serve a DHCP lease to all clients which request PXE boot (i.e., your apu2c4)
* serve via TFTP:
* the PXELINUX bootloader
* the router7 kernel
* an initrd archive containing the rtr7-recovery-init program and mke2fs
* serve via HTTP the boot and root images
* optionally serve via HTTP a backup.tar.gz image containing files for `/perm` (e.g. for moving to new hardware, rolling back corrupted state, or recovering from a disk failure)
* exit once the router successfully wrote the images to disk
## Configuration
### Interfaces
The `/perm/interfaces.json` configuration file will be [automatically created](https://github.com/rtr7/tools/blob/57c2cdc3b629d2fbd13564ae37f6282f6ee8427f/cmd/rtr7-recovery-init/recoveryinit.go#L320) if it is not present when you run the first recovery.
Example:
```json
{
"interfaces": [
{
"hardware_addr": "12:34:56:78:9a:b0",
"name": "lan0",
"addr": "192.168.0.1/24"
},
{
"hardware_addr": "12:34:56:78:9a:b2",
"name": "uplink0"
}
]
}
```
Schema: see [`InterfaceConfig`](https://github.com/rtr7/router7/blob/f86e20be5305fc0e7e77421e0f2abde98a84f2a7/internal/netconfig/netconfig.go#L183)
### Port Forwarding
The `/perm/portforwardings.json` configuration file can be created to define port forwarding rules.
Example:
```json
{
"forwardings": [
{
"proto": "tcp",
"port": "22",
"dest_addr": "10.0.0.10",
"dest_port": "22"
},
{
"proto": "tcp",
"port": "80",
"dest_addr": "10.0.0.10",
"dest_port": "80"
}
]
}
```
Schema: see [`portForwardings`](
https://github.com/rtr7/router7/blob/f86e20be5305fc0e7e77421e0f2abde98a84f2a7/internal/netconfig/netconfig.go#L431)
## Updates
Run e.g. `rtr7-safe-update -updates_dir=$HOME/router7/updates` to:
* verify the router currently has connectivity, abort the update otherwise
* download a backup archive of `/perm`
* build a new image
* update the router
* wait until the router restored connectivity, roll back the update using `rtr7-recover` otherwise
The update step uses kexec to reduce the downtime to approximately 15 seconds.
## Manual Recovery
Given `rtr7-safe-update`s safeguards, manual recovery should rarely be required.
To manually roll back to an older image, invoke `rtr7-safe-update` via the
`recover.bash` script in the image directory underneath `-updates_dir`, e.g.:
```shell
% cd ~/router7/updates/2018-07-03T17:33:52+02:00
% ./recover.bash
```
## Teensy rebootor {#rebootor}
The cheap and widely-available [Teensy++ USB development board](https://www.pjrc.com/store/teensypp.html) comes with a firmware called rebootor, which is used by the [`teensy_loader_cli`](https://www.pjrc.com/teensy/loader_cli.html) program to perform hard resets.
This setup can be used to programmatically reset the apu2c4 (from `rtr7-recover`) by connecting the Teensy++ to the [apu2c4s reset pins](http://pcengines.ch/pdf/apu2.pdf):
* connect the Teensy++s `GND` pin to the apu2c4 J2s pin 4 (`GND`)
* connect the Teensy++s `B7` pin to the apu2c4 J2s pin 5 (`3.3V`, resets when pulled to `GND`)
You can find a working rebootor firmware .hex file at https://github.com/PaulStoffregen/teensy_loader_cli/issues/38
## Prometheus
See https://github.com/rtr7/router7/tree/master/contrib/prometheus for example
configuration files, and install the [router7 Grafana
Dashboard](https://grafana.com/dashboards/8288).

View File

@ -1,2 +0,0 @@
User-Agent: *
sitemap: https://router7.org/sitemap.xml

View File

@ -1,6 +0,0 @@
{{ $htmlTable := .Inner | markdownify }}
{{ $class := .Get 0 }}
{{ $old := "<table>" }}
{{ $new := printf "<table class=\"%s\">" $class }}
{{ $htmlTable := replace $htmlTable $old $new }}
{{ $htmlTable | safeHTML }}

View File

@ -1,19 +0,0 @@
.bd-toc {
position: sticky;
top: 4rem;
height: calc(100vh - 4rem);
overflow-y: auto; }
.bd-toc ul {
list-style: none;
padding-left: 1em;
border-left: 1px solid #eee; }
.bd-toc li {
margin-top: 1em;
margin-bottom: 1em; }
/* TODO: move this to a separate style sheet */
.bigbutton {
margin-left: 1em;
margin-right: 1em; }

View File

@ -1 +0,0 @@
{"Target":"sass/sidebar.css","MediaType":"text/css","Data":{}}

View File

@ -1 +0,0 @@
router7.org

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2020 YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,2 +0,0 @@
+++
+++

View File

@ -1,23 +0,0 @@
.bd-toc {
position: sticky;
top: 4rem;
height: calc(100vh - 4rem);
overflow-y: auto
}
.bd-toc ul {
list-style: none;
padding-left: 1em;
border-left: 1px solid #eee;
}
.bd-toc li {
margin-top: 1em;
margin-bottom: 1em;
}
/* TODO: move this to a separate style sheet */
.bigbutton {
margin-left: 1em;
margin-right: 1em;
}

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
<div id="content">
<div class="container">
{{- partial "header.html" . -}}
{{ block "main" . }}
{{ end }}
</div>
</div>
{{- partial "footer.html" . -}}
</body>
</html>

View File

@ -1,31 +0,0 @@
{{ define "main" }}
<div class="row">
<div class="col-md-10">
{{- partial "nav.html" . -}}
{{ .Content }}
<h1>list template</h1>
<ul>
{{ range .Pages }}
<li>
<a href="{{ .Permalink }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
<hr>
<p class="small">
© 2018 Michael Stapelberg and contributors
</p>
</div>
<div class="col-md-2">
<aside class="bd-toc">
{{ .TableOfContents }}
</aside>
</div>
</div>
{{ end }}

View File

@ -1,20 +0,0 @@
{{ define "main" }}
<div class="row">
<div class="col-md-10">
{{- partial "nav.html" . -}}
{{ .Content }}
<hr>
<p class="small">
© 2018 Michael Stapelberg and contributors
</p>
</div>
<div class="col-md-2">
<aside class="bd-toc">
{{ .TableOfContents }}
</aside>
</div>
</div>
{{ end }}

View File

@ -1,17 +0,0 @@
{{ define "main" }}
<div class="row">
<div class="col-md-10">
{{- partial "nav.html" . -}}
{{ .Content }}
<hr>
<p class="small">
© 2018 Michael Stapelberg and contributors
</p>
</div>
<div class="col-md-2">
</div>
</div>
{{ end }}

View File

@ -1,7 +0,0 @@
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="/popper-1.16.0.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="/bootstrap-4.4.1.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -1,14 +0,0 @@
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/bootstrap-4.4.1.min.css" crossorigin="anonymous">
{{ $sass := resources.Get "sass/sidebar.scss" }}
{{ $style := $sass | resources.ToCSS }}
<link rel="stylesheet" href="{{ $style.Permalink }}">
<title>{{ .Title }}</title>
</head>

View File

@ -1,15 +0,0 @@
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">router7</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav ml-auto">
{{ $current := . }}
{{ range .Site.Menus.main }}
{{ $active := $current.IsMenuCurrent "main" . }}
<a class="nav-item nav-link {{ if $active }}active{{ end }}" href="{{ .URL }}">{{ .Title }} {{ if $active }}<span class="sr-only">(current)</span>{{ end }}</a>
{{ end }}
</div>
</div>
</nav>

View File

@ -1,21 +0,0 @@
# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "Distri"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.41.0"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""