Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f147fb47bb | ||
|
b6dfdcd5ef | ||
|
db53b259a6 |
14
.github/workflows/go.yml
vendored
14
.github/workflows/go.yml
vendored
@ -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
|
||||
|
53
Makefile
53
Makefile
@ -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 router7’s 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
163
README.md
@ -1,6 +1,6 @@
|
||||
# router7
|
||||
|
||||
[](https://github.com/rtr7/router7/actions/workflows/go.yml)
|
||||
[](https://travis-ci.org/rtr7/router7)
|
||||
[](https://godoc.org/github.com/rtr7/router7/cmd)
|
||||
[](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), OpenWrt’s DHCPv6 client. That version is incompatible with fiber7’s 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 don’t 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 [apu2c4’s reset pins](http://pcengines.ch/pdf/apu2.pdf):
|
||||
* connect the Teensy++’s `GND` pin to the apu2c4 J2’s pin 4 (`GND`)
|
||||
* connect the Teensy++’s `B7` pin to the apu2c4 J2’s 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)
|
||||
|
||||
Here’s 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">
|
||||
|
||||
Here’s 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 don’t 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).
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// Copyright 2018 Google Inc.
|
||||
|
@ -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
|
||||
|
@ -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 don’t 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) don’t 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 {
|
||||
|
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")}
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
router7.org
|
@ -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><public>:8053</code></td>
|
||||
<td><code>dnsd</code> metrics (forwarded requests)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><public>:8066</code></td>
|
||||
<td><code>netconfigd</code> metrics (nftables counters)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:80</code></td>
|
||||
<td>gokrazy web interface</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:67</code></td>
|
||||
<td><code>dhcp4d</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:58</code></td>
|
||||
<td><code>radvd</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:53</code></td>
|
||||
<td><code>dnsd</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:8077</code></td>
|
||||
<td><code>backupd</code> (serve backup.tar.gz)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:7733</code></td>
|
||||
<td><code>diagd</code> (perform diagnostics)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code><private>:5022</code></td>
|
||||
<td><code>captured</code> (serve captured packets)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<p>Here’s 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>Here’s 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>
|
7
docs/bootstrap-4.4.1.min.css
vendored
7
docs/bootstrap-4.4.1.min.css
vendored
File diff suppressed because one or more lines are too long
7
docs/bootstrap-4.4.1.min.js
vendored
7
docs/bootstrap-4.4.1.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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>, OpenWrt’s DHCPv6 client. That version is incompatible with fiber7’s 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 don’t 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>
|
@ -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 don’t 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 you’re 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">"Hostname"</span>: <span style="color:#e6db74">"router7"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"Packages"</span>: [
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gokrazy/fbstatus"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gokrazy/hello"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gokrazy/serial-busybox"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gokrazy/breakglass"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/rtr7/router7/cmd/..."</span>
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"SerialConsole"</span>: <span style="color:#e6db74">"ttyS0,115200"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"GokrazyPackages"</span>: [
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gokrazy/gokrazy/cmd/ntp"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"github.com/gokrazy/gokrazy/cmd/randomd"</span>
|
||||
</span></span><span style="display:flex;"><span> ],
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"KernelPackage"</span>: <span style="color:#e6db74">"github.com/rtr7/kernel"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"FirmwarePackage"</span>: <span style="color:#e6db74">"github.com/rtr7/kernel"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"EEPROMPackage"</span>: <span style="color:#e6db74">""</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">"interfaces"</span>: [
|
||||
</span></span><span style="display:flex;"><span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"hardware_addr"</span>: <span style="color:#e6db74">"12:34:56:78:9a:b0"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"lan0"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"addr"</span>: <span style="color:#e6db74">"192.168.0.1/24"</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">"hardware_addr"</span>: <span style="color:#e6db74">"12:34:56:78:9a:b2"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"name"</span>: <span style="color:#e6db74">"uplink0"</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">"forwardings"</span>: [
|
||||
</span></span><span style="display:flex;"><span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"proto"</span>: <span style="color:#e6db74">"tcp"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"port"</span>: <span style="color:#e6db74">"22"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"dest_addr"</span>: <span style="color:#e6db74">"10.0.0.10"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"dest_port"</span>: <span style="color:#e6db74">"22"</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">"proto"</span>: <span style="color:#e6db74">"tcp"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"port"</span>: <span style="color:#e6db74">"80"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"dest_addr"</span>: <span style="color:#e6db74">"10.0.0.10"</span>,
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">"dest_port"</span>: <span style="color:#e6db74">"80"</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">apu2c4’s reset pins</a>:</p>
|
||||
<ul>
|
||||
<li>connect the Teensy++’s <code>GND</code> pin to the apu2c4 J2’s pin 4 (<code>GND</code>)</li>
|
||||
<li>connect the Teensy++’s <code>B7</code> pin to the apu2c4 J2’s 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>
|
2
docs/jquery-3.4.1.slim.min.js
vendored
2
docs/jquery-3.4.1.slim.min.js
vendored
File diff suppressed because one or more lines are too long
5
docs/popper-1.16.0.min.js
vendored
5
docs/popper-1.16.0.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
sitemap: https://router7.org/sitemap.xml
|
@ -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; }
|
@ -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
86
go.mod
@ -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
356
go.sum
@ -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
51
init/init.go
Normal 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 {}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 won’t 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 {
|
||||
// Don’t 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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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: isn’t 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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
@ -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
|
@ -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), OpenWrt’s DHCPv6 client. That version is incompatible with fiber7’s 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 don’t have one already.
|
||||
|
||||
Other hardware might work, too, but is not tested.
|
@ -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>}}
|
||||
|
||||
Here’s 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">
|
||||
|
||||
Here’s 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">
|
@ -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 don’t 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 you’re 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 [apu2c4’s reset pins](http://pcengines.ch/pdf/apu2.pdf):
|
||||
* connect the Teensy++’s `GND` pin to the apu2c4 J2’s pin 4 (`GND`)
|
||||
* connect the Teensy++’s `B7` pin to the apu2c4 J2’s 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).
|
@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
sitemap: https://router7.org/sitemap.xml
|
@ -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 }}
|
@ -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; }
|
@ -1 +0,0 @@
|
||||
{"Target":"sass/sidebar.css","MediaType":"text/css","Data":{}}
|
@ -1 +0,0 @@
|
||||
router7.org
|
7
website/static/bootstrap-4.4.1.min.css
vendored
7
website/static/bootstrap-4.4.1.min.css
vendored
File diff suppressed because one or more lines are too long
7
website/static/bootstrap-4.4.1.min.js
vendored
7
website/static/bootstrap-4.4.1.min.js
vendored
File diff suppressed because one or more lines are too long
2
website/static/jquery-3.4.1.slim.min.js
vendored
2
website/static/jquery-3.4.1.slim.min.js
vendored
File diff suppressed because one or more lines are too long
5
website/static/popper-1.16.0.min.js
vendored
5
website/static/popper-1.16.0.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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.
|
@ -1,2 +0,0 @@
|
||||
+++
|
||||
+++
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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 }}
|
||||
|
@ -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 }}
|
@ -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 }}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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 = ""
|
Loading…
x
Reference in New Issue
Block a user